changeset 589:5db7e72d4219 build-default-240

Automated merge with ssh://hg.cryhavoc.org.uk/dottes
author Jim Hague <jim.hague@acm.org>
date Wed, 02 Nov 2016 00:21:30 +0000
parents de2122c78ea0 (current diff) afc031477784 (diff)
children a6fc091a8c5e
files
diffstat 14 files changed, 297 insertions(+), 245 deletions(-) [+]
line wrap: on
line diff
--- a/Session/MichaelTurnersWaltz.abc	Sat Oct 29 22:41:44 2016 +0100
+++ b/Session/MichaelTurnersWaltz.abc	Wed Nov 02 00:21:30 2016 +0000
@@ -1,11 +1,11 @@
 X: 1
 T: Michael Turner's Waltz
+Z: Cry Havoc Booke of Dottes. Creative Commons by-nc-sa 2.0 UK licenced.
 M: 3/4
 L: 1/8
 R: waltz
 Q: 140
 K: Gmaj
-Z: Cry Havoc Booke of Dottes. Creative Commons by-nc-sa 2.0 UK licenced.
 DGA | "G" B2 B2 "D" c2 | "G" d4 gf | "C" e2>f2 ge | "G" d2>"D" D2 GA |
         "G" B2 B2 "D" c2 | "G" d2>e2 cA | "G" G3 B "D" AF | "G" G3 :|
 AB | "D" c2>d2 cB | "D" A4 Bc | "G" d2>e2 dc | "Em" B4 gf |
--- a/Session/Railway.abc	Sat Oct 29 22:41:44 2016 +0100
+++ b/Session/Railway.abc	Wed Nov 02 00:21:30 2016 +0000
@@ -1,10 +1,10 @@
 X: 1
 T: Railway, The
+N: Change: FieryClockFace.abc
 M: 6/8
 L: 1/8
 Q: 160
 K: Gmaj
-N: Change: FieryClockFace.abc
 D | "G" G2 G GBd | g2 d d2 d | "Am" e2 d c2 B | "D7" ABA FED |
     "G" G2 G GBd | g2 d d2 d | "C" edc "G" BAG | "D7" A3 "G" G2 :|
 d | "G" b2 b b2 a | g2 g g2 d | "C" e2 d efg | "D" a2 g fed |
--- a/Session/RoguesMarch.abc	Sat Oct 29 22:41:44 2016 +0100
+++ b/Session/RoguesMarch.abc	Wed Nov 02 00:21:30 2016 +0000
@@ -1,10 +1,10 @@
 X: 1
 T: Rogues' March
+N: Change: CaptainLanoesQuickMarch.abc
 M: 6/8
 L: 1/4
 Q: 160
 K: Gmaj
-N: Change: CaptainLanoesQuickMarch.abc
 "G" B B/ B/c/d/ | "C" e e<e | "D" d/d/d/ d e/ | "D" d c/ B A/ |
 "G" B B/ B/c/d/ | "C" e e<e | "D" d/e/f/ "G" g/d/B/ | "D" A3/2 "G" G3/2 :|
 "Em" g3/2 "D" f3/2 | "C" e3/2 "D" d3/2 |\
--- a/abcfield.py	Sat Oct 29 22:41:44 2016 +0100
+++ b/abcfield.py	Wed Nov 02 00:21:30 2016 +0000
@@ -1,8 +1,20 @@
-#!/usr/bin/env python
+#!/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.
-# Recognise continuation fields and print those too.
+#
+# 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
@@ -87,7 +99,18 @@
     "ss" : ("&szlig;", "\\ss"),
 }
 
-def convertField(t, options):
+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('\\')
@@ -97,64 +120,121 @@
         abc = p[2][0:2]
         t = p[2][2:]
         if abc in accentedletters:
-            if options.html:
+            if latex:
+                res += accentedletters[abc][1]
+            else:
                 res += accentedletters[abc][0]
-            else:
-                res += accentedletters[abc][1]
         else:
             res += "\\" + abc
     return res
 
-def process(inf, options):
-    n = options.index
-    found = False
+# Convert Title fields from sort to display, so Bat, The->The Bat.
+def convertTitleToDisplay(t):
+    p = t.rpartition(',')
+    if p[1] == "":
+        return t
+    else:
+        return p[2].strip() + " " + p[0].strip()
+
+# 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")
+
+# 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 found:
-                if line[0] != '+':
+            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
-                line = line[2:].strip()
-            elif line[0] == options.field:
-                if n > 1:
-                    n = n - 1
-                    continue
-                else:
+                if line[0] == field:
                     line = line[2:].strip()
