]> git.donarmstrong.com Git - lilypond.git/blob - scripts/lilypond-book.py
849aeab201a23f83ce1b44d3b590402a513b85ac
[lilypond.git] / scripts / lilypond-book.py
1 #!@PYTHON@
2 # vim: set noexpandtab:
3 # TODO:
4 # * Figure out clean set of options. Hmm, isn't it pretty ok now?
5 # * add support for .lilyrc
6 # * EndLilyPondOutput is def'd as vfil. Causes large white gaps.
7 # * texinfo: add support for @pagesize
8
9 # todo: dimension handling (all the x2y) is clumsy. (tca: Thats
10 #       because the values are taken directly from texinfo.tex,
11 #       geometry.sty and article.cls. Give me a hint, and I'll
12 #       fix it.)
13
14 # This is was the idea for handling of comments:
15 #       Multiline comments, @ignore .. @end ignore is scanned for
16 #       in read_doc_file, and the chunks are marked as 'ignore', so
17 #       lilypond-book will not touch them any more. The content of the
18 #       chunks are written to the output file. Also 'include' and 'input'
19 #       regex has to check if they are commented out.
20 #
21 #       Then it is scanned for 'lilypond', 'lilypond-file' and 'lilypond-block'.
22 #       These three regex's has to check if they are on a commented line,
23 #       % for latex, @c for texinfo.
24 #
25 #       Then lines that are commented out with % (latex) and @c (Texinfo)
26 #       are put into chunks marked 'ignore'. This cannot be done before
27 #       searching for the lilypond-blocks because % is also the comment character
28 #       for lilypond.
29 #
30 #       The the rest of the rexeces are searched for. They don't have to test
31 #       if they are on a commented out line.
32
33 import os
34 import stat
35 import string
36 import re
37 import getopt
38 import sys
39 import __main__
40 import operator
41
42
43 program_version = '@TOPLEVEL_VERSION@'
44 if program_version == '@' + 'TOPLEVEL_VERSION' + '@':
45         program_version = '1.3.113'
46
47 include_path = [os.getcwd()]
48
49
50 # g_ is for global (?)
51
52 g_here_dir = os.getcwd ()
53 g_dep_prefix = ''
54 g_outdir = ''
55 g_force_lilypond_fontsize = 0
56 g_read_lys = 0
57 g_do_pictures = 1
58 g_num_cols = 1
59 format = ''
60 g_run_lilypond = 1
61 no_match = 'a\ba'
62
63 default_music_fontsize = 16
64 default_text_fontsize = 12
65 paperguru = None
66
67 # this code is ugly. It should be cleaned
68 class LatexPaper:
69         def __init__(self):
70                 self.m_paperdef =  {
71                         # the dimensions are from geometry.sty
72                         'a0paper': (mm2pt(841), mm2pt(1189)),
73                         'a1paper': (mm2pt(595), mm2pt(841)),
74                         'a2paper': (mm2pt(420), mm2pt(595)),
75                         'a3paper': (mm2pt(297), mm2pt(420)),
76                         'a4paper': (mm2pt(210), mm2pt(297)),
77                         'a5paper': (mm2pt(149), mm2pt(210)),
78                         'b0paper': (mm2pt(1000), mm2pt(1414)),
79                         'b1paper': (mm2pt(707), mm2pt(1000)),
80                         'b2paper': (mm2pt(500), mm2pt(707)),
81                         'b3paper': (mm2pt(353), mm2pt(500)),
82                         'b4paper': (mm2pt(250), mm2pt(353)),
83                         'b5paper': (mm2pt(176), mm2pt(250)),
84                         'letterpaper': (in2pt(8.5), in2pt(11)),
85                         'legalpaper': (in2pt(8.5), in2pt(14)),
86                         'executivepaper': (in2pt(7.25), in2pt(10.5))}
87                 self.m_use_geometry = None
88                 self.m_papersize = 'letterpaper'
89                 self.m_fontsize = 10
90                 self.m_num_cols = 1
91                 self.m_landscape = 0
92                 self.m_geo_landscape = 0
93                 self.m_geo_width = None
94                 self.m_geo_textwidth = None
95                 self.m_geo_lmargin = None
96                 self.m_geo_rmargin = None
97                 self.m_geo_includemp = None
98                 self.m_geo_marginparwidth = {10: 57, 11: 50, 12: 35}
99                 self.m_geo_marginparsep = {10: 11, 11: 10, 12: 10}
100                 self.m_geo_x_marginparwidth = None
101                 self.m_geo_x_marginparsep = None
102                 self.__body = None
103         def set_geo_option(self, name, value):
104                 if name == 'body' or name == 'text':
105                         if type(value) == type(""):
106                                 self.m_geo_textwidth =  value
107                         else:
108                                 self.m_geo_textwidth =  value[0]
109                         self.__body = 1
110                 elif name == 'portrait':
111                         self.m_geo_landscape = 0
112                 elif name == 'reversemp' or name == 'reversemarginpar':
113                         if self.m_geo_includemp == None:
114                                 self.m_geo_includemp = 1
115                 elif name == 'marginparwidth' or name == 'marginpar':
116                         self.m_geo_x_marginparwidth =  value
117                         self.m_geo_includemp = 1
118                 elif name == 'marginparsep':
119                         self.m_geo_x_marginparsep =  value
120                         self.m_geo_includemp = 1
121                 elif name == 'scale':
122                         if type(value) == type(""):
123                                 self.m_geo_width = self.get_paperwidth() * float(value)
124                         else:
125                                 self.m_geo_width = self.get_paperwidth() * float(value[0])
126                 elif name == 'hscale':
127                         self.m_geo_width = self.get_paperwidth() * float(value)
128                 elif name == 'left' or name == 'lmargin':
129                         self.m_geo_lmargin =  value
130                 elif name == 'right' or name == 'rmargin':
131                         self.m_geo_rmargin =  value
132                 elif name == 'hdivide' or name == 'divide':
133                         if value[0] not in ('*', ''):
134                                 self.m_geo_lmargin =  value[0]
135                         if value[1] not in ('*', ''):
136                                 self.m_geo_width =  value[1]
137                         if value[2] not in ('*', ''):
138                                 self.m_geo_rmargin =  value[2]
139                 elif name == 'hmargin':
140                         if type(value) == type(""):
141                                 self.m_geo_lmargin =  value
142                                 self.m_geo_rmargin =  value
143                         else:
144                                 self.m_geo_lmargin =  value[0]
145                                 self.m_geo_rmargin =  value[1]
146                 elif name == 'margin':#ugh there is a bug about this option in
147                                         # the geometry documentation
148                         if type(value) == type(""):
149                                 self.m_geo_lmargin =  value
150                                 self.m_geo_rmargin =  value
151                         else:
152                                 self.m_geo_lmargin =  value[0]
153                                 self.m_geo_rmargin =  value[0]
154                 elif name == 'total':
155                         if type(value) == type(""):
156                                 self.m_geo_width =  value
157                         else:
158                                 self.m_geo_width =  value[0]
159                 elif name == 'width' or name == 'totalwidth':
160                         self.m_geo_width =  value
161                 elif name == 'paper' or name == 'papername':
162                         self.m_papersize = value
163                 elif name[-5:] == 'paper':
164                         self.m_papersize = name
165                 else:
166                         self._set_dimen('m_geo_'+name, value)
167         def __setattr__(self, name, value):
168                 if type(value) == type("") and \
169                    dimension_conversion_dict.has_key (value[-2:]):
170                         f = dimension_conversion_dict[dim]
171                         self.__dict__[name] = f(float(value[:-2]))
172                 else:
173                         self.__dict__[name] = value
174                         
175         def __str__(self):
176                 s =  "LatexPaper:\n-----------"
177                 for v in self.__dict__.keys():
178                         if v[:2] == 'm_':
179                                 s = s +  str (v) + ' ' + str (self.__dict__[v])
180                 s = s +  "-----------"
181                 return s
182         
183         def get_linewidth(self):
184                 w = self._calc_linewidth()
185                 if self.m_num_cols == 2:
186                         return (w - 10) / 2
187                 else:
188                         return w
189         def get_paperwidth(self):
190                 #if self.m_use_geometry:
191                         return self.m_paperdef[self.m_papersize][self.m_landscape or self.m_geo_landscape]
192                 #return self.m_paperdef[self.m_papersize][self.m_landscape]
193         
194         def _calc_linewidth(self):
195                 # since geometry sometimes ignores 'includemp', this is
196                 # more complicated than it should be
197                 mp = 0
198                 if self.m_geo_includemp:
199                         if self.m_geo_x_marginparsep is not None:
200                                 mp = mp + self.m_geo_x_marginparsep
201                         else:
202                                 mp = mp + self.m_geo_marginparsep[self.m_fontsize]
203                         if self.m_geo_x_marginparwidth is not None:
204                                 mp = mp + self.m_geo_x_marginparwidth
205                         else:
206                                 mp = mp + self.m_geo_marginparwidth[self.m_fontsize]
207
208                 #ugh test if this is necessary                          
209                 if self.__body:
210                         mp = 0
211
212                 if not self.m_use_geometry:
213                         return latex_linewidths[self.m_papersize][self.m_fontsize]
214                 else:
215                         geo_opts = (a == None, b == None, c == None)
216                         
217                         if geo_opts == (1, 1, 1):
218                                 if self.m_geo_textwidth:
219                                         return self.m_geo_textwidth
220                                 w = self.get_paperwidth() * 0.8
221                                 return w - mp
222                         elif geo_opts == (0, 1, 1):
223                                  if self.m_geo_textwidth:
224                                         return self.m_geo_textwidth
225                                  return self.f1(self.m_geo_lmargin, mp)
226                         elif geo_opts == (1, 1, 0):
227                                  if self.m_geo_textwidth:
228                                         return self.m_geo_textwidth
229                                  return self.f1(self.m_geo_rmargin, mp)
230                         elif geo_opts \
231                                         in ((0, 0, 1), (1, 0, 0), (1, 0, 1)):
232                                 if self.m_geo_textwidth:
233                                         return self.m_geo_textwidth
234                                 return self.m_geo_width - mp
235                         elif geo_opts in ((0, 1, 0), (0, 0, 0)):
236                                 w = self.get_paperwidth() \
237                                   - self.m_geo_lmargin - self.m_geo_rmargin - mp
238                                 if w < 0:
239                                         w = 0
240                                 return w
241                         raise "Never do this!"
242         def f1(self, m, mp):
243                 tmp = self.get_paperwidth() - m * 2 - mp
244                 if tmp < 0:
245                         tmp = 0
246                 return tmp
247         def f2(self):
248                 tmp = self.get_paperwidth() - self.m_geo_lmargin \
249                         - self.m_geo_rmargin
250                 if tmp < 0:
251                         return 0
252                 return tmp
253
254 class TexiPaper:
255         def __init__(self):
256                 self.m_papersize = 'letterpaper'
257                 self.m_fontsize = 12
258         def get_linewidth(self):
259                 return texi_linewidths[self.m_papersize][self.m_fontsize]
260
261 def mm2pt(x):
262         return x * 2.8452756
263 def in2pt(x):
264         return x * 72.26999
265 def em2pt(x, fontsize = 10):
266         return {10: 10.00002, 11: 10.8448, 12: 11.74988}[fontsize] * x
267 def ex2pt(x, fontsize = 10):
268         return {10: 4.30554, 11: 4.7146, 12: 5.16667}[fontsize] * x
269
270 def pt2pt(x):
271         return x
272
273 dimension_conversion_dict ={
274         'mm': mm2pt,
275         'in': in2pt,
276         'em': em2pt,
277         'ex': ex2pt,
278         'pt': pt2pt
279         }
280
281         
282 # latex linewidths:
283 # indices are no. of columns, papersize,  fontsize
284 # Why can't this be calculated?
285 latex_linewidths = {
286         'a4paper':{10: 345, 11: 360, 12: 390},
287         'a4paper-landscape': {10: 598, 11: 596, 12:592},
288         'a5paper':{10: 276, 11: 276, 12: 276},
289         'b5paper':{10: 345, 11: 356, 12: 356},
290         'letterpaper':{10: 345, 11: 360, 12: 390},
291         'letterpaper-landscape':{10: 598, 11: 596, 12:596},
292         'legalpaper': {10: 345, 11: 360, 12: 390},
293         'executivepaper':{10: 345, 11: 360, 12: 379}}
294
295 texi_linewidths = {
296         'afourpaper': {12: mm2pt(160)},
297         'afourwide': {12: in2pt(6.5)},
298         'afourlatex': {12: mm2pt(150)},
299         'smallbook': {12: in2pt(5)},
300         'letterpaper': {12: in2pt(6)}}
301
302 option_definitions = [
303   ('EXT', 'f', 'format', 'set format.  EXT is one of texi and latex.'),
304   ('DIM',  '', 'default-music-fontsize', 'default fontsize for music.  DIM is assumed to be in points'),
305   ('DIM',  '', 'default-lilypond-fontsize', 'deprecated, use --default-music-fontsize'),
306   ('DIM', '', 'force-music-fontsize', 'force fontsize for all inline lilypond. DIM is assumed be to in points'),
307   ('DIM', '', 'force-lilypond-fontsize', 'deprecated, use --force-music-fontsize'),
308   ('DIR', 'I', 'include', 'include path'),
309   ('', 'M', 'dependencies', 'write dependencies'),
310   ('PREF', '',  'dep-prefix', 'prepend PREF before each -M dependency'),
311   ('', 'n', 'no-lily', 'don\'t run lilypond'),
312   ('', '', 'no-pictures', "don\'t generate pictures"),
313   ('', '', 'read-lys', "don't write ly files."),
314   ('FILE', 'o', 'outname', 'filename main output file'),
315   ('FILE', '', 'outdir', "where to place generated files"),
316   ('', 'v', 'version', 'print version information' ),
317   ('', 'h', 'help', 'print help'),
318   ]
319
320 # format specific strings, ie. regex-es for input, and % strings for output
321 output_dict= {
322         'latex': {
323                 'output-lilypond-fragment' : r"""\begin[eps,singleline,%s]{lilypond}
324   \context Staff <
325     \context Voice{
326       %s
327     }
328   >
329 \end{lilypond}""",
330                 'output-filename' : r'''
331
332 \verb+%s+:''',
333                 'output-lilypond': r"""\begin[%s]{lilypond}
334 %s
335 \end{lilypond}""",
336                 'output-verbatim': "\\begin{verbatim}%s\\end{verbatim}",
337                 'output-default-post': "\\def\postLilypondExample{}\n",
338                 'output-default-pre': "\\def\preLilypondExample{}\n",
339                 'usepackage-graphics': '\\usepackage{graphics}\n',
340                 'output-eps': '\\noindent\\parbox{\\lilypondepswidth{%(fn)s.eps}}{\includegraphics{%(fn)s.eps}}',
341                 'output-tex': '\\preLilypondExample \\input %(fn)s.tex \\postLilypondExample\n',
342                 'pagebreak': r'\pagebreak',
343                 },
344         'texi' : {'output-lilypond': """@lilypond[%s]
345 %s
346 @end lilypond 
347 """,
348                 'output-filename' : r'''
349
350 @file{%s}:''',    
351                   'output-lilypond-fragment': """@lilypond[%s]
352 \context Staff\context Voice{ %s }
353 @end lilypond """,
354                   'pagebreak': None,
355                   'output-verbatim': r"""@example
356 %s
357 @end example
358 """,
359
360 # do some tweaking: @ is needed in some ps stuff.
361 # override EndLilyPondOutput, since @tex is done
362 # in a sandbox, you can't do \input lilyponddefs at the
363 # top of the document.
364
365 # should also support fragment in
366                   
367                   'output-all': r"""
368 @tex
369 \catcode`\@=12
370 \input lilyponddefs
371 \def\EndLilyPondOutput{}
372 \input %(fn)s.tex
373 \catcode`\@=0
374 @end tex
375 @html
376 <p>
377 <img src=%(fn)s.png>
378 @end html
379 """,
380                 }
381         }
382
383 def output_verbatim (body):
384         if __main__.format == 'texi':
385                 body = re.sub ('([@{}])', '@\\1', body)
386         return get_output ('output-verbatim') % body
387
388
389 re_dict = {
390         'latex': {'input': r'(?m)^[^%\n]*?(?P<match>\\mbinput{?([^}\t \n}]*))',
391                   'include': r'(?m)^[^%\n]*?(?P<match>\\mbinclude{(?P<filename>[^}]+)})',
392                   'option-sep' : ', *',
393                   'header': r"\\documentclass\s*(\[.*?\])?",
394                   'geometry': r"^(?m)[^%\n]*?\\usepackage\s*(\[(?P<options>.*)\])?\s*{geometry}",
395                   'preamble-end': r'(?P<code>\\begin{document})',
396                   'verbatim': r"(?s)(?P<code>\\begin{verbatim}.*?\\end{verbatim})",
397                   'verb': r"(?P<code>\\verb(?P<del>.).*?(?P=del))",
398                   'lilypond-file': r'(?m)^[^%\n]*?(?P<match>\\lilypondfile(\[(?P<options>.*?)\])?\{(?P<filename>.+)})',
399                   'lilypond' : r'(?m)^[^%\n]*?(?P<match>\\lilypond(\[(?P<options>.*?)\])?{(?P<code>.*?)})',
400                   'lilypond-block': r"(?sm)^[^%\n]*?(?P<match>\\begin(\[(?P<options>.*?)\])?{lilypond}(?P<code>.*?)\\end{lilypond})",
401                   'def-post-re': r"\\def\\postLilypondExample",
402                   'def-pre-re': r"\\def\\preLilypondExample",             
403                   'usepackage-graphics': r"\usepackage{graphics}",
404                   'intertext': r',?\s*intertext=\".*?\"',
405                   'multiline-comment': no_match,
406                   'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>^%.*$\n+))",
407                   'numcols': r"(?P<code>\\(?P<num>one|two)column)",
408                   },
409
410
411         # why do we have distinction between @mbinclude and @include? 
412         'texi': {
413                  'include':  '(?m)^[^%\n]*?(?P<match>@mbinclude[ \n\t]+(?P<filename>[^\t \n]*))',
414                  'input': no_match,
415                  'header': no_match,
416                  'preamble-end': no_match,
417                  'landscape': no_match,
418                  'verbatim': r"""(?s)(?P<code>@example\s.*?@end example\s)""",
419                  'verb': r"""(?P<code>@code{.*?})""",
420                  'lilypond-file': '(?m)^(?!@c)(?P<match>@lilypondfile(\[(?P<options>.*?)\])?{(?P<filename>[^}]+)})',
421                  'lilypond' : '(?m)^(?!@c)(?P<match>@lilypond(\[(?P<options>.*?)\])?{(?P<code>.*?)})',
422                  'lilypond-block': r"""(?m)^(?!@c)(?P<match>(?s)(?P<match>@lilypond(\[(?P<options>.*?)\])?\s(?P<code>.*?)@end lilypond\s))""",
423                   'option-sep' : ', *',
424                   'intertext': r',?\s*intertext=\".*?\"',
425                   'multiline-comment': r"(?sm)^\s*(?!@c\s+)(?P<code>@ignore\s.*?@end ignore)\s",
426                   'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>@c.*$\n+))",
427                   'numcols': no_match,
428                  }
429         }
430
431
432 for r in re_dict.keys ():
433         olddict = re_dict[r]
434         newdict = {}
435         for k in olddict.keys ():
436                 newdict[k] = re.compile (olddict[k])
437         re_dict[r] = newdict
438
439         
440 def uniq (list):
441         list.sort ()
442         s = list
443         list = []
444         for x in s:
445                 if x not in list:
446                         list.append (x)
447         return list
448                 
449
450 def get_output (name):
451         return  output_dict[format][name]
452
453 def get_re (name):
454         return  re_dict[format][name]
455
456 def bounding_box_dimensions(fname):
457         if g_outdir:
458                 fname = os.path.join(g_outdir, fname)
459         try:
460                 fd = open(fname)
461         except IOError:
462                 error ("Error opening `%s'" % fname)
463         str = fd.read ()
464         s = re.search('%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)', str)
465         if s:
466                 return (int(s.group(3))-int(s.group(1)), 
467                         int(s.group(4))-int(s.group(2)))
468         else:
469                 return (0,0)
470
471
472 def error (str):
473         sys.stderr.write (str + "\n  Exiting ... \n\n")
474         raise 'Exiting.'
475
476
477 def compose_full_body (body, opts):
478         """Construct the lilypond code to send to Lilypond.
479         Add stuff to BODY using OPTS as options."""
480         music_size = default_music_fontsize
481         latex_size = default_text_fontsize
482         for o in opts:
483                 if g_force_lilypond_fontsize:
484                         music_size = g_force_lilypond_fontsize
485                 else:
486                         m = re.match ('([0-9]+)pt', o)
487                         if m:
488                                 music_size = string.atoi(m.group (1))
489
490                 m = re.match ('latexfontsize=([0-9]+)pt', o)
491                 if m:
492                         latex_size = string.atoi (m.group (1))
493
494         if re.search ('\\\\score', body):
495                 is_fragment = 0
496         else:
497                 is_fragment = 1
498         if 'fragment' in opts:
499                 is_fragment = 1
500         if 'nonfragment' in opts:
501                 is_fragment = 0
502
503         if is_fragment and not 'multiline' in opts:
504                 opts.append('singleline')
505         if 'singleline' in opts:
506                 l = -1.0;
507         else:
508                 l = __main__.paperguru.get_linewidth()
509         
510         if 'relative' in opts:#ugh only when is_fragment
511                 body = '\\relative c { %s }' % body
512         
513         if is_fragment:
514                 body = r"""\score { 
515  \notes { %s }
516   \paper { }  
517 }""" % body
518
519         opts = uniq (opts)
520         optstring = string.join (opts, ' ')
521         optstring = re.sub ('\n', ' ', optstring)
522         body = r"""
523 %% Generated automatically by: lilypond-book.py
524 %% options are %s  %%ughUGH not original options
525 \include "paper%d.ly"
526 \paper  { linewidth = %f \pt; } 
527 """ % (optstring, music_size, l) + body
528         return body
529
530 def parse_options_string(s):
531         d = {}
532         r1 = re.compile("((\w+)={(.*?)})((,\s*)|$)")
533         r2 = re.compile("((\w+)=(.*?))((,\s*)|$)")
534         r3 = re.compile("(\w+?)((,\s*)|$)")
535         while s:
536                 m = r1.match(s)
537                 if m:
538                         s = s[m.end():]
539                         d[m.group(2)] = re.split(",\s*", m.group(3))
540                         continue
541                 m = r2.match(s)
542                 if m:
543                         s = s[m.end():]
544                         d[m.group(2)] = m.group(3)
545                         continue
546                 m = r3.match(s)
547                 if m:
548                         s = s[m.end():]
549                         d[m.group(1)] = 1
550                         continue
551                 
552                 error ("format of option string invalid (was `%')" % s)
553         return d
554
555 def scan_latex_preamble(chunks):
556         # first we want to scan the \documentclass line
557         # it should be the first non-comment line
558         idx = 0
559         while 1:
560                 if chunks[idx][0] == 'ignore':
561                         idx = idx + 1
562                         continue
563                 m = get_re ('header').match(chunks[idx][1])
564                 options = re.split (',[\n \t]*', m.group(1)[1:-1])
565                 for o in options:
566                         if o == 'landscape':
567                                 paperguru.m_landscape = 1
568                         m = re.match("(.*?)paper", o)
569                         if m:
570                                 paperguru.m_papersize = m.group()
571                         else:
572                                 m = re.match("(\d\d)pt", o)
573                                 if m:
574                                         paperguru.m_fontsize = int(m.group(1))
575                         
576                 break
577         while chunks[idx][0] != 'preamble-end':
578                 if chunks[idx] == 'ignore':
579                         idx = idx + 1
580                         continue
581                 m = get_re ('geometry').search(chunks[idx][1])
582                 if m:
583                         paperguru.m_use_geometry = 1
584                         o = parse_options_string(m.group('options'))
585                         for k in o.keys():
586                                 paperguru.set_geo_option(k, o[k])
587                 idx = idx + 1
588
589 def scan_texi_preamble (chunks):
590         # this is not bulletproof..., it checks the first 10 chunks
591         for c in chunks[:10]: 
592                 if c[0] == 'input':
593                         for s in ('afourpaper', 'afourwide', 'letterpaper',
594                                   'afourlatex', 'smallbook'):
595                                 if string.find(c[1], "@%s" % s) != -1:
596                                         paperguru.m_papersize = s
597
598 def scan_preamble (chunks):
599         if __main__.format == 'texi':
600                 scan_texi_preamble(chunks)
601         else:
602                 assert __main__.format == 'latex'
603                 scan_latex_preamble(chunks)
604                 
605
606 def completize_preamble (chunks):
607         if __main__.format == 'texi':
608                 return chunks
609         pre_b = post_b = graphics_b = None
610         for chunk in chunks:
611                 if chunk[0] == 'preamble-end':
612                         break
613                 if chunk[0] == 'input':
614                         m = get_re('def-pre-re').search(chunk[1])
615                         if m:
616                                 pre_b = 1
617                 if chunk[0] == 'input':
618                         m = get_re('def-post-re').search(chunk[1])
619                         if m:
620                                 post_b = 1
621                 if chunk[0] == 'input':
622                         m = get_re('usepackage-graphics').search(chunk[1])
623                         if m:
624                                 graphics_b = 1
625         x = 0
626         while chunks[x][0] != 'preamble-end':
627                 x = x + 1
628         if not pre_b:
629                 chunks.insert(x, ('input', get_output ('output-default-pre')))
630         if not post_b:
631                 chunks.insert(x, ('input', get_output ('output-default-post')))
632         if not graphics_b:
633                 chunks.insert(x, ('input', get_output ('usepackage-graphics')))
634         return chunks
635
636
637 read_files = []
638 def find_file (name):
639         """
640         Search the include path for NAME. If found, return the (CONTENTS, PATH) of the file.
641         """
642         
643         f = None
644         nm = ''
645         for a in include_path:
646                 try:
647                         nm = os.path.join (a, name)
648                         f = open (nm)
649                         __main__.read_files.append (nm)
650                         break
651                 except IOError:
652                         pass
653         if f:
654                 sys.stderr.write ("Reading `%s'\n" % nm)
655                 return (f.read (), nm)
656         else:
657                 error ("File not found `%s'\n" % name)
658                 return ('', '')
659
660 def do_ignore(match_object):
661         return [('ignore', match_object.group('code'))]
662 def do_preamble_end(match_object):
663         return [('preamble-end', match_object.group('code'))]
664
665 def make_verbatim(match_object):
666         return [('verbatim', match_object.group('code'))]
667
668 def make_verb(match_object):
669         return [('verb', match_object.group('code'))]
670
671 def do_include_file(m):
672         "m: MatchObject"
673         return [('input', get_output ('pagebreak'))] \
674              + read_doc_file(m.group('filename')) \
675              + [('input', get_output ('pagebreak'))] 
676
677 def do_input_file(m):
678         return read_doc_file(m.group('filename'))
679
680 def make_lilypond(m):
681         if m.group('options'):
682                 options = m.group('options')
683         else:
684                 options = ''
685         return [('input', get_output('output-lilypond-fragment') % 
686                         (options, m.group('code')))]
687
688 def make_lilypond_file(m):
689         """
690
691         Find @lilypondfile{bla.ly} occurences and substitute bla.ly
692         into a @lilypond .. @end lilypond block.
693         
694         """
695         
696         if m.group('options'):
697                 options = m.group('options')
698         else:
699                 options = ''
700         (content, nm) = find_file(m.group('filename'))
701         options = "filename=%s," % nm + options
702
703         return [('input', get_output('output-lilypond') %
704                         (options, content))]
705
706 def make_lilypond_block(m):
707         if m.group('options'):
708                 options = get_re('option-sep').split (m.group('options'))
709         else:
710             options = []
711         options = filter(lambda s: s != '', options)
712         return [('lilypond', m.group('code'), options)]
713
714 def do_columns(m):
715         if __main__.format != 'latex':
716                 return []
717         if m.group('num') == 'one':
718                 return [('numcols', m.group('code'), 1)]
719         if m.group('num') == 'two':
720                 return [('numcols', m.group('code'), 2)]
721         
722 def chop_chunks(chunks, re_name, func, use_match=0):
723     newchunks = []
724     for c in chunks:
725         if c[0] == 'input':
726             str = c[1]
727             while str:
728                 m = get_re (re_name).search (str)
729                 if m == None:
730                     newchunks.append (('input', str))
731                     str = ''
732                 else:
733                     if use_match:
734                         newchunks.append (('input', str[:m.start ('match')]))
735                     else:
736                         newchunks.append (('input', str[:m.start (0)]))
737                     #newchunks.extend(func(m))
738                     # python 1.5 compatible:
739                     newchunks = newchunks + func(m)
740                     str = str [m.end(0):]
741         else:
742             newchunks.append(c)
743     return newchunks
744
745 def determine_format (str):
746         if __main__.format == '':
747                 
748                 latex =  re.search ('\\\\document', str[:200])
749                 texinfo =  re.search ('@node|@setfilename', str[:200])
750
751                 f = ''
752                 g = None
753                 
754                 if texinfo and latex == None:
755                         f = 'texi'
756                 elif latex and texinfo == None: 
757                         f = 'latex'
758                 else:
759                         error("error: can't determine format, please specify")
760                 __main__.format = f
761
762         if __main__.paperguru == None:
763                 if __main__.format == 'texi':
764                         g = TexiPaper()
765                 else:
766                         g = LatexPaper()
767                         
768                 __main__.paperguru = g
769
770
771 def read_doc_file (filename):
772         """Read the input file, find verbatim chunks and do \input and \include
773         """
774         (str, path) = find_file(filename)
775         determine_format (str)
776         
777         chunks = [('input', str)]
778         
779         # we have to check for verbatim before doing include,
780         # because we don't want to include files that are mentioned
781         # inside a verbatim environment
782         chunks = chop_chunks(chunks, 'verbatim', make_verbatim)
783         chunks = chop_chunks(chunks, 'verb', make_verb)
784         chunks = chop_chunks(chunks, 'multiline-comment', do_ignore)
785         #ugh fix input
786         chunks = chop_chunks(chunks, 'include', do_include_file, 1)
787         chunks = chop_chunks(chunks, 'input', do_input_file, 1)
788         return chunks
789
790
791 taken_file_names = {}
792 def schedule_lilypond_block (chunk):
793         """Take the body and options from CHUNK, figure out how the
794         real .ly should look, and what should be left MAIN_STR (meant
795         for the main file).  The .ly is written, and scheduled in
796         TODO.
797
798         Return: a chunk (TYPE_STR, MAIN_STR, OPTIONS, TODO, BASE)
799
800         TODO has format [basename, extension, extension, ... ]
801         
802         """
803         (type, body, opts) = chunk
804         assert type == 'lilypond'
805         file_body = compose_full_body (body, opts)
806         basename = 'lily-' + `abs(hash (file_body))`
807         for o in opts:
808                 m = re.search ('filename="(.*?)"', o)
809                 if m:
810                         basename = m.group (1)
811                         if not taken_file_names.has_key(basename):
812                             taken_file_names[basename] = 0
813                         else:
814                             taken_file_names[basename] = taken_file_names[basename] + 1
815                             basename = basename + "-%i" % taken_file_names[basename]
816         if not g_read_lys:
817                 update_file(file_body, os.path.join(g_outdir, basename) + '.ly')
818         needed_filetypes = ['tex']
819
820         if format  == 'texi':
821                 needed_filetypes.append('eps')
822                 needed_filetypes.append('png')
823         if 'eps' in opts and not ('eps' in needed_filetypes):
824                 needed_filetypes.append('eps')
825         pathbase = os.path.join (g_outdir, basename)
826         def f(base, ext1, ext2):
827                 a = os.path.isfile(base + ext2)
828                 if (os.path.isfile(base + ext1) and
829                     os.path.isfile(base + ext2) and
830                                 os.stat(base+ext1)[stat.ST_MTIME] >
831                                 os.stat(base+ext2)[stat.ST_MTIME]) or \
832                                 not os.path.isfile(base + ext2):
833                         return 1
834         todo = []
835         if 'tex' in needed_filetypes and f(pathbase, '.ly', '.tex'):
836                 todo.append('tex')
837         if 'eps' in needed_filetypes and f(pathbase, '.tex', '.eps'):
838                 todo.append('eps')
839         if 'png' in needed_filetypes and f(pathbase, '.eps', '.png'):
840                 todo.append('png')
841         newbody = ''
842
843         if 'printfilename' in opts:
844                 for o in opts:
845                         m= re.match ("filename=(.*)", o)
846                         if m:
847                                 newbody = newbody + get_output ("output-filename") % m.group(1)
848                                 break
849                 
850         
851         if 'verbatim' in opts:
852                 newbody = output_verbatim (body)
853
854         for o in opts:
855                 m = re.search ('intertext="(.*?)"', o)
856                 if m:
857                         newbody = newbody  + m.group (1) + "\n\n"
858         if format == 'latex':
859                 if 'eps' in opts:
860                         s = 'output-eps'
861                 else:
862                         s = 'output-tex'
863         else: # format == 'texi'
864                 s = 'output-all'
865         newbody = newbody + get_output (s) % {'fn': basename }
866         return ('lilypond', newbody, opts, todo, basename)
867
868 def process_lilypond_blocks(outname, chunks):#ugh rename
869         newchunks = []
870         # Count sections/chapters.
871         for c in chunks:
872                 if c[0] == 'lilypond':
873                         c = schedule_lilypond_block (c)
874                 elif c[0] == 'numcols':
875                         paperguru.m_num_cols = c[2]
876                 newchunks.append (c)
877         return newchunks
878
879
880 def find_eps_dims (match):
881         "Fill in dimensions of EPS files."
882         
883         fn =match.group (1)
884         dims = bounding_box_dimensions (fn)
885         if g_outdir:
886                 fn = os.path.join(g_outdir, fn)
887         
888         return '%ipt' % dims[0]
889
890
891 def system (cmd):
892         sys.stderr.write ("invoking `%s'\n" % cmd)
893         st = os.system (cmd)
894         if st:
895                 error ('Error command exited with value %d\n' % st)
896         return st
897
898 def compile_all_files (chunks):
899         global foutn
900         eps = []
901         tex = []
902         png = []
903
904         for c in chunks:
905                 if c[0] <> 'lilypond':
906                         continue
907                 base  = c[4]
908                 exts = c[3]
909                 for e in exts:
910                         if e == 'eps':
911                                 eps.append (base)
912                         elif e == 'tex':
913                                 #ugh
914                                 if base + '.ly' not in tex:
915                                         tex.append (base + '.ly')
916                         elif e == 'png' and g_do_pictures:
917                                 png.append (base)
918         d = os.getcwd()
919         if g_outdir:
920                 os.chdir(g_outdir)
921         if tex:
922                 # fixme: be sys-independent.
923                 def incl_opt (x):
924                         if g_outdir and x[0] <> '/' :
925                                 x = os.path.join (g_here_dir, x)
926                         return ' -I %s' % x
927
928                 incs = map (incl_opt, include_path)
929                 lilyopts = string.join (incs, ' ' )
930                 if do_deps:
931                         lilyopts = lilyopts + ' --dependencies '
932                         if g_outdir:
933                                 lilyopts = lilyopts + '--dep-prefix=' + g_outdir + '/'
934                 texfiles = string.join (tex, ' ')
935                 system ('lilypond --header=texidoc %s %s' % (lilyopts, texfiles))
936
937                 #
938                 # Ugh, fixing up dependencies for .tex generation
939                 #
940                 if do_deps:
941                         depfiles=map (lambda x: re.sub ('(.*)\.ly', '\\1.dep', x), tex)
942                         for i in depfiles:
943                                 text=open (i).read ()
944                                 text=re.sub ('\n([^:\n]*):', '\n' + foutn + ':', text)
945                                 open (i, 'w').write (text)
946
947         for e in eps:
948                 system(r"tex '\nonstopmode \input %s'" % e)
949                 system(r"dvips -E -o %s %s" % (e + '.eps', e))
950         for g in png:
951                 cmd = r"""gs -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r90 -dNOPAUSE %s -c quit | pnmcrop | pnmtopng > %s"""
952                 cmd = cmd % (g + '.eps', g + '.png')
953                 system (cmd)
954         os.chdir (d)
955
956
957 def update_file (body, name):
958         """
959         write the body if it has changed
960         """
961         same = 0
962         try:
963                 f = open (name)
964                 fs = f.read (-1)
965                 same = (fs == body)
966         except:
967                 pass
968
969         if not same:
970                 f = open (name , 'w')
971                 f.write (body)
972                 f.close ()
973         
974         return not same
975
976
977 def getopt_args (opts):
978         "Construct arguments (LONG, SHORT) for getopt from  list of options."
979         short = ''
980         long = []
981         for o in opts:
982                 if o[1]:
983                         short = short + o[1]
984                         if o[0]:
985                                 short = short + ':'
986                 if o[2]:
987                         l = o[2]
988                         if o[0]:
989                                 l = l + '='
990                         long.append (l)
991         return (short, long)
992
993 def option_help_str (o):
994         "Transform one option description (4-tuple ) into neatly formatted string"
995         sh = '  '       
996         if o[1]:
997                 sh = '-%s' % o[1]
998
999         sep = ' '
1000         if o[1] and o[2]:
1001                 sep = ','
1002                 
1003         long = ''
1004         if o[2]:
1005                 long= '--%s' % o[2]
1006
1007         arg = ''
1008         if o[0]:
1009                 if o[2]:
1010                         arg = '='
1011                 arg = arg + o[0]
1012         return '  ' + sh + sep + long + arg
1013
1014
1015 def options_help_str (opts):
1016         "Convert a list of options into a neatly formatted string"
1017         w = 0
1018         strs =[]
1019         helps = []
1020
1021         for o in opts:
1022                 s = option_help_str (o)
1023                 strs.append ((s, o[3]))
1024                 if len (s) > w:
1025                         w = len (s)
1026
1027         str = ''
1028         for s in strs:
1029                 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
1030         return str
1031
1032 def help():
1033         sys.stdout.write("""Usage: lilypond-book [options] FILE\n
1034 Generate hybrid LaTeX input from Latex + lilypond
1035 Options:
1036 """)
1037         sys.stdout.write (options_help_str (option_definitions))
1038         sys.stdout.write (r"""Warning all output is written in the CURRENT directory
1039
1040
1041
1042 Report bugs to bug-gnu-music@gnu.org.
1043
1044 Written by Tom Cato Amundsen <tca@gnu.org> and
1045 Han-Wen Nienhuys <hanwen@cs.uu.nl>
1046 """)
1047
1048         sys.exit (0)
1049
1050
1051 def write_deps (fn, target, chunks):
1052         global read_files
1053         sys.stdout.write('Writing `%s\'\n' % os.path.join(g_outdir, fn))
1054         f = open (os.path.join(g_outdir, fn), 'w')
1055         f.write ('%s%s: ' % (g_dep_prefix, target))
1056         for d in read_files:
1057                 f.write ('%s ' %  d)
1058         basenames=[]
1059         for c in chunks:
1060                 if c[0] == 'lilypond':
1061                         (type, body, opts, todo, basename) = c;
1062                         basenames.append (basename)
1063         for d in basenames:
1064                 if g_outdir:
1065                         d=g_outdir + '/' + d
1066                 if g_dep_prefix:
1067                         #if not os.isfile (d): # thinko?
1068                         if not re.search ('/', d):
1069                                 d = g_dep_prefix + d
1070                 f.write ('%s.tex ' %  d)
1071         f.write ('\n')
1072         #if len (basenames):
1073         #       for d in basenames:
1074         #               f.write ('%s.ly ' %  d)
1075         #       f.write (' : %s' % target)
1076         f.write ('\n')
1077         f.close ()
1078         read_files = []
1079
1080 def identify():
1081         sys.stdout.write ('lilypond-book (GNU LilyPond) %s\n' % program_version)
1082
1083 def print_version ():
1084         identify()
1085         sys.stdout.write (r"""Copyright 1998--1999
1086 Distributed under terms of the GNU General Public License. It comes with
1087 NO WARRANTY.
1088 """)
1089
1090
1091 def check_texidoc (chunks):
1092         n = []
1093         for c in chunks:
1094                 if c[0] == 'lilypond':
1095                         (type, body, opts, todo, basename) = c;
1096                         pathbase = os.path.join (g_outdir, basename)
1097                         if os.path.isfile (pathbase + '.texidoc'):
1098                                 body = '\n@include %s.texidoc\n' % basename + body
1099                                 c = (type, body, opts, todo, basename)
1100                 n.append (c)
1101         return n
1102
1103 def fix_epswidth (chunks):
1104         newchunks = []
1105         for c in chunks:
1106                 if c[0] == 'lilypond' and 'eps' in c[2]:
1107                         body = re.sub (r"""\\lilypondepswidth{(.*?)}""", find_eps_dims, c[1])
1108                         newchunks.append(('lilypond', body, c[2], c[3], c[4]))
1109                 else:
1110                         newchunks.append (c)
1111         return newchunks
1112
1113
1114 foutn=""
1115 def do_file(input_filename):
1116         global foutn
1117         file_settings = {}
1118         if outname:
1119                 my_outname = outname
1120         else:
1121                 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
1122         my_depname = my_outname + '.dep'                
1123
1124         chunks = read_doc_file(input_filename)
1125         chunks = chop_chunks(chunks, 'lilypond', make_lilypond, 1)
1126         chunks = chop_chunks(chunks, 'lilypond-file', make_lilypond_file, 1)
1127         chunks = chop_chunks(chunks, 'lilypond-block', make_lilypond_block, 1)
1128         chunks = chop_chunks(chunks, 'singleline-comment', do_ignore, 1)
1129         chunks = chop_chunks(chunks, 'preamble-end', do_preamble_end)
1130         chunks = chop_chunks(chunks, 'numcols', do_columns)
1131         #print "-" * 50
1132         #for c in chunks: print "c:", c;
1133         #sys.exit()
1134         scan_preamble(chunks)
1135         chunks = process_lilypond_blocks(my_outname, chunks)
1136
1137         foutn = os.path.join (g_outdir, my_outname + '.' + format)
1138
1139         # Do It.
1140         if __main__.g_run_lilypond:
1141                 compile_all_files (chunks)
1142                 chunks = fix_epswidth (chunks)
1143
1144         if __main__.format == 'texi':
1145                 chunks = check_texidoc (chunks)
1146
1147         x = 0
1148         chunks = completize_preamble (chunks)
1149         sys.stderr.write ("Writing `%s'\n" % foutn)
1150         fout = open (foutn, 'w')
1151         for c in chunks:
1152                 fout.write (c[1])
1153         fout.close ()
1154         # should chmod -w
1155
1156         if do_deps:
1157                 write_deps (my_depname, foutn, chunks)
1158
1159
1160 outname = ''
1161 try:
1162         (sh, long) = getopt_args (__main__.option_definitions)
1163         (options, files) = getopt.getopt(sys.argv[1:], sh, long)
1164 except getopt.error, msg:
1165         sys.stderr.write("error: %s" % msg)
1166         sys.exit(1)
1167
1168 do_deps = 0
1169 for opt in options:     
1170         o = opt[0]
1171         a = opt[1]
1172
1173         if o == '--include' or o == '-I':
1174                 include_path.append (a)
1175         elif o == '--version' or o == '-v':
1176                 print_version ()
1177                 sys.exit  (0)
1178         elif o == '--format' or o == '-f':
1179                 __main__.format = a
1180         elif o == '--outname' or o == '-o':
1181                 if len(files) > 1:
1182                         #HACK
1183                         sys.stderr.write("Lilypond-book is confused by --outname on multiple files")
1184                         sys.exit(1)
1185                 outname = a
1186         elif o == '--help' or o == '-h':
1187                 help ()
1188         elif o == '--no-lily' or o == '-n':
1189                 __main__.g_run_lilypond = 0
1190         elif o == '--dependencies' or o == '-M':
1191                 do_deps = 1
1192         elif o == '--default-music-fontsize':
1193                 default_music_fontsize = string.atoi (a)
1194         elif o == '--default-lilypond-fontsize':
1195                 print "--default-lilypond-fontsize is deprecated, use --default-music-fontsize"
1196                 default_music_fontsize = string.atoi (a)
1197         elif o == '--force-music-fontsize':
1198                 g_force_lilypond_fontsize = string.atoi(a)
1199         elif o == '--force-lilypond-fontsize':
1200                 print "--force-lilypond-fontsize is deprecated, use --default-lilypond-fontsize"
1201                 g_force_lilypond_fontsize = string.atoi(a)
1202         elif o == '--dep-prefix':
1203                 g_dep_prefix = a
1204         elif o == '--no-pictures':
1205                 g_do_pictures = 0
1206         elif o == '--read-lys':
1207                 g_read_lys = 1
1208         elif o == '--outdir':
1209                 g_outdir = a
1210
1211 identify()
1212 if g_outdir:
1213         if os.path.isfile(g_outdir):
1214                 error ("outdir is a file: %s" % g_outdir)
1215         if not os.path.exists(g_outdir):
1216                 os.mkdir(g_outdir)
1217 for input_filename in files:
1218         do_file(input_filename)
1219         
1220 #
1221 # Petr, ik zou willen dat ik iets zinvoller deed,
1222 # maar wat ik kan ik doen, het verandert toch niets?
1223 #   --hwn 20/aug/99