]> git.donarmstrong.com Git - lilypond.git/blob - python/book_snippets.py
Lilypond-Book: We need the [snippetfile].txt files for makeinfo...
[lilypond.git] / python / book_snippets.py
1 # -*- coding: utf-8 -*-
2
3 import book_base as BookBase
4 import lilylib as ly
5 global _;_=ly._
6 import re
7 import os
8 import copy
9 # TODO: We are using os.popen3, which has been deprecated since python 2.6. The
10 # suggested replacement is the Popen function of the subprocess module.
11 # Unfortunately, on windows this needs the msvcrt module, which doesn't seem
12 # to be available in GUB?!?!?!
13 # from subprocess import Popen, PIPE
14
15 progress = ly.progress
16 warning = ly.warning
17 error = ly.error
18
19
20
21
22
23 ####################################################################
24 # Snippet option handling
25 ####################################################################
26
27
28 #
29 # Is this pythonic?  Personally, I find this rather #define-nesque. --hwn
30 #
31 # Global definitions:
32 ADDVERSION = 'addversion'
33 AFTER = 'after'
34 ALT = 'alt'
35 BEFORE = 'before'
36 DOCTITLE = 'doctitle'
37 EXAMPLEINDENT = 'exampleindent'
38 FILENAME = 'filename'
39 FILTER = 'filter'
40 FRAGMENT = 'fragment'
41 LANG = 'lang'    ## TODO: This is handled nowhere!
42 LAYOUT = 'layout'
43 LILYQUOTE = 'lilyquote'
44 LINE_WIDTH = 'line-width'
45 NOFRAGMENT = 'nofragment'
46 NOGETTEXT = 'nogettext'
47 NOINDENT = 'noindent'
48 NOQUOTE = 'noquote'
49 INDENT = 'indent'
50 NORAGGED_RIGHT = 'noragged-right'
51 NOTES = 'body'
52 NOTIME = 'notime'
53 OUTPUT = 'output'
54 OUTPUTIMAGE = 'outputimage'
55 PAPER = 'paper'
56 PAPERSIZE = 'papersize'
57 PREAMBLE = 'preamble'
58 PRINTFILENAME = 'printfilename'
59 QUOTE = 'quote'
60 RAGGED_RIGHT = 'ragged-right'
61 RELATIVE = 'relative'
62 STAFFSIZE = 'staffsize'
63 TEXIDOC = 'texidoc'
64 VERBATIM = 'verbatim'
65 VERSION = 'lilypondversion'
66
67
68
69 # NOTIME and NOGETTEXT have no opposite so they aren't part of this
70 # dictionary.
71 # NOQUOTE is used internally only.
72 no_options = {
73     NOFRAGMENT: FRAGMENT,
74     NOINDENT: INDENT,
75 }
76
77 # Options that have no impact on processing by lilypond (or --process
78 # argument)
79 PROCESSING_INDEPENDENT_OPTIONS = (
80     ALT, NOGETTEXT, VERBATIM, ADDVERSION,
81     TEXIDOC, DOCTITLE, VERSION, PRINTFILENAME)
82
83
84
85 # Options without a pattern in snippet_options.
86 simple_options = [
87     EXAMPLEINDENT,
88     FRAGMENT,
89     NOFRAGMENT,
90     NOGETTEXT,
91     NOINDENT,
92     PRINTFILENAME,
93     DOCTITLE,
94     TEXIDOC,
95     LANG,
96     VERBATIM,
97     FILENAME,
98     ALT,
99     ADDVERSION
100 ]
101
102
103
104 ####################################################################
105 # LilyPond templates for the snippets
106 ####################################################################
107
108 snippet_options = {
109     ##
110     NOTES: {
111         RELATIVE: r'''\relative c%(relative_quotes)s''',
112     },
113
114     ##
115     PAPER: {
116         PAPERSIZE: r'''#(set-paper-size "%(papersize)s")''',
117         INDENT: r'''indent = %(indent)s''',
118         LINE_WIDTH: r'''line-width = %(line-width)s''',
119         QUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
120         LILYQUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
121         RAGGED_RIGHT: r'''ragged-right = ##t''',
122         NORAGGED_RIGHT: r'''ragged-right = ##f''',
123     },
124
125     ##
126     LAYOUT: {
127         NOTIME: r'''
128  \context {
129    \Score
130    timing = ##f
131  }
132  \context {
133    \Staff
134    \remove "Time_signature_engraver"
135  }''',
136     },
137
138     ##
139     PREAMBLE: {
140         STAFFSIZE: r'''#(set-global-staff-size %(staffsize)s)''',
141     },
142 }
143
144
145
146
147
148 FRAGMENT_LY = r'''
149 %(notes_string)s
150 {
151
152
153 %% ****************************************************************
154 %% ly snippet contents follows:
155 %% ****************************************************************
156 %(code)s
157
158
159 %% ****************************************************************
160 %% end ly snippet
161 %% ****************************************************************
162 }
163 '''
164
165 def classic_lilypond_book_compatibility (key, value):
166     if key == 'singleline' and value == None:
167         return (RAGGED_RIGHT, None)
168
169     m = re.search ('relative\s*([-0-9])', key)
170     if m:
171         return ('relative', m.group (1))
172
173     m = re.match ('([0-9]+)pt', key)
174     if m:
175         return ('staffsize', m.group (1))
176
177     if key == 'indent' or key == 'line-width':
178         m = re.match ('([-.0-9]+)(cm|in|mm|pt|staffspace)', value)
179         if m:
180             f = float (m.group (1))
181             return (key, '%f\\%s' % (f, m.group (2)))
182
183     return (None, None)
184
185
186 PREAMBLE_LY = '''%%%% Generated by %(program_name)s
187 %%%% Options: [%(option_string)s]
188 \\include "lilypond-book-preamble.ly"
189
190
191 %% ****************************************************************
192 %% Start cut-&-pastable-section
193 %% ****************************************************************
194
195 %(preamble_string)s
196
197 \paper {
198   %(paper_string)s
199   force-assignment = #""
200   line-width = #(- line-width (* mm  %(padding_mm)f))
201 }
202
203 \layout {
204   %(layout_string)s
205 }
206
207 %(safe_mode_string)s
208 '''
209
210
211 FULL_LY = '''
212
213
214 %% ****************************************************************
215 %% ly snippet:
216 %% ****************************************************************
217 %(code)s
218
219
220 %% ****************************************************************
221 %% end ly snippet
222 %% ****************************************************************
223 '''
224
225
226
227
228
229
230
231
232 ####################################################################
233 # Helper functions
234 ####################################################################
235
236 def ps_page_count (ps_name):
237     header = file (ps_name).read (1024)
238     m = re.search ('\n%%Pages: ([0-9]+)', header)
239     if m:
240         return int (m.group (1))
241     return 0
242
243 ly_var_def_re = re.compile (r'^([a-zA-Z]+)[\t ]*=', re.M)
244 ly_comment_re = re.compile (r'(%+[\t ]*)(.*)$', re.M)
245 ly_context_id_re = re.compile ('\\\\(?:new|context)\\s+(?:[a-zA-Z]*?(?:Staff\
246 (?:Group)?|Voice|FiguredBass|FretBoards|Names|Devnull))\\s+=\\s+"?([a-zA-Z]+)"?\\s+')
247
248 def ly_comment_gettext (t, m):
249     return m.group (1) + t (m.group (2))
250
251
252
253 class CompileError(Exception):
254   pass
255
256
257
258 ####################################################################
259 # Snippet classes
260 ####################################################################
261
262 class Chunk:
263     def replacement_text (self):
264         return ''
265
266     def filter_text (self):
267         return self.replacement_text ()
268
269     def is_plain (self):
270         return False
271
272 class Substring (Chunk):
273     """A string that does not require extra memory."""
274     def __init__ (self, source, start, end, line_number):
275         self.source = source
276         self.start = start
277         self.end = end
278         self.line_number = line_number
279         self.override_text = None
280
281     def is_plain (self):
282         return True
283
284     def replacement_text (self):
285         if self.override_text:
286             return self.override_text
287         else:
288             return self.source[self.start:self.end]
289
290
291
292 class Snippet (Chunk):
293     def __init__ (self, type, match, formatter, line_number, global_options):
294         self.type = type
295         self.match = match
296         self.checksum = 0
297         self.option_dict = {}
298         self.formatter = formatter
299         self.line_number = line_number
300         self.global_options = global_options
301         self.replacements = {'program_version': ly.program_version,
302                              'program_name': ly.program_name}
303
304     # return a shallow copy of the replacements, so the caller can modify
305     # it locally without interfering with other snippet operations
306     def get_replacements (self):
307         return copy.copy (self.replacements)
308
309     def replacement_text (self):
310         return self.match.group ('match')
311
312     def substring (self, s):
313         return self.match.group (s)
314
315     def __repr__ (self):
316         return `self.__class__` + ' type = ' + self.type
317
318
319
320 class IncludeSnippet (Snippet):
321     def processed_filename (self):
322         f = self.substring ('filename')
323         return os.path.splitext (f)[0] + self.formatter.default_extension
324
325     def replacement_text (self):
326         s = self.match.group ('match')
327         f = self.substring ('filename')
328         return re.sub (f, self.processed_filename (), s)
329
330
331
332 class LilypondSnippet (Snippet):
333     def __init__ (self, type, match, formatter, line_number, global_options):
334         Snippet.__init__ (self, type, match, formatter, line_number, global_options)
335         os = match.group ('options')
336         self.do_options (os, self.type)
337
338
339     def snippet_options (self):
340         return [];
341
342     def verb_ly_gettext (self, s):
343         lang = self.formatter.document_language
344         if not lang:
345             return s
346         try:
347             t = langdefs.translation[lang]
348         except:
349             return s
350
351         s = ly_comment_re.sub (lambda m: ly_comment_gettext (t, m), s)
352
353         if langdefs.LANGDICT[lang].enable_ly_identifier_l10n:
354             for v in ly_var_def_re.findall (s):
355                 s = re.sub (r"(?m)(?<!\\clef)(^|[' \\#])%s([^a-zA-Z])" % v,
356                             "\\1" + t (v) + "\\2",
357                             s)
358             for id in ly_context_id_re.findall (s):
359                 s = re.sub (r'(\s+|")%s(\s+|")' % id,
360                             "\\1" + t (id) + "\\2",
361                             s)
362         return s
363
364     def verb_ly (self):
365         verb_text = self.substring ('code')
366         if not NOGETTEXT in self.option_dict:
367             verb_text = self.verb_ly_gettext (verb_text)
368         if not verb_text.endswith ('\n'):
369             verb_text += '\n'
370         return verb_text
371
372     def ly (self):
373         contents = self.substring ('code')
374         return ('\\sourcefileline %d\n%s'
375                 % (self.line_number - 1, contents))
376
377     def full_ly (self):
378         s = self.ly ()
379         if s:
380             return self.compose_ly (s)
381         return ''
382
383     def split_options (self, option_string):
384         return self.formatter.split_snippet_options (option_string);
385
386     def do_options (self, option_string, type):
387         self.option_dict = {}
388
389         options = self.split_options (option_string)
390
391         for option in options:
392             if '=' in option:
393                 (key, value) = re.split ('\s*=\s*', option)
394                 self.option_dict[key] = value
395             else:
396                 if option in no_options:
397                     if no_options[option] in self.option_dict:
398                         del self.option_dict[no_options[option]]
399                 else:
400                     self.option_dict[option] = None
401
402
403         # If LINE_WIDTH is used without parameter, set it to default.
404         has_line_width = self.option_dict.has_key (LINE_WIDTH)
405         if has_line_width and self.option_dict[LINE_WIDTH] == None:
406             has_line_width = False
407             del self.option_dict[LINE_WIDTH]
408
409         # TODO: Can't we do that more efficiently (built-in python func?)
410         for k in self.formatter.default_snippet_options:
411             if k not in self.option_dict:
412                 self.option_dict[k] = self.formatter.default_snippet_options[k]
413
414         # RELATIVE does not work without FRAGMENT;
415         # make RELATIVE imply FRAGMENT
416         has_relative = self.option_dict.has_key (RELATIVE)
417         if has_relative and not self.option_dict.has_key (FRAGMENT):
418             self.option_dict[FRAGMENT] = None
419
420         if not has_line_width:
421             if type == 'lilypond' or FRAGMENT in self.option_dict:
422                 self.option_dict[RAGGED_RIGHT] = None
423
424             if type == 'lilypond':
425                 if LINE_WIDTH in self.option_dict:
426                     del self.option_dict[LINE_WIDTH]
427             else:
428                 if RAGGED_RIGHT in self.option_dict:
429                     if LINE_WIDTH in self.option_dict:
430                         del self.option_dict[LINE_WIDTH]
431
432             if QUOTE in self.option_dict or type == 'lilypond':
433                 if LINE_WIDTH in self.option_dict:
434                     del self.option_dict[LINE_WIDTH]
435
436         if not INDENT in self.option_dict:
437             self.option_dict[INDENT] = '0\\mm'
438
439         # Set a default line-width if there is none. We need this, because
440         # lilypond-book has set left-padding by default and therefore does
441         # #(define line-width (- line-width (* 3 mm)))
442         # TODO: Junk this ugly hack if the code gets rewritten to concatenate
443         # all settings before writing them in the \paper block.
444         if not LINE_WIDTH in self.option_dict:
445             if not QUOTE in self.option_dict:
446                 if not LILYQUOTE in self.option_dict:
447                     self.option_dict[LINE_WIDTH] = "#(- paper-width \
448 left-margin-default right-margin-default)"
449
450     def get_option_list (self):
451         if not 'option_list' in self.__dict__:
452             option_list = []
453             for (key, value) in self.option_dict.items ():
454                 if value == None:
455                     option_list.append (key)
456                 else:
457                     option_list.append (key + '=' + value)
458             option_list.sort ()
459             self.option_list = option_list
460         return self.option_list
461
462     def compose_ly (self, code):
463         if FRAGMENT in self.option_dict:
464             body = FRAGMENT_LY
465         else:
466             body = FULL_LY
467
468         # Defaults.
469         relative = 1
470         override = {}
471         # The original concept of the `exampleindent' option is broken.
472         # It is not possible to get a sane value for @exampleindent at all
473         # without processing the document itself.  Saying
474         #
475         #   @exampleindent 0
476         #   @example
477         #   ...
478         #   @end example
479         #   @exampleindent 5
480         #
481         # causes ugly results with the DVI backend of texinfo since the
482         # default value for @exampleindent isn't 5em but 0.4in (or a smaller
483         # value).  Executing the above code changes the environment
484         # indentation to an unknown value because we don't know the amount
485         # of 1em in advance since it is font-dependent.  Modifying
486         # @exampleindent in the middle of a document is simply not
487         # supported within texinfo.
488         #
489         # As a consequence, the only function of @exampleindent is now to
490         # specify the amount of indentation for the `quote' option.
491         #
492         # To set @exampleindent locally to zero, we use the @format
493         # environment for non-quoted snippets.
494         override[EXAMPLEINDENT] = r'0.4\in'
495         override[LINE_WIDTH] = '5\\in' # = texinfo_line_widths['@smallbook']
496         override.update (self.formatter.default_snippet_options)
497
498         option_list = []
499         for option in self.get_option_list ():
500             for name in PROCESSING_INDEPENDENT_OPTIONS:
501                 if option.startswith (name):
502                     break
503             else:
504                 option_list.append (option)
505         option_string = ','.join (option_list)
506         compose_dict = {}
507         compose_types = [NOTES, PREAMBLE, LAYOUT, PAPER]
508         for a in compose_types:
509             compose_dict[a] = []
510
511         option_names = self.option_dict.keys ()
512         option_names.sort ()
513         for key in option_names:
514             value = self.option_dict[key]
515             (c_key, c_value) = classic_lilypond_book_compatibility (key, value)
516             if c_key:
517                 if c_value:
518                     warning (
519                         _ ("deprecated ly-option used: %s=%s") % (key, value))
520                     warning (
521                         _ ("compatibility mode translation: %s=%s") % (c_key, c_value))
522                 else:
523                     warning (
524                         _ ("deprecated ly-option used: %s") % key)
525                     warning (
526                         _ ("compatibility mode translation: %s") % c_key)
527
528                 (key, value) = (c_key, c_value)
529
530             if value:
531                 override[key] = value
532             else:
533                 if not override.has_key (key):
534                     override[key] = None
535
536             found = 0
537             for typ in compose_types:
538                 if snippet_options[typ].has_key (key):
539                     compose_dict[typ].append (snippet_options[typ][key])
540                     found = 1
541                     break
542
543             if not found and key not in simple_options and key not in self.snippet_options ():
544                 warning (_ ("ignoring unknown ly option: %s") % key)
545
546         # URGS
547         if RELATIVE in override and override[RELATIVE]:
548             relative = int (override[RELATIVE])
549
550         relative_quotes = ''
551
552         # 1 = central C
553         if relative < 0:
554             relative_quotes += ',' * (- relative)
555         elif relative > 0:
556             relative_quotes += "'" * relative
557
558         # put paper-size first, if it exists
559         for i,elem in enumerate(compose_dict[PAPER]):
560             if elem.startswith("#(set-paper-size"):
561                 compose_dict[PAPER].insert(0, compose_dict[PAPER].pop(i))
562                 break
563
564         paper_string = '\n  '.join (compose_dict[PAPER]) % override
565         layout_string = '\n  '.join (compose_dict[LAYOUT]) % override
566         notes_string = '\n  '.join (compose_dict[NOTES]) % vars ()
567         preamble_string = '\n  '.join (compose_dict[PREAMBLE]) % override
568         padding_mm = self.global_options.padding_mm
569         if self.global_options.safe_mode:
570             safe_mode_string = "#(ly:set-option 'safe #t)"
571         else:
572             safe_mode_string = ""
573
574         d = globals().copy()
575         d.update (locals())
576         d.update (self.global_options.information)
577         return (PREAMBLE_LY + body) % d
578
579     def get_checksum (self):
580         if not self.checksum:
581             # Work-around for md5 module deprecation warning in python 2.5+:
582             try:
583                 from hashlib import md5
584             except ImportError:
585                 from md5 import md5
586
587             # We only want to calculate the hash based on the snippet
588             # code plus fragment options relevant to processing by
589             # lilypond, not the snippet + preamble
590             hash = md5 (self.relevant_contents (self.ly ()))
591             for option in self.get_option_list ():
592                 for name in PROCESSING_INDEPENDENT_OPTIONS:
593                     if option.startswith (name):
594                         break
595                 else:
596                     hash.update (option)
597
598             ## let's not create too long names.
599             self.checksum = hash.hexdigest ()[:10]
600
601         return self.checksum
602
603     def basename (self):
604         cs = self.get_checksum ()
605         name = '%s/lily-%s' % (cs[:2], cs[2:])
606         return name
607
608     final_basename = basename
609
610     def write_ly (self):
611         base = self.basename ()
612         path = os.path.join (self.global_options.lily_output_dir, base)
613         directory = os.path.split(path)[0]
614         if not os.path.isdir (directory):
615             os.makedirs (directory)
616         filename = path + '.ly'
617         if os.path.exists (filename):
618             existing = open (filename, 'r').read ()
619
620             if self.relevant_contents (existing) != self.relevant_contents (self.full_ly ()):
621                 warning ("%s: duplicate filename but different contents of orginal file,\n\
622 printing diff against existing file." % filename)
623                 ly.stderr_write (self.filter_pipe (self.full_ly (), 'diff -u %s -' % filename))
624         else:
625             out = file (filename, 'w')
626             out.write (self.full_ly ())
627             file (path + '.txt', 'w').write ('image of music')
628
629     def relevant_contents (self, ly):
630         return re.sub (r'\\(version|sourcefileline|sourcefilename)[^\n]*\n', '', ly)
631
632     def link_all_output_files (self, output_dir, output_dir_files, destination):
633         existing, missing = self.all_output_files (output_dir, output_dir_files)
634         if missing:
635             print '\nMissing', missing
636             raise CompileError(self.basename())
637         for name in existing:
638             if (self.global_options.use_source_file_names
639                 and isinstance (self, LilypondFileSnippet)):
640                 base, ext = os.path.splitext (name)
641                 components = base.split ('-')
642                 # ugh, assume filenames with prefix with one dash (lily-xxxx)
643                 if len (components) > 2:
644                     base_suffix = '-' + components[-1]
645                 else:
646                     base_suffix = ''
647                 final_name = self.final_basename () + base_suffix + ext
648             else:
649                 final_name = name
650             try:
651                 os.unlink (os.path.join (destination, final_name))
652             except OSError:
653                 pass
654
655             src = os.path.join (output_dir, name)
656             dst = os.path.join (destination, final_name)
657             dst_path = os.path.split(dst)[0]
658             if not os.path.isdir (dst_path):
659                 os.makedirs (dst_path)
660             os.link (src, dst)
661
662     def additional_files_to_consider (self, base, full):
663         return []
664     def additional_files_required (self, base, full):
665         return []
666
667
668     def all_output_files (self, output_dir, output_dir_files):
669         """Return all files generated in lily_output_dir, a set.
670
671         output_dir_files is the list of files in the output directory.
672         """
673         result = set ()
674         missing = set ()
675         base = self.basename()
676         full = os.path.join (output_dir, base)
677         def consider_file (name):
678             if name in output_dir_files:
679                 result.add (name)
680
681         def require_file (name):
682             if name in output_dir_files:
683                 result.add (name)
684             else:
685                 missing.add (name)
686
687         # UGH - junk self.global_options
688         skip_lily = self.global_options.skip_lilypond_run
689         for required in [base + '.ly',
690                          base + '.txt']:
691             require_file (required)
692         if not skip_lily:
693             require_file (base + '-systems.count')
694
695         if 'ddump-profile' in self.global_options.process_cmd:
696             require_file (base + '.profile')
697         if 'dseparate-log-file' in self.global_options.process_cmd:
698             require_file (base + '.log')
699
700         map (consider_file, [base + '.tex',
701                              base + '.eps',
702                              base + '.texidoc',
703                              base + '.doctitle',
704                              base + '-systems.texi',
705                              base + '-systems.tex',
706                              base + '-systems.pdftexi'])
707         if self.formatter.document_language:
708             map (consider_file,
709                  [base + '.texidoc' + self.formatter.document_language,
710                   base + '.doctitle' + self.formatter.document_language])
711
712         required_files = self.formatter.required_files (self, base, full, result)
713         for f in required_files:
714             require_file (f)
715
716         system_count = 0
717         if not skip_lily and not missing:
718             system_count = int(file (full + '-systems.count').read())
719
720         for number in range(1, system_count + 1):
721             systemfile = '%s-%d' % (base, number)
722             require_file (systemfile + '.eps')
723             consider_file (systemfile + '.pdf')
724
725             # We can't require signatures, since books and toplevel
726             # markups do not output a signature.
727             if 'ddump-signature' in self.global_options.process_cmd:
728                 consider_file (systemfile + '.signature')
729
730         map (consider_file, self.additional_files_to_consider (base, full))
731         map (require_file, self.additional_files_required (base, full))
732
733         return (result, missing)
734
735     def is_outdated (self, output_dir, current_files):
736         found, missing = self.all_output_files (output_dir, current_files)
737         return missing
738
739     def filter_pipe (self, input, cmd):
740         """Pass input through cmd, and return the result."""
741
742         if self.global_options.verbose:
743             progress (_ ("Running through filter `%s'\n") % cmd)
744
745         # TODO: Use Popen once we resolve the problem with msvcrt in Windows:
746         (stdin, stdout, stderr) = os.popen3 (cmd)
747         # p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
748         # (stdin, stdout, stderr) = (p.stdin, p.stdout, p.stderr)
749         stdin.write (input)
750         status = stdin.close ()
751
752         if not status:
753             status = 0
754             output = stdout.read ()
755             status = stdout.close ()
756             err = stderr.read ()
757
758         if not status:
759             status = 0
760         signal = 0x0f & status
761         if status or (not output and err):
762             exit_status = status >> 8
763             ly.error (_ ("`%s' failed (%d)") % (cmd, exit_status))
764             ly.error (_ ("The error log is as follows:"))
765             ly.stderr_write (err)
766             ly.stderr_write (stderr.read ())
767             exit (status)
768
769         if self.global_options.verbose:
770             progress ('\n')
771
772         return output
773
774     def get_snippet_code (self):
775         return self.substring ('code');
776
777     def filter_text (self):
778         """Run snippet bodies through a command (say: convert-ly).
779
780         This functionality is rarely used, and this code must have bitrot.
781         """
782         code = self.get_snippet_code ();
783         s = self.filter_pipe (code, self.global_options.filter_cmd)
784         d = {
785             'code': s,
786             'options': self.match.group ('options')
787         }
788         return self.formatter.output_simple_replacements (FILTER, d)
789
790     def replacement_text (self):
791         base = self.final_basename ()
792         return self.formatter.snippet_output (base, self)
793
794     def get_images (self):
795         rep = {'base': self.final_basename ()}
796
797         single = '%(base)s.png' % rep
798         multiple = '%(base)s-page1.png' % rep
799         images = (single,)
800         if (os.path.exists (multiple)
801             and (not os.path.exists (single)
802                  or (os.stat (multiple)[stat.ST_MTIME]
803                      > os.stat (single)[stat.ST_MTIME]))):
804             count = ps_page_count ('%(base)s.eps' % rep)
805             images = ['%s-page%d.png' % (rep['base'], page) for page in range (1, count+1)]
806             images = tuple (images)
807
808         return images
809
810
811
812 re_begin_verbatim = re.compile (r'\s+%.*?begin verbatim.*\n*', re.M)
813 re_end_verbatim = re.compile (r'\s+%.*?end verbatim.*$', re.M)
814
815 class LilypondFileSnippet (LilypondSnippet):
816     def __init__ (self, type, match, formatter, line_number, global_options):
817         LilypondSnippet.__init__ (self, type, match, formatter, line_number, global_options)
818         self.filename = self.substring ('filename')
819         self.ext = os.path.splitext (os.path.basename (self.filename))[1]
820         self.contents = file (BookBase.find_file (self.filename, global_options.include_path)).read ()
821
822     def get_snippet_code (self):
823         return self.contents;
824
825     def verb_ly (self):
826         s = self.contents
827         s = re_begin_verbatim.split (s)[-1]
828         s = re_end_verbatim.split (s)[0]
829         if not NOGETTEXT in self.option_dict:
830             s = self.verb_ly_gettext (s)
831         if not s.endswith ('\n'):
832             s += '\n'
833         return s
834
835     def ly (self):
836         name = self.filename
837         return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
838                 % (name, self.contents))
839
840     def final_basename (self):
841         if self.global_options.use_source_file_names:
842             base = os.path.splitext (os.path.basename (self.filename))[0]
843             return base
844         else:
845             return self.basename ()
846
847
848 class MusicXMLFileSnippet (LilypondFileSnippet):
849     def __init__ (self, type, match, formatter, line_number, global_options):
850         LilypondFileSnippet.__init__ (self, type, match, formatter, line_number, global_options)
851         self.compressed = False
852         self.converted_ly = None
853         self.musicxml_options_dict = {
854             'verbose': '--verbose',
855             'lxml': '--lxml',
856             'compressed': '--compressed',
857             'relative': '--relative',
858             'absolute': '--absolute',
859             'no-articulation-directions': '--no-articulation-directions',
860             'no-rest-positions': '--no-rest-positions',
861             'no-page-layout': '--no-page-layout',
862             'no-beaming': '--no-beaming',
863             'language': '--language',
864          }
865
866     def snippet_options (self):
867         return self.musicxml_options_dict.keys ()
868
869     def convert_from_musicxml (self):
870         name = self.filename
871         option_list = []
872         for (key, value) in self.option_dict.items ():
873             cmd_key = self.musicxml_options_dict.get (key, None)
874             if cmd_key == None:
875                 continue
876             if value == None:
877                 option_list.append (cmd_key)
878             else:
879                 option_list.append (cmd_key + '=' + value)
880         if ('.mxl' in name) and ('--compressed' not in option_list):
881             option_list.append ('--compressed')
882             self.compressed = True
883         opts = " ".join (option_list)
884         progress (_ ("Converting MusicXML file `%s'...\n") % self.filename)
885
886         ly_code = self.filter_pipe (self.contents, 'musicxml2ly %s --out=- - ' % opts)
887         return ly_code
888
889     def ly (self):
890         if self.converted_ly == None:
891             self.converted_ly = self.convert_from_musicxml ()
892         name = self.filename
893         return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
894                 % (name, self.converted_ly))
895
896     def additional_files_required (self, base, full):
897         result = [];
898         if self.compressed:
899             result.append (base + '.mxl')
900         else:
901             result.append (base + '.xml')
902         return result
903
904     def write_ly (self):
905         base = self.basename ()
906         path = os.path.join (self.global_options.lily_output_dir, base)
907         directory = os.path.split(path)[0]
908         if not os.path.isdir (directory):
909             os.makedirs (directory)
910
911         # First write the XML to a file (so we can link it!)
912         if self.compressed:
913             xmlfilename = path + '.mxl'
914         else:
915             xmlfilename = path + '.xml'
916         if os.path.exists (xmlfilename):
917             diff_against_existing = self.filter_pipe (self.contents, 'diff -u %s - ' % xmlfilename)
918             if diff_against_existing:
919                 warning (_ ("%s: duplicate filename but different contents of orginal file,\n\
920 printing diff against existing file.") % xmlfilename)
921                 ly.stderr_write (diff_against_existing)
922         else:
923             out = file (xmlfilename, 'w')
924             out.write (self.contents)
925             out.close ()
926
927         # also write the converted lilypond
928         filename = path + '.ly'
929         if os.path.exists (filename):
930             diff_against_existing = self.filter_pipe (self.full_ly (), 'diff -u %s -' % filename)
931             if diff_against_existing:
932                 warning (_ ("%s: duplicate filename but different contents of converted lilypond file,\n\
933 printing diff against existing file.") % filename)
934                 ly.stderr_write (diff_against_existing)
935         else:
936             out = file (filename, 'w')
937             out.write (self.full_ly ())
938             out.close ()
939             file (path + '.txt', 'w').write ('image of music')
940
941
942
943 class LilyPondVersionString (Snippet):
944     """A string that does not require extra memory."""
945     def __init__ (self, type, match, formatter, line_number, global_options):
946         Snippet.__init__ (self, type, match, formatter, line_number, global_options)
947
948     def replacement_text (self):
949         return self.formatter.output_simple (self.type, self)
950
951
952 snippet_type_to_class = {
953     'lilypond_file': LilypondFileSnippet,
954     'lilypond_block': LilypondSnippet,
955     'lilypond': LilypondSnippet,
956     'include': IncludeSnippet,
957     'lilypondversion': LilyPondVersionString,
958     'musicxml_file': MusicXMLFileSnippet,
959 }