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