]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
patch::: 1.3.113.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 @include %(fn)s-doc.texi
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
844 texidoc_re = re.compile (r'.*?\n\s*texidoc\s*=\s*"((([^"])|([^\\]\\"))*)".*', re.DOTALL)
845 def extract_texidoc (lyfile):
846         """
847         Extract the ``texidoc'' entry from the lyfile.ly, and write it to
848         lyfile-doc.texi.
849
850         Maybe this should be output by `lilypond --header=texidoc' or so?
851         """
852         outfile = os.path.basename (os.path.splitext (lyfile)[0]) + "-doc.texi"
853         sys.stderr.write ("Writing `%s'\n" % outfile)
854         f = open (lyfile)
855         s = f.read (-1)
856 #       doc = re.sub (r'(.|\n)*?\n\s*texidoc\s*=\s*"((([^"])|([^\\]\\"))*)"(.|\n)*', '\\2', s) + '\n'
857         m = texidoc_re.match (s)
858         doc = ''
859         if m:
860                 doc = m.group (1) + '\n'
861         f = open (outfile, 'w')
862         f.write (doc)
863         f.close ()
864
865 def compile_all_files (chunks):
866         eps = []
867         tex = []
868         png = []
869
870         for c in chunks:
871                 if c[0] <> 'lilypond':
872                         continue
873                 base  = c[4]
874                 exts = c[3]
875                 for e in exts:
876                         if e == 'eps':
877                                 eps.append (base)
878                         elif e == 'tex':
879                                 #ugh
880                                 if base + '.ly' not in tex:
881                                         tex.append (base + '.ly')
882                         elif e == 'png' and g_do_pictures:
883                                 png.append (base)
884         d = os.getcwd()
885         if g_outdir:
886                 os.chdir(g_outdir)
887         if tex:
888                 # fixme: be sys-independent.
889                 def incl_opt (x):
890                         if g_outdir and x[0] <> '/' :
891                                 x = os.path.join (g_here_dir, x)
892                         return ' -I %s' % x
893
894                 incs =  map (incl_opt, include_path)
895                 lilyopts = string.join (incs, ' ' )
896                 texfiles = string.join (tex, ' ')
897                 system ('lilypond %s %s' % (lilyopts, texfiles))
898                 for i in tex:
899                         extract_texidoc (i)
900         for e in eps:
901                 system(r"tex '\nonstopmode \input %s'" % e)
902                 system(r"dvips -E -o %s %s" % (e + '.eps', e))
903         for g in png:
904                 cmd = r"""gs -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r90 -dNOPAUSE %s -c quit | pnmcrop | pnmtopng > %s"""
905                 cmd = cmd % (g + '.eps', g + '.png')
906                 system (cmd)
907         if g_outdir:
908                 os.chdir(d)
909
910
911 def update_file (body, name):
912         """
913         write the body if it has changed
914         """
915         same = 0
916         try:
917                 f = open (name)
918                 fs = f.read (-1)
919                 same = (fs == body)
920         except:
921                 pass
922
923         if not same:
924                 f = open (name , 'w')
925                 f.write (body)
926                 f.close ()
927         
928         return not same
929
930
931 def getopt_args (opts):
932         "Construct arguments (LONG, SHORT) for getopt from  list of options."
933         short = ''
934         long = []
935         for o in opts:
936                 if o[1]:
937                         short = short + o[1]
938                         if o[0]:
939                                 short = short + ':'
940                 if o[2]:
941                         l = o[2]
942                         if o[0]:
943                                 l = l + '='
944                         long.append (l)
945         return (short, long)
946
947 def option_help_str (o):
948         "Transform one option description (4-tuple ) into neatly formatted string"
949         sh = '  '       
950         if o[1]:
951                 sh = '-%s' % o[1]
952
953         sep = ' '
954         if o[1] and o[2]:
955                 sep = ','
956                 
957         long = ''
958         if o[2]:
959                 long= '--%s' % o[2]
960
961         arg = ''
962         if o[0]:
963                 if o[2]:
964                         arg = '='
965                 arg = arg + o[0]
966         return '  ' + sh + sep + long + arg
967
968
969 def options_help_str (opts):
970         "Convert a list of options into a neatly formatted string"
971         w = 0
972         strs =[]
973         helps = []
974
975         for o in opts:
976                 s = option_help_str (o)
977                 strs.append ((s, o[3]))
978                 if len (s) > w:
979                         w = len (s)
980
981         str = ''
982         for s in strs:
983                 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
984         return str
985
986 def help():
987         sys.stdout.write("""Usage: lilypond-book [options] FILE\n
988 Generate hybrid LaTeX input from Latex + lilypond
989 Options:
990 """)
991         sys.stdout.write (options_help_str (option_definitions))
992         sys.stdout.write (r"""Warning all output is written in the CURRENT directory
993
994
995
996 Report bugs to bug-gnu-music@gnu.org.
997
998 Written by Tom Cato Amundsen <tca@gnu.org> and
999 Han-Wen Nienhuys <hanwen@cs.uu.nl>
1000 """)
1001
1002         sys.exit (0)
1003
1004
1005 def write_deps (fn, target):
1006         sys.stdout.write('writing `%s\'\n' % os.path.join(g_outdir, fn))
1007         f = open (os.path.join(g_outdir, fn), 'w')
1008         f.write ('%s%s: ' % (g_dep_prefix, target))
1009         for d in __main__.read_files:
1010                 f.write ('%s ' %  d)
1011         f.write ('\n')
1012         f.close ()
1013         __main__.read_files = []
1014
1015 def identify():
1016         sys.stdout.write ('lilypond-book (GNU LilyPond) %s\n' % program_version)
1017
1018 def print_version ():
1019         identify()
1020         sys.stdout.write (r"""Copyright 1998--1999
1021 Distributed under terms of the GNU General Public License. It comes with
1022 NO WARRANTY.
1023 """)
1024
1025 def do_file(input_filename):
1026         file_settings = {}
1027         if outname:
1028                 my_outname = outname
1029         else:
1030                 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
1031         my_depname = my_outname + '.dep'                
1032
1033         chunks = read_doc_file(input_filename)
1034         chunks = chop_chunks(chunks, 'lilypond', make_lilypond, 1)
1035         chunks = chop_chunks(chunks, 'lilypond-file', make_lilypond_file, 1)
1036         chunks = chop_chunks(chunks, 'lilypond-block', make_lilypond_block, 1)
1037         chunks = chop_chunks(chunks, 'singleline-comment', do_ignore, 1)
1038         chunks = chop_chunks(chunks, 'preamble-end', do_preamble_end)
1039         chunks = chop_chunks(chunks, 'numcols', do_columns)
1040         #print "-" * 50
1041         #for c in chunks: print "c:", c;
1042         #sys.exit()
1043         scan_preamble(chunks)
1044         chunks = process_lilypond_blocks(my_outname, chunks)
1045         # Do It.
1046         if __main__.g_run_lilypond:
1047                 compile_all_files (chunks)
1048                 newchunks = []
1049                 # finishing touch.
1050                 for c in chunks:
1051                         if c[0] == 'lilypond' and 'eps' in c[2]:
1052                                 body = re.sub (r"""\\lilypondepswidth{(.*?)}""", find_eps_dims, c[1])
1053                                 newchunks.append (('lilypond', body))
1054                         else:
1055                                 newchunks.append (c)
1056                 chunks = newchunks
1057         x = 0
1058         chunks = completize_preamble (chunks)
1059         foutn = os.path.join(g_outdir, my_outname + '.' + format)
1060         sys.stderr.write ("Writing `%s'\n" % foutn)
1061         fout = open (foutn, 'w')
1062         for c in chunks:
1063                 fout.write (c[1])
1064         fout.close ()
1065         # should chmod -w
1066
1067         if do_deps:
1068                 write_deps (my_depname, foutn)
1069
1070
1071 outname = ''
1072 try:
1073         (sh, long) = getopt_args (__main__.option_definitions)
1074         (options, files) = getopt.getopt(sys.argv[1:], sh, long)
1075 except getopt.error, msg:
1076         sys.stderr.write("error: %s" % msg)
1077         sys.exit(1)
1078
1079 do_deps = 0
1080 for opt in options:     
1081         o = opt[0]
1082         a = opt[1]
1083
1084         if o == '--include' or o == '-I':
1085                 include_path.append (a)
1086         elif o == '--version' or o == '-v':
1087                 print_version ()
1088                 sys.exit  (0)
1089         elif o == '--format' or o == '-f':
1090                 __main__.format = a
1091         elif o == '--outname' or o == '-o':
1092                 if len(files) > 1:
1093                         #HACK
1094                         sys.stderr.write("Lilypond-book is confused by --outname on multiple files")
1095                         sys.exit(1)
1096                 outname = a
1097         elif o == '--help' or o == '-h':
1098                 help ()
1099         elif o == '--no-lily' or o == '-n':
1100                 __main__.g_run_lilypond = 0
1101         elif o == '--dependencies' or o == '-M':
1102                 do_deps = 1
1103         elif o == '--default-music-fontsize':
1104                 default_music_fontsize = string.atoi (a)
1105         elif o == '--default-lilypond-fontsize':
1106                 print "--default-lilypond-fontsize is deprecated, use --default-music-fontsize"
1107                 default_music_fontsize = string.atoi (a)
1108         elif o == '--force-music-fontsize':
1109                 g_force_lilypond_fontsize = string.atoi(a)
1110         elif o == '--force-lilypond-fontsize':
1111                 print "--force-lilypond-fontsize is deprecated, use --default-lilypond-fontsize"
1112                 g_force_lilypond_fontsize = string.atoi(a)
1113         elif o == '--dep-prefix':
1114                 g_dep_prefix = a
1115         elif o == '--no-pictures':
1116                 g_do_pictures = 0
1117         elif o == '--read-lys':
1118                 g_read_lys = 1
1119         elif o == '--outdir':
1120                 g_outdir = a
1121
1122 identify()
1123 if g_outdir:
1124         if os.path.isfile(g_outdir):
1125                 error ("outdir is a file: %s" % g_outdir)
1126         if not os.path.exists(g_outdir):
1127                 os.mkdir(g_outdir)
1128 for input_filename in files:
1129         do_file(input_filename)
1130         
1131 #
1132 # Petr, ik zou willen dat ik iets zinvoller deed,
1133 # maar wat ik kan ik doen, het verandert toch niets?
1134 #   --hwn 20/aug/99