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