X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=scripts%2Flilypond-book.py;h=73b1cd9464006a9628bd5e288c0b4b03d2b29909;hb=c054eb280fd9953596eb164f67b0f9d5555c5a32;hp=dee8223de6e66d51cdd85f2b6268f963cc2ddba4;hpb=4ff305dfb178650c7b7b7424d614600a2b46fa1c;p=lilypond.git diff --git a/scripts/lilypond-book.py b/scripts/lilypond-book.py index dee8223de6..73b1cd9464 100644 --- a/scripts/lilypond-book.py +++ b/scripts/lilypond-book.py @@ -1,4 +1,20 @@ #!@TARGET_PYTHON@ +# -*- coding: utf-8 -*- + +# 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 . ''' Example usage: @@ -14,8 +30,6 @@ 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 +42,18 @@ TODO: ''' -import stat -import tempfile -import commands + +# TODO: Better solve the global_options copying to the snippets... + +import glob import os -import sys import re -import md5 -import operator +import stat +import sys +import tempfile +import imp +from optparse import OptionGroup + """ @relocate-preamble@ @@ -43,12 +61,17 @@ import operator import lilylib as ly import fontextract +import langdefs global _;_=ly._ +import book_base as BookBase +import book_snippets as BookSnippet +import book_html +import book_docbook +import book_texinfo +import book_latex -# Lilylib globals. -program_version = '@TOPLEVEL_VERSION@' -program_name = os.path.basename (sys.argv[0]) +ly.require_python_version () original_dir = os.getcwd () backend = 'ps' @@ -58,66 +81,58 @@ _ ("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 ', - 'Han-Wen Nienhuys ') + 'Han-Wen Nienhuys ') ################################################################ def exit (i): - if global_options.verbose: + if ly.is_verbose (): raise Exception (_ ('Exiting (%d)...') % i) else: sys.exit (i) -def identify (): - ly.encoded_write (sys.stdout, '%s (GNU LilyPond) %s\n' % (program_name, program_version)) - progress = ly.progress +warning = ly.warning +error = ly.error -def warning (s): - ly.stderr_write (program_name + ": " + _ ("warning: %s") % s + '\n') - -def error (s): - ly.stderr_write (program_name + ": " + _ ("error: %s") % s + '\n') - -def ps_page_count (ps_name): - header = open (ps_name).read (1024) - m = re.search ('\n%%Pages: ([0-9]+)', header) - if m: - return int (m.group (1)) - return 0 +def identify (): + progress('%s (GNU LilyPond) %s' % (ly.program_name, ly.program_version)) def warranty (): identify () 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--2015', + '\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', description=help_summary, + conflict_handler="resolve", add_help_option=False) 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", @@ -127,7 +142,7 @@ def get_option_parser (): p.add_option ("-I", '--include', help=_ ("add DIR to include path"), metavar=_ ("DIR"), action='append', dest='include_path', - default=[os.path.abspath (os.getcwd ())]) + default=[]) p.add_option ('--info-images-dir', help=_ ("format Texinfo output so that Info will " @@ -136,38 +151,79 @@ def get_option_parser (): action='store', dest='info_images_dir', default='') - p.add_option ('--left-padding', + 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-loglevel', + help=_ ("Print lilypond log messages according to LOGLEVEL"), + metavar=_ ("LOGLEVEL"), + action='store', dest='lily_loglevel', + default=os.environ.get ("LILYPOND_LOGLEVEL", None)) + + 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 ('--load-custom-package', help=_ ("Load the additional python PACKAGE (containing e.g. a custom output format)"), + metavar=_ ("PACKAGE"), + action='append', dest='custom_packages', + default=[]) + + p.add_option ("-l", "--loglevel", + help=_ ("Print log messages according to LOGLEVEL " + "(NONE, ERROR, WARNING, PROGRESS (default), DEBUG)"), + metavar=_ ("LOGLEVEL"), + action='callback', + callback=ly.handle_loglevel_option, + type='string') + 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') + action='store', + dest='process_cmd', default='') - p.add_option ('--pdf', + p.add_option ('--redirect-lilypond-output', + help = _ ("Redirect the lilypond output"), + action='store_true', + dest='redirect_output', default=False) + + p.add_option ('-s', '--safe', help=_ ("Compile snippets in safe mode"), action="store_true", - dest="create_pdf", - help=_ ("create PDF files for use with PDFTeX"), + default=False, + dest="safe_mode") + + 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 ('', '--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 ('--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", - default=False, - dest="verbose") + action="callback", + callback=ly.handle_loglevel_option, + callback_args=("DEBUG",)) p.version = "@TOPLEVEL_VERSION@" p.add_option("--version", @@ -177,1142 +233,59 @@ 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''')) + + group = OptionGroup (p, "Options only for the latex and texinfo backends") + group.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') + group.add_option ('--texinfo-program', + help=_ ("run executable PROG instead of texi2pdf"), + metavar=_ ("PROG"), + action='store', dest='texinfo_program', + default='texi2pdf') + group.add_option ('--pdf', + action="store_true", + dest="create_pdf", + help=_ ("create PDF files for use with PDFTeX"), + default=False) + p.add_option_group (group) + + p.add_option_group ('', + description=( + _ ("Report bugs via %s") + % ' http://post.gmane.org/post.php' + '?group=gmane.comp.gnu.lilypond.bugs') + '\n') + + + for formatter in BookBase.all_formats: + formatter.add_options (p) + 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' -global_options = None - - -default_ly_options = { 'alt': "[image of music]" } - -# -# Is this pythonic? Personally, I find this rather #define-nesque. --hwn -# -AFTER = 'after' -BEFORE = 'before' -DOCBOOK = 'docbook' -EXAMPLEINDENT = 'exampleindent' -FILTER = 'filter' -FRAGMENT = 'fragment' -HTML = 'html' -INDENT = 'indent' -LATEX = 'latex' -LAYOUT = 'layout' -LINE_WIDTH = 'line-width' -LILYQUOTE = 'lilyquote' -NOFRAGMENT = 'nofragment' -NOINDENT = 'noindent' -NOQUOTE = 'noquote' -NOTES = 'body' -NOTIME = 'notime' -OUTPUT = 'output' -OUTPUTIMAGE = 'outputimage' -PACKED = 'packed' -PAPER = 'paper' -PREAMBLE = 'preamble' -PRINTFILENAME = 'printfilename' -QUOTE = 'quote' -RAGGED_RIGHT = 'ragged-right' -RELATIVE = 'relative' -STAFFSIZE = 'staffsize' -TEXIDOC = 'texidoc' -TEXINFO = 'texinfo' -VERBATIM = 'verbatim' -FONTLOAD = 'fontload' -FILENAME = 'filename' -ALT = 'alt' - - -# NOTIME has no opposite so it isn't part of this dictionary. -# NOQUOTE is used internally only. -no_options = { - NOFRAGMENT: FRAGMENT, - NOINDENT: INDENT, -} - - -# Recognize special sequences in the input. -# -# (?Pregex) -- Assign result of REGEX to NAME. -# *? -- Match non-greedily. -# (?m) -- Multiline regex: Make ^ and $ match at each line. -# (?s) -- Make the dot match all characters including newline. -# (?x) -- Ignore whitespace in patterns. -no_match = 'a\ba' -snippet_res = { - ## - DOCBOOK: { - 'include': - no_match, - - 'lilypond': - r'''(?smx) - (?P - <(?P(inline)?)mediaobject>\s*\s*.*?)")?>(?P.*?)\s*\s*)''', - - 'lilypond_block': - r'''(?smx) - (?P - <(?P(inline)?)mediaobject>\s*\s*.*?)")?>(?P.*?)\s*\s*)''', - - 'lilypond_file': - r'''(?smx) - (?P - <(?P(inline)?)mediaobject>\s*\s*.*?\.ly)"\s*(role="(?P.*?)")?\s*(/>|>\s*)\s*\s*)''', - - 'multiline_comment': - r'''(?smx) - (?P - \s*(?!@c\s+) - (?P) - \s)''', - - 'singleline_comment': - no_match, - - 'verb': - no_match, - - 'verbatim': - no_match, - - }, - ## - HTML: { - 'include': - no_match, - - 'lilypond': - r'''(?mx) - (?P - .*?)\s*:)?\s* - (?P.*?) - />)''', - - 'lilypond_block': - r'''(?msx) - (?P - .*?)\s* - > - (?P.*?) - )''', - - 'lilypond_file': - r'''(?mx) - (?P - .*?)\s* - > - \s*(?P.*?)\s* - )''', - - 'multiline_comment': - r'''(?smx) - (?P - \s*(?!@c\s+) - (?P) - \s)''', - - 'singleline_comment': - no_match, - - 'verb': - r'''(?x) - (?P - (?P
.*?
))''', - - 'verbatim': - r'''(?x) - (?s) - (?P - (?P
\s.*?
\s))''', - }, - - ## - LATEX: { - 'include': - r'''(?smx) - ^[^%\n]*? - (?P - \\input\s*{ - (?P\S+?) - })''', - - 'lilypond': - r'''(?smx) - ^[^%\n]*? - (?P - \\lilypond\s*( - \[ - \s*(?P.*?)\s* - \])?\s*{ - (?P.*?) - })''', - - 'lilypond_block': - r'''(?smx) - ^[^%\n]*? - (?P - \\begin\s*( - \[ - \s*(?P.*?)\s* - \])?\s*{lilypond} - (?P.*?) - ^[^%\n]*? - \\end\s*{lilypond})''', - - 'lilypond_file': - r'''(?smx) - ^[^%\n]*? - (?P - \\lilypondfile\s*( - \[ - \s*(?P.*?)\s* - \])?\s*\{ - (?P\S+?) - })''', - - 'multiline_comment': - no_match, - - 'singleline_comment': - r'''(?mx) - ^.*? - (?P - (?P - %.*$\n+))''', - - 'verb': - r'''(?mx) - ^[^%\n]*? - (?P - (?P - \\verb(?P.) - .*? - (?P=del)))''', - - 'verbatim': - r'''(?msx) - ^[^%\n]*? - (?P - (?P - \\begin\s*{verbatim} - .*? - \\end\s*{verbatim}))''', - }, - - ## - TEXINFO: { - 'include': - r'''(?mx) - ^(?P - @include\s+ - (?P\S+))''', - - 'lilypond': - r'''(?smx) - ^[^\n]*?(?!@c\s+)[^\n]*? - (?P - @lilypond\s*( - \[ - \s*(?P.*?)\s* - \])?\s*{ - (?P.*?) - })''', - - 'lilypond_block': - r'''(?msx) - ^(?P - @lilypond\s*( - \[ - \s*(?P.*?)\s* - \])?\s+? - ^(?P.*?) - ^@end\s+lilypond)\s''', - - 'lilypond_file': - r'''(?mx) - ^(?P - @lilypondfile\s*( - \[ - \s*(?P.*?)\s* - \])?\s*{ - (?P\S+) - })''', - - 'multiline_comment': - r'''(?smx) - ^(?P - (?P - @ignore\s - .*? - @end\s+ignore))\s''', - - 'singleline_comment': - r'''(?mx) - ^.* - (?P - (?P - @c([ \t][^\n]*|)\n))''', - - # Don't do this: It interferes with @code{@{}. - # 'verb': r'''(?P@code{.*?})''', - - 'verbatim': - r'''(?sx) - (?P - (?P - @example - \s.*? - @end\s+example\s))''', - }, -} - - - - -format_res = { - DOCBOOK: { - 'intertext': r',?\s*intertext=\".*?\"', - 'option_sep': '\s*', - }, - HTML: { - 'intertext': r',?\s*intertext=\".*?\"', - 'option_sep': '\s*', - }, - - LATEX: { - 'intertext': r',?\s*intertext=\".*?\"', - 'option_sep': '\s*,\s*', - }, - - TEXINFO: { - 'intertext': r',?\s*intertext=\".*?\"', - 'option_sep': '\s*,\s*', - }, -} - -# Options without a pattern in ly_options. -simple_options = [ - EXAMPLEINDENT, - FRAGMENT, - NOFRAGMENT, - NOINDENT, - PRINTFILENAME, - TEXIDOC, - VERBATIM, - FONTLOAD, - FILENAME, - ALT -] - -ly_options = { - ## - NOTES: { - RELATIVE: r'''\relative c%(relative_quotes)s''', - }, - - ## - PAPER: { - INDENT: r'''indent = %(indent)s''', - - LINE_WIDTH: r'''line-width = %(line-width)s''', - - QUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''', - - LILYQUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s''', - - RAGGED_RIGHT: r'''ragged-right = ##t''', - - PACKED: r'''packed = ##t''', - }, - - ## - LAYOUT: { - NOTIME: r''' - \context { - \Score - timing = ##f - } - \context { - \Staff - \remove Time_signature_engraver - }''', - }, - - ## - PREAMBLE: { - STAFFSIZE: r'''#(set-global-staff-size %(staffsize)s)''', - }, -} - -output = { - ## - DOCBOOK: { - FILTER: r'''%(code)s''', - - OUTPUT: r''' - - - - - ''', - - VERBATIM: r'''%(verb)s''', - - PRINTFILENAME: '%(filename)s' - }, - ## - HTML: { - FILTER: r''' -%(code)s - -''', - - AFTER: r''' - -