-                    if len(options.starts) > 0:
-                        if line.find(options.starts) == 0:
-                            line = line[len(options.starts):].strip()
-                        else:
+                    if starts:
+                        if line.find(starts) != 0:
                             continue
-            else:
-                continue
-            found = True
-            print(convertField(line, options))
+                        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, field, n = 1, starts = None, latex = False):
+    res = getFieldText(inf, field, n, starts)
+    if res:
+        if field.upper() == "T":
+            res = convertTitleToDisplay(res)
+        elif field.upper() == "K":
+            res = convertKeyToDisplay(res)
+        res = convertAccents(res, latex)
+    return res
 
-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("-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="",
-                  help="report only if line starts CONTENT and remove CONTENT",
-                  metavar="CONTENT")
-(options, args) = parser.parse_args()
+if __name__ == "__main__":
+    def process(inf, options):
+        if options.display:
+            line = getFieldDisplayText(inf, 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
 
-if len(args) > 0:
-    for arg in args:
-        try:
-            inf = open(arg, "r")
-            process(inf, options)
-        finally:
-            inf.close()
-else:
-    process(sys.stdin, options)
-sys.exit(0)
+    # 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:
+            try:
+                inf = open(arg, "r")
+                res = res or process(inf, options)
+            finally:
+                inf.close()
+    else:
+        res = process(sys.stdin, options)
+    sys.exit(int(not res))
--- a/abcfirstline.py	Sat Oct 29 22:41:44 2016 +0100
+++ b/abcfirstline.py	Wed Nov 02 00:21:30 2016 +0000
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 #
 # Write out a modified version of a .abc file with just the data
 # to print the first line of the music only.
@@ -8,23 +8,23 @@
 
 def process(inf):
     continued = False
-    print "X:1"
+    print("X:1")
     for line in inf:
         line = line.strip()
         # If it is empty or starts "%", ignore it.
         if len(line) == 0 or line[0] == "%":
             continue
 
-        # Is it a header line? I.e. does it start LETTER COLON?
+        # Is it a header line? I.e. does it start LETTER (or +) COLON?
         # If so, output only ones we need.
         start = line[:2]
-        if len(start) > 1 and start[1] == ":" and start[0].isalpha():
+        if len(start) > 1 and start[1] == ":" and (start[0].isalpha() or start[0] == '+'):
             if start[0] in ["M", "K", "L"]:
-                print line
+                print(line)
         # Output line. If it is a continuation, output at most one
         # continuation.
         else:
-            print line
+            print(line)
             if continued or line[-1] != "\\":
                 break
             else:
--- a/abcrange.py	Sat Oct 29 22:41:44 2016 +0100
+++ b/abcrange.py	Wed Nov 02 00:21:30 2016 +0000
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 #
 # Find the range of a tune. Do minimal parsing of an ABC input file
 # and print the lowest and highest notes therein. Accidentals are
@@ -29,7 +29,7 @@
         # Is it a header line? I.e. does it start LETTER COLON?
         # If so, ignore.
         start = line[:2]
-        if len(start) > 1 and start[1] == ":" and start[0].isalpha():
+        if len(start) > 1 and start[1] == ":" and (start[0].isalpha() or start[0] == '+'):
             continue
 
         # Tune line.
@@ -82,7 +82,7 @@
                 lowest = note
             note = 0
 
-    print "{0}: {1} {2}".format(filename, highest, lowest)
+    print("{0}: {1} {2}".format(filename, highest, lowest))
 
 if len(sys.argv) > 1:
     for arg in sys.argv[1:]:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/abctemplate.py	Wed Nov 02 00:21:30 2016 +0000
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+#
+# Fill in a template with data from fields in an ABC file.
+# Fields have any ABC accented characters converted to HTML (default) or Latex.
+#
+# Rearrange some field contents 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.
+#
+# Templates are read from file, and are in Python standard library format.
+# The following values are substituted:
+# * name. The file base name. Base filename without extension.
+# * title. The tune title.
+# * subtitle. The tune subtitle (second Title field), if any.
+# * composer. The tune composer.
+# * key. The tune key.
+# * changefile. The name of the 'change' file, if any.
+# * changename. The change file base name.
+# * changetitle. The change file tune title.
+# * changevisibility. "yes" if there's a change value, otherwise "no".
+# * credit. The 'credit' value.
+# * creditvisibility. "yes" if there's a credit value, otherwise "no".
+#
+
+import argparse
+import pathlib
+import string
+
+from abcfield import getFieldDisplayText
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description='Substitute values from ABC file into template.')
+    parser.add_argument('-l', '--latex', dest='latex',
+                        action='store_true',
+                        help='output LaTeX formatted values (default is HTML)')
+    parser.add_argument('-t', '--template', dest='template',
+                        type=argparse.FileType('r'),
+                        required=True,
+                        help='template file')
+    parser.add_argument('-v', '--value', dest='values', action="append",
+                        default=[], help='define var=value items for templater')
+    parser.add_argument('input', type=argparse.FileType('r'),
+                        help='input ABC file')
+    args = parser.parse_args()
+
+    with args.input as f:
+        lines = f.readlines()
+
+    input_path = pathlib.Path(args.input.name)
+
+    vars = dict()
+    vars["changename"] = ""
+    vars["changetitle"] = ""
+    vars["changevisibility"] = "no"
+    vars["creditvisibility"] = "no"
+
+    vars["name"] = input_path.stem
+    vars["title"] = getFieldDisplayText(lines, "T", latex=args.latex)
+    vars["subtitle"] = getFieldDisplayText(lines, "T", n=2, latex=args.latex)
+    vars["composer"] = getFieldDisplayText(lines, "C", latex=args.latex)
+    vars["key"] = getFieldDisplayText(lines, "K", latex=args.latex)
+    vars["changefile"] = getFieldDisplayText(lines, "N", starts="Change:", latex=args.latex)
+    vars["credit"] = getFieldDisplayText(lines, "N", starts="Credit:", latex=args.latex)
+
+    if vars["changefile"]:
+        vars["changevisibility"] = "yes"
+        vars["changename"] = pathlib.Path(vars["changefile"]).stem
+        cf = pathlib.Path(input_path.parent, vars["changefile"])
+        with cf.open() as f:
+            vars["changetitle"] = getFieldDisplayText(f, "T", latex=args.latex)
+
+    if vars["credit"]:
+        vars["creditvisibility"] = "yes"
+
+    for val in args.values:
+        keyval = val.partition("=")
+        vars[keyval[0]] = keyval[2]
+
+    print(string.Template(args.template.read()).substitute(vars))
--- a/dottes.html.learnertune	Sat Oct 29 22:41:44 2016 +0100
+++ b/dottes.html.learnertune	Wed Nov 02 00:21:30 2016 +0000
@@ -2,7 +2,7 @@
 <html lang="en">
 <head>
   <meta charset="utf-8" />
