@heroiclands/package-build 22.16.0 → 22.17.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 +15 -0
- package/bin/package-build.mjs +14 -2
- package/docs/api.md +22 -22
- package/docs/authoring/dates-and-calendars.md +63 -11
- package/docs/authoring/links-and-markup.md +1 -1
- package/docs/commands.md +8 -6
- package/docs/reference/format-details.md +107 -165
- package/docs/reference/note-types.md +11 -10
- package/engine/calendar-format.mjs +441 -0
- package/engine/calendar-human.mjs +65 -3
- package/engine/calendar-notes.mjs +277 -96
- package/engine/content-format.yaml +2 -1
- package/engine/date-conversion.mjs +4 -4
- package/engine/markdown-expressions.mjs +2 -1
- package/engine/note-dates.mjs +50 -22
- package/engine/reckoning-markers.mjs +154 -256
- package/package.json +1 -1
- package/types/engine/calendar-format.d.mts +33 -0
- package/types/engine/calendar-notes.d.mts +3 -3
- package/types/engine/date-conversion.d.mts +1 -1
- package/types/engine/note-dates.d.mts +2 -2
- package/types/engine/reckoning-markers.d.mts +13 -28
|
@@ -64,7 +64,20 @@
|
|
|
64
64
|
* @module
|
|
65
65
|
*/
|
|
66
66
|
|
|
67
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
canonicalDayOffset,
|
|
69
|
+
canonicalYear,
|
|
70
|
+
dayOfYear,
|
|
71
|
+
daysInYear,
|
|
72
|
+
monthDayOfYear,
|
|
73
|
+
parseCanonicalDate,
|
|
74
|
+
} from "./calendars.mjs";
|
|
75
|
+
import {
|
|
76
|
+
CALENDAR_FORMAT_TOKENS,
|
|
77
|
+
calendarFormatSyntaxError,
|
|
78
|
+
readableCalendarFormatError,
|
|
79
|
+
selectCalendarFormat,
|
|
80
|
+
} from "./calendar-format.mjs";
|
|
68
81
|
import { positionInFrontmatter, positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
69
82
|
import { parseNoteDate } from "./note-dates.mjs";
|
|
70
83
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
@@ -89,6 +102,7 @@ export const CALENDAR_DATE_FORMAT_KEYS = Object.freeze([
|
|
|
89
102
|
"long",
|
|
90
103
|
"full",
|
|
91
104
|
"time",
|
|
105
|
+
"time12",
|
|
92
106
|
"weekHeader",
|
|
93
107
|
"yearHeader",
|
|
94
108
|
"yearLabel",
|
|
@@ -96,13 +110,7 @@ export const CALENDAR_DATE_FORMAT_KEYS = Object.freeze([
|
|
|
96
110
|
]);
|
|
97
111
|
|
|
98
112
|
/** Bare date tokens Calendaria substitutes inside authored format strings. */
|
|
99
|
-
export
|
|
100
|
-
new Set(
|
|
101
|
-
"YYYY YY Y MMMM MMM MM Mo M EEEEE EEEE EEE EE E dddd ddd dd Do DDD DD D d e GGGG GGG GG G QQQQ QQQ QQ Q zzzz z ww w W HH H hh h mm m ss s A a".split(
|
|
102
|
-
" ",
|
|
103
|
-
),
|
|
104
|
-
),
|
|
105
|
-
);
|
|
113
|
+
export { CALENDAR_FORMAT_TOKENS };
|
|
106
114
|
|
|
107
115
|
/**
|
|
108
116
|
* The `data:` keys a calendar note may write — the closed list, in authored
|
|
@@ -117,9 +125,9 @@ export const CALENDAR_FORMAT_TOKENS = Object.freeze(
|
|
|
117
125
|
export const CALENDAR_FIELDS = Object.freeze([
|
|
118
126
|
{
|
|
119
127
|
name: "epoch",
|
|
120
|
-
shape: "a
|
|
128
|
+
shape: "a canonical `<year>.<day>`",
|
|
121
129
|
kind: "string",
|
|
122
|
-
describe: "
|
|
130
|
+
describe: "Canonical day when calendar year 1, day 1 begins — `1.1`.",
|
|
123
131
|
},
|
|
124
132
|
{
|
|
125
133
|
name: "months",
|
|
@@ -139,22 +147,28 @@ export const CALENDAR_FIELDS = Object.freeze([
|
|
|
139
147
|
},
|
|
140
148
|
{
|
|
141
149
|
name: "seasons",
|
|
142
|
-
shape: "list of `{ name, abbreviation?,
|
|
150
|
+
shape: "list of `{ name, abbreviation?, start }`",
|
|
151
|
+
kind: "list",
|
|
152
|
+
describe: "The seasons this calendar marks, starting on numbered days of the year.",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "namedDays",
|
|
156
|
+
shape: "list of `{ name, abbreviation?, day }`",
|
|
143
157
|
kind: "list",
|
|
144
|
-
describe: "
|
|
158
|
+
describe: "Names assigned to particular days of the year.",
|
|
145
159
|
},
|
|
146
160
|
{
|
|
147
161
|
name: "eras",
|
|
148
|
-
shape: "list of `{ shortcode, name, marker?, abbreviation?, proclaimedBy?, start,
|
|
162
|
+
shape: "list of `{ shortcode, name, marker?, abbreviation?, proclaimedBy?, start, label? }`",
|
|
149
163
|
kind: "list",
|
|
150
164
|
describe:
|
|
151
165
|
"The year-counts kept in this calendar. A marker names one era and uses these months.",
|
|
152
166
|
},
|
|
153
167
|
{
|
|
154
|
-
name: "
|
|
155
|
-
shape: "map of
|
|
168
|
+
name: "formats",
|
|
169
|
+
shape: "map of named Calendaria format strings",
|
|
156
170
|
kind: "map",
|
|
157
|
-
describe: "
|
|
171
|
+
describe: "Named patterns for reading and writing this calendar's dates.",
|
|
158
172
|
},
|
|
159
173
|
]);
|
|
160
174
|
|
|
@@ -248,7 +262,7 @@ export const INVARIANT_FIELDS = Object.freeze([
|
|
|
248
262
|
},
|
|
249
263
|
{
|
|
250
264
|
name: "moon.newOn",
|
|
251
|
-
shape: "a date",
|
|
265
|
+
shape: "a day-precision date",
|
|
252
266
|
kind: "string",
|
|
253
267
|
describe: "A day the body was new, written in the reference calendar.",
|
|
254
268
|
},
|
|
@@ -363,46 +377,132 @@ function atData(note, keyPath, severity, message) {
|
|
|
363
377
|
};
|
|
364
378
|
}
|
|
365
379
|
|
|
366
|
-
/** Check each
|
|
380
|
+
/** Check each named pattern and the standard pattern's readable fields. */
|
|
367
381
|
export function checkCalendarDateFormats(note) {
|
|
368
|
-
const formats = dataOf(note).
|
|
369
|
-
if (formats === undefined
|
|
370
|
-
if (typeof formats !== "object" || Array.isArray(formats)
|
|
382
|
+
const formats = dataOf(note).formats;
|
|
383
|
+
if (formats === undefined) return [];
|
|
384
|
+
if (typeof formats !== "object" || Array.isArray(formats) || !Object.keys(formats).length)
|
|
385
|
+
return [
|
|
386
|
+
atData(note, ["formats"], "error", "data.formats needs at least one named pattern"),
|
|
387
|
+
];
|
|
371
388
|
const findings = [];
|
|
372
389
|
for (const [slot, value] of Object.entries(formats)) {
|
|
373
|
-
if (
|
|
390
|
+
if (!/^[A-Za-z][A-Za-z0-9]*$/.test(slot)) {
|
|
374
391
|
findings.push(
|
|
375
392
|
atData(
|
|
376
393
|
note,
|
|
377
|
-
["
|
|
394
|
+
["formats", slot],
|
|
378
395
|
"error",
|
|
379
|
-
`data.
|
|
396
|
+
`data.formats.${slot} needs a name made of letters and digits`,
|
|
380
397
|
),
|
|
381
398
|
);
|
|
382
399
|
continue;
|
|
383
400
|
}
|
|
384
401
|
if (typeof value !== "string") {
|
|
385
402
|
findings.push(
|
|
386
|
-
atData(
|
|
387
|
-
note,
|
|
388
|
-
["dateFormats", slot],
|
|
389
|
-
"error",
|
|
390
|
-
`data.dateFormats.${slot} must be a string`,
|
|
391
|
-
),
|
|
403
|
+
atData(note, ["formats", slot], "error", `data.formats.${slot} must be a string`),
|
|
392
404
|
);
|
|
393
405
|
continue;
|
|
394
406
|
}
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
407
|
+
const syntaxError = calendarFormatSyntaxError(value);
|
|
408
|
+
if (syntaxError) findings.push(atData(note, ["formats", slot], "error", syntaxError));
|
|
409
|
+
}
|
|
410
|
+
const standard = Object.hasOwn(formats, "std") ? "std" : Object.keys(formats)[0];
|
|
411
|
+
if (typeof formats[standard] === "string") {
|
|
412
|
+
const error = readableCalendarFormatError(formats[standard]);
|
|
413
|
+
if (error) findings.push(atData(note, ["formats", standard], "error", error));
|
|
414
|
+
}
|
|
415
|
+
return findings;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Validate ordered, one-based season starts and named day positions. */
|
|
419
|
+
function checkCalendarDayNames(note, daysPerYear) {
|
|
420
|
+
const data = dataOf(note);
|
|
421
|
+
const findings = [];
|
|
422
|
+
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
|
+
if (Array.isArray(seasons)) {
|
|
426
|
+
let prior = 0;
|
|
427
|
+
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
|
+
)
|
|
438
|
+
findings.push(
|
|
439
|
+
atData(
|
|
440
|
+
note,
|
|
441
|
+
["seasons", position, "start"],
|
|
442
|
+
"error",
|
|
443
|
+
"season starts must increase within the year",
|
|
444
|
+
),
|
|
445
|
+
);
|
|
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
|
+
prior = start;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
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
|
+
if (Array.isArray(namedDays)) {
|
|
466
|
+
const seen = new Set();
|
|
467
|
+
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;
|
|
478
|
+
if (
|
|
479
|
+
!Number.isSafeInteger(day) ||
|
|
480
|
+
day < 1 ||
|
|
481
|
+
(Number.isSafeInteger(daysPerYear) && day > daysPerYear) ||
|
|
482
|
+
seen.has(day)
|
|
483
|
+
)
|
|
484
|
+
findings.push(
|
|
485
|
+
atData(
|
|
486
|
+
note,
|
|
487
|
+
["namedDays", position, "day"],
|
|
488
|
+
"error",
|
|
489
|
+
"named days need distinct days within the year",
|
|
490
|
+
),
|
|
491
|
+
);
|
|
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
|
+
seen.add(day);
|
|
406
506
|
}
|
|
407
507
|
}
|
|
408
508
|
return findings;
|
|
@@ -466,16 +566,24 @@ export function checkWorldFacts(note, { index } = {}) {
|
|
|
466
566
|
findings.push(...dated);
|
|
467
567
|
}
|
|
468
568
|
if (written.includes("moon") && dataOf(note).moon?.newOn !== undefined) {
|
|
469
|
-
|
|
470
|
-
...
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
)
|
|
569
|
+
const parsed = parseNoteDate(dataOf(note).moon.newOn, {
|
|
570
|
+
...reckoningContext(index),
|
|
571
|
+
field: "data.moon.newOn",
|
|
572
|
+
allowUnknown: false,
|
|
573
|
+
file: note.file,
|
|
574
|
+
raw: note.raw,
|
|
575
|
+
keyPath: ["data", "moon", "newOn"],
|
|
576
|
+
});
|
|
577
|
+
findings.push(...parsed.findings);
|
|
578
|
+
if (parsed.date && parsed.date.precision !== "day")
|
|
579
|
+
findings.push(
|
|
580
|
+
atData(
|
|
581
|
+
note,
|
|
582
|
+
["moon", "newOn"],
|
|
583
|
+
"error",
|
|
584
|
+
"`data.moon.newOn` needs a specific day; write `<year>.<day>` or a day in a named calendar",
|
|
585
|
+
),
|
|
586
|
+
);
|
|
479
587
|
}
|
|
480
588
|
|
|
481
589
|
// The corpus questions, which need every note and so are asked only where
|
|
@@ -559,7 +667,7 @@ export function checkCalendarNote(note, { index } = {}) {
|
|
|
559
667
|
return findings;
|
|
560
668
|
}
|
|
561
669
|
|
|
562
|
-
for (const required of ["months", "epoch"]) {
|
|
670
|
+
for (const required of ["months", "epoch", "eras"]) {
|
|
563
671
|
if (written.includes(required)) continue;
|
|
564
672
|
findings.push({
|
|
565
673
|
file: note.file,
|
|
@@ -572,20 +680,53 @@ export function checkCalendarNote(note, { index } = {}) {
|
|
|
572
680
|
}
|
|
573
681
|
|
|
574
682
|
if (written.includes("epoch")) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
683
|
+
const epochText = String(dataOf(note).epoch);
|
|
684
|
+
if (!/^-?\d+\.\d+$/.test(epochText) || /^-?0+\./.test(epochText))
|
|
685
|
+
findings.push(
|
|
686
|
+
atData(
|
|
687
|
+
note,
|
|
688
|
+
["epoch"],
|
|
689
|
+
"error",
|
|
690
|
+
"data.epoch is a canonical <year>.<day> for calendar year 1, day 1",
|
|
691
|
+
),
|
|
692
|
+
);
|
|
693
|
+
const parsed = parseNoteDate(dataOf(note).epoch, {
|
|
694
|
+
...reckoningContext(index),
|
|
695
|
+
field: "data.epoch",
|
|
696
|
+
allowUnknown: false,
|
|
697
|
+
file: note.file,
|
|
698
|
+
raw: note.raw,
|
|
699
|
+
keyPath: ["data", "epoch"],
|
|
700
|
+
});
|
|
701
|
+
findings.push(...parsed.findings);
|
|
702
|
+
if (parsed.date && parsed.date.precision !== "day")
|
|
703
|
+
findings.push(
|
|
704
|
+
atData(
|
|
705
|
+
note,
|
|
706
|
+
["epoch"],
|
|
707
|
+
"error",
|
|
708
|
+
"`data.epoch` needs a specific day; write `<year>.<day>` or a day in a named calendar",
|
|
709
|
+
),
|
|
710
|
+
);
|
|
585
711
|
}
|
|
586
712
|
|
|
587
713
|
findings.push(...checkMonthSum(note, index));
|
|
714
|
+
findings.push(...checkCalendarDayNames(note, worldInvariants(index).year?.days));
|
|
588
715
|
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
|
+
}
|
|
589
730
|
findings.push(
|
|
590
731
|
...reckoningContext(index).findings.filter((finding) => finding.file === note.file),
|
|
591
732
|
);
|
|
@@ -749,7 +890,7 @@ function monthValues(months) {
|
|
|
749
890
|
);
|
|
750
891
|
}
|
|
751
892
|
|
|
752
|
-
/**
|
|
893
|
+
/** Calendaria stores a one-based ordinal; the authored order remains zero-based. */
|
|
753
894
|
function weekdayValues(weekdays) {
|
|
754
895
|
return (Array.isArray(weekdays) ? weekdays : []).map((day, position) =>
|
|
755
896
|
written({
|
|
@@ -760,42 +901,47 @@ function weekdayValues(weekdays) {
|
|
|
760
901
|
);
|
|
761
902
|
}
|
|
762
903
|
|
|
763
|
-
/** A season's
|
|
764
|
-
function seasonValues(seasons) {
|
|
765
|
-
return (Array.isArray(seasons) ? seasons : []).map((season) =>
|
|
904
|
+
/** A season's one-based start becomes an inclusive zero-based Calendaria range. */
|
|
905
|
+
function seasonValues(seasons, daysPerYear) {
|
|
906
|
+
return (Array.isArray(seasons) ? seasons : []).map((season, position, rows) =>
|
|
766
907
|
written({
|
|
767
908
|
name: String(season?.name ?? ""),
|
|
768
909
|
abbreviation:
|
|
769
910
|
season?.abbreviation === undefined ? undefined : String(season.abbreviation),
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
911
|
+
dayStart: season.start - 1,
|
|
912
|
+
dayEnd:
|
|
913
|
+
position + 1 < rows.length ? rows[position + 1].start - 2
|
|
914
|
+
: rows[0].start === 1 ? daysPerYear - 1
|
|
915
|
+
: rows[0].start - 2,
|
|
916
|
+
}),
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** A named day uses Calendaria's one-based year day number. */
|
|
921
|
+
function namedDayValues(namedDays) {
|
|
922
|
+
return (Array.isArray(namedDays) ? namedDays : []).map((day) =>
|
|
923
|
+
written({
|
|
924
|
+
name: day.name,
|
|
925
|
+
abbreviation: day.abbreviation,
|
|
926
|
+
dayNumber: day.day,
|
|
774
927
|
}),
|
|
775
928
|
);
|
|
776
929
|
}
|
|
777
930
|
|
|
778
931
|
/** The era rows, keyed by shortcode, with the addressing segments dropped. */
|
|
779
|
-
function eraEntries(eras
|
|
932
|
+
function eraEntries(eras) {
|
|
780
933
|
const entries = {};
|
|
781
|
-
|
|
934
|
+
const dated = (Array.isArray(eras) ? eras : [])
|
|
935
|
+
.filter((era) => Number.isSafeInteger(era?.start) && era.start >= 1)
|
|
936
|
+
.sort((a, b) => a.start - b.start);
|
|
937
|
+
for (const [position, era] of dated.entries()) {
|
|
782
938
|
const shortcode = String(era?.shortcode ?? "");
|
|
783
939
|
if (!shortcode) continue;
|
|
784
|
-
const start = parseNoteDate(era?.start, {
|
|
785
|
-
...dateContext,
|
|
786
|
-
allowUnknown: false,
|
|
787
|
-
ignoreEraBounds: true,
|
|
788
|
-
}).date;
|
|
789
|
-
const end = parseNoteDate(era?.end, {
|
|
790
|
-
...dateContext,
|
|
791
|
-
allowUnknown: false,
|
|
792
|
-
ignoreEraBounds: true,
|
|
793
|
-
}).date;
|
|
794
940
|
entries[shortcode] = written({
|
|
795
941
|
name: String(era?.name ?? ""),
|
|
796
942
|
abbreviation: era?.abbreviation === undefined ? undefined : String(era.abbreviation),
|
|
797
|
-
|
|
798
|
-
|
|
943
|
+
startYear: era.start,
|
|
944
|
+
endYear: dated[position + 1] ? dated[position + 1].start - 1 : null,
|
|
799
945
|
});
|
|
800
946
|
}
|
|
801
947
|
return entries;
|
|
@@ -817,6 +963,8 @@ function eraEntries(eras, dateContext = {}) {
|
|
|
817
963
|
function moonEntries(moon, body, name, months, dateContext = {}) {
|
|
818
964
|
if (!moon || !name) return null;
|
|
819
965
|
const date = parseNoteDate(moon.newOn, { ...dateContext, allowUnknown: false }).date;
|
|
966
|
+
if (date && date.precision !== "day")
|
|
967
|
+
throw new RangeError("data.moon.newOn needs a specific day");
|
|
820
968
|
const calendarDay =
|
|
821
969
|
date?.canonicalDay && months.length ? monthDayOfYear(months, date.canonicalDay) : null;
|
|
822
970
|
return {
|
|
@@ -862,7 +1010,26 @@ export function compileCalendar({ note, invariants, contentPackage, dateContext
|
|
|
862
1010
|
const data = dataOf(note);
|
|
863
1011
|
const year = invariants?.year ?? {};
|
|
864
1012
|
const months = Array.isArray(data.months) ? data.months : [];
|
|
865
|
-
const
|
|
1013
|
+
const dayNameError = checkCalendarDayNames(note, year.days)[0];
|
|
1014
|
+
if (dayNameError) throw new RangeError(dayNameError.message);
|
|
1015
|
+
const starts = Array.isArray(data.eras) ? data.eras.map((era) => era?.start) : [];
|
|
1016
|
+
if (
|
|
1017
|
+
starts.filter((start) => start === null).length !== 1 ||
|
|
1018
|
+
starts.filter((start) => start === 1).length !== 1 ||
|
|
1019
|
+
starts.some((start) => start !== null && (!Number.isSafeInteger(start) || start < 1)) ||
|
|
1020
|
+
new Set(starts).size !== starts.length
|
|
1021
|
+
)
|
|
1022
|
+
throw new RangeError(
|
|
1023
|
+
"data.eras needs one null start, one year-one start, and distinct positive year starts",
|
|
1024
|
+
);
|
|
1025
|
+
const formatError = checkCalendarDateFormats(note).find(
|
|
1026
|
+
(finding) => finding.severity === "error",
|
|
1027
|
+
);
|
|
1028
|
+
if (formatError) throw new RangeError(formatError.message);
|
|
1029
|
+
const epoch = parseCanonicalDate(String(data.epoch ?? ""), year.days);
|
|
1030
|
+
if (!epoch || epoch.year === 0 || epoch.seconds !== undefined)
|
|
1031
|
+
throw new RangeError("data.epoch needs a canonical <year>.<day> specific day");
|
|
1032
|
+
const epochOffset = canonicalDayOffset(canonicalYear(epoch.year), epoch.day, 1, year.days);
|
|
866
1033
|
const shortcode = String(note.fm?.shortcode ?? "");
|
|
867
1034
|
const name = String(note.fm?.name?.full ?? "");
|
|
868
1035
|
const description = String(note.fm?.description ?? "");
|
|
@@ -879,9 +1046,8 @@ export function compileCalendar({ note, invariants, contentPackage, dateContext
|
|
|
879
1046
|
name,
|
|
880
1047
|
description,
|
|
881
1048
|
years: {
|
|
882
|
-
//
|
|
883
|
-
|
|
884
|
-
yearZero: epoch?.year ?? 0,
|
|
1049
|
+
// Internal year zero displays as calendar year one at the epoch.
|
|
1050
|
+
yearZero: 1,
|
|
885
1051
|
// Required, non-nullable and with no initial, so it must be
|
|
886
1052
|
// written; with no weekdays it indexes nothing and 0 is the only
|
|
887
1053
|
// defensible value.
|
|
@@ -893,19 +1059,34 @@ export function compileCalendar({ note, invariants, contentPackage, dateContext
|
|
|
893
1059
|
months: { values: monthValues(months) },
|
|
894
1060
|
days: written({
|
|
895
1061
|
values: weekdayValues(data.weekdays),
|
|
1062
|
+
names: namedDayValues(data.namedDays),
|
|
896
1063
|
daysPerYear: year.days,
|
|
897
1064
|
hoursPerDay: year.hoursPerDay,
|
|
898
1065
|
minutesPerHour: year.minutesPerHour,
|
|
899
1066
|
secondsPerMinute: year.secondsPerMinute,
|
|
900
1067
|
}),
|
|
901
|
-
seasons: { values: seasonValues(data.seasons) },
|
|
902
|
-
//
|
|
903
|
-
epochDayOffset:
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1068
|
+
seasons: { values: seasonValues(data.seasons, year.days) },
|
|
1069
|
+
// Shift world time so the authored epoch is calendar year one, day one.
|
|
1070
|
+
epochDayOffset: epochOffset === 0 ? 0 : -epochOffset,
|
|
1071
|
+
eras: eraEntries(data.eras),
|
|
1072
|
+
dateFormats:
|
|
1073
|
+
data.formats && Object.keys(data.formats).length ?
|
|
1074
|
+
written({
|
|
1075
|
+
short: selectCalendarFormat(data).pattern,
|
|
1076
|
+
long: data.formats.long ?? selectCalendarFormat(data).pattern,
|
|
1077
|
+
full:
|
|
1078
|
+
data.formats.full ??
|
|
1079
|
+
data.formats.long ??
|
|
1080
|
+
selectCalendarFormat(data).pattern,
|
|
1081
|
+
...Object.fromEntries(
|
|
1082
|
+
CALENDAR_DATE_FORMAT_KEYS.filter(
|
|
1083
|
+
(key) => !["short", "long", "full"].includes(key),
|
|
1084
|
+
)
|
|
1085
|
+
.filter((key) => data.formats[key] !== undefined)
|
|
1086
|
+
.map((key) => [key, data.formats[key]]),
|
|
1087
|
+
),
|
|
1088
|
+
})
|
|
1089
|
+
: undefined,
|
|
909
1090
|
moons: moons ?? undefined,
|
|
910
1091
|
metadata: written({
|
|
911
1092
|
id: shortcode || undefined,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
2
|
|
|
3
|
-
import { formatCanonicalDate, parseCanonicalDate } from "./calendars.mjs";
|
|
3
|
+
import { eraYear, formatCanonicalDate, parseCanonicalDate } from "./calendars.mjs";
|
|
4
4
|
import { addressedCalendarDate } from "./calendar-human.mjs";
|
|
5
5
|
import { calendarEras, eraCovering, formatNoteDate, parseNoteDate } from "./note-dates.mjs";
|
|
6
6
|
|
|
@@ -23,13 +23,13 @@ export function dateFromCalendar(reference, value, context) {
|
|
|
23
23
|
if (eraCovering(date, eras, context.daysPerYear) !== context.eras.get(date.qualifier))
|
|
24
24
|
throw new RangeError(`date falls outside calendar ${reference}`);
|
|
25
25
|
return `${date.approximate ? "~" : ""}${formatCanonicalDate(
|
|
26
|
-
{ year: date.canonicalYear, day: date.canonicalDay, seconds: date.seconds },
|
|
26
|
+
{ year: eraYear(date.canonicalYear), day: date.canonicalDay, seconds: date.seconds },
|
|
27
27
|
context.daysPerYear,
|
|
28
28
|
)}`;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Convert a canonical day to the date in the covering era of a calendar. */
|
|
32
|
-
export function dateToCalendar(reference, value, context) {
|
|
32
|
+
export function dateToCalendar(reference, value, context, formatName) {
|
|
33
33
|
const text = String(value ?? "");
|
|
34
34
|
const canonical = parseCanonicalDate(
|
|
35
35
|
text.startsWith("~") ? text.slice(1) : text,
|
|
@@ -40,7 +40,7 @@ export function dateToCalendar(reference, value, context) {
|
|
|
40
40
|
const parsed = parseNoteDate(value, context).date;
|
|
41
41
|
const era = eraCovering(parsed, calendarEras(reference, context), context.daysPerYear);
|
|
42
42
|
if (!era) throw new RangeError(`canonical date falls outside calendar ${reference}`);
|
|
43
|
-
const printed = formatNoteDate(parsed, era, context.daysPerYear);
|
|
43
|
+
const printed = formatNoteDate(parsed, era, context.daysPerYear, formatName);
|
|
44
44
|
if (!printed || printed.day === null)
|
|
45
45
|
throw new RangeError(`canonical date cannot be expressed in calendar ${reference}`);
|
|
46
46
|
return printed.text;
|
|
@@ -80,7 +80,7 @@ export function renderMarkdownExpressions(
|
|
|
80
80
|
if (result.error) throw new RangeError(`SQL expression failed: ${result.error}`);
|
|
81
81
|
return result.value;
|
|
82
82
|
});
|
|
83
|
-
engine.registerHelper("dateformat", (calendar, authored) => {
|
|
83
|
+
engine.registerHelper("dateformat", (calendar, authored, formatOrOptions) => {
|
|
84
84
|
if (
|
|
85
85
|
!(typeof calendar === "string" || isAddressTuple(calendar)) ||
|
|
86
86
|
!["string", "number"].includes(typeof authored)
|
|
@@ -93,6 +93,7 @@ export function renderMarkdownExpressions(
|
|
|
93
93
|
parsed.date,
|
|
94
94
|
isAddressTuple(calendar) ? renderAddress(calendar) : calendar,
|
|
95
95
|
dates,
|
|
96
|
+
typeof formatOrOptions === "string" ? formatOrOptions : undefined,
|
|
96
97
|
);
|
|
97
98
|
if (!printable) throw new RangeError(`date cannot be printed in calendar ${calendar}`);
|
|
98
99
|
return printable.prose ?? printable.text;
|