]> git.donarmstrong.com Git - lilypond.git/blobdiff - scripts/lilypond-book.py
Docs-fr: fix PDF docs build
[lilypond.git] / scripts / lilypond-book.py
index 60886775165b63ffd9627c531db6f86f8d360b7b..d52e09ad2f2725c201523bd0f783b3a0d8190724 100644 (file)
@@ -1,5 +1,20 @@
 #!@TARGET_PYTHON@
 
+# This file is part of LilyPond, the GNU music typesetter.
+#
+# LilyPond is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# LilyPond is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with LilyPond.  If not, see <http://www.gnu.org/licenses/>.
+
 '''
 Example usage:
 
@@ -15,7 +30,7 @@ classic lilypond-book:
 TODO:
 
   *  this script is too complex. Modularize.
-  
+
   *  ly-options: intertext?
   *  --line-width?
   *  eps in latex / eps by lilypond -b ps?
@@ -28,14 +43,12 @@ TODO:
 
 '''
 
-import stat
-import tempfile
-import commands
+import glob
 import os
-import sys
 import re
-import md5
-import operator
+import stat
+import sys
+import tempfile
 
 """
 @relocate-preamble@
@@ -43,13 +56,26 @@ import operator
 
 import lilylib as ly
 import fontextract
+import langdefs
 global _;_=ly._
 
+ly.require_python_version ()
 
 # Lilylib globals.
 program_version = '@TOPLEVEL_VERSION@'
 program_name = os.path.basename (sys.argv[0])
 
+# Check if program_version contains @ characters. This will be the case if
+# the .py file is called directly while building the lilypond documentation.
+# If so, try to check for the env var LILYPOND_VERSION, which is set by our
+# makefiles and use its value.
+at_re = re.compile (r'@')
+if at_re.match (program_version):
+    if os.environ.has_key('LILYPOND_VERSION'):
+        program_version = os.environ['LILYPOND_VERSION']
+    else:
+        program_version = "unknown"
+
 original_dir = os.getcwd ()
 backend = 'ps'
 
@@ -58,13 +84,13 @@ _ ("Process LilyPond snippets in hybrid HTML, LaTeX, texinfo or DocBook document
 + '\n\n'
 + _ ("Examples:")
 + '''
- lilypond-book --filter="tr '[a-z]' '[A-Z]'" %(BOOK)s
lilypond-book --filter="convert-ly --no-version --from=2.0.0 -" %(BOOK)s
- lilypond-book --process='lilypond -I include' %(BOOK)s
lilypond-book --filter="tr '[a-z]' '[A-Z]'" %(BOOK)s
$ lilypond-book -F "convert-ly --no-version --from=2.0.0 -" %(BOOK)s
lilypond-book --process='lilypond -I include' %(BOOK)s
 ''' % {'BOOK': _ ("BOOK")})
 
 authors = ('Jan Nieuwenhuizen <janneke@gnu.org>',
-      'Han-Wen Nienhuys <hanwen@xs4all.nl>')
+           'Han-Wen Nienhuys <hanwen@xs4all.nl>')
 
 ################################################################
 def exit (i):
@@ -85,7 +111,7 @@ def error (s):
     ly.stderr_write (program_name + ": " + _ ("error: %s") % s + '\n')
 
 def ps_page_count (ps_name):
-    header = open (ps_name).read (1024)
+    header = file (ps_name).read (1024)
     m = re.search ('\n%%Pages: ([0-9]+)', header)
     if m:
         return int (m.group (1))
@@ -96,14 +122,14 @@ def warranty ():
     ly.encoded_write (sys.stdout, '''
 %s
 
-%s
+  %s
 
 %s
 %s
-''' % ( _ ('Copyright (c) %s by') % '2001--2007',
-    ' '.join (authors),
-   _ ("Distributed under terms of the GNU General Public License."),
-   _ ("It comes with NO WARRANTY.")))
+''' % ( _ ('Copyright (c) %s by') % '2001--2010',
+        '\n  '.join (authors),
+        _ ("Distributed under terms of the GNU General Public License."),
+        _ ("It comes with NO WARRANTY.")))
 
 def get_option_parser ():
     p = ly.get_option_parser (usage=_ ("%s [OPTION]... FILE") % 'lilypond-book',
@@ -113,11 +139,12 @@ def get_option_parser ():
     p.add_option ('-F', '--filter', metavar=_ ("FILTER"),
                   action="store",
                   dest="filter_cmd",
-                  help=_ ("pipe snippets through FILTER [convert-ly -n -]"),
+                  help=_ ("pipe snippets through FILTER [default: `convert-ly -n -']"),
                   default=None)
 
     p.add_option ('-f', '--format',
                   help=_ ("use output format FORMAT (texi [default], texi-html, latex, html, docbook)"),
+                  metavar=_ ("FORMAT"),
                   action='store')
 
     p.add_option("-h", "--help",
@@ -136,22 +163,30 @@ def get_option_parser ():
                   action='store', dest='info_images_dir',
                   default='')
 
-    p.add_option ('--left-padding', 
+    p.add_option ('--latex-program',
+                  help=_ ("run executable PROG instead of latex, or in\n\
+case --pdf option is set instead of pdflatex"),
+                  metavar=_ ("PROG"),
+                  action='store', dest='latex_program',
+                  default='latex')
+
+    p.add_option ('--left-padding',
                   metavar=_ ("PAD"),
                   dest="padding_mm",
                   help=_ ("pad left side of music to align music inspite of uneven bar numbers (in mm)"),
                   type="float",
                   default=3.0)
-    
+
+    p.add_option ('--lily-output-dir',
+                  help=_ ("write lily-XXX files to DIR, link into --output dir"),
+                  metavar=_ ("DIR"),
+                  action='store', dest='lily_output_dir',
+                  default=None)
+
     p.add_option ("-o", '--output', help=_ ("write output to DIR"),
                   metavar=_ ("DIR"),
-                  action='store', dest='output_name',
+                  action='store', dest='output_dir',
                   default='')
-    
-    p.add_option ('-P', '--process', metavar=_ ("COMMAND"),
-                  help = _ ("process ly_files using COMMAND FILE..."),
-                  action='store', 
-                  dest='process_cmd', default='lilypond -dbackend=eps')
 
     p.add_option ('--pdf',
                   action="store_true",
@@ -159,10 +194,27 @@ 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 ('-P', '--process', metavar=_ ("COMMAND"),
+                  help = _ ("process ly_files using COMMAND FILE..."),
+                  action='store',
+                  dest='process_cmd', 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 ('--use-source-file-names',
+                  help=_ ("write snippet output files with the same base name as their source file"),
+                  action='store_true', dest='use_source_file_names',
+                  default=False)
 
     p.add_option ('-V', '--verbose', help=_ ("be verbose"),
                   action="store_true",
@@ -177,14 +229,21 @@ must use this with dvips -h INPUT.psfonts'''),
     p.add_option ('-w', '--warranty',
                   help=_ ("show warranty and copyright"),
                   action='store_true')
-    p.add_option_group (ly.display_encode (_ ('Bugs')),
-                        description=(_ ("Report bugs via")
-                                     + ''' http://post.gmane.org/post.php'''
-                                     '''?group=gmane.comp.gnu.lilypond.bugs\n'''))
+    p.add_option_group ('',
+                        description=(
+        _ ("Report bugs via %s")
+        % ' http://post.gmane.org/post.php'
+        '?group=gmane.comp.gnu.lilypond.bugs') + '\n')
     return p
 
 lilypond_binary = os.path.join ('@bindir@', 'lilypond')
 
+# If we are called with full path, try to use lilypond binary
+# installed in the same path; this is needed in GUB binaries, where
+# @bindir is always different from the installed binary path.
+if 'bindir' in globals () and bindir:
+    lilypond_binary = os.path.join (bindir, 'lilypond')
+
 # Only use installed binary when we are installed too.
 if '@bindir@' == ('@' + 'bindir@') or not os.path.exists (lilypond_binary):
     lilypond_binary = 'lilypond'
@@ -194,6 +253,8 @@ global_options = None
 
 default_ly_options = { 'alt': "[image of music]" }
 
+document_language = ''
+
 #
 # Is this pythonic?  Personally, I find this rather #define-nesque. --hwn
 #
@@ -206,18 +267,20 @@ FILTER = 'filter'
 FRAGMENT = 'fragment'
 HTML = 'html'
 INDENT = 'indent'
+LANG = 'lang'
 LATEX = 'latex'
 LAYOUT = 'layout'
 LINE_WIDTH = 'line-width'
 LILYQUOTE = 'lilyquote'
 NOFRAGMENT = 'nofragment'
+NOGETTEXT = 'nogettext'
 NOINDENT = 'noindent'
 NOQUOTE = 'noquote'
+NORAGGED_RIGHT = 'noragged-right'
 NOTES = 'body'
 NOTIME = 'notime'
 OUTPUT = 'output'
 OUTPUTIMAGE = 'outputimage'
-PACKED = 'packed'
 PAPER = 'paper'
 PREAMBLE = 'preamble'
 PRINTFILENAME = 'printfilename'
@@ -225,26 +288,36 @@ QUOTE = 'quote'
 RAGGED_RIGHT = 'ragged-right'
 RELATIVE = 'relative'
 STAFFSIZE = 'staffsize'
+DOCTITLE = 'doctitle'
 TEXIDOC = 'texidoc'
 TEXINFO = 'texinfo'
 VERBATIM = 'verbatim'
-FONTLOAD = 'fontload'
+VERSION = 'lilypondversion'
 FILENAME = 'filename'
 ALT = 'alt'
 
 
-# NOTIME has no opposite so it isn't part of this dictionary.
+# 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,
 }
 
+# Options that have no impact on processing by lilypond (or --process
+# argument)
+PROCESSING_INDEPENDENT_OPTIONS = (
+    ALT, NOGETTEXT, VERBATIM, ADDVERSION,
+    TEXIDOC, DOCTITLE, VERSION, PRINTFILENAME)
 
 # Recognize special sequences in the input.
 #
 #   (?P<name>regex) -- Assign result of REGEX to NAME.
 #   *? -- Match non-greedily.
+#   (?!...) -- Match if `...' doesn't match next (without consuming
+#              the string).
+#
 #   (?m) -- Multiline regex: Make ^ and $ match at each line.
 #   (?s) -- Make the dot match all characters including newline.
 #   (?x) -- Ignore whitespace in patterns.
@@ -258,17 +331,36 @@ snippet_res = {
         'lilypond':
          r'''(?smx)
           (?P<match>
-          <(?P<inline>(inline)?)mediaobject>\s*<textobject.*?>\s*<programlisting\s+language="lilypond".*?(role="(?P<options>.*?)")?>(?P<code>.*?)</programlisting\s*>\s*</textobject\s*>\s*</(inline)?mediaobject>)''',
+          <(?P<inline>(inline)?)mediaobject>\s*
+          <textobject.*?>\s*
+          <programlisting\s+language="lilypond".*?(role="(?P<options>.*?)")?>
+          (?P<code>.*?)
+          </programlisting\s*>\s*
+          </textobject\s*>\s*
+          </(inline)?mediaobject>)''',
 
         'lilypond_block':
          r'''(?smx)
           (?P<match>
-          <(?P<inline>(inline)?)mediaobject>\s*<textobject.*?>\s*<programlisting\s+language="lilypond".*?(role="(?P<options>.*?)")?>(?P<code>.*?)</programlisting\s*>\s*</textobject\s*>\s*</(inline)?mediaobject>)''',
+          <(?P<inline>(inline)?)mediaobject>\s*
+          <textobject.*?>\s*
+          <programlisting\s+language="lilypond".*?(role="(?P<options>.*?)")?>
+          (?P<code>.*?)
+          </programlisting\s*>\s*
+          </textobject\s*>\s*
+          </(inline)?mediaobject>)''',
 
         'lilypond_file':
          r'''(?smx)
           (?P<match>
-          <(?P<inline>(inline)?)mediaobject>\s*<imageobject.*?>\s*<imagedata\s+fileref="(?P<filename>.*?\.ly)"\s*(role="(?P<options>.*?)")?\s*(/>|>\s*</imagedata>)\s*</imageobject>\s*</(inline)?mediaobject>)''',
+          <(?P<inline>(inline)?)mediaobject>\s*
+          <imageobject.*?>\s*
+          <imagedata\s+
+           fileref="(?P<filename>.*?\.ly)"\s*
+           (role="(?P<options>.*?)")?\s*
+           (/>|>\s*</imagedata>)\s*
+          </imageobject>\s*
+          </(inline)?mediaobject>)''',
 
         'multiline_comment':
          r'''(?smx)
@@ -284,9 +376,11 @@ snippet_res = {
          no_match,
 
         'verbatim':
-       no_match,
-       
-    }, 
+         no_match,
+
+        'lilypondversion':
+         no_match,
+    },
     ##
     HTML: {
         'include':
@@ -338,6 +432,11 @@ snippet_res = {
           (?s)
           (?P<match>
            (?P<code><pre>\s.*?</pre>\s))''',
+
+        'lilypondversion':
+         r'''(?mx)
+          (?P<match>
+          <lilypondversion\s*/>)''',
     },
 
     ##
@@ -411,6 +510,12 @@ snippet_res = {
            \\begin\s*{verbatim}
             .*?
            \\end\s*{verbatim}))''',
+
+        'lilypondversion':
+         r'''(?smx)
+          (?P<match>
+          \\lilypondversion)[^a-zA-Z]''',
+
     },
 
     ##
@@ -477,17 +582,21 @@ snippet_res = {
            @example
             \s.*?
            @end\s+example\s))''',
-    },
-}
 
+        'lilypondversion':
+         r'''(?mx)
+         [^@](?P<match>
+          @lilypondversion)[^a-zA-Z]''',
 
+    },
+}
 
 
 format_res = {
-    DOCBOOK: {        
-       'intertext': r',?\s*intertext=\".*?\"',
+    DOCBOOK: {
+        'intertext': r',?\s*intertext=\".*?\"',
         'option_sep': '\s*',
-    }, 
+    },
     HTML: {
         'intertext': r',?\s*intertext=\".*?\"',
         'option_sep': '\s*',
@@ -504,16 +613,19 @@ format_res = {
     },
 }
 
+
 # Options without a pattern in ly_options.
 simple_options = [
     EXAMPLEINDENT,
     FRAGMENT,
     NOFRAGMENT,
+    NOGETTEXT,
     NOINDENT,
     PRINTFILENAME,
+    DOCTITLE,
     TEXIDOC,
+    LANG,
     VERBATIM,
-    FONTLOAD,
     FILENAME,
     ALT,
     ADDVERSION
@@ -537,19 +649,19 @@ ly_options = {
 
         RAGGED_RIGHT: r'''ragged-right = ##t''',
 
-        PACKED: r'''packed = ##t''',
+        NORAGGED_RIGHT: r'''ragged-right = ##f''',
     },
 
     ##
     LAYOUT: {
         NOTIME: r'''
  \context {
-  \Score
-  timing = ##f
+   \Score
+   timing = ##f
  }
  \context {
-  \Staff
-  \remove Time_signature_engraver
+   \Staff
+   \remove "Time_signature_engraver"
  }''',
     },
 
@@ -561,19 +673,37 @@ ly_options = {
 
 output = {
     ##
-    DOCBOOK: {                 
-        FILTER: r'''<mediaobject><textobject><programlisting language="lilypond" role="%(options)s">%(code)s</programlisting></textobject></mediaobject>''', 
-    
-        OUTPUT: r'''
-        <imageobject role="latex">
-               <imagedata fileref="%(base)s.pdf" format="PDF"/>
-               </imageobject>
-               <imageobject role="html">
-               <imagedata fileref="%(base)s.png" format="PNG"/></imageobject>''',
-    
-        VERBATIM: r'''<programlisting>%(verb)s</programlisting>''',
-    
-        PRINTFILENAME: '<textobject><simpara><ulink url="%(base)s.ly"><filename>%(filename)s</filename></ulink></simpara></textobject>'
+    DOCBOOK: {
+        FILTER: r'''<mediaobject>
+  <textobject>
+    <programlisting language="lilypond"
+                    role="%(options)s">
+%(code)s
+    </programlisting>
+  </textobject>
+</mediaobject>''',
+
+        OUTPUT: r'''<imageobject role="latex">
+  <imagedata fileref="%(base)s.pdf" format="PDF"/>
+</imageobject>
+<imageobject role="html">
+  <imagedata fileref="%(base)s.png" format="PNG"/>
+</imageobject>''',
+
+        VERBATIM: r'''<programlisting>
+%(verb)s</programlisting>''',
+
+        VERSION: program_version,
+
+        PRINTFILENAME: r'''<textobject>
+  <simpara>
+    <ulink url="%(base)s.ly">
+      <filename>
+        %(filename)s
+      </filename>
+    </ulink>
+  </simpara>
+</textobject>'''
     },
     ##
     HTML: {
@@ -590,8 +720,10 @@ output = {
  <a href="%(base)s.ly">''',
 
         OUTPUT: r'''
-  <img align="center" valign="center"
-    border="0" src="%(image)s" alt="%(alt)s">''',
+  <img align="middle"
+       border="0"
+       src="%(image)s"
+       alt="%(alt)s">''',
 
         PRINTFILENAME: '<p><tt><a href="%(base)s.ly">%(filename)s</a></tt></p>',
 
@@ -602,34 +734,38 @@ output = {
 
         VERBATIM: r'''<pre>
 %(verb)s</pre>''',
+
+        VERSION: program_version,
     },
 
     ##
     LATEX: {
         OUTPUT: r'''{%%
-\parindent 0pt%%
-\ifx\preLilyPondExample \undefined%%
- \relax%%
-\else%%
- \preLilyPondExample%%
-\fi%%
+\parindent 0pt
+\ifx\preLilyPondExample \undefined
+\else
+  \expandafter\preLilyPondExample
+\fi
 \def\lilypondbook{}%%
-\input %(base)s-systems.tex%%
-\ifx\postLilyPondExample \undefined%%
- \relax%%
-\else%%
- \postLilyPondExample%%
-\fi%%
+\input %(base)s-systems.tex
+\ifx\postLilyPondExample \undefined
+\else
+  \expandafter\postLilyPondExample
+\fi
 }''',
 
         PRINTFILENAME: '''\\texttt{%(filename)s}
-    ''',
+''',
 
-        QUOTE: r'''\begin{quotation}%(str)s
+        QUOTE: r'''\begin{quotation}
+%(str)s
 \end{quotation}''',
 
         VERBATIM: r'''\noindent
-\begin{verbatim}%(verb)s\end{verbatim}''',
+\begin{verbatim}%(verb)s\end{verbatim}
+''',
+
+        VERSION: program_version,
 
         FILTER: r'''\begin{lilypond}[%(options)s]
 %(code)s
@@ -655,8 +791,10 @@ output = {
 @html
 <p>
  <a href="%(base)s.ly">
-  <img align="center" valign="center"
-    border="0" src="%(image)s" alt="%(alt)s">
+  <img align="middle"
+       border="0"
+       src="%(image)s"
+       alt="%(alt)s">
  </a>
 </p>
 @end html
@@ -685,6 +823,8 @@ output = {
 %(verb)s@end verbatim
 ''',
 
+        VERSION: program_version,
+
         ADDVERSION: r'''@example
 \version @w{"@version{}"}
 @end example
@@ -709,14 +849,12 @@ PREAMBLE_LY = '''%%%% Generated by %(program_name)s
 
 
 %% ****************************************************************
-%% Start cut-&-pastable-section 
+%% Start cut-&-pastable-section
 %% ****************************************************************
 
 %(preamble_string)s
 
 \paper {
-  #(define dump-extents #t)
-  %(font_dump_setting)s
   %(paper_string)s
   force-assignment = #""
   line-width = #(- line-width (* mm  %(padding_mm)f))
@@ -791,7 +929,7 @@ def find_file (name, raise_error=True):
         full = os.path.join (i, name)
         if os.path.exists (full):
             return full
-        
+
     if raise_error:
         error (_ ("file not found: %s") % name + '\n')
         exit (1)
@@ -802,27 +940,49 @@ def verbatim_html (s):
            re.sub ('<', '&lt;',
                re.sub ('&', '&amp;', s)))
 
-def split_options (option_string):
-    if option_string:
-        if global_options.format == HTML:
-            options = re.findall('[\w\.-:]+(?:\s*=\s*(?:"[^"]*"|\'[^\']*\'|\S+))?',option_string)
-            for i in range(len(options)):
-                options[i] = re.sub('^([^=]+=\s*)(?P<q>["\'])(.*)(?P=q)','\g<1>\g<3>',options[i])
-            return options
-        else:
-            return re.split (format_res[global_options.format]['option_sep'],
-                    option_string)
-    return []
+ly_var_def_re = re.compile (r'^([a-zA-Z]+)[\t ]*=', re.M)
+ly_comment_re = re.compile (r'(%+[\t ]*)(.*)$', re.M)
+ly_context_id_re = re.compile ('\\\\(?:new|context)\\s+(?:[a-zA-Z]*?(?:Staff\
+(?:Group)?|Voice|FiguredBass|FretBoards|Names|Devnull))\\s+=\\s+"?([a-zA-Z]+)"?\\s+')
 
-def set_default_options (source):
-    global default_ly_options
-    if not default_ly_options.has_key (LINE_WIDTH):
-        if global_options.format == LATEX:
+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)
+
+    if langdefs.LANGDICT[document_language].enable_ly_identifier_l10n:
+        for v in ly_var_def_re.findall (s):
+            s = re.sub (r"(?m)(^|[' \\#])%s([^a-zA-Z])" % v,
+                        "\\1" + t (v) + "\\2",
+                        s)
+        for id in ly_context_id_re.findall (s):
+            s = re.sub (r'(\s+|")%s(\s+|")' % id,
+                        "\\1" + t (id) + "\\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 global_options.format == TEXINFO:
-            for (k, v) in texinfo_line_widths.items ():
+            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:
                 #
@@ -831,8 +991,8 @@ def set_default_options (source):
                 # Bluntly search first K items of
                 # source.
                 # s = chunks[0].replacement_text ()
-                if re.search (k, source[:1024]):
-                    default_ly_options[LINE_WIDTH] = v
+                if re.search (regex, source[:1024]):
+                    default_ly_options[LINE_WIDTH] = texinfo_line_widths[regex]
                     break
 
 class Chunk:
@@ -842,23 +1002,18 @@ class Chunk:
     def filter_text (self):
         return self.replacement_text ()
 
-    def ly_is_outdated (self):
-        return 0
-
-    def png_is_outdated (self):
-        return 0
-
     def is_plain (self):
         return False
-    
+
 class Substring (Chunk):
+    """A string that does not require extra memory."""
     def __init__ (self, source, start, end, line_number):
         self.source = source
         self.start = start
         self.end = end
         self.line_number = line_number
         self.override_text = None
-        
+
     def is_plain (self):
         return True
 
@@ -872,7 +1027,7 @@ class Snippet (Chunk):
     def __init__ (self, type, match, format, line_number):
         self.type = type
         self.match = match
-        self.hash = 0
+        self.checksum = 0
         self.option_dict = {}
         self.format = format
         self.line_number = line_number
@@ -886,10 +1041,10 @@ class Snippet (Chunk):
     def __repr__ (self):
         return `self.__class__` + ' type = ' + self.type
 
-class Include_snippet (Snippet):
+class IncludeSnippet (Snippet):
     def processed_filename (self):
         f = self.substring ('filename')
-        return os.path.splitext (f)[0] + format2ext[global_options.format]
+        return os.path.splitext (f)[0] + format2ext[self.format]
 
     def replacement_text (self):
         s = self.match.group ('match')
@@ -897,14 +1052,19 @@ class Include_snippet (Snippet):
 
         return re.sub (f, self.processed_filename (), s)
 
-class Lilypond_snippet (Snippet):
+class LilypondSnippet (Snippet):
     def __init__ (self, type, match, format, line_number):
         Snippet.__init__ (self, type, match, format, line_number)
         os = match.group ('options')
         self.do_options (os, self.type)
 
     def verb_ly (self):
-        return self.substring ('code')
+        verb_text = self.substring ('code')
+        if not NOGETTEXT in self.option_dict:
+            verb_text = verb_ly_gettext (verb_text)
+        if not verb_text.endswith ('\n'):
+            verb_text += '\n'
+        return verb_text
 
     def ly (self):
         contents = self.substring ('code')
@@ -917,21 +1077,34 @@ class Lilypond_snippet (Snippet):
             return self.compose_ly (s)
         return ''
 
+    def split_options (self, option_string):
+        if option_string:
+            if self.format == HTML:
+                options = re.findall('[\w\.-:]+(?:\s*=\s*(?:"[^"]*"|\'[^\']*\'|\S+))?',
+                                     option_string)
+                options = [re.sub('^([^=]+=\s*)(?P<q>["\'])(.*)(?P=q)', '\g<1>\g<3>', opt)
+                           for opt in options]
+                return options
+            else:
+                return re.split (format_res[self.format]['option_sep'],
+                                 option_string)
+        return []
+
     def do_options (self, option_string, type):
         self.option_dict = {}
 
-        options = split_options (option_string)
+        options = self.split_options (option_string)
 
-        for i in options:
-            if '=' in i:
-                (key, value) = re.split ('\s*=\s*', i)
+        for option in options:
+            if '=' in option:
+                (key, value) = re.split ('\s*=\s*', option)
                 self.option_dict[key] = value
             else:
-                if i in no_options.keys ():
-                    if no_options[i] in self.option_dict.keys ():
-                        del self.option_dict[no_options[i]]
+                if option in no_options:
+                    if no_options[option] in self.option_dict:
+                        del self.option_dict[no_options[option]]
                 else:
-                    self.option_dict[i] = None
+                    self.option_dict[option] = None
 
         has_line_width = self.option_dict.has_key (LINE_WIDTH)
         no_line_width_value = 0
@@ -941,39 +1114,60 @@ class Lilypond_snippet (Snippet):
             no_line_width_value = 1
             del self.option_dict[LINE_WIDTH]
 
-        for i in default_ly_options.keys ():
-            if i not in self.option_dict.keys ():
-                self.option_dict[i] = default_ly_options[i]
+        for k in default_ly_options:
+            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.keys ():
+            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.keys ():
+                if LINE_WIDTH in self.option_dict:
                     del self.option_dict[LINE_WIDTH]
             else:
-                if RAGGED_RIGHT in self.option_dict.keys ():
-                    if LINE_WIDTH in self.option_dict.keys ():
+                if RAGGED_RIGHT in self.option_dict:
+                    if LINE_WIDTH in self.option_dict:
                         del self.option_dict[LINE_WIDTH]
 
-            if QUOTE in self.option_dict.keys () or type == 'lilypond':
-                if LINE_WIDTH in self.option_dict.keys ():
+            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.keys ():
+        if not INDENT in self.option_dict:
             self.option_dict[INDENT] = '0\\mm'
 
-        # The QUOTE pattern from ly_options only emits the `line-width'
-        # keyword.
-        if has_line_width and QUOTE in self.option_dict.keys ():
-            if no_line_width_value:
-                del self.option_dict[LINE_WIDTH]
-            else:
-                del self.option_dict[QUOTE]
+        # 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
 
     def compose_ly (self, code):
-        if FRAGMENT in self.option_dict.keys ():
+        if FRAGMENT in self.option_dict:
             body = FRAGMENT_LY
         else:
             body = FULL_LY
@@ -1009,36 +1203,32 @@ class Lilypond_snippet (Snippet):
         override.update (default_ly_options)
 
         option_list = []
-        for (key, value) in self.option_dict.items ():
-            if value == None:
-                option_list.append (key)
-            else:
-                option_list.append (key + '=' + value)
+        for option in self.get_option_list ():
+            if not any (option.startswith (name)
+                        for name in PROCESSING_INDEPENDENT_OPTIONS):
+                option_list.append (option)
         option_string = ','.join (option_list)
-
         compose_dict = {}
         compose_types = [NOTES, PREAMBLE, LAYOUT, PAPER]
         for a in compose_types:
             compose_dict[a] = []
 
-        for (key, value) in self.option_dict.items ():
-            (c_key, c_value) = \
-             classic_lilypond_book_compatibility (key, value)
+        option_names = self.option_dict.keys ()
+        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)))
+                    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))
+                    warning (
+                        _ ("deprecated ly-option used: %s") % key)
+                    warning (
+                        _ ("compatibility mode translation: %s") % c_key)
 
                 (key, value) = (c_key, c_value)
 
@@ -1059,7 +1249,7 @@ class Lilypond_snippet (Snippet):
                 warning (_ ("ignoring unknown ly option: %s") % key)
 
         # URGS
-        if RELATIVE in override.keys () and override[RELATIVE]:
+        if RELATIVE in override and override[RELATIVE]:
             relative = int (override[RELATIVE])
 
         relative_quotes = ''
@@ -1075,81 +1265,176 @@ class Lilypond_snippet (Snippet):
         notes_string = '\n  '.join (compose_dict[NOTES]) % vars ()
         preamble_string = '\n  '.join (compose_dict[PREAMBLE]) % override
         padding_mm = global_options.padding_mm
-        font_dump_setting = ''
-        if FONTLOAD in self.option_dict:
-            font_dump_setting = '#(define-public force-eps-font-include #t)\n'
 
         d = globals().copy()
         d.update (locals())
         return (PREAMBLE_LY + body) % d
 
-    def get_hash (self):
-        if not self.hash:
-            hash = md5.md5 (self.relevant_contents (self.full_ly ()))
+    def get_checksum (self):
+        if not self.checksum:
+            # Work-around for md5 module deprecation warning in python 2.5+:
+            try: 
+                from hashlib import md5
+            except ImportError:
+                from md5 import md5
+
+            # We only want to calculate the hash based on the snippet
+            # 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)
 
             ## let's not create too long names.
-            self.hash = hash.hexdigest ()[:10]
-            
-        return self.hash
+            self.checksum = hash.hexdigest ()[:10]
+
+        return self.checksum
 
     def basename (self):
-        if FILENAME in self.option_dict:
-            return self.option_dict[FILENAME]
-        if global_options.use_hash:
-            return 'lily-%s' % self.get_hash ()
-        raise 'to be done'
+        cs = self.get_checksum ()
+        name = '%s/lily-%s' % (cs[:2], cs[2:])
+        return name
+
+    final_basename = basename
 
     def write_ly (self):
-        outf = open (self.basename () + '.ly', 'w')
-        outf.write (self.full_ly ())
-        open (self.basename () + '.txt', 'w').write ('image of music')
+        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)
+        filename = path + '.ly'
+        if os.path.exists (filename):
+            diff_against_existing = filter_pipe (self.full_ly (), 'diff -u %s -' % filename)
+            if diff_against_existing:
+                warning ("%s: duplicate filename but different contents of orginal file,\n\
+printing diff against existing file." % filename)
+                ly.stderr_write (diff_against_existing)
+        else:
+            out = file (filename, 'w')
+            out.write (self.full_ly ())
+            file (path + '.txt', 'w').write ('image of music')
 
     def relevant_contents (self, ly):
         return re.sub (r'\\(version|sourcefileline|sourcefilename)[^\n]*\n', '', ly)
-             
-    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)
-
-        if (os.path.exists (ly_file)
-            and os.path.exists (systems_file)
-            and os.stat (systems_file)[stat.ST_SIZE]
-            and re.match ('% eof', open (systems_file).readlines ()[-1])
-            and (global_options.use_hash or FILENAME in self.option_dict)
-            and (self.relevant_contents (self.full_ly ())
-                 == self.relevant_contents (open (ly_file).read ()))):
-            return None
-
-        return self
-
-    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 not self.ly_is_outdated () and global_options.format in (HTML, TEXINFO):
-            if os.path.exists (eps_file):
-                page_count = ps_page_count (eps_file)
-                if page_count <= 1:
-                    return not os.path.exists (png_file)
+
+    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
+            raise CompileError(self.basename())
+        for name in existing:
+            if (global_options.use_source_file_names
+                and isinstance (self, LilypondFileSnippet)):
+                base, ext = os.path.splitext (name)
+                components = base.split ('-')
+                # ugh, assume filenames with prefix with one dash (lily-xxxx)
+                if len (components) > 2:
+                    base_suffix = '-' + components[-1]
                 else:
-                    return not reduce (operator.or_,
-                                       [find_file (base + '-page%d.png' % a, raise_error=False)
-                                        for a in range (1, page_count + 1)])
-        return True
-    
-    def texstr_is_outdated (self):
-        if backend == 'ps':
-            return 0
+                    base_suffix = ''
+                final_name = self.final_basename () + base_suffix + ext
+            else:
+                final_name = name
+            try:
+                os.unlink (os.path.join (destination, final_name))
+            except OSError:
+                pass
+
+            src = os.path.join (output_dir, name)
+            dst = os.path.join (destination, final_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, 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.
+        """
+        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 + '.texidoc',
+                             base + '.doctitle',
+                             base + '-systems.texi',
+                             base + '-systems.tex',
+                             base + '-systems.pdftexi'])
+        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:
+                require_file (base + '.png')
+            else:
+                for page in range (1, page_count + 1):
+                    require_file (base + '-page%d.png' % page)
 
