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