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