First draft of Classical + General Use
authorKris Kowal <kris.kowal@cixar.com>
Thu, 30 Aug 2012 02:49:36 +0000 (19:49 -0700)
committerKris Kowal <kris.kowal@cixar.com>
Thu, 30 Aug 2012 02:49:36 +0000 (19:49 -0700)
13 files changed:
.gitignore [new file with mode: 0644]
classical.js [new file with mode: 0644]
column.js [new file with mode: 0644]
custom-webfont.css [moved from style.css with 65% similarity]
general-use.js [new file with mode: 0644]
index.css
mediawiki/tengwar.php [new file with mode: 0644]
package.json
render.js [new file with mode: 0644]
test.css [new file with mode: 0644]
test.html [new file with mode: 0644]
test.js [new file with mode: 0644]
tests.js

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..3c3629e
--- /dev/null
@@ -0,0 +1 @@
+node_modules
diff --git a/classical.js b/classical.js
new file mode 100644 (file)
index 0000000..731ca24
--- /dev/null
@@ -0,0 +1,624 @@
+
+var Render = require("./render");
+var makeColumn = require("./column");
+
+exports.transcribe = transcribe;
+function transcribe(text) {
+    var tengwarObjects = parse(text);
+    return Render.transcribe(tengwarObjects);
+}
+
+exports.encode = encode;
+function encode(text) {
+    var tengwarObjects = parse(text);
+    return Render.encode(tengwarObjects);
+}
+
+exports.parse = parse;
+function parse(text, options) {
+    options = options || makeOptions();
+    return text.split(/\n\n\n+/).map(function (section) {
+        return section.split(/\n\n/).map(function (paragraph) {
+            return paragraph.split(/\n/).map(function (line) {
+                var words = [];
+                var word = [];
+                line.toLowerCase().replace(
+                    /([\wáéíóúëâêîôûñ']+)|(.)/g,
+                    function ($, contiguous, other) {
+                        if (contiguous) {
+                            try {
+                                word.push.apply(word, parseWord(contiguous, options));
+                            } catch (exception) {
+                                word.push(makeColumn().addError("Cannot transcribe " + JSON.stringify(word) + " because " + exception.message));
+                            }
+                        } else if (punctuation[other]) {
+                            word.push(makeColumn(punctuation[other]));
+                        } else if (other === " ") {
+                            words.push(word);
+                            word = [];
+                        } else {
+                            word.push(makeColumn().addError("Cannot transcribe " + JSON.stringify(other)));
+                        }
+                    }
+                );
+                if (word.length) {
+                    words.push(word);
+                }
+                return words;
+            });
+        });
+    });
+}
+
+// original mode: (classical)
+// interim: (classical + vilya? + aha? + longHalla?)
+// third age: (iuRising?) default
+
+function makeOptions() {
+    return {
+        vilya: false,
+        // false: (v: vala, w: wilya)
+        // true: (v: vilya, w: ERROR)
+        aha: false,
+        // between the original formation of the language,
+        // but before the third age,
+        // harma was renamed aha,
+        // and meant breath-h in initial position
+        longHalla: false,
+        // TODO indicates that halla should be used before medial L and W to
+        // indicate that these are pronounced with length.
+        // initial hl and hw remain short.
+        classical: false,
+        // before the third age
+        // affects use of "r" and "h"
+        // without classic, we default to the mode from the namarie poem.
+        // in the classical period, "r" was transcribed as "ore" only between
+        // vowels.
+        // in the third age, through the namarie poem, "r" is only "ore" before
+        // consontants and at the end of words.
+        // TODO figure out "h"
+        iuRising: false
+        // iuRising thirdAge: anna:y,u
+        // otherwise: ure:i
+        // in the third age, "iu" is a rising diphthong,
+        // whereas all others are falling.  rising means
+        // that they are stressed on the second sound, as
+        // in "yule".  whether to use yanta or anna is
+        // not attested.
+        // TODO doubled dots for í
+        // TODO triple dots for y
+        // TODO simplification of a, noting non-a
+    };
+};
+
+function parseWord(latin, options) {
+
+    // normalize
+    latin = latin.replace(substitutionsRe, function ($, key) {
+        return substitutions[key];
+    });
+
+    // the parser is a monadic state machine.
+    // each state is represented by a function that accepts
+    // a character.  parse functions accept a callback (for forwarding the
+    // result) and return a state.
+    var result;
+    var state = parseWordTail(function (transcription) {
+        result = transcription;
+        return expectEof();
+    }, options, []);
+    // drive the state machine
+    Array.prototype.forEach.call(latin, function (letter, i) {
+        state = state(letter);
+    });
+    // break break break
+    while (!result) {
+        state = state(""); // EOF
+    }
+    return result;
+
+}
+
+var substitutions = {
+    "k": "c",
+    "x": "cs",
+    "qu": "cw",
+    "q": "cw",
+    "ph": "f",
+    "ë": "e",
+    "â": "á",
+    "ê": "é",
+    "î": "í",
+    "ô": "ó",
+    "û": "ú"
+};
+
+var substitutionsRe = new RegExp("(" +
+    Object.keys(substitutions).join("|") +
+")", "ig");
+
+var vowels = "aeiouyáéíóú";
+var punctuation = {
+    "-": "comma",
+    ",": "comma",
+    ":": "comma",
+    ";": "full-stop",
+    ".": "full-stop",
+    "!": "exclamation-point",
+    "?": "question-mark",
+    "(": "open-paren",
+    ")": "close-paren",
+    ">": "flourish-left",
+    "<": "flourish-right"
+};
+
+// state machine
+
+function parseWordTail(callback, options, columns, previous) {
+    return parseColumn(function (moreColumns) {
+        if (!moreColumns.length) {
+            return callback(columns);
+        } else {
+            return parseWordTail(
+                callback,
+                options,
+                columns.concat(moreColumns),
+                moreColumns[moreColumns.length - 1] // previous
+            );
+        }
+    }, options, previous);
+}
+
+function parseColumn(callback, options, previous) {
+    return parseTengwa(function (columns) {
+        var previous = columns.pop();
+        return parseTehta(function (next) {
+            return callback(columns.concat(next).filter(Boolean));
+        }, options, previous);
+    }, options, previous);
+}
+
+function parseTengwa(callback, options, previous) {
+    return function (character) {
+        if (character === "n") { // n
+            return function (character) {
+                if (character === "n") { // nn
+                    return callback([makeColumn("numen").addBarBelow()]);
+                } else if (character === "t") { // nt
+                    return callback([makeColumn("tinco")]);
+                } else if (character === "d") { // nd
+                    return callback([makeColumn("ando")]);
+                } else if (character === "g") { // ng
+                    return function (character) {
+                        if (character === "w") { // ngw
+                            return callback([makeColumn("ungwe")]);
+                        } else { // ng
+                            return callback([makeColumn("anga")])(character);
+                        }
+                    };
+                } else if (character === "c") { // nc
+                    return function (character) {
+                        if (character === "w") { // ncw
+                            return callback([makeColumn("unque")]);
+                        } else { // nc
+                            return callback([makeColumn("anca")])(character);
+                        }
+                    };
+                } else {
+                    return callback([makeColumn("numen")])(character);
+                }
+            };
+        } else if (character === "m") {
+            return function (character) {
+                if (character === "m") { // mm
+                    return callback([makeColumn("malta").addBarBelow()]);
+                } else if (character === "p") { // mp
+                    return callback([makeColumn("ampa")]);
+                } else if (character === "b") { // mb
+                    return callback([makeColumn("umbar")]);
+                } else {
+                    return callback([makeColumn("malta")])(character);
+                }
+            };
+        } else if (character === "ñ") { // ñ
+            return function (character) {
+                if (character === "g") { // ñg
+                    return function (character) {
+                        if (character === "w") { // ñgw
+                            return callback([makeColumn("ungwe")]);
+                        } else { // ñg
+                            return callback([makeColumn("anga")])(character);
+                        }
+                    }
+                } else if (character === "c") { // ñc
+                    return function (character) {
+                        if (character === "w") { // ñcw
+                            return callback([makeColumn("unque")]);
+                        } else { // ñc
+                            return callback([makeColumn("anca")]);
+                        }
+                    }
+                } else {
+                    return callback([makeColumn("noldo")])(character);
+                }
+            };
+        } else if (character === "t") {
+            return function (character) {
+                if (character === "t") { // tt
+                    return function (character) {
+                        if (character === "y") { // tty
+                            return callback([makeColumn("tinco").addBelow("y").addBarBelow()]);
+                        } else { // tt
+                            return callback([makeColumn("tinco").addBarBelow()])(character);
+                        }
+                    };
+                } else if (character === "y") { // ty
+                    return callback([makeColumn("tinco").addBelow("y")]);
+                } else if (character === "h") {
+                    return callback([makeColumn("thule")]);
+                } else if (character === "s") {
+                    return function (character) {
+                        // TODO s-inverse, s-extended, s-flourish
+                        if (character === "") { // ts final
+                            return callback([makeColumn("tinco").addFollowing("s")])(character);
+                        } else { // ts medial
+                            return callback([
+                                makeColumn("tinco"),
+                                makeColumn("silme")
+                            ])(character);
+                        }
+                    };
+                } else { // t
+                    return callback([makeColumn("tinco")])(character);
+                }
+            };
+        } else if (character === "p") {
+            return function (character) {
+                if (character === "p") {
+                    return function (character) {
+                        if (character === "y") {
+                            return callback([makeColumn("parma").addBelow("y").addBarBelow()]);
+                        } else {
+                            return callback([makeColumn("parma").addBarBelow()])(character);
+                        }
+                    };
+                } else if (character === "y") { // py
+                    return callback([makeColumn("parma").addBelow("y")]);
+                } else if (character === "s") { // ps
+                    return function (character) {
+                        if (character === "") { // ps final
+                            return callback([makeColumn("parma").addFollowing("s")])(character);
+                        } else { // ps medial
+                            return callback([
+                                makeColumn("parma"),
+                                makeColumn("silme")
+                            ])(character);
+                        }
+                    };
+                } else { // t
+                    return callback([makeColumn("parma")])(character);
+                }
+            };
+        } else if (character === "c") {
+            return function (character) {
+                if (character === "c") {
+                    return callback([makeColumn("calma").addBarBelow()]);
+                } else if (character === "s") {
+                    return callback([makeColumn("calma").addBelow("s")]);
+                } else if (character === "h") {
+                    return callback([makeColumn("harma")]);
+                } else if (character === "w") {
+                    return callback([makeColumn("quesse")]);
+                } else {
+                    return callback([makeColumn("calma")])(character);
+                }
+            };
+        } else if (character === "f") {
+            return callback([makeColumn("formen")]);
+        } else if (character === "v") {
+            if (options.vilya) {
+                return callback([makeColumn("wilya")]); // vilya
+            } else {
+                return callback([makeColumn("vala")]);
+            }
+        } else if (character === "w") {
+            if (options.vilya) {
+                return callback([
+                    makeColumn("short-carrier").addAbove("u")
+                    .addError("Before the introduction of vala, wilya was called vilya and represented the v sound.  There is no tengwa to represent consonantal w.")
+                ]);
+            } else {
+                return callback([makeColumn("vala")]);
+            }
+        } else if (character === "r") {
+            return function (character) {
+                if (character === "d") { // rd
+                    return callback([makeColumn("arda")]);
+                } else if (character === "h") { // hr
+                    return callback([makeColumn("halla"), makeColumn("romen")]);
+                } else if (options.classical) {
+                    // pre-namarie style, ore when r between vowels
+                    if (
+                        previous &&
+                        previous.above &&
+                        character !== "" &&
+                        vowels.indexOf(character) !== -1
+                    ) {
+                        return callback([makeColumn("ore")])(character);
+                    } else {
+                        return callback([makeColumn("romen")])(character);
+                    }
+                } else {
+                    // pre-consonant and word-final
+                    if (character === "" || vowels.indexOf(character) === -1) { // ore
+                        return callback([makeColumn("ore")])(character);
+                    } else { // romen
+                        return callback([makeColumn("romen")])(character);
+                    }
+                }
+            };
+        } else if (character === "l") {
+            return function (character) {
+                if (character === "l") {
+                    return function (character) {
+                        if (character === "y") {
+                            return callback([makeColumn("lambe").addBelow("y").addBarBelow()]);
+                        } else {
+                            return callback([makeColumn("lambe").addBarBelow()])(character);
+                        }
+                    }
+                } else if (character === "y") {
+                    return callback([makeColumn("lambe").addBelow("y")]);
+                } else if (character === "h") { // hl
+                    return callback([makeColumn("halla"), makeColumn("lambe")]);
+                } else if (character === "d") {
+                    return callback([makeColumn("alda")]);
+                } else if (character === "b") {
+                    return callback([makeColumn("lambe"), makeColumn("umbar")]);
+                } else {
+                    return callback([makeColumn("lambe")])(character);
+                }
+            };
+        } else if (character === "s") {
+            return function (character) {
+                if (character === "s") { // ss
+                    return callback([makeColumn("esse")]);
+                } else {
+                    return callback([makeColumn("silme")])(character);
+                }
+            };
+        } else if (character === "h") {
+            return function (character) {
+                if (character === "l") {
+                    return callback([
+                        makeColumn("halla"),
+                        makeColumn("lambe")
+                    ]);
+                } else if (character === "r") {
+                    return callback([
+                        makeColumn("halla"),
+                        makeColumn("romen")
+                    ]);
+                } else if (character === "w") {
+                    return callback([makeColumn("hwesta")]);
+                } else if (character === "t") {
+                    return callback([makeColumn("harma")]);
+                } else if (character === "y") {
+                    if (options.classical && !options.aha) { // initial
+                        return callback([makeColumn("hyarmen")]);
+                    } else { // post-aha, through to the third-age
+                        return callback([makeColumn("hyarmen").addBelow("y")]);
+                    }
+                } else if (!previous) { // initial
+                    if (options.classical && !options.aha) {
+                        return callback([makeColumn("halla")])(character);
+                    } else { // post-aha
+                        return callback([makeColumn("harma")])(character);
+                    }
+                } else { // medial
+                    if (options.classical && !options.aha) { // initial
+                        return callback([makeColumn("harma")])(character);
+                    } else if (options.classical) { // post-aha
+                        return callback([makeColumn("hyarmen")])(character);
+                    } else { // namarie, third-age
+                        return callback([makeColumn("harma")])(character);
+                    }
+                }
+            };
+        } else if (character === "d") {
+            return callback([makeColumn("ando").addError("D cannot appear except after N, L, or R")]);
+        } else if (character === "b") {
+            return callback([makeColumn("umbar").addError("B cannot appear except after M or L")]);
+        } else if (character === "g") {
+            return callback([makeColumn("anga").addError("G cannot appear except after N or Ñ")]);
+        } else if (character === "j") {
+            return callback([makeColumn().addError("J cannot be transcribed in Classical Mode")]);
+        } else {
+            return callback([])(character);
+        }
+    };
+}
+
+function parseTehta(callback, options, previous) {
+    return function (character) {
+        if (character === "a") {
+            return function (character) {
+                if (character === "i") {
+                    return callback([previous, makeColumn("yanta", "a")]);
+                } else if (character === "u") {
+                    return callback([previous, makeColumn("ure", "a")]);
+                } else if (previous && previous.canAddAbove()) {
+                    previous.addAbove("a");
+                    return callback([previous])(character);
+                } else {
+                    return callback([previous, makeColumn("short-carrier", "a")])(character);
+                }
+            };
+        } else if (character === "e") {
+            return function (character) {
+                if (character === "u") {
+                    return callback([previous, makeColumn("ure", "e")]);
+                } else if (previous && previous.canAddAbove()) {
+                    previous.addAbove("e");
+                    return callback([previous])(character);
+                } else {
+                    return callback([previous, makeColumn("short-carrier", "e")])(character);
+                }
+            };
+        } else if (character === "i") {
+            return function (character) {
+                if (character === "u") {
+                    if (options.iuRising) {
+                        return callback([previous, makeColumn("anna", "u").addBelow("y")]);
+                    } else {
+                        return callback([previous, makeColumn("ure", "i")]);
+                    }
+                } else if (previous && previous.canAddAbove()) {
+                    previous.addAbove("i");
+                    return callback([previous])(character);
+                } else {
+                    return callback([previous, makeColumn("short-carrier", "i")])(character);
+                }
+            };
+        } else if (character === "o") {
+            return function (character) {
+                if (character === "i") {
+                    return callback([previous, makeColumn("yanta", "o")]);
+                } else if (previous && previous.canAddAbove()) {
+                    previous.addAbove("o");
+                    return callback([previous])(character);
+                } else {
+                    return callback([previous, makeColumn("short-carrier", "o")])(character);
+                }
+            };
+        } else if (character === "u") {
+            return function (character) {
+                if (character === "i") {
+                    return callback([previous, makeColumn("yanta", "u")]);
+                } else if (previous && previous.canAddAbove()) {
+                    previous.addAbove("u");
+                    return callback([previous])(character);
+                } else {
+                    return callback([previous, makeColumn("short-carrier", "u")])(character);
+                }
+            };
+        } else if (character === "y") {
+            if (previous && previous.canAddBelow()) {
+                return callback([previous.addBelow("y")]);
+            } else {
+                var next = makeColumn("anna").addBelow("y");
+                return parseTehta(function (moreColumns) {
+                    return callback([previous].concat(moreColumns));
+                }, options, next);
+            }
+        } else if (character === "á") {
+            return callback([previous, makeColumn("long-carrier", "a")]);
+        } else if (character === "é") {
+            return callback([previous, makeColumn("long-carrier", "e")]);
+        } else if (character === "í") {
+            return callback([previous, makeColumn("long-carrier", "i")]);
+        } else if (character === "ó") {
+            if (previous && previous.canAddAbove()) {
+                previous.addAbove('ó');
+                return callback([previous]);
+            } else {
+               return callback([previous, makeColumn("long-carrier", "o")]);
+            }
+        } else if (character === "ú") {
+            if (previous && previous.canAddAbove()) {
+                previous.addAbove('ú');
+                return callback([previous]);
+            } else {
+                return callback([previous, makeColumn("long-carrier", "u")]);
+            }
+        } else {
+            return callback([previous])(character);
+        }
+    };
+}
+
+// generic parser utilities
+
+function expect(expected, callback) {
+    var displayExpected = expected ? JSON.stringify(expected) : "end of word";
+    return function (character) {
+        if (character !== expected) {
+            var displayCharacter = character ? JSON.stringify(character) : "end of word";
+            throw new Error("Expected " + displayExpected + " but got " + displayCharacter);
+        } else {
+            return callback(expected);
+        }
+    }
+}
+
+function expectEof() {
+    return expect("", function () {
+        return function () {
+            return parseEof();
+        };
+    });
+}
+
+// Notes regarding "h":
+//
+// http://at.mansbjorkman.net/teng_quenya.htm#note_harma
+// originally:
+//  h represented ach-laut and was written with harma.
+//  h initial transcribed as halla
+//  h medial transcribed as harma
+//  hy transcribed as hyarmen
+// then harma became aha:
+//  then h in initial position became a breath-h, still spelled with harma, but
+//  renamed aha.
+//  h initial transcribed as harma
+//  h medial transcribed as hyarmen
+//  hy transcribed as hyarmen with underposed y
+// then, in the third age:
+//  the h in every position became a breath-h
+//  except before t, where it remained pronounced as ach-laut
+//  h initial ???
+//  h medial transcribed as harma
+//  h transcribed as halla or hyarmen in other positions (needs clarification)
+//
+// ach-laut (_ch_, /x/ phonetically, {h} by tolkien)
+//   original: harma in all positions
+//   altered: harma initially, halla in all other positions
+//   third-age: halla in all other positions
+// hy (/ç/ phonetically)
+//   original: hyarmen in all positions
+//   altered: hyarmen with y below
+//   third-age:
+// h (breath h)
+//   original: halla in all positions
+//   altered: hyarmen medially
+//   third-age:
+//
+// harma:
+//   original: ach-laut found in all positions
+//   altered: breath h initially (renamed aha), ach-laut medial
+//   third-age: ach-laut before t, breath h all other places
+// hyarmen:
+//   original: represented {hy}, palatalized h, in all positions
+//   altered: breath h medial, palatalized with y below
+//   third-age: same
+// halla:
+//   original: breath-h, presuming existed only initially
+//   altered: breath h initial
+//   third-age: only used for hl and hr
+//
+// hr: halla romen
+// hl: halla lambe
+// ht: harma
+// hy:
+//   original: hyarmen
+//   altered:
+//     initial: ERROR
+//     medial: hyarmen lower-y
+//   third age: hyarmen lower-y
+// ch: harma
+// h initial:
+//   original: halla
+//   altered: XXX
+//   third-age: harma
+// h medial: hyarmen
+
diff --git a/column.js b/column.js
new file mode 100644 (file)
index 0000000..833790e
--- /dev/null
+++ b/column.js
@@ -0,0 +1,63 @@
+
+module.exports = function makeColumn(tengwa, above, below) {
+    return new Column(tengwa, above, below);
+};
+
+var Column = function (tengwa, above, below) {
+    this.above = above;
+    this.barAbove = void 0;
+    this.tengwa = tengwa;
+    this.barBelow = void 0;
+    this.below = below;
+    this.following = void 0;
+    this.error = void 0;
+};
+
+Column.prototype.canAddAbove = function () {
+    return !this.above || (
+        (this.tengwa === "silme" || this.tengwa === "esse")
+        && !this.below
+    );
+};
+
+Column.prototype.addAbove = function (above) {
+    if (this.tengwa === "silme") {
+        this.tengwa = "silme-nuquerna";
+    }
+    if (this.tengwa === "esse") {
+        this.tengwa = "esse-nuquerna";
+    }
+    this.above = above;
+    return this;
+};
+
+Column.prototype.canAddBelow = function () {
+    return !this.below && this.tengwa !== "silme-nuquerna";
+};
+
+Column.prototype.addBelow = function (below) {
+    this.below = below;
+    return this;
+};
+
+Column.prototype.addBarAbove = function () {
+    this.barAbove = true;
+    return this;
+};
+
+Column.prototype.addBarBelow = function () {
+    this.barBelow = true;
+    return this;
+};
+
+Column.prototype.addFollowing = function (following) {
+    this.following = following;
+    return this;
+};
+
+Column.prototype.addError = function (error) {
+    this.errors = this.errors || [];
+    this.errors.push(error);
+    return this;
+};
+
similarity index 65%
rename from style.css
rename to custom-webfont.css
index a4d715f..6b98fe0 100644 (file)
--- a/style.css
@@ -3,18 +3,13 @@
     font-family: tengwar;
     src: url('custom-webfont.eot');
     src: url('custom-webfont.eot#iefix'),
-         url('custom-webfont.woff') format('woff'), 
-         url('custom-webfont.ttf') format('truetype'), 
+         url('custom-webfont.woff') format('woff'),
+         url('custom-webfont.ttf') format('truetype'),
          url('custom-webfont.svg#TengwarAnnatarItalic') format('svg');
     font-weight: normal;
     font-style: normal;
 }
-table {
-    padding: 4ex;
-}
-td {
-    width: 4em;
-}
+
 .tengwar {
     font-family: tengwar;
     font-size: 30px;
diff --git a/general-use.js b/general-use.js
new file mode 100644 (file)
index 0000000..0c7908c
--- /dev/null
@@ -0,0 +1,406 @@
+
+var Render = require("./render");
+var makeColumn = require("./column");
+
+// TODO rewrite using the parser technique from classical.js
+
+exports.transcribe = transcribe;
+function transcribe(text) {
+    return Render.transcribe(parse(text));
+}
+
+exports.encode = encode;
+function encode(text) {
+    return Render.encode(parse(text));
+}
+
+exports.parse = parse;
+function parse(latin) {
+    latin = latin.replace(/[,:] +/g, ",");
+    return latin.split(/\n\n\n+/).map(function (section) {
+        return section.split(/\n\n/).map(function (paragraph) {
+            return paragraph.split(/\n/).map(function (line) {
+                return line.split(/\s+/).map(function (word) {
+                    var columns = []
+                    word.replace(/([\wáéíóúÁÉÍÓÚëËâêîôûÂÊÎÔÛ']+)|(\W+)/g, function ($, word, others) {
+                        try {
+                            columns.push.apply(columns, parseWord(word || others));
+                        } catch (x) {
+                            console.log(parseWord(word || others), word || others);
+                        }
+                    });
+                    return columns;
+                });
+            });
+        });
+    });
+}
+
+function parseWord(latin) {
+    latin = latin
+    .toLowerCase()
+    .replace(substitutionsRe, function ($, key) {
+        return Render.decode(Mode.substitutions[key]);
+    });
+    if (Mode.words[latin])
+        return Render.decode(Mode.words[latin]);
+    var columns = [];
+    var length;
+    var first = true;
+    var maybeFinal;
+    while (latin.length) {
+        if (latin[0] != "s")
+            maybeFinal = undefined;
+        length = latin.length;
+        latin = latin
+        .replace(transcriptionsRe, function ($, vowel, tengwa, w, y, s, prime) {
+            //console.log(latin, [vowel, tengwa, w, y, s]);
+            w = w || ""; s = s || ""; y = y || "";
+            var value = Mode.transcriptions[tengwa];
+            tengwa = value.split(":")[0];
+            var tehtar = value.split(":").slice(1).join(":");
+            var voweled = value.split(":").filter(function (term) {
+                return Mode.vowelTranscriptions[term];
+            }).length;
+            if (vowel) {
+                if (!voweled) {
+                    // flip if necessary
+                    if (
+                        Render.tehtaForTengwa(tengwa, vowel) === null &&
+                        Render.tehtaForTengwa(tengwa + "-nuquerna", vowel) !== null
+                    ) {
+                        value = [tengwa + "-nuquerna"]
+                        .concat(tehtar)
+                        .concat([vowel])
+                        .filter(function (part) {
+                            return part;
+                        }).join(":");
+                    } else {
+                        value += ":" + vowel;
+                    }
+                } else {
+                    columns.push(parseWord(vowel));
+                }
+                voweled = true;
+            }
+            if (w && !voweled) {
+                value += ":w";
+                w = "";
+                voweled = true;
+            }
+            if (y) {
+                value += ":y";
+                y = "";
+            }
+            // must go last because it has a non-zero width
+            if (s && !w) {
+                var length = prime.length;
+                var possibilities = [
+                    "s",
+                    "s-inverse",
+                    "s-extended",
+                    "s-flourish"
+                ].filter(function (tehta) {
+                    return Render.tehtaForTengwa(tengwa, tehta);
+                });
+                while (possibilities.length && length) {
+                    possibilities.shift();
+                    length--;
+                }
+                if (possibilities.length) {
+                    if (value.split(":").indexOf("quesse") >= 0) {
+                        value = value + ":" + possibilities.shift();
+                        s = "";
+                    } else {
+                        maybeFinal = value + ":" + possibilities.shift();
+                    }
+                }
+            }
+            columns.push(value);
+            first = false;
+            return w + y + s;
+        });
+        if (length === latin.length) {
+            length = latin.length;
+            latin = latin.replace(vowelTranscriptionsRe, function ($, vowel) {
+                var value = Mode.vowelTranscriptions[vowel];
+                columns.push(value);
+                return "";
+            });
+            if (length === latin.length) {
+                //throw new Error("Can't transcribe " + latin.slice(1));
+                if (Mode.punctuation[latin[0]])
+                    columns.push(Mode.punctuation[latin[0]]);
+                latin = latin.slice(1);
+            }
+        }
+    }
+    if (columns.length) {
+        if (maybeFinal && columns[columns.length - 1] == "silme") {
+            columns.pop();
+            columns.pop();
+            columns.push(maybeFinal);
+        }
+        columns.push(columns.pop().replace("romen", "ore"));
+    }
+    /*
+    * failed attempt to distinguish yanta spelling of consonantal i
+    * automatically in iant, iaur, and ioreth but not in galadriel,
+    * moria
+    columns = columns.map(function (part, i) {
+        if (i === columns.length - 1)
+            return part;
+        if (part !== "short-carrier:i")
+            return part;
+        if (columns[i + 1].split(":").filter(function (term) {
+            return term === "a";
+        }).length) {
+            return "yanta";
+        } else {
+            return part;
+        }
+    });
+    */
+    /*
+    // abandoned trick to replace "is" with short-carrier with s hook
+    columns = columns.map(function (part, i) {
+        //console.log(part);
+        if (part === "silme-nuquerna:i")
+            return "short-carrier:s";
+        return part;
+    });
+    */
+    return Render.decodeWord(columns.join(";"));
+}
+
+// king's letter, general use
+var Mode = {
+    "substitutions": {
+        "k": "c",
+        "x": "cs",
+        "qu": "cw",
+        "q": "cw",
+        "ë": "e",
+        "â": "á",
+        "ê": "é",
+        "î": "í",
+        "ô": "ó",
+        "û": "ú"
+    },
+    "transcriptions": {
+
+        // consonants
+        "t": "tinco",
+        "nt": "tinco:tilde-above",
+        "tt": "tinco:tilde-below",
+
+        "p": "parma",
+        "mp": "parma:tilde-above",
+        "pp": "parma:tilde-below",
+
+        "ch'": "calma", // ch is palatal fricative, as in bach
+        "nch'": "calma:tilde-above",
+
+        "c": "quesse",
+        "nc": "quesse:tilde-above",
+
+        "d": "ando",
+        "nd": "ando:tilde-above",
+        "dd": "ando:tilde-below",
+
+        "b": "umbar",
+        "mb": "umbar:tilde-above",
+        "bb": "umbar:tilde-below",
+
+        "j": "anca",
+        "nj": "anca:tilde-above",
+
+        "g": "ungwe",
+        "ng": "ungwe:tilde-above",
+        "gg": "ungwe:tilde-below",
+
+        "th": "thule",
+        "nth": "thule:tilde-above",
+
+        "f": "formen",
+        "ph": "formen",
+        "mf": "formen:tilde-above",
+        "mph": "formen:tilde-above",
+
+        "sh": "harma",
+
+        "h": "hyarmen",
+        "ch": "hwesta",
+        "hw": "hwesta-sindarinwa",
+        "wh": "hwesta-sindarinwa",
+
+        "gh": "unque",
+        "ngh": "unque:tilde-above",
+
+        "dh": "anto",
+        "ndh": "anto:tilde-above",
+
+        "v": "ampa",
+        "bh": "ampa",
+        "mv": "ampa:tilde-above",
+        "mbh": "ampa:tilde-above",
+
+        "n": "numen",
+        "nn": "numen:tilde-above",
+
+        "m": "malta",
+        "mm": "malta:tilde-above",
+
+        "ng": "nwalme",
+        "ñ": "nwalme",
+        "nwal": "nwalme:w;lambe:a",
+
+        "r": "romen",
+        "rr": "romen:tilde-below",
+        "rh": "arda",
+
+        "l": "lambe",
+        "ll": "lambe:tilde-below",
+        "lh": "alda",
+
+        "s": "silme",
+        "ss": "silme:tilde-below",
+
+        "z": "esse",
+
+        "á": "wilya:a",
+        "é": "long-carrier:e",
+        "í": "long-carrier:i",
+        "ó": "long-carrier:o",
+        "ú": "long-carrier:u",
+        "w": "vala",
+
+        "ai": "anna:a",
+        "oi": "anna:o",
+        "ui": "anna:u",
+        "au": "vala:a",
+        "eu": "vala:e",
+        "iu": "vala:i",
+        "ae": "yanta:a",
+
+    },
+    "vowelTranscriptions": {
+
+        "a": "short-carrier:a",
+        "e": "short-carrier:e",
+        "i": "short-carrier:i",
+        "o": "short-carrier:o",
+        "u": "short-carrier:u",
+
+        "á": "wilya:a",
+        "é": "long-carrier:e",
+        "í": "long-carrier:i",
+        "ó": "short-carrier:ó",
+        "ú": "short-carrier:ú",
+
+        "w": "vala",
+        "y": "short-carrier:í"
+
+    },
+
+    "words": {
+        "iant": "yanta;tinco:tilde-above:a",
+        "iaur": "yanta;vala:a;ore",
+        "baranduiniant": "umbar;romen:a;ando:tilde-above:a;anna:u;yanta;anto:tilde-above:a",
+        "ioreth": "yanta;romen:o;thule:e",
+        "noldo": "nwalme;lambe:o;ando;short-carrier:o",
+        "noldor": "nwalme;lambe:o;ando;ore:o",
+        "is": "short-carrier:i:s"
+    },
+
+    "punctuation": {
+        "-": "comma",
+        ",": "comma",
+        ":": "comma",
+        ";": "full-stop",
+        ".": "full-stop",
+        "!": "exclamation-point",
+        "?": "question-mark",
+        "(": "open-paren",
+        ")": "close-paren",
+        ">": "flourish-left",
+        "<": "flourish-right"
+    },
+
+    "annotations": {
+        "tinco": {"tengwa": "t"},
+        "parma": {"tengwa": "p"},
+        "calma": {"tengwa": "c"},
+        "quesse": {"tengwa": "c"},
+        "ando": {"tengwa": "d"},
+        "umbar": {"tengwa": "b"},
+        "anga": {"tengwa": "ch"},
+        "ungwe": {"tengwa": "g"},
+        "thule": {"tengwa": "th"},
+        "formen": {"tengwa": "f"},
+        "hyarmen": {"tengwa": "h"},
+        "hwesta": {"tengwa": "kh"},
+        "unque": {"tengwa": "gh"},
+        "anto": {"tengwa": "dh"},
+        "anca": {"tengwa": "j"},
+        "ampa": {"tengwa": "v"},
+        "numen": {"tengwa": "n"},
+        "malta": {"tengwa": "m"},
+        "nwalme": {"tengwa": "ñ"},
+        "romen": {"tengwa": "r"},
+        "ore": {"tengwa": "-r"},
+        "lambe": {"tengwa": "l"},
+        "silme": {"tengwa": "s"},
+        "silme-nuquerna": {"tengwa": "s"},
+        "esse": {"tengwa": "z"},
+        "esse-nuquerna": {"tengwa": "z"},
+        "harma": {"tengwa": "sh"},
+        "alda": {"tengwa": "lh"},
+        "arda": {"tengwa": "rh"},
+        "wilya": {"tengwa": "a"},
+        "vala": {"tengwa": "w"},
+        "anna": {"tengwa": "i"},
+        "vala": {"tengwa": "w"},
+        "yanta": {"tengwa": "e"},
+        "hwesta-sindarinwa": {"tengwa": "wh"},
+        "s": {"following": "s"},
+        "s-inverse": {"following": "s<sub>2</sub>"},
+        "s-extended": {"following": "s<sub>3</sub>"},
+        "s-flourish": {"following": "s<sub>4</sub>"},
+        "long-carrier": {"tengwa": "´"},
+        "short-carrier": {},
+        "tilde-above": {"above": "nmñ-"},
+        "tilde-below": {"below": "2"},
+        "a": {"tehta-above": "a"},
+        "e": {"tehta-above": "e"},
+        "i": {"tehta-above": "i"},
+        "o": {"tehta-above": "o"},
+        "u": {"tehta-above": "u"},
+        "ó": {"tehta-above": "ó"},
+        "ú": {"tehta-above": "ú"},
+        "í": {"tehta-above": "y"},
+        "y": {"tehta-below": "y"},
+        "w": {"tehta-above": "w"},
+        "full-stop": {"tengwa": "."},
+        "exclamation-point": {"tengwa": "!"},
+        "question-mark": {"tengwa": "?"},
+        "comma": {"tengwa": "-"},
+        "open-paren": {"tengwa": "("},
+        "close-paren": {"tengwa": ")"},
+        "flourish-left": {"tengwa": "“"},
+        "flourish-right": {"tengwa": "”"}
+    }
+};
+
+var transcriptionsRe = new RegExp("^([aeiouóú]?'?)(" +
+    Object.keys(Mode.transcriptions).sort(function (a, b) {
+        return b.length - a.length;
+    }).join("|") +
+")(w?)(y?)(s?)('*)", "ig");
+var vowelTranscriptionsRe = new RegExp("^(" +
+    Object.keys(Mode.vowelTranscriptions).join("|") +
+")", "ig");
+var substitutionsRe = new RegExp("(" +
+    Object.keys(Mode.substitutions).join("|") +
+")", "ig");
+
index e57957d..6d4e80e 100644 (file)
--- a/index.css
+++ b/index.css
@@ -1,4 +1,20 @@
 
+@font-face {
+    font-family: tengwar;
+    src: url('custom-webfont.eot');
+    src: url('custom-webfont.eot#iefix'),
+         url('custom-webfont.woff') format('woff'),
+         url('custom-webfont.ttf') format('truetype'),
+         url('custom-webfont.svg#TengwarAnnatarItalic') format('svg');
+    font-weight: normal;
+    font-style: normal;
+}
+
+.tengwar {
+    font-family: tengwar;
+    font-size: 30px;
+}
+
 body {
     color: #ff0;
 
diff --git a/mediawiki/tengwar.php b/mediawiki/tengwar.php
new file mode 100644 (file)
index 0000000..81bf3f4
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+$wgHooks["ParserFirstCallInit"][] = "wfTengwarInit";
+
+function wfTengwarInit(Parser $parser) {
+    $parser->setHook("tengwar", "wfTengwar");
+    return true;
+}
+function wfTengwar($input, array $args) {
+    $mode = array_key_exists("mode", $args) ? $args["mode"] : "";
+    $encoded = array_key_exists("encoded", $args) ? $args["encoded"] : "";
+    $bindings = array_key_exists("bindings", $args) ? $args["bindings"] : "";
+    return "<span class=\"tengwar\" data-tengwar=\"" .
+        htmlspecialchars($input) .
+        "\" data-encoded=\"" .
+        htmlspecialchars($encoded) .
+        "\" data-bindings=\"" .
+        htmlspecialchars($bindings) .
+        "\" data-mode=\"" .
+        htmlspecialchars($mode) .
+        "\" ></span>";
+}
+
+?>
index 75c7a03..facbebc 100644 (file)
@@ -22,6 +22,7 @@
         "url": "http://github.com/kriskowal/tengwarjs.git"
     },
     "dependencies": {
+        "mr": "0.0.x"
     },
     "devDependencies": {
     }
diff --git a/render.js b/render.js
new file mode 100644 (file)
index 0000000..0d17859
--- /dev/null
+++ b/render.js
@@ -0,0 +1,664 @@
+
+var makeColumn = require("./column");
+
+var Font = {
+    "names": [
+        ["tinco", "parma", "calma", "quesse"],
+        ["ando", "umbar", "anga", "ungwe"],
+        ["thule", "formen", "harma", "hwesta"],
+        ["anto", "ampa", "anca", "unque"],
+        ["numen", "malta", "noldo", "nwalme"],
+        ["ore", "vala", "anna", "wilya"],
+        ["romen", "arda", "lambe", "alda"],
+        ["silme", "silme-nuquerna", "esse", "esse-nuquerna"],
+        ["hyarmen", "hwesta-sindarinwa", "yanta", "ure"],
+        ["halla", "short-carrier", "long-carrier", "round-carrier"],
+        ["tinco-extended", "parma-extended", "calma-extended", "quesse-extended"],
+    ],
+    "aliases": {
+        "vilya": "wilya",
+        "aha": "harma"
+    },
+    // classical
+    "tengwar": {
+        // 1
+        "tinco": "1", // t
+        "parma": "q", // p
+        "calma": "a", // c
+        "quesse": "z", // qu
+        // 2
+        "ando" : "2", // nd
+        "umbar": "w", // mb
+        "anga" : "s", // ng
+        "ungwe": "x", // ngw
+        // 3
+        "thule" : "3", // th
+        "formen": "e", // ph / f
+        "harma" : "d", // h / ch
+        "hwesta": "c", // hw / chw
+        // 4
+        "anto" : "4", // nt
+        "ampa" : "r", // mp
+        "anca" : "f", // nc
+        "unque": "v", // nqu
+        // 5
+        "numen" : "5", // n
+        "malta" : "t", // m
+        "noldo" : "g", // ng
+        "nwalme": "b", // ngw / nw
+        // 6
+        "ore"  : "6", // r
+        "vala" : "y", // v
+        "anna" : "h", // -
+        "wilya": "n", // w / v
+        // 7
+        "romen": "7", // medial r
+        "arda" : "u", // rd / rh
+        "lambe": "j", // l
+        "alda" : "m", // ld / lh
+        // 8
+        "silme":          "8", // s
+        "silme-nuquerna": "i", // s
+        "esse":           "k", // z
+        "esse-nuquerna":  ",", // z
+        // 9
+        "hyarmen":           "9", // hyarmen
+        "hwesta-sindarinwa": "o", // hwesta sindarinwa
+        "yanta":             "l", // yanta
+        "ure":               ".", // ure
+        // 10
+        "halla": "½", // halla
+        "short-carrier": "`",
+        "long-carrier": "~",
+        "round-carrier": "]",
+        // I
+        "tinco-extended": "!",
+        "parma-extended": "Q",
+        "calma-extended": "A",
+        "quesse-extended": "Z",
+        // punctuation
+        "comma": "=",
+        "full-stop": "-",
+        "exclamation-point": "Á",
+        "question-mark": "À",
+        "open-paren": "&#140;",
+        "close-paren": "&#156;",
+        "flourish-left": "&#286;",
+        "flourish-right": "&#287;",
+    },
+    "tehtar": {
+        "a": "#EDC",
+        "e": "$RFV",
+        "i": "%TGB",
+        "o": "^YHN",
+        "u": [
+            "&",
+            "U",
+            "J",
+            "M",
+            "&#256;", // backward hooks, from the alt font to the custom font
+            "&#257;",
+            "&#258;",
+            "&#259;"
+        ],
+        //"á": "",
+        "ó": [
+            "&#260;",
+            "&#261;",
+            "&#262;",
+            "&#263;"
+        ],
+        "ú": [
+            "&#264;",
+            "&#265;",
+            "&#266;",
+            "&#267;"
+        ],
+        "í": [
+            "&#212;",
+            "&#213;",
+            "&#214;",
+            "&#215;",
+        ],
+        "w": "èéêë",
+        "y": "ÌÍÎÏ´",
+        /*
+        "o-under": [
+            "ä",
+            "&#229;", // a ring above
+            "æ",
+            "ç",
+            "|"
+        ],
+        */
+        // TODO deal with the fact that all of these
+        // should only be final (for word spacing) except
+        // for the first S-hook for "calma" and "quesse"
+        // since they appear within the tengwa
+        "s": {
+            "special": true,
+            "tinco": "+",
+            "ando": "+",
+            "numen": "+",
+            "lambe": "_",
+            "calma": "|",
+            "quesse": "|",
+            "short-carrier": "}",
+        },
+        "s-inverse": {
+            "special": true,
+            "tinco": "¡"
+        },
+        "s-extended": {
+            "special": true,
+            "tinco": "&#199;"
+        },
+        "s-flourish": {
+            "special": true,
+            "tinco": "&#163;",
+            "lambe": "&#165;"
+        },
+        "tilde-above": "Pp",
+        "tilde-below": [
+            ":",
+            ";",
+            "&#176;",
+        ],
+        "tilde-high-above": ")0",
+        "tilde-far-below": "?/",
+        "bar-above": "{[",
+        "bar-below": [
+            '"',
+            "'",
+            "&#184;" // cedilla
+        ],
+        "bar-high-above": "ìî",
+        "bar-far-below": "íï"
+    },
+    "barsAndTildes": [
+        "tilde-above",
+        "tilde-below",
+        "tilde-high-above",
+        "tilde-far-below",
+        "bar-above",
+        "bar-below",
+        "bar-high-above",
+        "bar-high-below"
+    ],
+    "tehtaPositions": {
+        "tinco": {
+            "o": 3,
+            "w": 3,
+            "others": 2
+        },
+        "parma": {
+            "o": 3,
+            "w": 3,
+            "others": 2
+        },
+        "calma": {
+            "o": 3,
+            "w": 3,
+            "u": 3,
+            "others": 2
+        },
+        "quesse": {
+            "o": 3,
+            "w": 3,
+            "others": 2
+        },
+        "ando": {
+            "wide": true,
+            "e": 1,
+            "o": 2,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "umbar": {
+            "wide": true,
+            "e": 1,
+            "o": 2,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "anga": {
+            "wide": true,
+            "e": 1,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "ungwe": {
+            "wide": true,
+            "e": 1,
+            "o": 1,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "thule": {
+            "others": 3
+        },
+        "formen": 3,
+        "harma": {
+            "e": 0,
+            "o": 3,
+            "u": 7,
+            "ó": 2,
+            "ú": 2,
+            "w": 0,
+            "others": 1
+        },
+        "hwesta": {
+            "e": 0,
+            "o": 3,
+            "u": 7,
+            "w": 0,
+            "others": 1
+        },
+        "anto": {
+            "wide": true,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "ampa": {
+            "wide": true,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "anca": {
+            "wide": true,
+            "u": 7,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "unque": {
+            "wide": true,
+            "u": 7,
+            "others": 0
+        },
+        "numen": {
+            "wide": true,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "malta": {
+            "wide": true,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "noldo": {
+            "wide": true,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "nwalme": {
+            "wide": true,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "ore": {
+            "e": 3,
+            "o": 3,
+            "u": 3,
+            "ó": 3,
+            "ú": 3,
+            "others": 1
+        },
+        "vala": {
+            "e": 3,
+            "o": 3,
+            "u": 3,
+            "ó": 3,
+            "ú": 3,
+            "others": 1
+        },
+        "anna": {
+            "e": 3,
+            "o": 3,
+            "u": 3,
+            "ó": 2,
+            "ú": 2,
+            "others": 1
+        },
+        "wilya": {
+            "e": 3,
+            "o": 3,
+            "u": 3,
+            "ó": 3,
+            "ú": 3,
+            "others": 1
+        },
+        "romen": {
+            "e": 3,
+            "o": 3,
+            "u": 3,
+            "ó": 2,
+            "ú": 2,
+            "y": null,
+            "others": 1
+        },
+        "arda": {
+            "a": 1,
+            "e": 3,
+            "i": 1,
+            "o": 3,
+            "u": 3,
+            "í": 1,
+            "ó": 2,
+            "ú": 2,
+            "y": null,
+            "others": 0
+        },
+        "lambe": {
+            "wide": true,
+            "e": 1,
+            "y": 4,
+            "ó": 1,
+            "ú": 1,
+            "others": 0
+        },
+        "alda": {
+            "wide": true,
+            "others": 1
+        },
+        "silme": {
+            "y": 3,
+            "others": null
+        },
+        "silme-nuquerna": {
+            "e": 3,
+            "o": 3,
+            "u": 3,
+            "ó": 3,
+            "ú": 3,
+            "y": null,
+            "others": 1
+        },
+        "esse": {
+            "y": null,
+            "others": null
+        },
+        "esse-nuquerna": {
+            "e": 3,
+            "o": 3,
+            "u": 3,
+            "ó": 3,
+            "ú": 3,
+            "others": 1
+        },
+        "hyarmen": 3,
+        "hwesta-sindarinwa": {
+            "o": 2,
+            "u": 2,
+            "ó": 1,
+            "ú": 2,
+            "others": 0
+        },
+        "yanta": {
+            "e": 3,
+            "o": 3,
+            "u": 3,
+            "ó": 2,
+            "ú": 2,
+            "others": 1
+        },
+        "ure": {
+            "e": 3,
+            "o": 3,
+            "u": 3,
+            "ó": 3,
+            "ú": 3,
+            "others": 1
+        },
+        // should not occur:
+        "halla": {
+            "others": null
+        },
+        "short-carrier": 3,
+        "long-carrier": {
+            "y": null,
+            "others": 3
+        },
+        "round-carrier": 3,
+        "tinco-extended": 3,
+        "parma-extended": 3,
+        "calma-extended": {
+            "o": 3,
+            "u": 7,
+            "ó": 2,
+            "ú": 2,
+            "others": 1
+        },
+        "quesse-extended": {
+            "o": 0,
+            "u": 7,
+            "others": 1
+        }
+    },
+    "punctuation": {
+        "-": "comma",
+        ",": "comma",
+        ":": "comma",
+        ";": "full-stop",
+        ".": "full-stop",
+        "!": "exclamation-point",
+        "?": "question-mark",
+        "(": "open-paren",
+        ")": "close-paren",
+        ">": "flourish-left",
+        "<": "flourish-right"
+    },
+    "annotations": {
+        "tinco": {"tengwa": "t"},
+        "parma": {"tengwa": "p"},
+        "calma": {"tengwa": "c"},
+        "quesse": {"tengwa": "c"},
+        "ando": {"tengwa": "d"},
+        "umbar": {"tengwa": "b"},
+        "anga": {"tengwa": "ch"},
+        "ungwe": {"tengwa": "g"},
+        "thule": {"tengwa": "th"},
+        "formen": {"tengwa": "f"},
+        "hyarmen": {"tengwa": "h"},
+        "hwesta": {"tengwa": "kh"},
+        "unque": {"tengwa": "gh"},
+        "anto": {"tengwa": "dh"},
+        "anca": {"tengwa": "j"},
+        "ampa": {"tengwa": "v"},
+        "numen": {"tengwa": "n"},
+        "malta": {"tengwa": "m"},
+        "nwalme": {"tengwa": "ñ"},
+        "romen": {"tengwa": "r"},
+        "ore": {"tengwa": "-r"},
+        "lambe": {"tengwa": "l"},
+        "silme": {"tengwa": "s"},
+        "silme-nuquerna": {"tengwa": "s"},
+        "esse": {"tengwa": "z"},
+        "esse-nuquerna": {"tengwa": "z"},
+        "harma": {"tengwa": "sh"},
+        "alda": {"tengwa": "lh"},
+        "arda": {"tengwa": "rh"},
+        "wilya": {"tengwa": "a"},
+        "vala": {"tengwa": "w"},
+        "anna": {"tengwa": "i"},
+        "vala": {"tengwa": "w"},
+        "yanta": {"tengwa": "e"},
+        "hwesta-sindarinwa": {"tengwa": "wh"},
+        "s": {"following": "s"},
+        "s-inverse": {"following": "s<sub>2</sub>"},
+        "s-extended": {"following": "s<sub>3</sub>"},
+        "s-flourish": {"following": "s<sub>4</sub>"},
+        "long-carrier": {"tengwa": "´"},
+        "short-carrier": {},
+        "tilde-above": {"above": "nmñ-"},
+        "tilde-below": {"below": "2"},
+        "a": {"tehta-above": "a"},
+        "e": {"tehta-above": "e"},
+        "i": {"tehta-above": "i"},
+        "o": {"tehta-above": "o"},
+        "u": {"tehta-above": "u"},
+        "ó": {"tehta-above": "ó"},
+        "ú": {"tehta-above": "ú"},
+        "í": {"tehta-above": "y"},
+        "y": {"tehta-below": "y"},
+        "w": {"tehta-above": "w"},
+        "full-stop": {"tengwa": "."},
+        "exclamation-point": {"tengwa": "!"},
+        "question-mark": {"tengwa": "?"},
+        "comma": {"tengwa": "-"},
+        "open-paren": {"tengwa": "("},
+        "close-paren": {"tengwa": ")"},
+        "flourish-left": {"tengwa": "“"},
+        "flourish-right": {"tengwa": "”"}
+    }
+};
+
+exports.encode = encode;
+function encode(sections) {
+    return sections.map(function (section) {
+        return section.map(function (paragraph) {
+            return paragraph.map(function (line) {
+                return line.map(function (word) {
+                    return word.map(function (column) {
+                        var parts = [];
+                        if (column.below)
+                            parts.push(column.below);
+                        if (column.above)
+                            parts.push(column.above);
+                        if (column.barAbove)
+                            parts.push("bar-above");
+                        if (column.barBelow)
+                            parts.push("bar-below");
+                        if (column.following)
+                            parts.push(column.following);
+                        if (parts.length) {
+                            return column.tengwa + ":" + parts.join(",");
+                        } else {
+                            return column.tengwa;
+                        }
+                    }).join(";");
+                }).join(" ");;
+            }).join("\n");
+        }).join("\n\n");
+    }).join("\n\n\n");
+}
+
+exports.decode = decode;
+function decode(encoding) {
+    return encoding.split("\n\n\n").map(function (section) {
+        return section.split("\n\n").map(function (paragraph) {
+            return paragraph.split("\n").map(function (line) {
+                return line.split(" ").map(decodeWord);
+            });
+        });
+    });
+}
+
+exports.decodeWord = decodeWord;
+function decodeWord(word) {
+    return word.split(";").map(function (column) {
+        var parts = column.split(":");
+        var tengwa = parts.shift();
+        var tehtar = parts.length ? parts.shift().split(",") : [];
+        var result = makeColumn(tengwa);
+        tehtar.forEach(function (tehta) {
+            if (tehta === "bar-above") {
+                result.addBarAbove();
+            } else if (tehta === "bar-below") {
+                result.addBarBelow();
+            } else if (tehta === "y") {
+                result.addBelow("y");
+            } else if (
+                tehta === "s" ||
+                tehta === "s-inverse" ||
+                tehta === "s-extended" ||
+                tehta === "s-flourish"
+            ) {
+                if (
+                    tehta === "s" &&
+                    (tengwa === "calma" || tengwa === "quesse")
+                ) {
+                    result.addBelow(tehta);
+                } else {
+                    result.addFollowing(tehta);
+                }
+            } else {
+                result.addAbove(tehta);
+            }
+        });
+        return result;
+    });
+}
+
+exports.transcribe = transcribe;
+function transcribe(sections) {
+    return sections.map(function (section) {
+        return section.map(function (paragraph) {
+            return paragraph.map(function (line) {
+                return line.map(function (word) {
+                    return word.map(function (column) {
+                        var tengwa = column.tengwa || "anna";
+                        var tehtar = [];
+                        if (column.above) tehtar.push(column.above);
+                        if (column.below) tehtar.push(column.below);
+                        if (column.barBelow) tehtar.push("bar-below");
+                        if (column.barAbove) tehtar.push("bar-above");
+                        if (column.following) tehtar.push(column.following);
+                        var html = Font.tengwar[tengwa] + tehtar.map(function (tehta) {
+                            return tehtaForTengwa(tengwa, tehta);
+                        }).join("");
+                        if (column.errors) {
+                            html = "<abbr class=\"error\" title=\"" + column.errors.join("\n").replace(/"/g, "&quot;") + "\">" + html + "</abbr>";
+                        }
+                        return html;
+                    }).join("");
+                }).join(" ");;
+            }).join("\n");
+        }).join("\n\n");
+    }).join("\n\n\n");
+}
+
+exports.tehtaForTengwa = tehtaForTengwa;
+function tehtaForTengwa(tengwa, tehta) {
+    var tehtaKey = tehtaKeyForTengwa(tengwa, tehta);
+    if (tehtaKey === null)
+        return null;
+    return (
+        Font.tehtar[tehta][tengwa] ||
+        Font.tehtar[tehta][tehtaKey] ||
+        ""
+    );
+}
+
+function tehtaKeyForTengwa(tengwa, tehta) {
+    var positions = Font.tehtaPositions;
+    if (!Font.tehtar[tehta])
+        throw new Error("No tehta for: " + JSON.stringify(tehta));
+    if (Font.tehtar[tehta].special && !Font.tehtar[tehta][tengwa])
+        return null;
+    if (Font.barsAndTildes.indexOf(tehta) >= 0) {
+        if (["lambe", "alda"].indexOf(tengwa) >= 0 && Font.tehtar[tehta].length >= 2)
+            return 2;
+        return positions[tengwa].wide ? 0 : 1;
+    } else if (positions[tengwa] !== undefined) {
+        if (positions[tengwa][tehta] !== undefined) {
+            return positions[tengwa][tehta];
+        } else if (positions[tengwa].others !== undefined) {
+            return positions[tengwa].others;
+        } else {
+            return positions[tengwa];
+        }
+    }
+    return 0;
+}
+
diff --git a/test.css b/test.css
new file mode 100644 (file)
index 0000000..56072b5
--- /dev/null
+++ b/test.css
@@ -0,0 +1,145 @@
+
+@font-face {
+    font-family: tengwar;
+    src: url('custom-webfont.eot');
+    src: url('custom-webfont.eot#iefix'),
+         url('custom-webfont.woff') format('woff'), 
+         url('custom-webfont.ttf') format('truetype'), 
+         url('custom-webfont.svg#TengwarAnnatarItalic') format('svg');
+    font-weight: normal;
+    font-style: normal;
+}
+
+.tengwar {
+    font-family: tengwar;
+    font-size: 30px;
+}
+
+body {
+    color: #ff0;
+
+    background-repeat: no-repeat;
+    background: #fefcea; /* Old browsers */
+    background: -moz-linear-gradient(top, #fefcea 0%, #f9e63b 7%, #e0a928 53%, #d69a2c 94%, #c97e16 99%); /* FF3.6+ */
+    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#fefcea), color-stop(7%,#f9e63b), color-stop(53%,#e0a928), color-stop(94%,#d69a2c), color-stop(99%,#c97e16)); /* Chrome,Safari4+ */
+    background: -webkit-linear-gradient(top, #fefcea 0%,#f9e63b 7%,#e0a928 53%,#d69a2c 94%,#c97e16 99%); /* Chrome10+,Safari5.1+ */
+    background: -o-linear-gradient(top, #fefcea 0%,#f9e63b 7%,#e0a928 53%,#d69a2c 94%,#c97e16 99%); /* Opera11.10+ */
+    background: -ms-linear-gradient(top, #fefcea 0%,#f9e63b 7%,#e0a928 53%,#d69a2c 94%,#c97e16 99%); /* IE10+ */
+    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fefcea', endColorstr='#c97e16',GradientType=0 ); /* IE6-9 */
+    background: linear-gradient(top, #fefcea 0%,#f9e63b 7%,#e0a928 53%,#d69a2c 94%,#c97e16 99%); /* W3C */
+
+}
+
+body, textarea {
+    font-family: serif;
+    font-size: 25px;
+}
+
+textarea, .copy {
+    font-family: monospace;
+    color: black;
+}
+
+a {
+    color: #a00;
+}
+
+a:visited {
+    color: #c00;
+}
+
+a:hover {
+}
+
+a:focus {
+}
+
+#annotation table {
+    border-collapse: collapse;
+    margin-right: 1em;
+}
+
+#annotation td {
+    border: solid 1px;
+    width: 1em;
+    text-align: left;
+    font-size: 20px;
+    padding: 2px;
+    line-height: 100%;
+    text-shadow: 0px 0px 10px #730 ;
+    white-space: nowrap;
+    font-family: monospace;
+}
+
+#output {
+    text-align: center;
+    line-height: 150px;
+    font-size: 60px;
+    width: 100%;
+    text-shadow: 0px 0px 10px #730 ;
+}
+
+#output-box {
+    margin-bottom: 10px;
+    overflow: auto;
+}
+
+#input {
+    text-align: center;
+    width: 100%;
+}
+
+#about-box {
+    position: absolute;
+    bottom: 0;
+    right: 0;
+    padding-right: 1ex;
+    padding-bottom: .5ex;
+}
+
+#mode-box {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    padding-left: 1ex;
+    padding-bottom: .5ex;
+}
+
+.hbox {
+    display: -webkit-box;
+    display: -moz-box;
+    display: box;
+    -webkit-box-orient: horizontal;
+    -moz-box-orient: horizontal;
+    box-orient: horizontal;
+    -webkit-box-align: baseline;
+    -moz-box-align: baseline;
+    box-align: baseline;
+}
+.vbox {
+    display: -webkit-box;
+    display: -moz-box;
+    display: box;
+    -webkit-box-orient: vertical;
+    -moz-box-orient: vertical;
+    box-orient: vertical;
+}
+.strut {
+    -webkit-box-flex: 0;
+    -moz-box-flex: 0;
+    box-flex: 0;
+}
+.spring {
+    -webkit-box-flex: 1;
+    -moz-box-flex: 1;
+    box-flex: 1;
+}
+
+table {
+    padding: 4ex;
+}
+
+td {
+    width: 4em;
+}
+
diff --git a/test.html b/test.html
new file mode 100644 (file)
index 0000000..7035135
--- /dev/null
+++ b/test.html
@@ -0,0 +1,53 @@
+<html>
+    <head>
+        <meta http-equiv="content-type" content="text/html; charset=utf-8">
+        <title>Tengwar Transcriber</title>
+        <link rel="stylesheet" type="text/css" href="test.css">
+    </head>
+    <body>
+
+        <noscript>
+            <br>This transcriber makes extensive use
+            of JavaScript and very new CSS tricks.
+            You might try using the much older
+            <a href="http://tengwar.art.pl/tengwar/ott/start.php?l=en">
+                tengwar transcriber
+            </a> if your browser does not support
+            these features.
+        </noscript>
+
+        <div class="vbox" style="height: 100%; width: 100%">
+            <div class="vbox spring" id="output-box">
+                <div class="spring"></div>
+                <div id="output" class="strut tengwar"></div>
+                <div class="spring"></div>
+                <div class="strut hbox">
+                    <div class="spring"></div>
+                    <div id="annotation"></div>
+                    <div class="spring"></div>
+                </div>
+            </div>
+            <div class="strut hbox">
+                <div class="strut" style="width: 30ex"></div>
+                <div class="spring">
+                    <textarea id="input"></textarea>
+                </div>
+                <div class="strut" style="width: 30ex"></div>
+            </div>
+        </div>
+
+        <div id="mode-box">
+            <input type="radio" name="mode" id="input-general-use" value="general-use" checked>
+            <label for="input-general-use">General Use</label><br>
+            <input type="radio" name="mode" id="input-classical" value="classical">
+            <label for="input-classical">Classical</label><br>
+        </div>
+
+        <div id="about-box">
+            <a href="about.html">about</a>
+        </div>
+
+        <script src="node_modules/mr/bootstrap.js" data-module="test" charset="utf-8"></script>
+
+    </body>
+</html>
diff --git a/test.js b/test.js
new file mode 100644 (file)
index 0000000..d154e0f
--- /dev/null
+++ b/test.js
@@ -0,0 +1,26 @@
+
+var GeneralUse = require("./general-use");
+var Classical = require("./classical");
+
+var input = document.querySelector("#input");
+var generalUse = document.querySelector("#input-general-use");
+var classical = document.querySelector("#input-classical");
+var output = document.querySelector("#output");
+
+function update() {
+    var value = input.value;
+    var mode = generalUse.checked ? GeneralUse : Classical;
+    output.innerHTML = mode.transcribe(value);
+}
+
+function onupdate(event) {
+    update();
+}
+
+input.addEventListener("keyup", onupdate);
+input.addEventListener("keydown", onupdate);
+input.select();
+
+generalUse.addEventListener("change", onupdate);
+classical.addEventListener("change", onupdate);
+
index 5293a4c..f195577 100644 (file)
--- a/tests.js
+++ b/tests.js
@@ -87,3 +87,4 @@ Object.keys(tests).forEach(function (input) {
 
 if (require.main === module)
     require("test").run(exports);
+