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