@heroiclands/package-build 25.0.0 → 25.1.1

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.
Files changed (105) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -0
  3. package/bin/content-commands.mjs +30 -96
  4. package/bin/encoding-commands.mjs +146 -0
  5. package/bin/package-build.mjs +35 -24
  6. package/config.mjs +25 -0
  7. package/convert.mjs +182 -0
  8. package/docs/api.md +144 -6
  9. package/docs/authoring/links-and-markup.md +260 -481
  10. package/docs/commands.md +110 -119
  11. package/docs/configuration.md +31 -13
  12. package/docs/content-format.md +11 -4
  13. package/docs/diagnostics.md +43 -16
  14. package/docs/encoding-core.md +193 -0
  15. package/docs/guides/book.md +2 -2
  16. package/docs/guides/module-project.md +23 -1
  17. package/docs/guides/site.md +2 -2
  18. package/docs/guides/writing-in-invented-scripts.md +458 -575
  19. package/docs/reference/format-details.md +292 -404
  20. package/docs/reference/note-types.md +83 -63
  21. package/docs/source-preview.md +8 -0
  22. package/e2e.mjs +1 -1
  23. package/engine/base-compiler.mjs +27 -9
  24. package/engine/content-charset.mjs +13 -1
  25. package/engine/content-format.yaml +6 -2
  26. package/engine/content-index.mjs +6 -2
  27. package/engine/content-links.mjs +1 -0
  28. package/engine/content-scripts.mjs +614 -459
  29. package/engine/content-scroll.mjs +4 -4
  30. package/engine/content-spans.mjs +40 -9
  31. package/engine/content-verse.mjs +1 -1
  32. package/engine/converter-data.mjs +70 -0
  33. package/engine/dictionary-rows.mjs +2 -34
  34. package/engine/encoding-integrity.mjs +94 -0
  35. package/engine/encoding-language.mjs +506 -0
  36. package/engine/encoding-lexicon.mjs +701 -0
  37. package/engine/encoding-notes.mjs +186 -0
  38. package/engine/encoding-standard.mjs +42 -0
  39. package/engine/encoding-template.mjs +370 -0
  40. package/engine/encoding-terms.mjs +178 -0
  41. package/engine/encoding.mjs +952 -0
  42. package/engine/extension-attributes.mjs +2 -0
  43. package/engine/helpers.mjs +1 -1
  44. package/engine/index.mjs +7 -0
  45. package/engine/language-terms.mjs +17 -16
  46. package/engine/{logographic.mjs → language-writings.mjs} +29 -38
  47. package/engine/markup-validation.mjs +1 -1
  48. package/engine/note-vocabulary.mjs +4 -158
  49. package/engine/pdf-render.mjs +31 -3
  50. package/engine/roman-characters.mjs +135 -16
  51. package/engine/roman-run.mjs +149 -0
  52. package/engine/roman-verse.mjs +95 -0
  53. package/engine/run-rows.mjs +29 -31
  54. package/engine/run-words.mjs +2 -2
  55. package/engine/script-emphasis-terms.mjs +3 -0
  56. package/engine/script-emphasis.mjs +3 -10
  57. package/engine/script-font-delivery.mjs +9 -12
  58. package/engine/script-fonts.mjs +2 -1
  59. package/engine/site-build.mjs +14 -5
  60. package/engine/site-preview.mjs +12 -2
  61. package/engine/sql-tables.mjs +36 -31
  62. package/engine/verse-metre.mjs +1 -1
  63. package/engine/word-forms.mjs +1 -1
  64. package/engine/word-inflections.mjs +48 -0
  65. package/engine/word-rows.mjs +1 -1
  66. package/engine/word-terms.mjs +16 -17
  67. package/package.json +11 -5
  68. package/types/config.d.mts +5 -0
  69. package/types/convert.d.mts +44 -0
  70. package/types/e2e.d.mts +1 -1
  71. package/types/engine/base-compiler.d.mts +4 -2
  72. package/types/engine/content-index.d.mts +4 -1
  73. package/types/engine/content-scripts.d.mts +103 -42
  74. package/types/engine/converter-data.d.mts +11 -0
  75. package/types/engine/dictionary-rows.d.mts +1 -25
  76. package/types/engine/encoding-integrity.d.mts +14 -0
  77. package/types/engine/encoding-language.d.mts +36 -0
  78. package/types/engine/encoding-lexicon.d.mts +259 -0
  79. package/types/engine/encoding-notes.d.mts +39 -0
  80. package/types/engine/encoding-standard.d.mts +11 -0
  81. package/types/engine/encoding-template.d.mts +44 -0
  82. package/types/engine/encoding-terms.d.mts +143 -0
  83. package/types/engine/encoding.d.mts +155 -0
  84. package/types/engine/helpers.d.mts +1 -1
  85. package/types/engine/index.d.mts +4 -0
  86. package/types/engine/language-terms.d.mts +1 -1
  87. package/types/engine/{logographic.d.mts → language-writings.d.mts} +6 -6
  88. package/types/engine/markup-validation.d.mts +1 -1
  89. package/types/engine/pdf-render.d.mts +1 -1
  90. package/types/engine/roman-characters.d.mts +2 -2
  91. package/types/engine/roman-run.d.mts +101 -0
  92. package/types/engine/roman-verse.d.mts +23 -0
  93. package/types/engine/run-rows.d.mts +13 -20
  94. package/types/engine/script-emphasis-terms.d.mts +2 -0
  95. package/types/engine/script-emphasis.d.mts +1 -1
  96. package/types/engine/script-font-delivery.d.mts +2 -3
  97. package/types/engine/script-fonts.d.mts +5 -0
  98. package/types/engine/site-preview.d.mts +1 -1
  99. package/types/engine/word-inflections.d.mts +25 -0
  100. package/engine/map-lexicon.mjs +0 -1045
  101. package/engine/text-maps.mjs +0 -654
  102. package/engine/written-in.mjs +0 -204
  103. package/types/engine/map-lexicon.d.mts +0 -330
  104. package/types/engine/text-maps.d.mts +0 -189
  105. package/types/engine/written-in.d.mts +0 -76
