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