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