@heroiclands/package-build 24.1.1 → 25.1.0

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 (107) hide show
  1. package/CHANGELOG.md +36 -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 +154 -11
  9. package/docs/authoring/links-and-markup.md +260 -473
  10. package/docs/commands.md +118 -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 +135 -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 +453 -569
  19. package/docs/reference/format-details.md +290 -380
  20. package/docs/reference/note-types.md +80 -62
  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 +21 -1
  25. package/engine/content-format.yaml +7 -2
  26. package/engine/content-index.mjs +6 -2
  27. package/engine/content-links.mjs +1 -0
  28. package/engine/content-scripts.mjs +658 -423
  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 +699 -0
  37. package/engine/encoding-notes.mjs +186 -0
  38. package/engine/encoding-standard.mjs +42 -0
  39. package/engine/encoding-template.mjs +313 -0
  40. package/engine/encoding-terms.mjs +141 -0
  41. package/engine/encoding.mjs +946 -0
  42. package/engine/extension-attributes.mjs +2 -0
  43. package/engine/helpers.mjs +1 -1
  44. package/engine/index.mjs +10 -0
  45. package/engine/language-data.mjs +4 -1
  46. package/engine/language-terms.mjs +23 -12
  47. package/engine/{logographic.mjs → language-writings.mjs} +29 -38
  48. package/engine/markup-validation.mjs +1 -1
  49. package/engine/note-vocabulary.mjs +4 -158
  50. package/engine/pdf-render.mjs +31 -3
  51. package/engine/roman-characters.mjs +232 -0
  52. package/engine/roman-run.mjs +149 -0
  53. package/engine/roman-verse.mjs +95 -0
  54. package/engine/run-rows.mjs +29 -31
  55. package/engine/run-words.mjs +2 -2
  56. package/engine/script-emphasis-terms.mjs +3 -0
  57. package/engine/script-emphasis.mjs +3 -10
  58. package/engine/script-font-delivery.mjs +9 -12
  59. package/engine/script-fonts.mjs +2 -1
  60. package/engine/site-build.mjs +14 -5
  61. package/engine/site-preview.mjs +12 -2
  62. package/engine/sql-tables.mjs +36 -31
  63. package/engine/verse-metre.mjs +1 -1
  64. package/engine/word-forms.mjs +1 -1
  65. package/engine/word-inflections.mjs +48 -0
  66. package/engine/word-rows.mjs +1 -1
  67. package/engine/word-terms.mjs +16 -17
  68. package/package.json +11 -5
  69. package/types/config.d.mts +5 -0
  70. package/types/convert.d.mts +44 -0
  71. package/types/e2e.d.mts +1 -1
  72. package/types/engine/base-compiler.d.mts +4 -2
  73. package/types/engine/content-charset.d.mts +8 -0
  74. package/types/engine/content-index.d.mts +4 -1
  75. package/types/engine/content-scripts.d.mts +108 -42
  76. package/types/engine/converter-data.d.mts +11 -0
  77. package/types/engine/dictionary-rows.d.mts +1 -25
  78. package/types/engine/encoding-integrity.d.mts +14 -0
  79. package/types/engine/encoding-language.d.mts +36 -0
  80. package/types/engine/encoding-lexicon.d.mts +259 -0
  81. package/types/engine/encoding-notes.d.mts +39 -0
  82. package/types/engine/encoding-standard.d.mts +11 -0
  83. package/types/engine/encoding-template.d.mts +43 -0
  84. package/types/engine/encoding-terms.d.mts +105 -0
  85. package/types/engine/encoding.d.mts +155 -0
  86. package/types/engine/helpers.d.mts +1 -1
  87. package/types/engine/index.d.mts +5 -0
  88. package/types/engine/language-terms.d.mts +1 -1
  89. package/types/engine/{logographic.d.mts → language-writings.d.mts} +6 -6
  90. package/types/engine/markup-validation.d.mts +1 -1
  91. package/types/engine/pdf-render.d.mts +1 -1
  92. package/types/engine/roman-characters.d.mts +45 -0
  93. package/types/engine/roman-run.d.mts +101 -0
  94. package/types/engine/roman-verse.d.mts +23 -0
  95. package/types/engine/run-rows.d.mts +13 -20
  96. package/types/engine/script-emphasis-terms.d.mts +2 -0
  97. package/types/engine/script-emphasis.d.mts +1 -1
  98. package/types/engine/script-font-delivery.d.mts +2 -3
  99. package/types/engine/script-fonts.d.mts +5 -0
  100. package/types/engine/site-preview.d.mts +1 -1
  101. package/types/engine/word-inflections.d.mts +25 -0
  102. package/engine/map-lexicon.mjs +0 -1034
  103. package/engine/text-maps.mjs +0 -654
  104. package/engine/written-in.mjs +0 -204
  105. package/types/engine/map-lexicon.d.mts +0 -330
  106. package/types/engine/text-maps.d.mts +0 -189
  107. package/types/engine/written-in.d.mts +0 -76