-  <title>Cry Havoc tunes - learning @TITLE@</title>
+  <title>Cry Havoc tunes - learning ${title}</title>
   <link rel="stylesheet" href="../css/reset.css" />
   <link rel="stylesheet" href="../css/text.css" />
   <link rel="stylesheet" href="../css/960.css" />
@@ -33,7 +33,7 @@
         <div class="dottes-tune-header">
           <div class="dottes-tune-header-row">
             <div class="dottes-tune-header-left">
-              <a class="dottes-tune-icon-link" href="@TUNE@.html">
+              <a class="dottes-tune-icon-link" href="${name}.html">
                 <img class="dottes-tune-table-image" src="../img/music.png"
                      alt="Dottes">
               </a>
@@ -43,15 +43,15 @@
               </a>
             </div>
             <div class="dottes-tune-header-middle">
-              <h1>@TITLE@</h1>
-              <h2>@SUBTITLE@</h2>
+              <h1>${title}</h1>
+              <h2>${subtitle}</h2>
             </div>
             <div class="dottes-tune-header-right">
-              <em>@COMPOSER@</em>
+              <em>${composer}</em>
             </div>
           </div>
         </div>
-        <p>@TITLE@ is in the key of @KEY@.
+        <p>${title} is in the key of ${key}.
         <div class="dottes-tune-footer">
           <div class="dottes-tune-footer-row">
             <div class="dottes-tune-footer-left">
@@ -59,14 +59,14 @@
             </div>
             <div class="dottes-tune-footer-centre">
               <audio controls loop>
