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