@heroiclands/package-build 22.8.0 → 22.9.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 +26 -0
- package/content-config.mjs +0 -78
- package/docs/api.md +45 -9
- package/docs/configuration.md +1 -55
- package/docs/content-format.md +150 -14
- package/engine/base-compiler.mjs +9 -0
- package/engine/being-age.mjs +270 -0
- package/engine/calendar-notes.mjs +805 -0
- package/engine/calendars.mjs +115 -14
- package/engine/content-index.mjs +15 -1
- package/engine/index.mjs +6 -0
- package/engine/infobox.mjs +6 -0
- package/engine/note-dates.mjs +27 -21
- package/engine/note-vocabulary.mjs +57 -6
- package/engine/site-build.mjs +16 -0
- package/package.json +1 -1
- package/types/content-config.d.mts +0 -10
- package/types/engine/base-compiler.d.mts +1 -0
- package/types/engine/being-age.d.mts +92 -0
- package/types/engine/calendar-notes.d.mts +173 -0
- package/types/engine/calendars.d.mts +108 -17
- package/types/engine/index.d.mts +2 -0
- package/types/engine/note-dates.d.mts +6 -6
package/engine/calendars.mjs
CHANGED
|
@@ -52,14 +52,23 @@
|
|
|
52
52
|
* and off by one on the other, which is invisible in any list that does not
|
|
53
53
|
* span it.
|
|
54
54
|
*
|
|
55
|
-
* ## The month structure is the
|
|
55
|
+
* ## The month structure is the calendar's, and an absent list bounds nothing
|
|
56
56
|
*
|
|
57
|
-
* An era moves where year 1 sits; it does not divide the year differently.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
57
|
+
* An era moves where year 1 sits; it does not divide the year differently. The
|
|
58
|
+
* division belongs to the calendar, which is a note declaring an **ordered
|
|
59
|
+
* list** of months, each with a name and a day count. Position in the list is
|
|
60
|
+
* position in the year, so a five-day month sits wherever its people put it and
|
|
61
|
+
* needs no rule of its own.
|
|
62
|
+
*
|
|
63
|
+
* A date is bounded by the calendar it is written in, and a calendar whose list
|
|
64
|
+
* nobody has written down bounds nothing — a toolchain that invented twelve
|
|
65
|
+
* Gregorian months would refuse `667/2/30`, an ordinary date in a year of
|
|
66
|
+
* twelve thirty-day months.
|
|
67
|
+
*
|
|
68
|
+
* ## The lunar phase takes a floored modulo, not JavaScript's
|
|
69
|
+
*
|
|
70
|
+
* `%` takes the dividend's sign, so a day before the epoch yields `-7` where
|
|
71
|
+
* the phase is 23. {@link lunarPhase} is the one place that is written.
|
|
63
72
|
*
|
|
64
73
|
* @module
|
|
65
74
|
*/
|
|
@@ -118,15 +127,107 @@ export function dateSortKey(year, month, day) {
|
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
/**
|
|
121
|
-
*
|
|
130
|
+
* One month of a calendar, as the note declares it.
|
|
122
131
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
132
|
+
* @typedef {object} CalendarMonth
|
|
133
|
+
* @property {string} name - What the month is called.
|
|
134
|
+
* @property {string} [abbreviation] - A short form, where the note gives one.
|
|
135
|
+
* @property {number} days - How many days it holds.
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The months a calendar keeps, or `null` where it declares none.
|
|
140
|
+
*
|
|
141
|
+
* Separate from the check that reads it so a caller can ask what a calendar
|
|
142
|
+
* divides its year into without phrasing a finding about it. An empty list
|
|
143
|
+
* reads as no list: a calendar that declares nothing and one that declares
|
|
144
|
+
* nothing in a pair of brackets divide the year identically.
|
|
125
145
|
*
|
|
126
|
-
* @param {{months?:
|
|
127
|
-
*
|
|
128
|
-
* @returns {{months:
|
|
146
|
+
* @param {{months?: readonly CalendarMonth[]|null}} [calendar] - The calendar
|
|
147
|
+
* the date is written in, as its note declares it.
|
|
148
|
+
* @returns {{months: readonly CalendarMonth[]|null}} What it divides the year
|
|
149
|
+
* into.
|
|
129
150
|
*/
|
|
130
151
|
export function calendarStructure(calendar) {
|
|
131
|
-
|
|
152
|
+
const written = calendar?.months;
|
|
153
|
+
if (!Array.isArray(written) || written.length === 0) return { months: null };
|
|
154
|
+
return { months: written };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* How long one month of a calendar is, or `null` where the calendar is silent.
|
|
159
|
+
*
|
|
160
|
+
* @param {{months?: readonly CalendarMonth[]|null}} [calendar] - The calendar.
|
|
161
|
+
* @param {number} month - The month, numbered from 1.
|
|
162
|
+
* @returns {number|null} Its length in days, or `null`.
|
|
163
|
+
*/
|
|
164
|
+
export function daysInMonth(calendar, month) {
|
|
165
|
+
const { months } = calendarStructure(calendar);
|
|
166
|
+
if (!months || month < 1 || month > months.length) return null;
|
|
167
|
+
const days = months[month - 1]?.days;
|
|
168
|
+
return typeof days === "number" ? days : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* How many days the months add up to.
|
|
173
|
+
*
|
|
174
|
+
* The number a calendar's list claims the year is. Whether that is the year the
|
|
175
|
+
* world keeps is a question about the world, and is asked elsewhere.
|
|
176
|
+
*
|
|
177
|
+
* @param {readonly CalendarMonth[]} months - The ordered month list.
|
|
178
|
+
* @returns {number} The sum of their day counts.
|
|
179
|
+
*/
|
|
180
|
+
export function daysInYear(months) {
|
|
181
|
+
return months.reduce((sum, month) => sum + (Number(month?.days) || 0), 0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The day of the year each month opens on, in order, numbered from 1.
|
|
186
|
+
*
|
|
187
|
+
* The sum read as a sequence rather than as a total. Two lists holding the same
|
|
188
|
+
* numbers in a different order sum alike and open their months on different
|
|
189
|
+
* days, which is the error a total cannot see.
|
|
190
|
+
*
|
|
191
|
+
* @param {readonly CalendarMonth[]} months - The ordered month list.
|
|
192
|
+
* @returns {number[]} One entry per month: the day of the year it begins on.
|
|
193
|
+
*/
|
|
194
|
+
export function monthStarts(months) {
|
|
195
|
+
const starts = [];
|
|
196
|
+
let day = 1;
|
|
197
|
+
for (const month of months) {
|
|
198
|
+
starts.push(day);
|
|
199
|
+
day += Number(month?.days) || 0;
|
|
200
|
+
}
|
|
201
|
+
return starts;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Which day of the year a written month and day land on, numbered from 1.
|
|
206
|
+
*
|
|
207
|
+
* @param {readonly CalendarMonth[]} months - The ordered month list.
|
|
208
|
+
* @param {number} month - The month, numbered from 1.
|
|
209
|
+
* @param {number} day - The day within it, numbered from 1.
|
|
210
|
+
* @returns {number} The day of the year.
|
|
211
|
+
*/
|
|
212
|
+
export function dayOfYear(months, month, day) {
|
|
213
|
+
return monthStarts(months)[month - 1] + day - 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* How far into its cycle a moon is on a given day.
|
|
218
|
+
*
|
|
219
|
+
* **Floored, not truncated.** JavaScript's `%` takes the dividend's sign, so a
|
|
220
|
+
* day seven before the epoch yields `-7` where the phase is 23 — a value that
|
|
221
|
+
* is right on one side of the epoch and wrong on the other, which is invisible
|
|
222
|
+
* in any list that does not span it.
|
|
223
|
+
*
|
|
224
|
+
* @param {number} dayNumber - The day, on whatever continuous day count the
|
|
225
|
+
* caller keeps.
|
|
226
|
+
* @param {number} epochDay - The day of that same count on which the moon was
|
|
227
|
+
* new.
|
|
228
|
+
* @param {number} cycle - The cycle's length in days.
|
|
229
|
+
* @returns {number} Days into the cycle, `0` to `cycle - 1`.
|
|
230
|
+
*/
|
|
231
|
+
export function lunarPhase(dayNumber, epochDay, cycle) {
|
|
232
|
+
return (((dayNumber - epochDay) % cycle) + cycle) % cycle;
|
|
132
233
|
}
|
package/engine/content-index.mjs
CHANGED
|
@@ -137,6 +137,10 @@ import {
|
|
|
137
137
|
sortKeysDeep,
|
|
138
138
|
} from "./index-records.mjs";
|
|
139
139
|
import { isDraftNote } from "./note-vocabulary.mjs";
|
|
140
|
+
// A being's computed `age` — the middle state between an authored override and
|
|
141
|
+
// an unknown one — filled in against the package's own declared present before
|
|
142
|
+
// a record is built, so the index carries it without the note being written to.
|
|
143
|
+
import { applyComputedBeingAge, presentAmongFrontmatters } from "./being-age.mjs";
|
|
140
144
|
|
|
141
145
|
export {
|
|
142
146
|
authoredFrontmatter,
|
|
@@ -606,10 +610,20 @@ export function collectContentIndex(
|
|
|
606
610
|
// tree to walk and the index is its asset records. The caller decides
|
|
607
611
|
// whether an absent tree is a mistake; by the time the walk is reached it
|
|
608
612
|
// is simply a package with nothing to compile.
|
|
609
|
-
|
|
613
|
+
// Materialised rather than consumed lazily: the present is read from this
|
|
614
|
+
// same walk before the loop below reads it a second time, and
|
|
615
|
+
// `walkMarkdownTree` is a generator — a single-use one, exhausted the
|
|
616
|
+
// moment anything else iterates it first.
|
|
617
|
+
const notes = fs.existsSync(contentBase) ? [...walkMarkdownTree(contentBase, walkOpts)] : [];
|
|
618
|
+
|
|
619
|
+
// The package's declared present, read once from whichever `place` note in
|
|
620
|
+
// this same walk states one — the whole tree is already in memory, so no
|
|
621
|
+
// second read is needed to answer a question about all of it.
|
|
622
|
+
const present = presentAmongFrontmatters(notes.map((n) => n.frontmatter));
|
|
610
623
|
|
|
611
624
|
for (const { frontmatter, body, bodyLine, absPath } of notes) {
|
|
612
625
|
const fm = frontmatter ?? {};
|
|
626
|
+
applyComputedBeingAge(fm, present);
|
|
613
627
|
// The id the note's document is filed under, resolved before
|
|
614
628
|
// the record is built so the index publishes the address *and* the id
|
|
615
629
|
// that address derives.
|
package/engine/index.mjs
CHANGED
|
@@ -130,6 +130,12 @@ export * as calendars from "./calendars.mjs";
|
|
|
130
130
|
/** A note's dates: one authored string, one grammar, one record shape. */
|
|
131
131
|
export * as noteDates from "./note-dates.mjs";
|
|
132
132
|
|
|
133
|
+
/** Calendars as notes: what one may declare, and the definition emitted from it. */
|
|
134
|
+
export * as calendarNotes from "./calendar-notes.mjs";
|
|
135
|
+
|
|
136
|
+
/** A being's `age`, absent beside a dated `born`: the age the date computes to. */
|
|
137
|
+
export * as beingAge from "./being-age.mjs";
|
|
138
|
+
|
|
133
139
|
/** The one normalisation this build makes: prose to a URL-safe token. */
|
|
134
140
|
export * as contentSlug from "./content-slug.mjs";
|
|
135
141
|
|
package/engine/infobox.mjs
CHANGED
|
@@ -291,6 +291,12 @@ export const NOTE_FIELD_PRESENTATION = Object.freeze({
|
|
|
291
291
|
// than as six rows of numbers, exactly as the book prototype sets them.
|
|
292
292
|
// Qualified by type, because `weight` is also what a coin weighs.
|
|
293
293
|
"being.age": Object.freeze({ group: "appearance", phrase: (v) => `Age ${v}` }),
|
|
294
|
+
// The `~` estimate's numeric companion — beside `age` in the index for a
|
|
295
|
+
// machine to sort or filter on, not a second row for a reader who already
|
|
296
|
+
// meets it inside the appearance clause.
|
|
297
|
+
"being.ageYears": Object.freeze({
|
|
298
|
+
withheld: "the `age` row's own machinery, not a second fact about the subject",
|
|
299
|
+
}),
|
|
294
300
|
"being.height": Object.freeze({ group: "appearance", phrase: heightPhrase }),
|
|
295
301
|
"being.weight": Object.freeze({ group: "appearance", phrase: weightPhrase }),
|
|
296
302
|
"being.frame": Object.freeze({ group: "appearance", phrase: (v) => `${v} frame` }),
|
package/engine/note-dates.mjs
CHANGED
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
* `-1` sits immediately before `1`.
|
|
28
28
|
* - `YYYY` — required.
|
|
29
29
|
* - `MM`, `DD` — optional, in that order.
|
|
30
|
-
* - `ERA` —
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* - `ERA` — a calendar's address plus `.<era shortcode>`, in the four address
|
|
31
|
+
* forms {@link ERA_QUALIFIER_PATTERN} admits. Omitted, the date sits on the
|
|
32
|
+
* canonical axis and is attributed to nobody.
|
|
33
33
|
*
|
|
34
34
|
* **Precision falls out of how much is written.** `-984` is a year, `689/6` a
|
|
35
35
|
* month, `689/6/19` a day. There is no precision vocabulary, so there is
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
*/
|
|
99
99
|
|
|
100
100
|
import { ADDRESS_SEGMENT_PATTERN } from "./address-charset.mjs";
|
|
101
|
-
import { calendarStructure, canonicalYear, dateSortKey } from "./calendars.mjs";
|
|
101
|
+
import { calendarStructure, canonicalYear, dateSortKey, daysInMonth } from "./calendars.mjs";
|
|
102
102
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
103
103
|
|
|
104
104
|
/**
|
|
@@ -120,7 +120,7 @@ const ERA = `(?:${SEGMENT}-){0,3}${SEGMENT}\\.${SEGMENT}`;
|
|
|
120
120
|
/**
|
|
121
121
|
* What a date's trailing token may look like.
|
|
122
122
|
*
|
|
123
|
-
*
|
|
123
|
+
* A calendar note's address followed by `.<era shortcode>`. The `{0,3}`
|
|
124
124
|
* repetition is the four address forms exactly — bare `shortcode`,
|
|
125
125
|
* `type-shortcode`, `system-type-shortcode` and
|
|
126
126
|
* `package-system-type-shortcode` — so the hyphen separates an address's
|
|
@@ -182,9 +182,10 @@ function unknownRecord() {
|
|
|
182
182
|
* @param {unknown} value - The authored value. A number is what YAML hands
|
|
183
183
|
* back for a bare year, so it is accepted and stringified.
|
|
184
184
|
* @param {object} [options]
|
|
185
|
-
* @param {{months?:
|
|
186
|
-
*
|
|
187
|
-
* written month and day
|
|
185
|
+
* @param {{months?: readonly object[]|null}} [options.calendar] - The calendar
|
|
186
|
+
* the value is written in, as its note declares it. Its ordered month list
|
|
187
|
+
* bounds a written month and a written day, the day against the length of
|
|
188
|
+
* the month it names. A calendar that declares no list bounds nothing.
|
|
188
189
|
* @param {string} [options.field] - The key that carried it, named in every
|
|
189
190
|
* message. Omitted, a message names the value alone.
|
|
190
191
|
* @param {boolean} [options.allowUnknown=true] - Whether `"unknown"` is a
|
|
@@ -246,7 +247,7 @@ export function parseNoteDate(value, options) {
|
|
|
246
247
|
if (MALFORMED_ERA_PATTERN.test(trimmed)) {
|
|
247
248
|
return refuse(
|
|
248
249
|
`${subject} — an era is written ` +
|
|
249
|
-
`\`<
|
|
250
|
+
`\`<calendar shortcode>.<era shortcode>\`, lowercase letters ` +
|
|
250
251
|
`and digits only`,
|
|
251
252
|
);
|
|
252
253
|
}
|
|
@@ -256,14 +257,14 @@ export function parseNoteDate(value, options) {
|
|
|
256
257
|
// the rule rather than guessing a concrete replacement.
|
|
257
258
|
return refuse(
|
|
258
259
|
`${subject} names no era — a reckoning is written ` +
|
|
259
|
-
`\`<
|
|
260
|
+
`\`<calendar shortcode>.<era shortcode>\`: a year before ` +
|
|
260
261
|
`an era's epoch is written negative, and a year after it ` +
|
|
261
262
|
`simply drops the token`,
|
|
262
263
|
);
|
|
263
264
|
}
|
|
264
265
|
return refuse(
|
|
265
266
|
`${subject} is not a date — write ` +
|
|
266
|
-
`\`[~][-]YYYY[/MM[/DD]] [<
|
|
267
|
+
`\`[~][-]YYYY[/MM[/DD]] [<calendar shortcode>.<era shortcode>]\`: ` +
|
|
267
268
|
`a year, negative where it falls before the epoch it is counted ` +
|
|
268
269
|
`from, optionally a month and a day, and an era where the date is a ` +
|
|
269
270
|
`reckoning somebody kept`,
|
|
@@ -285,26 +286,31 @@ export function parseNoteDate(value, options) {
|
|
|
285
286
|
);
|
|
286
287
|
}
|
|
287
288
|
|
|
288
|
-
const { months
|
|
289
|
+
const { months } = calendarStructure(calendar);
|
|
289
290
|
if (month !== null && month < 1) {
|
|
290
291
|
return refuse(`${subject} writes month ${month}, and a month is numbered from 1`);
|
|
291
292
|
}
|
|
292
|
-
// An upper bound is checked only where the
|
|
293
|
-
// Gregorian months assumed here would refuse `667/2/30`, which is a
|
|
294
|
-
// date in a year of twelve thirty-day months.
|
|
295
|
-
if (month !== null && months !== null && month > months) {
|
|
293
|
+
// An upper bound is checked only where the calendar states its months.
|
|
294
|
+
// Twelve Gregorian months assumed here would refuse `667/2/30`, which is a
|
|
295
|
+
// correct date in a year of twelve thirty-day months.
|
|
296
|
+
if (month !== null && months !== null && month > months.length) {
|
|
296
297
|
return refuse(
|
|
297
|
-
`${subject} writes month ${month}, and
|
|
298
|
-
`
|
|
298
|
+
`${subject} writes month ${month}, and the calendar it is written in ` +
|
|
299
|
+
`keeps ${months.length} months`,
|
|
299
300
|
);
|
|
300
301
|
}
|
|
301
302
|
if (day !== null && day < 1) {
|
|
302
303
|
return refuse(`${subject} writes day ${day}, and a day is numbered from 1`);
|
|
303
304
|
}
|
|
304
|
-
|
|
305
|
+
// Per month, because the months differ: a five-day month sits in the list
|
|
306
|
+
// wherever its people put it, and a bound taken from the longest month
|
|
307
|
+
// would accept a date that calendar has no day for.
|
|
308
|
+
const length = month !== null ? daysInMonth(calendar, month) : null;
|
|
309
|
+
if (day !== null && length !== null && day > length) {
|
|
310
|
+
const named = months?.[month - 1]?.name;
|
|
305
311
|
return refuse(
|
|
306
|
-
`${subject} writes day ${day}, and
|
|
307
|
-
`
|
|
312
|
+
`${subject} writes day ${day}, and ${named ? `${named}` : `month ${month}`} ` +
|
|
313
|
+
`is ${length} days long`,
|
|
308
314
|
);
|
|
309
315
|
}
|
|
310
316
|
|
|
@@ -85,6 +85,16 @@ import { currentType } from "./ids.mjs";
|
|
|
85
85
|
// What a place is next to and reachable from — the two relation lists and
|
|
86
86
|
// the checks that hold them to their closed sets and to each other.
|
|
87
87
|
import { checkBorders, checkRoutes } from "./place-relations.mjs";
|
|
88
|
+
import {
|
|
89
|
+
CALENDAR_FIELDS,
|
|
90
|
+
INVARIANT_FIELDS,
|
|
91
|
+
checkCalendarNote,
|
|
92
|
+
checkWorldFacts,
|
|
93
|
+
} from "./calendar-notes.mjs";
|
|
94
|
+
// Whether a being's authored `age` disagrees with what `born` and the
|
|
95
|
+
// package's declared present compute — the closed `data:` container's own
|
|
96
|
+
// question, asked the way `checkPlace` and `checkCalendarNote` are.
|
|
97
|
+
import { checkBeingAge } from "./being-age.mjs";
|
|
88
98
|
import { checkHeld } from "./holdings.mjs";
|
|
89
99
|
import { checkCitedPopulations, checkPopulation } from "./populations.mjs";
|
|
90
100
|
// What trade a settlement supports — the scale and the check that holds a
|
|
@@ -306,6 +316,22 @@ const CHARGES = Object.freeze([
|
|
|
306
316
|
{ name: "charges.max", ...NUM, describe: "Most charges it can hold; unset means no maximum." },
|
|
307
317
|
]);
|
|
308
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Everything `place` asks of a whole note.
|
|
321
|
+
*
|
|
322
|
+
* Two questions about one subject, and they are independent: who holds this
|
|
323
|
+
* land, and — where the note is a body rather than somewhere within one — what
|
|
324
|
+
* the body states about itself. A type declares one whole-note check, so the
|
|
325
|
+
* two are composed here rather than either being folded into the other.
|
|
326
|
+
*
|
|
327
|
+
* @param {object} note - The note.
|
|
328
|
+
* @param {object} [opts] - The lint's options, passed through unchanged.
|
|
329
|
+
* @returns {object[]} Findings from both.
|
|
330
|
+
*/
|
|
331
|
+
function checkPlace(note, opts) {
|
|
332
|
+
return [...checkHeld(note, opts), ...checkWorldFacts(note, opts)];
|
|
333
|
+
}
|
|
334
|
+
|
|
309
335
|
/* --------------------------------------------------------------------- */
|
|
310
336
|
/* The vocabulary */
|
|
311
337
|
/* --------------------------------------------------------------------- */
|
|
@@ -565,6 +591,9 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
565
591
|
// lands with. Declared open until it does, because inventing the
|
|
566
592
|
// values here would put a second, weaker answer beside the real one.
|
|
567
593
|
subTypes: null,
|
|
594
|
+
// Whether an authored `age` disagrees with what `born` and the
|
|
595
|
+
// package's declared present compute — see `engine/being-age.mjs`.
|
|
596
|
+
check: checkBeingAge,
|
|
568
597
|
data: Object.freeze([
|
|
569
598
|
TOKEN_ICON,
|
|
570
599
|
TEMPLATE_PRIORITY,
|
|
@@ -612,6 +641,13 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
612
641
|
"for an estimate. Unstated beside a dated `born` it is computed; unstated " +
|
|
613
642
|
"beside an unknown or absent `born` the age is unknown.",
|
|
614
643
|
},
|
|
644
|
+
{
|
|
645
|
+
name: "ageYears",
|
|
646
|
+
...NUM,
|
|
647
|
+
describe:
|
|
648
|
+
"Written by the compiler beside an `age` estimate — the `~` stripped, " +
|
|
649
|
+
"the magnitude alone. A note never authors this.",
|
|
650
|
+
},
|
|
615
651
|
{ name: "height", ...NUM, describe: "Height in metres." },
|
|
616
652
|
{ name: "weight", ...NUM, describe: "Weight in kilograms." },
|
|
617
653
|
{
|
|
@@ -1044,16 +1080,27 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1044
1080
|
"bestiary",
|
|
1045
1081
|
"gathering",
|
|
1046
1082
|
]),
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
// the
|
|
1050
|
-
|
|
1083
|
+
// A lore note is prose, and what it *is* about is its subType — with
|
|
1084
|
+
// one exception. A calendar is a division of the year, and a division
|
|
1085
|
+
// is data; the check scopes the family to the subType that means it,
|
|
1086
|
+
// because `DataFieldSpec` declares the keys a type accepts and not the
|
|
1087
|
+
// subType that may write them.
|
|
1088
|
+
check: checkCalendarNote,
|
|
1089
|
+
data: CALENDAR_FIELDS,
|
|
1051
1090
|
}),
|
|
1052
1091
|
|
|
1053
1092
|
place: Object.freeze({
|
|
1054
1093
|
stubbable: true,
|
|
1055
|
-
subTypes: Object.freeze([
|
|
1056
|
-
|
|
1094
|
+
subTypes: Object.freeze([
|
|
1095
|
+
"world",
|
|
1096
|
+
"region",
|
|
1097
|
+
"settlement",
|
|
1098
|
+
"site",
|
|
1099
|
+
"structure",
|
|
1100
|
+
"feature",
|
|
1101
|
+
"celestial",
|
|
1102
|
+
]),
|
|
1103
|
+
check: checkPlace,
|
|
1057
1104
|
data: Object.freeze([
|
|
1058
1105
|
{
|
|
1059
1106
|
name: "demonym",
|
|
@@ -1104,6 +1151,10 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
1104
1151
|
"Journeys from this place's centre — where the destination lies, how " +
|
|
1105
1152
|
"it is travelled, and about how many days it takes.",
|
|
1106
1153
|
},
|
|
1154
|
+
// What a body states about itself: its size, its year, its moon.
|
|
1155
|
+
// Legal on a `world` or a `celestial` and nowhere else, which the
|
|
1156
|
+
// type's own check is what enforces.
|
|
1157
|
+
...INVARIANT_FIELDS,
|
|
1107
1158
|
]),
|
|
1108
1159
|
}),
|
|
1109
1160
|
|
package/engine/site-build.mjs
CHANGED
|
@@ -72,6 +72,9 @@ import { routerFor } from "./pack-router.mjs";
|
|
|
72
72
|
import { searchableFrontmatter } from "./note-package.mjs";
|
|
73
73
|
// The corpus, from the one pass that derives it.
|
|
74
74
|
import { indexRecordsFor } from "./content-index.mjs";
|
|
75
|
+
// A being's computed `age` — see `engine/being-age.mjs`. `pdf-build.mjs`
|
|
76
|
+
// reuses `collectContentPages`'s pages, so wiring it here reaches the book too.
|
|
77
|
+
import { applyComputedBeingAge, presentAmongRecords } from "./being-age.mjs";
|
|
75
78
|
import { isNoteRecord, noteFile } from "./index-records.mjs";
|
|
76
79
|
// The one statement of what an empty body means, shared with the index.
|
|
77
80
|
import { isStubNote } from "./note-state.mjs";
|
|
@@ -166,9 +169,22 @@ export function collectContentPages(contentBase, ctx) {
|
|
|
166
169
|
const addressFindings = [];
|
|
167
170
|
const fmLinkFindings = [];
|
|
168
171
|
|
|
172
|
+
// The package's declared present, read once from the same corpus the site
|
|
173
|
+
// and the PDF share — see `engine/being-age.mjs`. `pdf-build.mjs` reaches
|
|
174
|
+
// this through the pages this function returns, so one call covers both.
|
|
175
|
+
const present = presentAmongRecords(
|
|
176
|
+
ctx.records ??
|
|
177
|
+
indexRecordsFor({
|
|
178
|
+
contentBase,
|
|
179
|
+
config: ctx.config,
|
|
180
|
+
skipDirectories: ctx.skipDirectories,
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
|
|
169
184
|
for (const file of siteCorpusFiles(contentBase, ctx)) {
|
|
170
185
|
const note = readNote(file);
|
|
171
186
|
if (!note) continue;
|
|
187
|
+
applyComputedBeingAge(note.fm, present);
|
|
172
188
|
const { fm, body } = note;
|
|
173
189
|
// The configuration's, never a note's: `package:` is retired, so every
|
|
174
190
|
// note in the tree belongs to the package this repository compiles.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.9.1",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -844,16 +844,6 @@ export type ContentBuildConfig = {
|
|
|
844
844
|
* each pack followed by its companions.
|
|
845
845
|
*/
|
|
846
846
|
packDirectories: readonly string[];
|
|
847
|
-
/**
|
|
848
|
-
* How this package's year is divided, and
|
|
849
|
-
* the date it calls the present. Every
|
|
850
|
-
* value `null` when none is declared.
|
|
851
|
-
*/
|
|
852
|
-
calendar: Readonly<{
|
|
853
|
-
months: number | null;
|
|
854
|
-
monthDays: number | null;
|
|
855
|
-
present: string | null;
|
|
856
|
-
}>;
|
|
857
847
|
/**
|
|
858
848
|
* Passed through
|
|
859
849
|
* frozen, uninterpreted. `{}` when absent.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse an authored `age` into its estimate mark and its magnitude.
|
|
3
|
+
*
|
|
4
|
+
* @param {unknown} raw - The authored value — `34`, `"34"` or `"~34"`.
|
|
5
|
+
* @returns {{approximate: boolean, value: number}|null} The parts, or `null`
|
|
6
|
+
* for a value that does not read as a plain count of years.
|
|
7
|
+
*/
|
|
8
|
+
export function parseAgeMagnitude(raw: unknown): {
|
|
9
|
+
approximate: boolean;
|
|
10
|
+
value: number;
|
|
11
|
+
} | null;
|
|
12
|
+
/**
|
|
13
|
+
* The age two dates imply, in whole years, computed from the dates themselves
|
|
14
|
+
* rather than from the years alone.
|
|
15
|
+
*
|
|
16
|
+
* @param {unknown} bornRaw - The being's authored `born`, or `undefined`/`null`
|
|
17
|
+
* where it wrote none.
|
|
18
|
+
* @param {unknown} presentRaw - The package's `data.present`, as
|
|
19
|
+
* {@link module:engine/calendar-notes.worldInvariants} read it, or
|
|
20
|
+
* `undefined`/`null` where the package states none.
|
|
21
|
+
* @returns {number|null} The age, or `null` where `born` is `unknown` or
|
|
22
|
+
* absent, where no present is declared, or where either date fails to parse.
|
|
23
|
+
*/
|
|
24
|
+
export function computeAge(bornRaw: unknown, presentRaw: unknown): number | null;
|
|
25
|
+
/**
|
|
26
|
+
* Fill in a being's computed `age`, and the numeric companion an estimate
|
|
27
|
+
* carries beside it — mutating the frontmatter object handed in, never a file
|
|
28
|
+
* on disk.
|
|
29
|
+
*
|
|
30
|
+
* Every caller holds a frontmatter object that was parsed fresh from a note's
|
|
31
|
+
* text for its own pass — the content index, a compiled document, a rendered
|
|
32
|
+
* page — and none of those objects round-trips back to the `.md` file. So
|
|
33
|
+
* patching `data.age` in place here is exactly as safe as reading it, and
|
|
34
|
+
* reaches every surface that reads `data:` after this runs, with the note
|
|
35
|
+
* itself untouched.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} fm - A note's frontmatter, as freshly parsed.
|
|
38
|
+
* @param {unknown} presentRaw - The package's declared present, or
|
|
39
|
+
* `undefined`/`null` where it states none.
|
|
40
|
+
* @returns {void}
|
|
41
|
+
*/
|
|
42
|
+
export function applyComputedBeingAge(fm: object, presentRaw: unknown): void;
|
|
43
|
+
/**
|
|
44
|
+
* A being's `data.present`, read from every `place` note a batch of records or
|
|
45
|
+
* frontmatter objects carries.
|
|
46
|
+
*
|
|
47
|
+
* Two shapes reach this by way of two thin adapters, because the callers that
|
|
48
|
+
* need a present hold their notes two different ways: a flat index record
|
|
49
|
+
* (`type` and `data` at the top level) and a raw walk's `{frontmatter}` pair.
|
|
50
|
+
* Neither shape is the `{fm, type}` pair {@link worldInvariants} reads, so each
|
|
51
|
+
* adapter states the one conversion its caller needs rather than asking every
|
|
52
|
+
* caller to know it.
|
|
53
|
+
*
|
|
54
|
+
* @param {readonly object[]|undefined} records - Flat index records — a
|
|
55
|
+
* content-index record, or anything else spreading a note's frontmatter to
|
|
56
|
+
* its own top level.
|
|
57
|
+
* @returns {unknown} The present, or `null` where no `place` note in the batch
|
|
58
|
+
* declares one.
|
|
59
|
+
*/
|
|
60
|
+
export function presentAmongRecords(records: readonly object[] | undefined): unknown;
|
|
61
|
+
/**
|
|
62
|
+
* The present read from a batch of raw frontmatter objects.
|
|
63
|
+
*
|
|
64
|
+
* @param {readonly (object|null|undefined)[]|undefined} frontmatters - One
|
|
65
|
+
* entry per note walked, as parsed — never a record.
|
|
66
|
+
* @returns {unknown} The present, or `null` where none is declared.
|
|
67
|
+
*/
|
|
68
|
+
export function presentAmongFrontmatters(frontmatters: readonly (object | null | undefined)[] | undefined): unknown;
|
|
69
|
+
/**
|
|
70
|
+
* Check a being's authored `age` against what `born` and the package's present
|
|
71
|
+
* compute.
|
|
72
|
+
*
|
|
73
|
+
* Registered as `being`'s whole-note `check` in `note-vocabulary.mjs`, run the
|
|
74
|
+
* way `checkPlace` and `checkCalendarNote` are — beside every other check the
|
|
75
|
+
* closed `data:` container earns, with the same `index`.
|
|
76
|
+
*
|
|
77
|
+
* Two silences, both deliberate: beside `born: unknown` or an absent `born`
|
|
78
|
+
* there is nothing to disagree with, and a package that declares no present
|
|
79
|
+
* computes no age to disagree with either — both cases are read by
|
|
80
|
+
* {@link computeAge} returning `null`, and neither says a word.
|
|
81
|
+
*
|
|
82
|
+
* @param {object} note - The note, as the link index hands it over —
|
|
83
|
+
* {@link module:engine/calendar-notes.worldInvariants}'s `index.notes` shape.
|
|
84
|
+
* @param {object} [opts]
|
|
85
|
+
* @param {object} [opts.index] - The link index, holding the package's `place`
|
|
86
|
+
* notes. Its absence skips the check, the way a reference check with no
|
|
87
|
+
* index skips reporting every reference as dead.
|
|
88
|
+
* @returns {object[]} Findings — at most one `warning`, naming both values.
|
|
89
|
+
*/
|
|
90
|
+
export function checkBeingAge(note: object, { index }?: {
|
|
91
|
+
index?: object | undefined;
|
|
92
|
+
}): object[];
|