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