-                <source src="../@MASTERBOOKE@/normal-@TUNE@.mp3" type="audio/mpeg" />
-                <source src="../@MASTERBOOKE@/normal-@TUNE@.ogg" type="audio/ogg" />
+                <source src="../${masterbooke}/normal-${name}.mp3" type="audio/mpeg" />
+                <source src="../${masterbooke}/normal-${name}.ogg" type="audio/ogg" />
                 <object classid="clsid:22D6F312-B0F6-11D0-94AB-0080C74C7E95">
-                  <param name="FileName" value="normal-@TUNE@.mp3" />
+                  <param name="FileName" value="normal-${name}.mp3" />
                   <param name="autoStart" value="false" />
                   <param name="autoplay" value="false" />
                   <param name="playCount" value="100000" />
-                  <object type="audio/mpeg" data="normal-@TUNE@.mp3">
+                  <object type="audio/mpeg" data="normal-${name}.mp3">
                     <param name="controller" value="true" />
                     <param name="autoplay" value="false" />
                     <param name="playCount" value="100000" />
@@ -77,9 +77,9 @@
             <div class="dottes-tune-footer-right">
               <ul class="tune-data-list">
                 <li><a class="dottes-link-tune dottes-mp3" download
-                       href="../@MASTERBOOKE@/@TUNE@.mp3">MP3</a></li>
+                       href="../${masterbooke}/${name}.mp3">MP3</a></li>
                 <li><a class="dottes-link-tune dottes-ogg" download
-                       href="../@MASTERBOOKE@/@TUNE@.ogg">OGG</a></li>
+                       href="../${masterbooke}/${name}.ogg">OGG</a></li>
               </ul>
             </div>
           </div>
@@ -89,14 +89,14 @@
             </div>
             <div class="dottes-tune-footer-centre">
               <audio controls loop>
-                <source src="../@MASTERBOOKE@/littleslow-@TUNE@.mp3" type="audio/mpeg" />
-                <source src="../@MASTERBOOKE@/littleslow-@TUNE@.ogg" type="audio/ogg" />
+                <source src="../${masterbooke}/littleslow-${name}.mp3" type="audio/mpeg" />
+                <source src="../${masterbooke}/littleslow-${name}.ogg" type="audio/ogg" />
                 <object classid="clsid:22D6F312-B0F6-11D0-94AB-0080C74C7E95">
-                  <param name="FileName" value="littleslow-@TUNE@.mp3" />
+                  <param name="FileName" value="littleslow-${name}.mp3" />
                   <param name="autoStart" value="false" />
                   <param name="autoplay" value="false" />
                   <param name="playCount" value="100000" />
-                  <object type="audio/mpeg" data="littleslow-@TUNE@.mp3">
+                  <object type="audio/mpeg" data="littleslow-${name}.mp3">
                     <param name="controller" value="true" />
                     <param name="autoplay" value="false" />
                     <param name="playCount" value="100000" />
@@ -107,9 +107,9 @@
             <div class="dottes-tune-footer-right">
               <ul class="tune-data-list">
                 <li><a class="dottes-link-tune dottes-mp3" download
-                       href="../@MASTERBOOKE@/littleslow-@TUNE@.mp3">MP3</a></li>
+                       href="../${masterbooke}/littleslow-${name}.mp3">MP3</a></li>
                 <li><a class="dottes-link-tune dottes-ogg" download
-                       href="../@MASTERBOOKE@/littleslow-@TUNE@.ogg">OGG</a></li>
+                       href="../${masterbooke}/littleslow-${name}.ogg">OGG</a></li>
               </ul>
             </div>
           </div>
@@ -119,14 +119,14 @@
             </div>
             <div class="dottes-tune-footer-centre">
               <audio controls loop>
-                <source src="../@MASTERBOOKE@/slow-@TUNE@.mp3" type="audio/mpeg" />
-                <source src="../@MASTERBOOKE@/slow-@TUNE@.ogg" type="audio/ogg" />
+                <source src="../${masterbooke}/slow-${name}.mp3" type="audio/mpeg" />
+                <source src="../${masterbooke}/slow-${name}.ogg" type="audio/ogg" />
                 <object classid="clsid:22D6F312-B0F6-11D0-94AB-0080C74C7E95">
-                  <param name="FileName" value="slow-@TUNE@.mp3" />
+                  <param name="FileName" value="slow-${name}.mp3" />
                   <param name="autoStart" value="false" />
                   <param name="autoplay" value="false" />
                   <param name="playCount" value="100000" />