-        base = self.basename ()
-        return not (self.ly_is_outdated ()
-                    and find_file (base + '.texstr', raise_error=False))
+        system_count = 0
+        if not skip_lily and not missing:
+            system_count = int(file (full + '-systems.count').read())
+
+        for number in range(1, system_count + 1):
+            systemfile = '%s-%d' % (base, number)
+            require_file (systemfile + '.eps')
+            consider_file (systemfile + '.pdf')
+
+            # 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 = run_filter (code)
+        s = filter_pipe (code, global_options.filter_cmd)
         d = {
             'code': s,
             'options': self.match.group ('options')
@@ -1158,32 +1443,33 @@ class Lilypond_snippet (Snippet):
         return output[self.format][FILTER] % d
 
     def replacement_text (self):
-        func = Lilypond_snippet.__dict__['output_' + self.format]
+        func = LilypondSnippet.__dict__['output_' + self.format]
         return func (self)
 
     def get_images (self):
-        base = self.basename ()
-        # URGUGHUGHUGUGH
+        base = self.final_basename ()
+
         single = '%(base)s.png' % vars ()
         multiple = '%(base)s-page1.png' % vars ()
         images = (single,)
-        if os.path.exists (multiple) \
-         and (not os.path.exists (single) \
-            or (os.stat (multiple)[stat.ST_MTIME] \
-              > os.stat (single)[stat.ST_MTIME])):
+        if (os.path.exists (multiple)
+            and (not os.path.exists (single)
+                 or (os.stat (multiple)[stat.ST_MTIME]
+                     > os.stat (single)[stat.ST_MTIME]))):
             count = ps_page_count ('%(base)s.eps' % vars ())
-            images = ['%s-page%d.png' % (base, a) for a in range (1, count+1)]
+            images = ['%s-page%d.png' % (base, page) for page in range (1, count+1)]
             images = tuple (images)
+
         return images
 
     def output_docbook (self):
         str = ''
-        base = self.basename ()
+        base = self.final_basename ()
         for image in self.get_images ():
             (base, ext) = os.path.splitext (image)
             str += output[DOCBOOK][OUTPUT] % vars ()
-           str += self.output_print_filename (DOCBOOK)
-            if (self.substring('inline') == 'inline'): 
+            str += self.output_print_filename (DOCBOOK)
+            if (self.substring('inline') == 'inline'):
                 str = '<inlinemediaobject>' + str + '</inlinemediaobject>'
             else:
                 str = '<mediaobject>' + str + '</mediaobject>'
@@ -1191,11 +1477,11 @@ class Lilypond_snippet (Snippet):
                 verb = verbatim_html (self.verb_ly ())
                 str = output[DOCBOOK][VERBATIM] % vars () + str
         return str
-       
+
     def output_html (self):
         str = ''
-        base = self.basename ()
-        if global_options.format == HTML:
+        base = self.final_basename ()
+        if self.format == HTML:
             str += self.output_print_filename (HTML)
             if VERBATIM in self.option_dict:
                 verb = verbatim_html (self.verb_ly ())
@@ -1223,14 +1509,14 @@ class Lilypond_snippet (Snippet):
             info_image_path = os.path.join (global_options.info_images_dir, base)
             str += output[TEXINFO][OUTPUTIMAGE] % vars ()
 
-        base = self.basename ()
-        str += output[global_options.format][OUTPUT] % vars ()
+        base = self.final_basename ()
+        str += output[self.format][OUTPUT] % vars ()
         return str
 
     def output_latex (self):
         str = ''
-        base = self.basename ()
-        if global_options.format == LATEX:
+        base = self.final_basename ()
+        if self.format == LATEX:
             str += self.output_print_filename (LATEX)
             if VERBATIM in self.option_dict:
                 verb = self.verb_ly ()
@@ -1242,7 +1528,7 @@ class Lilypond_snippet (Snippet):
         if 0:
             breaks = self.ly ().count ("\n")
             str += "".ljust (breaks, "\n").replace ("\n","%\n")
-        
+
         if QUOTE in self.option_dict:
             str = output[LATEX][QUOTE] % vars ()
         return str
@@ -1250,7 +1536,7 @@ class Lilypond_snippet (Snippet):
     def output_print_filename (self, format):
         str = ''
         if PRINTFILENAME in self.option_dict:
-            base = self.basename ()
+            base = self.final_basename ()
             filename = os.path.basename (self.substring ('filename'))
             str = output[format][PRINTFILENAME] % vars ()
 
@@ -1258,10 +1544,20 @@ class Lilypond_snippet (Snippet):
 
     def output_texinfo (self):
         str = self.output_print_filename (TEXINFO)
-        base = self.basename ()
+        base = self.final_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\n' % open (translated_doctitle).read ()
+            elif os.path.exists (doctitle):
+                str += '@lydoctitle %s\n\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 = ''
@@ -1291,15 +1587,19 @@ class Lilypond_snippet (Snippet):
 re_begin_verbatim = re.compile (r'\s+%.*?begin verbatim.*\n*', re.M)
 re_end_verbatim = re.compile (r'\s+%.*?end verbatim.*$', re.M)
 
-class Lilypond_file_snippet (Lilypond_snippet):
+class LilypondFileSnippet (LilypondSnippet):
     def __init__ (self, type, match, format, line_number):
-        Lilypond_snippet.__init__ (self, type, match, format, line_number)
-        self.contents = open (find_file (self.substring ('filename'))).read ()
+        LilypondSnippet.__init__ (self, type, match, format, line_number)
+        self.contents = file (find_file (self.substring ('filename'))).read ()
 
     def verb_ly (self):
         s = self.contents
         s = re_begin_verbatim.split (s)[-1]
         s = re_end_verbatim.split (s)[0]
+        if not NOGETTEXT in self.option_dict:
+            s = verb_ly_gettext (s)
+        if not s.endswith ('\n'):
+            s += '\n'
         return s
 
     def ly (self):
@@ -1307,12 +1607,29 @@ class Lilypond_file_snippet (Lilypond_snippet):
         return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
                 % (name, self.contents))
 
