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