-                  <object type="audio/mpeg" data="slow-@TUNE@.mp3">
+                  <object type="audio/mpeg" data="slow-${name}.mp3">
                     <param name="controller" value="true" />
                     <param name="autoplay" value="false" />
                     <param name="playCount" value="100000" />
@@ -137,9 +137,9 @@
             <div class="dottes-tune-footer-right">
               <ul class="tune-data-list">
                 <li><a class="dottes-link-tune dottes-mp3" download
-                       href="../@MASTERBOOKE@/slow-@TUNE@.mp3">MP3</a></li>
+                       href="../${masterbooke}/slow-${name}.mp3">MP3</a></li>
                 <li><a class="dottes-link-tune dottes-ogg" download
-                       href="../@MASTERBOOKE@/slow-@TUNE@.ogg">OGG</a></li>
+                       href="../${masterbooke}/slow-${name}.ogg">OGG</a></li>
               </ul>
             </div>
           </div>
@@ -149,14 +149,14 @@
             </div>
             <div class="dottes-tune-footer-centre">
               <audio controls loop>
-                <source src="../@MASTERBOOKE@/veryslow-@TUNE@.mp3" type="audio/mpeg" />
-                <source src="../@MASTERBOOKE@/veryslow-@TUNE@.ogg" type="audio/ogg" />
+                <source src="../${masterbooke}/veryslow-${name}.mp3" type="audio/mpeg" />
+                <source src="../${masterbooke}/veryslow-${name}.ogg" type="audio/ogg" />
                 <object classid="clsid:22D6F312-B0F6-11D0-94AB-0080C74C7E95">
-                  <param name="FileName" value="veryslow-@TUNE@.mp3" />
+                  <param name="FileName" value="veryslow-${name}.mp3" />
                   <param name="autoStart" value="false" />
                   <param name="autoplay" value="false" />
                   <param name="playCount" value="100000" />
-                  <object type="audio/mpeg" data="veryslow-@TUNE@.mp3">
+                  <object type="audio/mpeg" data="veryslow-${name}.mp3">
                     <param name="controller" value="true" />
                     <param name="autoplay" value="false" />
                     <param name="playCount" value="100000" />
@@ -167,9 +167,9 @@
             <div class="dottes-tune-footer-right">
               <ul class="tune-data-list">
                 <li><a class="dottes-link-tune dottes-mp3" download
-                       href="../@MASTERBOOKE@/veryslow-@TUNE@.mp3">MP3</a></li>
+                       href="../${masterbooke}/veryslow-${name}.mp3">MP3</a></li>
                 <li><a class="dottes-link-tune dottes-ogg" download
-                       href="../@MASTERBOOKE@/veryslow-@TUNE@.ogg">OGG</a></li>
+                       href="../${masterbooke}/veryslow-${name}.ogg">OGG</a></li>
               </ul>
             </div>
           </div>
--- a/dottes.html.tune	Sat Oct 29 22:41:44 2016 +0100
+++ b/dottes.html.tune	Wed Nov 02 00:21:30 2016 +0000
@@ -2,7 +2,7 @@
 <html lang="en">
 <head>
   <meta charset="utf-8" />
-  <title>Cry Havoc tunes - @TITLE@</title>
+  <title>Cry Havoc tunes - ${title}</title>
   <link rel="stylesheet" href="../css/reset.css" />
   <link rel="stylesheet" href="../css/text.css" />
   <link rel="stylesheet" href="../css/960.css" />
@@ -24,7 +24,7 @@
         <div class="dottes-tune-header">
           <div class="dottes-tune-header-row">
             <div class="dottes-tune-header-left">
-              <a class="dottes-tune-icon-link" href="learner-@TUNE@.html">
+              <a class="dottes-tune-icon-link" href="learner-${name}.html">
                 <img class="dottes-tune-table-image" src="../img/learner.png"
                      alt="Learner">
               </a>
@@ -34,49 +34,49 @@
               </a>
             </div>
             <div class="dottes-tune-header-middle">
-              <h1>@TITLE@</h1>
-              <h2>@SUBTITLE@</h2>
+              <h1>${title}</h1>
+              <h2>${subtitle}</h2>
             </div>
             <div class="dottes-tune-header-right">
-              <em>@COMPOSER@</em>
+              <em>${composer}</em>
             </div>
           </div>
         </div>
