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