k-l-lambda's picture
added node-addon-lilypond
f65fe85
# book_snippets.py
# -*- coding: utf-8 -*-
#
# This file is part of LilyPond, the GNU music typesetter.
#
# Copyright (C) 2010--2020 Reinhold Kainhofer <[email protected]>
#
# 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/>.
import codecs
import copy
import hashlib
import os
import re
import shutil
import subprocess
import sys
import book_base
import lilylib as ly
progress = ly.progress
warning = ly.warning
error = ly.error
debug = ly.debug_output
####################################################################
# Snippet option handling
####################################################################
#
# Is this pythonic? Personally, I find this rather #define-nesque. --hwn
#
# Global definitions:
ADDVERSION = 'addversion'
AFTER = 'after'
ALT = 'alt'
BEFORE = 'before'
DOCTITLE = 'doctitle'
EXAMPLEINDENT = 'exampleindent'
FILENAME = 'filename'
FILTER = 'filter'
FRAGMENT = 'fragment'
LAYOUT = 'layout'
LINE_WIDTH = 'line-width'
NOFRAGMENT = 'nofragment'
NOGETTEXT = 'nogettext'
NOINDENT = 'noindent'
INDENT = 'indent'
NORAGGED_RIGHT = 'noragged-right'
NOTES = 'body'
NOTIME = 'notime'
OUTPUT = 'output'
OUTPUTIMAGE = 'outputimage'
PAPER = 'paper'
PAPERSIZE = 'papersize'
PREAMBLE = 'preamble'
PRINTFILENAME = 'printfilename'
QUOTE = 'quote'
RAGGED_RIGHT = 'ragged-right'
RELATIVE = 'relative'
STAFFSIZE = 'staffsize'
TEXIDOC = 'texidoc'
VERBATIM = 'verbatim'
VERSION = 'lilypondversion'
# NOTIME and NOGETTEXT have no opposite so they aren't part of this
# dictionary.
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)
# Options without a pattern in snippet_options.
simple_options = [
EXAMPLEINDENT,
FRAGMENT,
NOFRAGMENT,
NOGETTEXT,
NOINDENT,
PRINTFILENAME,
DOCTITLE,
TEXIDOC,
VERBATIM,
FILENAME,
ALT,
ADDVERSION
]
####################################################################
# LilyPond templates for the snippets
####################################################################
snippet_options = {
##
NOTES: {
RELATIVE: r'''\relative c%(relative_quotes)s''',
},
##
# TODO: Remove the 1mm additional padding in the line-width
# once lilypond creates tighter cropped images!
PAPER: {
PAPERSIZE: r'''#(set-paper-size "%(papersize)s")''',
INDENT: r'''indent = %(indent)s''',
LINE_WIDTH: r'''line-width = %(line-width)s
%% offset the left padding, also add 1mm as lilypond creates cropped
%% images with a little space on the right
line-width = #(- line-width (* mm %(padding_mm)f) (* mm 1))''',
QUOTE: r'''line-width = %(line-width)s - 2.0 * %(exampleindent)s
%% offset the left padding, also add 1mm as lilypond creates cropped
%% images with a little space on the right
line-width = #(- line-width (* mm %(padding_mm)f) (* mm 1))''',
RAGGED_RIGHT: r'''ragged-right = ##t''',
NORAGGED_RIGHT: r'''ragged-right = ##f''',
},
##
LAYOUT: {
NOTIME: r'''
\context {
\Score
timing = ##f
}
\context {
\Staff
\remove "Time_signature_engraver"
}''',
},
##
PREAMBLE: {
STAFFSIZE: r'''#(set-global-staff-size %(staffsize)s)''',
},
}
def classic_lilypond_book_compatibility(key, value):
if key == 'lilyquote':
return (QUOTE, value)
if key == 'singleline' and value is None:
return (RAGGED_RIGHT, None)
m = re.search(r'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)
PREAMBLE_LY = r'''%%%% Generated by lilypond-book
%%%% Options: [%(option_string)s]
\include "lilypond-book-preamble.ly"
%% ****************************************************************
%% Start cut-&-pastable-section
%% ****************************************************************
%(preamble_string)s
\paper {
%(paper_string)s
}
\layout {
%(layout_string)s
}
%(safe_mode_string)s
'''
FULL_LY = '''
%% ****************************************************************
%% ly snippet:
%% ****************************************************************
%(code)s
%% ****************************************************************
%% end ly snippet
%% ****************************************************************
'''
FRAGMENT_LY = r'''
%(notes_string)s
{
%% ****************************************************************
%% ly snippet contents follows:
%% ****************************************************************
%(code)s
%% ****************************************************************
%% end ly snippet
%% ****************************************************************
}
'''
####################################################################
# Helper functions
####################################################################
def ps_page_count(ps_name):
# Open .ps file in binary mode, it might contain embedded fonts.
header = open(ps_name, 'rb').read(1024)
m = re.search(b'\n%%Pages: ([0-9]+)', header)
if m:
return int(m.group(1))
return 0
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))
class CompileError(Exception):
pass
####################################################################
# Snippet classes
####################################################################
class Chunk:
def replacement_text(self):
return ''
def filter_text(self):
return self.replacement_text()
def is_plain(self):
return False
def __init__(self):
self._input_fullpath = ''
self._output_fullpath = ''
def set_document_fullpaths(self, in_fp: str, out_fp: str):
self._input_fullpath = in_fp
self._output_fullpath = out_fp
def input_fullpath(self) -> str:
"""The input file path where this chunk comes from."""
return self._input_fullpath
def output_fullpath(self) -> str:
"""The output file path that this chunk belongs to."""
return self._output_fullpath
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, formatter, line_number, global_options):
self.type = type
self.match = match
self.checksum = 0
self.option_dict = {}
self.formatter = formatter
self.line_number = line_number
self.global_options = global_options
self.replacements = {'program_version': global_options.information["program_version"],
'program_name': ly.program_name}
# return a shallow copy of the replacements, so the caller can modify
# it locally without interfering with other snippet operations
def get_replacements(self):
return copy.copy(self.replacements)
def replacement_text(self):
return self.match.group('match')
def substring(self, s):
return self.match.group(s)
def __repr__(self):
return repr(self.__class__) + ' type = ' + self.type
class IncludeSnippet (Snippet):
def processed_filename(self):
f = self.substring('filename')
return os.path.splitext(f)[0] + self.formatter.default_extension
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, formatter, line_number, global_options):
Snippet.__init__(self, type, match, formatter,
line_number, global_options)
self.filename = ''
self.ext = '.ly'
os = match.group('options')
self.parse_snippet_options(os, self.type)
def snippet_options(self):
return []
def verb_ly_gettext(self, s):
lang = self.formatter.document_language
if not lang:
return s
try:
t = langdefs.translation[lang]
except:
return s
# TODO: this part is flawed. langdefs is not imported,
# so the line under `try:` raises a NameError, which is
# catched by the too broad `except:` that was likely meant
# only to except KeyError. As a result, this function
# always returns `s` and the below code is never executed.
# Investigate what the intent was and change the code accordingly
# if possible. --jas
s = ly_comment_re.sub(lambda m: ly_comment_gettext(t, m), s)
if langdefs.LANGDICT[lang].enable_ly_identifier_l10n:
for v in ly_var_def_re.findall(s):
s = re.sub(r"(?m)(?<!\\clef)(^|[' \\#])%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
def verb_ly(self):
verb_text = self.substring('code')
if not NOGETTEXT in self.option_dict:
verb_text = self.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):
return self.formatter.split_snippet_options(option_string)
def parse_snippet_options(self, option_string, type):
self.snippet_option_dict = {}
# Split option string and create raw option_dict from it
options = self.split_options(option_string)
for option in options:
(key, value) = (option, None)
if '=' in option:
(key, value) = re.split(r'\s*=\s*', option)
else:
# a no... option removes a previous option if present!
if key in no_options:
if no_options[key] in self.option_dict:
del self.snippet_option_dict[no_options[key]]
key = None
# Check for deprecated options, replace them by new ones
(c_key, c_value) = classic_lilypond_book_compatibility(key, value)
if c_key:
if c_value:
warning(
_("deprecated ly-option used: %s=%s") % (key, value))
warning(
_("compatibility mode translation: %s=%s") % (c_key, c_value))
else:
warning(
_("deprecated ly-option used: %s") % key)
warning(
_("compatibility mode translation: %s") % c_key)
(key, value) = (c_key, c_value)
# Finally, insert the option:
if key:
self.snippet_option_dict[key] = value
# If LINE_WIDTH is used without parameter, set it to default.
has_line_width = LINE_WIDTH in self.snippet_option_dict
if has_line_width and self.snippet_option_dict[LINE_WIDTH] is None:
del self.snippet_option_dict[LINE_WIDTH]
# RELATIVE does not work without FRAGMENT, so imply that
if RELATIVE in self.snippet_option_dict:
self.snippet_option_dict[FRAGMENT] = None
# Now get the default options from the formatter object (HTML, latex,
# texinfo, etc.) and insert the explicit snippet options to get the
# list of all options for this snippet
# first, make sure we have an INDENT value as a fallback
self.option_dict = {INDENT: '0\\mm'}
self.option_dict.update(self.formatter.default_snippet_options)
self.option_dict.update(self.snippet_option_dict)
# also construct a list of all options (as strings) that influence the
# visual appearance of the snippet
lst = [x_y for x_y in iter(self.option_dict.items(
)) if x_y[0] not in PROCESSING_INDEPENDENT_OPTIONS]
option_list = []
for (key, value) in lst:
if value is None:
option_list.append(key)
else:
option_list.append(key + "=" + value)
option_list.sort()
self.outputrelevant_option_list = option_list
#print ("self.outputrelevant_option_list: %s\n" % self.outputrelevant_option_list);
# 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:
# self.option_dict[LINE_WIDTH] = "#(- paper-width \
# left-margin-default right-margin-default)"
# Get a list of all options (as string) that influence the snippet appearance
def get_outputrelevant_option_strings(self):
return self.outputrelevant_option_list
def compose_ly(self, code):
# 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 TeX 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.
#
# Update: since July 2011 we are running texinfo on a test file
# to detect the default exampleindent, so we might reintroduce
# further usage of exampleindent again.
#
# First, make sure we have some falback default value, auto-detected
# values and explicitly specified values will always override them:
override[EXAMPLEINDENT] = r'0.4\in'
override[LINE_WIDTH] = '5\\in'
override.update(self.formatter.default_snippet_options)
override['padding_mm'] = self.global_options.padding_mm
option_string = ','.join(self.get_outputrelevant_option_strings())
compose_dict = {}
compose_types = [NOTES, PREAMBLE, LAYOUT, PAPER]
for a in compose_types:
compose_dict[a] = []
option_names = sorted(self.option_dict.keys())
for key in option_names:
value = self.option_dict[key]
if value:
override[key] = value
else:
if key not in override:
override[key] = None
found = 0
for typ in compose_types:
if key in snippet_options[typ]:
compose_dict[typ].append(snippet_options[typ][key])
found = 1
break
if not found and key not in simple_options and key not in self.snippet_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
# put paper-size first, if it exists
for i, elem in enumerate(compose_dict[PAPER]):
if elem.startswith("#(set-paper-size"):
compose_dict[PAPER].insert(0, compose_dict[PAPER].pop(i))
break
paper_string = '\n '.join(compose_dict[PAPER]) % override
layout_string = '\n '.join(compose_dict[LAYOUT]) % override
notes_string = '\n '.join(compose_dict[NOTES]) % vars()
preamble_string = '\n '.join(compose_dict[PREAMBLE]) % override
padding_mm = self.global_options.padding_mm
if self.global_options.safe_mode:
safe_mode_string = "#(ly:set-option 'safe #t)"
else:
safe_mode_string = ""
d = globals().copy()
d.update(locals())
d.update(self.global_options.information)
if FRAGMENT in self.option_dict:
body = FRAGMENT_LY
else:
body = FULL_LY
return (PREAMBLE_LY + body) % d
def get_checksum(self):
if not self.checksum:
# 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 = hashlib.md5(self.relevant_contents(
self.ly()).encode('utf-8'))
for option in self.get_outputrelevant_option_strings():
hash.update(option.encode('utf-8'))
# let's not create too long names.
self.checksum = hash.hexdigest()[:10]
return self.checksum
def basename(self):
cs = self.get_checksum()
name = os.path.join(cs[:2], 'lily-%s' % cs[2:])
return name
final_basename = basename
def write_ly(self):
base = self.basename()
path = os.path.join(self.global_options.lily_output_dir, base)
directory = os.path.split(path)[0]
os.makedirs(directory, exist_ok=True)
filename = path + '.ly'
if os.path.exists(filename):
existing = codecs.open(filename, 'r', 'utf-8').read()
if self.relevant_contents(existing) != self.relevant_contents(self.full_ly()):
warning("%s: duplicate filename but different contents of original file,\n\
printing diff against existing file." % filename)
encoded = self.full_ly().encode('utf-8')
cmd = 'diff -u %s -' % filename
sys.stderr.write(self.filter_pipe(
encoded, cmd).decode('utf-8'))
else:
out = codecs.open(filename, 'w', 'utf-8')
out.write(self.full_ly())
def relevant_contents(self, ly):
return re.sub(r'\\(version|sourcefileline|sourcefilename)[^\n]*\n', '', ly)
def link_all_output_files(self, output_dir, destination):
existing, missing = self.all_output_files(output_dir)
if missing:
error(_('Missing files: %s') % ', '.join(missing))
raise CompileError(self.basename())
for name in existing:
if (self.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:
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]
os.makedirs(dst_path, exist_ok=True)
try:
if (self.global_options.use_source_file_names
and isinstance(self, LilypondFileSnippet)):
content = open(src, 'rb').read()
basename = self.basename().encode('utf-8')
final_basename = self.final_basename().encode('utf-8')
content = content.replace(basename, final_basename)
open(dst, 'wb').write(content)
else:
try:
os.link(src, dst)
except AttributeError:
shutil.copyfile(src, dst)
except (IOError, OSError):
error(_('Could not overwrite file %s') % dst)
raise CompileError(self.basename())
def additional_files_to_consider(self, base, full):
return []
def additional_files_required(self, base, full):
result = []
if self.ext != '.ly':
result.append(base + self.ext)
return result
def all_output_files(self, output_dir):
"""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 os.path.isfile(os.path.join(output_dir, name)):
result.add(name)
def require_file(name):
if os.path.isfile(os.path.join(output_dir, name)):
result.add(name)
else:
missing.add(name)
# UGH - junk self.global_options
skip_lily = self.global_options.skip_lilypond_run
require_file(base + '.ly')
if not skip_lily:
require_file(base + '-systems.count')
if 'dseparate-log-file' in self.global_options.process_cmd:
require_file(base + '.log')
for f in [base + '.tex',
base + '.eps',
base + '.pdf',
base + '.texidoc',
base + '.doctitle',
base + '-systems.texi',
base + '-systems.tex',
base + '-systems.pdftexi']:
consider_file(f)
if self.formatter.document_language:
for f in [base + '.texidoc' + self.formatter.document_language,
base + '.doctitle' + self.formatter.document_language]:
consider_file(f)
required_files = self.formatter.required_files(
self, base, full, result)
for f in required_files:
require_file(f)
system_count = 0
if not skip_lily and not missing:
system_count = int(open(full + '-systems.count', encoding="utf8").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 self.global_options.process_cmd:
consider_file(systemfile + '.signature')
for f in self.additional_files_to_consider(base, full):
consider_file(f)
for f in self.additional_files_required(base, full):
require_file(f)
return (result, missing)
def is_outdated(self, output_dir):
found, missing = self.all_output_files(output_dir)
return missing
def filter_pipe(self, input: bytes, cmd: str) -> bytes:
"""Pass input through cmd, and return the result.
Args:
input: the input
cmd: a shell command
Returns:
the filtered result
"""
debug(_("Running through filter `%s'") % cmd, True)
closefds = True
if sys.platform == "mingw32":
closefds = False
p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=closefds)
(stdin, stdout, stderr) = (p.stdin, p.stdout, p.stderr)
stdin.write(input)
status = stdin.close()
if not status:
status = 0
output = stdout.read()
status = stdout.close()
# assume stderr always is text
err = stderr.read().decode('utf-8')
if not status:
status = 0
signal = 0x0f & status
if status or (not output and err):
exit_status = status >> 8
ly.error(_("`%s' failed (%d)") % (cmd, exit_status))
ly.error(_("The error log is as follows:"))
sys.stderr.write(err)
exit(status)
debug('\n')
return output
def get_snippet_code(self) -> str:
return self.substring('code')
def filter_text(self):
"""Run snippet bodies through a command (say: convert-ly).
"""
code = self.get_snippet_code().encode('utf-8')
output = self.filter_pipe(code, self.global_options.filter_cmd)
options = self.match.group('options')
if options is None:
options = ''
d = {
'code': output.decode('utf-8'),
'options': options,
}
return self.formatter.output_simple_replacements(FILTER, d)
def replacement_text(self):
base = self.final_basename()
return self.formatter.snippet_output(base, self)
def get_images(self):
base = self.final_basename()
outdir = self.global_options.lily_output_dir
single_base= '%s.png' % base
single = os.path.join(outdir, single_base)
multiple = os.path.join(outdir, '%s-page1.png' % base)
images = (single_base,)
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(os.path.join(outdir, '%s.eps' % base))
images = ['%s-page%d.png' % (base, page)
for page in range(1, count+1)]
images = tuple(images)
return images
re_begin_verbatim = re.compile(r'\s+%.*?begin verbatim.*\n*', re.M)
re_end_verbatim = re.compile(r'\s+%.*?end verbatim.*$', re.M)
class LilypondFileSnippet (LilypondSnippet):
def __init__(self, type, match, formatter, line_number, global_options):
LilypondSnippet.__init__(
self, type, match, formatter, line_number, global_options)
self.filename = self.substring('filename')
self.contents = None
def get_contents(self) -> bytes:
if not self.contents:
self.contents = open(book_base.find_file(self.filename,
self.global_options.include_path, self.global_options.original_dir), 'rb').read()
return self.contents
def get_snippet_code(self) -> str:
return self.get_contents().decode('utf-8')
def verb_ly(self):
s = self.get_snippet_code()
s = re_begin_verbatim.split(s)[-1]
s = re_end_verbatim.split(s)[0]
if not NOGETTEXT in self.option_dict:
s = self.verb_ly_gettext(s)
if not s.endswith('\n'):
s += '\n'
return s
def ly(self):
name = self.filename
return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
% (name, self.get_snippet_code()))
def final_basename(self):
if self.global_options.use_source_file_names:
base = os.path.splitext(os.path.basename(self.filename))[0]
return base
else:
return self.basename()
class MusicXMLFileSnippet (LilypondFileSnippet):
def __init__(self, type, match, formatter, line_number, global_options):
LilypondFileSnippet.__init__(
self, type, match, formatter, line_number, global_options)
self.compressed = False
self.converted_ly = None
self.ext = os.path.splitext(os.path.basename(self.filename))[1]
self.musicxml_options_dict = {
'verbose': '--verbose',
'lxml': '--lxml',
'compressed': '--compressed',
'relative': '--relative',
'absolute': '--absolute',
'no-articulation-directions': '--no-articulation-directions',
'no-rest-positions': '--no-rest-positions',
'no-page-layout': '--no-page-layout',
'no-beaming': '--no-beaming',
'language': '--language',
}
def snippet_options(self):
return list(self.musicxml_options_dict.keys())
def convert_from_musicxml(self):
name = self.filename
xml2ly_option_list = []
for (key, value) in list(self.option_dict.items()):
cmd_key = self.musicxml_options_dict.get(key, None)
if cmd_key is None:
continue
if value is None:
xml2ly_option_list.append(cmd_key)
else:
xml2ly_option_list.append(cmd_key + '=' + value)
if ('.mxl' in name) and ('--compressed' not in xml2ly_option_list):
xml2ly_option_list.append('--compressed')
self.compressed = True
opts = " ".join(xml2ly_option_list)
progress(_("Converting MusicXML file `%s'...") % self.filename)
cmd = 'musicxml2ly %s --out=- - ' % opts
ly_code = self.filter_pipe(self.get_contents(), cmd).decode('utf-8')
return ly_code
def ly(self):
if self.converted_ly is None:
self.converted_ly = self.convert_from_musicxml()
name = self.filename
return ('\\sourcefilename \"%s\"\n\\sourcefileline 0\n%s'
% (name, self.converted_ly))
def write_ly(self):
base = self.basename()
path = os.path.join(self.global_options.lily_output_dir, base)
directory = os.path.split(path)[0]
os.makedirs(directory, exist_ok=True)
# First write the XML to a file (so we can link it!)
if self.compressed:
xmlfilename = path + '.mxl'
else:
xmlfilename = path + '.xml'
if os.path.exists(xmlfilename):
diff_against_existing = self.filter_pipe(
self.get_contents(), 'diff -u %s - ' % xmlfilename)
if diff_against_existing:
warning(_("%s: duplicate filename but different contents of original file,\n\
printing diff against existing file.") % xmlfilename)
sys.stderr.write(diff_against_existing.decode('utf-8'))
else:
out = open(xmlfilename, 'wb')
out.write(self.get_contents())
out.close()
# also write the converted lilypond
filename = path + '.ly'
if os.path.exists(filename):
encoded = self.full_ly().encode('utf-8')
cmd = 'diff -u %s -' % filename
diff_against_existing = self.filter_pipe(
encoded, cmd).decode('utf-8')
if diff_against_existing:
warning(_("%s: duplicate filename but different contents of converted lilypond file,\n\
printing diff against existing file.") % filename)
sys.stderr.write(diff_against_existing.decode('utf-8'))
else:
out = codecs.open(filename, 'w', 'utf-8')
out.write(self.full_ly())
out.close()
class LilyPondVersionString (Snippet):
"""A string that does not require extra memory."""
def __init__(self, type, match, formatter, line_number, global_options):
Snippet.__init__(self, type, match, formatter,
line_number, global_options)
def replacement_text(self):
return self.formatter.output_simple(self.type, self)
snippet_type_to_class = {
'lilypond_file': LilypondFileSnippet,
'lilypond_block': LilypondSnippet,
'lilypond': LilypondSnippet,
'include': IncludeSnippet,
'lilypondversion': LilyPondVersionString,
'musicxml_file': MusicXMLFileSnippet,
}