Improve support for English in general use mode
authorKris Kowal <kris.kowal@cixar.com>
Tue, 10 Dec 2013 05:57:04 +0000 (21:57 -0800)
committerKris Kowal <kris.kowal@cixar.com>
Tue, 10 Dec 2013 05:57:04 +0000 (21:57 -0800)
-   Infer an e in final position as silent and place an underpose dot
    instead of an overpose slash.
-   Distinguish e with a diaresis from final silent e
-   Support abbreviations for "of", "of the", "the", and "and".

18 files changed:
beleriand.js
classical.js
dan-smith.js
editor/about.html
editor/app.css
editor/index.html
editor/index.js
editor/modes.html
editor/modes.js
editor/package.json
general-use.js
normalize.js
notation.js
package.json
spec/black-speech-spec.js [deleted file]
spec/black-speech.js [deleted file]
spec/general-use-spec.js
spec/general-use.js

index f163908..5d9f3ae 100644 (file)
@@ -217,7 +217,7 @@ function parseTengwa(callback, options) {
                     return callback(makeColumn("round-carrier"))(character);
                 }
             };
-        } else if (character === "e") { // e
+        } else if (character === "e" || character === "ë") { // e
             return function (character) {
                 if (character === "i") { // ei
                     return callback(makeColumn("yanta").addAbove("í"));
index d8a44e7..c7c4533 100644 (file)
@@ -419,7 +419,7 @@ function parseTehta(callback, options, previous) {
                     return callback([previous, makeColumn("short-carrier").addAbove("a")])(character);
                 }
             };
-        } else if (character === "e") {
+        } else if (character === "e" || character === "ë") {
             var tehta = swapDotSlash("e", options);
             return function (character) {
                 if (character === "e") {
index 9c8973e..1e622cf 100644 (file)
@@ -64,8 +64,8 @@ exports.tengwar = {
     "full-stop": "-",
     "exclamation-point": "Á",
     "question-mark": "À",
-    "open-paren": "&#140;",
-    "close-paren": "&#156;",
+    "open-paren": "=", // alt "&#140;",
+    "close-paren": "=", // alt "&#156;",
     "flourish-left": "&#286;",
     "flourish-right": "&#287;",
     // numbers
index fb38b1d..4cbb1b0 100644 (file)
@@ -42,8 +42,8 @@
         
         <p><em><strong>Purpose:</strong></em> The transcriber is suitable for
         rendering <a href="http://tolkiengateway.net/wiki/Sindarin">Sindarin</a> in
-        <a href="http://at.mansbjorkman.net/teng_general.htm">General&nbsp;Use&nbsp</a>,
-        more or the
+        the mode for <a href="http://at.mansbjorkman.net/teng_general.htm">general&nbsp;use</a>,
+        or the
         <a href="http://at.mansbjorkman.net/teng_beleriand.htm">Mode&nbsp;of&nbsp;Beleriand</a>,
         and <a href="http://tolkiengateway.net/wiki/Quenay">Quenya</a> in the
         <a href="http://at.mansbjorkman.net/teng_quenya.htm">Classical</a>
 
         <script src="http://static.getclicky.com/js" type="text/javascript"></script> 
         <script type="text/javascript">clicky.init(237383);</script> 
-        <noscript><p><img alt="Clicky" width="1" height="1" src="http://in.getclicky.com/237383ns.gif" /></p></noscript> 
+        <noscript><p><img alt="Clicky" width="1" height="1" src="http://in.getclicky.com/237383ns.gif"></p></noscript> 
 
         <script type="text/javascript">
             var _gaq = _gaq || [];
index bde9f6b..686e726 100644 (file)
@@ -2,14 +2,9 @@
 body {
     margin: 0;
     padding: 0;
-    height: 100%;
-    width: 100%;
-
-    overflow: auto;
-
 }
 
-body.annatar {
+body.annatar #body-box {
 
     background-repeat: no-repeat;
     background: #fefcea; /* Old browsers */
@@ -25,7 +20,7 @@ body.annatar {
     text-shadow: 0px 0px 10px #730 ;
 }
 
-body.parmaite {
+body.parmaite #body-box {
     background-image: url(paper.jpg);
     background-repeat: no-repeat;
     background-size: 100%;
@@ -115,11 +110,15 @@ a:focus {
 /* layout */
 
 #body-box {
+    position: absolute;
+    bottom: 0;
+    top: 0;
+    left: 0;
+    right: 0;
+    overflow: auto;
     display: -webkit-flex;
     -webkit-flex-flow: column;
     -webkit-align-items: stretch;
-    height: 100%;
-    width: 100%;
 }
 
 #output-box {
index 527d8f5..771d22c 100644 (file)
@@ -1,3 +1,4 @@
+<!doctype html>
 <html>
     <head>
         <meta http-equiv="content-type" content="text/html; charset=utf-8">
index ec7d4b8..b3526ac 100644 (file)
@@ -1,7 +1,6 @@
 
 var Bindings = require("frb");
 require("frb/dom");
-var Properties = require("frb/properties");
 var QS = require("qs");
 var modes = require("tengwar/modes");
 var fonts = require("tengwar/fonts");
@@ -17,8 +16,11 @@ state.mode = state.mode || "general-use";
 state.font = state.font || "annatar";
 state.options = state.options || [];
 state.height = state.height || null;
+state.language = state.language || null;
 
-var bindings = Bindings.create(null, {
+var bindings = Bindings.defineBindings({
+    modes: modes,
+    fonts: fonts,
     state: state,
     window: window,
     bodyElement: document.body,
@@ -27,7 +29,23 @@ var bindings = Bindings.create(null, {
     dividerElement: document.querySelector("#divider"),
     inputBoxElement: document.querySelector("#input-box"),
     selectModeElement: document.querySelector("#select-mode"),
-    wikiTextElement: document.querySelector("#wiki-text")
+    wikiTextElement: document.querySelector("#wiki-text"),
+
+    searchString: function () {
+        var state = this.state;
+
+        // remove things that QS can't seem to handle
+        delete state[""]; // XXX QS bug interprets empty array as & and back to an empty assignment
+        if (state.options.length == 0) {
+            delete state.options;
+        }
+        var string = "?" + QS.stringify(this.state);
+        window.history.replaceState(this.state, "", string);
+
+        state.options = state.options || [];
+        return string;
+    }
+
 }, {
 
     "inputElement.value": {"<->": "state.q"},
@@ -47,38 +65,23 @@ var bindings = Bindings.create(null, {
     },
 
     "inputBoxElement.style.height": {
-        "<-": "heightPx"
-    },
-
-    "heightPx": {
-        "<-": "state.height",
-        convert: function (height) {
-            return height + "px";
-        },
-        revert: function (height) {
-            return parseInt(height, 10);
-        }
+        "<-": "state.height + 'px'"
     },
 
     "mode": {
-        args: ["state.mode"],
-        compute: function (mode) {
-            return modes[mode] || modes['general-use'];
-        }
+        "<-": "modes[state.mode] ?? modes['general-use']"
     },
 
     "font": {
-        args: ["state.font"],
-        compute: function (font) {
-            return fonts[font] || modes['annatar'];
-        }
+        "<-": "fonts[state.font] ?? fonts['annatar']"
     },
 
     "outputElement.innerHTML": {
-        args: ["mode", "font", "state.q", "state.options"],
-        compute: function (mode, font, input, flags) {
+        args: ["mode", "font", "state.language ?? 'unknown'", "state.q", "state.options"],
+        compute: function (mode, font, language, input, flags) {
             var options = {
                 font: font,
+                language: language,
                 block: true
             };
             flags.forEach(function (flag) {
@@ -92,25 +95,7 @@ var bindings = Bindings.create(null, {
     },
 
     "selectModeElement.href": {
-        "<-": "'modes.html' + searchString"
-    },
-
-    "searchString": {
-        args: ["state.q", "state.mode", "state.font", "state.height", "state.options"],
-        compute: function () {
-            var state = this.state;
-
-            // remove things that QS can't seem to handle
-            delete state[""]; // XXX QS bug interprets empty array as & and back to an empty assignment
-            if (state.options.length == 0) {
-                delete state.options;
-            }
-            var string = "?" + QS.stringify(this.state);
-            window.history.replaceState(this.state, "", string);
-
-            state.options = state.options || [];
-            return string;
-        }
+        "<-": "'modes.html' + searchString(state.q, state.mode, state.font, state.height ?? 0, state.options)"
     },
 
     "wikiTextElement.href": {
index 7ab4e47..db17000 100644 (file)
         <h3><em>For a Tengar Transcriber</em></h3>
 
         <ul>
-            <li><a href="#general-use">General Use Mode</a></li>
+            <li><a href="#general-use">Mode for general use</a></li>
             <ul>
                 <li><a href="#black-speech">Black Speech</a></li>
                 <li><a href="#kings-letter-general-use">The Third Version of the King’s Letter</a></li>
+                <li><a href="#english">English</a></li>
             </ul>
-            <li><a href="#classical">Classical Mode</a></li>
+            <li><a href="#classical">The Classical mode</a></li>
             <ul>
                 <li><a href="#namarie">Namárië</a></li>
             </ul>
@@ -35,7 +36,7 @@
 
 
         <a name="kings-letter-general-use"></a>
-        <h2><a class="dagger" href="http://at.mansbjorkman.net/teng_quenya.htm">General Use Mode</a></h2>
+        <h2><a class="dagger" href="http://at.mansbjorkman.net/teng_quenya.htm">Mode for general use</a></h2>
         <h3>As in the third version of <em><a class="dagger" href="http://tolkiengateway.net/wiki/King's_Letter">The King’s Letter</a></em></h3>
 
         <div>
@@ -57,8 +58,8 @@
         Westlands, will approach the Bridge of Baranduin on the eighth day of
         Spring, or in the Shire-reckoning the second day of April.</blockquote>
 
-        <p>General Use Mode uses diacritics to represent vowels, usually above
-        the tengwa the vowel precedes.  General Use mode is suitable for
+        <p>The mode for general use employs diacritics to represent vowels,
+        usually above the tengwa the vowel precedes.  The mode is suitable for
         representing most languages.  The King’s Letter is the longest example
         of
         <a class="dagger" href="http://tolkiengateway.net/wiki/Sindarin">Sindarin</a>
             One Ring to bring them all, and in the darkness bind them
         </blockquote>
 
-        <p>General Use Mode uses diacritics to represent vowels, usually above
-        the tengwa the vowel <em>precedes</em>.  The inscription on the One
-        Ring is a variant of General Use mode that reverses the direction of
-        curls for O and U, and uses extended tengwa for the SH and GH sounds.
-        The italic of the
+        <p>The mode for general use employs diacritics to represent vowels,
+        usually above the tengwa the vowel <em>precedes</em>.  The inscription
+        on the One Ring is a variant of mode for general use that reverses the
+        direction of curls for O and U, and uses extended tengwa for the SH and
+        GH sounds.  The italic of the
         <a class="dagger" href="http://home.student.uu.se/jowi4905/fonts/annatar.html">Tengwar Annatar</a>
         font by Johan Winge captures the flowing hand presumed of Sauron.</p>
 
 
+
+        <hr>
+
+
+        <a name="english"></a>
+        <h2><a class="dagger" href="http://at.mansbjorkman.net/teng_general_english.htm">English</a></h2>
+
+        <div>
+            <span id="english-general-use-tengwar"
+                class="dynamic-tengwar"
+                data-tengwar="The Lord of the Rings"
+                data-mode="general-use"
+                data-font="parmaite"
+                data-lang="english">
+                Rendering&hellip; (Requires JavaScript and Web Fonts)
+            </span>
+            <button id="english-general-use-button">Select</button>
+        </div>
+
+        <blockquote>The Lord of the Rings</blockquote>
+
+        <p>This is a slight adaptation of the mode for general use. The mode
+        introduces an underposed dot that stands for a silent, orthographic “e”
+        at the end of a word. Medial “e” can be transformed to a silent in in
+        this mode with a following tick, “<code>'</code>”. Also, the common
+        patterns, “of”, “the”, “of the”, and “and” have single-tengwa
+        shorthands, some with variations that can be introduced with a
+        following tick, or a tick between “<code>of'the</code>” to separate
+        these words.</p>
+
+        <p>The mode is haltingly suitable for both orthographic and phonetic
+        transliteration for the clever writer if they are willing to tease the
+        machine.</p>
+
+
         <hr>
 
 
index 31e94a6..1dad768 100644 (file)
@@ -30,7 +30,7 @@ var $$ = document.querySelectorAll.bind(document);
 function TengwarComponent(element, state) {
 
     // extract options from the mode string
-    var flags = element.dataset.mode.split(" ");
+    var flags = (element.dataset.mode || "").split(" ");
     flags.shift();
     var options = {};
     flags.forEach(function (flag) {
@@ -40,30 +40,24 @@ function TengwarComponent(element, state) {
         options[flag] = true;
     });
 
-    Bindings.create(null, {
+    Bindings.defineBindings({
+        fonts: fonts,
+        modes: modes,
         input: element.dataset.tengwar,
         element: element,
         options: options,
         state: state
     }, {
 
-        "element.classList.*": {
-            "<-": "('tengwar', state.font, state.mode, 'rendered')"
+        "element.classList.rangeContent()": {
+            "<-": "['tengwar', state.font, state.mode, 'rendered']"
         },
 
-        "mode": {
-            "args": ["state.mode"],
-            "compute": function (mode) {
-                return modes[mode];
-            }
-        },
+        "mode": {"<-": "modes[state.mode]"},
 
-        "options.font": {
-            "args": ["state.font"],
-            "compute": function (font) {
-                return fonts[font];
-            }
-        },
+        "options.font": {"<-": "fonts[state.font]"},
+
+        "options.language": {"<-": "state.language"},
 
         "element.innerHTML": {
             "args": ["options.font", "mode", "input", "options"],
@@ -81,7 +75,8 @@ function TengwarComponent(element, state) {
 Array.prototype.forEach.call($$(".dynamic-tengwar"), function (element) {
     TengwarComponent(element, {
         font: element.dataset.font,
-        mode: element.dataset.mode.split(" ")[0]
+        mode: (element.dataset.mode || "").split(" ")[0],
+        language: element.dataset.lang || "unknown"
     });
 });
 
@@ -90,7 +85,7 @@ function Button(element, state) {
         event.stopPropagation();
         event.preventDefault();
         state.options.sort();
-        window.location = "index.html?" + QS.stringify(state);
+        window.location = "./?" + QS.stringify(state);
     });
 }
 
@@ -110,13 +105,13 @@ var states = {};
         TengwarComponent(element, state);
     });
 
-    Bindings.create(null, {
+    Bindings.defineBindings({
         parmaite: document.getElementById(mode + "-font-parmaite"),
         annatar: document.getElementById(mode + "-font-annatar"),
         state: state
     }, {
-        "state.font = 'parmaite'": {"<->": "parmaite.checked"},
-        "state.font = 'annatar'": {"<->": "annatar.checked"}
+        "state.font == 'parmaite'": {"<->": "parmaite.checked"},
+        "state.font == 'annatar'": {"<->": "annatar.checked"}
     });
 
     Button(document.getElementById(mode + "-button"), state);
@@ -150,7 +145,16 @@ Button(document.getElementById("black-speech-button"), {
     q: document.getElementById("black-speech-tengwar").dataset.tengwar,
     font: "annatar",
     mode: "general-use",
-    options: ["black-speech"]
+    language: "blackSpeech",
+    options: []
+});
+
+Button(document.getElementById("english-general-use-button"), {
+    q: document.getElementById("english-general-use-tengwar").dataset.tengwar,
+    font: "parmaite",
+    mode: "general-use",
+    language: "english",
+    options: []
 });
 
 Button(document.getElementById("namarie-button"), {
@@ -260,7 +264,7 @@ Button(document.getElementById("namarie-button"), {
     }
 
 ].forEach(function (flag) {
-    Bindings.create(null, {
+    Bindings.defineBindings({
         option: flag.option,
         off: $("#" + flag.off),
         on: $("#" + flag.on),
@@ -278,7 +282,7 @@ Button(document.getElementById("namarie-button"), {
 });
 
 // special multi-way binding for treatment of H
-Bindings.create(null, {
+Bindings.defineBindings({
     halla: $("#classical-period-halla"),
     aha: $("#classical-period-aha"),
     hyarmen: $("#classical-period-hyarmen"),
index b119b95..264ee10 100644 (file)
@@ -1,27 +1,27 @@
 {
-    "name": "tengwar-editor",
-    "version": "0.0.0",
-    "dependencies": {
-        "frb": "0.0.x",
-        "mr": "0.0.x",
-        "qs": "0.1.x",
-        "tengwar": "0.1.x"
+  "name": "tengwar-editor",
+  "version": "0.0.0",
+  "dependencies": {
+    "frb": "~0.2.16",
+    "qs": "0.5.x",
+    "tengwar": "0.1.x",
+    "mr": "~0.14.2"
+  },
+  "bugs": {
+    "mail": "kris@cixar.com",
+    "web": "http://github.com/kriskowal/tengwarjs/issues"
+  },
+  "licenses": [
+    {
+      "type": "MIT",
+      "url": "http://github.com/kriskowal/tengwarjs/raw/master/LICENSE"
     },
-    "bugs": {
-        "mail": "kris@cixar.com",
-        "web": "http://github.com/kriskowal/tengwarjs/issues"
-    },
-    "licenses": [
-        {
-            "type": "MIT",
-            "url": "http://github.com/kriskowal/tengwarjs/raw/master/LICENSE"
-        },
-        {
-            "url": "http://github.com/kriskowal/tengwarjs/raw/master/tengwar-annatar/tngandoc.pdf"
-        }
-    ],
-    "repository": {
-        "type": "git",
-        "url": "http://github.com/kriskowal/tengwarjs.git"
+    {
+      "url": "http://github.com/kriskowal/tengwarjs/raw/master/tengwar-annatar/tngandoc.pdf"
     }
+  ],
+  "repository": {
+    "type": "git",
+    "url": "http://github.com/kriskowal/tengwarjs.git"
+  }
 }
index eaa6b72..124a4e2 100644 (file)
@@ -13,6 +13,10 @@ var defaults = {};
 exports.makeOptions = makeOptions;
 function makeOptions(options) {
     options = options || defaults;
+    // legacy
+    if (options.blackSpeech) {
+        options.language = "blackSpeech";
+    }
     return {
         font: options.font || TengwarAnnatar,
         block: options.block,
@@ -25,19 +29,21 @@ function makeOptions(options) {
         // or below.
         // false: by default, place a tilde above doubled nasals.
         // true: place the tilde below doubled nasals.
-        reverseCurls: options.reverseCurls || options.blackSpeech,
+        reverseCurls: options.reverseCurls || options.language === "blackSpeech",
         // false: by default, o is forward, u is backward
         // true: o is backward, u is forward
         swapDotSlash: options.swapDotSlash,
         // false: by default, e is a slash, i is a dot
         // true: e is a dot, i is a slash
-        medialOre: options.medialOre || options.blackSpeech,
+        medialOre: options.medialOre || options.language === "blackSpeech",
         // false: by default, ore only appears in final position
         // true: ore also appears before consonants, as in the ring inscription
-        blackSpeech: options.blackSpeech,
-        // false: sh is harma, gh is unque
-        // true: sh is calma-extended, gh is ungwe-extended, as in the ring
-        // inscription
+        language: options.language,
+        // by default, no change
+        // "english": final e implicitly silent
+        // "black speech": sh is calma-extended, gh is ungwe-extended, as in
+        // the ring inscription
+        // not "blackSpeech": sh is harma, gh is unque
         noAchLaut: options.noAchLaut,
         // false: "ch" is interpreted as ach-laut, "cc" as "ch" as in "chew"
         // true: "ch" is interpreted as "ch" as in chew
@@ -76,10 +82,27 @@ function parseWord(callback, options) {
     var font = options.font;
     var makeColumn = font.makeColumn;
     return scanWord(function (word) {
-        if (book[word]) {
+        if (options.language === "english" && word === "of") {
+            return function (character) {
+                var of = Notation.decodeWord(englishBook[word], makeColumn);
+                if (character === " ") {
+                    return scanWord(function (word, rewind) {
+                        if (word === "the") {
+                            return callback(Notation.decodeWord(englishBook["of the"], makeColumn));
+                        } else {
+                            return rewind(callback(of)(character));
+                        }
+                    });
+                } else {
+                    return callback(of)(character);
+                }
+            };
+        } else if (options.language === "english" && englishBook[word]) {
+            return callback(Notation.decodeWord(englishBook[word], makeColumn));
+        } else if (book[word]) {
             return callback(Notation.decodeWord(book[word], makeColumn));
         } else {
-            return callback(parseWordPiecewise(word, options));
+            return callback(parseWordPiecewise(word, word.length, options));
         }
     }, options);
 }
@@ -93,42 +116,60 @@ var book = {
     "noldor": "nwalme;lambe:o;ando;ore:o"
 };
 
-function scanWord(callback, options, word) {
+var englishBook = {
+    "of": "umbar-extended",
+    "of'": "umbar-extended:u",
+    "of the": "umbar-extended:tilde-below",
+    "of'the": "umbar-extended ando-extended",
+    "the": "ando-extended",
+    "the'": "ando-extended:i-below",
+    "and": "ando:tilde-above",
+    "and'": "ando:tilde-above,i-below",
+    "we": "vala:y"
+};
+
+function scanWord(callback, options, word, rewind) {
     word = word || "";
+    rewind = rewind || function (state) {
+        return state;
+    };
     return function (character) {
         if (Parser.isBreak(character)) {
-            return callback(word)(character);
+            return callback(word, rewind)(character);
         } else {
-            return scanWord(callback, options, word + character);
+            return scanWord(callback, options, word + character, function (state) {
+                return rewind(state)(character);
+            });
         }
     };
 }
 
-var parseWordPiecewise = Parser.makeParser(function (callback, options) {
-    return parseWordTail(callback, options, []);
+var parseWordPiecewise = Parser.makeParser(function (callback, length, options) {
+    return parseWordTail(callback, length, options, []);
 });
 
-function parseWordTail(callback, options, columns, previous) {
+function parseWordTail(callback, length, options, columns, previous) {
     return parseColumn(function (moreColumns) {
         if (!moreColumns.length) {
             return callback(columns);
         } else {
             return parseWordTail(
                 callback,
+                length,
                 options,
                 columns.concat(moreColumns),
                 moreColumns[moreColumns.length - 1] // previous
             );
         }
-    }, options, previous);
+    }, length, options, previous);
 }
 
-function parseColumn(callback, options, previous) {
+function parseColumn(callback, length, options, previous) {
     var font = options.font;
     var makeColumn = font.makeColumn;
 
     return parseTehta(function (tehta) {
-        return parseTengwa(function (column) {
+        return parseTengwa(function (column, tehta) {
             if (column) {
                 if (tehta) {
                     if (options.reverseCurls) {
@@ -147,7 +188,7 @@ function parseColumn(callback, options, previous) {
                         column.addAbove(tehta);
                         return parseTengwaAnnotations(function (column) {
                             return callback([column]);
-                        }, column);
+                        }, column, length, options);
                     } else {
                         // some tengwar inherently lack space above them
                         // and cannot be reversed to make room.
@@ -157,12 +198,12 @@ function parseColumn(callback, options, previous) {
                         // then follow up with this tengwa.
                         return parseTengwaAnnotations(function (column) {
                             return callback([makeCarrier(tehta, options), column]);
-                        }, column);
+                        }, column, length, options);
                     }
                 } else {
                     return parseTengwaAnnotations(function (column) {
                         return callback([column]);
-                    }, column);
+                    }, column, length, options);
                 }
             } else if (tehta) {
                 if (options.reverseCurls) {
@@ -173,7 +214,7 @@ function parseColumn(callback, options, previous) {
                 }
                 return parseTengwaAnnotations(function (carrier) {
                     return callback([carrier]);
-                }, makeCarrier(tehta, options));
+                }, makeCarrier(tehta, options), length, options);
             } else {
                 return function (character) {
                     if (Parser.isBreak(character)) {
@@ -183,12 +224,19 @@ function parseColumn(callback, options, previous) {
                     } else if (punctuation[character]) {
                         return callback([makeColumn(punctuation[character])]);
                     } else {
-                        return callback([makeColumn("ure").addError("Cannot transcribe " + JSON.stringify(character) + " in General Use Mode")]);
+                        return callback([
+                            makeColumn("ure")
+                            .addError(
+                                "Cannot transcribe " +
+                                JSON.stringify(character) +
+                                " in General Use Mode"
+                            )
+                        ]);
                     }
                 };
             }
         }, options, tehta);
-    });
+    }, options);
 
 }
 
@@ -204,8 +252,12 @@ function makeCarrier(tehta, options) {
     }
 }
 
-function parseTehta(callback) {
+function parseTehta(callback, options) {
     return function (character) {
+        var firstCharacter = character;
+        if (character === "ë" && options.language !== "english") {
+            character = "e";
+        }
         if (character === "") {
             return callback();
         } else if (lengthenableVowels.indexOf(character) !== -1) {
@@ -228,7 +280,7 @@ var lengthenableVowels = "aeiou";
 var longerVowels = {"a": "á", "e": "é", "i": "í", "o": "ó", "u": "ú"};
 var nonLengthenableVowels = "aeióú";
 var tehtarThatCanBeAddedAbove = "aeiouóú";
-var vowels = "aeiouáéíóú";
+var vowels = "aeëiouáéíóú";
 var shorterVowels = {"á": "a", "é": "e", "í": "i", "ó": "o", "ú": "u"};
 var reverseCurls = {"o": "u", "u": "o", "ó": "ú", "ú": "ó"};
 var swapDotSlash = {"i": "e", "e": "i"};
@@ -245,68 +297,68 @@ function parseTengwa(callback, options, tehta) {
             return function (character) {
                 if (character === "n") { // nn
                     if (options.doubleNasalsWithTildeBelow) {
-                        return callback(makeColumn("numen").addTildeBelow());
+                        return callback(makeColumn("numen").addTildeBelow(), tehta);
                     } else {
-                        return callback(makeColumn("numen").addTildeAbove());
+                        return callback(makeColumn("numen").addTildeAbove(), tehta);
                     }
                 } else if (character === "t") { // nt
                     return function (character) {
                         if (character === "h") { // nth
-                            return callback(makeColumn("thule").addTildeAbove());
+                            return callback(makeColumn("thule").addTildeAbove(), tehta);
                         } else { // nt.
-                            return callback(makeColumn("tinco").addTildeAbove())(character);
+                            return callback(makeColumn("tinco").addTildeAbove(), tehta)(character);
                         }
                     };
                 } else if (character === "d") { // nd
-                    return callback(makeColumn("ando").addTildeAbove());
+                    return callback(makeColumn("ando").addTildeAbove(), tehta);
                 } else if (character === "c") { // nc -> ñc
-                    return callback(makeColumn("quesse").addTildeAbove());
+                    return callback(makeColumn("quesse").addTildeAbove(), tehta);
                 } else if (character === "g") { // ng -> ñg
-                    return callback(makeColumn("ungwe").addTildeAbove());
+                    return callback(makeColumn("ungwe").addTildeAbove(), tehta);
                 } else if (character === "j") { // nj
-                    return callback(makeColumn("anca").addTildeAbove());
+                    return callback(makeColumn("anca").addTildeAbove(), tehta);
                 } else if (character === "f") { // nf -> nv
-                    return callback(makeColumn("numen"))("v");
+                    return callback(makeColumn("numen"), tehta)("v");
                 } else  if (character === "w") { // nw -> ñw
                     return function (character) {
                         if (character === "a") { // nwa
                             return function (character) { // nwal
                                 if (character === "l") {
-                                    return callback(makeColumn("nwalme").addAbove("w"))("a")(character);
+                                    return callback(makeColumn("nwalme").addAbove("w"), tehta)("a")(character);
                                 } else { // nwa.
-                                    return callback(makeColumn("numen").addAbove("w"))("a")(character);
+                                    return callback(makeColumn("numen").addAbove("w"), tehta)("a")(character);
                                 }
                             };
                         } else if (character === "nw'") { // nw' prime -> ñw
-                            return callback(makeColumn("nwalme").addAbove("w"));
+                            return callback(makeColumn("nwalme").addAbove("w"), tehta);
                         } else { // nw.
-                            return callback(makeColumn("numen").addAbove("w"))(character);
+                            return callback(makeColumn("numen").addAbove("w"), tehta)(character);
                         }
                     };
                 } else { // n.
-                    return callback(makeColumn("numen"))(character);
+                    return callback(makeColumn("numen"), tehta)(character);
                 }
             };
         } else if (character === "m") { // m
             return function (character) {
                 if (character === "m") { // mm
                     if (options.doubleNasalsWithTildeBelow) {
-                        return callback(makeColumn("malta").addTildeBelow());
+                        return callback(makeColumn("malta").addTildeBelow(), tehta);
                     } else {
-                        return callback(makeColumn("malta").addTildeAbove());
+                        return callback(makeColumn("malta").addTildeAbove(), tehta);
                     }
                 } else if (character === "p") { // mp
                     // mph is simplified to mf using the normalizer
-                    return callback(makeColumn("parma").addTildeAbove());
+                    return callback(makeColumn("parma").addTildeAbove(), tehta);
                 } else if (character === "b") { // mb
                     // mbh is simplified to mf using the normalizer
-                    return callback(makeColumn("umbar").addTildeAbove());
+                    return callback(makeColumn("umbar").addTildeAbove(), tehta);
                 } else if (character === "f") { // mf
-                    return callback(makeColumn("formen").addTildeAbove());
+                    return callback(makeColumn("formen").addTildeAbove(), tehta);
                 } else if (character === "v") { // mv
-                    return callback(makeColumn("ampa").addTildeAbove());
+                    return callback(makeColumn("ampa").addTildeAbove(), tehta);
                 } else { // m.
-                    return callback(makeColumn("malta"))(character);
+                    return callback(makeColumn("malta"), tehta)(character);
                 }
             };
         } else if (character === "ñ") { // ñ
@@ -314,40 +366,40 @@ function parseTengwa(callback, options, tehta) {
                 // ññ does not exist to the best of my knowledge
                 // ñw is handled naturally by following w
                 if (character === "c") { // ñc
-                    return callback(makeColumn("quesse").addTildeAbove());
+                    return callback(makeColumn("quesse").addTildeAbove(), tehta);
                 } else if (character === "g") { // ñg
-                    return callback(makeColumn("ungwe").addTildeAbove());
+                    return callback(makeColumn("ungwe").addTildeAbove(), tehta);
                 } else { // ñ.
-                    return callback(makeColumn("nwalme"))(character);
+                    return callback(makeColumn("nwalme"), tehta)(character);
                 }
             };
         } else if (character === "t") { // t
             return function (character) {
                 if (character === "t") { // tt
-                    return callback(makeColumn("tinco").addTildeBelow());
+                    return callback(makeColumn("tinco").addTildeBelow(), tehta);
                 } else if (character === "h") { // th
-                    return callback(makeColumn("thule"));
+                    return callback(makeColumn("thule"), tehta);
                 } else if (character === "c") { // tc
                     return function (character) {
                         if (character === "h") { // tch -> tinco calma
-                            return callback(makeColumn("tinco"))("c")("h")("'");
+                            return callback(makeColumn("tinco"), tehta)("c")("h")("'");
                         } else {
-                            return callback(makeColumn("tinco"))("c")(character);
+                            return callback(makeColumn("tinco"), tehta)("c")(character);
                         }
                     };
                 } else if (character === "s" && options.tsdz) { // ts
-                    return callback(makeColumn("calma"));
+                    return callback(makeColumn("calma"), tehta);
                 } else { // t.
-                    return callback(makeColumn("tinco"))(character);
+                    return callback(makeColumn("tinco"), tehta)(character);
                 }
             };
         } else if (character === "p") { // p
             return function (character) {
                 // ph is simplified to f by the normalizer
                 if (character === "p") { // pp
-                    return callback(makeColumn("parma").addTildeBelow());
+                    return callback(makeColumn("parma").addTildeBelow(), tehta);
                 } else { // p.
-                    return callback(makeColumn("parma"))(character);
+                    return callback(makeColumn("parma"), tehta)(character);
                 }
             };
         } else if (character === "c") { // c
@@ -355,68 +407,68 @@ function parseTengwa(callback, options, tehta) {
                 // cw should be handled either by following-w or a subsequent
                 // vala
                 if (character === "c") { // ch as in charm
-                    return callback(makeColumn("calma"));
+                    return callback(makeColumn("calma"), tehta);
                 } else if (character === "h") { // ch, ach-laut, as in bach
                     return Parser.countPrimes(function (primes) {
                         if (options.noAchLaut && !primes) {
-                            return callback(makeColumn("calma")); // ch as in charm
+                            return callback(makeColumn("calma"), tehta); // ch as in charm
                         } else {
-                            return callback(makeColumn("hwesta")); // ch as in bach
+                            return callback(makeColumn("hwesta"), tehta); // ch as in bach
                         }
                     });
                 } else { // c.
-                    return callback(makeColumn("quesse"))(character);
+                    return callback(makeColumn("quesse"), tehta)(character);
                 }
             };
         } else if (character === "d") {
             return function (character) {
                 if (character === "d") { // dd
-                    return callback(makeColumn("ando").addTildeBelow());
+                    return callback(makeColumn("ando").addTildeBelow(), tehta);
                 } else if (character === "j") { // dj
-                    return callback(makeColumn("anga"));
+                    return callback(makeColumn("anga"), tehta);
                 } else if (character === "z" && options.tsdz) { // dz
-                    return callback(makeColumn("anga"));
+                    return callback(makeColumn("anga"), tehta);
                 } else if (character === "h") { // dh
-                    return callback(makeColumn("anto"));
+                    return callback(makeColumn("anto"), tehta);
                 } else { // d.
-                    return callback(makeColumn("ando"))(character);
+                    return callback(makeColumn("ando"), tehta)(character);
                 }
             };
         } else if (character === "b") { // b
             return function (character) {
                 // bh is simplified to v by the normalizer
                 if (character === "b") { // bb
-                    return callback(makeColumn("umbar").addTildeBelow());
+                    return callback(makeColumn("umbar").addTildeBelow(), tehta);
                 } else { // b.
-                    return callback(makeColumn("umbar"))(character);
+                    return callback(makeColumn("umbar"), tehta)(character);
                 }
             };
         } else if (character === "g") { // g
             return function (character) {
                 if (character === "g") { // gg
-                    return callback(makeColumn("ungwe").addTildeBelow());
+                    return callback(makeColumn("ungwe").addTildeBelow(), tehta);
                 } else if (character === "h") { // gh
-                    if (options.blackSpeech) {
-                        return callback(makeColumn("ungwe-extended"));
+                    if (options.language === "blackSpeech") {
+                        return callback(makeColumn("ungwe-extended"), tehta);
                     } else {
-                        return callback(makeColumn("unque"));
+                        return callback(makeColumn("unque"), tehta);
                     }
                 } else { // g.
-                    return callback(makeColumn("ungwe"))(character);
+                    return callback(makeColumn("ungwe"), tehta)(character);
                 }
             };
         } else if (character === "f") { // f
             return function (character) {
                 if (character === "f") { // ff
-                    return callback(makeColumn("formen").addTildeBelow());
+                    return callback(makeColumn("formen").addTildeBelow(), tehta);
                 } else { // f.
-                    return callback(makeColumn("formen"))(character);
+                    return callback(makeColumn("formen"), tehta)(character);
                 }
             };
         } else if (character === "v") { // v
-            return callback(makeColumn("ampa"));
+            return callback(makeColumn("ampa"), tehta);
         } else if (character === "j") { // j
-            return callback(makeColumn("anca"));
+            return callback(makeColumn("anca"), tehta);
         } else if (character === "s") { // s
             return function (character) {
                 if (character === "s") { // ss
@@ -426,13 +478,13 @@ function parseTengwa(callback, options, tehta) {
                         if (primes > 1) {
                             column.addError("Silme does not have this many alternate forms.");
                         }
-                        return callback(column);
+                        return callback(column, tehta);
                     });
                 } else if (character === "h") { // sh
-                    if (options.blackSpeech) {
-                        return callback(makeColumn("calma-extended"));
+                    if (options.language === "blackSpeech") {
+                        return callback(makeColumn("calma-extended"), tehta);
                     } else {
-                        return callback(makeColumn("harma"));
+                        return callback(makeColumn("harma"), tehta);
                     }
                 } else { // s.
                     return Parser.countPrimes(function (primes) {
@@ -441,7 +493,7 @@ function parseTengwa(callback, options, tehta) {
                         if (primes > 1) {
                             column.addError("Silme does not have this many alternate forms.");
                         }
-                        return callback(column);
+                        return callback(column, tehta);
                     })(character);
                 }
             };
@@ -454,7 +506,7 @@ function parseTengwa(callback, options, tehta) {
                         if (primes > 1) {
                             column.addError("Esse does not have this many alternate forms.");
                         }
-                        return callback(column);
+                        return callback(column, tehta);
                     });
                 } else { // z.
                     return Parser.countPrimes(function (primes) {
@@ -463,76 +515,91 @@ function parseTengwa(callback, options, tehta) {
                         if (primes > 1) {
                             column.addError("Silme does not have this many alternate forms.");
                         }
-                        return callback(column);
+                        return callback(column, tehta);
                     })(character);
                 }
             };
         } else if (character === "h") { // h
             return function (character) {
                 if (character === "w") { // hw
-                    return callback(makeColumn("hwesta-sindarinwa"));
+                    return callback(makeColumn("hwesta-sindarinwa"), tehta);
                 } else { // h.
-                    return callback(makeColumn("hyarmen"))(character);
+                    return callback(makeColumn("hyarmen"), tehta)(character);
                 }
             };
         } else if (character === "r") { // r
             return function (character) {
                 if (character === "r") { // rr
-                    return callback(makeColumn("romen").addTildeBelow());
+                    return callback(makeColumn("romen").addTildeBelow(), tehta);
                 } else if (character === "h") { // rh
-                    return callback(makeColumn("arda"));
+                    return callback(makeColumn("arda"), tehta);
                 } else if (
                     Parser.isFinal(character) || (
                         options.medialOre &&
                         vowels.indexOf(character) === -1
                     )
                 ) { // r final (optionally r before consonant)
-                    return callback(makeColumn("ore"))(character);
+                    return callback(makeColumn("ore"), tehta)(character);
                 } else { // r.
-                    return callback(makeColumn("romen"))(character);
+                    return callback(makeColumn("romen"), tehta)(character);
                 }
             };
         } else if (character === "l") {
             return function (character) {
                 if (character === "l") { // ll
-                    return callback(makeColumn("lambe").addTildeBelow());
+                    return callback(makeColumn("lambe").addTildeBelow(), tehta);
                 } else if (character === "h") { // lh
-                    return callback(makeColumn("alda"));
+                    return callback(makeColumn("alda"), tehta);
                 } else { // l.
-                    return callback(makeColumn("lambe"))(character);
+                    return callback(makeColumn("lambe"), tehta)(character);
                 }
             };
         } else if (character === "i") { // i
-            return callback(makeColumn("anna"));
+            return callback(makeColumn("anna"), tehta);
         } else if (character === "u") { // u
-            return callback(makeColumn("vala"));
+            return callback(makeColumn("vala"), tehta);
         } else if (character === "w") { // w
             return function (character) {
                 if (character === "h") { // wh
-                    return callback(makeColumn("hwesta-sindarinwa"));
+                    return callback(makeColumn("hwesta-sindarinwa"), tehta);
                 } else { // w.
-                    return callback(makeColumn("vala"))(character);
+                    return callback(makeColumn("vala"), tehta)(character);
                 }
             };
         } else if (character === "e" && (!tehta || tehta === "a")) { // ae or e after consonants
-            return callback(makeColumn("yanta"));
+            return callback(makeColumn("yanta"), tehta);
+        } else if (character === "ë") { // if "ë" makes it this far, it's a diaresis for english
+            return callback(makeColumn("short-carrier").addAbove("e"));
         } else if (character === "y") {
-            return callback(makeColumn("wilya").addBelow("y"));
-            // TODO consider alt: return callback(makeColumn("long-carrier").addAbove("i"));
+            return Parser.countPrimes(function (primes) {
+                if (primes === 0) {
+                    return callback(makeColumn("wilya").addBelow("y"), tehta);
+                } else if (primes === 1) {
+                    return callback(makeColumn("long-carrier").addAbove("i"), tehta);
+                } else {
+                    return callback(makeColumn("ure").addError("Consonantal Y only has one variation"));
+                }
+            });
         } else if (shorterVowels[character]) {
-            return callback(makeCarrier(character, options).addAbove(shorterVowels[character]));
+            return callback(makeCarrier(character, options).addAbove(shorterVowels[character]), tehta);
+        } else if (character === "'" && options.language === "english" && tehta === "e") {
+            // final e' in english should be equivalent to diaresis
+            return callback(makeColumn("short-carrier").addAbove("e"));
+        } else if (character === "" && options.language === "english" && tehta === "e") {
+            // tehta deliberately consumed in this one case, not passed forward
+            return callback(makeColumn("short-carrier").addBelow("i-below"))(character);
         } else {
-            return callback()(character);
+            return callback(null, tehta)(character);
         }
     };
 }
 
 exports.parseTengwaAnnotations = parseTengwaAnnotations;
-function parseTengwaAnnotations(callback, column) {
+function parseTengwaAnnotations(callback, column, length, options) {
     return parseFollowingAbove(function (column) {
         return parseFollowingBelow(function (column) {
             return parseFollowing(callback, column);
-        }, column);
+        }, column, length, options);
     }, column);
 }
 
@@ -552,20 +619,36 @@ function parseFollowingAbove(callback, column) {
     }
 }
 
-function parseFollowingBelow(callback, column) {
+function parseFollowingBelow(callback, column, length, options) {
     return function (character) {
+        if (character === "ë" && options.language !== "english") {
+            character = "e";
+        }
         if (character === "y" && column.canAddBelow("y")) {
             return callback(column.addBelow("y"));
         } else if (character === "e" && column.canAddBelow("i-below")) {
             return Parser.countPrimes(function (primes) {
-                if (primes === 0) {
-                    return callback(column)(character);
-                } else {
-                    if (primes > 1) {
-                        column.addError("Following E has only one variation.");
+                return function (character) {
+                    if (Parser.isFinal(character) && options.language === "english" && length > 2) {
+                        if (primes === 0) {
+                            return callback(column.addBelow("i-below"))(character);
+                        } else {
+                            if (primes > 1) {
+                                column.addError("Following E has only one variation.");
+                            }
+                            return callback(column)("e")(character);
+                        }
+                    } else {
+                        if (primes === 0) {
+                            return callback(column)("e")(character);
+                        } else {
+                            if (primes > 1) {
+                                column.addError("Following E has only one variation.");
+                            }
+                            return callback(column.addBelow("i-below"))(character);
+                        }
                     }
-                    return callback(column.addBelow("i-below"));
-                }
+                };
             });
         } else {
             return callback(column)(character);
@@ -606,7 +689,7 @@ function parseFollowing(callback, column) {
                                 }
                                 return state;
                             }
-                            return callback(column);
+                            return callback(column)(character);
                         } else {
                             return rewind(callback(column)("s"))(character);
                         }
index d319229..4513428 100644 (file)
@@ -54,7 +54,7 @@ function toLowerCase(callback) {
 // the keys of this table are characters and clusters of characters that must
 // be simplified to the corresponding values before pumping them into an
 // adapted parser.  The adapted parser therefore only needs to handle the
-// normal phoneitc form of the cluster.
+// normal phonetic form of the cluster.
 var table = {
     "k": "c",
     "x": "cs",
@@ -64,7 +64,6 @@ var table = {
     "ph": "f",
     "b": "b",
     "bh": "v",
-    "ë": "e",
     "â": "á",
     "ê": "é",
     "î": "í",
index dea342a..beb702d 100644 (file)
@@ -53,7 +53,7 @@ function decodeWord(word, makeColumn) {
             if (tehta === "tilde-above") {
                 result.addTildeAbove();
             } else if (tehta === "tilde-below") {
-                result.addBarBelow();
+                result.addTildeBelow();
             } else if (tehta === "y") {
                 result.addBelow("y");
             } else if (
index e1ba2c5..830b3ed 100644 (file)
@@ -1,29 +1,29 @@
 {
-    "name": "tengwar",
-    "version": "0.1.2",
-    "homepage": "http://3rin.gs/tengwar",
-    "author": "Kris Kowal <kris@cixar.com> (http://github.com/kriskowal/)",
-    "bugs": {
-        "mail": "kris@cixar.com",
-        "web": "http://github.com/kriskowal/tengwarjs/issues"
+  "name": "tengwar",
+  "version": "0.1.2",
+  "homepage": "http://3rin.gs/tengwar",
+  "author": "Kris Kowal <kris@cixar.com> (http://github.com/kriskowal/)",
+  "bugs": {
+    "mail": "kris@cixar.com",
+    "url": "http://github.com/kriskowal/tengwarjs/issues"
+  },
+  "licenses": [
+    {
+      "type": "MIT",
+      "url": "http://github.com/kriskowal/tengwarjs/raw/master/LICENSE"
     },
-    "licenses": [
-        {
-            "type": "MIT",
-            "url": "http://github.com/kriskowal/tengwarjs/raw/master/LICENSE"
-        },
-        {
-            "url": "http://github.com/kriskowal/tengwarjs/raw/master/tengwar-annatar/tngandoc.pdf"
-        }
-    ],
-    "repository": {
-        "type": "git",
-        "url": "http://github.com/kriskowal/tengwarjs.git"
-    },
-    "devDependencies": {
-        "jasmine-node": "1.0.x"
-    },
-    "scripts": {
-        "test": "jasmine-node spec"
+    {
+      "url": "http://github.com/kriskowal/tengwarjs/raw/master/tengwar-annatar/tngandoc.pdf"
     }
+  ],
+  "repository": {
+    "type": "git",
+    "url": "http://github.com/kriskowal/tengwarjs.git"
+  },
+  "devDependencies": {
+    "jasmine-node": "~1"
+  },
+  "scripts": {
+    "test": "jasmine-node spec"
+  }
 }
diff --git a/spec/black-speech-spec.js b/spec/black-speech-spec.js
deleted file mode 100644 (file)
index 47047e4..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-
-var GeneralUse = require("../general-use");
-var tests = require("./black-speech");
-
-describe("black speech", function () {
-    Object.keys(tests).forEach(function (input) {
-        it("should encode " + input, function () {
-            expect(GeneralUse.encode(input, {
-                blackSpeech: true
-            })).toEqual(tests[input]);
-        });
-    });
-});
-
diff --git a/spec/black-speech.js b/spec/black-speech.js
deleted file mode 100644 (file)
index 7d3e2ea..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-module.exports = {
-    "ash": "calma-extended:a",
-    "nazg": "numen;esse-nuquerna:a;ungwe",
-    "durbatulûk": "ando;ore:o;umbar;tinco:a;lambe:o;quesse:ó", // swap o and u, medial ore before consonant
-    "gimbatul": "ungwe;umbar:i,tilde-above;tinco:a;lambe:o",
-    "thrakatulûk": "thule;romen;quesse:a;tinco:a;lambe:o;quesse:ó",
-    "agh": "ungwe-extended:a",
-    "burzumishi": "umbar;ore:o;esse;malta:o;calma-extended:i;short-carrier:i",
-    "krimpatul": "quesse;romen;parma:i,tilde-above;tinco:a;lambe:o"
-};
index c577700..612613b 100644 (file)
@@ -3,9 +3,17 @@ var GeneralUse = require("../general-use");
 var tests = require("./general-use");
 
 describe("general use", function () {
-    Object.keys(tests).forEach(function (input) {
-        it("should encode " + input, function () {
-            expect(GeneralUse.encode(input)).toEqual(tests[input]);
+    Object.keys(tests).forEach(function (language) {
+        var languageTests = tests[language];
+        describe(language, function () {
+            Object.keys(languageTests).forEach(function (input) {
+                it("should encode " + input, function () {
+                    var expected = languageTests[input];
+                    expect(GeneralUse.encode(input, {
+                        language: language
+                    })).toEqual(expected);
+                });
+            });
         });
     });
 });
index 3fe6359..38d59fd 100644 (file)
+
 module.exports = {
-    // sindarin (sorted)
-    "ainur": "anna:a;numen;ore:u",
-    "aldost": "lambe:a;ando;silme-nuquerna:o;tinco",
-    "amon sûl": "malta:a;numen:o silme;lambe:ú",
-    "aragorn": "romen:a;ungwe:a;romen:o;numen",
-    "Aragorn Arathorn:\nTelcontar, Elessar": "romen:a;ungwe:a;romen:o;numen romen:a;thule:a;romen:o;numen;comma\ntinco;lambe:e;quesse;tinco:o,tilde-above;ore:a;comma lambe:e;silme-nuquerna:e,tilde-below;ore:a",
-    "atto": "tinco:a,tilde-below;short-carrier:o",
-    "baranduiniant": "umbar;romen:a;ando:a,tilde-above;anna:u;yanta;anto:a,tilde-above",
-    "dagor bragolach": "ando;ungwe:a;ore:o umbar;romen;ungwe:a;lambe:o;hwesta:a",
-    "galadhrim": "ungwe;lambe:a;anto:a;romen;malta:i",
-    "galadriel": "ungwe;lambe:a;ando:a;romen;short-carrier:i;lambe:e",
-    "gandalf": "ungwe;ando:a,tilde-above;lambe:a;formen",
-    "glorfindel": "ungwe;lambe;romen:o;formen;ando:i,tilde-above;lambe:e",
-    "gwaith iaur arnor": "ungwe:w;anna:a;thule yanta;vala:a;ore romen:a;numen;ore:o",
-    "gwathló": "ungwe:w;thule:a;lambe;long-carrier:o",
-    "hwesta sindarinwa": "hwesta-sindarinwa;silme-nuquerna:e;tinco;short-carrier:a silme;ando:i,tilde-above;romen:a;short-carrier:i;numen:w;short-carrier:a",
-    "iant": "yanta;tinco:a,tilde-above",
-    "iaur": "yanta;vala:a;ore",
-    "isildur": "silme-nuquerna:i;lambe:i;ando;ore:u",
-    "lhûn": "alda;numen:ú",
-    "lothlórien": "lambe;thule:o;lambe;romen:ó;short-carrier:i;numen:e",
-    "mae govannen": "malta;yanta:a ungwe;ampa:o;numen:a,tilde-above;numen:e",
-    "mellon": "malta;lambe:e,tilde-below;numen:o",
-    "mordor": "malta;romen:o;ando;ore:o",
-    "moria": "malta;romen:o;short-carrier:i;short-carrier:a",
-    "noldor": "nwalme;lambe:o;ando;ore:o",
-    "periannath": "parma;romen:e;short-carrier:i;numen:a,tilde-above;thule:a",
-    "rhûn": "arda;numen:ú",
-    "tyelpe": "tinco:y;lambe:e;parma;short-carrier:e",
-    "varda": "ampa;romen:a;ando;short-carrier:a",
-    "á": "wilya:a",
-    "ñoldor": "nwalme;lambe:o;ando;ore:o",
-
-    // quenya (improper mode) (sorted)
-    "ardalambion": "romen:a;ando;lambe:a;umbar:a,tilde-above;short-carrier:i;numen:o",
-    "helcaraxë": "hyarmen;lambe:e;quesse;romen:a;quesse:a,s;short-carrier:e",
-    "hyarmen": "hyarmen:y;romen:a;malta;numen:e",
-    "istari": "silme-nuquerna:i;tinco;romen:a;short-carrier:i",
-    "sinome maruvan": "silme;numen:i;malta:o;short-carrier:e malta;romen:a;ampa:u;numen:a",
-    "telperion": "tinco;lambe:e;parma;romen:e;short-carrier:i;numen:o",
-    // TODO alt? "yuldar": "long-carrier:i;lambe:u;ando;ore:a",
-    "yuldar": "wilya:y;lambe:u;ando;ore:a",
-
-    // english (appropriate mode) (sorted)
-    "hobbits": "hyarmen;umbar:o,tilde-below;tinco:i,s-final",
-    "hobbits'": "hyarmen;umbar:o,tilde-below;tinco:i,s-inverse",
-    "hobbits''": "hyarmen;umbar:o,tilde-below;tinco:i,s-extended",
-    "hobbits'''": "hyarmen;umbar:o,tilde-below;tinco:i,s-flourish",
-
-    // old english
-    "írensaga": "long-carrier:i;romen;numen:e;silme;ungwe:a;short-carrier:a",
-
-    // interesting clusters
-    "xx": "quesse:s;quesse:s",
-    "tsts": "tinco;silme;tinco:s-final",
-    "iqs": "quesse:i;vala;silme",
-    "aty": "tinco:a,y",
-    "allys": "lambe:a,y,s-final,tilde-below",
-    "alyssa": "lambe:a,y;silme:tilde-below;short-carrier:a",
-    "ls": "lambe:s-final",
-    "ls'": "lambe:s-flourish",
-
-    // long vowels
-    "á": "wilya:a",
-    "aa": "wilya:a",
-    "é": "long-carrier:e",
-    "ee": "long-carrier:e",
-    "í": "long-carrier:i",
-    "ii": "long-carrier:i",
-    "ó": "long-carrier:o",
-    "oo": "long-carrier:o",
-    "ú": "long-carrier:u",
-    "uu": "long-carrier:u"
+
+    any: {
+
+        // interesting clusters
+        "xx": "quesse:s;quesse:s",
+        "tsts": "tinco;silme;tinco:s-final",
+        "iqs": "quesse:i;vala;silme",
+        "aty": "tinco:a,y",
+        "allys": "lambe:a,y,s-final,tilde-below",
+        "alyssa": "lambe:a,y;silme:tilde-below;short-carrier:a",
+        "ls": "lambe:s-final",
+        "ls'": "lambe:s-flourish",
+
+        // long vowels
+        "á": "wilya:a",
+        "aa": "wilya:a",
+        "é": "long-carrier:e",
+        "ee": "long-carrier:e",
+        "í": "long-carrier:i",
+        "ii": "long-carrier:i",
+        "ó": "long-carrier:o",
+        "oo": "long-carrier:o",
+        "ú": "long-carrier:u",
+        "uu": "long-carrier:u",
+
+        // final-e modification for non-english
+        "cake'": "quesse;quesse:a,i-below",
+
+    },
+
+    english: {
+
+        // english (appropriate mode) (sorted)
+        "cake": "quesse;quesse:a,i-below",
+        "cakes": "quesse;quesse:a;silme-nuquerna:e",
+        "cats.": "quesse;tinco:a,s-final;full-stop", // regression
+        "hobbits": "hyarmen;umbar:o,tilde-below;tinco:i,s-final",
+        "hobbits'": "hyarmen;umbar:o,tilde-below;tinco:i,s-inverse",
+        "hobbits''": "hyarmen;umbar:o,tilde-below;tinco:i,s-extended",
+        "hobbits'''": "hyarmen;umbar:o,tilde-below;tinco:i,s-flourish",
+        "there": "thule;romen:e,i-below",
+        "these": "thule;silme-nuquerna:e;short-carrier:i-below",
+        "these'": "thule;silme-nuquerna:e;short-carrier:e",
+        "finwë": "formen;short-carrier:i;numen:w;short-carrier:e",
+        "finwe": "formen;short-carrier:i;numen:w,i-below", // invalid input
+        "helcaraxë": "hyarmen;lambe:e;quesse;romen:a;quesse:a,s;short-carrier:e",
+        "helcaraxe": "hyarmen;lambe:e;quesse;romen:a;quesse:a,s;short-carrier:i-below", // invalid input
+
+        // abbreviated words
+        "of": "umbar-extended",
+        "the": "ando-extended",
+        "of the": "umbar-extended:tilde-below",
+        "of'the": "umbar-extended ando-extended",
+        "and": "ando:tilde-above",
+        "and'": "ando:i-below,tilde-above"
+
+    },
+
+    oldEnglish: {
+        "írensaga": "long-carrier:i;romen;numen:e;silme;ungwe:a;short-carrier:a"
+    },
+
+    sindarin: {
+        // (sorted)
+        "ainur": "anna:a;numen;ore:u",
+        "aldost": "lambe:a;ando;silme-nuquerna:o;tinco",
+        "amon sûl": "malta:a;numen:o silme;lambe:ú",
+        "aragorn": "romen:a;ungwe:a;romen:o;numen",
+        "Aragorn Arathorn:\nTelcontar, Elessar": "romen:a;ungwe:a;romen:o;numen romen:a;thule:a;romen:o;numen;comma\ntinco;lambe:e;quesse;tinco:o,tilde-above;ore:a;comma lambe:e;silme-nuquerna:e,tilde-below;ore:a",
+        "atto": "tinco:a,tilde-below;short-carrier:o",
+        "baranduiniant": "umbar;romen:a;ando:a,tilde-above;anna:u;yanta;anto:a,tilde-above",
+        "dagor bragolach": "ando;ungwe:a;ore:o umbar;romen;ungwe:a;lambe:o;hwesta:a",
+        "galadhrim": "ungwe;lambe:a;anto:a;romen;malta:i",
+        "galadriel": "ungwe;lambe:a;ando:a;romen;short-carrier:i;lambe:e",
+        "gandalf": "ungwe;ando:a,tilde-above;lambe:a;formen",
+        "glorfindel": "ungwe;lambe;romen:o;formen;ando:i,tilde-above;lambe:e",
+        "gwaith iaur arnor": "ungwe:w;anna:a;thule yanta;vala:a;ore romen:a;numen;ore:o",
+        "gwathló": "ungwe:w;thule:a;lambe;long-carrier:o",
+        "hwesta sindarinwa": "hwesta-sindarinwa;silme-nuquerna:e;tinco;short-carrier:a silme;ando:i,tilde-above;romen:a;short-carrier:i;numen:w;short-carrier:a",
+        "iant": "yanta;tinco:a,tilde-above",
+        "iaur": "yanta;vala:a;ore",
+        "isildur": "silme-nuquerna:i;lambe:i;ando;ore:u",
+        "lhûn": "alda;numen:ú",
+        "lothlórien": "lambe;thule:o;lambe;romen:ó;short-carrier:i;numen:e",
+        "mae govannen": "malta;yanta:a ungwe;ampa:o;numen:a,tilde-above;numen:e",
+        "mellon": "malta;lambe:e,tilde-below;numen:o",
+        "mordor": "malta;romen:o;ando;ore:o",
+        "moria": "malta;romen:o;short-carrier:i;short-carrier:a",
+        "noldor": "nwalme;lambe:o;ando;ore:o",
+        "periannath": "parma;romen:e;short-carrier:i;numen:a,tilde-above;thule:a",
+        "rhûn": "arda;numen:ú",
+        "tyelpe": "tinco:y;lambe:e;parma;short-carrier:e",
+        "varda": "ampa;romen:a;ando;short-carrier:a",
+        "á": "wilya:a",
+        "ñoldor": "nwalme;lambe:o;ando;ore:o",
+    },
+
+    blackSpeech: {
+        // in order of appearance in the ring poem
+        "ash": "calma-extended:a",
+        "nazg": "numen;esse-nuquerna:a;ungwe",
+        "durbatulûk": "ando;ore:o;umbar;tinco:a;lambe:o;quesse:ó", // swap o and u, medial ore before consonant
+        "gimbatul": "ungwe;umbar:i,tilde-above;tinco:a;lambe:o",
+        "thrakatulûk": "thule;romen;quesse:a;tinco:a;lambe:o;quesse:ó",
+        "agh": "ungwe-extended:a",
+        "burzumishi": "umbar;ore:o;esse;malta:o;calma-extended:i;short-carrier:i",
+        "krimpatul": "quesse;romen;parma:i,tilde-above;tinco:a;lambe:o"
+    },
+
+    quenya: {
+        // (improper mode) (sorted)
+        "ardalambion": "romen:a;ando;lambe:a;umbar:a,tilde-above;short-carrier:i;numen:o",
+        "ëa": "short-carrier:e;short-carrier:a",
+        "helcaraxë": "hyarmen;lambe:e;quesse;romen:a;quesse:a,s;short-carrier:e",
+        "hyarmen": "hyarmen:y;romen:a;malta;numen:e",
+        "istari": "silme-nuquerna:i;tinco;romen:a;short-carrier:i",
+        "sinome maruvan": "silme;numen:i;malta:o;short-carrier:e malta;romen:a;ampa:u;numen:a",
+        "telperion": "tinco;lambe:e;parma;romen:e;short-carrier:i;numen:o",
+        "yuldar": "wilya:y;lambe:u;ando;ore:a",
+        "y'uldar": "long-carrier:i;lambe:u;ando;ore:a"
+    }
 
 };
+