]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
patch::: 1.3.114.jcn1
[lilypond.git] / scripts / lilypond-book.py
1 #!@PYTHON@
2 # vim: set noexpandtab:
3 # TODO:
4 # * Figure out clean set of options. Hmm, isn't it pretty ok now?
5 # * add support for .lilyrc
6 # * EndLilyPondOutput is def'd as vfil. Causes large white gaps.
7 # * texinfo: add support for @pagesize
8
9 # todo: dimension handling (all the x2y) is clumsy. (tca: Thats
10 #       because the values are taken directly from texinfo.tex,
11 #       geometry.sty and article.cls. Give me a hint, and I'll
12 #       fix it.)
13
14 # This is was the idea for handling of comments:
15 #       Multiline comments, @ignore .. @end ignore is scanned for
16 #       in read_doc_file, and the chunks are marked as 'ignore', so
17 #       lilypond-book will not touch them any more. The content of the
18 #       chunks are written to the output file. Also 'include' and 'input'
19 #       regex has to check if they are commented out.
20 #
21 #       Then it is scanned for 'lilypond', 'lilypond-file' and 'lilypond-block'.
22 #       These three regex's has to check if they are on a commented line,
23 #       % for latex, @c for texinfo.
24 #
25 #       Then lines that are commented out with % (latex) and @c (Texinfo)
26 #       are put into chunks marked 'ignore'. This cannot be done before
27 #       searching for the lilypond-blocks because % is also the comment character
28 #       for lilypond.
29 #
30 #       The the rest of the rexeces are searched for. They don't have to test
31 #       if they are on a commented out line.
32
33 import os
34 import stat
35 import string
36 import re
37 import getopt
38 import sys
39 import __main__
40 import operator
41
42
43 program_version = '@TOPLEVEL_VERSION@'
44 if program_version == '@' + 'TOPLEVEL_VERSION' + '@':
45         program_version = '1.3.113'
46
47 include_path = [os.getcwd()]
48
49
50 # g_ is for global (?)
51
52 g_here_dir = os.getcwd ()
53 g_dep_prefix = ''
54 g_outdir = ''
55 g_force_lilypond_fontsize = 0
56 g_read_lys = 0
57 g_do_pictures = 1
58 g_num_cols = 1
59 format = ''
60 g_run_lilypond = 1
61 no_match = 'a\ba'
62
63 default_music_fontsize = 16
64 default_text_fontsize = 12
65
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 @include %(fn)s.texidoc
352 @tex
353 \catcode`\@=12
354 \input lilyponddefs
355 \def\EndLilyPondOutput{}
356 \input %(fn)s.tex
357 \catcode`\@=0
358 @end tex
359 @html
360 <p>
361 <img src=%(fn)s.png>
362 @end html
363 """,
364                 }
365         }
366
367 def output_verbatim (body):
368         if __main__.format == 'texi':
369                 body = re.sub ('([@{}])', '@\\1', body)
370         return get_output ('output-verbatim') % body
371
372
373 re_dict = {
374         'latex': {'input': r'(?m)^[^%\n]*?(?P<match>\\mbinput{?([^}\t \n}]*))',
375                   'include': r'(?m)^[^%\n]*?(?P<match>\\mbinclude{(?P<filename>[^}]+)})',
376                   'option-sep' : ', *',
377                   'header': r"\\documentclass\s*(\[.*?\])?",
378                   'geometry': r"^(?m)[^%\n]*?\\usepackage\s*(\[(?P<options>.*)\])?\s*{geometry}",
379                   'preamble-end': r'(?P<code>\\begin{document})',
380                   'verbatim': r"(?s)(?P<code>\\begin{verbatim}.*?\\end{verbatim})",
381                   'verb': r"(?P<code>\\verb(?P<del>.).*?(?P=del))",
382                   'lilypond-file': r'(?m)^[^%\n]*?(?P<match>\\lilypondfile(\[(?P<options>.*?)\])?\{(?P<filename>.+)})',
383                   'lilypond' : r'(?m)^[^%\n]*?(?P<match>\\lilypond(\[(?P<options>.*?)\])?{(?P<code>.*?)})',
384                   'lilypond-block': r"(?sm)^[^%\n]*?(?P<match>\\begin(\[(?P<options>.*?)\])?{lilypond}(?P<code>.*?)\\end{lilypond})",
385                   'def-post-re': r"\\def\\postLilypondExample",
386                   'def-pre-re': r"\\def\\preLilypondExample",             
387                   'usepackage-graphics': r"\usepackage{graphics}",
388                   'intertext': r',?\s*intertext=\".*?\"',
389                   'multiline-comment': no_match,
390                   'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>^%.*$\n+))",
391                   'numcols': r"(?P<code>\\(?P<num>one|two)column)",
392                   },
393         
394         'texi': {
395                  'include':  '(?m)^[^%\n]*?(?P<match>@mbinclude[ \n\t]+(?P<filename>[^\t \n]*))',
396                  'input': no_match,
397                  'header': no_match,
398                  'preamble-end': no_match,
399                  'landscape': no_match,
400                  'verbatim': r"""(?s)(?P<code>@example\s.*?@end example\s)""",
401                  'verb': r"""(?P<code>@code{.*?})""",
402                  'lilypond-file': '(?m)^(?!@c)(?P<match>@lilypondfile(\[(?P<options>.*?)\])?{(?P<filename>[^}]+)})',
403                  'lilypond' : '(?m)^(?!@c)(?P<match>@lilypond(\[(?P<options>.*?)\])?{(?P<code>.*?)})',
404                  'lilypond-block': r"""(?m)^(?!@c)(?P<match>(?s)(?P<match>@lilypond(\[(?P<options>.*?)\])?\s(?P<code>.*?)@end lilypond\s))""",
405                   'option-sep' : ', *',
406                   'intertext': r',?\s*intertext=\".*?\"',
407                   'multiline-comment': r"(?sm)^\s*(?!@c\s+)(?P<code>@ignore\s.*?@end ignore)\s",
408                   'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>@c.*$\n+))",
409                   'numcols': no_match,
410                  }
411         }
412
413
414 for r in re_dict.keys ():
415         olddict = re_dict[r]
416         newdict = {}
417         for k in olddict.keys ():
418                 newdict[k] = re.compile (olddict[k])
419         re_dict[r] = newdict
420
421         
422 def uniq (list):
423         list.sort ()
424         s = list
425         list = []
426         for x in s:
427                 if x not in list:
428                         list.append (x)
429         return list
430                 
431
432 def get_output (name):
433         return  output_dict[format][name]
434
435 def get_re (name):
436         return  re_dict[format][name]
437
438 def bounding_box_dimensions(fname):
439         if g_outdir:
440                 fname = os.path.join(g_outdir, fname)
441         try:
442                 fd = open(fname)
443         except IOError:
444                 error ("Error opening `%s'" % fname)
445         str = fd.read ()
446         s = re.search('%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)', str)
447         if s:
448                 return (int(s.group(3))-int(s.group(1)), 
449                         int(s.group(4))-int(s.group(2)))
450         else:
451                 return (0,0)
452
453
454 def error (str):
455         sys.stderr.write (str + "\n  Exiting ... \n\n")
456         raise 'Exiting.'
457
458
459 def compose_full_body (body, opts):
460         """Construct the lilypond code to send to Lilypond.
461         Add stuff to BODY using OPTS as options."""
462         music_size = default_music_fontsize
463         latex_size = default_text_fontsize
464         for o in opts:
465                 if g_force_lilypond_fontsize:
466                         music_size = g_force_lilypond_fontsize
467                 else:
468                         m = re.match ('([0-9]+)pt', o)
469                         if m:
470                                 music_size = string.atoi(m.group (1))
471
472                 m = re.match ('latexfontsize=([0-9]+)pt', o)
473                 if m:
474                         latex_size = string.atoi (m.group (1))
475
476         if re.search ('\\\\score', body):
477                 is_fragment = 0
478         else:
479                 is_fragment = 1
480         if 'fragment' in opts:
481                 is_fragment = 1
482         if 'nonfragment' in opts:
483                 is_fragment = 0
484
485         if is_fragment and not 'multiline' in opts:
486                 opts.append('singleline')
487         if 'singleline' in opts:
488                 l = -1.0;
489         else:
490                 l = paperguru.get_linewidth()
491         
492         if 'relative' in opts:#ugh only when is_fragment
493                 body = '\\relative c { %s }' % body
494         
495         if is_fragment:
496                 body = r"""\score { 
497  \notes { %s }
498   \paper { }  
499 }""" % body
500
501         opts = uniq (opts)
502         optstring = string.join (opts, ' ')
503         optstring = re.sub ('\n', ' ', optstring)
504         body = r"""
505 %% Generated by lilypond-book.py; options are %s  %%ughUGH not original options
506 \include "paper%d.ly"
507 \paper  { linewidth = %f \pt; } 
508 """ % (optstring, music_size, l) + body
509         return body
510
511 def parse_options_string(s):
512         d = {}
513         r1 = re.compile("((\w+)={(.*?)})((,\s*)|$)")
514         r2 = re.compile("((\w+)=(.*?))((,\s*)|$)")
515         r3 = re.compile("(\w+?)((,\s*)|$)")
516         while s:
517                 m = r1.match(s)
518                 if m:
519                         s = s[m.end():]
520                         d[m.group(2)] = re.split(",\s*", m.group(3))
521                         continue
522                 m = r2.match(s)
523                 if m:
524                         s = s[m.end():]
525                         d[m.group(2)] = m.group(3)
526                         continue
527                 m = r3.match(s)
528                 if m:
529                         s = s[m.end():]
530                         d[m.group(1)] = 1
531                         continue
532                 print "trøbbel:%s:" % s
533         return d
534
535 def scan_latex_preamble(chunks):
536         # first we want to scan the \documentclass line
537         # it should be the first non-comment line
538         idx = 0
539         while 1:
540                 if chunks[idx][0] == 'ignore':
541                         idx = idx + 1
542                         continue
543                 m = get_re ('header').match(chunks[idx][1])
544                 options = re.split (',[\n \t]*', m.group(1)[1:-1])
545                 for o in options:
546                         if o == 'landscape':
547                                 paperguru.m_landscape = 1
548                         m = re.match("(.*?)paper", o)
549                         if m:
550                                 paperguru.m_papersize = m.group()
551                         else:
552                                 m = re.match("(\d\d)pt", o)
553                                 if m:
554                                         paperguru.m_fontsize = int(m.group(1))
555                         
556                 break
557         while chunks[idx][0] != 'preamble-end':
558                 if chunks[idx] == 'ignore':
559                         idx = idx + 1
560                         continue
561                 m = get_re ('geometry').search(chunks[idx][1])
562                 if m:
563                         paperguru.m_use_geometry = 1
564                         o = parse_options_string(m.group('options'))
565                         for k in o.keys():
566                                 paperguru.set_geo_option(k, o[k])
567                 idx = idx + 1
568
569 def scan_texi_preamble (chunks):
570         # this is not bulletproof..., it checks the first 10 chunks
571         idx = 0
572         while 1:
573                 if chunks[idx][0] == 'input':
574                         for s in ('afourpaper', 'afourwide', 'letterpaper',
575                                   'afourlatex', 'smallbook'):
576                                 if string.find(chunks[idx][1], "@%s" % s) != -1:
577                                         paperguru.m_papersize = s
578                 idx = idx + 1
579                 if idx == 10 or idx == len(chunks):
580                         break
581
582 def scan_preamble (chunks):
583         if __main__.format == 'texi':
584                 scan_texi_preamble(chunks)
585         else:
586                 assert __main__.format == 'latex'
587                 scan_latex_preamble(chunks)
588                 
589
590 def completize_preamble (chunks):
591         if __main__.format == 'texi':
592                 return chunks
593         pre_b = post_b = graphics_b = None
594         for chunk in chunks:
595                 if chunk[0] == 'preamble-end':
596                         break
597                 if chunk[0] == 'input':
598                         m = get_re('def-pre-re').search(chunk[1])
599                         if m:
600                                 pre_b = 1
601                 if chunk[0] == 'input':
602                         m = get_re('def-post-re').search(chunk[1])
603                         if m:
604                                 post_b = 1
605                 if chunk[0] == 'input':
606                         m = get_re('usepackage-graphics').search(chunk[1])
607                         if m:
608                                 graphics_b = 1
609         x = 0
610         while chunks[x][0] != 'preamble-end':
611                 x = x + 1
612         if not pre_b:
613                 chunks.insert(x, ('input', get_output ('output-default-pre')))
614         if not post_b:
615                 chunks.insert(x, ('input', get_output ('output-default-post')))
616         if not graphics_b:
617                 chunks.insert(x, ('input', get_output ('usepackage-graphics')))
618         return chunks
619
620
621 read_files = []
622 def find_file (name):
623         f = None
624         for a in include_path:
625                 try:
626                         nm = os.path.join (a, name)
627                         f = open (nm)
628                         __main__.read_files.append (nm)
629                         break
630                 except IOError:
631                         pass
632         if f:
633                 return f.read ()
634         else:
635                 error ("File not found `%s'\n" % name)
636                 return ''
637
638 def do_ignore(match_object):
639         return [('ignore', match_object.group('code'))]
640 def do_preamble_end(match_object):
641         return [('preamble-end', match_object.group('code'))]
642
643 def make_verbatim(match_object):
644         return [('verbatim', match_object.group('code'))]
645
646 def make_verb(match_object):
647         return [('verb', match_object.group('code'))]
648
649 def do_include_file(m):
650         "m: MatchObject"
651         return [('input', get_output ('pagebreak'))] \
652              + read_doc_file(m.group('filename')) \
653              + [('input', get_output ('pagebreak'))] 
654
655 def do_input_file(m):
656         return read_doc_file(m.group('filename'))
657
658 def make_lilypond(m):
659         if m.group('options'):
660                 options = m.group('options')
661         else:
662                 options = ''
663         return [('input', get_output('output-lilypond-fragment') % 
664                         (options, m.group('code')))]
665
666 def make_lilypond_file(m):
667         if m.group('options'):
668                 options = m.group('options')
669         else:
670                 options = ''
671         return [('input', get_output('output-lilypond') %
672                         (options, find_file(m.group('filename'))))]
673
674 def make_lilypond_block(m):
675         if m.group('options'):
676                 options = get_re('option-sep').split (m.group('options'))
677         else:
678             options = []
679         options = filter(lambda s: s != '', options)
680         return [('lilypond', m.group('code'), options)]
681
682 def do_columns(m):
683         if __main__.format != 'latex':
684                 return []
685         if m.group('num') == 'one':
686                 return [('numcols', m.group('code'), 1)]
687         if m.group('num') == 'two':
688                 return [('numcols', m.group('code'), 2)]
689         
690 def chop_chunks(chunks, re_name, func, use_match=0):
691     newchunks = []
692     for c in chunks:
693         if c[0] == 'input':
694             str = c[1]
695             while str:
696                 m = get_re (re_name).search (str)
697                 if m == None:
698                     newchunks.append (('input', str))
699                     str = ''
700                 else:
701                     if use_match:
702                         newchunks.append (('input', str[:m.start ('match')]))
703                     else:
704                         newchunks.append (('input', str[:m.start (0)]))
705                     #newchunks.extend(func(m))
706                     # python 1.5 compatible:
707                     newchunks = newchunks + func(m)
708                     str = str [m.end(0):]
709         else:
710             newchunks.append(c)
711     return newchunks
712
713 def read_doc_file (filename):
714         """Read the input file, find verbatim chunks and do \input and \include
715         """
716         str = ''
717         str = find_file(filename)
718
719         if __main__.format == '':
720                 latex =  re.search ('\\\\document', str[:200])
721                 texinfo =  re.search ('@node|@setfilename', str[:200])
722                 if (texinfo and latex) or not (texinfo or latex):
723                         error("error: can't determine format, please specify")
724                 if texinfo:
725                         __main__.format = 'texi'
726                 else:
727                         __main__.format = 'latex'
728         if __main__.format == 'texi':
729                 __main__.paperguru = TexiPaper()
730         else:
731                 __main__.paperguru = LatexPaper()
732         chunks = [('input', str)]
733         # we have to check for verbatim before doing include,
734         # because we don't want to include files that are mentioned
735         # inside a verbatim environment
736         chunks = chop_chunks(chunks, 'verbatim', make_verbatim)
737         chunks = chop_chunks(chunks, 'verb', make_verb)
738         chunks = chop_chunks(chunks, 'multiline-comment', do_ignore)
739         #ugh fix input
740         chunks = chop_chunks(chunks, 'include', do_include_file, 1)
741         chunks = chop_chunks(chunks, 'input', do_input_file, 1)
742         return chunks
743
744
745 taken_file_names = {}
746 def schedule_lilypond_block (chunk):
747         """Take the body and options from CHUNK, figure out how the
748         real .ly should look, and what should be left MAIN_STR (meant
749         for the main file).  The .ly is written, and scheduled in
750         TODO.
751
752         Return: a chunk (TYPE_STR, MAIN_STR, OPTIONS, TODO, BASE)
753
754         TODO has format [basename, extension, extension, ... ]
755         
756         """
757         (type, body, opts) = chunk
758         assert type == 'lilypond'
759         file_body = compose_full_body (body, opts)
760         basename = `abs(hash (file_body))`
761         for o in opts:
762                 m = re.search ('filename="(.*?)"', o)
763                 if m:
764                         basename = m.group (1)
765                         if not taken_file_names.has_key(basename):
766                             taken_file_names[basename] = 0
767                         else:
768                             taken_file_names[basename] = taken_file_names[basename] + 1
769                             basename = basename + "-%i" % taken_file_names[basename]
770         if not g_read_lys:
771                 update_file(file_body, os.path.join(g_outdir, basename) + '.ly')
772         needed_filetypes = ['tex']
773
774         if format  == 'texi':
775                 needed_filetypes.append('eps')
776                 needed_filetypes.append('png')
777         if 'eps' in opts and not ('eps' in needed_filetypes):
778                 needed_filetypes.append('eps')
779         outname = os.path.join(g_outdir, basename)
780         def f(base, ext1, ext2):
781                 a = os.path.isfile(base + ext2)
782                 if (os.path.isfile(base + ext1) and
783                     os.path.isfile(base + ext2) and
784                                 os.stat(base+ext1)[stat.ST_MTIME] >
785                                 os.stat(base+ext2)[stat.ST_MTIME]) or \
786                                 not os.path.isfile(base + ext2):
787                         return 1
788         todo = []
789         if 'tex' in needed_filetypes and f(outname, '.ly', '.tex'):
790                 todo.append('tex')
791         if 'eps' in needed_filetypes and f(outname, '.tex', '.eps'):
792                 todo.append('eps')
793         if 'png' in needed_filetypes and f(outname, '.eps', '.png'):
794                 todo.append('png')
795         newbody = ''
796         if 'verbatim' in opts:
797                 newbody = output_verbatim (body)
798
799         for o in opts:
800                 m = re.search ('intertext="(.*?)"', o)
801                 if m:
802                         newbody = newbody  + m.group (1) + "\n\n"
803         if format == 'latex':
804                 if 'eps' in opts:
805                         s = 'output-eps'
806                 else:
807                         s = 'output-tex'
808         else: # format == 'texi'
809                 s = 'output-all'
810         newbody = newbody + get_output(s) % {'fn': basename }
811         return ('lilypond', newbody, opts, todo, basename)
812
813 def process_lilypond_blocks(outname, chunks):#ugh rename
814         newchunks = []
815         # Count sections/chapters.
816         for c in chunks:
817                 if c[0] == 'lilypond':
818                         c = schedule_lilypond_block (c)
819                 elif c[0] == 'numcols':
820                         paperguru.m_num_cols = c[2]
821                 newchunks.append (c)
822         return newchunks
823
824
825 def find_eps_dims (match):
826         "Fill in dimensions of EPS files."
827         
828         fn =match.group (1)
829         dims = bounding_box_dimensions (fn)
830         if g_outdir:
831                 fn = os.path.join(g_outdir, fn)
832         
833         return '%ipt' % dims[0]
834
835
836 def system (cmd):
837         sys.stderr.write ("invoking `%s'\n" % cmd)
838         st = os.system (cmd)
839         if st:
840                 error ('Error command exited with value %d\n' % st)
841         return st
842
843 def compile_all_files (chunks):
844         eps = []
845         tex = []
846         png = []
847
848         for c in chunks:
849                 if c[0] <> 'lilypond':
850                         continue
851                 base  = c[4]
852                 exts = c[3]
853                 for e in exts:
854                         if e == 'eps':
855                                 eps.append (base)
856                         elif e == 'tex':
857                                 #ugh
858                                 if base + '.ly' not in tex:
859                                         tex.append (base + '.ly')
860                         elif e == 'png' and g_do_pictures:
861                                 png.append (base)
862         d = os.getcwd()
863         if g_outdir:
864                 os.chdir(g_outdir)
865         if tex:
866                 # fixme: be sys-independent.
867                 def incl_opt (x):
868                         if g_outdir and x[0] <> '/' :
869                                 x = os.path.join (g_here_dir, x)
870                         return ' -I %s' % x
871
872                 incs =  map (incl_opt, include_path)
873                 lilyopts = string.join (incs, ' ' )
874                 texfiles = string.join (tex, ' ')
875                 system ('lilypond --header=texidoc %s %s' % (lilyopts, texfiles))
876         for e in eps:
877                 system(r"tex '\nonstopmode \input %s'" % e)
878                 system(r"dvips -E -o %s %s" % (e + '.eps', e))
879         for g in png:
880                 cmd = r"""gs -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r90 -dNOPAUSE %s -c quit | pnmcrop | pnmtopng > %s"""
881                 cmd = cmd % (g + '.eps', g + '.png')
882                 system (cmd)
883         if g_outdir:
884                 os.chdir(d)
885
886
887 def update_file (body, name):
888         """
889         write the body if it has changed
890         """
891         same = 0
892         try:
893                 f = open (name)
894                 fs = f.read (-1)
895                 same = (fs == body)
896         except:
897                 pass
898
899         if not same:
900                 f = open (name , 'w')
901                 f.write (body)
902                 f.close ()
903         
904         return not same
905
906
907 def getopt_args (opts):
908         "Construct arguments (LONG, SHORT) for getopt from  list of options."
909         short = ''
910         long = []
911         for o in opts:
912                 if o[1]:
913                         short = short + o[1]
914                         if o[0]:
915                                 short = short + ':'
916                 if o[2]:
917                         l = o[2]
918                         if o[0]:
919                                 l = l + '='
920                         long.append (l)
921         return (short, long)
922
923 def option_help_str (o):
924         "Transform one option description (4-tuple ) into neatly formatted string"
925         sh = '  '       
926         if o[1]:
927                 sh = '-%s' % o[1]
928
929         sep = ' '
930         if o[1] and o[2]:
931                 sep = ','
932                 
933         long = ''
934         if o[2]:
935                 long= '--%s' % o[2]
936
937         arg = ''
938         if o[0]:
939                 if o[2]:
940                         arg = '='
941                 arg = arg + o[0]
942         return '  ' + sh + sep + long + arg
943
944
945 def options_help_str (opts):
946         "Convert a list of options into a neatly formatted string"
947         w = 0
948         strs =[]
949         helps = []
950
951         for o in opts:
952                 s = option_help_str (o)
953                 strs.append ((s, o[3]))
954                 if len (s) > w:
955                         w = len (s)
956
957         str = ''
958         for s in strs:
959                 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
960         return str
961
962 def help():
963         sys.stdout.write("""Usage: lilypond-book [options] FILE\n
964 Generate hybrid LaTeX input from Latex + lilypond
965 Options:
966 """)
967         sys.stdout.write (options_help_str (option_definitions))
968         sys.stdout.write (r"""Warning all output is written in the CURRENT directory
969
970
971
972 Report bugs to bug-gnu-music@gnu.org.
973
974 Written by Tom Cato Amundsen <tca@gnu.org> and
975 Han-Wen Nienhuys <hanwen@cs.uu.nl>
976 """)
977
978         sys.exit (0)
979
980
981 def write_deps (fn, target):
982         sys.stdout.write('writing `%s\'\n' % os.path.join(g_outdir, fn))
983         f = open (os.path.join(g_outdir, fn), 'w')
984         f.write ('%s%s: ' % (g_dep_prefix, target))
985         for d in __main__.read_files:
986                 f.write ('%s ' %  d)
987         f.write ('\n')
988         f.close ()
989         __main__.read_files = []
990
991 def identify():
992         sys.stdout.write ('lilypond-book (GNU LilyPond) %s\n' % program_version)
993
994 def print_version ():
995         identify()
996         sys.stdout.write (r"""Copyright 1998--1999
997 Distributed under terms of the GNU General Public License. It comes with
998 NO WARRANTY.
999 """)
1000
1001 def do_file(input_filename):
1002         file_settings = {}
1003         if outname:
1004                 my_outname = outname
1005         else:
1006                 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
1007         my_depname = my_outname + '.dep'                
1008
1009         chunks = read_doc_file(input_filename)
1010         chunks = chop_chunks(chunks, 'lilypond', make_lilypond, 1)
1011         chunks = chop_chunks(chunks, 'lilypond-file', make_lilypond_file, 1)
1012         chunks = chop_chunks(chunks, 'lilypond-block', make_lilypond_block, 1)
1013         chunks = chop_chunks(chunks, 'singleline-comment', do_ignore, 1)
1014         chunks = chop_chunks(chunks, 'preamble-end', do_preamble_end)
1015         chunks = chop_chunks(chunks, 'numcols', do_columns)
1016         #print "-" * 50
1017         #for c in chunks: print "c:", c;
1018         #sys.exit()
1019         scan_preamble(chunks)
1020         chunks = process_lilypond_blocks(my_outname, chunks)
1021         # Do It.
1022         if __main__.g_run_lilypond:
1023                 compile_all_files (chunks)
1024                 newchunks = []
1025                 # finishing touch.
1026                 for c in chunks:
1027                         if c[0] == 'lilypond' and 'eps' in c[2]:
1028                                 body = re.sub (r"""\\lilypondepswidth{(.*?)}""", find_eps_dims, c[1])
1029                                 newchunks.append (('lilypond', body))
1030                         else:
1031                                 newchunks.append (c)
1032                 chunks = newchunks
1033         x = 0
1034         chunks = completize_preamble (chunks)
1035         foutn = os.path.join(g_outdir, my_outname + '.' + format)
1036         sys.stderr.write ("Writing `%s'\n" % foutn)
1037         fout = open (foutn, 'w')
1038         for c in chunks:
1039                 fout.write (c[1])
1040         fout.close ()
1041         # should chmod -w
1042
1043         if do_deps:
1044                 write_deps (my_depname, foutn)
1045
1046
1047 outname = ''
1048 try:
1049         (sh, long) = getopt_args (__main__.option_definitions)
1050         (options, files) = getopt.getopt(sys.argv[1:], sh, long)
1051 except getopt.error, msg:
1052         sys.stderr.write("error: %s" % msg)
1053         sys.exit(1)
1054
1055 do_deps = 0
1056 for opt in options:     
1057         o = opt[0]
1058         a = opt[1]
1059
1060         if o == '--include' or o == '-I':
1061                 include_path.append (a)
1062         elif o == '--version' or o == '-v':
1063                 print_version ()
1064                 sys.exit  (0)
1065         elif o == '--format' or o == '-f':
1066                 __main__.format = a
1067         elif o == '--outname' or o == '-o':
1068                 if len(files) > 1:
1069                         #HACK
1070                         sys.stderr.write("Lilypond-book is confused by --outname on multiple files")
1071                         sys.exit(1)
1072                 outname = a
1073         elif o == '--help' or o == '-h':
1074                 help ()
1075         elif o == '--no-lily' or o == '-n':
1076                 __main__.g_run_lilypond = 0
1077         elif o == '--dependencies' or o == '-M':
1078                 do_deps = 1
1079         elif o == '--default-music-fontsize':
1080                 default_music_fontsize = string.atoi (a)
1081         elif o == '--default-lilypond-fontsize':
1082                 print "--default-lilypond-fontsize is deprecated, use --default-music-fontsize"
1083                 default_music_fontsize = string.atoi (a)
1084         elif o == '--force-music-fontsize':
1085                 g_force_lilypond_fontsize = string.atoi(a)
1086         elif o == '--force-lilypond-fontsize':
1087                 print "--force-lilypond-fontsize is deprecated, use --default-lilypond-fontsize"
1088                 g_force_lilypond_fontsize = string.atoi(a)
1089         elif o == '--dep-prefix':
1090                 g_dep_prefix = a
1091         elif o == '--no-pictures':
1092                 g_do_pictures = 0
1093         elif o == '--read-lys':
1094                 g_read_lys = 1
1095         elif o == '--outdir':
1096                 g_outdir = a
1097
1098 identify()
1099 if g_outdir:
1100         if os.path.isfile(g_outdir):
1101                 error ("outdir is a file: %s" % g_outdir)
1102         if not os.path.exists(g_outdir):
1103                 os.mkdir(g_outdir)
1104 for input_filename in files:
1105         do_file(input_filename)
1106         
1107 #
1108 # Petr, ik zou willen dat ik iets zinvoller deed,
1109 # maar wat ik kan ik doen, het verandert toch niets?
1110 #   --hwn 20/aug/99