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