@heroiclands/package-build 22.28.0 → 23.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 (156) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +1 -1
  3. package/bin/content-commands.mjs +1 -1
  4. package/ci/content-format-reference.mjs +46 -0
  5. package/docs/api.md +125 -142
  6. package/docs/authoring/assets.md +1 -1
  7. package/docs/authoring/dates-and-calendars.md +12 -6
  8. package/docs/authoring/first-note.md +1 -1
  9. package/docs/authoring/frontmatter.md +2 -2
  10. package/docs/authoring/links-and-markup.md +145 -29
  11. package/docs/commands.md +8 -10
  12. package/docs/configuration.md +13 -11
  13. package/docs/content-format.md +3 -3
  14. package/docs/guides/book.md +59 -4
  15. package/docs/guides/icons.md +4 -5
  16. package/docs/guides/packs.md +6 -0
  17. package/docs/guides/reference-pages.md +21 -1
  18. package/docs/guides/release-and-deploy.md +17 -1
  19. package/docs/guides/site.md +17 -18
  20. package/docs/reference/format-details.md +904 -275
  21. package/docs/reference/note-types.md +282 -91
  22. package/docs/reference/theme-pages.md +58 -66
  23. package/engine/address-values.mjs +124 -4
  24. package/engine/address.mjs +44 -3
  25. package/engine/anchors.mjs +103 -3
  26. package/engine/art-slots.mjs +2 -9
  27. package/engine/base-compiler.mjs +25 -6
  28. package/engine/calendar-artifacts.mjs +4 -1
  29. package/engine/calendar-notes.mjs +166 -102
  30. package/engine/content-format-check.mjs +2 -3
  31. package/engine/content-format.mjs +44 -1
  32. package/engine/content-format.yaml +84 -16
  33. package/engine/content-images.mjs +10 -1
  34. package/engine/content-index.mjs +37 -9
  35. package/engine/content-links.mjs +24 -5
  36. package/engine/content-tables.mjs +24 -3
  37. package/engine/data-keys.mjs +347 -0
  38. package/engine/derived-sections.mjs +322 -0
  39. package/engine/diagnostics.mjs +28 -0
  40. package/engine/event-fields.mjs +187 -0
  41. package/engine/event-rows.mjs +126 -0
  42. package/engine/event-views.mjs +305 -0
  43. package/engine/exported-scene.mjs +29 -53
  44. package/engine/frontmatter-lint.mjs +103 -77
  45. package/engine/frontmatter.mjs +7 -2
  46. package/engine/generate.mjs +53 -0
  47. package/engine/generated-sections.mjs +223 -0
  48. package/engine/heading-attributes.mjs +4 -2
  49. package/engine/helpers.mjs +12 -1
  50. package/engine/holdings.mjs +12 -33
  51. package/engine/ids.mjs +17 -11
  52. package/engine/index.mjs +4 -4
  53. package/engine/infobox-render.mjs +21 -6
  54. package/engine/infobox.mjs +217 -99
  55. package/engine/journals.mjs +104 -47
  56. package/engine/literature-notes.mjs +136 -0
  57. package/engine/literature-works.mjs +161 -0
  58. package/engine/map-graphviz.mjs +4 -18
  59. package/engine/markdown-expressions.mjs +116 -11
  60. package/engine/metadata-index.mjs +34 -0
  61. package/engine/near-miss.mjs +90 -0
  62. package/engine/note-addresses.mjs +75 -19
  63. package/engine/note-dates.mjs +5 -3
  64. package/engine/note-event-terms.mjs +364 -0
  65. package/engine/note-events.mjs +823 -0
  66. package/engine/note-vocabulary.mjs +195 -92
  67. package/engine/office-holders.mjs +20 -54
  68. package/engine/packagebuild-index.mjs +16 -1
  69. package/engine/pdf-build.mjs +97 -60
  70. package/engine/pdf-index.mjs +263 -0
  71. package/engine/pdf-render.mjs +47 -5
  72. package/engine/place-relation-terms.mjs +174 -0
  73. package/engine/place-relations.mjs +72 -160
  74. package/engine/rank-ladder.mjs +14 -41
  75. package/engine/reckoning-markers.mjs +2 -0
  76. package/engine/region-events.mjs +6 -12
  77. package/engine/scenes.mjs +73 -468
  78. package/engine/site-build.mjs +74 -85
  79. package/engine/site-index.mjs +3 -1
  80. package/engine/site-maps.mjs +43 -29
  81. package/engine/site-preview.mjs +40 -31
  82. package/engine/social-ties.mjs +2 -1
  83. package/engine/sql-tables.mjs +212 -9
  84. package/engine/standing-terms.mjs +145 -6
  85. package/engine/standings.mjs +37 -49
  86. package/engine/stub-lint.mjs +6 -0
  87. package/engine/web-wikilinks.mjs +21 -1
  88. package/engine/wikilink-syntax.mjs +34 -4
  89. package/engine/wikilinks.mjs +27 -2
  90. package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
  91. package/hugo-theme/layouts/partials/page-body.html +0 -2
  92. package/hugo-theme/static/css/style.css +25 -55
  93. package/package.json +1 -2
  94. package/packagebuild-metadata.jsonl +13 -13
  95. package/sohl/default-item-art.mjs +6 -6
  96. package/sohl/note-schemas.mjs +8 -73
  97. package/types/engine/address-values.d.mts +50 -3
  98. package/types/engine/address.d.mts +21 -0
  99. package/types/engine/anchors.d.mts +49 -2
  100. package/types/engine/art-slots.d.mts +1 -1
  101. package/types/engine/base-compiler.d.mts +13 -4
  102. package/types/engine/calendar-notes.d.mts +7 -0
  103. package/types/engine/content-format.d.mts +52 -0
  104. package/types/engine/content-images.d.mts +5 -1
  105. package/types/engine/content-tables.d.mts +9 -1
  106. package/types/engine/data-keys.d.mts +145 -0
  107. package/types/engine/derived-sections.d.mts +73 -0
  108. package/types/engine/diagnostics.d.mts +15 -0
  109. package/types/engine/event-fields.d.mts +99 -0
  110. package/types/engine/event-rows.d.mts +12 -0
  111. package/types/engine/event-views.d.mts +67 -0
  112. package/types/engine/exported-scene.d.mts +5 -2
  113. package/types/engine/frontmatter-lint.d.mts +3 -3
  114. package/types/engine/generated-sections.d.mts +143 -0
  115. package/types/engine/holdings.d.mts +6 -20
  116. package/types/engine/ids.d.mts +15 -12
  117. package/types/engine/index.d.mts +1 -1
  118. package/types/engine/infobox.d.mts +13 -3
  119. package/types/engine/journals.d.mts +24 -11
  120. package/types/engine/literature-notes.d.mts +29 -0
  121. package/types/engine/literature-works.d.mts +67 -0
  122. package/types/engine/map-graphviz.d.mts +2 -10
  123. package/types/engine/markdown-expressions.d.mts +12 -3
  124. package/types/engine/near-miss.d.mts +42 -0
  125. package/types/engine/note-addresses.d.mts +7 -1
  126. package/types/engine/note-event-terms.d.mts +68 -0
  127. package/types/engine/note-events.d.mts +71 -0
  128. package/types/engine/note-vocabulary.d.mts +37 -8
  129. package/types/engine/office-holders.d.mts +3 -0
  130. package/types/engine/pdf-index.d.mts +35 -0
  131. package/types/engine/pdf-render.d.mts +8 -2
  132. package/types/engine/place-relation-terms.d.mts +83 -0
  133. package/types/engine/place-relations.d.mts +11 -66
  134. package/types/engine/rank-ladder.d.mts +2 -0
  135. package/types/engine/region-events.d.mts +6 -12
  136. package/types/engine/scenes.d.mts +6 -32
  137. package/types/engine/site-build.d.mts +8 -22
  138. package/types/engine/site-maps.d.mts +15 -7
  139. package/types/engine/sql-tables.d.mts +77 -2
  140. package/types/engine/standing-terms.d.mts +37 -4
  141. package/types/engine/standings.d.mts +14 -9
  142. package/types/engine/wikilink-syntax.d.mts +20 -0
  143. package/types/engine/wikilinks.d.mts +3 -2
  144. package/docs/guides/government-migration.md +0 -52
  145. package/engine/itinerary-scenes.mjs +0 -166
  146. package/engine/lore-events.mjs +0 -197
  147. package/engine/map-notes.mjs +0 -1309
  148. package/engine/map-raster.mjs +0 -51
  149. package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
  150. package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
  151. package/hugo-theme/layouts/partials/holdings.html +0 -61
  152. package/hugo-theme/layouts/partials/place-map.html +0 -9
  153. package/types/engine/itinerary-scenes.d.mts +0 -29
  154. package/types/engine/lore-events.d.mts +0 -14
  155. package/types/engine/map-notes.d.mts +0 -297
  156. package/types/engine/map-raster.d.mts +0 -8
