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