Support for the FreeMonoTengwar font and ConScript encoding
[tengwarjs.git] / general-use.js
1
2 var TengwarAnnatar = require("./tengwar-annatar");
3 var Notation = require("./notation");
4 var Parser = require("./parser");
5 var makeDocumentParser = require("./document-parser");
6 var normalize = require("./normalize");
7 var punctuation = require("./punctuation");
8 var parseNumber = require("./numbers");
9
10 exports.name = "General Use Mode";
11
12 var defaults = {};
13 exports.makeOptions = makeOptions;
14 function makeOptions(options) {
15 options = options || defaults;
16 // legacy
17 if (options.blackSpeech) {
18 options.language = "blackSpeech";
19 }
20 if (options.language === "blackSpeech") {
21 options.language = "black-speech";
22 }
23 return {
24 font: options.font || TengwarAnnatar,
25 block: options.block,
26 plain: options.plain,
27 doubleNasalsWithTildeBelow: options.doubleNasalsWithTildeBelow,
28 // Any tengwa can be doubled by placing a tilde above, and any tengwa
29 // can be prefixed with the nasal from the same series by putting a
30 // tilde below. Doubled nasals have the special distinction that
31 // either of these rules might apply so the tilde can go either above
32 // or below.
33 // false: by default, place a tilde above doubled nasals.
34 // true: place the tilde below doubled nasals.
35 reverseCurls: options.reverseCurls || options.language === "black-speech",
36 // false: by default, o is forward, u is backward
37 // true: o is backward, u is forward
38 swapDotSlash: options.swapDotSlash,
39 // false: by default, e is a slash, i is a dot
40 // true: e is a dot, i is a slash
41 medialOre: options.medialOre || options.language === "black-speech",
42 // false: by default, ore only appears in final position
43 // true: ore also appears before consonants, as in the ring inscription
44 language: options.language,
45 // by default, no change
46 // "english": final e implicitly silent
47 // "black speech": sh is calma-extended, gh is ungwe-extended, as in
48 // the ring inscription
49 // not "black-speech": sh is harma, gh is unque
50 noAchLaut: options.noAchLaut,
51 // false: "ch" is interpreted as ach-laut, "cc" as "ch" as in "chew"
52 // true: "ch" is interpreted as "ch" as in chew
53 sHook: options.sHook,
54 // false: "is" is silme with I tehta
55 // true: "is" is short carrier with S hook and I tehta
56 tsdz: options.tsdz,
57 // false: "ts" and "dz" are rendered as separate characters
58 // true: "ts" is IPA "c" and "dz" is IPA "dʒ"
59 duodecimal: options.duodecimal
60 // false: numbers are decimal by default
61 // true: numbers are duodecimal by default
62 };
63 }
64
65 exports.transcribe = transcribe;
66 function transcribe(text, options) {
67 options = makeOptions(options);
68 var font = options.font;
69 return font.transcribe(parse(text, options), options);
70 }
71
72 exports.encode = encode;
73 function encode(text, options) {
74 options = makeOptions(options);
75 return Notation.encode(parse(text, options), options);
76 }
77
78 var parse = exports.parse = makeDocumentParser(parseNormalWord, makeOptions);
79
80 function parseNormalWord(callback, options) {
81 return normalize(parseWord(callback, options));
82 }
83
84 function parseWord(callback, options) {
85 var font = options.font;
86 var makeColumn = font.makeColumn;
87 return scanWord(function (word, rewind) {
88 if (options.language === "english") {
89 if (word === "of") {
90 return function (character) {
91 if (Parser.isBreak(character)) {
92 return scanWord(function (word, rewind) {
93 if (word === "the") {
94 return callback([
95 makeOfThe(makeColumn)
96 ]);
97 } else if (word === "the'") {
98 return callback([
99 makeOf(makeColumn),
100 makeThePrime(makeColumn)
101 ]);
102 } else if (word === "the''") {
103 return callback([
104 makeOf(makeColumn),
105 makeThePrime(makeColumn)
106 ]);
107 } else {
108 return rewind(callback([
109 makeOf(makeColumn)
110 ]));
111 }
112 });
113 } else {
114 return callback([makeOf(makeColumn)])(character);
115 }
116 }
117 } else if (word === "of'") {
118 return scanWord(function (word, rewind) {
119 if (word === "the") {
120 return callback([
121 makeOfPrime(makeColumn),
122 makeThe(makeColumn)
123 ]);
124 } else if (word === "the'") {
125 return callback([
126 makeOfPrime(makeColumn),
127 makeThePrime(makeColumn)
128 ]);
129 } else if (word === "the''") {
130 return callback([
131 makeOfPrime(makeColumn),
132 makeThePrimePrime(makeColumn)
133 ]);
134 } else {
135 return rewind(callback([
136 makeOfPrime(makeColumn)
137 ]));
138 }
139 });
140 } else if (word === "the") {
141 return callback([
142 makeThe(makeColumn)
143 ]);
144 } else if (word === "the'") {
145 return callback([
146 makeThePrime(makeColumn)
147 ]);
148 } else if (word === "the''") {
149 return callback([
150 makeThePrimePrime(makeColumn)
151 ]);
152 } else if (word === "of'the") {
153 return callback([
154 makeOf(makeColumn),
155 ])("t")("h")("e");
156 } else if (word === "of'the'") {
157 return callback([
158 makeOfPrime(makeColumn)
159 ])("t")("h")("e")("'");
160 } else if (word === "and") {
161 return callback([
162 makeAnd(makeColumn)
163 ]);
164 } else if (word === "and'") {
165 return callback([
166 makeAndPrime(makeColumn)
167 ]);
168 } else if (word === "and''") {
169 return callback([
170 makeAndPrimePrime(makeColumn)
171 ]);
172 } else if (word === "we") {
173 return callback([
174 makeColumn("vala", {from: "w"}),
175 makeColumn("short-carrier", {from: ""})
176 .addAbove("e", {from: "e"})
177 .varies()
178 ]);
179 } else if (word === "we'") { // Unattested, my invention - kriskowal
180 return callback([
181 makeColumn("vala", {from: "w", diphthong: true})
182 .addBelow("y", {from: "ē"})
183 ]);
184 }
185 }
186 if (book[word]) {
187 return callback(Notation.decodeWord(book[word], makeColumn), {
188 from: word
189 });
190 } else {
191 return callback(parseWordPiecewise(word, word.length, options), word);
192 }
193 }, options);
194 }
195
196 var book = {
197 "iant": "yanta;tinco:a,tilde-above",
198 "iaur": "yanta;vala:a;ore",
199 "baranduiniant": "umbar;romen:a;ando:a,tilde-above;anna:u;yanta;anto:a,tilde-above",
200 "ioreth": "yanta;romen:o;thule:e",
201 "noldo": "nwalme;lambe:o;ando;short-carrier:o",
202 "noldor": "nwalme;lambe:o;ando;ore:o"
203 };
204
205 // TODO Fix bug where "of", "the", and "and" decompose with following
206 // punctuation.
207 function scanWord(callback, options, word, rewind) {
208 word = word || "";
209 rewind = rewind || function (state) {
210 return state;
211 };
212 return function (character) {
213 if (Parser.isBreak(character)) {
214 return callback(word, rewind)(character);
215 } else {
216 return scanWord(callback, options, word + character, function (state) {
217 return rewind(state)(character);
218 });
219 }
220 };
221 }
222
223 var parseWordPiecewise = Parser.makeParser(function (callback, length, options) {
224 return parseWordTail(callback, length, options, []);
225 });
226
227 function parseWordTail(callback, length, options, columns, previous) {
228 return parseColumn(function (moreColumns) {
229 if (!moreColumns.length) {
230 return callback(columns);
231 } else {
232 return parseWordTail(
233 callback,
234 length,
235 options,
236 columns.concat(moreColumns),
237 moreColumns[moreColumns.length - 1] // previous
238 );
239 }
240 }, length, options, previous);
241 }
242
243 function makeOf(makeColumn) {
244 return makeColumn("umbar-extended", {from: "of"})
245 .varies();
246 }
247
248 function makeOfPrime(makeColumn) {
249 return makeOf(makeColumn)
250 .addAbove("o", {from: "o", silent: true})
251 .varies(); // TODO is this supposed to be u above?
252 }
253
254 function makeOfPrimePrime(makeColumn) {
255 return makeColumn("formen", {from: "f"})
256 .addAbove("o", {from: "o"});
257 }
258
259 function makeThe(makeColumn) {
260 return makeColumn("ando-extended", {from: "the"})
261 .varies();
262 }
263
264 function makeThePrime(makeColumn) {
265 return makeThe(makeColumn).addBelow("i-below", {from: ""})
266 .varies();
267 }
268
269 function makeThePrimePrime(makeColumn) {
270 return makeColumn("thule", {from: "th"}).addBelow("i-below", {from: "e", silent: true});
271 }
272
273 function makeOfThe(makeColumn) {
274 return makeColumn("umbar-extended", {from: "of the"})
275 .addTildeBelow({from: ""});
276 }
277
278 function makeAnd(makeColumn) {
279 return makeColumn("ando", {from: "and"})
280 .addTildeAbove({from: ""});
281 }
282
283 function makeAndPrime(makeColumn) {
284 return makeAnd(makeColumn)
285 .addBelow("i-below", {from: ""})
286 .varies();
287 }
288
289 function makeAndPrimePrime(makeColumn) {
290 return makeColumn("ando", {from: "d"})
291 .addTildeAbove("n", {from: "n"})
292 .addAbove("a", {from: "a"});
293 }
294
295 function parseColumn(callback, length, options, previous) {
296 var font = options.font;
297 var makeColumn = font.makeColumn;
298
299 return parseTehta(function (tehta, tehtaFrom) {
300 return parseTengwa(function (column, tehta, tehtaFrom) {
301 if (column) {
302 if (tehta) {
303 if (options.reverseCurls) {
304 tehta = reverseCurls[tehta] || tehta;
305 }
306 if (options.swapDotSlash) {
307 tehta = swapDotSlash[tehta] || tehta;
308 }
309 if (column.tengwa === "silme" && tehta && options.sHook) {
310 return callback([
311 makeColumn("short-carrier", {from: ""})
312 .addAbove(tehta, {from: tehtaFrom})
313 .addBelow("s", {from: "s"})
314 ]);
315 } else if (options.language === "english" && shorterVowels[tehta]) {
316 // doubled vowels are composed from individual letters,
317 // not long forms.
318 return callback([
319 makeColumn("long-carrier", {from: shorterVowels[tehta]})
320 .addAbove(shorterVowels[tehta], {from: shorterVowels[tehta]}),
321 column
322 ]);
323 } else if (canAddAboveTengwa(tehta) && column.canAddAbove(tehta)) {
324 column.addAbove(tehta, {from: tehtaFrom});
325 return parseTengwaAnnotations(function (column) {
326 return callback([column]);
327 }, column, length, options);
328 } else {
329 // some tengwar inherently lack space above them
330 // and cannot be reversed to make room.
331 // some long tehtar cannot be placed on top of
332 // a tengwa.
333 // put the previous tehta over the appropriate carrier
334 // then follow up with this tengwa.
335 return parseTengwaAnnotations(function (column) {
336 return callback([makeCarrier(tehta, tehtaFrom, options), column]);
337 }, column, length, options);
338 }
339 } else {
340 return parseTengwaAnnotations(function (column) {
341 return callback([column]);
342 }, column, length, options);
343 }
344 } else if (tehta) {
345 if (options.reverseCurls) {
346 tehta = reverseCurls[tehta] || tehta;
347 }
348 if (options.swapDotSlash) {
349 tehta = swapDotSlash[tehta] || tehta;
350 }
351 return parseTengwaAnnotations(function (carrier) {
352 return callback([carrier]);
353 }, makeCarrier(tehta, tehtaFrom, options), length, options);
354 } else {
355 return function (character) {
356 if (Parser.isBreak(character)) {
357 return callback([]);
358 } else if (/\d/.test(character)) {
359 return parseNumber(callback, options)(character);
360 } else if (punctuation[character]) {
361 return callback([makeColumn(punctuation[character], {from: character})]);
362 } else {
363 return callback([
364 makeColumn("ure", {from: character})
365 .addError(
366 "Cannot transcribe " +
367 JSON.stringify(character) +
368 " in General Use Mode"
369 )
370 ]);
371 }
372 };
373 }
374 }, options, tehta, tehtaFrom);
375 }, options);
376
377 }
378
379 function makeCarrier(tehta, tehtaFrom, options) {
380 var font = options.font;
381 var makeColumn = font.makeColumn;
382 if (tehta === "á") {
383 return makeColumn("wilya", {from: "a"})
384 .addAbove("a", {from: "a"});
385 } else if (shorterVowels[tehta]) {
386 return makeColumn("long-carrier", {from: tehtaFrom})
387 .addAbove(shorterVowels[tehta], {from: ""});
388 } else {
389 return makeColumn("short-carrier", {from: tehtaFrom})
390 .addAbove(tehta, {from: ""});
391 }
392 }
393
394 function parseTehta(callback, options) {
395 return function (character) {
396 var firstCharacter = character;
397 if (character === "ë" && options.language !== "english") {
398 character = "e";
399 }
400 if (character === "") {
401 return callback();
402 } else if (lengthenableVowels.indexOf(character) !== -1) {
403 return function (nextCharacter) {
404 if (nextCharacter === character) {
405 return callback(longerVowels[character], longerVowels[character]);
406 } else {
407 return callback(character, character)(nextCharacter);
408 }
409 };
410 } else if (nonLengthenableVowels.indexOf(character) !== -1) {
411 return callback(character, character);
412 } else {
413 return callback()(character);
414 }
415 };
416 }
417
418 var lengthenableVowels = "aeiou";
419 var longerVowels = {"a": "á", "e": "é", "i": "í", "o": "ó", "u": "ú"};
420 var nonLengthenableVowels = "aeióú";
421 var tehtarThatCanBeAddedAbove = "aeiouóú";
422 var vowels = "aeëiouáéíóú";
423 var shorterVowels = {"á": "a", "é": "e", "í": "i", "ó": "o", "ú": "u"};
424 var reverseCurls = {"o": "u", "u": "o", "ó": "ú", "ú": "ó"};
425 var swapDotSlash = {"i": "e", "e": "i"};
426
427 function canAddAboveTengwa(tehta) {
428 return tehtarThatCanBeAddedAbove.indexOf(tehta) !== -1;
429 }
430
431 function parseTengwa(callback, options, tehta, tehtaFrom) {
432 var font = options.font;
433 var makeColumn = font.makeColumn;
434 return function (character) {
435 if (character === "n") {
436 return function (character) {
437 if (character === "n") { // nn
438 if (options.doubleNasalsWithTildeBelow) {
439 return callback(
440 makeColumn("numen", {from: "n"})
441 .addTildeBelow({from: "n"}),
442 tehta,
443 tehtaFrom
444 );
445 } else {
446 return callback(
447 makeColumn("numen", {from: "n"})
448 .addTildeAbove({from: "n"}),
449 tehta,
450 tehtaFrom
451 );
452 }
453 } else if (character === "t") { // nt
454 return function (character) {
455 if (character === "h") { // nth
456 return callback(
457 makeColumn("thule", {from: "th"})
458 .addTildeAbove({from: "n"}),
459 tehta,
460 tehtaFrom
461 );
462 } else { // nt.
463 return callback(
464 makeColumn("tinco", {from: "t"})
465 .addTildeAbove({from: "n"}),
466 tehta,
467 tehtaFrom
468 )(character);
469 }
470 };
471 } else if (character === "d") { // nd
472 return callback(makeColumn("ando", {from: "d"}).addTildeAbove({from: "n"}), tehta, tehtaFrom);
473 } else if (character === "c") { // nc -> ñc
474 return callback(makeColumn("quesse", {from: "c"}).addTildeAbove({from: "ñ"}), tehta, tehtaFrom);
475 } else if (character === "g") { // ng -> ñg
476 return callback(makeColumn("ungwe", {from: "g"}).addTildeAbove({from: "ñ"}), tehta, tehtaFrom);
477 } else if (character === "j") { // nj
478 return callback(makeColumn("anca", {from: "j"}).addTildeAbove({from: "n"}), tehta, tehtaFrom);
479 } else if (character === "f") { // nf -> nv
480 return callback(makeColumn("numen", {from: "n"}), tehta, tehtaFrom)("v");
481 } else if (character === "w") { // nw -> ñw
482 return function (character) {
483 if (character === "a") { // nwa
484 return function (character) { // nwal
485 if (character === "l") {
486 return callback(makeColumn("nwalme", {from: "n"}).addAbove("w", {from: "w"}), tehta, tehtaFrom)("a")(character);
487 } else { // nwa.
488 return callback(makeColumn("numen", {from: "n"}).addAbove("w", {from: "w"}), tehta, tehtaFrom)("a")(character);
489 }
490 };
491 } else if (character === "nw'") { // nw' prime -> ñw
492 return callback(makeColumn("nwalme", {from: "ñ"}).addAbove("w", {from: "w"}), tehta, tehtaFrom);
493 } else { // nw.
494 return callback(makeColumn("numen", {from: "n"}).addAbove("w", {from: "w"}), tehta, tehtaFrom)(character);
495 }
496 };
497 } else { // n.
498 return callback(makeColumn("numen", {from: "n"}), tehta, tehtaFrom)(character);
499 }
500 };
501 } else if (character === "m") { // m
502 return function (character) {
503 if (character === "m") { // mm
504 if (options.doubleNasalsWithTildeBelow) {
505 return callback(makeColumn("malta", {from: "m"}).addTildeBelow({from: "m"}), tehta, tehtaFrom);
506 } else {
507 return callback(makeColumn("malta", {from: "m"}).addTildeAbove({from: "m"}), tehta, tehtaFrom);
508 }
509 } else if (character === "p") { // mp
510 // mph is simplified to mf using the normalizer (deprecated TODO)
511 return callback(makeColumn("parma", {from: "p"}).addTildeAbove({from: "m"}), tehta, tehtaFrom);
512 } else if (character === "b") { // mb
513 // mbh is simplified to mf using the normalizer (deprecated TODO)
514 return callback(makeColumn("umbar", {from: "b"}).addTildeAbove({from: "m"}), tehta, tehtaFrom);
515 } else if (character === "f") { // mf
516 return callback(makeColumn("formen", {from: "f"}).addTildeAbove({from: "m"}), tehta, tehtaFrom);
517 } else if (character === "v") { // mv
518 return callback(makeColumn("ampa", {from: "v"}).addTildeAbove({from: "m"}), tehta, tehtaFrom);
519 } else { // m.
520 return callback(makeColumn("malta", {from: "m"}), tehta, tehtaFrom)(character);
521 }
522 };
523 } else if (character === "ñ") { // ñ
524 return function (character) {
525 // ññ does not exist to the best of my knowledge
526 // ñw is handled naturally by following w
527 if (character === "c") { // ñc
528 return callback(makeColumn("quesse", {from: "c"}).addTildeAbove({from: "ñ"}), tehta, tehtaFrom);
529 } else if (character === "g") { // ñg
530 return callback(makeColumn("ungwe", {from: "g"}).addTildeAbove({from: "ñ"}), tehta, tehtaFrom);
531 } else { // ñ.
532 return callback(makeColumn("nwalme", {from: "ñ"}), tehta, tehtaFrom)(character);
533 }
534 };
535 } else if (character === "t") { // t
536 return function (character) {
537 if (character === "t") { // tt
538 return callback(makeColumn("tinco", {from: "t"}).addTildeBelow({from: "t"}), tehta, tehtaFrom);
539 } else if (character === "h") { // th
540 return callback(makeColumn("thule", {from: "th"}), tehta, tehtaFrom);
541 } else if (character === "c") { // tc
542 return function (character) {
543 if (character === "h") { // tch -> tinco calma
544 return callback(makeColumn("tinco", {from: "t"}), tehta, tehtaFrom)("c")("h")("'");
545 } else {
546 return callback(makeColumn("tinco", {from: "t"}), tehta, tehtaFrom)("c")(character);
547 }
548 };
549 } else if (character === "s" && options.tsdz) { // ts
550 return callback(makeColumn("calma", {from: "ts"}), tehta, tehtaFrom);
551 } else { // t.
552 return callback(makeColumn("tinco", {from: "t"}), tehta, tehtaFrom)(character);
553 }
554 };
555 } else if (character === "p") { // p
556 return function (character) {
557 // ph is simplified to f by the normalizer (deprecated)
558 if (character === "p") { // pp
559 return callback(makeColumn("parma", {from: "p"}).addTildeBelow({from: "p"}), tehta, tehtaFrom);
560 } else if (character === "h") { // ph
561 return callback(makeColumn("formen", {from: "ph"}), tehta, tehtaFrom);
562 } else { // p.
563 return callback(makeColumn("parma", {from: "p"}), tehta, tehtaFrom)(character);
564 }
565 };
566 } else if (character === "c") { // c
567 return function (character) {
568 // cw should be handled either by following-w or a subsequent
569 // vala
570 if (character === "c") { // ch as in charm
571 return callback(makeColumn("calma", {from: "cc"}), tehta, tehtaFrom);
572 } else if (character === "h") { // ch, ach-laut, as in bach
573 return Parser.countPrimes(function (primes) {
574 if (options.noAchLaut && !primes) {
575 return callback(makeColumn("calma", {from: "ch"}), tehta, tehtaFrom); // ch as in charm
576 } else {
577 return callback(makeColumn("hwesta", {from: "ch"}), tehta, tehtaFrom); // ch as in bach
578 }
579 });
580 } else { // c.
581 return callback(makeColumn("quesse", {from: "c"}), tehta, tehtaFrom)(character);
582 }
583 };
584 } else if (character === "d") {
585 return function (character) {
586 if (character === "d") { // dd
587 return callback(makeColumn("ando", {from: "d"}).addTildeBelow({from: "d"}), tehta, tehtaFrom);
588 } else if (character === "j") { // dj
589 return callback(makeColumn("anga", {from: "dj"}), tehta, tehtaFrom);
590 } else if (character === "z" && options.tsdz) { // dz
591 // TODO annotate dz to indicate that options.tsdz affects this cluster
592 return callback(makeColumn("anga", {from: "dz"}), tehta, tehtaFrom);
593 } else if (character === "h") { // dh
594 return callback(makeColumn("anto", {from: "dh"}), tehta, tehtaFrom);
595 } else { // d.
596 return callback(makeColumn("ando", {from: "d"}), tehta, tehtaFrom)(character);
597 }
598 };
599 } else if (character === "b") { // b
600 return function (character) {
601 // bh is simplified to v by the normalizer (deprecated)
602 if (character === "b") { // bb
603 return callback(makeColumn("umbar", {from: "b"}).addTildeBelow({from: "b"}), tehta, tehtaFrom);
604 } else if (character === "bh") { // bh
605 return callback(makeColumn("ampa", {from: "bh (v)"}), tehta, tehtaFrom);
606 } else { // b.
607 return callback(makeColumn("umbar", {from: "b"}), tehta, tehtaFrom)(character);
608 }
609 };
610 } else if (character === "g") { // g
611 return function (character) {
612 if (character === "g") { // gg
613 return callback(makeColumn("ungwe", {from: "g"}).addTildeBelow({from: "g"}), tehta, tehtaFrom);
614 } else if (character === "h") { // gh
615 if (options.language === "black-speech") {
616 return callback(makeColumn("ungwe-extended", {from: "gh"}), tehta, tehtaFrom);
617 } else {
618 return callback(makeColumn("unque", {from: "gh"}), tehta, tehtaFrom);
619 }
620 } else { // g.
621 return callback(makeColumn("ungwe", {from: "g"}), tehta, tehtaFrom)(character);
622 }
623 };
624 } else if (character === "f") { // f
625 return function (character) {
626 if (character === "f") { // ff
627 return callback(makeColumn("formen", {from: "f"}).addTildeBelow({from: "f"}), tehta, tehtaFrom);
628 } else { // f.
629 return callback(makeColumn("formen", {from: "f"}), tehta, tehtaFrom)(character);
630 }
631 };
632 } else if (character === "v") { // v
633 return callback(makeColumn("ampa", {from: "v"}), tehta, tehtaFrom);
634 } else if (character === "j") { // j
635 return callback(makeColumn("anca", {from: "j"}), tehta, tehtaFrom);
636 } else if (character === "s") { // s
637 return function (character) {
638 if (character === "s") { // ss
639 return Parser.countPrimes(function (primes) {
640 var tengwa = primes > 0 ? "silme-nuquerna" : "silme";
641 var tengwaFrom = primes > 0 ? "s′" : "s";
642 var column = makeColumn(tengwa, {from: tengwaFrom}).addTildeBelow({from: "s"});
643 if (primes === 0) {
644 column.varies();
645 }
646 if (primes > 1) {
647 column.addError("Silme does not have this many alternate forms.");
648 }
649 return callback(column, tehta, tehtaFrom);
650 });
651 } else if (character === "h") { // sh
652 if (options.language === "black-speech") {
653 return callback(makeColumn("calma-extended", {from: "sh"}), tehta, tehtaFrom);
654 } else {
655 return callback(makeColumn("harma", {from: "sh"}), tehta, tehtaFrom);
656 }
657 } else { // s.
658 return Parser.countPrimes(function (primes) {
659 var tengwa = primes > 0 ? "silme-nuquerna" : "silme";
660 var tengwaFrom = primes > 0 ? "s′" : "s";
661 var column = makeColumn(tengwa, {from: tengwaFrom});
662 if (primes === 0) {
663 column.varies();
664 }
665 if (primes > 1) {
666 column.addError("Silme does not have this many alternate forms.");
667 }
668 return callback(column, tehta, tehtaFrom);
669 })(character);
670 }
671 };
672 } else if (character === "z") { // z
673 return function (character) {
674 if (character === "z") { // zz
675 return Parser.countPrimes(function (primes) {
676 var tengwa = primes > 0 ? "esse-nuquerna" : "esse";
677 var column = makeColumn(tengwa, {from: "z"}).addTildeBelow({from: "z"});
678 if (primes === 0) {
679 column.varies();
680 }
681 if (primes > 1) {
682 column.addError("Esse does not have this many alternate forms.");
683 }
684 return callback(column, tehta, tehtaFrom);
685 });
686 } else { // z.
687 return Parser.countPrimes(function (primes) {
688 var tengwa = primes > 0 ? "esse-nuquerna" : "esse";
689 var column = makeColumn(tengwa, {from: "z"});
690 if (primes === 0) {
691 column.varies();
692 }
693 if (primes > 1) {
694 column.addError("Silme does not have this many alternate forms.");
695 }
696 return callback(column, tehta, tehtaFrom);
697 })(character);
698 }
699 };
700 } else if (character === "h") { // h
701 return function (character) {
702 if (character === "w") { // hw
703 return callback(makeColumn("hwesta-sindarinwa", {from: "hw"}), tehta, tehtaFrom);
704 } else { // h.
705 return callback(makeColumn("hyarmen", {from: "h"}), tehta, tehtaFrom)(character);
706 }
707 };
708 } else if (character === "r") { // r
709 return function (character) {
710 if (character === "r") { // rr
711 return callback(makeColumn("romen", {from: "r"}).addTildeBelow({from: "r"}), tehta, tehtaFrom);
712 } else if (character === "h") { // rh
713 return callback(makeColumn("arda", {from: "rh"}), tehta, tehtaFrom);
714 } else if (
715 Parser.isFinal(character) || (
716 options.medialOre &&
717 vowels.indexOf(character) === -1
718 )
719 ) { // r final (optionally r before consonant)
720 return callback(makeColumn("ore", {from: "r", final: true}), tehta, tehtaFrom)(character);
721 } else { // r.
722 return callback(makeColumn("romen", {from: "r"}), tehta, tehtaFrom)(character);
723 }
724 };
725 } else if (character === "l") {
726 return function (character) {
727 if (character === "l") { // ll
728 return callback(makeColumn("lambe", {from: "l"}).addTildeBelow({from: "l"}), tehta, tehtaFrom);
729 } else if (character === "h") { // lh
730 return callback(makeColumn("alda", {from: "lh"}), tehta, tehtaFrom);
731 } else { // l.
732 return callback(makeColumn("lambe", {from: "l"}), tehta, tehtaFrom)(character);
733 }
734 };
735 } else if (character === "i") { // i
736 return callback(makeColumn("anna", {from: "i", diphthong: true}), tehta, tehtaFrom);
737 } else if (character === "u") { // u
738 return callback(makeColumn("vala", {from: "u", diphthong: true}), tehta, tehtaFrom);
739 } else if (character === "w") { // w
740 return function (character) {
741 if (character === "h") { // wh
742 return callback(makeColumn("hwesta-sindarinwa", {from: "wh"}), tehta, tehtaFrom);
743 } else { // w.
744 return callback(makeColumn("vala", {from: "w", dipththong: true}), tehta, tehtaFrom)(character);
745 }
746 };
747 } else if (character === "e" && (!tehta || tehta === "a")) { // ae or e after consonants
748 return callback(makeColumn("yanta", {from: "e", diphthong: true}), tehta, tehtaFrom);
749 } else if (character === "ë") { // if "ë" makes it this far, it's a diaresis for english
750 return callback(makeColumn("short-carrier", {from: ""}).addAbove("e", {from: "e"}));
751 } else if (character === "y") {
752 return Parser.countPrimes(function (primes) {
753 if (primes === 0) {
754 return callback(makeColumn("wilya", {from: ""}).addBelow("y", {from: "y"}), tehta, tehtaFrom);
755 } else if (primes === 1) {
756 return callback(makeColumn("long-carrier", {from: "y"}).addAbove("i", {from: ""}), tehta, tehtaFrom);
757 } else {
758 return callback(makeColumn("ure", {from: "y"}).addError("Consonantal Y only has one variation"));
759 }
760 });
761 } else if (shorterVowels[character]) {
762 return callback(
763 makeCarrier(character, character, options)
764 .addAbove(shorterVowels[character], {from: ""}),
765 tehta,
766 tehtaFrom
767 );
768 } else if (character === "'" && options.language === "english" && tehta === "e") {
769 // final e' in english should be equivalent to diaresis
770 return callback(
771 makeColumn("short-carrier", {from: ""})
772 .addAbove("e", {from: "e"})
773 );
774 } else if (character === "" && options.language === "english" && tehta === "e") {
775 // tehta deliberately consumed in this one case, not passed forward
776 return callback(
777 makeColumn("short-carrier", {from: ""})
778 .addBelow("i-below", {from: "e", silent: true})
779 )(character);
780 } else {
781 return callback(null, tehta, tehtaFrom)(character);
782 }
783 };
784 }
785
786 exports.parseTengwaAnnotations = parseTengwaAnnotations;
787 function parseTengwaAnnotations(callback, column, length, options) {
788 return parseFollowingAbove(function (column) {
789 return parseFollowingBelow(function (column) {
790 return parseFollowing(callback, column);
791 }, column, length, options);
792 }, column);
793 }
794
795 // add a following-w above the current character if the next character is W and
796 // there is room for it.
797 function parseFollowingAbove(callback, column) {
798 if (column.canAddAbove("w", "w")) {
799 return function (character) {
800 if (character === "w") {
801 return callback(column.addAbove("w", {from: "e"}));
802 } else {
803 return callback(column)(character);
804 }
805 };
806 } else {
807 return callback(column);
808 }
809 }
810
811 function parseFollowingBelow(callback, column, length, options) {
812 return function (character) {
813 if (character === "ë" && options.language !== "english") {
814 character = "e";
815 }
816 if (character === "y" && column.canAddBelow("y")) {
817 return callback(column.addBelow("y", {from: "y"}));
818 } else if (character === "e" && column.canAddBelow("i-below")) {
819 return Parser.countPrimes(function (primes) {
820 return function (character) {
821 if (Parser.isFinal(character) && options.language === "english" && length > 2) {
822 if (primes === 0) {
823 return callback(
824 column.addBelow("i-below", {from: "e", silent: true})
825 .varies()
826 )(character);
827 } else {
828 if (primes > 1) {
829 column.addError("Following E has only one variation.");
830 }
831 return callback(column)("e")(character);
832 }
833 } else {
834 if (primes === 0) {
835 return callback(column.varies())("e")(character);
836 } else {
837 if (primes > 1) {
838 column.addError("Following E has only one variation.");
839 }
840 return callback(column.addBelow("i-below", {from: "e", eilent: true}))(character);
841 }
842 }
843 };
844 });
845 } else {
846 return callback(column)(character);
847 }
848 };
849 }
850
851 function parseFollowing(callback, column) {
852 return function (character) {
853 if (character === "s") {
854 if (column.canAddBelow("s")) {
855 return Parser.countPrimes(function (primes, rewind) {
856 if (primes === 0) {
857 return callback(column.addBelow("s", {from: "s"}).varies());
858 } else if (primes) {
859 if (primes > 1) {
860 column.addError("Only one alternate form for following S.");
861 }
862 return rewind(callback(column)("s"));
863 }
864 });
865 } else {
866 return Parser.countPrimes(function (primes, rewind) {
867 return function (character) {
868 if (Parser.isFinal(character)) { // end of word
869 if (column.canAddFollowing("s-final") && primes-- === 0) {
870 column.addFollowing("s-final", {from: "s"});
871 } else if (column.canAddFollowing("s-inverse") && primes -- === 0) {
872 column.addFollowing("s-inverse", {from: "s"});
873 if (column.canAddFollowing("s-final")) {
874 column.varies();
875 }
876 } else if (column.canAddFollowing("s-extended") && primes-- === 0) {
877 column.addFollowing("s-extended", {from: "s"});
878 if (column.canAddFollowing("s-inverse")) {
879 column.varies();
880 }
881 } else if (column.canAddFollowing("s-flourish") && primes-- === 0) {
882 column.addFollowing("s-flourish", {from: "s"});
883 if (column.canAddFollowing("s-extended")) {
884 column.varies();
885 }
886 } else {
887 // rewind primes for subsequent alterations
888 var state = callback(column)("s");
889 while (primes-- > 0) {
890 state = state("'");
891 }
892 return state;
893 }
894 return callback(column)(character);
895 } else {
896 return rewind(callback(column)("s"))(character);
897 }
898 };
899 });
900 }
901 } else {
902 return callback(column)(character);
903 }
904 };
905 }
906