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