+    def final_basename (self):
+        if global_options.use_source_file_names:
+            base = os.path.splitext (os.path.basename (self.substring ('filename')))[0]
+            return base
+        else:
+            return self.basename ()
+
+
+class LilyPondVersionString (Snippet):
+    """A string that does not require extra memory."""
+    def __init__ (self, type, match, format, line_number):
+        Snippet.__init__ (self, type, match, format, line_number)
+
+    def replacement_text (self):
+        return output[self.format][self.type]
+
 
 snippet_type_to_class = {
-    'lilypond_file': Lilypond_file_snippet,
-    'lilypond_block': Lilypond_snippet,
-    'lilypond': Lilypond_snippet,
-    'include': Include_snippet,
+    'lilypond_file': LilypondFileSnippet,
+    'lilypond_block': LilypondSnippet,
+    'lilypond': LilypondSnippet,
+    'include': IncludeSnippet,
+    'lilypondversion': LilyPondVersionString,
 }
 
 def find_linestarts (s):
@@ -1331,16 +1648,16 @@ def find_linestarts (s):
     nls.append (len (s))
     return nls
 
-def find_toplevel_snippets (s, types):
+def find_toplevel_snippets (input_string, format, types):
     res = {}
-    for i in types:
-        res[i] = ly.re.compile (snippet_res[global_options.format][i])
+    for t in types:
+        res[t] = re.compile (snippet_res[format][t])
 
     snippets = []
     index = 0
     found = dict ([(t, None) for t in types])
 
