1 # -*- coding: utf-8 -*-
3 import book_base as BookBase
13 progress = ly.progress
16 debug = ly.debug_output
22 ####################################################################
23 # Snippet option handling
24 ####################################################################
28 # Is this pythonic? Personally, I find this rather #define-nesque. --hwn
31 ADDVERSION = 'addversion'
36 EXAMPLEINDENT = 'exampleindent'
41 LINE_WIDTH = 'line-width'
42 NOFRAGMENT = 'nofragment'
43 NOGETTEXT = 'nogettext'
46 NORAGGED_RIGHT = 'noragged-right'
50 OUTPUTIMAGE = 'outputimage'
52 PAPERSIZE = 'papersize'
54 PRINTFILENAME = 'printfilename'
56 RAGGED_RIGHT = 'ragged-right'
58 STAFFSIZE = 'staffsize'
61 VERSION = 'lilypondversion'
65 # NOTIME and NOGETTEXT have no opposite so they aren't part of this
72 # Options that have no impact on processing by lilypond (or --process
74 PROCESSING_INDEPENDENT_OPTIONS = (
75 ALT, NOGETTEXT, VERBATIM, ADDVERSION,
76 TEXIDOC, DOCTITLE, VERSION, PRINTFILENAME)
80 # Options without a pattern in snippet_options.
98 ####################################################################
99 # LilyPond templates for the snippets
100 ####################################################################
105 RELATIVE: r'''\relative c%(relative_quotes)s''',
109 # TODO: Remove the 1mm additional padding in the line-width
110 # once lilypond creates tighter cropped images!
112 PAPERSIZE: r'''#(set-paper-size "%(papersize)s")''',
113 INDENT: r'''indent = %(indent)s''',
114 LINE_WIDTH: r'''line-width = %(line-width)s
115 %% offset the left padding, also add 1mm as lilypond creates cropped
116 %% images with a little space on the right
117 line-width = #(- line-width (* mm %(padding_mm)f) (* mm 1))''',
118 QUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s
119 %% offset the left padding, also add 1mm as lilypond creates cropped
120 %% images with a little space on the right
121 line-width = #(- line-width (* mm %(padding_mm)f) (* mm 1))''',
122 RAGGED_RIGHT: r'''ragged-right = ##t''',
123 NORAGGED_RIGHT: r'''ragged-right = ##f''',
135 \remove "Time_signature_engraver"
141 STAFFSIZE: r'''#(set-global-staff-size %(staffsize)s)''',
149 def classic_lilypond_book_compatibility (key, value):
150 if key == 'lilyquote':
151 return (QUOTE, value)
152 if key == 'singleline' and value == None:
153 return (RAGGED_RIGHT, None)
155 m = re.search ('relative\s*([-0-9])', key)
157 return ('relative', m.group (1))
159 m = re.match ('([0-9]+)pt', key)
161 return ('staffsize', m.group (1))
163 if key == 'indent' or key == 'line-width':
164 m = re.match ('([-.0-9]+)(cm|in|mm|pt|staffspace)', value)
166 f = float (m.group (1))
167 return (key, '%f\\%s' % (f, m.group (2)))
172 PREAMBLE_LY = '''%%%% Generated by %(program_name)s
173 %%%% Options: [%(option_string)s]
174 \\include "lilypond-book-preamble.ly"
177 %% ****************************************************************
178 %% Start cut-&-pastable-section
179 %% ****************************************************************
198 %% ****************************************************************
200 %% ****************************************************************
204 %% ****************************************************************
206 %% ****************************************************************
214 %% ****************************************************************
215 %% ly snippet contents follows:
216 %% ****************************************************************
220 %% ****************************************************************
222 %% ****************************************************************
229 ####################################################################
231 ####################################################################
233 def ps_page_count (ps_name):
234 header = file (ps_name).read (1024)
235 m = re.search ('\n%%Pages: ([0-9]+)', header)
237 return int (m.group (1))
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+')
245 def ly_comment_gettext (t, m):
246 return m.group (1) + t (m.group (2))
250 class CompileError(Exception):
255 ####################################################################
257 ####################################################################
260 def replacement_text (self):
263 def filter_text (self):
264 return self.replacement_text ()
269 class Substring (Chunk):
270 """A string that does not require extra memory."""
271 def __init__ (self, source, start, end, line_number):
275 self.line_number = line_number
276 self.override_text = None
281 def replacement_text (self):
282 if self.override_text:
283 return self.override_text
285 return self.source[self.start:self.end]
289 class Snippet (Chunk):
290 def __init__ (self, type, match, formatter, line_number, global_options):
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}
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)
306 def replacement_text (self):
307 return self.match.group ('match')
309 def substring (self, s):
310 return self.match.group (s)
313 return `self.__class__` + ' type = ' + self.type
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
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)
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)
334 os = match.group ('options')
335 self.parse_snippet_options (os, self.type)
338 def snippet_options (self):
341 def verb_ly_gettext (self, s):
342 lang = self.formatter.document_language
346 t = langdefs.translation[lang]
350 s = ly_comment_re.sub (lambda m: ly_comment_gettext (t, m), s)
352 if langdefs.LANGDICT[lang].enable_ly_identifier_l10n:
353 for v in ly_var_def_re.findall (s):
354 s = re.sub (r"(?m)(?<!\\clef)(^|[' \\#])%s([^a-zA-Z])" % v,
355 "\\1" + t (v) + "\\2",
357 for id in ly_context_id_re.findall (s):
358 s = re.sub (r'(\s+|")%s(\s+|")' % id,
359 "\\1" + t (id) + "\\2",
364 verb_text = self.substring ('code')
365 if not NOGETTEXT in self.option_dict:
366 verb_text = self.verb_ly_gettext (verb_text)
367 if not verb_text.endswith ('\n'):
372 contents = self.substring ('code')
373 return ('\\sourcefileline %d\n%s'
374 % (self.line_number - 1, contents))
379 return self.compose_ly (s)
382 def split_options (self, option_string):
383 return self.formatter.split_snippet_options (option_string);
385 def parse_snippet_options (self, option_string, type):
386 self.snippet_option_dict = {}
388 # Split option string and create raw option_dict from it
389 options = self.split_options (option_string)
391 for option in options:
392 (key, value) = (option, None)
394 (key, value) = re.split ('\s*=\s*', option)
396 # a no... option removes a previous option if present!
397 if key in no_options:
398 if no_options[key] in self.option_dict:
399 del self.snippet_option_dict[no_options[key]]
401 # Check for deprecated options, replace them by new ones
402 (c_key, c_value) = classic_lilypond_book_compatibility (key, value)
406 _ ("deprecated ly-option used: %s=%s") % (key, value))
408 _ ("compatibility mode translation: %s=%s") % (c_key, c_value))
411 _ ("deprecated ly-option used: %s") % key)
413 _ ("compatibility mode translation: %s") % c_key)
414 (key, value) = (c_key, c_value)
415 # Finally, insert the option:
417 self.snippet_option_dict[key] = value
419 # If LINE_WIDTH is used without parameter, set it to default.
420 has_line_width = self.snippet_option_dict.has_key (LINE_WIDTH)
421 if has_line_width and self.snippet_option_dict[LINE_WIDTH] == None:
422 del self.snippet_option_dict[LINE_WIDTH]
424 # RELATIVE does not work without FRAGMENT, so imply that
425 if self.snippet_option_dict.has_key (RELATIVE):
426 self.snippet_option_dict[FRAGMENT] = None
428 # Now get the default options from the formatter object (HTML, latex,
429 # texinfo, etc.) and insert the explicit snippet options to get the
430 # list of all options for this snippet
431 # first, make sure we have an INDENT value as a fallback
432 self.option_dict = {INDENT: '0\\mm'};
433 self.option_dict.update (self.formatter.default_snippet_options);
434 self.option_dict.update (self.snippet_option_dict);
436 # also construct a list of all options (as strings) that influence the
437 # visual appearance of the snippet
438 lst = filter (lambda (x,y): x not in PROCESSING_INDEPENDENT_OPTIONS,
439 self.option_dict.iteritems ());
441 for (key, value) in lst:
443 option_list.append (key)
445 option_list.append (key + "=" + value)
447 self.outputrelevant_option_list = option_list
448 #print ("self.outputrelevant_option_list: %s\n" % self.outputrelevant_option_list);
451 # Set a default line-width if there is none. We need this, because
452 # lilypond-book has set left-padding by default and therefore does
453 # #(define line-width (- line-width (* 3 mm)))
454 # TODO: Junk this ugly hack if the code gets rewritten to concatenate
455 # all settings before writing them in the \paper block.
456 #if not LINE_WIDTH in self.option_dict:
457 #if not QUOTE in self.option_dict:
458 #self.option_dict[LINE_WIDTH] = "#(- paper-width \
459 #left-margin-default right-margin-default)"
461 # Get a list of all options (as string) that influence the snippet appearance
462 def get_outputrelevant_option_strings (self):
463 return self.outputrelevant_option_list
465 def compose_ly (self, code):
470 # The original concept of the `exampleindent' option is broken.
471 # It is not possible to get a sane value for @exampleindent at all
472 # without processing the document itself. Saying
480 # causes ugly results with the DVI backend of texinfo since the
481 # default value for @exampleindent isn't 5em but 0.4in (or a smaller
482 # value). Executing the above code changes the environment
483 # indentation to an unknown value because we don't know the amount
484 # of 1em in advance since it is font-dependent. Modifying
485 # @exampleindent in the middle of a document is simply not
486 # supported within texinfo.
488 # As a consequence, the only function of @exampleindent is now to
489 # specify the amount of indentation for the `quote' option.
491 # To set @exampleindent locally to zero, we use the @format
492 # environment for non-quoted snippets.
494 # Update: since July 2011 we are running texinfo on a test file
495 # to detect the default exampleindent, so we might reintroduce
496 # further usage of exampleindent again.
498 # First, make sure we have some falback default value, auto-detected
499 # values and explicitly specified values will always override them:
500 override[EXAMPLEINDENT] = r'0.4\in'
501 override[LINE_WIDTH] = '5\\in'
502 override.update (self.formatter.default_snippet_options)
503 override['padding_mm'] = self.global_options.padding_mm
505 option_string = ','.join (self.get_outputrelevant_option_strings ())
507 compose_types = [NOTES, PREAMBLE, LAYOUT, PAPER]
508 for a in compose_types:
511 option_names = self.option_dict.keys ()
513 for key in option_names:
514 value = self.option_dict[key]
517 override[key] = value
519 if not override.has_key (key):
523 for typ in compose_types:
524 if snippet_options[typ].has_key (key):
525 compose_dict[typ].append (snippet_options[typ][key])
529 if not found and key not in simple_options and key not in self.snippet_options ():
530 warning (_ ("ignoring unknown ly option: %s") % key)
533 if RELATIVE in override and override[RELATIVE]:
534 relative = int (override[RELATIVE])
540 relative_quotes += ',' * (- relative)
542 relative_quotes += "'" * relative
544 # put paper-size first, if it exists
545 for i,elem in enumerate(compose_dict[PAPER]):
546 if elem.startswith("#(set-paper-size"):
547 compose_dict[PAPER].insert(0, compose_dict[PAPER].pop(i))
550 paper_string = '\n '.join (compose_dict[PAPER]) % override
551 layout_string = '\n '.join (compose_dict[LAYOUT]) % override
552 notes_string = '\n '.join (compose_dict[NOTES]) % vars ()
553 preamble_string = '\n '.join (compose_dict[PREAMBLE]) % override
554 padding_mm = self.global_options.padding_mm
555 if self.global_options.safe_mode:
556 safe_mode_string = "#(ly:set-option 'safe #t)"
558 safe_mode_string = ""
562 d.update (self.global_options.information)
563 if FRAGMENT in self.option_dict:
567 return (PREAMBLE_LY + body) % d
569 def get_checksum (self):
570 if not self.checksum:
571 # Work-around for md5 module deprecation warning in python 2.5+:
573 from hashlib import md5
577 # We only want to calculate the hash based on the snippet
578 # code plus fragment options relevant to processing by
579 # lilypond, not the snippet + preamble
580 hash = md5 (self.relevant_contents (self.ly ()))
581 for option in self.get_outputrelevant_option_strings ():
584 ## let's not create too long names.
585 self.checksum = hash.hexdigest ()[:10]
590 cs = self.get_checksum ()
591 name = os.path.join (cs[:2], 'lily-%s' % cs[2:])
594 final_basename = basename
597 base = self.basename ()
598 path = os.path.join (self.global_options.lily_output_dir, base)
599 directory = os.path.split(path)[0]
600 if not os.path.isdir (directory):
601 os.makedirs (directory)
602 filename = path + '.ly'
603 if os.path.exists (filename):
604 existing = open (filename, 'r').read ()
606 if self.relevant_contents (existing) != self.relevant_contents (self.full_ly ()):
607 warning ("%s: duplicate filename but different contents of original file,\n\
608 printing diff against existing file." % filename)
609 ly.stderr_write (self.filter_pipe (self.full_ly (), 'diff -u %s -' % filename))
611 out = file (filename, 'w')
612 out.write (self.full_ly ())
613 file (path + '.txt', 'w').write ('image of music')
615 def relevant_contents (self, ly):
616 return re.sub (r'\\(version|sourcefileline|sourcefilename)[^\n]*\n', '', ly)
618 def link_all_output_files (self, output_dir, output_dir_files, destination):
619 existing, missing = self.all_output_files (output_dir, output_dir_files)
621 error (_ ('Missing files: %s') % ', '.join (missing))
622 raise CompileError(self.basename())
623 for name in existing:
624 if (self.global_options.use_source_file_names
625 and isinstance (self, LilypondFileSnippet)):
626 base, ext = os.path.splitext (name)
627 components = base.split ('-')
628 # ugh, assume filenames with prefix with one dash (lily-xxxx)
629 if len (components) > 2:
630 base_suffix = '-' + components[-1]
633 final_name = self.final_basename () + base_suffix + ext
637 os.unlink (os.path.join (destination, final_name))
641 src = os.path.join (output_dir, name)
642 dst = os.path.join (destination, final_name)
643 dst_path = os.path.split(dst)[0]
644 if not os.path.isdir (dst_path):
645 os.makedirs (dst_path)
647 if (self.global_options.use_source_file_names
648 and isinstance (self, LilypondFileSnippet)):
649 fout = open (dst, 'w')
650 fin = open (src, 'r')
651 for line in fin.readlines ():
652 fout.write (line.replace (self.basename (), self.final_basename ()))
658 except AttributeError:
659 shutil.copyfile (src, dst)
660 except (IOError, OSError):
661 error (_ ('Could not overwrite file %s') % dst)
662 raise CompileError(self.basename())
664 def additional_files_to_consider (self, base, full):
666 def additional_files_required (self, base, full):
668 if self.ext != '.ly':
669 result.append (base + self.ext)
673 def all_output_files (self, output_dir, output_dir_files):
674 """Return all files generated in lily_output_dir, a set.
676 output_dir_files is the list of files in the output directory.
680 base = self.basename()
681 full = os.path.join (output_dir, base)
682 def consider_file (name):
683 if name in output_dir_files:
686 def require_file (name):
687 if name in output_dir_files:
692 # UGH - junk self.global_options
693 skip_lily = self.global_options.skip_lilypond_run
694 for required in [base + '.ly',
696 require_file (required)
698 require_file (base + '-systems.count')
700 if 'ddump-profile' in self.global_options.process_cmd:
701 require_file (base + '.profile')
702 if 'dseparate-log-file' in self.global_options.process_cmd:
703 require_file (base + '.log')
705 map (consider_file, [base + '.tex',
710 base + '-systems.texi',
711 base + '-systems.tex',
712 base + '-systems.pdftexi'])
713 if self.formatter.document_language:
715 [base + '.texidoc' + self.formatter.document_language,
716 base + '.doctitle' + self.formatter.document_language])
718 required_files = self.formatter.required_files (self, base, full, result)
719 for f in required_files:
723 if not skip_lily and not missing:
724 system_count = int(file (full + '-systems.count').read())
726 for number in range(1, system_count + 1):
727 systemfile = '%s-%d' % (base, number)
728 require_file (systemfile + '.eps')
729 consider_file (systemfile + '.pdf')
731 # We can't require signatures, since books and toplevel
732 # markups do not output a signature.
733 if 'ddump-signature' in self.global_options.process_cmd:
734 consider_file (systemfile + '.signature')
736 map (consider_file, self.additional_files_to_consider (base, full))
737 map (require_file, self.additional_files_required (base, full))
739 return (result, missing)
741 def is_outdated (self, output_dir, current_files):
742 found, missing = self.all_output_files (output_dir, current_files)
745 def filter_pipe (self, input, cmd):
746 """Pass input through cmd, and return the result."""
748 debug (_ ("Running through filter `%s'") % cmd, True)
751 if (sys.platform == "mingw32"):
753 p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=closefds)
754 (stdin, stdout, stderr) = (p.stdin, p.stdout, p.stderr)
756 status = stdin.close ()
760 output = stdout.read ()
761 status = stdout.close ()
766 signal = 0x0f & status
767 if status or (not output and err):
768 exit_status = status >> 8
769 ly.error (_ ("`%s' failed (%d)") % (cmd, exit_status))
770 ly.error (_ ("The error log is as follows:"))
771 ly.stderr_write (err)
772 ly.stderr_write (stderr.read ())
779 def get_snippet_code (self):
780 return self.substring ('code');
782 def filter_text (self):
783 """Run snippet bodies through a command (say: convert-ly).
785 This functionality is rarely used, and this code must have bitrot.
787 code = self.get_snippet_code ();
788 s = self.filter_pipe (code, self.global_options.filter_cmd)
791 'options': self.match.group ('options')
793 return self.formatter.output_simple_replacements (FILTER, d)
795 def replacement_text (self):
796 base = self.final_basename ()
797 return self.formatter.snippet_output (base, self)
799 def get_images (self):
800 rep = {'base': self.final_basename ()}
802 single = '%(base)s.png' % rep
803 multiple = '%(base)s-page1.png' % rep
805 if (os.path.exists (multiple)
806 and (not os.path.exists (single)
807 or (os.stat (multiple)[stat.ST_MTIME]
808 > os.stat (single)[stat.ST_MTIME]))):
809 count = ps_page_count ('%(base)s.eps' % rep)
810 images = ['%s-page%d.png' % (rep['base'], page) for page in range (1, count+1)]
811 images = tuple (images)
817 re_begin_verbatim = re.compile (r'\s+%.*?begin verbatim.*\n*', re.M)
818 re_end_verbatim = re.compile (r'\s+%.*?end verbatim.*$', re.M)
820 class LilypondFileSnippet (LilypondSnippet):
821 def __init__ (self, type, match, formatter, line_number, global_options):
822 LilypondSnippet.__init__ (self, type, match, formatter, line_number, global_options)
823 self.filename = self.substring ('filename')
824 self.contents = file (BookBase.find_file (self.filename,
825 global_options.include_path, global_options.original_dir)).read ()
827 def get_snippet_code (self):
828 return self.contents;
832 s = re_begin_verbatim.split (s)[-1]
833 s = re_end_verbatim.split (s)[0]
834 if not NOGETTEXT in self.option_dict:
835 s = self.verb_ly_gettext (s)
836 if not s.endswith ('\n'):
842 return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
843 % (name, self.contents))
845 def final_basename (self):
846 if self.global_options.use_source_file_names:
847 base = os.path.splitext (os.path.basename (self.filename))[0]
850 return self.basename ()
853 class MusicXMLFileSnippet (LilypondFileSnippet):
854 def __init__ (self, type, match, formatter, line_number, global_options):
855 LilypondFileSnippet.__init__ (self, type, match, formatter, line_number, global_options)
856 self.compressed = False
857 self.converted_ly = None
858 self.ext = os.path.splitext (os.path.basename (self.filename))[1]
859 self.musicxml_options_dict = {
860 'verbose': '--verbose',
862 'compressed': '--compressed',
863 'relative': '--relative',
864 'absolute': '--absolute',
865 'no-articulation-directions': '--no-articulation-directions',
866 'no-rest-positions': '--no-rest-positions',
867 'no-page-layout': '--no-page-layout',
868 'no-beaming': '--no-beaming',
869 'language': '--language',
872 def snippet_options (self):
873 return self.musicxml_options_dict.keys ()
875 def convert_from_musicxml (self):
877 xml2ly_option_list = []
878 for (key, value) in self.option_dict.items ():
879 cmd_key = self.musicxml_options_dict.get (key, None)
883 xml2ly_option_list.append (cmd_key)
885 xml2ly_option_list.append (cmd_key + '=' + value)
886 if ('.mxl' in name) and ('--compressed' not in xml2ly_option_list):
887 xml2ly_option_list.append ('--compressed')
888 self.compressed = True
889 opts = " ".join (xml2ly_option_list)
890 progress (_ ("Converting MusicXML file `%s'...\n") % self.filename)
892 ly_code = self.filter_pipe (self.contents, 'musicxml2ly %s --out=- - ' % opts)
896 if self.converted_ly == None:
897 self.converted_ly = self.convert_from_musicxml ()
899 return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
900 % (name, self.converted_ly))
903 base = self.basename ()
904 path = os.path.join (self.global_options.lily_output_dir, base)
905 directory = os.path.split(path)[0]
906 if not os.path.isdir (directory):
907 os.makedirs (directory)
909 # First write the XML to a file (so we can link it!)
911 xmlfilename = path + '.mxl'
913 xmlfilename = path + '.xml'
914 if os.path.exists (xmlfilename):
915 diff_against_existing = self.filter_pipe (self.contents, 'diff -u %s - ' % xmlfilename)
916 if diff_against_existing:
917 warning (_ ("%s: duplicate filename but different contents of original file,\n\
918 printing diff against existing file.") % xmlfilename)
919 ly.stderr_write (diff_against_existing)
921 out = file (xmlfilename, 'w')
922 out.write (self.contents)
925 # also write the converted lilypond
926 filename = path + '.ly'
927 if os.path.exists (filename):
928 diff_against_existing = self.filter_pipe (self.full_ly (), 'diff -u %s -' % filename)
929 if diff_against_existing:
930 warning (_ ("%s: duplicate filename but different contents of converted lilypond file,\n\
931 printing diff against existing file.") % filename)
932 ly.stderr_write (diff_against_existing)
934 out = file (filename, 'w')
935 out.write (self.full_ly ())
937 file (path + '.txt', 'w').write ('image of music')
941 class LilyPondVersionString (Snippet):
942 """A string that does not require extra memory."""
943 def __init__ (self, type, match, formatter, line_number, global_options):
944 Snippet.__init__ (self, type, match, formatter, line_number, global_options)
946 def replacement_text (self):
947 return self.formatter.output_simple (self.type, self)
950 snippet_type_to_class = {
951 'lilypond_file': LilypondFileSnippet,
952 'lilypond_block': LilypondSnippet,
953 'lilypond': LilypondSnippet,
954 'include': IncludeSnippet,
955 'lilypondversion': LilyPondVersionString,
956 'musicxml_file': MusicXMLFileSnippet,