@heroiclands/package-build 24.1.1 → 25.1.0

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 (107) hide show
  1. package/CHANGELOG.md +36 -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 +154 -11
  9. package/docs/authoring/links-and-markup.md +260 -473
  10. package/docs/commands.md +118 -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 +135 -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 +453 -569
  19. package/docs/reference/format-details.md +290 -380
  20. package/docs/reference/note-types.md +80 -62
  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 +21 -1
  25. package/engine/content-format.yaml +7 -2
  26. package/engine/content-index.mjs +6 -2
  27. package/engine/content-links.mjs +1 -0
  28. package/engine/content-scripts.mjs +658 -423
  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 +699 -0
  37. package/engine/encoding-notes.mjs +186 -0
  38. package/engine/encoding-standard.mjs +42 -0
  39. package/engine/encoding-template.mjs +313 -0
  40. package/engine/encoding-terms.mjs +141 -0
  41. package/engine/encoding.mjs +946 -0
  42. package/engine/extension-attributes.mjs +2 -0
  43. package/engine/helpers.mjs +1 -1
  44. package/engine/index.mjs +10 -0
  45. package/engine/language-data.mjs +4 -1
  46. package/engine/language-terms.mjs +23 -12
  47. package/engine/{logographic.mjs → language-writings.mjs} +29 -38
  48. package/engine/markup-validation.mjs +1 -1
  49. package/engine/note-vocabulary.mjs +4 -158
  50. package/engine/pdf-render.mjs +31 -3
  51. package/engine/roman-characters.mjs +232 -0
  52. package/engine/roman-run.mjs +149 -0
  53. package/engine/roman-verse.mjs +95 -0
  54. package/engine/run-rows.mjs +29 -31
  55. package/engine/run-words.mjs +2 -2
  56. package/engine/script-emphasis-terms.mjs +3 -0
  57. package/engine/script-emphasis.mjs +3 -10
  58. package/engine/script-font-delivery.mjs +9 -12
  59. package/engine/script-fonts.mjs +2 -1
  60. package/engine/site-build.mjs +14 -5
  61. package/engine/site-preview.mjs +12 -2
  62. package/engine/sql-tables.mjs +36 -31
  63. package/engine/verse-metre.mjs +1 -1
  64. package/engine/word-forms.mjs +1 -1
  65. package/engine/word-inflections.mjs +48 -0
  66. package/engine/word-rows.mjs +1 -1
  67. package/engine/word-terms.mjs +16 -17
  68. package/package.json +11 -5
  69. package/types/config.d.mts +5 -0
  70. package/types/convert.d.mts +44 -0
  71. package/types/e2e.d.mts +1 -1
  72. package/types/engine/base-compiler.d.mts +4 -2
  73. package/types/engine/content-charset.d.mts +8 -0
  74. package/types/engine/content-index.d.mts +4 -1
  75. package/types/engine/content-scripts.d.mts +108 -42
  76. package/types/engine/converter-data.d.mts +11 -0
  77. package/types/engine/dictionary-rows.d.mts +1 -25
  78. package/types/engine/encoding-integrity.d.mts +14 -0
  79. package/types/engine/encoding-language.d.mts +36 -0
  80. package/types/engine/encoding-lexicon.d.mts +259 -0
  81. package/types/engine/encoding-notes.d.mts +39 -0
  82. package/types/engine/encoding-standard.d.mts +11 -0
  83. package/types/engine/encoding-template.d.mts +43 -0
  84. package/types/engine/encoding-terms.d.mts +105 -0
  85. package/types/engine/encoding.d.mts +155 -0
  86. package/types/engine/helpers.d.mts +1 -1
  87. package/types/engine/index.d.mts +5 -0
  88. package/types/engine/language-terms.d.mts +1 -1
  89. package/types/engine/{logographic.d.mts → language-writings.d.mts} +6 -6
  90. package/types/engine/markup-validation.d.mts +1 -1
  91. package/types/engine/pdf-render.d.mts +1 -1
  92. package/types/engine/roman-characters.d.mts +45 -0
  93. package/types/engine/roman-run.d.mts +101 -0
  94. package/types/engine/roman-verse.d.mts +23 -0
  95. package/types/engine/run-rows.d.mts +13 -20
  96. package/types/engine/script-emphasis-terms.d.mts +2 -0
  97. package/types/engine/script-emphasis.d.mts +1 -1
  98. package/types/engine/script-font-delivery.d.mts +2 -3
  99. package/types/engine/script-fonts.d.mts +5 -0
  100. package/types/engine/site-preview.d.mts +1 -1
  101. package/types/engine/word-inflections.d.mts +25 -0
  102. package/engine/map-lexicon.mjs +0 -1034
  103. package/engine/text-maps.mjs +0 -654
  104. package/engine/written-in.mjs +0 -204
  105. package/types/engine/map-lexicon.d.mts +0 -330
  106. package/types/engine/text-maps.d.mts +0 -189
  107. 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
@@ -227,6 +227,9 @@ export * as contentLint from "./content-lint.mjs";
227
227
 
228
228
  /** The charset authored content is held to, so a book can choose its face. */
229
229
  export * as contentCharset from "./content-charset.mjs";