-    line_starts = find_linestarts (s)
+    line_starts = find_linestarts (input_string)
     line_start_idx = 0
     # We want to search for multiple regexes, without searching
     # the string multiple times for one regex.
@@ -1355,15 +1672,14 @@ def find_toplevel_snippets (s, types):
         for type in types:
             if not found[type] or found[type][0] < index:
                 found[type] = None
-                
-                m = res[type].search (s[index:endex])
+
+                m = res[type].search (input_string[index:endex])
                 if not m:
                     continue
 
-                cl = Snippet
-                if snippet_type_to_class.has_key (type):
-                    cl = snippet_type_to_class[type]
-
+                klass = Snippet
+                if type in snippet_type_to_class:
+                    klass = snippet_type_to_class[type]
 
                 start = index + m.start ('match')
                 line_number = line_start_idx
@@ -1371,13 +1687,13 @@ def find_toplevel_snippets (s, types):
                     line_number += 1
 
                 line_number += 1
-                snip = cl (type, m, global_options.format, line_number)
+                snip = klass (type, m, format, line_number)
 
                 found[type] = (start, snip)
 
-            if found[type] \
-             and (not first \
-                or found[type][0] < found[first][0]):
+            if (found[type]
+                and (not first
+                     or found[type][0] < found[first][0])):
                 first = type
 
                 # FIXME.
