@heroiclands/package-build 25.0.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -0
  3. package/bin/content-commands.mjs +30 -96
  4. package/bin/encoding-commands.mjs +146 -0
  5. package/bin/package-build.mjs +35 -24
  6. package/config.mjs +25 -0
  7. package/convert.mjs +182 -0
  8. package/docs/api.md +144 -6
  9. package/docs/authoring/links-and-markup.md +260 -481
  10. package/docs/commands.md +110 -119
  11. package/docs/configuration.md +31 -13
  12. package/docs/content-format.md +11 -4
  13. package/docs/diagnostics.md +43 -16
  14. package/docs/encoding-core.md +193 -0
  15. package/docs/guides/book.md +2 -2
  16. package/docs/guides/module-project.md +23 -1
  17. package/docs/guides/site.md +2 -2
  18. package/docs/guides/writing-in-invented-scripts.md +458 -575
  19. package/docs/reference/format-details.md +292 -404
  20. package/docs/reference/note-types.md +83 -63
  21. package/docs/source-preview.md +8 -0
  22. package/e2e.mjs +1 -1
  23. package/engine/base-compiler.mjs +27 -9
  24. package/engine/content-charset.mjs +13 -1
  25. package/engine/content-format.yaml +6 -2
  26. package/engine/content-index.mjs +6 -2
  27. package/engine/content-links.mjs +1 -0
  28. package/engine/content-scripts.mjs +614 -459
  29. package/engine/content-scroll.mjs +4 -4
  30. package/engine/content-spans.mjs +40 -9
  31. package/engine/content-verse.mjs +1 -1
  32. package/engine/converter-data.mjs +70 -0
  33. package/engine/dictionary-rows.mjs +2 -34
  34. package/engine/encoding-integrity.mjs +94 -0
  35. package/engine/encoding-language.mjs +506 -0
  36. package/engine/encoding-lexicon.mjs +701 -0
  37. package/engine/encoding-notes.mjs +186 -0
  38. package/engine/encoding-standard.mjs +42 -0
  39. package/engine/encoding-template.mjs +370 -0
  40. package/engine/encoding-terms.mjs +178 -0
  41. package/engine/encoding.mjs +952 -0
  42. package/engine/extension-attributes.mjs +2 -0
  43. package/engine/helpers.mjs +1 -1
  44. package/engine/index.mjs +7 -0
  45. package/engine/language-terms.mjs +17 -16
  46. package/engine/{logographic.mjs → language-writings.mjs} +29 -38
  47. package/engine/markup-validation.mjs +1 -1
  48. package/engine/note-vocabulary.mjs +4 -158
  49. package/engine/pdf-render.mjs +31 -3
  50. package/engine/roman-characters.mjs +135 -16
  51. package/engine/roman-run.mjs +149 -0
  52. package/engine/roman-verse.mjs +95 -0
  53. package/engine/run-rows.mjs +29 -31
  54. package/engine/run-words.mjs +2 -2
  55. package/engine/script-emphasis-terms.mjs +3 -0
  56. package/engine/script-emphasis.mjs +3 -10
  57. package/engine/script-font-delivery.mjs +9 -12
  58. package/engine/script-fonts.mjs +2 -1
  59. package/engine/site-build.mjs +14 -5
  60. package/engine/site-preview.mjs +12 -2
  61. package/engine/sql-tables.mjs +36 -31
  62. package/engine/verse-metre.mjs +1 -1
  63. package/engine/word-forms.mjs +1 -1
  64. package/engine/word-inflections.mjs +48 -0
  65. package/engine/word-rows.mjs +1 -1
  66. package/engine/word-terms.mjs +16 -17
  67. package/package.json +11 -5
  68. package/types/config.d.mts +5 -0
  69. package/types/convert.d.mts +44 -0
  70. package/types/e2e.d.mts +1 -1
  71. package/types/engine/base-compiler.d.mts +4 -2
  72. package/types/engine/content-index.d.mts +4 -1
  73. package/types/engine/content-scripts.d.mts +103 -42
  74. package/types/engine/converter-data.d.mts +11 -0
  75. package/types/engine/dictionary-rows.d.mts +1 -25
  76. package/types/engine/encoding-integrity.d.mts +14 -0
  77. package/types/engine/encoding-language.d.mts +36 -0
  78. package/types/engine/encoding-lexicon.d.mts +259 -0
  79. package/types/engine/encoding-notes.d.mts +39 -0
  80. package/types/engine/encoding-standard.d.mts +11 -0
  81. package/types/engine/encoding-template.d.mts +44 -0
  82. package/types/engine/encoding-terms.d.mts +143 -0
  83. package/types/engine/encoding.d.mts +155 -0
  84. package/types/engine/helpers.d.mts +1 -1
  85. package/types/engine/index.d.mts +4 -0
  86. package/types/engine/language-terms.d.mts +1 -1
  87. package/types/engine/{logographic.d.mts → language-writings.d.mts} +6 -6
  88. package/types/engine/markup-validation.d.mts +1 -1
  89. package/types/engine/pdf-render.d.mts +1 -1
  90. package/types/engine/roman-characters.d.mts +2 -2
  91. package/types/engine/roman-run.d.mts +101 -0
  92. package/types/engine/roman-verse.d.mts +23 -0
  93. package/types/engine/run-rows.d.mts +13 -20
  94. package/types/engine/script-emphasis-terms.d.mts +2 -0
  95. package/types/engine/script-emphasis.d.mts +1 -1
  96. package/types/engine/script-font-delivery.d.mts +2 -3
  97. package/types/engine/script-fonts.d.mts +5 -0
  98. package/types/engine/site-preview.d.mts +1 -1
  99. package/types/engine/word-inflections.d.mts +25 -0
  100. package/engine/map-lexicon.mjs +0 -1045
  101. package/engine/text-maps.mjs +0 -654
  102. package/engine/written-in.mjs +0 -204
  103. package/types/engine/map-lexicon.d.mts +0 -330
  104. package/types/engine/text-maps.d.mts +0 -189
  105. package/types/engine/written-in.d.mts +0 -76
