@heroiclands/package-build 24.0.2 → 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 +35 -0
- package/bin/content-commands.mjs +8 -10
- package/content-config.mjs +1 -52
- package/docs/api.md +14 -8
- package/docs/authoring/links-and-markup.md +12 -3
- package/docs/commands.md +4 -6
- package/docs/configuration.md +13 -69
- package/docs/guides/site.md +3 -1
- package/docs/reference/format-details.md +31 -74
- package/docs/reference/note-types.md +3 -2
- package/engine/content-charset.mjs +1 -0
- package/engine/content-scripts.mjs +22 -1
- package/engine/core-report.mjs +10 -2
- package/engine/dictionary-layout.mjs +16 -3
- package/engine/foreign-catalog.mjs +11 -122
- package/engine/language-rows.mjs +3 -3
- package/engine/language-terms.mjs +7 -1
- package/engine/map-lexicon.mjs +37 -5
- package/engine/pdf-build.mjs +9 -178
- package/engine/run-words.mjs +7 -7
- package/engine/site-reach.mjs +23 -0
- package/engine/site-render.mjs +18 -3
- package/package.json +2 -1
- package/types/content-config.d.mts +1 -19
- package/types/engine/dictionary-layout.d.mts +5 -0
- package/types/engine/foreign-catalog.d.mts +0 -38
- package/types/engine/language-rows.d.mts +1 -1
- package/types/engine/map-lexicon.d.mts +3 -1
- package/types/engine/pdf-build.d.mts +5 -27
- package/types/engine/run-words.d.mts +2 -1
- package/types/engine/site-reach.d.mts +11 -0
- package/engine/asset-replacement.mjs +0 -93
- package/types/engine/asset-replacement.d.mts +0 -22
|
@@ -1026,12 +1026,12 @@ ships as the Foundry module `harn-ensemble`) writing `images/map.webp`:
|
|
|
1026
1026
|
And for the same note writing `sohl/assets/icons/noun/shieldnn.svg`, a file the
|
|
1027
1027
|
system ships and this repository does not hold:
|
|
1028
1028
|
|
|
1029
|
-
| Surface | Address
|
|
1030
|
-
| ----------- |
|
|
1031
|
-
| **Foundry** | `systems/sohl/icons/shieldnn.svg`
|
|
1032
|
-
| **Local** | `assets/icons/noun/shieldnn.svg`, in the `sohl` repository
|
|
1033
|
-
| **Web** | `https://cdn.heroiclands.org/sohl/icons/shieldnn.svg`
|
|
1034
|
-
| **Book** | not carried
|
|
1029
|
+
| Surface | Address |
|
|
1030
|
+
| ----------- | ---------------------------------------------------------- |
|
|
1031
|
+
| **Foundry** | `systems/sohl/icons/shieldnn.svg` |
|
|
1032
|
+
| **Local** | `assets/icons/noun/shieldnn.svg`, in the `sohl` repository |
|
|
1033
|
+
| **Web** | `https://cdn.heroiclands.org/sohl/icons/shieldnn.svg` |
|
|
1034
|
+
| **Book** | not carried |
|
|
1035
1035
|
|
|
1036
1036
|
**The content package name and Foundry id are separate values.**
|
|
1037
1037
|
`harnensemble` is what the content is called, what a note writes, and what the
|
|
@@ -1049,7 +1049,7 @@ does not know still resolves on the website and in the book — those need only
|
|
|
1049
1049
|
the name and the suffix — and has no Foundry address, which is refused rather
|
|
1050
1050
|
than guessed.
|
|
1051
1051
|
|
|
1052
|
-
|
|
1052
|
+
A package whose image files belong to another package's notes can declare
|
|
1053
1053
|
`packageBuild.assets[].bindsTo` on the relevant asset copy. Each file's basename
|
|
1054
1054
|
then acts as a shortcode for the declared package and type. The related
|
|
1055
1055
|
`relationships.requires[].contentIndex` supplies the address index; with that
|
|
@@ -1057,29 +1057,6 @@ index enabled, an asset with no matching note is a located build error. See
|
|
|
1057
1057
|
[asset bindings](../configuration.md#packagebuildstagedir-and-packagebuildassets)
|
|
1058
1058
|
for the configuration and cache requirements.
|
|
1059
1059
|
|
|
1060
|
-
**A package declared as a replacement is carried into the book, from its
|
|
1061
|
-
fetched archive, when it answers the address; every other foreign package
|
|
1062
|
-
stays not carried.** A relationship declaring `assetReplacement: true` names a
|
|
1063
|
-
dependency whose asset tree answers this package's own addresses first — see
|
|
1064
|
-
[`relationships`](../configuration.md#relationships) — and the book is the one
|
|
1065
|
-
surface that otherwise refuses a foreign file outright, so it is the one
|
|
1066
|
-
surface this changes. A `thalorna` note embedding
|
|
1067
|
-
`![[thalornaaltart-none-icon-anvil|An anvil]]` names `thalornaaltart`'s own
|
|
1068
|
-
address, exactly as a note citing `sohl`'s shared bestiary art would; when
|
|
1069
|
-
`thalornaaltart` is declared with `assetReplacement: true` and its fetched
|
|
1070
|
-
archive carries `icon-anvil` under its own package, the book stages that file
|
|
1071
|
-
from the archive instead of printing the caption with no picture. A note
|
|
1072
|
-
citing a different package's own address this way — `sohl`'s bestiary art,
|
|
1073
|
-
say — stays refused exactly as the row above states, because `sohl` was never
|
|
1074
|
-
declared as a replacement: a package answers in place of another only when it
|
|
1075
|
-
opted in, never because its archive happens to carry a file at the same type
|
|
1076
|
-
and shortcode.
|
|
1077
|
-
|
|
1078
|
-
A relationship declaring `assetReplacement: true` whose archive was never
|
|
1079
|
-
fetched is a configuration mistake, not an ordinary unresolved address: the
|
|
1080
|
-
book reports a located error naming the relationship and instructing
|
|
1081
|
-
`package-build deps fetch`, rather than printing a caption with no picture.
|
|
1082
|
-
|
|
1083
1060
|
**The host is configuration.** `site.assets` in `package-build.config.yaml` is
|
|
1084
1061
|
the root the web form is joined onto, and a package-owned image on a page with
|
|
1085
1062
|
none set is an error naming that key.
|
|
@@ -1670,16 +1647,16 @@ An **`Address` field** takes the address with no brackets:
|
|
|
1670
1647
|
```yaml
|
|
1671
1648
|
data:
|
|
1672
1649
|
parents:
|
|
1673
|
-
-
|
|
1650
|
+
- zemorhodhis
|
|
1674
1651
|
seat: tashal
|
|
1675
1652
|
```
|
|
1676
1653
|
|
|
1677
|
-
not `[[
|
|
1654
|
+
not `[[zemorhodhis]]`. The field is declared as an `Address`, so the schema knows
|
|
1678
1655
|
what the value is and reads it as one; brackets are a wikilink's punctuation,
|
|
1679
1656
|
which the reader would have to strip before it could do anything.
|
|
1680
1657
|
|
|
1681
1658
|
A frontmatter value is parsed by the address grammar above, so a single-segment
|
|
1682
|
-
value such as `
|
|
1659
|
+
value such as `zemorhodhis` names the target by its shortcode alone, with every
|
|
1683
1660
|
other segment supplied by the position. Frontmatter is structure rather than
|
|
1684
1661
|
prose: a field value is a reference something else will compile against, and it
|
|
1685
1662
|
should say exactly what it points at.
|
|
@@ -2822,37 +2799,6 @@ and keeps the excluded picture's address in its flag or `data-asset`. A
|
|
|
2822
2799
|
package that leaves out a picture a note references and ships no stand-in of
|
|
2823
2800
|
that type fails the build with an error at the referencing note.
|
|
2824
2801
|
|
|
2825
|
-
##### A declared replacement resolves first
|
|
2826
|
-
|
|
2827
|
-
A package may declare, on a `relationships.<kind>[]` entry, that a
|
|
2828
|
-
dependency's asset tree answers this package's own asset addresses ahead of
|
|
2829
|
-
its local record — `assetReplacement: true` in `package-build.config.yaml`.
|
|
2830
|
-
Once declared, an asset address resolves in this order:
|
|
2831
|
-
|
|
2832
|
-
1. **Every declared replacement, in declaration order.** The package segment
|
|
2833
|
-
of each relationship declaring `assetReplacement: true` is tried in the
|
|
2834
|
-
order those relationships are written, and the first one carrying the
|
|
2835
|
-
address wins.
|
|
2836
|
-
2. **Falling through to the package's own record** when no declared
|
|
2837
|
-
replacement carries the address — the resolution this package already
|
|
2838
|
-
performs with no `assetReplacement` declared at all.
|
|
2839
|
-
|
|
2840
|
-
**This reaches asset types only.** A being, an item or a lore note is never
|
|
2841
|
-
replaced this way, whatever a relationship declares: the rule tests the
|
|
2842
|
-
address's type, not the relationship. A replacement answers only `icon`,
|
|
2843
|
-
`image` and `audio` addresses whose package segment is this package's own; an
|
|
2844
|
-
address naming any other type, or naming a different package, is untouched.
|
|
2845
|
-
|
|
2846
|
-
**A replacement's own output address is never itself looked up against
|
|
2847
|
-
another replacement.** Resolving `thalornaaltart-none-image-thorn` never asks
|
|
2848
|
-
whether something replaces `thalornaaltart` — resolution is a single pass over
|
|
2849
|
-
the declared list, not a chain, which forecloses a cycle by construction.
|
|
2850
|
-
|
|
2851
|
-
This is distinct from a `foreign` lookup, which serves a declared dependency's
|
|
2852
|
-
own content for an address this package's local index does not carry. A
|
|
2853
|
-
replacement is consulted **before** the local address; a foreign lookup only
|
|
2854
|
-
**after** it misses locally.
|
|
2855
|
-
|
|
2856
2802
|
#### A font is not an asset
|
|
2857
2803
|
|
|
2858
2804
|
`assets/fonts` is not a root, and a font has no address.
|
|
@@ -3592,7 +3538,7 @@ for the keys.
|
|
|
3592
3538
|
| `joining` | `data.sounds.joining` | `first` and `second` (lists), `written`, `example` |
|
|
3593
3539
|
| `joins` | `data.sounds.joins` | `meeting`, `rule`, `letters` (a list), `where` |
|
|
3594
3540
|
| `marks` | `data.sounds.marks` | `mark`, `letters` (a list), `kind`, `limit`; a spelling guide's rows too, keyed by the guide |
|
|
3595
|
-
| `skeleton` | `data.sounds.skeleton` | `second` (a list, `[any]` for every other), `between`
|
|
3541
|
+
| `skeleton` | `data.sounds.skeleton` | `second` (a list, `[any]` for every other), `third` (the same), `between` |
|
|
3596
3542
|
| `frames` | `data.sounds.frames` | `frame`, `shape`, `class` (a list), `makes`, `examples` (a list of `{ root, form }`) |
|
|
3597
3543
|
| `read_past` | `data.sounds.readPast` | `written`, `readAs`, `what` |
|
|
3598
3544
|
| `sound_changes` | `data.soundChanges` | One row per step: `position` (the set's), `name`, `from_language`, `to_language`, `takes`, `step_position` (the step's in its set), `step`, `change`, `where` |
|
|
@@ -6272,10 +6218,11 @@ SoHL sets `system.quantity` to `1` for each compendium item.
|
|
|
6272
6218
|
| `sounds.marks` | `{ mark, letters, kind, limit }[]` | The marks the language writes on its letters |
|
|
6273
6219
|
| `sounds.marks[].mark` | `string`, required | The mark's name, as `acute` |
|
|
6274
6220
|
| `sounds.marks[].letters` | `string[]`, required | The letters that carry it |
|
|
6275
|
-
| `sounds.marks[].kind` | `stress` or `
|
|
6221
|
+
| `sounds.marks[].kind` | `stress`, `length` or `apart`, required | What it says of its letter |
|
|
6276
6222
|
| `sounds.marks[].limit` | `one` or `any`, required | How many a word may carry |
|
|
6277
|
-
| `sounds.skeleton` | `{ second, between? }[]`
|
|
6223
|
+
| `sounds.skeleton` | `{ second, third?, between? }[]` | The vowel set between a root's second and third consonants |
|
|
6278
6224
|
| `sounds.skeleton[].second` | `string[]` or `any`, required | The second consonants the row holds for; `any` for every other |
|
|
6225
|
+
| `sounds.skeleton[].third` | `string[]` or `any` | The third consonants the row holds for; every third consonant where absent |
|
|
6279
6226
|
| `sounds.skeleton[].between` | `string` | The vowel set between; nothing where absent or empty |
|
|
6280
6227
|
| `sounds.frames` | `{ frame, shape, class, makes, examples? }[]` | The patterns a root is set in |
|
|
6281
6228
|
| `sounds.frames[].frame` | `string`, required | The frame's name, which a word's `frame` names |
|
|
@@ -6690,9 +6637,18 @@ The lint holds each to what it means, each finding located at the key at fault:
|
|
|
6690
6637
|
Where no note states `data.concepts`, every id is unknown, and the message
|
|
6691
6638
|
says so.
|
|
6692
6639
|
|
|
6693
|
-
A language reads the words of its `data.family` chain as its own
|
|
6694
|
-
|
|
6695
|
-
|
|
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.
|
|
6696
6652
|
|
|
6697
6653
|
`package-build core` is the report. A concept no word serves is never a lint
|
|
6698
6654
|
finding; the report lists it so the language can be filled in. See
|
|
@@ -6917,10 +6873,11 @@ closed list where its values are words:
|
|
|
6917
6873
|
- `joins`: what happens where a consonant or a vowel meets another (`C+C`,
|
|
6918
6874
|
`V+C`, `V+V`): `merge`, `insert` the `letters` between, `drop` the first, or
|
|
6919
6875
|
`double` the consonant, everywhere or `where` stated.
|
|
6920
|
-
- `marks`: the marks written on letters, each saying `stress` or `
|
|
6876
|
+
- `marks`: the marks written on letters, each saying `stress`, `length`, or `apart` (the mark sets a vowel to be said apart from its neighbour, as a diaeresis does), and
|
|
6921
6877
|
whether a word carries `one` at most or `any`.
|
|
6922
6878
|
- `skeleton`: the vowel set between a root's second and third consonants, by
|
|
6923
|
-
the second consonant
|
|
6879
|
+
the second consonant and, where a row states `third`, the third; nothing
|
|
6880
|
+
where none is stated.
|
|
6924
6881
|
- `frames`: the patterns a root is set in, each with its shape, the word
|
|
6925
6882
|
classes it makes, what it makes, and the form each root takes in it.
|
|
6926
6883
|
- `mutations`: the sounds a word changes where a named mutation holds — a stop
|
|
@@ -7108,7 +7065,7 @@ data:
|
|
|
7108
7065
|
tongue: [skill-tantsoulng, skill-jurthatlng],
|
|
7109
7066
|
}
|
|
7110
7067
|
marks:
|
|
7111
|
-
- { mark: diaeresis, letters: [ë], kind:
|
|
7068
|
+
- { mark: diaeresis, letters: [ë], kind: apart, limit: any }
|
|
7112
7069
|
```
|
|
7113
7070
|
|
|
7114
7071
|
A guide declares no tags, so a letter row's `tongue` names language notes by
|
|
@@ -8311,7 +8268,7 @@ Content prepared to be played — a situation with its cast, places, and possibl
|
|
|
8311
8268
|
| `sounds.marks` | `{ mark, letters, kind, limit }[]` | The marks written on the letters, each row's keys as a language note's `sounds.marks` |
|
|
8312
8269
|
| `sounds.marks[].mark` | `string`, required | The mark's name, as `acute` |
|
|
8313
8270
|
| `sounds.marks[].letters` | `string[]`, required | The letters that carry it |
|
|
8314
|
-
| `sounds.marks[].kind` | `stress` or `
|
|
8271
|
+
| `sounds.marks[].kind` | `stress`, `length` or `apart`, required | What it says of its letter |
|
|
8315
8272
|
| `sounds.marks[].limit` | `one` or `any`, required | How many a word may carry |
|
|
8316
8273
|
|
|
8317
8274
|
A setting guide introduces one culture's view of the setting, so it names that
|
|
@@ -631,10 +631,11 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
631
631
|
| `data.sounds.marks` | list of `{ mark, letters, kind, limit }` | no | The marks the language writes on its letters. |
|
|
632
632
|
| `data.sounds.marks[].mark` | a string, such as `acute` | yes | The mark's name. |
|
|
633
633
|
| `data.sounds.marks[].letters` | a list of letters | yes | The letters that carry it. |
|
|
634
|
-
| `data.sounds.marks[].kind` | one of stress, length
|
|
634
|
+
| `data.sounds.marks[].kind` | one of stress, length, apart | yes | What the mark says of its letter. |
|
|
635
635
|
| `data.sounds.marks[].limit` | one of one, any | yes | How many a word may carry: `one` at most, or `any`. |
|
|
636
636
|
| `data.sounds.skeleton` | list of `{ second, between? }` | no | The vowel set between a root's second and third consonants. |
|
|
637
637
|
| `data.sounds.skeleton[].second` | a list of sounds, or `any` | yes | The second consonants of a root the row holds for; `any` for every other. |
|
|
638
|
+
| `data.sounds.skeleton[].third` | a list of sounds, or `any` | no | The third consonants of a root the row holds for; every third consonant where absent. |
|
|
638
639
|
| `data.sounds.skeleton[].between` | a vowel, such as `a` | no | The vowel set between the second and third consonants; nothing where absent. |
|
|
639
640
|
| `data.sounds.frames` | list of `{ frame, shape, class, makes, examples? }` | no | The patterns a root is set in. |
|
|
640
641
|
| `data.sounds.frames[].frame` | a string, such as `bare` | yes | The frame's name, which a word's `frame` names. |
|
|
@@ -1026,7 +1027,7 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
|
|
|
1026
1027
|
| `data.sounds.marks` | list of `{ mark, letters, kind, limit }` | no | The marks the language writes on its letters. |
|
|
1027
1028
|
| `data.sounds.marks[].mark` | a string, such as `acute` | yes | The mark's name. |
|
|
1028
1029
|
| `data.sounds.marks[].letters` | a list of letters | yes | The letters that carry it. |
|
|
1029
|
-
| `data.sounds.marks[].kind` | one of stress, length
|
|
1030
|
+
| `data.sounds.marks[].kind` | one of stress, length, apart | yes | What the mark says of its letter. |
|
|
1030
1031
|
| `data.sounds.marks[].limit` | one of one, any | yes | How many a word may carry: `one` at most, or `any`. |
|
|
1031
1032
|
| `data.map.name` | string | yes | What `map=` names the map by: lowercase letters, digits and `-`, unique in the package. |
|
|
1032
1033
|
| `data.map.logographic` | `true` | no | The script writes meanings: a mapped run's words are written by the language's lexicon alone, and the rows write only the text between them. |
|
|
@@ -172,6 +172,7 @@ const SCRIPT_BLOCKS = Object.freeze([
|
|
|
172
172
|
[0xf0000, 0xf2fff], // plane 15: Pirzath, Thuravark, Semaron, the reserved alphabets, Teng Chou
|
|
173
173
|
[0x1e960, 0x1ec6f], // Zethu overflow, Qalezu, Zethu
|
|
174
174
|
[0x1ecc0, 0x1ecff], // Qalezu catch-before forms
|
|
175
|
+
[0x1ed50, 0x1ed7f], // Veldikko
|
|
175
176
|
[0x13430, 0x1345f], // the Egyptian format controls Zethu uses
|
|
176
177
|
]);
|
|
177
178
|
|
|
@@ -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);
|
|
@@ -68,6 +68,19 @@ export const COMMON_MARK = "•";
|
|
|
68
68
|
/** The word-entry span class whose Foundry rendering is a native TOC heading. */
|
|
69
69
|
export const DICTIONARY_ENTRY_CLASS = "dictionary-entry";
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The class on every link an entry writes to another note, so a site can set
|
|
73
|
+
* the link as plain text where its target is not a page a reader can reach.
|
|
74
|
+
*/
|
|
75
|
+
export const LEXICAL_LINK_CLASS = "lexical-ref";
|
|
76
|
+
|
|
77
|
+
/** Wikilinks in a gloss, marked with {@link LEXICAL_LINK_CLASS}. */
|
|
78
|
+
const markLinks = (text) =>
|
|
79
|
+
String(text ?? "").replace(
|
|
80
|
+
/\[\[[^\]]*\]\](?!\{)/g,
|
|
81
|
+
(link) => `${link}{.${LEXICAL_LINK_CLASS}}`,
|
|
82
|
+
);
|
|
83
|
+
|
|
71
84
|
/**
|
|
72
85
|
* Render a dictionary entry's marked headword as an inline native heading in
|
|
73
86
|
* Foundry. Its paragraph becomes a div, so the heading has a valid container.
|
|
@@ -327,7 +340,7 @@ function render(
|
|
|
327
340
|
const text = label(address);
|
|
328
341
|
const ref = parseAddress(address, addressContext ?? {}, { declared: true });
|
|
329
342
|
if (ref.reason || !linkable(ref)) return text;
|
|
330
|
-
return `[[${renderAddress(ref)}|${String(text).replace(/\|/g, "/")}]]`;
|
|
343
|
+
return `[[${renderAddress(ref)}|${String(text).replace(/\|/g, "/")}]]{.${LEXICAL_LINK_CLASS}}`;
|
|
331
344
|
};
|
|
332
345
|
const tag = (language, text) => {
|
|
333
346
|
if (!text) return "";
|
|
@@ -367,7 +380,7 @@ function render(
|
|
|
367
380
|
const { address, anchor } = splitAnchor(row.word);
|
|
368
381
|
const ref = readCanonicalKey(address);
|
|
369
382
|
if (ref && anchor && linkable(ref))
|
|
370
|
-
headword = `**[[${renderAddress(ref)}#${anchor}|${String(row.name).replace(/\|/g, "/")}]]**`;
|
|
383
|
+
headword = `**[[${renderAddress(ref)}#${anchor}|${String(row.name).replace(/\|/g, "/")}]]{.${LEXICAL_LINK_CLASS}}**`;
|
|
371
384
|
}
|
|
372
385
|
parts.push(headword);
|
|
373
386
|
if (row.pronunciation) parts.push(`/${row.pronunciation}/`);
|
|
@@ -401,7 +414,7 @@ function render(
|
|
|
401
414
|
const bodies = Array.isArray(sense?.bodies) ? sense.bodies : [];
|
|
402
415
|
const by =
|
|
403
416
|
bodies.length && !sense.omitBody ? `(_${bodies.map(label).join(", ")}_) ` : "";
|
|
404
|
-
return `${by}${sentence(sense?.text)}`;
|
|
417
|
+
return `${by}${markLinks(sentence(sense?.text))}`;
|
|
405
418
|
};
|
|
406
419
|
if (senses.length === 1) parts.push(senseText(senses[0]));
|
|
407
420
|
else senses.forEach((sense, i) => parts.push(`**${i + 1}** ${senseText(sense)}`));
|
|
@@ -38,12 +38,6 @@
|
|
|
38
38
|
* local pack's output — the resolution logic needs no knowledge of where an
|
|
39
39
|
* item came from.
|
|
40
40
|
*
|
|
41
|
-
* `assetArchive: true` is the sibling flag, for a dependency whose bytes are
|
|
42
|
-
* wanted and whose items are not: an asset-replacement source such as
|
|
43
|
-
* `thalornaaltart` ships no Item packs at all. It unpacks the same release
|
|
44
|
-
* archive without extracting anything from it, so a relationship declaring
|
|
45
|
-
* both flags on one entry is fetched once rather than twice.
|
|
46
|
-
*
|
|
47
41
|
* **The network is never touched by a compile.** Fetching is its own command
|
|
48
42
|
* (`package-build deps fetch`), and a compile whose cache is cold fails saying
|
|
49
43
|
* so. A build that silently downloads is not reproducible, fails strangely
|
|
@@ -78,22 +72,17 @@ import {
|
|
|
78
72
|
const STAMP = ".complete";
|
|
79
73
|
|
|
80
74
|
/**
|
|
81
|
-
* Every declared relationship
|
|
82
|
-
*
|
|
83
|
-
* Shared by {@link itemCatalogRelationships} and
|
|
84
|
-
* {@link assetArchiveRelationships}: both read the same shape out of the same
|
|
85
|
-
* walk, filtered on a different key.
|
|
75
|
+
* Every declared relationship that opted into supplying an item catalogue.
|
|
86
76
|
*
|
|
87
77
|
* @param {object} config - The resolved build configuration.
|
|
88
|
-
* @param {"itemCatalog"|"assetArchive"} flag - Which boolean to filter on.
|
|
89
78
|
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
90
|
-
* The opted-in relationships.
|
|
79
|
+
* The opted-in relationships, in declaration order.
|
|
91
80
|
*/
|
|
92
|
-
function
|
|
81
|
+
export function itemCatalogRelationships(config) {
|
|
93
82
|
const out = [];
|
|
94
83
|
for (const [kind, entries] of Object.entries(config.relationships ?? {})) {
|
|
95
84
|
for (const rel of entries ?? []) {
|
|
96
|
-
if (rel
|
|
85
|
+
if (rel.itemCatalog) {
|
|
97
86
|
out.push({
|
|
98
87
|
id: rel.id,
|
|
99
88
|
manifest: rel.manifest,
|
|
@@ -106,29 +95,6 @@ function flaggedRelationships(config, flag) {
|
|
|
106
95
|
return out;
|
|
107
96
|
}
|
|
108
97
|
|
|
109
|
-
/**
|
|
110
|
-
* Every declared relationship that opted into supplying an item catalogue.
|
|
111
|
-
*
|
|
112
|
-
* @param {object} config - The resolved build configuration.
|
|
113
|
-
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
114
|
-
* The opted-in relationships, in declaration order.
|
|
115
|
-
*/
|
|
116
|
-
export function itemCatalogRelationships(config) {
|
|
117
|
-
return flaggedRelationships(config, "itemCatalog");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Every declared relationship that opted into supplying its release archive
|
|
122
|
-
* for asset bytes alone, with no item catalogue built from it.
|
|
123
|
-
*
|
|
124
|
-
* @param {object} config - The resolved build configuration.
|
|
125
|
-
* @returns {Array<{id: string, manifest: string, kind: string, verified: string|undefined}>}
|
|
126
|
-
* The opted-in relationships, in declaration order.
|
|
127
|
-
*/
|
|
128
|
-
export function assetArchiveRelationships(config) {
|
|
129
|
-
return flaggedRelationships(config, "assetArchive");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
98
|
/**
|
|
133
99
|
* The cache directory for one dependency at one version.
|
|
134
100
|
*
|
|
@@ -401,21 +367,14 @@ export function pinnedManifestUrl(url, verified) {
|
|
|
401
367
|
}
|
|
402
368
|
|
|
403
369
|
/**
|
|
404
|
-
* Resolve a dependency's manifest, pinned to `compatibility.verified
|
|
405
|
-
* both fetch paths require.
|
|
406
|
-
*
|
|
407
|
-
* Shared by {@link fetchCatalog} and {@link fetchAssetArchive}: both read the
|
|
408
|
-
* same manifest before deciding what to unpack from it.
|
|
370
|
+
* Resolve a dependency's manifest, pinned to `compatibility.verified`.
|
|
409
371
|
*
|
|
410
372
|
* @param {{id: string, manifest: string, verified?: string}} rel - The
|
|
411
373
|
* declared relationship.
|
|
412
|
-
* @param {string} subject - What floats when no version is verified —
|
|
413
|
-
* "catalogue" or "archive" — named in the warning a reader would otherwise
|
|
414
|
-
* get no word for.
|
|
415
374
|
* @returns {Promise<{manifest: object, version: string}>} The manifest and
|
|
416
375
|
* its resolved version.
|
|
417
376
|
*/
|
|
418
|
-
async function resolveManifest(rel
|
|
377
|
+
async function resolveManifest(rel) {
|
|
419
378
|
const { url, pinned } = pinnedManifestUrl(rel.manifest, rel.verified);
|
|
420
379
|
const manifest = await fetchManifest(url);
|
|
421
380
|
const version = manifest.version;
|
|
@@ -432,7 +391,7 @@ async function resolveManifest(rel, subject) {
|
|
|
432
391
|
}
|
|
433
392
|
if (!rel.verified) {
|
|
434
393
|
log.warn(
|
|
435
|
-
`${rel.id}: no \`compatibility.verified\`, so its
|
|
394
|
+
`${rel.id}: no \`compatibility.verified\`, so its catalogue floats ` +
|
|
436
395
|
`with whatever ${url} currently serves`,
|
|
437
396
|
);
|
|
438
397
|
}
|
|
@@ -449,7 +408,7 @@ async function resolveManifest(rel, subject) {
|
|
|
449
408
|
* @returns {Promise<string>} The dependency's cache directory.
|
|
450
409
|
*/
|
|
451
410
|
export async function fetchCatalog(config, rel) {
|
|
452
|
-
const { manifest, version } = await resolveManifest(rel
|
|
411
|
+
const { manifest, version } = await resolveManifest(rel);
|
|
453
412
|
const dir = catalogDir(config, rel.id, version);
|
|
454
413
|
if (isComplete(dir)) {
|
|
455
414
|
log.info(`${rel.id}@${version}: already cached`);
|
|
@@ -475,61 +434,6 @@ export async function fetchCatalog(config, rel) {
|
|
|
475
434
|
return dir;
|
|
476
435
|
}
|
|
477
436
|
|
|
478
|
-
/**
|
|
479
|
-
* Whether an asset-archive-only cache is present and complete.
|
|
480
|
-
*
|
|
481
|
-
* No item catalogue is built on this path, so completeness never asks for
|
|
482
|
-
* `item-packs.json` — an asset-only dependency, such as `thalornaaltart`,
|
|
483
|
-
* ships none.
|
|
484
|
-
*
|
|
485
|
-
* @param {string} dir - The dependency's cache directory.
|
|
486
|
-
* @returns {boolean} True when the archive was fetched to completion.
|
|
487
|
-
*/
|
|
488
|
-
const isArchiveComplete = (dir) => fs.existsSync(path.join(dir, STAMP));
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Fetch one dependency's release archive for its asset bytes alone, building
|
|
492
|
-
* no item catalogue from it.
|
|
493
|
-
*
|
|
494
|
-
* Idempotent: a complete cache for the resolved version is left alone. Where a
|
|
495
|
-
* relationship also declares `itemCatalog: true`, {@link fetchCatalog} already
|
|
496
|
-
* unpacked this same archive into this same cache directory, and
|
|
497
|
-
* {@link fetchAllCatalogs} does not call this function for it — the two never
|
|
498
|
-
* download the same bytes twice.
|
|
499
|
-
*
|
|
500
|
-
* @param {object} config - The resolved build configuration.
|
|
501
|
-
* @param {{id: string, manifest: string, verified?: string}} rel - The
|
|
502
|
-
* declared relationship.
|
|
503
|
-
* @returns {Promise<string>} The dependency's cache directory.
|
|
504
|
-
*/
|
|
505
|
-
export async function fetchAssetArchive(config, rel) {
|
|
506
|
-
const { manifest, version } = await resolveManifest(rel, "archive");
|
|
507
|
-
const dir = catalogDir(config, rel.id, version);
|
|
508
|
-
if (isArchiveComplete(dir)) {
|
|
509
|
-
log.info(`${rel.id}@${version}: already cached`);
|
|
510
|
-
return dir;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const download = manifest.download;
|
|
514
|
-
if (!download) {
|
|
515
|
-
throw new Error(`${rel.id}@${version}: its manifest declares no \`download\``);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Rebuild from empty: a previous run may have died partway, and a stale
|
|
519
|
-
// half-tree is worse than no tree.
|
|
520
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
521
|
-
const raw = path.join(dir, "package");
|
|
522
|
-
fs.mkdirSync(raw, { recursive: true });
|
|
523
|
-
|
|
524
|
-
log.info(`${rel.id}@${version}: downloading ${download}`);
|
|
525
|
-
await downloadAndUnzip(download, raw);
|
|
526
|
-
|
|
527
|
-
cacheSchemaArtifact(raw, dir);
|
|
528
|
-
// Last, so a fetch that died partway is never mistaken for a complete one.
|
|
529
|
-
fs.writeFileSync(path.join(dir, STAMP), `${version}\n`);
|
|
530
|
-
return dir;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
437
|
/**
|
|
534
438
|
* Locate a declared pack inside an unpacked archive.
|
|
535
439
|
*
|
|
@@ -811,33 +715,18 @@ export async function fetchAllMetadata(config) {
|
|
|
811
715
|
/**
|
|
812
716
|
* Fetch every opted-in dependency. The `deps fetch` command.
|
|
813
717
|
*
|
|
814
|
-
* A relationship declaring both `itemCatalog: true` and `assetArchive: true`
|
|
815
|
-
* is fetched once: {@link fetchCatalog} already unpacks the archive on its
|
|
816
|
-
* way to extracting the item catalogue, so this skips the archive-only fetch
|
|
817
|
-
* for any id {@link fetchCatalog} already handled.
|
|
818
718
|
*
|
|
819
719
|
* @param {object} config - The resolved build configuration.
|
|
820
720
|
* @returns {Promise<number>} How many dependencies were fetched.
|
|
821
721
|
*/
|
|
822
722
|
export async function fetchAllCatalogs(config) {
|
|
823
723
|
const itemRels = itemCatalogRelationships(config);
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
log.info(
|
|
827
|
-
"No relationship declares `itemCatalog: true` or `assetArchive: true`; " +
|
|
828
|
-
"nothing to fetch.",
|
|
829
|
-
);
|
|
724
|
+
if (!itemRels.length) {
|
|
725
|
+
log.info("No relationship declares `itemCatalog: true`; nothing to fetch.");
|
|
830
726
|
return 0;
|
|
831
727
|
}
|
|
832
728
|
for (const rel of itemRels) await fetchCatalog(config, rel);
|
|
833
|
-
|
|
834
|
-
let archiveFetched = 0;
|
|
835
|
-
for (const rel of archiveRels) {
|
|
836
|
-
if (alreadyFetched.has(rel.id)) continue;
|
|
837
|
-
await fetchAssetArchive(config, rel);
|
|
838
|
-
archiveFetched++;
|
|
839
|
-
}
|
|
840
|
-
return itemRels.length + archiveFetched;
|
|
729
|
+
return itemRels.length;
|
|
841
730
|
}
|
|
842
731
|
|
|
843
732
|
/**
|
package/engine/language-rows.mjs
CHANGED
|
@@ -253,7 +253,7 @@ export const LANGUAGE_COLUMN_TYPES = Object.freeze({
|
|
|
253
253
|
joining: { position: "BIGINT", first: LIST, second: LIST },
|
|
254
254
|
joins: { position: "BIGINT", letters: LIST },
|
|
255
255
|
marks: { position: "BIGINT", letters: LIST },
|
|
256
|
-
skeleton: { position: "BIGINT", second: LIST },
|
|
256
|
+
skeleton: { position: "BIGINT", second: LIST, third: LIST },
|
|
257
257
|
frames: {
|
|
258
258
|
position: "BIGINT",
|
|
259
259
|
class: LIST,
|
|
@@ -911,14 +911,14 @@ export function markRows(records) {
|
|
|
911
911
|
}
|
|
912
912
|
|
|
913
913
|
/**
|
|
914
|
-
* The `skeleton` rows: one per `data.sounds.skeleton` entry, `second`
|
|
914
|
+
* The `skeleton` rows: one per `data.sounds.skeleton` entry, `second` and `third` lists.
|
|
915
915
|
*
|
|
916
916
|
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
917
917
|
* @returns {object[]}
|
|
918
918
|
*/
|
|
919
919
|
export function skeletonRows(records) {
|
|
920
920
|
return listRows(records, (data) => soundsOf(data).skeleton, SKELETON_KEYS, {
|
|
921
|
-
lists: ["second"],
|
|
921
|
+
lists: ["second", "third"],
|
|
922
922
|
});
|
|
923
923
|
}
|
|
924
924
|
|
|
@@ -152,7 +152,7 @@ export const JOIN_MEETINGS = Object.freeze(["C+C", "V+C", "V+V"]);
|
|
|
152
152
|
export const JOIN_RULES = Object.freeze(["merge", "insert", "drop", "double"]);
|
|
153
153
|
|
|
154
154
|
/** What a written mark says of its letter. */
|
|
155
|
-
export const MARK_KINDS = Object.freeze(["stress", "length"]);
|
|
155
|
+
export const MARK_KINDS = Object.freeze(["stress", "length", "apart"]);
|
|
156
156
|
|
|
157
157
|
/** How many of a mark a word may carry: `one` at most, or `any` number. */
|
|
158
158
|
export const MARK_LIMITS = Object.freeze(["one", "any"]);
|
|
@@ -831,6 +831,12 @@ export const SKELETON_FIELDS = Object.freeze([
|
|
|
831
831
|
required: true,
|
|
832
832
|
describe: "The second consonants of a root the row holds for; `any` for every other.",
|
|
833
833
|
}),
|
|
834
|
+
Object.freeze({
|
|
835
|
+
name: "third",
|
|
836
|
+
...SOUNDS_OR_ANY,
|
|
837
|
+
describe:
|
|
838
|
+
"The third consonants of a root the row holds for; every third consonant where absent.",
|
|
839
|
+
}),
|
|
834
840
|
Object.freeze({
|
|
835
841
|
name: "between",
|
|
836
842
|
kind: "string",
|