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