''', - - BEFORE: r'''

- ''', - - OUTPUT: r''' - %(alt)s''', - - PRINTFILENAME: '

%(filename)s

', - - QUOTE: r'''
-%(str)s -
-''', - - VERBATIM: r'''
-%(verb)s
''', - }, - - ## - LATEX: { - OUTPUT: r'''{%% -\parindent 0pt%% -\ifx\preLilyPondExample \undefined%% - \relax%% -\else%% - \preLilyPondExample%% -\fi%% -\def\lilypondbook{}%% -\input %(base)s-systems.tex%% -\ifx\postLilyPondExample \undefined%% - \relax%% -\else%% - \postLilyPondExample%% -\fi%% -}''', - - PRINTFILENAME: '''\\texttt{%(filename)s} - ''', - - QUOTE: r'''\begin{quotation}%(str)s -\end{quotation}''', - - VERBATIM: r'''\noindent -\begin{verbatim}%(verb)s\end{verbatim}''', - - FILTER: r'''\begin{lilypond}[%(options)s] -%(code)s -\end{lilypond}''', - }, - - ## - TEXINFO: { - FILTER: r'''@lilypond[%(options)s] -%(code)s -@lilypond''', - - OUTPUT: r''' -@iftex -@include %(base)s-systems.texi -@end iftex -''', - - OUTPUTIMAGE: r'''@noindent -@ifinfo -@image{%(info_image_path)s,,,%(alt)s,%(ext)s} -@end ifinfo -@html -

