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