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