@@ -1393,14 +1709,14 @@ def find_toplevel_snippets (s, types):
                 endex = found[first][0]
 
         if not first:
-            snippets.append (Substring (s, 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]):
             line_start_idx += 1
 
         (start, snip) = found[first]
-        snippets.append (Substring (s, index, start, line_start_idx + 1))
+        snippets.append (Substring (input_string, index, start, line_start_idx + 1))
         snippets.append (snip)
         found[first] = None
         index = start + len (snip.match.group ('match'))
@@ -1408,6 +1724,8 @@ def find_toplevel_snippets (s, types):
     return snippets
 
 def filter_pipe (input, cmd):
+    """Pass input through cmd, and return the result."""
+
     if global_options.verbose:
         progress (_ ("Opening filter `%s'") % cmd)
 
@@ -1437,54 +1755,44 @@ def filter_pipe (input, cmd):
 
     return output
 
-def run_filter (s):
-    return filter_pipe (s, global_options.filter_cmd)
+def system_in_directory (cmd, directory):
+    """Execute a command in a different directory.
+
+    Because of win32 compatibility, we can't simply use subprocess.
+    """
+
+    current = os.getcwd()
+    os.chdir (directory)
+    ly.system(cmd, be_verbose=global_options.verbose,
+              progress_p=1)
+    os.chdir (current)
 
