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'
48 NORAGGED_RIGHT = 'noragged-right'
52 OUTPUTIMAGE = 'outputimage'
54 PAPERSIZE = 'papersize'
56 PRINTFILENAME = 'printfilename'
58 RAGGED_RIGHT = 'ragged-right'
60 STAFFSIZE = 'staffsize'
63 VERSION = 'lilypondversion'
67 # NOTIME and NOGETTEXT have no opposite so they aren't part of this
74 # Options that have no impact on processing by lilypond (or --process
76 PROCESSING_INDEPENDENT_OPTIONS = (
77 ALT, NOGETTEXT, VERBATIM, ADDVERSION,
78 TEXIDOC, DOCTITLE, VERSION, PRINTFILENAME)
82 # Options without a pattern in snippet_options.
100 ####################################################################
101 # LilyPond templates for the snippets
102 ####################################################################
107 RELATIVE: r'''\relative c%(relative_quotes)s''',
112 PAPERSIZE: r'''#(set-paper-size "%(papersize)s")''',
113 INDENT: r'''indent = %(indent)s''',
114 LINE_WIDTH: r'''line-width = %(line-width)s''',
115 QUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
116 LILYQUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
117 RAGGED_RIGHT: r'''ragged-right = ##t''',
118 NORAGGED_RIGHT: r'''ragged-right = ##f''',
130 \remove "Time_signature_engraver"
136 STAFFSIZE: r'''#(set-global-staff-size %(staffsize)s)''',
144 def classic_lilypond_book_compatibility (key, value):
145 if key == 'singleline' and value == None:
146 return (RAGGED_RIGHT, None)
148 m = re.search ('relative\s*([-0-9])', key)
150 return ('relative', m.group (1))
152 m = re.match ('([0-9]+)pt', key)
154 return ('staffsize', m.group (1))
156 if key == 'indent' or key == 'line-width':
157 m = re.match ('([-.0-9]+)(cm|in|mm|pt|staffspace)', value)
159 f = float (m.group (1))
160 return (key, '%f\\%s' % (f, m.group (2)))
165 PREAMBLE_LY = '''%%%% Generated by %(program_name)s
166 %%%% Options: [%(option_string)s]
167 \\include "lilypond-book-preamble.ly"
170 %% ****************************************************************
171 %% Start cut-&-pastable-section
172 %% ****************************************************************
178 line-width = #(- line-width (* mm %(padding_mm)f))
192 %% ****************************************************************
194 %% ****************************************************************
198 %% ****************************************************************
200 %% ****************************************************************
208 %% ****************************************************************
209 %% ly snippet contents follows:
210 %% ****************************************************************
214 %% ****************************************************************
216 %% ****************************************************************
223 ####################################################################
225 ####################################################################
227 def ps_page_count (ps_name):
228 header = file (ps_name).read (1024)
229 m = re.search ('\n%%Pages: ([0-9]+)', header)
231 return int (m.group (1))
234 ly_var_def_re = re.compile (r'^([a-zA-Z]+)[\t ]*=', re.M)
235 ly_comment_re = re.compile (r'(%+[\t ]*)(.*)$', re.M)
236 ly_context_id_re = re.compile ('\\\\(?:new|context)\\s+(?:[a-zA-Z]*?(?:Staff\
237 (?:Group)?|Voice|FiguredBass|FretBoards|Names|Devnull))\\s+=\\s+"?([a-zA-Z]+)"?\\s+')
239 def ly_comment_gettext (t, m):
240 return m.group (1) + t (m.group (2))
244 class CompileError(Exception):
249 ####################################################################
251 ####################################################################
254 def replacement_text (self):
257 def filter_text (self):
258 return self.replacement_text ()
263 class Substring (Chunk):
264 """A string that does not require extra memory."""
265 def __init__ (self, source, start, end, line_number):
269 self.line_number = line_number
270 self.override_text = None
275 def replacement_text (self):
276 if self.override_text:
277 return self.override_text
279 return self.source[self.start:self.end]
283 class Snippet (Chunk):
284 def __init__ (self, type, match, formatter, line_number, global_options):
288 self.option_dict = {}
289 self.formatter = formatter
290 self.line_number = line_number
291 self.global_options = global_options
292 self.replacements = {'program_version': ly.program_version,
293 'program_name': ly.program_name}
295 # return a shallow copy of the replacements, so the caller can modify
296 # it locally without interfering with other snippet operations
297 def get_replacements (self):
298 return copy.copy (self.replacements)
300 def replacement_text (self):
301 return self.match.group ('match')
303 def substring (self, s):
304 return self.match.group (s)
307 return `self.__class__` + ' type = ' + self.type
311 class IncludeSnippet (Snippet):
312 def processed_filename (self):
313 f = self.substring ('filename')
314 return os.path.splitext (f)[0] + self.formatter.default_extension
316 def replacement_text (self):
317 s = self.match.group ('match')
318 f = self.substring ('filename')
319 return re.sub (f, self.processed_filename (), s)
323 class LilypondSnippet (Snippet):
324 def __init__ (self, type, match, formatter, line_number, global_options):
325 Snippet.__init__ (self, type, match, formatter, line_number, global_options)
326 os = match.group ('options')
327 self.do_options (os, self.type)
330 def snippet_options (self):
333 def verb_ly_gettext (self, s):
334 lang = self.formatter.document_language
338 t = langdefs.translation[lang]
342 s = ly_comment_re.sub (lambda m: ly_comment_gettext (t, m), s)
344 if langdefs.LANGDICT[lang].enable_ly_identifier_l10n:
345 for v in ly_var_def_re.findall (s):
346 s = re.sub (r"(?m)(?<!\\clef)(^|[' \\#])%s([^a-zA-Z])" % v,
347 "\\1" + t (v) + "\\2",
349 for id in ly_context_id_re.findall (s):
350 s = re.sub (r'(\s+|")%s(\s+|")' % id,
351 "\\1" + t (id) + "\\2",
356 verb_text = self.substring ('code')
357 if not NOGETTEXT in self.option_dict:
358 verb_text = self.verb_ly_gettext (verb_text)
359 if not verb_text.endswith ('\n'):
364 contents = self.substring ('code')
365 return ('\\sourcefileline %d\n%s'
366 % (self.line_number - 1, contents))
371 return self.compose_ly (s)
374 def split_options (self, option_string):
375 return self.formatter.split_snippet_options (option_string);
377 def do_options (self, option_string, type):
378 self.option_dict = {}
380 options = self.split_options (option_string)
382 for option in options:
384 (key, value) = re.split ('\s*=\s*', option)
385 self.option_dict[key] = value
387 if option in no_options:
388 if no_options[option] in self.option_dict:
389 del self.option_dict[no_options[option]]
391 self.option_dict[option] = None
394 # If LINE_WIDTH is used without parameter, set it to default.
395 has_line_width = self.option_dict.has_key (LINE_WIDTH)
396 if has_line_width and self.option_dict[LINE_WIDTH] == None:
397 has_line_width = False
398 del self.option_dict[LINE_WIDTH]
400 # TODO: Can't we do that more efficiently (built-in python func?)
401 for k in self.formatter.default_snippet_options:
402 if k not in self.option_dict:
403 self.option_dict[k] = self.formatter.default_snippet_options[k]
405 # RELATIVE does not work without FRAGMENT;
406 # make RELATIVE imply FRAGMENT
407 has_relative = self.option_dict.has_key (RELATIVE)
408 if has_relative and not self.option_dict.has_key (FRAGMENT):
409 self.option_dict[FRAGMENT] = None
411 if not has_line_width:
412 if type == 'lilypond' or FRAGMENT in self.option_dict:
413 self.option_dict[RAGGED_RIGHT] = None
415 if type == 'lilypond':
416 if LINE_WIDTH in self.option_dict:
417 del self.option_dict[LINE_WIDTH]
419 if RAGGED_RIGHT in self.option_dict:
420 if LINE_WIDTH in self.option_dict:
421 del self.option_dict[LINE_WIDTH]
423 if QUOTE in self.option_dict or type == 'lilypond':
424 if LINE_WIDTH in self.option_dict:
425 del self.option_dict[LINE_WIDTH]
427 if not INDENT in self.option_dict:
428 self.option_dict[INDENT] = '0\\mm'
430 # Set a default line-width if there is none. We need this, because
431 # lilypond-book has set left-padding by default and therefore does
432 # #(define line-width (- line-width (* 3 mm)))
433 # TODO: Junk this ugly hack if the code gets rewritten to concatenate
434 # all settings before writing them in the \paper block.
435 if not LINE_WIDTH in self.option_dict:
436 if not QUOTE in self.option_dict:
437 if not LILYQUOTE in self.option_dict:
438 self.option_dict[LINE_WIDTH] = "#(- paper-width \
439 left-margin-default right-margin-default)"
441 def get_option_list (self):
442 if not 'option_list' in self.__dict__:
444 for (key, value) in self.option_dict.items ():
446 option_list.append (key)
448 option_list.append (key + '=' + value)
450 self.option_list = option_list
451 return self.option_list
453 def compose_ly (self, code):
458 # The original concept of the `exampleindent' option is broken.
459 # It is not possible to get a sane value for @exampleindent at all
460 # without processing the document itself. Saying
468 # causes ugly results with the DVI backend of texinfo since the
469 # default value for @exampleindent isn't 5em but 0.4in (or a smaller
470 # value). Executing the above code changes the environment
471 # indentation to an unknown value because we don't know the amount
472 # of 1em in advance since it is font-dependent. Modifying
473 # @exampleindent in the middle of a document is simply not
474 # supported within texinfo.
476 # As a consequence, the only function of @exampleindent is now to
477 # specify the amount of indentation for the `quote' option.
479 # To set @exampleindent locally to zero, we use the @format
480 # environment for non-quoted snippets.
481 override[EXAMPLEINDENT] = r'0.4\in'
482 override[LINE_WIDTH] = '5\\in' # = texinfo_line_widths['@smallbook']
483 override.update (self.formatter.default_snippet_options)
486 for option in self.get_option_list ():
487 for name in PROCESSING_INDEPENDENT_OPTIONS:
488 if option.startswith (name):
491 option_list.append (option)
492 option_string = ','.join (option_list)
494 compose_types = [NOTES, PREAMBLE, LAYOUT, PAPER]
495 for a in compose_types:
498 option_names = self.option_dict.keys ()
500 for key in option_names:
501 value = self.option_dict[key]
502 (c_key, c_value) = classic_lilypond_book_compatibility (key, value)
506 _ ("deprecated ly-option used: %s=%s") % (key, value))
508 _ ("compatibility mode translation: %s=%s") % (c_key, c_value))
511 _ ("deprecated ly-option used: %s") % key)
513 _ ("compatibility mode translation: %s") % c_key)
515 (key, value) = (c_key, c_value)
518 override[key] = value
520 if not override.has_key (key):
524 for typ in compose_types:
525 if snippet_options[typ].has_key (key):
526 compose_dict[typ].append (snippet_options[typ][key])
530 if not found and key not in simple_options and key not in self.snippet_options ():
531 warning (_ ("ignoring unknown ly option: %s") % key)
534 if RELATIVE in override and override[RELATIVE]:
535 relative = int (override[RELATIVE])
541 relative_quotes += ',' * (- relative)
543 relative_quotes += "'" * relative
545 # put paper-size first, if it exists
546 for i,elem in enumerate(compose_dict[PAPER]):
547 if elem.startswith("#(set-paper-size"):
548 compose_dict[PAPER].insert(0, compose_dict[PAPER].pop(i))
551 paper_string = '\n '.join (compose_dict[PAPER]) % override
552 layout_string = '\n '.join (compose_dict[LAYOUT]) % override
553 notes_string = '\n '.join (compose_dict[NOTES]) % vars ()
554 preamble_string = '\n '.join (compose_dict[PREAMBLE]) % override
555 padding_mm = self.global_options.padding_mm
556 if self.global_options.safe_mode:
557 safe_mode_string = "#(ly:set-option 'safe #t)"
559 safe_mode_string = ""
563 d.update (self.global_options.information)
564 if FRAGMENT in self.option_dict:
568 return (PREAMBLE_LY + body) % d
570 def get_checksum (self):
571 if not self.checksum:
572 # Work-around for md5 module deprecation warning in python 2.5+:
574 from hashlib import md5
578 # We only want to calculate the hash based on the snippet
579 # code plus fragment options relevant to processing by
580 # lilypond, not the snippet + preamble
581 hash = md5 (self.relevant_contents (self.ly ()))
582 for option in self.get_option_list ():
583 for name in PROCESSING_INDEPENDENT_OPTIONS:
584 if option.startswith (name):
589 ## let's not create too long names.
590 self.checksum = hash.hexdigest ()[:10]
595 cs = self.get_checksum ()
596 name = '%s/lily-%s' % (cs[:2], cs[2:])
599 final_basename = basename
602 base = self.basename ()
603 path = os.path.join (self.global_options.lily_output_dir, base)
604 directory = os.path.split(path)[0]
605 if not os.path.isdir (directory):
606 os.makedirs (directory)
607 filename = path + '.ly'
608 if os.path.exists (filename):
609 existing = open (filename, 'r').read ()
611 if self.relevant_contents (existing) != self.relevant_contents (self.full_ly ()):
612 warning ("%s: duplicate filename but different contents of orginal file,\n\
613 printing diff against existing file." % filename)
614 ly.stderr_write (self.filter_pipe (self.full_ly (), 'diff -u %s -' % filename))
616 out = file (filename, 'w')
617 out.write (self.full_ly ())
618 file (path + '.txt', 'w').write ('image of music')
620 def relevant_contents (self, ly):
621 return re.sub (r'\\(version|sourcefileline|sourcefilename)[^\n]*\n', '', ly)
623 def link_all_output_files (self, output_dir, output_dir_files, destination):
624 existing, missing = self.all_output_files (output_dir, output_dir_files)
626 print '\nMissing', missing
627 raise CompileError(self.basename())
628 for name in existing:
629 if (self.global_options.use_source_file_names
630 and isinstance (self, LilypondFileSnippet)):
631 base, ext = os.path.splitext (name)
632 components = base.split ('-')
633 # ugh, assume filenames with prefix with one dash (lily-xxxx)
634 if len (components) > 2:
635 base_suffix = '-' + components[-1]
638 final_name = self.final_basename () + base_suffix + ext
642 os.unlink (os.path.join (destination, final_name))
646 src = os.path.join (output_dir, name)
647 dst = os.path.join (destination, final_name)
648 dst_path = os.path.split(dst)[0]
649 if not os.path.isdir (dst_path):
650 os.makedirs (dst_path)
653 def additional_files_to_consider (self, base, full):
655 def additional_files_required (self, base, full):
659 def all_output_files (self, output_dir, output_dir_files):
660 """Return all files generated in lily_output_dir, a set.
662 output_dir_files is the list of files in the output directory.
666 base = self.basename()
667 full = os.path.join (output_dir, base)
668 def consider_file (name):
669 if name in output_dir_files:
672 def require_file (name):
673 if name in output_dir_files:
678 # UGH - junk self.global_options
679 skip_lily = self.global_options.skip_lilypond_run
680 for required in [base + '.ly',
682 require_file (required)
684 require_file (base + '-systems.count')
686 if 'ddump-profile' in self.global_options.process_cmd:
687 require_file (base + '.profile')
688 if 'dseparate-log-file' in self.global_options.process_cmd:
689 require_file (base + '.log')
691 map (consider_file, [base + '.tex',
695 base + '-systems.texi',
696 base + '-systems.tex',
697 base + '-systems.pdftexi'])
698 if self.formatter.document_language:
700 [base + '.texidoc' + self.formatter.document_language,
701 base + '.doctitle' + self.formatter.document_language])
703 required_files = self.formatter.required_files (self, base, full, result)
704 for f in required_files:
708 if not skip_lily and not missing:
709 system_count = int(file (full + '-systems.count').read())
711 for number in range(1, system_count + 1):
712 systemfile = '%s-%d' % (base, number)
713 require_file (systemfile + '.eps')
714 consider_file (systemfile + '.pdf')
716 # We can't require signatures, since books and toplevel
717 # markups do not output a signature.
718 if 'ddump-signature' in self.global_options.process_cmd:
719 consider_file (systemfile + '.signature')
721 map (consider_file, self.additional_files_to_consider (base, full))
722 map (require_file, self.additional_files_required (base, full))
724 return (result, missing)
726 def is_outdated (self, output_dir, current_files):
727 found, missing = self.all_output_files (output_dir, current_files)
730 def filter_pipe (self, input, cmd):
731 """Pass input through cmd, and return the result."""
733 if self.global_options.verbose:
734 progress (_ ("Running through filter `%s'\n") % cmd)
736 # TODO: Use Popen once we resolve the problem with msvcrt in Windows:
737 (stdin, stdout, stderr) = os.popen3 (cmd)
738 # p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
739 # (stdin, stdout, stderr) = (p.stdin, p.stdout, p.stderr)
741 status = stdin.close ()
745 output = stdout.read ()
746 status = stdout.close ()
751 signal = 0x0f & status
752 if status or (not output and err):
753 exit_status = status >> 8
754 ly.error (_ ("`%s' failed (%d)") % (cmd, exit_status))
755 ly.error (_ ("The error log is as follows:"))
756 ly.stderr_write (err)
757 ly.stderr_write (stderr.read ())
760 if self.global_options.verbose:
765 def get_snippet_code (self):
766 return self.substring ('code');
768 def filter_text (self):
769 """Run snippet bodies through a command (say: convert-ly).
771 This functionality is rarely used, and this code must have bitrot.
773 code = self.get_snippet_code ();
774 s = self.filter_pipe (code, self.global_options.filter_cmd)
777 'options': self.match.group ('options')
779 return self.formatter.output_simple_replacements (FILTER, d)
781 def replacement_text (self):
782 base = self.final_basename ()
783 return self.formatter.snippet_output (base, self)
785 def get_images (self):
786 rep = {'base': self.final_basename ()}
788 single = '%(base)s.png' % rep
789 multiple = '%(base)s-page1.png' % rep
791 if (os.path.exists (multiple)
792 and (not os.path.exists (single)
793 or (os.stat (multiple)[stat.ST_MTIME]
794 > os.stat (single)[stat.ST_MTIME]))):
795 count = ps_page_count ('%(base)s.eps' % rep)
796 images = ['%s-page%d.png' % (rep['base'], page) for page in range (1, count+1)]
797 images = tuple (images)
803 re_begin_verbatim = re.compile (r'\s+%.*?begin verbatim.*\n*', re.M)
804 re_end_verbatim = re.compile (r'\s+%.*?end verbatim.*$', re.M)
806 class LilypondFileSnippet (LilypondSnippet):
807 def __init__ (self, type, match, formatter, line_number, global_options):
808 LilypondSnippet.__init__ (self, type, match, formatter, line_number, global_options)
809 self.filename = self.substring ('filename')
810 self.ext = os.path.splitext (os.path.basename (self.filename))[1]
811 self.contents = file (BookBase.find_file (self.filename, global_options.include_path)).read ()
813 def get_snippet_code (self):
814 return self.contents;
818 s = re_begin_verbatim.split (s)[-1]
819 s = re_end_verbatim.split (s)[0]
820 if not NOGETTEXT in self.option_dict:
821 s = self.verb_ly_gettext (s)
822 if not s.endswith ('\n'):
828 return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
829 % (name, self.contents))
831 def final_basename (self):
832 if self.global_options.use_source_file_names:
833 base = os.path.splitext (os.path.basename (self.filename))[0]
836 return self.basename ()
839 class MusicXMLFileSnippet (LilypondFileSnippet):
840 def __init__ (self, type, match, formatter, line_number, global_options):
841 LilypondFileSnippet.__init__ (self, type, match, formatter, line_number, global_options)
842 self.compressed = False
843 self.converted_ly = None
844 self.musicxml_options_dict = {
845 'verbose': '--verbose',
847 'compressed': '--compressed',
848 'relative': '--relative',
849 'absolute': '--absolute',
850 'no-articulation-directions': '--no-articulation-directions',
851 'no-rest-positions': '--no-rest-positions',
852 'no-page-layout': '--no-page-layout',
853 'no-beaming': '--no-beaming',
854 'language': '--language',
857 def snippet_options (self):
858 return self.musicxml_options_dict.keys ()
860 def convert_from_musicxml (self):
862 xml2ly_option_list = []
863 for (key, value) in self.option_dict.items ():
864 cmd_key = self.musicxml_options_dict.get (key, None)
868 xml2ly_option_list.append (cmd_key)
870 xml2ly_option_list.append (cmd_key + '=' + value)
871 if ('.mxl' in name) and ('--compressed' not in xml2ly_option_list):
872 xml2ly_option_list.append ('--compressed')
873 self.compressed = True
874 opts = " ".join (xml2ly_option_list)
875 progress (_ ("Converting MusicXML file `%s'...\n") % self.filename)
877 ly_code = self.filter_pipe (self.contents, 'musicxml2ly %s --out=- - ' % opts)
881 if self.converted_ly == None:
882 self.converted_ly = self.convert_from_musicxml ()
884 return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
885 % (name, self.converted_ly))
887 def additional_files_required (self, base, full):
890 result.append (base + '.mxl')
892 result.append (base + '.xml')
896 base = self.basename ()
897 path = os.path.join (self.global_options.lily_output_dir, base)
898 directory = os.path.split(path)[0]
899 if not os.path.isdir (directory):
900 os.makedirs (directory)
902 # First write the XML to a file (so we can link it!)
904 xmlfilename = path + '.mxl'
906 xmlfilename = path + '.xml'
907 if os.path.exists (xmlfilename):
908 diff_against_existing = self.filter_pipe (self.contents, 'diff -u %s - ' % xmlfilename)
909 if diff_against_existing:
910 warning (_ ("%s: duplicate filename but different contents of orginal file,\n\
911 printing diff against existing file.") % xmlfilename)
912 ly.stderr_write (diff_against_existing)
914 out = file (xmlfilename, 'w')
915 out.write (self.contents)
918 # also write the converted lilypond
919 filename = path + '.ly'
920 if os.path.exists (filename):
921 diff_against_existing = self.filter_pipe (self.full_ly (), 'diff -u %s -' % filename)
922 if diff_against_existing:
923 warning (_ ("%s: duplicate filename but different contents of converted lilypond file,\n\
924 printing diff against existing file.") % filename)
925 ly.stderr_write (diff_against_existing)
927 out = file (filename, 'w')
928 out.write (self.full_ly ())
930 file (path + '.txt', 'w').write ('image of music')
934 class LilyPondVersionString (Snippet):
935 """A string that does not require extra memory."""
936 def __init__ (self, type, match, formatter, line_number, global_options):
937 Snippet.__init__ (self, type, match, formatter, line_number, global_options)
939 def replacement_text (self):
940 return self.formatter.output_simple (self.type, self)
943 snippet_type_to_class = {
944 'lilypond_file': LilypondFileSnippet,
945 'lilypond_block': LilypondSnippet,
946 'lilypond': LilypondSnippet,
947 'include': IncludeSnippet,
948 'lilypondversion': LilyPondVersionString,
949 'musicxml_file': MusicXMLFileSnippet,