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