@heroiclands/package-build 22.15.1 → 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 +110 -0
- package/README.md +39 -678
- package/bin/{content-build.mjs → content-commands.mjs} +197 -118
- package/bin/package-build.mjs +315 -42
- package/bin/report.mjs +1 -1
- package/ci/ci-docker.mjs +6 -3
- package/ci/ci-steps.mjs +6 -4
- package/ci/content-format-reference.mjs +106 -0
- package/config.mjs +22 -2
- package/content-config.mjs +74 -134
- package/docs/api.md +296 -236
- package/docs/authoring/assets.md +31 -0
- package/docs/authoring/dates-and-calendars.md +85 -0
- package/docs/authoring/first-note.md +48 -0
- package/docs/authoring/frontmatter.md +40 -0
- package/docs/authoring/links-and-markup.md +137 -0
- package/docs/commands.md +633 -162
- package/docs/configuration.md +176 -140
- package/docs/content-format.md +50 -4229
- package/docs/design.md +32 -0
- package/docs/diagnostics.md +29 -22
- package/docs/getting-started.md +21 -877
- package/docs/guides/book.md +153 -0
- package/docs/guides/e2e.md +143 -0
- package/docs/guides/icons.md +126 -0
- package/docs/guides/localization.md +120 -0
- package/docs/guides/module-project.md +915 -0
- package/docs/guides/packs.md +167 -0
- package/docs/guides/reference-pages.md +148 -0
- package/docs/guides/release-and-deploy.md +114 -0
- package/docs/guides/site.md +152 -0
- package/docs/guides/system-project.md +45 -0
- package/docs/index.md +16 -0
- package/docs/project-setup.md +54 -35
- package/docs/reference/format-details.md +4362 -0
- package/docs/reference/note-types.md +532 -0
- package/engine/address-render.mjs +26 -0
- package/engine/address.mjs +3 -20
- package/engine/anchors.mjs +15 -6
- package/engine/asset-bindings.mjs +6 -2
- package/engine/base-compiler.mjs +47 -11
- package/engine/being-measurements.mjs +70 -0
- package/engine/calendar-artifacts.mjs +1 -1
- package/engine/calendar-choice.mjs +32 -0
- package/engine/calendar-format.mjs +441 -0
- package/engine/calendar-human.mjs +133 -0
- package/engine/calendar-notes.mjs +307 -88
- package/engine/calendars.mjs +8 -1
- package/engine/changelog-lint.mjs +2 -2
- package/engine/code-fences.mjs +23 -97
- package/engine/compile-corpus.mjs +6 -7
- package/engine/content-admonitions.mjs +114 -0
- package/engine/content-captions.mjs +181 -0
- package/engine/content-embeds.mjs +2 -2
- package/engine/content-footnotes.mjs +34 -0
- package/engine/content-format-check.mjs +2 -2
- package/engine/content-format.mjs +127 -73
- package/engine/content-format.yaml +746 -0
- package/engine/content-html.mjs +1 -1
- package/engine/content-icons.mjs +45 -92
- package/engine/content-images.mjs +22 -74
- package/engine/content-index.mjs +12 -3
- package/engine/content-links.mjs +21 -26
- package/engine/content-lint.mjs +1 -5
- package/engine/content-secrets.mjs +3 -1
- package/engine/content-tables.mjs +47 -1257
- package/engine/culture-choice.mjs +32 -0
- package/engine/date-conversion.mjs +47 -0
- package/engine/declaration-check.mjs +87 -0
- package/engine/docs-checks.mjs +140 -0
- package/engine/documentation-links.mjs +31 -0
- package/engine/exported-scene.mjs +1 -1
- package/engine/extension-attributes.mjs +74 -0
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +4 -0
- package/engine/foreign-catalog.mjs +5 -5
- package/engine/format-generated.mjs +15 -0
- package/engine/foundry-entries.mjs +3 -1
- package/engine/frontmatter-lint.mjs +5 -1
- package/engine/frontmatter.mjs +10 -0
- package/engine/generate.mjs +2 -2
- package/engine/helpers.mjs +71 -117
- package/engine/homepage.mjs +2 -3
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +18 -3
- package/engine/infobox-registry.mjs +3 -1
- package/engine/infobox.mjs +38 -20
- package/engine/item-compiler.mjs +10 -8
- package/engine/item-docs.mjs +7 -6
- package/engine/journals.mjs +51 -10
- package/engine/macros.mjs +1 -1
- package/engine/map-build.mjs +1 -1
- package/engine/map-dot.mjs +1 -1
- package/engine/markdown-expressions.mjs +118 -0
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-dates.mjs +314 -64
- package/engine/note-package.mjs +5 -25
- package/engine/note-state.mjs +6 -2
- package/engine/note-vocabulary.mjs +74 -19
- package/engine/number-words.mjs +75 -0
- package/engine/office-holders.mjs +188 -0
- package/engine/pack-config.mjs +2 -15
- package/engine/pack-router.mjs +24 -5
- package/engine/packages.mjs +3 -2
- package/engine/pathnames.mjs +21 -24
- package/engine/pdf-build.mjs +217 -55
- package/engine/pdf-fonts.mjs +2 -2
- package/engine/pdf-images.mjs +82 -0
- package/engine/pdf-render.mjs +174 -22
- package/engine/populations.mjs +3 -2
- package/engine/project-init.mjs +499 -0
- package/engine/prose-config.mjs +1 -1
- package/engine/prose-lint.mjs +7 -6
- package/engine/readability-lint.mjs +86 -0
- package/engine/reckoning-markers.mjs +176 -166
- package/engine/scenes.mjs +5 -2
- package/engine/schema-check.mjs +3 -3
- package/engine/site-build.mjs +87 -37
- package/engine/site-config.mjs +8 -8
- package/engine/site-index.mjs +4 -0
- package/engine/site-maps.mjs +1 -1
- package/engine/site-preview.mjs +19 -8
- package/engine/social-ties.mjs +5 -1
- package/engine/sql-tables.mjs +92 -22
- package/engine/web-wikilinks.mjs +15 -31
- package/engine/wikilink-syntax.mjs +4 -1
- package/engine/wikilinks.mjs +12 -14
- package/hm3/item-fields.mjs +1 -1
- package/hm3/items.mjs +12 -45
- package/index.mjs +2 -14
- package/manifest.mjs +31 -4
- package/markdownlint-config.mjs +1 -1
- package/package.json +32 -11
- package/prettier-config.mjs +1 -1
- package/sohl/being-info.mjs +3 -145
- package/sohl/index.mjs +1 -1
- package/sohl/infobox.mjs +2 -1
- package/sohl/item-builders.mjs +3 -27
- package/sohl/item-fields.mjs +61 -22
- package/sohl/kb-passes.mjs +2 -2
- package/sohl/note-schemas.mjs +0 -8
- package/stage.mjs +19 -0
- package/types/config.d.mts +12 -3
- package/types/content-config.d.mts +15 -81
- package/types/engine/address-render.d.mts +13 -0
- package/types/engine/address.d.mts +2 -11
- package/types/engine/anchors.d.mts +3 -4
- package/types/engine/asset-bindings.d.mts +9 -2
- package/types/engine/base-compiler.d.mts +1 -2
- package/types/engine/being-measurements.d.mts +16 -0
- package/types/engine/calendar-choice.d.mts +14 -0
- package/types/engine/calendar-format.d.mts +33 -0
- package/types/engine/calendar-human.d.mts +4 -0
- package/types/engine/calendar-notes.d.mts +5 -3
- package/types/engine/calendars.d.mts +5 -3
- package/types/engine/code-fences.d.mts +6 -72
- package/types/engine/compile-corpus.d.mts +1 -3
- package/types/engine/content-admonitions.d.mts +24 -0
- package/types/engine/content-captions.d.mts +45 -0
- package/types/engine/content-footnotes.d.mts +10 -0
- package/types/engine/content-format-check.d.mts +17 -4
- package/types/engine/content-format.d.mts +24 -11
- package/types/engine/content-icons.d.mts +14 -23
- package/types/engine/content-images.d.mts +2 -2
- package/types/engine/content-index.d.mts +9 -2
- package/types/engine/content-links.d.mts +3 -5
- package/types/engine/content-tables.d.mts +11 -140
- package/types/engine/culture-choice.d.mts +14 -0
- package/types/engine/date-conversion.d.mts +4 -0
- package/types/engine/declaration-check.d.mts +24 -0
- package/types/engine/docs-checks.d.mts +28 -0
- package/types/engine/documentation-links.d.mts +7 -0
- package/types/engine/exported-scene.d.mts +6 -2
- package/types/engine/extension-attributes.d.mts +14 -0
- package/types/engine/field-spec.d.mts +9 -0
- package/types/engine/format-generated.d.mts +7 -0
- package/types/engine/frontmatter.d.mts +14 -4
- package/types/engine/generate.d.mts +1 -1
- package/types/engine/helpers.d.mts +37 -92
- package/types/engine/homepage.d.mts +2 -3
- package/types/engine/index.d.mts +5 -0
- package/types/engine/infobox-registry.d.mts +3 -1
- package/types/engine/infobox.d.mts +17 -6
- package/types/engine/item-compiler.d.mts +4 -4
- package/types/engine/item-docs.d.mts +4 -3
- package/types/engine/journals.d.mts +10 -7
- package/types/engine/map-graphviz.d.mts +1 -1
- package/types/engine/markdown-expressions.d.mts +17 -0
- package/types/engine/note-dates.d.mts +47 -1
- package/types/engine/note-package.d.mts +3 -19
- package/types/engine/note-state.d.mts +2 -1
- package/types/engine/note-vocabulary.d.mts +9 -0
- package/types/engine/number-words.d.mts +4 -0
- package/types/engine/office-holders.d.mts +14 -0
- package/types/engine/pack-config.d.mts +1 -1
- package/types/engine/packages.d.mts +3 -2
- package/types/engine/pdf-images.d.mts +38 -0
- package/types/engine/populations.d.mts +2 -2
- package/types/engine/project-init.d.mts +17 -0
- package/types/engine/prose-config.d.mts +1 -1
- package/types/engine/prose-lint.d.mts +3 -2
- package/types/engine/readability-lint.d.mts +22 -0
- package/types/engine/reckoning-markers.d.mts +13 -26
- package/types/engine/schema-check.d.mts +3 -3
- package/types/engine/site-build.d.mts +38 -28
- package/types/engine/site-config.d.mts +2 -2
- package/types/engine/site-index.d.mts +4 -0
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/social-ties.d.mts +8 -2
- package/types/engine/sql-tables.d.mts +13 -2
- package/types/engine/web-wikilinks.d.mts +2 -1
- package/types/engine/wikilinks.d.mts +7 -1
- package/types/hm3/items.d.mts +5 -6
- package/types/labels.d.mts +40 -0
- package/types/manifest.d.mts +3 -3
- package/types/sohl/actors.d.mts +2 -2
- package/types/sohl/being-info.d.mts +3 -43
- package/types/sohl/index.d.mts +1 -1
- package/types/sohl/infobox.d.mts +5 -2
- package/types/sohl/item-fields.d.mts +4 -13
- package/types/stage.d.mts +9 -0
- package/CONTENT.md +0 -3239
package/engine/note-dates.mjs
CHANGED
|
@@ -14,22 +14,27 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* Parse authored note dates into records used by content checks and renderers.
|
|
16
16
|
*
|
|
17
|
-
* A neutral
|
|
18
|
-
* the world's year. A
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* A neutral date is `<year>[.<day>[:HHMMSS]]`, where `day` is an ordinal
|
|
18
|
+
* within the world's year. A bare year spans that year; a day spans that day;
|
|
19
|
+
* a clock time identifies a second. A named date uses the `datefrom` form;
|
|
20
|
+
* its calendar and era resolve the value on the neutral timeline.
|
|
21
|
+
* Either form accepts `~` for uncertainty beyond its stated interval.
|
|
22
|
+
* `unknown` is unordered.
|
|
23
23
|
*
|
|
24
24
|
* `text` keeps the authored spelling. `canonicalYear` and `sort` are set for
|
|
25
|
-
* neutral and resolved
|
|
26
|
-
* `canonicalDay
|
|
27
|
-
* qualifier carries no resolved year until a caller supplies its era facts.
|
|
25
|
+
* neutral and resolved named dates. A neutral year retains `precision: "year"`
|
|
26
|
+
* and has no `canonicalDay`; day one is used only to calculate its bounds.
|
|
28
27
|
*
|
|
29
28
|
* @module
|
|
30
29
|
*/
|
|
31
30
|
|
|
32
31
|
import { ADDRESS_SEGMENT_PATTERN } from "./address-charset.mjs";
|
|
32
|
+
import { addressedCalendarDate } from "./calendar-human.mjs";
|
|
33
|
+
import {
|
|
34
|
+
calendarFormatHasClock,
|
|
35
|
+
formatCalendarPattern,
|
|
36
|
+
selectCalendarFormat,
|
|
37
|
+
} from "./calendar-format.mjs";
|
|
33
38
|
import {
|
|
34
39
|
calendarStructure,
|
|
35
40
|
canonicalDateFromOffset,
|
|
@@ -38,6 +43,8 @@ import {
|
|
|
38
43
|
dateSortKey,
|
|
39
44
|
dayOfYear,
|
|
40
45
|
daysInMonth,
|
|
46
|
+
eraYear,
|
|
47
|
+
monthDayOfYear,
|
|
41
48
|
parseCanonicalDate,
|
|
42
49
|
} from "./calendars.mjs";
|
|
43
50
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
@@ -78,9 +85,12 @@ export const ERA_QUALIFIER_PATTERN = new RegExp(`^${ERA}$`);
|
|
|
78
85
|
* @type {RegExp}
|
|
79
86
|
*/
|
|
80
87
|
export const NOTE_DATE_PATTERN = new RegExp(
|
|
81
|
-
`^(~)?\\s*(-?\\d+)(?:/(\\d+)(?:/(\\d+))?)?(?:\\s+(${ERA}))?$`,
|
|
88
|
+
`^(~)?\\s*(-?\\d+)(?:/(\\d+)(?:/(\\d+)(?::(\\d{6}))?)?)?(?:\\s+(${ERA}))?$`,
|
|
82
89
|
);
|
|
83
90
|
|
|
91
|
+
/** A calendar conversion written as a frontmatter date value. */
|
|
92
|
+
export const DATEFROM_PATTERN = /^datefrom\s+(\S+)\s+(.+)$/;
|
|
93
|
+
|
|
84
94
|
/** A day written with no month — ungrammatical, and worth its own sentence. */
|
|
85
95
|
const DAY_WITHOUT_MONTH_PATTERN = /^(?:~)?\s*-?\d+\/\/\d+(?:\s+\S+)?$/;
|
|
86
96
|
|
|
@@ -129,8 +139,12 @@ function unknownRecord() {
|
|
|
129
139
|
* the month it names. A calendar that declares no list bounds nothing.
|
|
130
140
|
* @param {Map<string, object>} [options.markers] - Resolved era markers from
|
|
131
141
|
* the corpus, each carrying its calendar and canonical start.
|
|
142
|
+
* @param {Map<string, object>} [options.eras] - Resolved calendar eras, keyed
|
|
143
|
+
* by their addressed qualifier.
|
|
144
|
+
* @param {boolean} [options.ignoreEraBounds=false] - Parse a calendar row's
|
|
145
|
+
* own boundary in a referenced era without applying that era's note span.
|
|
132
146
|
* @param {number} [options.daysPerYear] - The world's year length, required for
|
|
133
|
-
* canonical days and
|
|
147
|
+
* canonical days and named dates.
|
|
134
148
|
* @param {string} [options.field] - The key that carried it, named in every
|
|
135
149
|
* message. Omitted, a message names the value alone.
|
|
136
150
|
* @param {boolean} [options.allowUnknown=true] - Whether `"unknown"` is a
|
|
@@ -147,6 +161,8 @@ export function parseNoteDate(value, options) {
|
|
|
147
161
|
const {
|
|
148
162
|
calendar,
|
|
149
163
|
markers,
|
|
164
|
+
eras,
|
|
165
|
+
ignoreEraBounds = false,
|
|
150
166
|
daysPerYear,
|
|
151
167
|
field,
|
|
152
168
|
allowUnknown = true,
|
|
@@ -178,71 +194,106 @@ export function parseNoteDate(value, options) {
|
|
|
178
194
|
return { date: null, findings };
|
|
179
195
|
};
|
|
180
196
|
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
return refuse(
|
|
184
|
-
`${subject} — \`${UNKNOWN_DATE}\` records that something happened and ` +
|
|
185
|
-
`the date is not known, which only a field whose absence means it ` +
|
|
186
|
-
`never happened has any use for. \`${field ?? "this field"}\` is not ` +
|
|
187
|
-
`one, so leave it out instead`,
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const trimmed = text.trim();
|
|
192
|
-
const marked = /^([A-Z][A-Z0-9]*)\((.*)\)$/.exec(trimmed);
|
|
193
|
-
if (marked) {
|
|
194
|
-
const [, marker, payload] = marked;
|
|
195
|
-
const reckoning = markers?.get(marker);
|
|
196
|
-
if (!reckoning) return refuse(`${subject} names unknown reckoning marker ${marker}`);
|
|
197
|
+
const resolveEra = (parsed, reckoning, label) => {
|
|
198
|
+
if (parsed.year < 1) return refuse(`${subject} needs a positive year in ${label}`);
|
|
197
199
|
if (!Number.isSafeInteger(daysPerYear) || daysPerYear < 1)
|
|
198
|
-
return refuse(`${subject} needs the world's year length to resolve ${
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
allowUnknown: false,
|
|
203
|
-
file,
|
|
204
|
-
raw,
|
|
205
|
-
keyPath,
|
|
206
|
-
});
|
|
207
|
-
if (parsed.findings.some((finding) => finding.severity === "error") || !parsed.date)
|
|
208
|
-
return { date: null, findings: parsed.findings };
|
|
209
|
-
if (!parsed.date.known)
|
|
210
|
-
return refuse(`${subject} wraps an unknown date; write \`unknown\` by itself`);
|
|
211
|
-
if (parsed.date.era !== null)
|
|
200
|
+
return refuse(`${subject} needs the world's year length to resolve ${label}`);
|
|
201
|
+
const { months } = calendarStructure(reckoning.calendar);
|
|
202
|
+
if (!months) return refuse(`${subject} names ${label}, whose calendar declares no months`);
|
|
203
|
+
if (parsed.month !== null && parsed.month > months.length)
|
|
212
204
|
return refuse(
|
|
213
|
-
`${subject}
|
|
205
|
+
`${subject} writes month ${parsed.month}, and ${label} keeps ${months.length} months`,
|
|
214
206
|
);
|
|
215
|
-
|
|
207
|
+
const length = parsed.month === null ? null : daysInMonth(reckoning.calendar, parsed.month);
|
|
208
|
+
if (parsed.day !== null && length !== null && parsed.day > length)
|
|
216
209
|
return refuse(
|
|
217
|
-
`${subject}
|
|
210
|
+
`${subject} writes day ${parsed.day}, and month ${parsed.month} is ${length} days long`,
|
|
218
211
|
);
|
|
219
|
-
const
|
|
220
|
-
if (!months) return refuse(`${subject} names ${marker}, whose calendar declares no months`);
|
|
221
|
-
const ordinal = dayOfYear(months, parsed.date.month ?? 1, parsed.date.day ?? 1);
|
|
212
|
+
const ordinal = dayOfYear(months, parsed.month ?? 1, parsed.day ?? 1);
|
|
222
213
|
const start = canonicalDayOffset(reckoning.epochYear, reckoning.epochDay, 1, daysPerYear);
|
|
223
|
-
const yearOffset =
|
|
214
|
+
const yearOffset = reckoning.beforeEra ? -parsed.year : parsed.year - 1;
|
|
224
215
|
const offset = start + yearOffset * daysPerYear + ordinal - 1;
|
|
225
216
|
if (!Number.isSafeInteger(offset))
|
|
226
217
|
return refuse(`${subject} lies outside the supported canonical day range`);
|
|
227
|
-
|
|
228
|
-
|
|
218
|
+
const spanDays =
|
|
219
|
+
parsed.precision === "year" ? daysPerYear
|
|
220
|
+
: parsed.precision === "month" ? length
|
|
221
|
+
: 1;
|
|
222
|
+
if (
|
|
223
|
+
!ignoreEraBounds &&
|
|
224
|
+
reckoning.endOffset !== undefined &&
|
|
225
|
+
offset + spanDays - 1 > reckoning.endOffset
|
|
226
|
+
)
|
|
227
|
+
return refuse(`${subject} lies after the end of ${label}`);
|
|
229
228
|
const canonical = canonicalDateFromOffset(offset, 1, daysPerYear);
|
|
230
229
|
return {
|
|
231
230
|
date: {
|
|
232
|
-
...parsed
|
|
231
|
+
...parsed,
|
|
233
232
|
text,
|
|
234
233
|
era: reckoning.era,
|
|
235
|
-
|
|
234
|
+
qualifier: reckoning.qualifier,
|
|
236
235
|
canonicalYear: canonical.year,
|
|
237
236
|
canonicalDay: canonical.day,
|
|
238
|
-
|
|
237
|
+
spanDays,
|
|
238
|
+
sort:
|
|
239
|
+
canonical.year +
|
|
240
|
+
(canonical.day - 1 + (parsed.seconds ?? 0) / 86400) / daysPerYear,
|
|
239
241
|
},
|
|
240
|
-
findings
|
|
242
|
+
findings,
|
|
241
243
|
};
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (text.trim() === UNKNOWN_DATE) {
|
|
247
|
+
if (allowUnknown) return { date: unknownRecord(), findings };
|
|
248
|
+
return refuse(
|
|
249
|
+
`${subject} — \`${UNKNOWN_DATE}\` records that something happened and ` +
|
|
250
|
+
`the date is not known, which only a field whose absence means it ` +
|
|
251
|
+
`never happened has any use for. \`${field ?? "this field"}\` is not ` +
|
|
252
|
+
`one, so leave it out instead`,
|
|
253
|
+
);
|
|
242
254
|
}
|
|
243
255
|
|
|
244
|
-
|
|
245
|
-
|
|
256
|
+
const trimmed = text.trim();
|
|
257
|
+
const approximateInput = trimmed.startsWith("~");
|
|
258
|
+
const unmarked = approximateInput ? trimmed.slice(1).trim() : trimmed;
|
|
259
|
+
const conversion = DATEFROM_PATTERN.exec(unmarked);
|
|
260
|
+
if (conversion) {
|
|
261
|
+
try {
|
|
262
|
+
const [, reference, human] = conversion;
|
|
263
|
+
const addressed = addressedCalendarDate(
|
|
264
|
+
human.trim(),
|
|
265
|
+
calendarEras(reference, { eras }),
|
|
266
|
+
);
|
|
267
|
+
if (!addressed) return refuse(`${subject} needs a named calendar date`);
|
|
268
|
+
const parsed = parseNoteDate(`${approximateInput ? "~" : ""}${addressed}`, {
|
|
269
|
+
markers,
|
|
270
|
+
eras,
|
|
271
|
+
daysPerYear,
|
|
272
|
+
ignoreEraBounds,
|
|
273
|
+
allowUnknown: false,
|
|
274
|
+
file,
|
|
275
|
+
raw,
|
|
276
|
+
keyPath,
|
|
277
|
+
});
|
|
278
|
+
if (!parsed.date || parsed.findings.some((item) => item.severity === "error"))
|
|
279
|
+
return parsed;
|
|
280
|
+
return { date: { ...parsed.date, text }, findings: parsed.findings };
|
|
281
|
+
} catch (err) {
|
|
282
|
+
return refuse(`${subject} cannot resolve its calendar date: ${err.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (/^-?0+\.\d+(?::\d{6})?$/.test(unmarked))
|
|
286
|
+
return refuse(
|
|
287
|
+
`${subject} writes year 0, and no reckoning has one — the years either ` +
|
|
288
|
+
`side of an epoch are -1 and 1`,
|
|
289
|
+
);
|
|
290
|
+
if (field && !/^-?\d+(?:\.\d+(?::\d{6})?)?$/.test(unmarked))
|
|
291
|
+
return refuse(
|
|
292
|
+
`${subject} is not a frontmatter date — write ` +
|
|
293
|
+
"`<year>[.<day>[:HHMMSS]]` or `datefrom <calendar> <date>`",
|
|
294
|
+
);
|
|
295
|
+
if (unmarked.includes(".")) {
|
|
296
|
+
const canonical = parseCanonicalDate(unmarked, daysPerYear ?? Number.MAX_SAFE_INTEGER);
|
|
246
297
|
if (canonical) {
|
|
247
298
|
return {
|
|
248
299
|
date: {
|
|
@@ -252,14 +303,17 @@ export function parseNoteDate(value, options) {
|
|
|
252
303
|
year: canonical.year,
|
|
253
304
|
month: null,
|
|
254
305
|
day: canonical.day,
|
|
255
|
-
approximate:
|
|
306
|
+
approximate: approximateInput,
|
|
256
307
|
precision: "day",
|
|
257
|
-
canonicalYear: canonical.year,
|
|
308
|
+
canonicalYear: canonicalYear(canonical.year),
|
|
258
309
|
canonicalDay: canonical.day,
|
|
310
|
+
spanDays: 1,
|
|
259
311
|
...(canonical.seconds === undefined ? {} : { seconds: canonical.seconds }),
|
|
260
312
|
sort:
|
|
261
|
-
|
|
262
|
-
|
|
313
|
+
Number.isSafeInteger(daysPerYear) ?
|
|
314
|
+
canonicalYear(canonical.year) +
|
|
315
|
+
(canonical.day - 1 + (canonical.seconds ?? 0) / 86400) / daysPerYear
|
|
316
|
+
: null,
|
|
263
317
|
},
|
|
264
318
|
findings,
|
|
265
319
|
};
|
|
@@ -298,16 +352,22 @@ export function parseNoteDate(value, options) {
|
|
|
298
352
|
}
|
|
299
353
|
return refuse(
|
|
300
354
|
`${subject} is not a date — write ` +
|
|
301
|
-
`\`<year
|
|
302
|
-
|
|
355
|
+
`\`<year>[.<day>[:HHMMSS]]\` or ` +
|
|
356
|
+
`\`datefrom <calendar> <date>\``,
|
|
303
357
|
);
|
|
304
358
|
}
|
|
305
359
|
|
|
306
|
-
const [, tilde, yearText, monthText, dayText, era] = match;
|
|
360
|
+
const [, tilde, yearText, monthText, dayText, timeText, era] = match;
|
|
307
361
|
const approximate = tilde === "~";
|
|
308
362
|
const year = Number(yearText);
|
|
309
363
|
const month = monthText === undefined ? null : Number(monthText);
|
|
310
364
|
const day = dayText === undefined ? null : Number(dayText);
|
|
365
|
+
const seconds =
|
|
366
|
+
timeText === undefined ? undefined : (
|
|
367
|
+
Number(timeText.slice(0, 2)) * 3600 +
|
|
368
|
+
Number(timeText.slice(2, 4)) * 60 +
|
|
369
|
+
Number(timeText.slice(4, 6))
|
|
370
|
+
);
|
|
311
371
|
if (
|
|
312
372
|
!Number.isSafeInteger(year) ||
|
|
313
373
|
(month !== null && !Number.isSafeInteger(month)) ||
|
|
@@ -325,6 +385,13 @@ export function parseNoteDate(value, options) {
|
|
|
325
385
|
`side of an epoch are -1 and 1`,
|
|
326
386
|
);
|
|
327
387
|
}
|
|
388
|
+
if (
|
|
389
|
+
timeText !== undefined &&
|
|
390
|
+
(Number(timeText.slice(0, 2)) > 23 ||
|
|
391
|
+
Number(timeText.slice(2, 4)) > 59 ||
|
|
392
|
+
Number(timeText.slice(4, 6)) > 59)
|
|
393
|
+
)
|
|
394
|
+
return refuse(`${subject} needs a 24-hour time in HHMMSS form`);
|
|
328
395
|
|
|
329
396
|
const { months } = calendarStructure(calendar);
|
|
330
397
|
if (month !== null && month < 1) {
|
|
@@ -376,15 +443,198 @@ export function parseNoteDate(value, options) {
|
|
|
376
443
|
year,
|
|
377
444
|
month,
|
|
378
445
|
day,
|
|
446
|
+
...(seconds === undefined ? {} : { seconds }),
|
|
379
447
|
approximate,
|
|
380
448
|
precision,
|
|
381
449
|
};
|
|
382
450
|
// A named era's epoch is a fact the corpus states, so the numbers derived
|
|
383
451
|
// from it are written by the pass that resolves it and are absent until
|
|
384
452
|
// then. A bare value is already on the axis.
|
|
453
|
+
if (era !== undefined && eras) {
|
|
454
|
+
const reckoning = eras.get(era);
|
|
455
|
+
if (reckoning?.ambiguous)
|
|
456
|
+
return refuse(
|
|
457
|
+
`${subject} names ambiguous calendar era ${era}; qualify its package and system`,
|
|
458
|
+
);
|
|
459
|
+
if (!reckoning) return refuse(`${subject} names unknown calendar era ${era}`);
|
|
460
|
+
return resolveEra(date, reckoning, `calendar era ${era}`);
|
|
461
|
+
}
|
|
385
462
|
if (era === undefined) {
|
|
386
463
|
date.canonicalYear = canonicalYear(year);
|
|
387
|
-
|
|
464
|
+
if (precision === "year" && Number.isSafeInteger(daysPerYear)) date.spanDays = daysPerYear;
|
|
465
|
+
date.sort =
|
|
466
|
+
dateSortKey(date.canonicalYear, month, day) +
|
|
467
|
+
(seconds ?? 0) / 86400 / (daysPerYear ?? 1);
|
|
388
468
|
}
|
|
389
469
|
return { date, findings };
|
|
390
470
|
}
|
|
471
|
+
|
|
472
|
+
/** Select the single era covering the full precision of a resolved date. */
|
|
473
|
+
export function eraCovering(date, eraRows, daysPerYear) {
|
|
474
|
+
if (
|
|
475
|
+
!date?.known ||
|
|
476
|
+
!Number.isSafeInteger(date.canonicalYear) ||
|
|
477
|
+
!Number.isSafeInteger(daysPerYear) ||
|
|
478
|
+
!Number.isSafeInteger(date.spanDays)
|
|
479
|
+
)
|
|
480
|
+
return null;
|
|
481
|
+
const start = canonicalDayOffset(date.canonicalYear, date.canonicalDay ?? 1, 1, daysPerYear);
|
|
482
|
+
const end = start + date.spanDays - 1;
|
|
483
|
+
const matches = [...new Set(eraRows ?? [])].filter(
|
|
484
|
+
(era) =>
|
|
485
|
+
Number.isSafeInteger(era?.startOffset) &&
|
|
486
|
+
(era.firstEra === true || era.startOffset <= start) &&
|
|
487
|
+
(era.endOffset === undefined || end <= era.endOffset),
|
|
488
|
+
);
|
|
489
|
+
if (matches.length > 1)
|
|
490
|
+
throw new RangeError(
|
|
491
|
+
`date is covered by overlapping eras ${matches.map((era) => era.era).join(", ")}`,
|
|
492
|
+
);
|
|
493
|
+
return matches[0] ?? null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Resolve a calendar or one of its eras from an Address or shortcode. */
|
|
497
|
+
export function calendarEras(reference, context) {
|
|
498
|
+
const name = String(reference ?? "");
|
|
499
|
+
const rows = [...new Set(context?.eras?.values() ?? [])].filter(
|
|
500
|
+
(era) => era && !era.ambiguous && era.era,
|
|
501
|
+
);
|
|
502
|
+
const explicit = context?.eras?.get(name);
|
|
503
|
+
if (explicit?.ambiguous) throw new RangeError(`calendar era ${name} is ambiguous`);
|
|
504
|
+
if (explicit?.era) return [explicit];
|
|
505
|
+
const calendars = new Map();
|
|
506
|
+
for (const era of rows) {
|
|
507
|
+
const full = era.qualifier.slice(0, -(era.era.split(".").at(-1).length + 1));
|
|
508
|
+
const short = era.calendarShortcode;
|
|
509
|
+
if ([full, short, `lore-${short}`, `note-lore-${short}`].includes(name)) {
|
|
510
|
+
const list = calendars.get(full) ?? [];
|
|
511
|
+
list.push(era);
|
|
512
|
+
calendars.set(full, list);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (calendars.size === 0) throw new RangeError(`calendar ${name} does not resolve`);
|
|
516
|
+
if (calendars.size > 1)
|
|
517
|
+
throw new RangeError(`calendar ${name} is ambiguous; use its full Address`);
|
|
518
|
+
return [...calendars.values()][0];
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Print a resolved date using the era active in one addressed calendar. */
|
|
522
|
+
export function formatDateInCalendar(date, reference, context, formatName) {
|
|
523
|
+
const eras = calendarEras(reference, context);
|
|
524
|
+
const chosen = eraCovering(date, eras, context?.daysPerYear);
|
|
525
|
+
const name = String(reference ?? "");
|
|
526
|
+
if (context?.eras?.get(name)?.era && chosen === null)
|
|
527
|
+
throw new RangeError(`date falls outside calendar era ${name}`);
|
|
528
|
+
return formatNoteDate(date, chosen, context?.daysPerYear, formatName);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Print a resolved date in one era, retaining its authored precision. */
|
|
532
|
+
export function formatNoteDate(date, era, daysPerYear, formatName) {
|
|
533
|
+
if (!date?.known || !Number.isSafeInteger(date.canonicalYear)) return null;
|
|
534
|
+
if (!era) return { era: null, year: eraYear(date.canonicalYear), text: date.text, prose: null };
|
|
535
|
+
if (!Number.isSafeInteger(daysPerYear) || !Number.isSafeInteger(date.spanDays)) return null;
|
|
536
|
+
const target = era;
|
|
537
|
+
const start = canonicalDayOffset(date.canonicalYear, date.canonicalDay ?? 1, 1, daysPerYear);
|
|
538
|
+
if (start < target.startOffset && target.firstEra !== true) return null;
|
|
539
|
+
if (target.endOffset !== undefined && start + date.spanDays - 1 > target.endOffset) return null;
|
|
540
|
+
const epoch =
|
|
541
|
+
target.startOffset ??
|
|
542
|
+
canonicalDayOffset(target.epochYear, target.epochDay ?? 1, 1, daysPerYear);
|
|
543
|
+
const delta = start - epoch;
|
|
544
|
+
const cycle = Math.floor(delta / daysPerYear);
|
|
545
|
+
const year = target.beforeEra ? -cycle : cycle + 1;
|
|
546
|
+
if (year < 1) return null;
|
|
547
|
+
if (Math.floor((delta + date.spanDays - 1) / daysPerYear) !== cycle) return null;
|
|
548
|
+
|
|
549
|
+
let month = null;
|
|
550
|
+
let day = null;
|
|
551
|
+
if (date.precision !== "year") {
|
|
552
|
+
const months = calendarStructure(target.calendar).months;
|
|
553
|
+
if (!months) return null;
|
|
554
|
+
const ordinal = (((delta % daysPerYear) + daysPerYear) % daysPerYear) + 1;
|
|
555
|
+
const named = monthDayOfYear(months, ordinal);
|
|
556
|
+
if (!named) return null;
|
|
557
|
+
month = named.month;
|
|
558
|
+
if (date.precision === "month") {
|
|
559
|
+
if (named.day !== 1 || date.spanDays !== daysInMonth(target.calendar, month))
|
|
560
|
+
return null;
|
|
561
|
+
} else day = named.day;
|
|
562
|
+
}
|
|
563
|
+
const clock =
|
|
564
|
+
date.seconds === undefined ?
|
|
565
|
+
""
|
|
566
|
+
: ` ${String(Math.floor(date.seconds / 3600)).padStart(2, "0")}:` +
|
|
567
|
+
`${String(Math.floor(date.seconds / 60) % 60).padStart(2, "0")}:` +
|
|
568
|
+
`${String(date.seconds % 60).padStart(2, "0")}`;
|
|
569
|
+
const monthName =
|
|
570
|
+
month === null ? null : calendarStructure(target.calendar).months?.[month - 1]?.name;
|
|
571
|
+
const eraName =
|
|
572
|
+
target.abbreviation || target.marker || target.name || target.era.split(".").at(-1);
|
|
573
|
+
let text = `${date.approximate ? "~" : ""}${day === null ? "" : `${day} `}${monthName ? `${monthName} ` : ""}${year} ${eraName}${clock}`;
|
|
574
|
+
if (target.calendar?.formats) {
|
|
575
|
+
const { pattern } = selectCalendarFormat(target.calendar, formatName);
|
|
576
|
+
const hasClock = calendarFormatHasClock(pattern);
|
|
577
|
+
const rendered = formatCalendarPattern(
|
|
578
|
+
pattern,
|
|
579
|
+
{
|
|
580
|
+
calendar: target.calendar,
|
|
581
|
+
era: target,
|
|
582
|
+
eraYear: year,
|
|
583
|
+
calendarYear: target.beforeEra ? -year : target.startYear + year - 1,
|
|
584
|
+
month,
|
|
585
|
+
day,
|
|
586
|
+
dayOfYear: (((delta % daysPerYear) + daysPerYear) % daysPerYear) + 1,
|
|
587
|
+
seconds: date.seconds,
|
|
588
|
+
canonicalOffset: start,
|
|
589
|
+
calendarEpochOffset: target.calendarEpochOffset,
|
|
590
|
+
},
|
|
591
|
+
date.precision,
|
|
592
|
+
date.seconds !== undefined,
|
|
593
|
+
);
|
|
594
|
+
text = `${date.approximate ? "~" : ""}${rendered}${date.seconds !== undefined && !hasClock ? clock : ""}`;
|
|
595
|
+
}
|
|
596
|
+
const digits = `${Math.abs(year)}${month === null ? "" : `/${month}`}${day === null ? "" : `/${day}${clock.replaceAll(":", "").replace(/^ /, ":")}`}`;
|
|
597
|
+
const label =
|
|
598
|
+
typeof target.label === "string" ?
|
|
599
|
+
target.label
|
|
600
|
+
: target.label?.[year < 0 ? "before" : "after"];
|
|
601
|
+
const prose =
|
|
602
|
+
target.calendar?.formats ? null
|
|
603
|
+
: typeof label === "string" && label.includes("{date}") ?
|
|
604
|
+
label.replace("{date}", `${date.approximate ? "~" : ""}${digits}`)
|
|
605
|
+
: null;
|
|
606
|
+
return { era, year, month, day, text, prose };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Normalize a note's declared dates for indexes and generated pages. */
|
|
610
|
+
export function resolvedDateFields(fm, context) {
|
|
611
|
+
const fields =
|
|
612
|
+
fm?.type === "being" ?
|
|
613
|
+
[
|
|
614
|
+
["born", fm.data?.born],
|
|
615
|
+
["died", fm.data?.died],
|
|
616
|
+
]
|
|
617
|
+
: fm?.type === "lore" ?
|
|
618
|
+
[
|
|
619
|
+
["when", fm.data?.event?.when],
|
|
620
|
+
["until", fm.data?.event?.until],
|
|
621
|
+
]
|
|
622
|
+
: [];
|
|
623
|
+
const out = {};
|
|
624
|
+
for (const [key, value] of fields) {
|
|
625
|
+
if (value === undefined || value === null) continue;
|
|
626
|
+
const parsed = parseNoteDate(value, {
|
|
627
|
+
...context,
|
|
628
|
+
allowUnknown: key === "born" || key === "died",
|
|
629
|
+
});
|
|
630
|
+
if (parsed.date && !parsed.findings.some((finding) => finding.severity === "error")) {
|
|
631
|
+
const era =
|
|
632
|
+
parsed.date.marker ? context?.markers?.get(parsed.date.marker)
|
|
633
|
+
: parsed.date.qualifier ? context?.eras?.get(parsed.date.qualifier)
|
|
634
|
+
: null;
|
|
635
|
+
const printable = formatNoteDate(parsed.date, era, context?.daysPerYear);
|
|
636
|
+
out[key] = { ...parsed.date, prose: printable?.prose ?? null };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return out;
|
|
640
|
+
}
|
package/engine/note-package.mjs
CHANGED
|
@@ -43,12 +43,8 @@
|
|
|
43
43
|
* carries. There is no value that makes writing it correct, so the diagnostic
|
|
44
44
|
* says what to write instead rather than which value to change.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* frontmatter like any other field. {@link searchableFrontmatter} supplies the
|
|
49
|
-
* derived value to the search so the 45 authored clauses across `sohl` and
|
|
50
|
-
* `thalorna` keep matching. It is a *search* value, never an authored one, and
|
|
51
|
-
* it is never written back to a note.
|
|
46
|
+
* SQL queries can refer to the configured package. Search readers receive that
|
|
47
|
+
* derived value without adding a field to authored frontmatter.
|
|
52
48
|
*
|
|
53
49
|
* @module
|
|
54
50
|
*/
|
|
@@ -57,26 +53,10 @@ import { contentPackage } from "./content-package.mjs";
|
|
|
57
53
|
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
58
54
|
|
|
59
55
|
/**
|
|
60
|
-
* A
|
|
61
|
-
* though no note declares one.
|
|
62
|
-
*
|
|
63
|
-
* A `dataview` query resolves `package` out of frontmatter like any other
|
|
64
|
-
* field, so a collection note that scopes itself with `WHERE … and package =
|
|
65
|
-
* "sohl"` would match nothing now that the field is gone, and would render an
|
|
66
|
-
* **empty table** in silence. Supplying the derived value here is what kept the
|
|
67
|
-
* sweep mechanical rather than a trap — and a query that never mentions
|
|
68
|
-
* `package` is unaffected either way.
|
|
69
|
-
*
|
|
70
|
-
* The frontmatter is copied rather than written into: it is the note's own
|
|
71
|
-
* parsed object, shared with every other reader, and the derived package is a
|
|
72
|
-
* property of *this search*, not of the note.
|
|
73
|
-
*
|
|
56
|
+
* A copy of frontmatter carrying the configured package for query evaluation.
|
|
74
57
|
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
75
|
-
* @param {string} [configured] - The package this build compiles.
|
|
76
|
-
*
|
|
77
|
-
* carry it in a context object, so a caller's configuration drives every read.
|
|
78
|
-
* @returns {object|null|undefined} A shallow copy carrying the derived package,
|
|
79
|
-
* or whatever was passed when it is not frontmatter at all.
|
|
58
|
+
* @param {string} [configured] - The package this build compiles.
|
|
59
|
+
* @returns {object|null|undefined} Frontmatter with a derived package value.
|
|
80
60
|
*/
|
|
81
61
|
export function searchableFrontmatter(fm, configured) {
|
|
82
62
|
if (!fm || typeof fm !== "object") return fm;
|
package/engine/note-state.mjs
CHANGED
|
@@ -225,7 +225,8 @@ export function placeholderBody(body) {
|
|
|
225
225
|
*
|
|
226
226
|
* Fence blocks, headings, thematic breaks and HTML comments are dropped, and a
|
|
227
227
|
* wikilink counts as the one word it renders — `[[affiliation-meivor|Mëivōr]]`
|
|
228
|
-
* is a word, not three.
|
|
228
|
+
* is a word, not three. A dash, slash, or ellipsis between words separates
|
|
229
|
+
* them even without surrounding spaces.
|
|
229
230
|
*
|
|
230
231
|
* @param {string|null|undefined} body - The note body.
|
|
231
232
|
* @returns {number} The word count.
|
|
@@ -238,5 +239,8 @@ export function bodyWordCount(body) {
|
|
|
238
239
|
.split("\n")
|
|
239
240
|
.filter((line) => !STRUCTURE_LINE.test(line.trim()))
|
|
240
241
|
.join(" ");
|
|
241
|
-
return text
|
|
242
|
+
return text
|
|
243
|
+
.replace(/(?<=[\p{L}\p{N}])(?:[—–/…]|\.\.\.)(?=[\p{L}\p{N}])/gu, " ")
|
|
244
|
+
.split(/\s+/)
|
|
245
|
+
.filter((word) => /[\p{L}\p{N}]/u.test(word)).length;
|
|
242
246
|
}
|