1 # -*- coding: utf-8 -*-
3 import book_base as BookBase
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
15 progress = ly.progress
23 ####################################################################
24 # Snippet option handling
25 ####################################################################
29 # Is this pythonic? Personally, I find this rather #define-nesque. --hwn
32 ADDVERSION = 'addversion'
37 EXAMPLEINDENT = 'exampleindent'
42 LILYQUOTE = 'lilyquote'
43 LINE_WIDTH = 'line-width'
44 NOFRAGMENT = 'nofragment'
45 NOGETTEXT = 'nogettext'
49 NORAGGED_RIGHT = 'noragged-right'
53 OUTPUTIMAGE = 'outputimage'
55 PAPERSIZE = 'papersize'
57 PRINTFILENAME = 'printfilename'
59 RAGGED_RIGHT = 'ragged-right'
61 STAFFSIZE = 'staffsize'
64 VERSION = 'lilypondversion'
68 # NOTIME and NOGETTEXT have no opposite so they aren't part of this
70 # NOQUOTE is used internally only.
76 # Options that have no impact on processing by lilypond (or --process
78 PROCESSING_INDEPENDENT_OPTIONS = (
79 ALT, NOGETTEXT, VERBATIM, ADDVERSION,
80 TEXIDOC, DOCTITLE, VERSION, PRINTFILENAME)
84 # Options without a pattern in snippet_options.
102 ####################################################################
103 # LilyPond templates for the snippets
104 ####################################################################
109 RELATIVE: r'''\relative c%(relative_quotes)s''',
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''',
132 \remove "Time_signature_engraver"
138 STAFFSIZE: r'''#(set-global-staff-size %(staffsize)s)''',
146 def classic_lilypond_book_compatibility (key, value):
147 if key == 'singleline' and value == None:
148 return (RAGGED_RIGHT, None)
150 m = re.search ('relative\s*([-0-9])', key)
152 return ('relative', m.group (1))
154 m = re.match ('([0-9]+)pt', key)
156 return ('staffsize', m.group (1))
158 if key == 'indent' or key == 'line-width':
159 m = re.match ('([-.0-9]+)(cm|in|mm|pt|staffspace)', value)
161 f = float (m.group (1))
162 return (key, '%f\\%s' % (f, m.group (2)))
167 PREAMBLE_LY = '''%%%% Generated by %(program_name)s
168 %%%% Options: [%(option_string)s]
169 \\include "lilypond-book-preamble.ly"
172 %% ****************************************************************
173 %% Start cut-&-pastable-section
174 %% ****************************************************************
180 line-width = #(- line-width (* mm %(padding_mm)f))
194 %% ****************************************************************
196 %% ****************************************************************
200 %% ****************************************************************
202 %% ****************************************************************
210 %% ****************************************************************
211 %% ly snippet contents follows:
212 %% ****************************************************************
216 %% ****************************************************************
218 %% ****************************************************************
225 ####################################################################
227 ####################################################################
229 def ps_page_count (ps_name):
230 header = file (ps_name).read (1024)
231 m = re.search ('\n%%Pages: ([0-9]+)', header)
233 return int (m.group (1))
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+')
241 def ly_comment_gettext (t, m):
242 return m.group (1) + t (m.group (2))
246 class CompileError(Exception):
251 ####################################################################
253 ####################################################################
256 def replacement_text (self):
259 def filter_text (self):
260 return self.replacement_text ()
265 class Substring (Chunk):
266 """A string that does not require extra memory."""
267 def __init__ (self, source, start, end, line_number):
271 self.line_number = line_number
272 self.override_text = None
277 def replacement_text (self):
278 if self.override_text:
279 return self.override_text
281 return self.source[self.start:self.end]
285 class Snippet (Chunk):
286 def __init__ (self, type, match, formatter, line_number, global_options):
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}
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)
302 def replacement_text (self):
303 return self.match.group ('match')
305 def substring (self, s):
306 return self.match.group (s)
309 return `self.__class__` + ' type = ' + self.type
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
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)
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)
332 def snippet_options (self):
335 def verb_ly_gettext (self, s):
336 lang = self.formatter.document_language
340 t = langdefs.translation[lang]
344 s = ly_comment_re.sub (lambda m: ly_comment_gettext (t, m), s)
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",
351 for id in ly_context_id_re.findall (s):
352 s = re.sub (r'(\s+|")%s(\s+|")' % id,
353 "\\1" + t (id) + "\\2",
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'):
366 contents = self.substring ('code')
367 return ('\\sourcefileline %d\n%s'
368 % (self.line_number - 1, contents))
373 return self.compose_ly (s)
376 def split_options (self, option_string):
377 return self.formatter.split_snippet_options (option_string);
379 def do_options (self, option_string, type):
380 self.option_dict = {}
382 options = self.split_options (option_string)
384 for option in options:
386 (key, value) = re.split ('\s*=\s*', option)
387 self.option_dict[key] = value
389 if option in no_options:
390 if no_options[option] in self.option_dict:
391 del self.option_dict[no_options[option]]
393 self.option_dict[option] = None
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]
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]
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
413 if not has_line_width:
414 if type == 'lilypond' or FRAGMENT in self.option_dict:
415 self.option_dict[RAGGED_RIGHT] = None
417 if type == 'lilypond':
418 if LINE_WIDTH in self.option_dict:
419 del self.option_dict[LINE_WIDTH]
421 if RAGGED_RIGHT in self.option_dict:
422 if LINE_WIDTH in self.option_dict:
423 del self.option_dict[LINE_WIDTH]
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]
429 if not INDENT in self.option_dict:
430 self.option_dict[INDENT] = '0\\mm'
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)"
443 def get_option_list (self):
444 if not 'option_list' in self.__dict__:
446 for (key, value) in self.option_dict.items ():
448 option_list.append (key)
450 option_list.append (key + '=' + value)
452 self.option_list = option_list
453 return self.option_list
455 def compose_ly (self, code):
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
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.
478 # As a consequence, the only function of @exampleindent is now to
479 # specify the amount of indentation for the `quote' option.
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)
488 for option in self.get_option_list ():
489 for name in PROCESSING_INDEPENDENT_OPTIONS:
490 if option.startswith (name):
493 option_list.append (option)
494 option_string = ','.join (option_list)
496 compose_types = [NOTES, PREAMBLE, LAYOUT, PAPER]
497 for a in compose_types:
500 option_names = self.option_dict.keys ()
502 for key in option_names:
503 value = self.option_dict[key]
504 (c_key, c_value) = classic_lilypond_book_compatibility (key, value)
508 _ ("deprecated ly-option used: %s=%s") % (key, value))
510 _ ("compatibility mode translation: %s=%s") % (c_key, c_value))
513 _ ("deprecated ly-option used: %s") % key)
515 _ ("compatibility mode translation: %s") % c_key)
517 (key, value) = (c_key, c_value)
520 override[key] = value
522 if not override.has_key (key):
526 for typ in compose_types:
527 if snippet_options[typ].has_key (key):
528 compose_dict[typ].append (snippet_options[typ][key])
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)
536 if RELATIVE in override and override[RELATIVE]:
537 relative = int (override[RELATIVE])
543 relative_quotes += ',' * (- relative)
545 relative_quotes += "'" * relative
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))
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)"
561 safe_mode_string = ""
565 d.update (self.global_options.information)
566 if FRAGMENT in self.option_dict:
570 return (PREAMBLE_LY + body) % d
572 def get_checksum (self):
573 if not self.checksum:
574 # Work-around for md5 module deprecation warning in python 2.5+:
576 from hashlib import md5
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):
591 ## let's not create too long names.
592 self.checksum = hash.hexdigest ()[:10]
597 cs = self.get_checksum ()
598 name = '%s/lily-%s' % (cs[:2], cs[2:])
601 final_basename = basename
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 ()
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))
618 out = file (filename, 'w')
619 out.write (self.full_ly ())
620 file (path + '.txt', 'w').write ('image of music')
622 def relevant_contents (self, ly):
623 return re.sub (r'\\(version|sourcefileline|sourcefilename)[^\n]*\n', '', ly)
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)
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]
640 final_name = self.final_basename () + base_suffix + ext
644 os.unlink (os.path.join (destination, final_name))
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)
655 def additional_files_to_consider (self, base, full):
657 def additional_files_required (self, base, full):
661 def all_output_files (self, output_dir, output_dir_files):
662 """Return all files generated in lily_output_dir, a set.
664 output_dir_files is the list of files in the output directory.
668 base = self.basename()
669 full = os.path.join (output_dir, base)
670 def consider_file (name):
671 if name in output_dir_files:
674 def require_file (name):
675 if name in output_dir_files:
680 # UGH - junk self.global_options
681 skip_lily = self.global_options.skip_lilypond_run
682 for required in [base + '.ly',
684 require_file (required)
686 require_file (base + '-systems.count')
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')
693 map (consider_file, [base + '.tex',
697 base + '-systems.texi',
698 base + '-systems.tex',
699 base + '-systems.pdftexi'])
700 if self.formatter.document_language:
702 [base + '.texidoc' + self.formatter.document_language,
703 base + '.doctitle' + self.formatter.document_language])
705 required_files = self.formatter.required_files (self, base, full, result)
706 for f in required_files:
710 if not skip_lily and not missing:
711 system_count = int(file (full + '-systems.count').read())
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')
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')
723 map (consider_file, self.additional_files_to_consider (base, full))
724 map (require_file, self.additional_files_required (base, full))
726 return (result, missing)
728 def is_outdated (self, output_dir, current_files):
729 found, missing = self.all_output_files (output_dir, current_files)
732 def filter_pipe (self, input, cmd):
733 """Pass input through cmd, and return the result."""
735 if self.global_options.verbose:
736 progress (_ ("Running through filter `%s'\n") % cmd)
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)
743 status = stdin.close ()
747 output = stdout.read ()
748 status = stdout.close ()
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 ())
762 if self.global_options.verbose:
767 def get_snippet_code (self):
768 return self.substring ('code');
770 def filter_text (self):
771 """Run snippet bodies through a command (say: convert-ly).
773 This functionality is rarely used, and this code must have bitrot.
775 code = self.get_snippet_code ();
776 s = self.filter_pipe (code, self.global_options.filter_cmd)
779 'options': self.match.group ('options')
781 return self.formatter.output_simple_replacements (FILTER, d)
783 def replacement_text (self):
784 base = self.final_basename ()
785 return self.formatter.snippet_output (base, self)
787 def get_images (self):
788 rep = {'base': self.final_basename ()}
790 single = '%(base)s.png' % rep
791 multiple = '%(base)s-page1.png' % rep
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)
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)
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 ()
815 def get_snippet_code (self):
816 return 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'):
830 return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
831 % (name, self.contents))
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]
838 return self.basename ()
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',
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',
859 def snippet_options (self):
860 return self.musicxml_options_dict.keys ()
862 def convert_from_musicxml (self):
864 xml2ly_option_list = []
865 for (key, value) in self.option_dict.items ():
866 cmd_key = self.musicxml_options_dict.get (key, None)
870 xml2ly_option_list.append (cmd_key)
872 xml2ly_option_list.append (cmd_key + '=' + value)
873 if ('.mxl' in name) and ('--compressed' not in xml2ly_option_list):
874 xml2ly_option_list.append ('--compressed')
875 self.compressed = True
876 opts = " ".join (xml2ly_option_list)
877 progress (_ ("Converting MusicXML file `%s'...\n") % self.filename)
879 ly_code = self.filter_pipe (self.contents, 'musicxml2ly %s --out=- - ' % opts)
883 if self.converted_ly == None:
884 self.converted_ly = self.convert_from_musicxml ()
886 return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
887 % (name, self.converted_ly))
889 def additional_files_required (self, base, full):
892 result.append (base + '.mxl')
894 result.append (base + '.xml')
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)
904 # First write the XML to a file (so we can link it!)
906 xmlfilename = path + '.mxl'
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)
916 out = file (xmlfilename, 'w')
917 out.write (self.contents)
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)
929 out = file (filename, 'w')
930 out.write (self.full_ly ())
932 file (path + '.txt', 'w').write ('image of music')
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)
941 def replacement_text (self):
942 return self.formatter.output_simple (self.type, self)
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,