]> git.donarmstrong.com Git - lilypond.git/blobdiff - python/book_snippets.py
Web-ja: update introduction
[lilypond.git] / python / book_snippets.py
index a1b1328c575b4be2af6afe92c07d09183b9b1870..4bc4252e79da3bad9cb15876eb2f4218a4f24d2f 100644 (file)
@@ -6,15 +6,14 @@ global _;_=ly._
 import re
 import os
 import copy
-# TODO: We are using os.popen3, which has been deprecated since python 2.6. The
-# suggested replacement is the Popen function of the subprocess module.
-# Unfortunately, on windows this needs the msvcrt module, which doesn't seem
-# to be available in GUB?!?!?!
-# from subprocess import Popen, PIPE
+import shutil
+import subprocess
+import sys
 
 progress = ly.progress
 warning = ly.warning
 error = ly.error
+debug = ly.debug_output
 
 
 
@@ -38,14 +37,11 @@ EXAMPLEINDENT = 'exampleindent'
 FILENAME = 'filename'
 FILTER = 'filter'
 FRAGMENT = 'fragment'
-LANG = 'lang'    ## TODO: This is handled nowhere!
 LAYOUT = 'layout'
-LILYQUOTE = 'lilyquote'
 LINE_WIDTH = 'line-width'
 NOFRAGMENT = 'nofragment'
 NOGETTEXT = 'nogettext'
 NOINDENT = 'noindent'
-NOQUOTE = 'noquote'
 INDENT = 'indent'
 NORAGGED_RIGHT = 'noragged-right'
 NOTES = 'body'
@@ -53,6 +49,7 @@ NOTIME = 'notime'
 OUTPUT = 'output'
 OUTPUTIMAGE = 'outputimage'
 PAPER = 'paper'
+PAPERSIZE = 'papersize'
 PREAMBLE = 'preamble'
 PRINTFILENAME = 'printfilename'
 QUOTE = 'quote'
@@ -67,7 +64,6 @@ VERSION = 'lilypondversion'
 
 # NOTIME and NOGETTEXT have no opposite so they aren't part of this
 # dictionary.
