@heroiclands/package-build 22.16.0 → 22.17.1
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 +24 -0
- package/bin/content-commands.mjs +93 -14
- package/bin/package-build.mjs +14 -2
- package/config.mjs +54 -3
- 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 +78 -40
- package/docs/configuration.md +49 -22
- 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/readability-lint.mjs +196 -18
- package/engine/reckoning-markers.mjs +154 -256
- package/package.json +5 -1
- package/types/config.d.mts +14 -2
- 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/readability-lint.d.mts +11 -5
- package/types/engine/reckoning-markers.d.mts +13 -28
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 22.17.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
**Prose review**
|
|
8
|
+
|
|
9
|
+
- Prose lint focuses on difficult sentences by default and skips tables and code; simpler-word suggestions remain available on request.
|
|
10
|
+
- Prose score reports each note's readability and lets a content package define an advisory range for its writing.
|
|
11
|
+
|
|
12
|
+
## 22.17.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
**Calendar dates**
|
|
17
|
+
|
|
18
|
+
- Calendar notes anchor their first year to a canonical day, count eras from calendar years, and include an era for dates before year one.
|
|
19
|
+
- Named calendar formats control how dates print; the standard format also controls how authored calendar dates are read.
|
|
20
|
+
- Calendars can name special days and divide the year into seasons, with those names available in formatted dates.
|
|
21
|
+
|
|
22
|
+
**Content dates**
|
|
23
|
+
|
|
24
|
+
- A date written as a year covers that whole year; a written day covers that day, and a clock time identifies a moment.
|
|
25
|
+
- Calendar starting days and moon phase references require a day so their dates cannot silently shift to the first day of a year.
|
|
26
|
+
|
|
3
27
|
## 22.16.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
package/bin/content-commands.mjs
CHANGED
|
@@ -97,7 +97,7 @@ import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
|
|
|
97
97
|
import { schemaSubtypeOf } from "../engine/subtype-registry.mjs";
|
|
98
98
|
import { NOTE_VOCABULARY } from "../engine/note-vocabulary.mjs";
|
|
99
99
|
import { checkFormatting, checkPrettierConventions, lintMarkdown } from "../engine/prose-lint.mjs";
|
|
100
|
-
import { lintProse } from "../engine/readability-lint.mjs";
|
|
100
|
+
import { lintProse, scoreProseTree } from "../engine/readability-lint.mjs";
|
|
101
101
|
import { loadPackageBuildConfig } from "../config.mjs";
|
|
102
102
|
import {
|
|
103
103
|
authoredFrontmatter,
|
|
@@ -240,14 +240,14 @@ export function registerContentCommands(cli) {
|
|
|
240
240
|
.command(addressesCommand());
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
/**
|
|
243
|
+
/** Optional sentence findings and note-level readability reports. */
|
|
244
244
|
function proseCommand() {
|
|
245
245
|
return {
|
|
246
246
|
command: "prose <action> [path]",
|
|
247
247
|
describe: "Analyze note prose on demand",
|
|
248
248
|
builder: (yargs) =>
|
|
249
249
|
yargs
|
|
250
|
-
.positional("action", { choices: ["lint"], describe: "Analyze prose" })
|
|
250
|
+
.positional("action", { choices: ["lint", "score"], describe: "Analyze prose" })
|
|
251
251
|
.positional("path", {
|
|
252
252
|
type: "string",
|
|
253
253
|
describe: "One Markdown file or a content tree",
|
|
@@ -257,17 +257,97 @@ function proseCommand() {
|
|
|
257
257
|
type: "number",
|
|
258
258
|
describe: "Readability algorithms required (1–7)",
|
|
259
259
|
})
|
|
260
|
-
.option("min-words", {
|
|
260
|
+
.option("min-words", {
|
|
261
|
+
type: "number",
|
|
262
|
+
describe: "Minimum words per sentence or note",
|
|
263
|
+
})
|
|
264
|
+
.option("rules", {
|
|
265
|
+
choices: ["readability", "simplify", "all"],
|
|
266
|
+
describe: "Lint rule set",
|
|
267
|
+
})
|
|
268
|
+
.option("fail-on-warning", {
|
|
269
|
+
type: "boolean",
|
|
270
|
+
default: false,
|
|
271
|
+
describe: "Exit unsuccessfully when lint reports suggestions",
|
|
272
|
+
})
|
|
273
|
+
.option("fail-outside", {
|
|
274
|
+
type: "boolean",
|
|
275
|
+
default: false,
|
|
276
|
+
describe: "Exit unsuccessfully when a scored note is outside a configured band",
|
|
277
|
+
}),
|
|
261
278
|
handler: async (argv) => {
|
|
262
279
|
try {
|
|
263
280
|
const config = loadPackConfig();
|
|
264
|
-
const defaults = loadPackageBuildConfig()
|
|
281
|
+
const defaults = loadPackageBuildConfig();
|
|
282
|
+
const root = argv.path ?? config.paths.content;
|
|
283
|
+
const displayFile = (file) => path.relative(process.cwd(), path.resolve(file));
|
|
284
|
+
if (argv.action === "score") {
|
|
285
|
+
if (
|
|
286
|
+
argv.age !== undefined ||
|
|
287
|
+
argv.threshold !== undefined ||
|
|
288
|
+
argv.rules !== undefined ||
|
|
289
|
+
argv.failOnWarning
|
|
290
|
+
) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
"--age, --threshold, --rules, and --fail-on-warning apply only to prose lint",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const minWords = argv.minWords ?? defaults.proseScore.minWords;
|
|
296
|
+
if (!Number.isInteger(minWords) || minWords < 1) {
|
|
297
|
+
throw new Error("minWords must be a positive integer");
|
|
298
|
+
}
|
|
299
|
+
const result = await scoreProseTree(
|
|
300
|
+
root,
|
|
301
|
+
{ ...defaults.proseScore, minWords },
|
|
302
|
+
config.skipDirectories,
|
|
303
|
+
);
|
|
304
|
+
const fixed = (number, places = 1) => number.toFixed(places);
|
|
305
|
+
for (const note of result.notes) {
|
|
306
|
+
if (note.insufficient) {
|
|
307
|
+
console.log(
|
|
308
|
+
`${displayFile(note.file)}: insufficient prose (${note.words}/${minWords} words; coverage=${fixed(note.coveragePercent)}%)`,
|
|
309
|
+
);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const metrics = note.metrics;
|
|
313
|
+
const detail =
|
|
314
|
+
`flesch=${fixed(metrics.flesch)} syl/word=${fixed(metrics.syllablesPerWord, 2)} ` +
|
|
315
|
+
`mean=${fixed(metrics.meanSentenceWords)}w longest=${note.longestSentenceWords}w ` +
|
|
316
|
+
`unfamiliar=${fixed(metrics.unfamiliarWordPercent)}% ` +
|
|
317
|
+
`nominalizations/1k=${fixed(metrics.nominalizationsPer1000Words)} ` +
|
|
318
|
+
`words=${note.words} coverage=${fixed(note.coveragePercent)}%`;
|
|
319
|
+
if (note.violations.length) {
|
|
320
|
+
emitDiagnostic({
|
|
321
|
+
file: displayFile(note.file),
|
|
322
|
+
severity: "warning",
|
|
323
|
+
message: `prose-score/band: ${detail}; ${note.violations.join(", ")}`,
|
|
324
|
+
});
|
|
325
|
+
} else {
|
|
326
|
+
console.log(`${displayFile(note.file)}: ${detail}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const aggregate = result.summary;
|
|
330
|
+
console.log(
|
|
331
|
+
`Total: ${aggregate.scored} scored, ${aggregate.insufficient} insufficient, ` +
|
|
332
|
+
`${aggregate.outside} outside band; ${aggregate.words} words; ` +
|
|
333
|
+
`pooled flesch=${aggregate.flesch === null ? "n/a" : fixed(aggregate.flesch)} ` +
|
|
334
|
+
`syl/word=${aggregate.words ? fixed(aggregate.syllables / aggregate.words, 2) : "n/a"} ` +
|
|
335
|
+
`mean=${aggregate.sentences ? fixed(aggregate.words / aggregate.sentences) : "n/a"}w ` +
|
|
336
|
+
`unfamiliar=${aggregate.words ? fixed((100 * aggregate.unfamiliar) / aggregate.words) : "n/a"}% ` +
|
|
337
|
+
`nominalizations/1k=${aggregate.words ? fixed((1000 * aggregate.nominalizations) / aggregate.words) : "n/a"}`,
|
|
338
|
+
);
|
|
339
|
+
if (argv.failOutside && aggregate.outside) process.exitCode = 1;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (argv.failOutside) throw new Error("--fail-outside applies only to prose score");
|
|
265
343
|
const options = {
|
|
266
|
-
age: argv.age ?? defaults.age,
|
|
267
|
-
threshold: argv.threshold ?? defaults.threshold,
|
|
268
|
-
minWords: argv.minWords ?? defaults.minWords,
|
|
344
|
+
age: argv.age ?? defaults.proseLint.age,
|
|
345
|
+
threshold: argv.threshold ?? defaults.proseLint.threshold,
|
|
346
|
+
minWords: argv.minWords ?? defaults.proseLint.minWords,
|
|
347
|
+
rules: argv.rules ?? defaults.proseLint.rules,
|
|
269
348
|
};
|
|
270
349
|
for (const [key, value] of Object.entries(options)) {
|
|
350
|
+
if (key === "rules") continue;
|
|
271
351
|
if (
|
|
272
352
|
!Number.isInteger(value) ||
|
|
273
353
|
value < 1 ||
|
|
@@ -278,13 +358,12 @@ function proseCommand() {
|
|
|
278
358
|
);
|
|
279
359
|
}
|
|
280
360
|
}
|
|
281
|
-
const findings = await lintProse(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
);
|
|
286
|
-
for (const finding of findings) emitDiagnostic(finding);
|
|
361
|
+
const findings = await lintProse(root, options, config.skipDirectories);
|
|
362
|
+
for (const finding of findings) {
|
|
363
|
+
emitDiagnostic({ ...finding, file: displayFile(finding.file) });
|
|
364
|
+
}
|
|
287
365
|
log.info(`${findings.length} prose suggestion(s).`);
|
|
366
|
+
if (argv.failOnWarning && findings.length) process.exitCode = 1;
|
|
288
367
|
} catch (err) {
|
|
289
368
|
reportFailure(err);
|
|
290
369
|
process.exitCode = 1;
|
package/bin/package-build.mjs
CHANGED
|
@@ -540,7 +540,10 @@ function datefromCommand() {
|
|
|
540
540
|
type: "string",
|
|
541
541
|
describe: "Calendar Address or shortcode",
|
|
542
542
|
})
|
|
543
|
-
.positional("date", {
|
|
543
|
+
.positional("date", {
|
|
544
|
+
type: "string",
|
|
545
|
+
describe: "Day written in that calendar's std format (or first named format)",
|
|
546
|
+
}),
|
|
544
547
|
handler: handler((args) => {
|
|
545
548
|
console.log(dateFromCalendar(args.calendar, args.date, calendarConversionContext()));
|
|
546
549
|
}),
|
|
@@ -561,10 +564,19 @@ function datetoCommand() {
|
|
|
561
564
|
.positional("canonical-date", {
|
|
562
565
|
type: "string",
|
|
563
566
|
describe: "Canonical <year>.<day>[:HHMMSS]",
|
|
567
|
+
})
|
|
568
|
+
.option("format", {
|
|
569
|
+
type: "string",
|
|
570
|
+
describe: "Named calendar format; defaults to std or the first declared format",
|
|
564
571
|
}),
|
|
565
572
|
handler: handler((args) => {
|
|
566
573
|
console.log(
|
|
567
|
-
dateToCalendar(
|
|
574
|
+
dateToCalendar(
|
|
575
|
+
args.calendar,
|
|
576
|
+
args.canonicalDate,
|
|
577
|
+
calendarConversionContext(),
|
|
578
|
+
args.format,
|
|
579
|
+
),
|
|
568
580
|
);
|
|
569
581
|
}),
|
|
570
582
|
};
|
package/config.mjs
CHANGED
|
@@ -76,6 +76,7 @@ const SECTION_KEYS = [
|
|
|
76
76
|
"container",
|
|
77
77
|
"e2e",
|
|
78
78
|
"proseLint",
|
|
79
|
+
"proseScore",
|
|
79
80
|
];
|
|
80
81
|
|
|
81
82
|
/**
|
|
@@ -528,7 +529,8 @@ function normalizeExceptions(value, field, where) {
|
|
|
528
529
|
*
|
|
529
530
|
* @typedef {object} PackageBuildConfig
|
|
530
531
|
* @property {string} rootDir The repository root, from package-build.
|
|
531
|
-
* @property {{age: number, threshold: number, minWords: number}} proseLint Optional prose
|
|
532
|
+
* @property {{age: number, threshold: number, minWords: number, rules: string}} proseLint Optional prose lint settings.
|
|
533
|
+
* @property {{minWords: number, bands: Record<string, {min?: number, max?: number}>}} proseScore Optional note score settings.
|
|
532
534
|
* @property {string} packageKind `systems` or `modules`.
|
|
533
535
|
* @property {string} packageId The Foundry package id.
|
|
534
536
|
* @property {string} artifact Derived: `system` or `module`.
|
|
@@ -693,9 +695,20 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
693
695
|
|
|
694
696
|
const proseLint = section.proseLint ?? {};
|
|
695
697
|
if (!isMapping(proseLint)) fail("packageBuild.proseLint", "must be a mapping");
|
|
696
|
-
rejectUnknownKeys(
|
|
697
|
-
|
|
698
|
+
rejectUnknownKeys(
|
|
699
|
+
proseLint,
|
|
700
|
+
["age", "threshold", "minWords", "rules"],
|
|
701
|
+
"packageBuild.proseLint.",
|
|
702
|
+
);
|
|
703
|
+
const proseOptions = { age: 21, threshold: 5, minWords: 8, rules: "readability" };
|
|
698
704
|
for (const [key, value] of Object.entries(proseLint)) {
|
|
705
|
+
if (key === "rules") {
|
|
706
|
+
if (!["readability", "simplify", "all"].includes(value)) {
|
|
707
|
+
fail("packageBuild.proseLint.rules", "must be readability, simplify, or all");
|
|
708
|
+
}
|
|
709
|
+
proseOptions.rules = value;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
699
712
|
const upper = key === "threshold" ? 7 : Infinity;
|
|
700
713
|
if (!Number.isInteger(value) || value < 1 || value > upper) {
|
|
701
714
|
fail(
|
|
@@ -708,6 +721,43 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
708
721
|
proseOptions[key] = value;
|
|
709
722
|
}
|
|
710
723
|
|
|
724
|
+
const proseScore = section.proseScore ?? {};
|
|
725
|
+
if (!isMapping(proseScore)) fail("packageBuild.proseScore", "must be a mapping");
|
|
726
|
+
rejectUnknownKeys(proseScore, ["minWords", "bands"], "packageBuild.proseScore.");
|
|
727
|
+
const scoreMinWords = proseScore.minWords ?? 80;
|
|
728
|
+
if (!Number.isInteger(scoreMinWords) || scoreMinWords < 1) {
|
|
729
|
+
fail("packageBuild.proseScore.minWords", "must be a positive integer");
|
|
730
|
+
}
|
|
731
|
+
const bands = proseScore.bands ?? {};
|
|
732
|
+
if (!isMapping(bands)) fail("packageBuild.proseScore.bands", "must be a mapping");
|
|
733
|
+
const scoreMetrics = [
|
|
734
|
+
"flesch",
|
|
735
|
+
"syllablesPerWord",
|
|
736
|
+
"meanSentenceWords",
|
|
737
|
+
"longestSentenceWords",
|
|
738
|
+
"unfamiliarWordPercent",
|
|
739
|
+
"nominalizationsPer1000Words",
|
|
740
|
+
];
|
|
741
|
+
rejectUnknownKeys(bands, scoreMetrics, "packageBuild.proseScore.bands.");
|
|
742
|
+
const scoreBands = {};
|
|
743
|
+
for (const [metric, band] of Object.entries(bands)) {
|
|
744
|
+
const where = `packageBuild.proseScore.bands.${metric}`;
|
|
745
|
+
if (!isMapping(band)) fail(where, "must be a mapping");
|
|
746
|
+
rejectUnknownKeys(band, ["min", "max"], `${where}.`);
|
|
747
|
+
if (band.min === undefined && band.max === undefined) {
|
|
748
|
+
fail(where, "must specify min, max, or both");
|
|
749
|
+
}
|
|
750
|
+
for (const [limit, value] of Object.entries(band)) {
|
|
751
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
752
|
+
fail(`${where}.${limit}`, "must be a finite number");
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (band.min !== undefined && band.max !== undefined && band.min > band.max) {
|
|
756
|
+
fail(where, "min must not exceed max");
|
|
757
|
+
}
|
|
758
|
+
scoreBands[metric] = Object.freeze({ ...band });
|
|
759
|
+
}
|
|
760
|
+
|
|
711
761
|
if (section.assets !== undefined && !Array.isArray(section.assets)) {
|
|
712
762
|
fail("packageBuild.assets", "must be a list");
|
|
713
763
|
}
|
|
@@ -809,6 +859,7 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
809
859
|
return Object.freeze({
|
|
810
860
|
rootDir: shared.rootDir,
|
|
811
861
|
proseLint: Object.freeze(proseOptions),
|
|
862
|
+
proseScore: Object.freeze({ minWords: scoreMinWords, bands: Object.freeze(scoreBands) }),
|
|
812
863
|
// Where the package is assembled before it is zipped or deployed. Every
|
|
813
864
|
// asset destination is relative to it, so a repository's table says
|
|
814
865
|
// `lang`, not `build/stage/lang` — the latter is what each consumer's
|
package/docs/api.md
CHANGED
|
@@ -623,7 +623,7 @@ The closed registry of system ids, and the `none` that stands for no system at a
|
|
|
623
623
|
|
|
624
624
|
### `engine.calendars`
|
|
625
625
|
|
|
626
|
-
The axis every date normalises onto, the arithmetic over a calendar's month list, and the moon. A
|
|
626
|
+
The axis every date normalises onto, the arithmetic over a calendar's month list, and the moon. A calendar's `data.epoch` anchors its year 1, day 1 on that axis; its eras declare in-calendar start years. The year conversion is piecewise on the sign, because the authored numbering has no year zero while the normalised value does: authored `-1` is `0`, and `-1` and `1` are adjacent. `lunarPhase` uses a floored modulo on either side of the epoch.
|
|
627
627
|
|
|
628
628
|
The neutral day spelling is `<year>.<day>[:HHMMSS]`. The pure conversion functions take the world's year length and an explicit origin year, so their arithmetic names no month or era. A caller supplies those values from its content; no setting year is baked into the toolchain.
|
|
629
629
|
|
|
@@ -647,29 +647,29 @@ The neutral day spelling is `<year>.<day>[:HHMMSS]`. The pure conversion functio
|
|
|
647
647
|
|
|
648
648
|
### `engine.noteDates`
|
|
649
649
|
|
|
650
|
-
A date on a note is one authored
|
|
651
|
-
|
|
652
|
-
| Export | Signature
|
|
653
|
-
| ----------------------- |
|
|
654
|
-
| `UNKNOWN_DATE` | `const UNKNOWN_DATE`
|
|
655
|
-
| `NOTE_DATE_PATTERN` | `const NOTE_DATE_PATTERN`
|
|
656
|
-
| `DATEFROM_PATTERN` | `const DATEFROM_PATTERN`
|
|
657
|
-
| `ERA_QUALIFIER_PATTERN` | `const ERA_QUALIFIER_PATTERN`
|
|
658
|
-
| `parseNoteDate` | `parseNoteDate(value, options)`
|
|
659
|
-
| `eraCovering` | `eraCovering(date, eras, daysPerYear)`
|
|
660
|
-
| `formatNoteDate` | `formatNoteDate(date, era, daysPerYear)` | `{era, year, month, day, text, prose}` or `null` | printing a resolved date in a chosen era or its authored form |
|
|
661
|
-
| `formatDateInCalendar` | `formatDateInCalendar(date, reference, context)` | printable record or `null` | selecting a calendar's era and printing a resolved date in it |
|
|
662
|
-
| `calendarEras` | `calendarEras(reference, context)`
|
|
663
|
-
| `resolvedDateFields` | `resolvedDateFields(fm, context)`
|
|
650
|
+
A date on a note is one authored scalar. `<year>[.<day>[:HHMMSS]]` names a calendar-neutral year, day, or second; `datefrom vrcal 23 Taranis 326 VR` names a calendar, a named month, and an era. Year and day values span their written interval; a clock time identifies a second. Either form may begin with `~` for uncertainty beyond that interval. The `approximate` field records that explicit marker, while `precision` and `seconds` describe the interval or time. A resolved date carries a canonical year, span, and sort key; a neutral year has no `canonicalDay`, while day-precision dates have one. Print `prose ?? text`, order on `sort`, and do arithmetic on `canonicalYear`. The literal `unknown` has no sort key or canonical year. `resolvedDateFields` supplies identical derived records to JSONL and generated page frontmatter.
|
|
651
|
+
|
|
652
|
+
| Export | Signature | Returns | Use it when |
|
|
653
|
+
| ----------------------- | ------------------------------------------------------------- | -------------------------------------------------- | -------------------------------------------------------------- |
|
|
654
|
+
| `UNKNOWN_DATE` | `const UNKNOWN_DATE` | — | naming the literal that says a date is not recorded |
|
|
655
|
+
| `NOTE_DATE_PATTERN` | `const NOTE_DATE_PATTERN` | — | matching the date grammar directly |
|
|
656
|
+
| `DATEFROM_PATTERN` | `const DATEFROM_PATTERN` | — | recognizing a named calendar conversion in frontmatter |
|
|
657
|
+
| `ERA_QUALIFIER_PATTERN` | `const ERA_QUALIFIER_PATTERN` | — | checking that a string is an era qualifier a date could carry |
|
|
658
|
+
| `parseNoteDate` | `parseNoteDate(value, options)` | `{date, findings}` — `date` is `null` when refused | reading a note's authored date, with every finding it earned |
|
|
659
|
+
| `eraCovering` | `eraCovering(date, eras, daysPerYear)` | one era or `null`; throws on overlap | selecting the era covering a resolved date's full precision |
|
|
660
|
+
| `formatNoteDate` | `formatNoteDate(date, era, daysPerYear, formatName?)` | `{era, year, month, day, text, prose}` or `null` | printing a resolved date in a chosen era or its authored form |
|
|
661
|
+
| `formatDateInCalendar` | `formatDateInCalendar(date, reference, context, formatName?)` | printable record or `null` | selecting a calendar's era and printing a resolved date in it |
|
|
662
|
+
| `calendarEras` | `calendarEras(reference, context)` | ordered era records | resolving an addressed calendar or one of its eras |
|
|
663
|
+
| `resolvedDateFields` | `resolvedDateFields(fm, context)` | map of normalized dates | deriving being and lore event date records for index and pages |
|
|
664
664
|
|
|
665
665
|
### `engine.dateConversion`
|
|
666
666
|
|
|
667
|
-
The exact-day conversion functions use the corpus reckoning context.
|
|
667
|
+
The exact-day conversion functions use the corpus reckoning context. `dateFromCalendar` reads the `std` pattern, or the first named pattern. An era-relative year needs its era label. They reject dates outside the selected era; approximate input keeps its `~` prefix.
|
|
668
668
|
|
|
669
|
-
| Export | Signature
|
|
670
|
-
| ------------------ |
|
|
671
|
-
| `dateFromCalendar` | `dateFromCalendar(reference, value, context)`
|
|
672
|
-
| `dateToCalendar` | `dateToCalendar(reference, canonicalDate, context)` | calendar date string | expressing one canonical day in a calendar |
|
|
669
|
+
| Export | Signature | Returns | Use it when |
|
|
670
|
+
| ------------------ | ---------------------------------------------------------------- | --------------------- | --------------------------------------------- |
|
|
671
|
+
| `dateFromCalendar` | `dateFromCalendar(reference, value, context)` | canonical date string | converting one calendar day to the world axis |
|
|
672
|
+
| `dateToCalendar` | `dateToCalendar(reference, canonicalDate, context, formatName?)` | calendar date string | expressing one canonical day in a calendar |
|
|
673
673
|
|
|
674
674
|
### `engine.calendarChoice`
|
|
675
675
|
|
|
@@ -700,7 +700,7 @@ invokes the check during frontmatter lint.
|
|
|
700
700
|
|
|
701
701
|
### `engine.reckoningMarkers`
|
|
702
702
|
|
|
703
|
-
The corpus registry
|
|
703
|
+
The corpus registry anchors each calendar's year 1, day 1 to its canonical `data.epoch` and maps every declared era to that calendar. Exactly one `start: null` era covers earlier years and counts backwards; exactly one `start: 1` era begins the forward count. Other starts are distinct positive calendar years. Numeric order determines bounds, irrespective of array order. Duplicate starts and markers, missing required eras, and invalid epochs are findings. Short qualifiers are ambiguous when they name eras in several packages; a full Address selects one.
|
|
704
704
|
|
|
705
705
|
| Export | Signature | Returns | Use it when |
|
|
706
706
|
| ------------------------- | --------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------- |
|
|
@@ -720,7 +720,7 @@ A calendar is a `lore` note with `subType: calendar`, declaring the months it ke
|
|
|
720
720
|
| `CALENDAR_FORMAT_TOKENS` | `const CALENDAR_FORMAT_TOKENS` | — | checking bare date tokens in a format string |
|
|
721
721
|
| `INVARIANT_FIELDS` | `const INVARIANT_FIELDS` | — | reading the closed list of what a body may declare about itself |
|
|
722
722
|
| `checkCalendarNote` | `checkCalendarNote(note, opts)` | `object[]` — findings | holding a `lore` note to the family and to the year the world keeps |
|
|
723
|
-
| `checkCalendarDateFormats` | `checkCalendarDateFormats(note)` | `object[]` — findings | checking
|
|
723
|
+
| `checkCalendarDateFormats` | `checkCalendarDateFormats(note)` | `object[]` — findings | checking named patterns and the readable default |
|
|
724
724
|
| `checkWorldFacts` | `checkWorldFacts(note, opts)` | `object[]` — findings | holding a `place` note's world facts to one body, one year and one moon |
|
|
725
725
|
| `worldInvariants` | `worldInvariants(index)` | `{year, present, moon, body, moonName, …}` | reading the year, the present and the moon a package's own notes state |
|
|
726
726
|
| `quarterDays` | `quarterDays(daysPerYear)` | `number[]` — four days of the year | asking which days the year turns on |
|
|
@@ -7,27 +7,79 @@ subType: howto
|
|
|
7
7
|
|
|
8
8
|
# Dates, calendars, and eras
|
|
9
9
|
|
|
10
|
-
A canonical date
|
|
10
|
+
A canonical date uses the world's year and day count without choosing a culture's calendar. Write `<year>[.<day>[:HHMMSS]]`: `720` covers year 720, `720.136` covers its day 136, and `720.136:143005` identifies 14:30:05 on that day. The dot separates the year from the **day of year**, not a month. Negative years are valid, and the day must fit the world's declared year length. Prefix a date with `~` when its uncertainty extends outside the stated year, day, or second. `unknown` records an occurrence whose date is not known, in fields that permit it.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## Define a calendar
|
|
13
|
+
|
|
14
|
+
A `lore` note with `subType: calendar` names months, weekdays, seasons, named days, eras, and date formats. Its `data.epoch` is the **canonical day that equals calendar year 1, day 1**. For a calendar beginning on canonical day `1.91`, write `epoch: 1.91`: calendar year 1 still starts at its own day 1. A calendar does not state the length of the world year: its months must sum to `data.year.days` on the world's `place` note.
|
|
13
15
|
|
|
14
16
|
```yaml
|
|
17
|
+
shortcode: commoncal
|
|
18
|
+
name: { full: The Common Calendar }
|
|
19
|
+
type: lore
|
|
20
|
+
subType: calendar
|
|
15
21
|
data:
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
epoch: 1.1
|
|
23
|
+
months:
|
|
24
|
+
- { name: Floralis, abbreviation: Flor, days: 30 }
|
|
25
|
+
- { name: Lusenar, abbreviation: Luse, days: 31 }
|
|
26
|
+
- { name: Murkas, abbreviation: Murk, days: 30 }
|
|
27
|
+
- { name: Taranis, abbreviation: Tara, days: 31 }
|
|
28
|
+
- { name: Vulcar, abbreviation: Vulc, days: 30 }
|
|
29
|
+
- { name: Menaris, abbreviation: Mena, days: 31 }
|
|
30
|
+
- { name: Venuris, abbreviation: Venu, days: 30 }
|
|
31
|
+
- { name: Karnavar, abbreviation: Karn, days: 30 }
|
|
32
|
+
- { name: Morveth, abbreviation: Morv, days: 31 }
|
|
33
|
+
- { name: Thanaris, abbreviation: Than, days: 30 }
|
|
34
|
+
- { name: Aetheris, abbreviation: Aeth, days: 31 }
|
|
35
|
+
- { name: Janar, abbreviation: Jana, days: 30 }
|
|
36
|
+
weekdays:
|
|
37
|
+
- { name: Newday, abbreviation: New }
|
|
38
|
+
- { name: Tillday, abbreviation: Til }
|
|
39
|
+
- { name: Growday, abbreviation: Gro }
|
|
40
|
+
- { name: Harvestday, abbreviation: Hrv }
|
|
41
|
+
- { name: Reapday, abbreviation: Rep }
|
|
42
|
+
- { name: Slowday, abbreviation: Slo }
|
|
43
|
+
- { name: Setday, abbreviation: Set }
|
|
44
|
+
seasons:
|
|
45
|
+
- { name: Spring, abbreviation: Spr, start: 1 }
|
|
46
|
+
- { name: Summer, abbreviation: Sum, start: 92 }
|
|
47
|
+
- { name: Fall, abbreviation: Fal, start: 183 }
|
|
48
|
+
- { name: Winter, abbreviation: Win, start: 274 }
|
|
49
|
+
namedDays:
|
|
50
|
+
- { name: New Years Day, abbreviation: NY, day: 1 }
|
|
51
|
+
formats:
|
|
52
|
+
std: "MM/DD/Y GGG"
|
|
53
|
+
long: "D MMMM Y GGG"
|
|
54
|
+
eras:
|
|
55
|
+
- { shortcode: bvr, name: Before Vylarian Reckoning, abbreviation: BVR, start: null }
|
|
56
|
+
- { shortcode: vr, name: Vylarian Reckoning, abbreviation: VR, start: 1 }
|
|
18
57
|
```
|
|
19
58
|
|
|
20
|
-
`
|
|
59
|
+
The `weekdays` array gives the names in order, starting at index 0. Weeks continue across month and year boundaries. Leave `weekdays` out when the calendar has no week. `seasons[].start` is the one-based day of year on which a season begins. Starts increase in array order. A season continues through the day before the next, and the last wraps through the beginning of the next year when the first season starts after day 1. `namedDays[].day` is also a one-based day of year. Its name appears through `[namedDay]`; `[namedDayAbbr]` prints the abbreviation.
|
|
60
|
+
|
|
61
|
+
Every calendar has exactly one era with `start: null` and one with `start: 1`. The null era covers the years before calendar year 1 and counts them backwards: under `Y GGG`, package-build prints canonical `-50.2` as `50 BVR` when the epoch is `1.1`. This null-start era is an authoring extension and is omitted from the Calendaria definition; Foundry's calendar represents current time. Each other `start` is a distinct, positive **calendar year**, never a day-precise date: `start: 701` begins on day 1 of calendar year 701. `start: 1` begins on the canonical day given by `epoch`, even when that day is `1.91` or another date. Eras end where the next numeric start begins. Array order does not determine chronology. The optional `marker` identifies an era in authored dates and must be unique across the corpus.
|
|
62
|
+
|
|
63
|
+
`formats` contains any number of named Calendaria patterns. `std` is the default for `dateto`, `datefrom`, and `dateformat`; if absent, the first named pattern is the default. The default pattern must contain enough information to read a day back: a complete year (`Y`, `YYYY`, or `[yearInEra]`), a day of year (`DDD`) or month and day, and an era label when using `[yearInEra]`. `Y` means the calendar year in Calendaria; `[yearInEra]` means the year counted from the era's start. In package-build's null-start era, `Y`, `YY`, and `YYYY` print the positive historical year. Other named patterns can use display-only Calendaria tokens. Use `package-build dateto <calendar> <canonical-date> --format <name>` or `{{dateformat "commoncal" data.born "long"}}` to select another output format. The [format token reference](../reference/format-details.md#calendar-format-tokens) lists every Calendaria token and the input constraints.
|
|
64
|
+
|
|
65
|
+
## Write and convert dates
|
|
21
66
|
|
|
22
|
-
A
|
|
67
|
+
A frontmatter date can use the canonical form or `datefrom <calendar> <date in the default format>`. The calendar may be a shortcode or Address. A named date keeps the precision it states: with the sample `std` pattern, `326 VR` covers a year, `04/326 VR` covers Taranis, and `04/23/326 VR` covers its twenty-third day. A date with an era-relative year names its era.
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
data:
|
|
71
|
+
born: datefrom commoncal 04/23/326 VR
|
|
72
|
+
died: ~326.114
|
|
73
|
+
```
|
|
23
74
|
|
|
24
|
-
|
|
75
|
+
The conversion commands take the same forms:
|
|
25
76
|
|
|
26
77
|
```bash
|
|
27
|
-
package-build datefrom
|
|
28
|
-
package-build dateto
|
|
78
|
+
package-build datefrom commoncal '04/23/326 VR'
|
|
79
|
+
package-build dateto commoncal 326.114
|
|
80
|
+
package-build dateto commoncal 326.114 --format long
|
|
29
81
|
```
|
|
30
82
|
|
|
31
|
-
`datefrom`
|
|
83
|
+
The CLI `datefrom` command requires a day and reads the default (`std`, or first) pattern. `dateto` prints the era covering the canonical day. A clock time makes a date exact; a year or day without one spans the stated interval. `data.epoch` and `data.moon.newOn` require a day. The [detailed date rules](../reference/format-details.md#dates-and-calendars) describe precision and bounds.
|
|
32
84
|
|
|
33
|
-
In prose, `{{dateformat "
|
|
85
|
+
In prose, `{{dateformat "commoncal" data.born}}` prints a date in the chosen calendar. `{{dateformat data.calendar data.born}}` uses the note's calendar Address. A third argument chooses another named pattern. A being or place can set `data.calendar`; its infobox uses that calendar for dates.
|
|
@@ -134,4 +134,4 @@ The index contains {{digits (sql "SELECT COUNT(*) FROM entries")}} entries.
|
|
|
134
134
|
{{and (gt 3 5) (lt 4 2)}}
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
Use `{{dateformat data.calendar data.born}}` when the note supplies a calendar Address. SQL fences produce tables; the `sql` helper returns one value for an inline expression. Wrap a numeric result in `words` for running prose or `digits` for a grouped numeral. See [date rules](dates-and-calendars.md) and [SQL details](../reference/format-details.md#content-tables) for query options.
|
|
137
|
+
Use `{{dateformat data.calendar data.born}}` when the note supplies a calendar Address. A third argument selects a named output pattern, as in `{{dateformat data.calendar data.born "long"}}`. SQL fences produce tables; the `sql` helper returns one value for an inline expression. Wrap a numeric result in `words` for running prose or `digits` for a grouped numeral. See [date rules](dates-and-calendars.md) and [SQL details](../reference/format-details.md#content-tables) for query options.
|