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