#!@TARGET_PYTHON@
'''
Example usage:
test:
lilypond-book --filter="tr '[a-z]' '[A-Z]'" BOOK
convert-ly on book:
lilypond-book --filter="convert-ly --no-version --from=1.6.11 -" BOOK
classic lilypond-book:
lilypond-book --process="lilypond" BOOK.tely
TODO:
* this script is too complex. Modularize.
* ly-options: intertext?
* --line-width?
* eps in latex / eps by lilypond -b ps?
* check latex parameters, twocolumn, multicolumn?
* use --png --ps --pdf for making images?
* Converting from lilypond-book source, substitute:
@mbinclude foo.itely -> @include foo.itely
\mbinput -> \input
'''
import glob
import os
import re
import stat
import sys
import tempfile
"""
@relocate-preamble@
"""
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'
help_summary = (
_ ("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 -F "convert-ly --no-version --from=2.0.0 -" %(BOOK)s
$ lilypond-book --process='lilypond -I include' %(BOOK)s
''' % {'BOOK': _ ("BOOK")})
authors = ('Jan Nieuwenhuizen .*?)
.*?)
)
\s)''',
'singleline_comment':
no_match,
'verb':
no_match,
'verbatim':
no_match,
'lilypondversion':
no_match,
},
##
HTML: {
'include':
no_match,
'lilypond':
r'''(?mx)
(?P
.*?)
/>)''',
'lilypond_block':
r'''(?msx)
(?P
.*?)
)
\s)''',
'singleline_comment':
no_match,
'verb':
r'''(?x)
(?P
.*?
))''',
'verbatim':
r'''(?x)
(?s)
(?P\s.*?
\s))''',
'lilypondversion':
r'''(?mx)
(?P.*?)
})''',
'lilypond_block':
r'''(?smx)
^[^%\n]*?
(?P
.*?)
^[^%\n]*?
\\end\s*{lilypond})''',
'lilypond_file':
r'''(?smx)
^[^%\n]*?
(?P
%.*$\n+))''',
'verb':
r'''(?mx)
^[^%\n]*?
(?P
\\verb(?P
.)
.*?
(?P=del)))''',
'verbatim':
r'''(?msx)
^[^%\n]*?
(?P
\\begin\s*{verbatim}
.*?
\\end\s*{verbatim}))''',
'lilypondversion':
r'''(?smx)
(?P
.*?)
})''',
'lilypond_block':
r'''(?msx)
^(?P
.*?)
^@end\s+lilypond)\s''',
'lilypond_file':
r'''(?mx)
^(?P
@ignore\s
.*?
@end\s+ignore))\s''',
'singleline_comment':
r'''(?mx)
^.*
(?P
@c([ \t][^\n]*|)\n))''',
# Don't do this: It interferes with @code{@{}.
# 'verb': r'''(?P
@code{.*?})''',
'verbatim':
r'''(?sx)
(?P
@example
\s.*?
@end\s+example\s))''',
'lilypondversion':
r'''(?mx)
[^@](?P
''',
OUTPUT: r'''
''',
PRINTFILENAME: '',
QUOTE: r'''
%(str)s
''',
VERBATIM: r'''
%(verb)s
''',
VERSION: program_version,
},
##
LATEX: {
OUTPUT: r'''{%%
\parindent 0pt
\ifx\preLilyPondExample \undefined
\else
\expandafter\preLilyPondExample
\fi
\def\lilypondbook{}%%
\input %(base)s-systems.tex
\ifx\postLilyPondExample \undefined
\else
\expandafter\postLilyPondExample
\fi
}''',
PRINTFILENAME: '''\\texttt{%(filename)s}
''',
QUOTE: r'''\begin{quotation}
%(str)s
\end{quotation}''',
VERBATIM: r'''\noindent
\begin{verbatim}%(verb)s\end{verbatim}
''',
VERSION: program_version,
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
@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
%(version)s@verbatim
%(verb)s@end verbatim
''',
VERSION: program_version,
ADDVERSION: r'''@example
\version @w{"@version{}"}
@end example
'''
},
}
#
# 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
%% ****************************************************************
%% end ly snippet
%% ****************************************************************
}
'''
FULL_LY = '''
%% ****************************************************************
%% ly snippet:
%% ****************************************************************
%(code)s
%% ****************************************************************
%% 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)))
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 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 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:
#
# \input texinfo @c -*-texinfo-*-
#
# Bluntly search first K items of
# source.
# s = chunks[0].replacement_text ()
if re.search (regex, source[:1024]):
default_ly_options[LINE_WIDTH] = texinfo_line_widths[regex]
break
class Chunk:
def replacement_text (self):
return ''
def filter_text (self):
return self.replacement_text ()
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
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.checksum = 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 IncludeSnippet (Snippet):
def processed_filename (self):
f = self.substring ('filename')
return os.path.splitext (f)[0] + format2ext[self.format]
def replacement_text (self):
s = self.match.group ('match')
f = self.substring ('filename')
return re.sub (f, self.processed_filename (), s)
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):
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')
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 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["\'])(.*)(?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 = self.split_options (option_string)
for option in options:
if '=' in option:
(key, value) = re.split ('\s*=\s*', option)
self.option_dict[key] = value
else:
if option in no_options:
if no_options[option] in self.option_dict:
del self.option_dict[no_options[option]]
else:
self.option_dict[option] = 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 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:
self.option_dict[RAGGED_RIGHT] = None
if type == 'lilypond':
if LINE_WIDTH in self.option_dict:
del self.option_dict[LINE_WIDTH]
else:
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 or type == 'lilypond':
if LINE_WIDTH in self.option_dict:
del self.option_dict[LINE_WIDTH]
if not INDENT in self.option_dict:
self.option_dict[INDENT] = '0\\mm'
# The QUOTE pattern from ly_options only emits the `line-width'
# keyword.
if has_line_width and QUOTE in self.option_dict:
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:
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 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_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
hash = md5 (self.relevant_contents (self.full_ly ()))
## let's not create too long names.
self.checksum = hash.hexdigest ()[:10]
return self.checksum
def basename (self):
cs = self.get_checksum ()
name = '%s/lily-%s' % (cs[:2], cs[2:10])
return name
def write_ly (self):
base = self.basename ()
path = os.path.join (global_options.lily_output_dir, base)
directory = os.path.split(path)[0]
if not os.path.isdir (directory):
os.makedirs (directory)
out = file (path + '.ly', 'w')
out.write (self.full_ly ())
file (path + '.txt', 'w').write ('image of music')
def relevant_contents (self, ly):
return re.sub (r'\\(version|sourcefileline|sourcefilename)[^\n]*\n|' +
NOGETTEXT + '[,\]]', '', ly)
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:
try:
os.unlink (os.path.join (destination, name))
except OSError:
pass
src = os.path.join (output_dir, name)
dst = os.path.join (destination, name)
dst_path = os.path.split(dst)[0]
if not os.path.isdir (dst_path):
os.makedirs (dst_path)
os.link (src, dst)
def all_output_files (self, output_dir, 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)
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 = filter_pipe (code, global_options.filter_cmd)
d = {
'code': s,
'options': self.match.group ('options')
}
# TODO
return output[self.format][FILTER] % d
def replacement_text (self):
func = LilypondSnippet.__dict__['output_' + self.format]
return func (self)
def get_images (self):
base = self.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]))):
count = ps_page_count ('%(base)s.eps' % vars ())
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 ()
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 = '