Support for the FreeMonoTengwar font and ConScript encoding
[tengwarjs.git] / classical.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 = "Classical Mode";
11
12 var defaults = {};
13 exports.makeOptions = makeOptions;
14 function makeOptions(options) {
15 options = options || defaults;
16 return {
17 font: options.font || TengwarAnnatar,
18 block: options.block,
19 plain: options.plain,
20 vilya: options.vilya,
21 // false: (v: vala, w: wilya)
22 // true: (v: vilya, w: ERROR)
23 harma: options.harma,
24 // between the original formation of the language,
25 // but before the third age,
26 // harma was renamed aha,
27 // and meant breath-h in initial position
28 classicalH: options.classicalH,
29 classicalR: options.classicalR,
30 // before the third age
31 // affects use of "r" and "h"
32 // without classic, we default to the mode from the namarie poem.
33 // in the classical period, "r" was transcribed as "ore" only between
34 // vowels.
35 // in the third age, through the namarie poem, "r" is only "ore" before
36 // consontants and at the end of words.
37 swapDotSlash: options.swapDotSlash,
38 // false: by default, e is a slash, i is a dot
39 // true: e is a dot, i is a slash
40 // TODO figure out "h"
41 reverseCurls: options.reverseCurls,
42 // false: by default, o is forward, u is backward
43 // true: o is backward, u is forward
44 iuRising: options.iuRising,
45 // iuRising thirdAge: anna:y,u
46 // otherwise: ure:i
47 // in the third age, "iu" is a rising diphthong,
48 // whereas all others are falling. rising means
49 // that they are stressed on the second sound, as
50 // in "yule". whether to use yanta or anna is
51 // not attested.
52 longHalla: options.longHalla,
53 // TODO indicates that halla should be used before medial L and W to
54 // indicate that these are pronounced with length.
55 // initial hl and hw remain short.
56 // TODO doubled dots for í
57 // TODO triple dots for y
58 // TODO simplification of a, noting non-a
59 // TODO following W in this mode?
60 // TODO namarië does not use double U or O curls
61 // TODO namarië does not reverse esse for E tehta
62 duodecimal: options.duodecimal
63 };
64 };
65
66 exports.transcribe = transcribe;
67 function transcribe(text, options) {
68 options = makeOptions(options);
69 var font = options.font;
70 return font.transcribe(parse(text, options), options);
71 }
72
73 exports.encode = encode;
74 function encode(text, options) {
75 options = makeOptions(options);
76 return Notation.encode(parse(text, options), options);
77 }
78
79 var parse = exports.parse = makeDocumentParser(parseNormalWord, makeOptions);
80
81 function parseNormalWord(callback, options) {
82 return normalize(parseWord(callback, options));
83 }
84
85 function parseWord(callback, options, columns, previous) {
86 columns = columns || [];
87 return parseColumn(function (moreColumns) {
88 if (!moreColumns.length) {
89 return callback(columns);
90 } else {
91 return parseWord(
92 callback,
93 options,
94 columns.concat(moreColumns),
95 moreColumns[moreColumns.length - 1] // previous
96 );
97 }
98 }, options, previous);
99 }
100
101 function parseColumn(callback, options, previous) {
102 var font = options.font;
103 var makeColumn = font.makeColumn;
104 return parseTengwa(function (columns) {
105 var previous = columns.pop();
106 return parseTehta(function (next) {
107 var next = columns.concat(next).filter(Boolean)
108 if (next.length) {
109 return callback(next);
110 } else {
111 return function (character) {
112 if (Parser.isBreak(character)) {
113 return callback([])(character);
114 } else if (/\d/.test(character)) {
115 return parseNumber(callback, options)(character);
116 } else if (punctuation[character]) {
117 return callback([makeColumn(punctuation[character], {from: character})]);
118 } else {
119 return callback([makeColumn("ure", {from: ""}).addError(
120 "Cannot transcribe " + JSON.stringify(character) +
121 " in Classical Mode"
122 )]);
123 }
124 };
125 }
126 }, options, previous);
127 }, options, previous);
128 }
129
130 var vowels = "aeiouyáéíóú";
131
132 function parseTengwa(callback, options, previous) {
133 var font = options.font;
134 var makeColumn = font.makeColumn;
135 return function (character) {
136 if (character === "n") { // n
137 return function (character) {
138 if (character === "n") { // nn
139 return callback([makeColumn("numen", {from: "n"}).addTildeBelow({from: "n"})]);
140 } else if (character === "t") { // nt
141 return callback([makeColumn("anto", {from: "nt"})]);
142 } else if (character === "d") { // nd
143 return callback([makeColumn("ando", {from: "nd"})]);
144 } else if (character === "g") { // ng
145 return function (character) {
146 if (character === "w") { // ngw -> ñw
147 return callback([makeColumn("ungwe", {from: "ñgw"})]);
148 } else { // ng
149 return callback([makeColumn("anga", {from: "ñg"})])(character);
150 }
151 };
152 } else if (character === "c") { // nc
153 return function (character) {
154 if (character === "w") { // ncw
155 return callback([makeColumn("unque", {from: "ñcw"})]);
156 } else { // nc
157 return callback([makeColumn("anca", {from: "ñc"})])(character);
158 }
159 };
160 } else {
161 return callback([makeColumn("numen", {from: "n"})])(character);
162 }
163 };
164 } else if (character === "m") {
165 return function (character) {
166 if (character === "m") { // mm
167 return callback([makeColumn("malta", {from: "m"}).addTildeBelow({from: "m"})]);
168 } else if (character === "p") { // mp
169 return callback([makeColumn("ampa", {from: "mp"})]);
170 } else if (character === "b") { // mb
171 return callback([makeColumn("umbar", {from: "mb"})]);
172 } else {
173 return callback([makeColumn("malta", {from: "m"})])(character);
174 }
175 };
176 } else if (character === "ñ") { // ñ
177 return function (character) {
178 if (character === "g") { // ñg
179 return function (character) {
180 if (character === "w") { // ñgw
181 return callback([makeColumn("ungwe", {from: "ñgw"})]);
182 } else { // ñg
183 return callback([makeColumn("anga", {from: "ñg"})])(character);
184 }
185 }
186 } else if (character === "c") { // ñc
187 return function (character) {
188 if (character === "w") { // ñcw
189 return callback([makeColumn("unque", {from: "ñcw"})]);
190 } else { // ñc
191 return callback([makeColumn("anca", {from: "ñc"})]);
192 }
193 }
194 } else {
195 return callback([makeColumn("noldo", {from: "ñ"})])(character);
196 }
197 };
198 } else if (character === "t") {
199 return function (character) {
200 if (character === "t") { // tt
201 return function (character) {
202 if (character === "y") { // tty
203 return callback([makeColumn("tinco", {from: "t"}).addBelow("y", {from: "y"}).addTildeBelow({from: "t"})]);
204 } else { // tt
205 return callback([makeColumn("tinco", {from: "t"}).addTildeBelow({from: "t"})])(character);
206 }
207 };
208 } else if (character === "y") { // ty
209 return callback([makeColumn("tinco", {from: "t"}).addBelow("y", {from: "y"})]);
210 } else if (character === "h") { // th
211 return callback([makeColumn("thule", {from: "th"})]);
212 } else if (character === "s") {
213 return function (character) {
214 // TODO s-inverse, s-extended, s-flourish
215 if (Parser.isFinal(character)) { // ts final
216 return callback([makeColumn("tinco", {from: "t"}).addFollowing("s", {from: "s"})])(character);
217 } else { // ts medial
218 return callback([
219 makeColumn("tinco", {from: "t"}),
220 makeColumn("silme", {from: "s"})
221 ])(character);
222 }
223 };
224 } else { // t
225 return callback([makeColumn("tinco", {from: "t"})])(character);
226 }
227 };
228 } else if (character === "p") {
229 return function (character) {
230 if (character === "p") {
231 return function (character) {
232 if (character === "y") { // ppy
233 return callback([makeColumn("parma", {from: "p"}).addBelow("y", {from: "y"}).addTildeBelow({from: "p"})]);
234 } else { // pp
235 return callback([makeColumn("parma", {from: "p"}).addTildeBelow({from: "p"})])(character);
236 }
237 };
238 } else if (character === "y") { // py
239 return callback([makeColumn("parma", {from: "p"}).addBelow("y", {from: "y"})]);
240 } else if (character === "s") { // ps
241 return function (character) {
242 if (Parser.isFinal(character)) { // ps final
243 return callback([makeColumn("parma", {from: "p"}).addFollowing("s", {from: "s"})])(character);
244 } else { // ps medial
245 return callback([
246 makeColumn("parma", {from: "p"}),
247 makeColumn("silme", {from: "s"})
248 ])(character);
249 }
250 };
251 } else { // t
252 return callback([makeColumn("parma", {from: "p"})])(character);
253 }
254 };
255 } else if (character === "c") {
256 return function (character) {
257 if (character === "c") {
258 return callback([makeColumn("calma", {from: "c"}).addTildeBelow({from: "c"})]);
259 } else if (character === "s") {
260 return callback([makeColumn("calma", {from: "s"}).addBelow("s", {from: "s"})]);
261 } else if (character === "h") {
262 return callback([makeColumn("harma", {from: "ch"})]);
263 } else if (character === "w") {
264 return callback([makeColumn("quesse", {from: "chw"})]);
265 } else {
266 return callback([makeColumn("calma", {from: "c"})])(character);
267 }
268 };
269 } else if (character === "f") {
270 return callback([makeColumn("formen", {from: "f"})]);
271 } else if (character === "v") {
272 if (options.vilya) {
273 return callback([makeColumn("wilya", {from: "v", name: "vilya"})]);
274 } else {
275 return callback([makeColumn("vala", {from: "v", name: "vala"})]);
276 }
277 } else if (character === "w") {
278 if (options.vilya) {
279 return callback([])("u");
280 } else {
281 // TODO Fact-check this interpretation. It may be an error to
282 // use w as a consonant depending on whether we're speaking
283 // early or late classical.
284 return callback([makeColumn("wilya", {from: "w", name: "vilya"})]);
285 }
286 } else if (character === "r") { // r
287 return function (character) {
288 if (character === "d") { // rd
289 return callback([makeColumn("arda", {from: "rd"})]);
290 } else if (character === "h") { // rh -> hr
291 var error = "R should preceed H in the HR diagraph in Classical mode.";
292 return callback([
293 makeColumn("halla", {from: "h"}).addError(error),
294 makeColumn("romen", {from: "r"}).addError(error)
295 ]);
296 } else if (options.classicalR) {
297 // pre-namarie style, ore when r between vowels
298 if (
299 previous &&
300 previous.above &&
301 !Parser.isFinal(character) &&
302 vowels.indexOf(character) !== -1
303 ) {
304 return callback([makeColumn("ore", {from: "r"})])(character);
305 } else {
306 return callback([makeColumn("romen", {from: "r"})])(character);
307 }
308 } else {
309 // pre-consonant and word-final
310 if (Parser.isFinal(character) || vowels.indexOf(character) === -1) { // ore
311 return callback([makeColumn("ore", {from: "r"})])(character);
312 } else { // romen
313 return callback([makeColumn("romen", {from: "r"})])(character);
314 }
315 }
316 };
317 } else if (character === "l") {
318 return function (character) {
319 if (character === "l") {
320 return function (character) {
321 if (character === "y") { // lly
322 return callback([makeColumn("lambe", {from: "l"}).addBelow("y", {from: "y"}).addTildeBelow({from: "l"})]);
323 } else { // ll
324 return callback([makeColumn("lambe", {from: "l"}).addTildeBelow({from: "y"})])(character);
325 }
326 }
327 } else if (character === "y") { // ly
328 return callback([makeColumn("lambe", {from: "l"}).addBelow("y", {from: "y"})]);
329 } else if (character === "h") { // lh -> hl
330 var error = "L should preceed H in the HL diagraph in Classical mode.";
331 return callback([
332 makeColumn("halla", {from: "h"}).addError(error),
333 makeColumn("lambe", {from: "l"}).addError(error)
334 ]);
335 } else if (character === "d") { // ld
336 return callback([makeColumn("alda", {from: "ld"})]);
337 } else if (character === "b") { // lb
338 // TODO ascertain why this is a special case and make a note.
339 return callback([makeColumn("lambe", {from: "l"}), makeColumn("umbar", {from: "b"})]);
340 } else {
341 return callback([makeColumn("lambe", {from: "l"})])(character);
342 }
343 };
344 } else if (character === "s") {
345 return function (character) {
346 if (character === "s") { // ss
347 return callback([makeColumn("esse", {from: "ss"})]);
348 } else { // s.
349 return callback([makeColumn("silme", {from: "s"})])(character);
350 }
351 // Note that there is no sh phoneme in Classical Elvish languages
352 };
353 } else if (character === "h") {
354 return function (character) {
355 if (character === "l") { // hl
356 return callback([
357 makeColumn("halla", {from: "h"}),
358 makeColumn("lambe", {from: "l"})
359 ]);
360 } else if (character === "r") {
361 return callback([
362 makeColumn("halla", {from: "h"}),
363 makeColumn("romen", {from: "r"})
364 ]);
365 } else if (character === "w") { // hw
366 return callback([makeColumn("hwesta", {from: "hw"})]);
367 } else if (character === "t") { // ht
368 // TODO find a reference and example that substantiates
369 // this interpretation. Did I invent this to make harma
370 // expressible?
371 return callback([makeColumn("harma", {from: "ht"})]);
372 } else if (character === "y") { // hy
373 if (options.classicalH && !options.harma) { // oldest form
374 return callback([makeColumn("hyarmen", {from: "hy"})]);
375 } else { // post-aha, through to the third-age
376 return callback([makeColumn("hyarmen", {from: "hy"}).addBelow("y", {from: "y"})]);
377 }
378 } else { // h
379 if (options.classicalH) {
380 if (options.harma) { // before harma became aha initially
381 if (previous) { // medial
382 return callback([makeColumn("halla", {from: "h"})])(character);
383 } else { // initial
384 return callback([makeColumn("harma", {from: "h"})])(character);
385 }
386 } else { // harmen renamed and resounded as aha in initial position
387 if (previous) { // medial
388 return callback([makeColumn("hyarmen", {from: "h"})])(character);
389 } else { // initial
390 return callback([makeColumn("halla", {from: "h"})])(character);
391 }
392 }
393 } else { // third age, namarië
394 return callback([makeColumn("hyarmen", {from: "h"})])(character);
395 }
396 }
397 };
398 } else if (character === "d") {
399 return callback([makeColumn("ando", {from: "d"}).addError("D cannot appear except after N, L, or R in Classical Mode")]);
400 } else if (character === "b") {
401 return callback([makeColumn("umbar", {from: "b"}).addError("B cannot appear except after M or L in Classical Mode")]);
402 } else if (character === "g") {
403 return callback([makeColumn("anga", {from: "g"}).addError("G cannot appear except after N or Ñ in Classical Mode")]);
404 } else if (character === "j") {
405 return callback([makeColumn("ure", {from: "j"}).addError("J cannot be transcribed in Classical Mode")]);
406 } else {
407 return callback([])(character);
408 }
409 };
410 }
411
412 function parseTehta(callback, options, previous) {
413 var font = options.font;
414 var makeColumn = font.makeColumn;
415 return function (character) {
416 if (character === "a") {
417 return function (character) {
418 if (character === "a") {
419 return parseTehta(callback, options, previous)("á");
420 } else if (character === "i") {
421 return callback([previous, makeColumn("yanta", {from: "i", diphthong: true}).addAbove("a", {from: "a"})]);
422 } else if (character === "u") {
423 return callback([previous, makeColumn("ure", {from: "u", diphthong: true}).addAbove("a", {from: "a"})]);
424 } else if (previous && previous.canAddAbove("a")) {
425 return callback([previous.addAbove("a", {from: "a"})])(character);
426 } else {
427 return callback([previous, makeColumn("short-carrier", {from: "a"}).addAbove("a", {from: ""})])(character);
428 }
429 };
430 } else if (character === "e" || character === "ë") {
431 var tehta = swapDotSlash("e", options);
432 return function (character) {
433 if (character === "e") {
434 return parseTehta(callback, options, previous)("é");
435 } else if (character === "u") {
436 return callback([previous, makeColumn("ure", {from: "u", diphthong: true}).addAbove(tehta, {from: "e"})]);
437 } else if (previous && previous.canAddAbove("e")) {
438 return callback([previous.addAbove(tehta, {from: "e"})])(character);
439 } else {
440 return callback([previous, makeColumn("short-carrier", {from: "e"}).addAbove(tehta, {from: ""})])(character);
441 }
442 };
443 } else if (character === "i") {
444 var iTehta = swapDotSlash("i", options);
445 return function (character) {
446 if (character === "i") {
447 return parseTehta(callback, options, previous)("í");
448 } else if (character === "u") {
449 if (options.iuRising) {
450 return callback([previous, makeColumn("anna", {from: "i", diphthong: true}).addAbove(reverseCurls("u", options), {from: "u"}).addBelow("y", {from: "y"})]);
451 } else {
452 return callback([previous, makeColumn("ure", {from: "u", diphthong: true}).addAbove(iTehta, {from: "i"})]);
453 }
454 } else if (previous && previous.canAddAbove(iTehta)) {
455 return callback([previous.addAbove(iTehta, {from: "i"})])(character);
456 } else {
457 return callback([previous, makeColumn("short-carrier", {from: "i"}).addAbove(iTehta, {from: ""})])(character);
458 }
459 };
460 } else if (character === "o") {
461 return function (character) {
462 if (character === "o") {
463 return parseTehta(callback, options, previous)("ó");
464 } else if (character === "i") {
465 return callback([previous, makeColumn("yanta", {from: "i", diphthong: true}).addAbove(reverseCurls("o", options), {from: "o"})]);
466 } else if (previous && previous.canAddAbove("o")) {
467 return callback([previous.addAbove(reverseCurls("o", options), {from: "o"})])(character);
468 } else {
469 return callback([previous, makeColumn("short-carrier", {from: "o"}).addAbove(reverseCurls("o", options), {from: ""})])(character);
470 }
471 };
472 } else if (character === "u") {
473 return function (character) {
474 if (character === "u") {
475 return parseTehta(callback, options, previous)("ú");
476 } else if (character === "i") {
477 return callback([previous, makeColumn("yanta", {from: "i", diphthong: true}).addAbove(reverseCurls("u", options), {from: "u"})]);
478 } else if (previous && previous.canAddAbove("u")) {
479 return callback([previous.addAbove(reverseCurls("u", options), {from: "u"})])(character);
480 } else {
481 return callback([previous, makeColumn("short-carrier", {from: "u"}).addAbove(reverseCurls("u", options), {from: ""})])(character);
482 }
483 };
484 } else if (character === "y") {
485 if (previous && previous.canAddBelow("y")) {
486 return callback([previous.addBelow("y", {from: "y"})]);
487 } else {
488 var next = makeColumn("anna", {from: ""}).addBelow("y", {from: "y"});
489 return parseTehta(function (moreColumns) {
490 return callback([previous].concat(moreColumns));
491 }, options, next);
492 }
493 } else if (character === "á") {
494 return callback([previous, makeColumn("long-carrier", {from: "á"}).addAbove("a", {from: ""})]);
495 } else if (character === "é") {
496 return callback([previous, makeColumn("long-carrier", {from: "é"}).addAbove(swapDotSlash("e", options), {from: ""})]);
497 } else if (character === "í") {
498 return callback([previous, makeColumn("long-carrier", {from: "í"}).addAbove(swapDotSlash("i", options), {from: ""})]);
499 } else if (character === "ó") {
500 if (previous && previous.canAddAbove("ó")) {
501 return callback([previous.addAbove(reverseCurls("ó", options), {from: "ó"})]);
502 } else {
503 return callback([previous, makeColumn("long-carrier", {from: "ó"}).addAbove(reverseCurls("o", options), {from: ""})]);
504 }
505 } else if (character === "ú") {
506 if (previous && previous.canAddAbove("ú")) {
507 return callback([previous.addAbove(reverseCurls("ú", options), {from: "ú"})]);
508 } else {
509 return callback([previous, makeColumn("long-carrier", {from: "ú"}).addAbove(reverseCurls("u", options), {from: ""})]);
510 }
511 } else {
512 return callback([previous])(character);
513 }
514 };
515 }
516
517 var curlReversals = {"o": "u", "u": "o", "ó": "ú", "ú": "ó"};
518 function reverseCurls(tehta, options) {
519 if (options.reverseCurls) {
520 tehta = curlReversals[tehta] || tehta;
521 }
522 return tehta;
523 }
524
525 var dotSlashSwaps = {"e": "i", "i": "e"};
526 function swapDotSlash(tehta, options) {
527 if (options.swapDotSlash) {
528 tehta = dotSlashSwaps[tehta] || tehta;
529 }
530 return tehta;
531 }
532
533 // Notes regarding "h":
534 //
535 // http://at.mansbjorkman.net/teng_quenya.htm#note_harma
536 // originally:
537 // h represented ach-laut and was written with harma.
538 // h initial transcribed as halla
539 // h medial transcribed as harma
540 // hy transcribed as hyarmen
541 // then harma became aha:
542 // then h in initial position became a breath-h, still spelled with harma, but
543 // renamed aha.
544 // h initial transcribed as harma
545 // h medial transcribed as hyarmen
546 // hy transcribed as hyarmen with underposed y
547 // then, in the third age:
548 // the h in every position became a breath-h
549 // except before t, where it remained pronounced as ach-laut
550 // h initial ???
551 // h medial transcribed as harma
552 // h transcribed as halla or hyarmen in other positions (needs clarification)
553 //
554 // ach-laut (_ch_, /x/ phonetically, {h} by tolkien)
555 // original: harma in all positions
556 // altered: harma initially, halla in all other positions
557 // third-age: halla in all other positions
558 // hy (/ç/ phonetically)
559 // original: hyarmen in all positions
560 // altered: hyarmen with y below
561 // third-age:
562 // h (breath h)
563 // original: halla in all positions
564 // altered: hyarmen medially
565 // third-age:
566 //
567 // harma:
568 // original: ach-laut found in all positions
569 // altered: breath h initially (renamed aha), ach-laut medial
570 // third-age: ach-laut before t, breath h all other places
571 // hyarmen:
572 // original: represented {hy}, palatalized h, in all positions
573 // altered: breath h medial, palatalized with y below
574 // third-age: same
575 // halla:
576 // original: breath-h, presuming existed only initially
577 // altered: breath h initial
578 // third-age: only used for hl and hr
579 //
580 // hr: halla romen
581 // hl: halla lambe
582 // ht: harma
583 // hy:
584 // original: hyarmen
585 // altered:
586 // initial: ERROR
587 // medial: hyarmen lower-y
588 // third age: hyarmen lower-y
589 // ch: harma
590 // h initial:
591 // original: halla
592 // altered: XXX
593 // third-age: harma
594 // h medial: hyarmen
595