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