@heroiclands/package-build 25.1.0 → 25.1.1
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/docs/api.md +7 -0
- package/docs/encoding-core.md +65 -7
- package/docs/guides/writing-in-invented-scripts.md +7 -0
- package/docs/reference/format-details.md +66 -63
- package/docs/reference/note-types.md +54 -51
- package/engine/encoding-lexicon.mjs +4 -2
- package/engine/encoding-template.mjs +99 -42
- package/engine/encoding-terms.mjs +42 -5
- package/engine/encoding.mjs +28 -22
- package/package.json +1 -1
- package/types/engine/encoding-template.d.mts +1 -0
- package/types/engine/encoding-terms.d.mts +42 -4
- package/types/engine/encoding.d.mts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 25.1.1
|
|
4
|
+
|
|
5
|
+
**Language conversion**
|
|
6
|
+
|
|
7
|
+
- Scripts can opt into conventional spellings for unwritten vowels and shared signs without hidden recovery marks; exact conversion remains the default.
|
|
8
|
+
- Signs can vary before consonants or at word and line boundaries, and explicit word dividers can become spaces.
|
|
9
|
+
- Accent settings can explicitly allow or forbid diacritics.
|
|
10
|
+
|
|
11
|
+
**Spelling checks**
|
|
12
|
+
|
|
13
|
+
- English translations in name registers no longer trigger spelling errors for the invented language.
|
|
14
|
+
|
|
3
15
|
## 25.1.0
|
|
4
16
|
|
|
5
17
|
**Writing and conversion**
|
package/docs/api.md
CHANGED
|
@@ -2799,6 +2799,13 @@ expose their schema `key`.
|
|
|
2799
2799
|
| `encodeText` | `encodeText(encoding, text, {font, verse})` | string | encoding exactly by rows and inline codes |
|
|
2800
2800
|
| `decodeText` | `decodeText(encoding, text, {font})` | string | decoding by longest output matches |
|
|
2801
2801
|
|
|
2802
|
+
Encoding rules default to exact, uniquely decodable outputs. A note may state
|
|
2803
|
+
`rules.roundTrip: canonical` to permit aliases and empty code sequences;
|
|
2804
|
+
`decodeText` then returns the authored conventional reading without restoring
|
|
2805
|
+
erased Roman characters. Contextual rules accept `before: vowel|consonant`
|
|
2806
|
+
and `at: start|end|line-start`; supplied conditions combine with AND. Accent
|
|
2807
|
+
permission blocks accept explicit Boolean values. See [Encoding text](encoding-core.md).
|
|
2808
|
+
|
|
2802
2809
|
### `engine.encodingLanguage`
|
|
2803
2810
|
|
|
2804
2811
|
Pure language-aware conversion takes a compiled encoding and optional plain
|
package/docs/encoding-core.md
CHANGED
|
@@ -48,18 +48,75 @@ second behaviour.
|
|
|
48
48
|
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
49
|
| `normalization` | Required `NFC`; input is normalized before matching, with original source offsets retained. |
|
|
50
50
|
| `matching` | Required `longest-first`. |
|
|
51
|
+
| `roundTrip` | Optional `exact` (default) or `canonical`; canonical permits authored aliases and empty code sequences. |
|
|
52
|
+
| `word-separators` | Optional list of source word dividers that emit shared word spaces after contextual alternatives. |
|
|
51
53
|
| `case` | `{single-letter: preserve, consonant-group: lowercase-or-initial-uppercase, other-group-cases: forbidden}`; single letters retain authored case, and groups use lowercase or an initial capital. |
|
|
52
54
|
| `stress` | `{modifier, position: after-stressed-vowel, printing: false, effect: stress-only, changes-sound: false, changes-length: false, vowel-group-position: first-vowel}`; `modifier` names a font code and acute mappings append it to the unchanged vowel. |
|
|
53
55
|
| `vowel-groups` | `{reading: established, allow-splitting: false, unrecognized-adjacent-vowels: separate, groups: [...]}`; the explicit group list determines where an acute belongs on its first vowel. |
|
|
54
|
-
| `diaeresis` | `{allowed:
|
|
55
|
-
| `grave-accent` | `{allowed:
|
|
56
|
-
| `contextual-mappings` | A list of `{match, before
|
|
56
|
+
| `diaeresis` | `{allowed: boolean}` permits or forbids explicitly mapped diaeresis characters. |
|
|
57
|
+
| `grave-accent` | `{allowed: boolean}` permits or forbids explicitly mapped grave-accent characters. |
|
|
58
|
+
| `contextual-mappings` | A list of `{match, emit, before?, at?}`; following-letter or source-position conditions precede unconditional mappings. |
|
|
57
59
|
| `y` | `{kind: consonant, sound: "y in yes", vowel-reading: false}`; y cannot also appear in `data.vowels`. |
|
|
58
60
|
|
|
61
|
+
The accent blocks accept an explicit Boolean `allowed`. False forbids that
|
|
62
|
+
accent; true permits it only where the character is accepted and mapped.
|
|
63
|
+
Omitting the block also permits such explicit mappings. Neither choice invents
|
|
64
|
+
an accent pronunciation or adds a character to the inventory.
|
|
65
|
+
|
|
66
|
+
`rules.roundTrip` is optional and defaults to `exact`. Exact mappings must be
|
|
67
|
+
uniquely decodable across adjacent mappings and spaces; ambiguous outputs and
|
|
68
|
+
empty code sequences are refused. This remains the default for Aelwythi.
|
|
69
|
+
|
|
70
|
+
An encoding whose orthography omits distinctions may explicitly state
|
|
71
|
+
`roundTrip: canonical`. Its mappings and conditional emissions may use `[]`
|
|
72
|
+
for an inherent vowel or deleted boundary marker, and different Roman sources
|
|
73
|
+
may name the same output. Decoding returns a conventional spelling. For an output it prefers the first
|
|
74
|
+
authored alias whose following-letter condition matches the decoded suffix,
|
|
75
|
+
with conditional rows before general mappings and longest complete native
|
|
76
|
+
matches first. When none applies, the first authored reading is retained:
|
|
77
|
+
an inherent vowel or deleted marker may have hidden the original condition. It does not invent characters for
|
|
78
|
+
empty outputs or store invisible metadata to restore the original spelling.
|
|
79
|
+
A language's authoritative vocabulary can recover fuller word spellings.
|
|
80
|
+
Shared word spaces remain spaces in both modes.
|
|
81
|
+
|
|
82
|
+
Contextual entries use `{match, emit, before?, at?}` and require at least one
|
|
83
|
+
condition. `before` is `vowel` or `consonant`; `at` is `start`, `end` or
|
|
84
|
+
`line-start`. `start` and `end` refer to word boundaries. A consonant is a
|
|
85
|
+
letter outside the declared `data.vowels`. Line start follows a newline or
|
|
86
|
+
verse slash and permits preceding indentation carets. Multiple conditions
|
|
87
|
+
combine with AND. Entries for the same source may use different conditions;
|
|
88
|
+
repeating the same source and condition tuple is an error. Equal-length
|
|
89
|
+
contextual matches retain authored order.
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
rules:
|
|
93
|
+
normalization: NFC
|
|
94
|
+
matching: longest-first
|
|
95
|
+
roundTrip: canonical
|
|
96
|
+
contextual-mappings:
|
|
97
|
+
- { match: e, before: consonant, emit: [] }
|
|
98
|
+
- { match: e, at: end, emit: [] }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Conditions govern forward matching. Inverse following-letter choices use the
|
|
102
|
+
visible decoded suffix; canonical mode keeps its authored fallback when that
|
|
103
|
+
suffix has lost an inherent vowel or marker. Position conditions govern only
|
|
104
|
+
forward matching. No mode invents deleted markers. Exact decoding retains
|
|
105
|
+
strict following-letter checks.
|
|
106
|
+
|
|
107
|
+
`rules.word-separators` lists source word dividers, such as `['-']`, that emit
|
|
108
|
+
the shared ASCII word space. Contextual rules take priority, so a canonical
|
|
109
|
+
note can emit `[]` for an edge hyphen and a word space for an internal hyphen.
|
|
110
|
+
These sources cannot also be ordinary mappings, replace shared punctuation,
|
|
111
|
+
or contain whitespace, literal markers or structural controls. Separator
|
|
112
|
+
sources participate in inferred `accepts`; no font SPACE code is invented.
|
|
113
|
+
Since a divider and an ordinary space share output, such a note selects
|
|
114
|
+
canonical round trips. Decoding keeps the shared space as a space.
|
|
115
|
+
|
|
59
116
|
A contextual match cannot contain whitespace, literal markers or forbidden
|
|
60
|
-
accents.
|
|
61
|
-
|
|
62
|
-
|
|
117
|
+
accents. Both modes enforce the same accepted characters, normalization,
|
|
118
|
+
case, stress, accent and literal-control rules. The compiler does not infer
|
|
119
|
+
linguistic vowel groups or silently add missing letter mappings.
|
|
63
120
|
|
|
64
121
|
Lint checks every authored word, variant and registered name for a language
|
|
65
122
|
with an encoding document, including unused entries. It also checks actual
|
|
@@ -110,7 +167,8 @@ fixtures, not an authored input format.
|
|
|
110
167
|
`EncodingError` and returns no partial text. `offset` is a zero-based UTF-16
|
|
111
168
|
offset into the input, suitable for composing with a Markdown offset map.
|
|
112
169
|
`line` and `column` are one-based Unicode code-point positions; schema errors
|
|
113
|
-
expose their `key`.
|
|
170
|
+
expose their `key`. Exact decoding resolves the complete unambiguous output sequence. Canonical
|
|
171
|
+
decoding selects the authored conventional reading.
|
|
114
172
|
|
|
115
173
|
`encodePieces` uses the same rules and returns `{from, to, start, end, row}`
|
|
116
174
|
records. `start` and exclusive `end` delimit original UTF-16 input, including
|
|
@@ -40,6 +40,13 @@ Language identification alone retains Roman text. `enc=` selects an encoding
|
|
|
40
40
|
document, which infers its language and font. Unknown source characters are
|
|
41
41
|
errors; a face drawing a character does not make it a spelling mapping.
|
|
42
42
|
|
|
43
|
+
An encoding uses exact round trips by default. If its script omits Roman
|
|
44
|
+
distinctions, its note can explicitly select `rules.roundTrip: canonical`.
|
|
45
|
+
Decoding then gives an authored conventional spelling; authoritative lexicon
|
|
46
|
+
entries can recover fuller word forms. The encoder adds no invisible metadata
|
|
47
|
+
to restore erased spelling. See the [canonical encoding contract](../encoding-core.md#executable-rules)
|
|
48
|
+
for aliases, inherent vowels and shared word separators.
|
|
49
|
+
|
|
43
50
|
## Language and native presentation
|
|
44
51
|
|
|
45
52
|
`enc=` names an encoding document, with `doc` as its default Address type:
|
|
@@ -8084,69 +8084,72 @@ Content prepared to be played — a situation with its cast, places, and possibl
|
|
|
8084
8084
|
- settingguide: A culture's guide to the setting, which names its culture in `data.culture`. It orients a reader to a setting or region, bringing its places, peoples, institutions, and daily life into one usable frame with links to detailed notes.
|
|
8085
8085
|
- post: A blog post, which states the day it was published in `data.date` and may name its writer in `data.author`. A site whose `site.feed` names a section lists its posts there, newest first, and feeds them as Atom.
|
|
8086
8086
|
|
|
8087
|
-
| `data` property | Values
|
|
8088
|
-
| ------------------------------------------------- |
|
|
8089
|
-
| `language` | a configured language alias
|
|
8090
|
-
| `font` | an installed font slug
|
|
8091
|
-
| `accepts` | a list of characters or ranges
|
|
8092
|
-
| `vowels` | a string
|
|
8093
|
-
| `mappings` | Roman strings to glyph codes
|
|
8094
|
-
| `rules` | the closed encoding rules vocabulary
|
|
8095
|
-
| `rules.normalization` | "NFC"
|
|
8096
|
-
| `rules.matching` | "longest-first"
|
|
8097
|
-
| `rules.
|
|
8098
|
-
| `rules.
|
|
8099
|
-
| `rules.case
|
|
8100
|
-
| `rules.case.
|
|
8101
|
-
| `rules.
|
|
8102
|
-
| `rules.
|
|
8103
|
-
| `rules.stress
|
|
8104
|
-
| `rules.stress.
|
|
8105
|
-
| `rules.stress.
|
|
8106
|
-
| `rules.stress.
|
|
8107
|
-
| `rules.stress.
|
|
8108
|
-
| `rules.stress.
|
|
8109
|
-
| `rules.
|
|
8110
|
-
| `rules.vowel-
|
|
8111
|
-
| `rules.vowel-groups
|
|
8112
|
-
| `rules.vowel-groups.
|
|
8113
|
-
| `rules.vowel-groups.
|
|
8114
|
-
| `rules.
|
|
8115
|
-
| `rules.
|
|
8116
|
-
| `rules.
|
|
8117
|
-
| `rules.
|
|
8118
|
-
| `rules.
|
|
8119
|
-
| `rules.
|
|
8120
|
-
| `rules.contextual-mappings
|
|
8121
|
-
| `rules.contextual-mappings[].
|
|
8122
|
-
| `rules.
|
|
8123
|
-
| `rules.
|
|
8124
|
-
| `rules.
|
|
8125
|
-
| `rules.y
|
|
8126
|
-
| `
|
|
8127
|
-
| `
|
|
8128
|
-
| `
|
|
8129
|
-
| `
|
|
8130
|
-
| `concepts
|
|
8131
|
-
| `
|
|
8132
|
-
| `
|
|
8133
|
-
| `
|
|
8134
|
-
| `
|
|
8135
|
-
| `
|
|
8136
|
-
| `metres
|
|
8137
|
-
| `
|
|
8138
|
-
| `
|
|
8139
|
-
| `
|
|
8140
|
-
| `sounds
|
|
8141
|
-
| `sounds.letters
|
|
8142
|
-
| `sounds.letters[].
|
|
8143
|
-
| `sounds.letters[].
|
|
8144
|
-
| `sounds.letters[].
|
|
8145
|
-
| `sounds.
|
|
8146
|
-
| `sounds.
|
|
8147
|
-
| `sounds.
|
|
8148
|
-
| `sounds.marks
|
|
8149
|
-
| `sounds.marks[].
|
|
8087
|
+
| `data` property | Values | Description |
|
|
8088
|
+
| ------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
8089
|
+
| `language` | a configured language alias | The configured language alias an encoding document writes. |
|
|
8090
|
+
| `font` | an installed font slug | The encoding's installed font, which supplies all glyph codes. |
|
|
8091
|
+
| `accepts` | a list of characters or ranges | Accepted Roman letters and additions to standard punctuation. |
|
|
8092
|
+
| `vowels` | a string | The letters a contextual vowel match reads as vowels. |
|
|
8093
|
+
| `mappings` | Roman strings to glyph codes | Roman source strings mapped to codes published by the selected font. |
|
|
8094
|
+
| `rules` | the closed encoding rules vocabulary | Executable normalization, matching, case, stress, vowel groups, accents and contextual mappings. |
|
|
8095
|
+
| `rules.normalization` | "NFC" | Required value: "NFC". |
|
|
8096
|
+
| `rules.matching` | "longest-first" | Required value: "longest-first". |
|
|
8097
|
+
| `rules.roundTrip` | a string | Exact by default; canonical permits aliases and empty code sequences and decodes an authored conventional spelling. |
|
|
8098
|
+
| `rules.word-separators` | a list of nonempty NFC source sequences | Source word dividers written as shared spaces, after contextual alternatives; cannot also be ordinary mappings or replace shared punctuation. |
|
|
8099
|
+
| `rules.case` | a map | Case handling for letters and consonant groups. |
|
|
8100
|
+
| `rules.case.single-letter` | "preserve" | Required value: "preserve". |
|
|
8101
|
+
| `rules.case.consonant-group` | "lowercase-or-initial-uppercase" | Required value: "lowercase-or-initial-uppercase". |
|
|
8102
|
+
| `rules.case.other-group-cases` | "forbidden" | Required value: "forbidden". |
|
|
8103
|
+
| `rules.stress` | a map | The font's nonprinting stress modifier after an acute vowel. |
|
|
8104
|
+
| `rules.stress.modifier` | a string | The font code for U+E0114. |
|
|
8105
|
+
| `rules.stress.position` | "after-stressed-vowel" | Required value: "after-stressed-vowel". |
|
|
8106
|
+
| `rules.stress.printing` | false | Required value: false. |
|
|
8107
|
+
| `rules.stress.effect` | "stress-only" | Required value: "stress-only". |
|
|
8108
|
+
| `rules.stress.changes-sound` | false | Required value: false. |
|
|
8109
|
+
| `rules.stress.changes-length` | false | Required value: false. |
|
|
8110
|
+
| `rules.stress.vowel-group-position` | "first-vowel" | Required value: "first-vowel". |
|
|
8111
|
+
| `rules.vowel-groups` | a map | Explicit recognized vowel groups and their acute placement. |
|
|
8112
|
+
| `rules.vowel-groups.reading` | "established" | Required value: "established". |
|
|
8113
|
+
| `rules.vowel-groups.allow-splitting` | false | Required value: false. |
|
|
8114
|
+
| `rules.vowel-groups.unrecognized-adjacent-vowels` | "separate" | Required value: "separate". |
|
|
8115
|
+
| `rules.vowel-groups.groups` | a list of vowel groups | The complete authored group inventory. |
|
|
8116
|
+
| `rules.diaeresis` | a map | Whether diaeresis mappings are allowed. |
|
|
8117
|
+
| `rules.diaeresis.allowed` | true or false | False forbids the accent; true permits explicitly mapped and accepted characters. |
|
|
8118
|
+
| `rules.grave-accent` | a map | Whether grave-accent mappings are allowed. |
|
|
8119
|
+
| `rules.grave-accent.allowed` | true or false | False forbids the accent; true permits explicitly mapped and accepted characters. |
|
|
8120
|
+
| `rules.contextual-mappings` | a list of conditional mappings | Alternatives conditioned on the following letter or source position, applied before unconditional mappings. |
|
|
8121
|
+
| `rules.contextual-mappings[].match` | a string | The source sequence. |
|
|
8122
|
+
| `rules.contextual-mappings[].before` | a string | Match before a declared vowel or another letter. |
|
|
8123
|
+
| `rules.contextual-mappings[].at` | a string | Match at word start, word end or line start. |
|
|
8124
|
+
| `rules.contextual-mappings[].emit` | a glyph code or list of codes (empty only with canonical round trips) | The font codes written by this alternative. |
|
|
8125
|
+
| `rules.y` | a map | The consonant reading of y. |
|
|
8126
|
+
| `rules.y.kind` | "consonant" | Required value: "consonant". |
|
|
8127
|
+
| `rules.y.sound` | "y in yes" | Required value: "y in yes". |
|
|
8128
|
+
| `rules.y.vowel-reading` | false | Required value: false. |
|
|
8129
|
+
| `culture` | `Address` | `settingguide` only, and required there: the culture the guide introduces, naming a lore note with `subType: culture` |
|
|
8130
|
+
| `concepts` | list of `{ id, gloss, group }` | A core vocabulary's concept list, any subType. See [the core vocabulary](#the-core-vocabulary) |
|
|
8131
|
+
| `date` | `YYYY-MM-DD` | `post` only, and required there: the day the post was published |
|
|
8132
|
+
| `author` | string | `post` only: who wrote the post; a post naming none carries the site's byline, the first entry of `authors` |
|
|
8133
|
+
| `concepts[].id` | string | Required: lowercase letters, digits and `-`, unique in the package |
|
|
8134
|
+
| `concepts[].gloss` | string | Required: what the concept means, in brief |
|
|
8135
|
+
| `concepts[].group` | string | Required: the group the report lists the concept under |
|
|
8136
|
+
| `metres` | `{ name, beats, kind }[]` | The metres a verse fence's `scan=` names; any subType. See [metres](#metres) |
|
|
8137
|
+
| `metres[].name` | string | Required: what `scan=` names it by, lowercase letters, digits and `-`, unique in the package |
|
|
8138
|
+
| `metres[].beats` | `integer`, required | The beats of one line, above zero |
|
|
8139
|
+
| `metres[].kind` | `accentual` | Required: how the beats are counted |
|
|
8140
|
+
| `sounds` | `{ letters?, marks? }` | A spelling guide's letters and marks, for every tongue of the setting; any subType. See [a spelling guide's sounds](#a-spelling-guides-sounds) |
|
|
8141
|
+
| `sounds.letters` | `{ letters, kind, like?, place?, ipa?, tongue? }[]` | The letters, each row's keys as a language note's `sounds.letters` |
|
|
8142
|
+
| `sounds.letters[].letters` | `string[]`, required | The letters or digraphs the row describes |
|
|
8143
|
+
| `sounds.letters[].kind` | `string`, required | What kind of sound they write, as `consonant` or `vowel pair` |
|
|
8144
|
+
| `sounds.letters[].like` | `string` | What they sound like |
|
|
8145
|
+
| `sounds.letters[].place` | `string` | Where in the mouth they are made |
|
|
8146
|
+
| `sounds.letters[].ipa` | `string` | The sounds they write, in the International Phonetic Alphabet |
|
|
8147
|
+
| `sounds.letters[].tongue` | an Address, or a list of them | The language notes that write the letters; every tongue where absent |
|
|
8148
|
+
| `sounds.marks` | `{ mark, letters, kind, limit }[]` | The marks written on the letters, each row's keys as a language note's `sounds.marks` |
|
|
8149
|
+
| `sounds.marks[].mark` | `string`, required | The mark's name, as `acute` |
|
|
8150
|
+
| `sounds.marks[].letters` | `string[]`, required | The letters that carry it |
|
|
8151
|
+
| `sounds.marks[].kind` | `stress`, `length` or `apart`, required | What it says of its letter |
|
|
8152
|
+
| `sounds.marks[].limit` | `one` or `any`, required | How many a word may carry |
|
|
8150
8153
|
|
|
8151
8154
|
A setting guide introduces one culture's view of the setting, so it names that
|
|
8152
8155
|
culture, and a `doc` of any other `subType` has no culture of its own to name.
|
|
@@ -1014,57 +1014,60 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
|
|
|
1014
1014
|
|
|
1015
1015
|
**Inner keys** — a key a field's value does not declare is an error at its own line and column.
|
|
1016
1016
|
|
|
1017
|
-
| Key | Shape
|
|
1018
|
-
| ------------------------------------------------------ |
|
|
1019
|
-
| `data.rules.normalization` | "NFC"
|
|
1020
|
-
| `data.rules.matching` | "longest-first"
|
|
1021
|
-
| `data.rules.
|
|
1022
|
-
| `data.rules.
|
|
1023
|
-
| `data.rules.case
|
|
1024
|
-
| `data.rules.case.
|
|
1025
|
-
| `data.rules.
|
|
1026
|
-
| `data.rules.
|
|
1027
|
-
| `data.rules.stress
|
|
1028
|
-
| `data.rules.stress.
|
|
1029
|
-
| `data.rules.stress.
|
|
1030
|
-
| `data.rules.stress.
|
|
1031
|
-
| `data.rules.stress.
|
|
1032
|
-
| `data.rules.stress.
|
|
1033
|
-
| `data.rules.
|
|
1034
|
-
| `data.rules.vowel-
|
|
1035
|
-
| `data.rules.vowel-groups
|
|
1036
|
-
| `data.rules.vowel-groups.
|
|
1037
|
-
| `data.rules.vowel-groups.
|
|
1038
|
-
| `data.rules.
|
|
1039
|
-
| `data.rules.
|
|
1040
|
-
| `data.rules.
|
|
1041
|
-
| `data.rules.
|
|
1042
|
-
| `data.rules.
|
|
1043
|
-
| `data.rules.
|
|
1044
|
-
| `data.rules.contextual-mappings
|
|
1045
|
-
| `data.rules.contextual-mappings[].
|
|
1046
|
-
| `data.rules.
|
|
1047
|
-
| `data.rules.
|
|
1048
|
-
| `data.rules.
|
|
1049
|
-
| `data.rules.y
|
|
1050
|
-
| `data.
|
|
1051
|
-
| `data.
|
|
1052
|
-
| `data.
|
|
1053
|
-
| `data.
|
|
1054
|
-
| `data.
|
|
1055
|
-
| `data.
|
|
1056
|
-
| `data.sounds.letters
|
|
1057
|
-
| `data.sounds.letters[].
|
|
1058
|
-
| `data.sounds.letters[].
|
|
1059
|
-
| `data.sounds.letters[].
|
|
1060
|
-
| `data.sounds.
|
|
1061
|
-
| `data.sounds.
|
|
1062
|
-
| `data.sounds.
|
|
1063
|
-
| `data.sounds.marks
|
|
1064
|
-
| `data.sounds.marks[].
|
|
1065
|
-
| `data.
|
|
1066
|
-
| `data.
|
|
1067
|
-
| `data.
|
|
1017
|
+
| Key | Shape | Required | Meaning |
|
|
1018
|
+
| ------------------------------------------------------ | --------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1019
|
+
| `data.rules.normalization` | "NFC" | yes | Required value: "NFC". |
|
|
1020
|
+
| `data.rules.matching` | "longest-first" | yes | Required value: "longest-first". |
|
|
1021
|
+
| `data.rules.roundTrip` | a string | no | Exact by default; canonical permits aliases and empty code sequences and decodes an authored conventional spelling. |
|
|
1022
|
+
| `data.rules.word-separators` | a list of nonempty NFC source sequences | no | Source word dividers written as shared spaces, after contextual alternatives; cannot also be ordinary mappings or replace shared punctuation. |
|
|
1023
|
+
| `data.rules.case` | a map | no | Case handling for letters and consonant groups. |
|
|
1024
|
+
| `data.rules.case.single-letter` | "preserve" | yes | Required value: "preserve". |
|
|
1025
|
+
| `data.rules.case.consonant-group` | "lowercase-or-initial-uppercase" | yes | Required value: "lowercase-or-initial-uppercase". |
|
|
1026
|
+
| `data.rules.case.other-group-cases` | "forbidden" | yes | Required value: "forbidden". |
|
|
1027
|
+
| `data.rules.stress` | a map | no | The font's nonprinting stress modifier after an acute vowel. |
|
|
1028
|
+
| `data.rules.stress.modifier` | a string | yes | The font code for U+E0114. |
|
|
1029
|
+
| `data.rules.stress.position` | "after-stressed-vowel" | yes | Required value: "after-stressed-vowel". |
|
|
1030
|
+
| `data.rules.stress.printing` | false | yes | Required value: false. |
|
|
1031
|
+
| `data.rules.stress.effect` | "stress-only" | yes | Required value: "stress-only". |
|
|
1032
|
+
| `data.rules.stress.changes-sound` | false | yes | Required value: false. |
|
|
1033
|
+
| `data.rules.stress.changes-length` | false | yes | Required value: false. |
|
|
1034
|
+
| `data.rules.stress.vowel-group-position` | "first-vowel" | yes | Required value: "first-vowel". |
|
|
1035
|
+
| `data.rules.vowel-groups` | a map | no | Explicit recognized vowel groups and their acute placement. |
|
|
1036
|
+
| `data.rules.vowel-groups.reading` | "established" | yes | Required value: "established". |
|
|
1037
|
+
| `data.rules.vowel-groups.allow-splitting` | false | yes | Required value: false. |
|
|
1038
|
+
| `data.rules.vowel-groups.unrecognized-adjacent-vowels` | "separate" | yes | Required value: "separate". |
|
|
1039
|
+
| `data.rules.vowel-groups.groups` | a list of vowel groups | yes | The complete authored group inventory. |
|
|
1040
|
+
| `data.rules.diaeresis` | a map | no | Whether diaeresis mappings are allowed. |
|
|
1041
|
+
| `data.rules.diaeresis.allowed` | true or false | yes | False forbids the accent; true permits explicitly mapped and accepted characters. |
|
|
1042
|
+
| `data.rules.grave-accent` | a map | no | Whether grave-accent mappings are allowed. |
|
|
1043
|
+
| `data.rules.grave-accent.allowed` | true or false | yes | False forbids the accent; true permits explicitly mapped and accepted characters. |
|
|
1044
|
+
| `data.rules.contextual-mappings` | a list of conditional mappings | no | Alternatives conditioned on the following letter or source position, applied before unconditional mappings. |
|
|
1045
|
+
| `data.rules.contextual-mappings[].match` | a string | yes | The source sequence. |
|
|
1046
|
+
| `data.rules.contextual-mappings[].before` | a string | no | Match before a declared vowel or another letter. |
|
|
1047
|
+
| `data.rules.contextual-mappings[].at` | a string | no | Match at word start, word end or line start. |
|
|
1048
|
+
| `data.rules.contextual-mappings[].emit` | a glyph code or list of codes (empty only with canonical round trips) | yes | The font codes written by this alternative. |
|
|
1049
|
+
| `data.rules.y` | a map | no | The consonant reading of y. |
|
|
1050
|
+
| `data.rules.y.kind` | "consonant" | yes | Required value: "consonant". |
|
|
1051
|
+
| `data.rules.y.sound` | "y in yes" | yes | Required value: "y in yes". |
|
|
1052
|
+
| `data.rules.y.vowel-reading` | false | yes | Required value: false. |
|
|
1053
|
+
| `data.concepts[].id` | lowercase letters, digits and `-` | yes | The concept's id, which a word's `concept` names. Unique in the package. |
|
|
1054
|
+
| `data.concepts[].gloss` | a short gloss, such as `a smell; a scent` | yes | What the concept means, in brief. |
|
|
1055
|
+
| `data.concepts[].group` | a group name, such as `senses` | yes | The group the report lists the concept under. |
|
|
1056
|
+
| `data.sounds.letters` | list of `{ letters, kind, like?, place?, ipa?, tongue? }` | no | The letters the language writes, and the sounds they stand for. |
|
|
1057
|
+
| `data.sounds.letters[].letters` | a list of letters | yes | The letters or digraphs the row describes, as written. |
|
|
1058
|
+
| `data.sounds.letters[].kind` | a string, such as `consonant` or `long vowel` | yes | What kind of sound they write. |
|
|
1059
|
+
| `data.sounds.letters[].like` | a string | no | What they sound like; absent where the row states only their kind and place. |
|
|
1060
|
+
| `data.sounds.letters[].place` | a string, such as `lips` or `tongue-tip` | no | Where in the mouth they are made. |
|
|
1061
|
+
| `data.sounds.letters[].ipa` | a string in the International Phonetic Alphabet, such as `ʃ` | no | The sounds they write, in the International Phonetic Alphabet. |
|
|
1062
|
+
| `data.sounds.letters[].tongue` | a tag or a language note's Address, or a list of them | no | The tongues that write them, each a tag `data.tongues` declares or the Address of a language note; every one where absent. A `doc` note's rows name language notes only. |
|
|
1063
|
+
| `data.sounds.marks` | list of `{ mark, letters, kind, limit }` | no | The marks the language writes on its letters. |
|
|
1064
|
+
| `data.sounds.marks[].mark` | a string, such as `acute` | yes | The mark's name. |
|
|
1065
|
+
| `data.sounds.marks[].letters` | a list of letters | yes | The letters that carry it. |
|
|
1066
|
+
| `data.sounds.marks[].kind` | one of stress, length, apart | yes | What the mark says of its letter. |
|
|
1067
|
+
| `data.sounds.marks[].limit` | one of one, any | yes | How many a word may carry: `one` at most, or `any`. |
|
|
1068
|
+
| `data.metres[].name` | string | yes | What a verse fence's `scan=` names the metre by: lowercase letters, digits and `-`, unique in the package. |
|
|
1069
|
+
| `data.metres[].beats` | a whole number above zero | yes | The beats of one line. |
|
|
1070
|
+
| `data.metres[].kind` | one of accentual | yes | How the beats are counted: `accentual`, one beat for each stressed syllable the language's stress rule and the beat convention give. |
|
|
1068
1071
|
|
|
1069
1072
|
### macro
|
|
1070
1073
|
|
|
@@ -20,7 +20,7 @@ import { isNoteRecord } from "./index-records.mjs";
|
|
|
20
20
|
import { nearest } from "./near-miss.mjs";
|
|
21
21
|
import { isLanguageRecord, lineageChain, noteAddressOf } from "./word-rows.mjs";
|
|
22
22
|
import { derivedForms, foldWord } from "./word-forms.mjs";
|
|
23
|
-
import { WRITTEN_ENDINGS } from "./language-terms.mjs";
|
|
23
|
+
import { WRITTEN_ENDINGS, RESERVED_TONGUE } from "./language-terms.mjs";
|
|
24
24
|
import { LANGUAGE_SUBTYPE, glossText } from "./word-terms.mjs";
|
|
25
25
|
import { writingsOf } from "./language-writings.mjs";
|
|
26
26
|
import { encodeText, validateSource, EncodingError } from "./encoding.mjs";
|
|
@@ -658,7 +658,9 @@ export function checkEncodingLexicon(note, ctx) {
|
|
|
658
658
|
);
|
|
659
659
|
listOf(element?.examples).forEach((form, j) => add(form, ["elements", i, "examples", j]));
|
|
660
660
|
});
|
|
661
|
-
listOf(data.register).forEach((name, i) =>
|
|
661
|
+
listOf(data.register).forEach((name, i) => {
|
|
662
|
+
if (name?.tongue !== RESERVED_TONGUE) add(name?.name, ["register", i, "name"]);
|
|
663
|
+
});
|
|
662
664
|
const findings = [],
|
|
663
665
|
seen = new Set();
|
|
664
666
|
for (const encoding of encodings)
|
|
@@ -61,10 +61,13 @@ export function templateProgram(data, options, fault) {
|
|
|
61
61
|
"A glyph code must resolve to nonempty Unicode scalar text",
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
|
-
const resolve = (value, key) => {
|
|
64
|
+
const resolve = (value, key, allowEmpty = false) => {
|
|
65
65
|
const sequence = typeof value === "string" ? [value] : value;
|
|
66
|
-
if (!Array.isArray(sequence) || !sequence.length)
|
|
67
|
-
fault(
|
|
66
|
+
if (!Array.isArray(sequence) || (!sequence.length && !allowEmpty))
|
|
67
|
+
fault(
|
|
68
|
+
key,
|
|
69
|
+
"A mapping is a glyph code or a code sequence; an empty sequence requires canonical round trips",
|
|
70
|
+
);
|
|
68
71
|
return sequence
|
|
69
72
|
.map((name) => {
|
|
70
73
|
if (
|
|
@@ -82,6 +85,8 @@ export function templateProgram(data, options, fault) {
|
|
|
82
85
|
[
|
|
83
86
|
"normalization",
|
|
84
87
|
"matching",
|
|
88
|
+
"roundTrip",
|
|
89
|
+
"word-separators",
|
|
85
90
|
"case",
|
|
86
91
|
"stress",
|
|
87
92
|
"vowel-groups",
|
|
@@ -95,6 +100,9 @@ export function templateProgram(data, options, fault) {
|
|
|
95
100
|
const rules = data.rules;
|
|
96
101
|
exact(rules.normalization, "NFC", "rules.normalization");
|
|
97
102
|
exact(rules.matching, "longest-first", "rules.matching");
|
|
103
|
+
if (rules.roundTrip !== undefined && !["exact", "canonical"].includes(rules.roundTrip))
|
|
104
|
+
fault("rules.roundTrip", "roundTrip is exact or canonical");
|
|
105
|
+
const canonical = rules.roundTrip === "canonical";
|
|
98
106
|
if (rules.case !== undefined)
|
|
99
107
|
fixed(
|
|
100
108
|
rules.case,
|
|
@@ -106,7 +114,11 @@ export function templateProgram(data, options, fault) {
|
|
|
106
114
|
"rules.case",
|
|
107
115
|
);
|
|
108
116
|
for (const key of ["diaeresis", "grave-accent"])
|
|
109
|
-
if (rules[key] !== undefined)
|
|
117
|
+
if (rules[key] !== undefined) {
|
|
118
|
+
closed(rules[key], ["allowed"], `rules.${key}`);
|
|
119
|
+
if (typeof rules[key].allowed !== "boolean")
|
|
120
|
+
fault(`rules.${key}.allowed`, "allowed must be true or false");
|
|
121
|
+
}
|
|
110
122
|
const vowels = data.vowels ?? "";
|
|
111
123
|
if (!scalar(vowels)) fault("vowels", "vowels must be a Unicode scalar string");
|
|
112
124
|
const vowelSet = new Set([...vowels].map(base));
|
|
@@ -189,8 +201,9 @@ export function templateProgram(data, options, fault) {
|
|
|
189
201
|
if (/[\p{White_Space}⟦⟧]/u.test(from))
|
|
190
202
|
fault(key, "Whitespace and literal markers belong to the shared standard");
|
|
191
203
|
const decomposed = from.normalize("NFD");
|
|
192
|
-
if (rules.diaeresis && decomposed.includes("\u0308"))
|
|
193
|
-
|
|
204
|
+
if (rules.diaeresis?.allowed === false && decomposed.includes("\u0308"))
|
|
205
|
+
fault(key, "Diaeresis is forbidden");
|
|
206
|
+
if (rules["grave-accent"]?.allowed === false && decomposed.includes("\u0300"))
|
|
194
207
|
fault(key, "Grave accents are forbidden");
|
|
195
208
|
};
|
|
196
209
|
const validateOutput = (from, to, key) => {
|
|
@@ -215,10 +228,36 @@ export function templateProgram(data, options, fault) {
|
|
|
215
228
|
for (const [from, output] of Object.entries(data.mappings)) {
|
|
216
229
|
const key = `mappings.${from}`;
|
|
217
230
|
validateSource(from, key);
|
|
218
|
-
const to = resolve(output, key);
|
|
231
|
+
const to = resolve(output, key, canonical);
|
|
219
232
|
validateOutput(from, to, key);
|
|
220
233
|
mappings.push([from, to]);
|
|
221
234
|
}
|
|
235
|
+
const separators = [];
|
|
236
|
+
if (rules["word-separators"] !== undefined) {
|
|
237
|
+
if (!Array.isArray(rules["word-separators"]))
|
|
238
|
+
fault("rules.word-separators", "word-separators must be a list");
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
for (const [i, from] of rules["word-separators"].entries()) {
|
|
241
|
+
const key = `rules.word-separators.${i}`;
|
|
242
|
+
validateSource(from, key);
|
|
243
|
+
if (
|
|
244
|
+
[...from].some(
|
|
245
|
+
(char) =>
|
|
246
|
+
"^/{}".includes(char) || /[\p{Cc}\p{Cf}\u{E0100}-\u{E01EF}]/u.test(char),
|
|
247
|
+
) ||
|
|
248
|
+
STANDARD_ROWS.some((row) => (Array.isArray(row) ? row[0] : row.from) === from)
|
|
249
|
+
)
|
|
250
|
+
fault(
|
|
251
|
+
key,
|
|
252
|
+
"Word separators cannot replace shared punctuation or contain structural controls",
|
|
253
|
+
);
|
|
254
|
+
if (seen.has(from)) fault(key, "A word separator is stated twice");
|
|
255
|
+
if (Object.hasOwn(data.mappings, from))
|
|
256
|
+
fault(key, "A word separator cannot also have an ordinary mapping");
|
|
257
|
+
seen.add(from);
|
|
258
|
+
separators.push({ from, to: STANDARD_CONTROLS.wordSeparator });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
222
261
|
const contextual = [];
|
|
223
262
|
if (rules["contextual-mappings"] !== undefined) {
|
|
224
263
|
if (!Array.isArray(rules["contextual-mappings"]))
|
|
@@ -226,21 +265,36 @@ export function templateProgram(data, options, fault) {
|
|
|
226
265
|
const seen = new Set();
|
|
227
266
|
for (const [i, row] of rules["contextual-mappings"].entries()) {
|
|
228
267
|
const key = `rules.contextual-mappings.${i}`;
|
|
229
|
-
closed(row, ["match", "before", "emit"], key);
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (
|
|
268
|
+
closed(row, ["match", "before", "at", "emit"], key);
|
|
269
|
+
if (!scalar(row.match) || !row.match || row.match !== row.match.normalize("NFC"))
|
|
270
|
+
fault(`${key}.match`, "A contextual match must be nonempty NFC text");
|
|
271
|
+
if (row.before === undefined && row.at === undefined)
|
|
272
|
+
fault(key, "A contextual mapping requires before or at");
|
|
273
|
+
if (row.before !== undefined && !["vowel", "consonant"].includes(row.before))
|
|
274
|
+
fault(`${key}.before`, "before is vowel or consonant");
|
|
275
|
+
if (row.at !== undefined && !["start", "end", "line-start"].includes(row.at))
|
|
276
|
+
fault(`${key}.at`, "at is start, end or line-start");
|
|
277
|
+
if (row.before && !vowels)
|
|
278
|
+
fault(
|
|
279
|
+
`${key}.before`,
|
|
280
|
+
"Before-vowel or before-consonant mappings require declared vowels",
|
|
281
|
+
);
|
|
282
|
+
const condition = JSON.stringify([row.match, row.before ?? null, row.at ?? null]);
|
|
283
|
+
if (seen.has(condition))
|
|
284
|
+
fault(
|
|
285
|
+
`${key}.match`,
|
|
286
|
+
"A contextual mapping repeats the same source and conditions",
|
|
287
|
+
);
|
|
239
288
|
validateSource(row.match, `${key}.match`);
|
|
240
|
-
const to = resolve(row.emit, `${key}.emit
|
|
289
|
+
const to = resolve(row.emit, `${key}.emit`, canonical);
|
|
241
290
|
validateOutput(row.match, to, `${key}.emit`);
|
|
242
|
-
seen.add(
|
|
243
|
-
contextual.push({
|
|
291
|
+
seen.add(condition);
|
|
292
|
+
contextual.push({
|
|
293
|
+
from: row.match,
|
|
294
|
+
to,
|
|
295
|
+
...(row.before !== undefined ? { before: row.before } : {}),
|
|
296
|
+
...(row.at !== undefined ? { at: row.at } : {}),
|
|
297
|
+
});
|
|
244
298
|
}
|
|
245
299
|
}
|
|
246
300
|
if (rules.case)
|
|
@@ -256,26 +310,32 @@ export function templateProgram(data, options, fault) {
|
|
|
256
310
|
...STANDARD_ROWS.filter((row) => !overridden.has(Array.isArray(row) ? row[0] : row.from)),
|
|
257
311
|
...contextual,
|
|
258
312
|
...mappings,
|
|
313
|
+
...separators,
|
|
259
314
|
];
|
|
260
|
-
|
|
261
|
-
[
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
315
|
+
const sourceKey = (row) => {
|
|
316
|
+
if (Array.isArray(row)) return `mappings.${row[0]}`;
|
|
317
|
+
const separator = separators.indexOf(row);
|
|
318
|
+
if (separator >= 0) return `rules.word-separators.${separator}`;
|
|
319
|
+
const context = contextual.indexOf(row);
|
|
320
|
+
return context < 0 ? "rules" : `rules.contextual-mappings.${context}`;
|
|
321
|
+
};
|
|
322
|
+
if (!canonical)
|
|
323
|
+
assertUniqueOutputs(
|
|
324
|
+
[
|
|
325
|
+
...rows.map((row) =>
|
|
326
|
+
Array.isArray(row) ?
|
|
327
|
+
{ from: row[0], to: row[1], key: sourceKey(row) }
|
|
328
|
+
: { ...row, key: sourceKey(row) },
|
|
329
|
+
),
|
|
330
|
+
{ from: " ", to: " ", key: "rules" },
|
|
331
|
+
],
|
|
332
|
+
fault,
|
|
333
|
+
);
|
|
275
334
|
const accepts = data.accepts ?? [
|
|
276
335
|
...new Set([
|
|
277
336
|
...Object.keys(data.mappings).join(""),
|
|
278
337
|
...contextual.map((r) => r.from).join(""),
|
|
338
|
+
...separators.map((row) => row.from).join(""),
|
|
279
339
|
]),
|
|
280
340
|
];
|
|
281
341
|
if (!Array.isArray(accepts)) fault("accepts", "accepts must be a list of characters or ranges");
|
|
@@ -285,17 +345,14 @@ export function templateProgram(data, options, fault) {
|
|
|
285
345
|
fonts: [data.font],
|
|
286
346
|
accepts: [...accepts, ...STANDARD_ROWS.map((r) => (Array.isArray(r) ? r[0] : r.from))],
|
|
287
347
|
separator: " ",
|
|
348
|
+
roundTrip: canonical ? "canonical" : "exact",
|
|
288
349
|
vowels,
|
|
289
350
|
rows,
|
|
290
351
|
codes: Object.entries(options.codes),
|
|
291
352
|
controls: STANDARD_CONTROLS,
|
|
292
353
|
...(rules.case ? { groupCase: "initial" } : {}),
|
|
293
354
|
},
|
|
294
|
-
rowKeys: rows.map(
|
|
295
|
-
if (Array.isArray(row)) return `mappings.${row[0]}`;
|
|
296
|
-
const index = contextual.indexOf(row);
|
|
297
|
-
return index < 0 ? "rules" : `rules.contextual-mappings.${index}`;
|
|
298
|
-
}),
|
|
355
|
+
rowKeys: rows.map(sourceKey),
|
|
299
356
|
metadata: {
|
|
300
357
|
name: options.name ?? `${data.language}-${data.font}`,
|
|
301
358
|
language: data.language,
|
|
@@ -305,8 +362,8 @@ export function templateProgram(data, options, fault) {
|
|
|
305
362
|
vowelGroups: Object.freeze(vowelGroups),
|
|
306
363
|
maxVowelGroup: Math.max(0, ...vowelGroups.map((group) => [...group].length)),
|
|
307
364
|
stress,
|
|
308
|
-
diaeresis:
|
|
309
|
-
grave:
|
|
365
|
+
diaeresis: rules.diaeresis?.allowed === false,
|
|
366
|
+
grave: rules["grave-accent"]?.allowed === false,
|
|
310
367
|
}),
|
|
311
368
|
},
|
|
312
369
|
};
|
|
@@ -28,7 +28,7 @@ const fixed = (name, value) => ({
|
|
|
28
28
|
const block = (name, describe, fields) => ({ name, kind: "map", shape: "a map", describe, fields });
|
|
29
29
|
const sequence = {
|
|
30
30
|
kind: "string-or-list",
|
|
31
|
-
shape: "a glyph code or list of codes",
|
|
31
|
+
shape: "a glyph code or list of codes (empty only with canonical round trips)",
|
|
32
32
|
entries: {
|
|
33
33
|
kind: "string",
|
|
34
34
|
shape: "a glyph code",
|
|
@@ -39,6 +39,25 @@ const sequence = {
|
|
|
39
39
|
export const ENCODING_RULE_FIELDS = Object.freeze([
|
|
40
40
|
fixed("normalization", "NFC"),
|
|
41
41
|
fixed("matching", "longest-first"),
|
|
42
|
+
{
|
|
43
|
+
...text(
|
|
44
|
+
"roundTrip",
|
|
45
|
+
"Exact by default; canonical permits aliases and empty code sequences and decodes an authored conventional spelling.",
|
|
46
|
+
),
|
|
47
|
+
allowed: ["exact", "canonical"],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "word-separators",
|
|
51
|
+
kind: "list",
|
|
52
|
+
shape: "a list of nonempty NFC source sequences",
|
|
53
|
+
entries: {
|
|
54
|
+
kind: "string",
|
|
55
|
+
shape: "a word separator",
|
|
56
|
+
describe: "A source sequence that writes the shared word space.",
|
|
57
|
+
},
|
|
58
|
+
describe:
|
|
59
|
+
"Source word dividers written as shared spaces, after contextual alternatives; cannot also be ordinary mappings or replace shared punctuation.",
|
|
60
|
+
},
|
|
42
61
|
block("case", "Case handling for letters and consonant groups.", [
|
|
43
62
|
fixed("single-letter", "preserve"),
|
|
44
63
|
fixed("consonant-group", "lowercase-or-initial-uppercase"),
|
|
@@ -70,19 +89,37 @@ export const ENCODING_RULE_FIELDS = Object.freeze([
|
|
|
70
89
|
describe: "The complete authored group inventory.",
|
|
71
90
|
},
|
|
72
91
|
]),
|
|
73
|
-
|
|
74
|
-
|
|
92
|
+
...["diaeresis", "grave-accent"].map((name) =>
|
|
93
|
+
block(name, `Whether ${name} mappings are allowed.`, [
|
|
94
|
+
{
|
|
95
|
+
name: "allowed",
|
|
96
|
+
kind: "boolean",
|
|
97
|
+
shape: "true or false",
|
|
98
|
+
required: true,
|
|
99
|
+
describe:
|
|
100
|
+
"False forbids the accent; true permits explicitly mapped and accepted characters.",
|
|
101
|
+
},
|
|
102
|
+
]),
|
|
103
|
+
),
|
|
75
104
|
{
|
|
76
105
|
name: "contextual-mappings",
|
|
77
106
|
kind: "list",
|
|
78
107
|
shape: "a list of conditional mappings",
|
|
79
|
-
describe:
|
|
108
|
+
describe:
|
|
109
|
+
"Alternatives conditioned on the following letter or source position, applied before unconditional mappings.",
|
|
80
110
|
entries: {
|
|
81
111
|
kind: "map",
|
|
82
112
|
shape: "a contextual mapping",
|
|
83
113
|
fields: [
|
|
84
114
|
text("match", "The source sequence.", true),
|
|
85
|
-
|
|
115
|
+
{
|
|
116
|
+
...text("before", "Match before a declared vowel or another letter."),
|
|
117
|
+
allowed: ["vowel", "consonant"],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
...text("at", "Match at word start, word end or line start."),
|
|
121
|
+
allowed: ["start", "end", "line-start"],
|
|
122
|
+
},
|
|
86
123
|
{
|
|
87
124
|
name: "emit",
|
|
88
125
|
...sequence,
|
package/engine/encoding.mjs
CHANGED
|
@@ -322,15 +322,13 @@ function boundary(char) {
|
|
|
322
322
|
// Python str.isspace also includes the four information separators.
|
|
323
323
|
return SPACE.test(char) || /[\u001C-\u001F]/u.test(char) || BOUNDARY.test(char);
|
|
324
324
|
}
|
|
325
|
+
function beforeHolds(condition, char, vowels) {
|
|
326
|
+
return Boolean(
|
|
327
|
+
char && (condition === "vowel" ? vowels.has(char) : !vowels.has(char) && LETTER.test(char)),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
325
330
|
function holds(row, text, start, end, vowels) {
|
|
326
|
-
if (row.before)
|
|
327
|
-
const char = firstAt(text, end);
|
|
328
|
-
if (
|
|
329
|
-
!char ||
|
|
330
|
-
(row.before === "vowel" ? !vowels.has(char) : vowels.has(char) || !LETTER.test(char))
|
|
331
|
-
)
|
|
332
|
-
return false;
|
|
333
|
-
}
|
|
331
|
+
if (row.before && !beforeHolds(row.before, firstAt(text, end), vowels)) return false;
|
|
334
332
|
if (row.at === "line-start") {
|
|
335
333
|
const prefix = text.slice(
|
|
336
334
|
Math.max(text.lastIndexOf("\n", start - 1), text.lastIndexOf("/", start - 1)) + 1,
|
|
@@ -834,7 +832,7 @@ function decodeRawText(enc, text, { font } = {}) {
|
|
|
834
832
|
}
|
|
835
833
|
|
|
836
834
|
/**
|
|
837
|
-
* Decode
|
|
835
|
+
* Decode exact output or the conventional reading of an explicitly canonical note.
|
|
838
836
|
* @param {ReturnType<typeof compileEncoding>} enc - Compiled encoding note.
|
|
839
837
|
* @param {string} text - Native text with optional literal spans.
|
|
840
838
|
* @param {{font?: string}} [options] - Selected encoding face.
|
|
@@ -845,17 +843,19 @@ export function decodeText(enc, text, options = {}) {
|
|
|
845
843
|
const readings = tiers(enc, options.font)[0].readings;
|
|
846
844
|
if (typeof text !== "string") throw new EncodingError("Text must be a string");
|
|
847
845
|
if (!templateReaders.has(enc)) {
|
|
848
|
-
const root = { children: new Map(), row: undefined };
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
node
|
|
856
|
-
|
|
857
|
-
node.row = row;
|
|
846
|
+
const root = { children: new Map(), row: undefined, rows: [] };
|
|
847
|
+
const rows = new Set([...readings.values()].flat().concat(enc.shared.rows));
|
|
848
|
+
for (const row of rows) {
|
|
849
|
+
if (!row.to) continue;
|
|
850
|
+
let node = root;
|
|
851
|
+
for (const char of row.to) {
|
|
852
|
+
if (!node.children.has(char))
|
|
853
|
+
node.children.set(char, { children: new Map(), row: undefined, rows: [] });
|
|
854
|
+
node = node.children.get(char);
|
|
858
855
|
}
|
|
856
|
+
node.row ??= row;
|
|
857
|
+
node.rows.push(row);
|
|
858
|
+
}
|
|
859
859
|
templateReaders.set(enc, root);
|
|
860
860
|
}
|
|
861
861
|
const reader = templateReaders.get(enc);
|
|
@@ -888,9 +888,15 @@ export function decodeText(enc, text, options = {}) {
|
|
|
888
888
|
cursor = cursor.children.get(char);
|
|
889
889
|
if (!cursor) break;
|
|
890
890
|
next += char.length;
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
891
|
+
if (!cursor.row || !paths.has(next)) continue;
|
|
892
|
+
const following = firstAt(paths.get(next).from, 0);
|
|
893
|
+
const preferred = cursor.rows.find(
|
|
894
|
+
(row) => !row.before || beforeHolds(row.before, following, enc.vowels),
|
|
895
|
+
);
|
|
896
|
+
// Canonical source can omit the vowel or marker that selected a form.
|
|
897
|
+
// Prefer applicable aliases, then retain the first authored reading.
|
|
898
|
+
const row = preferred ?? (enc.roundTrip === "canonical" ? cursor.row : undefined);
|
|
899
|
+
if (!row) {
|
|
894
900
|
failedContext = Math.min(failedContext, at);
|
|
895
901
|
continue;
|
|
896
902
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "25.1.
|
|
3
|
+
"version": "25.1.1",
|
|
4
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",
|
|
@@ -12,6 +12,17 @@ export const ENCODING_RULE_FIELDS: readonly ({
|
|
|
12
12
|
shape: string;
|
|
13
13
|
describe: any;
|
|
14
14
|
fields: any;
|
|
15
|
+
} | {
|
|
16
|
+
name: string;
|
|
17
|
+
kind: string;
|
|
18
|
+
shape: string;
|
|
19
|
+
entries: {
|
|
20
|
+
kind: string;
|
|
21
|
+
shape: string;
|
|
22
|
+
describe: string;
|
|
23
|
+
fields?: undefined;
|
|
24
|
+
};
|
|
25
|
+
describe: string;
|
|
15
26
|
} | {
|
|
16
27
|
name: string;
|
|
17
28
|
kind: string;
|
|
@@ -20,13 +31,21 @@ export const ENCODING_RULE_FIELDS: readonly ({
|
|
|
20
31
|
entries: {
|
|
21
32
|
kind: string;
|
|
22
33
|
shape: string;
|
|
23
|
-
fields: {
|
|
34
|
+
fields: ({
|
|
35
|
+
name: any;
|
|
36
|
+
kind: string;
|
|
37
|
+
shape: string;
|
|
38
|
+
describe: any;
|
|
39
|
+
required: boolean;
|
|
40
|
+
} | {
|
|
41
|
+
allowed: string[];
|
|
24
42
|
name: any;
|
|
25
43
|
kind: string;
|
|
26
44
|
shape: string;
|
|
27
45
|
describe: any;
|
|
28
46
|
required: boolean;
|
|
29
|
-
}[];
|
|
47
|
+
})[];
|
|
48
|
+
describe?: undefined;
|
|
30
49
|
};
|
|
31
50
|
})[];
|
|
32
51
|
/** The additional data keys a document may use to define its encoding. */
|
|
@@ -82,6 +101,17 @@ export const ENCODING_NOTE_FIELDS: readonly ({
|
|
|
82
101
|
shape: string;
|
|
83
102
|
describe: any;
|
|
84
103
|
fields: any;
|
|
104
|
+
} | {
|
|
105
|
+
name: string;
|
|
106
|
+
kind: string;
|
|
107
|
+
shape: string;
|
|
108
|
+
entries: {
|
|
109
|
+
kind: string;
|
|
110
|
+
shape: string;
|
|
111
|
+
describe: string;
|
|
112
|
+
fields?: undefined;
|
|
113
|
+
};
|
|
114
|
+
describe: string;
|
|
85
115
|
} | {
|
|
86
116
|
name: string;
|
|
87
117
|
kind: string;
|
|
@@ -90,13 +120,21 @@ export const ENCODING_NOTE_FIELDS: readonly ({
|
|
|
90
120
|
entries: {
|
|
91
121
|
kind: string;
|
|
92
122
|
shape: string;
|
|
93
|
-
fields: {
|
|
123
|
+
fields: ({
|
|
124
|
+
name: any;
|
|
125
|
+
kind: string;
|
|
126
|
+
shape: string;
|
|
127
|
+
describe: any;
|
|
128
|
+
required: boolean;
|
|
129
|
+
} | {
|
|
130
|
+
allowed: string[];
|
|
94
131
|
name: any;
|
|
95
132
|
kind: string;
|
|
96
133
|
shape: string;
|
|
97
134
|
describe: any;
|
|
98
135
|
required: boolean;
|
|
99
|
-
}[];
|
|
136
|
+
})[];
|
|
137
|
+
describe?: undefined;
|
|
100
138
|
};
|
|
101
139
|
})[];
|
|
102
140
|
describe: string;
|
|
@@ -128,7 +128,7 @@ export function encodeText(enc: ReturnType<typeof compileEncoding>, text: string
|
|
|
128
128
|
verse?: boolean;
|
|
129
129
|
}): string;
|
|
130
130
|
/**
|
|
131
|
-
* Decode
|
|
131
|
+
* Decode exact output or the conventional reading of an explicitly canonical note.
|
|
132
132
|
* @param {ReturnType<typeof compileEncoding>} enc - Compiled encoding note.
|
|
133
133
|
* @param {string} text - Native text with optional literal spans.
|
|
134
134
|
* @param {{font?: string}} [options] - Selected encoding face.
|