@heroiclands/package-build 8.0.0 → 9.0.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 (75) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/bin/content-build.mjs +44 -118
  3. package/bin/package-build.mjs +27 -69
  4. package/bin/report.mjs +1 -2
  5. package/bundle.mjs +2 -10
  6. package/config.mjs +31 -106
  7. package/container.mjs +13 -57
  8. package/content-config.mjs +45 -164
  9. package/coverage.mjs +14 -55
  10. package/deploy.mjs +4 -13
  11. package/e2e.mjs +16 -55
  12. package/engine/address-diff.mjs +1 -4
  13. package/engine/base-compiler.mjs +10 -28
  14. package/engine/code-fences.mjs +4 -13
  15. package/engine/compendiums.mjs +13 -37
  16. package/engine/content-address.mjs +2 -6
  17. package/engine/content-links.mjs +13 -44
  18. package/engine/content-lint.mjs +4 -15
  19. package/engine/content-slug.mjs +2 -6
  20. package/engine/content-tables.mjs +26 -79
  21. package/engine/diagnostics.mjs +4 -15
  22. package/engine/field-reference.mjs +6 -20
  23. package/engine/field-spec.mjs +1 -3
  24. package/engine/foreign-catalog.mjs +7 -22
  25. package/engine/foreign-manifests.mjs +1 -4
  26. package/engine/frontmatter-lint.mjs +6 -18
  27. package/engine/frontmatter.mjs +3 -8
  28. package/engine/generate.mjs +4 -16
  29. package/engine/helpers.mjs +13 -52
  30. package/engine/homepage.mjs +4 -15
  31. package/engine/ids.mjs +3 -12
  32. package/engine/item-registry.mjs +2 -6
  33. package/engine/journals.mjs +4 -14
  34. package/engine/kb-manifest.mjs +5 -17
  35. package/engine/macros.mjs +2 -10
  36. package/engine/manifest-emit.mjs +6 -17
  37. package/engine/map-notes.mjs +19 -69
  38. package/engine/note-package.mjs +1 -4
  39. package/engine/pack-config.mjs +17 -38
  40. package/engine/pack-router.mjs +1 -2
  41. package/engine/prose-config.mjs +20 -4
  42. package/engine/prose-lint.mjs +6 -14
  43. package/engine/region-events.mjs +1 -3
  44. package/engine/scene-levels.mjs +8 -22
  45. package/engine/scenes.mjs +13 -47
  46. package/engine/schema-check.mjs +1 -4
  47. package/engine/schema-extract.mjs +70 -45
  48. package/engine/site-build.mjs +12 -37
  49. package/engine/site-index.mjs +3 -15
  50. package/engine/web-wikilinks.mjs +4 -13
  51. package/engine/wikilinks.mjs +115 -167
  52. package/index.mjs +1 -5
  53. package/lang.mjs +1 -3
  54. package/manifest.mjs +10 -37
  55. package/markdownlint-config.mjs +1 -5
  56. package/package.json +1 -1
  57. package/sohl/actors.mjs +10 -40
  58. package/sohl/being-info.mjs +3 -6
  59. package/sohl/index.mjs +1 -6
  60. package/sohl/item-builders.mjs +1 -3
  61. package/sohl/item-fields.mjs +16 -34
  62. package/sohl/items.mjs +1 -3
  63. package/sohl/kb-passes.mjs +29 -39
  64. package/sohl/skill-base.mjs +7 -23
  65. package/stage.mjs +3 -13
  66. package/templates.mjs +4 -15
  67. package/types/bundle.d.mts +1 -1
  68. package/types/container.d.mts +2 -2
  69. package/types/coverage.d.mts +1 -1
  70. package/types/e2e.d.mts +4 -4
  71. package/types/engine/generate.d.mts +1 -1
  72. package/types/engine/helpers.d.mts +1 -1
  73. package/types/engine/schema-extract.d.mts +1 -1
  74. package/types/engine/site-index.d.mts +1 -1
  75. package/types/manifest.d.mts +1 -1
