@heroiclands/package-build 0.6.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG-content-build.md +965 -0
- package/CHANGELOG.md +68 -0
- package/CONTENT.md +824 -0
- package/MIGRATING.md +127 -0
- package/README.md +29 -17
- package/bin/content-build.mjs +1082 -0
- package/bin/package-build.mjs +2 -2
- package/bin/report.mjs +2 -2
- package/config.mjs +21 -18
- package/content-config.mjs +1424 -0
- package/coverage.mjs +1 -1
- package/e2e.mjs +1 -1
- package/engine/abbreviations.mjs +0 -0
- package/engine/base-compiler.mjs +608 -0
- package/engine/code-fences.mjs +311 -0
- package/engine/compendiums.mjs +353 -0
- package/engine/content-address.mjs +141 -0
- package/engine/content-links.mjs +438 -0
- package/engine/content-lint.mjs +204 -0
- package/engine/content-package.mjs +67 -0
- package/engine/content-slug.mjs +169 -0
- package/engine/content-tables.mjs +1216 -0
- package/engine/content-tree.mjs +56 -0
- package/engine/diagnostics.mjs +270 -0
- package/engine/field-reference.mjs +242 -0
- package/engine/field-spec.mjs +218 -0
- package/engine/foreign-catalog.mjs +450 -0
- package/engine/foreign-manifests.mjs +129 -0
- package/engine/frontmatter-lint.mjs +343 -0
- package/engine/frontmatter.mjs +258 -0
- package/engine/generate.mjs +305 -0
- package/engine/helpers.mjs +809 -0
- package/engine/ids.mjs +192 -0
- package/engine/index.mjs +142 -0
- package/engine/item-docs.mjs +153 -0
- package/engine/item-registry.mjs +129 -0
- package/engine/journals.mjs +374 -0
- package/engine/kb-manifest.mjs +473 -0
- package/engine/macros.mjs +333 -0
- package/engine/manifest-emit.mjs +339 -0
- package/engine/map-notes.mjs +1302 -0
- package/engine/pack-config.mjs +479 -0
- package/engine/pack-router.mjs +254 -0
- package/engine/prose-config.mjs +238 -0
- package/engine/prose-lint.mjs +294 -0
- package/engine/region-events.mjs +79 -0
- package/engine/scene-levels.mjs +420 -0
- package/engine/scenes.mjs +528 -0
- package/engine/site-build.mjs +778 -0
- package/engine/site-index.mjs +332 -0
- package/engine/web-wikilinks.mjs +330 -0
- package/engine/wikilink-syntax.mjs +105 -0
- package/engine/wikilinks.mjs +645 -0
- package/index.mjs +42 -25
- package/lang.mjs +1 -1
- package/manifest.mjs +79 -5
- package/markdownlint-config.mjs +45 -0
- package/package.json +78 -35
- package/prettier-config.mjs +34 -0
- package/sohl/actors.mjs +581 -0
- package/sohl/affiliation-standings.mjs +44 -0
- package/sohl/being-info.mjs +173 -0
- package/sohl/default-item-art.mjs +82 -0
- package/sohl/index.mjs +55 -0
- package/sohl/item-builders.mjs +114 -0
- package/sohl/item-fields.mjs +867 -0
- package/sohl/items.mjs +197 -0
- package/sohl/kb-passes.mjs +207 -0
- package/sohl/note-schemas.mjs +322 -0
- package/templates.mjs +1 -1
- package/types/config.d.mts +1 -1
- package/types/content-config.d.mts +571 -0
- package/types/engine/abbreviations.d.mts +44 -0
- package/types/engine/base-compiler.d.mts +320 -0
- package/types/engine/code-fences.d.mts +102 -0
- package/types/engine/compendiums.d.mts +77 -0
- package/types/engine/content-address.d.mts +61 -0
- package/types/engine/content-links.d.mts +87 -0
- package/types/engine/content-lint.d.mts +47 -0
- package/types/engine/content-package.d.mts +36 -0
- package/types/engine/content-slug.d.mts +59 -0
- package/types/engine/content-tables.d.mts +131 -0
- package/types/engine/content-tree.d.mts +12 -0
- package/types/engine/diagnostics.d.mts +152 -0
- package/types/engine/field-reference.d.mts +20 -0
- package/types/engine/field-spec.d.mts +181 -0
- package/types/engine/foreign-catalog.d.mts +103 -0
- package/types/engine/foreign-manifests.d.mts +43 -0
- package/types/engine/frontmatter-lint.d.mts +74 -0
- package/types/engine/frontmatter.d.mts +99 -0
- package/types/engine/generate.d.mts +56 -0
- package/types/engine/helpers.d.mts +348 -0
- package/types/engine/ids.d.mts +126 -0
- package/types/engine/index.d.mts +33 -0
- package/types/engine/item-docs.d.mts +73 -0
- package/types/engine/item-registry.d.mts +57 -0
- package/types/engine/journals.d.mts +121 -0
- package/types/engine/kb-manifest.d.mts +214 -0
- package/types/engine/macros.d.mts +201 -0
- package/types/engine/manifest-emit.d.mts +137 -0
- package/types/engine/map-notes.d.mts +283 -0
- package/types/engine/pack-config.d.mts +77 -0
- package/types/engine/pack-router.d.mts +53 -0
- package/types/engine/prose-config.d.mts +121 -0
- package/types/engine/prose-lint.d.mts +59 -0
- package/types/engine/region-events.d.mts +43 -0
- package/types/engine/scene-levels.d.mts +36 -0
- package/types/engine/scenes.d.mts +48 -0
- package/types/engine/site-build.d.mts +214 -0
- package/types/engine/site-index.d.mts +141 -0
- package/types/engine/web-wikilinks.d.mts +67 -0
- package/types/engine/wikilink-syntax.d.mts +101 -0
- package/types/engine/wikilinks.d.mts +172 -0
- package/types/index.d.mts +3 -0
- package/types/manifest.d.mts +52 -4
- package/types/markdownlint-config.d.mts +8 -0
- package/types/prettier-config.d.mts +1 -0
- package/types/sohl/actors.d.mts +65 -0
- package/types/sohl/affiliation-standings.d.mts +22 -0
- package/types/sohl/being-info.d.mts +78 -0
- package/types/sohl/default-item-art.d.mts +28 -0
- package/types/sohl/index.d.mts +7 -0
- package/types/sohl/item-builders.d.mts +11 -0
- package/types/sohl/item-fields.d.mts +20 -0
- package/types/sohl/items.d.mts +19 -0
- package/types/sohl/kb-passes.d.mts +71 -0
- package/types/sohl/note-schemas.d.mts +7 -0
|
@@ -0,0 +1,254 @@
|
|
|
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
|
+
/**
|
|
15
|
+
* Which pack a note's document lands in, when a document type has more than
|
|
16
|
+
* one (#1566).
|
|
17
|
+
*
|
|
18
|
+
* A repository used to be able to ship exactly one pack per document type: the
|
|
19
|
+
* pipeline ran one compile pass per type and routed every note of that type
|
|
20
|
+
* into that pass's pack. Editorial grouping of same-type documents into
|
|
21
|
+
* separate compendiums is ordinary Foundry practice — "Core Spells" and
|
|
22
|
+
* "Expanded Spells" are two Item packs — and `sohl-kethira-basic` shipped
|
|
23
|
+
* three. Collapsing them is a breaking change for every existing world,
|
|
24
|
+
* because a compendium UUID carries its pack name
|
|
25
|
+
* (`Compendium.<pkg>.characteristics.Item.<id>`).
|
|
26
|
+
*
|
|
27
|
+
* **A note declares its pack; the configuration declares the default.** The
|
|
28
|
+
* routing key is one frontmatter field, `pack:`, naming a pack from the
|
|
29
|
+
* configured list — declarative and inspectable, so a maintainer reads where a
|
|
30
|
+
* note lands rather than tracing a predicate. Its `type:` still selects the
|
|
31
|
+
* *compiler*; the declaration selects *which pack of that type* receives the
|
|
32
|
+
* document, and the two are orthogonal.
|
|
33
|
+
*
|
|
34
|
+
* The rules, in full:
|
|
35
|
+
*
|
|
36
|
+
* - A note that declares nothing lands in the **default** pack of its document
|
|
37
|
+
* type. A type with exactly one pack is that type's default implicitly, which
|
|
38
|
+
* is what keeps every existing one-pack-per-type configuration — and every
|
|
39
|
+
* note in it — behaving identically.
|
|
40
|
+
* - A type with several packs designates its default with `default: true`.
|
|
41
|
+
* Where none does, a declaration is **mandatory** for every note of that type:
|
|
42
|
+
* an undeclared note routes nowhere and fails the build.
|
|
43
|
+
* - A declared name that no pack answers to — or that names a pack holding
|
|
44
|
+
* another document type, or a companion pack, which no note may address —
|
|
45
|
+
* fails the build, naming the note and what it asked for. A silent fall-back
|
|
46
|
+
* to the default would be #1502 in a new costume.
|
|
47
|
+
* - A note's **derived** documents are routed by the default of *their* type,
|
|
48
|
+
* not by the note's declaration: an item note's prose compiles into a
|
|
49
|
+
* JournalEntry, and `pack:` names where the *item* goes.
|
|
50
|
+
*
|
|
51
|
+
* @module
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import { packForType } from "./ids.mjs";
|
|
55
|
+
import { loadPackConfig } from "./pack-config.mjs";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A note that cannot be routed to a pack. Thrown rather than returned so no
|
|
59
|
+
* caller can carry on with a plausible-looking default.
|
|
60
|
+
*/
|
|
61
|
+
export class PackRoutingError extends Error {
|
|
62
|
+
/** @param {string} message */
|
|
63
|
+
constructor(message) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "PackRoutingError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The frontmatter field a note declares its pack in.
|
|
71
|
+
*
|
|
72
|
+
* Deliberately close to `package:` and deliberately not the same word: a note's
|
|
73
|
+
* `package:` says which *distribution* owns it, `pack:` which *compendium*
|
|
74
|
+
* receives its document.
|
|
75
|
+
*/
|
|
76
|
+
export const PACK_FIELD = "pack";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build the router for one configured pack list.
|
|
80
|
+
*
|
|
81
|
+
* Pure — it reads the list and nothing else, so a consumer's routing can be
|
|
82
|
+
* tested without a content tree or a config file on disk.
|
|
83
|
+
*
|
|
84
|
+
* @param {readonly object[]} packs - The resolved `packs` list from
|
|
85
|
+
* `defineConfig`.
|
|
86
|
+
* @returns {{resolve: (fm: object, docType: string) => string,
|
|
87
|
+
* resolveOrNull: (fm: object, docType: string) => string|undefined,
|
|
88
|
+
* packsOfType: (docType: string) => string[],
|
|
89
|
+
* defaultOf: (docType: string) => string|undefined}} The router.
|
|
90
|
+
*/
|
|
91
|
+
export function createPackRouter(packs) {
|
|
92
|
+
/** Routable packs — a companion is written by its parent's pass. */
|
|
93
|
+
const byName = new Map();
|
|
94
|
+
/** Companion names, so addressing one can be refused by name. */
|
|
95
|
+
const companions = new Set();
|
|
96
|
+
/** @type {Map<string, string[]>} */
|
|
97
|
+
const byType = new Map();
|
|
98
|
+
/** @type {Map<string, string>} */
|
|
99
|
+
const defaults = new Map();
|
|
100
|
+
|
|
101
|
+
for (const pack of packs ?? []) {
|
|
102
|
+
byName.set(pack.name, pack);
|
|
103
|
+
const names = byType.get(pack.type) ?? [];
|
|
104
|
+
names.push(pack.name);
|
|
105
|
+
byType.set(pack.type, names);
|
|
106
|
+
if (pack.default) defaults.set(pack.type, pack.name);
|
|
107
|
+
for (const companion of pack.companions ?? []) {
|
|
108
|
+
companions.add(companion.name);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// A type with exactly one pack needs no `default: true` to have one: the
|
|
112
|
+
// single-pack layout is the common case, and requiring the flag there would
|
|
113
|
+
// make every existing configuration invalid for no gain.
|
|
114
|
+
for (const [type, names] of byType) {
|
|
115
|
+
if (names.length === 1) defaults.set(type, names[0]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @param {object} fm */
|
|
119
|
+
const noteLabel = (fm) =>
|
|
120
|
+
fm?.name?.full ?? fm?.shortcode ?? fm?.id ?? "a note";
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The pack one pass should write this note's document to.
|
|
124
|
+
*
|
|
125
|
+
* @param {object} fm - The note's frontmatter.
|
|
126
|
+
* @param {string} docType - The document type the calling pass writes.
|
|
127
|
+
* @returns {string} The pack name.
|
|
128
|
+
* @throws {PackRoutingError} When the note routes nowhere.
|
|
129
|
+
*/
|
|
130
|
+
function resolve(fm, docType) {
|
|
131
|
+
const declared = fm?.[PACK_FIELD];
|
|
132
|
+
// The declaration names where the note's *own* document goes. A pass
|
|
133
|
+
// writing a document derived from it — an item's prose becoming a
|
|
134
|
+
// JournalEntry — is not what the author was addressing.
|
|
135
|
+
const ownDocType = packForType(fm?.type).docType;
|
|
136
|
+
|
|
137
|
+
if (declared != null && declared !== "" && docType === ownDocType) {
|
|
138
|
+
if (companions.has(declared)) {
|
|
139
|
+
throw new PackRoutingError(
|
|
140
|
+
`${noteLabel(fm)} declares \`pack: ${declared}\`, which is a ` +
|
|
141
|
+
`companion pack. A companion is written by another pack's ` +
|
|
142
|
+
`pass, so no note may be routed into one.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const pack = byName.get(declared);
|
|
146
|
+
if (!pack) {
|
|
147
|
+
throw new PackRoutingError(
|
|
148
|
+
`${noteLabel(fm)} declares \`pack: ${declared}\`, which no ` +
|
|
149
|
+
`configured pack answers to. Declare it in ` +
|
|
150
|
+
`package-build.config.yaml, or correct the note. ` +
|
|
151
|
+
`Packs of type ${docType}: ` +
|
|
152
|
+
`${(byType.get(docType) ?? []).join(", ") || "(none)"}.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (pack.type !== docType) {
|
|
156
|
+
throw new PackRoutingError(
|
|
157
|
+
`${noteLabel(fm)} is a ${docType} but declares ` +
|
|
158
|
+
`\`pack: ${declared}\`, which holds ${pack.type} ` +
|
|
159
|
+
`documents. A note's \`pack:\` names a pack of its own ` +
|
|
160
|
+
`document type.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return declared;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const fallback = defaults.get(docType);
|
|
167
|
+
if (!fallback) {
|
|
168
|
+
const candidates = byType.get(docType) ?? [];
|
|
169
|
+
throw new PackRoutingError(
|
|
170
|
+
candidates.length ?
|
|
171
|
+
`${noteLabel(fm)} declares no \`pack:\`, and no ${docType} ` +
|
|
172
|
+
`pack is marked \`default: true\` — so it routes ` +
|
|
173
|
+
`nowhere. Mark one of ${candidates.join(", ")} as the ` +
|
|
174
|
+
`default, or declare the pack on the note.`
|
|
175
|
+
: `${noteLabel(fm)} compiles into a ${docType}, but no pack ` +
|
|
176
|
+
`of that type is configured.`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return fallback;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
resolve,
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* {@link resolve} for a caller that must not fail the build — the link
|
|
187
|
+
* index, which addresses every note it can and leaves the compilers to
|
|
188
|
+
* report the ones it cannot.
|
|
189
|
+
*
|
|
190
|
+
* @param {object} fm - The note's frontmatter.
|
|
191
|
+
* @param {string} docType - The document type being addressed.
|
|
192
|
+
* @returns {string|undefined} The pack name, or `undefined`.
|
|
193
|
+
*/
|
|
194
|
+
resolveOrNull(fm, docType) {
|
|
195
|
+
try {
|
|
196
|
+
return resolve(fm, docType);
|
|
197
|
+
} catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Every routable pack of a document type, in configured order.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} docType - The Foundry document type.
|
|
206
|
+
* @returns {string[]} The pack names.
|
|
207
|
+
*/
|
|
208
|
+
packsOfType(docType) {
|
|
209
|
+
return [...(byType.get(docType) ?? [])];
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The pack of a type that receives notes declaring none.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} docType - The Foundry document type.
|
|
216
|
+
* @returns {string|undefined} The pack name, or `undefined` when the
|
|
217
|
+
* type has several packs and none is marked default.
|
|
218
|
+
*/
|
|
219
|
+
defaultOf(docType) {
|
|
220
|
+
return defaults.get(docType);
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const routers = new WeakMap();
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The router for a resolved configuration, built once per configuration.
|
|
229
|
+
*
|
|
230
|
+
* @param {object} config - A configuration from `defineConfig`.
|
|
231
|
+
* @returns {ReturnType<typeof createPackRouter>} Its router.
|
|
232
|
+
*/
|
|
233
|
+
export function routerFor(config) {
|
|
234
|
+
let router = routers.get(config);
|
|
235
|
+
if (!router) {
|
|
236
|
+
router = createPackRouter(config.packs);
|
|
237
|
+
routers.set(config, router);
|
|
238
|
+
}
|
|
239
|
+
return router;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The consuming repository's own router — what every module that emits a UUID
|
|
244
|
+
* asks where a note's document lives.
|
|
245
|
+
*
|
|
246
|
+
* An accessor rather than a hoisted constant, so that importing this module
|
|
247
|
+
* needs no configuration (#2). {@link routerFor} keeps one router per
|
|
248
|
+
* configuration object, so repeated calls return the same instance.
|
|
249
|
+
*
|
|
250
|
+
* @returns {ReturnType<typeof createPackRouter>} This repository's router.
|
|
251
|
+
*/
|
|
252
|
+
export function packRouter() {
|
|
253
|
+
return routerFor(loadPackConfig());
|
|
254
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
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
|
+
/**
|
|
15
|
+
* The prose conventions every content repository writes to — one Prettier
|
|
16
|
+
* configuration and one markdownlint rule set, declared here so a note
|
|
17
|
+
* formatted in one repository is formatted the same way in the next (#69).
|
|
18
|
+
*
|
|
19
|
+
* These used to exist in exactly one consumer. The SoHL repository carried
|
|
20
|
+
* both; `sohl-thalorna` had Prettier but never ran it from `lint`; and
|
|
21
|
+
* `sohl-kethira-basic` had neither, so the package least likely to have been
|
|
22
|
+
* proofread was checked for addresses and nothing else. A rule set copied into
|
|
23
|
+
* three repositories is one rule with three implementations, which is the drift
|
|
24
|
+
* #20 exists to remove — so it is declared once, here, and every consumer
|
|
25
|
+
* invokes it.
|
|
26
|
+
*
|
|
27
|
+
* **Neither of these is an override.** A consumer that declares its own
|
|
28
|
+
* Prettier config or its own `.markdownlint-cli2.jsonc` wins; what ships here
|
|
29
|
+
* is the default a repository gets for declaring nothing. Repository *layout*
|
|
30
|
+
* knowledge — which paths to skip — stays with the repository that has the
|
|
31
|
+
* layout, in its own ignore files.
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The Prettier options every content repository shares, before any per-language
|
|
38
|
+
* adjustment.
|
|
39
|
+
*
|
|
40
|
+
* The values are not arbitrary: they are the ones the SoHL repository has
|
|
41
|
+
* always used, kept identical here so a module or a note moving between
|
|
42
|
+
* repositories does not reformat on arrival. Changing one of these reformats
|
|
43
|
+
* every consumer, so treat it as a breaking change to the shared tree rather
|
|
44
|
+
* than a preference.
|
|
45
|
+
*
|
|
46
|
+
* @type {Readonly<object>}
|
|
47
|
+
*/
|
|
48
|
+
export const PRETTIER_BASE = Object.freeze({
|
|
49
|
+
printWidth: 80,
|
|
50
|
+
tabWidth: 4,
|
|
51
|
+
useTabs: false,
|
|
52
|
+
semi: true,
|
|
53
|
+
singleQuote: false,
|
|
54
|
+
quoteProps: "as-needed",
|
|
55
|
+
trailingComma: "all",
|
|
56
|
+
bracketSpacing: true,
|
|
57
|
+
bracketSameLine: true,
|
|
58
|
+
arrowParens: "always",
|
|
59
|
+
endOfLine: "lf",
|
|
60
|
+
experimentalTernaries: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What markdown gets on top of {@link PRETTIER_BASE}.
|
|
65
|
+
*
|
|
66
|
+
* Markdown indents at 2, not the global 4. Notes are the thing several
|
|
67
|
+
* repositories exchange, so their indentation is the one value that most needs
|
|
68
|
+
* to be the same everywhere — a note's YAML frontmatter is nested lists, and at
|
|
69
|
+
* 4 every note reindents away from the form it was written in.
|
|
70
|
+
*
|
|
71
|
+
* **Declared apart from the `overrides` block, not derived from it.** Prettier
|
|
72
|
+
* applies `overrides` only while resolving a config *file*; options handed to
|
|
73
|
+
* it directly keep the global values, so a consumer with no config of its own
|
|
74
|
+
* silently got markdown at 4 (#76). The runner needs the adjustment as data it
|
|
75
|
+
* can apply itself, and {@link PRETTIER_CONFIG} composes the same values into
|
|
76
|
+
* the shape a config file wants — one source, two presentations.
|
|
77
|
+
*
|
|
78
|
+
* @type {Readonly<object>}
|
|
79
|
+
*/
|
|
80
|
+
export const PRETTIER_MARKDOWN = Object.freeze({ tabWidth: 2 });
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The shared configuration in the shape a Prettier **config file** takes.
|
|
84
|
+
*
|
|
85
|
+
* This is what a consumer's `prettier.config.mjs` re-exports, and it is the
|
|
86
|
+
* form in which the markdown adjustment works: resolved from the consumer's own
|
|
87
|
+
* root, `**\/*.md` matches that repository's markdown. Shipped from inside
|
|
88
|
+
* `node_modules` it would match nothing, because Prettier resolves an
|
|
89
|
+
* override's glob relative to the config file's own directory — which is why
|
|
90
|
+
* the runner applies {@link PRETTIER_MARKDOWN} itself rather than pointing
|
|
91
|
+
* Prettier at this file.
|
|
92
|
+
*
|
|
93
|
+
* @type {Readonly<object>}
|
|
94
|
+
*/
|
|
95
|
+
export const PRETTIER_CONFIG = Object.freeze({
|
|
96
|
+
...PRETTIER_BASE,
|
|
97
|
+
overrides: Object.freeze([
|
|
98
|
+
Object.freeze({
|
|
99
|
+
files: "**/*.md",
|
|
100
|
+
options: PRETTIER_MARKDOWN,
|
|
101
|
+
}),
|
|
102
|
+
]),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The shared options for one file, with the per-language adjustment applied.
|
|
107
|
+
*
|
|
108
|
+
* What a consumer's own Prettier config would have produced, for a repository
|
|
109
|
+
* that declares none.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} file - Path of the file about to be formatted.
|
|
112
|
+
* @returns {object} Options to hand Prettier directly. Never carries
|
|
113
|
+
* `overrides`: passing that inline is what silently did nothing (#76).
|
|
114
|
+
*/
|
|
115
|
+
export function sharedPrettierOptionsFor(file) {
|
|
116
|
+
return /\.md$/i.test(file) ?
|
|
117
|
+
{ ...PRETTIER_BASE, ...PRETTIER_MARKDOWN }
|
|
118
|
+
: { ...PRETTIER_BASE };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The markdownlint rules — the structural checks Prettier cannot make.
|
|
123
|
+
*
|
|
124
|
+
* Prettier already formats every hand-written `.md` file, and it is indifferent
|
|
125
|
+
* to structure: it will happily reformat a document whose heading levels skip
|
|
126
|
+
* from h3 to h5, whose two sibling sections claim the same anchor, or whose
|
|
127
|
+
* link is `(text)[url]`. Those are the defects this set is for.
|
|
128
|
+
*
|
|
129
|
+
* **THE RULE SET IS DELIBERATELY NARROW, and stays that way on purpose.**
|
|
130
|
+
* Turning on markdownlint's defaults over a content tree produces tens of
|
|
131
|
+
* thousands of findings, almost all of them line length, list indentation and
|
|
132
|
+
* blank-line placement — Prettier's territory, or a second formatter's taste
|
|
133
|
+
* imposed on prose it already owns. So `default` is off and each rule below is
|
|
134
|
+
* enabled by name, with the reason it earns its place. Add a rule only if it
|
|
135
|
+
* can report that a page is *wrong*.
|
|
136
|
+
*
|
|
137
|
+
* @type {Readonly<object>}
|
|
138
|
+
*/
|
|
139
|
+
export const MARKDOWNLINT_CONFIG = Object.freeze({
|
|
140
|
+
default: false,
|
|
141
|
+
|
|
142
|
+
/* ── Document structure ─────────────────────────────────────────────── */
|
|
143
|
+
|
|
144
|
+
// MD001 — heading levels increment by one. A skipped level (h3 → h5) breaks
|
|
145
|
+
// the outline every consumer derives from it: a knowledgebase's on-page
|
|
146
|
+
// table of contents, a section nav, and the reading order of a page nobody
|
|
147
|
+
// re-reads top to bottom.
|
|
148
|
+
MD001: true,
|
|
149
|
+
|
|
150
|
+
// MD024 — no duplicate sibling headings. Two identical headings under one
|
|
151
|
+
// parent generate two identical anchors and the second is unreachable, so a
|
|
152
|
+
// `#fragment` link silently lands on the wrong section. Content notes link
|
|
153
|
+
// by anchor everywhere and this package validates those links, which cannot
|
|
154
|
+
// help when the anchor exists but points at the wrong one of two.
|
|
155
|
+
//
|
|
156
|
+
// `siblings_only` is what makes the rule usable: repeating "## Notes" under
|
|
157
|
+
// each of several parents is a normal shape for a reference page, and only
|
|
158
|
+
// a repeat within one parent is ambiguous.
|
|
159
|
+
MD024: Object.freeze({ siblings_only: true }),
|
|
160
|
+
|
|
161
|
+
// MD056 — every table row has the column count its header declares. A row
|
|
162
|
+
// with too few or too many cells renders as a broken table, and the failure
|
|
163
|
+
// is invisible in the source.
|
|
164
|
+
MD056: true,
|
|
165
|
+
|
|
166
|
+
/* ── Links that do not link ─────────────────────────────────────────── */
|
|
167
|
+
|
|
168
|
+
// MD011 — reversed link syntax: `(text)[url]` renders as literal text with
|
|
169
|
+
// no link at all. A pure typo class, and easy to miss in review.
|
|
170
|
+
MD011: true,
|
|
171
|
+
|
|
172
|
+
// MD034 — no bare URLs. Whether a bare URL becomes a link depends on the
|
|
173
|
+
// renderer, and this content is rendered by three of them (Hugo/goldmark
|
|
174
|
+
// for a knowledgebase, Foundry's own markdown for compendium journals, and
|
|
175
|
+
// GitHub for the repository pages). Writing the link explicitly is the only
|
|
176
|
+
// form that renders the same everywhere.
|
|
177
|
+
MD034: true,
|
|
178
|
+
|
|
179
|
+
// MD039 — no spaces inside link text. `[ Skills ](url)` carries the spaces
|
|
180
|
+
// into the rendered anchor, which shows up as a stray underline and as a
|
|
181
|
+
// mismatched link title in any index built from it.
|
|
182
|
+
MD039: true,
|
|
183
|
+
|
|
184
|
+
// MD042 — no empty links. `[Skills]()` renders as a dead anchor that looks
|
|
185
|
+
// exactly like a working one.
|
|
186
|
+
MD042: true,
|
|
187
|
+
|
|
188
|
+
// MD052 / MD053 — reference links resolve, and definitions are used. An
|
|
189
|
+
// undefined reference renders as literal `[text][ref]` brackets; an orphaned
|
|
190
|
+
// definition is a link someone meant to make and did not.
|
|
191
|
+
MD052: true,
|
|
192
|
+
MD053: true,
|
|
193
|
+
|
|
194
|
+
/* ── Emphasis markers ───────────────────────────────────────────────── */
|
|
195
|
+
|
|
196
|
+
// MD049 / MD050 — one marker for each kind of emphasis: `_emphasis_` and
|
|
197
|
+
// `**strong**`. Both are style rules rather than correctness ones, and both
|
|
198
|
+
// are already satisfied wherever Prettier runs, because Prettier normalises
|
|
199
|
+
// to exactly this pair. That is the point of enabling them: the convention
|
|
200
|
+
// otherwise holds as a *side effect* of the formatter's default, so it would
|
|
201
|
+
// lapse silently if that default changed or a path were added to an ignore
|
|
202
|
+
// file. Stated here, it is a decision rather than an accident — and it is
|
|
203
|
+
// the one rule a repository with no Prettier in its lint chain was missing
|
|
204
|
+
// entirely.
|
|
205
|
+
//
|
|
206
|
+
// Note for anyone reading rendered output: neither marker is an underline.
|
|
207
|
+
// Markdown has no underline; `_x_` and `*x*` both mean emphasis, and the
|
|
208
|
+
// choice between them is purely which one these repositories write.
|
|
209
|
+
MD049: Object.freeze({ style: "underscore" }),
|
|
210
|
+
MD050: Object.freeze({ style: "asterisk" }),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The globs `content-build markdown` checks when a consumer names no paths.
|
|
215
|
+
*
|
|
216
|
+
* Every markdown file the repository tracks, which is what a consumer means by
|
|
217
|
+
* "lint my markdown". What to *skip* is the repository's own business and comes
|
|
218
|
+
* from its `.gitignore` (honoured by default) and its own configuration.
|
|
219
|
+
*
|
|
220
|
+
* @type {readonly string[]}
|
|
221
|
+
*/
|
|
222
|
+
export const MARKDOWN_GLOBS = Object.freeze(["**/*.md"]);
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Markdown every consumer has and nobody hand-writes.
|
|
226
|
+
*
|
|
227
|
+
* `CHANGELOG.md` is assembled by `changeset version` from the `.changeset/*.md`
|
|
228
|
+
* bodies on every release. It is not in anyone's `.gitignore` — it is
|
|
229
|
+
* committed — so nothing else excludes it, and linting it reports on the
|
|
230
|
+
* generator: the findings are real (a heading level changesets chose, an
|
|
231
|
+
* asterisk it emitted) and unfixable, because the next release rewrites the
|
|
232
|
+
* file. Every repository here releases through changesets, so this is a fact
|
|
233
|
+
* about the shared toolchain rather than any one repository's layout, and it
|
|
234
|
+
* belongs in the default instead of being rediscovered three times.
|
|
235
|
+
*
|
|
236
|
+
* @type {readonly string[]}
|
|
237
|
+
*/
|
|
238
|
+
export const MARKDOWN_IGNORES = Object.freeze(["CHANGELOG.md"]);
|