-def is_derived_class (cl, baseclass):
-    if cl == baseclass:
-        return 1
-    for b in cl.__bases__:
-        if is_derived_class (b, baseclass):
-            return 1
-    return 0
 
-def process_snippets (cmd, ly_snippets, texstr_snippets, png_snippets):
-    ly_names = filter (lambda x: x,
-                       map (Lilypond_snippet.basename, ly_snippets))
-    texstr_names = filter (lambda x: x,
-                           map (Lilypond_snippet.basename, texstr_snippets))
-    
-    png_names = filter (lambda x: x,
-                        map (Lilypond_snippet.basename, png_snippets))
-
-    status = 0
-    def my_system (cmd):
-        status = ly.system (cmd,
-                            be_verbose=global_options.verbose, 
-                            progress_p=1)
-
-    if global_options.format in (HTML, TEXINFO) and '--formats' not in cmd:
+def process_snippets (cmd, snippets,
+                      format, lily_output_dir):
+    """Run cmd on all of the .ly files from snippets."""
+
+    if not snippets:
+        return
+
+    if format in (HTML, TEXINFO) and '--formats' not in cmd:
         cmd += ' --formats=png '
-    elif global_options.format in (DOCBOOK) and '--formats' not in cmd:
+    elif format in (DOCBOOK) and '--formats' not in cmd:
         cmd += ' --formats=png,pdf '
 