@@ -99,18 +99,12 @@ export function anchorsOf(body) {
99
99
  * @param {readonly string[]} [opts.skipDirectories] - Passed to the walk.
100
100
  * @returns {object} The notes, the index, and the resolvers built over it.
101
101
  */
102
- export function buildLinkIndex(
103
- contentBase,
104
- { manifestDir, skipDirectories } = {},
105
- ) {
102
+ export function buildLinkIndex(contentBase, { manifestDir, skipDirectories } = {}) {
106
103
  const notes = [];
107
104
  const frontmatterLinks = [];
108
105
  const walkOpts = skipDirectories ? { skipDirectories } : undefined;
109
106
 
110
- for (const { frontmatter: fm, absPath } of walkMarkdownTree(
111
- contentBase,
112
- walkOpts,
113
- )) {
107
+ for (const { frontmatter: fm, absPath } of walkMarkdownTree(contentBase, walkOpts)) {
114
108
  if (!fm || typeof fm.type !== "string") continue;
115
109
  // The raw text is kept beside the parsed body: a consumer's own checks
116
110
  // may need what frontmatter carried, which the body has dropped.
@@ -182,10 +176,7 @@ export function buildLinkIndex(
182
176
  : { index: new Map(), packages: new Set(), stale: [] };
183
177
  for (const v of foreign.index.values()) if (v.type) types.add(v.type);
184
178
 
185
- const packages = new Set([
186
- ...(byKey.size ? [pkg] : []),
187
- ...foreign.packages,
188
- ]);
179
+ const packages = new Set([...(byKey.size ? [pkg] : []), ...foreign.packages]);
189
180
 
190
181
  /** The searchable universe a `dataview` table draws its rows from. */
191
182
  const tableDocs = notes.map((n) => ({
@@ -223,10 +214,7 @@ export function buildLinkIndex(
223
214
  const seen = new Map();
224
215
  // Code is verbatim, so a `[[…]]` inside a fence, an indented block or
225
216
  // an inline span is not a link — the compilers make none of it either.
226
- for (const [all, rawInner] of matchAllOutsideCode(
227
- body,
228
- new RegExp(WIKILINK.source, "g"),
229
- )) {
217
+ for (const [all, rawInner] of matchAllOutsideCode(body, new RegExp(WIKILINK.source, "g"))) {
230
218
  const { target, anchor } = parseWikilink(rawInner);
231
219
  const occurrence = (seen.get(all) ?? 0) + 1;
232
220
  seen.set(all, occurrence);
@@ -258,18 +246,13 @@ export function buildLinkIndex(
258
246
  */
259
247
  function resolve(note, target) {
260
248
  const direct =
261
- byAlias.get(`${note.type}|${target}`.toLowerCase()) ??
262
- byKey.get(target.toLowerCase());
249
+ byAlias.get(`${note.type}|${target}`.toLowerCase()) ?? byKey.get(target.toLowerCase());
263
250
  if (direct) return direct;
264
251
  const qualified = readQualifier(target, types, packages);
265
252
  if (!qualified || qualified.reason) return undefined;
266
253
  return byKey.get(
267
254
  qualified.package ?
268
- canonicalKey(
269
- qualified.package,
270
- qualified.type,
271
- qualified.shortcode,
272
- )
255
+ canonicalKey(qualified.package, qualified.type, qualified.shortcode)
273
256
  : `${qualified.type}/${qualified.shortcode}`.toLowerCase(),
274
257
  );
275
258
  }
@@ -284,11 +267,7 @@ export function buildLinkIndex(
284
267
  const q = readQualifier(target, types, packages);
285
268
  if (!q || q.reason) return null;
286
269
  if (q.package) {
287
- return (
288
- foreign.index.get(
289
- canonicalKey(q.package, q.type, q.shortcode),
290
- ) ?? null
291
- );
270
+ return foreign.index.get(canonicalKey(q.package, q.type, q.shortcode)) ?? null;
292
271
  }
293
272
  // A bare address names no package, so it resolves against any foreign
294
273
  // one that publishes it. Claimed by two, it is ambiguous and the author
@@ -397,8 +376,7 @@ function landingTarget(url, bases) {
397
376
  }
398
377
  if (!/^https?:$/.test(parsed.protocol)) return null;
399
378
  if (!SITE_HOST.test(parsed.hostname)) return null;
400
- const pathname =
401
- parsed.pathname.endsWith("/") ? parsed.pathname : `${parsed.pathname}/`;
379
+ const pathname = parsed.pathname.endsWith("/") ? parsed.pathname : `${parsed.pathname}/`;
402
380
  for (const [pkg, base] of bases) {
403
381
  if (pathname === base) return { pkg, base };
404
382
  }
@@ -444,8 +422,7 @@ function readAddress(url, packages) {
444
422
  segments = value.split("?")[0].split("#")[0].split("/").filter(Boolean);
445
423
  }
446
424
 
447
- const prefix =
448
- shape !== "relative" && packages.has(segments[0]) ? segments[0] : null;
425
+ const prefix = shape !== "relative" && packages.has(segments[0]) ? segments[0] : null;
449
426
  return { shape, segments, prefix };
450
427
  }
451
428
 
@@ -533,10 +510,7 @@ export function auditHomepageLinks(index) {
533
510
  );
534
511
  }
535
512
 
536
- for (const { field, url, kind } of homepageAddresses(
537
- note.fm,
538
- note.body,
539
- )) {
513
+ for (const { field, url, kind } of homepageAddresses(note.fm, note.body)) {
540
514
  // Counted for every address, checked or not, so the count is
541
515
  // the literal's nth appearance in the file rather than the nth
542
516
  // *finding* about it — two rules can fire on one address.
@@ -583,8 +557,7 @@ export function auditHomepageLinks(index) {
583
557
  `relocation does not leave this page behind`,
584
558
  );
585
559
  } else if (shape === "rooted" && kind === "url") {
586
- const rest =
587
- prefix ? segments.slice(1).join("/") : segments.join("/");
560
+ const rest = prefix ? segments.slice(1).join("/") : segments.join("/");
588
561
  report(
589
562
  field,
590
563
  url,
@@ -596,9 +569,7 @@ export function auditHomepageLinks(index) {
596
569
  // `href:` is the field for an address already resolved.
597
570
  !rest ?
598
571
  `url "${url}" addresses ` +
599
- (prefix ?
600
- `package "${prefix}"'s landing`
601
- : `the site root`) +
572
+ (prefix ? `package "${prefix}"'s landing` : `the site root`) +
602
573
  `, but a landing's url: is package-relative and ` +
603
574
  `cannot leave this package — write ` +
604
575
  `href: "${url}", which is used verbatim`
@@ -737,9 +708,7 @@ export function auditLinks(index) {
737
708
  export function walkReachability(index, { root, scope, stopAt = () => false }) {
738
709
  const rootNote = index.notes.find((n) => n.rel === root);
739
710
  if (!rootNote) {
740
- throw new Error(
741
- `no note at ${root}, so the corpus has no page to be read from`,
742
- );
711
+ throw new Error(`no note at ${root}, so the corpus has no page to be read from`);
743
712
  }
744
713
 
745
714
  const reached = new Set([rootNote]);
@@ -106,10 +106,7 @@ export function isValidShortcode(value) {
106
106
  function collectNotes(contentBase, { skipDirectories } = {}) {
107
107
  const notes = [];
108
108
  const walkOpts = skipDirectories ? { skipDirectories } : undefined;
109
- for (const { frontmatter: fm, absPath } of walkMarkdownTree(
110
- contentBase,
111
- walkOpts,
112
- )) {
109
+ for (const { frontmatter: fm, absPath } of walkMarkdownTree(contentBase, walkOpts)) {
113
110
  if (!fm || !fm.type) continue;
114
111
  notes.push({
115
112
  fm,
@@ -134,10 +131,7 @@ function collectNotes(contentBase, { skipDirectories } = {}) {
134
131
  * severity: "error"|"warning", message: string}>, notes: number,
135
132
  * keys: number}} The findings, and what was inspected to produce them.
136
133
  */
137
- export function lintContentTree(
138
- contentBase,
139
- { skipDirectories, contentPackage } = {},
140
- ) {
134
+ export function lintContentTree(contentBase, { skipDirectories, contentPackage } = {}) {
141
135
  const findings = [];
142
136
  const notes = collectNotes(contentBase, { skipDirectories });
143
137
 
@@ -211,15 +205,10 @@ export function lintContentTree(
211
205
  // is a place an author has to go and edit, and a finding naming only
212
206
  // the key sends them hunting for the other one.
213
207
  for (const { file, absPath } of files) {
214
- const others = files
215
- .filter((f) => f.file !== file)
216
- .map((f) => f.file);
208
+ const others = files.filter((f) => f.file !== file).map((f) => f.file);
217
209
  findings.push({
218
210
  file,
219
- ...positionInFrontmatter(
220
- fs.readFileSync(absPath, "utf8"),
221
- "shortcode",
222
- ),
211
+ ...positionInFrontmatter(fs.readFileSync(absPath, "utf8"), "shortcode"),
223
212
  severity: "error",
224
213
  message:
225
214
  `duplicate address "${key}", also declared by ` +
@@ -133,13 +133,9 @@ export function contentSlug(name) {
133
133
  throw new Error("content note has no name, so it has no URL");
134
134
  }
135
135
  const normalised = slugify(raw);
136
- const slug = abbreviateTokens(normalised.split("-").filter(Boolean)).join(
137
- "-",
138
- );
136
+ const slug = abbreviateTokens(normalised.split("-").filter(Boolean)).join("-");
139
137
  if (!slug) {
140
- throw new Error(
141
- `name "${raw}" has no URL-safe characters, so it cannot address a page`,
142
- );
138
+ throw new Error(`name "${raw}" has no URL-safe characters, so it cannot address a page`);
143
139
  }
144
140
  return slug;
145
141
  }
@@ -199,13 +199,7 @@ function tokenize(source) {
199
199
  const CLAUSE_RANK = { TABLE: 0, FROM: 1, WHERE: 2, SORT: 3, LIMIT: 4 };
200
200
 
201
201
  /** Query types and data commands that are recognised only to be refused. */
202
- const UNSUPPORTED_CLAUSES = new Set([
203
- "LIST",
204
- "TASK",
205
- "CALENDAR",
206
- "GROUP",
207
- "FLATTEN",
208
- ]);
202
+ const UNSUPPORTED_CLAUSES = new Set(["LIST", "TASK", "CALENDAR", "GROUP", "FLATTEN"]);
209
203
 
210
204
  /**
211
205
  * Group a query's tokens by clause.
@@ -227,21 +221,15 @@ function splitClauses(tokens) {
227
221
  if (token.value === "(" || token.value === "[") depth++;
228
222
  else if (token.value === ")" || token.value === "]") depth--;
229
223
  }
230
- const word =
231
- token.kind === "ident" ? token.value.toUpperCase() : undefined;
224
+ const word = token.kind === "ident" ? token.value.toUpperCase() : undefined;
232
225
  if (depth === 0 && word && UNSUPPORTED_CLAUSES.has(word)) {
233
226
  // `GROUP BY` is two words; report it the way an author wrote it.
234
227
  const label =
235
- (
236
- word === "GROUP" &&
237
- tokens[i + 1]?.value?.toUpperCase?.() === "BY"
238
- ) ?
228
+ word === "GROUP" && tokens[i + 1]?.value?.toUpperCase?.() === "BY" ?
239
229
  "GROUP BY"
240
230
  : word;
241
231
  if (current === null) {
242
- throw new Error(
243
- `only TABLE queries are supported; this block is a ${label} query`,
244
- );
232
+ throw new Error(`only TABLE queries are supported; this block is a ${label} query`);
245
233
  }
246
234
  throw new Error(`the ${label} clause is not supported`);
247
235
  }
@@ -333,25 +321,19 @@ class ExprParser {
333
321
  /** Is the next token this operator/punctuation? */
334
322
  at(value) {
335
323
  const t = this.peek();
336
- return (
337
- (t.kind === "op" || t.kind === "punct") && t.value === String(value)
338
- );
324
+ return (t.kind === "op" || t.kind === "punct") && t.value === String(value);
339
325
  }
340
326
 
341
327
  /** Is the next token this (case-insensitive) bare word? */
342
328
  atWord(word) {
343
329
  const t = this.peek();
344
- return (
345
- t.kind === "ident" && t.value.toLowerCase() === word.toLowerCase()
346
- );
330
+ return t.kind === "ident" && t.value.toLowerCase() === word.toLowerCase();
347
331
  }
348
332
 
349
333
  /** Consume the next token if it matches; report otherwise. */
350
334
  expect(value) {
351
335
  if (!this.at(value)) {
352
- throw new Error(
353
- `expected "${value}", got "${this.peek().text || "end of query"}"`,
354
- );
336
+ throw new Error(`expected "${value}", got "${this.peek().text || "end of query"}"`);
355
337
  }
356
338
  return this.next();
357
339
  }
@@ -394,10 +376,7 @@ class ExprParser {
394
376
  parseComparison() {
395
377
  const left = this.parseUnary();
396
378
  const t = this.peek();
397
- if (
398
- t.kind === "op" &&
399
- ["=", "==", "!=", ">", ">=", "<", "<="].includes(t.value)
400
- ) {
379
+ if (t.kind === "op" && ["=", "==", "!=", ">", ">=", "<", "<="].includes(t.value)) {
401
380
  this.next();
402
381
  return {
403
382
  kind: "compare",
@@ -502,9 +481,7 @@ class ExprParser {
502
481
  }
503
482
  return { kind: "field", path: t.value };
504
483
  }
505
- throw new Error(
506
- `unexpected "${t.text || "end of query"}" in expression`,
507
- );
484
+ throw new Error(`unexpected "${t.text || "end of query"}" in expression`);
508
485
  }
509
486
 
510
487
  /**
@@ -544,9 +521,7 @@ export function parseDataviewQuery(source) {
544
521
  if (parser.atWord("without")) {
545
522
  parser.next();
546
523
  if (!parser.atWord("id")) {
547
- throw new Error(
548
- `expected "WITHOUT ID", got "WITHOUT ${parser.peek().text}"`,
549
- );
524
+ throw new Error(`expected "WITHOUT ID", got "WITHOUT ${parser.peek().text}"`);
550
525
  }
551
526
  parser.next();
552
527
  withoutId = true;
@@ -586,9 +561,7 @@ export function parseDataviewQuery(source) {
586
561
  const fromParser = new ExprParser(clauses.get("FROM"), text);
587
562
  from = fromParser.parseExpression();
588
563
  if (!fromParser.done) {
589
- throw new Error(
590
- `unexpected "${fromParser.peek().text}" in the FROM clause`,
591
- );
564
+ throw new Error(`unexpected "${fromParser.peek().text}" in the FROM clause`);
592
565
  }
593
566
  }
594
567
 
@@ -598,9 +571,7 @@ export function parseDataviewQuery(source) {
598
571
  const whereParser = new ExprParser(clauses.get("WHERE"), text);
599
572
  where = whereParser.parseExpression();
600
573
  if (!whereParser.done) {
601
- throw new Error(
602
- `unexpected "${whereParser.peek().text}" in the WHERE clause`,
603
- );
574
+ throw new Error(`unexpected "${whereParser.peek().text}" in the WHERE clause`);
604
575
  }
605
576
  }
606
577
 
@@ -614,10 +585,7 @@ export function parseDataviewQuery(source) {
614
585
  if (sortParser.atWord("desc") || sortParser.atWord("descending")) {
615
586
  sortParser.next();
616
587
  descending = true;
617
- } else if (
618
- sortParser.atWord("asc") ||
619
- sortParser.atWord("ascending")
620
- ) {
588
+ } else if (sortParser.atWord("asc") || sortParser.atWord("ascending")) {
621
589
  sortParser.next();
622
590
  }
623
591
  sort.push({ expr, descending });
@@ -648,8 +616,7 @@ const makeLink = (display) => ({ __link: true, display: String(display) });
648
616
 
649
617
  const isLink = (v) => Boolean(v) && typeof v === "object" && v.__link === true;
650
618
 
651
- const isEmpty = (v) =>
652
- v == null || v === "" || (Array.isArray(v) && v.length === 0);
619
+ const isEmpty = (v) => v == null || v === "" || (Array.isArray(v) && v.length === 0);
653
620
 
654
621
  /** Dataview treats an absent, empty, zero, or false value as false. */
655
622
  const truthy = (v) => {
@@ -813,9 +780,7 @@ function containsValue(haystack, needle, mode) {
813
780
  if (haystack == null) return false;
814
781
  if (Array.isArray(haystack)) {
815
782
  return haystack.some((v) =>
816
- mode === "exact" ?
817
- looseEquals(v, needle)
818
- : containsValue(v, needle, mode),
783
+ mode === "exact" ? looseEquals(v, needle) : containsValue(v, needle, mode),
819
784
  );
820
785
  }
821
786
  if (typeof haystack === "object" && !isLink(haystack)) {
@@ -842,8 +807,7 @@ function toRegExp(pattern, anchored) {
842
807
 
843
808
  /** The implementations behind {@link FUNCTIONS}. */
844
809
  const CALLS = {
845
- link: (args) =>
846
- makeLink(args.length > 1 && !isEmpty(args[1]) ? asText(args[1]) : ""),
810
+ link: (args) => makeLink(args.length > 1 && !isEmpty(args[1]) ? asText(args[1]) : ""),
847
811
  contains: ([a, b]) => containsValue(a, b, "loose"),
848
812
  icontains: ([a, b]) => containsValue(a, b, "insensitive"),
849
813
  econtains: ([a, b]) => containsValue(a, b, "exact"),
@@ -867,10 +831,8 @@ const CALLS = {
867
831
  .filter((v) => !isEmpty(v))
868
832
  .map(asText)
869
833
  .join(sep === undefined ? ", " : asText(sep)),
870
- regexmatch: ([pattern, value]) =>
871
- toRegExp(pattern, true).test(asText(value)),
872
- regextest: ([pattern, value]) =>
873
- toRegExp(pattern, false).test(asText(value)),
834
+ regexmatch: ([pattern, value]) => toRegExp(pattern, true).test(asText(value)),
835
+ regextest: ([pattern, value]) => toRegExp(pattern, false).test(asText(value)),
874
836
  };
875
837
 
876
838
  /**
@@ -980,11 +942,7 @@ function matchesSource(node, doc) {
980
942
  const folder = value.replace(/\/+$/, "");
981
943
  const path = doc.path ?? "";
982
944
  if (folder === "" || folder === "/") return true;
983
- return (
984
- path === folder ||
985
- path === `${folder}.md` ||
986
- path.startsWith(`${folder}/`)
987
- );
945
+ return path === folder || path === `${folder}.md` || path.startsWith(`${folder}/`);
988
946
  }
989
947
  default:
990
948
  throw new Error(
@@ -1015,10 +973,7 @@ export function selectRows(spec, docs, self) {
1015
973
  });
1016
974
  matched.sort((a, b) => {
1017
975
  for (const { expr, descending } of spec.sort) {
1018
- const order = compareValues(
1019
- evaluate(expr, a, self),
1020
- evaluate(expr, b, self),
1021
- );
976
+ const order = compareValues(evaluate(expr, a, self), evaluate(expr, b, self));
1022
977
  if (order !== 0) return descending ? -order : order;
1023
978
  }
1024
979
  const byPath = compareValues(a.path, b.path);
@@ -1032,8 +987,7 @@ export function selectRows(spec, docs, self) {
1032
987
  /* ------------------------------------------------------------------------ */
1033
988
 
1034
989
  /** Escape the characters that would break out of a markdown table cell. */
1035
- const escapeCell = (text) =>
1036
- text.replace(/\|/g, "\\|").replace(/[\r\n]+/g, " ");
990
+ const escapeCell = (text) => text.replace(/\|/g, "\\|").replace(/[\r\n]+/g, " ");
1037
991
 
1038
992
  /**
1039
993
  * Render one evaluated value as table text.
@@ -1074,8 +1028,7 @@ export function renderContentTable(spec, rows, linkable, self) {
1074
1028
  spec.columns.map((column) => {
1075
1029
  const value = evaluate(column.expr, doc, self);
1076
1030
  if (!isLink(value)) return formatValue(value, column.header);
1077
- const display =
1078
- value.display || asText(FILE_FIELDS.name(doc)) || EMPTY_CELL;
1031
+ const display = value.display || asText(FILE_FIELDS.name(doc)) || EMPTY_CELL;
1079
1032
  const text = escapeCell(display);
1080
1033
  if (!linkable(doc)) return text;
1081
1034
  // A wikilink's own separator is a literal `|`, written `\|` inside a
@@ -1087,12 +1040,9 @@ export function renderContentTable(spec, rows, linkable, self) {
1087
1040
  );
1088
1041
 
1089
1042
  const align = spec.columns.map((_column, i) => {
1090
- const shown = cells
1091
- .map((row) => row[i])
1092
- .filter((c) => c !== EMPTY_CELL);
1043
+ const shown = cells.map((row) => row[i]).filter((c) => c !== EMPTY_CELL);
1093
1044
  const numeric =
1094
- shown.length > 0 &&
1095
- shown.every((c) => Number.isFinite(Number(c)) && c.trim() !== "");
1045
+ shown.length > 0 && shown.every((c) => Number.isFinite(Number(c)) && c.trim() !== "");
1096
1046
  return numeric ? "---:" : "---";
1097
1047
  });
1098
1048
 
@@ -1172,9 +1122,7 @@ export function expandContentTables(
1172
1122
  // Find this fence's closing line: the same marker character, at least
1173
1123
  // as long, with nothing after it.
1174
1124
  let close = i + 1;
1175
- const closer = new RegExp(
1176
- `^[ \\t]*${marker[0]}{${marker.length},}[ \\t]*$`,
1177
- );
1125
+ const closer = new RegExp(`^[ \\t]*${marker[0]}{${marker.length},}[ \\t]*$`);
1178
1126
  while (close < lines.length && !closer.test(lines[close])) close++;
1179
1127
  const isQuery = /^dataview\b/i.test(info.trim());
1180
1128
  const block = lines.slice(i, Math.min(close + 1, lines.length));
@@ -1204,8 +1152,7 @@ export function expandContentTables(
1204
1152
  }
1205
1153
  // A markdown table must be its own block: keep one blank line on each
1206
1154
  // side of it, without inventing a leading or trailing one.
1207
- if (out.length > 0 && out[out.length - 1].trim() !== "")
1208
- emit("", i, true);
1155
+ if (out.length > 0 && out[out.length - 1].trim() !== "") emit("", i, true);
1209
1156
  for (const row of table.split("\n")) emit(`${indent}${row}`, i, true);
1210
1157
  if (close + 1 < lines.length && lines[close + 1].trim() !== "") {
1211
1158
  emit("", i, true);
@@ -145,11 +145,7 @@ export function emitDiagnostic(d) {
145
145
  * {@link expandContentTables}.
146
146
  * @returns {{line: number, column: number|undefined, generated: boolean}}
147
147
  */
148
- export function positionInBody(
149
- body,
150
- offset,
151
- { bodyLine = 1, bodyColumn = 1, lineMap } = {},
152
- ) {
148
+ export function positionInBody(body, offset, { bodyLine = 1, bodyColumn = 1, lineMap } = {}) {
153
149
  const upTo = String(body ?? "").slice(0, Math.max(0, offset));
154
150
  const nl = upTo.lastIndexOf("\n");
155
151
  const scannedLine = upTo.length === 0 ? 0 : upTo.split("\n").length - 1;
@@ -209,10 +205,7 @@ export function positionInFrontmatter(raw, key, value = undefined) {
209
205
  keyLine = i;
210
206
  break;
211
207
  }
212
- if (
213
- keyLine === -1 &&
214
- new RegExp(`^\\s*${escape(key)}\\s*:`).test(lines[i])
215
- ) {
208
+ if (keyLine === -1 && new RegExp(`^\\s*${escape(key)}\\s*:`).test(lines[i])) {
216
209
  keyLine = i;
217
210
  if (wanted == null) break;
218
211
  }
@@ -321,13 +314,9 @@ export function positionOfYamlPath(text, keyPath, { key = false } = {}) {
321
314
  if (key && node !== undefined) {
322
315
  const last = keyPath[keyPath.length - 1];
323
316
  const parent =
324
- keyPath.length === 1 ?
325
- doc.contents
326
- : doc.getIn(keyPath.slice(0, -1), true);
317
+ keyPath.length === 1 ? doc.contents : doc.getIn(keyPath.slice(0, -1), true);
327
318
  const pair = parent?.items?.find?.(
328
- (item) =>
329
- item?.key != null &&
330
- String(item.key.value) === String(last),
319
+ (item) => item?.key != null && String(item.key.value) === String(last),
331
320
  );
332
321
  if (pair?.key?.range) node = pair.key;
333
322
  }
@@ -47,10 +47,8 @@ import { loadPackConfig } from "./pack-config.mjs";
47
47
  function renderDefault(value) {
48
48
  if (value === undefined) return "—";
49
49
  if (value === null) return "`null`";
50
- if (typeof value === "string")
51
- return value === "" ? '`""`' : `\`${value}\``;
52
- if (Array.isArray(value))
53
- return value.length === 0 ? "`[]`" : `\`${JSON.stringify(value)}\``;
50
+ if (typeof value === "string") return value === "" ? '`""`' : `\`${value}\``;
51
+ if (Array.isArray(value)) return value.length === 0 ? "`[]`" : `\`${JSON.stringify(value)}\``;
54
52
  if (typeof value === "object") {
55
53
  const json = JSON.stringify(value);
56
54
  return json === "{}" ? "`{}`" : `\`${json}\``;
@@ -83,17 +81,10 @@ function cell(text) {
83
81
  * @returns {string[]} Markdown lines.
84
82
  */
85
83
  function padTable(rows) {
86
- const widths = rows[0].map((_, column) =>
87
- Math.max(...rows.map((row) => row[column].length)),
88
- );
89
- const line = (cells) =>
90
- `| ${cells.map((c, i) => c.padEnd(widths[i])).join(" | ")} |`;
84
+ const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
85
+ const line = (cells) => `| ${cells.map((c, i) => c.padEnd(widths[i])).join(" | ")} |`;
91
86
  const [header, ...body] = rows;
92
- return [
93
- line(header),
94
- `| ${widths.map((w) => "-".repeat(w)).join(" | ")} |`,
95
- ...body.map(line),
96
- ];
87
+ return [line(header), `| ${widths.map((w) => "-".repeat(w)).join(" | ")} |`, ...body.map(line)];
97
88
  }
98
89
 
99
90
  /**
@@ -156,12 +147,7 @@ function workedExample(type, fields) {
156
147
  for (const field of required) {
157
148
  lines.push(` ${field.name}: <${field.shape ?? "value"}>`);
158
149
  }
159
- lines.push(
160
- "---",
161
- "",
162
- "The prose here compiles into the item's documentation.",
163
- "```",
164
- );
150
+ lines.push("---", "", "The prose here compiles into the item's documentation.", "```");
165
151
  return lines;
166
152
  }
167
153
 
@@ -178,9 +178,7 @@ export function setPath(target, dotted, value) {
178
178
  */
179
179
  export function readField(field, fm) {
180
180
  if (field.name === undefined) {
181
- return typeof field.value === "function" ?
182
- field.value(fm)
183
- : field.value;
181
+ return typeof field.value === "function" ? field.value(fm) : field.value;
184
182
  }
185
183
  const raw = sohlField(fm, field.name, field.default);
186
184
  return field.read ? field.read(raw, { fm, field }) : raw;
@@ -203,9 +203,7 @@ export function writeZipEntries(files, dest) {
203
203
  * @returns {Promise<void>}
204
204
  */
205
205
  async function extractItemPacks(id, version, manifest, root, dir) {
206
- const itemPacks = (manifest.packs ?? []).filter(
207
- (pack) => pack.type === "Item",
208
- );
206
+ const itemPacks = (manifest.packs ?? []).filter((pack) => pack.type === "Item");
209
207
  if (!itemPacks.length) {
210
208
  throw new Error(
211
209
  `${id}@${version}: its manifest declares no Item packs, so it ` +
@@ -239,9 +237,7 @@ async function extractItemPacks(id, version, manifest, root, dir) {
239
237
  async function downloadAndUnzip(url, dest) {
240
238
  const res = await fetch(url, { redirect: "follow" });
241
239
  if (!res.ok) {
242
- throw new Error(
243
- `could not download ${url}: HTTP ${res.status} ${res.statusText}`,
244
- );
240
+ throw new Error(`could not download ${url}: HTTP ${res.status} ${res.statusText}`);
245
241
  }
246
242
  writeZipEntries(unzipSync(new Uint8Array(await res.arrayBuffer())), dest);
247
243
  }
@@ -272,10 +268,7 @@ export function pinnedManifestUrl(url, verified) {
272
268
  if (at === -1) return { url, pinned: false };
273
269
  const tag = verified.startsWith("v") ? verified : `v${verified}`;
274
270
  return {
275
- url:
276
- url.slice(0, at) +
277
- `/releases/download/${tag}/` +
278
- url.slice(at + marker.length),
271
+ url: url.slice(0, at) + `/releases/download/${tag}/` + url.slice(at + marker.length),
279
272
  pinned: true,
280
273
  };
281
274
  }
@@ -318,9 +311,7 @@ export async function fetchCatalog(config, rel) {
318
311
 
319
312
  const download = manifest.download;
320
313
  if (!download) {
321
- throw new Error(
322
- `${rel.id}@${version}: its manifest declares no \`download\``,
323
- );
314
+ throw new Error(`${rel.id}@${version}: its manifest declares no \`download\``);
324
315
  }
325
316
 
326
317
  // Rebuild from empty: a previous run may have died partway, and a stale
@@ -398,9 +389,7 @@ export async function fetchCatalogFromPath(config, rel, source) {
398
389
  throw new Error(`${rel.id}: nothing at ${source}`);
399
390
  }
400
391
 
401
- const staging = fs.mkdtempSync(
402
- path.join(os.tmpdir(), `content-build-${rel.id}-`),
403
- );
392
+ const staging = fs.mkdtempSync(path.join(os.tmpdir(), `content-build-${rel.id}-`));
404
393
  try {
405
394
  let root;
406
395
  if (fs.statSync(source).isDirectory()) {
@@ -419,9 +408,7 @@ export async function fetchCatalogFromPath(config, rel, source) {
419
408
  );
420
409
  }
421
410
  if (manifest.id && manifest.id !== rel.id) {
422
- throw new Error(
423
- `${rel.id}: ${source} is package "${manifest.id}", not "${rel.id}"`,
424
- );
411
+ throw new Error(`${rel.id}: ${source} is package "${manifest.id}", not "${rel.id}"`);
425
412
  }
426
413
  const version = manifest.version;
427
414
  if (!version) {
@@ -448,9 +435,7 @@ export async function fetchCatalogFromPath(config, rel, source) {
448
435
  export async function fetchAllCatalogs(config) {
449
436
  const rels = itemCatalogRelationships(config);
450
437
  if (!rels.length) {
451
- log.info(
452
- "No relationship declares `itemCatalog: true`; nothing to fetch.",
453
- );
438
+ log.info("No relationship declares `itemCatalog: true`; nothing to fetch.");
454
439
  return 0;
455
440
  }
456
441
  for (const rel of rels) await fetchCatalog(config, rel);
@@ -106,10 +106,7 @@ export function formatUnaddressableFinding(finding, manifestDir) {
106
106
  const file = path.join(manifestDir, `${finding.package}.json`);
107
107
  let at = {};
108
108
  try {
109
- at = positionOfLiteral(
110
- fs.readFileSync(file, "utf8"),
111
- `"${finding.sampleKey}"`,
112
- );
109
+ at = positionOfLiteral(fs.readFileSync(file, "utf8"), `"${finding.sampleKey}"`);
113
110
  } catch {
114
111
  // Unreadable here is not itself the finding — `loadForeignManifests`
115
112
  // already reports that as a stale manifest. The file is simply all that