]> git.donarmstrong.com Git - lilypond.git/blobdiff - scripts/lilypond-book.py
Merge branch 'lilypond/translation' of ssh://trettig@git.sv.gnu.org/srv/git/lilypond...
[lilypond.git] / scripts / lilypond-book.py
index 3ceb9c4518b99bdf4106e7396b752c932c17542d..8e14256ccb8a96712a83b05ab235f1d389a7c26d 100644 (file)
@@ -28,7 +28,7 @@ TODO:
 
 '''
 
-import commands
+import glob
 import md5
 import os
 import re
@@ -42,6 +42,7 @@ import tempfile
 
 import lilylib as ly
 import fontextract
+import langdefs
 global _;_=ly._
 
 
@@ -135,6 +136,12 @@ def get_option_parser ():
                   action='store', dest='info_images_dir',
                   default='')
 
+    p.add_option ('--latex-program',
+                  help=_ ("run executable PROG instead of latex"),
+                  metavar=_ ("PROG"),
+                  action='store', dest='latex_program',
+                  default='latex')
+
     p.add_option ('--left-padding', 
                   metavar=_ ("PAD"),
                   dest="padding_mm",
@@ -147,6 +154,18 @@ def get_option_parser ():
                   action='store', dest='output_dir',
                   default='')
     
+    p.add_option ('--skip-lily-check',
+                  help=_ ("do not fail if no lilypond output is found."),
+                  metavar=_ ("DIR"),
+                  action='store_true', dest='skip_lilypond_run',
+                  default=False)
+
+    p.add_option ('--skip-png-check',
+                  help=_ ("do not fail if no PNG images are found for EPS files"),
+                  metavar=_ ("DIR"),
+                  action='store_true', dest='skip_png_check',
+                  default=False)
+    
     p.add_option ('--lily-output-dir',
                   help=_ ("write lily-XXX files to DIR, link into --output dir."),
                   metavar=_ ("DIR"),
@@ -164,11 +183,6 @@ def get_option_parser ():
                   help=_ ("create PDF files for use with PDFTeX"),
                   default=False)
 
-    p.add_option ('', '--psfonts', action="store_true", dest="psfonts",
-                  help=_ ('''extract all PostScript fonts into INPUT.psfonts for LaTeX
-must use this with dvips -h INPUT.psfonts'''),
-                  default=None)
-
     p.add_option ('-V', '--verbose', help=_ ("be verbose"),
                   action="store_true",
                   default=False,
@@ -199,6 +213,8 @@ global_options = None
 
 default_ly_options = { 'alt': "[image of music]" }
 
+document_language = ''
+
 #
 # Is this pythonic?  Personally, I find this rather #define-nesque. --hwn
 #
@@ -211,6 +227,7 @@ FILTER = 'filter'
 FRAGMENT = 'fragment'
 HTML = 'html'
 INDENT = 'indent'
+LANG = 'lang'
 LATEX = 'latex'
 LAYOUT = 'layout'
 LINE_WIDTH = 'line-width'
@@ -218,6 +235,7 @@ LILYQUOTE = 'lilyquote'
 NOFRAGMENT = 'nofragment'
 NOINDENT = 'noindent'
 NOQUOTE = 'noquote'
+NORAGGED_RIGHT = 'noragged-right'
 NOTES = 'body'
 NOTIME = 'notime'
 OUTPUT = 'output'
@@ -230,6 +248,7 @@ QUOTE = 'quote'
 RAGGED_RIGHT = 'ragged-right'
 RELATIVE = 'relative'
 STAFFSIZE = 'staffsize'
+DOCTITLE = 'doctitle'
 TEXIDOC = 'texidoc'
 TEXINFO = 'texinfo'
 VERBATIM = 'verbatim'
@@ -516,7 +535,9 @@ simple_options = [
     NOFRAGMENT,
     NOINDENT,
     PRINTFILENAME,
+    DOCTITLE,
     TEXIDOC,
+    LANG,
     VERBATIM,
     FONTLOAD,
     FILENAME,
@@ -542,6 +563,8 @@ ly_options = {
 
         RAGGED_RIGHT: r'''ragged-right = ##t''',
 
+        NORAGGED_RIGHT: r'''ragged-right = ##f''',
+
         PACKED: r'''packed = ##t''',
     },
 
@@ -807,13 +830,41 @@ def verbatim_html (s):
            re.sub ('<', '&lt;',
                re.sub ('&', '&amp;', s)))
 