-        
-    # UGH
-    # the --process=CMD switch is a bad idea
-    # it is too generic for lilypond-book.
-    if texstr_names:
-        my_system (' '.join ([cmd, '--backend texstr',
-                              'snippet-map.ly'] + texstr_names))
-        for l in texstr_names:
-            my_system ('latex %s.texstr' % l)
+    checksum = snippet_list_checksum (snippets)
+    contents = '\n'.join (['snippet-map-%d.ly' % checksum]
+                          + list (set ([snip.basename() + '.ly' for snip in snippets])))
+    name = os.path.join (lily_output_dir,
+                         'snippet-names-%d.ly' % checksum)
+    file (name, 'wb').write (contents)
 
-    if ly_names:
-        open ('snippet-names', 'wb').write ('\n'.join (['snippet-map.ly']
-                                                       + ly_names))
-        
-        my_system (' '.join ([cmd, 'snippet-names']))
+    system_in_directory (' '.join ([cmd, ly.mkarg (name)]),
+                         lily_output_dir)
 
 
+###
+# Retrieve dimensions from LaTeX
 LATEX_INSPECTION_DOCUMENT = r'''
 \nonstopmode
 %(preamble)s
@@ -1500,13 +1808,13 @@ def get_latex_textwidth (source):
     m = re.search (r'''(?P<preamble>\\begin\s*{document})''', source)
     if m == None:
         warning (_ ("cannot find \\begin{document} in LaTeX document"))
-        
+
         ## what's a sensible default?
         return 550.0
-    
+
     preamble = source[:m.start (0)]
     latex_document = LATEX_INSPECTION_DOCUMENT % vars ()
-    
+
     (handle, tmpfile) = tempfile.mkstemp('.tex')
     logfile = os.path.splitext (tmpfile)[0] + '.log'
     logfile = os.path.split (logfile)[1]
@@ -1514,25 +1822,26 @@ def get_latex_textwidth (source):
     tmp_handle = os.fdopen (handle,'w')
     tmp_handle.write (latex_document)
     tmp_handle.close ()
-    
-    ly.system ('latex %s' % tmpfile, be_verbose=global_options.verbose)
-    parameter_string = open (logfile).read()
-    
+
+    ly.system ('%s %s' % (global_options.latex_program, tmpfile),
+               be_verbose=global_options.verbose)
+    parameter_string = file (logfile).read()
+
     os.unlink (tmpfile)
     os.unlink (logfile)
 
     columns = 0
-    m = re.search ('columns=([0-9.]*)', parameter_string)
+    m = re.search ('columns=([0-9.]+)', parameter_string)
     if m:
         columns = int (m.group (1))
 
     columnsep = 0
-    m = re.search ('columnsep=([0-9.]*)pt', parameter_string)
+    m = re.search ('columnsep=([0-9.]+)pt', parameter_string)
     if m:
         columnsep = float (m.group (1))
 
     textwidth = 0
-    m = re.search ('textwidth=([0-9.]*)pt', parameter_string)
+    m = re.search ('textwidth=([0-9.]+)pt', parameter_string)
     if m:
         textwidth = float (m.group (1))
         if columns:
@@ -1548,22 +1857,8 @@ def modify_preamble (chunk):
                r"\\usepackage{graphics}" + '\n'
                + r"\\begin{document}",
                str)
-        chunk.override_text = str 
-        
-    
+        chunk.override_text = str
 
-ext2format = {
-    '.html': HTML,
-    '.itely': TEXINFO,
-    '.latex': LATEX,
-    '.lytex': LATEX,
-    '.tely': TEXINFO,
-    '.tex': LATEX,
-    '.texi': TEXINFO,
-    '.texinfo': TEXINFO,
-    '.xml': HTML,
-    '.lyxml': DOCBOOK
-}
 
 format2ext = {
     HTML: '.html',
@@ -1573,70 +1868,99 @@ format2ext = {
     DOCBOOK: '.xml'
 }
 
-class Compile_error:
+class CompileError(Exception):
     pass
 
+def snippet_list_checksum (snippets):
+    return hash (' '.join([l.basename() for l in snippets]))
+
 def write_file_map (lys, name):
-    snippet_map = open ('snippet-map.ly', 'w')
+    snippet_map = file (os.path.join (
+        global_options.lily_output_dir,
+        'snippet-map-%d.ly' % snippet_list_checksum (lys)), 'w')
+
     snippet_map.write ("""
 #(define version-seen #t)
 #(define output-empty-score-list #f)
-#(ly:add-file-name-alist '(
-""")
-    for ly in lys:
-        snippet_map.write ('("%s.ly" . "%s")\n'
-                 % (ly.basename (),
-                   name))
-
-    snippet_map.write ('))\n')
-
-def do_process_cmd (chunks, input_name):
-    all_lys = filter (lambda x: is_derived_class (x.__class__,
-                           Lilypond_snippet),
-                      chunks)
-
-    write_file_map (all_lys, input_name)
-    ly_outdated = filter (lambda x: is_derived_class (x.__class__,
-                                                      Lilypond_snippet)
-                          and x.ly_is_outdated (), chunks)
-    texstr_outdated = filter (lambda x: is_derived_class (x.__class__,
-                                                          Lilypond_snippet)
-                              and x.texstr_is_outdated (),
-                              chunks)
-    png_outdated = filter (lambda x: is_derived_class (x.__class__,
-                                                        Lilypond_snippet)
-                           and x.png_is_outdated (),
-                           chunks)
-
-    outdated = png_outdated + texstr_outdated + ly_outdated
-    
+#(ly:add-file-name-alist '(%s
+    ))\n
+""" % '\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)]
+
+    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..."))
-    map (Lilypond_snippet.write_ly, ly_outdated)
+    for snippet in outdated:
+        snippet.write_ly()
     progress ('\n')
 
     if outdated:
         progress (_ ("Processing..."))
         progress ('\n')
-        process_snippets (global_options.process_cmd, ly_outdated, texstr_outdated, png_outdated)
+        process_snippets (options.process_cmd, outdated,
+                          options.format, options.lily_output_dir)
+
     else:
         progress (_ ("All snippets are up to date..."))
+
+    if options.lily_output_dir != options.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,
+                                           options.output_dir)
+
     progress ('\n')
 
+
+###
+# Format guessing data
+ext2format = {
+    '.html': HTML,
+    '.itely': TEXINFO,
+    '.latex': LATEX,
+    '.lytex': LATEX,
+    '.tely': TEXINFO,
+    '.tex': LATEX,
+    '.texi': TEXINFO,
+    '.texinfo': TEXINFO,
+    '.xml': HTML,
+    '.lyxml': DOCBOOK
+}
+
 def guess_format (input_filename):
     format = None
     e = os.path.splitext (input_filename)[1]
-    if e in ext2format.keys ():
+    if e in ext2format:
         # FIXME
         format = ext2format[e]
     else:
-        error (_ ("cannot determine format for: %s" \
-              % input_filename))
+        error (_ ("cannot determine format for: %s"
+                  % input_filename))
         exit (1)
     return format
 
 def write_if_updated (file_name, lines):
     try:
-        f = open (file_name)
+        f = file (file_name)
         oldstr = f.read ()
         new_str = ''.join (lines)
         if oldstr == new_str:
@@ -1646,13 +1970,19 @@ 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)
-    open (file_name, 'w').writelines (lines)
+    file (file_name, 'w').writelines (lines)
     progress ('\n')
 
+
 def note_input_file (name, inputs=[]):
     ## hack: inputs is mutable!
     inputs.append (name)
@@ -1666,7 +1996,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
@@ -1680,39 +2010,41 @@ def do_file (input_filename):
             input_fullname = find_file (input_filename)
 
         note_input_file (input_fullname)
-        in_handle = open (input_fullname)
+        in_handle = file (input_fullname)
 
     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])
+        input_base = os.path.basename (
+            os.path.splitext (input_filename)[0])
 
-    # Only default to stdout when filtering.
-    if global_options.output_name == '-' or (not global_options.output_name and global_options.filter_cmd):
-        output_filename = '-'
-        output_file = sys.stdout
+    # don't complain when global_options.output_dir is existing
+    if not global_options.output_dir:
+        global_options.output_dir = os.getcwd()
     else:
-        # don't complain when global_options.output_name is existing
-        output_filename = input_base + format2ext[global_options.format]
-        if global_options.output_name:
-            if not os.path.isdir (global_options.output_name):
-                os.mkdir (global_options.output_name, 0777)
-            os.chdir (global_options.output_name)
-        else: 
-            if (os.path.exists (input_filename) 
-                and os.path.exists (output_filename) 
-                and samefile (output_filename, input_fullname)):
-             error (
-             _ ("Output would overwrite input file; use --output."))
-             exit (2)
+        global_options.output_dir = os.path.abspath(global_options.output_dir)
+
+        if not os.path.isdir (global_options.output_dir):
+            os.mkdir (global_options.output_dir, 0777)
+        os.chdir (global_options.output_dir)
+
+    output_filename = os.path.join(global_options.output_dir,
+                                   input_base + format2ext[global_options.format])
+    if (os.path.exists (input_filename)
+        and os.path.exists (output_filename)
+        and samefile (output_filename, input_fullname)):
+     error (
+     _ ("Output would overwrite input file; use --output."))
+     exit (2)
 
     try:
         progress (_ ("Reading %s...") % input_fullname)
         source = in_handle.read ()
         progress ('\n')
 
-        set_default_options (source)
+        set_default_options (source, default_ly_options, global_options.format)
 
 
         # FIXME: Containing blocks must be first, see
@@ -1726,9 +2058,10 @@ def do_file (input_filename):
             'lilypond_file',
             'include',
             'lilypond',
+            'lilypondversion',
         )
         progress (_ ("Dissecting..."))
-        chunks = find_toplevel_snippets (source, snippet_types)
+        chunks = find_toplevel_snippets (source, global_options.format, snippet_types)
 
         if global_options.format == LATEX:
             for c in chunks:
@@ -1742,81 +2075,58 @@ def do_file (input_filename):
             write_if_updated (output_filename,
                      [c.filter_text () for c in chunks])
         elif global_options.process_cmd:
-            do_process_cmd (chunks, input_fullname)
+            do_process_cmd (chunks, input_fullname, global_options)
             progress (_ ("Compiling %s...") % output_filename)
             progress ('\n')
             write_if_updated (output_filename,
                      [s.replacement_text ()
                      for s in chunks])
-        
+
         def process_include (snippet):
             os.chdir (original_dir)
             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: is_derived_class (x.__class__,
-                                     Include_snippet),
-                       chunks))
+                              filter (lambda x: isinstance (x, IncludeSnippet),
+                                      chunks))
 
+        return chunks + reduce (lambda x, y: x + y, include_chunks, [])
 
-        return chunks + reduce (lambda x,y: x + y, include_chunks, [])
-        
-    except Compile_error:
+    except CompileError:
         os.chdir (original_dir)
         progress (_ ("Removing `%s'") % output_filename)
         progress ('\n')
-        raise Compile_error
+        raise CompileError
 
 def do_options ():
-
     global global_options
 
     opt_parser = get_option_parser()
     (global_options, args) = opt_parser.parse_args ()
-
     if global_options.format in ('texi-html', 'texi'):
         global_options.format = TEXINFO
-    global_options.use_hash = True
 
     global_options.include_path =  map (os.path.abspath, global_options.include_path)
-    
+
     if global_options.warranty:
         warranty ()
         exit (0)
     if not args or len (args) > 1:
         opt_parser.print_help ()
         exit (2)
-        
-    return args
-
-def psfonts_warning (options, basename):
-    if options.format in (TEXINFO, LATEX):
-        psfonts_file = os.path.join (options.output_name, basename + '.psfonts')
-        output = os.path.join (options.output_name, basename +  '.dvi' )
 
-        if not options.create_pdf:
-            if not options.psfonts:
-                warning (_ ("option --psfonts not used"))
-                warning (_ ("processing with dvips will have no fonts"))
-            else:
-                progress ('\n')
-                progress (_ ("DVIPS usage:"))
-                progress ('\n')
-                progress ("    dvips -h %(psfonts_file)s %(output)s" % vars ())
-                progress ('\n')
+    return args
 
 def main ():
     # FIXME: 85 lines of `main' macramee??
     files = do_options ()
 
-    file = files[0]
-
-    basename = os.path.splitext (file)[0]
+    basename = os.path.splitext (files[0])[0]
     basename = os.path.split (basename)[1]
-    
+
     if not global_options.format:
         global_options.format = guess_format (files[0])
 
@@ -1824,64 +2134,61 @@ def main ():
     if global_options.format in (TEXINFO, HTML, DOCBOOK):
         formats += ',png'
 
-        
     if global_options.process_cmd == '':
-        global_options.process_cmd = (lilypond_binary 
-                                      + ' --formats=%s --backend eps ' % formats)
+        global_options.process_cmd = (lilypond_binary
+                                      + ' --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.
         global_options.process_cmd += ' --formats=eps '
         if global_options.create_pdf:
             global_options.process_cmd += "--pdf -dinclude-eps-fonts -dgs-load-fonts "
-    
+            if global_options.latex_program == 'latex':
+                global_options.latex_program = 'pdflatex'
+
     if global_options.verbose:
         global_options.process_cmd += " --verbose "
 
     if global_options.padding_mm:
         global_options.process_cmd += " -deps-box-padding=%f " % global_options.padding_mm
-        
-    global_options.process_cmd += " -dread-file-list "
 
-    identify ()
+    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)
+        if not os.path.isdir (global_options.lily_output_dir):
+            os.makedirs (global_options.lily_output_dir)
+    else:
+        global_options.lily_output_dir = os.path.abspath(global_options.output_dir)
+
+
+    identify ()
     try:
-        chunks = do_file (file)
-        if global_options.psfonts:
-            fontextract.verbose = global_options.verbose
-            snippet_chunks = filter (lambda x: is_derived_class (x.__class__,
-                                       Lilypond_snippet),
-                        chunks)
-
-            psfonts_file = basename + '.psfonts' 
-            if not global_options.verbose:
-                progress (_ ("Writing fonts to %s...") % psfonts_file)
-            fontextract.extract_fonts (psfonts_file,
-                         [x.basename() + '.eps'
-                          for x in snippet_chunks])
-            if not global_options.verbose:
-                progress ('\n')
-            
-    except Compile_error:
+        chunks = do_file (files[0])
+    except CompileError:
         exit (1)
 
-    psfonts_warning (global_options, basename)
-
     inputs = note_input_file ('')
     inputs.pop ()
 
-    base_file_name = os.path.splitext (os.path.basename (file))[0]
-    dep_file = os.path.join (global_options.output_name, base_file_name + '.dep')
-    final_output_file = os.path.join (global_options.output_name,
+    base_file_name = os.path.splitext (os.path.basename (files[0]))[0]
+    dep_file = os.path.join (global_options.output_dir, base_file_name + '.dep')
+    final_output_file = os.path.join (global_options.output_dir,
                      base_file_name
                      + '.%s' % global_options.format)
-    
+
     os.chdir (original_dir)
-    open (dep_file, 'w').write ('%s: %s' % (final_output_file, ' '.join (inputs)))
+    file (dep_file, 'w').write ('%s: %s'
+                                % (final_output_file, ' '.join (inputs)))
 
 if __name__ == '__main__':
     main ()