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