@heroiclands/package-build 23.12.6 → 23.12.8

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 (94) hide show
  1. package/CHANGELOG.md +18 -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/assets.md +1 -1
  8. package/docs/authoring/links-and-markup.md +439 -47
  9. package/docs/commands.md +137 -3
  10. package/docs/configuration.md +73 -3
  11. package/docs/content-format.md +3 -1
  12. package/docs/guides/book.md +11 -0
  13. package/docs/guides/module-project.md +16 -0
  14. package/docs/guides/site.md +19 -0
  15. package/docs/reference/format-details.md +378 -6
  16. package/docs/reference/note-types.md +36 -11
  17. package/e2e.mjs +1 -1
  18. package/engine/asset-shipping.mjs +22 -8
  19. package/engine/base-compiler.mjs +2 -0
  20. package/engine/bundles.mjs +7 -2
  21. package/engine/compile-corpus.mjs +3 -1
  22. package/engine/content-charset.mjs +21 -2
  23. package/engine/content-format.yaml +1 -0
  24. package/engine/content-index.mjs +12 -0
  25. package/engine/content-poetry.mjs +22 -4
  26. package/engine/content-scripts.mjs +1219 -0
  27. package/engine/content-session.mjs +2 -0
  28. package/engine/data-keys.mjs +5 -1
  29. package/engine/dictionary-layout.mjs +18 -12
  30. package/engine/dictionary-rows.mjs +11 -2
  31. package/engine/helpers.mjs +8 -2
  32. package/engine/index-records.mjs +1 -0
  33. package/engine/journals.mjs +14 -3
  34. package/engine/language-data.mjs +137 -1
  35. package/engine/language-tag.mjs +103 -0
  36. package/engine/language-terms.mjs +16 -4
  37. package/engine/map-lexicon.mjs +762 -0
  38. package/engine/markup-validation.mjs +17 -3
  39. package/engine/names.mjs +1 -1
  40. package/engine/note-vocabulary.mjs +113 -0
  41. package/engine/pdf-build.mjs +28 -10
  42. package/engine/pdf-render.mjs +116 -11
  43. package/engine/run-rows.mjs +119 -0
  44. package/engine/run-words.mjs +266 -0
  45. package/engine/scenes.mjs +1 -0
  46. package/engine/script-font-delivery.mjs +321 -0
  47. package/engine/script-fonts.mjs +172 -0
  48. package/engine/site-build.mjs +10 -3
  49. package/engine/site-config.mjs +5 -0
  50. package/engine/site-root.mjs +19 -5
  51. package/engine/sql-tables.mjs +111 -1
  52. package/engine/text-maps.mjs +607 -0
  53. package/engine/verb-pattern.mjs +99 -0
  54. package/engine/word-pattern.mjs +27 -0
  55. package/engine/word-rows.mjs +8 -2
  56. package/engine/word-terms.mjs +44 -0
  57. package/hugo-theme/layouts/_default/baseof.html +3 -0
  58. package/hugo-theme/static/css/style.css +10 -0
  59. package/manifest.mjs +9 -1
  60. package/package.json +2 -2
  61. package/types/content-config.d.mts +26 -0
  62. package/types/e2e.d.mts +2 -2
  63. package/types/engine/address-fields.d.mts +1 -1
  64. package/types/engine/address-values.d.mts +1 -1
  65. package/types/engine/asset-shipping.d.mts +5 -0
  66. package/types/engine/compile-corpus.d.mts +2 -1
  67. package/types/engine/content-format-check.d.mts +1 -1
  68. package/types/engine/content-index.d.mts +11 -3
  69. package/types/engine/content-poetry.d.mts +7 -0
  70. package/types/engine/content-scripts.d.mts +226 -0
  71. package/types/engine/data-keys.d.mts +2 -2
  72. package/types/engine/dictionary-rows.d.mts +15 -0
  73. package/types/engine/helpers.d.mts +5 -1
  74. package/types/engine/journals.d.mts +7 -2
  75. package/types/engine/language-data.d.mts +2 -0
  76. package/types/engine/language-tag.d.mts +8 -0
  77. package/types/engine/language-terms.d.mts +2 -1
  78. package/types/engine/map-lexicon.d.mts +274 -0
  79. package/types/engine/markup-validation.d.mts +7 -2
  80. package/types/engine/note-vocabulary.d.mts +1 -1
  81. package/types/engine/pdf-build.d.mts +26 -3
  82. package/types/engine/pdf-coverage.d.mts +1 -1
  83. package/types/engine/pdf-render.d.mts +17 -2
  84. package/types/engine/run-rows.d.mts +89 -0
  85. package/types/engine/run-words.d.mts +86 -0
  86. package/types/engine/script-font-delivery.d.mts +134 -0
  87. package/types/engine/script-fonts.d.mts +168 -0
  88. package/types/engine/site-build.d.mts +2 -1
  89. package/types/engine/site-root.d.mts +11 -3
  90. package/types/engine/text-maps.d.mts +175 -0
  91. package/types/engine/verb-pattern.d.mts +65 -0
  92. package/types/engine/word-pattern.d.mts +13 -0
  93. package/types/engine/word-terms.d.mts +6 -0
  94. package/types/release.d.mts +1 -1