@@ -146,6 +146,8 @@ export function refusedAttributes(values) {
146
146
  problems.push(
147
147
  `set ${key} with ${key.toLowerCase() === "id" ? "#id" : ".class"} rather than ${key}=`,
148
148
  );
149
+ } else if (key === "map") {
150
+ problems.push("map is not an attribute");
149
151
  } else if (/^on/i.test(key)) {
150
152
  problems.push(`${key} is an event handler and is not written`);
151
153
  }
@@ -194,7 +194,7 @@ function localAssetImageInfo() {
194
194
  * {@link module:engine/content-figures.scanFigures}.
195
195
  * @param {import("./content-scripts.mjs").RunContext} [runs] - The package's
196
196
  * languages, faces and maps. Given, text marked with `lang=`, `font=` and
197
- * `map=` is rewritten first, so it is tagged for its language, mapped and
197
+ * `enc=` is rewritten first, so it is tagged for its language, mapped and
198
198
  * set in its font.
199
199
  * @returns {string} The rendered HTML.
200
200
  */
package/engine/index.mjs CHANGED
@@ -331,3 +331,10 @@ export {
331
331
 
332
332
  /** The curated region-event vocabulary, shared with the Foundry runtime. */
333
333
  export * as regionEvents from "./region-events.mjs";
334
+
335
+ /** Browser-safe encoding-document compilation and conversion. */
336
+ export * as encoding from "./encoding.mjs";
337
+ export * as encodingLanguage from "./encoding-language.mjs";
338
+ /** Consumer-root file loading and authored language projections. */
339
+ export * as encodingNotes from "./encoding-notes.mjs";
340
+ export * as encodingLexicon from "./encoding-lexicon.mjs";
@@ -1696,7 +1696,7 @@ export const VERB_PERSON_FIELDS = Object.freeze(
1696
1696
  );
1697
1697
 
1698
1698
  /**
1699
- * The endings `data.grammar.native` writes under a logographic map: the noun
1699
+ * The endings `data.grammar.native` writes under a encoding: the noun
1700
1700
  * plural, then the six persons of the verb pattern.
1701
1701
  */
1702
1702
  export const WRITTEN_ENDINGS = Object.freeze([
@@ -1717,18 +1717,18 @@ const GRAMMAR_NATIVE_FIELDS = Object.freeze([
1717
1717
  "for a verb's present form.",
1718
1718
  }),
1719
1719
  Object.freeze({
1720
- name: "map",
1720
+ name: "enc",
1721
1721
  kind: "string",
1722
1722
  required: true,
1723
- shape: "a logographic mapping note's `data.map.name`",
1724
- describe: "The logographic map the ending is written for.",
1723
+ shape: "an encoding document Address",
1724
+ describe: "The encoding the ending is written for.",
1725
1725
  }),
1726
1726
  Object.freeze({
1727
1727
  name: "font",
1728
1728
  kind: "string",
1729
- shape: "a face the map lists",
1729
+ shape: "a face the encoding lists",
1730
1730
  describe:
1731
- "The one face the writing is for; unstated, it is for every face the map lists, " +
1731
+ "The one face the writing is for; unstated, it is for every face the encoding lists, " +
1732
1732
  "and a writing stated for a face wins over it there.",
1733
1733
  }),
1734
1734
  Object.freeze({
@@ -1870,15 +1870,15 @@ const GRAMMAR_FIELDS = Object.freeze([
1870
1870
  Object.freeze({
1871
1871
  name: "native",
1872
1872
  kind: "list",
1873
- shape: "a list of `{ ending, map, font?, text }`",
1873
+ shape: "a list of `{ ending, enc, font?, text }`",
1874
1874
  entries: Object.freeze({
1875
1875
  kind: "map",
1876
- shape: "a map — `{ ending, map, font?, text }`",
1876
+ shape: "a map — `{ ending, enc, font?, text }`",
1877
1877
  fields: GRAMMAR_NATIVE_FIELDS,
1878
1878
  }),
1879
1879
  describe:
1880
- "Under a logographic map, the signs a noun's plural or a verb's present form is " +
1881
- "written with after its stem's writing, one entry per ending, map and face.",
1880
+ "Under an encoding, the signs a noun's plural or a verb's present form is " +
1881
+ "written with after its stem's writing, one entry per ending, encoding and face.",
1882
1882
  }),
1883
1883
  ]);
1884
1884
 
@@ -1893,11 +1893,12 @@ export const LANGUAGE_DATA_FIELDS = Object.freeze([
1893
1893
  Object.freeze({
1894
1894
  name: "roman",
1895
1895
  kind: "string",
1896
- shape: "a literal NFC string of permitted characters, including case, accents, digits and punctuation",
1896
+ shape: "a literal NFC string of additional source characters, including exact letter case and digits",
1897
1897
  describe:
1898
- "The exact Roman source characters this language permits, each once. No ranges, case " +
1899
- "expansion or inherited inventory; spaces and line breaks are structural. An empty " +
1900
- "string declares no written characters. Required on each configured language note.",
1898
+ "The additional Roman source characters the language permits, each once. Standard " +
1899
+ "punctuation and structural whitespace remain available. No ranges, case expansion " +
1900
+ "or inherited inventory. An empty string declares no additions. Required on each " +
1901
+ "configured language note.",
1901
1902
  }),
1902
1903
  Object.freeze({
1903
1904
  name: "grammar",
@@ -1906,8 +1907,8 @@ export const LANGUAGE_DATA_FIELDS = Object.freeze([
1906
1907
  fields: GRAMMAR_FIELDS,
1907
1908
  describe:
1908
1909
  "Optional noun plurals, one verb pattern, agreement, participles and the endings' " +
1909
- "logographic writings, for dictionary presentation, the mapped-run word check and " +
1910
- "logographic maps. Only the shape of supplied keys is " +
1910
+ "native writings, for dictionary presentation, language word checks and encodings. " +
1911
+ "Only the shape of supplied keys is " +
1911
1912
  "checked; no word must follow or state a rule. A language skill note only.",
1912
1913
  }),
1913
1914
  listOfMaps(
@@ -12,39 +12,19 @@
12
12
  */
13
13
 
14
14
  /**
15
- * **A logographic map writes meanings, so a word's writing comes from the
16
- * lexicon and never from its sound.** A mapping note stating
17
- * `data.map.logographic: true` writes each word of a run in this order:
18
- *
19
- * 1. **The word's own `native` entry** for the map, and for the run's face
20
- * where an entry names one.
21
- * 2. **Otherwise its parts.** A word stating `built` is written by the
22
- * writings of its pieces joined in order: a piece is a word of the
23
- * lexicon, matched exactly and then with its joining hyphens taken off, or
24
- * an affix of `data.affixes` with its own `native`; a word piece resolves
25
- * by this same rule. A noun's plural and a verb's present forms are written
26
- * by the stem's writing followed by the ending's, which
27
- * `data.grammar.native` states by the ending's name: `plural`, or one of
28
- * the six persons. A hyphenated word no entry states is written by its
29
- * parts, each by this rule.
30
- * 3. **Otherwise the word has no writing**, which a run reports at the word.
31
- *
32
- * A run's word is matched case folded, so one roman form may stand for more
33
- * than one entry. Where those entries are written alike the word takes that
34
- * writing; where they differ, or one has a writing and another none, the word
35
- * is ambiguous, and the finding names every entry's gloss. A part matched to
36
- * entries written differently makes the word ambiguous the same way.
37
- *
38
- * @module
15
+ * Native word spellings under any encoding: authored entries first, then all
16
+ * stated parts, affixes and grammatical endings. Ambiguous spellings remain
17
+ * findings, with every matching gloss. Exact lexical forms precede folded
18
+ * lookup, preserving explicitly distinct capitalized spellings.
39
19
  */
40
20
 
41
- import { pluralOf } from "./dictionary-rows.mjs";
21
+ import { pluralOf } from "./word-inflections.mjs";
42
22
  import { verbForm, verbPattern } from "./verb-pattern.mjs";
43
23
  import { parseWordClass } from "./word-classes.mjs";
44
24
  import { foldWord } from "./word-forms.mjs";
45
25
  import { glossText } from "./word-terms.mjs";
46
26
 
47
- /** Whether a value is a plain map. */
27
+ /** Whether a value is a plain object. */
48
28
  const isMap = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
49
29
 
50
30
  /** A list value, or an empty one. */
@@ -65,17 +45,18 @@ const JOINS = /[–-]/u;
65
45
  */
66
46
 
67
47
  /**
68
- * The text a list of native entries states for a map in a face: the entry for
48
+ * The text a list of native entries states for a enc in a face: the entry for
69
49
  * the face, else the entry for no face.
70
50
  *
71
51
  * @param {unknown} natives - A `native` list as authored.
72
- * @param {string} map - The map's name.
52
+ * @param {string} enc - The encoding's name.
73
53
  * @param {string|undefined} font - The face.
74
54
  * @returns {string|undefined} The text.
75
55
  */
76
- export function nativeText(natives, map, font) {
56
+
57
+ export function nativeText(natives, enc, font) {
77
58
  const entries = listOf(natives).filter(
78
- (entry) => isMap(entry) && entry.map === map && typeof entry.text === "string",
59
+ (entry) => isMap(entry) && entry.enc === enc && typeof entry.text === "string",
79
60
  );
80
61
  const own = font === undefined ? undefined : entries.find((entry) => entry.font === font);
81
62
  return (own ?? entries.find((entry) => typeof entry.font !== "string"))?.text;
@@ -100,15 +81,15 @@ function oneOf(form, candidates) {
100
81
  }
101
82
 
102
83
  /**
103
- * The writings of a language's words under one logographic map in one face.
84
+ * The writings of a language's words under one encoding in one face.
104
85
  *
105
86
  * @param {Record<string, any>|undefined} data - The language note's `data`.
106
- * @param {string} map - The map's name.
87
+ * @param {string} enc - The encoding's name.
107
88
  * @param {string|undefined} font - The face.
108
89
  * @returns {{word: (word: string) => Writing, entry: (index: number) => Writing}}
109
90
  * The writing of a run's word, and of the `data.words` entry at an index.
110
91
  */
111
- export function writingsOf(data, map, font) {
92
+ export function writingsOf(data, enc, font) {
112
93
  const words = listOf(data?.words);
113
94
  const affixes = listOf(data?.affixes).filter(isMap);
114
95
  const grammar = isMap(data?.grammar) ? data.grammar : undefined;
@@ -127,7 +108,7 @@ export function writingsOf(data, map, font) {
127
108
  */
128
109
  const entry = (index, visiting) => {
129
110
  const word = words[index];
130
- const own = nativeText(word?.native, map, font);
111
+ const own = nativeText(word?.native, enc, font);
131
112
  if (own !== undefined) return { text: own, source: "own" };
132
113
  const pieces = listOf(word?.built).filter((piece) => typeof piece === "string");
133
114
  if (!pieces.length || visiting.has(index)) return { missing: true };
@@ -148,7 +129,7 @@ export function writingsOf(data, map, font) {
148
129
  gloss: glossOf(word),
149
130
  })),
150
131
  ...matchedAffixes.map((affix) => {
151
- const text = nativeText(affix.native, map, font);
132
+ const text = nativeText(affix.native, enc, font);
152
133
  return {
153
134
  writing: /** @type {Writing} */ (
154
135
  text === undefined ? { missing: true } : { text, source: "own" }
@@ -192,7 +173,7 @@ export function writingsOf(data, map, font) {
192
173
  const endingText = (ending) =>
193
174
  nativeText(
194
175
  listOf(endings).filter((one) => isMap(one) && one.ending === ending),
195
- map,
176
+ enc,
196
177
  font,
197
178
  );
198
179
 
@@ -205,8 +186,18 @@ export function writingsOf(data, map, font) {
205
186
  const word = (written) => {
206
187
  const folded = foldWord(written);
207
188
  const none = new Set();
208
- const stated = indexed.filter(({ word: one }) => foldWord(one.form) === folded);
209
- if (stated.length) return oneOf(written, candidatesOf(stated, [], none));
189
+ const stages = [
190
+ indexed.filter(({ word: one }) => one.form === written),
191
+ indexed.filter(({ word: one }) => listOf(one.variants).includes(written)),
192
+ indexed.filter(({ word: one }) => foldWord(one.form) === folded),
193
+ indexed.filter(({ word: one }) =>
194
+ listOf(one.variants).some(
195
+ (form) => typeof form === "string" && foldWord(form) === folded,
196
+ ),
197
+ ),
198
+ ];
199
+ for (const stated of stages)
200
+ if (stated.length) return oneOf(written, candidatesOf(stated, [], none));
210
201
  const stripped = indexed.filter(({ word: one }) => bare(foldWord(one.form)) === folded);
211
202
  if (stripped.length) return oneOf(written, candidatesOf(stripped, [], none));
212
203
  const derived = [];
@@ -43,7 +43,7 @@ function shortcodeErrors(body) {
43
43
  * @param {import("./content-session.mjs").ContentSyntax} [syntax] - Exact-text scanner projections.
44
44
  * @param {import("./content-scripts.mjs").RunContext} [runs] - The package's
45
45
  * languages, faces and maps. Given, text marked with `lang=`, `font=` and
46
- * `map=` is checked against them; a caller with no package configuration
46
+ * `enc=` is checked against them; a caller with no package configuration
47
47
  * leaves it out.
48
48
  * @returns {Array<{file: string, line: number, column: number, severity: string, message: string}>} Syntax errors,
49
49
  * then warnings.
@@ -140,6 +140,7 @@ import {
140
140
  } from "./language-words.mjs";
141
141
  import { checkGlossary } from "./glossary-entries.mjs";
142
142
  import { GUIDE_SOUND_FIELDS, LANGUAGE_DATA_FIELDS } from "./language-terms.mjs";
143
+ import { ENCODING_NOTE_FIELDS } from "./encoding-terms.mjs";
143
144
  import { checkGuideSounds, checkLanguageData } from "./language-data.mjs";
144
145
  import { checkAffiliationReach } from "./culture-reach.mjs";
145
146
  import { checkLanguageNames, requiredGivenGenders } from "./language-names.mjs";
@@ -159,11 +160,9 @@ import { checkCitedPopulations, checkPopulation } from "./populations.mjs";
159
160
  import { MARKET_CLASSES, checkMarket } from "./market-class.mjs";
160
161
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
161
162
  import { checkSettlementTables } from "./settlement-tables.mjs";
162
- import { checkMapField } from "./text-maps.mjs";
163
163
  import { checkConceptList, checkCoreFlag } from "./core-vocabulary.mjs";
164
164
  import { METRE_KINDS, checkMetresField } from "./metre-declaration.mjs";
165
165
  import { checkLanguageEmphasis, checkScriptField } from "./script-emphasis.mjs";
166
- import { WRITTEN_IN_KEY, checkWrittenIn } from "./written-in.mjs";
167
166
  import {
168
167
  EMPHASIS_CONVENTIONS,
169
168
  LANGUAGE_EMPHASES,
@@ -1591,47 +1590,6 @@ export const BEING_ARCHETYPES = Object.freeze({
1591
1590
  guildsperson: "Has professional training, standing, or connections that open doors.",
1592
1591
  });
1593
1592
 
1594
- /**
1595
- * One row of a mapping note's `data.map.rows` or of a face's own `rows`.
1596
- *
1597
- * @type {object}
1598
- */
1599
- const MAP_ROW_ENTRY = Object.freeze({
1600
- kind: "list-or-map",
1601
- shape: "`[from, to]` or `{ from, to, before?, at? }`",
1602
- entries: Object.freeze({ ...TEXT, shape: "a string" }),
1603
- fields: Object.freeze([
1604
- {
1605
- name: "from",
1606
- ...TEXT,
1607
- shape: "a string of one or more characters",
1608
- required: true,
1609
- describe: "The one or more characters the row matches.",
1610
- },
1611
- {
1612
- name: "to",
1613
- ...TEXT,
1614
- shape: "a string, which may be empty",
1615
- required: true,
1616
- allowEmpty: true,
1617
- describe: "What the row writes in their place; empty to drop them.",
1618
- },
1619
- {
1620
- name: "before",
1621
- ...TEXT,
1622
- allowed: Object.freeze(["vowel", "consonant"]),
1623
- describe:
1624
- "`vowel` or `consonant`: what must follow the match for the " + "row to apply.",
1625
- },
1626
- {
1627
- name: "at",
1628
- ...TEXT,
1629
- allowed: Object.freeze(["start", "end"]),
1630
- describe: "`start` or `end`: where the match must sit for the row to " + "apply.",
1631
- },
1632
- ]),
1633
- });
1634
-
1635
1593
  /**
1636
1594
  * Every note type this toolchain compiles, and the closed vocabulary it
1637
1595
  * declares.
@@ -2187,36 +2145,10 @@ export const NOTE_VOCABULARY = Object.freeze({
2187
2145
  "and `words` marks it with a word, so either is a finding. Only a skill of " +
2188
2146
  "`subType: language` states it.",
2189
2147
  },
2190
- {
2191
- name: WRITTEN_IN_KEY,
2192
- kind: "map",
2193
- shape: "a map — `{ map, font }`",
2194
- check: checkWrittenIn,
2195
- fields: Object.freeze([
2196
- {
2197
- name: "map",
2198
- kind: "string",
2199
- shape: "a mapping note's `data.map.name`",
2200
- required: true,
2201
- describe: "The map the language's text is written by.",
2202
- },
2203
- {
2204
- name: "font",
2205
- kind: "string",
2206
- shape: "an installed face's slug",
2207
- required: true,
2208
- describe: "The face the text is set in, one the map is drawn in.",
2209
- },
2210
- ]),
2211
- describe:
2212
- "The script the language is written in, as the default map and face selected " +
2213
- "by a run with explicit `lang=` and `.nativefont`; a run's own `map=` or " +
2214
- "`font=` overrides its half. Only a skill of `subType: language` states it.",
2215
- },
2216
2148
  {
2217
2149
  name: "script",
2218
2150
  kind: "map",
2219
- shape: "a map — `{ fonts, emphasis, strongEmphasis?, direction? }`",
2151
+ shape: "a map — `{ fonts, emphasis, direction? }`",
2220
2152
  check: checkScriptField,
2221
2153
  fields: Object.freeze([
2222
2154
  {
@@ -2240,17 +2172,10 @@ export const NOTE_VOCABULARY = Object.freeze({
2240
2172
  required: true,
2241
2173
  describe:
2242
2174
  "How the script marks a stressed word: `rubric` sets it in red, " +
2243
- "`overline` draws a line above it. It sets `*…*` and `**…**` inside a " +
2175
+ "`overline` draws a line above it. It sets `.emph` and accepted English emphasis inside a " +
2244
2176
  "run in one of its faces, unless the run's language states its own " +
2245
2177
  "`emphasis`.",
2246
2178
  },
2247
- {
2248
- name: "strongEmphasis",
2249
- kind: "string",
2250
- shape: "`rubric` or `overline`",
2251
- allowed: EMPHASIS_CONVENTIONS,
2252
- describe: "The convention `**…**` takes instead of `emphasis`'s.",
2253
- },
2254
2179
  {
2255
2180
  name: "direction",
2256
2181
  kind: "string",
@@ -2355,6 +2280,7 @@ export const NOTE_VOCABULARY = Object.freeze({
2355
2280
  ...checkPostFields(note),
2356
2281
  ],
2357
2282
  data: Object.freeze([
2283
+ ...ENCODING_NOTE_FIELDS,
2358
2284
  CONCEPTS_FIELD,
2359
2285
  POST_DATE_FIELD,
2360
2286
  POST_AUTHOR_FIELD,
@@ -2375,86 +2301,6 @@ export const NOTE_VOCABULARY = Object.freeze({
2375
2301
  "tongue of the setting: each letter row's `tongue` names the language notes " +
2376
2302
  "that write it.",
2377
2303
  },
2378
- {
2379
- name: "map",
2380
- kind: "map",
2381
- shape: "a map — `{ name, logographic?, vowels?, fonts, rows }`",
2382
- check: checkMapField,
2383
- fields: Object.freeze([
2384
- {
2385
- name: "name",
2386
- ...TEXT,
2387
- required: true,
2388
- describe:
2389
- "What `map=` names the map by: lowercase letters, digits and `-`, " +
2390
- "unique in the package.",
2391
- },
2392
- {
2393
- name: "logographic",
2394
- kind: "boolean",
2395
- shape: "`true`",
2396
- describe:
2397
- "The script writes meanings: a mapped run's words are written by " +
2398
- "the language's lexicon alone, and the rows write only the text " +
2399
- "between them.",
2400
- },
2401
- {
2402
- name: "vowels",
2403
- ...TEXT,
2404
- describe:
2405
- "The characters a row's `before: vowel` reads as vowels; any other " +
2406
- "letter is a consonant.",
2407
- },
2408
- {
2409
- name: "rows",
2410
- kind: "list",
2411
- shape: "list of `[from, to]` or `{ from, to, before?, at? }`",
2412
- required: true,
2413
- entries: MAP_ROW_ENTRY,
2414
- describe:
2415
- "The rewrites, each `[from, to]` or `{ from, to, before?, at? }`: " +
2416
- "`before` is `vowel` or `consonant`, `at` is `start` or `end`. The " +
2417
- "longest `from` wins, and a character no row matches passes through.",
2418
- },
2419
- {
2420
- name: "fonts",
2421
- kind: "list",
2422
- shape: "list of `{ font, rows? }`",
2423
- required: true,
2424
- entries: Object.freeze({
2425
- kind: "map",
2426
- shape: "a map — `{ font, rows? }`",
2427
- fields: Object.freeze([
2428
- {
2429
- name: "font",
2430
- ...TEXT,
2431
- required: true,
2432
- shape: "an installed face's slug",
2433
- describe:
2434
- "A face the map is drawn in, by the slug `font=` names it by.",
2435
- },
2436
- {
2437
- name: "rows",
2438
- kind: "list",
2439
- shape: "list of `[from, to]` or `{ from, to, before?, at? }`",
2440
- entries: MAP_ROW_ENTRY,
2441
- describe:
2442
- "Rows for this face alone, tried before the shared `rows`; " +
2443
- "the same shapes and conditions.",
2444
- },
2445
- ]),
2446
- }),
2447
- describe:
2448
- "The faces the map is drawn in, at least one, each with any rows of " +
2449
- "its own. A run in another face is refused, and each face must draw " +
2450
- "every character its rows and the lexicon's native forms write.",
2451
- },
2452
- ]),
2453
- describe:
2454
- "A mapping, which native text marked with `lang=`, `.nativefont` and its " +
2455
- "`map=` name is rewritten by before it is set. See " +
2456
- "[mapping notes](format-details.md#mapping-notes).",
2457
- },
2458
2304
  {
2459
2305
  name: "metres",
2460
2306
  kind: "list",
@@ -318,12 +318,24 @@ function reportUnrenderable(source, definitions, opts, original) {
318
318
  if (!findings) return;
319
319
  const file = opts.file ? { file: opts.file } : {};
320
320
  /**
321
- * @param {{line: number, column?: number}} position - Where it is.
321
+ * @param {{file?: string, line: number, column?: number}} position - Where it is.
322
322
  * @param {"warning"|"error"} severity - Which level.
323
323
  * @param {string} message - What is wrong.
324
324
  * @returns {void}
325
325
  */
326
326
  const report = (position, severity, message) => {
327
+ if (
328
+ position.file &&
329
+ position.file !== opts.file &&
330
+ findings.some(
331
+ (one) =>
332
+ one.file === position.file &&
333
+ one.line === position.line &&
334
+ one.column === position.column &&
335
+ one.message === message,
336
+ )
337
+ )
338
+ return;
327
339
  findings.push({ ...file, ...position, severity, message });
328
340
  };
329
341
 
@@ -357,7 +369,13 @@ function reportUnrenderable(source, definitions, opts, original) {
357
369
  ...scanRuns(source, opts.runs).errors,
358
370
  ...markupAnchorFindings(source),
359
371
  ])
360
- report(linePosition(error.line, error.column, opts), "error", error.message);
372
+ report(
373
+ error.file ?
374
+ { file: error.file, line: error.line, column: error.column }
375
+ : linePosition(error.line, error.column, opts),
376
+ "error",
377
+ error.message,
378
+ );
361
379
  for (const error of scanFigures(source).errors)
362
380
  report(linePosition(error.line, error.column, opts), "error", error.message);
363
381
  // An image sharing its paragraph with other text, or an address or title
@@ -450,7 +468,7 @@ function reportUnrenderable(source, definitions, opts, original) {
450
468
  * finding.
451
469
  * @param {import("./content-scripts.mjs").RunContext} [opts.runs] - The
452
470
  * package's languages, faces and maps, which text marked with `lang=`,
453
- * `font=` and `map=` is checked against and set by.
471
+ * `font=` and `enc=` is checked against and set by.
454
472
  * @param {number} [opts.bodyLine] - The 1-based file line the body starts on, so
455
473
  * a finding's line is the file's rather than the body's.
456
474
  * @param {number} [opts.bodyColumn] - The 1-based column the body starts at.
@@ -1413,6 +1431,16 @@ function renderInline(token, ctx) {
1413
1431
  out.push(inlineRaw(child.content));
1414
1432
  break;
1415
1433
  case "span_open": {
1434
+ if (["em", "strong"].includes(child.tag)) {
1435
+ const classes = (child.attrGet?.("class") ?? "").split(/\s+/);
1436
+ out.push(
1437
+ classes.includes("emph-rubric") ?
1438
+ `#text(fill: ${ctx.textFill ?? "book-rubric"})[`
1439
+ : classes.includes("emph-overline") ? "#overline["
1440
+ : `#${child.tag === "em" ? "emph" : "strong"}[`,
1441
+ );
1442
+ break;
1443
+ }
1416
1444
  const sign = renderGlyph(children, i, ctx) ?? renderCartouche(children, i, ctx);
1417
1445
  if (sign) {
1418
1446
  out.push(sign.typst);