277 lines
9.1 KiB
Python
Executable File
277 lines
9.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Extact a text field (title, by default) from a .abc file, and print it out
|
|
# with any ABC accented characters converted to HTML (default) or Latex.
|
|
#
|
|
# Optionally rearrange a field into display format:
|
|
# * In Title fields, change 'sort' form such as 'Exploding Potato, The'
|
|
# to display format 'The Exploding Potato'.
|
|
# * In Key fields, translate the ABC key representation to full text,
|
|
# e.g. G#dor becomes G# Dorian.
|
|
#
|
|
# Recognise continuation header fields and print those too. The ABC standard
|
|
# defines continuation fields as starting ':+'. Regrettably none of the tools
|
|
# I am using the Booke recognise that syntax, so I am adopting a Booke
|
|
# convention of '<header>:+' *also* being a continuation. Note that a
|
|
# continuation is a distinct line in the field value; the value has a line
|
|
# break between it and the previous line.
|
|
#
|
|
|
|
import optparse
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
accentedletters = {
|
|
# Acute accents
|
|
"'A" : ("Á", "\\'{A}"),
|
|
"'E" : ("É", "\\'{E}"),
|
|
"'I" : ("Í", "\\'{I}"),
|
|
"'O" : ("Ó", "\\'{O}"),
|
|
"'U" : ("Ú", "\\'{U}"),
|
|
"'Y" : ("Ý", "\\'{Y}"),
|
|
"'a" : ("á", "\\'{a}"),
|
|
"'e" : ("é", "\\'{e}"),
|
|
"'i" : ("í", "\\'{i}"),
|
|
"'o" : ("ó", "\\'{o}"),
|
|
"'u" : ("ú", "\\'{u}"),
|
|
"'y" : ("ý", "\\'{y}"),
|
|
|
|
# Grave accents
|
|
"`A" : ("À", "\\`{A}"),
|
|
"`E" : ("È", "\\`{E}"),
|
|
"`I" : ("Ì", "\\`{I}"),
|
|
"`O" : ("Ò", "\\`{O}"),
|
|
"`U" : ("Ù", "\\`{U}"),
|
|
"`a" : ("à", "\\`{a}"),
|
|
"`e" : ("è", "\\`{e}"),
|
|
"`i" : ("ì", "\\`{i}"),
|
|
"`o" : ("ò", "\\`{o}"),
|
|
"`u" : ("ù", "\\`{u}"),
|
|
|
|
# Umlauts
|
|
"\"A" : ("Ä", "\\\"{A}"),
|
|
"\"E" : ("Ë", "\\\"{E}"),
|
|
"\"I" : ("Ï", "\\\"{I}"),
|
|
"\"O" : ("Ö", "\\\"{O}"),
|
|
"\"U" : ("Ü", "\\\"{U}"),
|
|
"\"Y" : ("Ÿ", "\\\"{Y}"),
|
|
"\"a" : ("ä", "\\\"{a}"),
|
|
"\"e" : ("ë", "\\\"{e}"),
|
|
"\"i" : ("ï", "\\\"{\i}"),
|
|
"\"o" : ("ö", "\\\"{o}"),
|
|
"\"u" : ("ü", "\\\"{u}"),
|
|
"\"y" : ("ÿ", "\\\"{y}"),
|
|
|
|
# Circumflexes
|
|
"^A" : ("Â", "\\^{A}"),
|
|
"^E" : ("Ê", "\\^{E}"),
|
|
"^I" : ("Î", "\\^{I}"),
|
|
"^O" : ("Ô", "\\^{O}"),
|
|
"^U" : ("Û", "\\^{U}"),
|
|
"^a" : ("â", "\\^{a}"),
|
|
"^e" : ("ê", "\\^{e}"),
|
|
"^i" : ("î", "\\^{\i}"),
|
|
"^o" : ("ô", "\\^{o}"),
|
|
"^u" : ("û", "\\^{u}"),
|
|
|
|
# Tilde
|
|
"~A" : ("Ã", "\\~{A}"),
|
|
"~N" : ("Ñ", "\\~{N}"),
|
|
"~O" : ("Õ", "\\~{O}"),
|
|
"~a" : ("ã", "\\~{a}"),
|
|
"~n" : ("ñ", "\\~{n}"),
|
|
"~o" : ("õ", "\\~{o}"),
|
|
|
|
# Cedilla
|
|
",C" : ("Ç", "\\c{C}"),
|
|
",c" : ("ç", "\\c{c}"),
|
|
|
|
# Slash
|
|
"/O" : ("Ø", "\\O"),
|
|
"/o" : ("ø", "\\o"),
|
|
|
|
# Ring
|
|
"AA" : ("Å", "\\r{A}"),
|
|
"aa" : ("å", "\\r{a}"),
|
|
|
|
# Ligatures
|
|
"AE" : ("Æ", "\\AE"),
|
|
"ae" : ("æ", "\\ae"),
|
|
"ss" : ("ß", "\\ss"),
|
|
}
|
|
|
|
abckeys = {
|
|
"m": "Minor",
|
|
"min": "Minor",
|
|
"mix": "Mixolydian",
|
|
"dor": "Dorian",
|
|
"phr": "Phrygian",
|
|
"lyd": "Lydian",
|
|
"loc": "Locrian",
|
|
}
|
|
|
|
# Convert ABC accented chars to HTML entities or LaTex.
|
|
def convertAccents(t, latex=False):
|
|
res = ""
|
|
while True:
|
|
p = t.partition('\\')
|
|
res += p[0]
|
|
if p[1] == "":
|
|
break
|
|
abc = p[2][0:2]
|
|
t = p[2][2:]
|
|
if abc in accentedletters:
|
|
if latex:
|
|
res += accentedletters[abc][1]
|
|
else:
|
|
res += accentedletters[abc][0]
|
|
else:
|
|
res += "\\" + abc
|
|
return res
|
|
|
|
# Convert Title fields from sort to display, so Bat, The->The Bat.
|
|
def convertTitleToDisplay(t):
|
|
p = t.rpartition(',')
|
|
if p[1] == "":
|
|
return t
|
|
else:
|
|
first = p[2].strip()
|
|
second = p[0].strip()
|
|
return (first + " " if first.isalnum() else first) + second
|
|
|
|
# Convert Key field from ABC to display, so G#dor->G# Dorian.
|
|
def convertKeyToDisplay(t):
|
|
letter = t[0].upper()
|
|
accidental = ""
|
|
mode = ""
|
|
try:
|
|
accidental = t[1]
|
|
if accidental == '#' or accidental == 'b':
|
|
mode = t[2:]
|
|
else:
|
|
accidental = ""
|
|
mode = t[1:]
|
|
except IndexError:
|
|
pass
|
|
mode = mode.strip().lower()
|
|
return letter + accidental + ' ' + abckeys.get(mode, "Major")
|
|
|
|
# Convert input string from Markdown to HTML or LaTeX. Fix up link
|
|
# targets so any 'foo.abc' target links to the tune with that name.
|
|
def convertMarkdown(t, latex):
|
|
if latex:
|
|
target = "--to=latex"
|
|
else:
|
|
target = "--to=html"
|
|
res = subprocess.check_output(['pandoc', '--from=markdown', target], input=t, universal_newlines=True)
|
|
if latex:
|
|
res = re.sub(r'\\href{(.*?).abc}', r'\\hyperlink{\1}', res)
|
|
else:
|
|
res = re.sub(r'href="(.*?).abc"', r'href="\1.html"', res)
|
|
return res.strip()
|
|
|
|
# Implement a custom Markdown shorthand for referencing ABC files.
|
|
# <foo.abc> will expand to ['title of foo'](foo.abc).
|
|
def expandCustomMarkdown(t, dir, latex):
|
|
# Given a match to (foo.abc), return a markdown link to the tune with the
|
|
# title (and subtitle, if present) of the tune as the text of the link.
|
|
def getTitle(m):
|
|
fname = m.group(1) + ".abc"
|
|
path = pathlib.Path(dir, fname)
|
|
with path.open() as f:
|
|
title = getFieldDisplayText(f, dir, "T", latex=latex)
|
|
f.seek(0)
|
|
subtitle = getFieldDisplayText(f, dir, "T", n=2, latex=latex)
|
|
if len(subtitle) > 0:
|
|
title = title + " (" + subtitle + ")"
|
|
return "[" + title + "](" + fname + ")"
|
|
return re.sub(r'<(.*?).abc>', getTitle, t)
|
|
|
|
# Return the raw text for a given field. Optionally the nth field is taken,
|
|
# or the field data must start with a designated string to be recognised.
|
|
def getFieldText(inf, field, n = 1, starts = None):
|
|
res = ""
|
|
for line in inf:
|
|
line = line.strip()
|
|
if len(line) > 2 and line[1] == ':':
|
|
if line[0] == "+" or (line[0] == field and line[2] == "+"):
|
|
if not res:
|
|
continue
|
|
if line[0] == "+":
|
|
line = line[2:]
|
|
else:
|
|
line = line[3:]
|
|
res = res + '\n' + line.strip()
|
|
else:
|
|
if res:
|
|
break
|
|
if line[0] == field:
|
|
line = line[2:].strip()
|
|
if starts:
|
|
if line.find(starts) != 0:
|
|
continue
|
|
line = line[len(starts):].strip()
|
|
if n > 1:
|
|
n = n - 1
|
|
continue
|
|
res = line
|
|
return res
|
|
|
|
# Return display text for a given field.
|
|
def getFieldDisplayText(inf, dir, field, n = 1, starts = None, latex = False):
|
|
res = getFieldText(inf, field, n, starts)
|
|
if res:
|
|
res = convertAccents(res, latex)
|
|
if field.upper() == "T":
|
|
res = convertTitleToDisplay(res)
|
|
elif field.upper() == "K":
|
|
res = convertKeyToDisplay(res)
|
|
elif field.upper() in ["H", "N"]:
|
|
res = convertMarkdown(expandCustomMarkdown(res, dir, latex), latex)
|
|
return res
|
|
|
|
if __name__ == "__main__":
|
|
def process(inf, dir, options):
|
|
if options.display:
|
|
line = getFieldDisplayText(inf, dir, options.field, options.index, options.starts, options.latex)
|
|
else:
|
|
line = getFieldText(inf, options.field, options.index, options.starts)
|
|
if line:
|
|
print(line)
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
# execute only if run as a script
|
|
parser = optparse.OptionParser(usage="usage: %prog [options] [filename]\n\n"
|
|
" Extract field data from ABC file.")
|
|
parser.add_option("-f", "--field", dest="field", default="T",
|
|
help="extract the field FIELD", metavar="FIELD")
|
|
parser.add_option("-l", "--latex", dest="latex",
|
|
action="store_true", default=False,
|
|
help="convert special characters for LaTeX")
|
|
parser.add_option("-d", "--display", dest="display",
|
|
action="store_true", default=False,
|
|
help="convert to display text")
|
|
parser.add_option("-n", "--index", dest="index",
|
|
action="store", type="int", default=1,
|
|
help="report INDEXth value [default: %default]",
|
|
metavar="INDEX")
|
|
parser.add_option("-s", "--starts", dest="starts",
|
|
action="store", type="string", default=None,
|
|
help="report only if line starts CONTENT and remove CONTENT",
|
|
metavar="CONTENT")
|
|
(options, args) = parser.parse_args()
|
|
|
|
res = False
|
|
if len(args) > 0:
|
|
for arg in args:
|
|
path = pathlib.Path(arg)
|
|
with path.open() as f:
|
|
res = res or process(f, path.parent, options)
|
|
else:
|
|
res = process(sys.stdin, ".", options)
|
|
sys.exit(int(not res))
|