-# NOQUOTE is used internally only.
 no_options = {
     NOFRAGMENT: FRAGMENT,
     NOINDENT: INDENT,
@@ -91,7 +87,6 @@ simple_options = [
     PRINTFILENAME,
     DOCTITLE,
     TEXIDOC,
-    LANG,
     VERBATIM,
     FILENAME,
     ALT,
@@ -111,11 +106,19 @@ snippet_options = {
     },
 
     ##
+    # TODO: Remove the 1mm additional padding in the line-width
+    #       once lilypond creates tighter cropped images!
     PAPER: {
+        PAPERSIZE: r'''#(set-paper-size "%(papersize)s")''',
         INDENT: r'''indent = %(indent)s''',
-        LINE_WIDTH: r'''line-width = %(line-width)s''',
-        QUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
-        LILYQUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''',
+        LINE_WIDTH: r'''line-width = %(line-width)s
+  %% offset the left padding, also add 1mm as lilypond creates cropped
+  %% images with a little space on the right
+  line-width = #(- line-width (* mm  %(padding_mm)f) (* mm 1))''',
+        QUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s
+  %% offset the left padding, also add 1mm as lilypond creates cropped
+  %% images with a little space on the right
+  line-width = #(- line-width (* mm  %(padding_mm)f) (* mm 1))''',
         RAGGED_RIGHT: r'''ragged-right = ##t''',
         NORAGGED_RIGHT: r'''ragged-right = ##f''',
     },
@@ -143,24 +146,9 @@ snippet_options = {
 
 
 
-FRAGMENT_LY = r'''
-%(notes_string)s
-{
-
-
-%% ****************************************************************
-%% ly snippet contents follows:
-%% ****************************************************************
-%(code)s
-
-
-%% ****************************************************************
-%% end ly snippet
-%% ****************************************************************
-}
-'''
-
 def classic_lilypond_book_compatibility (key, value):
+    if key == 'lilyquote':
+        return (QUOTE, value)
     if key == 'singleline' and value == None:
         return (RAGGED_RIGHT, None)
 
@@ -194,8 +182,6 @@ PREAMBLE_LY = '''%%%% Generated by %(program_name)s
 
 \paper {
   %(paper_string)s
-  force-assignment = #""
-  line-width = #(- line-width (* mm  %(padding_mm)f))
 }
 
 \layout {
@@ -220,10 +206,23 @@ FULL_LY = '''
 %% ****************************************************************
 '''
 
+FRAGMENT_LY = r'''
+%(notes_string)s
+{
 
 
+%% ****************************************************************
+%% ly snippet contents follows:
+%% ****************************************************************
+%(code)s
 
 
+%% ****************************************************************
+%% end ly snippet
+%% ****************************************************************
+}
+'''
+
 
 
 
@@ -330,8 +329,10 @@ class IncludeSnippet (Snippet):
 class LilypondSnippet (Snippet):
     def __init__ (self, type, match, formatter, line_number, global_options):
         Snippet.__init__ (self, type, match, formatter, line_number, global_options)
+        self.filename = ''
+        self.ext = '.ly'
         os = match.group ('options')
-        self.do_options (os, self.type)
+        self.parse_snippet_options (os, self.type)
 
 
     def snippet_options (self):
@@ -381,87 +382,87 @@ class LilypondSnippet (Snippet):
     def split_options (self, option_string):
         return self.formatter.split_snippet_options (option_string);
 
-    def do_options (self, option_string, type):
-        self.option_dict = {}
+    def parse_snippet_options (self, option_string, type):
+        self.snippet_option_dict = {}
 
+        # Split option string and create raw option_dict from it
         options = self.split_options (option_string)
 
         for option in options:
+            (key, value) = (option, None)
             if '=' in option:
                 (key, value) = re.split ('\s*=\s*', option)
-                self.option_dict[key] = value
             else:
-                if option in no_options:
-                    if no_options[option] in self.option_dict:
-                        del self.option_dict[no_options[option]]
+                # a no... option removes a previous option if present!
+                if key in no_options:
+                    if no_options[key] in self.option_dict:
+                        del self.snippet_option_dict[no_options[key]]
+                        key = None
+            # Check for deprecated options, replace them by new ones
+            (c_key, c_value) = classic_lilypond_book_compatibility (key, value)
+            if c_key:
+                if c_value:
+                    warning (
+                        _ ("deprecated ly-option used: %s=%s") % (key, value))
+                    warning (
+                        _ ("compatibility mode translation: %s=%s") % (c_key, c_value))
                 else:
-                    self.option_dict[option] = None
-
+                    warning (
+                        _ ("deprecated ly-option used: %s") % key)
+                    warning (
+                        _ ("compatibility mode translation: %s") % c_key)
+                (key, value) = (c_key, c_value)
+            # Finally, insert the option:
+            if key:
+                self.snippet_option_dict[key] = value
 
         # If LINE_WIDTH is used without parameter, set it to default.
-        has_line_width = self.option_dict.has_key (LINE_WIDTH)
-        if has_line_width and self.option_dict[LINE_WIDTH] == None:
-            has_line_width = False
-            del self.option_dict[LINE_WIDTH]
-
-        # TODO: Can't we do that more efficiently (built-in python func?)
-        for k in self.formatter.default_snippet_options:
-            if k not in self.option_dict:
-                self.option_dict[k] = self.formatter.default_snippet_options[k]
-
-        # RELATIVE does not work without FRAGMENT;
-        # make RELATIVE imply FRAGMENT
-        has_relative = self.option_dict.has_key (RELATIVE)
-        if has_relative and not self.option_dict.has_key (FRAGMENT):
-            self.option_dict[FRAGMENT] = None
-
-        if not has_line_width:
-            if type == 'lilypond' or FRAGMENT in self.option_dict:
-                self.option_dict[RAGGED_RIGHT] = None
-
-            if type == 'lilypond':
-                if LINE_WIDTH in self.option_dict:
-                    del self.option_dict[LINE_WIDTH]
+        has_line_width = self.snippet_option_dict.has_key (LINE_WIDTH)
+        if has_line_width and self.snippet_option_dict[LINE_WIDTH] == None:
+            del self.snippet_option_dict[LINE_WIDTH]
+
+        # RELATIVE does not work without FRAGMENT, so imply that
+        if self.snippet_option_dict.has_key (RELATIVE):
+            self.snippet_option_dict[FRAGMENT] = None
+
+        # Now get the default options from the formatter object (HTML, latex,
+        # texinfo, etc.) and insert the explicit snippet options to get the
+        # list of all options for this snippet
+        # first, make sure we have an INDENT value as a fallback
+        self.option_dict = {INDENT: '0\\mm'};
+        self.option_dict.update (self.formatter.default_snippet_options);
+        self.option_dict.update (self.snippet_option_dict);
+
+        # also construct a list of all options (as strings) that influence the
+        # visual appearance of the snippet
+        lst = filter (lambda (x,y): x not in PROCESSING_INDEPENDENT_OPTIONS,
+                      self.option_dict.iteritems ());
+        option_list = []
+        for (key, value) in lst:
+            if value == None:
+                option_list.append (key)
             else:
-                if RAGGED_RIGHT in self.option_dict:
-                    if LINE_WIDTH in self.option_dict:
-                        del self.option_dict[LINE_WIDTH]
+                option_list.append (key + "=" + value)
+        option_list.sort ()
+        self.outputrelevant_option_list = option_list
+        #print ("self.outputrelevant_option_list: %s\n" % self.outputrelevant_option_list);
 
-            if QUOTE in self.option_dict or type == 'lilypond':
-                if LINE_WIDTH in self.option_dict:
-                    del self.option_dict[LINE_WIDTH]
-
-        if not INDENT in self.option_dict:
-            self.option_dict[INDENT] = '0\\mm'
 
         # Set a default line-width if there is none. We need this, because
         # lilypond-book has set left-padding by default and therefore does
         # #(define line-width (- line-width (* 3 mm)))
         # TODO: Junk this ugly hack if the code gets rewritten to concatenate
         # all settings before writing them in the \paper block.
-        if not LINE_WIDTH in self.option_dict:
-            if not QUOTE in self.option_dict:
-                if not LILYQUOTE in self.option_dict:
-                    self.option_dict[LINE_WIDTH] = "#(- paper-width \
-left-margin-default right-margin-default)"
-
-    def get_option_list (self):
-        if not 'option_list' in self.__dict__:
-            option_list = []
-            for (key, value) in self.option_dict.items ():
-                if value == None:
-                    option_list.append (key)
-                else:
-                    option_list.append (key + '=' + value)
-            option_list.sort ()
-            self.option_list = option_list
-        return self.option_list
+        #if not LINE_WIDTH in self.option_dict:
+            #if not QUOTE in self.option_dict:
+                #self.option_dict[LINE_WIDTH] = "#(- paper-width \
+#left-margin-default right-margin-default)"
+
+    # Get a list of all options (as string) that influence the snippet appearance
+    def get_outputrelevant_option_strings (self):
+        return self.outputrelevant_option_list
 
     def compose_ly (self, code):
-        if FRAGMENT in self.option_dict:
-            body = FRAGMENT_LY
-        else:
-            body = FULL_LY
 
         # Defaults.
         relative = 1
@@ -489,18 +490,19 @@ left-margin-default right-margin-default)"
         #
         # To set @exampleindent locally to zero, we use the @format
         # environment for non-quoted snippets.
+        #
+        # Update: since July 2011 we are running texinfo on a test file
+        #         to detect the default exampleindent, so we might reintroduce
+        #         further usage of exampleindent again.
+        #
+        # First, make sure we have some falback default value, auto-detected
+        # values and explicitly specified values will always override them:
         override[EXAMPLEINDENT] = r'0.4\in'
-        override[LINE_WIDTH] = '5\\in' # = texinfo_line_widths['@smallbook']
+        override[LINE_WIDTH] = '5\\in'
         override.update (self.formatter.default_snippet_options)
+        override['padding_mm'] = self.global_options.padding_mm
 
-        option_list = []
-        for option in self.get_option_list ():
-            for name in PROCESSING_INDEPENDENT_OPTIONS:
-                if option.startswith (name):
-                    break
-            else:
-                option_list.append (option)
-        option_string = ','.join (option_list)
+        option_string = ','.join (self.get_outputrelevant_option_strings ())
         compose_dict = {}
         compose_types = [NOTES, PREAMBLE, LAYOUT, PAPER]
         for a in compose_types:
@@ -510,20 +512,6 @@ left-margin-default right-margin-default)"
         option_names.sort ()
         for key in option_names:
             value = self.option_dict[key]
-            (c_key, c_value) = classic_lilypond_book_compatibility (key, value)
-            if c_key:
-                if c_value:
-                    warning (
-                        _ ("deprecated ly-option used: %s=%s") % (key, value))
-                    warning (
-                        _ ("compatibility mode translation: %s=%s") % (c_key, c_value))
-                else:
-                    warning (
-                        _ ("deprecated ly-option used: %s") % key)
-                    warning (
-                        _ ("compatibility mode translation: %s") % c_key)
-
-                (key, value) = (c_key, c_value)
 
             if value:
                 override[key] = value
@@ -553,6 +541,12 @@ left-margin-default right-margin-default)"
         elif relative > 0:
             relative_quotes += "'" * relative
 
+        # put paper-size first, if it exists
+        for i,elem in enumerate(compose_dict[PAPER]):
+            if elem.startswith("#(set-paper-size"):
+                compose_dict[PAPER].insert(0, compose_dict[PAPER].pop(i))
+                break
+
         paper_string = '\n  '.join (compose_dict[PAPER]) % override
         layout_string = '\n  '.join (compose_dict[LAYOUT]) % override
         notes_string = '\n  '.join (compose_dict[NOTES]) % vars ()
@@ -566,6 +560,10 @@ left-margin-default right-margin-default)"
         d = globals().copy()
         d.update (locals())
         d.update (self.global_options.information)
+        if FRAGMENT in self.option_dict:
+            body = FRAGMENT_LY
+        else:
+            body = FULL_LY
         return (PREAMBLE_LY + body) % d
 
     def get_checksum (self):
@@ -580,12 +578,8 @@ left-margin-default right-margin-default)"
             # code plus fragment options relevant to processing by
             # lilypond, not the snippet + preamble
             hash = md5 (self.relevant_contents (self.ly ()))
-            for option in self.get_option_list ():
-                for name in PROCESSING_INDEPENDENT_OPTIONS:
-                    if option.startswith (name):
-                        break
-                else:
-                    hash.update (option)
+            for option in self.get_outputrelevant_option_strings ():
+                hash.update (option)
 
             ## let's not create too long names.
             self.checksum = hash.hexdigest ()[:10]
@@ -594,7 +588,7 @@ left-margin-default right-margin-default)"
 
     def basename (self):
         cs = self.get_checksum ()
-        name = '%s/lily-%s' % (cs[:2], cs[2:])
+        name = os.path.join (cs[:2], 'lily-%s' % cs[2:])
         return name
 
     final_basename = basename
@@ -610,7 +604,7 @@ left-margin-default right-margin-default)"
             existing = open (filename, 'r').read ()
 
             if self.relevant_contents (existing) != self.relevant_contents (self.full_ly ()):
-                warning ("%s: duplicate filename but different contents of orginal file,\n\
+                warning ("%s: duplicate filename but different contents of original file,\n\
 printing diff against existing file." % filename)
                 ly.stderr_write (self.filter_pipe (self.full_ly (), 'diff -u %s -' % filename))
         else:
@@ -624,7 +618,7 @@ printing diff against existing file." % filename)
     def link_all_output_files (self, output_dir, output_dir_files, destination):
         existing, missing = self.all_output_files (output_dir, output_dir_files)
         if missing:
-            print '\nMissing', missing
+            error (_ ('Missing files: %s') % ', '.join (missing))
             raise CompileError(self.basename())
         for name in existing:
             if (self.global_options.use_source_file_names
@@ -649,7 +643,31 @@ printing diff against existing file." % filename)
             dst_path = os.path.split(dst)[0]
             if not os.path.isdir (dst_path):
                 os.makedirs (dst_path)
-            os.link (src, dst)
+            try:
+                if (self.global_options.use_source_file_names
+                        and isinstance (self, LilypondFileSnippet)):
+                    fout = open (dst, 'w')
+                    fin = open (src, 'r')
+                    for line in fin.readlines ():
+                        fout.write (line.replace (self.basename (), self.final_basename ()))
+                    fout.close ()
+                    fin.close ()
+                else:
+                    try:
+                        os.link (src, dst)
+                    except AttributeError:
+                        shutil.copyfile (src, dst)
+            except (IOError, OSError):
+                error (_ ('Could not overwrite file %s') % dst)
+                raise CompileError(self.basename())
+
+    def additional_files_to_consider (self, base, full):
+        return []
+    def additional_files_required (self, base, full):
+        result = [];
+        if self.ext != '.ly':
+            result.append (base + self.ext)
+        return result
 
 
     def all_output_files (self, output_dir, output_dir_files):
@@ -686,6 +704,7 @@ printing diff against existing file." % filename)
 
         map (consider_file, [base + '.tex',
                              base + '.eps',
+                             base + '.pdf',
                              base + '.texidoc',
                              base + '.doctitle',
                              base + '-systems.texi',
@@ -714,6 +733,8 @@ printing diff against existing file." % filename)
             if 'ddump-signature' in self.global_options.process_cmd:
                 consider_file (systemfile + '.signature')
 
+        map (consider_file, self.additional_files_to_consider (base, full))
+        map (require_file, self.additional_files_required (base, full))
 
         return (result, missing)
 
@@ -724,13 +745,13 @@ printing diff against existing file." % filename)
     def filter_pipe (self, input, cmd):
         """Pass input through cmd, and return the result."""
 
-        if self.global_options.verbose:
-            progress (_ ("Opening filter `%s'\n") % cmd)
+        debug (_ ("Running through filter `%s'") % cmd, True)
 
-        # TODO: Use Popen once we resolve the problem with msvcrt in Windows:
-        (stdin, stdout, stderr) = os.popen3 (cmd)
-        # p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
-        # (stdin, stdout, stderr) = (p.stdin, p.stdout, p.stderr)
+        closefds = True
+        if (sys.platform == "mingw32"):
+            closefds = False
+        p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=closefds)
+        (stdin, stdout, stderr) = (p.stdin, p.stdout, p.stderr)
         stdin.write (input)
         status = stdin.close ()
 
@@ -738,21 +759,20 @@ printing diff against existing file." % filename)
             status = 0
             output = stdout.read ()
             status = stdout.close ()
-            error = stderr.read ()
+            err = stderr.read ()
 
         if not status:
             status = 0
         signal = 0x0f & status
-        if status or (not output and error):
+        if status or (not output and err):
             exit_status = status >> 8
             ly.error (_ ("`%s' failed (%d)") % (cmd, exit_status))
             ly.error (_ ("The error log is as follows:"))
-            ly.stderr_write (error)
+            ly.stderr_write (err)
             ly.stderr_write (stderr.read ())
             exit (status)
 
-        if self.global_options.verbose:
-            progress ('\n')
+        debug ('\n')
 
         return output
 
@@ -800,7 +820,9 @@ re_end_verbatim = re.compile (r'\s+%.*?end verbatim.*$', re.M)
 class LilypondFileSnippet (LilypondSnippet):
     def __init__ (self, type, match, formatter, line_number, global_options):
         LilypondSnippet.__init__ (self, type, match, formatter, line_number, global_options)
-        self.contents = file (BookBase.find_file (self.substring ('filename'), global_options.include_path)).read ()
+        self.filename = self.substring ('filename')
+        self.contents = file (BookBase.find_file (self.filename,
+            global_options.include_path, global_options.original_dir)).read ()
 
     def get_snippet_code (self):
         return self.contents;
@@ -816,18 +838,106 @@ class LilypondFileSnippet (LilypondSnippet):
         return s
 
     def ly (self):
-        name = self.substring ('filename')
+        name = self.filename
         return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
                 % (name, self.contents))
 
     def final_basename (self):
         if self.global_options.use_source_file_names:
-            base = os.path.splitext (os.path.basename (self.substring ('filename')))[0]
+            base = os.path.splitext (os.path.basename (self.filename))[0]
             return base
         else:
             return self.basename ()
 
 
+class MusicXMLFileSnippet (LilypondFileSnippet):
+    def __init__ (self, type, match, formatter, line_number, global_options):
+        LilypondFileSnippet.__init__ (self, type, match, formatter, line_number, global_options)
+        self.compressed = False
+        self.converted_ly = None
+        self.ext = os.path.splitext (os.path.basename (self.filename))[1]
+        self.musicxml_options_dict = {
+           'verbose': '--verbose',
+           'lxml': '--lxml',
+           'compressed': '--compressed',
+           'relative': '--relative',
+           'absolute': '--absolute',
+           'no-articulation-directions': '--no-articulation-directions',
+           'no-rest-positions': '--no-rest-positions',
+           'no-page-layout': '--no-page-layout',
+           'no-beaming': '--no-beaming',
+           'language': '--language',
+        }
+
+    def snippet_options (self):
+        return self.musicxml_options_dict.keys ()
+
+    def convert_from_musicxml (self):
+        name = self.filename
+        xml2ly_option_list = []
+        for (key, value) in self.option_dict.items ():
+            cmd_key = self.musicxml_options_dict.get (key, None)
+            if cmd_key == None:
+                continue
+            if value == None:
+                xml2ly_option_list.append (cmd_key)
+            else:
+                xml2ly_option_list.append (cmd_key + '=' + value)
+        if ('.mxl' in name) and ('--compressed' not in xml2ly_option_list):
+            xml2ly_option_list.append ('--compressed')
+            self.compressed = True
+        opts = " ".join (xml2ly_option_list)
+        progress (_ ("Converting MusicXML file `%s'...\n") % self.filename)
+
+        ly_code = self.filter_pipe (self.contents, 'musicxml2ly %s --out=- - ' % opts)
+        return ly_code
+
+    def ly (self):
+        if self.converted_ly == None:
+            self.converted_ly = self.convert_from_musicxml ()
+        name = self.filename
+        return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
+                % (name, self.converted_ly))
+
+    def write_ly (self):
+        base = self.basename ()
+        path = os.path.join (self.global_options.lily_output_dir, base)
+        directory = os.path.split(path)[0]
+        if not os.path.isdir (directory):
+            os.makedirs (directory)
+
+        # First write the XML to a file (so we can link it!)
+        if self.compressed:
+            xmlfilename = path + '.mxl'
+        else:
+            xmlfilename = path + '.xml'
+        if os.path.exists (xmlfilename):
+            diff_against_existing = self.filter_pipe (self.contents, 'diff -u %s - ' % xmlfilename)
+            if diff_against_existing:
+                warning (_ ("%s: duplicate filename but different contents of original file,\n\
+printing diff against existing file.") % xmlfilename)
+                ly.stderr_write (diff_against_existing)
+        else:
+            out = file (xmlfilename, 'w')
+            out.write (self.contents)
+            out.close ()
+
+        # also write the converted lilypond
+        filename = path + '.ly'
+        if os.path.exists (filename):
+            diff_against_existing = self.filter_pipe (self.full_ly (), 'diff -u %s -' % filename)
+            if diff_against_existing:
+                warning (_ ("%s: duplicate filename but different contents of converted lilypond file,\n\
+printing diff against existing file.") % filename)
+                ly.stderr_write (diff_against_existing)
+        else:
+            out = file (filename, 'w')
+            out.write (self.full_ly ())
+            out.close ()
+            file (path + '.txt', 'w').write ('image of music')
+
+
+
 class LilyPondVersionString (Snippet):
     """A string that does not require extra memory."""
     def __init__ (self, type, match, formatter, line_number, global_options):
@@ -843,4 +953,5 @@ snippet_type_to_class = {
     'lilypond': LilypondSnippet,
     'include': IncludeSnippet,
     'lilypondversion': LilyPondVersionString,
+    'musicxml_file': MusicXMLFileSnippet,
 }