package/convert.mjs ADDED
@@ -0,0 +1,182 @@
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
+ /** Browser conversion from one module's versioned, self-contained data file. */
14
+ import { renderAddress } from "./engine/address-render.mjs";
15
+ import { compileEncoding, encodeText, decodeText, EncodingError } from "./engine/encoding.mjs";
16
+ import { encodeWithLanguage, decodeWithLanguage } from "./engine/encoding-language.mjs";
17
+ /**
18
+ * @typedef {{language:string,data:Record<string,any>}} ConverterLexicon
19
+ * @typedef {{name:string,lexicon?:ConverterLexicon}} ConverterLanguage
20
+ * @typedef {{format:2,contentPackage:string,encodings:Record<string,any>,languages:Record<string,ConverterLanguage>}} ConverterData
21
+ * @typedef {{enc?:string,font?:string,verse?:boolean}} ConversionOptions
22
+ */
23
+ const compiled = new WeakMap();
24
+ const validated = new WeakSet();
25
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
26
+ function keys(value, allowed, where) {
27
+ if (!object(value)) throw new TypeError(`${where} must be an object`);
28
+ for (const key of Object.keys(value))
29
+ if (!allowed.includes(key)) throw new TypeError(`${where}.${key} is unknown`);
30
+ }
31
+ function definition(value, name) {
32
+ if (!object(value)) throw new TypeError("An encoding definition must be an object");
33
+ keys(value, ["data", "codes"], `encodings.${name}`);
34
+ let known = compiled.get(value);
35
+ if (!known) {
36
+ known = compileEncoding(value.data, { codes: value.codes, name });
37
+ compiled.set(value, known);
38
+ }
39
+ return known;
40
+ }
41
+
42
+ function lexicalData(value, where) {
43
+ keys(value, ["words", "affixes", "grammar", "register"], where);
44
+ for (const field of ["words", "affixes", "register"]) {
45
+ if (value[field] === undefined) continue;
46
+ if (!Array.isArray(value[field])) throw new TypeError(`${where}.${field} must be an array`);
47
+ for (const row of value[field]) {
48
+ const name = field === "register" ? "name" : "form";
49
+ if (!object(row) || typeof row[name] !== "string" || !row[name])
50
+ throw new TypeError(`${where}.${field} needs ${name}`);
51
+ if (
52
+ row.native !== undefined &&
53
+ (!Array.isArray(row.native) ||
54
+ row.native.some(
55
+ (native) =>
56
+ !object(native) ||
57
+ typeof native.enc !== "string" ||
58
+ typeof native.text !== "string" ||
59
+ (native.font !== undefined && typeof native.font !== "string"),
60
+ ))
61
+ )
62
+ throw new TypeError(`${where}.${field} has invalid native writings`);
63
+ }
64
+ }
65
+ if (value.grammar !== undefined && !object(value.grammar))
66
+ throw new TypeError(`${where}.grammar must be an object`);
67
+ }
68
+
69
+ /** Validate the format-2 converter data without opening files or using Node.
70
+ * @param {ConverterData} data Parsed JSON, treated as read-only.
71
+ * @returns {ConverterData} The validated data.
72
+ */
73
+ export function validateData(data) {
74
+ if (validated.has(data)) return data;
75
+ keys(data, ["format", "contentPackage", "encodings", "languages"], "data");
76
+ if (data.format !== 2) throw new TypeError("Converter data format must be 2");
77
+ if (
78
+ typeof data.contentPackage !== "string" ||
79
+ !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(data.contentPackage)
80
+ )
81
+ throw new TypeError("Converter data needs its canonical contentPackage");
82
+ if (!object(data.encodings) || !object(data.languages))
83
+ throw new TypeError("Converter encodings and languages must be objects");
84
+ for (const [name, value] of Object.entries(data.encodings)) {
85
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*-note-doc-[a-z0-9]+$/u.test(name))
86
+ throw new TypeError(`Invalid encoding document Address ${name}`);
87
+ const encoding = definition(value, name);
88
+ if (!Object.hasOwn(data.languages, encoding.language))
89
+ throw new TypeError(`Encoding ${name} needs configured language ${encoding.language}`);
90
+ }
91
+ for (const [language, value] of Object.entries(data.languages)) {
92
+ keys(value, ["name", "lexicon"], `languages.${language}`);
93
+ if (typeof value.name !== "string")
94
+ throw new TypeError(`Language ${language} needs its name`);
95
+ if (value.lexicon !== undefined) {
96
+ keys(value.lexicon, ["language", "data"], `languages.${language}.lexicon`);
97
+ if (typeof value.lexicon.language !== "string" || !object(value.lexicon.data))
98
+ throw new TypeError(`Language ${language} has invalid lexicon inputs`);
99
+ lexicalData(value.lexicon.data, `languages.${language}.lexicon.data`);
100
+ }
101
+ }
102
+ validated.add(data);
103
+ return data;
104
+ }
105
+
106
+ function conversion(data, language, text, options, direction) {
107
+ validateData(data);
108
+ if (typeof text !== "string") throw new TypeError("Conversion input must be text");
109
+ let entry = language === null || language === undefined ? undefined : data.languages[language];
110
+ if (language !== null && language !== undefined && !Object.hasOwn(data.languages, language))
111
+ throw new TypeError(`Unknown language ${language}`);
112
+ const name = options.enc ?? "default";
113
+ if (name === "default") return { text, findings: [] };
114
+ const short = /^(?:(?:note-)?doc-|doc\/)?([a-z0-9]+)$/u.exec(name);
115
+ const selected =
116
+ Object.hasOwn(data.encodings, name) ? name
117
+ : short ?
118
+ renderAddress({
119
+ package: data.contentPackage,
120
+ system: "note",
121
+ type: "doc",
122
+ shortcode: short[1],
123
+ })
124
+ : undefined;
125
+ if (!selected || !Object.hasOwn(data.encodings, selected))
126
+ throw new TypeError(`Unknown encoding document ${name}`);
127
+ const encoding = definition(data.encodings[selected], selected);
128
+ if (entry && language !== encoding.language)
129
+ throw new TypeError(`Encoding ${selected} writes ${encoding.language}, not ${language}`);
130
+ entry ??= data.languages[encoding.language];
131
+ const settings = {
132
+ font: options.font ?? encoding.font,
133
+ verse: options.verse,
134
+ lexicon: entry?.lexicon,
135
+ };
136
+ if (entry?.lexicon)
137
+ return direction === "encode" ?
138
+ encodeWithLanguage(encoding, text, settings)
139
+ : decodeWithLanguage(encoding, text, settings);
140
+ try {
141
+ return {
142
+ text:
143
+ direction === "encode" ?
144
+ encodeText(encoding, text, settings)
145
+ : decodeText(encoding, text, settings),
146
+ findings: [],
147
+ };
148
+ } catch (error) {
149
+ if (!(error instanceof EncodingError)) throw error;
150
+ return {
151
+ text: "",
152
+ findings: [
153
+ {
154
+ severity: "error",
155
+ message: error.message,
156
+ offset: error.offset ?? 0,
157
+ line: error.line ?? 1,
158
+ column: error.column ?? 1,
159
+ },
160
+ ],
161
+ };
162
+ }
163
+ }
164
+
165
+ /** Encode with the selected encoding document's language, optionally stated explicitly.
166
+ * @param {ConverterData} data The module's parsed converter data.
167
+ * @param {string|null|undefined} language A language key, or null to infer it from the encoding.
168
+ * @param {string} text Roman input.
169
+ * @param {ConversionOptions} [options] Encoding, face or verse overrides.
170
+ */
171
+ export function encode(data, language, text, options = {}) {
172
+ return conversion(data, language, text, options, "encode");
173
+ }
174
+ /** Decode with the selected encoding document's optional language lexicon.
175
+ * @param {ConverterData} data The module's parsed converter data.
176
+ * @param {string|null|undefined} language A language key, or null to infer it from the encoding.
177
+ * @param {string} text Encoded Unicode input.
178
+ * @param {ConversionOptions} [options] Encoding or face overrides.
179
+ */
180
+ export function decode(data, language, text, options = {}) {
181
+ return conversion(data, language, text, options, "decode");
182
+ }
package/docs/api.md CHANGED
@@ -1343,6 +1343,13 @@ index for checking page links.
1343
1343
 