- - %(alt)s - -

-@end html -''', - - PRINTFILENAME: ''' -@html - -@end html -@file{%(filename)s} -@html - -@end html - ''', - - QUOTE: r'''@quotation -%(str)s@end quotation -''', - - NOQUOTE: r'''@format -%(str)s@end format -''', - - VERBATIM: r'''@exampleindent 0 -@verbatim -%(verb)s@end verbatim -''', - }, -} - -# -# Maintain line numbers. -# - -## TODO -if 0: - for f in [HTML, LATEX]: - for s in (QUOTE, VERBATIM): - output[f][s] = output[f][s].replace("\n"," ") - - -PREAMBLE_LY = '''%%%% Generated by %(program_name)s -%%%% Options: [%(option_string)s] -\\include "lilypond-book-preamble.ly" - - -%% **************************************************************** -%% 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)) -} - -\layout { - %(layout_string)s -} -''' - -FRAGMENT_LY = r''' -%(notes_string)s -{ - - -%% **************************************************************** -%% ly snippet contents follows: -%% **************************************************************** -%(code)s - +# Need to shell-quote, issue 3468 -%% **************************************************************** -%% end ly snippet -%% **************************************************************** -} -''' - -FULL_LY = ''' +import pipes +lilypond_binary = pipes.quote (lilypond_binary) - -%% **************************************************************** -%% ly snippet: -%% **************************************************************** -%(code)s +global_options = None -%% **************************************************************** -%% end ly snippet -%% **************************************************************** -''' -texinfo_line_widths = { - '@afourpaper': '160\\mm', - '@afourwide': '6.5\\in', - '@afourlatex': '150\\mm', - '@smallbook': '5\\in', - '@letterpaper': '6\\in', -} - -def classic_lilypond_book_compatibility (key, value): - if key == 'singleline' and value == None: - return (RAGGED_RIGHT, None) - - m = re.search ('relative\s*([-0-9])', key) - if m: - return ('relative', m.group (1)) - - m = re.match ('([0-9]+)pt', key) - if m: - return ('staffsize', m.group (1)) - - if key == 'indent' or key == 'line-width': - m = re.match ('([-.0-9]+)(cm|in|mm|pt|staffspace)', value) - if m: - f = float (m.group (1)) - return (key, '%f\\%s' % (f, m.group (2))) - - return (None, None) - -def find_file (name, raise_error=True): - for i in global_options.include_path: - 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) - return '' - -def verbatim_html (s): - return re.sub ('>', '>', - re.sub ('<', '<', - re.sub ('&', '&', 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["\'])(.*)(?P=q)','\g<1>\g<3>',options[i]) - return options - else: - return re.split (format_res[global_options.format]['option_sep'], - option_string) - return [] - -def set_default_options (source): - global default_ly_options - if not default_ly_options.has_key (LINE_WIDTH): - if global_options.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 (): - # FIXME: @layout is usually not in - # chunk #0: - # - # \input texinfo @c -*-texinfo-*- - # - # Bluntly search first K items of - # source. - # s = chunks[0].replacement_text () - if re.search (k, source[:1024]): - default_ly_options[LINE_WIDTH] = v - break - -class Chunk: - def replacement_text (self): - return '' - - 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): - 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 - - def replacement_text (self): - if self.override_text: - return self.override_text - else: - return self.source[self.start:self.end] - -class Snippet (Chunk): - def __init__ (self, type, match, format, line_number): - self.type = type - self.match = match - self.hash = 0 - self.option_dict = {} - self.format = format - self.line_number = line_number - - def replacement_text (self): - return self.match.group ('match') - - def substring (self, s): - return self.match.group (s) - - def __repr__ (self): - return `self.__class__` + ' type = ' + self.type - -class Include_snippet (Snippet): - def processed_filename (self): - f = self.substring ('filename') - return os.path.splitext (f)[0] + format2ext[global_options.format] - - def replacement_text (self): - s = self.match.group ('match') - f = self.substring ('filename') - - return re.sub (f, self.processed_filename (), s) - -class Lilypond_snippet (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') - - def ly (self): - contents = self.substring ('code') - return ('\\sourcefileline %d\n%s' - % (self.line_number - 1, contents)) - - def full_ly (self): - s = self.ly () - if s: - return self.compose_ly (s) - return '' - - def do_options (self, option_string, type): - self.option_dict = {} - - options = split_options (option_string) - - for i in options: - if '=' in i: - (key, value) = re.split ('\s*=\s*', i) - 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]] - else: - self.option_dict[i] = None - - has_line_width = self.option_dict.has_key (LINE_WIDTH) - no_line_width_value = 0 - - # If LINE_WIDTH is used without parameter, set it to default. - if has_line_width and self.option_dict[LINE_WIDTH] == None: - 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] - - if not has_line_width: - if type == 'lilypond' or FRAGMENT in self.option_dict.keys (): - self.option_dict[RAGGED_RIGHT] = None - - if type == 'lilypond': - if LINE_WIDTH in self.option_dict.keys (): - del self.option_dict[LINE_WIDTH] - else: - if RAGGED_RIGHT in self.option_dict.keys (): - if LINE_WIDTH in self.option_dict.keys (): - del self.option_dict[LINE_WIDTH] - - if QUOTE in self.option_dict.keys () or type == 'lilypond': - if LINE_WIDTH in self.option_dict.keys (): - del self.option_dict[LINE_WIDTH] - - if not INDENT in self.option_dict.keys (): - 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] - - def compose_ly (self, code): - if FRAGMENT in self.option_dict.keys (): - body = FRAGMENT_LY - else: - body = FULL_LY - - # Defaults. - relative = 1 - override = {} - # The original concept of the `exampleindent' option is broken. - # It is not possible to get a sane value for @exampleindent at all - # without processing the document itself. Saying - # - # @exampleindent 0 - # @example - # ... - # @end example - # @exampleindent 5 - # - # causes ugly results with the DVI backend of texinfo since the - # default value for @exampleindent isn't 5em but 0.4in (or a smaller - # value). Executing the above code changes the environment - # indentation to an unknown value because we don't know the amount - # of 1em in advance since it is font-dependent. Modifying - # @exampleindent in the middle of a document is simply not - # supported within texinfo. - # - # As a consequence, the only function of @exampleindent is now to - # specify the amount of indentation for the `quote' option. - # - # To set @exampleindent locally to zero, we use the @format - # environment for non-quoted snippets. - override[EXAMPLEINDENT] = r'0.4\in' - override[LINE_WIDTH] = texinfo_line_widths['@smallbook'] - 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) - 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) - if c_key: - if c_value: - warning \ - (_ ("deprecated ly-option used: %s=%s" \ - % (key, value))) - warning \ - (_ ("compatibility mode translation: %s=%s" \ - % (c_key, c_value))) - else: - warning \ - (_ ("deprecated ly-option used: %s" \ - % key)) - warning \ - (_ ("compatibility mode translation: %s" \ - % c_key)) - - (key, value) = (c_key, c_value) - - if value: - override[key] = value - else: - if not override.has_key (key): - override[key] = None - - found = 0 - for type in compose_types: - if ly_options[type].has_key (key): - compose_dict[type].append (ly_options[type][key]) - found = 1 - break - - if not found and key not in simple_options: - warning (_ ("ignoring unknown ly option: %s") % key) - - # URGS - if RELATIVE in override.keys () and override[RELATIVE]: - relative = int (override[RELATIVE]) - - relative_quotes = '' - - # 1 = central C - if relative < 0: - relative_quotes += ',' * (- relative) - elif relative > 0: - relative_quotes += "'" * relative - - paper_string = '\n '.join (compose_dict[PAPER]) % override - layout_string = '\n '.join (compose_dict[LAYOUT]) % override - 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 ())) - - ## let's not create too long names. - self.hash = hash.hexdigest ()[:10] - - return self.hash - - 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' - - def write_ly (self): - outf = open (self.basename () + '.ly', 'w') - outf.write (self.full_ly ()) - open (self.basename () + '.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) - 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 = self.basename () - return not (self.ly_is_outdated () - and find_file (base + '.texstr', raise_error=False)) - - def filter_text (self): - code = self.substring ('code') - s = run_filter (code) - d = { - 'code': s, - 'options': self.match.group ('options') - } - # TODO - return output[self.format][FILTER] % d - - def replacement_text (self): - func = Lilypond_snippet.__dict__['output_' + self.format] - return func (self) - - def get_images (self): - base = self.basename () - # URGUGHUGHUGUGH - 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])): - count = ps_page_count ('%(base)s.eps' % vars ()) - images = ['%s-page%d.png' % (base, a) for a in range (1, count+1)] - images = tuple (images) - return images - - def output_docbook (self): - str = '' - base = self.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 = '' + str + '' - else: - str = '' + str + '' - if VERBATIM in self.option_dict: - 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: - str += self.output_print_filename (HTML) - if VERBATIM in self.option_dict: - verb = verbatim_html (self.verb_ly ()) - str += output[HTML][VERBATIM] % vars () - if QUOTE in self.option_dict: - str = output[HTML][QUOTE] % vars () - - str += output[HTML][BEFORE] % vars () - for image in self.get_images (): - (base, ext) = os.path.splitext (image) - alt = self.option_dict[ALT] - str += output[HTML][OUTPUT] % vars () - str += output[HTML][AFTER] % vars () - return str - - def output_info (self): - str = '' - for image in self.get_images (): - (base, ext) = os.path.splitext (image) - - # URG, makeinfo implicitly prepends dot to extension. - # Specifying no extension is most robust. - ext = '' - alt = self.option_dict[ALT] - 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 () - return str - - def output_latex (self): - str = '' - base = self.basename () - if global_options.format == LATEX: - str += self.output_print_filename (LATEX) - if VERBATIM in self.option_dict: - verb = self.verb_ly () - str += (output[LATEX][VERBATIM] % vars ()) - - str += (output[LATEX][OUTPUT] % vars ()) - - ## todo: maintain breaks - 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 - - def output_print_filename (self, format): - str = '' - if PRINTFILENAME in self.option_dict: - base = self.basename () - filename = self.substring ('filename') - str = output[global_options.format][PRINTFILENAME] % vars () - - return str - - def output_texinfo (self): - str = '' - if self.output_print_filename (TEXINFO): - str += ('@html\n' - + self.output_print_filename (HTML) - + '\n@end html\n') - str += ('@tex\n' - + self.output_print_filename (LATEX) - + '\n@end tex\n') - base = self.basename () - if TEXIDOC in self.option_dict: - texidoc = base + '.texidoc' - if os.path.exists (texidoc): - str += '@include %(texidoc)s\n\n' % vars () - - substr = '' - if VERBATIM in self.option_dict: - verb = self.verb_ly () - substr += output[TEXINFO][VERBATIM] % vars () - if not QUOTE in self.option_dict: - substr = output[TEXINFO][NOQUOTE] % {'str':substr} - substr += self.output_info () - if LILYQUOTE in self.option_dict: - substr = output[TEXINFO][QUOTE] % {'str':substr} - str += substr - -# str += ('@ifinfo\n' + self.output_info () + '\n@end ifinfo\n') -# str += ('@tex\n' + self.output_latex () + '\n@end tex\n') -# str += ('@html\n' + self.output_html () + '\n@end html\n') - - if QUOTE in self.option_dict: - str = output[TEXINFO][QUOTE] % vars () - - # need par after image - str += '\n' - - return str - -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): - 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 () - - def verb_ly (self): - s = self.contents - s = re_begin_verbatim.split (s)[-1] - s = re_end_verbatim.split (s)[0] - return s - - def ly (self): - name = self.substring ('filename') - return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s' - % (name, self.contents)) - - -snippet_type_to_class = { - 'lilypond_file': Lilypond_file_snippet, - 'lilypond_block': Lilypond_snippet, - 'lilypond': Lilypond_snippet, - 'include': Include_snippet, -} def find_linestarts (s): nls = [0] @@ -1330,16 +303,17 @@ def find_linestarts (s): nls.append (len (s)) return nls -def find_toplevel_snippets (s, types): +def find_toplevel_snippets (input_string, formatter): res = {} - for i in types: - res[i] = ly.re.compile (snippet_res[global_options.format][i]) + types = formatter.supported_snippet_types () + for t in types: + res[t] = re.compile (formatter.snippet_regexp (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. @@ -1347,22 +321,18 @@ def find_toplevel_snippets (s, types): # where we search. # Since every part of the string is traversed at most once for # every type of snippet, this is linear. - while 1: first = None endex = 1 << 30 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 = global_options.formatter.snippet_class (type) start = index + m.start ('match') line_number = line_start_idx @@ -1370,13 +340,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, formatter, line_number, global_options) 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. @@ -1392,267 +362,161 @@ 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 (BookSnippet.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 (BookSnippet.Substring (input_string, index, start, line_start_idx + 1)) snippets.append (snip) found[first] = None index = start + len (snip.match.group ('match')) return snippets -def filter_pipe (input, cmd): - if global_options.verbose: - progress (_ ("Opening filter `%s'") % cmd) - - (stdin, stdout, stderr) = os.popen3 (cmd) - stdin.write (input) - status = stdin.close () - - if not status: - status = 0 - output = stdout.read () - status = stdout.close () - error = stderr.read () - - if not status: - status = 0 - signal = 0x0f & status - if status or (not output and error): - exit_status = status >> 8 - error (_ ("`%s' failed (%d)") % (cmd, exit_status)) - error (_ ("The error log is as follows:")) - ly.stderr_write (error) - ly.stderr_write (stderr.read ()) - exit (status) - - if global_options.verbose: - progress ('\n') - - return output - -def run_filter (s): - return filter_pipe (s, global_options.filter_cmd) - -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: - cmd += ' --formats=png ' - elif global_options.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) - - if ly_names: - open ('snippet-names', 'wb').write ('\n'.join (['snippet-map.ly'] - + ly_names)) - - my_system (' '.join ([cmd, 'snippet-names'])) - - -LATEX_INSPECTION_DOCUMENT = r''' -\nonstopmode -%(preamble)s -\begin{document} -\typeout{textwidth=\the\textwidth} -\typeout{columnsep=\the\columnsep} -\makeatletter\if@twocolumn\typeout{columns=2}\fi\makeatother -\end{document} -''' +def system_in_directory (cmd, directory, logfile): + """Execute a command in a different directory. + + Because of win32 compatibility, we can't simply use subprocess. + """ + + current = os.getcwd() + os.chdir (directory) + """NB - ignore_error is deliberately set to the same value + as redirect_output - this is not a typo.""" + retval = ly.system(cmd, + be_verbose=ly.is_verbose (), + redirect_output=global_options.redirect_output, + log_file=logfile, + progress_p=1, + ignore_error=global_options.redirect_output) + if retval != 0: + print ("Error trapped by lilypond-book") + print ("\nPlease see " + logfile + ".log\n") + sys.exit(1) + + os.chdir (current) + + +def process_snippets (cmd, snippets, + formatter, lily_output_dir): + """Run cmd on all of the .ly files from snippets.""" + + if not snippets: + return + + cmd = formatter.adjust_snippet_command (cmd) -# Do we need anything else besides `textwidth'? -def get_latex_textwidth (source): - m = re.search (r'''(?P\\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] - - 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() - - os.unlink (tmpfile) - os.unlink (logfile) - - columns = 0 - 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) - if m: - columnsep = float (m.group (1)) - - textwidth = 0 - m = re.search ('textwidth=([0-9.]*)pt', parameter_string) - if m: - textwidth = float (m.group (1)) - if columns: - textwidth = (textwidth - columnsep) / columns - - return textwidth - -def modify_preamble (chunk): - str = chunk.replacement_text () - if (re.search (r"\\begin *{document}", str) - and not re.search ("{graphic[sx]", str)): - str = re.sub (r"\\begin{document}", - r"\\usepackage{graphics}" + '\n' - + r"\\begin{document}", - 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', - # TEXINFO: '.texinfo', - TEXINFO: '.texi', - LATEX: '.tex', - DOCBOOK: '.xml' -} - -class Compile_error: - pass + 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) + logfile = name.replace('.ly', '') + file (name, 'wb').write (contents) + + system_in_directory (' '.join ([cmd, ly.mkarg (name.replace (os.path.sep, '/'))]), + lily_output_dir, + logfile) + +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 ().replace('\\','/'), 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 = [] + def globquote(x): + return re.sub ("[][*?]", r"[\g<0>]", x) + for subdir in glob.glob (os.path.join (globquote (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, BookSnippet.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) - progress ('\n') + for snippet in outdated: + snippet.write_ly() 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.formatter, options.lily_output_dir) + else: progress (_ ("All snippets are up to date...")) - progress ('\n') + + progress (_ ("Linking files...")) + abs_lily_output_dir = os.path.join (options.original_dir, options.lily_output_dir) + abs_output_dir = os.path.join (options.original_dir, options.output_dir) + if abs_lily_output_dir != abs_output_dir: + output_files = split_output_files (abs_lily_output_dir) + for snippet in snippets: + snippet.link_all_output_files (abs_lily_output_dir, + output_files, + abs_output_dir) + + +### +# Format guessing data def guess_format (input_filename): format = None e = os.path.splitext (input_filename)[1] - if e in ext2format.keys (): - # FIXME - format = ext2format[e] - else: - error (_ ("cannot determine format for: %s" \ - % input_filename)) - exit (1) - return format + for formatter in BookBase.all_formats: + if formatter.can_handle_extension (e): + return formatter + error (_ ("cannot determine format for: %s" % input_filename)) + exit (1) 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: progress (_ ("%s is up to date.") % file_name) - progress ('\n') # this prevents make from always rerunning lilypond-book: - # .texi target must be touched in order to be up to date - if global_options.format == 'texinfo': - os.utime (file_name, None) + # 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) - progress ('\n') + file (file_name, 'w').writelines (lines) + def note_input_file (name, inputs=[]): ## hack: inputs is mutable! @@ -1667,222 +531,240 @@ def samefile (f1, f2): f2 = re.sub ("//*", "/", f2) return f1 == f2 -def do_file (input_filename): +def do_file (input_filename, included=False): # Ugh. + input_absname = input_filename if not input_filename or input_filename == '-': in_handle = sys.stdin input_fullname = '' else: if os.path.exists (input_filename): input_fullname = input_filename - elif global_options.format == LATEX and ly.search_exe_path ('kpsewhich'): - input_fullname = os.popen ('kpsewhich ' + input_filename).read()[:-1] else: - input_fullname = find_file (input_filename) + input_fullname = global_options.formatter.input_fullname (input_filename) + # Normalize path to absolute path, since we will change cwd to the output dir! + # Otherwise, "lilypond-book -o out test.tex" will complain that it is + # overwriting the input file (which it is actually not), since the + # input filename is relative to the CWD... + input_absname = os.path.abspath (input_fullname) note_input_file (input_fullname) - in_handle = open (input_fullname) + in_handle = file (input_fullname) if input_filename == '-': + global_options.input_dir = os.getcwd () 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]) + global_options.input_dir = os.path.split (input_absname)[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 + global_options.formatter.default_extension) + if (os.path.exists (input_filename) + and os.path.exists (output_filename) + and samefile (output_filename, input_absname)): + 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) - - - # FIXME: Containing blocks must be first, see - # find_toplevel_snippets. - snippet_types = ( - 'multiline_comment', - 'verbatim', - 'lilypond_block', - # 'verb', - 'singleline_comment', - 'lilypond_file', - 'include', - 'lilypond', - ) + + if not included: + global_options.formatter.init_default_snippet_options (source) + + progress (_ ("Dissecting...")) - chunks = find_toplevel_snippets (source, snippet_types) + chunks = find_toplevel_snippets (source, global_options.formatter) - if global_options.format == LATEX: - for c in chunks: - if (c.is_plain () and - re.search (r"\\begin *{document}", c.replacement_text())): - modify_preamble (c) - break - progress ('\n') + # Let the formatter modify the chunks before further processing + chunks = global_options.formatter.process_chunks (chunks) if global_options.filter_cmd: 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, BookSnippet.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 BookSnippet.CompileError: os.chdir (original_dir) progress (_ ("Removing `%s'") % output_filename) - progress ('\n') - raise Compile_error + raise BookSnippet.CompileError + +def adjust_include_path (path, outpath): + """Rewrite an include path relative to the dir where lilypond is launched. + Always use forward slashes since this is what lilypond expects.""" + path = os.path.expanduser (path) + path = os.path.expandvars (path) + path = os.path.normpath (path) + if os.path.isabs (outpath): + return os.path.abspath (path).replace (os.path.sep, '/') + if os.path.isabs (path): + return path.replace (os.path.sep, '/') + return os.path.join (inverse_relpath (original_dir, outpath), path).replace (os.path.sep, '/') + +def inverse_relpath (path, relpath): + """Given two paths, the second relative to the first, + return the first path relative to the second. + Always use forward slashes since this is what lilypond expects.""" + if os.path.isabs (relpath): + return os.path.abspath (path).replace (os.path.sep, '/') + relparts = [''] + parts = os.path.normpath (path).split (os.path.sep) + for part in os.path.normpath (relpath).split (os.path.sep): + if part == '..': + relparts.append (parts[-1]) + parts.pop () + else: + relparts.append ('..') + parts.append (part) + return '/'.join (relparts[::-1]) 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.information = {'program_version': ly.program_version, 'program_name': ly.program_name } + global_options.original_dir = original_dir + + if global_options.lily_output_dir: + global_options.lily_output_dir = os.path.expanduser (global_options.lily_output_dir) + for i, path in enumerate(global_options.include_path): + global_options.include_path[i] = adjust_include_path (path, global_options.lily_output_dir) + global_options.include_path.insert (0, inverse_relpath (original_dir, global_options.lily_output_dir)) + + elif global_options.output_dir: + global_options.output_dir = os.path.expanduser (global_options.output_dir) + for i, path in enumerate(global_options.include_path): + global_options.include_path[i] = adjust_include_path (path, global_options.output_dir) + global_options.include_path.insert (0, inverse_relpath (original_dir, global_options.output_dir)) + + global_options.include_path.insert (0, "./") + + # Load the python packages (containing e.g. custom formatter classes) + # passed on the command line + nr = 0 + for i in global_options.custom_packages: + nr += 1 + progress (str(imp.load_source ("book_custom_package%s" % nr, i))) + - 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?? + if (os.environ.has_key ("LILYPOND_BOOK_LOGLEVEL")): + ly.set_loglevel (os.environ["LILYPOND_BOOK_LOGLEVEL"]) 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]) - formats = 'ps' - if global_options.format in (TEXINFO, HTML, DOCBOOK): - formats += ',png' + if global_options.format: + # Retrieve the formatter for the given format + for formatter in BookBase.all_formats: + if formatter.can_handle_format (global_options.format): + global_options.formatter = formatter + else: + global_options.formatter = guess_format (files[0]) + global_options.format = global_options.formatter.format + + # make the global options available to the formatters: + global_options.formatter.global_options = global_options + formats = global_options.formatter.image_formats - 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]) - - 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.verbose: - global_options.process_cmd += " --verbose " + includes = global_options.include_path + global_options.process_cmd += ' '.join ([' -I %s' % ly.mkarg (p) + for p in includes]) + + global_options.formatter.process_options (global_options) + + if global_options.lily_loglevel: + ly.debug_output (_ ("Setting LilyPond's loglevel to %s") % global_options.lily_loglevel, True) + global_options.process_cmd += " --loglevel=%s" % global_options.lily_loglevel + elif ly.is_verbose (): + if os.environ.get ("LILYPOND_LOGLEVEL", None): + ly.debug_output (_ ("Setting LilyPond's loglevel to %s (from environment variable LILYPOND_LOGLEVEL)") % os.environ.get ("LILYPOND_LOGLEVEL", None), True) + global_options.process_cmd += " --loglevel=%s" % os.environ.get ("LILYPOND_LOGLEVEL", None) + else: + ly.debug_output (_ ("Setting LilyPond's output to --verbose, implied by lilypond-book's setting"), True) + 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) + + relative_output_dir = 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 BookSnippet.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 - + '.%s' % global_options.format) - + 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 (relative_output_dir, + base_file_name + global_options.formatter.default_extension) + os.chdir (original_dir) - open (dep_file, 'w').write ('%s: %s' % (final_output_file, ' '.join (inputs))) + file (dep_file, 'w').write ('%s: %s\n' + % (final_output_file, ' '.join (inputs))) if __name__ == '__main__': main ()