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");
10 exports
.name
= "Classical Mode";
13 exports
.makeOptions
= makeOptions
;
14 function makeOptions(options
) {
15 options
= options
|| defaults
;
17 font
: options
.font
|| TengwarAnnatar
,
21 // false: (v: vala, w: wilya)
22 // true: (v: vilya, w: ERROR)
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
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
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
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
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
);
73 exports
.encode
= encode
;
74 function encode(text
, options
) {
75 options
= makeOptions(options
);
76 return Notation
.encode(parse(text
, options
), options
);
79 var parse
= exports
.parse
= makeDocumentParser(parseNormalWord
, makeOptions
);
81 function parseNormalWord(callback
, options
) {
82 return normalize(parseWord(callback
, options
));
85 function parseWord(callback
, options
, columns
, previous
) {
86 columns
= columns
|| [];
87 return parseColumn(function (moreColumns
) {
88 if (!moreColumns
.length
) {
89 return callback(columns
);
94 columns
.concat(moreColumns
),
95 moreColumns
[moreColumns
.length
- 1] // previous
98 }, options
, previous
);
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
)
109 return callback(next
);
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
})]);
119 return callback([makeColumn("ure", {from
: ""}).addError(
120 "Cannot transcribe " + JSON
.stringify(character
) +
126 }, options
, previous
);
127 }, options
, previous
);
130 var vowels
= "aeiouyáéíóú";
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"})]);
149 return callback([makeColumn("anga", {from
: "ñg"})])(character
);
152 } else if (character
=== "c") { // nc
153 return function (character
) {
154 if (character
=== "w") { // ncw
155 return callback([makeColumn("unque", {from
: "ñcw"})]);
157 return callback([makeColumn("anca", {from
: "ñc"})])(character
);
161 return callback([makeColumn("numen", {from
: "n"})])(character
);
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"})]);
173 return callback([makeColumn("malta", {from
: "m"})])(character
);
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"})]);
183 return callback([makeColumn("anga", {from
: "ñg"})])(character
);
186 } else if (character
=== "c") { // ñc
187 return function (character
) {
188 if (character
=== "w") { // ñcw
189 return callback([makeColumn("unque", {from
: "ñcw"})]);
191 return callback([makeColumn("anca", {from
: "ñc"})]);
195 return callback([makeColumn("noldo", {from
: "ñ"})])(character
);
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"})]);
205 return callback([makeColumn("tinco", {from
: "t"}).addTildeBelow({from
: "t"})])(character
);
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
219 makeColumn("tinco", {from
: "t"}),
220 makeColumn("silme", {from
: "s"})
225 return callback([makeColumn("tinco", {from
: "t"})])(character
);
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"})]);
235 return callback([makeColumn("parma", {from
: "p"}).addTildeBelow({from
: "p"})])(character
);
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
246 makeColumn("parma", {from
: "p"}),
247 makeColumn("silme", {from
: "s"})
252 return callback([makeColumn("parma", {from
: "p"})])(character
);
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"})]);
266 return callback([makeColumn("calma", {from
: "c"})])(character
);
269 } else if (character
=== "f") {
270 return callback([makeColumn("formen", {from
: "f"})]);
271 } else if (character
=== "v") {
273 return callback([makeColumn("wilya", {from
: "v", name
: "vilya"})]);
275 return callback([makeColumn("vala", {from
: "v", name
: "vala"})]);
277 } else if (character
=== "w") {
279 return callback([])("u");
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"})]);
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.";
293 makeColumn("halla", {from
: "h"}).addError(error
),
294 makeColumn("romen", {from
: "r"}).addError(error
)
296 } else if (options
.classicalR
) {
297 // pre-namarie style, ore when r between vowels
301 !Parser
.isFinal(character
) &&
302 vowels
.indexOf(character
) !== -1
304 return callback([makeColumn("ore", {from
: "r"})])(character
);
306 return callback([makeColumn("romen", {from
: "r"})])(character
);
309 // pre-consonant and word-final
310 if (Parser
.isFinal(character
) || vowels
.indexOf(character
) === -1) { // ore
311 return callback([makeColumn("ore", {from
: "r"})])(character
);
313 return callback([makeColumn("romen", {from
: "r"})])(character
);
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"})]);
324 return callback([makeColumn("lambe", {from
: "l"}).addTildeBelow({from
: "y"})])(character
);
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.";
332 makeColumn("halla", {from
: "h"}).addError(error
),
333 makeColumn("lambe", {from
: "l"}).addError(error
)
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"})]);
341 return callback([makeColumn("lambe", {from
: "l"})])(character
);
344 } else if (character
=== "s") {
345 return function (character
) {
346 if (character
=== "s") { // ss
347 return callback([makeColumn("esse", {from
: "ss"})]);
349 return callback([makeColumn("silme", {from
: "s"})])(character
);
351 // Note that there is no sh phoneme in Classical Elvish languages
353 } else if (character
=== "h") {
354 return function (character
) {
355 if (character
=== "l") { // hl
357 makeColumn("halla", {from
: "h"}),
358 makeColumn("lambe", {from
: "l"})
360 } else if (character
=== "r") {
362 makeColumn("halla", {from
: "h"}),
363 makeColumn("romen", {from
: "r"})
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
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"})]);
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
);
384 return callback([makeColumn("harma", {from
: "h"})])(character
);
386 } else { // harmen renamed and resounded as aha in initial position
387 if (previous
) { // medial
388 return callback([makeColumn("hyarmen", {from
: "h"})])(character
);
390 return callback([makeColumn("halla", {from
: "h"})])(character
);
393 } else { // third age, namarië
394 return callback([makeColumn("hyarmen", {from
: "h"})])(character
);
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")]);
407 return callback([])(character
);
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
);
427 return callback([previous
, makeColumn("short-carrier", {from
: "a"}).addAbove("a", {from
: ""})])(character
);
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
);
440 return callback([previous
, makeColumn("short-carrier", {from
: "e"}).addAbove(tehta
, {from
: ""})])(character
);
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"})]);
452 return callback([previous
, makeColumn("ure", {from
: "u", diphthong
: true}).addAbove(iTehta
, {from
: "i"})]);
454 } else if (previous
&& previous
.canAddAbove(iTehta
)) {
455 return callback([previous
.addAbove(iTehta
, {from
: "i"})])(character
);
457 return callback([previous
, makeColumn("short-carrier", {from
: "i"}).addAbove(iTehta
, {from
: ""})])(character
);
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
);
469 return callback([previous
, makeColumn("short-carrier", {from
: "o"}).addAbove(reverseCurls("o", options
), {from
: ""})])(character
);
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
);
481 return callback([previous
, makeColumn("short-carrier", {from
: "u"}).addAbove(reverseCurls("u", options
), {from
: ""})])(character
);
484 } else if (character
=== "y") {
485 if (previous
&& previous
.canAddBelow("y")) {
486 return callback([previous
.addBelow("y", {from
: "y"})]);
488 var next
= makeColumn("anna", {from
: ""}).addBelow("y", {from
: "y"});
489 return parseTehta(function (moreColumns
) {
490 return callback([previous
].concat(moreColumns
));
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
: "ó"})]);
503 return callback([previous
, makeColumn("long-carrier", {from
: "ó"}).addAbove(reverseCurls("o", options
), {from
: ""})]);
505 } else if (character
=== "ú") {
506 if (previous
&& previous
.canAddAbove("ú")) {
507 return callback([previous
.addAbove(reverseCurls("ú", options
), {from
: "ú"})]);
509 return callback([previous
, makeColumn("long-carrier", {from
: "ú"}).addAbove(reverseCurls("u", options
), {from
: ""})]);
512 return callback([previous
])(character
);
517 var curlReversals
= {"o": "u", "u": "o", "ó": "ú", "ú": "ó"};
518 function reverseCurls(tehta
, options
) {
519 if (options
.reverseCurls
) {
520 tehta
= curlReversals
[tehta
] || tehta
;
525 var dotSlashSwaps
= {"e": "i", "i": "e"};
526 function swapDotSlash(tehta
, options
) {
527 if (options
.swapDotSlash
) {
528 tehta
= dotSlashSwaps
[tehta
] || tehta
;
533 // Notes regarding "h":
535 // http://at.mansbjorkman.net/teng_quenya
.htm
#note_harma
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
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
551 // h medial transcribed as harma
552 // h transcribed as halla or hyarmen in other positions (needs clarification)
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
563 // original: halla in all positions
564 // altered: hyarmen medially
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
572 // original: represented {hy}, palatalized h, in all positions
573 // altered: breath h medial, palatalized with y below
576 // original: breath-h, presuming existed only initially
577 // altered: breath h initial
578 // third-age: only used for hl and hr
587 // medial: hyarmen lower-y
588 // third age: hyarmen lower-y