lilypond-1.3.144
[lilypond.git] / scripts / lilypond-book.py
blobe15ee356ddaff5485255bcb2291298bdc053c8e0
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
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.)
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.
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.
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.
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.
33 import os
34 import stat
35 import string
36 import re
37 import getopt
38 import sys
39 import __main__
40 import operator
43 program_version = '@TOPLEVEL_VERSION@'
44 if program_version == '@' + 'TOPLEVEL_VERSION' + '@':
45 program_version = '1.4pre'
47 include_path = [os.getcwd()]
50 # g_ is for global (?)
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'
63 default_music_fontsize = 16
64 default_text_fontsize = 12
65 paperguru = None
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
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
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]
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]
208 #ugh test if this is necessary
209 if self.__body:
210 mp = 0
212 if not self.m_use_geometry:
213 return latex_linewidths[self.m_papersize][self.m_fontsize]
214 else:
215 geo_opts = (self.m_geo_lmargin == None,
216 self.m_geo_width == None,
217 self.m_geo_rmargin == None)
219 if geo_opts == (1, 1, 1):
220 if self.m_geo_textwidth:
221 return self.m_geo_textwidth
222 w = self.get_paperwidth() * 0.8
223 return w - mp
224 elif geo_opts == (0, 1, 1):
225 if self.m_geo_textwidth:
226 return self.m_geo_textwidth
227 return self.f1(self.m_geo_lmargin, mp)
228 elif geo_opts == (1, 1, 0):
229 if self.m_geo_textwidth:
230 return self.m_geo_textwidth
231 return self.f1(self.m_geo_rmargin, mp)
232 elif geo_opts \
233 in ((0, 0, 1), (1, 0, 0), (1, 0, 1)):
234 if self.m_geo_textwidth:
235 return self.m_geo_textwidth
236 return self.m_geo_width - mp
237 elif geo_opts in ((0, 1, 0), (0, 0, 0)):
238 w = self.get_paperwidth() \
239 - self.m_geo_lmargin - self.m_geo_rmargin - mp
240 if w < 0:
241 w = 0
242 return w
243 raise "Never do this!"
244 def f1(self, m, mp):
245 tmp = self.get_paperwidth() - m * 2 - mp
246 if tmp < 0:
247 tmp = 0
248 return tmp
249 def f2(self):
250 tmp = self.get_paperwidth() - self.m_geo_lmargin \
251 - self.m_geo_rmargin
252 if tmp < 0:
253 return 0
254 return tmp
256 class TexiPaper:
257 def __init__(self):
258 self.m_papersize = 'letterpaper'
259 self.m_fontsize = 12
260 def get_linewidth(self):
261 return texi_linewidths[self.m_papersize][self.m_fontsize]
263 def mm2pt(x):
264 return x * 2.8452756
265 def in2pt(x):
266 return x * 72.26999
267 def em2pt(x, fontsize = 10):
268 return {10: 10.00002, 11: 10.8448, 12: 11.74988}[fontsize] * x
269 def ex2pt(x, fontsize = 10):
270 return {10: 4.30554, 11: 4.7146, 12: 5.16667}[fontsize] * x
272 def pt2pt(x):
273 return x
275 dimension_conversion_dict ={
276 'mm': mm2pt,
277 'in': in2pt,
278 'em': em2pt,
279 'ex': ex2pt,
280 'pt': pt2pt
284 # latex linewidths:
285 # indices are no. of columns, papersize, fontsize
286 # Why can't this be calculated?
287 latex_linewidths = {
288 'a4paper':{10: 345, 11: 360, 12: 390},
289 'a4paper-landscape': {10: 598, 11: 596, 12:592},
290 'a5paper':{10: 276, 11: 276, 12: 276},
291 'b5paper':{10: 345, 11: 356, 12: 356},
292 'letterpaper':{10: 345, 11: 360, 12: 390},
293 'letterpaper-landscape':{10: 598, 11: 596, 12:596},
294 'legalpaper': {10: 345, 11: 360, 12: 390},
295 'executivepaper':{10: 345, 11: 360, 12: 379}}
297 texi_linewidths = {
298 'afourpaper': {12: mm2pt(160)},
299 'afourwide': {12: in2pt(6.5)},
300 'afourlatex': {12: mm2pt(150)},
301 'smallbook': {12: in2pt(5)},
302 'letterpaper': {12: in2pt(6)}}
304 option_definitions = [
305 ('EXT', 'f', 'format', 'set format. EXT is one of texi and latex.'),
306 ('DIM', '', 'default-music-fontsize', 'default fontsize for music. DIM is assumed to be in points'),
307 ('DIM', '', 'default-lilypond-fontsize', 'deprecated, use --default-music-fontsize'),
308 ('DIM', '', 'force-music-fontsize', 'force fontsize for all inline lilypond. DIM is assumed be to in points'),
309 ('DIM', '', 'force-lilypond-fontsize', 'deprecated, use --force-music-fontsize'),
310 ('DIR', 'I', 'include', 'include path'),
311 ('', 'M', 'dependencies', 'write dependencies'),
312 ('PREF', '', 'dep-prefix', 'prepend PREF before each -M dependency'),
313 ('', 'n', 'no-lily', 'don\'t run lilypond'),
314 ('', '', 'no-pictures', "don\'t generate pictures"),
315 ('', '', 'read-lys', "don't write ly files."),
316 ('FILE', 'o', 'outname', 'filename main output file'),
317 ('FILE', '', 'outdir', "where to place generated files"),
318 ('', 'v', 'version', 'print version information' ),
319 ('', 'h', 'help', 'print help'),
322 # format specific strings, ie. regex-es for input, and % strings for output
323 output_dict= {
324 'latex': {
325 'output-lilypond-fragment' : r"""\begin[eps,singleline,%s]{lilypond}
326 \context Staff <
327 \context Voice{
331 \end{lilypond}""",
332 'output-filename' : r'''
334 \verb+%s+:''',
335 'output-lilypond': r"""\begin[%s]{lilypond}
337 \end{lilypond}""",
338 'output-verbatim': "\\begin{verbatim}%s\\end{verbatim}",
339 'output-default-post': "\\def\postLilypondExample{}\n",
340 'output-default-pre': "\\def\preLilypondExample{}\n",
341 'usepackage-graphics': '\\usepackage{graphics}\n',
342 'output-eps': '\\noindent\\parbox{\\lilypondepswidth{%(fn)s.eps}}{\includegraphics{%(fn)s.eps}}',
343 'output-tex': '\\preLilypondExample \\input %(fn)s.tex \\postLilypondExample\n',
344 'pagebreak': r'\pagebreak',
346 'texi' : {'output-lilypond': """@lilypond[%s]
348 @end lilypond
349 """,
350 'output-filename' : r'''
352 @file{%s}:''',
353 'output-lilypond-fragment': """@lilypond[%s]
354 \context Staff\context Voice{ %s }
355 @end lilypond """,
356 'pagebreak': None,
357 'output-verbatim': r"""@example
359 @end example
360 """,
362 # do some tweaking: @ is needed in some ps stuff.
363 # override EndLilyPondOutput, since @tex is done
364 # in a sandbox, you can't do \input lilyponddefs at the
365 # top of the document.
367 # should also support fragment in
369 'output-all': r"""
370 @tex
371 \catcode`\@=12
372 \input lilyponddefs
373 \def\EndLilyPondOutput{}
374 \input %(fn)s.tex
375 \catcode`\@=0
376 @end tex
377 @html
379 <img src=%(fn)s.png>
380 @end html
381 """,
385 def output_verbatim (body):
386 if __main__.format == 'texi':
387 body = re.sub ('([@{}])', '@\\1', body)
388 return get_output ('output-verbatim') % body
391 re_dict = {
392 'latex': {'input': r'(?m)^[^%\n]*?(?P<match>\\mbinput{?([^}\t \n}]*))',
393 'include': r'(?m)^[^%\n]*?(?P<match>\\mbinclude{(?P<filename>[^}]+)})',
394 'option-sep' : ', *',
395 'header': r"\\documentclass\s*(\[.*?\])?",
396 'geometry': r"^(?m)[^%\n]*?\\usepackage\s*(\[(?P<options>.*)\])?\s*{geometry}",
397 'preamble-end': r'(?P<code>\\begin{document})',
398 'verbatim': r"(?s)(?P<code>\\begin{verbatim}.*?\\end{verbatim})",
399 'verb': r"(?P<code>\\verb(?P<del>.).*?(?P=del))",
400 'lilypond-file': r'(?m)^[^%\n]*?(?P<match>\\lilypondfile(\[(?P<options>.*?)\])?\{(?P<filename>.+)})',
401 'lilypond' : r'(?m)^[^%\n]*?(?P<match>\\lilypond(\[(?P<options>.*?)\])?{(?P<code>.*?)})',
402 'lilypond-block': r"(?sm)^[^%\n]*?(?P<match>\\begin(\[(?P<options>.*?)\])?{lilypond}(?P<code>.*?)\\end{lilypond})",
403 'def-post-re': r"\\def\\postLilypondExample",
404 'def-pre-re': r"\\def\\preLilypondExample",
405 'usepackage-graphics': r"\usepackage{graphics}",
406 'intertext': r',?\s*intertext=\".*?\"',
407 'multiline-comment': no_match,
408 'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>^%.*$\n+))",
409 'numcols': r"(?P<code>\\(?P<num>one|two)column)",
413 # why do we have distinction between @mbinclude and @include?
414 'texi': {
415 'include': '(?m)^[^%\n]*?(?P<match>@mbinclude[ \n\t]+(?P<filename>[^\t \n]*))',
416 'input': no_match,
417 'header': no_match,
418 'preamble-end': no_match,
419 'landscape': no_match,
420 'verbatim': r"""(?s)(?P<code>@example\s.*?@end example\s)""",
421 'verb': r"""(?P<code>@code{.*?})""",
422 'lilypond-file': '(?m)^(?!@c)(?P<match>@lilypondfile(\[(?P<options>.*?)\])?{(?P<filename>[^}]+)})',
423 'lilypond' : '(?m)^(?!@c)(?P<match>@lilypond(\[(?P<options>.*?)\])?{(?P<code>.*?)})',
424 'lilypond-block': r"""(?m)^(?!@c)(?P<match>(?s)(?P<match>@lilypond(\[(?P<options>.*?)\])?\s(?P<code>.*?)@end lilypond\s))""",
425 'option-sep' : ', *',
426 'intertext': r',?\s*intertext=\".*?\"',
427 'multiline-comment': r"(?sm)^\s*(?!@c\s+)(?P<code>@ignore\s.*?@end ignore)\s",
428 'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>@c.*$\n+))",
429 'numcols': no_match,
434 for r in re_dict.keys ():
435 olddict = re_dict[r]
436 newdict = {}
437 for k in olddict.keys ():
438 newdict[k] = re.compile (olddict[k])
439 re_dict[r] = newdict
442 def uniq (list):
443 list.sort ()
444 s = list
445 list = []
446 for x in s:
447 if x not in list:
448 list.append (x)
449 return list
452 def get_output (name):
453 return output_dict[format][name]
455 def get_re (name):
456 return re_dict[format][name]
458 def bounding_box_dimensions(fname):
459 if g_outdir:
460 fname = os.path.join(g_outdir, fname)
461 try:
462 fd = open(fname)
463 except IOError:
464 error ("Error opening `%s'" % fname)
465 str = fd.read ()
466 s = re.search('%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)', str)
467 if s:
468 return (int(s.group(3))-int(s.group(1)),
469 int(s.group(4))-int(s.group(2)))
470 else:
471 return (0,0)
474 def error (str):
475 sys.stderr.write (str + "\n Exiting ... \n\n")
476 raise 'Exiting.'
479 def compose_full_body (body, opts):
480 """Construct the lilypond code to send to Lilypond.
481 Add stuff to BODY using OPTS as options."""
482 music_size = default_music_fontsize
483 latex_size = default_text_fontsize
484 for o in opts:
485 if g_force_lilypond_fontsize:
486 music_size = g_force_lilypond_fontsize
487 else:
488 m = re.match ('([0-9]+)pt', o)
489 if m:
490 music_size = string.atoi(m.group (1))
492 m = re.match ('latexfontsize=([0-9]+)pt', o)
493 if m:
494 latex_size = string.atoi (m.group (1))
496 if re.search ('\\\\score', body):
497 is_fragment = 0
498 else:
499 is_fragment = 1
500 if 'fragment' in opts:
501 is_fragment = 1
502 if 'nofragment' in opts:
503 is_fragment = 0
505 if is_fragment and not 'multiline' in opts:
506 opts.append('singleline')
507 if 'singleline' in opts:
508 l = -1.0;
509 else:
510 l = __main__.paperguru.get_linewidth()
512 if 'relative' in opts:#ugh only when is_fragment
513 body = '\\relative c { %s }' % body
515 if is_fragment:
516 body = r"""\score {
517 \notes { %s }
518 \paper { }
519 }""" % body
521 opts = uniq (opts)
522 optstring = string.join (opts, ' ')
523 optstring = re.sub ('\n', ' ', optstring)
524 body = r"""
525 %% Generated automatically by: lilypond-book.py
526 %% options are %s %%ughUGH not original options
527 \include "paper%d.ly"
528 \paper { linewidth = %f \pt; }
529 """ % (optstring, music_size, l) + body
530 return body
532 def parse_options_string(s):
533 d = {}
534 r1 = re.compile("((\w+)={(.*?)})((,\s*)|$)")
535 r2 = re.compile("((\w+)=(.*?))((,\s*)|$)")
536 r3 = re.compile("(\w+?)((,\s*)|$)")
537 while s:
538 m = r1.match(s)
539 if m:
540 s = s[m.end():]
541 d[m.group(2)] = re.split(",\s*", m.group(3))
542 continue
543 m = r2.match(s)
544 if m:
545 s = s[m.end():]
546 d[m.group(2)] = m.group(3)
547 continue
548 m = r3.match(s)
549 if m:
550 s = s[m.end():]
551 d[m.group(1)] = 1
552 continue
554 error ("format of option string invalid (was `%')" % s)
555 return d
557 def scan_latex_preamble(chunks):
558 # first we want to scan the \documentclass line
559 # it should be the first non-comment line
560 idx = 0
561 while 1:
562 if chunks[idx][0] == 'ignore':
563 idx = idx + 1
564 continue
565 m = get_re ('header').match(chunks[idx][1])
566 if m.group (1):
567 options = re.split (',[\n \t]*', m.group(1)[1:-1])
568 else:
569 options = []
570 for o in options:
571 if o == 'landscape':
572 paperguru.m_landscape = 1
573 m = re.match("(.*?)paper", o)
574 if m:
575 paperguru.m_papersize = m.group()
576 else:
577 m = re.match("(\d\d)pt", o)
578 if m:
579 paperguru.m_fontsize = int(m.group(1))
581 break
582 while chunks[idx][0] != 'preamble-end':
583 if chunks[idx] == 'ignore':
584 idx = idx + 1
585 continue
586 m = get_re ('geometry').search(chunks[idx][1])
587 if m:
588 paperguru.m_use_geometry = 1
589 o = parse_options_string(m.group('options'))
590 for k in o.keys():
591 paperguru.set_geo_option(k, o[k])
592 idx = idx + 1
594 def scan_texi_preamble (chunks):
595 # this is not bulletproof..., it checks the first 10 chunks
596 for c in chunks[:10]:
597 if c[0] == 'input':
598 for s in ('afourpaper', 'afourwide', 'letterpaper',
599 'afourlatex', 'smallbook'):
600 if string.find(c[1], "@%s" % s) != -1:
601 paperguru.m_papersize = s
603 def scan_preamble (chunks):
604 if __main__.format == 'texi':
605 scan_texi_preamble(chunks)
606 else:
607 assert __main__.format == 'latex'
608 scan_latex_preamble(chunks)
611 def completize_preamble (chunks):
612 if __main__.format == 'texi':
613 return chunks
614 pre_b = post_b = graphics_b = None
615 for chunk in chunks:
616 if chunk[0] == 'preamble-end':
617 break
618 if chunk[0] == 'input':
619 m = get_re('def-pre-re').search(chunk[1])
620 if m:
621 pre_b = 1
622 if chunk[0] == 'input':
623 m = get_re('def-post-re').search(chunk[1])
624 if m:
625 post_b = 1
626 if chunk[0] == 'input':
627 m = get_re('usepackage-graphics').search(chunk[1])
628 if m:
629 graphics_b = 1
630 x = 0
631 while chunks[x][0] != 'preamble-end':
632 x = x + 1
633 if not pre_b:
634 chunks.insert(x, ('input', get_output ('output-default-pre')))
635 if not post_b:
636 chunks.insert(x, ('input', get_output ('output-default-post')))
637 if not graphics_b:
638 chunks.insert(x, ('input', get_output ('usepackage-graphics')))
639 return chunks
642 read_files = []
643 def find_file (name):
645 Search the include path for NAME. If found, return the (CONTENTS, PATH) of the file.
648 f = None
649 nm = ''
650 for a in include_path:
651 try:
652 nm = os.path.join (a, name)
653 f = open (nm)
654 __main__.read_files.append (nm)
655 break
656 except IOError:
657 pass
658 if f:
659 sys.stderr.write ("Reading `%s'\n" % nm)
660 return (f.read (), nm)
661 else:
662 error ("File not found `%s'\n" % name)
663 return ('', '')
665 def do_ignore(match_object):
666 return [('ignore', match_object.group('code'))]
667 def do_preamble_end(match_object):
668 return [('preamble-end', match_object.group('code'))]
670 def make_verbatim(match_object):
671 return [('verbatim', match_object.group('code'))]
673 def make_verb(match_object):
674 return [('verb', match_object.group('code'))]
676 def do_include_file(m):
677 "m: MatchObject"
678 return [('input', get_output ('pagebreak'))] \
679 + read_doc_file(m.group('filename')) \
680 + [('input', get_output ('pagebreak'))]
682 def do_input_file(m):
683 return read_doc_file(m.group('filename'))
685 def make_lilypond(m):
686 if m.group('options'):
687 options = m.group('options')
688 else:
689 options = ''
690 return [('input', get_output('output-lilypond-fragment') %
691 (options, m.group('code')))]
693 def make_lilypond_file(m):
696 Find @lilypondfile{bla.ly} occurences and substitute bla.ly
697 into a @lilypond .. @end lilypond block.
701 if m.group('options'):
702 options = m.group('options')
703 else:
704 options = ''
705 (content, nm) = find_file(m.group('filename'))
706 options = "filename=%s," % nm + options
708 return [('input', get_output('output-lilypond') %
709 (options, content))]
711 def make_lilypond_block(m):
712 if m.group('options'):
713 options = get_re('option-sep').split (m.group('options'))
714 else:
715 options = []
716 options = filter(lambda s: s != '', options)
717 return [('lilypond', m.group('code'), options)]
719 def do_columns(m):
720 if __main__.format != 'latex':
721 return []
722 if m.group('num') == 'one':
723 return [('numcols', m.group('code'), 1)]
724 if m.group('num') == 'two':
725 return [('numcols', m.group('code'), 2)]
727 def chop_chunks(chunks, re_name, func, use_match=0):
728 newchunks = []
729 for c in chunks:
730 if c[0] == 'input':
731 str = c[1]
732 while str:
733 m = get_re (re_name).search (str)
734 if m == None:
735 newchunks.append (('input', str))
736 str = ''
737 else:
738 if use_match:
739 newchunks.append (('input', str[:m.start ('match')]))
740 else:
741 newchunks.append (('input', str[:m.start (0)]))
742 #newchunks.extend(func(m))
743 # python 1.5 compatible:
744 newchunks = newchunks + func(m)
745 str = str [m.end(0):]
746 else:
747 newchunks.append(c)
748 return newchunks
750 def determine_format (str):
751 if __main__.format == '':
753 latex = re.search ('\\\\document', str[:200])
754 texinfo = re.search ('@node|@setfilename', str[:200])
756 f = ''
757 g = None
759 if texinfo and latex == None:
760 f = 'texi'
761 elif latex and texinfo == None:
762 f = 'latex'
763 else:
764 error("error: can't determine format, please specify")
765 __main__.format = f
767 if __main__.paperguru == None:
768 if __main__.format == 'texi':
769 g = TexiPaper()
770 else:
771 g = LatexPaper()
773 __main__.paperguru = g
776 def read_doc_file (filename):
777 """Read the input file, find verbatim chunks and do \input and \include
779 (str, path) = find_file(filename)
780 determine_format (str)
782 chunks = [('input', str)]
784 # we have to check for verbatim before doing include,
785 # because we don't want to include files that are mentioned
786 # inside a verbatim environment
787 chunks = chop_chunks(chunks, 'verbatim', make_verbatim)
788 chunks = chop_chunks(chunks, 'verb', make_verb)
789 chunks = chop_chunks(chunks, 'multiline-comment', do_ignore)
790 #ugh fix input
791 chunks = chop_chunks(chunks, 'include', do_include_file, 1)
792 chunks = chop_chunks(chunks, 'input', do_input_file, 1)
793 return chunks
796 taken_file_names = {}
797 def schedule_lilypond_block (chunk):
798 """Take the body and options from CHUNK, figure out how the
799 real .ly should look, and what should be left MAIN_STR (meant
800 for the main file). The .ly is written, and scheduled in
801 TODO.
803 Return: a chunk (TYPE_STR, MAIN_STR, OPTIONS, TODO, BASE)
805 TODO has format [basename, extension, extension, ... ]
808 (type, body, opts) = chunk
809 assert type == 'lilypond'
810 file_body = compose_full_body (body, opts)
811 basename = 'lily-' + `abs(hash (file_body))`
812 for o in opts:
813 m = re.search ('filename="(.*?)"', o)
814 if m:
815 basename = m.group (1)
816 if not taken_file_names.has_key(basename):
817 taken_file_names[basename] = 0
818 else:
819 taken_file_names[basename] = taken_file_names[basename] + 1
820 basename = basename + "-%i" % taken_file_names[basename]
821 if not g_read_lys:
822 update_file(file_body, os.path.join(g_outdir, basename) + '.ly')
823 needed_filetypes = ['tex']
825 if format == 'texi':
826 needed_filetypes.append('eps')
827 needed_filetypes.append('png')
828 if 'eps' in opts and not ('eps' in needed_filetypes):
829 needed_filetypes.append('eps')
830 pathbase = os.path.join (g_outdir, basename)
831 def f(base, ext1, ext2):
832 a = os.path.isfile(base + ext2)
833 if (os.path.isfile(base + ext1) and
834 os.path.isfile(base + ext2) and
835 os.stat(base+ext1)[stat.ST_MTIME] >
836 os.stat(base+ext2)[stat.ST_MTIME]) or \
837 not os.path.isfile(base + ext2):
838 return 1
839 todo = []
840 if 'tex' in needed_filetypes and f(pathbase, '.ly', '.tex'):
841 todo.append('tex')
842 if 'eps' in needed_filetypes and f(pathbase, '.tex', '.eps'):
843 todo.append('eps')
844 if 'png' in needed_filetypes and f(pathbase, '.eps', '.png'):
845 todo.append('png')
846 newbody = ''
848 if 'printfilename' in opts:
849 for o in opts:
850 m= re.match ("filename=(.*)", o)
851 if m:
852 newbody = newbody + get_output ("output-filename") % m.group(1)
853 break
856 if 'verbatim' in opts:
857 newbody = output_verbatim (body)
859 for o in opts:
860 m = re.search ('intertext="(.*?)"', o)
861 if m:
862 newbody = newbody + m.group (1) + "\n\n"
863 if format == 'latex':
864 if 'eps' in opts:
865 s = 'output-eps'
866 else:
867 s = 'output-tex'
868 else: # format == 'texi'
869 s = 'output-all'
870 newbody = newbody + get_output (s) % {'fn': basename }
871 return ('lilypond', newbody, opts, todo, basename)
873 def process_lilypond_blocks(outname, chunks):#ugh rename
874 newchunks = []
875 # Count sections/chapters.
876 for c in chunks:
877 if c[0] == 'lilypond':
878 c = schedule_lilypond_block (c)
879 elif c[0] == 'numcols':
880 paperguru.m_num_cols = c[2]
881 newchunks.append (c)
882 return newchunks
885 def find_eps_dims (match):
886 "Fill in dimensions of EPS files."
888 fn =match.group (1)
889 dims = bounding_box_dimensions (fn)
890 if g_outdir:
891 fn = os.path.join(g_outdir, fn)
893 return '%ipt' % dims[0]
896 def system (cmd):
897 sys.stderr.write ("invoking `%s'\n" % cmd)
898 st = os.system (cmd)
899 if st:
900 error ('Error command exited with value %d\n' % st)
901 return st
903 def compile_all_files (chunks):
904 global foutn
905 eps = []
906 tex = []
907 png = []
909 for c in chunks:
910 if c[0] <> 'lilypond':
911 continue
912 base = c[4]
913 exts = c[3]
914 for e in exts:
915 if e == 'eps':
916 eps.append (base)
917 elif e == 'tex':
918 #ugh
919 if base + '.ly' not in tex:
920 tex.append (base + '.ly')
921 elif e == 'png' and g_do_pictures:
922 png.append (base)
923 d = os.getcwd()
924 if g_outdir:
925 os.chdir(g_outdir)
926 if tex:
927 # fixme: be sys-independent.
928 def incl_opt (x):
929 if g_outdir and x[0] <> '/' :
930 x = os.path.join (g_here_dir, x)
931 return ' -I %s' % x
933 incs = map (incl_opt, include_path)
934 lilyopts = string.join (incs, ' ' )
935 if do_deps:
936 lilyopts = lilyopts + ' --dependencies '
937 if g_outdir:
938 lilyopts = lilyopts + '--dep-prefix=' + g_outdir + '/'
939 texfiles = string.join (tex, ' ')
940 system ('lilypond --header=texidoc %s %s' % (lilyopts, texfiles))
943 # Ugh, fixing up dependencies for .tex generation
945 if do_deps:
946 depfiles=map (lambda x: re.sub ('(.*)\.ly', '\\1.dep', x), tex)
947 for i in depfiles:
948 text=open (i).read ()
949 text=re.sub ('\n([^:\n]*):', '\n' + foutn + ':', text)
950 open (i, 'w').write (text)
952 for e in eps:
953 system(r"tex '\nonstopmode \input %s'" % e)
954 system(r"dvips -E -o %s %s" % (e + '.eps', e))
955 for g in png:
956 cmd = r"""gs -sDEVICE=pgm -dTextAlphaBits=4 -dGraphicsAlphaBits=4 -q -sOutputFile=- -r90 -dNOPAUSE %s -c quit | pnmcrop | pnmtopng > %s"""
957 cmd = cmd % (g + '.eps', g + '.png')
958 system (cmd)
959 os.chdir (d)
962 def update_file (body, name):
964 write the body if it has changed
966 same = 0
967 try:
968 f = open (name)
969 fs = f.read (-1)
970 same = (fs == body)
971 except:
972 pass
974 if not same:
975 f = open (name , 'w')
976 f.write (body)
977 f.close ()
979 return not same
982 def getopt_args (opts):
983 "Construct arguments (LONG, SHORT) for getopt from list of options."
984 short = ''
985 long = []
986 for o in opts:
987 if o[1]:
988 short = short + o[1]
989 if o[0]:
990 short = short + ':'
991 if o[2]:
992 l = o[2]
993 if o[0]:
994 l = l + '='
995 long.append (l)
996 return (short, long)
998 def option_help_str (o):
999 "Transform one option description (4-tuple ) into neatly formatted string"
1000 sh = ' '
1001 if o[1]:
1002 sh = '-%s' % o[1]
1004 sep = ' '
1005 if o[1] and o[2]:
1006 sep = ','
1008 long = ''
1009 if o[2]:
1010 long= '--%s' % o[2]
1012 arg = ''
1013 if o[0]:
1014 if o[2]:
1015 arg = '='
1016 arg = arg + o[0]
1017 return ' ' + sh + sep + long + arg
1020 def options_help_str (opts):
1021 "Convert a list of options into a neatly formatted string"
1022 w = 0
1023 strs =[]
1024 helps = []
1026 for o in opts:
1027 s = option_help_str (o)
1028 strs.append ((s, o[3]))
1029 if len (s) > w:
1030 w = len (s)
1032 str = ''
1033 for s in strs:
1034 str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0]) + 3), s[1])
1035 return str
1037 def help():
1038 sys.stdout.write("""Usage: lilypond-book [options] FILE\n
1039 Generate hybrid LaTeX input from Latex + lilypond
1040 Options:
1041 """)
1042 sys.stdout.write (options_help_str (option_definitions))
1043 sys.stdout.write (r"""Warning all output is written in the CURRENT directory
1047 Report bugs to bug-gnu-music@gnu.org.
1049 Written by Tom Cato Amundsen <tca@gnu.org> and
1050 Han-Wen Nienhuys <hanwen@cs.uu.nl>
1051 """)
1053 sys.exit (0)
1056 def write_deps (fn, target, chunks):
1057 global read_files
1058 sys.stdout.write('Writing `%s\'\n' % os.path.join(g_outdir, fn))
1059 f = open (os.path.join(g_outdir, fn), 'w')
1060 f.write ('%s%s: ' % (g_dep_prefix, target))
1061 for d in read_files:
1062 f.write ('%s ' % d)
1063 basenames=[]
1064 for c in chunks:
1065 if c[0] == 'lilypond':
1066 (type, body, opts, todo, basename) = c;
1067 basenames.append (basename)
1068 for d in basenames:
1069 if g_outdir:
1070 d=g_outdir + '/' + d
1071 if g_dep_prefix:
1072 #if not os.isfile (d): # thinko?
1073 if not re.search ('/', d):
1074 d = g_dep_prefix + d
1075 f.write ('%s.tex ' % d)
1076 f.write ('\n')
1077 #if len (basenames):
1078 # for d in basenames:
1079 # f.write ('%s.ly ' % d)
1080 # f.write (' : %s' % target)
1081 f.write ('\n')
1082 f.close ()
1083 read_files = []
1085 def identify():
1086 sys.stdout.write ('lilypond-book (GNU LilyPond) %s\n' % program_version)
1088 def print_version ():
1089 identify()
1090 sys.stdout.write (r"""Copyright 1998--1999
1091 Distributed under terms of the GNU General Public License. It comes with
1092 NO WARRANTY.
1093 """)
1096 def check_texidoc (chunks):
1097 n = []
1098 for c in chunks:
1099 if c[0] == 'lilypond':
1100 (type, body, opts, todo, basename) = c;
1101 pathbase = os.path.join (g_outdir, basename)
1102 if os.path.isfile (pathbase + '.texidoc'):
1103 body = '\n@include %s.texidoc\n' % basename + body
1104 c = (type, body, opts, todo, basename)
1105 n.append (c)
1106 return n
1108 def fix_epswidth (chunks):
1109 newchunks = []
1110 for c in chunks:
1111 if c[0] == 'lilypond' and 'eps' in c[2]:
1112 body = re.sub (r"""\\lilypondepswidth{(.*?)}""", find_eps_dims, c[1])
1113 newchunks.append(('lilypond', body, c[2], c[3], c[4]))
1114 else:
1115 newchunks.append (c)
1116 return newchunks
1119 foutn=""
1120 def do_file(input_filename):
1121 global foutn
1122 file_settings = {}
1123 if outname:
1124 my_outname = outname
1125 else:
1126 my_outname = os.path.basename(os.path.splitext(input_filename)[0])
1127 my_depname = my_outname + '.dep'
1129 chunks = read_doc_file(input_filename)
1130 chunks = chop_chunks(chunks, 'lilypond', make_lilypond, 1)
1131 chunks = chop_chunks(chunks, 'lilypond-file', make_lilypond_file, 1)
1132 chunks = chop_chunks(chunks, 'lilypond-block', make_lilypond_block, 1)
1133 chunks = chop_chunks(chunks, 'singleline-comment', do_ignore, 1)
1134 chunks = chop_chunks(chunks, 'preamble-end', do_preamble_end)
1135 chunks = chop_chunks(chunks, 'numcols', do_columns)
1136 #print "-" * 50
1137 #for c in chunks: print "c:", c;
1138 #sys.exit()
1139 scan_preamble(chunks)
1140 chunks = process_lilypond_blocks(my_outname, chunks)
1142 foutn = os.path.join (g_outdir, my_outname + '.' + format)
1144 # Do It.
1145 if __main__.g_run_lilypond:
1146 compile_all_files (chunks)
1147 chunks = fix_epswidth (chunks)
1149 if __main__.format == 'texi':
1150 chunks = check_texidoc (chunks)
1152 x = 0
1153 chunks = completize_preamble (chunks)
1154 sys.stderr.write ("Writing `%s'\n" % foutn)
1155 fout = open (foutn, 'w')
1156 for c in chunks:
1157 fout.write (c[1])
1158 fout.close ()
1159 # should chmod -w
1161 if do_deps:
1162 write_deps (my_depname, foutn, chunks)
1165 outname = ''
1166 try:
1167 (sh, long) = getopt_args (__main__.option_definitions)
1168 (options, files) = getopt.getopt(sys.argv[1:], sh, long)
1169 except getopt.error, msg:
1170 sys.stderr.write("error: %s" % msg)
1171 sys.exit(1)
1173 do_deps = 0
1174 for opt in options:
1175 o = opt[0]
1176 a = opt[1]
1178 if o == '--include' or o == '-I':
1179 include_path.append (a)
1180 elif o == '--version' or o == '-v':
1181 print_version ()
1182 sys.exit (0)
1183 elif o == '--format' or o == '-f':
1184 __main__.format = a
1185 elif o == '--outname' or o == '-o':
1186 if len(files) > 1:
1187 #HACK
1188 sys.stderr.write("Lilypond-book is confused by --outname on multiple files")
1189 sys.exit(1)
1190 outname = a
1191 elif o == '--help' or o == '-h':
1192 help ()
1193 elif o == '--no-lily' or o == '-n':
1194 __main__.g_run_lilypond = 0
1195 elif o == '--dependencies' or o == '-M':
1196 do_deps = 1
1197 elif o == '--default-music-fontsize':
1198 default_music_fontsize = string.atoi (a)
1199 elif o == '--default-lilypond-fontsize':
1200 print "--default-lilypond-fontsize is deprecated, use --default-music-fontsize"
1201 default_music_fontsize = string.atoi (a)
1202 elif o == '--force-music-fontsize':
1203 g_force_lilypond_fontsize = string.atoi(a)
1204 elif o == '--force-lilypond-fontsize':
1205 print "--force-lilypond-fontsize is deprecated, use --default-lilypond-fontsize"
1206 g_force_lilypond_fontsize = string.atoi(a)
1207 elif o == '--dep-prefix':
1208 g_dep_prefix = a
1209 elif o == '--no-pictures':
1210 g_do_pictures = 0
1211 elif o == '--read-lys':
1212 g_read_lys = 1
1213 elif o == '--outdir':
1214 g_outdir = a
1216 identify()
1217 if g_outdir:
1218 if os.path.isfile(g_outdir):
1219 error ("outdir is a file: %s" % g_outdir)
1220 if not os.path.exists(g_outdir):
1221 os.mkdir(g_outdir)
1222 for input_filename in files:
1223 do_file(input_filename)
1226 # Petr, ik zou willen dat ik iets zinvoller deed,
1227 # maar wat ik kan ik doen, het verandert toch niets?
1228 # --hwn 20/aug/99