230
+
231
+ /** Exact permitted Roman source characters for configured languages. */
232
+ export * as romanCharacters from "./roman-characters.mjs";
230
233
  export * as pathCharset from "./path-charset.mjs";
231
234
 
232
235
  /** Naming an interface icon in a note, without drawing one there. */
@@ -328,3 +331,10 @@ export {
328
331
 
329
332
  /** The curated region-event vocabulary, shared with the Foundry runtime. */
330
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";
@@ -73,6 +73,7 @@ import {
73
73
  import { LANGUAGE_SUBTYPE } from "./word-terms.mjs";
74
74
  import { WORD_CLASS_BASES } from "./word-classes.mjs";
75
75
  import { nearest } from "./near-miss.mjs";
76
+ import { romanInventoryProblems } from "./roman-characters.mjs";
76
77
 
77
78
  /** The bases a frame's `class` names. */
78
79
  const CLASS_BASES = new Set(WORD_CLASS_BASES.map((item) => item.token));
@@ -614,7 +615,9 @@ export function checkLanguageData(note, { index, types } = {}) {
614
615
  ),
615
616
  );
616
617
 
617
- const findings = [];
618
+ const findings = romanInventoryProblems(data.roman).map((message) =>
619
+ finding(["data", "roman"], "error", message),
620
+ );
618
621
 
619
622
  // Only the shape of a supplied declaration: no word is compared with it.
620
623
  const grammar = isMap(data.grammar) ? data.grammar : {};
@@ -30,6 +30,7 @@
30
30
 
31
31
  /** The `data` keys a language note states its sounds, names and history in. */
32
32
  export const LANGUAGE_DATA_KEYS = Object.freeze([
33
+ "roman",
33
34
  "tongues",
34
35
  "register",
35
36
  "retired",
@@ -1695,7 +1696,7 @@ export const VERB_PERSON_FIELDS = Object.freeze(
1695
1696
  );
1696
1697
 
1697
1698
  /**
1698
- * The endings `data.grammar.native` writes under a logographic map: the noun
1699
+ * The endings `data.grammar.native` writes under a encoding: the noun
1699
1700
  * plural, then the six persons of the verb pattern.
1700
1701
  */
1701
1702
  export const WRITTEN_ENDINGS = Object.freeze([
@@ -1716,18 +1717,18 @@ const GRAMMAR_NATIVE_FIELDS = Object.freeze([
1716
1717
  "for a verb's present form.",
1717
1718
  }),
1718
1719
  Object.freeze({
1719
- name: "map",
1720
+ name: "enc",
1720
1721
  kind: "string",
1721
1722
  required: true,
1722
- shape: "a logographic mapping note's `data.map.name`",
1723
- describe: "The logographic map the ending is written for.",
1723
+ shape: "an encoding document Address",
1724
+ describe: "The encoding the ending is written for.",
1724
1725
  }),
1725
1726
  Object.freeze({
1726
1727
  name: "font",
1727
1728
  kind: "string",
1728
- shape: "a face the map lists",
1729
+ shape: "a face the encoding lists",
1729
1730
  describe:
1730
- "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, " +
1731
1732
  "and a writing stated for a face wins over it there.",
1732
1733
  }),
1733
1734
  Object.freeze({
@@ -1869,15 +1870,15 @@ const GRAMMAR_FIELDS = Object.freeze([
1869
1870
  Object.freeze({
1870
1871
  name: "native",
1871
1872
  kind: "list",
1872
- shape: "a list of `{ ending, map, font?, text }`",
1873
+ shape: "a list of `{ ending, enc, font?, text }`",
1873
1874
  entries: Object.freeze({
1874
1875
  kind: "map",
1875
- shape: "a map — `{ ending, map, font?, text }`",
1876
+ shape: "a map — `{ ending, enc, font?, text }`",
1876
1877
  fields: GRAMMAR_NATIVE_FIELDS,
1877
1878
  }),
1878
1879
  describe:
1879
- "Under a logographic map, the signs a noun's plural or a verb's present form is " +
1880
- "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.",
1881
1882
  }),
1882
1883
  ]);
1883
1884
 
@@ -1889,6 +1890,16 @@ const GRAMMAR_FIELDS = Object.freeze([
1889
1890
  * @type {readonly object[]}
1890
1891
  */
1891
1892
  export const LANGUAGE_DATA_FIELDS = Object.freeze([
1893
+ Object.freeze({
1894
+ name: "roman",
1895
+ kind: "string",
1896
+ shape: "a literal NFC string of additional source characters, including exact letter case and digits",
1897
+ describe:
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.",
1902
+ }),
1892
1903
  Object.freeze({
1893
1904
  name: "grammar",
1894
1905
  kind: "map",
@@ -1896,8 +1907,8 @@ export const LANGUAGE_DATA_FIELDS = Object.freeze([
1896
1907
  fields: GRAMMAR_FIELDS,
1897
1908
  describe:
1898
1909
  "Optional noun plurals, one verb pattern, agreement, participles and the endings' " +
1899
- "logographic writings, for dictionary presentation, the mapped-run word check and " +
1900
- "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 " +
1901
1912
  "checked; no word must follow or state a rule. A language skill note only.",
1902
1913
  }),
1903
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);