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