]> git.donarmstrong.com Git - lilypond.git/blob - scripts/mudela-book.py
8a334b40b68ff142b668fe101fd8e10404020a14
[lilypond.git] / scripts / mudela-book.py
1 #!@PYTHON@
2 # vim: set noexpandtab:
3 # TODO:
4 # * Figure out clean set of options.
5 # * add support for .lilyrc
6 # * %\def\preMudelaExample should be ignored by mudela-book because
7 #   it is commented out
8 # * if you run mudela-book once with --no-pictures, and then again
9 #   without the option, then the pngs will not be created. You have
10 #   to delete the generated .ly files and  rerun mudela-book.
11 # * kontroller hvordan det skannes etter preMudelaExample i preamble
12 #   det ser ut til at \usepackage{graphics} legges til bare hvis
13 #   preMudelaExample ikke finnes.
14 # * add suppoert for @c comments. Check that preamble scanning works after this.
15
16 # * in LaTeX, commenting out blocks like this
17 # %\begin{mudela}
18 # %c d e
19 # %\end{mudela} works as expected.
20 # * \usepackage{landscape} is gone. Convince me it is really neede to get it back.
21 # * We are calculating more of the linewidths, for example 2 col from 1 col.
22
23
24
25 # This is was the idea for handling of comments:
26 #       Multiline comments, @ignore .. @end ignore is scanned for
27 #       in read_doc_file, and the chunks are marked as 'ignore', so
28 #       mudela-book will not touch them any more. The content of the
29 #       chunks are written to the output file. Also 'include' and 'input'
30 #       regex has to check if they are commented out.
31 #
32 #       Then it is scanned for 'mudela', 'mudela-file' and 'mudela-block'.
33 #       These three regex's has to check if they are on a commented line,
34 #       % for latex, @c for texinfo.
35 #
36 #       Then lines that are commented out with % (latex) and @c (Texinfo)
37 #       are put into chunks marked 'ignore'. This cannot be done before
38 #       searching for the mudela-blocks because % is also the comment character
39 #       for lilypond.
40 #
41 #       The the rest of the rexeces are searched for. They don't have to test
42 #       if they are on a commented out line.
43
44 import os
45 import stat
46 import string
47 import re
48 import getopt
49 import sys
50 import __main__
51 import operator
52
53
54 program_version = '@TOPLEVEL_VERSION@'
55 if program_version == '@' + 'TOPLEVEL_VERSION' + '@':
56         program_version = '1.3.85'      
57
58 include_path = [os.getcwd()]
59
60 g_dep_prefix = ''
61 g_outdir = ''
62 g_force_mudela_fontsize = 0
63 g_read_lys = 0
64 g_do_pictures = 1
65 g_num_cols = 1
66 format = ''
67 g_run_lilypond = 1
68 no_match = 'a\ba'
69
70 default_music_fontsize = 16
71 default_text_fontsize = 12
72
73
74 class LatexPaper:
75         def __init__(self):
76                 self.m_paperdef =  {
77                         # the dimentions are from geometry.sty
78                         'a0paper': (mm2pt(841), mm2pt(1189)),
79                         'a1paper': (mm2pt(595), mm2pt(841)),
80                         'a2paper': (mm2pt(420), mm2pt(595)),
81                         'a3paper': (mm2pt(297), mm2pt(420)),
82                         'a4paper': (mm2pt(210), mm2pt(297)),
83                         'a5paper': (mm2pt(149), mm2pt(210)),
84                         'b0paper': (mm2pt(1000), mm2pt(1414)),
85                         'b1paper': (mm2pt(707), mm2pt(1000)),
86                         'b2paper': (mm2pt(500), mm2pt(707)),
87                         'b3paper': (mm2pt(353), mm2pt(500)),
88                         'b4paper': (mm2pt(250), mm2pt(353)),
89                         'b5paper': (mm2pt(176), mm2pt(250)),
90                         'letterpaper': (in2pt(8.5), in2pt(11)),
91                         'legalpaper': (in2pt(8.5), in2pt(14)),
92                         'executivepaper': (in2pt(7.25), in2pt(10.5))}
93                 self.m_use_geometry = None
94                 self.m_papersize = 'letterpaper'
95                 self.m_fontsize = 10
96                 self.m_num_cols = 1
97                 self.m_landscape = 0
98                 self.m_geo_landscape = 0
99                 self.m_geo_width = None
100                 self.m_geo_textwidth = None
101                 self.m_geo_lmargin = None
102                 self.m_geo_rmargin = None
103                 self.m_geo_includemp = None
104                 self.m_geo_marginparwidth = {10: 57, 11: 50, 12: 35}
105                 self.m_geo_marginparsep = {10: 11, 11: 10, 12: 10}
106                 self.m_geo_x_marginparwidth = None
107                 self.m_geo_x_marginparsep = None
108                 self.__body = None
109         def set_geo_option(self, name, value):
110                 if name == 'body' or name == 'text':
111                         if type(value) == type(""):
112                                 self._set_dimen('m_geo_textwidth', value)
113                         else:
114                                 self._set_dimen('m_geo_textwidth', value[0])
115                         self.__body = 1
116                 elif name == 'portrait':
117                         self.m_geo_landscape = 0
118                 elif name == 'reversemp' or name == 'reversemarginpar':
119                         if self.m_geo_includemp == None:
120                                 self.m_geo_includemp = 1
121                 elif name == 'marginparwidth' or name == 'marginpar':
122                         self._set_dimen('m_geo_x_marginparwidth', value)
123                         self.m_geo_includemp = 1
124                 elif name == 'marginparsep':
125                         self._set_dimen('m_geo_x_marginparsep', value)
126                         self.m_geo_includemp = 1
127                 elif name == 'scale':
128                         if type(value) == type(""):
129                                 self.m_geo_width = self.get_paperwidth() * float(value)
130                         else:
131                                 self.m_geo_width = self.get_paperwidth() * float(value[0])
132                 elif name == 'hscale':
133                         self.m_geo_width = self.get_paperwidth() * float(value)
134                 elif name == 'left' or name == 'lmargin':
135                         self._set_dimen('m_geo_lmargin', value)
136                 elif name == 'right' or name == 'rmargin':
137                         self._set_dimen('m_geo_rmargin', value)
138                 elif name == 'hdivide' or name == 'divide':
139                         if value[0] not in ('*', ''):
140                                 self._set_dimen('m_geo_lmargin', value[0])
141                         if value[1] not in ('*', ''):
142                                 self._set_dimen('m_geo_width', value[1])
143                         if value[2] not in ('*', ''):
144                                 self._set_dimen('m_geo_rmargin', value[2])
145                 elif name == 'hmargin':
146                         if type(value) == type(""):
147                                 self._set_dimen('m_geo_lmargin', value)
148                                 self._set_dimen('m_geo_rmargin', value)
149                         else:
150                                 self._set_dimen('m_geo_lmargin', value[0])
151                                 self._set_dimen('m_geo_rmargin', value[1])
152                 elif name == 'margin':#ugh there is a bug about this option in
153                                         # the geometry documentation
154                         if type(value) == type(""):
155                                 self._set_dimen('m_geo_lmargin', value)
156                                 self._set_dimen('m_geo_rmargin', value)
157                         else:
158                                 self._set_dimen('m_geo_lmargin', value[0])
159                                 self._set_dimen('m_geo_rmargin', value[0])
160                 elif name == 'total':
161                         if type(value) == type(""):
162                                 self._set_dimen('m_geo_width', value)
163                         else:
164                                 self._set_dimen('m_geo_width', value[0])
165                 elif name == 'width' or name == 'totalwidth':
166                         self._set_dimen('m_geo_width', value)
167                 elif name == 'paper' or name == 'papername':
168                         self.m_papersize = value
169                 elif name[-5:] == 'paper':
170                         self.m_papersize = name
171                 else:
172                         self._set_dimen('m_geo_'+name, value)
173         def _set_dimen(self, name, value):
174                 if type(value) == type("") and value[-2:] == 'pt':
175                         self.__dict__[name] = float(value[:-2])
176                 elif type(value) == type("") and value[-2:] == 'mm':
177                         self.__dict__[name] = mm2pt(float(value[:-2]))
178                 elif type(value) == type("") and value[-2:] == 'cm':
179                         self.__dict__[name] = 10 * mm2pt(float(value[:-2]))
180                 elif type(value) == type("") and value[-2:] == 'in':
181                         self.__dict__[name] = in2pt(float(value[:-2]))
182                 else:
183                         self.__dict__[name] = value
184         def display(self):
185                 print "LatexPaper:\n-----------"
186                 for v in self.__dict__.keys():
187                         if v[:2] == 'm_':
188                                 print v, self.__dict__[v]
189                 print "-----------"
190         def get_linewidth(self):
191                 w = self._calc_linewidth()
192                 if self.m_num_cols == 2:
193                         return (w - 10) / 2
194                 else:
195                         return w
196         def get_paperwidth(self):
197                 #if self.m_use_geometry:
198                         return self.m_paperdef[self.m_papersize][self.m_landscape or self.m_geo_landscape]
199                 #return self.m_paperdef[self.m_papersize][self.m_landscape]
200         
201         def _calc_linewidth(self):
202                 # since geometry sometimes ignores 'includemp', this is
203                 # more complicated than it should be
204                 mp = 0
205                 if self.m_geo_includemp:
206                         if self.m_geo_x_marginparsep is not None:
207                                 mp = mp + self.m_geo_x_marginparsep
208                         else:
209                                 mp = mp + self.m_geo_marginparsep[self.m_fontsize]
210                         if self.m_geo_x_marginparwidth is not None:
211                                 mp = mp + self.m_geo_x_marginparwidth
212                         else:
213                                 mp = mp + self.m_geo_marginparwidth[self.m_fontsize]
214                 if self.__body:#ugh test if this is necessary
215                         mp = 0
216                 def tNone(a, b, c):
217                         return a == None, b == None, c == None
218                 if not self.m_use_geometry:
219                         return latex_linewidths[self.m_papersize][self.m_fontsize]
220                 else:
221                         if tNone(self.m_geo_lmargin, self.m_geo_width,
222                                 self.m_geo_rmargin) == (1, 1, 1):
223                                 if self.m_geo_textwidth:
224                                         return self.m_geo_textwidth
225                                 w = self.get_paperwidth() * 0.8
226                                 return w - mp
227                         elif tNone(self.m_geo_lmargin, self.m_geo_width,
228                                  self.m_geo_rmargin) == (0, 1, 1):
229                                  if self.m_geo_textwidth:
230                                         return self.m_geo_textwidth
231                                  return self.f1(self.m_geo_lmargin, mp)
232                         elif tNone(self.m_geo_lmargin, self.m_geo_width,
233                                  self.m_geo_rmargin) == (1, 1, 0):
234                                  if self.m_geo_textwidth:
235                                         return self.m_geo_textwidth
236                                  return self.f1(self.m_geo_rmargin, mp)
237                         elif tNone(self.m_geo_lmargin, self.m_geo_width,
238                                 self.m_geo_rmargin) \
239                                         in ((0, 0, 1), (1, 0, 0), (1, 0, 1)):
240                                 if self.m_geo_textwidth:
241                                         return self.m_geo_textwidth
242                                 return self.m_geo_width - mp
243                         elif tNone(self.m_geo_lmargin, self.m_geo_width,
244                                 self.m_geo_rmargin) in ((0, 1, 0), (0, 0, 0)):
245                                 w = self.get_paperwidth() - self.m_geo_lmargin - self.m_geo_rmargin - mp
246                                 if w < 0:
247                                         w = 0
248                                 return w
249                         raise "Never do this!"
250         def f1(self, m, mp):
251                 tmp = self.get_paperwidth() - m * 2 - mp
252                 if tmp < 0:
253                         tmp = 0
254                 return tmp
255         def f2(self):
256                 tmp = self.get_paperwidth() - self.m_geo_lmargin \
257                         - self.m_geo_rmargin
258                 if tmp < 0:
259                         return 0
260                 return tmp
261
262 class TexiPaper:
263         def __init__(self):
264                 self.m_papersize = 'a4'
265                 self.m_fontsize = 12
266         def get_linewidth(self):
267                 return texi_linewidths[self.m_papersize][self.m_fontsize]
268
269 def mm2pt(x):
270         return x * 2.8452756
271 def in2pt(x):
272         return x * 72.26999
273 def em2pt(x, fontsize):
274         return {10: 10.00002, 11: 10.8448, 12: 11.74988}[fontsize] * x
275 def ex2pt(x, fontsize):
276         return {10: 4.30554, 11: 4.7146, 12: 5.16667}[fontsize] * x
277         
278 # latex linewidths:
279 # indices are no. of columns, papersize,  fontsize
280 # Why can't this be calculated?
281 latex_linewidths = {
282         'a4paper':{10: 345, 11: 360, 12: 390},
283         'a4paper-landscape': {10: 598, 11: 596, 12:592},
284         'a5paper':{10: 276, 11: 276, 12: 276},
285         'b5paper':{10: 345, 11: 356, 12: 356},
286         'letterpaper':{10: 345, 11: 360, 12: 390},
287         'letterpaper-landscape':{10: 598, 11: 596, 12:596},
288         'legalpaper': {10: 345, 11: 360, 12: 390},
289         'executivepaper':{10: 345, 11: 360, 12: 379}}
290
291 texi_linewidths = {
292         'a4': {12: 455},
293         'a4wide': {12: 470},
294         'smallbook': {12: 361},
295         'texidefault': {12: 433}}
296
297 option_definitions = [
298   ('EXT', 'f', 'format', 'set format.  EXT is one of texi and latex.'),
299   ('DIM',  '', 'default-music-fontsize', 'default fontsize for music.  DIM is assumed to be in points'),
300   ('DIM',  '', 'default-mudela-fontsize', 'deprecated, use --default-music-fontsize'),
301   ('DIM', '', 'force-music-fontsize', 'force fontsize for all inline mudela. DIM is assumed be to in points'),
302   ('DIM', '', 'force-mudela-fontsize', 'deprecated, use --force-music-fontsize'),
303   ('DIR', 'I', 'include', 'include path'),
304   ('', 'M', 'dependencies', 'write dependencies'),
305   ('PREF', '',  'dep-prefix', 'prepend PREF before each -M dependency'),
306   ('', 'n', 'no-lily', 'don\'t run lilypond'),
307   ('', '', 'no-pictures', "don\'t generate pictures"),
308   ('', '', 'read-lys', "don't write ly files."),
309   ('FILE', 'o', 'outname', 'filename main output file'),
310   ('FILE', '', 'outdir', "where to place generated files"),
311   ('', 'v', 'version', 'print version information' ),
312   ('', 'h', 'help', 'print help'),
313   ]
314
315 # format specific strings, ie. regex-es for input, and % strings for output
316 output_dict= {
317         'latex': {
318                 'output-mudela-fragment' : r"""\begin[eps,singleline,%s]{mudela}
319   \context Staff <
320     \context Voice{
321       %s
322     }
323   >
324 \end{mudela}""", 
325                 'output-mudela':r"""\begin[%s]{mudela}
326 %s
327 \end{mudela}""",
328                 'output-verbatim': "\\begin{verbatim}%s\\end{verbatim}",
329                 'output-default-post': r"""\def\postMudelaExample{}""",
330                 'output-default-pre': r"""\def\preMudelaExample{}""",
331                 'output-eps': '\\noindent\\parbox{\\mudelaepswidth{%(fn)s.eps}}{\includegraphics{%(fn)s.eps}}',
332                 'output-tex': '\\preMudelaExample \\input %(fn)s.tex \\postMudelaExample\n',
333                 'pagebreak': r'\pagebreak',
334                 },
335         'texi' : {'output-mudela': """@mudela[%s]
336 %s
337 @end mudela 
338 """,
339                   'output-mudela-fragment': """@mudela[%s]
340 \context Staff\context Voice{ %s }
341 @end mudela """,
342                   'pagebreak': None,
343                   'output-verbatim': r"""@example
344 %s
345 @end example
346 """,
347
348 # do some tweaking: @ is needed in some ps stuff.
349 # override EndLilyPondOutput, since @tex is done
350 # in a sandbox, you can't do \input lilyponddefs at the
351 # top of the document.
352
353 # should also support fragment in
354                   
355                   'output-all': r"""@tex
356 \catcode`\@=12
357 \input lilyponddefs
358 \def\EndLilyPondOutput{}
359 \input %(fn)s.tex
360 \catcode`\@=0
361 @end tex
362 @html
363 <p>
364 <img src=%(fn)s.png>
365 @end html
366 """,
367                 }
368         }
369
370 def output_verbatim (body):#ugh .format
371         if __main__.format == 'texi':
372                 body = re.sub ('([@{}])', '@\\1', body)
373         return get_output ('output-verbatim') % body
374
375 def output_mbverbatim (body):#ugh .format
376         if __main__.format == 'texi':
377                 body = re.sub ('([@{}])', '@\\1', body)
378         return get_output ('output-verbatim') % body
379
380 re_dict = {
381         'latex': {'input': r'(?m)^[^%\n]*?(?P<match>\\mbinput{?([^}\t \n}]*))',
382                   'include': r'(?m)^[^%\n]*?(?P<match>\\mbinclude{(?P<filename>[^}]+)})',
383                   'option-sep' : ', *',
384                   'header': r"\\documentclass\s*(\[.*?\])?",
385                   'geometry': r"^(?m)[^%\n]*?\\usepackage\s*(\[(?P<options>.*)\])?\s*{geometry}",
386                   'preamble-end': r'(?P<code>\\begin{document})',
387                   'verbatim': r"(?s)(?P<code>\\begin{verbatim}.*?\\end{verbatim})",
388                   'verb': r"(?P<code>\\verb(?P<del>.).*?(?P=del))",
389                   'mudela-file': r'(?m)^[^%\n]*?(?P<match>\\mudelafile(\[(?P<options>.*?)\])?\{(?P<filename>.+)})',
390                   'mudela' : r'(?m)^[^%\n]*?(?P<match>\\mudela(\[(?P<options>.*?)\])?{(?P<code>.*?)})',
391                   'mudela-block': r"(?sm)^[^%\n]*?(?P<match>\\begin(\[(?P<options>.*?)\])?{mudela}(?P<code>.*?)\\end{mudela})",
392                   'def-post-re': r"\\def\\postMudelaExample",
393                   'def-pre-re': r"\\def\\preMudelaExample",               
394                   'intertext': r',?\s*intertext=\".*?\"',
395                   'multiline-comment': no_match,
396                   'singleline-comment': r"(?m)(?P<code>^%.*$\n+)",
397                   'numcols': r"(?P<code>\\(?P<num>one|two)column)",
398                   },
399         
400         'texi': {
401                  'include':  '(?m)^[^%\n]*?(?P<match>@mbinclude[ \n\t]+(?P<filename>[^\t \n]*))',
402                  'input': no_match,
403                  'header': no_match,
404                  'preamble-end': no_match,
405                  'landscape': no_match,
406                  'verbatim': r"""(?s)(?P<code>@example\s.*?@end example\s)""",
407                  'verb': r"""(?P<code>@code{.*?})""",
408                  'mudela-file': '(?P<match>@mudelafile(\[(?P<options>.*?)\])?{(?P<filename>[^}]+)})',
409                  'mudela' : '(?m)^(?!@c)(?P<match>@mudela(\[(?P<options>.*?)\])?{(?P<code>.*?)})',
410                  #ugh add check for @c
411                  'mudela-block': r"""(?m)^(?!@c)(?P<match>(?s)(?P<match>@mudela(\[(?P<options>.*?)\])?\s(?P<code>.*?)@end mudela\s))""",
412                   'option-sep' : ', *',
413                   'intertext': r',?\s*intertext=\".*?\"',
414                   #ugh fix
415                   'multiline-comment': r"(?s)(?P<code>@ignore\s.*?@end ignore)\s",
416                   'singleline-comment': r"(?m)(?P<code>^@c.*$\n+)",
417                   'numcols': no_match,
418                  }
419         }
420
421
422 for r in re_dict.keys ():
423         olddict = re_dict[r]
424         newdict = {}
425         for k in olddict.keys ():
426                 newdict[k] = re.compile (olddict[k])
427         re_dict[r] = newdict
428
429         
430 def uniq (list):
431         list.sort ()
432         s = list
433         list = []
434         for x in s:
435                 if x not in list:
436                         list.append (x)
437         return list
438                 
439
440 def get_output (name):
441         return  output_dict[format][name]
442
443 def get_re (name):
444         return  re_dict[format][name]
445
446 def bounding_box_dimensions(fname):
447         try:
448                 fd = open(fname)
449         except IOError:
450                 error ("Error opening `%s'" % fname)
451         str = fd.read ()
452         s = re.search('%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)', str)
453         if s:
454                 return (int(s.group(3))-int(s.group(1)), 
455                         int(s.group(4))-int(s.group(2)))
456         else:
457                 return (0,0)
458
459
460 def error (str):
461         sys.stderr.write (str + "\n  Exiting ... \n\n")
462         raise 'Exiting.'
463
464
465 def compose_full_body (body, opts):
466         """Construct the mudela code to send to Lilypond.
467         Add stuff to BODY using OPTS as options."""
468         music_size = default_music_fontsize
469         latex_size = default_text_fontsize
470         for o in opts:
471                 if g_force_mudela_fontsize:
472                         music_size = g_force_mudela_fontsize
473                 else:
474                         m = re.match ('([0-9]+)pt', o)
475                         if m:
476                                 music_size = string.atoi(m.group (1))
477
478                 m = re.match ('latexfontsize=([0-9]+)pt', o)
479                 if m:
480                         latex_size = string.atoi (m.group (1))
481
482         if re.search ('\\\\score', body):
483                 is_fragment = 0
484         else:
485                 is_fragment = 1
486         if 'fragment' in opts:
487                 is_fragment = 1
488         if 'nonfragment' in opts:
489                 is_fragment = 0
490
491         if is_fragment and not 'multiline' in opts:
492                 opts.append('singleline')
493         if 'singleline' in opts:
494                 l = -1.0;
495         else:
496                 l = paperguru.get_linewidth()
497         
498         if 'relative' in opts:#ugh only when is_fragment
499                 body = '\\relative c { %s }' % body
500         
501         if is_fragment:
502                 body = r"""\score { 
503  \notes { %s }
504   \paper { }  
505 }""" % body
506
507         opts = uniq (opts)
508         optstring = string.join (opts, ' ')
509         optstring = re.sub ('\n', ' ', optstring)
510         body = r"""
511 %% Generated by mudela-book.py; options are %s  %%ughUGH not original options
512 \include "paper%d.ly"
513 \paper  { linewidth = %f \pt; } 
514 """ % (optstring, music_size, l) + body
515         return body
516
517 def parse_options_string(s):
518         d = {}
519         r1 = re.compile("((\w+)={(.*?)})((,\s*)|$)")
520         r2 = re.compile("((\w+)=(.*?))((,\s*)|$)")
521         r3 = re.compile("(\w+?)((,\s*)|$)")
522         while s:
523                 m = r1.match(s)
524                 if m:
525                         s = s[m.end():]
526                         d[m.group(2)] = re.split(",\s*", m.group(3))
527                         continue
528                 m = r2.match(s)
529                 if m:
530                         s = s[m.end():]
531                         d[m.group(2)] = m.group(3)
532                         continue
533                 m = r3.match(s)
534                 if m:
535                         s = s[m.end():]
536                         d[m.group(1)] = 1
537                         continue
538                 print "trøbbel:%s:" % s
539         return d
540
541 def scan_latex_preamble(chunks):
542         # first we want to scan the \documentclass line
543         # it should be the first non-comment line
544         idx = 0
545         while 1:
546                 if chunks[idx][0] == 'ignore':
547                         idx = idx + 1
548                         continue
549                 m = get_re ('header').match(chunks[idx][1])
550                 options = re.split (',[\n \t]*', m.group(1)[1:-1])
551                 for o in options:
552                         if o == 'landscape':
553                                 paperguru.m_landscape = 1
554                         m = re.match("(.*?)paper", o)
555                         if m:
556                                 paperguru.m_papersize = m.group()
557                         else:
558                                 m = re.match("(\d\d)pt", o)
559                                 if m:
560                                         paperguru.m_fontsize = int(m.group(1))
561                         
562                 break
563         while chunks[idx][0] != 'preamble-end':
564                 if chunks[idx] == 'ignore':
565                         idx = idx + 1
566                         continue
567                 m = get_re ('geometry').search(chunks[idx][1])
568                 if m:
569                         paperguru.m_use_geometry = 1
570                         o = parse_options_string(m.group('options'))
571                         for k in o.keys():
572                                 paperguru.set_geo_option(k, o[k])
573                 idx = idx + 1
574
575 def scan_preamble (chunks):
576         if __main__.format == 'texi':
577                 #ugh has to be fixed when @c comments are implemented
578                 # also the searching here is far from bullet proof.
579                 if string.find(chunks[0][1], "@afourpaper") != -1:
580                         paperguru.m_papersize = 'a4'
581                 elif string.find(chunks[0][1], "@afourwide") != -1:
582                         paperguru.m_papersize = 'a4wide'
583                 elif string.find(chunks[0][1], "@smallbook") != -1:
584                         paperguru.m_papersize = 'smallbook'
585         else:
586                 assert __main__.format == 'latex'
587                 scan_latex_preamble(chunks)
588                 
589
590 def completize_preamble (str):
591         m = get_re ('preamble-end').search( str)
592         if not m:
593                 return str
594         
595         preamble = str [:m.start (0)]
596         str = str [m.start(0):]
597         
598         if not get_re('def-post-re').search (preamble):
599                 preamble = preamble + get_output('output-default-post')
600         if not get_re ('def-pre-re').search(  preamble):
601                 preamble = preamble + get_output ('output-default-pre')
602
603         # UGH ! BUG!
604         #if  re.search ('\\\\includegraphics', str) and not re.search ('usepackage{graphics}',str):
605
606         preamble = preamble + '\\usepackage{graphics}\n'
607
608         return preamble + str
609
610
611 read_files = []
612 def find_file (name):
613         f = None
614         for a in include_path:
615                 try:
616                         nm = os.path.join (a, name)
617                         f = open (nm)
618                         __main__.read_files.append (nm)
619                         break
620                 except IOError:
621                         pass
622         if f:
623                 return f.read ()
624         else:
625                 error ("File not found `%s'\n" % name)
626                 return ''
627
628 def do_ignore(match_object):
629         return [('ignore', match_object.group('code'))]
630 def do_preamble_end(match_object):
631         return [('preamble-end', match_object.group('code'))]
632
633 def make_verbatim(match_object):
634         return [('verbatim', match_object.group('code'))]
635
636 def make_verb(match_object):
637         return [('verb', match_object.group('code'))]
638
639 def do_include_file(m):
640         "m: MatchObject"
641         return [('input', get_output ('pagebreak'))] \
642              + read_doc_file(m.group('filename')) \
643              + [('input', get_output ('pagebreak'))] 
644
645 def do_input_file(m):
646         return read_doc_file(m.group('filename'))
647
648 def make_mudela(m):
649         if m.group('options'):
650                 options = m.group('options')
651         else:
652                 options = ''
653         return [('input', get_output('output-mudela-fragment') % 
654                         (options, m.group('code')))]
655
656 def make_mudela_file(m):
657         if m.group('options'):
658                 options = m.group('options')
659         else:
660                 options = ''
661         return [('input', get_output('output-mudela') %
662                         (options, find_file(m.group('filename'))))]
663
664 def make_mudela_block(m):
665         if m.group('options'):
666                 options = get_re('option-sep').split (m.group('options'))
667         else:
668             options = []
669         options = filter(lambda s: s != '', options)
670         if 'mbverbatim' in options:#ugh this is ugly and only for texi format
671                 s  = m.group()
672                 im = get_re('intertext').search(s)
673                 if im:
674                         s = s[:im.start()] + s[im.end():]
675                 im = re.search('mbverbatim', s)
676                 if im:
677                         s = s[:im.start()] + s[im.end():]
678                 if s[:9] == "@mudela[]":
679                         s = "@mudela" + s[9:]
680                 return [('mudela', m.group('code'), options, s)]
681         return [('mudela', m.group('code'), options)]
682
683 def do_columns(m):
684         if __main__.format != 'latex':
685                 return []
686         if m.group('num') == 'one':
687                 return [('numcols', m.group('code'), 1)]
688         if m.group('num') == 'two':
689                 return [('numcols', m.group('code'), 2)]
690         
691 def new_chop_chunks(chunks, re_name, func):
692     newchunks = []
693     for c in chunks:
694         if c[0] == 'input':
695             str = c[1]
696             while str:
697                 m = get_re (re_name).search (str)
698                 if m == None:
699                     newchunks.append (('input', str))
700                     str = ''
701                 else:
702                     newchunks.append (('input', str[:m.start ('match')]))
703                     #newchunks.extend(func(m))
704                     # python 1.5 compatible:
705                     newchunks = newchunks + func(m)
706                     str = str [m.end(0):]
707         else:
708             newchunks.append(c)
709     return newchunks
710
711 def chop_chunks(chunks, re_name, func):
712     newchunks = []
713     for c in chunks:
714         if c[0] == 'input':
715             str = c[1]
716             while str:
717                 m = get_re (re_name).search (str)
718                 if m == None:
719                     newchunks.append (('input', str))
720                     str = ''
721                 else:
722                     newchunks.append (('input', str[:m.start (0)]))
723                     #newchunks.extend(func(m))
724                     # python 1.5 compatible:
725                     newchunks = newchunks + func(m)
726                     str = str [m.end(0):]
727         else:
728             newchunks.append(c)
729     return newchunks
730
731 def read_doc_file (filename):
732         """Read the input file, find verbatim chunks and do \input and \include
733         """
734         str = ''
735         str = find_file(filename)
736
737         if __main__.format == '':
738                 latex =  re.search ('\\\\document', str[:200])
739                 texinfo =  re.search ('@node|@setfilename', str[:200])
740                 if (texinfo and latex) or not (texinfo or latex):
741                         error("error: can't determine format, please specify")
742                 if texinfo:
743                         __main__.format = 'texi'
744                 else:
745                         __main__.format = 'latex'
746         if __main__.format == 'texi':
747                 __main__.paperguru = TexiPaper()
748         else:
749                 __main__.paperguru = LatexPaper()
750         chunks = [('input', str)]
751         # we have to check for verbatim before doing include,
752         # because we don't want to include files that are mentioned
753         # inside a verbatim environment
754         chunks = chop_chunks(chunks, 'verbatim', make_verbatim)
755         chunks = chop_chunks(chunks, 'verb', make_verb)
756         chunks = chop_chunks(chunks, 'multiline-comment', do_ignore)
757         #ugh fix input
758         chunks = new_chop_chunks(chunks, 'include', do_include_file)
759         chunks = new_chop_chunks(chunks, 'input', do_input_file)
760         return chunks
761
762
763 taken_file_names = {}
764 def schedule_mudela_block (chunk):
765         """Take the body and options from CHUNK, figure out how the
766         real .ly should look, and what should be left MAIN_STR (meant
767         for the main file).  The .ly is written, and scheduled in
768         TODO.
769
770         Return: a chunk (TYPE_STR, MAIN_STR, OPTIONS, TODO, BASE)
771
772         TODO has format [basename, extension, extension, ... ]
773         
774         """
775         if len(chunk) == 3:
776                 (type, body, opts) = chunk
777                 complete_body = None
778         else:# mbverbatim
779                 (type, body, opts, complete_body) = chunk
780         assert type == 'mudela'
781         file_body = compose_full_body (body, opts)
782         basename = `abs(hash (file_body))`
783         for o in opts:
784                 m = re.search ('filename="(.*?)"', o)
785                 if m:
786                         basename = m.group (1)
787                         if not taken_file_names.has_key(basename):
788                             taken_file_names[basename] = 0
789                         else:
790                             taken_file_names[basename] = taken_file_names[basename] + 1
791                             basename = basename + "-%i" % taken_file_names[basename]
792         # writes the file if necessary, returns true if it was written
793         if not g_read_lys:
794                 update_file(file_body, os.path.join(g_outdir, basename) + '.ly')
795         needed_filetypes = ['tex']
796
797         if format  == 'texi':
798                 needed_filetypes.append('eps')
799                 needed_filetypes.append('png')
800         if 'eps' in opts and not ('eps' in needed_filetypes):
801                 needed_filetypes.append('eps')
802         outname = os.path.join(g_outdir, basename)
803         if not os.path.isfile(outname + '.tex') \
804                 or os.stat(outname+'.ly')[stat.ST_MTIME] > \
805                         os.stat(outname+'.tex')[stat.ST_MTIME]:
806                 todo = needed_filetypes
807         else:
808                 todo = []
809                 
810         newbody = ''
811         if 'verbatim' in opts:
812                 newbody = output_verbatim (body)
813         elif 'mbverbatim' in opts:
814                 newbody = output_mbverbatim (complete_body)
815
816         for o in opts:
817                 m = re.search ('intertext="(.*?)"', o)
818                 if m:
819                         newbody = newbody  + m.group (1) + "\n\n"
820         if format == 'latex':
821                 if 'eps' in opts:
822                         s = 'output-eps'
823                 else:
824                         s = 'output-tex'
825         else: # format == 'texi'
826                 s = 'output-all'
827         newbody = newbody + get_output(s) % {'fn': basename }
828         return ('mudela', newbody, opts, todo, basename)
829
830 def process_mudela_blocks(outname, chunks):#ugh rename
831         newchunks = []
832         # Count sections/chapters.
833         for c in chunks:
834                 if c[0] == 'mudela':
835                         c = schedule_mudela_block (c)
836                 elif c[0] == 'numcols':
837                         paperguru.m_num_cols = c[2]
838                 newchunks.append (c)
839         return newchunks
840
841
842 def find_eps_dims (match):
843         "Fill in dimensions of EPS files."
844         
845         fn =match.group (1)
846         dims = bounding_box_dimensions (fn)
847
848         return '%ipt' % dims[0]
849
850
851 def system (cmd):
852         sys.stderr.write ("invoking `%s'\n" % cmd)
853         st = os.system (cmd)
854         if st:
855                 error ('Error command exited with value %d\n' % st)
856         return st
857
858 def compile_all_files (chunks):
859         eps = []
860         tex = []
861         png = []
862
863         for c in chunks:
864                 if c[0] <> 'mudela':
865                         continue
866                 base  = c[4]
867                 exts = c[3]
868                 for e in exts:
869                         if e == 'eps':
870                                 eps.append (base)
871                         elif e == 'tex':
872                                 #ugh
873                                 if base + '.ly' not in tex:
874                                         tex.append (base + '.ly')
875                         elif e == 'png' and g_do_pictures:
876                                 png.append (base)
877         d = os.getcwd()
878         if g_outdir:
879                 os.chdir(g_outdir)
880         if tex:
881                 lilyopts = map (lambda x:  '-I ' + x, include_path)
882                 lilyopts = string.join (lilyopts, ' ' )
883                 texfiles = string.join (tex, ' ')
884                 system ('lilypond %s %s' % (lilyopts, texfiles))
885         for e in eps:
886                 system(r"tex '\nonstopmode \input %s'" % e)
887                 system(r"dvips -E -o %s %s" % (e + '.eps', e))
888         for g in png:
889                 cmd = r"""gs -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r90 -dNOPAUSE %s -c quit | pnmcrop | pnmtopng > %s"""
890                 cmd = cmd % (g + '.eps', g + '.png')
891                 system (cmd)
892         if g_outdir:
893                 os.chdir(d)
894
895
896 def update_file (body, name):
897         """
898         write the body if it has changed
899         """
900         same = 0
901         try:
902                 f = open (name)
903                 fs = f.read (-1)
904                 same = (fs == body)
905         except:
906                 pass
907
908         if not same:
909                 f = open (name , 'w')
910                 f.write (body)
911                 f.close ()
912         
913         return not same
914
915
916 def getopt_args (opts):
917         "Construct arguments (LONG, SHORT) for getopt from  list of options."
918         short = ''
919         long = []
920         for o in opts:
921                 if o[1]:
922                         short = short + o[1]
923                         if o[0]:
924                                 short = short + ':'
925                 if o[2]:
926                         l = o[2]
927                         if o[0]:
928                                 l = l + '='
929                         long.append (l)
930         return (short, long)
931
932 def option_help_str (o):
933         "Transform one option description (4-tuple ) into neatly formatted string"
934         sh = '  '       
935         if o[1]:
936                 sh = '-%s' % o[1]
937
938         sep = ' '
939         if o[1] and o[2]:
940                 sep = ','
941                 
942         long = ''
943         if o[2]:
944                 long= '--%s' % o[2]
945
946         arg = ''
947         if o[0]:
948                 if o[2]:
949                         arg = '='
950                 arg = arg + o[0]
951         return '  ' + sh + sep + long + arg
952
953
954 def options_help_str (opts):
955         "Convert a list of options into a neatly formatted string"
956         w = 0
957         strs =[]
958         helps = []
959
960         for o in opts:
961                 s = option_help_str (o)
962                 strs.append ((s, o[3]))
963                 if len (s) > w:
964                         w = len (s)
965
966         str = ''
967         for s in strs:
968                 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
969         return str
970
971 def help():
972         sys.stdout.write("""Usage: mudela-book [options] FILE\n
973 Generate hybrid LaTeX input from Latex + mudela
974 Options:
975 """)
976         sys.stdout.write (options_help_str (option_definitions))
977         sys.stdout.write (r"""Warning all output is written in the CURRENT directory
978
979
980
981 Report bugs to bug-gnu-music@gnu.org.
982
983 Written by Tom Cato Amundsen <tca@gnu.org> and
984 Han-Wen Nienhuys <hanwen@cs.uu.nl>
985 """)
986
987         sys.exit (0)
988
989
990 def write_deps (fn, target):
991         sys.stdout.write('writing `%s\'\n' % os.path.join(g_outdir, fn))
992         f = open (os.path.join(g_outdir, fn), 'w')
993         f.write ('%s%s: ' % (g_dep_prefix, target))
994         for d in __main__.read_files:
995                 f.write ('%s ' %  d)
996         f.write ('\n')
997         f.close ()
998         __main__.read_files = []
999
1000 def identify():
1001         sys.stdout.write ('mudela-book (GNU LilyPond) %s\n' % program_version)
1002
1003 def print_version ():
1004         identify()
1005         sys.stdout.write (r"""Copyright 1998--1999
1006 Distributed under terms of the GNU General Public License. It comes with
1007 NO WARRANTY.
1008 """)
1009
1010 def do_file(input_filename):
1011         file_settings = {}
1012         if outname:
1013                 my_outname = outname
1014         else:
1015                 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
1016         my_depname = my_outname + '.dep'                
1017
1018         chunks = read_doc_file(input_filename)
1019         chunks = new_chop_chunks(chunks, 'mudela', make_mudela)
1020         chunks = new_chop_chunks(chunks, 'mudela-file', make_mudela_file)
1021         chunks = new_chop_chunks(chunks, 'mudela-block', make_mudela_block)
1022         chunks = chop_chunks(chunks, 'singleline-comment', do_ignore)
1023         chunks = chop_chunks(chunks, 'preamble-end', do_preamble_end)
1024         chunks = chop_chunks(chunks, 'numcols', do_columns)
1025         #print "-" * 50
1026         #for c in chunks: print "c:", c;
1027         #sys.exit()
1028         scan_preamble(chunks)
1029         chunks = process_mudela_blocks(my_outname, chunks)
1030         # Do It.
1031         if __main__.g_run_lilypond:
1032                 compile_all_files (chunks)
1033                 newchunks = []
1034                 # finishing touch.
1035                 for c in chunks:
1036                         if c[0] == 'mudela' and 'eps' in c[2]:
1037                                 body = re.sub (r"""\\mudelaepswidth{(.*?)}""", find_eps_dims, c[1])
1038                                 newchunks.append (('mudela', body))
1039                         else:
1040                                 newchunks.append (c)
1041                 chunks = newchunks
1042
1043         if chunks and chunks[0][0] == 'input':
1044                 chunks[0] = ('input', completize_preamble (chunks[0][1]))
1045
1046         foutn = os.path.join(g_outdir, my_outname + '.' + format)
1047         sys.stderr.write ("Writing `%s'\n" % foutn)
1048         fout = open (foutn, 'w')
1049         for c in chunks:
1050                 fout.write (c[1])
1051         fout.close ()
1052
1053         if do_deps:
1054                 write_deps (my_depname, foutn)
1055
1056
1057 outname = ''
1058 try:
1059         (sh, long) = getopt_args (__main__.option_definitions)
1060         (options, files) = getopt.getopt(sys.argv[1:], sh, long)
1061 except getopt.error, msg:
1062         sys.stderr.write("error: %s" % msg)
1063         sys.exit(1)
1064
1065 do_deps = 0
1066 for opt in options:     
1067         o = opt[0]
1068         a = opt[1]
1069
1070         if o == '--include' or o == '-I':
1071                 include_path.append (a)
1072         elif o == '--version' or o == '-v':
1073                 print_version ()
1074                 sys.exit  (0)
1075         elif o == '--format' or o == '-f':
1076                 __main__.format = a
1077         elif o == '--outname' or o == '-o':
1078                 if len(files) > 1:
1079                         #HACK
1080                         sys.stderr.write("Mudela-book is confused by --outname on multiple files")
1081                         sys.exit(1)
1082                 outname = a
1083         elif o == '--help' or o == '-h':
1084                 help ()
1085         elif o == '--no-lily' or o == '-n':
1086                 __main__.g_run_lilypond = 0
1087         elif o == '--dependencies' or o == '-M':
1088                 do_deps = 1
1089         elif o == '--default-music-fontsize':
1090                 default_music_fontsize = string.atoi (a)
1091         elif o == '--default-mudela-fontsize':
1092                 print "--default-mudela-fontsize is deprecated, use --default-music-fontsize"
1093                 default_music_fontsize = string.atoi (a)
1094         elif o == '--force-music-fontsize':
1095                 g_force_mudela_fontsize = string.atoi(a)
1096         elif o == '--force-mudela-fontsize':
1097                 print "--force-mudela-fontsize is deprecated, use --default-mudela-fontsize"
1098                 g_force_mudela_fontsize = string.atoi(a)
1099         elif o == '--dep-prefix':
1100                 g_dep_prefix = a
1101         elif o == '--no-pictures':
1102                 g_do_pictures = 0
1103         elif o == '--read-lys':
1104                 g_read_lys = 1
1105         elif o == '--outdir':
1106                 g_outdir = a
1107
1108 identify()
1109 if g_outdir:
1110         if os.path.isfile(g_outdir):
1111                 error ("outdir is a file: %s" % g_outdir)
1112         if not os.path.exists(g_outdir):
1113                 os.mkdir(g_outdir)
1114 for input_filename in files:
1115         do_file(input_filename)
1116         
1117 #
1118 # Petr, ik zou willen dat ik iets zinvoller deed,
1119 # maar wat ik kan ik doen, het verandert toch niets?
1120 #   --hwn 20/aug/99