@heroiclands/package-build 23.12.6 → 23.12.7
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 +12 -0
- package/assets/styles/content-base.css +25 -0
- package/bin/content-commands.mjs +264 -3
- package/bin/package-build.mjs +25 -0
- package/content-config.mjs +72 -0
- package/docs/api.md +10 -9
- package/docs/authoring/links-and-markup.md +439 -47
- package/docs/commands.md +127 -0
- package/docs/configuration.md +66 -3
- package/docs/content-format.md +3 -1
- package/docs/guides/book.md +11 -0
- package/docs/guides/module-project.md +16 -0
- package/docs/guides/site.md +19 -0
- package/docs/reference/format-details.md +372 -6
- package/docs/reference/note-types.md +36 -11
- package/engine/base-compiler.mjs +2 -0
- package/engine/bundles.mjs +7 -2
- package/engine/compile-corpus.mjs +3 -1
- package/engine/content-charset.mjs +21 -2
- package/engine/content-format.yaml +1 -0
- package/engine/content-index.mjs +12 -0
- package/engine/content-poetry.mjs +22 -4
- package/engine/content-scripts.mjs +1219 -0
- package/engine/content-session.mjs +2 -0
- package/engine/data-keys.mjs +5 -1
- package/engine/dictionary-layout.mjs +18 -12
- package/engine/dictionary-rows.mjs +11 -2
- package/engine/helpers.mjs +8 -2
- package/engine/index-records.mjs +1 -0
- package/engine/journals.mjs +14 -3
- package/engine/language-data.mjs +137 -1
- package/engine/language-tag.mjs +103 -0
- package/engine/language-terms.mjs +16 -4
- package/engine/map-lexicon.mjs +762 -0
- package/engine/markup-validation.mjs +17 -3
- package/engine/names.mjs +1 -1
- package/engine/note-vocabulary.mjs +113 -0
- package/engine/pdf-build.mjs +28 -10
- package/engine/pdf-render.mjs +116 -11
- package/engine/run-rows.mjs +119 -0
- package/engine/run-words.mjs +266 -0
- package/engine/scenes.mjs +1 -0
- package/engine/script-font-delivery.mjs +321 -0
- package/engine/script-fonts.mjs +172 -0
- package/engine/site-build.mjs +10 -3
- package/engine/site-config.mjs +5 -0
- package/engine/site-root.mjs +19 -5
- package/engine/sql-tables.mjs +111 -1
- package/engine/text-maps.mjs +607 -0
- package/engine/verb-pattern.mjs +99 -0
- package/engine/word-pattern.mjs +27 -0
- package/engine/word-rows.mjs +8 -2
- package/engine/word-terms.mjs +44 -0
- package/hugo-theme/layouts/_default/baseof.html +3 -0
- package/hugo-theme/static/css/style.css +10 -0
- package/manifest.mjs +9 -1
- package/package.json +1 -1
- package/types/content-config.d.mts +26 -0
- package/types/engine/compile-corpus.d.mts +2 -1
- package/types/engine/content-index.d.mts +11 -3
- package/types/engine/content-poetry.d.mts +7 -0
- package/types/engine/content-scripts.d.mts +226 -0
- package/types/engine/data-keys.d.mts +2 -2
- package/types/engine/dictionary-rows.d.mts +15 -0
- package/types/engine/helpers.d.mts +5 -1
- package/types/engine/journals.d.mts +7 -2
- package/types/engine/language-data.d.mts +2 -0
- package/types/engine/language-tag.d.mts +8 -0
- package/types/engine/language-terms.d.mts +2 -1
- package/types/engine/map-lexicon.d.mts +274 -0
- package/types/engine/markup-validation.d.mts +7 -2
- package/types/engine/pdf-build.d.mts +26 -3
- package/types/engine/pdf-render.d.mts +17 -2
- package/types/engine/run-rows.d.mts +89 -0
- package/types/engine/run-words.d.mts +86 -0
- package/types/engine/script-font-delivery.d.mts +134 -0
- package/types/engine/script-fonts.d.mts +168 -0
- package/types/engine/site-build.d.mts +2 -1
- package/types/engine/site-root.d.mts +11 -3
- package/types/engine/text-maps.d.mts +175 -0
- package/types/engine/verb-pattern.d.mts +65 -0
- package/types/engine/word-pattern.d.mts +13 -0
- package/types/engine/word-terms.d.mts +6 -0
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
* **The faces a consumer installed.** `@heroiclands/fonts` owns the facts
|
|
16
|
+
* about its faces — family, class, direction, features and the code points
|
|
17
|
+
* each one draws — in its `fonts.json`, and this module reads them from the
|
|
18
|
+
* **consuming package's** install.
|
|
19
|
+
*
|
|
20
|
+
* package-build declares no dependency on `@heroiclands/fonts` in any field:
|
|
21
|
+
* the fonts package depends on package-build to build, so the consumer adds it
|
|
22
|
+
* as a devDependency of its own and package-build resolves it from the
|
|
23
|
+
* consumer's root. Resolving from this module's own location would find
|
|
24
|
+
* whatever happens to sit above package-build's install, or nothing.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import { createRequire } from "node:module";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
|
|
33
|
+
/** The npm name of the fonts package. */
|
|
34
|
+
export const FONTS_PACKAGE = "@heroiclands/fonts";
|
|
35
|
+
|
|
36
|
+
/** The manifest the fonts package publishes at its root. */
|
|
37
|
+
export const FONTS_MANIFEST = "fonts.json";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One code point a face draws, as `fonts.json` lists it.
|
|
41
|
+
*
|
|
42
|
+
* @typedef {object} FaceCodepoint
|
|
43
|
+
* @property {string} codepoint - `U+` and the hexadecimal code point.
|
|
44
|
+
* @property {string} glyph - The glyph's name in the face.
|
|
45
|
+
* @property {string} kind - What it is: `letter`, `numeral`, `mark` and so on.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* One entry of `fonts.json`.
|
|
50
|
+
*
|
|
51
|
+
* @typedef {object} FontEntry
|
|
52
|
+
* @property {string} [name] - Its unique key; `family` when absent.
|
|
53
|
+
* @property {string} family - The CSS and Typst font family.
|
|
54
|
+
* @property {string} class - The class that sets text in it, `hl-<slug>`.
|
|
55
|
+
* @property {Record<string, string>} files - Its font files, by format.
|
|
56
|
+
* @property {string} direction - `ltr` or `rtl`.
|
|
57
|
+
* @property {string[]} features - OpenType features it turns on.
|
|
58
|
+
* @property {FaceCodepoint[]} [codepoints] - Every code point it draws.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A face as a run reads it.
|
|
63
|
+
*
|
|
64
|
+
* @typedef {object} Face
|
|
65
|
+
* @property {string} slug - What `font=` names it by: its class without `hl-`.
|
|
66
|
+
* @property {string} family - The CSS and Typst font family.
|
|
67
|
+
* @property {string} class - The class that sets text in it.
|
|
68
|
+
* @property {string} direction - `ltr` or `rtl`.
|
|
69
|
+
* @property {string[]} features - OpenType features it turns on.
|
|
70
|
+
* @property {ReadonlySet<number>} draws - Every code point it draws.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The installed fonts package, as a consumer sees it.
|
|
75
|
+
*
|
|
76
|
+
* @typedef {object} ScriptFonts
|
|
77
|
+
* @property {string} root - The package's directory.
|
|
78
|
+
* @property {string} version - Its `package.json` version.
|
|
79
|
+
* @property {{families: FontEntry[]}|undefined} manifest - Its `fonts.json`,
|
|
80
|
+
* `undefined` when the installed version ships none.
|
|
81
|
+
* @property {Map<string, Face>} faces - Each face by its slug, in manifest
|
|
82
|
+
* order; empty without a manifest.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The slug `font=` names a face by: its class without the `hl-` prefix.
|
|
87
|
+
*
|
|
88
|
+
* @param {FontEntry} entry - A `fonts.json` entry.
|
|
89
|
+
* @returns {string|undefined} The slug, or `undefined` for an entry with no class.
|
|
90
|
+
*/
|
|
91
|
+
export function faceSlug(entry) {
|
|
92
|
+
const name = typeof entry?.class === "string" ? entry.class : "";
|
|
93
|
+
return name ? name.replace(/^hl-/, "") : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The code point a `U+XXXX` string names.
|
|
98
|
+
*
|
|
99
|
+
* @param {unknown} value - The listed code point.
|
|
100
|
+
* @returns {number|undefined} The code point, or `undefined` when it names none.
|
|
101
|
+
*/
|
|
102
|
+
function codepointOf(value) {
|
|
103
|
+
const match = /^U\+([0-9A-Fa-f]{4,6})$/.exec(String(value ?? ""));
|
|
104
|
+
return match ? Number.parseInt(match[1], 16) : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The fonts package the consumer at `rootDir` installed.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} rootDir - The consuming package's root, holding its
|
|
111
|
+
* `package.json`.
|
|
112
|
+
* @returns {ScriptFonts|undefined} The package, or `undefined` when the
|
|
113
|
+
* consumer has not installed it.
|
|
114
|
+
* @throws {Error} When the installed `fonts.json` is not readable JSON, or
|
|
115
|
+
* holds no `families` list.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveScriptFonts(rootDir) {
|
|
118
|
+
const require = createRequire(path.join(rootDir, "package.json"));
|
|
119
|
+
let packageJson;
|
|
120
|
+
try {
|
|
121
|
+
packageJson = require.resolve(`${FONTS_PACKAGE}/package.json`);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (/** @type {{code?: string}} */ (err)?.code === "MODULE_NOT_FOUND") return undefined;
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
const root = path.dirname(packageJson);
|
|
127
|
+
const { version } = JSON.parse(fs.readFileSync(packageJson, "utf8"));
|
|
128
|
+
|
|
129
|
+
const manifestFile = path.join(root, FONTS_MANIFEST);
|
|
130
|
+
let manifest;
|
|
131
|
+
if (fs.existsSync(manifestFile)) {
|
|
132
|
+
try {
|
|
133
|
+
manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
|
|
134
|
+
} catch (err) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`${manifestFile}: error: ${FONTS_PACKAGE} ${version} ships a ` +
|
|
137
|
+
`${FONTS_MANIFEST} that is not readable JSON: ${/** @type {Error} */ (err).message}`,
|
|
138
|
+
{ cause: err },
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(manifest?.families)) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`${manifestFile}: error: ${FONTS_PACKAGE} ${version} ships a ` +
|
|
144
|
+
`${FONTS_MANIFEST} with no \`families\` list`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const faces = new Map();
|
|
150
|
+
for (const entry of manifest?.families ?? []) {
|
|
151
|
+
const slug = faceSlug(entry);
|
|
152
|
+
if (!slug || faces.has(slug)) continue;
|
|
153
|
+
const draws = new Set();
|
|
154
|
+
for (const listed of entry.codepoints ?? []) {
|
|
155
|
+
const point = codepointOf(listed?.codepoint);
|
|
156
|
+
if (point !== undefined) draws.add(point);
|
|
157
|
+
}
|
|
158
|
+
faces.set(
|
|
159
|
+
slug,
|
|
160
|
+
Object.freeze({
|
|
161
|
+
slug,
|
|
162
|
+
family: entry.family,
|
|
163
|
+
class: entry.class,
|
|
164
|
+
direction: entry.direction === "rtl" ? "rtl" : "ltr",
|
|
165
|
+
features: Object.freeze([...(entry.features ?? [])]),
|
|
166
|
+
draws,
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { root, version, manifest, faces };
|
|
172
|
+
}
|
package/engine/site-build.mjs
CHANGED
|
@@ -65,6 +65,7 @@ import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
|
|
|
65
65
|
import { eventNoteIndex } from "./event-fields.mjs";
|
|
66
66
|
import { expandContentTables } from "./content-tables.mjs";
|
|
67
67
|
import { renderSpans, scanSpans } from "./content-spans.mjs";
|
|
68
|
+
import { renderRuns, runContext, scanRuns } from "./content-scripts.mjs";
|
|
68
69
|
import { renderBlocks, renderWithheldSections, scanBlocks } from "./content-blocks.mjs";
|
|
69
70
|
import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
|
|
70
71
|
import { renderFigureBlocks, scanFigures } from "./content-figures.mjs";
|
|
@@ -728,6 +729,7 @@ export function renderSitePage(
|
|
|
728
729
|
figuresByAddress = new Map(),
|
|
729
730
|
events,
|
|
730
731
|
drawings = new Map(),
|
|
732
|
+
runs = runContext(config),
|
|
731
733
|
},
|
|
732
734
|
) {
|
|
733
735
|
const tableErrors = [];
|
|
@@ -879,9 +881,12 @@ export function renderSitePage(
|
|
|
879
881
|
const authoredLinks = resolved.length;
|
|
880
882
|
const resolvedBody =
|
|
881
883
|
generated ? `${authoredBody}\n${protectCode(generated, resolve)}` : authoredBody;
|
|
882
|
-
const figured = renderFigureBlocks(
|
|
883
|
-
|
|
884
|
-
|
|
884
|
+
const figured = renderFigureBlocks(
|
|
885
|
+
renderRuns(resolvedBody, runs, undefined, { target: "site" }).markdown,
|
|
886
|
+
undefined,
|
|
887
|
+
undefined,
|
|
888
|
+
{ resolveRole: (address) => roleByWebSrc.get(address) },
|
|
889
|
+
);
|
|
885
890
|
for (const error of figureScan.errors)
|
|
886
891
|
captionErrors.push({
|
|
887
892
|
file: page.file,
|
|
@@ -893,6 +898,7 @@ export function renderSitePage(
|
|
|
893
898
|
...scanBlocks(page.body).errors,
|
|
894
899
|
...scanAlerts(page.body).errors,
|
|
895
900
|
...scanSpans(page.body).errors,
|
|
901
|
+
...scanRuns(page.body, runs).errors,
|
|
896
902
|
...markupAnchorFindings(page.body),
|
|
897
903
|
])
|
|
898
904
|
secretErrors.push({
|
|
@@ -1098,6 +1104,7 @@ export function renderPages(pages, options) {
|
|
|
1098
1104
|
figuresByAddress,
|
|
1099
1105
|
events,
|
|
1100
1106
|
drawings: maps,
|
|
1107
|
+
runs: runContext(config, records),
|
|
1101
1108
|
});
|
|
1102
1109
|
tableErrors.push(...result.tableErrors);
|
|
1103
1110
|
expressionErrors.push(...result.expressionErrors);
|
package/engine/site-config.mjs
CHANGED
|
@@ -47,6 +47,7 @@ import { stringify as stringifyToml } from "smol-toml";
|
|
|
47
47
|
|
|
48
48
|
import { checkHomepage, fail } from "../config.mjs";
|
|
49
49
|
import { slugify } from "./content-slug.mjs";
|
|
50
|
+
import { siteFontsParam } from "./script-font-delivery.mjs";
|
|
50
51
|
|
|
51
52
|
/** The Hugo source directory, relative to the repository root. */
|
|
52
53
|
export const HUGO_SOURCE = "build/hugo";
|
|
@@ -402,6 +403,10 @@ export function hugoConfig({ config, navigation, themesDir }) {
|
|
|
402
403
|
if (config.site.assets) params.cdnBaseURL = config.site.assets;
|
|
403
404
|
params.brand = { ...BRAND };
|
|
404
405
|
if (config.site.notfound) params.notfound = structuredClone(config.site.notfound);
|
|
406
|
+
// Named only when the build copies the fonts, so the theme links nothing
|
|
407
|
+
// it does not ship.
|
|
408
|
+
const scriptFonts = siteFontsParam(config);
|
|
409
|
+
if (scriptFonts) params.scriptFonts = scriptFonts;
|
|
405
410
|
|
|
406
411
|
const generated = {
|
|
407
412
|
baseURL: config.homepage,
|
package/engine/site-root.mjs
CHANGED
|
@@ -111,16 +111,29 @@ export function noindexHeaders() {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
* The `_headers` file's contents: the `noindex` rules, and
|
|
114
|
+
* The `_headers` file's contents: the `noindex` rules, and the script fonts'
|
|
115
|
+
* cache rule when the site carries them.
|
|
115
116
|
*
|
|
116
117
|
* No `Cache-Control` is pinned on the prefix root. It is the homepage, and a
|
|
117
118
|
* lifetime on it would hold a stale copy at the most-linked address after a
|
|
118
|
-
* deploy; Pages' own defaults for a page apply.
|
|
119
|
+
* deploy; Pages' own defaults for a page apply. The fonts sit under a path
|
|
120
|
+
* that names their version, so a year's lifetime never serves a stale file.
|
|
119
121
|
*
|
|
122
|
+
* @param {object} [options] - Options.
|
|
123
|
+
* @param {string} [options.pkg] - The content package, which is the prefix.
|
|
124
|
+
* @param {boolean} [options.fonts] - Whether the site carries the fonts.
|
|
120
125
|
* @returns {string} The file's contents.
|
|
121
126
|
*/
|
|
122
|
-
export function headers() {
|
|
123
|
-
|
|
127
|
+
export function headers({ pkg, fonts = false } = {}) {
|
|
128
|
+
const lines = noindexHeaders();
|
|
129
|
+
if (fonts && pkg) {
|
|
130
|
+
lines.push(
|
|
131
|
+
`/${pkg}/fonts/heroiclands/*`,
|
|
132
|
+
" Cache-Control: public, max-age=31536000, immutable",
|
|
133
|
+
"",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return lines.join("\n");
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
/**
|
|
@@ -256,7 +269,8 @@ export async function writeSiteRoot({ pkg, out, search = true, indexer = indexSi
|
|
|
256
269
|
}
|
|
257
270
|
|
|
258
271
|
const file = path.join(root, "_headers");
|
|
259
|
-
fs.
|
|
272
|
+
const fonts = fs.existsSync(path.join(rendered, "fonts", "heroiclands"));
|
|
273
|
+
fs.writeFileSync(file, headers({ pkg, fonts }));
|
|
260
274
|
fs.rmSync(path.join(root, "_redirects"), { force: true });
|
|
261
275
|
|
|
262
276
|
const dir = path.join(rendered, SEARCH_DIR);
|
package/engine/sql-tables.mjs
CHANGED
|
@@ -34,6 +34,9 @@ import { encodeAddresses, flattenPublishedAddresses } from "./address-values.mjs
|
|
|
34
34
|
import { EVENT_COLUMNS, EVENT_SORT_COLUMNS, eventCalendars, eventRows } from "./event-rows.mjs";
|
|
35
35
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
36
36
|
import { NAME_COLUMNS, namesRows } from "./name-rows.mjs";
|
|
37
|
+
import { RUN_COLUMNS, ownerOf, runsRows } from "./run-rows.mjs";
|
|
38
|
+
import { MAP_COLUMNS, mapRows } from "./text-maps.mjs";
|
|
39
|
+
import { NATIVE_COLUMNS, nativeRows } from "./map-lexicon.mjs";
|
|
37
40
|
import {
|
|
38
41
|
GLOSSARY_COLUMNS,
|
|
39
42
|
AFFIX_COLUMNS,
|
|
@@ -276,6 +279,8 @@ export async function openNotesDatabase(
|
|
|
276
279
|
await createRelations(connection, jsonl, encoded, "", audience);
|
|
277
280
|
await createEventRelation(connection, path.join(base, "events.jsonl"), encoded, "", audience);
|
|
278
281
|
await createNameRelation(connection, path.join(base, "names.jsonl"), encoded, "", audience);
|
|
282
|
+
await createRunRelation(connection, path.join(base, "runs.jsonl"), encoded, "", audience);
|
|
283
|
+
await createMapRelation(connection, path.join(base, "maps.jsonl"), encoded, "", audience);
|
|
279
284
|
await createWordRelations(connection, path.join(base, "notes"), encoded, "", audience);
|
|
280
285
|
|
|
281
286
|
// One schema per declared dependency, so `FROM sohl.notes` reads the notes
|
|
@@ -312,6 +317,20 @@ export async function openNotesDatabase(
|
|
|
312
317
|
`${schema}.`,
|
|
313
318
|
audience,
|
|
314
319
|
);
|
|
320
|
+
await createRunRelation(
|
|
321
|
+
connection,
|
|
322
|
+
flat.replace(/\.jsonl$/, "-runs.jsonl"),
|
|
323
|
+
depRecords,
|
|
324
|
+
`${schema}.`,
|
|
325
|
+
audience,
|
|
326
|
+
);
|
|
327
|
+
await createMapRelation(
|
|
328
|
+
connection,
|
|
329
|
+
flat.replace(/\.jsonl$/, "-maps.jsonl"),
|
|
330
|
+
depRecords,
|
|
331
|
+
`${schema}.`,
|
|
332
|
+
audience,
|
|
333
|
+
);
|
|
315
334
|
await createWordRelations(
|
|
316
335
|
connection,
|
|
317
336
|
flat.replace(/\.jsonl$/, ""),
|
|
@@ -346,6 +365,8 @@ export async function openNotesDatabase(
|
|
|
346
365
|
for (const relation of [
|
|
347
366
|
"events",
|
|
348
367
|
"names",
|
|
368
|
+
"runs",
|
|
369
|
+
"maps",
|
|
349
370
|
"words",
|
|
350
371
|
"affixes",
|
|
351
372
|
"fields",
|
|
@@ -608,6 +629,95 @@ async function createNameRelation(connection, file, records, prefix, audience) {
|
|
|
608
629
|
);
|
|
609
630
|
}
|
|
610
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Create the `runs` relation: one row per span, div and poetry fence an own
|
|
634
|
+
* note marks with `lang=`, `font=` or `map=`, with the columns
|
|
635
|
+
* {@link module:engine/run-rows.RUN_COLUMNS} names. The columns are declared
|
|
636
|
+
* rather than inferred, so every value stays a string and none becomes a
|
|
637
|
+
* struct key whose case DuckDB folds.
|
|
638
|
+
*
|
|
639
|
+
* @param {object} connection - An open DuckDB connection.
|
|
640
|
+
* @param {string} file - Where to write the rows.
|
|
641
|
+
* @param {object[]} records - Index records, every Address a string.
|
|
642
|
+
* @param {string} prefix - A schema to qualify the relation name with.
|
|
643
|
+
* @param {"all"|"public"} audience - Whether to leave out GM notes.
|
|
644
|
+
* @returns {Promise<void>}
|
|
645
|
+
*/
|
|
646
|
+
async function createRunRelation(connection, file, records, prefix, audience) {
|
|
647
|
+
const rows = runsRows(
|
|
648
|
+
audience === "public" ? records.filter((record) => !isGmNote(record)) : records,
|
|
649
|
+
);
|
|
650
|
+
await createTypedRelation(connection, file, `${prefix}runs`, rows, RUN_COLUMNS, [
|
|
651
|
+
"line",
|
|
652
|
+
"column",
|
|
653
|
+
]);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Create the `maps` relation: one row per row of a mapping note's
|
|
658
|
+
* `data.map`, with the columns {@link module:engine/text-maps.MAP_COLUMNS}
|
|
659
|
+
* names; and the `natives` relation, one row per native form of a language's
|
|
660
|
+
* word, with the columns {@link module:engine/map-lexicon.NATIVE_COLUMNS}
|
|
661
|
+
* names. A row is a row here, never a struct key, so `A` and `a` are two rows.
|
|
662
|
+
*
|
|
663
|
+
* @param {object} connection - An open DuckDB connection.
|
|
664
|
+
* @param {string} file - Where to write the rows.
|
|
665
|
+
* @param {object[]} records - Index records, every Address a string.
|
|
666
|
+
* @param {string} prefix - A schema to qualify the relation name with.
|
|
667
|
+
* @param {"all"|"public"} audience - Whether to leave out GM notes.
|
|
668
|
+
* @returns {Promise<void>}
|
|
669
|
+
*/
|
|
670
|
+
async function createMapRelation(connection, file, records, prefix, audience) {
|
|
671
|
+
const rows = mapRows(
|
|
672
|
+
audience === "public" ? records.filter((record) => !isGmNote(record)) : records,
|
|
673
|
+
ownerOf,
|
|
674
|
+
);
|
|
675
|
+
await createTypedRelation(connection, file, `${prefix}maps`, rows, MAP_COLUMNS, ["row"]);
|
|
676
|
+
const visible = audience === "public" ? records.filter((record) => !isGmNote(record)) : records;
|
|
677
|
+
await createTypedRelation(
|
|
678
|
+
connection,
|
|
679
|
+
file.replace(/maps\.jsonl$/, "natives.jsonl"),
|
|
680
|
+
`${prefix}natives`,
|
|
681
|
+
nativeRows(visible),
|
|
682
|
+
NATIVE_COLUMNS,
|
|
683
|
+
["word", "native"],
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Create a relation whose columns are declared rather than inferred, so every
|
|
689
|
+
* value stays a string or a number and none becomes a struct key whose case
|
|
690
|
+
* DuckDB folds. An empty relation keeps its columns.
|
|
691
|
+
*
|
|
692
|
+
* @param {object} connection - An open DuckDB connection.
|
|
693
|
+
* @param {string} file - Where to write the rows.
|
|
694
|
+
* @param {string} name - The relation, schema-qualified where it is a dependency's.
|
|
695
|
+
* @param {object[]} rows - Its rows.
|
|
696
|
+
* @param {readonly string[]} columns - Its columns, in order.
|
|
697
|
+
* @param {readonly string[]} integers - The columns that are `BIGINT`; the
|
|
698
|
+
* rest are `VARCHAR`.
|
|
699
|
+
* @returns {Promise<void>}
|
|
700
|
+
*/
|
|
701
|
+
async function createTypedRelation(connection, file, name, rows, columns, integers) {
|
|
702
|
+
const columnTypes = columns.map((column) => [
|
|
703
|
+
column,
|
|
704
|
+
integers.includes(column) ? "BIGINT" : "VARCHAR",
|
|
705
|
+
]);
|
|
706
|
+
if (!rows.length) {
|
|
707
|
+
const nulls = columnTypes.map(([column, type]) => `NULL::${type} AS "${column}"`);
|
|
708
|
+
await connection.run(`CREATE VIEW ${name} AS SELECT ${nulls.join(", ")} WHERE false`);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
fs.writeFileSync(file, rows.map((row) => JSON.stringify(row)).join("\n"));
|
|
712
|
+
const schema = columnTypes.map(([column, type]) => `'${column}': '${type}'`).join(", ");
|
|
713
|
+
await createTableFromFile(
|
|
714
|
+
connection,
|
|
715
|
+
name,
|
|
716
|
+
`SELECT * FROM read_json('${file.replace(/'/g, "''")}', columns={${schema}}, ` +
|
|
717
|
+
"format='newline_delimited', maximum_object_size=20000000)",
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
611
721
|
/**
|
|
612
722
|
* Create a relation as a table, so the JSONL behind it is parsed once when the
|
|
613
723
|
* database opens rather than on every query. A view over `read_json_auto`
|
|
@@ -699,7 +809,7 @@ async function createWordRelations(connection, stem, records, prefix, audience)
|
|
|
699
809
|
senses: "STRUCT(text VARCHAR, bodies VARCHAR[])[]",
|
|
700
810
|
usage: "VARCHAR[]",
|
|
701
811
|
depth: "BIGINT",
|
|
702
|
-
verbs: `STRUCT(invariant BOOLEAN, persons STRUCT(${VERB_PERSON_FIELDS.map((person) => `"${person.name}" VARCHAR`).join(", ")}))`,
|
|
812
|
+
verbs: `STRUCT(invariant BOOLEAN, replaces VARCHAR, persons STRUCT(${VERB_PERSON_FIELDS.map((person) => `"${person.name}" VARCHAR`).join(", ")}))`,
|
|
703
813
|
};
|
|
704
814
|
const dictionaryFile = `${stem}-dictionary.jsonl`;
|
|
705
815
|
if (dictionary.length) {
|