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