]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
release: 1.3.146
[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
499 def error (str):
500         sys.stderr.write (str + "\n  Exiting ... \n\n")
501         raise 'Exiting.'
502
503
504 def compose_full_body (body, opts):
505         """Construct the lilypond code to send to Lilypond.
506         Add stuff to BODY using OPTS as options."""
507         music_size = default_music_fontsize
508         latex_size = default_text_fontsize
509         for o in opts:
510                 if g_force_lilypond_fontsize:
511                         music_size = g_force_lilypond_fontsize
512                 else:
513                         m = re.match ('([0-9]+)pt', o)
514                         if m:
515                                 music_size = string.atoi(m.group (1))
516
517                 m = re.match ('latexfontsize=([0-9]+)pt', o)
518                 if m:
519                         latex_size = string.atoi (m.group (1))
520
521         if re.search ('\\\\score', body):
522                 is_fragment = 0
523         else:
524                 is_fragment = 1
525         if 'fragment' in opts:
526                 is_fragment = 1
527         if 'nofragment' in opts:
528                 is_fragment = 0
529
530         if is_fragment and not 'multiline' in opts:
531                 opts.append('singleline')
532         if 'singleline' in opts:
533                 l = -1.0;
534         else:
535                 l = __main__.paperguru.get_linewidth()
536         
537         if 'relative' in opts:#ugh only when is_fragment
538                 body = '\\relative c { %s }' % body
539         
540         if is_fragment:
541                 body = r"""\score { 
542  \notes { %s }
543   \paper { }  
544 }""" % body
545
546         opts = uniq (opts)
547         optstring = string.join (opts, ' ')
548         optstring = re.sub ('\n', ' ', optstring)
549         body = r"""
550 %% Generated automatically by: lilypond-book.py
551 %% options are %s  %%ughUGH not original options
552 \include "paper%d.ly"
553 \paper  { linewidth = %f \pt } 
554 """ % (optstring, music_size, l) + body
555         return body
556
557 def parse_options_string(s):
558         d = {}
559         r1 = re.compile("((\w+)={(.*?)})((,\s*)|$)")
560         r2 = re.compile("((\w+)=(.*?))((,\s*)|$)")
561         r3 = re.compile("(\w+?)((,\s*)|$)")
562         while s:
563                 m = r1.match(s)
564                 if m:
565                         s = s[m.end():]
566                         d[m.group(2)] = re.split(",\s*", m.group(3))
567                         continue
568                 m = r2.match(s)
569                 if m:
570                         s = s[m.end():]
571                         d[m.group(2)] = m.group(3)
572                         continue
573                 m = r3.match(s)
574                 if m:
575                         s = s[m.end():]
576                         d[m.group(1)] = 1
577                         continue
578                 
579                 error ("format of option string invalid (was `%')" % s)
580         return d
581
582 def scan_latex_preamble(chunks):
583         # first we want to scan the \documentclass line
584         # it should be the first non-comment line
585         idx = 0
586         while 1:
587                 if chunks[idx][0] == 'ignore':
588                         idx = idx + 1
589                         continue
590                 m = get_re ('header').match(chunks[idx][1])
591                 if m.group (1):
592                         options = re.split (',[\n \t]*', m.group(1)[1:-1])
593                 else:
594                         options = []
595                 for o in options:
596                         if o == 'landscape':
597                                 paperguru.m_landscape = 1
598                         m = re.match("(.*?)paper", o)
599                         if m:
600                                 paperguru.m_papersize = m.group()
601                         else:
602                                 m = re.match("(\d\d)pt", o)
603                                 if m:
604                                         paperguru.m_fontsize = int(m.group(1))
605                         
606                 break
607         while chunks[idx][0] != 'preamble-end':
608                 if chunks[idx] == 'ignore':
609                         idx = idx + 1
610                         continue
611                 m = get_re ('geometry').search(chunks[idx][1])
612                 if m:
613                         paperguru.m_use_geometry = 1
614                         o = parse_options_string(m.group('options'))
615                         for k in o.keys():
616                                 paperguru.set_geo_option(k, o[k])
617                 idx = idx + 1
618
619 def scan_texi_preamble (chunks):
620         # this is not bulletproof..., it checks the first 10 chunks
621         for c in chunks[:10]: 
622                 if c[0] == 'input':
623                         for s in ('afourpaper', 'afourwide', 'letterpaper',
624                                   'afourlatex', 'smallbook'):
625                                 if string.find(c[1], "@%s" % s) != -1:
626                                         paperguru.m_papersize = s
627
628 def scan_preamble (chunks):
629         if __main__.format == 'texi':
630                 scan_texi_preamble(chunks)
631         else:
632                 assert __main__.format == 'latex'
633                 scan_latex_preamble(chunks)
634                 
635
636 def completize_preamble (chunks):
637         if __main__.format == 'texi':
638                 return chunks
639         pre_b = post_b = graphics_b = None
640         for chunk in chunks:
641                 if chunk[0] == 'preamble-end':
642                         break
643                 if chunk[0] == 'input':
644                         m = get_re('def-pre-re').search(chunk[1])
645                         if m:
646                                 pre_b = 1
647                 if chunk[0] == 'input':
648                         m = get_re('def-post-re').search(chunk[1])
649                         if m:
650                                 post_b = 1
651                 if chunk[0] == 'input':
652                         m = get_re('usepackage-graphics').search(chunk[1])
653                         if m:
654                                 graphics_b = 1
655         x = 0
656         while chunks[x][0] != 'preamble-end':
657                 x = x + 1
658         if not pre_b:
659                 chunks.insert(x, ('input', get_output ('output-default-pre')))
660         if not post_b:
661                 chunks.insert(x, ('input', get_output ('output-default-post')))
662         if not graphics_b:
663                 chunks.insert(x, ('input', get_output ('usepackage-graphics')))
664         return chunks
665
666
667 read_files = []
668 def find_file (name):
669         """
670         Search the include path for NAME. If found, return the (CONTENTS, PATH) of the file.
671         """
672         
673         f = None
674         nm = ''
675         for a in include_path:
676                 try:
677                         nm = os.path.join (a, name)
678                         f = open (nm)
679                         __main__.read_files.append (nm)
680                         break
681                 except IOError:
682                         pass
683         if f:
684                 sys.stderr.write ("Reading `%s'\n" % nm)
685                 return (f.read (), nm)
686         else:
687                 error ("File not found `%s'\n" % name)
688                 return ('', '')
689
690 def do_ignore(match_object):
691         return [('ignore', match_object.group('code'))]
692 def do_preamble_end(match_object):
693         return [('preamble-end', match_object.group('code'))]
694
695 def make_verbatim(match_object):
696         return [('verbatim', match_object.group('code'))]
697
698 def make_verb(match_object):
699         return [('verb', match_object.group('code'))]
700
701 def do_include_file(m):
702         "m: MatchObject"
703         return [('input', get_output ('pagebreak'))] \
704              + read_doc_file(m.group('filename')) \
705              + [('input', get_output ('pagebreak'))] 
706
707 def do_input_file(m):
708         return read_doc_file(m.group('filename'))
709
710 def make_lilypond(m):
711         if m.group('options'):
712                 options = m.group('options')
713         else:
714                 options = ''
715         return [('input', get_output('output-lilypond-fragment') % 
716                         (options, m.group('code')))]
717
718 def make_lilypond_file(m):
719         """
720
721         Find @lilypondfile{bla.ly} occurences and substitute bla.ly
722         into a @lilypond .. @end lilypond block.
723         
724         """
725         
726         if m.group('options'):
727                 options = m.group('options')
728         else:
729                 options = ''
730         (content, nm) = find_file(m.group('filename'))
731         options = "filename=%s," % nm + options
732
733         return [('input', get_output('output-lilypond') %
734                         (options, content))]
735
736 def make_lilypond_block(m):
737         if m.group('options'):
738                 options = get_re('option-sep').split (m.group('options'))
739         else:
740             options = []
741         options = filter(lambda s: s != '', options)
742         return [('lilypond', m.group('code'), options)]
743
744 def do_columns(m):
745         if __main__.format != 'latex':
746                 return []
747         if m.group('num') == 'one':
748                 return [('numcols', m.group('code'), 1)]
749         if m.group('num') == 'two':
750                 return [('numcols', m.group('code'), 2)]
751         
752 def chop_chunks(chunks, re_name, func, use_match=0):
753     newchunks = []
754     for c in chunks:
755         if c[0] == 'input':
756             str = c[1]
757             while str:
758                 m = get_re (re_name).search (str)
759                 if m == None:
760                     newchunks.append (('input', str))
761                     str = ''
762                 else:
763                     if use_match:
764                         newchunks.append (('input', str[:m.start ('match')]))
765                     else:
766                         newchunks.append (('input', str[:m.start (0)]))
767                     #newchunks.extend(func(m))
768                     # python 1.5 compatible:
769                     newchunks = newchunks + func(m)
770                     str = str [m.end(0):]
771         else:
772             newchunks.append(c)
773     return newchunks
774
775 def determine_format (str):
776         if __main__.format == '':
777                 
778                 latex =  re.search ('\\\\document', str[:200])
779                 texinfo =  re.search ('@node|@setfilename', str[:200])
780
781                 f = ''
782                 g = None
783                 
784                 if texinfo and latex == None:
785                         f = 'texi'
786                 elif latex and texinfo == None: 
787                         f = 'latex'
788                 else:
789                         error("error: can't determine format, please specify")
790                 __main__.format = f
791
792         if __main__.paperguru == None:
793                 if __main__.format == 'texi':
794                         g = TexiPaper()
795                 else:
796                         g = LatexPaper()
797                         
798                 __main__.paperguru = g
799
800
801 def read_doc_file (filename):
802         """Read the input file, find verbatim chunks and do \input and \include
803         """
804         (str, path) = find_file(filename)
805         determine_format (str)
806         
807         chunks = [('input', str)]
808         
809         # we have to check for verbatim before doing include,
810         # because we don't want to include files that are mentioned
811         # inside a verbatim environment
812         chunks = chop_chunks(chunks, 'verbatim', make_verbatim)
813         chunks = chop_chunks(chunks, 'verb', make_verb)
814         chunks = chop_chunks(chunks, 'multiline-comment', do_ignore)
815         #ugh fix input
816         chunks = chop_chunks(chunks, 'include', do_include_file, 1)
817         chunks = chop_chunks(chunks, 'input', do_input_file, 1)
818         return chunks
819
820
821 taken_file_names = {}
822 def schedule_lilypond_block (chunk):
823         """Take the body and options from CHUNK, figure out how the
824         real .ly should look, and what should be left MAIN_STR (meant
825         for the main file).  The .ly is written, and scheduled in
826         TODO.
827
828         Return: a chunk (TYPE_STR, MAIN_STR, OPTIONS, TODO, BASE)
829
830         TODO has format [basename, extension, extension, ... ]
831         
832         """
833         (type, body, opts) = chunk
834         assert type == 'lilypond'
835         file_body = compose_full_body (body, opts)
836         basename = 'lily-' + `abs(hash (file_body))`
837         for o in opts:
838                 m = re.search ('filename="(.*?)"', o)
839                 if m:
840                         basename = m.group (1)
841                         if not taken_file_names.has_key(basename):
842                             taken_file_names[basename] = 0
843                         else:
844                             taken_file_names[basename] = taken_file_names[basename] + 1
845                             basename = basename + "-%i" % taken_file_names[basename]
846         if not g_read_lys:
847                 update_file(file_body, os.path.join(g_outdir, basename) + '.ly')
848         needed_filetypes = ['tex']
849
850         if format  == 'texi':
851                 needed_filetypes.append('eps')
852                 needed_filetypes.append('png')
853         if 'eps' in opts and not ('eps' in needed_filetypes):
854                 needed_filetypes.append('eps')
855         pathbase = os.path.join (g_outdir, basename)
856         def f(base, ext1, ext2):
857                 a = os.path.isfile(base + ext2)
858                 if (os.path.isfile(base + ext1) and
859                     os.path.isfile(base + ext2) and
860                                 os.stat(base+ext1)[stat.ST_MTIME] >
861                                 os.stat(base+ext2)[stat.ST_MTIME]) or \
862                                 not os.path.isfile(base + ext2):
863                         return 1
864         todo = []
865         if 'tex' in needed_filetypes and f(pathbase, '.ly', '.tex'):
866                 todo.append('tex')
867         if 'eps' in needed_filetypes and f(pathbase, '.tex', '.eps'):
868                 todo.append('eps')
869         if 'png' in needed_filetypes and f(pathbase, '.eps', '.png'):
870                 todo.append('png')
871         newbody = ''
872
873         if 'printfilename' in opts:
874                 for o in opts:
875                         m= re.match ("filename=(.*)", o)
876                         if m:
877                                 newbody = newbody + get_output ("output-filename") % m.group(1)
878                                 break
879                 
880         
881         if 'verbatim' in opts:
882                 newbody = output_verbatim (body)
883
884         for o in opts:
885                 m = re.search ('intertext="(.*?)"', o)
886                 if m:
887                         newbody = newbody  + m.group (1) + "\n\n"
888         if format == 'latex':
889                 if 'eps' in opts:
890                         s = 'output-eps'
891                 else:
892                         s = 'output-tex'
893         else: # format == 'texi'
894                 s = 'output-all'
895         newbody = newbody + get_output (s) % {'fn': basename }
896         return ('lilypond', newbody, opts, todo, basename)
897
898 def process_lilypond_blocks(outname, chunks):#ugh rename
899         newchunks = []
900         # Count sections/chapters.
901         for c in chunks:
902                 if c[0] == 'lilypond':
903                         c = schedule_lilypond_block (c)
904                 elif c[0] == 'numcols':
905                         paperguru.m_num_cols = c[2]
906                 newchunks.append (c)
907         return newchunks
908
909
910 def find_eps_dims (match):
911         "Fill in dimensions of EPS files."
912         
913         fn =match.group (1)
914         dims = bounding_box_dimensions (fn)
915         if g_outdir:
916                 fn = os.path.join(g_outdir, fn)
917         
918         return '%ipt' % dims[0]
919
920
921 def system (cmd):
922         sys.stderr.write ("invoking `%s'\n" % cmd)
923         st = os.system (cmd)
924         if st:
925                 error ('Error command exited with value %d\n' % st)
926         return st
927
928 def compile_all_files (chunks):
929         global foutn
930         eps = []
931         tex = []
932         png = []
933
934         for c in chunks:
935                 if c[0] <> 'lilypond':
936                         continue
937                 base  = c[4]
938                 exts = c[3]
939                 for e in exts:
940                         if e == 'eps':
941                                 eps.append (base)
942                         elif e == 'tex':
943                                 #ugh
944                                 if base + '.ly' not in tex:
945                                         tex.append (base + '.ly')
946                         elif e == 'png' and g_do_pictures:
947                                 png.append (base)
948         d = os.getcwd()
949         if g_outdir:
950                 os.chdir(g_outdir)
951         if tex:
952                 # fixme: be sys-independent.
953                 def incl_opt (x):
954                         if g_outdir and x[0] <> '/' :
955                                 x = os.path.join (g_here_dir, x)
956                         return ' -I %s' % x
957
958                 incs = map (incl_opt, include_path)
959                 lilyopts = string.join (incs, ' ' )
960                 if do_deps:
961                         lilyopts = lilyopts + ' --dependencies '
962                         if g_outdir:
963                                 lilyopts = lilyopts + '--dep-prefix=' + g_outdir + '/'
964                 texfiles = string.join (tex, ' ')
965                 system ('lilypond --header=texidoc %s %s' % (lilyopts, texfiles))
966
967                 #
968                 # Ugh, fixing up dependencies for .tex generation
969                 #
970                 if do_deps:
971                         depfiles=map (lambda x: re.sub ('(.*)\.ly', '\\1.dep', x), tex)
972                         for i in depfiles:
973                                 text=open (i).read ()
974                                 text=re.sub ('\n([^:\n]*):', '\n' + foutn + ':', text)
975                                 open (i, 'w').write (text)
976
977         for e in eps:
978                 system(r"tex '\nonstopmode \input %s'" % e)
979                 system(r"dvips -E -o %s %s" % (e + '.eps', e))
980         for g in png:
981                 cmd = r"""gs -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r90 -dNOPAUSE %s -c quit | pnmcrop | pnmtopng > %s"""
982                 cmd = cmd % (g + '.eps', g + '.png')
983                 system (cmd)
984         os.chdir (d)
985
986
987 def update_file (body, name):
988         """
989         write the body if it has changed
990         """
991         same = 0
992         try:
993                 f = open (name)
994                 fs = f.read (-1)
995                 same = (fs == body)
996         except:
997                 pass
998
999         if not same:
1000                 f = open (name , 'w')
1001                 f.write (body)
1002                 f.close ()
1003         
1004         return not same
1005
1006
1007 def getopt_args (opts):
1008         "Construct arguments (LONG, SHORT) for getopt from  list of options."
1009         short = ''
1010         long = []
1011         for o in opts:
1012                 if o[1]:
1013                         short = short + o[1]
1014                         if o[0]:
1015                                 short = short + ':'
1016                 if o[2]:
1017                         l = o[2]
1018                         if o[0]:
1019                                 l = l + '='
1020                         long.append (l)
1021         return (short, long)
1022
1023 def option_help_str (o):
1024         "Transform one option description (4-tuple ) into neatly formatted string"
1025         sh = '  '       
1026         if o[1]:
1027                 sh = '-%s' % o[1]
1028
1029         sep = ' '
1030         if o[1] and o[2]:
1031                 sep = ','
1032                 
1033         long = ''
1034         if o[2]:
1035                 long= '--%s' % o[2]
1036
1037         arg = ''
1038         if o[0]:
1039                 if o[2]:
1040                         arg = '='
1041                 arg = arg + o[0]
1042         return '  ' + sh + sep + long + arg
1043
1044
1045 def options_help_str (opts):
1046         "Convert a list of options into a neatly formatted string"
1047         w = 0
1048         strs =[]
1049         helps = []
1050
1051         for o in opts:
1052                 s = option_help_str (o)
1053                 strs.append ((s, o[3]))
1054                 if len (s) > w:
1055                         w = len (s)
1056
1057         str = ''
1058         for s in strs:
1059                 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
1060         return str
1061
1062 def help():
1063         sys.stdout.write("""Usage: lilypond-book [options] FILE\n
1064 Generate hybrid LaTeX input from Latex + lilypond
1065 Options:
1066 """)
1067         sys.stdout.write (options_help_str (option_definitions))
1068         sys.stdout.write (r"""Warning all output is written in the CURRENT directory
1069
1070
1071
1072 Report bugs to bug-gnu-music@gnu.org.
1073
1074 Written by Tom Cato Amundsen <tca@gnu.org> and
1075 Han-Wen Nienhuys <hanwen@cs.uu.nl>
1076 """)
1077
1078         sys.exit (0)
1079
1080
1081 def write_deps (fn, target, chunks):
1082         global read_files
1083         sys.stdout.write('Writing `%s\'\n' % os.path.join(g_outdir, fn))
1084         f = open (os.path.join(g_outdir, fn), 'w')
1085         f.write ('%s%s: ' % (g_dep_prefix, target))
1086         for d in read_files:
1087                 f.write ('%s ' %  d)
1088         basenames=[]
1089         for c in chunks:
1090                 if c[0] == 'lilypond':
1091                         (type, body, opts, todo, basename) = c;
1092                         basenames.append (basename)
1093         for d in basenames:
1094                 if g_outdir:
1095                         d=g_outdir + '/' + d
1096                 if g_dep_prefix:
1097                         #if not os.isfile (d): # thinko?
1098                         if not re.search ('/', d):
1099                                 d = g_dep_prefix + d
1100                 f.write ('%s.tex ' %  d)
1101         f.write ('\n')
1102         #if len (basenames):
1103         #       for d in basenames:
1104         #               f.write ('%s.ly ' %  d)
1105         #       f.write (' : %s' % target)
1106         f.write ('\n')
1107         f.close ()
1108         read_files = []
1109
1110 def identify():
1111         sys.stdout.write ('lilypond-book (GNU LilyPond) %s\n' % program_version)
1112
1113 def print_version ():
1114         identify()
1115         sys.stdout.write (r"""Copyright 1998--1999
1116 Distributed under terms of the GNU General Public License. It comes with
1117 NO WARRANTY.
1118 """)
1119
1120
1121 def check_texidoc (chunks):
1122         n = []
1123         for c in chunks:
1124                 if c[0] == 'lilypond':
1125                         (type, body, opts, todo, basename) = c;
1126                         pathbase = os.path.join (g_outdir, basename)
1127                         if os.path.isfile (pathbase + '.texidoc'):
1128                                 body = '\n@include %s.texidoc\n' % basename + body
1129                                 c = (type, body, opts, todo, basename)
1130                 n.append (c)
1131         return n
1132
1133 def fix_epswidth (chunks):
1134         newchunks = []
1135         for c in chunks:
1136                 if c[0] == 'lilypond' and 'eps' in c[2]:
1137                         body = re.sub (r"""\\lilypondepswidth{(.*?)}""", find_eps_dims, c[1])
1138                         newchunks.append(('lilypond', body, c[2], c[3], c[4]))
1139                 else:
1140                         newchunks.append (c)
1141         return newchunks
1142
1143
1144 foutn=""
1145 def do_file(input_filename):
1146         global foutn
1147         file_settings = {}
1148         if outname:
1149                 my_outname = outname
1150         else:
1151                 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
1152         my_depname = my_outname + '.dep'                
1153
1154         chunks = read_doc_file(input_filename)
1155         chunks = chop_chunks(chunks, 'lilypond', make_lilypond, 1)
1156         chunks = chop_chunks(chunks, 'lilypond-file', make_lilypond_file, 1)
1157         chunks = chop_chunks(chunks, 'lilypond-block', make_lilypond_block, 1)
1158         chunks = chop_chunks(chunks, 'singleline-comment', do_ignore, 1)
1159         chunks = chop_chunks(chunks, 'preamble-end', do_preamble_end)
1160         chunks = chop_chunks(chunks, 'numcols', do_columns)
1161         #print "-" * 50
1162         #for c in chunks: print "c:", c;
1163         #sys.exit()
1164         scan_preamble(chunks)
1165         chunks = process_lilypond_blocks(my_outname, chunks)
1166
1167         foutn = os.path.join (g_outdir, my_outname + '.' + format)
1168
1169         # Do It.
1170         if __main__.g_run_lilypond:
1171                 compile_all_files (chunks)
1172                 chunks = fix_epswidth (chunks)
1173
1174         if __main__.format == 'texi':
1175                 chunks = check_texidoc (chunks)
1176
1177         x = 0
1178         chunks = completize_preamble (chunks)
1179         sys.stderr.write ("Writing `%s'\n" % foutn)
1180         fout = open (foutn, 'w')
1181         for c in chunks:
1182                 fout.write (c[1])
1183         fout.close ()
1184         # should chmod -w
1185
1186         if do_deps:
1187                 write_deps (my_depname, foutn, chunks)
1188
1189
1190 outname = ''
1191 try:
1192         (sh, long) = getopt_args (__main__.option_definitions)
1193         (options, files) = getopt.getopt(sys.argv[1:], sh, long)
1194 except getopt.error, msg:
1195         sys.stderr.write("error: %s" % msg)
1196         sys.exit(1)
1197
1198 do_deps = 0
1199 for opt in options:     
1200         o = opt[0]
1201         a = opt[1]
1202
1203         if o == '--include' or o == '-I':
1204                 include_path.append (a)
1205         elif o == '--version' or o == '-v':
1206                 print_version ()
1207                 sys.exit  (0)
1208         elif o == '--format' or o == '-f':
1209                 __main__.format = a
1210         elif o == '--outname' or o == '-o':
1211                 if len(files) > 1:
1212                         #HACK
1213                         sys.stderr.write("Lilypond-book is confused by --outname on multiple files")
1214                         sys.exit(1)
1215                 outname = a
1216         elif o == '--help' or o == '-h':
1217                 help ()
1218         elif o == '--no-lily' or o == '-n':
1219                 __main__.g_run_lilypond = 0
1220         elif o == '--dependencies' or o == '-M':
1221                 do_deps = 1
1222         elif o == '--default-music-fontsize':
1223                 default_music_fontsize = string.atoi (a)
1224         elif o == '--default-lilypond-fontsize':
1225                 print "--default-lilypond-fontsize is deprecated, use --default-music-fontsize"
1226                 default_music_fontsize = string.atoi (a)
1227         elif o == '--force-music-fontsize':
1228                 g_force_lilypond_fontsize = string.atoi(a)
1229         elif o == '--force-lilypond-fontsize':
1230                 print "--force-lilypond-fontsize is deprecated, use --default-lilypond-fontsize"
1231                 g_force_lilypond_fontsize = string.atoi(a)
1232         elif o == '--dep-prefix':
1233                 g_dep_prefix = a
1234         elif o == '--no-pictures':
1235                 g_do_pictures = 0
1236         elif o == '--read-lys':
1237                 g_read_lys = 1
1238         elif o == '--outdir':
1239                 g_outdir = a
1240
1241 identify()
1242 if g_outdir:
1243         if os.path.isfile(g_outdir):
1244                 error ("outdir is a file: %s" % g_outdir)
1245         if not os.path.exists(g_outdir):
1246                 os.mkdir(g_outdir)
1247 setup_environment ()
1248 for input_filename in files:
1249         do_file(input_filename)
1250         
1251 #
1252 # Petr, ik zou willen dat ik iets zinvoller deed,
1253 # maar wat ik kan ik doen, het verandert toch niets?
1254 #   --hwn 20/aug/99