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