@heroiclands/package-build 23.12.6 → 23.12.7

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 (83) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/assets/styles/content-base.css +25 -0
  3. package/bin/content-commands.mjs +264 -3
  4. package/bin/package-build.mjs +25 -0
  5. package/content-config.mjs +72 -0
  6. package/docs/api.md +10 -9
  7. package/docs/authoring/links-and-markup.md +439 -47
  8. package/docs/commands.md +127 -0
  9. package/docs/configuration.md +66 -3
  10. package/docs/content-format.md +3 -1
  11. package/docs/guides/book.md +11 -0
  12. package/docs/guides/module-project.md +16 -0
  13. package/docs/guides/site.md +19 -0
  14. package/docs/reference/format-details.md +372 -6
  15. package/docs/reference/note-types.md +36 -11
  16. package/engine/base-compiler.mjs +2 -0
  17. package/engine/bundles.mjs +7 -2
  18. package/engine/compile-corpus.mjs +3 -1
  19. package/engine/content-charset.mjs +21 -2
  20. package/engine/content-format.yaml +1 -0
  21. package/engine/content-index.mjs +12 -0
  22. package/engine/content-poetry.mjs +22 -4
  23. package/engine/content-scripts.mjs +1219 -0
  24. package/engine/content-session.mjs +2 -0
  25. package/engine/data-keys.mjs +5 -1
  26. package/engine/dictionary-layout.mjs +18 -12
  27. package/engine/dictionary-rows.mjs +11 -2
  28. package/engine/helpers.mjs +8 -2
  29. package/engine/index-records.mjs +1 -0
  30. package/engine/journals.mjs +14 -3
  31. package/engine/language-data.mjs +137 -1
  32. package/engine/language-tag.mjs +103 -0
  33. package/engine/language-terms.mjs +16 -4
  34. package/engine/map-lexicon.mjs +762 -0
  35. package/engine/markup-validation.mjs +17 -3
  36. package/engine/names.mjs +1 -1
  37. package/engine/note-vocabulary.mjs +113 -0
  38. package/engine/pdf-build.mjs +28 -10
  39. package/engine/pdf-render.mjs +116 -11
  40. package/engine/run-rows.mjs +119 -0
  41. package/engine/run-words.mjs +266 -0
  42. package/engine/scenes.mjs +1 -0
  43. package/engine/script-font-delivery.mjs +321 -0
  44. package/engine/script-fonts.mjs +172 -0
  45. package/engine/site-build.mjs +10 -3
  46. package/engine/site-config.mjs +5 -0
  47. package/engine/site-root.mjs +19 -5
  48. package/engine/sql-tables.mjs +111 -1
  49. package/engine/text-maps.mjs +607 -0
  50. package/engine/verb-pattern.mjs +99 -0
  51. package/engine/word-pattern.mjs +27 -0
  52. package/engine/word-rows.mjs +8 -2
  53. package/engine/word-terms.mjs +44 -0
  54. package/hugo-theme/layouts/_default/baseof.html +3 -0
  55. package/hugo-theme/static/css/style.css +10 -0
  56. package/manifest.mjs +9 -1
  57. package/package.json +1 -1
  58. package/types/content-config.d.mts +26 -0
  59. package/types/engine/compile-corpus.d.mts +2 -1
  60. package/types/engine/content-index.d.mts +11 -3
  61. package/types/engine/content-poetry.d.mts +7 -0
  62. package/types/engine/content-scripts.d.mts +226 -0
  63. package/types/engine/data-keys.d.mts +2 -2
  64. package/types/engine/dictionary-rows.d.mts +15 -0
  65. package/types/engine/helpers.d.mts +5 -1
  66. package/types/engine/journals.d.mts +7 -2
  67. package/types/engine/language-data.d.mts +2 -0
  68. package/types/engine/language-tag.d.mts +8 -0
  69. package/types/engine/language-terms.d.mts +2 -1
  70. package/types/engine/map-lexicon.d.mts +274 -0
  71. package/types/engine/markup-validation.d.mts +7 -2
  72. package/types/engine/pdf-build.d.mts +26 -3
  73. package/types/engine/pdf-render.d.mts +17 -2
  74. package/types/engine/run-rows.d.mts +89 -0
  75. package/types/engine/run-words.d.mts +86 -0
  76. package/types/engine/script-font-delivery.d.mts +134 -0
  77. package/types/engine/script-fonts.d.mts +168 -0
  78. package/types/engine/site-build.d.mts +2 -1
  79. package/types/engine/site-root.d.mts +11 -3
  80. package/types/engine/text-maps.d.mts +175 -0
  81. package/types/engine/verb-pattern.d.mts +65 -0
  82. package/types/engine/word-pattern.d.mts +13 -0
  83. package/types/engine/word-terms.d.mts +6 -0