@@ -14,6 +14,7 @@ import { calendariaEnvelope, compileCalendars } from "./calendar-notes.mjs";
14
14
  import { formatDiagnostic, positionOfFrontmatterPath } from "./diagnostics.mjs";
15
15
  import { authoredFrontmatter, isNoteRecord, noteFile } from "./index-records.mjs";
16
16
  import { metadataFileName } from "./packages.mjs";
17
+ import { flattenPublishedAddresses } from "./address-values.mjs";
17
18
 
18
19
  // The import envelope requires an ISO instant. A fixed value keeps an export
19
20
  // reproducible from its source rather than stamping the build machine's clock.
@@ -43,7 +44,9 @@ function readIndex(file) {
43
44
  .filter(Boolean)
44
45
  .map((line, index) => {
45
46
  try {
46
- return JSON.parse(line);
47
+ // Each published Address is an object; the calendar compiler
48
+ // reads the string an author writes.
49
+ return flattenPublishedAddresses(JSON.parse(line));
47
50
  } catch (cause) {
48
51
  const error = new Error(
49
52
  `${display}:${index + 1}: error: invalid content index record`,
@@ -112,6 +112,122 @@ export const CALENDAR_DATE_FORMAT_KEYS = Object.freeze([
112
112
  /** Bare date tokens Calendaria substitutes inside authored format strings. */
113
113
  export { CALENDAR_FORMAT_TOKENS };
114
114
 
115
+ /** An entry's `name`, which every calendar list states. */
116
+ const NAME = Object.freeze({
117
+ name: "name",
118
+ kind: "string",
119
+ required: true,
120
+ shape: "a string",
121
+ describe: "What it is called.",
122
+ });
123
+ /** An entry's short form, which every calendar list may state. */
124
+ const ABBREVIATION = Object.freeze({
125
+ name: "abbreviation",
126
+ kind: "string",
127
+ shape: "a string",
128
+ describe: "Its short form, for a compact date.",
129
+ });
130
+
131
+ /**
132
+ * The keys of one entry in each of a calendar's lists, as inner-key
133
+ * declarations.
134
+ *
135
+ * @type {Readonly<Record<string, readonly import("./data-keys.mjs").InnerKeySpec[]>>}
136
+ */
137
+ export const CALENDAR_ENTRY_FIELDS = Object.freeze({
138
+ months: Object.freeze([
139
+ NAME,
140
+ ABBREVIATION,
141
+ Object.freeze({
142
+ name: "days",
143
+ kind: "integer",
144
+ required: true,
145
+ shape: "a whole number of days",
146
+ describe: "How many days the month holds.",
147
+ }),
148
+ ]),
149
+ weekdays: Object.freeze([NAME, ABBREVIATION]),
150
+ seasons: Object.freeze([
151
+ NAME,
152
+ ABBREVIATION,
153
+ Object.freeze({
154
+ name: "start",
155
+ kind: "integer",
156
+ required: true,
157
+ shape: "a one-based day of the year, as a whole number",
158
+ describe: "The day of the year the season begins.",
159
+ }),
160
+ ]),
161
+ namedDays: Object.freeze([
162
+ NAME,
163
+ ABBREVIATION,
164
+ Object.freeze({
165
+ name: "day",
166
+ kind: "integer",
167
+ required: true,
168
+ shape: "a one-based day of the year, as a whole number",
169
+ describe: "The day of the year it names.",
170
+ }),
171
+ ]),
172
+ eras: Object.freeze([
173
+ Object.freeze({
174
+ name: "shortcode",
175
+ kind: "string",
176
+ required: true,
177
+ shape:
178
+ "the era's own shortcode, unique within the calendar — an era is " +
179
+ "addressed `<calendar shortcode>.<era shortcode>`",
180
+ describe: "The era's own segment of `<calendar>.<era>`.",
181
+ }),
182
+ NAME,
183
+ Object.freeze({
184
+ name: "marker",
185
+ kind: "string",
186
+ shape: "uppercase letters and digits, beginning with a letter",
187
+ describe: "The marker an authored date names the era by, unique across the corpus.",
188
+ }),
189
+ ABBREVIATION,
190
+ Object.freeze({
191
+ name: "proclaimedBy",
192
+ kind: "address",
193
+ shape: "an Address",
194
+ describe: "The body that began the reckoning.",
195
+ }),
196
+ Object.freeze({
197
+ name: "start",
198
+ kind: "integer",
199
+ required: true,
200
+ nullable: true,
201
+ shape: "`null`, or the calendar year the era begins, as a whole number",
202
+ describe: "The calendar year the era begins; `null` for the era before year 1.",
203
+ }),
204
+ Object.freeze({
205
+ name: "label",
206
+ kind: "scalar-or-map",
207
+ shape: "a string, or an `{ after, before }` map",
208
+ fields: Object.freeze([
209
+ Object.freeze({
210
+ name: "after",
211
+ kind: "string",
212
+ shape: "a string with one `{date}` slot",
213
+ describe: "How a date in the era reads.",
214
+ }),
215
+ Object.freeze({
216
+ name: "before",
217
+ kind: "string",
218
+ shape: "a string with one `{date}` slot",
219
+ describe: "How a date before the era's start reads.",
220
+ }),
221
+ ]),
222
+ describe: "How a date in the era reads, around one `{date}` slot.",
223
+ }),
224
+ ]),
225
+ });
226
+
227
+ /** One list entry of a calendar field, as a map of its declared keys. */
228
+ const entriesOf = (field, shape) =>
229
+ Object.freeze({ kind: "map", shape, fields: CALENDAR_ENTRY_FIELDS[field] });
230
+
115
231
  /**
116
232
  * The `data:` keys a calendar note may write — the closed list, in authored
117
233
  * order.
@@ -133,6 +249,7 @@ export const CALENDAR_FIELDS = Object.freeze([
133
249
  name: "months",
134
250
  shape: "list of `{ name, abbreviation?, days }`",
135
251
  kind: "list",
252
+ entries: entriesOf("months", "a map — `{ name, abbreviation?, days }`"),
136
253
  describe:
137
254
  "The months this calendar keeps, in order — position in the list is " +
138
255
  "position in the year, and the day counts sum to the world's year.",
@@ -141,6 +258,7 @@ export const CALENDAR_FIELDS = Object.freeze([
141
258
  name: "weekdays",
142
259
  shape: "list of `{ name, abbreviation? }`",
143
260
  kind: "list",
261
+ entries: entriesOf("weekdays", "a map — `{ name, abbreviation? }`"),
144
262
  describe:
145
263
  "The days of the week this calendar names, in order. A calendar with " +
146
264
  "no week writes none.",
@@ -149,18 +267,24 @@ export const CALENDAR_FIELDS = Object.freeze([
149
267
  name: "seasons",
150
268
  shape: "list of `{ name, abbreviation?, start }`",
151
269
  kind: "list",
270
+ entries: entriesOf("seasons", "a map — `{ name, abbreviation?, start }`"),
152
271
  describe: "The seasons this calendar marks, starting on numbered days of the year.",
153
272
  },
154
273
  {
155
274
  name: "namedDays",
156
275
  shape: "list of `{ name, abbreviation?, day }`",
157
276
  kind: "list",
277
+ entries: entriesOf("namedDays", "a map — `{ name, abbreviation?, day }`"),
158
278
  describe: "Names assigned to particular days of the year.",
159
279
  },
160
280
  {
161
281
  name: "eras",
162
282
  shape: "list of `{ shortcode, name, marker?, abbreviation?, proclaimedBy?, start, label? }`",
163
283
  kind: "list",
284
+ entries: entriesOf(
285
+ "eras",
286
+ "a map — `{ shortcode, name, marker?, abbreviation?, proclaimedBy?, start, label? }`",
287
+ ),
164
288
  describe:
165
289
  "The year-counts kept in this calendar. A marker names one era and uses these months.",
166
290
  },
@@ -168,6 +292,7 @@ export const CALENDAR_FIELDS = Object.freeze([
168
292
  name: "formats",
169
293
  shape: "map of named Calendaria format strings",
170
294
  kind: "map",
295
+ values: Object.freeze({ kind: "string", shape: "a Calendaria format string" }),
171
296
  describe: "Named patterns for reading and writing this calendar's dates.",
172
297
  },
173
298
  ]);
@@ -367,6 +492,22 @@ function notesCarrying(index, key) {
367
492
  return carrying.sort((a, b) => String(a.rel ?? a.file).localeCompare(String(b.rel ?? b.file)));
368
493
  }
369
494
 
495
+ /**
496
+ * A whole number however YAML spelled it, or `undefined` — the shape the
497
+ * inner-key check holds a `start` or a `day` to. A value that is not one is
498
+ * that check's single finding, and the order of the year is not asked of it.
499
+ *
500
+ * @param {unknown} value - The authored value.
501
+ * @returns {number|undefined} The whole number it states.
502
+ */
503
+ function whole(value) {
504
+ const read =
505
+ typeof value === "number" ? value
506
+ : typeof value === "string" && value.trim() !== "" ? Number(value)
507
+ : NaN;
508
+ return Number.isSafeInteger(read) ? read : undefined;
509
+ }
510
+
370
511
  /** A finding positioned at a `data:` key of a note. */
371
512
  function atData(note, keyPath, severity, message) {
372
513
  return {
@@ -381,7 +522,9 @@ function atData(note, keyPath, severity, message) {
381
522
  export function checkCalendarDateFormats(note) {
382
523
  const formats = dataOf(note).formats;
383
524
  if (formats === undefined) return [];
384
- if (typeof formats !== "object" || Array.isArray(formats) || !Object.keys(formats).length)
525
+ // A value that is not a map is the container's shape finding.
526
+ if (!formats || typeof formats !== "object" || Array.isArray(formats)) return [];
527
+ if (!Object.keys(formats).length)
385
528
  return [
386
529
  atData(note, ["formats"], "error", "data.formats needs at least one named pattern"),
387
530
  ];
@@ -398,12 +541,8 @@ export function checkCalendarDateFormats(note) {
398
541
  );
399
542
  continue;
400
543
  }
401
- if (typeof value !== "string") {
402
- findings.push(
403
- atData(note, ["formats", slot], "error", `data.formats.${slot} must be a string`),
404
- );
405
- continue;
406
- }
544
+ // A pattern that is not a string is the inner-key check's finding.
545
+ if (typeof value !== "string") continue;
407
546
  const syntaxError = calendarFormatSyntaxError(value);
408
547
  if (syntaxError) findings.push(atData(note, ["formats", slot], "error", syntaxError));
409
548
  }
@@ -419,22 +558,16 @@ export function checkCalendarDateFormats(note) {
419
558
  function checkCalendarDayNames(note, daysPerYear) {
420
559
  const data = dataOf(note);
421
560
  const findings = [];
561
+ // A list's shape, an entry's keys and an absent or mistyped name, start or
562
+ // day are the inner-key check's findings, made from
563
+ // `CALENDAR_ENTRY_FIELDS`. What is checked here is the order of the year.
422
564
  const seasons = data.seasons;
423
- if (seasons !== undefined && !Array.isArray(seasons))
424
- findings.push(atData(note, ["seasons"], "error", "data.seasons must be a list"));
425
565
  if (Array.isArray(seasons)) {
426
566
  let prior = 0;
427
567
  for (const [position, season] of seasons.entries()) {
428
- const start = season?.start;
429
- if (typeof season?.name !== "string" || !season.name.trim())
430
- findings.push(
431
- atData(note, ["seasons", position, "name"], "error", "a season needs a name"),
432
- );
433
- if (
434
- !Number.isSafeInteger(start) ||
435
- start <= prior ||
436
- (Number.isSafeInteger(daysPerYear) && start > daysPerYear)
437
- )
568
+ const start = whole(season?.start);
569
+ if (start === undefined) continue;
570
+ if (start <= prior || (Number.isSafeInteger(daysPerYear) && start > daysPerYear))
438
571
  findings.push(
439
572
  atData(
440
573
  note,
@@ -443,40 +576,16 @@ function checkCalendarDayNames(note, daysPerYear) {
443
576
  "season starts must increase within the year",
444
577
  ),
445
578
  );
446
- if (
447
- Object.keys(season ?? {}).some(
448
- (key) => !["name", "abbreviation", "start"].includes(key),
449
- )
450
- )
451
- findings.push(
452
- atData(
453
- note,
454
- ["seasons", position],
455
- "error",
456
- "a season uses only name, abbreviation, and start",
457
- ),
458
- );
459
579
  prior = start;
460
580
  }
461
581
  }
462
582
  const namedDays = data.namedDays;
463
- if (namedDays !== undefined && !Array.isArray(namedDays))
464
- findings.push(atData(note, ["namedDays"], "error", "data.namedDays must be a list"));
465
583
  if (Array.isArray(namedDays)) {
466
584
  const seen = new Set();
467
585
  for (const [position, namedDay] of namedDays.entries()) {
468
- if (typeof namedDay?.name !== "string" || !namedDay.name.trim())
469
- findings.push(
470
- atData(
471
- note,
472
- ["namedDays", position, "name"],
473
- "error",
474
- "a named day needs a name",
475
- ),
476
- );
477
- const day = namedDay?.day;
586
+ const day = whole(namedDay?.day);
587
+ if (day === undefined) continue;
478
588
  if (
479
- !Number.isSafeInteger(day) ||
480
589
  day < 1 ||
481
590
  (Number.isSafeInteger(daysPerYear) && day > daysPerYear) ||
482
591
  seen.has(day)
@@ -489,19 +598,6 @@ function checkCalendarDayNames(note, daysPerYear) {
489
598
  "named days need distinct days within the year",
490
599
  ),
491
600
  );
492
- if (
493
- Object.keys(namedDay ?? {}).some(
494
- (key) => !["name", "abbreviation", "day"].includes(key),
495
- )
496
- )
497
- findings.push(
498
- atData(
499
- note,
500
- ["namedDays", position],
501
- "error",
502
- "a named day uses only name, abbreviation, and day",
503
- ),
504
- );
505
601
  seen.add(day);
506
602
  }
507
603
  }
@@ -713,20 +809,6 @@ export function checkCalendarNote(note, { index } = {}) {
713
809
  findings.push(...checkMonthSum(note, index));
714
810
  findings.push(...checkCalendarDayNames(note, worldInvariants(index).year?.days));
715
811
  findings.push(...checkEraShortcodes(note));
716
- for (const [position, day] of (Array.isArray(dataOf(note).weekdays) ?
717
- dataOf(note).weekdays
718
- : []
719
- ).entries()) {
720
- if (day?.ordinal !== undefined)
721
- findings.push(
722
- atData(
723
- note,
724
- ["weekdays", position, "ordinal"],
725
- "error",
726
- "weekday order is its array position, starting at zero; omit ordinal",
727
- ),
728
- );
729
- }
730
812
  findings.push(
731
813
  ...reckoningContext(index).findings.filter((finding) => finding.file === note.file),
732
814
  );
@@ -752,6 +834,9 @@ export function checkCalendarNote(note, { index } = {}) {
752
834
  function checkMonthSum(note, index) {
753
835
  const months = dataOf(note).months;
754
836
  if (!Array.isArray(months) || months.length === 0) return [];
837
+ // A month whose `days` is not a whole number is the inner-key check's
838
+ // finding, and a sum over it would restate that finding as a second one.
839
+ if (months.some((month) => whole(month?.days) === undefined)) return [];
755
840
  const days = worldInvariants(index).year?.days;
756
841
  if (typeof days !== "number") return [];
757
842
  const sum = daysInYear(months);
@@ -786,18 +871,8 @@ function checkEraShortcodes(note) {
786
871
  const findings = [];
787
872
  eras.forEach((era, position) => {
788
873
  const shortcode = String(era?.shortcode ?? "");
789
- if (!shortcode) {
790
- findings.push(
791
- atData(
792
- note,
793
- ["eras", position],
794
- "error",
795
- "an era is addressed `<calendar shortcode>.<era shortcode>`, so " +
796
- "every row states a `shortcode` of its own",
797
- ),
798
- );
799
- return;
800
- }
874
+ // Absent: the inner-key check's finding.
875
+ if (!shortcode) return;
801
876
  if (seen.has(shortcode)) {
802
877
  findings.push(
803
878
  atData(
@@ -810,26 +885,15 @@ function checkEraShortcodes(note) {
810
885
  );
811
886
  }
812
887
  seen.add(shortcode);
813
- if (era.label === undefined) return;
888
+ // A label's shape and keys are the inner-key check's; its `{date}`
889
+ // slot is this one's.
814
890
  const label = era.label;
815
- const forms = typeof label === "string" ? { after: label } : label;
816
- if (!forms || typeof forms !== "object" || Array.isArray(forms)) {
817
- findings.push(
818
- atData(
819
- note,
820
- ["eras", position, "label"],
821
- "error",
822
- "an era label is a string or an {after, before} map",
823
- ),
824
- );
825
- return;
826
- }
891
+ const forms =
892
+ typeof label === "string" ? { after: label }
893
+ : label && typeof label === "object" && !Array.isArray(label) ? label
894
+ : {};
827
895
  for (const [key, value] of Object.entries(forms)) {
828
- if (
829
- !["after", "before"].includes(key) ||
830
- typeof value !== "string" ||
831
- value.split("{date}").length !== 2
832
- )
896
+ if (typeof value === "string" && value.split("{date}").length !== 2)
833
897
  findings.push(
834
898
  atData(
835
899
  note,
@@ -106,7 +106,7 @@ import { RETIRED_FIELD_ALIASES } from "./retired-fields.mjs";
106
106
  import { authoredKey } from "./system-block.mjs";
107
107
  // The top-level vocabulary, read rather than restated — a second list of the
108
108
  // keys a note's own identity uses is one more thing to drift.
109
- import { NOTE_TOP_LEVEL_KEY_SET, isLegacyDataField } from "./note-vocabulary.mjs";
109
+ import { NOTE_TOP_LEVEL_KEY_SET } from "./note-vocabulary.mjs";
110
110
 
111
111
  /**
112
112
  * Every field path any subtype of a published schema declares.
@@ -371,8 +371,7 @@ export function measureNote(note, format, { severity = "warning" } = {}) {
371
371
  [...spec.dataKeys].map((key) => RETIRED_FIELD_ALIASES[key]).filter(Boolean),
372
372
  );
373
373
  for (const key of Object.keys(data ?? {})) {
374
- if (spec.dataKeys.has(key) || renamedDataKeys.has(key) || isLegacyDataField(type, key))
375
- continue;
374
+ if (spec.dataKeys.has(key) || renamedDataKeys.has(key)) continue;
376
375
  const guess = nearest(key, spec.dataKeys);
377
376
  add(
378
377
  "unknown-data-key",
@@ -52,11 +52,23 @@ export const CONTENT_FORMAT_PATH = path.join(
52
52
  * property — what a note actually writes. `appearance.eye_color` is authored
53
53
  * as `appearance`, so that is the key recorded.
54
54
  * @property {Set<string>} dataPaths - The declared paths, whole.
55
+ * @property {Map<string, DataRow>} [dataRows] - Each documented path's row, as
56
+ * a reference document states it. Read from a Markdown specification only.
55
57
  * @property {string[]} subTypes - The declared `subType` values,
56
58
  * in document order — empty when it states none, which is the ordinary case
57
59
  * for a type that has no `subType` at all.
58
60
  */
59
61
 
62
+ /**
63
+ * One row of a `data` property table, as a reference document states it.
64
+ *
65
+ * @typedef {object} DataRow
66
+ * @property {string} shape - The second cell, the value's shape in words.
67
+ * @property {string} text - The whole row as written, for a reader looking for
68
+ * a marker anywhere in it.
69
+ * @property {number} line - 1-based line of the row.
70
+ */
71
+
60
72
  /**
61
73
  * One `system.*` target the specification names for one note type.
62
74
  *
@@ -95,6 +107,12 @@ export const CONTENT_FORMAT_PATH = path.join(
95
107
  * @property {MappingClaim[]} claims - Every `system.*` target, in document order.
96
108
  * @property {Map<string, VocabularySpec>} vocabularies - Key → the values it
97
109
  * admits, for every closed vocabulary the document states as a table.
110
+ * @property {Map<string, DataRow>} [sharedDataRows] - The rows of the shared
111
+ * `data` property table — the keys every type accepts — by path. Read from a
112
+ * Markdown specification only.
113
+ * @property {Map<string, DataRow>} [eventRows] - The rows of the event key
114
+ * tables, by path from one event entry (`names[].by`). Read from a Markdown
115
+ * specification only.
98
116
  */
99
117
 
100
118
  /**
@@ -245,6 +263,10 @@ export function parseContentFormat(text, { file = CONTENT_FORMAT_PATH } = {}) {
245
263
  const claims = [];
246
264
  /** @type {Map<string, VocabularySpec>} */
247
265
  const vocabularies = new Map();
266
+ /** @type {Map<string, DataRow>} */
267
+ const sharedDataRows = new Map();
268
+ /** @type {Map<string, DataRow>} */
269
+ const eventRows = new Map();
248
270
 
249
271
  const lines = String(text ?? "").split("\n");
250
272
  /** @type {TypeSpec|undefined} */
@@ -262,6 +284,7 @@ export function parseContentFormat(text, { file = CONTENT_FORMAT_PATH } = {}) {
262
284
  line: i + 1,
263
285
  dataKeys: new Set(),
264
286
  dataPaths: new Set(),
287
+ dataRows: new Map(),
265
288
  subTypes: [],
266
289
  };
267
290
  types.set(current.name, current);
@@ -299,6 +322,14 @@ export function parseContentFormat(text, { file = CONTENT_FORMAT_PATH } = {}) {
299
322
  table = { kind: "data", systems: [] };
300
323
  continue;
301
324
  }
325
+ if (cells[0] === "shared `data` property") {
326
+ table = { kind: "shared-data", systems: [] };
327
+ continue;
328
+ }
329
+ if (cells[0] === "event key" || cells[0] === "nested key") {
330
+ table = { kind: "event", systems: [] };
331
+ continue;
332
+ }
302
333
  if (cells[0] === "shared source") {
303
334
  table = {
304
335
  kind: "mapping",
@@ -343,6 +374,18 @@ export function parseContentFormat(text, { file = CONTENT_FORMAT_PATH } = {}) {
343
374
  if (!declared) continue;
344
375
  current.dataPaths.add(declared);
345
376
  current.dataKeys.add(declared.split(".")[0]);
377
+ current.dataRows?.set(declared, { shape: cells[1] ?? "", text: line, line: i + 1 });
378
+ continue;
379
+ }
380
+
381
+ if (table.kind === "shared-data" || table.kind === "event") {
382
+ const declared = code(cells[0]);
383
+ if (declared)
384
+ (table.kind === "event" ? eventRows : sharedDataRows).set(declared, {
385
+ shape: cells[1] ?? "",
386
+ text: line,
387
+ line: i + 1,
388
+ });
346
389
  continue;
347
390
  }
348
391
 
@@ -365,7 +408,7 @@ export function parseContentFormat(text, { file = CONTENT_FORMAT_PATH } = {}) {
365
408
  }
366
409
  }
367
410
 
368
- return { file, types, claims, vocabularies };
411
+ return { file, types, claims, vocabularies, sharedDataRows, eventRows };
369
412
  }
370
413
 
371
414
  /**