+ly_var_def_re = re.compile (r'^([a-zA-Z]+)[\t ]*=', re.M)
+ly_comment_re = re.compile (r'(%+[\t ]*)(.*)$', re.M)
+
+def ly_comment_gettext (t, m):
+    return m.group (1) + t (m.group (2))
+
+def verb_ly_gettext (s):
+    if not document_language:
+        return s
+    try:
+        t = langdefs.translation[document_language]
+    except:
+        return s
+
+    s = ly_comment_re.sub (lambda m: ly_comment_gettext (t, m), s)
+    
+    for v in ly_var_def_re.findall (s):
+        s = re.sub (r"(?m)(^|[' \\#])%s([^a-zA-Z])" % v,
+                    "\\1" + t (v) + "\\2",
+                    s)
+    return s
 
+texinfo_lang_re = re.compile ('(?m)^@documentlanguage (.*?)( |$)')
 def set_default_options (source, default_ly_options, format):
+    global document_language
     if LINE_WIDTH not in default_ly_options:
         if format == LATEX:
             textwidth = get_latex_textwidth (source)
             default_ly_options[LINE_WIDTH] = '%.0f\\pt' % textwidth
         elif format == TEXINFO:
+            m = texinfo_lang_re.search (source)
+            if m and not m.group (1).startswith ('en'):
+                document_language = m.group (1)
+            else:
+                document_language = ''
             for regex in texinfo_line_widths:
                 # FIXME: @layout is usually not in
                 # chunk #0:
@@ -834,12 +885,6 @@ class Chunk:
     def filter_text (self):
         return self.replacement_text ()
 
-    def ly_is_outdated (self):
-        return False
-
-    def png_is_outdated (self):
-        return False
-
     def is_plain (self):
         return False
     
@@ -897,7 +942,7 @@ class LilypondSnippet (Snippet):
         self.do_options (os, self.type)
 
     def verb_ly (self):
-        return self.substring ('code')
+        return verb_ly_gettext (self.substring ('code'))
 
     def ly (self):
         contents = self.substring ('code')
@@ -910,8 +955,7 @@ class LilypondSnippet (Snippet):
             return self.compose_ly (s)
         return ''
 
-    @staticmethod
-    def split_options (option_string):
+    def split_options (self, option_string):
         if option_string:
             if self.format == HTML:
                 options = re.findall('[\w\.-:]+(?:\s*=\s*(?:"[^"]*"|\'[^\']*\'|\S+))?',
@@ -927,7 +971,7 @@ class LilypondSnippet (Snippet):
     def do_options (self, option_string, type):
         self.option_dict = {}
 
-        options = split_options (option_string)
+        options = self.split_options (option_string)
 
         for option in options:
             if '=' in option:
@@ -952,6 +996,12 @@ class LilypondSnippet (Snippet):
             if k not in self.option_dict:
                 self.option_dict[k] = default_ly_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
@@ -1033,18 +1083,14 @@ class LilypondSnippet (Snippet):
             if c_key:
                 if c_value:
                     warning (
-                        _ ("deprecated ly-option used: %s=%s"
-                           % (key, value)))
+                        _ ("deprecated ly-option used: %s=%s") % (key, value))
                     warning (
-                        _ ("compatibility mode translation: %s=%s"
-                           % (c_key, c_value)))
+                        _ ("compatibility mode translation: %s=%s") % (c_key, c_value))
                 else:
                     warning (
-                        _ ("deprecated ly-option used: %s"
-                           % key))
+                        _ ("deprecated ly-option used: %s") % key)
                     warning (
-                        _ ("compatibility mode translation: %s"
-                           % c_key))
+                        _ ("compatibility mode translation: %s") % c_key)
 
                 (key, value) = (c_key, c_value)
 
@@ -1099,27 +1145,28 @@ class LilypondSnippet (Snippet):
         return self.checksum
 
     def basename (self):
-        if FILENAME in self.option_dict:
-            return self.option_dict[FILENAME]
-
         cs = self.get_checksum ()
-
-        # TODO: use xx/xxxxx directory layout.
-        name = 'lily-%s' % cs[:10]
-        if global_options.lily_output_dir:
-            name = os.path.join (global_options.lily_output_dir, name)
+        name = '%s/lily-%s' % (cs[:2], cs[2:10])
         return name
 
     def write_ly (self):
-        out = file (self.basename () + '.ly', 'w')
+        base = self.basename ()
+        path = os.path.join (global_options.lily_output_dir, base)
+        directory = os.path.split(path)[0]
+        if not os.path.isdir (directory):
+            os.makedirs (directory)
+        out = file (path + '.ly', 'w')
         out.write (self.full_ly ())
