@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.
- package/CHANGELOG.md +36 -0
- package/README.md +1 -0
- package/bin/content-commands.mjs +30 -96
- package/bin/encoding-commands.mjs +146 -0
- package/bin/package-build.mjs +35 -24
- package/config.mjs +25 -0
- package/convert.mjs +182 -0
- package/docs/api.md +154 -11
- package/docs/authoring/links-and-markup.md +260 -473
- package/docs/commands.md +118 -119
- package/docs/configuration.md +31 -13
- package/docs/content-format.md +11 -4
- package/docs/diagnostics.md +43 -16
- package/docs/encoding-core.md +135 -0
- package/docs/guides/book.md +2 -2
- package/docs/guides/module-project.md +23 -1
- package/docs/guides/site.md +2 -2
- package/docs/guides/writing-in-invented-scripts.md +453 -569
- package/docs/reference/format-details.md +290 -380
- package/docs/reference/note-types.md +80 -62
- package/docs/source-preview.md +8 -0
- package/e2e.mjs +1 -1
- package/engine/base-compiler.mjs +27 -9
- package/engine/content-charset.mjs +21 -1
- package/engine/content-format.yaml +7 -2
- package/engine/content-index.mjs +6 -2
- package/engine/content-links.mjs +1 -0
- package/engine/content-scripts.mjs +658 -423
- package/engine/content-scroll.mjs +4 -4
- package/engine/content-spans.mjs +40 -9
- package/engine/content-verse.mjs +1 -1
- package/engine/converter-data.mjs +70 -0
- package/engine/dictionary-rows.mjs +2 -34
- package/engine/encoding-integrity.mjs +94 -0
- package/engine/encoding-language.mjs +506 -0
- package/engine/encoding-lexicon.mjs +699 -0
- package/engine/encoding-notes.mjs +186 -0
- package/engine/encoding-standard.mjs +42 -0
- package/engine/encoding-template.mjs +313 -0
- package/engine/encoding-terms.mjs +141 -0
- package/engine/encoding.mjs +946 -0
- package/engine/extension-attributes.mjs +2 -0
- package/engine/helpers.mjs +1 -1
- package/engine/index.mjs +10 -0
- package/engine/language-data.mjs +4 -1
- package/engine/language-terms.mjs +23 -12
- package/engine/{logographic.mjs → language-writings.mjs} +29 -38
- package/engine/markup-validation.mjs +1 -1
- package/engine/note-vocabulary.mjs +4 -158
- package/engine/pdf-render.mjs +31 -3
- package/engine/roman-characters.mjs +232 -0
- package/engine/roman-run.mjs +149 -0
- package/engine/roman-verse.mjs +95 -0
- package/engine/run-rows.mjs +29 -31
- package/engine/run-words.mjs +2 -2
- package/engine/script-emphasis-terms.mjs +3 -0
- package/engine/script-emphasis.mjs +3 -10
- package/engine/script-font-delivery.mjs +9 -12
- package/engine/script-fonts.mjs +2 -1
- package/engine/site-build.mjs +14 -5
- package/engine/site-preview.mjs +12 -2
- package/engine/sql-tables.mjs +36 -31
- package/engine/verse-metre.mjs +1 -1
- package/engine/word-forms.mjs +1 -1
- package/engine/word-inflections.mjs +48 -0
- package/engine/word-rows.mjs +1 -1
- package/engine/word-terms.mjs +16 -17
- package/package.json +11 -5
- package/types/config.d.mts +5 -0
- package/types/convert.d.mts +44 -0
- package/types/e2e.d.mts +1 -1
- package/types/engine/base-compiler.d.mts +4 -2
- package/types/engine/content-charset.d.mts +8 -0
- package/types/engine/content-index.d.mts +4 -1
- package/types/engine/content-scripts.d.mts +108 -42
- package/types/engine/converter-data.d.mts +11 -0
- package/types/engine/dictionary-rows.d.mts +1 -25
- package/types/engine/encoding-integrity.d.mts +14 -0
- package/types/engine/encoding-language.d.mts +36 -0
- package/types/engine/encoding-lexicon.d.mts +259 -0
- package/types/engine/encoding-notes.d.mts +39 -0
- package/types/engine/encoding-standard.d.mts +11 -0
- package/types/engine/encoding-template.d.mts +43 -0
- package/types/engine/encoding-terms.d.mts +105 -0
- package/types/engine/encoding.d.mts +155 -0
- package/types/engine/helpers.d.mts +1 -1
- package/types/engine/index.d.mts +5 -0
- package/types/engine/language-terms.d.mts +1 -1
- package/types/engine/{logographic.d.mts → language-writings.d.mts} +6 -6
- package/types/engine/markup-validation.d.mts +1 -1
- package/types/engine/pdf-render.d.mts +1 -1
- package/types/engine/roman-characters.d.mts +45 -0
- package/types/engine/roman-run.d.mts +101 -0
- package/types/engine/roman-verse.d.mts +23 -0
- package/types/engine/run-rows.d.mts +13 -20
- package/types/engine/script-emphasis-terms.d.mts +2 -0
- package/types/engine/script-emphasis.d.mts +1 -1
- package/types/engine/script-font-delivery.d.mts +2 -3
- package/types/engine/script-fonts.d.mts +5 -0
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/word-inflections.d.mts +25 -0
- package/engine/map-lexicon.mjs +0 -1034
- package/engine/text-maps.mjs +0 -654
- package/engine/written-in.mjs +0 -204
- package/types/engine/map-lexicon.d.mts +0 -330
- package/types/engine/text-maps.d.mts +0 -189
- package/types/engine/written-in.d.mts +0 -76
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* **Delivering the installed faces to each target.** A consumer that installs
|
|
16
16
|
* `@heroiclands/fonts` and sets text in one of its faces, with `font=` or by a
|
|
17
|
-
*
|
|
17
|
+
* encoding document's `data.font`, gets the fonts on the site, in the Foundry
|
|
18
18
|
* module and in its books; without both, nothing ships and nothing links to it.
|
|
19
19
|
*
|
|
20
20
|
* - **Site and Foundry** receive `fonts.css`, listing only the WOFF2 sources,
|
|
@@ -32,7 +32,6 @@ import YAML from "yaml";
|
|
|
32
32
|
import { baseStyleLayer } from "./base-styles.mjs";
|
|
33
33
|
import { scanRuns } from "./content-scripts.mjs";
|
|
34
34
|
import { FONTS_MANIFEST, resolveScriptFonts } from "./script-fonts.mjs";
|
|
35
|
-
import { LANGUAGE_SUBTYPE } from "./word-terms.mjs";
|
|
36
35
|
|
|
37
36
|
/**
|
|
38
37
|
* Where the files land in the staged Foundry module, relative to its root.
|
|
@@ -72,14 +71,14 @@ const usedFaces = new WeakMap();
|
|
|
72
71
|
const FRONTMATTER = /^\uFEFF?---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
73
72
|
|
|
74
73
|
/**
|
|
75
|
-
* The face
|
|
74
|
+
* The face an encoding document names in `data.font`, where it is installed.
|
|
76
75
|
*
|
|
77
76
|
* @param {string} source - The note's file.
|
|
78
77
|
* @param {import("./script-fonts.mjs").ScriptFonts} installed - The fonts package.
|
|
79
78
|
* @param {import("./content-session.mjs").ContentNoteSource} [parsed] - Already parsed authored note.
|
|
80
79
|
* @returns {string|undefined} The face's slug.
|
|
81
80
|
*/
|
|
82
|
-
function
|
|
81
|
+
function encodingNoteFace(source, installed, parsed) {
|
|
83
82
|
const yaml = FRONTMATTER.exec(source)?.[1];
|
|
84
83
|
if (yaml === undefined) return undefined;
|
|
85
84
|
let fm;
|
|
@@ -88,18 +87,16 @@ function writtenInFace(source, installed, parsed) {
|
|
|
88
87
|
} catch {
|
|
89
88
|
return undefined;
|
|
90
89
|
}
|
|
91
|
-
if (fm?.type !== "
|
|
92
|
-
|
|
93
|
-
const font = fm.data?.writtenIn?.font;
|
|
90
|
+
if (fm?.type !== "doc" || !fm.data?.mappings || !fm.data?.rules) return undefined;
|
|
91
|
+
const font = fm.data?.font;
|
|
94
92
|
return typeof font === "string" && installed.faces.has(font) ? font : undefined;
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
/**
|
|
98
96
|
* The faces the package's notes set text in, read from the notes
|
|
99
97
|
* themselves: every Markdown file under the content tree that writes `font=`
|
|
100
|
-
* is scanned for its runs, and
|
|
101
|
-
*
|
|
102
|
-
* in it.
|
|
98
|
+
* is scanned for its runs, and encoding documents supply the fonts their
|
|
99
|
+
* encoded runs infer.
|
|
103
100
|
*
|
|
104
101
|
* @param {DeliveryConfig} config - The resolved configuration.
|
|
105
102
|
* @param {import("./script-fonts.mjs").ScriptFonts} installed - The fonts package.
|
|
@@ -123,8 +120,8 @@ export function facesUsed(config, installed, session) {
|
|
|
123
120
|
if (entry.isDirectory()) walk(full);
|
|
124
121
|
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
125
122
|
const source = session?.sources.get(full)?.raw ?? fs.readFileSync(full, "utf8");
|
|
126
|
-
if (/^\s*
|
|
127
|
-
const face =
|
|
123
|
+
if (/^\s*font:/m.test(source)) {
|
|
124
|
+
const face = encodingNoteFace(source, installed, session?.parsed.get(full));
|
|
128
125
|
if (face) used.add(face);
|
|
129
126
|
}
|
|
130
127
|
if (!/\bfont=/.test(source)) continue;
|
package/engine/script-fonts.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* each one draws — in its `fonts.json`, and this module reads them from the
|
|
18
18
|
* **consuming package's** install.
|
|
19
19
|
*
|
|
20
|
-
* package-build declares no dependency on `@heroiclands/fonts
|
|
20
|
+
* package-build declares no runtime dependency on `@heroiclands/fonts`:
|
|
21
21
|
* the fonts package depends on package-build to build, so the consumer adds it
|
|
22
22
|
* as a devDependency of its own and package-build resolves it from the
|
|
23
23
|
* consumer's root. Resolving from this module's own location would find
|
|
@@ -42,6 +42,7 @@ export const FONTS_MANIFEST = "fonts.json";
|
|
|
42
42
|
* @typedef {object} FaceCodepoint
|
|
43
43
|
* @property {string} codepoint - `U+` and the hexadecimal code point.
|
|
44
44
|
* @property {string} glyph - The glyph's name in the face.
|
|
45
|
+
* @property {string} [code] - The font's case-sensitive glyph code.
|
|
45
46
|
* @property {string} kind - What it is: `letter`, `numeral`, `mark` and so on.
|
|
46
47
|
*/
|
|
47
48
|
|
package/engine/site-build.mjs
CHANGED
|
@@ -807,8 +807,8 @@ export function renderSitePage(
|
|
|
807
807
|
...markupAnchorFindings(page.body),
|
|
808
808
|
])
|
|
809
809
|
secretErrors.push({
|
|
810
|
-
file: page.file,
|
|
811
|
-
line: (page.bodyLine ?? 1) + error.line - 1,
|
|
810
|
+
file: error.file ?? page.file,
|
|
811
|
+
line: error.file ? error.line : (page.bodyLine ?? 1) + error.line - 1,
|
|
812
812
|
column: error.column,
|
|
813
813
|
message: error.message,
|
|
814
814
|
});
|
|
@@ -818,8 +818,8 @@ export function renderSitePage(
|
|
|
818
818
|
headingErrors.push({ file: page.file, ...error });
|
|
819
819
|
for (const error of footnoteFindings(page.body))
|
|
820
820
|
footnoteErrors.push({
|
|
821
|
-
file: page.file,
|
|
822
|
-
line: (page.bodyLine ?? 1) + error.line - 1,
|
|
821
|
+
file: error.file ?? page.file,
|
|
822
|
+
line: error.file ? error.line : (page.bodyLine ?? 1) + error.line - 1,
|
|
823
823
|
column: error.column,
|
|
824
824
|
message: error.message,
|
|
825
825
|
});
|
|
@@ -1008,11 +1008,20 @@ export function renderPages(pages, options) {
|
|
|
1008
1008
|
drawings: maps,
|
|
1009
1009
|
runs,
|
|
1010
1010
|
});
|
|
1011
|
+
const externalRunFindings = new Set();
|
|
1011
1012
|
for (const page of pages) {
|
|
1012
1013
|
const result = renderOne(page);
|
|
1013
1014
|
tableErrors.push(...result.tableErrors);
|
|
1014
1015
|
expressionErrors.push(...result.expressionErrors);
|
|
1015
|
-
secretErrors.push(
|
|
1016
|
+
secretErrors.push(
|
|
1017
|
+
...result.secretErrors.filter((error) => {
|
|
1018
|
+
if (!error.file || error.file === page.file) return true;
|
|
1019
|
+
const key = JSON.stringify([error.file, error.line, error.column, error.message]);
|
|
1020
|
+
if (externalRunFindings.has(key)) return false;
|
|
1021
|
+
externalRunFindings.add(key);
|
|
1022
|
+
return true;
|
|
1023
|
+
}),
|
|
1024
|
+
);
|
|
1016
1025
|
captionErrors.push(...result.captionErrors);
|
|
1017
1026
|
headingErrors.push(...result.headingErrors);
|
|
1018
1027
|
footnoteErrors.push(...result.footnoteErrors);
|
package/engine/site-preview.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { PREVIEW_HELPERS, colored, wikiSource } from "./preview-source.mjs";
|
|
|
21
21
|
import { dataFields } from "./note-vocabulary.mjs";
|
|
22
22
|
import { applyComputedBeingAge } from "./being-age.mjs";
|
|
23
23
|
import { DEFAULT_CURRENT_DATE, normalizeCurrentDate } from "./current-date.mjs";
|
|
24
|
+
import { scanRuns } from "./content-scripts.mjs";
|
|
24
25
|
|
|
25
26
|
export { PREVIEW_HELPERS };
|
|
26
27
|
|
|
@@ -76,7 +77,7 @@ function localInfobox(fm, md) {
|
|
|
76
77
|
* @param {string} text - Markdown, optionally beginning with YAML frontmatter.
|
|
77
78
|
* @param {{dark?: boolean, infobox?: boolean, file?: string, currentDate?: string,
|
|
78
79
|
* index?: import("./preview-index.mjs").PreviewIndex}} [options] - Explicit palette, profile visibility, diagnostic filename, neutral current date (default "720.1") and the content index, from {@link module:engine/preview-index.previewIndex}.
|
|
79
|
-
* @returns {{ok: boolean, html: string, frontmatter: object, findings: object[]}} HTML and metadata with locally calculable ages; unavailable ages remain absent or null.
|
|
80
|
+
* @returns {{ok: boolean, html: string, frontmatter: object, findings: object[]}} HTML and metadata with locally calculable ages; unavailable ages remain absent or null. Error findings set `ok` to false while retaining the HTML.
|
|
80
81
|
*/
|
|
81
82
|
export function renderPreview(
|
|
82
83
|
text,
|
|
@@ -84,6 +85,7 @@ export function renderPreview(
|
|
|
84
85
|
) {
|
|
85
86
|
const date = normalizeCurrentDate(currentDate);
|
|
86
87
|
const findings = [];
|
|
88
|
+
let bodyLine = 1;
|
|
87
89
|
let frontmatter = {},
|
|
88
90
|
body = String(text ?? "");
|
|
89
91
|
try {
|
|
@@ -92,6 +94,7 @@ export function renderPreview(
|
|
|
92
94
|
parsed.data && typeof parsed.data === "object" && !Array.isArray(parsed.data) ?
|
|
93
95
|
structuredClone(parsed.data)
|
|
94
96
|
: {};
|
|
97
|
+
bodyLine = body.slice(0, body.length - parsed.content.length).split("\n").length;
|
|
95
98
|
body = parsed.content;
|
|
96
99
|
} catch (error) {
|
|
97
100
|
findings.push({ file, severity: "warning", message: String(error.message) });
|
|
@@ -123,6 +126,13 @@ export function renderPreview(
|
|
|
123
126
|
let markdown = authored;
|
|
124
127
|
let figures = scan.figures;
|
|
125
128
|
if (indexed) {
|
|
129
|
+
for (const error of scanRuns(body, index.runs).errors)
|
|
130
|
+
findings.push({
|
|
131
|
+
file,
|
|
132
|
+
severity: "error",
|
|
133
|
+
...error,
|
|
134
|
+
line: error.file && error.file !== file ? error.line : bodyLine + error.line - 1,
|
|
135
|
+
});
|
|
126
136
|
const expanded = index.expand(authored, { fm: frontmatter, file });
|
|
127
137
|
markdown = expanded.markdown;
|
|
128
138
|
figures = expanded.figures;
|
|
@@ -134,7 +144,7 @@ export function renderPreview(
|
|
|
134
144
|
});
|
|
135
145
|
const profile = infobox ? localInfobox(frontmatter, renderer.md) : "";
|
|
136
146
|
return {
|
|
137
|
-
ok:
|
|
147
|
+
ok: !findings.some((finding) => finding.severity === "error"),
|
|
138
148
|
html: `<div class="hl-preview hl-preview-${dark ? "dark" : "light"}">${profile}<main class="hl-preview-main">${content}</main></div>`,
|
|
139
149
|
frontmatter,
|
|
140
150
|
findings,
|
package/engine/sql-tables.mjs
CHANGED
|
@@ -36,8 +36,7 @@ import { EVENT_COLUMNS, EVENT_SORT_COLUMNS, eventCalendars, eventRows } from "./
|
|
|
36
36
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
37
37
|
import { NAME_COLUMNS, namesRows } from "./name-rows.mjs";
|
|
38
38
|
import { RUN_COLUMNS, ownerOf, runsRows } from "./run-rows.mjs";
|
|
39
|
-
import {
|
|
40
|
-
import { NATIVE_COLUMNS, nativeRows } from "./map-lexicon.mjs";
|
|
39
|
+
import { NATIVE_COLUMNS, nativeRows } from "./encoding-lexicon.mjs";
|
|
41
40
|
import {
|
|
42
41
|
GLOSSARY_COLUMNS,
|
|
43
42
|
AFFIX_COLUMNS,
|
|
@@ -217,6 +216,31 @@ export function findSqlBlocks(markdown) {
|
|
|
217
216
|
return blocks;
|
|
218
217
|
}
|
|
219
218
|
|
|
219
|
+
/** Keep encoding source keys as case-sensitive values in the SQL projection.
|
|
220
|
+
* The canonical index retains its authored mapping object; DuckDB structs
|
|
221
|
+
* cannot distinguish a and A as field names.
|
|
222
|
+
*/
|
|
223
|
+
function sqlRecord(record) {
|
|
224
|
+
const mappings = record?.data?.mappings;
|
|
225
|
+
if (
|
|
226
|
+
record?.type !== "doc" ||
|
|
227
|
+
!mappings ||
|
|
228
|
+
typeof mappings !== "object" ||
|
|
229
|
+
Array.isArray(mappings)
|
|
230
|
+
)
|
|
231
|
+
return record;
|
|
232
|
+
return {
|
|
233
|
+
...record,
|
|
234
|
+
data: {
|
|
235
|
+
...record.data,
|
|
236
|
+
mappings: Object.entries(mappings).map(([from, value]) => ({
|
|
237
|
+
from,
|
|
238
|
+
codes: Array.isArray(value) ? value : [value],
|
|
239
|
+
})),
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
220
244
|
/**
|
|
221
245
|
* Open a DuckDB view over the content index.
|
|
222
246
|
*
|
|
@@ -274,7 +298,7 @@ export async function openNotesDatabase(
|
|
|
274
298
|
const base = dir ?? fs.mkdtempSync(path.join(os.tmpdir(), "content-sql-"));
|
|
275
299
|
fs.mkdirSync(base, { recursive: true });
|
|
276
300
|
const jsonl = path.join(base, "notes.jsonl");
|
|
277
|
-
const encoded = records.map((record) => encodeAddresses(record));
|
|
301
|
+
const encoded = records.map((record) => sqlRecord(encodeAddresses(record)));
|
|
278
302
|
|
|
279
303
|
const instance = await DuckDBInstance.create(":memory:");
|
|
280
304
|
const connection = await instance.connect();
|
|
@@ -284,7 +308,7 @@ export async function openNotesDatabase(
|
|
|
284
308
|
} catch (err) {
|
|
285
309
|
// DuckDB names the clashing key but not the notes that spell it.
|
|
286
310
|
if (/Duplicate name ".*" in struct/.test(String(err?.message))) {
|
|
287
|
-
const lines = keyCaseCollisionDiagnostics(
|
|
311
|
+
const lines = keyCaseCollisionDiagnostics(encoded, { contentBase });
|
|
288
312
|
if (lines.length) {
|
|
289
313
|
const located = new Error(lines.join("\n"));
|
|
290
314
|
/** @type {any} */ (located).located = true;
|
|
@@ -296,7 +320,7 @@ export async function openNotesDatabase(
|
|
|
296
320
|
await createEventRelation(connection, path.join(base, "events.jsonl"), encoded, "", audience);
|
|
297
321
|
await createNameRelation(connection, path.join(base, "names.jsonl"), encoded, "", audience);
|
|
298
322
|
await createRunRelation(connection, path.join(base, "runs.jsonl"), encoded, "", audience);
|
|
299
|
-
await
|
|
323
|
+
await createNativeRelation(connection, path.join(base, "natives.jsonl"), encoded, "", audience);
|
|
300
324
|
await createWordRelations(connection, path.join(base, "notes"), encoded, "", audience);
|
|
301
325
|
|
|
302
326
|
// One schema per declared dependency, so `FROM sohl.notes` reads the notes
|
|
@@ -317,7 +341,7 @@ export async function openNotesDatabase(
|
|
|
317
341
|
.readFileSync(dep.file, "utf8")
|
|
318
342
|
.split("\n")
|
|
319
343
|
.filter((line) => line.trim())
|
|
320
|
-
.map((line) => flattenPublishedAddresses(JSON.parse(line)));
|
|
344
|
+
.map((line) => sqlRecord(flattenPublishedAddresses(JSON.parse(line))));
|
|
321
345
|
await createRelations(connection, flat, depRecords, `${schema}.`, audience);
|
|
322
346
|
await createEventRelation(
|
|
323
347
|
connection,
|
|
@@ -340,9 +364,9 @@ export async function openNotesDatabase(
|
|
|
340
364
|
`${schema}.`,
|
|
341
365
|
audience,
|
|
342
366
|
);
|
|
343
|
-
await
|
|
367
|
+
await createNativeRelation(
|
|
344
368
|
connection,
|
|
345
|
-
flat.replace(/\.jsonl$/, "-
|
|
369
|
+
flat.replace(/\.jsonl$/, "-natives.jsonl"),
|
|
346
370
|
depRecords,
|
|
347
371
|
`${schema}.`,
|
|
348
372
|
audience,
|
|
@@ -382,7 +406,6 @@ export async function openNotesDatabase(
|
|
|
382
406
|
"events",
|
|
383
407
|
"names",
|
|
384
408
|
"runs",
|
|
385
|
-
"maps",
|
|
386
409
|
"words",
|
|
387
410
|
"affixes",
|
|
388
411
|
"fields",
|
|
@@ -643,7 +666,7 @@ async function createNameRelation(connection, file, records, prefix, audience) {
|
|
|
643
666
|
|
|
644
667
|
/**
|
|
645
668
|
* Create the `runs` relation: one row per span, div and verse fence an own
|
|
646
|
-
* note marks with `lang=`, `font=` or `
|
|
669
|
+
* note marks with `lang=`, `font=` or `enc=`, with the columns
|
|
647
670
|
* {@link module:engine/run-rows.RUN_COLUMNS} names. The columns are declared
|
|
648
671
|
* rather than inferred, so every value stays a string and none becomes a
|
|
649
672
|
* struct key whose case DuckDB folds.
|
|
@@ -665,30 +688,12 @@ async function createRunRelation(connection, file, records, prefix, audience) {
|
|
|
665
688
|
]);
|
|
666
689
|
}
|
|
667
690
|
|
|
668
|
-
/**
|
|
669
|
-
|
|
670
|
-
* `data.map`, with the columns {@link module:engine/text-maps.MAP_COLUMNS}
|
|
671
|
-
* names; and the `natives` relation, one row per native form of a language's
|
|
672
|
-
* word, with the columns {@link module:engine/map-lexicon.NATIVE_COLUMNS}
|
|
673
|
-
* names. A row is a row here, never a struct key, so `A` and `a` are two rows.
|
|
674
|
-
*
|
|
675
|
-
* @param {object} connection - An open DuckDB connection.
|
|
676
|
-
* @param {string} file - Where to write the rows.
|
|
677
|
-
* @param {object[]} records - Index records, every Address a string.
|
|
678
|
-
* @param {string} prefix - A schema to qualify the relation name with.
|
|
679
|
-
* @param {"all"|"public"} audience - Whether to leave out GM notes.
|
|
680
|
-
* @returns {Promise<void>}
|
|
681
|
-
*/
|
|
682
|
-
async function createMapRelation(connection, file, records, prefix, audience) {
|
|
683
|
-
const rows = mapRows(
|
|
684
|
-
audience === "public" ? records.filter((record) => !isGmNote(record)) : records,
|
|
685
|
-
ownerOf,
|
|
686
|
-
);
|
|
687
|
-
await createTypedRelation(connection, file, `${prefix}maps`, rows, MAP_COLUMNS, ["row"]);
|
|
691
|
+
/** Project authored native entries into the natives SQL relation. */
|
|
692
|
+
async function createNativeRelation(connection, file, records, prefix, audience) {
|
|
688
693
|
const visible = audience === "public" ? records.filter((record) => !isGmNote(record)) : records;
|
|
689
694
|
await createTypedRelation(
|
|
690
695
|
connection,
|
|
691
|
-
file
|
|
696
|
+
file,
|
|
692
697
|
`${prefix}natives`,
|
|
693
698
|
nativeRows(visible),
|
|
694
699
|
NATIVE_COLUMNS,
|
package/engine/verse-metre.mjs
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
import { nearest } from "./near-miss.mjs";
|
|
53
53
|
import { isNoteRecord } from "./index-records.mjs";
|
|
54
54
|
import { METRE_KINDS, METRE_NAME } from "./metre-declaration.mjs";
|
|
55
|
-
import { languageRecordsByName } from "./
|
|
55
|
+
import { languageRecordsByName } from "./encoding-lexicon.mjs";
|
|
56
56
|
import { STRESS_LONGER, STRESS_MARKS, STRESS_TWO } from "./language-terms.mjs";
|
|
57
57
|
import { parseWordClass } from "./word-classes.mjs";
|
|
58
58
|
import { derivedForms, foldWord } from "./word-forms.mjs";
|
package/engine/word-forms.mjs
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
* @module
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
|
-
import { pluralByRule, pluralOf } from "./
|
|
53
|
+
import { pluralByRule, pluralOf } from "./word-inflections.mjs";
|
|
54
54
|
import { replaceClosing, verbForm, verbPattern } from "./verb-pattern.mjs";
|
|
55
55
|
import { parseWordClass } from "./word-classes.mjs";
|
|
56
56
|
import { WORD } from "./word-pattern.mjs";
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
import { parseWordClass } from "./word-classes.mjs";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A noun's stated plural, or the first literal ending replacement that fits.
|
|
18
|
+
*
|
|
19
|
+
* @param {{form: string, class?: unknown, plural?: unknown}} word - A word of
|
|
20
|
+
* `data.words`.
|
|
21
|
+
* @param {Record<string, any>|undefined} grammar - Its language's
|
|
22
|
+
* `data.grammar`.
|
|
23
|
+
* @returns {string|null} The plural, or `null` for a word that is no noun or
|
|
24
|
+
* that no rule fits.
|
|
25
|
+
*/
|
|
26
|
+
export function pluralOf(word, grammar) {
|
|
27
|
+
if (parseWordClass(word.class).base !== "n") return null;
|
|
28
|
+
if (typeof word.plural === "string" && word.plural) return word.plural;
|
|
29
|
+
return pluralByRule(word.form, grammar);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The plural a language's `data.grammar.plural` rule gives a form: the first
|
|
34
|
+
* literal ending replacement that fits, whatever the form's class.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} form - The form.
|
|
37
|
+
* @param {Record<string, any>|undefined} grammar - Its language's
|
|
38
|
+
* `data.grammar`.
|
|
39
|
+
* @returns {string|null} The plural, or `null` where no rule fits.
|
|
40
|
+
*/
|
|
41
|
+
export function pluralByRule(form, grammar) {
|
|
42
|
+
for (const rule of Array.isArray(grammar?.plural) ? grammar.plural : []) {
|
|
43
|
+
if (typeof rule?.ending !== "string" || typeof rule.becomes !== "string") continue;
|
|
44
|
+
if (form.endsWith(rule.ending))
|
|
45
|
+
return (rule.ending ? form.slice(0, -rule.ending.length) : form) + rule.becomes;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
package/engine/word-rows.mjs
CHANGED
|
@@ -71,7 +71,7 @@ export const GLOSSARY_COLUMNS = Object.freeze(["note", "position", ...GLOSSARY_E
|
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* The affix's own keys an affix row carries: all but `native`, its writings
|
|
74
|
-
* under
|
|
74
|
+
* under an encoding, which `package-build lexicon` resolves.
|
|
75
75
|
*/
|
|
76
76
|
const AFFIX_ROW_KEYS = Object.freeze(AFFIX_KEYS.filter((key) => key !== "native"));
|
|
77
77
|
|
package/engine/word-terms.mjs
CHANGED
|
@@ -106,28 +106,28 @@ export const WORD_SOURCE_FIELDS = Object.freeze([
|
|
|
106
106
|
*/
|
|
107
107
|
export const WORD_NATIVE_FIELDS = Object.freeze([
|
|
108
108
|
Object.freeze({
|
|
109
|
-
name: "
|
|
109
|
+
name: "enc",
|
|
110
110
|
kind: "string",
|
|
111
111
|
required: true,
|
|
112
|
-
shape: "
|
|
113
|
-
describe: "The
|
|
112
|
+
shape: "an encoding document Address",
|
|
113
|
+
describe: "The encoding the native form is written for.",
|
|
114
114
|
}),
|
|
115
115
|
Object.freeze({
|
|
116
116
|
name: "font",
|
|
117
117
|
kind: "string",
|
|
118
|
-
shape: "a face the
|
|
118
|
+
shape: "a face the encoding lists",
|
|
119
119
|
describe:
|
|
120
|
-
"The one face the native form is for; unstated, it is for every face the
|
|
120
|
+
"The one face the native form is for; unstated, it is for every face the encoding lists, " +
|
|
121
121
|
"and a form stated for a face wins over it there.",
|
|
122
122
|
}),
|
|
123
123
|
Object.freeze({
|
|
124
124
|
name: "text",
|
|
125
125
|
kind: "string",
|
|
126
126
|
required: true,
|
|
127
|
-
shape: "the word in the script, as the
|
|
127
|
+
shape: "the word in the script, as the encoding's characters",
|
|
128
128
|
describe:
|
|
129
|
-
"The word as the script writes it, where the
|
|
130
|
-
"or
|
|
129
|
+
"The word as the script writes it, where the encoding's rows would write it otherwise, " +
|
|
130
|
+
"or by an explicit native spelling.",
|
|
131
131
|
}),
|
|
132
132
|
]);
|
|
133
133
|
|
|
@@ -253,17 +253,16 @@ export const WORD_FIELDS = Object.freeze([
|
|
|
253
253
|
Object.freeze({
|
|
254
254
|
name: "native",
|
|
255
255
|
kind: "list",
|
|
256
|
-
shape: "a list of `{
|
|
256
|
+
shape: "a list of `{ enc, font?, text }`",
|
|
257
257
|
entries: Object.freeze({
|
|
258
258
|
kind: "map",
|
|
259
|
-
shape: "a map — `{
|
|
259
|
+
shape: "a map — `{ enc, font?, text }`",
|
|
260
260
|
fields: WORD_NATIVE_FIELDS,
|
|
261
261
|
}),
|
|
262
262
|
describe:
|
|
263
|
-
"The word's
|
|
264
|
-
"
|
|
265
|
-
"forms
|
|
266
|
-
"which its derived forms and the words built on it are written from.",
|
|
263
|
+
"The word's own writing for an encoding and optional face. It precedes complete " +
|
|
264
|
+
"parts and the encoding's rows; an entry for a face precedes an unqualified entry. " +
|
|
265
|
+
"Derived forms can use the stem and explicitly written grammatical endings.",
|
|
267
266
|
}),
|
|
268
267
|
]);
|
|
269
268
|
|
|
@@ -426,14 +425,14 @@ export const AFFIX_FIELDS = Object.freeze([
|
|
|
426
425
|
Object.freeze({
|
|
427
426
|
name: "native",
|
|
428
427
|
kind: "list",
|
|
429
|
-
shape: "a list of `{
|
|
428
|
+
shape: "a list of `{ enc, font?, text }`",
|
|
430
429
|
entries: Object.freeze({
|
|
431
430
|
kind: "map",
|
|
432
|
-
shape: "a map — `{
|
|
431
|
+
shape: "a map — `{ enc, font?, text }`",
|
|
433
432
|
fields: WORD_NATIVE_FIELDS,
|
|
434
433
|
}),
|
|
435
434
|
describe:
|
|
436
|
-
"The affix's writing under
|
|
435
|
+
"The affix's writing under an encoding, which a word whose `built` names the " +
|
|
437
436
|
"affix is written with.",
|
|
438
437
|
}),
|
|
439
438
|
]);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package
|
|
3
|
+
"version": "25.1.0",
|
|
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",
|
|
7
7
|
"main": "./index.mjs",
|
|
@@ -97,7 +97,12 @@
|
|
|
97
97
|
"import": "./templates.mjs"
|
|
98
98
|
},
|
|
99
99
|
"./changelog": "./changelog.cjs",
|
|
100
|
-
"./package.json": "./package.json"
|
|
100
|
+
"./package.json": "./package.json",
|
|
101
|
+
"./convert": {
|
|
102
|
+
"types": "./types/convert.d.mts",
|
|
103
|
+
"import": "./convert.mjs",
|
|
104
|
+
"default": "./convert.mjs"
|
|
105
|
+
}
|
|
101
106
|
},
|
|
102
107
|
"bin": {
|
|
103
108
|
"package-build": "./bin/package-build.mjs",
|
|
@@ -134,7 +139,8 @@
|
|
|
134
139
|
"stage.mjs",
|
|
135
140
|
"templates.mjs",
|
|
136
141
|
"types",
|
|
137
|
-
"vendor"
|
|
142
|
+
"vendor",
|
|
143
|
+
"convert.mjs"
|
|
138
144
|
],
|
|
139
145
|
"dependencies": {
|
|
140
146
|
"@duckdb/node-api": "^1.5.5-r.4",
|
|
@@ -184,7 +190,7 @@
|
|
|
184
190
|
},
|
|
185
191
|
"devDependencies": {
|
|
186
192
|
"@changesets/cli": "^3.0.0",
|
|
187
|
-
"@heroiclands/fonts": "^0.
|
|
193
|
+
"@heroiclands/fonts": "^1.0.0",
|
|
188
194
|
"@types/node": "^26.2.0",
|
|
189
195
|
"esbuild": "^0.28.2",
|
|
190
196
|
"markdownlint": "^0.42.0",
|
package/types/config.d.mts
CHANGED
|
@@ -113,6 +113,7 @@ export const DERIVED_MANIFEST_KEYS: Readonly<Record<string, string>>;
|
|
|
113
113
|
* named first in the manifest's `styles`.
|
|
114
114
|
* @property {Readonly<Record<string, unknown>>} manifest The manifest
|
|
115
115
|
* specification, emitted as declared.
|
|
116
|
+
* @property {string|null} converterData Optional JSON path relative to the build stage.
|
|
116
117
|
* @property {string|null} manifestFlags Module to load a `flags` function
|
|
117
118
|
* from, for namespaced flags a repository has
|
|
118
119
|
* to compute. `null` when it declares none.
|
|
@@ -260,6 +261,10 @@ export type PackageBuildConfig = {
|
|
|
260
261
|
* specification, emitted as declared.
|
|
261
262
|
*/
|
|
262
263
|
manifest: Readonly<Record<string, unknown>>;
|
|
264
|
+
/**
|
|
265
|
+
* Optional JSON path relative to the build stage.
|
|
266
|
+
*/
|
|
267
|
+
converterData: string | null;
|
|
263
268
|
/**
|
|
264
269
|
* Module to load a `flags` function
|
|
265
270
|
* from, for namespaced flags a repository has
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Validate the format-2 converter data without opening files or using Node.
|
|
2
|
+
* @param {ConverterData} data Parsed JSON, treated as read-only.
|
|
3
|
+
* @returns {ConverterData} The validated data.
|
|
4
|
+
*/
|
|
5
|
+
export function validateData(data: ConverterData): ConverterData;
|
|
6
|
+
/** Encode with the selected encoding document's language, optionally stated explicitly.
|
|
7
|
+
* @param {ConverterData} data The module's parsed converter data.
|
|
8
|
+
* @param {string|null|undefined} language A language key, or null to infer it from the encoding.
|
|
9
|
+
* @param {string} text Roman input.
|
|
10
|
+
* @param {ConversionOptions} [options] Encoding, face or verse overrides.
|
|
11
|
+
*/
|
|
12
|
+
export function encode(data: ConverterData, language: string | null | undefined, text: string, options?: ConversionOptions): {
|
|
13
|
+
text: string;
|
|
14
|
+
findings: object[];
|
|
15
|
+
};
|
|
16
|
+
/** Decode with the selected encoding document's optional language lexicon.
|
|
17
|
+
* @param {ConverterData} data The module's parsed converter data.
|
|
18
|
+
* @param {string|null|undefined} language A language key, or null to infer it from the encoding.
|
|
19
|
+
* @param {string} text Encoded Unicode input.
|
|
20
|
+
* @param {ConversionOptions} [options] Encoding or face overrides.
|
|
21
|
+
*/
|
|
22
|
+
export function decode(data: ConverterData, language: string | null | undefined, text: string, options?: ConversionOptions): {
|
|
23
|
+
text: string;
|
|
24
|
+
findings: object[];
|
|
25
|
+
};
|
|
26
|
+
export type ConverterLexicon = {
|
|
27
|
+
language: string;
|
|
28
|
+
data: Record<string, any>;
|
|
29
|
+
};
|
|
30
|
+
export type ConverterLanguage = {
|
|
31
|
+
name: string;
|
|
32
|
+
lexicon?: ConverterLexicon;
|
|
33
|
+
};
|
|
34
|
+
export type ConverterData = {
|
|
35
|
+
format: 2;
|
|
36
|
+
contentPackage: string;
|
|
37
|
+
encodings: Record<string, any>;
|
|
38
|
+
languages: Record<string, ConverterLanguage>;
|
|
39
|
+
};
|
|
40
|
+
export type ConversionOptions = {
|
|
41
|
+
enc?: string;
|
|
42
|
+
font?: string;
|
|
43
|
+
verse?: boolean;
|
|
44
|
+
};
|
package/types/e2e.d.mts
CHANGED
|
@@ -494,7 +494,7 @@ export const E2E_SCENE_ID: "heroiclandsScene";
|
|
|
494
494
|
/** How the suite may be run. */
|
|
495
495
|
export const E2E_MODES: readonly string[];
|
|
496
496
|
/** The Chrome for Testing build shared by local and CI Cypress runs. */
|
|
497
|
-
export const CHROME_BUILD_ID: "158.0.
|
|
497
|
+
export const CHROME_BUILD_ID: "158.0.8122.0";
|
|
498
498
|
/**
|
|
499
499
|
* The disposable world's identity and credentials.
|
|
500
500
|
*/
|
|
@@ -476,10 +476,11 @@ export class BasePackCompiler {
|
|
|
476
476
|
* naming the file alone beats naming a line that is not the problem.
|
|
477
477
|
*
|
|
478
478
|
* @param {string} message - What is wrong, in one sentence.
|
|
479
|
-
* @param {{line?: number, column?: number}} [position] - Where, if known.
|
|
479
|
+
* @param {{file?: string, line?: number, column?: number}} [position] - Where, if known.
|
|
480
480
|
* @returns {void}
|
|
481
481
|
*/
|
|
482
482
|
noteWarn(message: string, position?: {
|
|
483
|
+
file?: string;
|
|
483
484
|
line?: number;
|
|
484
485
|
column?: number;
|
|
485
486
|
}): void;
|
|
@@ -487,10 +488,11 @@ export class BasePackCompiler {
|
|
|
487
488
|
* Reports an error about the note being compiled.
|
|
488
489
|
*
|
|
489
490
|
* @param {string} message - What is wrong, in one sentence.
|
|
490
|
-
* @param {{line?: number, column?: number}} [position] - Where, if known.
|
|
491
|
+
* @param {{file?: string, line?: number, column?: number}} [position] - Where, if known.
|
|
491
492
|
* @returns {void}
|
|
492
493
|
*/
|
|
493
494
|
noteError(message: string, position?: {
|
|
495
|
+
file?: string;
|
|
494
496
|
line?: number;
|
|
495
497
|
column?: number;
|
|
496
498
|
}): void;
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* @returns {boolean} Whether Tier 1 admits it.
|
|
10
10
|
*/
|
|
11
11
|
export function isLetterTier(cp: number): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a code point belongs to a declared setting-script encoding block.
|
|
14
|
+
* This identifies native input independently of a language's Roman inventory;
|
|
15
|
+
* a selected native face must still draw the individual character.
|
|
16
|
+
* @param {number} cp - A Unicode code point.
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
export function isSettingScriptCodePoint(cp: number): boolean;
|
|
12
20
|
/**
|
|
13
21
|
* Whether the charset admits a code point anywhere in a note.
|
|
14
22
|
*
|