@@ -0,0 +1,99 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **A language's one regular verb pattern, read once for every reader.** A
16
+ * language's `data.grammar.verbs` is `{ invariant: true }` or six `persons`
17
+ * endings, optionally with `replaces`. A present form is the citation form
18
+ * with the ending added; where the pattern states `replaces`, a citation form
19
+ * closing on those letters has them swapped for the ending, and one closing
20
+ * otherwise takes the ending whole, as a suffix's `replaces` does.
21
+ *
22
+ * The dictionary's verb table and a mapped run's lexicon check both read the
23
+ * pattern through {@link verbPattern} and build forms with {@link verbForm}.
24
+ *
25
+ * @module
26
+ */
27
+
28
+ import { VERB_PERSON_FIELDS } from "./language-terms.mjs";
29
+
30
+ /** Whether a value is a plain map. */
31
+ const isMap = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
32
+
33
+ /**
34
+ * A stated verb pattern, as its readers use it.
35
+ *
36
+ * @typedef {object} VerbPattern
37
+ * @property {boolean} invariant - Whether verbs keep the citation form for
38
+ * every person.
39
+ * @property {string|undefined} replaces - The letters each ending takes the
40
+ * place of, where the pattern states them.
41
+ * @property {Array<{name: string, label: string, ending: string}>} endings -
42
+ * The six endings in presentation order; empty for an invariant pattern.
43
+ */
44
+
45
+ /**
46
+ * Read a language's verb pattern.
47
+ *
48
+ * @param {unknown} verbs - The language's `data.grammar.verbs`, as authored or
49
+ * as a SQL row carries it (absent fields `null`).
50
+ * @returns {VerbPattern|undefined} The pattern, or `undefined` where none is
51
+ * stated in full.
52
+ */
53
+ export function verbPattern(verbs) {
54
+ if (!isMap(verbs)) return undefined;
55
+ const replaces =
56
+ typeof verbs.replaces === "string" && verbs.replaces.trim() ? verbs.replaces : undefined;
57
+ if (verbs.invariant === true) return { invariant: true, replaces, endings: [] };
58
+ const persons = isMap(verbs.persons) ? verbs.persons : undefined;
59
+ if (!persons || !VERB_PERSON_FIELDS.every(({ name }) => typeof persons[name] === "string"))
60
+ return undefined;
61
+ return {
62
+ invariant: false,
63
+ replaces,
64
+ endings: VERB_PERSON_FIELDS.map(({ name, label }) => ({
65
+ name,
66
+ label,
67
+ ending: persons[name],
68
+ })),
69
+ };
70
+ }
71
+
72
+ /**
73
+ * A stem with its closing letters swapped for an ending, or with the ending
74
+ * added where the stem does not close on them.
75
+ *
76
+ * @param {string} stem - The stem.
77
+ * @param {string} ending - The ending.
78
+ * @param {string|undefined} letters - The closing letters the ending takes
79
+ * the place of; none adds the ending.
80
+ * @returns {string} The form.
81
+ */
82
+ export function replaceClosing(stem, ending, letters) {
83
+ return letters && stem.endsWith(letters) ?
84
+ stem.slice(0, -letters.length) + ending
85
+ : stem + ending;
86
+ }
87
+
88
+ /**
89
+ * The present form of a verb for one ending.
90
+ *
91
+ * @param {string} citation - The citation form.
92
+ * @param {string} ending - The person's ending; empty leaves the citation
93
+ * form, less any closing `replaces`.
94
+ * @param {string} [replaces] - The pattern's `replaces`.
95
+ * @returns {string} The form.
96
+ */
97
+ export function verbForm(citation, ending, replaces) {
98
+ return replaceClosing(citation, ending, replaces);
99
+ }
@@ -0,0 +1,27 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **What a word is in running text.** A word is a maximal run of letters,
16
+ * combining marks and digits, and an apostrophe (`'` or `’`), a hyphen or an
17
+ * en dash joins two such runs into one word: `k'aralzon`, `Arzen-Thalor`.
18
+ * Anything else, whitespace and punctuation included, sits between words.
19
+ *
20
+ * The name catalog reads a label's last word by it, and a mapped run reads
21
+ * each word it looks up in a lexicon by it.
22
+ *
23
+ * @module
24
+ */
25
+
26
+ /** One word; global, so `matchAll` walks every word of a text. */
27
+ export const WORD = /[\p{L}\p{M}\p{N}]+(?:[’'–-][\p{L}\p{M}\p{N}]+)*/gu;
@@ -47,6 +47,12 @@ import {
47
47
  glossText,
48
48
  } from "./word-terms.mjs";
49
49
 
50
+ /**
51
+ * The word's own keys a word row carries: all but `native`, whose entries are
52
+ * rows of the `natives` relation.
53
+ */
54
+ const WORD_ROW_KEYS = Object.freeze(WORD_KEYS.filter((key) => key !== "native"));
55
+
50
56
  /** The columns a word row carries before the word's own keys. */
51
57
  const WORD_IDENTITY_COLUMNS = Object.freeze(["language", "position"]);
52
58
 
@@ -56,7 +62,7 @@ const WORD_DERIVED_COLUMNS = Object.freeze(["built_text", "built_parts", "senses
56
62
  /** Every column of the `words` relation, in order. */
57
63
  export const WORD_COLUMNS = Object.freeze([
58
64
  ...WORD_IDENTITY_COLUMNS,
59
- ...WORD_KEYS,
65
+ ...WORD_ROW_KEYS,
60
66
  ...WORD_DERIVED_COLUMNS,
61
67
  ]);
62
68
 
@@ -260,7 +266,7 @@ export function wordRows(records, all = records) {
260
266
  words.forEach((entry, position) => {
261
267
  if (!isMap(entry)) return;
262
268
  const row = { language, position };
263
- for (const key of WORD_KEYS) row[key] = entry[key] ?? null;
269
+ for (const key of WORD_ROW_KEYS) row[key] = entry[key] ?? null;
264
270
  if (row.name_sort === null) row.name_sort = row.form;
265
271
  const built = Array.isArray(entry.built) ? entry.built.map(String) : [];
266
272
  row.built_text = built.length ? built.join(" + ") : null;
@@ -99,6 +99,37 @@ export const WORD_SOURCE_FIELDS = Object.freeze([
99
99
  }),
100
100
  ]);
101
101
 
102
+ /**
103
+ * The keys of one entry of a word's `native`.
104
+ *
105
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
106
+ */
107
+ export const WORD_NATIVE_FIELDS = Object.freeze([
108
+ Object.freeze({
109
+ name: "map",
110
+ kind: "string",
111
+ required: true,
112
+ shape: "a mapping note's `data.map.name`",
113
+ describe: "The map the native form is written for.",
114
+ }),
115
+ Object.freeze({
116
+ name: "font",
117
+ kind: "string",
118
+ shape: "a face the map lists",
119
+ describe:
120
+ "The one face the native form is for; unstated, it is for every face the map lists, " +
121
+ "and a form stated for a face wins over it there.",
122
+ }),
123
+ Object.freeze({
124
+ name: "text",
125
+ kind: "string",
126
+ required: true,
127
+ shape: "the word in the script, as the map's characters",
128
+ describe:
129
+ "The word as the script writes it, where the map's rows would write it otherwise.",
130
+ }),
131
+ ]);
132
+
102
133
  /**
103
134
  * The keys one word of a language note's `data.words` carries.
104
135
  *
@@ -209,6 +240,19 @@ export const WORD_FIELDS = Object.freeze([
209
240
  shape: "a frame of the language's `sounds.frames`",
210
241
  describe: "The frame a root is set in, named in the language's `sounds.frames`.",
211
242
  }),
243
+ Object.freeze({
244
+ name: "native",
245
+ kind: "list",
246
+ shape: "a list of `{ map, font?, text }`",
247
+ entries: Object.freeze({
248
+ kind: "map",
249
+ shape: "a map — `{ map, font?, text }`",
250
+ fields: WORD_NATIVE_FIELDS,
251
+ }),
252
+ describe:
253
+ "The word's native form under a map, stated only where the map's rows spell it " +
254
+ "differently. Its derived forms are spelled by the rows.",
255
+ }),
212
256
  ]);
213
257
 
214
258
  /** The keys of {@link WORD_FIELDS}, in declared order. */
@@ -9,6 +9,9 @@
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Lora:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600&display=swap" rel="stylesheet">
11
11
  <link rel="stylesheet" href="{{ "css/style.css" | relURL }}">
12
+ {{/* Script fonts: the site build sets params.scriptFonts only when it has
13
+ copied them. */}}
14
+ {{ with .Site.Params.scriptFonts }}<link rel="stylesheet" href="{{ . | relURL }}">{{ end }}
12
15
  {{/* Icon glyphs. SoHL documentation writes icons as <i class="fa-solid …">
13
16
  or <i class="ginf-…">, the same markup the Foundry client renders, so a
14
17
  page reads identically in a journal and here. Font Awesome's FREE set is
@@ -3022,3 +3022,13 @@ summary.info-sidebar-title::after {
3022
3022
  .single-body .dictionary > div > dd > :is(p, ul, ol, dl, blockquote, pre, table, div) { text-indent:0; }
3023
3023
  .single-body .dictionary > div > dd > p:first-child { display:inline; }
3024
3024
  @media (max-width:600px) { .single-body .dictionary { --indent:100px; } }
3025
+
3026
+ /* ---- Text in one of the faces ----
3027
+ The font family and features come from the face's own class. A
3028
+ right-to-left block or verse reads from the right. `font=serif`, `sans`
3029
+ and `mono` set the site's own serif and mono, and the reader's sans. */
3030
+ .script-block { margin:1rem 0; line-height:1.7; }
3031
+ .script-block[dir="rtl"], .poetry[dir="rtl"] { text-align:start; }
3032
+ .font-serif { font-family:var(--font-body); }
3033
+ .font-sans { font-family:sans-serif; }
3034
+ .font-mono { font-family:var(--font-mono); }
package/manifest.mjs CHANGED
@@ -54,6 +54,7 @@ import fsSync from "node:fs";
54
54
  import path from "node:path";
55
55
 
56
56
  import { withBaseStyle } from "./engine/base-styles.mjs";
57
+ import { scriptFontFiles, withScriptFontStyle } from "./engine/script-font-delivery.mjs";
57
58
  import { emitDiagnostic, formatDiagnostic, positionOfYamlPath } from "./engine/diagnostics.mjs";
58
59
  import { metadataFileName } from "./engine/metadata-index.mjs";
59
60
 
@@ -571,11 +572,18 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
571
572
  // the five that do have no stylesheet of their own to name it from. See
572
573
  // `engine/base-styles.mjs` for the layer it loads into and why that is what
573
574
  // lets a package override it.
574
- const styles = withBaseStyle({
575
+ const withBase = withBaseStyle({
575
576
  declared: declared.styles,
576
577
  artifact: /** @type {"system"|"module"} */ (artifact),
577
578
  enabled: config.packageBuild?.baseStyles !== false,
578
579
  });
580
+ // The installed script fonts' stylesheet follows it, when the module
581
+ // carries them: see `engine/script-font-delivery.mjs`.
582
+ const styles = withScriptFontStyle({
583
+ styles: withBase,
584
+ artifact: /** @type {"system"|"module"} */ (artifact),
585
+ delivered: scriptFontFiles(config, ["woff2"]) !== undefined,
586
+ });
579
587
  if (styles !== undefined) merged.styles = styles;
580
588
 
581
589
  // The index every consumer resolves this package's addresses through.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "23.12.6",
3
+ "version": "23.12.7",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -703,6 +703,12 @@ export type ContentBuildConfigInput = {
703
703
  * vocabulary is their union.
704
704
  */
705
705
  itemBuilders?: Record<string, ItemBuilderEntry> | readonly ItemRegistrySpec[] | undefined;
706
+ /**
707
+ * The languages a
708
+ * note may tag text with, by short
709
+ * name. Default `{}`.
710
+ */
711
+ languages?: Record<string, LanguageSpec> | undefined;
706
712
  /**
707
713
  * Packs to compile. More than one entry
708
714
  * may share a `type`: a note then names
@@ -891,6 +897,10 @@ export type ContentBuildConfig = {
891
897
  * none.
892
898
  */
893
899
  icons: import("./engine/content-icons.mjs").IconRegistry;
900
+ /**
901
+ * Frozen; `{}` when absent.
902
+ */
903
+ languages: Readonly<Record<string, Readonly<LanguageSpec>>>;
894
904
  packs: readonly Readonly<ResolvedPackSpec>[];
895
905
  /**
896
906
  * Derived: every pack directory
@@ -918,3 +928,19 @@ export type ContentBuildConfig = {
918
928
  relationships: Readonly<Relationships>;
919
929
  publish: Readonly<PublishSwitches>;
920
930
  };
931
+ /**
932
+ * One entry of `languages`.
933
+ */
934
+ export type LanguageSpec = {
935
+ /**
936
+ * Its BCP 47 tag. A constructed language takes
937
+ * `art-x-<name>`.
938
+ */
939
+ tag: string;
940
+ /**
941
+ * The Address of its language note, whose
942
+ * `data.words` is the lexicon a mapped run in the
943
+ * language reads first.
944
+ */
945
+ note?: string | undefined;
946
+ };
@@ -16,7 +16,7 @@
16
16
  * the index refuses is a note the compile must still *report*, exactly as the
17
17
  * compile loop reported it when the loop was the first to see it.
18
18
  * @param {import("./content-session.mjs").ContentSession} [opts.session] - Captured context shared with other operations.
19
- * @returns {Promise<{currentDate: string, records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[], syntax?: import("./content-session.mjs").ContentSyntax}>} The corpus,
19
+ * @returns {Promise<{currentDate: string, runs: import("./content-scripts.mjs").RunContext, records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[], syntax?: import("./content-session.mjs").ContentSyntax}>} The corpus,
20
20
  * its indexes, and the notes it could not record.
21
21
  */
22
22
  export function buildCompileCorpus({ contentBase, assetsBase, skipDirectories, router, config, problems, session, }: {
@@ -29,6 +29,7 @@ export function buildCompileCorpus({ contentBase, assetsBase, skipDirectories, r
29
29
  session?: import("./content-session.mjs").ContentSession | undefined;
30
30
  }): Promise<{
31
31
  currentDate: string;
32
+ runs: import("./content-scripts.mjs").RunContext;
32
33
  records: object[];
33
34
  parsed: Map<string, object>;
34
35
  linkIndex: object;
@@ -81,6 +81,8 @@ export function asciiAliases(aliases: unknown): Array<string>;
81
81
  * `name` reads `Map 1` rather than `Figure 1` where it is due — see
82
82
  * {@link module:engine/content-figures.scanFigures}.
83
83
  * @param {import("./content-session.mjs").ContentSyntax} [options.syntax] - Session scanner projections.
84
+ * @param {import("./content-scripts.mjs").RunContext} [options.runContext] -
85
+ * The configured languages, which give each run's `lang=` its tag.
84
86
  * @returns {Record<string, any>} The record, keys sorted at every depth. A
85
87
  * **stub** — a note with an empty body, on a type an empty body suppresses —
86
88
  * carries `address` and `anchors` as `null`: it publishes no page, so it
@@ -92,7 +94,7 @@ export function asciiAliases(aliases: unknown): Array<string>;
92
94
  * otherwise be overwritten without a word. `file` and, where the file was
93
95
  * read, `position` ride on the error.
94
96
  */
95
- export function buildIndexRecord({ frontmatter, relPath, absPath, raw, contentPackage, body, bodyLine, bodyColumn, nameTypes, manifest, addressContext, dateContext, resolveRole, syntax, }: {
97
+ export function buildIndexRecord({ frontmatter, relPath, absPath, raw, contentPackage, body, bodyLine, bodyColumn, nameTypes, manifest, addressContext, dateContext, resolveRole, syntax, runContext, }: {
96
98
  frontmatter: Record<string, any>;
97
99
  relPath: string;
98
100
  absPath?: string | undefined;
@@ -107,12 +109,13 @@ export function buildIndexRecord({ frontmatter, relPath, absPath, raw, contentPa
107
109
  dateContext?: object | undefined;
108
110
  resolveRole?: ((address: string) => string | undefined) | undefined;
109
111
  syntax?: import("./content-session.mjs").ContentSyntax | undefined;
112
+ runContext?: import("./content-scripts.mjs").RunContext | undefined;
110
113
  }): Record<string, any>;
111
114
  /**
112
115
  * Derive every index record belonging to one parsed note.
113
116
  * @param {Parameters<typeof buildIndexRecord>[0]} options - Authored note and its context.
114
117
  */
115
- export function indexRecordsForNote({ frontmatter, body, bodyLine, bodyColumn, nameTypes, relPath, absPath, raw, contentPackage, manifest, addressContext, dateContext, resolveRole, syntax, }: Parameters<typeof buildIndexRecord>[0]): object[];
118
+ export function indexRecordsForNote({ frontmatter, body, bodyLine, bodyColumn, nameTypes, relPath, absPath, raw, contentPackage, manifest, addressContext, dateContext, resolveRole, syntax, runContext, }: Parameters<typeof buildIndexRecord>[0]): object[];
116
119
  /**
117
120
  * Write each note's resolved culture into its record, as `data.culture`.
118
121
  *
@@ -209,6 +212,8 @@ export function resolveCultures(records: Array<Record<string, any>>, { contentPa
209
212
  * calendar note's shortcode; see {@link module:engine/note-calendars}.
210
213
  * @param {string} [options.configFile] - The configuration file, where an
211
214
  * unknown `defaultCalendar` is reported.
215
+ * @param {Readonly<Record<string, {tag: string}>>} [options.languages] - The
216
+ * configured languages, which give each run's `lang=` its tag.
212
217
  * @param {(key: string) => ({culture?: unknown}|undefined)} [options.foreignRecord] -
213
218
  * Another package's entry for a canonical address, read for a parent that
214
219
  * package publishes; see {@link resolveCultures}.
@@ -221,7 +226,7 @@ export function resolveCultures(records: Array<Record<string, any>>, { contentPa
221
226
  * @returns {Array<Record<string, any>>} The records, in a total order that does
222
227
  * not depend on directory-read order.
223
228
  */
224
- export function collectContentIndex(contentBase: string, { contentPackage, currentDate, defaultCalendar, configFile, skipDirectories, assetsBase, manifest, problems, addressContext, foreignRecord, parsed, sources, syntax, nameTypes, }: {
229
+ export function collectContentIndex(contentBase: string, { contentPackage, currentDate, defaultCalendar, configFile, languages, skipDirectories, assetsBase, manifest, problems, addressContext, foreignRecord, parsed, sources, syntax, nameTypes, }: {
225
230
  contentPackage: string;
226
231
  skipDirectories: readonly string[];
227
232
  assetsBase?: string | undefined;
@@ -231,6 +236,9 @@ export function collectContentIndex(contentBase: string, { contentPackage, curre
231
236
  currentDate?: string | undefined;
232
237
  defaultCalendar?: string | undefined;
233
238
  configFile?: string | undefined;
239
+ languages?: Readonly<Record<string, {
240
+ tag: string;
241
+ }>> | undefined;
234
242
  foreignRecord?: ((key: string) => ({
235
243
  culture?: unknown;
236
244
  } | undefined)) | undefined;
@@ -49,3 +49,10 @@ export function renderPoetry(source: string): {
49
49
  message: string;
50
50
  }[];
51
51
  };
52
+ /**
53
+ * The keys a poetry fence's attribute block takes. `lang`, `font` and `map`
54
+ * are read by `scanRuns` in content-scripts.mjs; `dir`, and on the site
55
+ * `data-pagefind-ignore`, are written by its rewrite of a fence set in one of
56
+ * the faces, and reach the element as they are written.
57
+ */
58
+ export const POETRY_KEYS: readonly string[];
@@ -0,0 +1,226 @@
1
+ /**
2
+ * The context a resolved configuration gives, and the maps an index holds.
3
+ *
4
+ * @param {{languages?: object, rootDir?: string}|undefined} config - The
5
+ * resolved configuration.
6
+ * @param {Iterable<Record<string, any>>} [records] - The content index's
7
+ * records, whose mapping notes give the maps. Without them no map is known.
8
+ * @returns {RunContext} Its languages, the faces installed where it sits, and
9
+ * the maps. A fonts package that cannot be read gives no faces; a run naming
10
+ * one is reported.
11
+ */
12
+ export function runContext(config: {
13
+ languages?: object;
14
+ rootDir?: string;
15
+ } | undefined, records?: Iterable<Record<string, any>>): RunContext;
16
+ /**
17
+ * Find the spans, divs and poetry fences marked with `lang=`, `font=` or
18
+ * `map=`, and every fault in their markup and their text.
19
+ *
20
+ * @param {string} source - Authored Markdown: a note body, or a whole file
21
+ * whose frontmatter is blanked.
22
+ * @param {RunContext} [ctx] - The configured languages, the installed faces
23
+ * and the maps. Without them every `lang=` must be a tag, every face and
24
+ * map is unknown, and only `mono`, `serif` and `sans` are fonts.
25
+ * @param {import("./content-session.mjs").ContentSyntax} [syntax] - Session
26
+ * scanner projections, so the source's spans and blocks are scanned once.
27
+ * @returns {{runs: Run[], errors: Array<{line: number, column: number, message: string}>, warnings: Array<{line: number, column: number, message: string}>}}
28
+ * The runs in source order, and each finding at its 1-based position. A
29
+ * warning is a `lang=` read as a tag that sits within one edit of a
30
+ * declared name.
31
+ */
32
+ export function scanRuns(source: string, ctx?: RunContext, syntax?: import("./content-session.mjs").ContentSyntax): {
33
+ runs: Run[];
34
+ errors: Array<{
35
+ line: number;
36
+ column: number;
37
+ message: string;
38
+ }>;
39
+ warnings: Array<{
40
+ line: number;
41
+ column: number;
42
+ message: string;
43
+ }>;
44
+ };
45
+ /**
46
+ * Rewrite text marked with `lang=`, `font=` and `map=` into the plain span,
47
+ * div and poetry grammar the rest of each chain renders, one line for one line.
48
+ *
49
+ * - **The map, if any, is applied first.** Characters Markdown would read are
50
+ * then backslash-escaped, so they reach every later pass as characters.
51
+ * - **The construct carries** the language's tag as `lang`, the face's class
52
+ * (with `script`, and `script-block` on a div) or the generic family's
53
+ * class. On the site, a face's construct also carries
54
+ * `data-pagefind-ignore`, since site search cannot read its code points;
55
+ * Foundry and the book receive no search marker.
56
+ * - **A div or fence in a right-to-left face** carries `dir=rtl`. No direction
57
+ * mark, `<bdo>` or `<bdi>` is ever written: the code points carry their own
58
+ * direction.
59
+ *
60
+ * A run with a finding is left as written; {@link scanRuns} reports it.
61
+ *
62
+ * @param {string} source - Authored Markdown: a note body.
63
+ * @param {RunContext} [ctx] - The configured languages, faces and maps.
64
+ * @param {import("./content-session.mjs").ContentSyntax} [syntax] - Session
65
+ * scanner projections.
66
+ * @param {{target?: "site"|"foundry"|"book"}} [options] - The target the
67
+ * Markdown is rendered for; only `site` writes the search marker.
68
+ * @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
69
+ * The rewritten Markdown, and the findings of {@link scanRuns}.
70
+ */
71
+ export function renderRuns(source: string, ctx?: RunContext, syntax?: import("./content-session.mjs").ContentSyntax, options?: {
72
+ target?: "site" | "foundry" | "book";
73
+ }): {
74
+ markdown: string;
75
+ errors: Array<{
76
+ line: number;
77
+ column: number;
78
+ message: string;
79
+ }>;
80
+ };
81
+ /** The keys this module reads, which no construct passes through as written. */
82
+ export const RUN_KEYS: readonly string[];
83
+ /** The generic families `font=` may name, each with the class that sets it. */
84
+ export const GENERIC_FONTS: Readonly<{
85
+ mono: "font-mono";
86
+ serif: "font-serif";
87
+ sans: "font-sans";
88
+ }>;
89
+ /**
90
+ * One marked construct, with what rendering it needs beside its {@link Run}.
91
+ */
92
+ export type RunDetail = {
93
+ /**
94
+ * - The run.
95
+ */
96
+ run: Run;
97
+ /**
98
+ * -
99
+ * What the construct carries besides the run keys.
100
+ */
101
+ own: {
102
+ id: string;
103
+ classes: string[];
104
+ values: Array<[string, string]>;
105
+ };
106
+ /**
107
+ * - The run keys it states itself.
108
+ */
109
+ keys: Record<string, string>;
110
+ /**
111
+ * - Whether its text is set.
112
+ */
113
+ plain: boolean;
114
+ /**
115
+ * - Its face.
116
+ */
117
+ face: import("./script-fonts.mjs").Face | undefined;
118
+ /**
119
+ * - Its generic family's class.
120
+ */
121
+ generic: string | undefined;
122
+ /**
123
+ * - Its map.
124
+ */
125
+ textMap: import("./text-maps.mjs").TextMap | undefined;
126
+ /**
127
+ * - Its map,
128
+ * face and lexicon, which its text is set by.
129
+ */
130
+ mapping: import("./map-lexicon.mjs").Mapping | undefined;
131
+ /**
132
+ * - The marked spans inside it.
133
+ */
134
+ holes: Map<number, number>;
135
+ /**
136
+ * - A div's or fence's body, as
137
+ * offsets: the first body line's start to the end of the last.
138
+ */
139
+ body: [number, number] | undefined;
140
+ /**
141
+ * - A span's offset of its closing `]`.
142
+ */
143
+ contentEnd: number | undefined;
144
+ };
145
+ /**
146
+ * What a run is checked against.
147
+ */
148
+ export type RunContext = {
149
+ /**
150
+ * - The
151
+ * configured languages.
152
+ */
153
+ languages?: Readonly<Record<string, {
154
+ tag: string;
155
+ }>> | undefined;
156
+ /**
157
+ * -
158
+ * The installed faces by slug, from
159
+ * {@link module :engine/script-fonts.resolveScriptFonts}.
160
+ */
161
+ faces?: ReadonlyMap<string, import("./script-fonts.mjs").Face> | undefined;
162
+ /**
163
+ * -
164
+ * The mapping notes' maps by name, from
165
+ * {@link module :engine/text-maps.mapsFromRecords}.
166
+ */
167
+ maps?: ReadonlyMap<string, import("./text-maps.mjs").TextMap> | undefined;
168
+ /**
169
+ * -
170
+ * Each configured language's lexicon by name, from
171
+ * {@link module :engine/map-lexicon.lexiconsFromRecords}.
172
+ */
173
+ lexicons?: ReadonlyMap<string, import("./map-lexicon.mjs").Lexicon> | undefined;
174
+ /**
175
+ * -
176
+ * What each configured language's mapped runs are checked against, from
177
+ * {@link module :engine/run-words.wordBooksFromRecords}.
178
+ */
179
+ wordBooks?: ReadonlyMap<string, import("./run-words.mjs").WordBook> | undefined;
180
+ };
181
+ /**
182
+ * One span, div or poetry fence carrying `lang=`, `font=` or `map=`. Its
183
+ * `lang`, `font` and `map` are the ones it is set with: its own, else the
184
+ * innermost div or fence around it that states one.
185
+ */
186
+ export type Run = {
187
+ /**
188
+ * - Which construct.
189
+ */
190
+ kind: "span" | "block" | "poetry";
191
+ /**
192
+ * - The language as written.
193
+ */
194
+ lang: string | undefined;
195
+ /**
196
+ * - The BCP 47 tag it resolves to; `undefined`
197
+ * when it resolves to none.
198
+ */
199
+ tag: string | undefined;
200
+ /**
201
+ * - The font as written.
202
+ */
203
+ font: string | undefined;
204
+ /**
205
+ * - The map as written.
206
+ */
207
+ map: string | undefined;
208
+ /**
209
+ * - 1-based line of the construct's opening.
210
+ */
211
+ line: number;
212
+ /**
213
+ * - 1-based UTF-16 column of its opening.
214
+ */
215
+ column: number;
216
+ /**
217
+ * - A span's offset of `[`; a div's or fence's
218
+ * 0-based opening line.
219
+ */
220
+ start: number;
221
+ /**
222
+ * - A span's offset past its attributes; a div's or
223
+ * fence's 0-based closing line.
224
+ */
225
+ end: number;
226
+ };
@@ -5,7 +5,7 @@
5
5
  * @property {string} name - The key as an author writes it.
6
6
  * @property {string} [kind] - Its value's shape: `string`, `date`, `number`,
7
7
  * `integer`, `boolean`, `address`, `list`, `map`, `scalar-or-map`,
8
- * `string-or-list`, or `string-list-or-map` — see {@link fitsKind}. Absent, the check
8
+ * `string-or-list`, `string-list-or-map` or `list-or-map` — see {@link fitsKind}. Absent, the check
9
9
  * makes no claim about the value.
10
10
  * @property {readonly string[]} [allowed] - The closed set a present value is
11
11
  * one of, where the key takes a fixed word.
@@ -97,7 +97,7 @@ export type InnerKeySpec = {
97
97
  /**
98
98
  * - Its value's shape: `string`, `date`, `number`,
99
99
  * `integer`, `boolean`, `address`, `list`, `map`, `scalar-or-map`,
100
- * `string-or-list`, or `string-list-or-map` — see {@link fitsKind}. Absent, the check
100
+ * `string-or-list`, `string-list-or-map` or `list-or-map` — see {@link fitsKind}. Absent, the check
101
101
  * makes no claim about the value.
102
102
  */
103
103
  kind?: string | undefined;