-        file (self.basename () + '.txt', 'w').write ('image of music')
+        file (path + '.txt', 'w').write ('image of music')
 
     def relevant_contents (self, ly):
         return re.sub (r'\\(version|sourcefileline|sourcefilename)[^\n]*\n', '', ly)
 
     def link_all_output_files (self, output_dir, output_dir_files, destination):
-        existing = self.all_output_files (output_dir_files)
+        existing, missing = self.all_output_files (output_dir, output_dir_files)
+        if missing:
+            print '\nMissing', missing
+            raise CompileError(self.basename())
         for name in existing:
             try:
                 os.unlink (os.path.join (destination, name))
@@ -1128,93 +1175,92 @@ class LilypondSnippet (Snippet):
 
             src = os.path.join (output_dir, name)
             dst = os.path.join (destination, name)
+            dst_path = os.path.split(dst)[0]
+            if not os.path.isdir (dst_path):
+                os.makedirs (dst_path)
             os.link (src, dst)
 
         
-    def all_output_files (self, output_dir_files):
-        """Return all files generated in lily_output_dir.
+    def all_output_files (self, output_dir, output_dir_files):
+        """Return all files generated in lily_output_dir, a set.
 
         output_dir_files is the list of files in the output directory.
         """
-        base = os.path.basename(self.basename())
-        result = set()
-        for required in [base + '.ly',
-                         base + '.txt']:
-            result.add (required)
-        
+        result = set ()
+        missing = set ()
+        base = self.basename()
+        full = os.path.join (output_dir, base)
         def consider_file (name):
             if name in output_dir_files:
                 result.add (name)
+             
+        def require_file (name):
+            if name in output_dir_files:
+                result.add (name)
+            else:
+                missing.add (name)
+
+        # UGH - junk global_options
+        skip_lily = global_options.skip_lilypond_run
+        for required in [base + '.ly',
+                         base + '.txt']:
+            require_file (required)
+        if not skip_lily:
+            require_file (base + '-systems.count')
+
+        if 'ddump-profile' in global_options.process_cmd:
+            require_file (base + '.profile')
+        if 'dseparate-log-file' in global_options.process_cmd:
+            require_file (base + '.log')
 
         map (consider_file, [base + '.tex',
                              base + '.eps',
-                             base + '-systems.count',
+                             base + '.texidoc',
+                             base + '.doctitle',
                              base + '-systems.texi',
                              base + '-systems.tex',
                              base + '-systems.pdftexi'])
-
-        if base + '.eps' in result:
-            page_count = ps_page_count (self.basename() + '.eps')
+        if document_language:
+            map (consider_file,
+                 [base + '.texidoc' + document_language,
+                  base + '.doctitle' + document_language])
+
+        # UGH - junk global_options
+        if (base + '.eps' in result and self.format in (HTML, TEXINFO)
+            and not global_options.skip_png_check):
+            page_count = ps_page_count (full + '.eps')
             if page_count <= 1:
-                consider_file (base + '.png')
+                require_file (base + '.png')
             else:
                 for page in range (1, page_count + 1):
-                    consider_file (base + '-page%d.png' % page)
-
-        if (base + '-systems.count') in result:
-            system_count = int(file (self.basename () + '-systems.count').read())
-            for number in range(1, system_count + 1):
-                systemfile = '%s-%d' % (base, number)
-                consider_file (systemfile + '.eps')
-                consider_file (systemfile + '.pdf')
-        
-        return result
-             
-    def ly_is_outdated (self):
-        base = self.basename ()
-        ly_file = find_file (base + '.ly', raise_error=False)
-        tex_file = find_file (base + '.tex', raise_error=False)
-        systems_file = find_file (base + '-systems.tex', raise_error=False)
+                    require_file (base + '-page%d.png' % page)
 
-        if (not os.path.exists (ly_file)
-            or not os.path.exists (systems_file)):
-            return True
+        system_count = 0
+        if not skip_lily and not missing:
+            system_count = int(file (full + '-systems.count').read())
 
-        lines = file (systems_file).readlines ()
-        if not lines:
-            return True
+        for number in range(1, system_count + 1):
+            systemfile = '%s-%d' % (base, number)
+            require_file (systemfile + '.eps')
+            consider_file (systemfile + '.pdf')
 
-        if not re.match ('% eof', lines[-1]):
-            return true
-        
-        return False
-
-    def png_is_outdated (self):
-        base = self.basename ()
-        eps_file = find_file (base + '.eps', raise_error=False)
-        png_file = find_file (base + '.png', raise_error=False)
-        if self.ly_is_outdated ():
-            return True
-        
-        if (self.format in (HTML, TEXINFO)
-            and os.path.exists (eps_file)):
-            page_count = ps_page_count (eps_file)
-            if page_count <= 1:
-                return not os.path.exists (png_file)
-            else:
-                for page in range (1, page_count + 1):
-                    if not find_file (base + '-page%d.png' % page,
-                                      raise_error=False):
-                        return True
-                
-        return False
+            # We can't require signatures, since books and toplevel
+            # markups do not output a signature.
+            if 'ddump-signature' in global_options.process_cmd:
+                consider_file (systemfile + '.signature')
+             
+       
+        return (result, missing)
+    
+    def is_outdated (self, output_dir, current_files):
+        found, missing = self.all_output_files (output_dir, current_files)
+        return missing
     
     def filter_text (self):
         """Run snippet bodies through a command (say: convert-ly).
 
         This functionality is rarely used, and this code must have bitrot.
         """
-        
         code = self.substring ('code')
         s = filter_pipe (code, global_options.filter_cmd)
         d = {
@@ -1327,9 +1373,19 @@ class LilypondSnippet (Snippet):
     def output_texinfo (self):
         str = self.output_print_filename (TEXINFO)
         base = self.basename ()
+        if DOCTITLE in self.option_dict:
+            doctitle = base + '.doctitle'
+            translated_doctitle = doctitle + document_language
+            if os.path.exists (translated_doctitle):
+                str += '@lydoctitle %s\n' % open (translated_doctitle).read ()
+            elif os.path.exists (doctitle):
+                str += '@lydoctitle %s\n' % open (doctitle).read ()
         if TEXIDOC in self.option_dict:
             texidoc = base + '.texidoc'
-            if os.path.exists (texidoc):
+            translated_texidoc = texidoc + document_language
+            if os.path.exists (translated_texidoc):
+                str += '@include %(translated_texidoc)s\n\n' % vars ()
+            elif os.path.exists (texidoc):
                 str += '@include %(texidoc)s\n\n' % vars ()
 
         substr = ''
@@ -1368,7 +1424,7 @@ class LilypondFileSnippet (LilypondSnippet):
         s = self.contents
         s = re_begin_verbatim.split (s)[-1]
         s = re_end_verbatim.split (s)[0]
-        return s
+        return verb_ly_gettext (s)
 
     def ly (self):
         name = self.substring ('filename')
@@ -1402,7 +1458,7 @@ def find_linestarts (s):
 def find_toplevel_snippets (input_string, format, types):
     res = {}
     for t in types:
-        res[t] = ly.re.compile (snippet_res[format][t])
+        res[t] = re.compile (snippet_res[format][t])
 
     snippets = []
     index = 0
@@ -1460,7 +1516,7 @@ def find_toplevel_snippets (input_string, format, types):
                 endex = found[first][0]
 
         if not first:
-            snippets.append (Substring (input_string, index, len (s), line_start_idx))
+            snippets.append (Substring (input_string, index, len (input_string), line_start_idx))
             break
 
         while (start > line_starts[line_start_idx+1]):
@@ -1533,9 +1589,9 @@ def process_snippets (cmd, snippets,
 
     checksum = snippet_list_checksum (snippets)
     contents = '\n'.join (['snippet-map-%d.ly' % checksum] 
-                          + [snip.basename() for snip in snippets])
+                          + [snip.basename() + '.ly' for snip in snippets])
     name = os.path.join (lily_output_dir,
-                         'snippet-names-%d' % checksum)
+                         'snippet-names-%d.ly' % checksum)
     file (name, 'wb').write (contents)
 
     system_in_directory (' '.join ([cmd, name]),
@@ -1574,7 +1630,8 @@ def get_latex_textwidth (source):
     tmp_handle.write (latex_document)
     tmp_handle.close ()
     
-    ly.system ('latex %s' % tmpfile, be_verbose=global_options.verbose)
+    ly.system ('%s %s' % (global_options.latex_program, tmpfile),
+               be_verbose=global_options.verbose)
     parameter_string = file (logfile).read()
     
     os.unlink (tmpfile)
@@ -1634,13 +1691,27 @@ def write_file_map (lys, name):
 #(define output-empty-score-list #f)
 #(ly:add-file-name-alist '(%s
     ))\n
-""" % '\n'.join('("%s.ly" . "%s")\n' % (ly.basename (), name)
-                for ly in lys))
+""" % '\n'.join(['("%s.ly" . "%s")\n' % (ly.basename (), name)
+                 for ly in lys]))
+
+def split_output_files(directory):
+    """Returns directory entries in DIRECTORY/XX/ , where XX are hex digits.
+
+    Return value is a set of strings.
+    """
+    files = []
+    for subdir in glob.glob (os.path.join (directory, '[a-f0-9][a-f0-9]')):
+        base_subdir = os.path.split (subdir)[1]
+        sub_files = [os.path.join (base_subdir, name)
+                     for name in os.listdir (subdir)]
+        files += sub_files
+    return set (files)
 
 def do_process_cmd (chunks, input_name, options):
     snippets = [c for c in chunks if isinstance (c, LilypondSnippet)]
-    outdated = [c for c in snippets
-                if (c.ly_is_outdated () or c.png_is_outdated ())]
+
+    output_files = split_output_files (options.lily_output_dir)
+    outdated = [c for c in snippets if c.is_outdated (options.lily_output_dir, output_files)]
     
     write_file_map (outdated, input_name)    
     progress (_ ("Writing snippets..."))
@@ -1658,7 +1729,7 @@ def do_process_cmd (chunks, input_name, options):
         progress (_ ("All snippets are up to date..."))
 
     if options.lily_output_dir != options.output_dir:
-        output_files = set(os.listdir(options.lily_output_dir))
+        output_files = split_output_files (options.lily_output_dir)
         for snippet in snippets:
             snippet.link_all_output_files (options.lily_output_dir,
                                            output_files,
@@ -1706,9 +1777,14 @@ def write_if_updated (file_name, lines):
             # this prevents make from always rerunning lilypond-book:
             # output file must be touched in order to be up to date
             os.utime (file_name, None)
+            return
     except:
         pass
 
+    output_dir = os.path.dirname (file_name)
+    if not os.path.exists (output_dir):
+        os.makedirs (output_dir)
+
     progress (_ ("Writing `%s'...") % file_name)
     file (file_name, 'w').writelines (lines)
     progress ('\n')
@@ -1727,7 +1803,7 @@ def samefile (f1, f2):
         f2 = re.sub ("//*", "/", f2)
         return f1 == f2
 
-def do_file (input_filename):
+def do_file (input_filename, included=False):
     # Ugh.
     if not input_filename or input_filename == '-':
         in_handle = sys.stdin
@@ -1745,6 +1821,8 @@ def do_file (input_filename):
 
     if input_filename == '-':
         input_base = 'stdin'
+    elif included:
+        input_base = os.path.splitext (input_filename)[0]
     else:
         input_base = os.path.basename (
             os.path.splitext (input_filename)[0])
@@ -1815,11 +1893,11 @@ def do_file (input_filename):
             name = snippet.substring ('filename')
             progress (_ ("Processing include: %s") % name)
             progress ('\n')
-            return do_file (name)
+            return do_file (name, included=True)
 
         include_chunks = map (process_include,
-                   filter (lambda x: isinstance (x, IncludeSnippet),
-                       chunks))
+                              filter (lambda x: isinstance (x, IncludeSnippet),
+                                      chunks))
 
         return chunks + reduce (lambda x, y: x + y, include_chunks, [])
         
@@ -1867,8 +1945,13 @@ def main ():
                                       + ' --formats=%s -dbackend=eps ' % formats)
 
     if global_options.process_cmd:
-        global_options.process_cmd += ' '.join ([(' -I %s' % ly.mkarg (p))
-                                                 for p in global_options.include_path])
+        includes = global_options.include_path
+        if global_options.lily_output_dir:
+            # This must be first, so lilypond prefers to read .ly
+            # files in the other lybookdb dir.
+            includes = [os.path.abspath(global_options.lily_output_dir)] + includes
+        global_options.process_cmd += ' '.join ([' -I %s' % ly.mkarg (p)
+                                                 for p in includes])
 
     if global_options.format in (TEXINFO, LATEX):
         ## prevent PDF from being switched on by default.
@@ -1882,7 +1965,7 @@ def main ():
     if global_options.padding_mm:
         global_options.process_cmd += " -deps-box-padding=%f " % global_options.padding_mm
         
-    global_options.process_cmd += " -dread-file-list "
+    global_options.process_cmd += " -dread-file-list -dno-strip-output-dir"
 
     if global_options.lily_output_dir:
         global_options.lily_output_dir = os.path.abspath(global_options.lily_output_dir)