@heroiclands/package-build 23.10.0 → 23.12.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 +28 -0
- package/README.md +1 -0
- package/assets/preview/check-sanitizer.go +68 -0
- package/assets/preview/gitea-theme.js +36 -0
- package/assets/preview/gitea.ini +127 -0
- package/assets/styles/content-base.css +119 -16
- package/bin/content-commands.mjs +2 -2
- package/bin/package-build-preview.mjs +75 -0
- package/content-config.mjs +11 -0
- package/docs/api.md +131 -45
- package/docs/authoring/first-note.md +10 -0
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +111 -6
- package/docs/commands.md +46 -2
- package/docs/configuration.md +21 -4
- package/docs/content-format.md +4 -2
- package/docs/design.md +19 -0
- package/docs/guides/book.md +11 -0
- package/docs/guides/site.md +7 -0
- package/docs/reference/format-details.md +82 -27
- package/docs/reference/note-types.md +0 -1
- package/docs/reference/theme-pages.md +1 -1
- package/docs/source-preview.md +172 -0
- package/engine/anchors.mjs +36 -4
- package/engine/base-compiler.mjs +7 -8
- package/engine/being-age.mjs +19 -105
- package/engine/calendar-notes.mjs +3 -25
- package/engine/compile-corpus.mjs +2 -1
- package/engine/content-format.yaml +0 -1
- package/engine/content-index.mjs +9 -9
- package/engine/content-links.mjs +41 -18
- package/engine/content-spans.mjs +54 -7
- package/engine/current-date.mjs +37 -0
- package/engine/definition-list.mjs +58 -0
- package/engine/frontmatter-lint.mjs +7 -2
- package/engine/helpers.mjs +6 -2
- package/engine/index.mjs +2 -0
- package/engine/infobox.mjs +1 -5
- package/engine/note-vocabulary.mjs +2 -2
- package/engine/npc-generate.mjs +7 -6
- package/engine/pdf-render.mjs +36 -3
- package/engine/preview-current-date.mjs +60 -0
- package/engine/site-build.mjs +5 -5
- package/engine/site-index.mjs +28 -6
- package/engine/site-preview.mjs +641 -325
- package/engine/web-wikilinks.mjs +33 -12
- package/engine/wikilink-syntax.mjs +65 -8
- package/engine/wikilinks.mjs +29 -11
- package/hugo-theme/layouts/_default/list.html +1 -1
- package/hugo-theme/layouts/blog/list.html +1 -1
- package/hugo-theme/layouts/blog-post/single.html +1 -1
- package/hugo-theme/layouts/partials/dictionary-lists.html +53 -0
- package/hugo-theme/layouts/partials/footer.html +2 -0
- package/hugo-theme/layouts/partials/page-body.html +1 -1
- package/hugo-theme/static/css/style.css +39 -9
- package/hugo-theme/static/js/inline-spoilers.js +23 -0
- package/package.json +8 -2
- package/types/content-config.d.mts +8 -0
- package/types/engine/anchors.d.mts +2 -1
- package/types/engine/base-compiler.d.mts +1 -1
- package/types/engine/being-age.d.mts +10 -44
- package/types/engine/calendar-notes.d.mts +2 -6
- package/types/engine/compile-corpus.d.mts +2 -1
- package/types/engine/content-index.d.mts +3 -1
- package/types/engine/content-spans.d.mts +7 -1
- package/types/engine/current-date.d.mts +9 -0
- package/types/engine/definition-list.d.mts +8 -0
- package/types/engine/frontmatter-lint.d.mts +6 -2
- package/types/engine/index.d.mts +2 -0
- package/types/engine/npc-generate.d.mts +2 -2
- package/types/engine/preview-current-date.d.mts +8 -0
- package/types/engine/site-index.d.mts +5 -1
- package/types/engine/site-preview.d.mts +40 -33
- package/types/engine/wikilink-syntax.d.mts +40 -3
package/engine/being-age.mjs
CHANGED
|
@@ -11,51 +11,9 @@
|
|
|
11
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* **The middle state of `age`: absent beside a dated `born`, it follows from
|
|
16
|
-
* the date.**
|
|
17
|
-
*
|
|
18
|
-
* `age` is a three-state override — authored it wins, absent beside a dated
|
|
19
|
-
* `born` it is computed, absent beside `born: unknown` or an absent `born` it
|
|
20
|
-
* stays unknown. This module builds the middle state, which nothing else in
|
|
21
|
-
* the toolchain does: `born` and `age` are read straight through to every
|
|
22
|
-
* compiled surface exactly as authored, unless a note's own package states the
|
|
23
|
-
* day the setting stops.
|
|
24
|
-
*
|
|
25
|
-
* ## Where the present comes from
|
|
26
|
-
*
|
|
27
|
-
* A world's `present` is a fact about the world, not about a calendar or a
|
|
28
|
-
* build configuration — it is `data.present` on the package's own `place` note
|
|
29
|
-
* of `subType: world` (or `celestial`), read through
|
|
30
|
-
* {@link module:engine/calendar-notes.worldInvariants}. A package with no such
|
|
31
|
-
* note states no present, computes no age, and says nothing about it: the
|
|
32
|
-
* absence is silent, exactly as a package that declares no calendar is asked
|
|
33
|
-
* no calendar questions.
|
|
34
|
-
*
|
|
35
|
-
* ## The arithmetic is on two dates, not on two years
|
|
36
|
-
*
|
|
37
|
-
* A year subtracted from a year is wrong whenever the birthday has not yet
|
|
38
|
-
* happened in the present's year — the common shape, not the exception. So
|
|
39
|
-
* {@link computeAge} compares the two dates' canonical day ordinals where an
|
|
40
|
-
* era marker supplies them. The birthday that still lies ahead in the current
|
|
41
|
-
* year does not count as a completed year. An unmarked slash date has no
|
|
42
|
-
* calendar to convert, so it uses its written month and day as its ordinal.
|
|
43
|
-
*
|
|
44
|
-
* ## `~` follows the convention {@link module:engine/note-dates} sets
|
|
45
|
-
*
|
|
46
|
-
* A date's `~` marks the value approximate without changing what it parses to,
|
|
47
|
-
* so a renderer keeps the authored text and a comparison still has a number.
|
|
48
|
-
* {@link parseAgeMagnitude} draws the same distinction for `age` — the tilde,
|
|
49
|
-
* the magnitude — without routing through {@link module:engine/note-dates}
|
|
50
|
-
* itself: `age` is a plain count of years, not a date, so it has no month, no
|
|
51
|
-
* day, no era, and — unlike a date — no refusal of `0`. Reusing the date
|
|
52
|
-
* grammar's month/day/era machinery here would accept an age nothing about the
|
|
53
|
-
* field means, and refuse the one age an infant can have.
|
|
54
|
-
*
|
|
55
|
-
* @module
|
|
56
|
-
*/
|
|
14
|
+
/** Authored ages and setting-wide current-date age calculations. @module */
|
|
57
15
|
|
|
58
|
-
import {
|
|
16
|
+
import { DEFAULT_CURRENT_DATE } from "./current-date.mjs";
|
|
59
17
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
60
18
|
import { parseNoteDate } from "./note-dates.mjs";
|
|
61
19
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
@@ -107,11 +65,9 @@ export function parseAgeMagnitude(raw) {
|
|
|
107
65
|
*
|
|
108
66
|
* @param {unknown} bornRaw - The being's authored `born`, or `undefined`/`null`
|
|
109
67
|
* where it wrote none.
|
|
110
|
-
* @param {unknown} presentRaw - The
|
|
111
|
-
* {@link module:engine/calendar-notes.worldInvariants} read it, or
|
|
112
|
-
* `undefined`/`null` where the package states none.
|
|
68
|
+
* @param {unknown} presentRaw - The configured neutral current date.
|
|
113
69
|
* @returns {number|null} The age, or `null` where `born` is `unknown` or
|
|
114
|
-
* absent
|
|
70
|
+
* absent or future, or where either date fails to parse.
|
|
115
71
|
*/
|
|
116
72
|
export function computeAge(bornRaw, presentRaw, dateContext = {}) {
|
|
117
73
|
if (bornRaw === undefined || bornRaw === null) return null;
|
|
@@ -126,11 +82,15 @@ export function computeAge(bornRaw, presentRaw, dateContext = {}) {
|
|
|
126
82
|
// A resolved marker supplies a canonical day. An unmarked slash date uses
|
|
127
83
|
// its written month and day. Missing parts read as the first day that the
|
|
128
84
|
// value can name.
|
|
129
|
-
const bornOrdinal =
|
|
130
|
-
|
|
85
|
+
const bornOrdinal =
|
|
86
|
+
born.canonicalDay ?? (born.month === null ? 1 : (born.month ?? 1) * 100 + (born.day ?? 1));
|
|
87
|
+
const presentOrdinal =
|
|
88
|
+
present.canonicalDay ??
|
|
89
|
+
(present.month === null ? 1 : (present.month ?? 1) * 100 + (present.day ?? 1));
|
|
131
90
|
const notYetHappened = bornOrdinal > presentOrdinal;
|
|
132
91
|
|
|
133
|
-
|
|
92
|
+
const age = present.canonicalYear - born.canonicalYear - (notYetHappened ? 1 : 0);
|
|
93
|
+
return age < 0 ? null : age;
|
|
134
94
|
}
|
|
135
95
|
|
|
136
96
|
/**
|
|
@@ -146,8 +106,7 @@ export function computeAge(bornRaw, presentRaw, dateContext = {}) {
|
|
|
146
106
|
* itself untouched.
|
|
147
107
|
*
|
|
148
108
|
* @param {object} fm - A note's frontmatter, as freshly parsed.
|
|
149
|
-
* @param {unknown} presentRaw - The
|
|
150
|
-
* `undefined`/`null` where it states none.
|
|
109
|
+
* @param {unknown} presentRaw - The configured neutral current date.
|
|
151
110
|
* @returns {void}
|
|
152
111
|
*/
|
|
153
112
|
export function applyComputedBeingAge(fm, presentRaw, dateContext = {}) {
|
|
@@ -171,63 +130,21 @@ export function applyComputedBeingAge(fm, presentRaw, dateContext = {}) {
|
|
|
171
130
|
}
|
|
172
131
|
|
|
173
132
|
/**
|
|
174
|
-
*
|
|
175
|
-
* frontmatter objects carries.
|
|
176
|
-
*
|
|
177
|
-
* Two shapes reach this by way of two thin adapters, because the callers that
|
|
178
|
-
* need a present hold their notes two different ways: a flat index record
|
|
179
|
-
* (`type` and `data` at the top level) and a raw walk's `{frontmatter}` pair.
|
|
180
|
-
* Neither shape is the `{fm, type}` pair {@link worldInvariants} reads, so each
|
|
181
|
-
* adapter states the one conversion its caller needs rather than asking every
|
|
182
|
-
* caller to know it.
|
|
183
|
-
*
|
|
184
|
-
* @param {readonly object[]|undefined} records - Flat index records — a
|
|
185
|
-
* content-index record, or anything else spreading a note's frontmatter to
|
|
186
|
-
* its own top level.
|
|
187
|
-
* @returns {unknown} The present, or `null` where no `place` note in the batch
|
|
188
|
-
* declares one.
|
|
189
|
-
*/
|
|
190
|
-
export function presentAmongRecords(records) {
|
|
191
|
-
return worldInvariants({
|
|
192
|
-
notes: (records ?? []).map((record) => ({ fm: record, type: record?.type })),
|
|
193
|
-
}).present;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* The present read from a batch of raw frontmatter objects.
|
|
198
|
-
*
|
|
199
|
-
* @param {readonly (object|null|undefined)[]|undefined} frontmatters - One
|
|
200
|
-
* entry per note walked, as parsed — never a record.
|
|
201
|
-
* @returns {unknown} The present, or `null` where none is declared.
|
|
202
|
-
*/
|
|
203
|
-
export function presentAmongFrontmatters(frontmatters) {
|
|
204
|
-
return worldInvariants({
|
|
205
|
-
notes: (frontmatters ?? []).map((fm) => ({ fm, type: fm?.type })),
|
|
206
|
-
}).present;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Check a being's authored `age` against what `born` and the package's present
|
|
211
|
-
* compute.
|
|
133
|
+
* Check an authored age against the birth date and configured current date.
|
|
212
134
|
*
|
|
213
135
|
* Registered as `being`'s whole-note `check` in `note-vocabulary.mjs`, run the
|
|
214
136
|
* way `checkPlace` and `checkCalendarNote` are — beside every other check the
|
|
215
137
|
* closed `data:` container earns, with the same `index`.
|
|
216
138
|
*
|
|
217
|
-
*
|
|
218
|
-
* there is nothing to disagree with, and a package that declares no present
|
|
219
|
-
* computes no age to disagree with either — both cases are read by
|
|
220
|
-
* {@link computeAge} returning `null`, and neither says a word.
|
|
139
|
+
* An unknown or absent birth date supplies no computed age to compare.
|
|
221
140
|
*
|
|
222
|
-
* @param {object} note - The note, as the link index hands it over
|
|
223
|
-
* {@link module:engine/calendar-notes.worldInvariants}'s `index.notes` shape.
|
|
141
|
+
* @param {object} note - The note, as the link index hands it over.
|
|
224
142
|
* @param {object} [opts]
|
|
225
|
-
* @param {object} [opts.index] -
|
|
226
|
-
*
|
|
227
|
-
* index skips reporting every reference as dead.
|
|
143
|
+
* @param {object} [opts.index] - Calendar context for authored birth dates.
|
|
144
|
+
* @param {string} [opts.currentDate="720.1"] - The configured neutral current date.
|
|
228
145
|
* @returns {object[]} Findings — at most one `warning`, naming both values.
|
|
229
146
|
*/
|
|
230
|
-
export function checkBeingAge(note, { index } = {}) {
|
|
147
|
+
export function checkBeingAge(note, { index, currentDate = DEFAULT_CURRENT_DATE } = {}) {
|
|
231
148
|
if (!isBeingType(note?.type ?? note?.fm?.type)) return [];
|
|
232
149
|
const data = dataOf(note?.fm);
|
|
233
150
|
if (!data) return [];
|
|
@@ -235,10 +152,7 @@ export function checkBeingAge(note, { index } = {}) {
|
|
|
235
152
|
const bornRaw = data.born;
|
|
236
153
|
const ageRaw = data.age;
|
|
237
154
|
if (ageRaw === undefined || ageRaw === null || ageRaw === "") return [];
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const present = worldInvariants(index).present;
|
|
241
|
-
const computed = computeAge(bornRaw, present, reckoningContext(index));
|
|
155
|
+
const computed = computeAge(bornRaw, currentDate, reckoningContext(index));
|
|
242
156
|
if (computed === null) return [];
|
|
243
157
|
|
|
244
158
|
const authored = parseAgeMagnitude(ageRaw);
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* A **world fact** is how long the year is, how the day divides, and how the
|
|
21
21
|
* moon moves. Every people on the world lives under the same ones, so no
|
|
22
22
|
* calendar may restate them: they are read from the package's `place` notes —
|
|
23
|
-
* `data.year`
|
|
23
|
+
* `data.year` from the world, `data.moon` from the moon —
|
|
24
24
|
* and written by the emitters.
|
|
25
25
|
*
|
|
26
26
|
* A **calendar** is one people's division of those days: the months they name,
|
|
@@ -301,7 +301,7 @@ export const CALENDAR_FIELDS = Object.freeze([
|
|
|
301
301
|
* The `data:` keys a world or a celestial body may write.
|
|
302
302
|
*
|
|
303
303
|
* Four groups, and which note carries which is a fact about the body rather
|
|
304
|
-
* than a convention: a planet states its own size, tilt
|
|
304
|
+
* than a convention: a planet states its own size, tilt and year; a
|
|
305
305
|
* moon states its own size, orbit and cycle.
|
|
306
306
|
*
|
|
307
307
|
* @type {readonly object[]}
|
|
@@ -349,12 +349,6 @@ export const INVARIANT_FIELDS = Object.freeze([
|
|
|
349
349
|
kind: "number",
|
|
350
350
|
describe: "How many seconds the minute divides into.",
|
|
351
351
|
},
|
|
352
|
-
{
|
|
353
|
-
name: "present",
|
|
354
|
-
shape: "a date",
|
|
355
|
-
kind: "string",
|
|
356
|
-
describe: "The day the setting stops and play begins — what an age is computed against.",
|
|
357
|
-
},
|
|
358
352
|
{
|
|
359
353
|
name: "body.diameterKm",
|
|
360
354
|
shape: "number",
|
|
@@ -438,14 +432,13 @@ function keysWritten(note, keys) {
|
|
|
438
432
|
* note has no year, and every question that depends on one goes unasked.
|
|
439
433
|
*
|
|
440
434
|
* @param {object} [index] - The link index, holding every note in the package.
|
|
441
|
-
* @returns {{year: object|null,
|
|
435
|
+
* @returns {{year: object|null, moon: object|null,
|
|
442
436
|
* body: object|null, moonName: string|null, yearNote: object|null,
|
|
443
437
|
* moonNote: object|null}} What the package's notes state.
|
|
444
438
|
*/
|
|
445
439
|
export function worldInvariants(index) {
|
|
446
440
|
const empty = {
|
|
447
441
|
year: null,
|
|
448
|
-
present: null,
|
|
449
442
|
moon: null,
|
|
450
443
|
body: null,
|
|
451
444
|
moonName: null,
|
|
@@ -459,7 +452,6 @@ export function worldInvariants(index) {
|
|
|
459
452
|
const data = dataOf(note);
|
|
460
453
|
if (!found.yearNote && data.year && typeof data.year === "object") {
|
|
461
454
|
found.year = data.year;
|
|
462
|
-
found.present = data.present ?? null;
|
|
463
455
|
found.yearNote = note;
|
|
464
456
|
}
|
|
465
457
|
if (!found.moonNote && data.moon && typeof data.moon === "object") {
|
|
@@ -619,9 +611,6 @@ function checkCalendarDayNames(note, daysPerYear) {
|
|
|
619
611
|
* - a moon with no year is refused, because a cycle measured in days has
|
|
620
612
|
* nothing to be measured against.
|
|
621
613
|
*
|
|
622
|
-
* `data.present` is read through the parser a note's dates go through, with the
|
|
623
|
-
* corpus in hand, so a present written in an era resolves where it is written.
|
|
624
|
-
*
|
|
625
614
|
* @param {object} note - The note, as the link index hands it over.
|
|
626
615
|
* @param {object} [opts]
|
|
627
616
|
* @param {object} [opts.index] - The link index, holding every note.
|
|
@@ -650,17 +639,6 @@ export function checkWorldFacts(note, { index } = {}) {
|
|
|
650
639
|
return findings;
|
|
651
640
|
}
|
|
652
641
|
|
|
653
|
-
if (written.includes("present")) {
|
|
654
|
-
const { findings: dated } = parseNoteDate(dataOf(note).present, {
|
|
655
|
-
...reckoningContext(index),
|
|
656
|
-
field: "data.present",
|
|
657
|
-
allowUnknown: false,
|
|
658
|
-
file: note.file,
|
|
659
|
-
raw: note.raw,
|
|
660
|
-
keyPath: ["data", "present"],
|
|
661
|
-
});
|
|
662
|
-
findings.push(...dated);
|
|
663
|
-
}
|
|
664
642
|
if (written.includes("moon") && dataOf(note).moon?.newOn !== undefined) {
|
|
665
643
|
const parsed = parseNoteDate(dataOf(note).moon.newOn, {
|
|
666
644
|
...reckoningContext(index),
|
|
@@ -61,7 +61,7 @@ import { prepareTreeSqlTables } from "./sql-tables.mjs";
|
|
|
61
61
|
* the index refuses is a note the compile must still *report*, exactly as the
|
|
62
62
|
* compile loop reported it when the loop was the first to see it.
|
|
63
63
|
* @param {import("./content-session.mjs").ContentSession} [opts.session] - Captured context shared with other operations.
|
|
64
|
-
* @returns {Promise<{records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[], syntax?: import("./content-session.mjs").ContentSyntax}>} The corpus,
|
|
64
|
+
* @returns {Promise<{currentDate: string, records: object[], parsed: Map<string, object>, linkIndex: object, sqlTables: Map<string, object[]>|undefined, problems: object[], syntax?: import("./content-session.mjs").ContentSyntax}>} The corpus,
|
|
65
65
|
* its indexes, and the notes it could not record.
|
|
66
66
|
*/
|
|
67
67
|
export async function buildCompileCorpus({
|
|
@@ -99,6 +99,7 @@ export async function buildCompileCorpus({
|
|
|
99
99
|
const sqlOptions = { ...corpusOptions, skipDirectories, parsed };
|
|
100
100
|
const sqlTables = await prepareTreeSqlTables(contentBase, sqlOptions);
|
|
101
101
|
return {
|
|
102
|
+
currentDate: resolved.currentDate,
|
|
102
103
|
records,
|
|
103
104
|
parsed,
|
|
104
105
|
problems: collected,
|
package/engine/content-index.mjs
CHANGED
|
@@ -179,7 +179,8 @@ import { isDraftNote } from "./note-vocabulary.mjs";
|
|
|
179
179
|
// A being's computed `age` — the middle state between an authored override and
|
|
180
180
|
// an unknown one — filled in against the package's own declared present before
|
|
181
181
|
// a record is built, so the index carries it without the note being written to.
|
|
182
|
-
import { applyComputedBeingAge
|
|
182
|
+
import { applyComputedBeingAge } from "./being-age.mjs";
|
|
183
|
+
import { normalizeCurrentDate } from "./current-date.mjs";
|
|
183
184
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
184
185
|
import { resolvedDateFields } from "./note-dates.mjs";
|
|
185
186
|
|
|
@@ -1097,6 +1098,7 @@ export function resolveCultures(
|
|
|
1097
1098
|
* asserts that it does not exist. The same holds for a culture conflict: a
|
|
1098
1099
|
* reader receives each as a finding, and the emitter throws once, listing
|
|
1099
1100
|
* every one.
|
|
1101
|
+
* @param {string} [options.currentDate="720.1"] - Setting-wide neutral current date.
|
|
1100
1102
|
* @param {(key: string) => ({culture?: unknown}|undefined)} [options.foreignRecord] -
|
|
1101
1103
|
* Another package's entry for a canonical address, read for a parent that
|
|
1102
1104
|
* package publishes; see {@link resolveCultures}.
|
|
@@ -1113,6 +1115,7 @@ export function collectContentIndex(
|
|
|
1113
1115
|
contentBase,
|
|
1114
1116
|
{
|
|
1115
1117
|
contentPackage,
|
|
1118
|
+
currentDate,
|
|
1116
1119
|
skipDirectories,
|
|
1117
1120
|
assetsBase,
|
|
1118
1121
|
manifest,
|
|
@@ -1134,10 +1137,7 @@ export function collectContentIndex(
|
|
|
1134
1137
|
// tree to walk and the index is its asset records. The caller decides
|
|
1135
1138
|
// whether an absent tree is a mistake; by the time the walk is reached it
|
|
1136
1139
|
// is simply a package with nothing to compile.
|
|
1137
|
-
//
|
|
1138
|
-
// same walk before the loop below reads it a second time, and
|
|
1139
|
-
// `walkMarkdownTree` is a generator — a single-use one, exhausted the
|
|
1140
|
-
// moment anything else iterates it first.
|
|
1140
|
+
// Materialize parsed notes once for shared calendar and source context.
|
|
1141
1141
|
const notes = fs.existsSync(contentBase) ? [...walkMarkdownTree(contentBase, walkOpts)] : [];
|
|
1142
1142
|
const parsedNotes = [];
|
|
1143
1143
|
for (const note of notes) {
|
|
@@ -1165,10 +1165,8 @@ export function collectContentIndex(
|
|
|
1165
1165
|
problems.push(problem);
|
|
1166
1166
|
}
|
|
1167
1167
|
|
|
1168
|
-
//
|
|
1169
|
-
|
|
1170
|
-
// second read is needed to answer a question about all of it.
|
|
1171
|
-
const present = presentAmongFrontmatters(parsedNotes.map((n) => n.frontmatter));
|
|
1168
|
+
// Every derived age and recurring event uses the same configured date.
|
|
1169
|
+
const present = normalizeCurrentDate(currentDate);
|
|
1172
1170
|
// Carried on the same context `resolvedDateFields` reads: a recurring
|
|
1173
1171
|
// event's `next` is computed against this present exactly as a being's
|
|
1174
1172
|
// `age` already is, above.
|
|
@@ -1497,6 +1495,7 @@ export function indexRecordsFor({
|
|
|
1497
1495
|
};
|
|
1498
1496
|
return collectContentIndex(tree, {
|
|
1499
1497
|
contentPackage: resolved.contentPackage,
|
|
1498
|
+
currentDate: resolved.currentDate,
|
|
1500
1499
|
addressContext: {
|
|
1501
1500
|
...noteAddressContext(resolved),
|
|
1502
1501
|
declaresAddress: dependencyAddresses(resolved),
|
|
@@ -1680,6 +1679,7 @@ export async function prepareContentIndex({ contentBase, config, session } = {})
|
|
|
1680
1679
|
});
|
|
1681
1680
|
const index = buildSiteIndex(pages, {
|
|
1682
1681
|
package: resolved.contentPackage,
|
|
1682
|
+
currentDate: resolved.currentDate,
|
|
1683
1683
|
records: visible,
|
|
1684
1684
|
foreignIndex: foreign.index,
|
|
1685
1685
|
syntax: session?.syntax,
|
package/engine/content-links.mjs
CHANGED
|
@@ -444,13 +444,17 @@ export function buildLinkIndex(
|
|
|
444
444
|
// Code is verbatim, so a `[[…]]` inside a fence, an indented block or an
|
|
445
445
|
// inline span is not a link — the compilers make none of it either.
|
|
446
446
|
for (const [all, rawInner] of matchAllOutsideCode(body, new RegExp(WIKILINK.source, "g"))) {
|
|
447
|
-
const parsed = readWikilink(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
447
|
+
const parsed = readWikilink(
|
|
448
|
+
rawInner,
|
|
449
|
+
{
|
|
450
|
+
package: pkg,
|
|
451
|
+
system: "note",
|
|
452
|
+
types,
|
|
453
|
+
packages,
|
|
454
|
+
noIndexPackages,
|
|
455
|
+
},
|
|
456
|
+
all,
|
|
457
|
+
);
|
|
454
458
|
const target = parsed.target.kind ? parsed.rawTarget : parsed.target;
|
|
455
459
|
const { anchor, rawTarget } = parsed;
|
|
456
460
|
const occurrence = (seen.get(all) ?? 0) + 1;
|
|
@@ -459,6 +463,7 @@ export function buildLinkIndex(
|
|
|
459
463
|
// in the file. A link a table generated is not in the file at all,
|
|
460
464
|
// so the search simply fails and a finding names the file.
|
|
461
465
|
out.push({
|
|
466
|
+
attributeProblems: parsed.attributes?.problems ?? [],
|
|
462
467
|
target,
|
|
463
468
|
rawTarget,
|
|
464
469
|
anchor,
|
|
@@ -962,15 +967,23 @@ export function auditLinks(index) {
|
|
|
962
967
|
|
|
963
968
|
const deadAnchors = [];
|
|
964
969
|
for (const note of notes) {
|
|
965
|
-
for (const {
|
|
970
|
+
for (const {
|
|
971
|
+
target,
|
|
972
|
+
rawTarget,
|
|
973
|
+
anchor,
|
|
974
|
+
text,
|
|
975
|
+
occurrence,
|
|
976
|
+
labelled,
|
|
977
|
+
attributeProblems,
|
|
978
|
+
} of linksOf(note)) {
|
|
966
979
|
if (!anchor || !labelled) continue;
|
|
967
980
|
const local = target ? resolve(target) : note;
|
|
968
981
|
const foreign = !local && target ? manifestHit(target) : null;
|
|
969
982
|
// An unresolved address is reported below. A foreign sheet opens
|
|
970
983
|
// its system document, so only the readable note view names prose
|
|
971
|
-
// sections
|
|
984
|
+
// sections, including private destinations.
|
|
972
985
|
if (!local) {
|
|
973
|
-
if (!foreign
|
|
986
|
+
if (!foreign) continue;
|
|
974
987
|
const qualified = readQualifier(
|
|
975
988
|
target,
|
|
976
989
|
index.types,
|
|
@@ -1016,7 +1029,24 @@ export function auditLinks(index) {
|
|
|
1016
1029
|
const unlabelledLinks = [];
|
|
1017
1030
|
const usedManifest = new Set();
|
|
1018
1031
|
for (const note of notes) {
|
|
1019
|
-
for (const {
|
|
1032
|
+
for (const {
|
|
1033
|
+
target,
|
|
1034
|
+
rawTarget,
|
|
1035
|
+
anchor,
|
|
1036
|
+
text,
|
|
1037
|
+
occurrence,
|
|
1038
|
+
labelled,
|
|
1039
|
+
attributeProblems,
|
|
1040
|
+
} of linksOf(note)) {
|
|
1041
|
+
if (attributeProblems.length)
|
|
1042
|
+
deadAddresses.push({
|
|
1043
|
+
note,
|
|
1044
|
+
target: rawTarget,
|
|
1045
|
+
text,
|
|
1046
|
+
occurrence,
|
|
1047
|
+
reason: "invalid-attributes",
|
|
1048
|
+
problems: attributeProblems,
|
|
1049
|
+
});
|
|
1020
1050
|
// The label is required whatever the link part is, an anchor
|
|
1021
1051
|
// included — so this is tested before the same-page form.
|
|
1022
1052
|
if (!labelled) {
|
|
@@ -1063,9 +1093,6 @@ export function auditLinks(index) {
|
|
|
1063
1093
|
}
|
|
1064
1094
|
const resolved = index.resolveAddress(target);
|
|
1065
1095
|
if (resolved) {
|
|
1066
|
-
if ((isGmNote(resolved.fm) || resolved.gm) && !isGmNote(note.fm)) {
|
|
1067
|
-
deadAddresses.push({ ...at, reason: "gm" });
|
|
1068
|
-
}
|
|
1069
1096
|
continue;
|
|
1070
1097
|
}
|
|
1071
1098
|
// A stub is in the index, so the refusal can name it. Asked before
|
|
@@ -1084,10 +1111,6 @@ export function auditLinks(index) {
|
|
|
1084
1111
|
// report.
|
|
1085
1112
|
const manifest = manifestHit(target);
|
|
1086
1113
|
if (manifest) {
|
|
1087
|
-
if (manifest.gm && !isGmNote(note.fm)) {
|
|
1088
|
-
deadAddresses.push({ ...at, reason: "gm" });
|
|
1089
|
-
continue;
|
|
1090
|
-
}
|
|
1091
1114
|
usedManifest.add(encodeAddresses(target).toLowerCase());
|
|
1092
1115
|
continue;
|
|
1093
1116
|
}
|
package/engine/content-spans.mjs
CHANGED
|
@@ -21,21 +21,47 @@ import { inlineText } from "./name-markup.mjs";
|
|
|
21
21
|
|
|
22
22
|
/** Add bracket spans after recognized Markdown links and footnotes.
|
|
23
23
|
* @param {import("markdown-it")} md - Markdown parser to extend.
|
|
24
|
+
* @param {object} [options] - Surface presentation.
|
|
25
|
+
* @param {boolean} [options.source] - Emit opaque authored source tokens for buffer preview.
|
|
26
|
+
* @param {boolean} [options.spoilers] - Add focusable CSS spoiler wrappers (Foundry).
|
|
24
27
|
*/
|
|
25
|
-
export function spanMarkdownPlugin(md) {
|
|
28
|
+
export function spanMarkdownPlugin(md, { spoilers = false, source = false } = {}) {
|
|
29
|
+
if (spoilers) {
|
|
30
|
+
md.renderer.rules.span_open = (tokens, index, options, env, renderer) => {
|
|
31
|
+
const token = tokens[index];
|
|
32
|
+
const secret = (token.attrGet("class") ?? "").split(/\s+/).includes("secret");
|
|
33
|
+
if (secret) {
|
|
34
|
+
token.attrSet("data-inline-spoiler", "");
|
|
35
|
+
token.attrSet("tabindex", "0");
|
|
36
|
+
if (!token.attrGet("title"))
|
|
37
|
+
token.attrSet("title", "Spoiler: hover or focus to reveal");
|
|
38
|
+
if (!token.attrGet("aria-label"))
|
|
39
|
+
token.attrSet("aria-label", "Spoiler; focus to reveal");
|
|
40
|
+
}
|
|
41
|
+
return renderer.renderToken(tokens, index, options);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
26
44
|
md.inline.ruler.after("link", "bracket_span", (state, silent) => {
|
|
27
45
|
const start = state.pos;
|
|
28
46
|
if (
|
|
29
47
|
state.src[start] !== "[" ||
|
|
48
|
+
// Raw wikilinks are handled before spans render. Refuse their
|
|
49
|
+
// adjacent closing brackets without parsing every label twice.
|
|
50
|
+
(state.src[start + 1] === "[" &&
|
|
51
|
+
state.src[state.src.indexOf("]", start + 2) + 1] === "]") ||
|
|
30
52
|
/@[A-Za-z]+$/.test(state.src.slice(0, start)) ||
|
|
31
|
-
state.src[start + 1] === "[" ||
|
|
32
53
|
state.src[start + 1] === "^" ||
|
|
33
54
|
state.src[start + 1] === "!" ||
|
|
34
55
|
["!", "[", "]"].includes(state.src[start - 1])
|
|
35
56
|
)
|
|
36
57
|
return false;
|
|
37
58
|
const end = state.md.helpers.parseLinkLabel(state, start, false);
|
|
38
|
-
if (
|
|
59
|
+
if (
|
|
60
|
+
end < 0 ||
|
|
61
|
+
["(", "["].includes(state.src[end + 1]) ||
|
|
62
|
+
(state.src[start + 1] === "[" && state.src[end + 1] !== "{")
|
|
63
|
+
)
|
|
64
|
+
return false;
|
|
39
65
|
let finish = end + 1;
|
|
40
66
|
let parsed = { id: "", classes: [], values: {}, problems: [] };
|
|
41
67
|
if (state.src[finish] === "{") {
|
|
@@ -69,6 +95,22 @@ export function spanMarkdownPlugin(md) {
|
|
|
69
95
|
state.pos = finish;
|
|
70
96
|
return true;
|
|
71
97
|
}
|
|
98
|
+
if (source) {
|
|
99
|
+
// An incomplete attribute stays opaque through the end of its line.
|
|
100
|
+
if (state.src[finish] === "{") {
|
|
101
|
+
const newline = state.src.indexOf("\n", finish);
|
|
102
|
+
finish = newline < 0 ? state.posMax : newline;
|
|
103
|
+
}
|
|
104
|
+
const token = state.push("span_source", "", 0);
|
|
105
|
+
token.content = state.src.slice(start, finish);
|
|
106
|
+
token.meta = {
|
|
107
|
+
text: state.src.slice(start + 1, end),
|
|
108
|
+
labelEnd: end - start,
|
|
109
|
+
secret: parsed.classes.includes("secret"),
|
|
110
|
+
};
|
|
111
|
+
state.pos = finish;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
72
114
|
const token = state.push("span_open", "span", 1);
|
|
73
115
|
token.meta = {
|
|
74
116
|
span: true,
|
|
@@ -259,10 +301,15 @@ export function renderSpans(source) {
|
|
|
259
301
|
)
|
|
260
302
|
.sort((a, b) => b.start - a.start)) {
|
|
261
303
|
const attrs = span.attrs.map(([key, value]) => ` ${key}="${escape(value)}"`).join("");
|
|
262
|
-
markdown
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
304
|
+
const inner = renderSpans(markdown.slice(span.start + 1, span.contentEnd)).markdown;
|
|
305
|
+
const secret = span.attrs.some(
|
|
306
|
+
([key, value]) => key === "class" && value.split(/\s+/).includes("secret"),
|
|
307
|
+
);
|
|
308
|
+
const content =
|
|
309
|
+
secret ?
|
|
310
|
+
`<span${attrs} data-inline-secret><button type="button" class="inline-secret-toggle" aria-label="Show spoiler" aria-expanded="false">Show spoiler</button><span class="inline-secret-content" hidden>${inner}</span></span>`
|
|
311
|
+
: `<span${attrs}>${inner}</span>`;
|
|
312
|
+
markdown = markdown.slice(0, span.start) + content + markdown.slice(span.end);
|
|
266
313
|
}
|
|
267
314
|
return { markdown, errors };
|
|
268
315
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Setting-wide neutral current date, independent of configuration loading. */
|
|
15
|
+
import { parseNoteDate } from "./note-dates.mjs";
|
|
16
|
+
|
|
17
|
+
/** Default day on the neutral setting timeline. */
|
|
18
|
+
export const DEFAULT_CURRENT_DATE = "720.1";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate a quoted neutral date and preserve its authored day digits.
|
|
22
|
+
* @param {unknown} [value] - Explicit configuration or adapter override.
|
|
23
|
+
* @returns {string} Validated neutral date; undefined selects the default.
|
|
24
|
+
* @throws {TypeError} An explicit value is not a valid neutral date string.
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeCurrentDate(value = undefined) {
|
|
27
|
+
if (value === undefined) return DEFAULT_CURRENT_DATE;
|
|
28
|
+
if (typeof value !== "string" || !/^-?\d+(?:\.\d+)?(?::\d{6})?$/.test(value.trim()))
|
|
29
|
+
throw new TypeError('currentDate must be a quoted neutral date: "<year>[.<day>[:HHMMSS]]"');
|
|
30
|
+
const text = value.trim();
|
|
31
|
+
const parsed = parseNoteDate(text, { allowUnknown: false });
|
|
32
|
+
if (!parsed.date || parsed.findings.some((finding) => finding.severity === "error"))
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
`currentDate: ${parsed.findings[0]?.message ?? "invalid neutral date"}`,
|
|
35
|
+
);
|
|
36
|
+
return text;
|
|
37
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Semantic dictionary rows for HTML definition lists. @module */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Group each term and its definitions without reparsing their Markdown tokens.
|
|
18
|
+
* Register after markdown-it-deflist; nested blocks and footnotes retain their
|
|
19
|
+
* document context.
|
|
20
|
+
* @param {import("markdown-it")} md - An HTML Markdown parser with definition lists.
|
|
21
|
+
*/
|
|
22
|
+
export function dictionaryListPlugin(md) {
|
|
23
|
+
md.core.ruler.push("dictionary_list_rows", (state) => {
|
|
24
|
+
const output = [],
|
|
25
|
+
lists = [];
|
|
26
|
+
const row = (nesting) => {
|
|
27
|
+
const token = new state.Token(
|
|
28
|
+
nesting === 1 ? "dictionary_row_open" : "dictionary_row_close",
|
|
29
|
+
"div",
|
|
30
|
+
nesting,
|
|
31
|
+
);
|
|
32
|
+
token.block = true;
|
|
33
|
+
output.push(token);
|
|
34
|
+
};
|
|
35
|
+
for (const token of state.tokens) {
|
|
36
|
+
if (token.type === "dl_open") {
|
|
37
|
+
if (!(token.attrGet("class") ?? "").split(/\s+/).includes("dictionary"))
|
|
38
|
+
token.attrJoin("class", "dictionary");
|
|
39
|
+
lists.push({ row: false });
|
|
40
|
+
} else if (token.type === "dt_open" && lists.length) {
|
|
41
|
+
const list = lists.at(-1);
|
|
42
|
+
if (list.row) row(-1);
|
|
43
|
+
row(1);
|
|
44
|
+
list.row = true;
|
|
45
|
+
} else if (token.type === "dd_open" && lists.length) {
|
|
46
|
+
// A loose definition begins with an inline first paragraph.
|
|
47
|
+
token.block = false;
|
|
48
|
+
} else if (token.type === "dt_close" && lists.length) {
|
|
49
|
+
// Inter-tag whitespace would add a word space to the fixed gap.
|
|
50
|
+
token.block = false;
|
|
51
|
+
} else if (token.type === "dl_close" && lists.length) {
|
|
52
|
+
if (lists.pop().row) row(-1);
|
|
53
|
+
}
|
|
54
|
+
output.push(token);
|
|
55
|
+
}
|
|
56
|
+
state.tokens = output;
|
|
57
|
+
});
|
|
58
|
+
}
|