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