Spaces:
Sleeping
Sleeping
# 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, | |
} | |