@heroiclands/package-build 22.22.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 +32 -0
- package/bin/content-commands.mjs +51 -14
- package/ci/content-format-reference.mjs +1 -1
- package/content-config.mjs +80 -2
- package/docs/api.md +85 -72
- package/docs/authoring/assets.md +14 -0
- package/docs/authoring/dates-and-calendars.md +37 -1
- package/docs/authoring/links-and-markup.md +112 -31
- package/docs/commands.md +12 -10
- package/docs/configuration.md +90 -13
- package/docs/content-format.md +1 -1
- package/docs/guides/book.md +5 -4
- package/docs/reference/format-details.md +323 -60
- package/docs/reference/note-types.md +9 -9
- package/engine/anchors.mjs +16 -10
- 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 +54 -2
- package/engine/bundles.mjs +13 -1
- package/engine/content-blocks.mjs +4 -4
- package/engine/content-embeds.mjs +19 -0
- package/engine/content-figures.mjs +468 -0
- package/engine/content-footnotes.mjs +10 -2
- package/engine/content-format.yaml +1 -1
- package/engine/content-images.mjs +126 -25
- package/engine/content-index.mjs +36 -10
- 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 +73 -1
- package/engine/heading-attributes.mjs +31 -18
- package/engine/helpers.mjs +97 -30
- package/engine/infobox-registry.mjs +4 -1
- package/engine/infobox.mjs +47 -8
- package/engine/itinerary-scenes.mjs +4 -10
- package/engine/journals.mjs +264 -39
- package/engine/lore-events.mjs +197 -0
- package/engine/markdown-expressions.mjs +257 -7
- package/engine/note-dates.mjs +305 -12
- package/engine/note-vocabulary.mjs +11 -5
- package/engine/pathnames.mjs +36 -0
- package/engine/pdf-build.mjs +357 -34
- package/engine/pdf-images.mjs +64 -0
- package/engine/pdf-render.mjs +160 -36
- package/engine/scenes.mjs +16 -4
- package/engine/site-build.mjs +127 -13
- package/engine/site-index.mjs +72 -1
- package/engine/site-preview.mjs +2 -0
- package/engine/system-block.mjs +44 -16
- package/hm3/actors.mjs +2 -2
- package/manifest.mjs +30 -24
- package/package.json +1 -1
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/item-fields.mjs +4 -3
- package/types/content-config.d.mts +48 -1
- 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 +0 -17
- 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 +7 -0
- package/types/engine/content-images.d.mts +78 -12
- package/types/engine/content-index.d.mts +8 -2
- 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/heading-attributes.d.mts +13 -8
- package/types/engine/helpers.d.mts +23 -7
- package/types/engine/infobox-registry.d.mts +4 -1
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/journals.d.mts +31 -9
- package/types/engine/lore-events.d.mts +14 -0
- package/types/engine/markdown-expressions.d.mts +23 -2
- package/types/engine/note-dates.d.mts +64 -1
- 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 +12 -0
- package/types/engine/site-build.d.mts +14 -3
- package/types/engine/site-index.d.mts +54 -0
- package/types/engine/system-block.d.mts +26 -2
- package/types/hm3/actors.d.mts +18 -0
- package/types/manifest.d.mts +19 -23
- package/engine/content-captions.mjs +0 -230
- package/types/engine/content-captions.d.mts +0 -58
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
|
|
|
@@ -199,6 +207,10 @@ export function parseNoteDate(value, options) {
|
|
|
199
207
|
daysPerYear,
|
|
200
208
|
field,
|
|
201
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,
|
|
202
214
|
file,
|
|
203
215
|
raw,
|
|
204
216
|
keyPath,
|
|
@@ -331,7 +343,14 @@ export function parseNoteDate(value, options) {
|
|
|
331
343
|
return refuse(`${subject} cannot resolve its calendar date: ${err.message}`);
|
|
332
344
|
}
|
|
333
345
|
}
|
|
334
|
-
|
|
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
|
+
)
|
|
335
354
|
return refuse(
|
|
336
355
|
`${subject} writes year 0, and no reckoning has one — the years either ` +
|
|
337
356
|
`side of an epoch are -1 and 1`,
|
|
@@ -343,6 +362,28 @@ export function parseNoteDate(value, options) {
|
|
|
343
362
|
);
|
|
344
363
|
if (unmarked.includes(".")) {
|
|
345
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
|
+
}
|
|
346
387
|
if (canonical) {
|
|
347
388
|
return {
|
|
348
389
|
date: {
|
|
@@ -655,19 +696,276 @@ export function formatNoteDate(date, era, daysPerYear, formatName) {
|
|
|
655
696
|
return { era, year, month, day, text, prose };
|
|
656
697
|
}
|
|
657
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
|
+
|
|
658
956
|
/** Normalize a note's declared dates for indexes and generated pages. */
|
|
659
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
|
+
}
|
|
660
963
|
const fields =
|
|
661
964
|
fm?.type === "being" ?
|
|
662
965
|
[
|
|
663
966
|
["born", fm.data?.born],
|
|
664
967
|
["died", fm.data?.died],
|
|
665
968
|
]
|
|
666
|
-
: fm?.type === "lore" ?
|
|
667
|
-
[
|
|
668
|
-
["when", fm.data?.event?.when],
|
|
669
|
-
["until", fm.data?.event?.until],
|
|
670
|
-
]
|
|
671
969
|
: [];
|
|
672
970
|
const out = {};
|
|
673
971
|
for (const [key, value] of fields) {
|
|
@@ -677,12 +975,7 @@ export function resolvedDateFields(fm, context) {
|
|
|
677
975
|
allowUnknown: key === "born" || key === "died",
|
|
678
976
|
});
|
|
679
977
|
if (parsed.date && !parsed.findings.some((finding) => finding.severity === "error")) {
|
|
680
|
-
|
|
681
|
-
parsed.date.marker ? context?.markers?.get(parsed.date.marker)
|
|
682
|
-
: parsed.date.qualifier ? context?.eras?.get(parsed.date.qualifier)
|
|
683
|
-
: null;
|
|
684
|
-
const printable = formatNoteDate(parsed.date, era, context?.daysPerYear);
|
|
685
|
-
out[key] = { ...parsed.date, prose: printable?.prose ?? null };
|
|
978
|
+
out[key] = withMarkerProse(parsed.date, context);
|
|
686
979
|
}
|
|
687
980
|
}
|
|
688
981
|
return out;
|
|
@@ -107,6 +107,7 @@ import { STANDING_BODY_TYPES } from "./standing-terms.mjs";
|
|
|
107
107
|
import { checkDatedOffices } from "./office-holders.mjs";
|
|
108
108
|
import { checkRankLadder } from "./rank-ladder.mjs";
|
|
109
109
|
import { checkCalendarChoice } from "./calendar-choice.mjs";
|
|
110
|
+
import { checkLoreEvents } from "./lore-events.mjs";
|
|
110
111
|
import { checkCultureChoice } from "./culture-choice.mjs";
|
|
111
112
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
112
113
|
import { parseNoteDate } from "./note-dates.mjs";
|
|
@@ -469,7 +470,9 @@ export const SHARED_DATA_FIELDS = Object.freeze([
|
|
|
469
470
|
...LINK,
|
|
470
471
|
ref: "image",
|
|
471
472
|
accepts: ART_SLOTS.find((slot) => slot.key === "banner").accepts,
|
|
472
|
-
describe:
|
|
473
|
+
describe:
|
|
474
|
+
"The page's hero image — an `image` address, cut to 1792×768. " +
|
|
475
|
+
"Reaches no compiled document.",
|
|
473
476
|
}),
|
|
474
477
|
]);
|
|
475
478
|
|
|
@@ -1493,10 +1496,13 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1493
1496
|
data: Object.freeze([
|
|
1494
1497
|
...CALENDAR_FIELDS,
|
|
1495
1498
|
{
|
|
1496
|
-
name: "
|
|
1497
|
-
kind: "
|
|
1498
|
-
shape: "
|
|
1499
|
-
|
|
1499
|
+
name: "events",
|
|
1500
|
+
kind: "list",
|
|
1501
|
+
shape: "list of `{ when, until?, recurs? }`",
|
|
1502
|
+
check: checkLoreEvents,
|
|
1503
|
+
describe:
|
|
1504
|
+
"This note's dated occurrences — a founding once, an anniversary " +
|
|
1505
|
+
"that recurs, or a list of recorded happenings.",
|
|
1500
1506
|
},
|
|
1501
1507
|
]),
|
|
1502
1508
|
}),
|
package/engine/pathnames.mjs
CHANGED
|
@@ -443,3 +443,39 @@ export function resolvePathname(raw, config) {
|
|
|
443
443
|
pdf: `${ASSETS_SEGMENT}/${suffix}`,
|
|
444
444
|
};
|
|
445
445
|
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* The pathname one authored image address keys an asset's role and pixel
|
|
449
|
+
* size under — `<package>/assets/<suffix>`, the same key
|
|
450
|
+
* {@link module:engine/art-fields.assetImageInfoByPathname} builds from the
|
|
451
|
+
* asset record itself.
|
|
452
|
+
*
|
|
453
|
+
* **Why this exists apart from `resolvePathname`'s own forms.** An embed
|
|
454
|
+
* rewrites to that exact pathname — see
|
|
455
|
+
* {@link module:engine/content-embeds.resolveEmbeds} — but an author's own
|
|
456
|
+
* body image is free to write the bare, own-package form instead
|
|
457
|
+
* (`images/m.webp`), which names the identical file without naming the
|
|
458
|
+
* package. The two forms have to key the same lookup, and `resolvePathname`
|
|
459
|
+
* already answers which package a bare form belongs to; this just restates
|
|
460
|
+
* that answer in the one shape a renderer looks a picture's role up by.
|
|
461
|
+
*
|
|
462
|
+
* **Never throws.** A lookup is a best-effort read, not a validation — the
|
|
463
|
+
* pathname lint is what refuses a Foundry-spelled address — so a value
|
|
464
|
+
* {@link resolvePathname} refuses outright answers `null` here as well, the
|
|
465
|
+
* same as a blank or an external one.
|
|
466
|
+
*
|
|
467
|
+
* @param {string} src - The address, as authored or as an embed resolved it.
|
|
468
|
+
* @param {object} config - The resolved build configuration.
|
|
469
|
+
* @returns {string|null} The pathname, or `null` for an address naming no
|
|
470
|
+
* asset record to look up.
|
|
471
|
+
*/
|
|
472
|
+
export function assetPathnameKey(src, config) {
|
|
473
|
+
let forms;
|
|
474
|
+
try {
|
|
475
|
+
forms = resolvePathname(src, config);
|
|
476
|
+
} catch {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
if (!forms || forms.state !== "package" || !forms.package) return null;
|
|
480
|
+
return `${forms.package}/${ASSETS_SEGMENT}/${forms.suffix}`;
|
|
481
|
+
}
|