]> git.donarmstrong.com Git - lilypond.git/blob - scripts/mudela-book.py
release: 1.3.94
[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"(?sm)^\s*(?!@c\s+)(?P<code>@ignore\s.*?@end ignore)\s",
416                   'singleline-comment': r"(?m)^.*?(?P<match>(?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 chop_chunks(chunks, re_name, func, use_match=0):
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                     if use_match:
703                         newchunks.append (('input', str[:m.start ('match')]))
704                     else:
705                         newchunks.append (('input', str[:m.start (0)]))
706                     #newchunks.extend(func(m))
707                     # python 1.5 compatible:
708                     newchunks = newchunks + func(m)
709                     str = str [m.end(0):]
710         else:
711             newchunks.append(c)
712     return newchunks
713
714 def read_doc_file (filename):
715         """Read the input file, find verbatim chunks and do \input and \include
716         """
717         str = ''
718         str = find_file(filename)
719
720         if __main__.format == '':
721                 latex =  re.search ('\\\\document', str[:200])
722                 texinfo =  re.search ('@node|@setfilename', str[:200])
723                 if (texinfo and latex) or not (texinfo or latex):
724                         error("error: can't determine format, please specify")
725                 if texinfo:
726                         __main__.format = 'texi'
727                 else:
728                         __main__.format = 'latex'
729         if __main__.format == 'texi':
730                 __main__.paperguru = TexiPaper()
731         else:
732                 __main__.paperguru = LatexPaper()
733         chunks = [('input', str)]
734         # we have to check for verbatim before doing include,
735         # because we don't want to include files that are mentioned
736         # inside a verbatim environment
737         chunks = chop_chunks(chunks, 'verbatim', make_verbatim)
738         chunks = chop_chunks(chunks, 'verb', make_verb)
739         chunks = chop_chunks(chunks, 'multiline-comment', do_ignore)
740         #ugh fix input
741         chunks = chop_chunks(chunks, 'include', do_include_file, 1)
742         chunks = chop_chunks(chunks, 'input', do_input_file, 1)
743         return chunks
744
745
746 taken_file_names = {}
747 def schedule_mudela_block (chunk):
748         """Take the body and options from CHUNK, figure out how the
749         real .ly should look, and what should be left MAIN_STR (meant
750         for the main file).  The .ly is written, and scheduled in
751         TODO.
752
753         Return: a chunk (TYPE_STR, MAIN_STR, OPTIONS, TODO, BASE)
754
755         TODO has format [basename, extension, extension, ... ]
756         
757         """
758         if len(chunk) == 3:
759                 (type, body, opts) = chunk
760                 complete_body = None
761         else:# mbverbatim
762                 (type, body, opts, complete_body) = chunk
763         assert type == 'mudela'
764         file_body = compose_full_body (body, opts)
765         basename = `abs(hash (file_body))`
766         for o in opts:
767                 m = re.search ('filename="(.*?)"', o)
768                 if m:
769                         basename = m.group (1)
770                         if not taken_file_names.has_key(basename):
771                             taken_file_names[basename] = 0
772                         else:
773                             taken_file_names[basename] = taken_file_names[basename] + 1
774                             basename = basename + "-%i" % taken_file_names[basename]
775         # writes the file if necessary, returns true if it was written
776         if not g_read_lys:
777                 update_file(file_body, os.path.join(g_outdir, basename) + '.ly')
778         needed_filetypes = ['tex']
779
780         if format  == 'texi':
781                 needed_filetypes.append('eps')
782                 needed_filetypes.append('png')
783         if 'eps' in opts and not ('eps' in needed_filetypes):
784                 needed_filetypes.append('eps')
785         outname = os.path.join(g_outdir, basename)
786         if not os.path.isfile(outname + '.tex') \
787                 or os.stat(outname+'.ly')[stat.ST_MTIME] > \
788                         os.stat(outname+'.tex')[stat.ST_MTIME]:
789                 todo = needed_filetypes
790         else:
791                 todo = []
792                 
793         newbody = ''
794         if 'verbatim' in opts:
795                 newbody = output_verbatim (body)
796         elif 'mbverbatim' in opts:
797                 newbody = output_mbverbatim (complete_body)
798
799         for o in opts:
800                 m = re.search ('intertext="(.*?)"', o)
801                 if m:
802                         newbody = newbody  + m.group (1) + "\n\n"
803         if format == 'latex':
804                 if 'eps' in opts:
805                         s = 'output-eps'
806                 else:
807                         s = 'output-tex'
808         else: # format == 'texi'
809                 s = 'output-all'
810         newbody = newbody + get_output(s) % {'fn': basename }
811         return ('mudela', newbody, opts, todo, basename)
812
813 def process_mudela_blocks(outname, chunks):#ugh rename
814         newchunks = []
815         # Count sections/chapters.
816         for c in chunks:
817                 if c[0] == 'mudela':
818                         c = schedule_mudela_block (c)
819                 elif c[0] == 'numcols':
820                         paperguru.m_num_cols = c[2]
821                 newchunks.append (c)
822         return newchunks
823
824
825 def find_eps_dims (match):
826         "Fill in dimensions of EPS files."
827         
828         fn =match.group (1)
829         dims = bounding_box_dimensions (fn)
830
831         return '%ipt' % dims[0]
832
833
834 def system (cmd):
835         sys.stderr.write ("invoking `%s'\n" % cmd)
836         st = os.system (cmd)
837         if st:
838                 error ('Error command exited with value %d\n' % st)
839         return st
840
841 def compile_all_files (chunks):
842         eps = []
843         tex = []
844         png = []
845
846         for c in chunks:
847                 if c[0] <> 'mudela':
848                         continue
849                 base  = c[4]
850                 exts = c[3]
851                 for e in exts:
852                         if e == 'eps':
853                                 eps.append (base)
854                         elif e == 'tex':
855                                 #ugh
856                                 if base + '.ly' not in tex:
857                                         tex.append (base + '.ly')
858                         elif e == 'png' and g_do_pictures:
859                                 png.append (base)
860         d = os.getcwd()
861         if g_outdir:
862                 os.chdir(g_outdir)
863         if tex:
864                 lilyopts = map (lambda x:  '-I ' + x, include_path)
865                 lilyopts = string.join (lilyopts, ' ' )
866                 texfiles = string.join (tex, ' ')
867                 system ('lilypond %s %s' % (lilyopts, texfiles))
868         for e in eps:
869                 system(r"tex '\nonstopmode \input %s'" % e)
870                 system(r"dvips -E -o %s %s" % (e + '.eps', e))
871         for g in png:
872                 cmd = r"""gs -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r90 -dNOPAUSE %s -c quit | pnmcrop | pnmtopng > %s"""
873                 cmd = cmd % (g + '.eps', g + '.png')
874                 system (cmd)
875         if g_outdir:
876                 os.chdir(d)
877
878
879 def update_file (body, name):
880         """
881         write the body if it has changed
882         """
883         same = 0
884         try:
885                 f = open (name)
886                 fs = f.read (-1)
887                 same = (fs == body)
888         except:
889                 pass
890
891         if not same:
892                 f = open (name , 'w')
893                 f.write (body)
894                 f.close ()
895         
896         return not same
897
898
899 def getopt_args (opts):
900         "Construct arguments (LONG, SHORT) for getopt from  list of options."
901         short = ''
902         long = []
903         for o in opts:
904                 if o[1]:
905                         short = short + o[1]
906                         if o[0]:
907                                 short = short + ':'
908                 if o[2]:
909                         l = o[2]
910                         if o[0]:
911                                 l = l + '='
912                         long.append (l)
913         return (short, long)
914
915 def option_help_str (o):
916         "Transform one option description (4-tuple ) into neatly formatted string"
917         sh = '  '       
918         if o[1]:
919                 sh = '-%s' % o[1]
920
921         sep = ' '
922         if o[1] and o[2]:
923                 sep = ','
924                 
925         long = ''
926         if o[2]:
927                 long= '--%s' % o[2]
928
929         arg = ''
930         if o[0]:
931                 if o[2]:
932                         arg = '='
933                 arg = arg + o[0]
934         return '  ' + sh + sep + long + arg
935
936
937 def options_help_str (opts):
938         "Convert a list of options into a neatly formatted string"
939         w = 0
940         strs =[]
941         helps = []
942
943         for o in opts:
944                 s = option_help_str (o)
945                 strs.append ((s, o[3]))
946                 if len (s) > w:
947                         w = len (s)
948
949         str = ''
950         for s in strs:
951                 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
952         return str
953
954 def help():
955         sys.stdout.write("""Usage: mudela-book [options] FILE\n
956 Generate hybrid LaTeX input from Latex + mudela
957 Options:
958 """)
959         sys.stdout.write (options_help_str (option_definitions))
960         sys.stdout.write (r"""Warning all output is written in the CURRENT directory
961
962
963
964 Report bugs to bug-gnu-music@gnu.org.
965
966 Written by Tom Cato Amundsen <tca@gnu.org> and
967 Han-Wen Nienhuys <hanwen@cs.uu.nl>
968 """)
969
970         sys.exit (0)
971
972
973 def write_deps (fn, target):
974         sys.stdout.write('writing `%s\'\n' % os.path.join(g_outdir, fn))
975         f = open (os.path.join(g_outdir, fn), 'w')
976         f.write ('%s%s: ' % (g_dep_prefix, target))
977         for d in __main__.read_files:
978                 f.write ('%s ' %  d)
979         f.write ('\n')
980         f.close ()
981         __main__.read_files = []
982
983 def identify():
984         sys.stdout.write ('mudela-book (GNU LilyPond) %s\n' % program_version)
985
986 def print_version ():
987         identify()
988         sys.stdout.write (r"""Copyright 1998--1999
989 Distributed under terms of the GNU General Public License. It comes with
990 NO WARRANTY.
991 """)
992
993 def do_file(input_filename):
994         file_settings = {}
995         if outname:
996                 my_outname = outname
997         else:
998                 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
999         my_depname = my_outname + '.dep'                
1000
1001         chunks = read_doc_file(input_filename)
1002         chunks = chop_chunks(chunks, 'mudela', make_mudela, 1)
1003         chunks = chop_chunks(chunks, 'mudela-file', make_mudela_file, 1)
1004         chunks = chop_chunks(chunks, 'mudela-block', make_mudela_block, 1)
1005         chunks = chop_chunks(chunks, 'singleline-comment', do_ignore, 1)
1006         chunks = chop_chunks(chunks, 'preamble-end', do_preamble_end)
1007         chunks = chop_chunks(chunks, 'numcols', do_columns)
1008         #print "-" * 50
1009         #for c in chunks: print "c:", c;
1010         #sys.exit()
1011         scan_preamble(chunks)
1012         chunks = process_mudela_blocks(my_outname, chunks)
1013         # Do It.
1014         if __main__.g_run_lilypond:
1015                 compile_all_files (chunks)
1016                 newchunks = []
1017                 # finishing touch.
1018                 for c in chunks:
1019                         if c[0] == 'mudela' and 'eps' in c[2]:
1020                                 body = re.sub (r"""\\mudelaepswidth{(.*?)}""", find_eps_dims, c[1])
1021                                 newchunks.append (('mudela', body))
1022                         else:
1023                                 newchunks.append (c)
1024                 chunks = newchunks
1025
1026         if chunks and chunks[0][0] == 'input':
1027                 chunks[0] = ('input', completize_preamble (chunks[0][1]))
1028
1029         foutn = os.path.join(g_outdir, my_outname + '.' + format)
1030         sys.stderr.write ("Writing `%s'\n" % foutn)
1031         fout = open (foutn, 'w')
1032         for c in chunks:
1033                 fout.write (c[1])
1034         fout.close ()
1035
1036         if do_deps:
1037                 write_deps (my_depname, foutn)
1038
1039
1040 outname = ''
1041 try:
1042         (sh, long) = getopt_args (__main__.option_definitions)
1043         (options, files) = getopt.getopt(sys.argv[1:], sh, long)
1044 except getopt.error, msg:
1045         sys.stderr.write("error: %s" % msg)
1046         sys.exit(1)
1047
1048 do_deps = 0
1049 for opt in options:     
1050         o = opt[0]
1051         a = opt[1]
1052
1053         if o == '--include' or o == '-I':
1054                 include_path.append (a)
1055         elif o == '--version' or o == '-v':
1056                 print_version ()
1057                 sys.exit  (0)
1058         elif o == '--format' or o == '-f':
1059                 __main__.format = a
1060         elif o == '--outname' or o == '-o':
1061                 if len(files) > 1:
1062                         #HACK
1063                         sys.stderr.write("Mudela-book is confused by --outname on multiple files")
1064                         sys.exit(1)
1065                 outname = a
1066         elif o == '--help' or o == '-h':
1067                 help ()
1068         elif o == '--no-lily' or o == '-n':
1069                 __main__.g_run_lilypond = 0
1070         elif o == '--dependencies' or o == '-M':
1071                 do_deps = 1
1072         elif o == '--default-music-fontsize':
1073                 default_music_fontsize = string.atoi (a)
1074         elif o == '--default-mudela-fontsize':
1075                 print "--default-mudela-fontsize is deprecated, use --default-music-fontsize"
1076                 default_music_fontsize = string.atoi (a)
1077         elif o == '--force-music-fontsize':
1078                 g_force_mudela_fontsize = string.atoi(a)
1079         elif o == '--force-mudela-fontsize':
1080                 print "--force-mudela-fontsize is deprecated, use --default-mudela-fontsize"
1081                 g_force_mudela_fontsize = string.atoi(a)
1082         elif o == '--dep-prefix':
1083                 g_dep_prefix = a
1084         elif o == '--no-pictures':
1085                 g_do_pictures = 0
1086         elif o == '--read-lys':
1087                 g_read_lys = 1
1088         elif o == '--outdir':
1089                 g_outdir = a
1090
1091 identify()
1092 if g_outdir:
1093         if os.path.isfile(g_outdir):
1094                 error ("outdir is a file: %s" % g_outdir)
1095         if not os.path.exists(g_outdir):
1096                 os.mkdir(g_outdir)
1097 for input_filename in files:
1098         do_file(input_filename)
1099         
1100 #
1101 # Petr, ik zou willen dat ik iets zinvoller deed,
1102 # maar wat ik kan ik doen, het verandert toch niets?
1103 #   --hwn 20/aug/99