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