]> git.donarmstrong.com Git - lilypond.git/blob - scripts/filter-lilypond-book.py
This commit was manufactured by cvs2svn to create tag 'lilypond_2_1_17'.
[lilypond.git] / scripts / filter-lilypond-book.py
1 #!@PYTHON@
2
3 import string
4
5 ################################################################
6 # Users of python modules should include this snippet
7 # and customize variables below.
8
9 # We'll suffer this path init stuff as long as we don't install our
10 # python packages in <prefix>/lib/pythonx.y (and don't kludge around
11 # it as we do with teTeX on Red Hat Linux: set some environment var
12 # (PYTHONPATH) in profile)
13
14 # If set, LILYPONDPREFIX must take prevalence
15 # if datadir is not set, we're doing a build and LILYPONDPREFIX
16 import getopt, os, sys
17 datadir = '@local_lilypond_datadir@'
18 if not os.path.isdir (datadir):
19         datadir = '@lilypond_datadir@'
20 if os.environ.has_key ('LILYPONDPREFIX') :
21         datadir = os.environ['LILYPONDPREFIX']
22         while datadir[-1] == os.sep:
23                 datadir= datadir[:-1]
24
25 sys.path.insert (0, os.path.join (datadir, 'python'))
26
27 # Customize these
28 #if __name__ == '__main__':
29
30 import lilylib as ly
31 global _;_=ly._
32 global re;re = ly.re
33
34 # lilylib globals
35 program_version = '@TOPLEVEL_VERSION@'
36 #program_name = 'new-book'
37 program_name = 'filter-lilypond-book'
38 verbose_p = 0
39 pseudo_filter_p = 0
40 original_dir = os.getcwd ()
41
42
43 # help_summary = _ ("Process LilyPond snippets in hybrid html, LaTeX or texinfo document")
44 help_summary = _ ("""Process ly snippets from lilypond-book source.  Example usage:
45
46    filter-lilypond-book --filter="tr '[a-z]' '[A-Z]'" BOOK
47    filter-lilypond-book --filter="convert-ly --no-version --from=1.6.11 -" BOOK
48
49 """)
50 copyright = ('Tom Cato Amundsen <tca@gnu.org>',
51              'Han-Wen Nienhuys <hanwen@cs.uu.nl>')
52
53 option_definitions = [
54         (_ ("EXT"), 'f', 'format', _ ("use output format EXT (texi [default], texi-html, latex, html)")),
55         (_ ("FILTER"), 'F', 'filter', _ ("pipe snippets through FILTER [convert-ly -n -]")),
56         ('', 'h', 'help', _ ("print this help")),
57         (_ ("COMMAND"), 'P', 'process', _ ("process ly_files using COMMAND FILE...")),
58         ('', 'V', 'verbose', _ ("be verbose")),
59         ('', 'v', 'version', _ ("print version information")),
60         ('', 'w', 'warranty', _ ("show warranty and copyright")),
61         ]
62
63 include_path = [os.getcwd ()]
64
65 lilypond_binary = os.path.join ('@bindir@', 'lilypond-bin')
66
67 # only use installed binary  when we're installed too.
68 if '@bindir@' == ('@' + 'bindir@') or not os.path.exists (lilypond_binary):
69         lilypond_binary = 'lilypond-bin'
70
71
72 format = 0
73 filter_cmd = 'convert-ly --no-version --from=2.0.0 -'
74 #filter_cmd = 0
75 #process_cmd = 'convert-ly --no-version --from=2.0.0'
76 process_cmd = 0
77
78 ################################################################
79 # Recognize special sequences in the input 
80
81
82 # Warning: This uses extended regular expressions.  Tread with care.
83 #
84 # legenda
85 #
86 # (?P<name>regex) -- assign result of REGEX to NAME
87 # *? -- match non-greedily.
88 # (?m) -- multiline regex: make ^ and $ match at each line
89 # (?s) -- make the dot match all characters including newline
90 no_match = 'a\ba'
91 re_dict = {
92         'html': {
93                 'include':  no_match,
94                 'input': no_match,
95                 'header': no_match,
96                 'preamble-end': no_match,
97                 'landscape': no_match,
98                 'verbatim': r'''(?s)(?P<code><pre>\s.*?</pre>\s)''',
99                 'verb': r'''(?P<code><pre>.*?</pre>)''',
100                 'lilypond-file': r'(?m)(?P<match><lilypondfile(?P<options>[^>]+)?>\s*(?P<filename>[^<]+)\s*</lilypondfile>)',
101                 'lilypond' : '(?m)(?P<match><lilypond((?P<options>[^:]*):)(?P<code>.*?)/>)',
102                 'lilypond-block': r'''(?ms)(?P<match><lilypond(?P<options>[^>]+)?>(?P<code>.*?)</lilypond>)''',
103                 'option-sep' : '\s*',
104                 'intertext': r',?\s*intertext=\".*?\"',
105                 'multiline-comment': r"(?sm)\s*(?!@c\s+)(?P<code><!--\s.*?!-->)\s",
106                 'singleline-comment': no_match,
107                 'numcols': no_match,
108                 'multicols': no_match,
109                 'ly2dvi': r'(?m)(?P<match><ly2dvifile(?P<options>[^>]+)?>\s*(?P<filename>[^<]+)\s*</ly2dvifile>)',
110                 },
111
112         'latex': {
113                 'input': r'(?m)^[^%\n]*?(?P<match>\\mbinput{?([^}\t \n}]*))',
114                 'include': r'(?m)^[^%\n]*?(?P<match>\\mbinclude{(?P<filename>[^}]+)})',
115                 'option-sep' : ',\s*',
116                 'header': r"\n*\\documentclass\s*(\[.*?\])?",
117                 'preamble-end': r'(?P<code>\\begin\s*{document})',
118                 'verbatim': r"(?s)(?P<code>\\begin\s*{verbatim}.*?\\end{verbatim})",
119                 'verb': r"(?P<code>\\verb(?P<del>.).*?(?P=del))",
120                 'lilypond-file': r'(?m)^[^%\n]*?(?P<match>\\lilypondfile\s*(\[(?P<options>.*?)\])?\s*\{(?P<filename>.+)})',
121                 'lilypond' : r'(?m)^[^%\n]*?(?P<match>\\lilypond\s*(\[(?P<options>.*?)\])?\s*{(?P<code>.*?)})',
122                 'lilypond-block': r"(?sm)^[^%\n]*?(?P<match>\\begin\s*(\[(?P<options>.*?)\])?\s*{lilypond}(?P<code>.*?)\\end{lilypond})",
123                 'def-post-re': r"\\def\\postLilyPondExample",
124                 'def-pre-re': r"\\def\\preLilyPondExample",
125                 'usepackage-graphics': r"\usepackage\s*{graphics}",
126                 'intertext': r',?\s*intertext=\".*?\"',
127                 'multiline-comment': no_match,
128                 'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>^%.*$\n+))",
129                 'numcols': r"(?P<code>\\(?P<num>one|two)column)",
130                 'multicols': r"(?P<code>\\(?P<be>begin|end)\s*{multicols}({(?P<num>\d+)?})?)",
131                 'ly2dvi': no_match,
132
133                 },
134
135         # why do we have distinction between @mbinclude and @include?
136
137         'texinfo': {
138                 'include':  '(?m)^[^%\n]*?(?P<match>@mbinclude\s+(?P<filename>\S*))',
139                 'input': no_match,
140                 'header': no_match,
141                 'preamble-end': no_match,
142                 'landscape': no_match,
143                 'verbatim': r'''(?s)(?P<code>@example\s.*?@end example\s)''',
144                 'verb': r'''(?P<code>@code{.*?})''',
145                 'lilypond-file': '(?m)^(?P<match>@lilypondfile(\[(?P<options>[^]]*)\])?{(?P<filename>[^}]+)})',
146                 'lilypond' : '(?m)^(?P<match>@lilypond(\[(?P<options>[^]]*)\])?{(?P<code>.*?)})',
147                 'lilypond-block': r'''(?ms)^(?P<match>@lilypond(\[(?P<options>[^]]*)\])?\s(?P<code>.*?)@end lilypond)\s''',
148                 'option-sep' : ',\s*',
149                 'intertext': r',?\s*intertext=\".*?\"',
150                 'multiline-comment': r"(?sm)^\s*(?!@c\s+)(?P<code>@ignore\s.*?@end ignore)\s",
151                 'singleline-comment': r"(?m)^.*?(?P<match>(?P<code>@c.*$\n+))",
152                 'numcols': no_match,
153                 'multicols': no_match,
154                 'ly2dvi': no_match,
155                 }
156         }
157
158 def compose_ly (code, options):
159         m = re.search (r'''\\score''', code)
160         if not m and (not options \
161                       or not 'nofragment' in options \
162                       or 'fragment' in options):
163                 body = r'''\score{\notes{ %(code)s }}''' % vars ()
164         else:
165                 body = code
166         ### todo: add options
167         return body
168
169
170 LATEX = 'latex'
171 HTML = 'html'
172 TEXINFO = 'texinfo'
173 BEFORE = 'before'
174 AFTER = 'after'
175
176 output = {
177         HTML : {
178         BEFORE: '',
179         AFTER: '',
180         },
181         
182         LATEX : {
183         BEFORE: '',
184         AFTER: '',
185         },
186         
187         TEXINFO :       {
188         BEFORE: '',
189         AFTER: '',
190         },
191         
192         }
193
194
195 # BARF
196 # use lilypond-bin for latex (.lytex) books,
197 # and lilypond --preview for html, texinfo books?
198 def to_eps (file):
199         cmd = r'latex "\nonstopmode \input %s"' % file
200         # Ugh.  (La)TeX writes progress and error messages on stdout
201         # Redirect to stderr
202         cmd = '(( %s  >&2 ) >&- )' % cmd
203         ly.system (cmd)
204         ly.system ('dvips -Ppdf -u+lilypond.map -E -o %s.eps %s' \
205                    % (file, file))
206
207 ## make source, index statics of Snippet?
208 index = 0
209
210 class Snippet:
211         def __init__ (self, type, index, match):
212                 self.type = type
213                 self.index = index
214                 self.match = match
215                 self.hash = 0
216
217         def start (self, s):
218                 return self.index + self.match.start (s)
219
220         def end (self, s):
221                 return self.index + self.match.end (s)
222
223         def substring (self, source, s):
224                 return source[self.start (s):self.end (s)]
225
226         def ly (self, source):
227                 if self.type == 'lilypond-block' or self.type == 'lilypond':
228                         return compose_ly (self.substring (source, 'code'),
229                                            self.match.group ('options'))
230                 return ''
231         
232         def get_hash (self, source):
233                 if not self.hash:
234                         self.hash = abs (hash (self.substring (source,
235                                                                'code')))
236                 return self.hash
237
238         def basename (self, source):
239                 return 'lily-%d' % self.get_hash (source)
240
241         def write_ly (self, source):
242                 h = open (self.basename (source) + '.ly', 'w')
243                 h.write (self.ly (source))
244                 h.close ()
245
246         def output_html (self, source):
247                 base = self.basename (source)
248                 h.write (output[HTML][BEFORE])
249                 h.write ('<src image="%(base)s.png">' % vars ())
250                 h.write (output[HTML][AFTER])
251                         
252         def output_latex (self, source):
253                 h.write (output[HTML][BEFORE])
254                 name = self.basename (source) + '.tex'
255                 h.write (open (name).read ())
256                 h.write (output[HTML][AFTER])
257                         
258         def output_texinfo (self, source):
259                 h.write ('\n@tex\n')
260                 self.output_latex (source)
261                 h.write ('\n@end tex\n')
262                 
263                 h.write ('\n@html\n')
264                 self.output_html (source)
265                 h.write ('\n@end html\n')
266                         
267         def outdated_p (self, source):
268                 base = self.basename (source)
269                 if os.path.exists (base + '.ly') \
270                    and os.path.exists (base + '.tex') \
271                    and self.ly (source) == open (base + '.ly').read ():
272                         # TODO: something smart with target formats
273                         # (ps, png) and m/atimes
274                         return None
275                 return self
276
277 def find_snippets (s, type):
278         re = ly.re.compile (re_dict[format][type])
279         i = 0
280         snippets = []
281         m = re.search (s[i:])
282         while m:
283                 snippets.append (Snippet (type, i, m))
284                 i = i + m.end (0)
285                 m = re.search (s[i:])
286         return snippets
287
288 def filter_pipe (input, cmd):
289         if verbose_p:
290                 ly.progress (_ ("Opening filter `%s\'") % cmd)
291                 
292         stdin, stdout, stderr = os.popen3 (cmd)
293         stdin.write (input)
294         status = stdin.close ()
295
296         if not status:
297                 status = 0
298                 output = stdout.read ()
299                 status = stdout.close ()
300                 error = stderr.read ()
301                 
302         if not status:
303                 status = 0
304         signal = 0x0f & status
305         if status or (not output and error):
306                 exit_status = status >> 8
307                 ly.error (_ ("`%s\' failed (%d)") % (cmd, exit_status))
308                 ly.error (_ ("The error log is as follows:"))
309                 sys.stderr.write (error)
310                 sys.stderr.write (stderr.read ())
311                 ly.exit (status)
312         
313         if verbose_p:
314                 ly.progress ('\n')
315
316         return output
317         
318 def run_filter (s):
319         return filter_pipe (s, filter_cmd)
320
321 def compare_index (a, b):
322         return a.start (0) - b.start (0)
323
324 # apply FUNC to every toplevel block in SNIPPETS, ie, enclosed
325 # snippets are skipped.  return list with all non-empty return values
326 # of FUNC
327
328 # Hmm, do we need enclosed snippets at all?  Maybe use MAP_SNIPPETS
329 # once and use simple filter/map on that resulting toplevel list iso
330 # silly map_snippets/do_snippets.
331 def map_snippets (source, snippets, func):
332         global index
333         index = 0
334         lst = []
335         for i in snippets:
336                 if i.start (0) < index:
337                         continue
338                 # lst.append (func (i, source))
339                 x = func (i, source)
340                 if x:
341                         lst.append (x)
342                 index = i.end (0)
343         return lst
344
345 # apply FUNC to every toplevel block in SNIPPETS, ie, enclosed
346 # snippets are skipped.  return last snippet's index
347 def do_snippets (source, snippets, func):
348         global index
349         index = 0
350         for i in snippets:
351                 if i.start (0) < index:
352                         continue
353                 func (i, source)
354                 # ugr, moved to FUNC
355                 #index = i.end ('code')
356         return index
357
358 def process_snippets (source, snippets, cmd):
359         names = map_snippets (source, snippets, Snippet.basename)
360         if names:
361                 ly.system (string.join ([cmd] + names))
362
363         if format == HTML or format == TEXINFO:
364                 for i in names:
365                         to_eps (i)
366                         ly.make_ps_images (i + '.eps', resolution=110)
367                 
368
369 def do_file (input_filename):
370         global format
371         
372         if not format:
373                 ext2format = {
374                         '.html' : HTML,
375                         '.itely' : TEXINFO,
376                         '.lytex' : LATEX,
377                         '.tely' : TEXINFO,
378                         '.tex': LATEX,
379                         '.texi' : TEXINFO,
380                         '.xml' : HTML,
381                         }
382                                
383                 e = os.path.splitext (input_filename)[1]
384                 if e in ext2format.keys ():
385                         format = ext2format[e]
386                 else:
387                         ly.error (_ ("cannot determine format for: %s" \
388                                      % input_filename))
389
390         global h
391
392         h = sys.stdin
393         if input_filename != '-':
394                 h = open (input_filename)
395         source = h.read ()
396
397         #snippet_types = ('lilypond', 'lilypond-block')
398         snippet_types = ('verbatim', 'verb', 'multiline-comment',
399                          'lilypond', 'lilypond-block')
400         snippets = []
401         for i in snippet_types:
402                 snippets += find_snippets (source, i)
403
404         snippets.sort (compare_index)
405
406         h = sys.stdout
407
408         def filter_source (snippet, source):
409                 global index
410                 # Hmm, why is verbatim's group called 'code'; rename to 'verb'?
411                 #if snippet.match.group ('code'):
412                 # urg
413                 if snippet.type == 'lilypond' or snippet.type == 'lilypond-block':
414                         h.write (source[index:snippet.start ('code')])
415                         h.write (run_filter (snippet.substring (source, 'code')))
416                         h.write (source[snippet.end ('code'):snippet.end (0)])
417                 else:
418                         h.write (source[index:snippet.end (0)])
419                 index = snippet.end (0)
420
421         # TODO: output dict?
422
423         snippet_output = eval ("Snippet.output_" + format)
424         def compile_output (snippet, source):
425                 global index
426                 # Hmm, why is verbatim's group called 'code'; rename to 'verb'?
427                 # if snippet.match.group ('code'):
428                 # urg
429                 if snippet.type == 'lilypond' \
430                        or snippet.type == 'lilypond-block':
431                         h.write (source[index:snippet.start (0)])
432                         snippet_output (snippet, source)
433                 index = snippet.end (0)
434
435
436         global index
437         if filter_cmd:
438                 index = do_snippets (source, snippets, filter_source)
439                 h.write (source[index:])
440         elif process_cmd:
441                 outdated = map_snippets (source, snippets, Snippet.outdated_p)
442                 do_snippets (source, snippets, Snippet.write_ly)
443                 process_snippets (source, outdated, process_cmd)
444                 do_snippets (source, snippets, compile_output)
445                 h.write (source[index:])
446
447 def do_options ():
448         global format
449         global filter_cmd, process_cmd, verbose_p
450         
451         (sh, long) = ly.getopt_args (option_definitions)
452         try:
453                 (options, files) = getopt.getopt (sys.argv[1:], sh, long)
454         except getopt.error, s:
455                 sys.stderr.write ('\n')
456                 ly.error (_ ("getopt says: `%s\'" % s))
457                 sys.stderr.write ('\n')
458                 ly.help ()
459                 ly.exit (2)
460
461         for opt in options:
462                 o = opt[0]
463                 a = opt[1]
464
465                 if 0:
466                         pass
467                 elif o == '--version' or o == '-v':
468                         ly.identify (sys.stdout)
469                         sys.exit (0)
470                 elif o == '--verbose' or o == '-V':
471                         verbose_p = 1
472                 elif o == '--filter' or o == '-F':
473                         filter_cmd = a
474                         process_cmd = 0
475                 elif o == '--format' or o == '-f':
476                         format = a
477                         if a == 'texi-html':
478                                 format = 'texi'
479                 elif o == '--help' or o == '-h':
480                         ly.help ()
481                         sys.exit (0)
482                 elif o == '--process' or o == '-P':
483                         process_cmd = a
484                         filter_cmd = 0
485                 elif o == '--warranty' or o == '-w':
486                         if 1 or status:
487                                 ly.warranty ()
488                         sys.exit (0)
489         return files
490
491 def main ():
492         files = do_options ()
493         ly.identify (sys.stderr)
494         ly.setup_environment ()
495         if files:
496                 do_file (files[0])
497
498 if __name__ == '__main__':
499         main ()