-        <img class="dottes-png" src="@TUNE@.png" alt="@TITLE@ dots">
-        <div class="dottes-credit-@CREDITVISIBILITY@">
-          From @CREDIT@.
+        <img class="dottes-png" src="${name}.png" alt="${title} dots">
+        <div class="dottes-credit-${creditvisibility}">
+          From ${credit}.
         </div>
-        <div class="dottes-change-@CHANGEVISIBILITY@">
+        <div class="dottes-change-${changevisibility}">
           Change: <a class="dottes-change-link"
-                     href="@CHANGETUNE@">@CHANGETITLE@</a>
+                     href="${changename}.html">${changetitle}</a>
         </div>
         <div class="dottes-tune-footer">
           <div class="dottes-tune-footer-row">
             <div class="dottes-tune-footer-left">
               <ul class="tune-data-list">
                 <li><a class="dottes-link-tune dottes-pdf" download
-                       href="@TUNE@.pdf">PDF</a></li>
+                       href="${name}.pdf">PDF</a></li>
                 <li><a class="dottes-link-tune dottes-midi" download
-                       href="../@MASTERBOOKE@/@TUNE@.mid">MIDI</a></li>
+                       href="../${masterbooke}/${name}.mid">MIDI</a></li>
                 <li><a class="dottes-link-tune dottes-mp3" download
-                       href="../@MASTERBOOKE@/@TUNE@.mp3">MP3</a></li>
+                       href="../${masterbooke}/${name}.mp3">MP3</a></li>
                 <li><a class="dottes-link-tune dottes-ogg" download
-                       href="../@MASTERBOOKE@/@TUNE@.ogg">OGG</a></li>
+                       href="../${masterbooke}/${name}.ogg">OGG</a></li>
                 <li><a class="dottes-link-tune dottes-abc" download
-                       href="@TUNE@.abc">ABC</a></li>
+                       href="${name}.abc">ABC</a></li>
                 <li><a class="dottes-link-tune dottes-xml" download
-                       href="@TUNE@.xml">XML</a></li>
+                       href="${name}.xml">XML</a></li>
               </ul>
             </div>
             <div class="dottes-tune-footer-centre">
               <audio controls>
-                <source src="../@MASTERBOOKE@/@TUNE@.mp3" type="audio/mpeg" />
-                <source src="../@MASTERBOOKE@/@TUNE@.ogg" type="audio/ogg" />
+                <source src="../${masterbooke}/${name}.mp3" type="audio/mpeg" />
+                <source src="../${masterbooke}/${name}.ogg" type="audio/ogg" />
                 <object classid="clsid:22D6F312-B0F6-11D0-94AB-0080C74C7E95">
-                  <param name="FileName" value="@TUNE@.mp3" />
+                  <param name="FileName" value="${name}.mp3" />
                   <param name="autoStart" value="false" />
                   <param name="autoplay" value="false" />
-                  <object type="audio/mpeg" data="@TUNE@.mp3">
+                  <object type="audio/mpeg" data="${name}.mp3">
                     <param name="controller" value="true" />
                     <param name="autoplay" value="false" />
                   </object>
@@ -84,7 +84,7 @@
               </audio>
             </div>
             <div class="dottes-tune-footer-right">
-              Last changed @LASTCHANGED@
+              Last changed ${lastchanged}
             </div>
           </div>
         </div>
--- a/dottes.html.tuneindex	Sat Oct 29 22:41:44 2016 +0100
+++ b/dottes.html.tuneindex	Wed Nov 02 00:21:30 2016 +0000
@@ -1,14 +1,14 @@
 <div class="dottes-tune-list-item">
   <div class="dottes-tune-list-item-link">
-    <a class="dottes-tune-link" href="@TUNE@.html">@TITLE@</a>
-    <a href="learner-@TUNE@.html">
+    <a class="dottes-tune-link" href="${name}.html">${title}</a>
+    <a href="learner-${name}.html">
       <img class="dottes-tune-table-image" src="../img/learner.png" alt="Learner">
     </a>
   </div>
   <div class="dottes-tune-list-item-image">
-    <a href="@TUNE@.html">
-      <img class="dottes-tune-table-image" src="firstline-@TUNE@.png"
-           alt="@TITLE@ first line">
+    <a href="${name}.html">
+      <img class="dottes-tune-table-image" src="firstline-${name}.png"
+           alt="${title} first line">
     </a>
   </div>
 </div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dottes.tex.firstline-tune	Wed Nov 02 00:21:30 2016 +0000
