@heroiclands/package-build 22.15.1 → 22.16.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 +95 -0
- package/README.md +39 -678
- package/bin/{content-build.mjs → content-commands.mjs} +197 -118
- package/bin/package-build.mjs +303 -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 +294 -234
- package/docs/authoring/assets.md +31 -0
- package/docs/authoring/dates-and-calendars.md +33 -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 +631 -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 +4420 -0
- package/docs/reference/note-types.md +531 -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-human.mjs +71 -0
- package/engine/calendar-notes.mjs +41 -3
- 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 +745 -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 +117 -0
- package/engine/metadata-index.mjs +7 -3
- package/engine/note-addresses.mjs +1 -0
- package/engine/note-dates.mjs +283 -61
- 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 +205 -93
- 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-human.d.mts +4 -0
- package/types/engine/calendar-notes.d.mts +2 -0
- 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 +4 -2
- 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
|
@@ -15,21 +15,19 @@
|
|
|
15
15
|
* Parse authored note dates into records used by content checks and renderers.
|
|
16
16
|
*
|
|
17
17
|
* A neutral day is `<year>.<day>[:HHMMSS]`, where `day` is an ordinal within
|
|
18
|
-
* the world's year. A
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* precision, and `unknown` is unordered. The parser also accepts unmarked
|
|
22
|
-
* slash dates and addressed era qualifiers.
|
|
18
|
+
* the world's year. A named date is `datefrom <calendar> <date>`; its calendar
|
|
19
|
+
* and era resolve the value on the neutral timeline. Either form accepts a
|
|
20
|
+
* leading `~` for approximation. `unknown` is unordered.
|
|
23
21
|
*
|
|
24
22
|
* `text` keeps the authored spelling. `canonicalYear` and `sort` are set for
|
|
25
|
-
* neutral and resolved
|
|
26
|
-
*
|
|
27
|
-
* qualifier carries no resolved year until a caller supplies its era facts.
|
|
23
|
+
* neutral and resolved named dates. Resolved dates also carry `canonicalDay`,
|
|
24
|
+
* the ordinal day within the neutral year.
|
|
28
25
|
*
|
|
29
26
|
* @module
|
|
30
27
|
*/
|
|
31
28
|
|
|
32
29
|
import { ADDRESS_SEGMENT_PATTERN } from "./address-charset.mjs";
|
|
30
|
+
import { addressedCalendarDate } from "./calendar-human.mjs";
|
|
33
31
|
import {
|
|
34
32
|
calendarStructure,
|
|
35
33
|
canonicalDateFromOffset,
|
|
@@ -38,6 +36,8 @@ import {
|
|
|
38
36
|
dateSortKey,
|
|
39
37
|
dayOfYear,
|
|
40
38
|
daysInMonth,
|
|
39
|
+
eraYear,
|
|
40
|
+
monthDayOfYear,
|
|
41
41
|
parseCanonicalDate,
|
|
42
42
|
} from "./calendars.mjs";
|
|
43
43
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
@@ -78,9 +78,12 @@ export const ERA_QUALIFIER_PATTERN = new RegExp(`^${ERA}$`);
|
|
|
78
78
|
* @type {RegExp}
|
|
79
79
|
*/
|
|
80
80
|
export const NOTE_DATE_PATTERN = new RegExp(
|
|
81
|
-
`^(~)?\\s*(-?\\d+)(?:/(\\d+)(?:/(\\d+))?)?(?:\\s+(${ERA}))?$`,
|
|
81
|
+
`^(~)?\\s*(-?\\d+)(?:/(\\d+)(?:/(\\d+)(?::(\\d{6}))?)?)?(?:\\s+(${ERA}))?$`,
|
|
82
82
|
);
|
|
83
83
|
|
|
84
|
+
/** A calendar conversion written as a frontmatter date value. */
|
|
85
|
+
export const DATEFROM_PATTERN = /^datefrom\s+(\S+)\s+(.+)$/;
|
|
86
|
+
|
|
84
87
|
/** A day written with no month — ungrammatical, and worth its own sentence. */
|
|
85
88
|
const DAY_WITHOUT_MONTH_PATTERN = /^(?:~)?\s*-?\d+\/\/\d+(?:\s+\S+)?$/;
|
|
86
89
|
|
|
@@ -129,8 +132,12 @@ function unknownRecord() {
|
|
|
129
132
|
* the month it names. A calendar that declares no list bounds nothing.
|
|
130
133
|
* @param {Map<string, object>} [options.markers] - Resolved era markers from
|
|
131
134
|
* the corpus, each carrying its calendar and canonical start.
|
|
135
|
+
* @param {Map<string, object>} [options.eras] - Resolved calendar eras, keyed
|
|
136
|
+
* by their addressed qualifier.
|
|
137
|
+
* @param {boolean} [options.ignoreEraBounds=false] - Parse a calendar row's
|
|
138
|
+
* own boundary in a referenced era without applying that era's note span.
|
|
132
139
|
* @param {number} [options.daysPerYear] - The world's year length, required for
|
|
133
|
-
* canonical days and
|
|
140
|
+
* canonical days and named dates.
|
|
134
141
|
* @param {string} [options.field] - The key that carried it, named in every
|
|
135
142
|
* message. Omitted, a message names the value alone.
|
|
136
143
|
* @param {boolean} [options.allowUnknown=true] - Whether `"unknown"` is a
|
|
@@ -147,6 +154,8 @@ export function parseNoteDate(value, options) {
|
|
|
147
154
|
const {
|
|
148
155
|
calendar,
|
|
149
156
|
markers,
|
|
157
|
+
eras,
|
|
158
|
+
ignoreEraBounds = false,
|
|
150
159
|
daysPerYear,
|
|
151
160
|
field,
|
|
152
161
|
allowUnknown = true,
|
|
@@ -178,71 +187,109 @@ export function parseNoteDate(value, options) {
|
|
|
178
187
|
return { date: null, findings };
|
|
179
188
|
};
|
|
180
189
|
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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}`);
|
|
190
|
+
const resolveEra = (parsed, reckoning, label) => {
|
|
191
|
+
if (!ignoreEraBounds && parsed.year < 0 && reckoning.firstEra === false)
|
|
192
|
+
return refuse(
|
|
193
|
+
`${subject} counts backward from ${label}, but only the first era of a calendar may do so`,
|
|
194
|
+
);
|
|
197
195
|
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)
|
|
196
|
+
return refuse(`${subject} needs the world's year length to resolve ${label}`);
|
|
197
|
+
const { months } = calendarStructure(reckoning.calendar);
|
|
198
|
+
if (!months) return refuse(`${subject} names ${label}, whose calendar declares no months`);
|
|
199
|
+
if (parsed.month !== null && parsed.month > months.length)
|
|
212
200
|
return refuse(
|
|
213
|
-
`${subject}
|
|
201
|
+
`${subject} writes month ${parsed.month}, and ${label} keeps ${months.length} months`,
|
|
214
202
|
);
|
|
215
|
-
|
|
203
|
+
const length = parsed.month === null ? null : daysInMonth(reckoning.calendar, parsed.month);
|
|
204
|
+
if (parsed.day !== null && length !== null && parsed.day > length)
|
|
216
205
|
return refuse(
|
|
217
|
-
`${subject}
|
|
206
|
+
`${subject} writes day ${parsed.day}, and month ${parsed.month} is ${length} days long`,
|
|
218
207
|
);
|
|
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);
|
|
208
|
+
const ordinal = dayOfYear(months, parsed.month ?? 1, parsed.day ?? 1);
|
|
222
209
|
const start = canonicalDayOffset(reckoning.epochYear, reckoning.epochDay, 1, daysPerYear);
|
|
223
|
-
const yearOffset = parsed.
|
|
210
|
+
const yearOffset = parsed.year > 0 ? parsed.year - 1 : parsed.year;
|
|
224
211
|
const offset = start + yearOffset * daysPerYear + ordinal - 1;
|
|
225
212
|
if (!Number.isSafeInteger(offset))
|
|
226
213
|
return refuse(`${subject} lies outside the supported canonical day range`);
|
|
227
|
-
|
|
228
|
-
|
|
214
|
+
const spanDays =
|
|
215
|
+
parsed.precision === "year" ? daysPerYear
|
|
216
|
+
: parsed.precision === "month" ? length
|
|
217
|
+
: 1;
|
|
218
|
+
if (
|
|
219
|
+
!ignoreEraBounds &&
|
|
220
|
+
reckoning.endOffset !== undefined &&
|
|
221
|
+
offset + spanDays - 1 > reckoning.endOffset
|
|
222
|
+
)
|
|
223
|
+
return refuse(`${subject} lies after the end of ${label}`);
|
|
229
224
|
const canonical = canonicalDateFromOffset(offset, 1, daysPerYear);
|
|
230
225
|
return {
|
|
231
226
|
date: {
|
|
232
|
-
...parsed
|
|
227
|
+
...parsed,
|
|
233
228
|
text,
|
|
234
229
|
era: reckoning.era,
|
|
235
|
-
|
|
230
|
+
qualifier: reckoning.qualifier,
|
|
236
231
|
canonicalYear: canonical.year,
|
|
237
232
|
canonicalDay: canonical.day,
|
|
238
|
-
|
|
233
|
+
spanDays,
|
|
234
|
+
sort:
|
|
235
|
+
canonical.year +
|
|
236
|
+
(canonical.day - 1 + (parsed.seconds ?? 0) / 86400) / daysPerYear,
|
|
239
237
|
},
|
|
240
|
-
findings
|
|
238
|
+
findings,
|
|
241
239
|
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (text.trim() === UNKNOWN_DATE) {
|
|
243
|
+
if (allowUnknown) return { date: unknownRecord(), findings };
|
|
244
|
+
return refuse(
|
|
245
|
+
`${subject} — \`${UNKNOWN_DATE}\` records that something happened and ` +
|
|
246
|
+
`the date is not known, which only a field whose absence means it ` +
|
|
247
|
+
`never happened has any use for. \`${field ?? "this field"}\` is not ` +
|
|
248
|
+
`one, so leave it out instead`,
|
|
249
|
+
);
|
|
242
250
|
}
|
|
243
251
|
|
|
244
|
-
|
|
245
|
-
|
|
252
|
+
const trimmed = text.trim();
|
|
253
|
+
const approximateInput = trimmed.startsWith("~");
|
|
254
|
+
const unmarked = approximateInput ? trimmed.slice(1).trim() : trimmed;
|
|
255
|
+
const conversion = DATEFROM_PATTERN.exec(unmarked);
|
|
256
|
+
if (conversion) {
|
|
257
|
+
try {
|
|
258
|
+
const [, reference, human] = conversion;
|
|
259
|
+
const addressed = addressedCalendarDate(
|
|
260
|
+
human.trim(),
|
|
261
|
+
calendarEras(reference, { eras }),
|
|
262
|
+
);
|
|
263
|
+
if (!addressed) return refuse(`${subject} needs a named calendar date`);
|
|
264
|
+
const parsed = parseNoteDate(`${approximateInput ? "~" : ""}${addressed}`, {
|
|
265
|
+
markers,
|
|
266
|
+
eras,
|
|
267
|
+
daysPerYear,
|
|
268
|
+
ignoreEraBounds,
|
|
269
|
+
allowUnknown: false,
|
|
270
|
+
file,
|
|
271
|
+
raw,
|
|
272
|
+
keyPath,
|
|
273
|
+
});
|
|
274
|
+
if (!parsed.date || parsed.findings.some((item) => item.severity === "error"))
|
|
275
|
+
return parsed;
|
|
276
|
+
return { date: { ...parsed.date, text }, findings: parsed.findings };
|
|
277
|
+
} catch (err) {
|
|
278
|
+
return refuse(`${subject} cannot resolve its calendar date: ${err.message}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (/^-?0+\.\d+(?::\d{6})?$/.test(unmarked))
|
|
282
|
+
return refuse(
|
|
283
|
+
`${subject} writes year 0, and no reckoning has one — the years either ` +
|
|
284
|
+
`side of an epoch are -1 and 1`,
|
|
285
|
+
);
|
|
286
|
+
if (field && !/^-?\d+\.\d+(?::\d{6})?$/.test(unmarked))
|
|
287
|
+
return refuse(
|
|
288
|
+
`${subject} is not a frontmatter date — write ` +
|
|
289
|
+
"`<year>.<day>[:HHMMSS]` or `datefrom <calendar> <date>`",
|
|
290
|
+
);
|
|
291
|
+
if (unmarked.includes(".")) {
|
|
292
|
+
const canonical = parseCanonicalDate(unmarked, daysPerYear ?? Number.MAX_SAFE_INTEGER);
|
|
246
293
|
if (canonical) {
|
|
247
294
|
return {
|
|
248
295
|
date: {
|
|
@@ -252,14 +299,17 @@ export function parseNoteDate(value, options) {
|
|
|
252
299
|
year: canonical.year,
|
|
253
300
|
month: null,
|
|
254
301
|
day: canonical.day,
|
|
255
|
-
approximate:
|
|
302
|
+
approximate: approximateInput,
|
|
256
303
|
precision: "day",
|
|
257
304
|
canonicalYear: canonical.year,
|
|
258
305
|
canonicalDay: canonical.day,
|
|
306
|
+
spanDays: 1,
|
|
259
307
|
...(canonical.seconds === undefined ? {} : { seconds: canonical.seconds }),
|
|
260
308
|
sort:
|
|
261
|
-
|
|
262
|
-
|
|
309
|
+
Number.isSafeInteger(daysPerYear) ?
|
|
310
|
+
canonical.year +
|
|
311
|
+
(canonical.day - 1 + (canonical.seconds ?? 0) / 86400) / daysPerYear
|
|
312
|
+
: null,
|
|
263
313
|
},
|
|
264
314
|
findings,
|
|
265
315
|
};
|
|
@@ -299,15 +349,21 @@ export function parseNoteDate(value, options) {
|
|
|
299
349
|
return refuse(
|
|
300
350
|
`${subject} is not a date — write ` +
|
|
301
351
|
`\`<year>.<day>[:HHMMSS]\` or ` +
|
|
302
|
-
|
|
352
|
+
`\`datefrom <calendar> <date>\``,
|
|
303
353
|
);
|
|
304
354
|
}
|
|
305
355
|
|
|
306
|
-
const [, tilde, yearText, monthText, dayText, era] = match;
|
|
356
|
+
const [, tilde, yearText, monthText, dayText, timeText, era] = match;
|
|
307
357
|
const approximate = tilde === "~";
|
|
308
358
|
const year = Number(yearText);
|
|
309
359
|
const month = monthText === undefined ? null : Number(monthText);
|
|
310
360
|
const day = dayText === undefined ? null : Number(dayText);
|
|
361
|
+
const seconds =
|
|
362
|
+
timeText === undefined ? undefined : (
|
|
363
|
+
Number(timeText.slice(0, 2)) * 3600 +
|
|
364
|
+
Number(timeText.slice(2, 4)) * 60 +
|
|
365
|
+
Number(timeText.slice(4, 6))
|
|
366
|
+
);
|
|
311
367
|
if (
|
|
312
368
|
!Number.isSafeInteger(year) ||
|
|
313
369
|
(month !== null && !Number.isSafeInteger(month)) ||
|
|
@@ -325,6 +381,13 @@ export function parseNoteDate(value, options) {
|
|
|
325
381
|
`side of an epoch are -1 and 1`,
|
|
326
382
|
);
|
|
327
383
|
}
|
|
384
|
+
if (
|
|
385
|
+
timeText !== undefined &&
|
|
386
|
+
(Number(timeText.slice(0, 2)) > 23 ||
|
|
387
|
+
Number(timeText.slice(2, 4)) > 59 ||
|
|
388
|
+
Number(timeText.slice(4, 6)) > 59)
|
|
389
|
+
)
|
|
390
|
+
return refuse(`${subject} needs a 24-hour time in HHMMSS form`);
|
|
328
391
|
|
|
329
392
|
const { months } = calendarStructure(calendar);
|
|
330
393
|
if (month !== null && month < 1) {
|
|
@@ -376,15 +439,174 @@ export function parseNoteDate(value, options) {
|
|
|
376
439
|
year,
|
|
377
440
|
month,
|
|
378
441
|
day,
|
|
442
|
+
...(seconds === undefined ? {} : { seconds }),
|
|
379
443
|
approximate,
|
|
380
444
|
precision,
|
|
381
445
|
};
|
|
382
446
|
// A named era's epoch is a fact the corpus states, so the numbers derived
|
|
383
447
|
// from it are written by the pass that resolves it and are absent until
|
|
384
448
|
// then. A bare value is already on the axis.
|
|
449
|
+
if (era !== undefined && eras) {
|
|
450
|
+
const reckoning = eras.get(era);
|
|
451
|
+
if (reckoning?.ambiguous)
|
|
452
|
+
return refuse(
|
|
453
|
+
`${subject} names ambiguous calendar era ${era}; qualify its package and system`,
|
|
454
|
+
);
|
|
455
|
+
if (!reckoning) return refuse(`${subject} names unknown calendar era ${era}`);
|
|
456
|
+
return resolveEra(date, reckoning, `calendar era ${era}`);
|
|
457
|
+
}
|
|
385
458
|
if (era === undefined) {
|
|
386
459
|
date.canonicalYear = canonicalYear(year);
|
|
387
|
-
|
|
460
|
+
if (precision === "year" && Number.isSafeInteger(daysPerYear)) date.spanDays = daysPerYear;
|
|
461
|
+
date.sort =
|
|
462
|
+
dateSortKey(date.canonicalYear, month, day) +
|
|
463
|
+
(seconds ?? 0) / 86400 / (daysPerYear ?? 1);
|
|
388
464
|
}
|
|
389
465
|
return { date, findings };
|
|
390
466
|
}
|
|
467
|
+
|
|
468
|
+
/** Select the single era covering the full precision of a resolved date. */
|
|
469
|
+
export function eraCovering(date, eraRows, daysPerYear) {
|
|
470
|
+
if (
|
|
471
|
+
!date?.known ||
|
|
472
|
+
!Number.isSafeInteger(date.canonicalYear) ||
|
|
473
|
+
!Number.isSafeInteger(daysPerYear) ||
|
|
474
|
+
!Number.isSafeInteger(date.spanDays)
|
|
475
|
+
)
|
|
476
|
+
return null;
|
|
477
|
+
const start = canonicalDayOffset(date.canonicalYear, date.canonicalDay ?? 1, 1, daysPerYear);
|
|
478
|
+
const end = start + date.spanDays - 1;
|
|
479
|
+
const matches = [...new Set(eraRows ?? [])].filter(
|
|
480
|
+
(era) =>
|
|
481
|
+
Number.isSafeInteger(era?.startOffset) &&
|
|
482
|
+
(era.firstEra === true || era.startOffset <= start) &&
|
|
483
|
+
(era.endOffset === undefined || end <= era.endOffset),
|
|
484
|
+
);
|
|
485
|
+
if (matches.length > 1)
|
|
486
|
+
throw new RangeError(
|
|
487
|
+
`date is covered by overlapping eras ${matches.map((era) => era.era).join(", ")}`,
|
|
488
|
+
);
|
|
489
|
+
return matches[0] ?? null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Resolve a calendar or one of its eras from an Address or shortcode. */
|
|
493
|
+
export function calendarEras(reference, context) {
|
|
494
|
+
const name = String(reference ?? "");
|
|
495
|
+
const rows = [...new Set(context?.eras?.values() ?? [])].filter(
|
|
496
|
+
(era) => era && !era.ambiguous && era.era,
|
|
497
|
+
);
|
|
498
|
+
const explicit = context?.eras?.get(name);
|
|
499
|
+
if (explicit?.ambiguous) throw new RangeError(`calendar era ${name} is ambiguous`);
|
|
500
|
+
if (explicit?.era) return [explicit];
|
|
501
|
+
const calendars = new Map();
|
|
502
|
+
for (const era of rows) {
|
|
503
|
+
const full = era.qualifier.slice(0, -(era.era.split(".").at(-1).length + 1));
|
|
504
|
+
const short = era.calendarShortcode;
|
|
505
|
+
if ([full, short, `lore-${short}`, `note-lore-${short}`].includes(name)) {
|
|
506
|
+
const list = calendars.get(full) ?? [];
|
|
507
|
+
list.push(era);
|
|
508
|
+
calendars.set(full, list);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (calendars.size === 0) throw new RangeError(`calendar ${name} does not resolve`);
|
|
512
|
+
if (calendars.size > 1)
|
|
513
|
+
throw new RangeError(`calendar ${name} is ambiguous; use its full Address`);
|
|
514
|
+
return [...calendars.values()][0];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Print a resolved date using the era active in one addressed calendar. */
|
|
518
|
+
export function formatDateInCalendar(date, reference, context) {
|
|
519
|
+
const eras = calendarEras(reference, context);
|
|
520
|
+
const chosen = eraCovering(date, eras, context?.daysPerYear);
|
|
521
|
+
const name = String(reference ?? "");
|
|
522
|
+
if (context?.eras?.get(name)?.era && chosen === null)
|
|
523
|
+
throw new RangeError(`date falls outside calendar era ${name}`);
|
|
524
|
+
return formatNoteDate(date, chosen, context?.daysPerYear);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Print a resolved date in one era, retaining its authored precision. */
|
|
528
|
+
export function formatNoteDate(date, era, daysPerYear) {
|
|
529
|
+
if (!date?.known || !Number.isSafeInteger(date.canonicalYear)) return null;
|
|
530
|
+
if (!era) return { era: null, year: eraYear(date.canonicalYear), text: date.text, prose: null };
|
|
531
|
+
if (!Number.isSafeInteger(daysPerYear) || !Number.isSafeInteger(date.spanDays)) return null;
|
|
532
|
+
const target = era;
|
|
533
|
+
const start = canonicalDayOffset(date.canonicalYear, date.canonicalDay ?? 1, 1, daysPerYear);
|
|
534
|
+
if (start < target.startOffset && target.firstEra !== true) return null;
|
|
535
|
+
if (target.endOffset !== undefined && start + date.spanDays - 1 > target.endOffset) return null;
|
|
536
|
+
const epoch =
|
|
537
|
+
target.startOffset ??
|
|
538
|
+
canonicalDayOffset(target.epochYear, target.epochDay ?? 1, 1, daysPerYear);
|
|
539
|
+
const delta = start - epoch;
|
|
540
|
+
const cycle = Math.floor(delta / daysPerYear);
|
|
541
|
+
const year = cycle >= 0 ? cycle + 1 : cycle;
|
|
542
|
+
if (Math.floor((delta + date.spanDays - 1) / daysPerYear) !== cycle) return null;
|
|
543
|
+
|
|
544
|
+
let month = null;
|
|
545
|
+
let day = null;
|
|
546
|
+
if (date.precision !== "year") {
|
|
547
|
+
const months = calendarStructure(target.calendar).months;
|
|
548
|
+
if (!months) return null;
|
|
549
|
+
const ordinal = (((delta % daysPerYear) + daysPerYear) % daysPerYear) + 1;
|
|
550
|
+
const named = monthDayOfYear(months, ordinal);
|
|
551
|
+
if (!named) return null;
|
|
552
|
+
month = named.month;
|
|
553
|
+
if (date.precision === "month") {
|
|
554
|
+
if (named.day !== 1 || date.spanDays !== daysInMonth(target.calendar, month))
|
|
555
|
+
return null;
|
|
556
|
+
} else day = named.day;
|
|
557
|
+
}
|
|
558
|
+
const clock =
|
|
559
|
+
date.seconds === undefined ?
|
|
560
|
+
""
|
|
561
|
+
: ` ${String(Math.floor(date.seconds / 3600)).padStart(2, "0")}:` +
|
|
562
|
+
`${String(Math.floor(date.seconds / 60) % 60).padStart(2, "0")}:` +
|
|
563
|
+
`${String(date.seconds % 60).padStart(2, "0")}`;
|
|
564
|
+
const monthName =
|
|
565
|
+
month === null ? null : calendarStructure(target.calendar).months?.[month - 1]?.name;
|
|
566
|
+
const eraName =
|
|
567
|
+
target.abbreviation || target.marker || target.name || target.era.split(".").at(-1);
|
|
568
|
+
const text = `${date.approximate ? "~" : ""}${day === null ? "" : `${day} `}${monthName ? `${monthName} ` : ""}${year} ${eraName}${clock}`;
|
|
569
|
+
const digits = `${Math.abs(year)}${month === null ? "" : `/${month}`}${day === null ? "" : `/${day}${clock.replaceAll(":", "").replace(/^ /, ":")}`}`;
|
|
570
|
+
const label =
|
|
571
|
+
typeof target.label === "string" ?
|
|
572
|
+
target.label
|
|
573
|
+
: target.label?.[year < 0 ? "before" : "after"];
|
|
574
|
+
const prose =
|
|
575
|
+
typeof label === "string" && label.includes("{date}") ?
|
|
576
|
+
label.replace("{date}", `${date.approximate ? "~" : ""}${digits}`)
|
|
577
|
+
: null;
|
|
578
|
+
return { era, year, month, day, text, prose };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** Normalize a note's declared dates for indexes and generated pages. */
|
|
582
|
+
export function resolvedDateFields(fm, context) {
|
|
583
|
+
const fields =
|
|
584
|
+
fm?.type === "being" ?
|
|
585
|
+
[
|
|
586
|
+
["born", fm.data?.born],
|
|
587
|
+
["died", fm.data?.died],
|
|
588
|
+
]
|
|
589
|
+
: fm?.type === "lore" ?
|
|
590
|
+
[
|
|
591
|
+
["when", fm.data?.event?.when],
|
|
592
|
+
["until", fm.data?.event?.until],
|
|
593
|
+
]
|
|
594
|
+
: [];
|
|
595
|
+
const out = {};
|
|
596
|
+
for (const [key, value] of fields) {
|
|
597
|
+
if (value === undefined || value === null) continue;
|
|
598
|
+
const parsed = parseNoteDate(value, {
|
|
599
|
+
...context,
|
|
600
|
+
allowUnknown: key === "born" || key === "died",
|
|
601
|
+
});
|
|
602
|
+
if (parsed.date && !parsed.findings.some((finding) => finding.severity === "error")) {
|
|
603
|
+
const era =
|
|
604
|
+
parsed.date.marker ? context?.markers?.get(parsed.date.marker)
|
|
605
|
+
: parsed.date.qualifier ? context?.eras?.get(parsed.date.qualifier)
|
|
606
|
+
: null;
|
|
607
|
+
const printable = formatNoteDate(parsed.date, era, context?.daysPerYear);
|
|
608
|
+
out[key] = { ...parsed.date, prose: printable?.prose ?? null };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return out;
|
|
612
|
+
}
|
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
|
}
|