]> git.donarmstrong.com Git - lilypond.git/blob - scripts/mudela-book.py
release: 1.2.2
[lilypond.git] / scripts / mudela-book.py
1 #!@PYTHON@
2
3 import os
4 import string
5 import re
6 import getopt
7 import sys
8 import __main__
9
10 outdir = 'out'
11 initfile = ''
12 program_version = '@TOPLEVEL_VERSION@'
13
14 cwd = os.getcwd ()
15 include_path = [cwd]
16
17 # TODO: use splitting iso. \mudelagraphic.
18
19 #
20 default_music_fontsize = 16
21 default_text_fontsize = 12
22
23 # latex linewidths:
24 # indices are no. of columns, papersize,  fontsize
25 # Why can't this be calculated?
26 latex_linewidths = {
27  1: {'a4':{10: 345, 11: 360, 12: 390},
28          'a5':{10: 276, 11: 276, 12: 276},
29          'b5':{10: 345, 11: 356, 12: 356},
30          'letter':{10: 345, 11: 360, 12: 390},
31          'legal': {10: 345, 11: 360, 12: 390},
32          'executive':{10: 345, 11: 360, 12: 379}},
33  2: {'a4':{10: 167, 11: 175, 12: 190},
34          'a5':{10: 133, 11: 133, 12: 133},
35          'b5':{10: 167, 11: 173, 12: 173},
36          'letter':{10: 167, 11: 175, 12: 190},
37          'legal':{10: 167, 11: 175, 12: 190},
38          'executive':{10: 167, 11: 175, 12: 184}}}
39
40
41 options = [
42   ('', 'h', 'help', 'print help'),
43   ('EXT', 'f', 'format', 'set format.  EXT is one of texi and latex.'),
44   ('', 'v', 'version', 'print version information' ),
45   ('FILE', 'o', 'outname', 'prefix for filenames'),
46   ('DIM',  '', 'default-mudela-fontsize', 'default fontsize for music.  DIM is assumed to in points'),
47 #  ('DIM', '', 'force-mudela-fontsize', 'force fontsize for all inline mudela. DIM is assumed to in points'),
48   ('', '', 'force-verbatim', 'make all mudela verbatim'),
49   ('', 'M', 'dependencies', 'write dependencies'),
50   ('DIR', 'I', 'include', 'include path'),
51   ('', '', 'init', 'mudela-book initfile')
52   ]
53
54 format = 'latex'
55 no_match = 'a\ba'
56
57 # format specific strings, ie. regex-es for input, and % strings for output
58 re_dict = {
59         'latex': {'input': '\\\\input{?([^}\t \n}]*)',
60                   'include': '\\\\include{([^}]+)}',
61                   'include-mudela':r"""\begin%s{mudela}
62 %s
63 \end{mudela}""",
64                   'header': r"""\\documentclass(\[.*?\])?""",
65                   'preamble-end': '\\\\begin{document}',
66                   'verbatim': r"""(?s)\\begin{verbatim}(.*?)\\end{verbatim}""",
67                   'verb': r"""\\verb(.)(.*?)\1""",
68                   'mudela-file': '\\\\mudelafile(\[[^\\]]+\])?{([^}]+)}',
69                   'mudela' : '\\\\mudela(\[.*?\])?{(.*?)}',
70                   'mudela-block': r"""(?s)\\begin(\[.*?\])?{mudela}(.*?)\\end{mudela}""",
71                   'interesting-cs': '\\\\(chapter|section|mudelagraphic|twocolumn|onecolumn)',
72                   'quote-verbatim': r"""\begin{verbatim}%s\end{verbatim}""",
73                   'def-post-re': r"""\\def\\postMudelaExample""",
74                   'def-pre-re': r"""\\def\\preMudelaExample""",           
75                   'default-post': r"""\def\postMudelaExample{}""",
76                   'default-pre': r"""\def\preMudelaExample{}""",
77                   'output-eps': '\\noindent\\parbox{\\mudelaepswidth{%s.eps}}{\includegraphics{%s.eps}}',
78                   'output-tex': '\\preMudelaExample \\input %s.tex \\postMudelaExample\n'
79                   },
80         'texi': {'input': '@include[ \n\t]+([^\t \n]*)',
81                  'include': no_match,
82                  'include-mudela': """@mudela[%s]
83 %s
84 @end mudela
85 """,
86                  'header': no_match,
87                  'preamble-end': no_match,
88                  'verbatim': r"""(?s)@example(.*?)@end example$""",
89                  'verb': r"""@code{(.*?)}""",
90                  'mudela-file': '@mudelafile(\[[^\\]]+\])?{([^}]+)}',
91                  'mudela' : '@mudela(\[.*?\])?{(.*?)}',
92                  'mudela-block': r"""(?s)@mudela(\[.*?\])?(.*?)@end mudela""",
93                  'interesting-cs': r"""[\\@](node|mudelagraphic)""",
94                  'quote-verbatim': r"""@example
95 %s
96 @end example""",
97                  'output-all': r"""@tex
98 \input %s.tex
99 @end tex
100 @html
101 <img src=%s.png>
102 @end html
103 """,
104                 }
105         }
106
107
108
109
110 def get_re (name):
111         return  re_dict[format][name]
112
113
114 def bounding_box_dimensions(fname):
115         try:
116                 fd = open(fname)
117         except IOError:
118                 error ("Error opening `%s'" % fname)
119         str = fd.read (-1)
120         s = re.search('%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)', str)
121         if s:
122                 return (int(s.group(3))-int(s.group(1)), 
123                         int(s.group(4))-int(s.group(2)))
124         else:
125                 return (0,0)
126
127
128 def find_file (name):
129         for a in include_path:
130                 try:
131                         nm = os.path.join (a, name)
132                         f = open (nm)
133                         return nm
134                 except IOError:
135                         pass
136         return ''
137
138 def error (str):
139         sys.stderr.write (str + "\n  Exiting ... \n\n")
140         raise 'Exiting.'
141
142
143 def compose_full_body (body, opts):
144         "Construct the text of an input file: add stuff to BODY using OPTS as options."
145         paper = 'a4'
146         music_size = default_music_fontsize
147         latex_size = default_text_fontsize
148
149         cols = 1
150         for o in opts:
151                 m = re.search ('^(.*)paper$', o)
152                 if m:
153                         paper = m.group (1)
154                 
155
156                 m = re.match ('([0-9]+)pt', o)
157                 if m:
158                         music_size = string.atoi(m.group (1))
159
160                 m = re.match ('latexfontsize=([0-9]+)pt', o)
161                 if m:
162                         latex_size = string.atoi (m.group (1))
163
164
165         if 'twocolumn' in opts:
166                 cols = 2
167                 
168         if 'fragment' or 'singleline' in opts:
169                 l = -1.0;
170         else:
171                 l = latex_linewidths[cols][paper][latex_size]
172
173         if not 'nofly' in opts and not re.search ('\\\\score', body):
174                 opts.append ('fly')
175
176
177         if 'fly' in opts:
178                 body = r"""\score { 
179   \notes\relative c {
180     %s
181   }
182   \paper { }  
183 }""" % body
184
185                 
186         body = r"""
187 %% Generated by mudela-book.py
188 \include "paper%d.ly"
189 \paper  { linewidth = %f \pt; } 
190 """ % (music_size, l) + body
191
192         return body
193
194
195 #
196 # Petr, ik zou willen dat ik iets zinvoller deed,
197 # maar wat ik kan ik doen, het verandert toch niets?
198 #   --hwn 20/aug/99
199 #
200
201
202 def read_tex_file (filename):
203         """Read the input file, substituting for \input, \include, \mudela{} and \mudelafile"""
204         str = ''
205         for fn in [filename, filename+'.tex', filename+'.doc']:
206                 try:
207                         f = open(fn)
208                         str = f.read (-1)
209                 except:
210                         pass
211                 
212
213         if not str:
214                 raise IOError
215
216         retdeps =  [filename]
217
218         def inclusion_func (match, surround):
219                 insert = match.group (0)
220                 try:
221                         (insert, d) = read_tex_file (match.group(1))
222                         deps = deps + d
223                         insert = surround + insert + surround
224                 except:
225                         sys.stderr.write("warning: can't find %s, let's hope latex will\n" % m.group(1))
226
227                 return (insert, deps)
228         
229         def include_func (match, d = retdeps):
230                 (s,d) = inclusion_func (match, '\\newpage ', retdeps)
231                 retdeps = retdeps + d
232                 return s
233
234         str = re.sub (get_re ('input'), include_func, str)
235
236         def input_func (match, d = retdeps):
237                 (s,d) = inclusion_func (match, '', retdeps)
238                 retdeps = retdeps + d
239                 return s
240
241         str = re.sub (get_re ('include'), input_func, str)
242         
243         return (str, retdeps)
244
245 def scan_preamble (str):
246         options = []
247         m = re.search (get_re ('header'), str)
248
249         # should extract paper & fontsz.
250         if m and m.group (1):
251                 options = options + re.split (',[\n \t]*', m.group(1)[1:-1])
252
253         def verbose_fontsize ( x):
254                 if o.match('[0-9]+pt'):
255                         return 'latexfontsize=' + x
256                 else:
257                         return x 
258                         
259         options = map (verbose_fontsize, options)
260
261         return options
262
263
264 def completize_preamble (str):
265         m = re.search (get_re ('preamble-end'), str)
266         if not m:
267                 return str
268         
269         preamble = str [:m.start (0)]
270         str = str [m.start(0):]
271         
272         if not re.search (get_re('def-post-re'), preamble):
273                 preamble = preamble + get_re('default-post')
274         if not re.search (get_re ('def-pre-re'),  preamble):
275                 preamble = preamble + get_re ('default-pre')
276
277         if  re.search ('\\\\includegraphics', str) and not re.search ('usepackage{graphics}',str):
278                 preamble = preamble + '\\usepackage{graphics}\n'
279
280         return preamble + str
281         
282 def find_mudela_sections (str):
283         """Find mudela blocks, while watching for verbatim. Returns
284         (STR,MUDS) with \mudelagraphic substituted for the blocks in STR,
285         and the blocks themselves MUDS"""
286         
287         mudelas = []
288         verbblocks = []
289         noverbblocks = []
290
291         while str:
292                 m = re.search (get_re ('verbatim'), str)
293                 m2 = re.search (get_re ("verb"), str)
294
295                 if  m == None and m2 == None:
296                         noverbblocks.append (str)
297                         str = ''
298                         break
299
300                 if m == None:
301                         m = m2
302
303                 if m2 and m2.start (0) < m.start (0):
304                         m = m2
305                         
306                 noverbblocks.append (str[:m.start (0)])
307                 verbblocks.append (m.group (0))
308                 str = str [m.end(0):]
309
310         def mudela_short (match):
311                 "Find \mudela{}, and substitute appropriate \begin / \end blocks."
312                 opts = match.group (1)
313                 if opts:
314                         opts = ',' + opts[1:-1]
315                 else:
316                         opts = ''
317                 return r"""\begin[eps,fragment%s]{mudela}
318   \context Staff <
319     \context Voice{
320       %s
321     }
322   >
323 \end{mudela}""" % (opts, match.group (2))
324
325         def mudela_file (match):
326                 "Find \mudelafile, and substitute appropriate \begin / \end blocks."
327                 d = [] #, d = retdeps
328                 full_path = find_file (match.group (2))
329                 if not full_path:
330                         error("error: can't find file `%s'\n" % match.group(2))
331
332                 d.append (full_path)
333                 f = open (full_path)
334                 str = f.read (-1)
335                 opts = match.group (1)
336                 if opts:
337                         opts = re.split (',[ \n\t]*', opts[1:-1])
338                 else:
339                         opts = []
340
341                 if re.search ('.fly$', full_path):
342                         opts.append ('fly')
343                 elif re.search ('.sly$', full_path):
344                         opts = opts + [ 'fly','fragment']
345                 elif re.search ('.ly$', full_path):
346                         opts .append ('nofly')
347                         
348                 str_opts = string.join (opts, ',')
349                 if str_opts: str_opts = '[' + str_opts + ']'
350
351
352                 str = "%% copied from %s" % full_path + str 
353                 return get_re ('include-mudela') % (str_opts, str)
354
355
356         def find_one_mudela_block (match,muds =mudelas):
357                 "extract body and options from a mudela block, and append into MUDELAS"
358                 opts = match.group (1)
359                 if opts:
360                         opts = opts[1:-1]
361                 else:
362                         opts = ''
363                         
364                 body = match.group (2)
365                 optlist = re.split (', *', opts)
366                 muds.append ((body, optlist))
367
368                 return '\\mudelagraphic\n'#  UGH.
369
370         doneblocks = []
371         for b in noverbblocks:
372                 b = re.sub (get_re('mudela-file'),  mudela_file, b)
373                 b = re.sub (get_re('mudela'), mudela_short, b)
374                 b = re.sub (get_re ("mudela-block"),  find_one_mudela_block, b)
375                 doneblocks.append (b)
376
377         allblocks = []
378         verbblocks.append ('')
379         while doneblocks:
380                 allblocks = allblocks + doneblocks[0:1] +  verbblocks[0:1]
381                 verbblocks = verbblocks[1:]
382                 doneblocks =doneblocks[1:]
383
384         str = string.join (allblocks,'')
385
386         return (str, mudelas)
387
388
389 def eps_file_cs (base):
390         if format == 'latex':
391                 return
392         els
393
394 def tex_file_cs (base):
395         return 
396
397
398
399
400 def make_files (str, mudelas, filename):        
401         (chapter, section, count) = (0,0,0)
402         total = 0
403         done = ''
404
405         # number them.
406         numbering = []
407         current_opts = []
408         while str:
409                 m = re.search (get_re ('interesting-cs'), str)
410                 if not m:
411                         done = done + str
412                         str = ''
413                         break
414                 
415                 done = done + str[:m.end (0)]
416                 str = str[m.end(0):]
417                 g = m.group (1)
418
419                 if g == 'twocolumn':
420                         current_opts.append ('twocolumn')
421                 elif g  == 'onecolumn':
422                         try:
423                                 current_opts.remove ('twocolumn')
424                         except IndexError:
425                                 pass
426                 if g == 'mudelagraphic':
427                         numbering.append ((chapter, section, count, current_opts[:]))
428                         count = count + 1
429                 elif g == 'chapter':
430                         (chapter, section, count)  = (chapter + 1, 0, 0)
431                 elif g == 'section' or g == 'node':
432                         (section, count)  = (section + 1, 0)
433                         
434
435         todo = []
436         str = done
437         
438
439         done = ''
440         while str:
441                 m = re.search ('\\\\mudelagraphic', str)
442                 if not m:
443                         done = done + str;
444                         str = ''
445                         break
446
447                 done = done + str[:m.start(0)]
448                 str = str[m.end(0):]
449                 
450                 (c1,c2,c3, file_opts) = numbering[0]
451                 (body, opts) = mudelas[0]
452                 numbering = numbering[1:]
453                 mudelas = mudelas[1:]
454                 
455                 opts = opts + file_opts
456
457                 base = '%s-%d.%d.%d'  % (filename, c1, c2,c3)
458                 if 'verbatim' in opts:
459                         done = done + get_re ('quote-verbatim') % body
460                         
461
462                 body = compose_full_body (body, opts)
463                 updated = update_file (body, base + '.ly')
464                 def is_updated (extension, t = todo):
465                         for x in t:
466                                 if t[0] == extension:
467                                         return 1
468                         return 0
469
470                 if not os.path.isfile (base + '.tex') or updated:
471                         todo.append (('tex', base, opts))
472                         updated = 1
473
474                 for o in opts:
475                         m = re.search ('intertext="(.*?)"', o)
476                         if m:
477                                 done = done  + m.group (1)
478
479                 if format == 'texi':
480                         opts.append ('png')
481                         
482                 if 'png' in opts:
483                         opts.append ('eps')
484
485                 if 'eps' in opts and (is_updated ('tex') or
486                                       not os.path.isfile (base + '.eps')):
487                         todo.append (('eps', base, opts))
488
489                 if 'png' in opts and (is_updated ('eps') or
490                                       not os.path.isfile (base + '.png')):
491                         todo.append (('png', base, opts))
492                         
493                 if format == 'latex':
494                         if 'eps' in opts :
495                                 done = done + get_re ('output-eps') %  (base, base )
496                         else:
497                                 done = done + get_re ('output-tex') % base
498                 elif format == 'texi':
499                         done = done + get_re ('output-all') % (base, base) 
500
501
502         compile_all_files (todo)
503
504         def find_eps_dims (match):
505                 "Fill in dimensions of EPS files."
506                 fn =match.group (1)
507                 dims = bounding_box_dimensions (fn)
508                 
509                 return '%ipt' % dims[0]
510
511         done = re.sub (r"""\\mudelaepswidth{(.*?)}""", find_eps_dims, done)
512
513         return done 
514
515 def system (cmd):
516         sys.stderr.write ("invoking `%s'\n" % cmd)
517         st = os.system (cmd)
518         if st:
519                 sys.stderr.write ('Error command exited with value %d\n' % st)
520         return st
521
522 def compile_all_files ( list):
523         eps = []
524         tex = []
525         gif = []
526         for l in list:
527                 if l[0] == 'eps':
528                         eps.append (l[1])
529                 elif l[0] == 'tex':
530                         tex.append (l[1] + '.ly')
531                 elif l[0] == 'png':
532                         gif.append (l[1])
533
534         if tex:
535                 lilyopts = map (lambda x:  '-I ' + x, include_path)
536                 texfiles = string.join (tex, ' ')
537                 lilyopts = string.join (lilyopts, ' ' )
538                 system ('lilypond %s %s' % (lilyopts, texfiles))
539
540
541         for e in eps:
542                 cmd = r"""tex %s; dvips -E -o %s %s""" % \
543                       (e, e + '.eps', e)
544                 system (cmd)
545
546         for g in gif:
547                 cmd = r"""gs -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r90 -dNOPAUSE %s -c quit | pnmcrop | pnmtopng > %s"""
548
549                 cmd = cmd % (g + '.eps', g + '.png')
550                 system (cmd)
551
552         
553 def update_file (body, name):
554         same = 0
555         try:
556                 f = open (name)
557                 fs = f.read (-1)
558                 same = (fs == body)
559         except:
560                 pass
561
562         if not same:
563                 f = open (name , 'w')
564                 f.write (body)
565                 f.close ()
566
567         
568         return not same
569
570
571
572 def getopt_args (opts):
573         "Construct arguments (LONG, SHORT) for getopt from  list of options."
574         short = ''
575         long = []
576         for o in opts:
577                 if o[1]:
578                         short = short + o[1]
579                         if o[0]:
580                                 short = short + ':'
581                 if o[2]:
582                         l = o[2]
583                         if o[0]:
584                                 l = l + '='
585                         long.append (l)
586         return (short, long)
587
588 def option_help_str (o):
589         "Transform one option description (4-tuple ) into neatly formatted string"
590         sh = '  '       
591         if o[1]:
592                 sh = '-%s' % o[1]
593
594         sep = ' '
595         if o[1] and o[2]:
596                 sep = ','
597                 
598         long = ''
599         if o[2]:
600                 long= '--%s' % o[2]
601
602         arg = ''
603         if o[0]:
604                 if o[2]:
605                         arg = '='
606                 arg = arg + o[0]
607         return '  ' + sh + sep + long + arg
608
609
610 def options_help_str (opts):
611         "Transform a list of options into a neatly formatted string"
612         w = 0
613         strs =[]
614         helps = []
615         for o in opts:
616                 s = option_help_str (o)
617                 strs.append ((s, o[3]))
618                 if len (s) > w:
619                         w = len (s)
620
621         str = ''
622         for s in strs:
623                 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
624         return str
625
626 def help():
627         sys.stdout.write("""Usage: mudela-book [options] FILE\n
628 Generate hybrid LaTeX input from Latex + mudela
629 Options:
630 """)
631         sys.stdout.write (options_help_str (options))
632         sys.stdout.write (r"""Warning all output is written in the CURRENT directory
633
634
635
636 Report bugs to bug-gnu-music@gnu.org.
637
638 Written by Tom Cato Amundsen <tomcato@xoommail.com> and
639 Han-Wen Nienhuys <hanwen@cs.uu.nl>
640 """)
641
642         sys.exit (0)
643
644
645 def write_deps (fn, target,  deps):
646         sys.stdout.write('writing `%s\'\n' % fn)
647
648         f = open (fn, 'w')
649         
650         target = target + '.latex'
651         f.write ('%s: %s\n'% (target, string.join (deps, ' ')))
652         f.close ()
653
654                 
655 def identify():
656         sys.stdout.write ('mudela-book (GNU LilyPond) %s\n' % program_version)
657
658 def print_version ():
659         identify()
660         sys.stdout.write (r"""Copyright 1998--1999
661 Distributed under terms of the GNU General Public License. It comes with
662 NO WARRANTY.
663 """)
664
665
666 def main():
667         global outdir, initfile, defined_mudela_cmd, defined_mudela_cmd_re
668         outname = ''
669         try:
670                 (sh, long) = getopt_args (__main__.options)
671                 (options, files) = getopt.getopt(sys.argv[1:], sh, long)
672         except getopt.error, msg:
673                 sys.stderr.write("error: %s" % msg)
674                 sys.exit(1)
675
676         do_deps = 0
677         for opt in options:     
678                 o = opt[0]
679                 a = opt[1]
680                 
681                 if o == '--include' or o == '-I':
682                         include_path.append (a)
683                 elif o == '--version':
684                         print_version ()
685                         sys.exit  (0)
686
687                 elif o == '--format' or o == '-o':
688                         __main__.format = a
689                 elif o == '--outname' or o == '-o':
690                         if len(files) > 1:
691                                 #HACK
692                                 sys.stderr.write("Mudela-book is confused by --outname on multiple files")
693                                 sys.exit(1)
694                         outname = a
695                 elif o == '--outdir' or o == '-d':
696                         outdir = a
697                 elif o == '--help' or o == '-h':
698                         help ()
699                 elif o == '--dependencies':
700                         do_deps = 1
701                 elif o == '--default-mudela-fontsize':
702                         default_music_fontsize = string.atoi (a)
703                 elif o == '--init':
704                         initfile =  a
705         
706         identify()
707
708         for input_filename in files:
709                 file_settings = {}
710                 if outname:
711                         my_outname = outname
712                 else:
713                         my_outname = os.path.basename(os.path.splitext(input_filename)[0])
714                 my_depname = my_outname + '.dep'                
715
716                 (input, deps) = read_tex_file (input_filename)
717                 (input, muds) = find_mudela_sections (input)
718                 output = make_files  (input, muds, my_outname)
719                 output = completize_preamble (output)
720
721                 foutn = my_outname + '.' + format
722                 sys.stderr.write ("Writing `%s'\n" % foutn)
723                 fout = open (foutn, 'w')
724
725                 fout.write (output)
726                 fout.close ()
727
728                 if do_deps:
729                         write_deps (my_depname, my_outname, deps)
730
731 main()