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