@heroiclands/package-build 24.0.3 → 24.0.4
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 +17 -0
- package/docs/authoring/links-and-markup.md +11 -2
- package/docs/reference/format-details.md +12 -3
- package/engine/content-scripts.mjs +22 -1
- package/engine/core-report.mjs +10 -2
- package/engine/map-lexicon.mjs +37 -5
- package/engine/run-words.mjs +7 -7
- package/package.json +1 -1
- package/types/engine/map-lexicon.d.mts +3 -1
- package/types/engine/run-words.d.mts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 24.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
**Languages**
|
|
8
|
+
|
|
9
|
+
- Core vocabulary coverage uses only the nearest definition when a language
|
|
10
|
+
redefines an inherited expression.
|
|
11
|
+
|
|
12
|
+
- Inherited dictionary words appear in a daughter language’s script with the
|
|
13
|
+
applicable native spelling.
|
|
14
|
+
|
|
15
|
+
**Verse**
|
|
16
|
+
|
|
17
|
+
- Parallel verse keeps its Roman reading beside the native script, with
|
|
18
|
+
gaps between stanzas.
|
|
19
|
+
|
|
3
20
|
## 24.0.3
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
|
@@ -490,7 +490,11 @@ column stacks one or more **entries**, joined by `:`. An entry is
|
|
|
490
490
|
- **`mapping`** names a [mapping note](#mapping-notes), which rewrites the
|
|
491
491
|
line before it is set. Left out, the line is set as written.
|
|
492
492
|
- **`font`** names a face or a generic family, as `font=` does. Left out, the
|
|
493
|
-
line
|
|
493
|
+
line keeps the page's normal typeface.
|
|
494
|
+
|
|
495
|
+
A language-only entry stays in Roman letters even when the language's
|
|
496
|
+
`data.writtenIn` names its usual script. Ordinary language-marked prose still
|
|
497
|
+
selects that script automatically; columns use the entry's explicit mapping.
|
|
494
498
|
|
|
495
499
|
A poem has at most two columns, and either may be left out. In every column
|
|
496
500
|
the first entry is set in the normal tone and each later one in grey.
|
|
@@ -805,7 +809,12 @@ is one of them.
|
|
|
805
809
|
|
|
806
810
|
**With `lang=`, the language's dictionary comes first.** Where
|
|
807
811
|
[`languages.<name>.note`](../configuration.md#languages) names the language's
|
|
808
|
-
note, each word of the run is looked up in its `data.words
|
|
812
|
+
note, each word of the run is looked up in its `data.words` and the words of
|
|
813
|
+
its `data.family` chain. The nearest exact NFC form supplies the entire word
|
|
814
|
+
entry, including variants and native spellings; an override replaces the whole
|
|
815
|
+
ancestor entry. Case and word separators remain part of that identity.
|
|
816
|
+
Grammar, registered names and the default writing script remain the daughter's
|
|
817
|
+
own. An inherited native spelling applies only to the map and face it names:
|
|
809
818
|
|
|
810
819
|
1. **A word whose entry states a `native` form for the map** is written by it:
|
|
811
820
|
the form stated for the run's face where there is one, else the form stated
|
|
@@ -6637,9 +6637,18 @@ The lint holds each to what it means, each finding located at the key at fault:
|
|
|
6637
6637
|
Where no note states `data.concepts`, every id is unknown, and the message
|
|
6638
6638
|
says so.
|
|
6639
6639
|
|
|
6640
|
-
A language reads the words of its `data.family` chain as its own
|
|
6641
|
-
|
|
6642
|
-
|
|
6640
|
+
A language reads the words of its `data.family` chain as its own. The nearest
|
|
6641
|
+
definition of a form wins, using the dictionary's matching: Unicode NFC, with
|
|
6642
|
+
case and word separators kept. If the daughter defines the same form, only
|
|
6643
|
+
that definition's `concept` ids count; an untagged definition covers none of
|
|
6644
|
+
the ancestor's concepts. A different inherited expression can still cover
|
|
6645
|
+
them. `data.core` is not inherited: each language that must cover the list
|
|
6646
|
+
states it.
|
|
6647
|
+
|
|
6648
|
+
A `form` may contain several words. Its `gloss` records the expression's
|
|
6649
|
+
meaning, `built` may name its existing pieces, and `concept` may name every
|
|
6650
|
+
concept those senses support. The report counts declared ids; it does not
|
|
6651
|
+
prove an expression's meaning or infer coverage from grammar or untagged prose.
|
|
6643
6652
|
|
|
6644
6653
|
`package-build core` is the report. A concept no word serves is never a lint
|
|
6645
6654
|
finding; the report lists it so the language can be filled in. See
|
|
@@ -91,6 +91,9 @@ import {
|
|
|
91
91
|
expandVerseOrigins,
|
|
92
92
|
scanVerse,
|
|
93
93
|
sourcePosition,
|
|
94
|
+
hasColumns,
|
|
95
|
+
verseColumns,
|
|
96
|
+
STANZA_BREAK,
|
|
94
97
|
} from "./content-verse.mjs";
|
|
95
98
|
import { scanSpans } from "./content-spans.mjs";
|
|
96
99
|
import { scanScrolls, scrollParagraphs } from "./content-scroll.mjs";
|
|
@@ -908,8 +911,23 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
908
911
|
collect(0, text);
|
|
909
912
|
|
|
910
913
|
const poems = new Map();
|
|
914
|
+
const romanColumnLines = new Set();
|
|
911
915
|
for (const poem of scanVerse(text).blocks) {
|
|
912
916
|
poems.set(poem.start, poem);
|
|
917
|
+
// Expansion writes one span per display entry. An entry with no map
|
|
918
|
+
// keeps the source spelling and page font, even when its language
|
|
919
|
+
// normally selects a script. The fence itself holds only structure.
|
|
920
|
+
if (poem.classes.includes(COLUMNS_CLASS)) {
|
|
921
|
+
const entries = verseColumns(poem, lines[poem.start]).flatMap(
|
|
922
|
+
(column) => column.entries,
|
|
923
|
+
);
|
|
924
|
+
let entry = 0;
|
|
925
|
+
for (let line = poem.start + 1; line < poem.end; line++) {
|
|
926
|
+
if (!lines[line].trim() || STANZA_BREAK.test(lines[line].trim())) continue;
|
|
927
|
+
if (entries[entry++ % entries.length]?.map === undefined)
|
|
928
|
+
romanColumnLines.add(line);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
913
931
|
const keys = runKeys(poem.attributes);
|
|
914
932
|
if (!Object.keys(keys).length) continue;
|
|
915
933
|
const opening = lines[poem.start];
|
|
@@ -1272,7 +1290,10 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1272
1290
|
const keys = Object.assign({}, ...around(construct).map((c) => c.keys), construct.keys);
|
|
1273
1291
|
// The language's script fills what the run leaves unstated, once its
|
|
1274
1292
|
// own keys are known to name something.
|
|
1275
|
-
|
|
1293
|
+
const columnSource = construct.kind === "span" && romanColumnLines.has(construct.line0);
|
|
1294
|
+
const columnFence = construct.kind === "verse" && hasColumns(construct.node);
|
|
1295
|
+
if (named && !columnSource && !columnFence)
|
|
1296
|
+
Object.assign(keys, fromWrittenIn(keys, ctx.writtenIn, maps));
|
|
1276
1297
|
if (construct.kind === "scroll" && named) {
|
|
1277
1298
|
scrollFace(construct, keys);
|
|
1278
1299
|
named = errors.length === unchecked;
|
package/engine/core-report.mjs
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* The list is the `data.concepts` of the package's `doc` notes. A language
|
|
19
19
|
* must cover it where it states `data.core: true`. A word covers every
|
|
20
20
|
* concept its `concept` names, and a language reads the words of its
|
|
21
|
-
* `data.family` chain as its own
|
|
21
|
+
* `data.family` chain as its own, with the nearest definition of each form
|
|
22
|
+
* winning. A missing concept is a fact to work through,
|
|
22
23
|
* never a finding.
|
|
23
24
|
*
|
|
24
25
|
* @module
|
|
@@ -114,9 +115,16 @@ export function coreReport(records, { language } = {}) {
|
|
|
114
115
|
concepts: concepts.length,
|
|
115
116
|
languages: chosen.map((record) => {
|
|
116
117
|
const have = new Set();
|
|
118
|
+
const forms = new Set();
|
|
117
119
|
for (const key of chain(noteAddressOf(record) ?? ""))
|
|
118
|
-
for (const word of byKey.get(key)?.data?.words ?? [])
|
|
120
|
+
for (const word of byKey.get(key)?.data?.words ?? []) {
|
|
121
|
+
if (!isMap(word) || typeof word.form !== "string") continue;
|
|
122
|
+
// Match dictionary headwords: NFC, retaining case and word separators.
|
|
123
|
+
const form = word.form.normalize("NFC");
|
|
124
|
+
if (forms.has(form)) continue;
|
|
125
|
+
forms.add(form);
|
|
119
126
|
for (const id of idsOf(word)) have.add(id);
|
|
127
|
+
}
|
|
120
128
|
const missing = [];
|
|
121
129
|
for (const group of groups) {
|
|
122
130
|
const members = concepts.filter((concept) => concept.group === group);
|
package/engine/map-lexicon.mjs
CHANGED
|
@@ -59,7 +59,7 @@ import { fromWrittenIn } from "./written-in.mjs";
|
|
|
59
59
|
import { writingsOf } from "./logographic.mjs";
|
|
60
60
|
import { FONTS_PACKAGE } from "./script-fonts.mjs";
|
|
61
61
|
import { GENERIC_FAMILIES, applyMap } from "./text-maps.mjs";
|
|
62
|
-
import { isLanguageRecord, noteAddressOf } from "./word-rows.mjs";
|
|
62
|
+
import { isLanguageRecord, lineageChain, noteAddressOf } from "./word-rows.mjs";
|
|
63
63
|
import { derivedForms } from "./word-forms.mjs";
|
|
64
64
|
import { LANGUAGE_SUBTYPE, glossText } from "./word-terms.mjs";
|
|
65
65
|
|
|
@@ -183,7 +183,7 @@ function lexiconOf(language, data) {
|
|
|
183
183
|
* @returns {string|undefined} The Address.
|
|
184
184
|
*/
|
|
185
185
|
function noteKey(note, contentPackage, types) {
|
|
186
|
-
if (
|
|
186
|
+
if (!contentPackage) return undefined;
|
|
187
187
|
const tuple = parseAddress(
|
|
188
188
|
note,
|
|
189
189
|
{ package: contentPackage, system: "note", type: "skill", types: new Set(types) },
|
|
@@ -248,7 +248,9 @@ function languageNotes(languages, contentPackage, records) {
|
|
|
248
248
|
|
|
249
249
|
/**
|
|
250
250
|
* Each configured language's lexicon, read from the language note its `note`
|
|
251
|
-
* names
|
|
251
|
+
* names and its family chain. The nearest exact NFC form supplies the whole
|
|
252
|
+
* word; grammar, registers and other settings remain the daughter's own.
|
|
253
|
+
* A language with no `note`, or one naming no language note, has none.
|
|
252
254
|
*
|
|
253
255
|
* @param {Iterable<Record<string, any>>|undefined} records - Index records.
|
|
254
256
|
* @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
|
|
@@ -256,9 +258,39 @@ function languageNotes(languages, contentPackage, records) {
|
|
|
256
258
|
* @returns {Map<string, Lexicon>} The lexicons by language name.
|
|
257
259
|
*/
|
|
258
260
|
export function lexiconsFromRecords(records, config = {}) {
|
|
261
|
+
const list = [...(records ?? [])];
|
|
262
|
+
const byKey = recordsByKey(list, config.contentPackage);
|
|
263
|
+
const types = new Set(["skill", ...list.map((record) => record?.type).filter(Boolean)]);
|
|
264
|
+
const lineage = {
|
|
265
|
+
has: (key) => isLanguage(byKey.get(key)),
|
|
266
|
+
parentOf: (key) => {
|
|
267
|
+
const record = byKey.get(key);
|
|
268
|
+
return noteKey(record.data?.family, record.package ?? config.contentPackage, types);
|
|
269
|
+
},
|
|
270
|
+
};
|
|
259
271
|
const lexicons = new Map();
|
|
260
|
-
for (const [name, record] of
|
|
261
|
-
|
|
272
|
+
for (const [name, { key, record }] of languageNotes(
|
|
273
|
+
config.languages,
|
|
274
|
+
config.contentPackage,
|
|
275
|
+
list,
|
|
276
|
+
)) {
|
|
277
|
+
if (!record) continue;
|
|
278
|
+
const chain = lineageChain(key, lineage);
|
|
279
|
+
let data = record.data;
|
|
280
|
+
if (chain.length > 1) {
|
|
281
|
+
const words = new Map();
|
|
282
|
+
for (const ancestor of chain)
|
|
283
|
+
for (const word of listOf(byKey.get(ancestor).data?.words)) {
|
|
284
|
+
if (!isMap(word) || typeof word.form !== "string") continue;
|
|
285
|
+
// The nearest complete definition wins before word matching
|
|
286
|
+
// folds case or splits compounds. Only words are inherited.
|
|
287
|
+
const form = word.form.normalize("NFC");
|
|
288
|
+
if (!words.has(form)) words.set(form, word);
|
|
289
|
+
}
|
|
290
|
+
data = { ...data, words: [...words.values()] };
|
|
291
|
+
}
|
|
292
|
+
lexicons.set(name, lexiconOf(name, data));
|
|
293
|
+
}
|
|
262
294
|
return lexicons;
|
|
263
295
|
}
|
|
264
296
|
|
package/engine/run-words.mjs
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* @module
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import { lexiconsFromRecords } from "./map-lexicon.mjs";
|
|
40
40
|
import { RESERVED_TONGUE } from "./language-terms.mjs";
|
|
41
41
|
import { addWords, foldWord, lexiconWords } from "./word-forms.mjs";
|
|
42
42
|
import { WORD } from "./word-pattern.mjs";
|
|
@@ -82,7 +82,8 @@ export function registeredNameWords(data) {
|
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
84
|
* Each configured language's word book: one per language whose `note` names a
|
|
85
|
-
* language note.
|
|
85
|
+
* language note. Its effective lexicon shares the renderer's nearest inherited
|
|
86
|
+
* words, and derives forms and registered names from its own settings.
|
|
86
87
|
*
|
|
87
88
|
* @param {Iterable<Record<string, any>>|undefined} records - Index records.
|
|
88
89
|
* @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
|
|
@@ -92,11 +93,10 @@ export function registeredNameWords(data) {
|
|
|
92
93
|
export function wordBooksFromRecords(records, config = {}) {
|
|
93
94
|
const list = [...(records ?? [])];
|
|
94
95
|
const books = new Map();
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const names = registeredNameWords(record.data);
|
|
96
|
+
const lexicons = lexiconsFromRecords(list, config);
|
|
97
|
+
for (const [name, lexicon] of lexicons) {
|
|
98
|
+
const own = lexiconWords(lexicon.data);
|
|
99
|
+
const names = registeredNameWords(lexicon.data);
|
|
100
100
|
books.set(
|
|
101
101
|
name,
|
|
102
102
|
Object.freeze({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "24.0.
|
|
3
|
+
"version": "24.0.4",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Each configured language's lexicon, read from the language note its `note`
|
|
3
|
-
* names
|
|
3
|
+
* names and its family chain. The nearest exact NFC form supplies the whole
|
|
4
|
+
* word; grammar, registers and other settings remain the daughter's own.
|
|
5
|
+
* A language with no `note`, or one naming no language note, has none.
|
|
4
6
|
*
|
|
5
7
|
* @param {Iterable<Record<string, any>>|undefined} records - Index records.
|
|
6
8
|
* @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
|
|
@@ -17,7 +17,8 @@ export function registeredNameWords(data: Record<string, any> | undefined): Set<
|
|
|
17
17
|
*/
|
|
18
18
|
/**
|
|
19
19
|
* Each configured language's word book: one per language whose `note` names a
|
|
20
|
-
* language note.
|
|
20
|
+
* language note. Its effective lexicon shares the renderer's nearest inherited
|
|
21
|
+
* words, and derives forms and registered names from its own settings.
|
|
21
22
|
*
|
|
22
23
|
* @param {Iterable<Record<string, any>>|undefined} records - Index records.
|
|
23
24
|
* @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
|