@@ -11,7 +11,16 @@
11
11
  * SPDX-License-Identifier: GPL-3.0-or-later
12
12
  */
13
13
 
14
- /** A language's exact Roman source characters, before any script conversion. */
14
+ /** A language's Roman additions and the shared punctuation standard. */
15
+
16
+ import { isSettingScriptCodePoint } from "./content-charset.mjs";
17
+
18
+ /** Sentence terminals in the shared Roman standard. */
19
+ const TERMINALS = new Set(".?‼!");
20
+ /** Unconditional punctuation; ^ is conditional indentation or a language addition. */
21
+ const STANDARD = new Set(".?‼!,«»‹›—…/{}⟦⟧");
22
+ /** Exactly the encoding definition's word-separating whitespace class. */
23
+ export const isRomanWhitespace = (char) => /^[\p{Zs}\u0009-\u000D]$/u.test(char);
15
24
 
16
25
  /** Unicode graphemes keep every canonically composing sequence together. */
17
26
  const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
@@ -38,6 +47,21 @@ export function romanInventoryProblems(value) {
38
47
  );
39
48
  if (/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value))
40
49
  problems.push("`data.roman` contains a control, format or unpaired surrogate character");
50
+ const supplied = [...new Set(value)].filter(
51
+ (char) => STANDARD.has(char) || /[;:"“”]/u.test(char),
52
+ );
53
+ if (supplied.length)
54
+ problems.push(
55
+ "`data.roman` lists only language additions, without standard punctuation, literal markers, semicolons, colons or English quotation marks: " +
56
+ supplied.map((char) => `\`${char}\``).join(", "),
57
+ );
58
+ const native = [...new Set(value)].filter((char) =>
59
+ isSettingScriptCodePoint(char.codePointAt(0)),
60
+ );
61
+ if (native.length)
62
+ problems.push(
63
+ "`data.roman` lists Roman additions, without setting-script characters; write exact native characters through codage",
64
+ );
41
65
  const seen = new Set();
42
66
  const repeated = new Set();
43
67
  for (const char of value) {
@@ -85,29 +109,124 @@ export function romanInventoriesOf(records, languages = {}) {
85
109
  * No punctuation, case or normalization repair is performed.
86
110
  * @param {string} text - Visible Roman source.
87
111
  * @param {RomanInventory} inventory - Its language's declared characters.
88
- * @param {(char: string) => boolean} [native] - Whether a directly typed native sign is drawn in the selected native face.
89
112
  * @returns {Array<{offset: number, message: string}>}
90
113
  */
91
- export function romanTextProblems(text, inventory, native = () => false) {
114
+ export function romanTextProblems(text, inventory) {
92
115
  const out = [];
93
116
  if (!inventory.characters) return out;
94
- for (const { segment: cluster, index } of graphemes.segment(text)) {
95
- let offset = index;
96
- if (cluster !== cluster.normalize("NFC")) {
97
- out.push({
117
+ const decomposed = new Map(
118
+ [...graphemes.segment(text)]
119
+ .filter(({ segment }) => segment !== segment.normalize("NFC"))
120
+ .map(({ segment, index }) => [index, segment]),
121
+ );
122
+ const quotes = [];
123
+ let sentenceStart = true;
124
+ const report = (offset, message) => out.push({ offset, message });
125
+ for (let offset = 0; offset < text.length;) {
126
+ const char = String.fromCodePoint(text.codePointAt(offset));
127
+ if (char === "⟦") {
128
+ // The encoding validates codage and its boundary grammar. Roman
129
+ // punctuation and language letters never reinterpret its contents.
130
+ const close = text.indexOf("⟧", offset + 1);
131
+ offset = close < 0 ? text.length : close + 1;
132
+ sentenceStart = false;
133
+ continue;
134
+ }
135
+ const cluster = decomposed.get(offset);
136
+ if (cluster) {
137
+ report(
98
138
  offset,
99
- message: `\`${cluster}\` is not NFC spelling in \`${inventory.language}\`; write \`${cluster.normalize("NFC")}\``,
100
- });
139
+ `\`${cluster}\` is not NFC spelling in \`${inventory.language}\`; write \`${cluster.normalize("NFC")}\``,
140
+ );
141
+ offset += cluster.length;
142
+ sentenceStart = false;
101
143
  continue;
102
144
  }
103
- for (const char of cluster) {
104
- if (!/[ \t\r\n]/u.test(char) && !inventory.characters.has(char) && !native(char))
105
- out.push({
106
- offset,
107
- message: `\`${char}\` (${codeName(char)}) is not in \`${inventory.language}\`'s \`data.roman\` inventory`,
108
- });
145
+ if (isRomanWhitespace(char)) {
109
146
  offset += char.length;
147
+ continue;
110
148
  }
149
+ if (char === "^") {
150
+ const start =
151
+ Math.max(text.lastIndexOf("\n", offset - 1), text.lastIndexOf("/", offset - 1)) + 1;
152
+ if (/^\^*$/.test(text.slice(start, offset))) {
153
+ // Indentation is layout and leaves sentence-start ellipsis
154
+ // semantics intact. The reference encoder uses these bounds.
155
+ offset += char.length;
156
+ continue;
157
+ }
158
+ }
159
+ if (text.startsWith("<<", offset) || text.startsWith(">>", offset)) {
160
+ report(
161
+ offset,
162
+ `write \`${char === "<" ? "«" : "»"}\` for a quotation, not \`${char}${char}\``,
163
+ );
164
+ offset += 2;
165
+ continue;
166
+ }
167
+ if (text.startsWith("...", offset)) {
168
+ report(offset, "write the ellipsis `…`, not three periods `...`");
169
+ offset += 3;
170
+ continue;
171
+ }
172
+ if (char === ";" || char === ":") {
173
+ report(
174
+ offset,
175
+ `\`${char}\` (${codeName(char)}) is not standard Roman punctuation; write \`,\` for a pause within a sentence or \`.\` between complete sentences`,
176
+ );
177
+ } else if (/^["“”]$/u.test(char)) {
178
+ report(
179
+ offset,
180
+ "write `«…»` for an outer quotation and `‹…›` for a quotation inside it",
181
+ );
182
+ } else if (isSettingScriptCodePoint(char.codePointAt(0))) {
183
+ report(
184
+ offset,
185
+ `write the setting-script character ${codeName(char)} as \`⟦${codeName(char)}⟧\` or by its encoding's codage name`,
186
+ );
187
+ } else if (char === "^" && !inventory.characters.has(char)) {
188
+ report(
189
+ offset,
190
+ "`^` is line-start indentation; elsewhere it must be a declared language addition in `data.roman`",
191
+ );
192
+ } else if (!STANDARD.has(char) && !inventory.characters.has(char)) {
193
+ report(
194
+ offset,
195
+ `\`${char}\` (${codeName(char)}) is not in \`${inventory.language}\`'s \`data.roman\` inventory`,
196
+ );
197
+ }
198
+ if (char === "«" || char === "‹") {
199
+ const expected = quotes.length % 2 === 0 ? "«" : "‹";
200
+ if (char !== expected)
201
+ report(offset, `a quotation at this depth opens with \`${expected}\``);
202
+ quotes.push({ char, offset });
203
+ sentenceStart = true;
204
+ } else if (char === "»" || char === "›") {
205
+ const opening = quotes.at(-1);
206
+ const expected = opening?.char === "«" ? "»" : "›";
207
+ if (!opening) report(offset, `quotation closer \`${char}\` has no opening quotation`);
208
+ else {
209
+ if (char !== expected)
210
+ report(offset, `this quotation closes with \`${expected}\`, not \`${char}\``);
211
+ quotes.pop();
212
+ sentenceStart = false;
213
+ }
214
+ } else if (char === "—" || (char === "…" && !sentenceStart)) {
215
+ const rest = text.slice(offset + char.length);
216
+ if ([...rest].some((next) => !isRomanWhitespace(next)) && !TERMINALS.has(rest[0]))
217
+ report(
218
+ offset,
219
+ `\`${char}\` is immediately followed by a terminal (\`.\`, \`?\`, \`‼\` or \`!\`) except at the end of a run`,
220
+ );
221
+ sentenceStart = false;
222
+ } else if (TERMINALS.has(char)) sentenceStart = true;
223
+ else sentenceStart = false;
224
+ offset += char.length;
111
225
  }
112
- return out;
226
+ for (const opening of quotes)
227
+ report(
228
+ opening.offset,
229
+ `quotation \`${opening.char}\` needs its closing \`${opening.char === "«" ? "»" : "›"}\``,
230
+ );
231
+ return out.sort((a, b) => a.offset - b.offset);
113
232
  }
@@ -0,0 +1,149 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * Project an authored run to logical Roman text while retaining source offsets.
16
+ * Presentation-only children contribute their text; names contribute standard
17
+ * braces. The caller decides which children are independent runs and which
18
+ * ranges are layout or emphasis delimiters.
19
+ * @module
20
+ */
21
+
22
+ /**
23
+ * @typedef {object} RomanSpan
24
+ * @property {number} start - Opening bracket offset.
25
+ * @property {number} end - Offset past the attribute block.
26
+ * @property {number} contentEnd - Closing bracket offset.
27
+ * @property {Array<[string, string]>} [attrs] - Authored attributes.
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} ProjectedSpan
32
+ * @property {RomanSpan} source - The authored child.
33
+ * @property {number} from - Its first logical UTF-16 offset, including name braces.
34
+ * @property {number} to - Offset past its logical text and braces.
35
+ * @property {number} contentFrom - Logical text after its opening name brace.
36
+ * @property {number} contentTo - Offset past the logical text, before its closing name brace.
37
+ * @property {boolean} name - Whether this child marks a name.
38
+ * @property {boolean} included - Whether this child contributes its own text.
39
+ */
40
+
41
+ /**
42
+ * Remove Markdown escapes, inline presentation syntax and excluded layout.
43
+ * The returned offsets refer to the original source, in UTF-16 units, matching
44
+ * the source scanner. A supplementary character has two entries, both pointing
45
+ * at its original first unit. Synthetic braces point at the name's brackets.
46
+ * @param {string} source - Complete authored source.
47
+ * @param {object} [options] - Bounds and projections chosen by the run scanner.
48
+ * @param {number} [options.from] - First included source offset.
49
+ * @param {number} [options.to] - Offset past the included source.
50
+ * @param {readonly RomanSpan[]} [options.spans] - Scanned spans, with absolute offsets.
51
+ * @param {(span: RomanSpan) => boolean} [options.includeSpan] - Whether a child inherits this run.
52
+ * @param {(span: RomanSpan) => string} [options.opaqueSpan] - Placeholder for an independent child. The default empty name keeps sentence state without introducing that child's letters.
53
+ * @param {ReadonlyArray<readonly [number, number]>} [options.exclude] - Absolute source ranges that carry layout or presentation syntax.
54
+ * @param {boolean} [options.markNames] - Whether name children contribute braces.
55
+ * @returns {{text: string, offsets: number[], spans: ProjectedSpan[]}} Logical text and source locations, with child ranges for presentation.
56
+ */
57
+ export function plainRomanRun(
58
+ source,
59
+ {
60
+ from = 0,
61
+ to = source.length,
62
+ spans = [],
63
+ includeSpan = () => true,
64
+ opaqueSpan = () => "{}",
65
+ exclude = [],
66
+ markNames = true,
67
+ } = {},
68
+ ) {
69
+ let text = "";
70
+ const offsets = [];
71
+ const projected = [];
72
+ const children = [...spans]
73
+ .filter((span) => span.start >= from && span.end <= to)
74
+ .sort((a, b) => a.start - b.start || b.end - a.end);
75
+ const byStart = new Map(children.map((span) => [span.start, span]));
76
+ const excluded = [...exclude].sort((a, b) => a[0] - b[0]);
77
+ let excludedIndex = 0;
78
+ const append = (value, offset) => {
79
+ text += value;
80
+ for (let unit = 0; unit < value.length; unit++) offsets.push(offset);
81
+ };
82
+ const walk = (first, past) => {
83
+ let at = first;
84
+ while (at < past) {
85
+ while (excluded[excludedIndex]?.[1] <= at) excludedIndex++;
86
+ const omitted = excluded[excludedIndex];
87
+ if (omitted && omitted[0] <= at) {
88
+ at = Math.min(past, omitted[1]);
89
+ continue;
90
+ }
91
+ const candidate = byStart.get(at);
92
+ const span = candidate && candidate.end <= past ? candidate : undefined;
93
+ if (span) {
94
+ const name = (span.attrs?.find(([key]) => key === "class")?.[1] ?? "")
95
+ .split(/\s+/)
96
+ .includes("name");
97
+ const included = includeSpan(span);
98
+ const range = {
99
+ source: span,
100
+ from: text.length,
101
+ to: text.length,
102
+ contentFrom: text.length,
103
+ contentTo: text.length,
104
+ name,
105
+ included,
106
+ };
107
+ projected.push(range);
108
+ if (included) {
109
+ if (name && markNames) append("{", span.start);
110
+ range.contentFrom = text.length;
111
+ walk(span.start + 1, span.contentEnd);
112
+ range.contentTo = text.length;
113
+ if (name && markNames) append("}", span.contentEnd);
114
+ } else {
115
+ append(opaqueSpan(span), span.start);
116
+ range.contentTo = text.length;
117
+ }
118
+ range.to = text.length;
119
+ at = span.end;
120
+ continue;
121
+ }
122
+ // A codage's notation belongs to its encoding. Do not unescape or
123
+ // interpret anything between its immutable boundaries.
124
+ if (source[at] === "⟦") {
125
+ const close = source.indexOf("⟧", at + 1);
126
+ const end = close >= 0 && close < past ? close + 1 : past;
127
+ while (at < end) {
128
+ const char = String.fromCodePoint(source.codePointAt(at));
129
+ append(char, at);
130
+ at += char.length;
131
+ }
132
+ continue;
133
+ }
134
+ const escaped =
135
+ source[at] === "\\" && at + 1 < past && /[!-/:-@[-`{-~]/.test(source[at + 1]);
136
+ const offset = at + (escaped ? 1 : 0);
137
+ // A terminal Markdown line-break slash is layout, not Roman.
138
+ if (source[at] === "\\" && !escaped && /[\r\n]/.test(source[at + 1] ?? "")) {
139
+ at++;
140
+ continue;
141
+ }
142
+ const char = String.fromCodePoint(source.codePointAt(offset));
143
+ append(char, offset);
144
+ at = offset + char.length;
145
+ }
146
+ };
147
+ walk(from, to);
148
+ return { text, offsets, spans: projected };
149
+ }
@@ -0,0 +1,95 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /** Logical verse text and separately located structural boundaries. @module */
15
+ import { verseLine } from "./content-verse.mjs";
16
+ import { plainRomanRun } from "./roman-run.mjs";
17
+
18
+ /**
19
+ * Remove visual indentation before checking or encoding Roman verse. Each
20
+ * content line keeps its own boundary, including the last line before a stanza
21
+ * or canto; those extra structural controls do not consume its line boundary.
22
+ * The caller writes the boundaries through its selected encoding.
23
+ * @param {string} source - Complete authored source.
24
+ * @param {Parameters<typeof plainRomanRun>[1] & {isCantoLine?: (line: string) => boolean}} [options] - Run bounds, spans and layout projection.
25
+ * @returns {ReturnType<typeof plainRomanRun> & {boundaries: Array<{kind: "line"|"stanza"|"canto", at: number, sourceOffset: number}>, errors: Array<{offset: number, message: string}>}} Visible text, positions and structural events.
26
+ */
27
+ export function plainRomanVerse(source, options = {}) {
28
+ const {
29
+ from = 0,
30
+ to = source.length,
31
+ exclude = [],
32
+ isCantoLine = (line) => /^\s*---\s+\{\.canto\}\s*$/.test(line),
33
+ } = options;
34
+ const lines = [];
35
+ const layout = [...exclude];
36
+ let at = from;
37
+ for (const match of source.slice(from, to).matchAll(/([^\r\n]*)(\r\n|\r|\n|$)/g)) {
38
+ if (!match[0]) continue;
39
+ const line = match[1];
40
+ const end = at + line.length;
41
+ const kind =
42
+ isCantoLine(line) ? "canto"
43
+ : line.trim() ? "content"
44
+ : "blank";
45
+ lines.push({ from: at, to: end, kind });
46
+ if (kind === "canto") layout.push([at, end]);
47
+ else if (kind === "content") {
48
+ const { markerStart, markerLength } = verseLine(line);
49
+ if (markerLength) layout.push([at + markerStart, at + markerStart + markerLength]);
50
+ }
51
+ at += match[0].length;
52
+ }
53
+ const projected = plainRomanRun(source, { ...options, from, to, exclude: layout });
54
+ const logicalAt = (offset) => {
55
+ let first = 0,
56
+ past = projected.offsets.length;
57
+ while (first < past) {
58
+ const middle = (first + past) >>> 1;
59
+ if (projected.offsets[middle] < offset) first = middle + 1;
60
+ else past = middle;
61
+ }
62
+ return first;
63
+ };
64
+ /** @type {Array<{kind: "line"|"stanza"|"canto", at: number, sourceOffset: number}>} */
65
+ const boundaries = [];
66
+ for (let index = 0; index < lines.length; index++) {
67
+ const line = lines[index];
68
+ if (line.kind === "content")
69
+ boundaries.push({ kind: "line", at: logicalAt(line.to), sourceOffset: line.to });
70
+ else if (line.kind === "canto")
71
+ boundaries.push({ kind: "canto", at: logicalAt(line.from), sourceOffset: line.from });
72
+ else if (lines[index - 1]?.kind === "content") {
73
+ const following = lines.slice(index + 1).find((next) => next.kind !== "blank");
74
+ if (following?.kind === "content")
75
+ boundaries.push({
76
+ kind: "stanza",
77
+ at: logicalAt(line.from),
78
+ sourceOffset: line.from,
79
+ });
80
+ }
81
+ }
82
+ const errors = [];
83
+ for (let index = 0; index < projected.text.length; index++) {
84
+ if (projected.text[index] === "⟦") {
85
+ const close = projected.text.indexOf("⟧", index + 1);
86
+ index = close < 0 ? projected.text.length : close;
87
+ } else if (projected.text[index] === "/")
88
+ errors.push({
89
+ offset: projected.offsets[index],
90
+ message:
91
+ "a verse fence uses authored line endings for verse boundaries; write no literal `/` inside it",
92
+ });
93
+ }
94
+ return { ...projected, boundaries, errors };
95
+ }
@@ -12,15 +12,14 @@
12
12
  */
13
13
 
14
14
  /**
15
- * Spans, divs and verse fences marked with `lang=`, `font=` or `map=`, projected
15
+ * Spans, divs and verse fences marked with `lang=`, `font=` or `enc=`, projected
16
16
  * from own-note index records into the SQL `runs` relation. Every value is a
17
17
  * column value and none is a key, so two notes writing one tag in different
18
18
  * case are two rows. Documentation records and assets contribute no rows.
19
19
  *
20
- * Native runs retain their explicit opt-in so language defaults can be filled
21
- * by {@link fillWrittenIn} once every record exists. Roman language-only runs
22
- * retain null map/font fields; ordinary generic-family inheritance is already
23
- * projected by the shared scanner.
20
+ * Encoding-document language and font defaults are filled by
21
+ * {@link fillEncodingRuns} once every document exists. Roman runs use the
22
+ * default encoding; ordinary font inheritance is projected by the scanner.
24
23
  *
25
24
  * @module
26
25
  */
@@ -29,9 +28,8 @@ import { canonicalKey, readCanonicalKey } from "./address.mjs";
29
28
  import { encodeAddresses } from "./address-values.mjs";
30
29
  import { isNoteRecord } from "./index-records.mjs";
31
30
  import { scanRuns } from "./content-scripts.mjs";
32
- import { languageRecordsByName } from "./map-lexicon.mjs";
33
- import { mapsFromRecords } from "./text-maps.mjs";
34
- import { writtenInOf } from "./written-in.mjs";
31
+ import { resolveScriptFonts } from "./script-fonts.mjs";
32
+ import { encodingsFromRecords, encodingAddress } from "./encoding-notes.mjs";
35
33
 
36
34
  /** Every column of the `runs` relation, in order. */
37
35
  export const RUN_COLUMNS = Object.freeze([
@@ -40,7 +38,7 @@ export const RUN_COLUMNS = Object.freeze([
40
38
  "lang",
41
39
  "tag",
42
40
  "font",
43
- "map",
41
+ "enc",
44
42
  "line",
45
43
  "column",
46
44
  ]);
@@ -54,11 +52,8 @@ export const RUN_COLUMNS = Object.freeze([
54
52
  * own `lang=`, else the one the div or fence around it states.
55
53
  * @property {string|null} tag - The BCP 47 tag it resolves to, or null.
56
54
  * @property {string|null} font - Its explicit font, an inherited ordinary
57
- * prose family, or the configured default for an opted-in native run.
58
- * @property {string|null} map - Its explicit map or the configured map of an
59
- * opted-in native run; null for Roman language-only text.
60
- * @property {boolean} [nativefont] - Explicit native opt-in on this construct;
61
- * absent on Roman runs. This internal flag is not a SQL column.
55
+ * prose family, or the font of the selected encoding document.
56
+ * @property {string|null} enc - Its resolved encoding document, or default for Roman text.
62
57
  * @property {number} line - 1-based file line of its opening.
63
58
  * @property {number} column - 1-based UTF-16 file column of its opening.
64
59
  */
@@ -86,36 +81,39 @@ export function recordRuns(body, { context, bodyLine = 1, bodyColumn = 1, syntax
86
81
  lang: run.lang ?? null,
87
82
  tag: run.tag ?? null,
88
83
  font: run.font ?? null,
89
- map: run.map ?? null,
90
- ...(run.nativefont ? { nativefont: true } : {}),
84
+ enc: run.enc ?? null,
91
85
  line: bodyLine + run.line - 1,
92
86
  column: run.column + (run.line === 1 ? bodyColumn - 1 : 0),
93
87
  }));
94
88
  }
95
89
 
96
90
  /**
97
- * Give explicitly native recorded runs the map and face their language names in
98
- * `data.writtenIn`, where the run leaves them unstated, as a run is set. Read
99
- * once every record exists, since a run's language note may be recorded after
100
- * the note that marks it.
91
+ * Resolve recorded encoding Addresses and fill the language and font supplied
92
+ * by each selected document, once every index record exists.
101
93
  *
102
94
  * @param {Record<string, any>[]} records - The package's index records; each
103
95
  * note record's `runs` is updated in place.
104
- * @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
96
+ * @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string, rootDir?:string, paths?:{content?:string}}} config -
105
97
  * The configured languages, whose `note` names each language note.
106
98
  */
107
- export function fillWrittenIn(records, config) {
108
- const maps = mapsFromRecords(records);
109
- const scripts = writtenInOf(languageRecordsByName(records, config), maps);
110
- if (!scripts.size) return;
99
+ export function fillEncodingRuns(records, config) {
100
+ const { encodings } = encodingsFromRecords(
101
+ records,
102
+ config,
103
+ config?.rootDir ? resolveScriptFonts(config.rootDir) : undefined,
104
+ );
111
105
  for (const record of records) {
112
106
  if (!record || !isNoteRecord(record) || !Array.isArray(record.runs)) continue;
113
107
  for (const run of record.runs) {
114
- if (!run.nativefont) continue;
115
- const script = scripts.get(run.lang);
116
- if (!script) continue;
117
- if (run.map == null) run.map = script.map;
118
- if (run.font == null) run.font = script.font;
108
+ run.enc ??= "default";
109
+ const encoding =
110
+ encodings.get(run.enc) ??
111
+ encodings.get(encodingAddress(run.enc, config.contentPackage));
112
+ if (!encoding) continue;
113
+ run.enc = encoding.name;
114
+ run.lang ??= encoding.language;
115
+ run.font ??= encoding.font;
116
+ run.tag ??= config.languages?.[run.lang]?.tag ?? null;
119
117
  }
120
118
  }
121
119
  }
@@ -150,7 +148,7 @@ export function runsRows(records) {
150
148
  lang: run.lang ?? null,
151
149
  tag: run.tag ?? null,
152
150
  font: run.font ?? null,
153
- map: run.map ?? null,
151
+ enc: run.enc ?? null,
154
152
  line: run.line,
155
153
  column: run.column,
156
154
  });
@@ -13,7 +13,7 @@
13
13
 
14
14
  /**
15
15
  * **A mapped run's words are words of its language.** In a run marked with
16
- * `lang=`, whose language's `note` names its language note, and with `map=`,
16
+ * `lang=`, whose language's `note` names its language note, and with `enc=`,
17
17
  * each word (see {@link module:engine/word-pattern.WORD}) is accepted when,
18
18
  * case folded, it is:
19
19
  *
@@ -36,7 +36,7 @@
36
36
  * @module
37
37
  */
38
38
 
39
- import { lexiconsFromRecords } from "./map-lexicon.mjs";
39
+ import { lexiconsFromRecords } from "./encoding-lexicon.mjs";
40
40
  import { RESERVED_TONGUE } from "./language-terms.mjs";
41
41
  import { addWords, foldWord, lexiconWords } from "./word-forms.mjs";
42
42
  import { WORD } from "./word-pattern.mjs";
@@ -44,3 +44,6 @@ export const SCRIPT_DIRECTIONS = Object.freeze(["vertical-rl"]);
44
44
 
45
45
  /** The `subType` of a script note. */
46
46
  export const SCRIPT_SUBTYPE = "script";
47
+
48
+ /** The ordinary prose families accepted by font=. */
49
+ export const GENERIC_FAMILIES = Object.freeze(["mono", "serif", "sans"]);
@@ -21,14 +21,12 @@
21
21
  * script:
22
22
  * fonts: [semaron-kuriale]
23
23
  * emphasis: rubric
24
- * strongEmphasis: overline
25
24
  * ```
26
25
  *
27
26
  * - **`fonts`** lists installed faces by the slug `font=` names them by. A
28
27
  * face belongs to one script.
29
28
  * - **`emphasis`** is `rubric` (red text) or `overline` (a line above the
30
29
  * word), and sets both `*…*` and `**…**`.
31
- * - **`strongEmphasis`**, where stated, sets `**…**` alone.
32
30
  *
33
31
  * Inside a run in one of those faces, `*…*` and `**…**` stay an `<em>` and a
34
32
  * `<strong>` on the site and in Foundry, carrying {@link module:engine/script-emphasis-terms.EMPHASIS_CLASSES}'s
@@ -48,7 +46,7 @@ import {
48
46
  LANGUAGE_EMPHASES,
49
47
  SCRIPT_SUBTYPE,
50
48
  } from "./script-emphasis-terms.mjs";
51
- import { GENERIC_FAMILIES } from "./text-maps.mjs";
49
+ import { GENERIC_FAMILIES } from "./script-emphasis-terms.mjs";
52
50
  import { LANGUAGE_SUBTYPE } from "./word-terms.mjs";
53
51
 
54
52
  export {
@@ -97,12 +95,7 @@ export function emphasesFromRecords(records) {
97
95
  const script = record.data?.script;
98
96
  if (!isMap(script) || !Array.isArray(script.fonts) || !isConvention(script.emphasis))
99
97
  continue;
100
- const strong =
101
- script.strongEmphasis === undefined || script.strongEmphasis === null ?
102
- script.emphasis
103
- : script.strongEmphasis;
104
- if (!isConvention(strong)) continue;
105
- const emphasis = Object.freeze({ em: script.emphasis, strong });
98
+ const emphasis = Object.freeze({ em: script.emphasis, strong: script.emphasis });
106
99
  for (const slug of script.fonts)
107
100
  if (typeof slug === "string" && !out.has(slug)) out.set(slug, emphasis);
108
101
  }
@@ -114,7 +107,7 @@ export function emphasesFromRecords(records) {
114
107
  *
115
108
  * @param {ReadonlyMap<string, Record<string, any>>} byName - Each configured
116
109
  * language's note record, by language name, from
117
- * {@link module:engine/map-lexicon.languageRecordsByName}.
110
+ * {@link module:engine/encoding-lexicon.languageRecordsByName}.
118
111
  * @returns {Map<string, LanguageEmphasis>} The conventions, by language name;
119
112
  * a language whose note states none, or one the build does not know, is
120
113
  * absent.