@heroiclands/package-build 22.21.0 → 22.23.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.
- package/CHANGELOG.md +223 -0
- package/assets/styles/content-base.css +162 -0
- package/bin/content-commands.mjs +77 -17
- package/bin/package-build.mjs +15 -2
- package/ci/content-format-reference.mjs +68 -1
- package/config.mjs +26 -0
- package/content-config.mjs +153 -6
- package/docs/api.md +120 -89
- package/docs/authoring/assets.md +14 -0
- package/docs/authoring/dates-and-calendars.md +41 -3
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +325 -35
- package/docs/commands.md +12 -10
- package/docs/configuration.md +117 -14
- package/docs/content-format.md +4 -2
- package/docs/guides/book.md +5 -4
- package/docs/guides/reference-pages.md +15 -0
- package/docs/reference/format-details.md +545 -104
- package/docs/reference/note-types.md +30 -13
- package/engine/actor-compiler.mjs +23 -0
- package/engine/anchored-sections.mjs +35 -4
- package/engine/anchors.mjs +26 -30
- package/engine/art-fields.mjs +118 -0
- package/engine/art-slots.mjs +7 -1
- package/engine/asset-index.mjs +160 -10
- package/engine/asset-replacement.mjs +93 -0
- package/engine/asset-types.mjs +240 -0
- package/engine/base-compiler.mjs +139 -22
- package/engine/base-styles.mjs +149 -0
- package/engine/bundles.mjs +13 -2
- package/engine/compendiums.mjs +8 -1
- package/engine/content-blocks.mjs +410 -0
- package/engine/content-embeds.mjs +19 -0
- package/engine/content-figures.mjs +468 -0
- package/engine/content-footnotes.mjs +197 -0
- package/engine/content-format-check.mjs +15 -12
- package/engine/content-format.yaml +1 -1
- package/engine/content-images.mjs +126 -25
- package/engine/content-index.mjs +38 -12
- package/engine/content-links.mjs +14 -6
- package/engine/content-tables.mjs +45 -4
- package/engine/content-tasklists.mjs +189 -0
- package/engine/docs-checks.mjs +4 -4
- package/engine/extension-attributes.mjs +48 -2
- package/engine/field-reference.mjs +31 -10
- package/engine/field-spec.mjs +10 -8
- package/engine/foreign-catalog.mjs +143 -18
- package/engine/frontmatter-lint.mjs +120 -65
- package/engine/frontmatter.mjs +3 -3
- package/engine/heading-attributes.mjs +321 -0
- package/engine/helpers.mjs +109 -41
- package/engine/homepage.mjs +8 -8
- package/engine/index.mjs +3 -0
- package/engine/infobox-registry.mjs +4 -1
- package/engine/infobox-render.mjs +15 -3
- package/engine/infobox.mjs +129 -15
- package/engine/itinerary-scenes.mjs +4 -10
- package/engine/journals.mjs +294 -50
- package/engine/lore-events.mjs +197 -0
- package/engine/macros.mjs +1 -1
- package/engine/markdown-expressions.mjs +434 -59
- package/engine/note-dates.mjs +354 -12
- package/engine/note-frontmatter.mjs +7 -16
- package/engine/note-vocabulary.mjs +183 -42
- package/engine/page-lists.mjs +358 -0
- package/engine/pathnames.mjs +36 -0
- package/engine/pdf-build.mjs +431 -74
- package/engine/pdf-images.mjs +64 -0
- package/engine/pdf-render.mjs +563 -66
- package/engine/rank-ladder.mjs +198 -0
- package/engine/reckoning-markers.mjs +1 -1
- package/engine/retired-fields.mjs +8 -13
- package/engine/scenes.mjs +26 -7
- package/engine/site-build.mjs +166 -23
- package/engine/site-config.mjs +10 -1
- package/engine/site-index.mjs +90 -3
- package/engine/site-preview.mjs +20 -7
- package/engine/social-ties.mjs +52 -14
- package/engine/sql-tables.mjs +66 -9
- package/engine/standings.mjs +89 -6
- package/engine/system-block.mjs +56 -30
- package/engine/web-wikilinks.mjs +48 -1
- package/hm3/actors.mjs +8 -5
- package/manifest.mjs +43 -24
- package/package.json +1 -1
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/actors.mjs +22 -99
- package/sohl/item-fields.mjs +5 -5
- package/sohl/items.mjs +1 -1
- package/sohl/note-schemas.mjs +11 -21
- package/types/config.d.mts +9 -0
- package/types/content-config.d.mts +48 -1
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/anchored-sections.d.mts +24 -0
- package/types/engine/anchors.d.mts +9 -4
- package/types/engine/art-fields.d.mts +64 -0
- package/types/engine/asset-index.d.mts +43 -0
- package/types/engine/asset-replacement.d.mts +22 -0
- package/types/engine/asset-types.d.mts +18 -0
- package/types/engine/base-compiler.d.mts +19 -17
- package/types/engine/base-styles.d.mts +93 -0
- package/types/engine/content-blocks.d.mts +87 -0
- package/types/engine/content-embeds.d.mts +16 -0
- package/types/engine/content-figures.d.mts +101 -0
- package/types/engine/content-footnotes.d.mts +80 -0
- package/types/engine/content-format-check.d.mts +8 -6
- package/types/engine/content-images.d.mts +78 -12
- package/types/engine/content-index.d.mts +8 -2
- package/types/engine/content-tables.d.mts +12 -4
- package/types/engine/content-tasklists.d.mts +72 -0
- package/types/engine/extension-attributes.d.mts +22 -1
- package/types/engine/field-reference.d.mts +21 -0
- package/types/engine/field-spec.d.mts +20 -16
- package/types/engine/foreign-catalog.d.mts +39 -0
- package/types/engine/frontmatter.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +179 -0
- package/types/engine/helpers.d.mts +29 -10
- package/types/engine/homepage.d.mts +7 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-registry.d.mts +4 -1
- package/types/engine/infobox-render.d.mts +6 -1
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/journals.d.mts +41 -12
- package/types/engine/lore-events.d.mts +14 -0
- package/types/engine/markdown-expressions.d.mts +46 -2
- package/types/engine/note-dates.d.mts +64 -1
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-vocabulary.d.mts +82 -6
- package/types/engine/page-lists.d.mts +126 -0
- package/types/engine/pathnames.d.mts +26 -0
- package/types/engine/pdf-build.d.mts +28 -6
- package/types/engine/pdf-images.d.mts +52 -0
- package/types/engine/pdf-render.d.mts +84 -2
- package/types/engine/rank-ladder.d.mts +32 -0
- package/types/engine/retired-fields.d.mts +4 -6
- package/types/engine/site-build.d.mts +27 -4
- package/types/engine/site-config.d.mts +9 -0
- package/types/engine/site-index.d.mts +67 -1
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -7
- package/types/engine/standings.d.mts +21 -3
- package/types/engine/system-block.d.mts +37 -14
- package/types/engine/web-wikilinks.d.mts +3 -1
- package/types/hm3/actors.d.mts +21 -3
- package/types/manifest.d.mts +19 -23
- package/types/sohl/actors.d.mts +6 -7
- package/engine/content-admonitions.mjs +0 -114
- package/engine/content-captions.mjs +0 -181
- package/engine/content-secrets.mjs +0 -112
- package/types/engine/content-admonitions.d.mts +0 -24
- package/types/engine/content-captions.d.mts +0 -45
- package/types/engine/content-secrets.d.mts +0 -17
package/engine/note-dates.mjs
CHANGED
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
* neutral and resolved named dates. A neutral year retains `precision: "year"`
|
|
26
26
|
* and has no `canonicalDay`; day one is used only to calculate its bounds.
|
|
27
27
|
*
|
|
28
|
+
* **Year 0 means any year**, and is accepted only where a caller passes
|
|
29
|
+
* `allowZeroYear` — a `lore` event's `when`. `when: "0.5"` is the fifth day of
|
|
30
|
+
* the year, in every year: the value carries `year: 0` and a `canonicalDay`,
|
|
31
|
+
* but no `canonicalYear` and no `sort`, since it names no point on the axis
|
|
32
|
+
* and cannot date its own note. Every other field keeps the ordinary
|
|
33
|
+
* refusal, and a bare `0` with no day is refused everywhere — any year with
|
|
34
|
+
* no day names nothing.
|
|
35
|
+
*
|
|
28
36
|
* @module
|
|
29
37
|
*/
|
|
30
38
|
|
|
@@ -117,6 +125,39 @@ function unknownRecord() {
|
|
|
117
125
|
};
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Whether a number-valued date may have lost a trailing zero off its day.
|
|
130
|
+
*
|
|
131
|
+
* YAML reads an unquoted `<year>.<day>` as a float, and a float carries no
|
|
132
|
+
* trailing zero: `born: 667.130` and `born: 667.13` both arrive as `667.13`.
|
|
133
|
+
* The authored digits are unrecoverable by then, so the question a reader needs
|
|
134
|
+
* answered is whether a longer day was *possible* — and it was exactly when the
|
|
135
|
+
* value with one more zero on the end still names a day the world's year holds.
|
|
136
|
+
*
|
|
137
|
+
* One zero settles it. A second only makes the day ten times longer again, so a
|
|
138
|
+
* value whose single-zero expansion already falls outside the year has no
|
|
139
|
+
* reading but the one it was handed.
|
|
140
|
+
*
|
|
141
|
+
* **The world's year length is what makes the question answerable**, so a tree
|
|
142
|
+
* whose notes declare none gets no claim either way — the same rule the upper
|
|
143
|
+
* bound on a day follows. Refusing every float there would red a whole corpus
|
|
144
|
+
* over a test nothing could evaluate.
|
|
145
|
+
*
|
|
146
|
+
* @param {number} value - The authored value, as YAML parsed it.
|
|
147
|
+
* @param {number|undefined} daysPerYear - The world's year length.
|
|
148
|
+
* @returns {boolean} True when the authored day is ambiguous.
|
|
149
|
+
*/
|
|
150
|
+
function dayCouldHaveLostAZero(value, daysPerYear) {
|
|
151
|
+
if (Number.isInteger(value)) return false;
|
|
152
|
+
const fraction = String(Math.abs(value)).split(".")[1] ?? "";
|
|
153
|
+
// An exponent-form string carries no day at all.
|
|
154
|
+
if (!/^\d+$/.test(fraction)) return false;
|
|
155
|
+
const tenfold = Number(`${fraction}0`);
|
|
156
|
+
if (!Number.isSafeInteger(tenfold)) return false;
|
|
157
|
+
if (!Number.isSafeInteger(daysPerYear) || daysPerYear < 1) return false;
|
|
158
|
+
return tenfold <= daysPerYear;
|
|
159
|
+
}
|
|
160
|
+
|
|
120
161
|
/**
|
|
121
162
|
* Parse one authored date.
|
|
122
163
|
*
|
|
@@ -166,6 +207,10 @@ export function parseNoteDate(value, options) {
|
|
|
166
207
|
daysPerYear,
|
|
167
208
|
field,
|
|
168
209
|
allowUnknown = true,
|
|
210
|
+
// Narrows the year-0 refusal for exactly one caller: an `events`
|
|
211
|
+
// entry's `when`, where year 0 means "this day, every year" rather
|
|
212
|
+
// than a point on the axis. Every other field keeps the refusal.
|
|
213
|
+
allowZeroYear = false,
|
|
169
214
|
file,
|
|
170
215
|
raw,
|
|
171
216
|
keyPath,
|
|
@@ -194,6 +239,22 @@ export function parseNoteDate(value, options) {
|
|
|
194
239
|
return { date: null, findings };
|
|
195
240
|
};
|
|
196
241
|
|
|
242
|
+
// An unquoted `<year>.<day>` reaches here as a float, and YAML has already
|
|
243
|
+
// dropped a trailing zero off the day: `667.13` and `667.130` arrive as one
|
|
244
|
+
// value, so nothing downstream can tell day 13 from day 130. Refused where
|
|
245
|
+
// the lost zero would still name a day inside the world's year, and read as
|
|
246
|
+
// written where no such day exists — `675.281` cannot have been `675.2810`.
|
|
247
|
+
if (typeof value === "number" && dayCouldHaveLostAZero(value, daysPerYear)) {
|
|
248
|
+
const grown = `${text}0`;
|
|
249
|
+
return refuse(
|
|
250
|
+
`${subject} is a number, and a day's trailing zero is lost before a date ` +
|
|
251
|
+
`is read — \`${text}\` and \`${grown}\` reach this check as one value. ` +
|
|
252
|
+
`Quote a canonical date that states a day, \`"${text}"\` or ` +
|
|
253
|
+
`\`"${grown}"\`, so the digits the note states are the digits the ` +
|
|
254
|
+
`build reads`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
197
258
|
const resolveEra = (parsed, reckoning, label) => {
|
|
198
259
|
if (parsed.year < 1) return refuse(`${subject} needs a positive year in ${label}`);
|
|
199
260
|
if (!Number.isSafeInteger(daysPerYear) || daysPerYear < 1)
|
|
@@ -282,7 +343,14 @@ export function parseNoteDate(value, options) {
|
|
|
282
343
|
return refuse(`${subject} cannot resolve its calendar date: ${err.message}`);
|
|
283
344
|
}
|
|
284
345
|
}
|
|
285
|
-
|
|
346
|
+
// Negative zero (`-0.<day>`) is refused unconditionally — it is the same
|
|
347
|
+
// year as bare `0` written the other way, and `allowZeroYear` means only
|
|
348
|
+
// "any year", never "any year, backwards". A bare positive `0.<day>` is
|
|
349
|
+
// refused everywhere except the one caller that passes `allowZeroYear`.
|
|
350
|
+
if (
|
|
351
|
+
/^-0+\.\d+(?::\d{6})?$/.test(unmarked) ||
|
|
352
|
+
(!allowZeroYear && /^0+\.\d+(?::\d{6})?$/.test(unmarked))
|
|
353
|
+
)
|
|
286
354
|
return refuse(
|
|
287
355
|
`${subject} writes year 0, and no reckoning has one — the years either ` +
|
|
288
356
|
`side of an epoch are -1 and 1`,
|
|
@@ -294,6 +362,28 @@ export function parseNoteDate(value, options) {
|
|
|
294
362
|
);
|
|
295
363
|
if (unmarked.includes(".")) {
|
|
296
364
|
const canonical = parseCanonicalDate(unmarked, daysPerYear ?? Number.MAX_SAFE_INTEGER);
|
|
365
|
+
if (canonical && canonical.year === 0) {
|
|
366
|
+
// Year 0 means "this day, every year" — it names no point on the
|
|
367
|
+
// axis, so it carries no `canonicalYear` and no `sort`: nothing
|
|
368
|
+
// orders it and it cannot date its note, the same silence
|
|
369
|
+
// `unknown` keeps for the fields that carry no axis position.
|
|
370
|
+
return {
|
|
371
|
+
date: {
|
|
372
|
+
text,
|
|
373
|
+
known: true,
|
|
374
|
+
era: null,
|
|
375
|
+
year: 0,
|
|
376
|
+
month: null,
|
|
377
|
+
day: canonical.day,
|
|
378
|
+
approximate: approximateInput,
|
|
379
|
+
precision: "day",
|
|
380
|
+
canonicalDay: canonical.day,
|
|
381
|
+
spanDays: 1,
|
|
382
|
+
...(canonical.seconds === undefined ? {} : { seconds: canonical.seconds }),
|
|
383
|
+
},
|
|
384
|
+
findings,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
297
387
|
if (canonical) {
|
|
298
388
|
return {
|
|
299
389
|
date: {
|
|
@@ -606,19 +696,276 @@ export function formatNoteDate(date, era, daysPerYear, formatName) {
|
|
|
606
696
|
return { era, year, month, day, text, prose };
|
|
607
697
|
}
|
|
608
698
|
|
|
699
|
+
/**
|
|
700
|
+
* The occurrences of one recurring series overlapping a bound.
|
|
701
|
+
*
|
|
702
|
+
* **No materialized occurrence list.** An annual series anchored centuries
|
|
703
|
+
* back has thousands of occurrences to the present, and the only questions a
|
|
704
|
+
* build asks are "is this year one of them" and "which is next" — so an
|
|
705
|
+
* omitted bound answers with a single occurrence rather than every one a live
|
|
706
|
+
* series could have:
|
|
707
|
+
*
|
|
708
|
+
* - `to` omitted: the first occurrence at or after `from` (default, the
|
|
709
|
+
* anchor's own year) — "next".
|
|
710
|
+
* - `from` omitted, `to` given: the last occurrence at or before `to` —
|
|
711
|
+
* "the greatest occurrence at or before `until`".
|
|
712
|
+
* - Both given: every occurrence between them, inclusive — a membership
|
|
713
|
+
* probe is `from === to`.
|
|
714
|
+
*
|
|
715
|
+
* **The lower bound is not optional.** A candidate year before the anchor's
|
|
716
|
+
* own year is never an occurrence, however a bare modulo would read it: `%`
|
|
717
|
+
* takes the sign of its dividend in both JavaScript and SQL, so a year
|
|
718
|
+
* exactly one period before the anchor would otherwise match. `k` is clamped
|
|
719
|
+
* at zero rather than left to run negative.
|
|
720
|
+
*
|
|
721
|
+
* **The period advances the canonical year, never the era-relative year.**
|
|
722
|
+
* `date.canonicalYear` is already on the continuous axis `canonicalYear`
|
|
723
|
+
* builds — era year `-1` sits immediately before era year `1`, but canonical
|
|
724
|
+
* year `0` is an ordinary integer between them — so stepping by whole
|
|
725
|
+
* multiples of `every` on `canonicalYear` crosses an epoch with no special
|
|
726
|
+
* case. Counting in era-relative years instead would need a correction at
|
|
727
|
+
* every crossing and silently loses one year each time it is forgotten.
|
|
728
|
+
*
|
|
729
|
+
* **Precision, approximation and the day of year carry through untouched.**
|
|
730
|
+
* Every occurrence is the anchor's own record with only `canonicalYear`
|
|
731
|
+
* advanced, so a day-precision anchor's `canonicalDay` — including one inside
|
|
732
|
+
* a short intercalary month — names the same day of every occurrence, and an
|
|
733
|
+
* approximate anchor's occurrences are approximate too.
|
|
734
|
+
*
|
|
735
|
+
* @param {object} date - The anchor's resolved record
|
|
736
|
+
* ({@link parseNoteDate}), carrying a real `canonicalYear`. A year-0 "any
|
|
737
|
+
* year" anchor carries none and has no series through this function — its
|
|
738
|
+
* `recurs` is refused, and its own next occurrence is computed directly
|
|
739
|
+
* from the day of year alone.
|
|
740
|
+
* @param {{every?: number, on?: object[]}} [recurs] - The declared
|
|
741
|
+
* recurrence: a whole-year period, or an enumeration of further resolved
|
|
742
|
+
* date records (each already carrying its own `canonicalYear`). Neither
|
|
743
|
+
* admits the other.
|
|
744
|
+
* @param {object} [bounds]
|
|
745
|
+
* @param {number} [bounds.from] - The lower bound, inclusive, as a canonical
|
|
746
|
+
* year.
|
|
747
|
+
* @param {number} [bounds.to] - The upper bound, inclusive, as a canonical
|
|
748
|
+
* year.
|
|
749
|
+
* @returns {object[]} Occurrence records, in increasing order.
|
|
750
|
+
*/
|
|
751
|
+
export function occurrencesOf(date, recurs, { from, to } = {}) {
|
|
752
|
+
if (!date?.known || !Number.isSafeInteger(date.canonicalYear)) return [];
|
|
753
|
+
const anchor = date.canonicalYear;
|
|
754
|
+
|
|
755
|
+
if (Array.isArray(recurs?.on)) {
|
|
756
|
+
if (from === undefined && to === undefined) return [date];
|
|
757
|
+
const series = [date, ...recurs.on]
|
|
758
|
+
.filter((occ) => occ?.known && Number.isSafeInteger(occ.canonicalYear))
|
|
759
|
+
.sort((a, b) => a.canonicalYear - b.canonicalYear);
|
|
760
|
+
const inRange = series.filter(
|
|
761
|
+
(occ) =>
|
|
762
|
+
(from === undefined || occ.canonicalYear >= from) &&
|
|
763
|
+
(to === undefined || occ.canonicalYear <= to),
|
|
764
|
+
);
|
|
765
|
+
if (from === undefined) return inRange.slice(-1);
|
|
766
|
+
if (to === undefined) return inRange.slice(0, 1);
|
|
767
|
+
return inRange;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const every = recurs?.every;
|
|
771
|
+
if (!Number.isSafeInteger(every) || every < 1) {
|
|
772
|
+
const solo = (from === undefined || anchor >= from) && (to === undefined || anchor <= to);
|
|
773
|
+
return solo ? [date] : [];
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const lowestK = from === undefined ? 0 : Math.max(0, Math.ceil((from - anchor) / every));
|
|
777
|
+
if (to === undefined) return [{ ...date, canonicalYear: anchor + lowestK * every }];
|
|
778
|
+
|
|
779
|
+
const highestK = Math.floor((to - anchor) / every);
|
|
780
|
+
if (highestK < lowestK) return [];
|
|
781
|
+
if (from === undefined) return [{ ...date, canonicalYear: anchor + highestK * every }];
|
|
782
|
+
|
|
783
|
+
const out = [];
|
|
784
|
+
for (let k = lowestK; k <= highestK; k += 1)
|
|
785
|
+
out.push({ ...date, canonicalYear: anchor + k * every });
|
|
786
|
+
return out;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** A resolved date's printed form, in the era its own value selects. */
|
|
790
|
+
function withMarkerProse(date, context) {
|
|
791
|
+
const era =
|
|
792
|
+
date.marker ? context?.markers?.get(date.marker)
|
|
793
|
+
: date.qualifier ? context?.eras?.get(date.qualifier)
|
|
794
|
+
: null;
|
|
795
|
+
const printable = formatNoteDate(date, era, context?.daysPerYear);
|
|
796
|
+
return { ...date, prose: printable?.prose ?? null };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* A resolved date's printed form, re-selecting the era that covers its own
|
|
801
|
+
* position rather than keeping whatever era its anchor was authored in.
|
|
802
|
+
*
|
|
803
|
+
* `when`/`until`/`on` are checked exactly as `born`/`died` are: an
|
|
804
|
+
* era-qualified value carries a `marker` or a `qualifier`, which names the
|
|
805
|
+
* calendar as well as the era, and a bare canonical value carries neither and
|
|
806
|
+
* prints bare. What is new here is that a *generated* occurrence keeps the
|
|
807
|
+
* anchor's marker or qualifier — advancing the year only — so that calendar's
|
|
808
|
+
* **whole** era list is swept through {@link eraCovering} for the
|
|
809
|
+
* occurrence's own position, rather than assuming it stays in the era the
|
|
810
|
+
* anchor was in. A later occurrence may therefore print in a different era
|
|
811
|
+
* than its anchor, or print bare where no count was proclaimed for that year;
|
|
812
|
+
* neither is a finding, since nobody authored the occurrence.
|
|
813
|
+
*
|
|
814
|
+
* @param {object} date - The resolved date, generated or authored.
|
|
815
|
+
* @param {object} [context] - As {@link resolvedDateFields} reads it.
|
|
816
|
+
* @returns {object} The date, carrying `prose`.
|
|
817
|
+
*/
|
|
818
|
+
function withSweptEraProse(date, context, { generated = false } = {}) {
|
|
819
|
+
if (!date?.known) return { ...date, prose: null };
|
|
820
|
+
const anchorEra =
|
|
821
|
+
date.marker ? context?.markers?.get(date.marker)
|
|
822
|
+
: date.qualifier ? context?.eras?.get(date.qualifier)
|
|
823
|
+
: null;
|
|
824
|
+
let era = null;
|
|
825
|
+
if (anchorEra?.calendarShortcode) {
|
|
826
|
+
try {
|
|
827
|
+
const calendarRows = calendarEras(anchorEra.calendarShortcode, context);
|
|
828
|
+
era = eraCovering(date, calendarRows, context?.daysPerYear);
|
|
829
|
+
} catch {
|
|
830
|
+
// Ambiguous or overlapping eras are a corpus defect reported
|
|
831
|
+
// where the eras themselves are declared; an occurrence prints
|
|
832
|
+
// bare rather than failing a build over a note that authored
|
|
833
|
+
// nothing wrong.
|
|
834
|
+
era = null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const printable = formatNoteDate(date, era, context?.daysPerYear);
|
|
838
|
+
return {
|
|
839
|
+
...date,
|
|
840
|
+
prose: printable?.prose ?? null,
|
|
841
|
+
// `formatNoteDate`'s own bare `text` echoes `date.text` — the
|
|
842
|
+
// anchor's authored spelling, which a *generated* occurrence has
|
|
843
|
+
// outgrown. Only a generated occurrence with no era to print in
|
|
844
|
+
// needs a text of its own, built from its own canonical year.
|
|
845
|
+
...(generated && !era ?
|
|
846
|
+
{
|
|
847
|
+
text:
|
|
848
|
+
(date.approximate ? "~" : "") +
|
|
849
|
+
(date.precision === "day" && Number.isSafeInteger(date.canonicalDay) ?
|
|
850
|
+
`${eraYear(date.canonicalYear)}.${date.canonicalDay}`
|
|
851
|
+
: String(eraYear(date.canonicalYear))),
|
|
852
|
+
}
|
|
853
|
+
: {}),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** The package's declared present, resolved to a date record, or `null`. */
|
|
858
|
+
function resolvedPresent(context) {
|
|
859
|
+
if (context?.present === undefined || context?.present === null) return null;
|
|
860
|
+
const date = parseNoteDate(context.present, { ...context, allowUnknown: false }).date;
|
|
861
|
+
return date?.known ? date : null;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* One `data.events` entry, resolved: its own `when`, `until`, `recurs` and
|
|
866
|
+
* `next`.
|
|
867
|
+
*
|
|
868
|
+
* `recurs.first` and `recurs.last` are canonical years rather than the
|
|
869
|
+
* fractional `sort` a day-precision date carries, because `every` counts
|
|
870
|
+
* whole years — a SQL fence testing membership with
|
|
871
|
+
* `(year - recurs.first) % recurs.every = 0 AND year >= recurs.first` reads
|
|
872
|
+
* them as the plain integers they are.
|
|
873
|
+
*
|
|
874
|
+
* @param {unknown} entry - The authored `{ when, until?, recurs? }`.
|
|
875
|
+
* @param {object} [context] - As {@link resolvedDateFields} reads it.
|
|
876
|
+
* @returns {object} The resolved entry; a key is present only where the note
|
|
877
|
+
* authored or the build computed something for it.
|
|
878
|
+
*/
|
|
879
|
+
function resolveEventEntry(entry, context) {
|
|
880
|
+
const row = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : {};
|
|
881
|
+
const out = {};
|
|
882
|
+
|
|
883
|
+
const when = parseNoteDate(row.when, {
|
|
884
|
+
...context,
|
|
885
|
+
allowUnknown: true,
|
|
886
|
+
allowZeroYear: true,
|
|
887
|
+
}).date;
|
|
888
|
+
if (when?.known) out.when = withSweptEraProse(when, context);
|
|
889
|
+
|
|
890
|
+
const until = parseNoteDate(row.until, { ...context, allowUnknown: false }).date;
|
|
891
|
+
if (until?.known) out.until = withSweptEraProse(until, context);
|
|
892
|
+
|
|
893
|
+
if (!when?.known) return out;
|
|
894
|
+
|
|
895
|
+
// Year 0: an annual day-of-year marker, recurring by construction. Its
|
|
896
|
+
// own next occurrence is computed directly from the day of year rather
|
|
897
|
+
// than through `occurrencesOf`, which needs a real anchor year — and it
|
|
898
|
+
// carries no `recurs` record, since the entry is already annual.
|
|
899
|
+
if (when.year === 0) {
|
|
900
|
+
const present = resolvedPresent(context);
|
|
901
|
+
if (present && Number.isSafeInteger(when.canonicalDay)) {
|
|
902
|
+
const dueThisYear =
|
|
903
|
+
!Number.isSafeInteger(present.canonicalDay) ||
|
|
904
|
+
present.canonicalDay <= when.canonicalDay;
|
|
905
|
+
const year = dueThisYear ? present.canonicalYear : present.canonicalYear + 1;
|
|
906
|
+
if (!out.until || year <= out.until.canonicalYear) {
|
|
907
|
+
out.next = withSweptEraProse({ ...when, canonicalYear: year }, context, {
|
|
908
|
+
generated: true,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return out;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const recurs = row.recurs;
|
|
916
|
+
if (!recurs || typeof recurs !== "object" || Array.isArray(recurs)) return out;
|
|
917
|
+
|
|
918
|
+
const recursOut = {};
|
|
919
|
+
let series;
|
|
920
|
+
// An enumeration is bounded by construction — its own last entry — so
|
|
921
|
+
// `last` is never null for `on`; a period is live unless `until` bounds
|
|
922
|
+
// it, so `last` is null exactly where there is no `until` to bound it.
|
|
923
|
+
if (Number.isInteger(recurs.every) && recurs.every >= 1) {
|
|
924
|
+
recursOut.every = recurs.every;
|
|
925
|
+
series = { every: recurs.every };
|
|
926
|
+
recursOut.last =
|
|
927
|
+
out.until ?
|
|
928
|
+
(occurrencesOf(when, series, { to: out.until.canonicalYear }).at(-1)
|
|
929
|
+
?.canonicalYear ?? null)
|
|
930
|
+
: null;
|
|
931
|
+
} else if (Array.isArray(recurs.on)) {
|
|
932
|
+
const onDates = recurs.on
|
|
933
|
+
.map((raw) => parseNoteDate(raw, { ...context, allowUnknown: false }).date)
|
|
934
|
+
.filter((parsedOn) => parsedOn?.known);
|
|
935
|
+
recursOut.on = onDates.map((onDate) => withSweptEraProse(onDate, context));
|
|
936
|
+
series = { on: onDates };
|
|
937
|
+
recursOut.last =
|
|
938
|
+
onDates.length ? onDates[onDates.length - 1].canonicalYear : when.canonicalYear;
|
|
939
|
+
} else {
|
|
940
|
+
return out;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
recursOut.first = when.canonicalYear;
|
|
944
|
+
out.recurs = recursOut;
|
|
945
|
+
|
|
946
|
+
const present = resolvedPresent(context);
|
|
947
|
+
if (present?.known) {
|
|
948
|
+
const candidate = occurrencesOf(when, series, { from: present.canonicalYear })[0];
|
|
949
|
+
if (candidate && (!out.until || candidate.canonicalYear <= out.until.canonicalYear)) {
|
|
950
|
+
out.next = withSweptEraProse(candidate, context, { generated: true });
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return out;
|
|
954
|
+
}
|
|
955
|
+
|
|
609
956
|
/** Normalize a note's declared dates for indexes and generated pages. */
|
|
610
957
|
export function resolvedDateFields(fm, context) {
|
|
958
|
+
if (fm?.type === "lore") {
|
|
959
|
+
const events = Array.isArray(fm.data?.events) ? fm.data.events : [];
|
|
960
|
+
const resolved = events.map((entry) => resolveEventEntry(entry, context));
|
|
961
|
+
return resolved.length ? { events: resolved } : {};
|
|
962
|
+
}
|
|
611
963
|
const fields =
|
|
612
964
|
fm?.type === "being" ?
|
|
613
965
|
[
|
|
614
966
|
["born", fm.data?.born],
|
|
615
967
|
["died", fm.data?.died],
|
|
616
968
|
]
|
|
617
|
-
: fm?.type === "lore" ?
|
|
618
|
-
[
|
|
619
|
-
["when", fm.data?.event?.when],
|
|
620
|
-
["until", fm.data?.event?.until],
|
|
621
|
-
]
|
|
622
969
|
: [];
|
|
623
970
|
const out = {};
|
|
624
971
|
for (const [key, value] of fields) {
|
|
@@ -628,12 +975,7 @@ export function resolvedDateFields(fm, context) {
|
|
|
628
975
|
allowUnknown: key === "born" || key === "died",
|
|
629
976
|
});
|
|
630
977
|
if (parsed.date && !parsed.findings.some((finding) => finding.severity === "error")) {
|
|
631
|
-
|
|
632
|
-
parsed.date.marker ? context?.markers?.get(parsed.date.marker)
|
|
633
|
-
: parsed.date.qualifier ? context?.eras?.get(parsed.date.qualifier)
|
|
634
|
-
: null;
|
|
635
|
-
const printable = formatNoteDate(parsed.date, era, context?.daysPerYear);
|
|
636
|
-
out[key] = { ...parsed.date, prose: printable?.prose ?? null };
|
|
978
|
+
out[key] = withMarkerProse(parsed.date, context);
|
|
637
979
|
}
|
|
638
980
|
}
|
|
639
981
|
return out;
|
|
@@ -7,22 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
import YAML from "yaml";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"tags",
|
|
18
|
-
"data",
|
|
19
|
-
"hm3",
|
|
20
|
-
"sohl",
|
|
21
|
-
"dnd5e",
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
/** Membership test for the top-level vocabulary. */
|
|
25
|
-
export const NOTE_TOP_LEVEL_KEY_SET = new Set(NOTE_TOP_LEVEL_KEYS);
|
|
10
|
+
// The top-level vocabulary itself is declared beside the other two closed
|
|
11
|
+
// regions, so the lint, the formatter and the reference read one list.
|
|
12
|
+
export {
|
|
13
|
+
NOTE_TOP_LEVEL_FIELDS,
|
|
14
|
+
NOTE_TOP_LEVEL_KEYS,
|
|
15
|
+
NOTE_TOP_LEVEL_KEY_SET,
|
|
16
|
+
} from "./note-vocabulary.mjs";
|
|
26
17
|
|
|
27
18
|
/** The name properties every note accepts, in display order. */
|
|
28
19
|
export const NOTE_NAME_KEYS = Object.freeze(["full", "aliases"]);
|