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