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