@heroiclands/package-build 25.6.0 → 25.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/docs/api.md +2 -2
- package/docs/encoding-core.md +62 -9
- package/docs/guides/writing-in-invented-scripts.md +8 -2
- package/docs/reference/format-details.md +69 -68
- package/docs/reference/note-types.md +56 -55
- package/engine/encoding-integrity.mjs +3 -3
- package/engine/encoding-template.mjs +52 -4
- package/engine/encoding-terms.mjs +28 -13
- package/engine/encoding.mjs +174 -23
- package/engine/language-terms.mjs +3 -3
- package/package.json +1 -1
- package/types/engine/content-scripts.d.mts +6 -0
- package/types/engine/encoding-integrity.d.mts +2 -1
- package/types/engine/encoding-lexicon.d.mts +6 -0
- package/types/engine/encoding-template.d.mts +6 -0
- package/types/engine/encoding.d.mts +12 -0
- package/types/engine/language-terms.d.mts +2 -2
package/engine/encoding.mjs
CHANGED
|
@@ -114,14 +114,14 @@ function rows(value, key, vowels, allowEmpty) {
|
|
|
114
114
|
if (!Array.isArray(value)) fault(key, `\`${key}\` must be a list of rows`);
|
|
115
115
|
return value.map((value, order) => {
|
|
116
116
|
const at = `${key}.${order}`;
|
|
117
|
-
let from, to, before, position;
|
|
117
|
+
let from, to, after, before, position;
|
|
118
118
|
if (Array.isArray(value)) {
|
|
119
119
|
if (value.length !== 2) fault(at, "A row is two strings, [from, to]");
|
|
120
120
|
[from, to] = value;
|
|
121
121
|
} else if (object(value)) {
|
|
122
|
-
keys(value, new Set(["from", "to", "before", "at"]), at);
|
|
123
|
-
({ from, to, before, at: position } = value);
|
|
124
|
-
} else fault(at, "A row is [from, to] or {from, to, before?, at?}");
|
|
122
|
+
keys(value, new Set(["from", "to", "after", "before", "at"]), at);
|
|
123
|
+
({ from, to, after, before, at: position } = value);
|
|
124
|
+
} else fault(at, "A row is [from, to] or {from, to, after?, before?, at?}");
|
|
125
125
|
string(from, `${at}.from`, true);
|
|
126
126
|
string(to, `${at}.to`);
|
|
127
127
|
if (from.includes(OPEN) || from.includes(CLOSE))
|
|
@@ -130,12 +130,23 @@ function rows(value, key, vowels, allowEmpty) {
|
|
|
130
130
|
fault(`${at}.from`, "Whitespace is stated by separator");
|
|
131
131
|
if (!to && !allowEmpty && from !== " ")
|
|
132
132
|
fault(`${at}.to`, "An exact row must write a nonempty output");
|
|
133
|
+
if (after !== undefined) {
|
|
134
|
+
if (
|
|
135
|
+
!["vowel", "consonant"].includes(after) &&
|
|
136
|
+
(typeof after !== "string" ||
|
|
137
|
+
!LETTER.test(after) ||
|
|
138
|
+
after !== after.normalize("NFC"))
|
|
139
|
+
)
|
|
140
|
+
fault(`${at}.after`, "after is one NFC Unicode letter, vowel or consonant");
|
|
141
|
+
if (["vowel", "consonant"].includes(after) && !vowels)
|
|
142
|
+
fault(`${at}.after`, "after categories need vowels");
|
|
143
|
+
}
|
|
133
144
|
if (before !== undefined && !["vowel", "consonant"].includes(before))
|
|
134
145
|
fault(`${at}.before`, "before is vowel or consonant");
|
|
135
146
|
if (before && !vowels) fault(`${at}.before`, "before needs vowels");
|
|
136
147
|
if (position !== undefined && !["start", "end", "line-start"].includes(position))
|
|
137
148
|
fault(`${at}.at`, "at is start, end or line-start");
|
|
138
|
-
return Object.freeze({ from, to, before, at: position, order, key: at });
|
|
149
|
+
return Object.freeze({ from, to, after, before, at: position, order, key: at });
|
|
139
150
|
});
|
|
140
151
|
}
|
|
141
152
|
function indexRows(rows) {
|
|
@@ -149,7 +160,8 @@ function indexRows(rows) {
|
|
|
149
160
|
group.sort(
|
|
150
161
|
(a, b) =>
|
|
151
162
|
length(b.from) - length(a.from) ||
|
|
152
|
-
Number(Boolean(b.
|
|
163
|
+
Number(Boolean(b.after || b.before || b.at)) -
|
|
164
|
+
Number(Boolean(a.after || a.before || a.at)) ||
|
|
153
165
|
a.order - b.order,
|
|
154
166
|
);
|
|
155
167
|
return index;
|
|
@@ -328,6 +340,15 @@ function beforeHolds(condition, char, vowels) {
|
|
|
328
340
|
);
|
|
329
341
|
}
|
|
330
342
|
function holds(row, text, start, end, vowels) {
|
|
343
|
+
if (row.after) {
|
|
344
|
+
const preceding = start > 0 ? previousAt(text, start) : undefined;
|
|
345
|
+
if (
|
|
346
|
+
["vowel", "consonant"].includes(row.after) ?
|
|
347
|
+
!beforeHolds(row.after, preceding, vowels)
|
|
348
|
+
: preceding !== row.after
|
|
349
|
+
)
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
331
352
|
if (row.before && !beforeHolds(row.before, firstAt(text, end), vowels)) return false;
|
|
332
353
|
if (row.at === "line-start") {
|
|
333
354
|
let slash = text.lastIndexOf("/", start - 1);
|
|
@@ -470,7 +491,11 @@ function checkGroupCase(enc, groups, text, at, bound) {
|
|
|
470
491
|
* @param {string} text
|
|
471
492
|
* @param {{font?: string, verse?: boolean, start?: number, end?: number}} [options]
|
|
472
493
|
*/
|
|
473
|
-
function encodeRawPieces(
|
|
494
|
+
function encodeRawPieces(
|
|
495
|
+
enc,
|
|
496
|
+
text,
|
|
497
|
+
{ font, verse = false, start = 0, end = text.length, stops = [] } = {},
|
|
498
|
+
) {
|
|
474
499
|
const groups = tiers(enc, font);
|
|
475
500
|
if (typeof text !== "string") throw new EncodingError("Text must be a string");
|
|
476
501
|
if (
|
|
@@ -485,9 +510,13 @@ function encodeRawPieces(enc, text, { font, verse = false, start = 0, end = text
|
|
|
485
510
|
});
|
|
486
511
|
const bound = end;
|
|
487
512
|
const out = [];
|
|
488
|
-
let at = start
|
|
513
|
+
let at = start,
|
|
514
|
+
stop = 0;
|
|
489
515
|
while (at < bound) {
|
|
490
516
|
const first = firstAt(text, at);
|
|
517
|
+
while (stop < stops.length && stops[stop] <= at) stop++;
|
|
518
|
+
// A row never spans the start of a diaeresis vowel written by its plain vowel.
|
|
519
|
+
const limit = Math.min(bound, stops[stop] ?? bound);
|
|
491
520
|
if (first === OPEN) {
|
|
492
521
|
const close = span(text.slice(0, bound), at, OPEN, CLOSE);
|
|
493
522
|
out.push(
|
|
@@ -545,7 +574,7 @@ function encodeRawPieces(enc, text, { font, verse = false, start = 0, end = text
|
|
|
545
574
|
continue;
|
|
546
575
|
}
|
|
547
576
|
if (!enc.template) checkGroupCase(enc, groups, text, at, bound);
|
|
548
|
-
const row = enc.accepted.has(first) ? rowAt(enc, groups, text, at,
|
|
577
|
+
const row = enc.accepted.has(first) ? rowAt(enc, groups, text, at, limit) : null;
|
|
549
578
|
if (!row)
|
|
550
579
|
refused(
|
|
551
580
|
enc,
|
|
@@ -688,7 +717,13 @@ function sourceDetails(enc, text, options) {
|
|
|
688
717
|
pointSlice(normalized.slice(0, bound), at, enc.template.maxVowelGroup)
|
|
689
718
|
: "";
|
|
690
719
|
const lower = lookahead.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
691
|
-
const group = enc.template?.vowelGroups.find(
|
|
720
|
+
const group = enc.template?.vowelGroups.find(
|
|
721
|
+
(value) =>
|
|
722
|
+
lower.startsWith(value) &&
|
|
723
|
+
[...lookahead]
|
|
724
|
+
.slice(1, [...value].length)
|
|
725
|
+
.every((letter) => !/[\u0308\u0300]/u.test(letter.normalize("NFD"))),
|
|
726
|
+
);
|
|
692
727
|
if (group) {
|
|
693
728
|
let cursor = at;
|
|
694
729
|
for (let i = 0; i < [...group].length; i++) {
|
|
@@ -713,7 +748,16 @@ function sourceDetails(enc, text, options) {
|
|
|
713
748
|
});
|
|
714
749
|
throw error;
|
|
715
750
|
}
|
|
716
|
-
|
|
751
|
+
const letters = enc.template?.apart?.letters;
|
|
752
|
+
if (!letters) return { ...projection, folded: normalized, stops: [] };
|
|
753
|
+
const stops = [];
|
|
754
|
+
let folded = "";
|
|
755
|
+
for (let at = 0; at < normalized.length; at++) {
|
|
756
|
+
const plain = at >= begin && at < bound ? letters[normalized[at]] : undefined;
|
|
757
|
+
if (plain) stops.push(at);
|
|
758
|
+
folded += plain ?? normalized[at];
|
|
759
|
+
}
|
|
760
|
+
return { ...projection, folded, stops };
|
|
717
761
|
}
|
|
718
762
|
|
|
719
763
|
/**
|
|
@@ -736,16 +780,49 @@ export function validateSource(enc, text, options = {}) {
|
|
|
736
780
|
*/
|
|
737
781
|
export function encodePieces(enc, text, options = {}) {
|
|
738
782
|
if (!enc?.template) return encodeRawPieces(enc, text, options);
|
|
739
|
-
const { normalized, sourceAt, begin, bound } = sourceDetails(enc, text, options);
|
|
783
|
+
const { normalized, folded, stops, sourceAt, begin, bound } = sourceDetails(enc, text, options);
|
|
740
784
|
try {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
785
|
+
const pieces = encodeRawPieces(enc, folded, {
|
|
786
|
+
...options,
|
|
787
|
+
start: begin,
|
|
788
|
+
end: bound,
|
|
789
|
+
stops,
|
|
790
|
+
});
|
|
791
|
+
if (enc.roundTrip === "exact" && stops.length) {
|
|
792
|
+
const native = pieces.map((one) => one.to).join("");
|
|
793
|
+
const decoded = decodeTemplatePieces(enc, native, options);
|
|
794
|
+
let nativeAt = 0;
|
|
795
|
+
for (const one of pieces) {
|
|
796
|
+
const expected = normalized.slice(one.start, one.end);
|
|
797
|
+
const marked = stops.find(
|
|
798
|
+
(at) =>
|
|
799
|
+
at >= one.start &&
|
|
800
|
+
at < one.end &&
|
|
801
|
+
splitsDeclaredGroup(enc, normalized, at, begin, bound),
|
|
802
|
+
);
|
|
803
|
+
if (
|
|
804
|
+
marked !== undefined &&
|
|
805
|
+
!decoded.some(
|
|
806
|
+
(step) =>
|
|
807
|
+
step.start === nativeAt &&
|
|
808
|
+
step.end === nativeAt + one.to.length &&
|
|
809
|
+
step.from === expected,
|
|
810
|
+
)
|
|
811
|
+
)
|
|
812
|
+
textError(
|
|
813
|
+
folded,
|
|
814
|
+
marked,
|
|
815
|
+
"The encoding cannot distinguish this diaeresis split in an exact round trip",
|
|
816
|
+
);
|
|
817
|
+
nativeAt += one.to.length;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return pieces.map((one) => ({
|
|
821
|
+
...one,
|
|
822
|
+
from: text.slice(sourceAt[one.start], sourceAt[one.end]),
|
|
823
|
+
start: sourceAt[one.start],
|
|
824
|
+
end: sourceAt[one.end],
|
|
825
|
+
}));
|
|
749
826
|
} catch (error) {
|
|
750
827
|
if (error instanceof EncodingError && error.offset !== undefined)
|
|
751
828
|
throw new EncodingError(error.message, {
|
|
@@ -845,6 +922,12 @@ function decodeRawText(enc, text, { font } = {}) {
|
|
|
845
922
|
*/
|
|
846
923
|
export function decodeText(enc, text, options = {}) {
|
|
847
924
|
if (!enc?.template) return decodeRawText(enc, text, options);
|
|
925
|
+
return decodeTemplatePieces(enc, text, options)
|
|
926
|
+
.map((one) => one.from)
|
|
927
|
+
.join("");
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function decodeTemplatePieces(enc, text, options = {}) {
|
|
848
931
|
const readings = tiers(enc, options.font)[0].readings;
|
|
849
932
|
if (typeof text !== "string") throw new EncodingError("Text must be a string");
|
|
850
933
|
if (!templateReaders.has(enc)) {
|
|
@@ -905,7 +988,7 @@ export function decodeText(enc, text, options = {}) {
|
|
|
905
988
|
failedContext = Math.min(failedContext, at);
|
|
906
989
|
continue;
|
|
907
990
|
}
|
|
908
|
-
paths.set(at, { from: row.from, next });
|
|
991
|
+
paths.set(at, { from: row.from, row, next });
|
|
909
992
|
}
|
|
910
993
|
}
|
|
911
994
|
if (!paths.has(0)) {
|
|
@@ -950,8 +1033,76 @@ export function decodeText(enc, text, options = {}) {
|
|
|
950
1033
|
const out = [];
|
|
951
1034
|
for (let at = 0; at < text.length;) {
|
|
952
1035
|
const step = paths.get(at);
|
|
953
|
-
out.push(step.from);
|
|
1036
|
+
out.push({ from: step.from, row: step.row, start: at, end: step.next });
|
|
954
1037
|
at = step.next;
|
|
955
1038
|
}
|
|
956
|
-
|
|
1039
|
+
const apart = enc.template.apart;
|
|
1040
|
+
if (!apart) return out;
|
|
1041
|
+
return restoreDiaeresis(enc, out, options);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/** Restore only joins contradicted by the selected native row boundaries. */
|
|
1045
|
+
function restoreDiaeresis(enc, pieces, options) {
|
|
1046
|
+
const text = pieces.map((one) => one.from).join("");
|
|
1047
|
+
const letters = [];
|
|
1048
|
+
let offset = 0;
|
|
1049
|
+
for (const [part, piece] of pieces.entries()) {
|
|
1050
|
+
for (const char of piece.from) {
|
|
1051
|
+
letters.push({ char, part, offset, literal: !piece.row });
|
|
1052
|
+
offset += char.length;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const groups = tiers(enc, options.font);
|
|
1056
|
+
for (let i = 0; i < letters.length; i++) {
|
|
1057
|
+
if (letters[i].literal) continue;
|
|
1058
|
+
const group = enc.template.apart.groups.find((value) => {
|
|
1059
|
+
const span = [...value];
|
|
1060
|
+
if (
|
|
1061
|
+
!span.every((char, k) => {
|
|
1062
|
+
const letter = letters[i + k];
|
|
1063
|
+
return (
|
|
1064
|
+
letter &&
|
|
1065
|
+
!letter.literal &&
|
|
1066
|
+
letter.char.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase() ===
|
|
1067
|
+
char &&
|
|
1068
|
+
(k === 0 || letter.char.normalize("NFD").length === 1)
|
|
1069
|
+
);
|
|
1070
|
+
})
|
|
1071
|
+
)
|
|
1072
|
+
return false;
|
|
1073
|
+
const row = rowAt(enc, groups, text, letters[i].offset);
|
|
1074
|
+
return row && row.from.toLowerCase() === value;
|
|
1075
|
+
});
|
|
1076
|
+
if (!group) continue;
|
|
1077
|
+
const end = i + [...group].length;
|
|
1078
|
+
let boundary = i + 1;
|
|
1079
|
+
while (boundary < end && letters[boundary].part === letters[i].part) boundary++;
|
|
1080
|
+
if (boundary === end) {
|
|
1081
|
+
i = end - 1;
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
const letter = letters[boundary];
|
|
1085
|
+
const marked = (letter.char + "\u0308").normalize("NFC");
|
|
1086
|
+
if (enc.template.apart.letters[marked]) {
|
|
1087
|
+
letter.char = marked;
|
|
1088
|
+
i = boundary - 1;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const output = pieces.map((piece) => ({ ...piece, from: "" }));
|
|
1092
|
+
for (const letter of letters) output[letter.part].from += letter.char;
|
|
1093
|
+
return output;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/** Whether a derived mark actually splits one of this note's declared groups. */
|
|
1097
|
+
function splitsDeclaredGroup(enc, text, at, begin, bound) {
|
|
1098
|
+
for (const group of enc.template.apart.groups) {
|
|
1099
|
+
const count = [...group].length;
|
|
1100
|
+
// The supported diaeresis letters are one NFC UTF-16 unit each.
|
|
1101
|
+
for (let start = Math.max(begin, at - count + 1); start < at; start++) {
|
|
1102
|
+
const span = pointSlice(text.slice(0, bound), start, count);
|
|
1103
|
+
if (start + span.length <= at) continue;
|
|
1104
|
+
if (span.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase() === group) return true;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return false;
|
|
957
1108
|
}
|
|
@@ -154,10 +154,10 @@ export const JOIN_RULES = Object.freeze(["merge", "insert", "drop", "double"]);
|
|
|
154
154
|
|
|
155
155
|
/**
|
|
156
156
|
* What a written mark says of its letter: `stress` falls on it, `length` holds
|
|
157
|
-
* it longer, `apart` says it apart from its neighbour,
|
|
158
|
-
* which vowel is said.
|
|
157
|
+
* it longer, `apart` says it apart from its neighbour, `quality` changes
|
|
158
|
+
* which vowel is said, and `tone` marks a rising or falling pitch.
|
|
159
159
|
*/
|
|
160
|
-
export const MARK_KINDS = Object.freeze(["stress", "length", "apart", "quality"]);
|
|
160
|
+
export const MARK_KINDS = Object.freeze(["stress", "length", "apart", "quality", "tone"]);
|
|
161
161
|
|
|
162
162
|
/** How many of a mark a word may carry: `one` at most, or `any` number. */
|
|
163
163
|
export const MARK_LIMITS = Object.freeze(["one", "any"]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "25.6.
|
|
3
|
+
"version": "25.6.2",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package \u2014 content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -261,6 +261,12 @@ export type RunContext = {
|
|
|
261
261
|
maxVowelGroup: number;
|
|
262
262
|
stress: "󠄔" | undefined;
|
|
263
263
|
diaeresis: boolean;
|
|
264
|
+
apart: Readonly<{
|
|
265
|
+
letters: Readonly<{
|
|
266
|
+
[k: string]: string;
|
|
267
|
+
}>;
|
|
268
|
+
groups: any;
|
|
269
|
+
}> | undefined;
|
|
264
270
|
grave: boolean;
|
|
265
271
|
}> | undefined;
|
|
266
272
|
fonts: readonly any[];
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Check codeword integrity with a trie and finite Sardinas–Patterson residuals.
|
|
4
4
|
* Source-equivalent compositions are redundant rules, not distinct spellings.
|
|
5
|
-
* @param {Array<{from:string,to:string,key:string,before?:string,at?:string}>} rows - Validated ordinary rows.
|
|
5
|
+
* @param {Array<{from:string,to:string,key:string,after?:string,before?:string,at?:string}>} rows - Validated ordinary rows.
|
|
6
6
|
* @param {(key:string,message:string)=>never} fault - Positioned schema-error constructor.
|
|
7
7
|
*/
|
|
8
8
|
export function assertUniqueOutputs(rows: Array<{
|
|
9
9
|
from: string;
|
|
10
10
|
to: string;
|
|
11
11
|
key: string;
|
|
12
|
+
after?: string;
|
|
12
13
|
before?: string;
|
|
13
14
|
at?: string;
|
|
14
15
|
}>, fault: (key: string, message: string) => never): void;
|
|
@@ -90,6 +90,12 @@ export function encodingFor(ctx: import("./content-scripts.mjs").RunContext, { e
|
|
|
90
90
|
maxVowelGroup: number;
|
|
91
91
|
stress: "󠄔" | undefined;
|
|
92
92
|
diaeresis: boolean;
|
|
93
|
+
apart: Readonly<{
|
|
94
|
+
letters: Readonly<{
|
|
95
|
+
[k: string]: string;
|
|
96
|
+
}>;
|
|
97
|
+
groups: any;
|
|
98
|
+
}> | undefined;
|
|
93
99
|
grave: boolean;
|
|
94
100
|
}> | undefined;
|
|
95
101
|
fonts: readonly any[];
|
|
@@ -38,6 +38,12 @@ export function templateProgram(data: any, options: {
|
|
|
38
38
|
maxVowelGroup: number;
|
|
39
39
|
stress: "󠄔" | undefined;
|
|
40
40
|
diaeresis: boolean;
|
|
41
|
+
apart: Readonly<{
|
|
42
|
+
letters: Readonly<{
|
|
43
|
+
[k: string]: string;
|
|
44
|
+
}>;
|
|
45
|
+
groups: any;
|
|
46
|
+
}> | undefined;
|
|
41
47
|
grave: boolean;
|
|
42
48
|
}>;
|
|
43
49
|
};
|
|
@@ -16,6 +16,12 @@ export function compileEncoding(data: any, options?: {
|
|
|
16
16
|
maxVowelGroup: number;
|
|
17
17
|
stress: "󠄔" | undefined;
|
|
18
18
|
diaeresis: boolean;
|
|
19
|
+
apart: Readonly<{
|
|
20
|
+
letters: Readonly<{
|
|
21
|
+
[k: string]: string;
|
|
22
|
+
}>;
|
|
23
|
+
groups: any;
|
|
24
|
+
}> | undefined;
|
|
19
25
|
grave: boolean;
|
|
20
26
|
}> | undefined;
|
|
21
27
|
fonts: readonly any[];
|
|
@@ -59,6 +65,12 @@ export function compileRows(definition: any, metadata?: Partial<ReturnType<typeo
|
|
|
59
65
|
maxVowelGroup: number;
|
|
60
66
|
stress: "󠄔" | undefined;
|
|
61
67
|
diaeresis: boolean;
|
|
68
|
+
apart: Readonly<{
|
|
69
|
+
letters: Readonly<{
|
|
70
|
+
[k: string]: string;
|
|
71
|
+
}>;
|
|
72
|
+
groups: any;
|
|
73
|
+
}> | undefined;
|
|
62
74
|
grave: boolean;
|
|
63
75
|
}> | undefined;
|
|
64
76
|
fonts: readonly any[];
|
|
@@ -96,8 +96,8 @@ export const JOIN_MEETINGS: readonly string[];
|
|
|
96
96
|
export const JOIN_RULES: readonly string[];
|
|
97
97
|
/**
|
|
98
98
|
* What a written mark says of its letter: `stress` falls on it, `length` holds
|
|
99
|
-
* it longer, `apart` says it apart from its neighbour,
|
|
100
|
-
* which vowel is said.
|
|
99
|
+
* it longer, `apart` says it apart from its neighbour, `quality` changes
|
|
100
|
+
* which vowel is said, and `tone` marks a rising or falling pitch.
|
|
101
101
|
*/
|
|
102
102
|
export const MARK_KINDS: readonly string[];
|
|
103
103
|
/** How many of a mark a word may carry: `one` at most, or `any` number. */
|