]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
patch::: 1.3.141.jcn2
[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                 if m.group (1):
567                         options = re.split (',[\n \t]*', m.group(1)[1:-1])
568                 else:
569                         options = []
570                 for o in options:
571                         if o == 'landscape':
572                                 paperguru.m_landscape = 1
573                         m = re.match("(.*?)paper", o)
574                         if m:
575                                 paperguru.m_papersize = m.group()
576                         else:
577                                 m = re.match("(\d\d)pt", o)
578                                 if m:
579                                         paperguru.m_fontsize = int(m.group(1))
580                         
581                 break
582         while chunks[idx][0] != 'preamble-end':
583                 if chunks[idx] == 'ignore':
584                         idx = idx + 1
585                         continue
586                 m = get_re ('geometry').search(chunks[idx][1])
587                 if m:
588                         paperguru.m_use_geometry = 1
589                         o = parse_options_string(m.group('options'))
590                         for k in o.keys():
591                                 paperguru.set_geo_option(k, o[k])
592                 idx = idx + 1
593
594 def scan_texi_preamble (chunks):
595         # this is not bulletproof..., it checks the first 10 chunks
596         for c in chunks[:10]: 
597                 if c[0] == 'input':
598                         for s in ('afourpaper', 'afourwide', 'letterpaper',
599                                   'afourlatex', 'smallbook'):
600                                 if string.find(c[1], "@%s" % s) != -1:
601                                         paperguru.m_papersize = s
602
603 def scan_preamble (chunks):
604         if __main__.format == 'texi':
605                 scan_texi_preamble(chunks)
606         else:
607                 assert __main__.format == 'latex'
608                 scan_latex_preamble(chunks)
609                 
610
611 def completize_preamble (chunks):
612         if __main__.format == 'texi':
613                 return chunks
614         pre_b = post_b = graphics_b = None
615         for chunk in chunks:
616                 if chunk[0] == 'preamble-end':
617                         break
618                 if chunk[0] == 'input':
619                         m = get_re('def-pre-re').search(chunk[1])
620                         if m:
621                                 pre_b = 1
622                 if chunk[0] == 'input':
623                         m = get_re('def-post-re').search(chunk[1])
624                         if m:
625                                 post_b = 1
626                 if chunk[0] == 'input':
627                         m = get_re('usepackage-graphics').search(chunk[1])
628                         if m:
629                                 graphics_b = 1
630         x = 0
631         while chunks[x][0] != 'preamble-end':
632                 x = x + 1
633         if not pre_b:
634                 chunks.insert(x, ('input', get_output ('output-default-pre')))
635         if not post_b:
636                 chunks.insert(x, ('input', get_output ('output-default-post')))
637         if not graphics_b:
638                 chunks.insert(x, ('input', get_output ('usepackage-graphics')))
639         return chunks
640
641
642 read_files = []
643 def find_file (name):
644         """
645         Search the include path for NAME. If found, return the (CONTENTS, PATH) of the file.
646         """
647         
648         f = None
649         nm = ''
650         for a in include_path:
651                 try:
652                         nm = os.path.join (a, name)
653                         f = open (nm)
654                         __main__.read_files.append (nm)
655                         break
656                 except IOError:
657                         pass
658         if f:
659                 sys.stderr.write ("Reading `%s'\n" % nm)
660                 return (f.read (), nm)
661         else:
662                 error ("File not found `%s'\n" % name)
663                 return ('', '')
664
665 def do_ignore(match_object):
666         return [('ignore', match_object.group('code'))]
667 def do_preamble_end(match_object):
668         return [('preamble-end', match_object.group('code'))]
669
670 def make_verbatim(match_object):
671         return [('verbatim', match_object.group('code'))]
672
673 def make_verb(match_object):
674         return [('verb', match_object.group('code'))]
675
676 def do_include_file(m):
677         "m: MatchObject"
678         return [('input', get_output ('pagebreak'))] \
679              + read_doc_file(m.group('filename')) \
680              + [('input', get_output ('pagebreak'))] 
681
682 def do_input_file(m):
683         return read_doc_file(m.group('filename'))
684
685 def make_lilypond(m):
686         if m.group('options'):
687                 options = m.group('options')
688         else:
689                 options = ''
690         return [('input', get_output('output-lilypond-fragment') % 
691                         (options, m.group('code')))]
692
693 def make_lilypond_file(m):
694         """
695
696         Find @lilypondfile{bla.ly} occurences and substitute bla.ly
697         into a @lilypond .. @end lilypond block.
698         
699         """
700         
701         if m.group('options'):
702                 options = m.group('options')
703         else:
704                 options = ''
705         (content, nm) = find_file(m.group('filename'))
706         options = "filename=%s," % nm + options
707
708         return [('input', get_output('output-lilypond') %
709                         (options, content))]
710
711 def make_lilypond_block(m):
712         if m.group('options'):
713                 options = get_re('option-sep').split (m.group('options'))
714         else:
715             options = []
716         options = filter(lambda s: s != '', options)
717         return [('lilypond', m.group('code'), options)]
718
719 def do_columns(m):
720         if __main__.format != 'latex':
721                 return []
722         if m.group('num') == 'one':
723                 return [('numcols', m.group('code'), 1)]
724         if m.group('num') == 'two':
725                 return [('numcols', m.group('code'), 2)]
726         
727 def chop_chunks(chunks, re_name, func, use_match=0):
728     newchunks = []
729     for c in chunks:
730         if c[0] == 'input':
731             str = c[1]
732             while str:
733                 m = get_re (re_name).search (str)
734                 if m == None:
735                     newchunks.append (('input', str))
736                     str = ''
737                 else:
738                     if use_match:
739                         newchunks.append (('input', str[:m.start ('match')]))
740                     else:
741                         newchunks.append (('input', str[:m.start (0)]))
742                     #newchunks.extend(func(m))
743                     # python 1.5 compatible:
744                     newchunks = newchunks + func(m)
745                     str = str [m.end(0):]
746         else:
747             newchunks.append(c)
748     return newchunks
749
750 def determine_format (str):
751         if __main__.format == '':
752                 
753                 latex =  re.search ('\\\\document', str[:200])
754                 texinfo =  re.search ('@node|@setfilename', str[:200])
755
756                 f = ''
757                 g = None
758                 
759                 if texinfo and latex == None:
760                         f = 'texi'
761                 elif latex and texinfo == None: 
762                         f = 'latex'
763                 else:
764                         error("error: can't determine format, please specify")
765                 __main__.format = f
766
767         if __main__.paperguru == None:
768                 if __main__.format == 'texi':
769                         g = TexiPaper()
770                 else:
771                         g = LatexPaper()
772                         
773                 __main__.paperguru = g
774
775
776 def read_doc_file (filename):
777         """Read the input file, find verbatim chunks and do \input and \include
778         """
779         (str, path) = find_file(filename)
780         determine_format (str)
781         
782         chunks = [('input', str)]
783         
784         # we have to check for verbatim before doing include,
785         # because we don't want to include files that are mentioned
786         # inside a verbatim environment
787         chunks = chop_chunks(chunks, 'verbatim', make_verbatim)
788         chunks = chop_chunks(chunks, 'verb', make_verb)
789         chunks = chop_chunks(chunks, 'multiline-comment', do_ignore)
790         #ugh fix input
791         chunks = chop_chunks(chunks, 'include', do_include_file, 1)
792         chunks = chop_chunks(chunks, 'input', do_input_file, 1)
793         return chunks
794
795
796 taken_file_names = {}
797 def schedule_lilypond_block (chunk):
798         """Take the body and options from CHUNK, figure out how the
799         real .ly should look, and what should be left MAIN_STR (meant
800         for the main file).  The .ly is written, and scheduled in
801         TODO.
802
803         Return: a chunk (TYPE_STR, MAIN_STR, OPTIONS, TODO, BASE)
804
805         TODO has format [basename, extension, extension, ... ]
806         
807         """
808         (type, body, opts) = chunk
809         assert type == 'lilypond'
810         file_body = compose_full_body (body, opts)
811         basename = 'lily-' + `abs(hash (file_body))`
812         for o in opts:
813                 m = re.search ('filename="(.*?)"', o)
814                 if m:
815                         basename = m.group (1)
816                         if not taken_file_names.has_key(basename):
817                             taken_file_names[basename] = 0
818                         else:
819                             taken_file_names[basename] = taken_file_names[basename] + 1
820                             basename = basename + "-%i" % taken_file_names[basename]
821         if not g_read_lys:
822                 update_file(file_body, os.path.join(g_outdir, basename) + '.ly')
823         needed_filetypes = ['tex']
824
825         if format  == 'texi':
826                 needed_filetypes.append('eps')
827                 needed_filetypes.append('png')
828         if 'eps' in opts and not ('eps' in needed_filetypes):
829                 needed_filetypes.append('eps')
830         pathbase = os.path.join (g_outdir, basename)
831         def f(base, ext1, ext2):
832                 a = os.path.isfile(base + ext2)
833                 if (os.path.isfile(base + ext1) and
834                     os.path.isfile(base + ext2) and
835                                 os.stat(base+ext1)[stat.ST_MTIME] >
836                                 os.stat(base+ext2)[stat.ST_MTIME]) or \
837                                 not os.path.isfile(base + ext2):
838                         return 1
839         todo = []
840         if 'tex' in needed_filetypes and f(pathbase, '.ly', '.tex'):
841                 todo.append('tex')
842         if 'eps' in needed_filetypes and f(pathbase, '.tex', '.eps'):
843                 todo.append('eps')
844         if 'png' in needed_filetypes and f(pathbase, '.eps', '.png'):
845                 todo.append('png')
846         newbody = ''
847
848         if 'printfilename' in opts:
849                 for o in opts:
850                         m= re.match ("filename=(.*)", o)
851                         if m:
852                                 newbody = newbody + get_output ("output-filename") % m.group(1)
853                                 break
854                 
855         
856         if 'verbatim' in opts:
857                 newbody = output_verbatim (body)
858
859         for o in opts:
860                 m = re.search ('intertext="(.*?)"', o)
861                 if m:
862                         newbody = newbody  + m.group (1) + "\n\n"
863         if format == 'latex':
864                 if 'eps' in opts:
865                         s = 'output-eps'
866                 else:
867                         s = 'output-tex'
868         else: # format == 'texi'
869                 s = 'output-all'
870         newbody = newbody + get_output (s) % {'fn': basename }
871         return ('lilypond', newbody, opts, todo, basename)
872
873 def process_lilypond_blocks(outname, chunks):#ugh rename
874         newchunks = []
875         # Count sections/chapters.
876         for c in chunks:
877                 if c[0] == 'lilypond':
878                         c = schedule_lilypond_block (c)
879                 elif c[0] == 'numcols':
880                         paperguru.m_num_cols = c[2]
881                 newchunks.append (c)
882         return newchunks
883
884
885 def find_eps_dims (match):
886         "Fill in dimensions of EPS files."
887         
888         fn =match.group (1)
889         dims = bounding_box_dimensions (fn)
890         if g_outdir:
891                 fn = os.path.join(g_outdir, fn)
892         
893         return '%ipt' % dims[0]
894
895
896 def system (cmd):
897         sys.stderr.write ("invoking `%s'\n" % cmd)
898         st = os.system (cmd)
899         if st:
900                 error ('Error command exited with value %d\n' % st)
901         return st
902
903 def compile_all_files (chunks):
904         global foutn
905         eps = []
906         tex = []
907         png = []
908
909         for c in chunks:
910                 if c[0] <> 'lilypond':
911                         continue
912                 base  = c[4]
913                 exts = c[3]
914                 for e in exts:
915                         if e == 'eps':
916                                 eps.append (base)
917                         elif e == 'tex':
918                                 #ugh
919                                 if base + '.ly' not in tex:
920                                         tex.append (base + '.ly')
921                         elif e == 'png' and g_do_pictures:
922                                 png.append (base)
923         d = os.getcwd()
924         if g_outdir:
925                 os.chdir(g_outdir)
926         if tex:
927                 # fixme: be sys-independent.
928                 def incl_opt (x):
929                         if g_outdir and x[0] <> '/' :
930                                 x = os.path.join (g_here_dir, x)
931                         return ' -I %s' % x
932
933                 incs = map (incl_opt, include_path)
934                 lilyopts = string.join (incs, ' ' )
935                 if do_deps:
936                         lilyopts = lilyopts + ' --dependencies '
937                         if g_outdir:
938                                 lilyopts = lilyopts + '--dep-prefix=' + g_outdir + '/'
939                 texfiles = string.join (tex, ' ')
940                 system ('lilypond --header=texidoc %s %s' % (lilyopts, texfiles))
941
942                 #
943                 # Ugh, fixing up dependencies for .tex generation
944                 #
945                 if do_deps:
946                         depfiles=map (lambda x: re.sub ('(.*)\.ly', '\\1.dep', x), tex)
947                         for i in depfiles:
948                                 text=open (i).read ()
949                                 text=re.sub ('\n([^:\n]*):', '\n' + foutn + ':', text)
950                                 open (i, 'w').write (text)
951
952         for e in eps:
953                 system(r"tex '\nonstopmode \input %s'" % e)
954                 system(r"dvips -E -o %s %s" % (e + '.eps', e))
955         for g in png:
956                 cmd = r"""gs -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r90 -dNOPAUSE %s -c quit | pnmcrop | pnmtopng > %s"""
957                 cmd = cmd % (g + '.eps', g + '.png')
958                 system (cmd)
959         os.chdir (d)
960
961
962 def update_file (body, name):
963         """
964         write the body if it has changed
965         """
966         same = 0
967         try:
968                 f = open (name)
969                 fs = f.read (-1)
970                 same = (fs == body)
971         except:
972                 pass
973
974         if not same:
975                 f = open (name , 'w')
976                 f.write (body)
977                 f.close ()
978         
979         return not same
980
981
982 def getopt_args (opts):
983         "Construct arguments (LONG, SHORT) for getopt from  list of options."
984         short = ''
985         long = []
986         for o in opts:
987                 if o[1]:
988                         short = short + o[1]
989                         if o[0]:
990                                 short = short + ':'
991                 if o[2]:
992                         l = o[2]
993                         if o[0]:
994                                 l = l + '='
995                         long.append (l)
996         return (short, long)
997
998 def option_help_str (o):
999         "Transform one option description (4-tuple ) into neatly formatted string"
1000         sh = '  '       
1001         if o[1]:
1002                 sh = '-%s' % o[1]
1003
1004         sep = ' '
1005         if o[1] and o[2]:
1006                 sep = ','
1007                 
1008         long = ''
1009         if o[2]:
1010                 long= '--%s' % o[2]
1011
1012         arg = ''
1013         if o[0]:
1014                 if o[2]:
1015                         arg = '='
1016                 arg = arg + o[0]
1017         return '  ' + sh + sep + long + arg
1018
1019
1020 def options_help_str (opts):
1021         "Convert a list of options into a neatly formatted string"
1022         w = 0
1023         strs =[]
1024         helps = []
1025
1026         for o in opts:
1027                 s = option_help_str (o)
1028                 strs.append ((s, o[3]))
1029                 if len (s) > w:
1030                         w = len (s)
1031
1032         str = ''
1033         for s in strs:
1034                 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
1035         return str
1036
1037 def help():
1038         sys.stdout.write("""Usage: lilypond-book [options] FILE\n
1039 Generate hybrid LaTeX input from Latex + lilypond
1040 Options:
1041 """)
1042         sys.stdout.write (options_help_str (option_definitions))
1043         sys.stdout.write (r"""Warning all output is written in the CURRENT directory
1044
1045
1046
1047 Report bugs to bug-gnu-music@gnu.org.
1048
1049 Written by Tom Cato Amundsen <tca@gnu.org> and
1050 Han-Wen Nienhuys <hanwen@cs.uu.nl>
1051 """)
1052
1053         sys.exit (0)
1054
1055
1056 def write_deps (fn, target, chunks):
1057         global read_files
1058         sys.stdout.write('Writing `%s\'\n' % os.path.join(g_outdir, fn))
1059         f = open (os.path.join(g_outdir, fn), 'w')
1060         f.write ('%s%s: ' % (g_dep_prefix, target))
1061         for d in read_files:
1062                 f.write ('%s ' %  d)
1063         basenames=[]
1064         for c in chunks:
1065                 if c[0] == 'lilypond':
1066                         (type, body, opts, todo, basename) = c;
1067                         basenames.append (basename)
1068         for d in basenames:
1069                 if g_outdir:
1070                         d=g_outdir + '/' + d
1071                 if g_dep_prefix:
1072                         #if not os.isfile (d): # thinko?
1073                         if not re.search ('/', d):
1074                                 d = g_dep_prefix + d
1075                 f.write ('%s.tex ' %  d)
1076         f.write ('\n')
1077         #if len (basenames):
1078         #       for d in basenames:
1079         #               f.write ('%s.ly ' %  d)
1080         #       f.write (' : %s' % target)
1081         f.write ('\n')
1082         f.close ()
1083         read_files = []
1084
1085 def identify():
1086         sys.stdout.write ('lilypond-book (GNU LilyPond) %s\n' % program_version)
1087
1088 def print_version ():
1089         identify()
1090         sys.stdout.write (r"""Copyright 1998--1999
1091 Distributed under terms of the GNU General Public License. It comes with
1092 NO WARRANTY.
1093 """)
1094
1095
1096 def check_texidoc (chunks):
1097         n = []
1098         for c in chunks:
1099                 if c[0] == 'lilypond':
1100                         (type, body, opts, todo, basename) = c;
1101                         pathbase = os.path.join (g_outdir, basename)
1102                         if os.path.isfile (pathbase + '.texidoc'):
1103                                 body = '\n@include %s.texidoc\n' % basename + body
1104                                 c = (type, body, opts, todo, basename)
1105                 n.append (c)
1106         return n
1107
1108 def fix_epswidth (chunks):
1109         newchunks = []
1110         for c in chunks:
1111                 if c[0] == 'lilypond' and 'eps' in c[2]:
1112                         body = re.sub (r"""\\lilypondepswidth{(.*?)}""", find_eps_dims, c[1])
1113                         newchunks.append(('lilypond', body, c[2], c[3], c[4]))
1114                 else:
1115                         newchunks.append (c)
1116         return newchunks
1117
1118
1119 foutn=""
1120 def do_file(input_filename):
1121         global foutn
1122         file_settings = {}
1123         if outname:
1124                 my_outname = outname
1125         else:
1126                 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
1127         my_depname = my_outname + '.dep'                
1128
1129         chunks = read_doc_file(input_filename)
1130         chunks = chop_chunks(chunks, 'lilypond', make_lilypond, 1)
1131         chunks = chop_chunks(chunks, 'lilypond-file', make_lilypond_file, 1)
1132         chunks = chop_chunks(chunks, 'lilypond-block', make_lilypond_block, 1)
1133         chunks = chop_chunks(chunks, 'singleline-comment', do_ignore, 1)
1134         chunks = chop_chunks(chunks, 'preamble-end', do_preamble_end)
1135         chunks = chop_chunks(chunks, 'numcols', do_columns)
1136         #print "-" * 50
1137         #for c in chunks: print "c:", c;
1138         #sys.exit()
1139         scan_preamble(chunks)
1140         chunks = process_lilypond_blocks(my_outname, chunks)
1141
1142         foutn = os.path.join (g_outdir, my_outname + '.' + format)
1143
1144         # Do It.
1145         if __main__.g_run_lilypond:
1146                 compile_all_files (chunks)
1147                 chunks = fix_epswidth (chunks)
1148
1149         if __main__.format == 'texi':
1150                 chunks = check_texidoc (chunks)
1151
1152         x = 0
1153         chunks = completize_preamble (chunks)
1154         sys.stderr.write ("Writing `%s'\n" % foutn)
1155         fout = open (foutn, 'w')
1156         for c in chunks:
1157                 fout.write (c[1])
1158         fout.close ()
1159         # should chmod -w
1160
1161         if do_deps:
1162                 write_deps (my_depname, foutn, chunks)
1163
1164
1165 outname = ''
1166 try:
1167         (sh, long) = getopt_args (__main__.option_definitions)
1168         (options, files) = getopt.getopt(sys.argv[1:], sh, long)
1169 except getopt.error, msg:
1170         sys.stderr.write("error: %s" % msg)
1171         sys.exit(1)
1172
1173 do_deps = 0
1174 for opt in options:     
1175         o = opt[0]
1176         a = opt[1]
1177
1178         if o == '--include' or o == '-I':
1179                 include_path.append (a)
1180         elif o == '--version' or o == '-v':
1181                 print_version ()
1182                 sys.exit  (0)
1183         elif o == '--format' or o == '-f':
1184                 __main__.format = a
1185         elif o == '--outname' or o == '-o':
1186                 if len(files) > 1:
1187                         #HACK
1188                         sys.stderr.write("Lilypond-book is confused by --outname on multiple files")
1189                         sys.exit(1)
1190                 outname = a
1191         elif o == '--help' or o == '-h':
1192                 help ()
1193         elif o == '--no-lily' or o == '-n':
1194                 __main__.g_run_lilypond = 0
1195         elif o == '--dependencies' or o == '-M':
1196                 do_deps = 1
1197         elif o == '--default-music-fontsize':
1198                 default_music_fontsize = string.atoi (a)
1199         elif o == '--default-lilypond-fontsize':
1200                 print "--default-lilypond-fontsize is deprecated, use --default-music-fontsize"
1201                 default_music_fontsize = string.atoi (a)
1202         elif o == '--force-music-fontsize':
1203                 g_force_lilypond_fontsize = string.atoi(a)
1204         elif o == '--force-lilypond-fontsize':
1205                 print "--force-lilypond-fontsize is deprecated, use --default-lilypond-fontsize"
1206                 g_force_lilypond_fontsize = string.atoi(a)
1207         elif o == '--dep-prefix':
1208                 g_dep_prefix = a
1209         elif o == '--no-pictures':
1210                 g_do_pictures = 0
1211         elif o == '--read-lys':
1212                 g_read_lys = 1
1213         elif o == '--outdir':
1214                 g_outdir = a
1215
1216 identify()
1217 if g_outdir:
1218         if os.path.isfile(g_outdir):
1219                 error ("outdir is a file: %s" % g_outdir)
1220         if not os.path.exists(g_outdir):
1221                 os.mkdir(g_outdir)
1222 for input_filename in files:
1223         do_file(input_filename)
1224         
1225 #
1226 # Petr, ik zou willen dat ik iets zinvoller deed,
1227 # maar wat ik kan ik doen, het verandert toch niets?
1228 #   --hwn 20/aug/99