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