]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
release: 1.3.151
[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[dim]
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>
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  %%ughUGH not original options
566 \include "paper%d.ly"
567 \paper  { linewidth = %f \pt } 
568 """ % (optstring, music_size, l) + body
569         return body
570
571 def parse_options_string(s):
572         d = {}
573         r1 = re.compile("((\w+)={(.*?)})((,\s*)|$)")
574         r2 = re.compile("((\w+)=(.*?))((,\s*)|$)")
575         r3 = re.compile("(\w+?)((,\s*)|$)")
576         while s:
577                 m = r1.match(s)
578                 if m:
579                         s = s[m.end():]
580                         d[m.group(2)] = re.split(",\s*", m.group(3))
581                         continue
582                 m = r2.match(s)
583                 if m:
584                         s = s[m.end():]
585                         d[m.group(2)] = m.group(3)
586                         continue
587                 m = r3.match(s)
588                 if m:
589                         s = s[m.end():]
590                         d[m.group(1)] = 1
591                         continue
592                 
593                 error ("format of option string invalid (was `%')" % s)
594         return d
595
596 def scan_latex_preamble(chunks):
597         # first we want to scan the \documentclass line
598         # it should be the first non-comment line
599         idx = 0
600         while 1:
601                 if chunks[idx][0] == 'ignore':
602                         idx = idx + 1
603                         continue
604                 m = get_re ('header').match(chunks[idx][1])
605                 if m.group (1):
606                         options = re.split (',[\n \t]*', m.group(1)[1:-1])
607                 else:
608                         options = []
609                 for o in options:
610                         if o == 'landscape':
611                                 paperguru.m_landscape = 1
612                         m = re.match("(.*?)paper", o)
613                         if m:
614                                 paperguru.m_papersize = m.group()
615                         else:
616                                 m = re.match("(\d\d)pt", o)
617                                 if m:
618                                         paperguru.m_fontsize = int(m.group(1))
619                         
620                 break
621         while chunks[idx][0] != 'preamble-end':
622                 if chunks[idx] == 'ignore':
623                         idx = idx + 1
624                         continue
625                 m = get_re ('geometry').search(chunks[idx][1])
626                 if m:
627                         paperguru.m_use_geometry = 1
628                         o = parse_options_string(m.group('options'))
629                         for k in o.keys():
630                                 paperguru.set_geo_option(k, o[k])
631                 idx = idx + 1
632
633 def scan_texi_preamble (chunks):
634         # this is not bulletproof..., it checks the first 10 chunks
635         for c in chunks[:10]: 
636                 if c[0] == 'input':
637                         for s in ('afourpaper', 'afourwide', 'letterpaper',
638                                   'afourlatex', 'smallbook'):
639                                 if string.find(c[1], "@%s" % s) != -1:
640                                         paperguru.m_papersize = s
641
642 def scan_preamble (chunks):
643         if __main__.format == 'texi':
644                 scan_texi_preamble(chunks)
645         else:
646                 assert __main__.format == 'latex'
647                 scan_latex_preamble(chunks)
648                 
649
650 def completize_preamble (chunks):
651         if __main__.format == 'texi':
652                 return chunks
653         pre_b = post_b = graphics_b = None
654         for chunk in chunks:
655                 if chunk[0] == 'preamble-end':
656                         break
657                 if chunk[0] == 'input':
658                         m = get_re('def-pre-re').search(chunk[1])
659                         if m:
660                                 pre_b = 1
661                 if chunk[0] == 'input':
662                         m = get_re('def-post-re').search(chunk[1])
663                         if m:
664                                 post_b = 1
665                 if chunk[0] == 'input':
666                         m = get_re('usepackage-graphics').search(chunk[1])
667                         if m:
668                                 graphics_b = 1
669         x = 0
670         while chunks[x][0] != 'preamble-end':
671                 x = x + 1
672         if not pre_b:
673                 chunks.insert(x, ('input', get_output ('output-default-pre')))
674         if not post_b:
675                 chunks.insert(x, ('input', get_output ('output-default-post')))
676         if not graphics_b:
677                 chunks.insert(x, ('input', get_output ('usepackage-graphics')))
678         return chunks
679
680
681 read_files = []
682 def find_file (name):
683         """
684         Search the include path for NAME. If found, return the (CONTENTS, PATH) of the file.
685         """
686         
687         f = None
688         nm = ''
689         for a in include_path:
690                 try:
691                         nm = os.path.join (a, name)
692                         f = open (nm)
693                         __main__.read_files.append (nm)
694                         break
695                 except IOError:
696                         pass
697         if f:
698                 sys.stderr.write ("Reading `%s'\n" % nm)
699                 return (f.read (), nm)
700         else:
701                 error ("File not found `%s'\n" % name)
702                 return ('', '')
703
704 def do_ignore(match_object):
705         return [('ignore', match_object.group('code'))]
706 def do_preamble_end(match_object):
707         return [('preamble-end', match_object.group('code'))]
708
709 def make_verbatim(match_object):
710         return [('verbatim', match_object.group('code'))]
711
712 def make_verb(match_object):
713         return [('verb', match_object.group('code'))]
714
715 def do_include_file(m):
716         "m: MatchObject"
717         return [('input', get_output ('pagebreak'))] \
718              + read_doc_file(m.group('filename')) \
719              + [('input', get_output ('pagebreak'))] 
720
721 def do_input_file(m):
722         return read_doc_file(m.group('filename'))
723
724 def make_lilypond(m):
725         if m.group('options'):
726                 options = m.group('options')
727         else:
728                 options = ''
729         return [('input', get_output('output-lilypond-fragment') % 
730                         (options, m.group('code')))]
731
732 def make_lilypond_file(m):
733         """
734
735         Find @lilypondfile{bla.ly} occurences and substitute bla.ly
736         into a @lilypond .. @end lilypond block.
737         
738         """
739         
740         if m.group('options'):
741                 options = m.group('options')
742         else:
743                 options = ''
744         (content, nm) = find_file(m.group('filename'))
745         options = "filename=%s," % nm + options
746
747         return [('input', get_output('output-lilypond') %
748                         (options, content))]
749
750 def make_lilypond_block(m):
751         if m.group('options'):
752                 options = get_re('option-sep').split (m.group('options'))
753         else:
754             options = []
755         options = filter(lambda s: s != '', options)
756         return [('lilypond', m.group('code'), options)]
757
758 def do_columns(m):
759         if __main__.format != 'latex':
760                 return []
761         if m.group('num') == 'one':
762                 return [('numcols', m.group('code'), 1)]
763         if m.group('num') == 'two':
764                 return [('numcols', m.group('code'), 2)]
765         
766 def chop_chunks(chunks, re_name, func, use_match=0):
767     newchunks = []
768     for c in chunks:
769         if c[0] == 'input':
770             str = c[1]
771             while str:
772                 m = get_re (re_name).search (str)
773                 if m == None:
774                     newchunks.append (('input', str))
775                     str = ''
776                 else:
777                     if use_match:
778                         newchunks.append (('input', str[:m.start ('match')]))
779                     else:
780                         newchunks.append (('input', str[:m.start (0)]))
781                     #newchunks.extend(func(m))
782                     # python 1.5 compatible:
783                     newchunks = newchunks + func(m)
784                     str = str [m.end(0):]
785         else:
786             newchunks.append(c)
787     return newchunks
788
789 def determine_format (str):
790         if __main__.format == '':
791                 
792                 latex =  re.search ('\\\\document', str[:200])
793                 texinfo =  re.search ('@node|@setfilename', str[:200])
794
795                 f = ''
796                 g = None
797                 
798                 if texinfo and latex == None:
799                         f = 'texi'
800                 elif latex and texinfo == None: 
801                         f = 'latex'
802                 else:
803                         error("error: can't determine format, please specify")
804                 __main__.format = f
805
806         if __main__.paperguru == None:
807                 if __main__.format == 'texi':
808                         g = TexiPaper()
809                 else:
810                         g = LatexPaper()
811                         
812                 __main__.paperguru = g
813
814
815 def read_doc_file (filename):
816         """Read the input file, find verbatim chunks and do \input and \include
817         """
818         (str, path) = find_file(filename)
819         determine_format (str)
820         
821         chunks = [('input', str)]
822         
823         # we have to check for verbatim before doing include,
824         # because we don't want to include files that are mentioned
825         # inside a verbatim environment
826         chunks = chop_chunks(chunks, 'verbatim', make_verbatim)
827         chunks = chop_chunks(chunks, 'verb', make_verb)
828         chunks = chop_chunks(chunks, 'multiline-comment', do_ignore)
829         #ugh fix input
830         chunks = chop_chunks(chunks, 'include', do_include_file, 1)
831         chunks = chop_chunks(chunks, 'input', do_input_file, 1)
832         return chunks
833
834
835 taken_file_names = {}
836 def schedule_lilypond_block (chunk):
837         """Take the body and options from CHUNK, figure out how the
838         real .ly should look, and what should be left MAIN_STR (meant
839         for the main file).  The .ly is written, and scheduled in
840         TODO.
841
842         Return: a chunk (TYPE_STR, MAIN_STR, OPTIONS, TODO, BASE)
843
844         TODO has format [basename, extension, extension, ... ]
845         
846         """
847         (type, body, opts) = chunk
848         assert type == 'lilypond'
849         file_body = compose_full_body (body, opts)
850         basename = 'lily-' + `abs(hash (file_body))`
851         for o in opts:
852                 m = re.search ('filename="(.*?)"', o)
853                 if m:
854                         basename = m.group (1)
855                         if not taken_file_names.has_key(basename):
856                             taken_file_names[basename] = 0
857                         else:
858                             taken_file_names[basename] = taken_file_names[basename] + 1
859                             basename = basename + "-%i" % taken_file_names[basename]
860         if not g_read_lys:
861                 update_file(file_body, os.path.join(g_outdir, basename) + '.ly')
862         needed_filetypes = ['tex']
863
864         if format  == 'texi':
865                 needed_filetypes.append('eps')
866                 needed_filetypes.append('png')
867         if 'eps' in opts and not ('eps' in needed_filetypes):
868                 needed_filetypes.append('eps')
869         pathbase = os.path.join (g_outdir, basename)
870         def f(base, ext1, ext2):
871                 a = os.path.isfile(base + ext2)
872                 if (os.path.isfile(base + ext1) and
873                     os.path.isfile(base + ext2) and
874                                 os.stat(base+ext1)[stat.ST_MTIME] >
875                                 os.stat(base+ext2)[stat.ST_MTIME]) or \
876                                 not os.path.isfile(base + ext2):
877                         return 1
878         todo = []
879         if 'tex' in needed_filetypes and f(pathbase, '.ly', '.tex'):
880                 todo.append('tex')
881         if 'eps' in needed_filetypes and f(pathbase, '.tex', '.eps'):
882                 todo.append('eps')
883         if 'png' in needed_filetypes and f(pathbase, '.eps', '.png'):
884                 todo.append('png')
885         newbody = ''
886
887         if 'printfilename' in opts:
888                 for o in opts:
889                         m= re.match ("filename=(.*)", o)
890                         if m:
891                                 newbody = newbody + get_output ("output-filename") % m.group(1)
892                                 break
893                 
894         
895         if 'verbatim' in opts:
896                 newbody = output_verbatim (body)
897
898         for o in opts:
899                 m = re.search ('intertext="(.*?)"', o)
900                 if m:
901                         newbody = newbody  + m.group (1) + "\n\n"
902         if format == 'latex':
903                 if 'eps' in opts:
904                         s = 'output-eps'
905                 else:
906                         s = 'output-tex'
907         else: # format == 'texi'
908                 s = 'output-all'
909         newbody = newbody + get_output (s) % {'fn': basename }
910         return ('lilypond', newbody, opts, todo, basename)
911
912 def process_lilypond_blocks(outname, chunks):#ugh rename
913         newchunks = []
914         # Count sections/chapters.
915         for c in chunks:
916                 if c[0] == 'lilypond':
917                         c = schedule_lilypond_block (c)
918                 elif c[0] == 'numcols':
919                         paperguru.m_num_cols = c[2]
920                 newchunks.append (c)
921         return newchunks
922
923
924 def find_eps_dims (match):
925         "Fill in dimensions of EPS files."
926         
927         fn =match.group (1)
928         dims = bounding_box_dimensions (fn)
929         if g_outdir:
930                 fn = os.path.join(g_outdir, fn)
931         
932         return '%ipt' % dims[0]
933
934
935 def system (cmd):
936         sys.stderr.write ("invoking `%s'\n" % cmd)
937         st = os.system (cmd)
938         if st:
939                 error ('Error command exited with value %d\n' % st)
940         return st
941
942 def compile_all_files (chunks):
943         global foutn
944         eps = []
945         tex = []
946         png = []
947
948         for c in chunks:
949                 if c[0] <> 'lilypond':
950                         continue
951                 base  = c[4]
952                 exts = c[3]
953                 for e in exts:
954                         if e == 'eps':
955                                 eps.append (base)
956                         elif e == 'tex':
957                                 #ugh
958                                 if base + '.ly' not in tex:
959                                         tex.append (base + '.ly')
960                         elif e == 'png' and g_do_pictures:
961                                 png.append (base)
962         d = os.getcwd()
963         if g_outdir:
964                 os.chdir(g_outdir)
965         if tex:
966                 # fixme: be sys-independent.
967                 def incl_opt (x):
968                         if g_outdir and x[0] <> '/' :
969                                 x = os.path.join (g_here_dir, x)
970                         return ' -I %s' % x
971
972                 incs = map (incl_opt, include_path)
973                 lilyopts = string.join (incs, ' ' )
974                 if do_deps:
975                         lilyopts = lilyopts + ' --dependencies '
976                         if g_outdir:
977                                 lilyopts = lilyopts + '--dep-prefix=' + g_outdir + '/'
978                 texfiles = string.join (tex, ' ')
979                 system ('lilypond --header=texidoc %s %s' % (lilyopts, texfiles))
980
981                 #
982                 # Ugh, fixing up dependencies for .tex generation
983                 #
984                 if do_deps:
985                         depfiles=map (lambda x: re.sub ('(.*)\.ly', '\\1.dep', x), tex)
986                         for i in depfiles:
987                                 text=open (i).read ()
988                                 text=re.sub ('\n([^:\n]*):', '\n' + foutn + ':', text)
989                                 open (i, 'w').write (text)
990
991         for e in eps:
992                 system(r"tex '\nonstopmode \input %s'" % e)
993                 system(r"dvips -E -o %s %s" % (e + '.eps', e))
994         for g in png:
995                 cmd = r"""gs -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r90 -dNOPAUSE %s -c quit | pnmcrop | pnmtopng > %s"""
996                 cmd = cmd % (g + '.eps', g + '.png')
997                 try:
998                         status = system (cmd)
999                 except:
1000                         os.unlink (g + '.png')
1001                         error ("Removing output file")
1002                 
1003         os.chdir (d)
1004
1005
1006 def update_file (body, name):
1007         """
1008         write the body if it has changed
1009         """
1010         same = 0
1011         try:
1012                 f = open (name)
1013                 fs = f.read (-1)
1014                 same = (fs == body)
1015         except:
1016                 pass
1017
1018         if not same:
1019                 f = open (name , 'w')
1020                 f.write (body)
1021                 f.close ()
1022         
1023         return not same
1024
1025
1026 def getopt_args (opts):
1027         "Construct arguments (LONG, SHORT) for getopt from  list of options."
1028         short = ''
1029         long = []
1030         for o in opts:
1031                 if o[1]:
1032                         short = short + o[1]
1033                         if o[0]:
1034                                 short = short + ':'
1035                 if o[2]:
1036                         l = o[2]
1037                         if o[0]:
1038                                 l = l + '='
1039                         long.append (l)
1040         return (short, long)
1041
1042 def option_help_str (o):
1043         "Transform one option description (4-tuple ) into neatly formatted string"
1044         sh = '  '       
1045         if o[1]:
1046                 sh = '-%s' % o[1]
1047
1048         sep = ' '
1049         if o[1] and o[2]:
1050                 sep = ','
1051                 
1052         long = ''
1053         if o[2]:
1054                 long= '--%s' % o[2]
1055
1056         arg = ''
1057         if o[0]:
1058                 if o[2]:
1059                         arg = '='
1060                 arg = arg + o[0]
1061         return '  ' + sh + sep + long + arg
1062
1063
1064 def options_help_str (opts):
1065         "Convert a list of options into a neatly formatted string"
1066         w = 0
1067         strs =[]
1068         helps = []
1069
1070         for o in opts:
1071                 s = option_help_str (o)
1072                 strs.append ((s, o[3]))
1073                 if len (s) > w:
1074                         w = len (s)
1075
1076         str = ''
1077         for s in strs:
1078                 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
1079         return str
1080
1081 def help():
1082         sys.stdout.write("""Usage: lilypond-book [options] FILE\n
1083 Generate hybrid LaTeX input from Latex + lilypond
1084 Options:
1085 """)
1086         sys.stdout.write (options_help_str (option_definitions))
1087         sys.stdout.write (r"""Warning all output is written in the CURRENT directory
1088
1089
1090
1091 Report bugs to bug-gnu-music@gnu.org.
1092
1093 Written by Tom Cato Amundsen <tca@gnu.org> and
1094 Han-Wen Nienhuys <hanwen@cs.uu.nl>
1095 """)
1096
1097         sys.exit (0)
1098
1099
1100 def write_deps (fn, target, chunks):
1101         global read_files
1102         sys.stdout.write('Writing `%s\'\n' % os.path.join(g_outdir, fn))
1103         f = open (os.path.join(g_outdir, fn), 'w')
1104         f.write ('%s%s: ' % (g_dep_prefix, target))
1105         for d in read_files:
1106                 f.write ('%s ' %  d)
1107         basenames=[]
1108         for c in chunks:
1109                 if c[0] == 'lilypond':
1110                         (type, body, opts, todo, basename) = c;
1111                         basenames.append (basename)
1112         for d in basenames:
1113                 if g_outdir:
1114                         d=g_outdir + '/' + d
1115                 if g_dep_prefix:
1116                         #if not os.isfile (d): # thinko?
1117                         if not re.search ('/', d):
1118                                 d = g_dep_prefix + d
1119                 f.write ('%s.tex ' %  d)
1120         f.write ('\n')
1121         #if len (basenames):
1122         #       for d in basenames:
1123         #               f.write ('%s.ly ' %  d)
1124         #       f.write (' : %s' % target)
1125         f.write ('\n')
1126         f.close ()
1127         read_files = []
1128
1129 def identify():
1130         sys.stdout.write ('lilypond-book (GNU LilyPond) %s\n' % program_version)
1131
1132 def print_version ():
1133         identify()
1134         sys.stdout.write (r"""Copyright 1998--1999
1135 Distributed under terms of the GNU General Public License. It comes with
1136 NO WARRANTY.
1137 """)
1138
1139
1140 def check_texidoc (chunks):
1141         n = []
1142         for c in chunks:
1143                 if c[0] == 'lilypond':
1144                         (type, body, opts, todo, basename) = c;
1145                         pathbase = os.path.join (g_outdir, basename)
1146                         if os.path.isfile (pathbase + '.texidoc'):
1147                                 body = '\n@include %s.texidoc\n' % basename + body
1148                                 c = (type, body, opts, todo, basename)
1149                 n.append (c)
1150         return n
1151
1152 def fix_epswidth (chunks):
1153         newchunks = []
1154         for c in chunks:
1155                 if c[0] == 'lilypond' and 'eps' in c[2]:
1156                         body = re.sub (r"""\\lilypondepswidth{(.*?)}""", find_eps_dims, c[1])
1157                         newchunks.append(('lilypond', body, c[2], c[3], c[4]))
1158                 else:
1159                         newchunks.append (c)
1160         return newchunks
1161
1162
1163 foutn=""
1164 def do_file(input_filename):
1165         global foutn
1166         file_settings = {}
1167         if outname:
1168                 my_outname = outname
1169         else:
1170                 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
1171         my_depname = my_outname + '.dep'                
1172
1173         chunks = read_doc_file(input_filename)
1174         chunks = chop_chunks(chunks, 'lilypond', make_lilypond, 1)
1175         chunks = chop_chunks(chunks, 'lilypond-file', make_lilypond_file, 1)
1176         chunks = chop_chunks(chunks, 'lilypond-block', make_lilypond_block, 1)
1177         chunks = chop_chunks(chunks, 'singleline-comment', do_ignore, 1)
1178         chunks = chop_chunks(chunks, 'preamble-end', do_preamble_end)
1179         chunks = chop_chunks(chunks, 'numcols', do_columns)
1180         #print "-" * 50
1181         #for c in chunks: print "c:", c;
1182         #sys.exit()
1183         scan_preamble(chunks)
1184         chunks = process_lilypond_blocks(my_outname, chunks)
1185
1186         foutn = os.path.join (g_outdir, my_outname + '.' + format)
1187
1188         # Do It.
1189         if __main__.g_run_lilypond:
1190                 compile_all_files (chunks)
1191                 chunks = fix_epswidth (chunks)
1192
1193         if __main__.format == 'texi':
1194                 chunks = check_texidoc (chunks)
1195
1196         x = 0
1197         chunks = completize_preamble (chunks)
1198         sys.stderr.write ("Writing `%s'\n" % foutn)
1199         fout = open (foutn, 'w')
1200         for c in chunks:
1201                 fout.write (c[1])
1202         fout.close ()
1203         # should chmod -w
1204
1205         if do_deps:
1206                 write_deps (my_depname, foutn, chunks)
1207
1208
1209 outname = ''
1210 try:
1211         (sh, long) = getopt_args (__main__.option_definitions)
1212         (options, files) = getopt.getopt(sys.argv[1:], sh, long)
1213 except getopt.error, msg:
1214         sys.stderr.write("error: %s" % msg)
1215         sys.exit(1)
1216
1217 do_deps = 0
1218 for opt in options:     
1219         o = opt[0]
1220         a = opt[1]
1221
1222         if o == '--include' or o == '-I':
1223                 include_path.append (a)
1224         elif o == '--version' or o == '-v':
1225                 print_version ()
1226                 sys.exit  (0)
1227         elif o == '--format' or o == '-f':
1228                 __main__.format = a
1229         elif o == '--outname' or o == '-o':
1230                 if len(files) > 1:
1231                         #HACK
1232                         sys.stderr.write("Lilypond-book is confused by --outname on multiple files")
1233                         sys.exit(1)
1234                 outname = a
1235         elif o == '--help' or o == '-h':
1236                 help ()
1237         elif o == '--no-lily' or o == '-n':
1238                 __main__.g_run_lilypond = 0
1239         elif o == '--dependencies' or o == '-M':
1240                 do_deps = 1
1241         elif o == '--default-music-fontsize':
1242                 default_music_fontsize = string.atoi (a)
1243         elif o == '--default-lilypond-fontsize':
1244                 print "--default-lilypond-fontsize is deprecated, use --default-music-fontsize"
1245                 default_music_fontsize = string.atoi (a)
1246         elif o == '--force-music-fontsize':
1247                 g_force_lilypond_fontsize = string.atoi(a)
1248         elif o == '--force-lilypond-fontsize':
1249                 print "--force-lilypond-fontsize is deprecated, use --default-lilypond-fontsize"
1250                 g_force_lilypond_fontsize = string.atoi(a)
1251         elif o == '--dep-prefix':
1252                 g_dep_prefix = a
1253         elif o == '--no-pictures':
1254                 g_do_pictures = 0
1255         elif o == '--read-lys':
1256                 g_read_lys = 1
1257         elif o == '--outdir':
1258                 g_outdir = a
1259
1260 identify()
1261 if g_outdir:
1262         if os.path.isfile(g_outdir):
1263                 error ("outdir is a file: %s" % g_outdir)
1264         if not os.path.exists(g_outdir):
1265                 os.mkdir(g_outdir)
1266 setup_environment ()
1267 for input_filename in files:
1268         do_file(input_filename)
1269         
1270 #
1271 # Petr, ik zou willen dat ik iets zinvoller deed,
1272 # maar wat ik kan ik doen, het verandert toch niets?
1273 #   --hwn 20/aug/99