@heroiclands/package-build 20.3.1 → 20.5.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 +351 -0
- package/CONTENT.md +322 -110
- package/README.md +21 -10
- package/bin/content-build.mjs +204 -92
- package/bin/package-build.mjs +48 -15
- package/config.mjs +6 -8
- package/content-config.mjs +518 -151
- package/docs/api.md +1352 -0
- package/docs/commands.md +1609 -0
- package/docs/configuration.md +1432 -0
- package/docs/content-format.md +73 -74
- package/docs/diagnostics.md +356 -0
- package/docs/getting-started.md +813 -0
- package/docs/project-setup.md +469 -0
- package/e2e.mjs +4 -4
- package/engine/actor-compiler.mjs +64 -61
- package/engine/address-charset.mjs +6 -6
- package/engine/address-diff.mjs +57 -52
- package/engine/anchored-sections.mjs +1 -1
- package/engine/anchors.mjs +1 -1
- package/engine/base-compiler.mjs +42 -38
- package/engine/bundle-notes.mjs +4 -4
- package/engine/bundles.mjs +17 -8
- package/engine/code-fences.mjs +3 -3
- package/engine/compendiums.mjs +8 -8
- package/engine/compile-corpus.mjs +4 -5
- package/engine/content-address.mjs +24 -24
- package/engine/content-charset.mjs +1 -1
- package/engine/content-format-check.mjs +24 -25
- package/engine/content-format.mjs +8 -8
- package/engine/content-html.mjs +202 -0
- package/engine/content-icons.mjs +196 -165
- package/engine/content-index.mjs +61 -40
- package/engine/content-links.mjs +74 -41
- package/engine/content-lint.mjs +64 -34
- package/engine/content-package.mjs +7 -9
- package/engine/content-slug.mjs +2 -2
- package/engine/content-tables.mjs +38 -34
- package/engine/content-tree.mjs +1 -1
- package/engine/derived-fields.mjs +174 -0
- package/engine/diagnostics.mjs +3 -4
- package/engine/document-subtypes.mjs +6 -7
- package/engine/field-reference.mjs +8 -8
- package/engine/field-spec.mjs +29 -30
- package/engine/folder-notes.mjs +18 -16
- package/engine/foreign-catalog.mjs +8 -9
- package/engine/foundry-entries.mjs +19 -20
- package/engine/frontmatter-lint.mjs +95 -103
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +44 -39
- package/engine/helpers.mjs +147 -84
- package/engine/homepage.mjs +20 -22
- package/engine/ids.mjs +15 -15
- package/engine/index-records.mjs +4 -4
- package/engine/index.mjs +26 -10
- package/engine/item-compiler.mjs +54 -29
- package/engine/item-docs.mjs +11 -13
- package/engine/item-registry.mjs +13 -13
- package/engine/journals.mjs +24 -15
- package/engine/macros.mjs +3 -3
- package/engine/map-notes.mjs +6 -6
- package/engine/metadata-index.mjs +5 -5
- package/engine/note-claims.mjs +51 -41
- package/engine/note-ids.mjs +5 -5
- package/engine/note-package.mjs +4 -4
- package/engine/note-renames.mjs +3 -3
- package/engine/note-schemas.mjs +3 -8
- package/engine/note-vocabulary.mjs +56 -54
- package/engine/pack-config.mjs +39 -25
- package/engine/pack-router.mjs +0 -0
- package/engine/pdf-build.mjs +464 -0
- package/engine/pdf-fonts.mjs +420 -0
- package/engine/pdf-render.mjs +798 -0
- package/engine/pdf-toc.mjs +525 -0
- package/engine/prose-config.mjs +9 -9
- package/engine/prose-lint.mjs +12 -13
- package/engine/region-events.mjs +2 -2
- package/engine/retired-fields.mjs +24 -22
- package/engine/runtime-only-fields.mjs +1 -1
- package/engine/scene-levels.mjs +1 -1
- package/engine/scenes.mjs +33 -25
- package/engine/schema-check.mjs +21 -21
- package/engine/schema-extract.mjs +1 -1
- package/engine/site-build.mjs +66 -50
- package/engine/site-index.mjs +17 -18
- package/engine/sql-tables.mjs +21 -13
- package/engine/subtype-registry.mjs +5 -4
- package/engine/system-block.mjs +18 -19
- package/engine/systems.mjs +2 -2
- package/engine/web-wikilinks.mjs +41 -38
- package/engine/wikilink-syntax.mjs +16 -16
- package/engine/wikilinks.mjs +40 -42
- package/hm3/actors.mjs +30 -22
- package/hm3/document-subtypes.mjs +5 -5
- package/hm3/index.mjs +2 -2
- package/hm3/item-builders.mjs +1 -1
- package/hm3/item-fields.mjs +3 -3
- package/hm3/items.mjs +28 -8
- package/hm3/template-priority.mjs +2 -2
- package/lang.mjs +3 -3
- package/manifest.mjs +17 -20
- package/package.json +1 -2
- package/release.mjs +66 -6
- package/sohl/actors.mjs +36 -28
- package/sohl/affiliation-standings.mjs +2 -2
- package/sohl/being-info.mjs +5 -5
- package/sohl/default-item-art.mjs +5 -5
- package/sohl/document-subtypes.mjs +5 -5
- package/sohl/index.mjs +3 -3
- package/sohl/item-builders.mjs +7 -7
- package/sohl/item-fields.mjs +8 -9
- package/sohl/items.mjs +28 -6
- package/sohl/kb-passes.mjs +5 -5
- package/sohl/note-schemas.mjs +12 -12
- package/sohl/skill-base.mjs +1 -1
- package/types/content-config.d.mts +91 -39
- package/types/e2e.d.mts +3 -3
- package/types/engine/actor-compiler.d.mts +45 -40
- package/types/engine/address-charset.d.mts +6 -6
- package/types/engine/address-diff.d.mts +60 -6
- package/types/engine/base-compiler.d.mts +27 -21
- package/types/engine/bundle-notes.d.mts +3 -3
- package/types/engine/bundles.d.mts +10 -1
- package/types/engine/code-fences.d.mts +3 -3
- package/types/engine/compendiums.d.mts +3 -3
- package/types/engine/compile-corpus.d.mts +1 -1
- package/types/engine/content-address.d.mts +20 -20
- package/types/engine/content-format-check.d.mts +6 -6
- package/types/engine/content-format.d.mts +2 -2
- package/types/engine/content-html.d.mts +78 -0
- package/types/engine/content-icons.d.mts +139 -92
- package/types/engine/content-index.d.mts +59 -15
- package/types/engine/content-links.d.mts +8 -8
- package/types/engine/content-lint.d.mts +8 -6
- package/types/engine/content-package.d.mts +6 -8
- package/types/engine/content-tables.d.mts +49 -18
- package/types/engine/derived-fields.d.mts +101 -0
- package/types/engine/diagnostics.d.mts +2 -2
- package/types/engine/document-subtypes.d.mts +3 -3
- package/types/engine/field-spec.d.mts +37 -39
- package/types/engine/folder-notes.d.mts +4 -5
- package/types/engine/foreign-catalog.d.mts +3 -3
- package/types/engine/foundry-entries.d.mts +6 -7
- package/types/engine/frontmatter-lint.d.mts +10 -9
- package/types/engine/frontmatter.d.mts +7 -7
- package/types/engine/generate.d.mts +7 -7
- package/types/engine/helpers.d.mts +128 -56
- package/types/engine/homepage.d.mts +16 -18
- package/types/engine/ids.d.mts +13 -13
- package/types/engine/index-records.d.mts +3 -3
- package/types/engine/index.d.mts +6 -0
- package/types/engine/item-compiler.d.mts +21 -5
- package/types/engine/item-docs.d.mts +2 -2
- package/types/engine/item-registry.d.mts +6 -6
- package/types/engine/journals.d.mts +12 -3
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/metadata-index.d.mts +4 -4
- package/types/engine/note-claims.d.mts +30 -22
- package/types/engine/note-ids.d.mts +4 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-renames.d.mts +3 -3
- package/types/engine/note-vocabulary.d.mts +31 -203
- package/types/engine/pack-config.d.mts +7 -7
- package/types/engine/pack-router.d.mts +1 -1
- package/types/engine/pdf-build.d.mts +42 -0
- package/types/engine/pdf-fonts.d.mts +30 -0
- package/types/engine/pdf-render.d.mts +144 -0
- package/types/engine/pdf-toc.d.mts +114 -0
- package/types/engine/prose-config.d.mts +9 -9
- package/types/engine/prose-lint.d.mts +3 -4
- package/types/engine/region-events.d.mts +2 -2
- package/types/engine/retired-fields.d.mts +10 -9
- package/types/engine/scenes.d.mts +10 -1
- package/types/engine/schema-check.d.mts +13 -13
- package/types/engine/site-build.d.mts +52 -25
- package/types/engine/site-index.d.mts +3 -4
- package/types/engine/sql-tables.d.mts +11 -5
- package/types/engine/subtype-registry.d.mts +3 -3
- package/types/engine/system-block.d.mts +3 -3
- package/types/engine/web-wikilinks.d.mts +7 -7
- package/types/engine/wikilink-syntax.d.mts +17 -17
- package/types/engine/wikilinks.d.mts +14 -14
- package/types/hm3/actors.d.mts +9 -1
- package/types/hm3/document-subtypes.d.mts +1 -1
- package/types/hm3/items.d.mts +9 -2
- package/types/hm3/template-priority.d.mts +1 -1
- package/types/manifest.d.mts +8 -8
- package/types/release.d.mts +15 -4
- package/types/sohl/actors.d.mts +12 -4
- package/types/sohl/affiliation-standings.d.mts +2 -2
- package/types/sohl/being-info.d.mts +2 -2
- package/types/sohl/document-subtypes.d.mts +1 -1
- package/types/sohl/items.d.mts +9 -2
- package/types/sohl/note-schemas.d.mts +1 -1
- package/MIGRATING.md +0 -608
|
@@ -0,0 +1,525 @@
|
|
|
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 document tree a PDF is built from, and the plan it resolves to (#316).
|
|
16
|
+
*
|
|
17
|
+
* The packs and the website both render the *whole* content tree: every note
|
|
18
|
+
* becomes a document and a page, and the three surfaces agreeing about what the
|
|
19
|
+
* content is, is the point. **A book is not that.** It is a selection — a
|
|
20
|
+
* declared structure whose leaves pick notes out of the corpus by a `WHERE`
|
|
21
|
+
* clause, interleaved with prose that may not live in the content tree at all.
|
|
22
|
+
*
|
|
23
|
+
* Three consequences follow, and they are behaviour rather than oversights:
|
|
24
|
+
*
|
|
25
|
+
* - **A note no clause selects is not in the book.** A project decides what its
|
|
26
|
+
* own volume carries, so an omission is an editorial act and this pass has no
|
|
27
|
+
* standing to call it a defect.
|
|
28
|
+
* - **A note several clauses select appears several times.** A reference work
|
|
29
|
+
* may legitimately carry an entry under more than one heading, so each
|
|
30
|
+
* occurrence is its own page, its own outline node and its own anchor — and
|
|
31
|
+
* inbound links are pointed at the first, so a wikilink resolves to one place
|
|
32
|
+
* however often the book prints it.
|
|
33
|
+
* - **Prose from `file:` is in the book but not of the tree.** It carries no
|
|
34
|
+
* address, so nothing can link *to* it, but it is anchored for the table of
|
|
35
|
+
* contents and its own links still resolve.
|
|
36
|
+
*
|
|
37
|
+
* **What is here is pure.** The tree is validated, flattened and planned without
|
|
38
|
+
* reading a file or opening a database: the filters are run by the caller and
|
|
39
|
+
* handed back as `selections`, the way `expandContentTables` is handed its
|
|
40
|
+
* `sqlTables` because DuckDB is async and the pass is not. That is what lets the
|
|
41
|
+
* whole structure of a 2,500-entry book be asserted in a unit test with no
|
|
42
|
+
* renderer present, which is most of what #316 asks for.
|
|
43
|
+
*
|
|
44
|
+
* **Every gate reports; none exits.** A filter that will not parse, a `file:`
|
|
45
|
+
* that resolves nowhere and a filter reaching another package's schema are
|
|
46
|
+
* findings the command decides about, as everywhere else in this engine.
|
|
47
|
+
*
|
|
48
|
+
* @module
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { slugify } from "./content-slug.mjs";
|
|
52
|
+
import { positionOfYamlPath } from "./diagnostics.mjs";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Presentation a node may declare, and that its descendants inherit.
|
|
56
|
+
*
|
|
57
|
+
* Reserved now although the first release renders none of them, because the
|
|
58
|
+
* shape of the file is the thing consumers commit to: a book that has to be
|
|
59
|
+
* restructured to gain a running head has the wrong format, not the wrong
|
|
60
|
+
* renderer. Inheritance is what makes them worth declaring at all — `Gear` says
|
|
61
|
+
* once which infobox its entries use, and nine sections beneath it agree.
|
|
62
|
+
*
|
|
63
|
+
* @type {readonly string[]}
|
|
64
|
+
*/
|
|
65
|
+
export const PRESENTATION_KEYS = Object.freeze(["header", "footer", "infobox", "page"]);
|
|
66
|
+
|
|
67
|
+
/** Keys a section node may carry. @type {readonly string[]} */
|
|
68
|
+
const SECTION_KEYS = Object.freeze(["sectionName", "contents", ...PRESENTATION_KEYS]);
|
|
69
|
+
|
|
70
|
+
/** Keys a content entry may carry instead of being a section. */
|
|
71
|
+
const LEAF_KEYS = Object.freeze(["file", "filter"]);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A qualified table name — `sohl.notes` rather than `notes`.
|
|
75
|
+
*
|
|
76
|
+
* The build owns the `SELECT … FROM notes`, so a filter cannot name a table at
|
|
77
|
+
* all; the one way back out of that is a subquery in the `WHERE` clause, which
|
|
78
|
+
* would reach a dependency's schema and put another package's notes in this
|
|
79
|
+
* package's book. Matched loosely on purpose: this refuses a shape rather than
|
|
80
|
+
* parsing SQL, and a false positive is a filter that has to be rewritten while a
|
|
81
|
+
* false negative is a book quietly containing someone else's content.
|
|
82
|
+
*/
|
|
83
|
+
const QUALIFIED_TABLE = /\bfrom\s+["'`]?[A-Za-z_][\w-]*["'`]?\s*\./i;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate the raw tree and flatten it into nodes.
|
|
87
|
+
*
|
|
88
|
+
* The format has no chapter/section distinction: every node is a section, and
|
|
89
|
+
* the leaves of the *document* are the notes and prose its contents resolve to.
|
|
90
|
+
* Depth is whatever the tree says, so nothing here caps or normalises it.
|
|
91
|
+
*
|
|
92
|
+
* `contents` is an **ordered, heterogeneous** list — prose, filters and child
|
|
93
|
+
* sections interleave in the sequence written, and that sequence is preserved
|
|
94
|
+
* exactly. A section that opens with a `file:` and then lists its entries is
|
|
95
|
+
* saying something different from one that does the reverse.
|
|
96
|
+
*
|
|
97
|
+
* @param {unknown} raw - The parsed document, as YAML returns it.
|
|
98
|
+
* @param {object} [opts] - Options.
|
|
99
|
+
* @param {string} [opts.text] - The file's source, for finding positions. Every
|
|
100
|
+
* finding without one is still reported, just without a locator.
|
|
101
|
+
* @returns {{nodes: object[], findings: object[]}} The flattened tree and what
|
|
102
|
+
* was wrong with it.
|
|
103
|
+
*/
|
|
104
|
+
export function parseDocumentTree(raw, { text } = {}) {
|
|
105
|
+
const findings = [];
|
|
106
|
+
const nodes = [];
|
|
107
|
+
if (!raw || typeof raw !== "object" || !Array.isArray(raw.contents)) {
|
|
108
|
+
findings.push({
|
|
109
|
+
severity: "error",
|
|
110
|
+
message: "the document tree must be a mapping with a `contents:` list",
|
|
111
|
+
});
|
|
112
|
+
return { nodes, findings };
|
|
113
|
+
}
|
|
114
|
+
walkSections(raw.contents, ["contents"], [], {}, { nodes, findings, text });
|
|
115
|
+
return { nodes, findings };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* One level of `contents`, recursing into the sections it holds.
|
|
120
|
+
*
|
|
121
|
+
* Presentation is resolved on the way down rather than looked up on the way
|
|
122
|
+
* back: a node's own keys override what it inherits, and its children see the
|
|
123
|
+
* merged result, so nothing downstream has to walk back up an ancestry chain to
|
|
124
|
+
* learn which infobox an entry uses.
|
|
125
|
+
*
|
|
126
|
+
* @param {unknown[]} contents - The list to read.
|
|
127
|
+
* @param {Array<string|number>} keyPath - Where it sits in the document.
|
|
128
|
+
* @param {string[]} trail - Section titles above this level.
|
|
129
|
+
* @param {object} inherited - Presentation from ancestors.
|
|
130
|
+
* @param {object} ctx - `{ nodes, findings, text }`, accumulated.
|
|
131
|
+
* @returns {void}
|
|
132
|
+
*/
|
|
133
|
+
function walkSections(contents, keyPath, trail, inherited, ctx) {
|
|
134
|
+
contents.forEach((entry, i) => {
|
|
135
|
+
const at = [...keyPath, i];
|
|
136
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
137
|
+
ctx.findings.push(finding(ctx, at, "each entry in `contents:` must be a mapping"));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!("sectionName" in entry)) {
|
|
141
|
+
// A bare `file:` or `filter:` at this level belongs to the enclosing
|
|
142
|
+
// section and is collected there, not here.
|
|
143
|
+
if (!LEAF_KEYS.some((k) => k in entry)) {
|
|
144
|
+
ctx.findings.push(
|
|
145
|
+
finding(ctx, at, "expected `sectionName:`, `file:` or `filter:`"),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const title = entry.sectionName;
|
|
151
|
+
if (typeof title !== "string" || !title.trim()) {
|
|
152
|
+
ctx.findings.push(
|
|
153
|
+
finding(ctx, [...at, "sectionName"], "`sectionName:` must be a name"),
|
|
154
|
+
);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
for (const key of Object.keys(entry)) {
|
|
158
|
+
if (!SECTION_KEYS.includes(key)) {
|
|
159
|
+
ctx.findings.push(
|
|
160
|
+
finding(ctx, [...at, key], `unknown key \`${key}:\` on a section`, {
|
|
161
|
+
key: true,
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const presentation = { ...inherited };
|
|
168
|
+
for (const key of PRESENTATION_KEYS) {
|
|
169
|
+
if (entry[key] !== undefined) presentation[key] = entry[key];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const here = [...trail, title];
|
|
173
|
+
const list = Array.isArray(entry.contents) ? entry.contents : [];
|
|
174
|
+
if (entry.contents !== undefined && !Array.isArray(entry.contents)) {
|
|
175
|
+
ctx.findings.push(
|
|
176
|
+
finding(ctx, [...at, "contents"], "`contents:` must be a list", { key: true }),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
ctx.nodes.push({
|
|
181
|
+
title,
|
|
182
|
+
trail: here,
|
|
183
|
+
depth: here.length,
|
|
184
|
+
keyPath: at,
|
|
185
|
+
presentation,
|
|
186
|
+
items: collectItems(list, [...at, "contents"], here, ctx),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
walkSections(list, [...at, "contents"], here, presentation, ctx);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* A section's own leaves, in the order written.
|
|
195
|
+
*
|
|
196
|
+
* Child sections are *not* included: they are walked separately so the flattened
|
|
197
|
+
* node list stays in document order, and a section's items are only the prose
|
|
198
|
+
* and the filters that belong to it directly.
|
|
199
|
+
*
|
|
200
|
+
* @param {unknown[]} contents - The section's `contents`.
|
|
201
|
+
* @param {Array<string|number>} keyPath - Where that list sits.
|
|
202
|
+
* @param {string[]} trail - The section's titles, for a stable item id.
|
|
203
|
+
* @param {object} ctx - `{ findings, text }`.
|
|
204
|
+
* @returns {object[]} Items, tagged by kind.
|
|
205
|
+
*/
|
|
206
|
+
function collectItems(contents, keyPath, trail, ctx) {
|
|
207
|
+
const items = [];
|
|
208
|
+
contents.forEach((entry, i) => {
|
|
209
|
+
if (!entry || typeof entry !== "object" || "sectionName" in entry) return;
|
|
210
|
+
const at = [...keyPath, i];
|
|
211
|
+
if (typeof entry.file === "string" && entry.file.trim()) {
|
|
212
|
+
items.push({ kind: "prose", file: entry.file.trim(), keyPath: at, id: id(trail, i) });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (typeof entry.filter === "string" && entry.filter.trim()) {
|
|
216
|
+
const where = entry.filter.trim();
|
|
217
|
+
if (QUALIFIED_TABLE.test(where)) {
|
|
218
|
+
ctx.findings.push(
|
|
219
|
+
finding(
|
|
220
|
+
ctx,
|
|
221
|
+
[...at, "filter"],
|
|
222
|
+
"a filter may not name another package's notes — a book " +
|
|
223
|
+
"selects from its own project only",
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
items.push({ kind: "filter", where, keyPath: at, id: id(trail, i) });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if ("file" in entry || "filter" in entry) {
|
|
232
|
+
ctx.findings.push(finding(ctx, at, "`file:` and `filter:` must be non-empty strings"));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return items;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A stable identity for one item, so selections can be keyed without the tree.
|
|
240
|
+
*
|
|
241
|
+
* The key is opaque — it is a `Map` key and nothing ever takes it apart again —
|
|
242
|
+
* so the only requirement is that two different items cannot produce the same
|
|
243
|
+
* string. `JSON.stringify` of the trail and the index gives that for free: it
|
|
244
|
+
* quotes and escapes each title, so a title containing the separator cannot
|
|
245
|
+
* forge a neighbour's key.
|
|
246
|
+
*
|
|
247
|
+
* Joining on a delimiter would work too, but every delimiter safe enough to be
|
|
248
|
+
* collision-proof is a control character, and a control character written into
|
|
249
|
+
* source is a character nobody can see in a diff, a test failure or a review.
|
|
250
|
+
* A JSON array is printable, greppable, and reads as itself when a plan is
|
|
251
|
+
* dumped.
|
|
252
|
+
*
|
|
253
|
+
* @param {string[]} trail - The owning section's titles.
|
|
254
|
+
* @param {number} index - The item's place in `contents`.
|
|
255
|
+
* @returns {string} The key.
|
|
256
|
+
*/
|
|
257
|
+
function id(trail, index) {
|
|
258
|
+
return JSON.stringify([...trail, index]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* A finding, positioned in the source when the source was supplied.
|
|
263
|
+
*
|
|
264
|
+
* Carries no `file`: only the caller knows which document this was, which is the
|
|
265
|
+
* rule every rule in this engine follows.
|
|
266
|
+
*
|
|
267
|
+
* @param {object} ctx - `{ text }`.
|
|
268
|
+
* @param {Array<string|number>} keyPath - Where the problem is.
|
|
269
|
+
* @param {string} message - What is wrong.
|
|
270
|
+
* @param {object} [opts] - Passed to {@link positionOfYamlPath}.
|
|
271
|
+
* @returns {object} The finding.
|
|
272
|
+
*/
|
|
273
|
+
function finding(ctx, keyPath, message, opts) {
|
|
274
|
+
return { severity: "error", message, ...position(ctx.text, keyPath, opts) };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Run every filter, and report the ones that would not run.
|
|
279
|
+
*
|
|
280
|
+
* The I/O half, kept apart from the planner for the reason the whole engine
|
|
281
|
+
* keeps them apart: the plan is then assertable without a database. The build
|
|
282
|
+
* owns the statement — `SELECT * FROM notes WHERE <clause>` — which is what
|
|
283
|
+
* makes a filter unable to reach another package's schema, unable to project
|
|
284
|
+
* something that is not a note, and unable to pick up the `doc<type>`
|
|
285
|
+
* documentation rows that ride the same index as the notes they document.
|
|
286
|
+
*
|
|
287
|
+
* **A filter that selects nothing is an error**, and the distinction that makes
|
|
288
|
+
* that consistent is worth stating. A *note* no clause selects is expected: the
|
|
289
|
+
* book is a selection and a project decides what its own volume carries. A
|
|
290
|
+
* *clause* that selects no note is not the same thing — a filter is a deliberate
|
|
291
|
+
* act, so one matching nothing is either wrong or left over from a structure
|
|
292
|
+
* that has moved on, and in both cases the tree should not carry it. Reported
|
|
293
|
+
* with the section's name and the filter's position, so the choice between
|
|
294
|
+
* fixing it and deleting it is the author's.
|
|
295
|
+
*
|
|
296
|
+
* @param {object[]} nodes - From {@link parseDocumentTree}.
|
|
297
|
+
* @param {{query: (sql: string) => Promise<{rows: object[]}>}} db - An open
|
|
298
|
+
* database, from {@link module:engine/sql-tables.openNotesDatabase}.
|
|
299
|
+
* @param {object} [opts] - Options.
|
|
300
|
+
* @param {(record: object) => boolean} [opts.keep] - Which rows are notes.
|
|
301
|
+
* @param {string} [opts.text] - The document's source, for finding positions.
|
|
302
|
+
* @returns {Promise<{selections: Map<string, object[]>, findings: object[]}>}
|
|
303
|
+
*/
|
|
304
|
+
export async function runTreeFilters(nodes, db, { keep = () => true, text } = {}) {
|
|
305
|
+
const selections = new Map();
|
|
306
|
+
const findings = [];
|
|
307
|
+
for (const node of nodes) {
|
|
308
|
+
for (const item of node.items) {
|
|
309
|
+
if (item.kind !== "filter") continue;
|
|
310
|
+
const where = `\`${node.trail.join(" › ")}\``;
|
|
311
|
+
try {
|
|
312
|
+
const { rows } = await db.query(`SELECT * FROM notes WHERE ${item.where}`);
|
|
313
|
+
const kept = rows.filter(keep);
|
|
314
|
+
selections.set(item.id, kept);
|
|
315
|
+
if (!kept.length) {
|
|
316
|
+
findings.push({
|
|
317
|
+
severity: "error",
|
|
318
|
+
message:
|
|
319
|
+
`${where}: the filter selected no notes — ` +
|
|
320
|
+
`\`${item.where}\` matches nothing in this project, so ` +
|
|
321
|
+
`either it is wrong or the section should not be here`,
|
|
322
|
+
...position(text, [...item.keyPath, "filter"]),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
} catch (err) {
|
|
326
|
+
findings.push({
|
|
327
|
+
severity: "error",
|
|
328
|
+
message:
|
|
329
|
+
`${where}: the filter did not run — ` + String(err.message).split("\n")[0],
|
|
330
|
+
...position(text, [...item.keyPath, "filter"]),
|
|
331
|
+
});
|
|
332
|
+
selections.set(item.id, []);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return { selections, findings };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Resolve the flattened tree into the document plan.
|
|
341
|
+
*
|
|
342
|
+
* The plan is an ordered list of entries — `section`, `prose`, `note` — and it
|
|
343
|
+
* is the artifact worth having. Order, depth, the outline, the table of
|
|
344
|
+
* contents, anchor uniqueness and every link destination are all readable from
|
|
345
|
+
* it, so nearly the whole of what #316 asks for can be asserted here, on data,
|
|
346
|
+
* without a renderer or a PDF. Only how the result *looks* needs eyes.
|
|
347
|
+
*
|
|
348
|
+
* **Notes are sorted, prose is not.** A section's entries come out in
|
|
349
|
+
* `nameAscii` order — the ASCII fold `buildIndexRecord` already derives, so a
|
|
350
|
+
* circumflex sorts with its letter instead of after `Z` as a raw codepoint
|
|
351
|
+
* comparison would put it. Prose keeps the position it was written in, because
|
|
352
|
+
* its place in the sequence is the author's statement.
|
|
353
|
+
*
|
|
354
|
+
* **A section with nothing in it does not print**, and emptiness is judged after
|
|
355
|
+
* its descendants are: a section holding only sections that all resolved to
|
|
356
|
+
* nothing is itself empty. In a correct tree this never fires — a filter that
|
|
357
|
+
* selects nothing is reported by {@link runTreeFilters} as the error it is — so
|
|
358
|
+
* this is the graceful half of that failure rather than a feature: a build whose
|
|
359
|
+
* filters are broken still produces a readable document to look at while they
|
|
360
|
+
* are fixed. A section holding only prose is not empty; it is a preface.
|
|
361
|
+
*
|
|
362
|
+
* @param {object[]} nodes - From {@link parseDocumentTree}.
|
|
363
|
+
* @param {object} [opts] - Options.
|
|
364
|
+
* @param {Map<string, object[]>} [opts.selections] - Records each filter
|
|
365
|
+
* selected, keyed by item id. Run by the caller: DuckDB is async and this is
|
|
366
|
+
* not.
|
|
367
|
+
* @returns {{entries: object[], links: Map<string, string>, stats: object}}
|
|
368
|
+
* The plan, the address→anchor map inbound wikilinks resolve through, and
|
|
369
|
+
* what the selection came to.
|
|
370
|
+
*/
|
|
371
|
+
export function planDocument(nodes, { selections = new Map() } = {}) {
|
|
372
|
+
const entries = [];
|
|
373
|
+
const links = new Map();
|
|
374
|
+
const seen = new Map();
|
|
375
|
+
let selected = 0;
|
|
376
|
+
|
|
377
|
+
// Depth-first in document order. `nodes` is already in that order, so a
|
|
378
|
+
// section's children are every later node whose trail begins with its own.
|
|
379
|
+
const roots = nodes.filter((n) => n.depth === 1);
|
|
380
|
+
for (const root of roots) emitSection(root, nodes, { entries, links, seen, selections });
|
|
381
|
+
|
|
382
|
+
for (const node of nodes) {
|
|
383
|
+
for (const item of node.items) {
|
|
384
|
+
if (item.kind === "filter") selected += (selections.get(item.id) ?? []).length;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
entries,
|
|
390
|
+
links,
|
|
391
|
+
stats: {
|
|
392
|
+
sections: entries.filter((e) => e.kind === "section").length,
|
|
393
|
+
prose: entries.filter((e) => e.kind === "prose").length,
|
|
394
|
+
notes: entries.filter((e) => e.kind === "note").length,
|
|
395
|
+
distinct: links.size,
|
|
396
|
+
repeated: selected - links.size,
|
|
397
|
+
skipped: nodes.length - entries.filter((e) => e.kind === "section").length,
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Emit one section and everything beneath it, or nothing if it is empty.
|
|
404
|
+
*
|
|
405
|
+
* Builds the section's own entries into a scratch list first, so the decision to
|
|
406
|
+
* drop it can be made after its children have answered — which is the only way
|
|
407
|
+
* a section of empty sections is itself recognised as empty.
|
|
408
|
+
*
|
|
409
|
+
* @param {object} node - The section.
|
|
410
|
+
* @param {object[]} nodes - Every node, in document order.
|
|
411
|
+
* @param {object} ctx - `{ entries, links, seen, selections }`.
|
|
412
|
+
* @returns {number} How many entries it contributed.
|
|
413
|
+
*/
|
|
414
|
+
function emitSection(node, nodes, ctx) {
|
|
415
|
+
const start = ctx.entries.length;
|
|
416
|
+
ctx.entries.push({
|
|
417
|
+
kind: "section",
|
|
418
|
+
title: node.title,
|
|
419
|
+
trail: node.trail,
|
|
420
|
+
depth: node.depth,
|
|
421
|
+
presentation: node.presentation,
|
|
422
|
+
anchor: anchorFor(slugify(node.trail.join(" ")) || "section", ctx.seen),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const children = nodes.filter(
|
|
426
|
+
(n) =>
|
|
427
|
+
n.depth === node.depth + 1 &&
|
|
428
|
+
n.trail.length === node.trail.length + 1 &&
|
|
429
|
+
node.trail.every((t, i) => n.trail[i] === t),
|
|
430
|
+
);
|
|
431
|
+
let contributed = 0;
|
|
432
|
+
|
|
433
|
+
for (const item of node.items) {
|
|
434
|
+
if (item.kind === "prose") {
|
|
435
|
+
ctx.entries.push({
|
|
436
|
+
kind: "prose",
|
|
437
|
+
file: item.file,
|
|
438
|
+
trail: node.trail,
|
|
439
|
+
depth: node.depth,
|
|
440
|
+
presentation: node.presentation,
|
|
441
|
+
anchor: anchorFor(slugify(item.file.replace(/\.md$/i, "")) || "prose", ctx.seen),
|
|
442
|
+
});
|
|
443
|
+
contributed++;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
for (const record of sortRecords(ctx.selections.get(item.id) ?? [])) {
|
|
447
|
+
const slug = record?.address?.slug ?? record?.shortcode ?? String(ctx.entries.length);
|
|
448
|
+
const anchor = anchorFor(slug, ctx.seen);
|
|
449
|
+
// First occurrence wins the address: however many times the book
|
|
450
|
+
// prints an entry, `[[weapongear-dagger]]` reaches one page.
|
|
451
|
+
if (!ctx.links.has(slug)) ctx.links.set(slug, anchor);
|
|
452
|
+
ctx.entries.push({
|
|
453
|
+
kind: "note",
|
|
454
|
+
record,
|
|
455
|
+
trail: node.trail,
|
|
456
|
+
depth: node.depth,
|
|
457
|
+
presentation: node.presentation,
|
|
458
|
+
anchor,
|
|
459
|
+
});
|
|
460
|
+
contributed++;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
for (const child of children) contributed += emitSection(child, nodes, ctx);
|
|
465
|
+
|
|
466
|
+
if (!contributed) {
|
|
467
|
+
ctx.entries.length = start;
|
|
468
|
+
return 0;
|
|
469
|
+
}
|
|
470
|
+
return contributed + 1;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* A section's notes, in the order the book prints them.
|
|
475
|
+
*
|
|
476
|
+
* Ordering is deliberately not in the file format: a filter says *which* notes,
|
|
477
|
+
* never in what sequence. The tiebreak on address keeps the result stable when
|
|
478
|
+
* two entries share a name, which a corpus of 2,500 does eventually.
|
|
479
|
+
*
|
|
480
|
+
* @param {object[]} records - Selected records.
|
|
481
|
+
* @returns {object[]} A sorted copy.
|
|
482
|
+
*/
|
|
483
|
+
function sortRecords(records) {
|
|
484
|
+
return [...records].sort((a, b) => {
|
|
485
|
+
const an = a?.nameAscii ?? a?.name?.full ?? "";
|
|
486
|
+
const bn = b?.nameAscii ?? b?.name?.full ?? "";
|
|
487
|
+
if (an !== bn) return an < bn ? -1 : 1;
|
|
488
|
+
const as = a?.address?.slug ?? "";
|
|
489
|
+
const bs = b?.address?.slug ?? "";
|
|
490
|
+
return (
|
|
491
|
+
as < bs ? -1
|
|
492
|
+
: as > bs ? 1
|
|
493
|
+
: 0
|
|
494
|
+
);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* A unique anchor, suffixed when the base is already taken.
|
|
500
|
+
*
|
|
501
|
+
* A note selected by two sections is two pages, and two pages cannot share a
|
|
502
|
+
* destination: the outline, the table of contents and any link would all reach
|
|
503
|
+
* whichever the renderer happened to emit last.
|
|
504
|
+
*
|
|
505
|
+
* @param {string} base - The preferred anchor.
|
|
506
|
+
* @param {Map<string, number>} seen - How many times each base has been used.
|
|
507
|
+
* @returns {string} The anchor.
|
|
508
|
+
*/
|
|
509
|
+
function anchorFor(base, seen) {
|
|
510
|
+
const n = (seen.get(base) ?? 0) + 1;
|
|
511
|
+
seen.set(base, n);
|
|
512
|
+
return n === 1 ? base : `${base}-${n}`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Where a node sits in the source, when the source was supplied.
|
|
517
|
+
*
|
|
518
|
+
* @param {string|undefined} text - The document source.
|
|
519
|
+
* @param {Array<string|number>} keyPath - Path to the node.
|
|
520
|
+
* @param {object} [opts] - Options.
|
|
521
|
+
* @returns {object} Spreadable position fields.
|
|
522
|
+
*/
|
|
523
|
+
function position(text, keyPath, opts) {
|
|
524
|
+
return text ? positionOfYamlPath(text, keyPath, opts) : {};
|
|
525
|
+
}
|
package/engine/prose-config.mjs
CHANGED
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* The prose conventions every content repository writes to — one Prettier
|
|
16
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
|
|
17
|
+
* formatted in one repository is formatted the same way in the next.
|
|
18
18
|
*
|
|
19
|
-
* These
|
|
19
|
+
* These belong in no single consumer. The SoHL repository carried
|
|
20
20
|
* both; `sohl-thalorna` had Prettier but never ran it from `lint`; and
|
|
21
21
|
* `sohl-kethira-basic` had neither, so the package least likely to have been
|
|
22
22
|
* proofread was checked for addresses and nothing else. A rule set copied into
|
|
23
23
|
* three repositories is one rule with three implementations, which is the drift
|
|
24
|
-
*
|
|
24
|
+
* this exists to remove — so it is declared once, here, and every consumer
|
|
25
25
|
* invokes it.
|
|
26
26
|
*
|
|
27
27
|
* **Neither of these is an override.** A consumer that declares its own
|
|
@@ -89,7 +89,7 @@ export const PRETTIER_BASE = Object.freeze({
|
|
|
89
89
|
* **Declared apart from the `overrides` block, not derived from it.** Prettier
|
|
90
90
|
* applies `overrides` only while resolving a config *file*; options handed to
|
|
91
91
|
* it directly keep the global values, so a consumer with no config of its own
|
|
92
|
-
* silently got markdown at 4
|
|
92
|
+
* silently got markdown at 4. The runner needs the adjustment as data it
|
|
93
93
|
* can apply itself, and {@link PRETTIER_CONFIG} composes the same values into
|
|
94
94
|
* the shape a config file wants — one source, two presentations.
|
|
95
95
|
*
|
|
@@ -128,7 +128,7 @@ export const PRETTIER_CONFIG = Object.freeze({
|
|
|
128
128
|
*
|
|
129
129
|
* @param {string} file - Path of the file about to be formatted.
|
|
130
130
|
* @returns {object} Options to hand Prettier directly. Never carries
|
|
131
|
-
* `overrides`: passing that inline is what silently did nothing
|
|
131
|
+
* `overrides`: passing that inline is what silently did nothing.
|
|
132
132
|
*/
|
|
133
133
|
export function sharedPrettierOptionsFor(file) {
|
|
134
134
|
return /\.md$/i.test(file) ? { ...PRETTIER_BASE, ...PRETTIER_MARKDOWN } : { ...PRETTIER_BASE };
|
|
@@ -140,11 +140,11 @@ export function sharedPrettierOptionsFor(file) {
|
|
|
140
140
|
* The runner resolves each file's options as *either* the consumer's own config
|
|
141
141
|
* or {@link sharedPrettierOptionsFor}, never a merge. That is what bare Prettier
|
|
142
142
|
* does and it is the contract — but it means the conventions this package exists
|
|
143
|
-
* to publish hold by convention alone, and they lapse in two opposite directions
|
|
144
|
-
*
|
|
143
|
+
* to publish hold by convention alone, and they lapse in two opposite directions.
|
|
144
|
+
* A consumer that declares any config of its own gets whatever that
|
|
145
145
|
* config says: spread {@link PRETTIER_BASE} without the markdown override and
|
|
146
|
-
* every note reindents at 4, the reindentation the override was added to prevent
|
|
147
|
-
*
|
|
146
|
+
* every note reindents at 4, the reindentation the override was added to prevent;
|
|
147
|
+
* write a partial `.prettierrc` such as `{"tabWidth": 2}` and
|
|
148
148
|
* `printWidth`, `trailingComma`, `experimentalTernaries` and the rest fall back
|
|
149
149
|
* to Prettier's own defaults. A consumer that declares *nothing* formats one way
|
|
150
150
|
* under this command and another under a bare `npx prettier`.
|
package/engine/prose-lint.mjs
CHANGED
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Running the shared prose conventions over a repository — Prettier for
|
|
16
|
-
* formatting, markdownlint for the structure Prettier is indifferent to
|
|
16
|
+
* formatting, markdownlint for the structure Prettier is indifferent to.
|
|
17
17
|
*
|
|
18
18
|
* Both are thin: the rules live in `./prose-config.mjs` and the tools are
|
|
19
19
|
* Prettier and markdownlint themselves. What this module adds is the two things
|
|
20
20
|
* a consumer would otherwise have to get right on its own — supplying the
|
|
21
21
|
* shared configuration *as a default a local one overrides*, and reporting
|
|
22
|
-
* findings in the one parseable form every check in this package emits
|
|
22
|
+
* findings in the one parseable form every check in this package emits.
|
|
23
23
|
*
|
|
24
24
|
* **Neither tool's own file discovery is reimplemented.** Prettier decides what
|
|
25
25
|
* it formats and what an ignore file excludes, through `getFileInfo`;
|
|
@@ -68,7 +68,7 @@ const IGNORE_FILES = Object.freeze([".gitignore", ".prettierignore"]);
|
|
|
68
68
|
* `format` is *assumed* idempotent and is not guaranteed to be: a single pass
|
|
69
69
|
* can leave text the next pass would still change, and a `--write` run that
|
|
70
70
|
* takes one pass then reports success has called such a file formatted while
|
|
71
|
-
* `prettier --check` still rejects it
|
|
71
|
+
* `prettier --check` still rejects it. Formatting to a fixpoint removes
|
|
72
72
|
* the assumption — the file lands on the value repeated formatting converges
|
|
73
73
|
* to, whatever it took to get there.
|
|
74
74
|
*
|
|
@@ -86,7 +86,7 @@ const MAX_FORMAT_PASSES = 3;
|
|
|
86
86
|
* configured to do means asking about a file. These are the two answers that
|
|
87
87
|
* differ: markdown carries the shared `tabWidth` override and everything else
|
|
88
88
|
* does not, so a single probe would check half the conventions and miss the one
|
|
89
|
-
* most worth checking
|
|
89
|
+
* most worth checking.
|
|
90
90
|
*
|
|
91
91
|
* Ordinary names at the repository root, and neither has to exist —
|
|
92
92
|
* `resolveConfig` reads the path to walk up from it and to match `overrides`
|
|
@@ -147,8 +147,7 @@ function walkFiles(root) {
|
|
|
147
147
|
* @param {boolean} [opts.write=false] - Rewrite unformatted files in place
|
|
148
148
|
* rather than reporting them. Each file is formatted to a fixpoint (up to
|
|
149
149
|
* {@link MAX_FORMAT_PASSES} passes), so a written tree is one a second run
|
|
150
|
-
* leaves alone; a file that will not converge is reported and left unchanged
|
|
151
|
-
* (#125).
|
|
150
|
+
* leaves alone; a file that will not converge is reported and left unchanged.
|
|
152
151
|
* @param {object} [opts.prettier] - The Prettier module, for tests.
|
|
153
152
|
* @returns {Promise<{findings: Array<{file: string, severity: string,
|
|
154
153
|
* message: string}>, checked: number, written: string[]}>} The findings, how
|
|
@@ -185,7 +184,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
185
184
|
});
|
|
186
185
|
// `resolveConfig` has already applied any `overrides` the consumer's
|
|
187
186
|
// own config declares. The shared fallback has to apply its own, since
|
|
188
|
-
// Prettier ignores an `overrides` block passed inline
|
|
187
|
+
// Prettier ignores an `overrides` block passed inline.
|
|
189
188
|
const options = {
|
|
190
189
|
...(local ?? sharedPrettierOptionsFor(file)),
|
|
191
190
|
filepath: file,
|
|
@@ -199,7 +198,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
199
198
|
try {
|
|
200
199
|
if (write) {
|
|
201
200
|
// Format to a fixpoint rather than once, so what lands on disk
|
|
202
|
-
// is what a second run would have produced
|
|
201
|
+
// is what a second run would have produced.
|
|
203
202
|
let formatted = source;
|
|
204
203
|
let converged = false;
|
|
205
204
|
for (let pass = 0; pass < MAX_FORMAT_PASSES; pass += 1) {
|
|
@@ -221,7 +220,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
221
220
|
file,
|
|
222
221
|
severity: "error",
|
|
223
222
|
// No line or column: the verdict is about the whole
|
|
224
|
-
// file, and
|
|
223
|
+
// file, and the rule is to drop a field rather than
|
|
225
224
|
// invent one.
|
|
226
225
|
message:
|
|
227
226
|
`did not converge after ${MAX_FORMAT_PASSES} formatting passes; ` +
|
|
@@ -242,7 +241,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
242
241
|
file,
|
|
243
242
|
severity: "error",
|
|
244
243
|
// No line or column: Prettier's answer is about the whole
|
|
245
|
-
// file, and
|
|
244
|
+
// file, and the rule is to drop a field rather than
|
|
246
245
|
// invent one.
|
|
247
246
|
message: "is not formatted; run `content-build format --write` to fix it",
|
|
248
247
|
});
|
|
@@ -272,7 +271,7 @@ export async function checkFormatting(root, opts = {}) {
|
|
|
272
271
|
*
|
|
273
272
|
* The two cases read differently on purpose. A key set to something else is a
|
|
274
273
|
* choice someone made and can defend; a key that is simply absent is the
|
|
275
|
-
* silent half
|
|
274
|
+
* silent half — a consumer does not choose Prettier's default, it
|
|
276
275
|
* arrived because declaring one option discards every option not restated.
|
|
277
276
|
*
|
|
278
277
|
* @param {{key: string, shared: unknown, local: unknown}} divergence - From
|
|
@@ -291,7 +290,7 @@ function divergenceMessage({ key, shared, local }, scope = "") {
|
|
|
291
290
|
|
|
292
291
|
/**
|
|
293
292
|
* Report where a repository's own Prettier configuration parts from the shared
|
|
294
|
-
* one — or that it has none at all
|
|
293
|
+
* one — or that it has none at all.
|
|
295
294
|
*
|
|
296
295
|
* **Warnings, every one of them.** A consumer's config wins by design and this
|
|
297
296
|
* does not change that; it only refuses to let the divergence be silent, which
|
|
@@ -312,7 +311,7 @@ function divergenceMessage({ key, shared, local }, scope = "") {
|
|
|
312
311
|
* @returns {Promise<{findings: Array<{file?: string, severity: string,
|
|
313
312
|
* message: string}>, configFile: string|null}>} The findings and the config
|
|
314
313
|
* file they are about, which is `null` when the repository declares none. A
|
|
315
|
-
* finding about a missing file carries no `file`:
|
|
314
|
+
* finding about a missing file carries no `file`: the rule is to drop a
|
|
316
315
|
* field rather than invent one.
|
|
317
316
|
*/
|
|
318
317
|
export async function checkPrettierConventions(root, opts = {}) {
|