@@ -0,0 +1,1 @@
+\showfirstline{$name}{$title}{$graphicsdir/firstline-$name}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dottes.tex.tune	Wed Nov 02 00:21:30 2016 +0000
@@ -0,0 +1,1 @@
+\showtune{$name}{$title}{$subtitle}{$composer}{$graphicsdir/$name}{$changename}{$changetitle}{$credit}
--- a/makeBookeTunePages.sh	Sat Oct 29 22:41:44 2016 +0100
+++ b/makeBookeTunePages.sh	Wed Nov 02 00:21:30 2016 +0000
@@ -8,13 +8,6 @@
 # makeGraphics.sh to make these.
 #
 
-# Restore titles like 'Exploding Potato, The' to the
-# expected 'The Exploding Potato'.
-fixtitle()
-{
-    retval=`echo "$1" | sed -e "s/\(.*\), *\(.*\)/\2 \1/"`
-}
-
 if [ $# != 1 ]; then
     echo "Usage: makeBookeTunePages.sh <book dir name>"
     exit 1
@@ -37,27 +30,6 @@
 find $booke -name "*.abc" | sort |
     while read filename
     do
-        name=`basename $filename .abc`
-        title=`$dir/abcfield.py --field T --latex $filename`
-        fixtitle "$title"
-        title=$retval
-        subtitle=`$dir/abcfield.py --index 2 --field T --latex $filename`
-        fixtitle "$subtitle"
-        subtitle=$retval
-        composer=`$dir/abcfield.py --field C --latex $filename`
-
-        changefile=`$dir/abcfield.py --field N --starts "Change:" $filename`
-        changename=""
-        changetitle=""
-        if [ -n "$changefile" ]; then
-            changename=`basename $changefile .abc`
-            changetitle=`$dir/abcfield.py --field T --latex $booke/$changefile`
-            fixtitle "$changetitle"
-            changetitle=$retval
-        fi
-
-        credit=`$dir/abcfield.py --field N --starts "Credit:" $filename`
-        echo -E "\showtune{$name}{$title}{$subtitle}{$composer}{$graphicsdir/$name}{$changename}{$changetitle}{$credit}" >> $tunesoutput
-
-        echo -E "\showfirstline{$name}{$title}{$graphicsdir/firstline-$name}" >> $indexoutput
+        $dir/abctemplate.py --latex --value "graphicsdir=$graphicsdir" --template $dir/dottes.tex.tune $filename >> $tunesoutput
+        $dir/abctemplate.py --latex --value "graphicsdir=$graphicsdir" --template $dir/dottes.tex.firstline-tune $filename >> $indexoutput
     done
--- a/makeWeb.sh	Sat Oct 29 22:41:44 2016 +0100
+++ b/makeWeb.sh	Wed Nov 02 00:21:30 2016 +0000
@@ -7,44 +7,6 @@
 
 #set -x
 
-# Restore titles like 'Exploding Potato, The' to the
-# expected 'The Exploding Potato'.
-fixtitle()
-{
-    retval=`echo "$1" | sed -e "s/\(.*\), *\(.*\)/\2 \1/"`
-}
-
-# Format a key in ABC (G, Gmin, etc.) in standard presentation format.
-fixkey()
-{
-    letter=${1:0:1}
-    accidental=${1:1:1}
-    if [ "$accidental" != "#" -a "$accidental" != "b" ]; then
-        accidental=""
-        mode=${1:1:3}
-    else
-        mode=${1:2:3}
-    fi
-    mode=${mode,,}
-    mode=${mode/ //g}
-    if [ "$mode" = "m" -o "$mode" = "min" ]; then
-        mode="Minor"
-    elif [ "$mode" = "mix" ]; then
-        mode="Mixolydian"
-    elif [ "$mode" = "dor" ]; then
-        mode="Dorian"
-    elif [ "$mode" = "phr" ]; then
-        mode="Phrygian"
-    elif [ "$mode" = "lyd" ]; then
-        mode="Lydian"
-    elif [ "$mode" = "loc" ]; then
-        mode="Locrian"
-    else
-        mode="Major"
-    fi
-    retval="${letter}${accidental} ${mode}"
-}
-
 if [ $# -lt 2 -o $# -gt 3 ]; then
     echo "Usage: makeWeb.sh <book dir name> <master book dir name> [<instrument name>]"
     exit 1
@@ -104,34 +66,6 @@
     do
         name=`basename $filename .abc`
 
-        # Extract items to substitute in the web page.
-        title=`$dir/abcfield.py --field T $filename`
-        fixtitle "$title"
-        title=$retval
-        subtitle=`$dir/abcfield.py --index 2 --field T $filename`
-        fixtitle "$subtitle"
-        subtitle=$retval
-        composer=`$dir/abcfield.py --field C  $filename`
-        changefile=`$dir/abcfield.py --field N --starts "Change:" $filename`
-        changetitle=""
-        changevisibility="no"
-        if [ -n "$changefile" ]; then
-            changetitle=`$dir/abcfield.py --field T $bookedir/$changefile`
-            changevisibility="yes"
-
-            fixtitle "$changetitle"
-            changetitle=$retval
-        fi
-        credit=`$dir/abcfield.py --field N --starts "Credit:" $filename`
-        creditvisibility="no"
-        if [ -n "$credit" ]; then
-            creditvisibility="yes"
-        fi
-        lastchanged=`hg log --limit 1 --template "{date|shortdate}" $masterbookedir/${name}.abc`
-        key=`$dir/abcfield.py --field K $filename`
-        fixkey $key
-        key=$retval
-
         # Copy the ABC into the web.
         cp $filename $webdir
 
@@ -148,38 +82,14 @@
             popd > /dev/null
         fi
 
+        # Get date and time of last change to tune.
+        lastchanged=`hg log --limit 1 --template "{date|shortdate}" $masterbookedir/${name}.abc`
+
         # Generate the tune web page.
         tunepage=${name}.html
         learnerpage=learner-${name}.html
 
-        # If the title contains HTML character entities, escape
-        # initial '&' in the title - it means things to sed.
-        sed -e "s/@TITLE@/${title//&/\\&}/" \
-            -e "s/@SUBTITLE@/${subtitle}/" \
-            -e "s/@COMPOSER@/${composer}/" \
-            -e "s/@KEY@/${key}/" \
-            -e "s/@MASTERBOOKE@/${masterbooke}/" \
-            -e "s/@CHANGETITLE@/${changetitle//&/\\&}/" \
-            -e "s/@CHANGETUNE@/${changefile/.abc/.html}/" \
-            -e "s/@CHANGEVISIBILITY@/${changevisibility}/" \
-            -e "s/@CREDIT@/${credit}/" \
-            -e "s/@CREDITVISIBILITY@/${creditvisibility}/" \
-            -e "s/@LASTCHANGED@/${lastchanged}/" \
-            -e "s/@TUNE@/${name}/" dottes.html.tune > $webdir/$tunepage
-
-        sed -e "s/@TITLE@/${title//&/\\&}/" \
-            -e "s/@SUBTITLE@/${subtitle}/" \
-            -e "s/@COMPOSER@/${composer}/" \
-            -e "s/@KEY@/${key}/" \
-            -e "s/@MASTERBOOKE@/${masterbooke}/" \
-            -e "s/@CHANGETITLE@/${changetitle//&/\\&}/" \
-            -e "s/@CHANGETUNE@/${changefile/.abc/.html}/" \
-            -e "s/@CHANGEVISIBILITY@/${changevisibility}/" \
-            -e "s/@CREDIT@/${credit}/" \
-            -e "s/@CREDITVISIBILITY@/${creditvisibility}/" \
-            -e "s/@LASTCHANGED@/${lastchanged}/" \
-            -e "s/@TUNE@/${name}/" dottes.html.learnertune > $webdir/$learnerpage
-
-        sed -e "s/@TITLE@/${title//&/\\&}/" \
-            -e "s/@TUNE@/${name}/" dottes.html.tuneindex >> $webdir/$tunelist
+        $dir/abctemplate.py --value "masterbooke=${masterbooke}" --value "lastchanged=${lastchanged}" --template dottes.html.tune $filename > $webdir/$tunepage
+        $dir/abctemplate.py --value "masterbooke=${masterbooke}" --value "lastchanged=${lastchanged}" --template dottes.html.learnertune $filename > $webdir/$learnerpage
+        $dir/abctemplate.py --template dottes.html.tuneindex $filename >> $webdir/$tunelist
     done