Support for the FreeMonoTengwar font and ConScript encoding
[tengwarjs.git] / beleriand.js
1
2 // TODO parse following "w"
3
4 var TengwarParmaite = require("./tengwar-parmaite");
5 var Parser = require("./parser");
6 var Notation = require("./notation");
7 var makeDocumentParser = require("./document-parser");
8 var normalize = require("./normalize");
9 var punctuation = require("./punctuation");
10 var parseNumber = require("./numbers");
11
12 exports.name = "Mode of Beleriand";
13
14 var defaults = {};
15 exports.makeOptions = makeOptions;
16 function makeOptions(options) {
17 options = options || defaults;
18 return {
19 font: options.font || TengwarParmaite,
20 block: options.block,
21 plain: options.plain,
22 duodecimal: options.duodecimal
23 };
24 }
25
26 exports.transcribe = transcribe;
27 function transcribe(text, options) {
28 options = makeOptions(options);
29 var font = options.font;
30 return font.transcribe(parse(text, options), options);
31 }
32
33 exports.encode = encode;
34 function encode(text, options) {
35 options = makeOptions(options);
36 return Notation.encode(parse(text, options), options);
37 }
38
39 var parse = exports.parse = makeDocumentParser(parseNormalWord, makeOptions);
40
41 function parseNormalWord(callback, options) {
42 return normalize(parseWord(callback, options));
43 }
44
45 function parseWord(callback, options, columns) {
46 columns = columns || [];
47 return parseColumn(function (column) {
48 if (column) {
49 return parseWord(
50 callback,
51 options,
52 columns.concat([column])
53 );
54 } else {
55 return function (character) {
56 if (/\d/.test(character)) {
57 return parseNumber(function (number) {
58 return parseWord(callback, options, columns.concat(number));
59 }, options)(character);
60 } else {
61 return callback(columns)(character);
62 }
63 };
64 }
65 }, options);
66 }
67
68 function parseColumn(callback, options) {
69 return parseTengwa(function (column) {
70 if (column) {
71 return parseFollowingS(callback, column);
72 } else {
73 return callback();
74 }
75 }, options);
76 }
77
78 function parseTengwa(callback, options) {
79 var font = options.font;
80 var makeColumn = font.makeColumn;
81 return function (character) {
82 if (character === "n") { // n
83 return function (character) {
84 if (character === "t" || character === "d") { // n{t,d}
85 return parseTengwa(function (column) {
86 return callback(column.addTildeAbove());
87 }, options)(character);
88 } else if (character === "c" || character === "g") { // n{c,g}
89 return parseTengwa(callback, options)("ñ")(character);
90 } else if (character === "n") { // nn
91 return callback(makeColumn("numen", {from : "nn"}));
92 } else { // n.
93 return callback(makeColumn("ore", {from: "n"}))(character);
94 }
95 };
96 } else if (character === "t") { // t
97 return function (character) {
98 if (character === "h") { // th
99 return callback(makeColumn("thule", {from: "th"}));
100 } else { // t.
101 return callback(makeColumn("tinco", {from: "t"}))(character);
102 }
103 };
104 } else if (character === "d") { // d
105 return function (character) {
106 if (character === "h") { // dh
107 return callback(makeColumn("anto", {from: "dh"}));
108 } else { // d.
109 return callback(makeColumn("ando", {from: "d"}))(character);
110 }
111 };
112 } else if (character === "m") { // m
113 return function (character) {
114 if (
115 character === "p" || character === "b" ||
116 character === "f" || character === "v"
117 ) {
118 return parseTengwa(function (column) {
119 return callback(column.addTildeAbove({from: character}));
120 }, options)(character);
121 } else if (character === "m") { // mm
122 return callback(makeColumn("malta", {from: "mm"}));
123 } else { // m.
124 return callback(makeColumn("vala", {from: "m"}))(character);
125 }
126 };
127 } else if (character === "p") { // p
128 return callback(makeColumn("parma", {from: "p"}));
129 } else if (character === "b") { // b
130 return callback(makeColumn("umbar", {from: "b"}));
131 } else if (character === "f") { // f
132 return function (character) {
133 if (Parser.isFinal(character)) { // f final
134 return callback(makeColumn("ampa", {from: "f", final: true}))(character);
135 } else {
136 return callback(makeColumn("formen", {from: "f", medial: true}))(character);
137 }
138 };
139 } else if (character === "v") { // v
140 return callback(makeColumn("ampa", {from: "v"}));
141 } else if (character === "ñ") { // ñ
142 return function (character) {
143 if (character === "c" || character === "g") {
144 return parseTengwa(function (column) {
145 if (column.tengwa === "halla") {
146 column.addError("Lenited G (halla) should not be nasalized with prefix N");
147 }
148 return callback(column.addTildeAbove({from: characte}));
149 }, options)(character);
150 } else { // ñ.
151 return callback(makeColumn("noldo", {from: "ñ"}))(character);
152 }
153 };
154 } else if (character === "c") { // c
155 return function (character) {
156 if (character === "h") { // ch
157 return function (character) {
158 if (character === "w") { // chw
159 return callback(makeColumn("hwesta", {from: "chw"}));
160 } else { // ch.
161 return callback(makeColumn("harma", {from: "ch"}))(character);
162 }
163 };
164 } else if (character === "w") { // cw
165 return callback(makeColumn("quesse", {from: "cw"}));
166 } else { // c.
167 return callback(makeColumn("calma", {from: "c"}))(character);
168 }
169 };
170 } else if (character === "g") {
171 return function (character) {
172 if (character === "h") { // gh
173 return function (character) {
174 if (character === "w") { // ghw
175 return callback(makeColumn("unque", {from: "ghw"}));
176 } else { // gh.
177 return callback(makeColumn("anca", {from: "gh"}))(character);
178 }
179 };
180 } else if (character === "w") { // gw
181 return callback(makeColumn("ungwe", {from: "gw"}));
182 } else if (character === "'") { // g'
183 return callback(makeColumn("halla", {from: "g"})); // gasdil
184 } else { // g.
185 return callback(makeColumn("anga", {from: "g"}).varies())(character);
186 }
187 };
188 } else if (character === "r") { // r
189 return function (character) {
190 if (character === "h") { // rh
191 return callback(makeColumn("arda", {from: "rh"}));
192 } else {
193 return callback(makeColumn("romen", {from: "r"}))(character);
194 }
195 };
196 } else if (character === "l") { // l
197 return function (character) {
198 if (character === "h") { // lh
199 return callback(makeColumn("alda", {from: "lh"}));
200 } else {
201 return callback(makeColumn("lambe", {from: "l"}))(character);
202 }
203 };
204 } else if (character === "s") { // s
205 return callback(makeColumn("silme", {from: "s"}));
206 } else if (character === "a") { // a
207 return function (character) {
208 if (character === "i") { // ai
209 return callback(makeColumn("round-carrier", {from: "a", diphthong: true}).addAbove("í", {from: "i"}));
210 } else if (character === "u") { // au
211 return callback(makeColumn("round-carrier", {from: "a", diphthong: true}).addAbove("w", {from: "u"}));
212 } else if (character === "'") { // a'
213 return callback(makeColumn("round-carrier", {from: "a"}).addAbove("i", {from: "a"}));
214 } else if (character === "a") { // aa
215 return callback(makeColumn("round-carrier", {from: "a", long: true}).addAbove("e", {from: "a"}));
216 } else { // a.
217 return callback(makeColumn("round-carrier", {from: "a"}).varies())(character);
218 }
219 };
220 } else if (character === "e" || character === "ë") { // e
221 return function (character) {
222 if (character === "i") { // ei
223 return callback(makeColumn("yanta", {from: "e"}).addAbove("í", {from: "i"}));
224 } else if (character === "e") {
225 return callback(makeColumn("yanta", {from: "e", long: true}).addAbove("e", {from: "e"}));
226 } else { // e.
227 return callback(makeColumn("yanta", {from: "e"}))(character);
228 }
229 };
230 } else if (character === "i") { // i
231 return function (character) {
232 if (character === "i") { // ii -> í
233 return parseColumn(callback, options)("í");
234 } else {
235 return Parser.countPrimes(function (primes) {
236 if (primes === 0) {
237 return callback(makeColumn("short-carrier", {from: "i"}).varies());
238 } else if (primes === 1) {
239 return callback(makeColumn("short-carrier", {from: "i"}).addAbove("i", {from: ""}).varies());
240 } else if (primes === 2) {
241 return callback(makeColumn("long-carrier", {from: "i", long: true}).addAbove("i", {from: ""}).varies());
242 } else if (primes === 3) {
243 return callback(makeColumn("long-carrier", {from: "i", long: true}));
244 } else {
245 return callback(makeColumn("long-carrier").addAbove("i").addError("I only has four variants between short or long and dotted or not."));
246 }
247 })(character);
248 }
249 };
250 } else if (character === "o") {
251 return function (character) {
252 if (character === "o") { // oo
253 return callback(makeColumn("anna", {from: "o"}).addAbove("e", {from: "o"}));
254 } else {
255 return callback(makeColumn("anna", {from: "o"}))(character);
256 }
257 };
258 } else if (character === "u") {
259 return function (character) {
260 if (character === "i") {
261 return callback(makeColumn("ure", {from: "u", diphthong: true}).addAbove("í", {from: "i"}));
262 } else if (character === "u") {
263 return callback(makeColumn("ure", {from: "u", long: true}).addAbove("e", {from: "u"}));
264 } else {
265 return callback(makeColumn("ure", {from: "u"}))(character);
266 }
267 };
268 } else if (character === "w") { // w
269 return function (character) {
270 if (character === "w") { // ww
271 return callback(makeColumn("wilya", {from: "w"}).addAbove("e", {from: "w"}));
272 } else { // w.
273 return callback(makeColumn("wilya", {from: "w"}))(character);
274 }
275 };
276 } else if (character === "y") {
277 return function (character) {
278 if (character === "y") { // yy
279 return callback(makeColumn("silme-nuquerna", {from: "y"}).addAbove("e", {from: "y"}));
280 } else { // y.
281 return callback(makeColumn("silme-nuquerna", {from: "y"}))(character);
282 }
283 };
284 } else if (character === "á") {
285 return callback(makeColumn("round-carrier", {from: "a", long: true}).addAbove("e", {from: "a"}));
286 } else if (character === "é") {
287 return callback(makeColumn("yanta", {from: "é"}).addAbove("e"));
288 } else if (character === "í") {
289 return Parser.countPrimes(function (primes) {
290 if (primes === 0) {
291 return callback(makeColumn("short-carrier", {from: "i"}).addAbove("e", {from: "i"}).varies());
292 } else if (primes === 1) {
293 return callback(makeColumn("long-carrier", {from: "í", long: true}).addAbove("e", {from: ""}));
294 } else {
295 return callback(makeColumn("long-carrier").addAbove("e").addError("Í only has one variant."));
296 }
297 });
298 } else if (character === "ó") {
299 return callback(makeColumn("anna", {from: "ó", long: true}).addAbove("e", {from: ""}));
300 } else if (character === "ú") {
301 return callback(makeColumn("ure", {from :"ú", long: true}).addAbove("e", {from: ""}));
302 } else if (character === "h") {
303 return function (character) {
304 //if (character === "m") { // TODO
305 // return callback(makeColumn("ore-nasalized"));
306 if (character === "w") {
307 return callback(makeColumn("hwesta-sindarinwa", {from: "hw"}));
308 } else {
309 return callback(makeColumn("hyarmen", {from: "h"}))(character);
310 }
311 };
312 } else if (character === "z") {
313 return callback(makeColumn("silme", {from: "z"}).addError("Z does not appear in the mode of Beleriand"));
314 } else if (punctuation[character]) {
315 return callback(makeColumn(punctuation[character], {from: character}));
316 } else if (Parser.isBreak(character) || /\d/.test(character)) {
317 return callback()(character);
318 } else {
319 return callback(makeColumn("anna", {from: character}).addError("Unexpected character: " + JSON.stringify(character)));
320 }
321 };
322 }
323
324 function parseFollowingS(callback, column) {
325 return function (character) {
326 if (character === "s") {
327 if (column.canAddBelow("s")) {
328 return callback(column.addBelow("s", {from: "s"}));
329 } else {
330 return Parser.countPrimes(function (primes) {
331 return function (character) {
332 if (Parser.isFinal(character)) { // end of word
333 if (column.canAddFollowing("s-final") && primes-- === 0) {
334 column.addFollowing("s-final", {from: "s"});
335 } else if (column.canAddFollowing("s-inverse") && primes -- === 0) {
336 column.addFollowing("s-inverse");
337 } else if (column.canAddFollowing("s-extended") && primes-- === 0) {
338 column.addFollowing("s-extended", {from: "s"});
339 } else if (column.canAddFollowing("s-flourish")) {
340 column.addFollowing("s-flourish", {from: "s"});
341 if (primes > 0) {
342 column.addError(
343 "Following S only has 3 alternate " +
344 "flourishes."
345 );
346 }
347 } else {
348 return callback(column)("s", {from: "s"})(character);
349 }
350 return callback(column)(character);
351 } else {
352 return callback(column)("s", {from: "s"})(character);
353 }
354 };
355 });
356 }
357 } else {
358 return callback(column)(character);
359 }
360 };
361 }
362