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