@@ -270,6 +270,11 @@ export function assetsManifest(staged, stageDir) {
270
270
  * stage before a file is copied: a package whose provenance cannot be read is
271
271
  * not one whose shipping can be decided.
272
272
  *
273
+ * The pictures are written beside whatever a `packageBuild.assets` row already
274
+ * put under `icons/` and `images/`; nothing there is removed. A picture whose
275
+ * deployed path is already a file in the stage is a finding, and no picture is
276
+ * written. Clearing an earlier run's pictures is the stage reset's work.
277
+ *
273
278
  * @param {object} opts - Options.
274
279
  * @param {string} opts.assetsBase - The package's asset directory, the roots'
275
280
  * parent.
@@ -304,15 +309,24 @@ export function stagePackageAssets({
304
309
  if (findings.length) return { staged: [], excluded: [], findings, manifest: null };
305
310
 
306
311
  const { staged, excluded } = selectStagedAssets(records, shippingLicenses);
307
- for (const { root } of DEPLOYED_ASSET_TYPES) {
308
- fs.rmSync(path.join(stageDir, root), { recursive: true, force: true });
312
+ const planned = staged.map((record) => ({
313
+ record,
314
+ from: path.join(assetsBase, ...record.asset.path.split("/")),
315
+ to: path.join(stageDir, .../** @type {string} */ (deployedAssetPath(record)).split("/")),
316
+ }));
317
+ for (const { record, from, to } of planned) {
318
+ if (!fs.existsSync(to)) continue;
319
+ findings.push({
320
+ file: to,
321
+ severity: "error",
322
+ message:
323
+ `\`${deployedAssetPath(record)}\` is already in the stage, copied by a ` +
324
+ `\`packageBuild.assets\` row; the picture \`${path.relative(process.cwd(), from)}\` ` +
325
+ "was not staged over it",
326
+ });
309
327
  }
310
- for (const record of staged) {
311
- const from = path.join(assetsBase, ...record.asset.path.split("/"));
312
- const to = path.join(
313
- stageDir,
314
- .../** @type {string} */ (deployedAssetPath(record)).split("/"),
315
- );
328
+ if (findings.length) return { staged: [], excluded: [], findings, manifest: null };
329
+ for (const { from, to } of planned) {
316
330
  fs.mkdirSync(path.dirname(to), { recursive: true });
317
331
  const transformed = transform ? transform(from) : null;
318
332
  if (transformed != null) fs.writeFileSync(to, transformed);
@@ -66,6 +66,7 @@ import { authoredFrontmatter } from "./index-records.mjs";
66
66
  import { markupAnchorFindings } from "./anchors.mjs";
67
67
  import { scanSpans } from "./content-spans.mjs";
68
68
  import { scanBlocks } from "./content-blocks.mjs";
69
+ import { scanRuns } from "./content-scripts.mjs";
69
70
  import { scanHeadingAttributes, withheldSections } from "./heading-attributes.mjs";
70
71
  import { scanFigures } from "./content-figures.mjs";
71
72
  import { footnoteFindings } from "./content-footnotes.mjs";
@@ -691,6 +692,7 @@ export class BasePackCompiler {
691
692
  ...(scanned?.blocks ?? scanBlocks(body)).errors,
692
693
  ...(scanned?.alerts ?? scanAlerts(body)).errors,
693
694
  ...(scanned?.spans ?? scanSpans(body)).errors,
695
+ ...scanRuns(body, this.corpus?.runs, syntax).errors,
694
696
  ...markupAnchorFindings(body, syntax),
695
697
  ]) {
696
698
  findings.push({
@@ -332,8 +332,13 @@ export class Bundles extends BasePackCompiler {
332
332
  // it compiles into already has somewhere to put the prose.
333
333
  description:
334
334
  markdown.trim() ?
335
- renderFoundryMarkdown(markdown, undefined, undefined, undefined, (pathname) =>
336
- roles.get(pathname),
335
+ renderFoundryMarkdown(
336
+ markdown,
337
+ undefined,
338
+ undefined,
339
+ undefined,
340
+ (pathname) => roles.get(pathname),
341
+ this.corpus?.runs,
337
342
  )
338
343
  : "",
339
344
  folder: this.folderResolver(authoredFolder, { isAddress: folderIsAddress }),
@@ -41,6 +41,7 @@
41
41
  import { indexRecordsFor } from "./content-index.mjs";
42
42
  import { buildContentLinkIndex } from "./helpers.mjs";
43
43
  import { loadPackConfig } from "./pack-config.mjs";
44
+ import { runContext } from "./content-scripts.mjs";
44
45
  import { prepareTreeSqlTables } from "./sql-tables.mjs";
45
46
 
46
47
  /**
@@ -61,7 +62,7 @@ import { prepareTreeSqlTables } from "./sql-tables.mjs";
61
62
  * the index refuses is a note the compile must still *report*, exactly as the
62
63
  * compile loop reported it when the loop was the first to see it.
63
64
  * @param {import("./content-session.mjs").ContentSession} [opts.session] - Captured context shared with other operations.
64
- * @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,
65
+ * @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,
65
66
  * its indexes, and the notes it could not record.
66
67
  */
67
68
  export async function buildCompileCorpus({
@@ -100,6 +101,7 @@ export async function buildCompileCorpus({
100
101
  const sqlTables = await prepareTreeSqlTables(contentBase, sqlOptions);
101
102
  return {
102
103
  currentDate: resolved.currentDate,
104
+ runs: runContext(resolved, records),
103
105
  records,
104
106
  parsed,
105
107
  problems: collected,
@@ -162,6 +162,19 @@ export const NOTATION = Object.freeze(
162
162
  ]),
163
163
  );
164
164
 
165
+ /**
166
+ * Tier 4 — the blocks the HeroicLands script fonts are registered in, so a
167
+ * mapping note or a `font=` run never draws a warning for a script character.
168
+ *
169
+ * @type {ReadonlyArray<readonly [number, number]>}
170
+ */
171
+ const SCRIPT_BLOCKS = Object.freeze([
172
+ [0xf0000, 0xf2fff], // plane 15: Pirzath, Thuravark, Semaron, the reserved alphabets, Teng Chou
173
+ [0x1e960, 0x1ec6f], // Zethu overflow, Qalezu, Zethu
174
+ [0x1ecc0, 0x1ecff], // Qalezu catch-before forms
175
+ [0x13430, 0x1345f], // the Egyptian format controls Zethu uses
176
+ ]);
177
+
165
178
  /**
166
179
  * Whether the charset admits a code point anywhere in a note.
167
180
  *
@@ -169,7 +182,12 @@ export const NOTATION = Object.freeze(
169
182
  * @returns {boolean} Whether it is allowed outside a code fence.
170
183
  */
171
184
  export function isAllowedCodePoint(cp) {
172
- return isLetterTier(cp) || TYPOGRAPHY.has(cp) || NOTATION.has(cp);
185
+ return (
186
+ isLetterTier(cp) ||
187
+ TYPOGRAPHY.has(cp) ||
188
+ NOTATION.has(cp) ||
189
+ SCRIPT_BLOCKS.some(([from, to]) => cp >= from && cp <= to)
190
+ );
173
191
  }
174
192
 
175
193
  /**
@@ -362,7 +380,8 @@ export function checkText(text, file) {
362
380
  line: i + 1,
363
381
  column,
364
382
  severity: /** @type {const} */ ("warning"),
365
- message: `\`${ch}\` ${hex(cp)} is ${refusalFor(cp)}`,
383
+ // A Private Use character draws as nothing, so quoting it leaves an empty span.
384
+ message: `${/\p{Co}/u.test(ch) ? "" : `\`${ch}\` `}${hex(cp)} is ${refusalFor(cp)}`,
366
385
  });
367
386
  }
368
387
  }
@@ -363,6 +363,7 @@ types:
363
363
  doc:
364
364
  data:
365
365
  - culture
366
+ - map
366
367
  subTypes:
367
368
  - rules
368
369
  - userguide
@@ -106,6 +106,7 @@ import { AddressLink } from "./address-values.mjs";
106
106
  import { scanSpans } from "./content-spans.mjs";
107
107
  import { rebaseNameSpans } from "./name-markup.mjs";
108
108
  import { analyzeNameSpans } from "./names.mjs";
109
+ import { recordRuns } from "./run-rows.mjs";
109
110
  import fs from "node:fs";
110
111
  import path from "node:path";
111
112
 
@@ -448,6 +449,8 @@ function assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage, raw)
448
449
  * `name` reads `Map 1` rather than `Figure 1` where it is due — see
449
450
  * {@link module:engine/content-figures.scanFigures}.
450
451
  * @param {import("./content-session.mjs").ContentSyntax} [options.syntax] - Session scanner projections.
452
+ * @param {import("./content-scripts.mjs").RunContext} [options.runContext] -
453
+ * The configured languages, which give each run's `lang=` its tag.
451
454
  * @returns {Record<string, any>} The record, keys sorted at every depth. A
452
455
  * **stub** — a note with an empty body, on a type an empty body suppresses —
453
456
  * carries `address` and `anchors` as `null`: it publishes no page, so it
@@ -474,6 +477,7 @@ export function buildIndexRecord({
474
477
  dateContext,
475
478
  resolveRole,
476
479
  syntax,
480
+ runContext,
477
481
  }) {
478
482
  assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage, raw);
479
483
 
@@ -541,6 +545,7 @@ export function buildIndexRecord({
541
545
  ...(Object.keys(resolvedDates).length ? { resolvedDates } : {}),
542
546
  package: contentPackage,
543
547
  names: marked.names,
548
+ runs: recordRuns(source, { context: runContext, bodyLine, bodyColumn, syntax }),
544
549
  address: stub ? null : address,
545
550
  nameAscii: asciiName(frontmatter?.name?.full),
546
551
  aliasesAscii: asciiAliases(frontmatter?.name?.aliases),
@@ -695,6 +700,7 @@ export function indexRecordsForNote({
695
700
  dateContext,
696
701
  resolveRole,
697
702
  syntax,
703
+ runContext,
698
704
  }) {
699
705
  resolveNoteId(frontmatter, { pkg: contentPackage });
700
706
  const record = buildIndexRecord({
@@ -712,6 +718,7 @@ export function indexRecordsForNote({
712
718
  dateContext,
713
719
  resolveRole,
714
720
  syntax,
721
+ runContext,
715
722
  });
716
723
  const records = [decodeIndexAddresses(record, addressContext ?? { package: contentPackage })];
717
724
  const address = noteAddress(frontmatter, contentPackage);
@@ -1067,6 +1074,8 @@ export function resolveCultures(
1067
1074
  * calendar note's shortcode; see {@link module:engine/note-calendars}.
1068
1075
  * @param {string} [options.configFile] - The configuration file, where an
1069
1076
  * unknown `defaultCalendar` is reported.
1077
+ * @param {Readonly<Record<string, {tag: string}>>} [options.languages] - The
1078
+ * configured languages, which give each run's `lang=` its tag.
1070
1079
  * @param {(key: string) => ({culture?: unknown}|undefined)} [options.foreignRecord] -
1071
1080
  * Another package's entry for a canonical address, read for a parent that
1072
1081
  * package publishes; see {@link resolveCultures}.
@@ -1086,6 +1095,7 @@ export function collectContentIndex(
1086
1095
  currentDate,
1087
1096
  defaultCalendar,
1088
1097
  configFile,
1098
+ languages,
1089
1099
  skipDirectories,
1090
1100
  assetsBase,
1091
1101
  manifest,
@@ -1192,6 +1202,7 @@ export function collectContentIndex(
1192
1202
  dateContext: dates,
1193
1203
  resolveRole,
1194
1204
  syntax,
1205
+ runContext: { languages },
1195
1206
  });
1196
1207
  records.push(...made);
1197
1208
  bodies.push({ record: made[0], body });
@@ -1478,6 +1489,7 @@ export function indexRecordsFor({
1478
1489
  currentDate: resolved.currentDate,
1479
1490
  defaultCalendar: resolved.defaultCalendar ?? undefined,
1480
1491
  configFile: configFileIn(resolved.rootDir, CONFIG_FILENAMES),
1492
+ languages: resolved.languages,
1481
1493
  addressContext: {
1482
1494
  ...noteAddressContext(resolved),
1483
1495
  declaresAddress: dependencyAddresses(resolved),
@@ -77,6 +77,24 @@ import { parseExtensionAttributes, refusedAttributes } from "./extension-attribu
77
77
 
78
78
  const structureParser = new MarkdownIt({ html: true });
79
79
 
80
+ /**
81
+ * The keys a poetry fence's attribute block takes. `lang`, `font` and `map`
82
+ * are read by `scanRuns` in content-scripts.mjs; `dir`, and on the site
83
+ * `data-pagefind-ignore`, are written by its rewrite of a fence set in one of
84
+ * the faces, and reach the element as they are written.
85
+ */
86
+ export const POETRY_KEYS = Object.freeze([
87
+ "form",
88
+ "meter",
89
+ "rhyme",
90
+ "syllables",
91
+ "lang",
92
+ "font",
93
+ "map",
94
+ "dir",
95
+ "data-pagefind-ignore",
96
+ ]);
97
+
80
98
  /** Read poetry code fences; start/end are inclusive zero-based source lines.
81
99
  * @param {string} source - Authored Markdown.
82
100
  */
@@ -157,15 +175,13 @@ export function scanPoetry(source) {
157
175
  for (const message of [...parsed.problems, ...refusedAttributes(parsed.values)])
158
176
  errors.push({ ...at, message });
159
177
  for (const [key, value] of Object.entries(parsed.values)) {
160
- if (!["form", "meter", "rhyme", "syllables", "lang"].includes(key))
178
+ if (!POETRY_KEYS.includes(key))
161
179
  errors.push({ ...at, message: `${key}= is not a poetry attribute` });
162
180
  else if (key === "syllables" && !/^[1-9]\d*(?:,[1-9]\d*)*$/.test(value))
163
181
  errors.push({
164
182
  ...at,
165
183
  message: "poetry syllables= needs positive comma-separated counts",
166
184
  });
167
- else if (key === "lang" && !/^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(value))
168
- errors.push({ ...at, message: "poetry lang= needs a language tag" });
169
185
  }
170
186
  }
171
187
  if (opening?.name === "poetry")
@@ -197,7 +213,9 @@ export function renderPoetry(source) {
197
213
  if (block.id) attrs.push(`id="${escape(slugify(block.id))}"`);
198
214
  for (const [key, value] of Object.entries(block.attributes))
199
215
  if (!refusedAttributes({ [key]: value }).length)
200
- attrs.push(`${key === "lang" ? key : `data-${key}`}="${escape(value)}"`);
216
+ attrs.push(
217
+ `${["lang", "dir", "data-pagefind-ignore"].includes(key) ? key : `data-${key}`}="${escape(value)}"`,
218
+ );
201
219
  let indent = block.indent;
202
220
  if (
203
221
  indent.length >= 4 &&