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