1344
1344
  `renderPreview(text, { dark = false, infobox = true, file = "", currentDate = "720.1", index })` renders the current Markdown buffer directly to HTML. Frontmatter is optional. Without `index` it needs no package configuration, saved note, content tree, dependency cache, database or Pandoc installation. The result is `{ ok, html, frontmatter, findings }`; `file` only labels diagnostics. Malformed frontmatter produces a warning and remains visible without preventing a preview update.
1345
1345
 
1346
+ With `index`, every marked-text run error appears in `findings` as
1347
+ `{ file, severity: "error", line, column, message }`. Positions refer to the
1348
+ authored buffer, including frontmatter lines, before indexed expansion. Any
1349
+ error finding makes `ok` false. `html` is returned even when `ok` is false, with
1350
+ recoverable Roman text for a native-script run that cannot be set. Without
1351
+ `index`, marked text remains source and produces no run findings.
1352
+
1346
1353
  With `index`, from [`engine.previewIndex`](#enginepreviewindex), the preview expands the buffer as the website expands a page and renders it through the same renderer: links resolve, embeds draw their pictures, marked text is mapped and set in its face, icons draw, figure numbers carry their asset roles, and a concealed link is concealed. A query, which a buffer cannot run, stays highlighted source.
1347
1354
 
1348
1355
  | Export | Signature | Returns | Use it when |
@@ -1384,17 +1391,30 @@ Linting a content tree's **addresses** — the rules every package's notes are a
1384
1391
 
1385
1392
  The charset authored content is held to, so a book can choose its face. The packs and the website render in whatever font the reader's browser or Foundry supplies, and a glyph nobody has is somebody else's problem. **A book is not that.** A PDF embeds the faces it sets, so every character in the corpus is a claim on the book's typeface — and the claim is silent, which is what makes it expensive.
1386
1393
 
1387
- | Export | Signature | Returns | Use it when |
1388
- | -------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
1389
- | `isLetterTier` | `function isLetterTier(cp)` | {boolean} Whether Tier 1 admits it. | Tier 1 — the letters, and the two whitespace characters a file is made of. |
1390
- | `TYPOGRAPHY` | `const TYPOGRAPHY` | — | Tier 2 — typography, enumerated one codepoint at a time. |
1391
- | `NOTATION` | `const NOTATION` | — | Tier 3 — the notation the rules and price tables are written in. |
1392
- | `isAllowedCodePoint` | `function isAllowedCodePoint(cp)` | {boolean} Whether it is allowed outside a code fence. | Whether the charset admits a code point anywhere in a note. |
1393
- | `isDiagramCodePoint` | `function isDiagramCodePoint(cp)` | {boolean} Whether a fence may carry it. | Whether a code point is diagram furniture, admitted inside a fence only. |
1394
- | `refusalFor` | `function refusalFor(cp)` | {string} A clause naming what it is and what to do instead. | The reason a code point is refused. |
1395
- | `decomposedRuns` | `function decomposedRuns(text)` | {Array<{sequence: string, composed: string, index: number}>} Each offending run, in the order it appears. | Every non-NFC run in a string, with the composed form it should have been. |
1396
- | `checkText` | `function checkText(text, file)` | {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} What is wrong, in file order. | Check one file's text against the charset and the normalization rule. |
1397
- | `lintContentCharset` | `function lintContentCharset(contentBase,` | `{{findings: Array<{file: string, line: number, column: number, severity: "warning", message: string}>, files: number}}` The findings, and how many files produced them. | Walk a content tree and check every authored file in it. |
1394
+ | Export | Signature | Returns | Use it when |
1395
+ | -------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
1396
+ | `isLetterTier` | `function isLetterTier(cp)` | {boolean} Whether Tier 1 admits it. | Tier 1 — the letters, and the two whitespace characters a file is made of. |
1397
+ | `TYPOGRAPHY` | `const TYPOGRAPHY` | — | Tier 2 — typography, enumerated one codepoint at a time. |
1398
+ | `NOTATION` | `const NOTATION` | — | Tier 3 — the notation the rules and price tables are written in. |
1399
+ | `isSettingScriptCodePoint` | `isSettingScriptCodePoint(cp)` | `boolean` | recognizing native setting-script input before the selected face checks the individual glyph |
1400
+ | `isAllowedCodePoint` | `function isAllowedCodePoint(cp)` | {boolean} Whether it is allowed outside a code fence. | Whether the charset admits a code point anywhere in a note. |
1401
+ | `isDiagramCodePoint` | `function isDiagramCodePoint(cp)` | {boolean} Whether a fence may carry it. | Whether a code point is diagram furniture, admitted inside a fence only. |
1402
+ | `refusalFor` | `function refusalFor(cp)` | {string} A clause naming what it is and what to do instead. | The reason a code point is refused. |
1403
+ | `decomposedRuns` | `function decomposedRuns(text)` | {Array<{sequence: string, composed: string, index: number}>} Each offending run, in the order it appears. | Every non-NFC run in a string, with the composed form it should have been. |
1404
+ | `checkText` | `function checkText(text, file)` | {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} What is wrong, in file order. | Check one file's text against the charset and the normalization rule. |
1405
+ | `lintContentCharset` | `function lintContentCharset(contentBase,` | `{{findings: Array<{file: string, line: number, column: number, severity: "warning", message: string}>, files: number}}` The findings, and how many files produced them. | Walk a content tree and check every authored file in it. |
1406
+
1407
+ ### `engine.romanCharacters`
1408
+
1409
+ Exact permitted Roman source characters for configured languages, before script
1410
+ encoding. Inventories come from `data.roman` on each language's own note.
1411
+
1412
+ | Export | Signature | Returns | Use it when |
1413
+ | ------------------------ | ---------------------------------------- | ----------------------------- | --------------------------------------------------------------------------------------------- |
1414
+ | `isRomanWhitespace` | `isRomanWhitespace(char)` | `boolean` | recognizing standard spaces and ASCII tab or line whitespace |
1415
+ | `romanInventoryProblems` | `romanInventoryProblems(value)` | `string[]` | checking NFC, repeated characters and forbidden whitespace or controls in a literal inventory |
1416
+ | `romanInventoriesOf` | `romanInventoriesOf(records, languages)` | `Map<string, RomanInventory>` | looking up a configured language's own inventory by name or BCP 47 tag |
1417
+ | `romanTextProblems` | `romanTextProblems(text, inventory)` | `{offset, message}[]` | validating standard Roman punctuation and language additions with exact UTF-16 offsets |
1398
1418
 
1399
1419
  ### `engine.pathCharset`
1400
1420
 
@@ -2761,3 +2781,126 @@ language contributes words, affixes and elements, in that order. Words and
2761
2781
  affixes supply `gloss`, including an affix matched by its `front` form. Elements
2762
2782
  supply `sense` and match their `form` and `variants`. Exact matches precede matches without joining hyphens; case and
2763
2783
  accents are preserved. A missing gloss returns `null`.
2784
+
2785
+ ### `engine.encoding`
2786
+
2787
+ Browser-safe compilation of an encoding document with separately resolved font glyph codes;
2788
+ see [encoding text](encoding-core.md). No files are read. Text errors expose a
2789
+ UTF-16 `offset` and one-based code-point `line` and `column`; definition errors
2790
+ expose their schema `key`.
2791
+
2792
+ | Export | Signature | Returns | Use it when |
2793
+ | ----------------- | ------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------- |
2794
+ | `EncodingError` | `new EncodingError(message, details)` | error | locating a schema fault or unidentified character |
2795
+ | `compileEncoding` | `compileEncoding(data, {codes, name})` | compiled encoding | validating executable document mappings and rules |
2796
+ | `compileRows` | `compileRows(definition)` | internal row program | executing captured row-conformance fixtures; application inputs use compileEncoding |
2797
+ | `validateSource` | `validateSource(encoding, text, options)` | void | validating accepted Roman spelling and rules independently of mapping coverage |
2798
+ | `encodePieces` | `encodePieces(encoding, text, options)` | piece array | retaining original UTF-16 `start` and `end` offsets while encoding |
2799
+ | `encodeText` | `encodeText(encoding, text, {font, verse})` | string | encoding exactly by rows and inline codes |
2800
+ | `decodeText` | `decodeText(encoding, text, {font})` | string | decoding by longest output matches |
2801
+
2802
+ ### `engine.encodingLanguage`
2803
+
2804
+ Pure language-aware conversion takes a compiled encoding and optional plain
2805
+ `lexicon: {language, data}` from `serializeLexicon`. The build and browser use
2806
+ the same functions. Findings retain input offsets; an encoding refusal returns
2807
+ no partial encoded output. Decoding preserves the plain skeleton of an unknown
2808
+ unmarked word and reports it.
2809
+
2810
+ | Export | Signature | Returns | Use it when |
2811
+ | -------------------- | ------------------------------------------------------------ | -------------------------- | ---------------------------------------------------------------------- |
2812
+ | `encodeWithLanguage` | `encodeWithLanguage(encoding, text, {font, verse, lexicon})` | `{text, pieces, findings}` | writing authored native entries or complete parts before encoding rows |
2813
+ | `decodeWithLanguage` | `decodeWithLanguage(encoding, text, {font, lexicon})` | `{text, findings}` | reading native exceptions and recovering unmarked lexical skeletons |
2814
+
2815
+ ### `engine.encodingNotes`
2816
+
2817
+ Node-side document resolution reads encoding records from the content index
2818
+ and glyph codes from the selected font's `fonts.json`. It opens no font encoding
2819
+ files and enforces no encoding-file release floor. Diagnostics retain the
2820
+ note or font manifest's own source position.
2821
+
2822
+ | Export | Signature | Returns | Use it when |
2823
+ | ---------------------- | ---------------------------------------------- | ------------------------------------ | ------------------------------------------------------------- |
2824
+ | `encodingAddress` | `encodingAddress(value, contentPackage)` | string or undefined | resolving a document Address with default type doc |
2825
+ | `encodingData` | `encodingData(data)` | plain executable data | separating ordinary document metadata |
2826
+ | `fontCodes` | `fontCodes(fonts, font)` | code-to-Unicode object | reading only the selected font's published glyph codes |
2827
+ | `encodingsFromRecords` | `encodingsFromRecords(records, config, fonts)` | `{encodings, definitions, findings}` | compiling addressed documents and preparing serialized inputs |
2828
+
2829
+ ### `engine.encodingLexicon`
2830
+
2831
+ Node-side note projection and validation. `serializeLexicon` emits only word,
2832
+ affix, grammar and name-register data read by conversion; the result contains
2833
+ no Maps or callbacks.
2834
+
2835
+ | Export | Signature | Returns | Use it when |
2836
+ | ----------------------- | ----------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------ |
2837
+ | `lexiconsFromRecords` | `lexiconsFromRecords(records, config)` | Map | projecting language words, including nearest complete inherited definitions |
2838
+ | `languageRecordsByName` | `languageRecordsByName(records, config)` | Map | finding each configured language note |
2839
+ | `languageNoteFindings` | `languageNoteFindings(config, records)` | findings | locating configured language notes or Roman inventories that are missing |
2840
+ | `serializeLexicon` | `serializeLexicon(lexicon)` | plain JSON object | supplying converter input to a browser or another process |
2841
+ | `encodingFor` | `encodingFor(context, {enc, font, lang})` | selected conversion or undefined | choosing an addressed document, its face and optional language lexicon |
2842
+ | `fontProblem` | `fontProblem(encoding, font)` | string or undefined | explaining an unsupported face choice |
2843
+ | `lexiconEntries` | `lexiconEntries(context, {lang, enc, font, derived})` | entries or problem | listing authored or derived forms with own, parts or row spellings |
2844
+ | `checkLexiconNatives` | `checkLexiconNatives(note, context)` | findings | validating encoding, face, duplicate slots and visible native code points |
2845
+ | `checkEncodingLexicon` | `checkEncodingLexicon(note, context)` | findings | checking every authoritative word and name against its language's encoding documents |
2846
+ | `NATIVE_COLUMNS` | constant | column names | reading the natives SQL projection, including its enc column |
2847
+ | `nativeRows` | `nativeRows(records)` | rows | projecting authored native entries in source order |
2848
+
2849
+ ## `./convert`
2850
+
2851
+ ### Browser conversion
2852
+
2853
+ `@heroiclands/package-build/convert` exports:
2854
+
2855
+ | Export | Signature | Returns |
2856
+ | -------------- | ---------------------------------------- | ---------------------------------------------------------- |
2857
+ | `encode` | `encode(data, language, text, options?)` | Converted text, findings and source pieces for a language. |
2858
+ | `decode` | `decode(data, language, text, options?)` | Roman text and findings. |
2859
+ | `validateData` | `validateData(data)` | The validated format-2 data. |
2860
+
2861
+ Its module graph uses no Node built-in and can be bundled for
2862
+ a browser without shims. Load the module's [format-2 data file](reference/format-details.md#converter-data-format)
2863
+ once and treat it as read-only.
2864
+
2865
+ ```js
2866
+ import { encode, decode, validateData } from "@heroiclands/package-build/convert";
2867
+ const data = validateData(await (await fetch("data/converter.json")).json());
2868
+ const native = encode(data, "aelw", "say", { enc: "aelwsemaronenc" });
2869
+ const roman = decode(data, "aelw", native.text, { enc: "aelwsemaronenc" });
2870
+ const inferred = encode(data, null, "say", { enc: "aelwsemaronenc" });
2871
+ ```
2872
+
2873
+ Both functions take `(data, language, text, options?)` and return
2874
+ `{text, findings}`; encoding with a language also returns source-positioned
2875
+ `pieces`. A language key selects its encoding, face and lexical inputs.
2876
+ `options.enc` and `options.font` override the encoding and face; `options.verse`
2877
+ asks the encoder to interpret verse structure. With `language: null`, `enc` is
2878
+ required and only the encoding's rows apply.
2879
+
2880
+ Language conversion uses the same pure `encodeWithLanguage` and
2881
+ `decodeWithLanguage` functions as the build. Recorded native writings and
2882
+ parts precede encoding rows, and decoding can recover an unmarked word from
2883
+ the lexicon. An unknown word gives a positioned finding. A bare refusal
2884
+ returns empty text and an error finding. Findings carry `severity`, `message`,
2885
+ `offset` (UTF-16), and one-based Unicode-code-point `line` and `column`.
2886
+ Invalid data, unknown languages and missing encoding selections throw an
2887
+ error before conversion. `validateData(data)` returns the validated data or
2888
+ throws; it opens no files.
2889
+
2890
+ The Node-side `engine/converter-data.mjs` module prepares these inputs from an
2891
+ indexed package:
2892
+
2893
+ | Export | Signature | Returns |
2894
+ | -------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------- |
2895
+ | `buildConverterData` | `buildConverterData(config, records)` | Format-1 converter data from the build context. |
2896
+ | `writeConverterData` | `writeConverterData({config, packageBuild, records?})` | `{path, data}` when the optional output is configured; otherwise `undefined`. |
2897
+
2898
+ The content run scanner reports diagnostic columns in Unicode code points.
2899
+ Its run offsets, run columns and SQL `runs.column` values retain UTF-16 source
2900
+ units. Encoding-file diagnostics retain their own file and position.
2901
+
2902
+ An omitted or null language is inferred from the selected encoding document,
2903
+ including its lexical native overrides and decoding recovery. Short document
2904
+ references resolve in `data.contentPackage`; foreign encodings require their
2905
+ full canonical Address. Use the pure engine's `encodeText` and `decodeText`
2906
+ when conversion must ignore vocabulary, as the CLI commands do.