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