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