]> git.donarmstrong.com Git - lilypond.git/blob - python/book_snippets.py
Merge branch 'lilypond/translation' of ssh://git.sv.gnu.org/srv/git/lilypond into...
[lilypond.git] / python / book_snippets.py
1 # -*- coding: utf-8 -*-
2
3 import book_base as BookBase
4 import lilylib as ly
5 global _;_=ly._
6 import re
7 import os
8 import copy
9 # TODO: We are using os.popen3, which has been deprecated since python 2.6. The
10 # suggested replacement is the Popen function of the subprocess module.
11 # Unfortunately, on windows this needs the msvcrt module, which doesn't seem
12 # to be available in GUB?!?!?!
13 # from subprocess import Popen, PIPE
14
15 progress = ly.progress
16 warning = ly.warning
17 error = ly.error
18
19
20
21
22
23 ####################################################################
24 # Snippet option handling
25 ####################################################################
26
27
28 #
29 # Is this pythonic?  Personally, I find this rather #define-nesque. --hwn
30 #
31 # Global definitions:
32 ADDVERSION = 'addversion'
33 AFTER = 'after'
34 ALT = 'alt'
35 BEFORE = 'before'
36 DOCTITLE = 'doctitle'
37 EXAMPLEINDENT = 'exampleindent'
38 FILENAME = 'filename'
39 FILTER = 'filter'
40 FRAGMENT = 'fragment'
41 LANG = 'lang'    ## TODO: This is handled nowhere!
42 LAYOUT = 'layout'
43 LILYQUOTE = 'lilyquote'
44 LINE_WIDTH = 'line-width'
45 NOFRAGMENT = 'nofragment'
46 NOGETTEXT = 'nogettext'
47 NOINDENT = 'noindent'
48 NOQUOTE = 'noquote'
49 INDENT = 'indent'
50 NORAGGED_RIGHT = 'noragged-right'
51 NOTES = 'body'
52 NOTIME = 'notime'
53 OUTPUT = 'output'
54 OUTPUTIMAGE = 'outputimage'
55 PAPER = 'paper'
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 # NOQUOTE is used internally only.
71 no_options = {
72     NOFRAGMENT: FRAGMENT,
73     NOINDENT: INDENT,
74 }
75
76 # Options that have no impact on processing by lilypond (or --process
77 # argument)
78 PROCESSING_INDEPENDENT_OPTIONS = (
79     ALT, NOGETTEXT, VERBATIM, ADDVERSION,
80     TEXIDOC, DOCTITLE, VERSION, PRINTFILENAME)
81
82
83
84 # Options without a pattern in snippet_options.
85 simple_options = [
86     EXAMPLEINDENT,
87     FRAGMENT,
88     NOFRAGMENT,
89     NOGETTEXT,
90     NOINDENT,
91     PRINTFILENAME,
92     DOCTITLE,
93     TEXIDOC,
94     LANG,
95     VERBATIM,
96     FILENAME,
97     ALT,
98     ADDVERSION
99 ]
100
101
102
103 ####################################################################
104 # LilyPond templates for the snippets
105 ####################################################################
106
107 snippet_options = {
108     ##
109     NOTES: {
110         RELATIVE: r'''\relative c%(relative_quotes)s''',
111     },
112
113     ##
114     PAPER: {
115         INDENT: r'''indent = %(indent)s''',
116         LINE_WIDTH: r'''line-width = %(line-width)s''',
117         QUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
118         LILYQUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
119         RAGGED_RIGHT: r'''ragged-right = ##t''',
120         NORAGGED_RIGHT: r'''ragged-right = ##f''',
121     },
122
123     ##
124     LAYOUT: {
125         NOTIME: r'''
126  \context {
127    \Score
128    timing = ##f
129  }
130  \context {
131    \Staff
132    \remove "Time_signature_engraver"
133  }''',
134     },
135
136     ##
137     PREAMBLE: {
138         STAFFSIZE: r'''#(set-global-staff-size %(staffsize)s)''',
139     },
140 }
141
142
143
144
145
146 FRAGMENT_LY = r'''
147 %(notes_string)s
148 {
149
150
151 %% ****************************************************************
152 %% ly snippet contents follows:
153 %% ****************************************************************
154 %(code)s
155
156
157 %% ****************************************************************
158 %% end ly snippet
159 %% ****************************************************************
160 }
161 '''
162
163 def classic_lilypond_book_compatibility (key, 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   force-assignment = #""
198   line-width = #(- line-width (* mm  %(padding_mm)f))
199 }
200
201 \layout {
202   %(layout_string)s
203 }
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
222
223
224
225
226
227
228 ####################################################################
229 # Helper functions
230 ####################################################################
231
232 def ps_page_count (ps_name):
233     header = file (ps_name).read (1024)
234     m = re.search ('\n%%Pages: ([0-9]+)', header)
235     if m:
236         return int (m.group (1))
237     return 0
238
239 ly_var_def_re = re.compile (r'^([a-zA-Z]+)[\t ]*=', re.M)
240 ly_comment_re = re.compile (r'(%+[\t ]*)(.*)$', re.M)
241 ly_context_id_re = re.compile ('\\\\(?:new|context)\\s+(?:[a-zA-Z]*?(?:Staff\
242 (?:Group)?|Voice|FiguredBass|FretBoards|Names|Devnull))\\s+=\\s+"?([a-zA-Z]+)"?\\s+')
243
244 def ly_comment_gettext (t, m):
245     return m.group (1) + t (m.group (2))
246
247
248
249 class CompileError(Exception):
250   pass
251
252
253
254 ####################################################################
255 # Snippet classes
256 ####################################################################
257
258 class Chunk:
259     def replacement_text (self):
260         return ''
261
262     def filter_text (self):
263         return self.replacement_text ()
264
265     def is_plain (self):
266         return False
267
268 class Substring (Chunk):
269     """A string that does not require extra memory."""
270     def __init__ (self, source, start, end, line_number):
271         self.source = source
272         self.start = start
273         self.end = end
274         self.line_number = line_number
275         self.override_text = None
276
277     def is_plain (self):
278         return True
279
280     def replacement_text (self):
281         if self.override_text:
282             return self.override_text
283         else:
284             return self.source[self.start:self.end]
285
286
287
288 class Snippet (Chunk):
289     def __init__ (self, type, match, formatter, line_number, global_options):
290         self.type = type
291         self.match = match
292         self.checksum = 0
293         self.option_dict = {}
294         self.formatter = formatter
295         self.line_number = line_number
296         self.global_options = global_options
297         self.replacements = {'program_version': ly.program_version,
298                              'program_name': ly.program_name}
299
300     # return a shallow copy of the replacements, so the caller can modify
301     # it locally without interfering with other snippet operations
302     def get_replacements (self):
303         return copy.copy (self.replacements)
304
305     def replacement_text (self):
306         return self.match.group ('match')
307
308     def substring (self, s):
309         return self.match.group (s)
310
311     def __repr__ (self):
312         return `self.__class__` + ' type = ' + self.type
313
314
315
316 class IncludeSnippet (Snippet):
317     def processed_filename (self):
318         f = self.substring ('filename')
319         return os.path.splitext (f)[0] + self.formatter.default_extension
320
321     def replacement_text (self):
322         s = self.match.group ('match')
323         f = self.substring ('filename')
324         return re.sub (f, self.processed_filename (), s)
325
326
327
328 class LilypondSnippet (Snippet):
329     def __init__ (self, type, match, formatter, line_number, global_options):
330         Snippet.__init__ (self, type, match, formatter, line_number, global_options)
331         os = match.group ('options')
332         self.do_options (os, self.type)
333
334
335     def snippet_options (self):
336         return [];
337
338     def verb_ly_gettext (self, s):
339         lang = self.formatter.document_language
340         if not lang:
341             return s
342         try:
343             t = langdefs.translation[lang]
344         except:
345             return s
346
347         s = ly_comment_re.sub (lambda m: ly_comment_gettext (t, m), s)
348
349         if langdefs.LANGDICT[lang].enable_ly_identifier_l10n:
350             for v in ly_var_def_re.findall (s):
351                 s = re.sub (r"(?m)(?<!\\clef)(^|[' \\#])%s([^a-zA-Z])" % v,
352                             "\\1" + t (v) + "\\2",
353                             s)
354             for id in ly_context_id_re.findall (s):
355                 s = re.sub (r'(\s+|")%s(\s+|")' % id,
356                             "\\1" + t (id) + "\\2",
357                             s)
358         return s
359
360     def verb_ly (self):
361         verb_text = self.substring ('code')
362         if not NOGETTEXT in self.option_dict:
363             verb_text = self.verb_ly_gettext (verb_text)
364         if not verb_text.endswith ('\n'):
365             verb_text += '\n'
366         return verb_text
367
368     def ly (self):
369         contents = self.substring ('code')
370         return ('\\sourcefileline %d\n%s'
371                 % (self.line_number - 1, contents))
372
373     def full_ly (self):
374         s = self.ly ()
375         if s:
376             return self.compose_ly (s)
377         return ''
378
379     def split_options (self, option_string):
380         return self.formatter.split_snippet_options (option_string);
381
382     def do_options (self, option_string, type):
383         self.option_dict = {}
384
385         options = self.split_options (option_string)
386
387         for option in options:
388             if '=' in option:
389                 (key, value) = re.split ('\s*=\s*', option)
390                 self.option_dict[key] = value
391             else:
392                 if option in no_options:
393                     if no_options[option] in self.option_dict:
394                         del self.option_dict[no_options[option]]
395                 else:
396                     self.option_dict[option] = None
397
398
399         # If LINE_WIDTH is used without parameter, set it to default.
400         has_line_width = self.option_dict.has_key (LINE_WIDTH)
401         if has_line_width and self.option_dict[LINE_WIDTH] == None:
402             has_line_width = False
403             del self.option_dict[LINE_WIDTH]
404
405         # TODO: Can't we do that more efficiently (built-in python func?)
406         for k in self.formatter.default_snippet_options:
407             if k not in self.option_dict:
408                 self.option_dict[k] = self.formatter.default_snippet_options[k]
409
410         # RELATIVE does not work without FRAGMENT;
411         # make RELATIVE imply FRAGMENT
412         has_relative = self.option_dict.has_key (RELATIVE)
413         if has_relative and not self.option_dict.has_key (FRAGMENT):
414             self.option_dict[FRAGMENT] = None
415
416         if not has_line_width:
417             if type == 'lilypond' or FRAGMENT in self.option_dict:
418                 self.option_dict[RAGGED_RIGHT] = None
419
420             if type == 'lilypond':
421                 if LINE_WIDTH in self.option_dict:
422                     del self.option_dict[LINE_WIDTH]
423             else:
424                 if RAGGED_RIGHT in self.option_dict:
425                     if LINE_WIDTH in self.option_dict:
426                         del self.option_dict[LINE_WIDTH]
427
428             if QUOTE in self.option_dict or type == 'lilypond':
429                 if LINE_WIDTH in self.option_dict:
430                     del self.option_dict[LINE_WIDTH]
431
432         if not INDENT in self.option_dict:
433             self.option_dict[INDENT] = '0\\mm'
434
435         # Set a default line-width if there is none. We need this, because
436         # lilypond-book has set left-padding by default and therefore does
437         # #(define line-width (- line-width (* 3 mm)))
438         # TODO: Junk this ugly hack if the code gets rewritten to concatenate
439         # all settings before writing them in the \paper block.
440         if not LINE_WIDTH in self.option_dict:
441             if not QUOTE in self.option_dict:
442                 if not LILYQUOTE in self.option_dict:
443                     self.option_dict[LINE_WIDTH] = "#(- paper-width \
444 left-margin-default right-margin-default)"
445
446     def get_option_list (self):
447         if not 'option_list' in self.__dict__:
448             option_list = []
449             for (key, value) in self.option_dict.items ():
450                 if value == None:
451                     option_list.append (key)
452                 else:
453                     option_list.append (key + '=' + value)
454             option_list.sort ()
455             self.option_list = option_list
456         return self.option_list
457
458     def compose_ly (self, code):
459         if FRAGMENT in self.option_dict:
460             body = FRAGMENT_LY
461         else:
462             body = FULL_LY
463
464         # Defaults.
465         relative = 1
466         override = {}
467         # The original concept of the `exampleindent' option is broken.
468         # It is not possible to get a sane value for @exampleindent at all
469         # without processing the document itself.  Saying
470         #
471         #   @exampleindent 0
472         #   @example
473         #   ...
474         #   @end example
475         #   @exampleindent 5
476         #
477         # causes ugly results with the DVI backend of texinfo since the
478         # default value for @exampleindent isn't 5em but 0.4in (or a smaller
479         # value).  Executing the above code changes the environment
480         # indentation to an unknown value because we don't know the amount
481         # of 1em in advance since it is font-dependent.  Modifying
482         # @exampleindent in the middle of a document is simply not
483         # supported within texinfo.
484         #
485         # As a consequence, the only function of @exampleindent is now to
486         # specify the amount of indentation for the `quote' option.
487         #
488         # To set @exampleindent locally to zero, we use the @format
489         # environment for non-quoted snippets.
490         override[EXAMPLEINDENT] = r'0.4\in'
491         override[LINE_WIDTH] = '5\\in' # = texinfo_line_widths['@smallbook']
492         override.update (self.formatter.default_snippet_options)
493
494         option_list = []
495         for option in self.get_option_list ():
496             for name in PROCESSING_INDEPENDENT_OPTIONS:
497                 if option.startswith (name):
498                     break
499             else:
500                 option_list.append (option)
501         option_string = ','.join (option_list)
502         compose_dict = {}
503         compose_types = [NOTES, PREAMBLE, LAYOUT, PAPER]
504         for a in compose_types:
505             compose_dict[a] = []
506
507         option_names = self.option_dict.keys ()
508         option_names.sort ()
509         for key in option_names:
510             value = self.option_dict[key]
511             (c_key, c_value) = classic_lilypond_book_compatibility (key, value)
512             if c_key:
513                 if c_value:
514                     warning (
515                         _ ("deprecated ly-option used: %s=%s") % (key, value))
516                     warning (
517                         _ ("compatibility mode translation: %s=%s") % (c_key, c_value))
518                 else:
519                     warning (
520                         _ ("deprecated ly-option used: %s") % key)
521                     warning (
522                         _ ("compatibility mode translation: %s") % c_key)
523
524                 (key, value) = (c_key, c_value)
525
526             if value:
527                 override[key] = value
528             else:
529                 if not override.has_key (key):
530                     override[key] = None
531
532             found = 0
533             for typ in compose_types:
534                 if snippet_options[typ].has_key (key):
535                     compose_dict[typ].append (snippet_options[typ][key])
536                     found = 1
537                     break
538
539             if not found and key not in simple_options and key not in self.snippet_options ():
540                 warning (_ ("ignoring unknown ly option: %s") % key)
541
542         # URGS
543         if RELATIVE in override and override[RELATIVE]:
544             relative = int (override[RELATIVE])
545
546         relative_quotes = ''
547
548         # 1 = central C
549         if relative < 0:
550             relative_quotes += ',' * (- relative)
551         elif relative > 0:
552             relative_quotes += "'" * relative
553
554         paper_string = '\n  '.join (compose_dict[PAPER]) % override
555         layout_string = '\n  '.join (compose_dict[LAYOUT]) % override
556         notes_string = '\n  '.join (compose_dict[NOTES]) % vars ()
557         preamble_string = '\n  '.join (compose_dict[PREAMBLE]) % override
558         padding_mm = self.global_options.padding_mm
559
560         d = globals().copy()
561         d.update (locals())
562         d.update (self.global_options.information)
563         return (PREAMBLE_LY + body) % d
564
565     def get_checksum (self):
566         if not self.checksum:
567             # Work-around for md5 module deprecation warning in python 2.5+:
568             try:
569                 from hashlib import md5
570             except ImportError:
571                 from md5 import md5
572
573             # We only want to calculate the hash based on the snippet
574             # code plus fragment options relevant to processing by
575             # lilypond, not the snippet + preamble
576             hash = md5 (self.relevant_contents (self.ly ()))
577             for option in self.get_option_list ():
578                 for name in PROCESSING_INDEPENDENT_OPTIONS:
579                     if option.startswith (name):
580                         break
581                 else:
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 = '%s/lily-%s' % (cs[:2], 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 orginal 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             print '\nMissing', 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             os.link (src, dst)
647
648
649     def all_output_files (self, output_dir, output_dir_files):
650         """Return all files generated in lily_output_dir, a set.
651
652         output_dir_files is the list of files in the output directory.
653         """
654         result = set ()
655         missing = set ()
656         base = self.basename()
657         full = os.path.join (output_dir, base)
658         def consider_file (name):
659             if name in output_dir_files:
660                 result.add (name)
661
662         def require_file (name):
663             if name in output_dir_files:
664                 result.add (name)
665             else:
666                 missing.add (name)
667
668         # UGH - junk self.global_options
669         skip_lily = self.global_options.skip_lilypond_run
670         for required in [base + '.ly',
671                          base + '.txt']:
672             require_file (required)
673         if not skip_lily:
674             require_file (base + '-systems.count')
675
676         if 'ddump-profile' in self.global_options.process_cmd:
677             require_file (base + '.profile')
678         if 'dseparate-log-file' in self.global_options.process_cmd:
679             require_file (base + '.log')
680
681         map (consider_file, [base + '.tex',
682                              base + '.eps',
683                              base + '.texidoc',
684                              base + '.doctitle',
685                              base + '-systems.texi',
686                              base + '-systems.tex',
687                              base + '-systems.pdftexi'])
688         if self.formatter.document_language:
689             map (consider_file,
690                  [base + '.texidoc' + self.formatter.document_language,
691                   base + '.doctitle' + self.formatter.document_language])
692
693         required_files = self.formatter.required_files (self, base, full, result)
694         for f in required_files:
695             require_file (f)
696
697         system_count = 0
698         if not skip_lily and not missing:
699             system_count = int(file (full + '-systems.count').read())
700
701         for number in range(1, system_count + 1):
702             systemfile = '%s-%d' % (base, number)
703             require_file (systemfile + '.eps')
704             consider_file (systemfile + '.pdf')
705
706             # We can't require signatures, since books and toplevel
707             # markups do not output a signature.
708             if 'ddump-signature' in self.global_options.process_cmd:
709                 consider_file (systemfile + '.signature')
710
711
712         return (result, missing)
713
714     def is_outdated (self, output_dir, current_files):
715         found, missing = self.all_output_files (output_dir, current_files)
716         return missing
717
718     def filter_pipe (self, input, cmd):
719         """Pass input through cmd, and return the result."""
720
721         if self.global_options.verbose:
722             progress (_ ("Opening filter `%s'\n") % cmd)
723
724         # TODO: Use Popen once we resolve the problem with msvcrt in Windows:
725         (stdin, stdout, stderr) = os.popen3 (cmd)
726         # p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
727         # (stdin, stdout, stderr) = (p.stdin, p.stdout, p.stderr)
728         stdin.write (input)
729         status = stdin.close ()
730
731         if not status:
732             status = 0
733             output = stdout.read ()
734             status = stdout.close ()
735             error = stderr.read ()
736
737         if not status:
738             status = 0
739         signal = 0x0f & status
740         if status or (not output and error):
741             exit_status = status >> 8
742             ly.error (_ ("`%s' failed (%d)") % (cmd, exit_status))
743             ly.error (_ ("The error log is as follows:"))
744             ly.stderr_write (error)
745             ly.stderr_write (stderr.read ())
746             exit (status)
747
748         if self.global_options.verbose:
749             progress ('\n')
750
751         return output
752
753     def get_snippet_code (self):
754         return self.substring ('code');
755
756     def filter_text (self):
757         """Run snippet bodies through a command (say: convert-ly).
758
759         This functionality is rarely used, and this code must have bitrot.
760         """
761         code = self.get_snippet_code ();
762         s = self.filter_pipe (code, self.global_options.filter_cmd)
763         d = {
764             'code': s,
765             'options': self.match.group ('options')
766         }
767         return self.formatter.output_simple_replacements (FILTER, d)
768
769     def replacement_text (self):
770         base = self.final_basename ()
771         return self.formatter.snippet_output (base, self)
772
773     def get_images (self):
774         rep = {'base': self.final_basename ()}
775
776         single = '%(base)s.png' % rep
777         multiple = '%(base)s-page1.png' % rep
778         images = (single,)
779         if (os.path.exists (multiple)
780             and (not os.path.exists (single)
781                  or (os.stat (multiple)[stat.ST_MTIME]
782                      > os.stat (single)[stat.ST_MTIME]))):
783             count = ps_page_count ('%(base)s.eps' % rep)
784             images = ['%s-page%d.png' % (rep['base'], page) for page in range (1, count+1)]
785             images = tuple (images)
786
787         return images
788
789
790
791 re_begin_verbatim = re.compile (r'\s+%.*?begin verbatim.*\n*', re.M)
792 re_end_verbatim = re.compile (r'\s+%.*?end verbatim.*$', re.M)
793
794 class LilypondFileSnippet (LilypondSnippet):
795     def __init__ (self, type, match, formatter, line_number, global_options):
796         LilypondSnippet.__init__ (self, type, match, formatter, line_number, global_options)
797         self.contents = file (BookBase.find_file (self.substring ('filename'), global_options.include_path)).read ()
798
799     def get_snippet_code (self):
800         return self.contents;
801
802     def verb_ly (self):
803         s = self.contents
804         s = re_begin_verbatim.split (s)[-1]
805         s = re_end_verbatim.split (s)[0]
806         if not NOGETTEXT in self.option_dict:
807             s = self.verb_ly_gettext (s)
808         if not s.endswith ('\n'):
809             s += '\n'
810         return s
811
812     def ly (self):
813         name = self.substring ('filename')
814         return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
815                 % (name, self.contents))
816
817     def final_basename (self):
818         if self.global_options.use_source_file_names:
819             base = os.path.splitext (os.path.basename (self.substring ('filename')))[0]
820             return base
821         else:
822             return self.basename ()
823
824
825 class LilyPondVersionString (Snippet):
826     """A string that does not require extra memory."""
827     def __init__ (self, type, match, formatter, line_number, global_options):
828         Snippet.__init__ (self, type, match, formatter, line_number, global_options)
829
830     def replacement_text (self):
831         return self.formatter.output_simple (self.type, self)
832
833
834 snippet_type_to_class = {
835     'lilypond_file': LilypondFileSnippet,
836     'lilypond_block': LilypondSnippet,
837     'lilypond': LilypondSnippet,
838     'include': IncludeSnippet,
839     'lilypondversion': LilyPondVersionString,
840 }