@heroiclands/package-build 25.1.3 → 25.2.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 +29 -0
- package/assets/styles/content-base.css +0 -9
- package/bin/content-commands.mjs +22 -16
- package/bin/package-build.mjs +24 -25
- package/content-config.mjs +7 -34
- package/docs/api.md +38 -21
- package/docs/authoring/first-note.md +3 -3
- package/docs/authoring/frontmatter.md +30 -2
- package/docs/authoring/links-and-markup.md +92 -31
- package/docs/commands.md +103 -59
- package/docs/configuration.md +10 -45
- package/docs/content-format.md +7 -5
- package/docs/diagnostics.md +75 -0
- package/docs/guides/book.md +3 -3
- package/docs/guides/module-project.md +1 -1
- package/docs/guides/reference-pages.md +7 -3
- package/docs/project-setup.md +3 -0
- package/docs/reference/format-details.md +299 -185
- package/docs/reference/note-types.md +16 -17
- package/engine/changelog-group.mjs +84 -173
- package/engine/changelog-labels.mjs +73 -0
- package/engine/changelog-lint.mjs +168 -44
- package/engine/content-expand.mjs +58 -38
- package/engine/content-format.yaml +0 -2
- package/engine/content-index.mjs +49 -23
- package/engine/content-links.mjs +4 -0
- package/engine/content-lint.mjs +2 -3
- package/engine/content-render.mjs +6 -2
- package/engine/culture-parents.mjs +1 -1
- package/engine/culture-reach.mjs +10 -1
- package/engine/diagnostics.mjs +3 -2
- package/engine/dictionary-rows.mjs +3 -1
- package/engine/first-use.mjs +301 -87
- package/engine/frontmatter-lint.mjs +40 -14
- package/engine/glossary-entries.mjs +5 -5
- package/engine/glossary-language.mjs +21 -44
- package/engine/homepage.mjs +0 -4
- package/engine/index-records.mjs +3 -1
- package/engine/infobox.mjs +6 -3
- package/engine/language-names.mjs +3 -2
- package/engine/language-rows.mjs +6 -7
- package/engine/lexical-sections.mjs +3 -1
- package/engine/literature-notes.mjs +4 -76
- package/engine/metadata-index.mjs +15 -8
- package/engine/name-forms.mjs +99 -0
- package/engine/name-refs.mjs +143 -0
- package/engine/name-rows.mjs +11 -1
- package/engine/names.mjs +27 -22
- package/engine/note-addresses.mjs +0 -3
- package/engine/note-frontmatter.mjs +3 -5
- package/engine/note-language.mjs +348 -0
- package/engine/note-vocabulary.mjs +13 -11
- package/engine/pdf-index.mjs +3 -2
- package/engine/pdf-render.mjs +2 -4
- package/engine/settlement-tables.mjs +4 -3
- package/engine/site-build.mjs +2 -7
- package/engine/site-search.mjs +3 -5
- package/engine/site-theme.mjs +1 -1
- package/engine/sql-tables.mjs +29 -14
- package/engine/wikilink-syntax.mjs +2 -0
- package/engine/word-citations.mjs +3 -3
- package/engine/word-rows.mjs +3 -1
- package/engine/word-terms.mjs +2 -2
- package/package.json +1 -1
- package/site-theme/static/css/style.css +0 -4
- package/types/engine/changelog-group.d.mts +6 -10
- package/types/engine/changelog-labels.d.mts +10 -0
- package/types/engine/changelog-lint.d.mts +11 -6
- package/types/engine/content-index.d.mts +16 -16
- package/types/engine/diagnostics.d.mts +1 -1
- package/types/engine/first-use.d.mts +97 -1
- package/types/engine/frontmatter-lint.d.mts +9 -2
- package/types/engine/glossary-language.d.mts +13 -6
- package/types/engine/homepage.d.mts +0 -3
- package/types/engine/literature-notes.d.mts +2 -27
- package/types/engine/name-forms.d.mts +61 -0
- package/types/engine/name-refs.d.mts +51 -0
- package/types/engine/name-rows.d.mts +4 -0
- package/types/engine/names.d.mts +12 -92
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-language.d.mts +47 -0
- package/types/engine/settlement-tables.d.mts +1 -1
- package/types/engine/site-build.d.mts +2 -6
|
@@ -15,18 +15,18 @@ The [first-note guide](../authoring/first-note.md) teaches the shape of a note.
|
|
|
15
15
|
|
|
16
16
|
These are the only keys a note writes at its top level, in the order the formatter puts them. A key absent from this table is a lint error at its own line: the region is closed, so the key reaches no document and no page. Facts about the subject belong under `data`, and values for one game system belong inside that system's block.
|
|
17
17
|
|
|
18
|
-
| Key | Meaning
|
|
19
|
-
| ------------- |
|
|
20
|
-
| `shortcode` | The note's own address segment, unique within its type.
|
|
21
|
-
| `name` | The display names — a required `full`,
|
|
22
|
-
| `type` | What the note is about, which decides its vocabulary and its document.
|
|
23
|
-
| `subType` | The type's own genre, where it declares one.
|
|
24
|
-
| `description` | The short page summary.
|
|
25
|
-
| `tags` | Draft state, GM routing, and the descriptive labels a page list reads.
|
|
26
|
-
| `data` | The facts about the subject itself, shared by every system.
|
|
27
|
-
| `dnd5e` | What the `dnd5e` system makes of the subject — its document's mechanics, routing and art.
|
|
28
|
-
| `hm3` | What the `hm3` system makes of the subject — its document's mechanics, routing and art.
|
|
29
|
-
| `sohl` | What the `sohl` system makes of the subject — its document's mechanics, routing and art.
|
|
18
|
+
| Key | Meaning |
|
|
19
|
+
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| `shortcode` | The note's own address segment, unique within its type. |
|
|
21
|
+
| `name` | The display names — a required `full`, the English name; the other names, listed under the code of their language; and any `sort`. |
|
|
22
|
+
| `type` | What the note is about, which decides its vocabulary and its document. |
|
|
23
|
+
| `subType` | The type's own genre, where it declares one. |
|
|
24
|
+
| `description` | The short page summary. |
|
|
25
|
+
| `tags` | Draft state, GM routing, and the descriptive labels a page list reads. |
|
|
26
|
+
| `data` | The facts about the subject itself, shared by every system. |
|
|
27
|
+
| `dnd5e` | What the `dnd5e` system makes of the subject — its document's mechanics, routing and art. |
|
|
28
|
+
| `hm3` | What the `hm3` system makes of the subject — its document's mechanics, routing and art. |
|
|
29
|
+
| `sohl` | What the `sohl` system makes of the subject — its document's mechanics, routing and art. |
|
|
30
30
|
|
|
31
31
|
## Shared `data` fields
|
|
32
32
|
|
|
@@ -34,6 +34,7 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
34
34
|
|
|
35
35
|
| Field | Shape | Meaning |
|
|
36
36
|
| ----------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
37
|
+
| `data.lang` | a language code `languages:` declares, or `en` | The language the note is in. Absent, the content index derives it: the note's culture's language, then its `data.parents`' nearest first, then `en`. |
|
|
37
38
|
| `data.seeAlso` | list of `{ address, gloss? }` | Deliberate cross-references in reading order, with optional glosses; shown on the site, in journals and in books. |
|
|
38
39
|
| `data.calendar` | an Address | The calendar this note's dates print in, as a calendar lore note. Absent, the note takes its culture's calendar, then — for a place — its parents', then the configured `defaultCalendar`. |
|
|
39
40
|
| `data.id` | string | A fixed document identity when the derived identity is unsuitable. |
|
|
@@ -139,7 +140,6 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
139
140
|
| `data.lore` | list of Addresses | Lore concerning it — the peoples it draws on, the god a faith venerates, its law, its calendar. |
|
|
140
141
|
| `data.parents` | list of Addresses | Affiliations it is subordinate to. |
|
|
141
142
|
| `data.culture` | an Address | The people this affiliation belongs to, as a culture lore note. |
|
|
142
|
-
| `data.language` | an Address | The trade, temple or order tongue the affiliation keeps, as a language skill note. A glossary entry that states no `language` is in it. The affiliation chooses its own common vocabulary and context meanings. |
|
|
143
143
|
| `data.glossary` | list of `{ name, name_sort?, common?, definition?, pronunciation?, language? }` | Its own glossary entries: common vocabulary, dictionary words with a contextual meaning, and phrases, compounds or terms of art. A named dictionary word supplies its senses and class unless `definition` supplies the sense. Common marks belong to this owner. Every entry resolves to a language. Empty when unstated. See [glossary terms](format-details.md#glossary-terms). |
|
|
144
144
|
| `data.relations` | a map keyed by Address | Standing with other affiliations — aligned, unaligned, rival, nemesis. |
|
|
145
145
|
|
|
@@ -200,7 +200,7 @@ These fields are accepted by every note type. A field's value can still be irrel
|
|
|
200
200
|
| `data.glossary[].common` | `true` or `false` | no | Core vocabulary for this glossary's owner; `false` when unstated. |
|
|
201
201
|
| `data.glossary[].definition` | a string | no | The sense text, overriding the word's or governance sources' senses. Unstated, a named dictionary word supplies all its senses and class; a named rank or office supplies its descriptions. |
|
|
202
202
|
| `data.glossary[].pronunciation` | a respelling such as `sol-MEH-reh` | no | A respelling, held to the same rule as a word's; the dictionary word's when unstated. |
|
|
203
|
-
| `data.glossary[].language` | a language skill Address | no | The language the term is in. Unstated, it is the
|
|
203
|
+
| `data.glossary[].language` | a language skill Address | no | The language the term is in. Unstated, it is the note of the language the note is in, its `data.lang` as the content index derives it. |
|
|
204
204
|
|
|
205
205
|
**System mappings**
|
|
206
206
|
|
|
@@ -791,11 +791,10 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
|
|
|
791
791
|
| `data.namedDays` | list of `{ name, abbreviation?, day }` | Names assigned to particular days of the year. |
|
|
792
792
|
| `data.eras` | list of `{ shortcode, name, marker?, abbreviation?, proclaimedBy?, start, label? }` | The year-counts kept in this calendar. A marker names one era and uses these months. |
|
|
793
793
|
| `data.formats` | map of named Calendaria format strings | Named patterns for reading and writing this calendar's dates. |
|
|
794
|
-
| `data.parents` | list of Addresses | Culture only: parent culture lore notes, searched in listed depth-first order for omitted
|
|
794
|
+
| `data.parents` | list of Addresses | Culture only: parent culture lore notes, searched in listed depth-first order for an omitted appearance profile, and nearest first for an omitted `data.lang`. |
|
|
795
795
|
| `data.culture` | an Address | The people this lore belongs to — for a work of literature, the people whose work it is — as a culture lore note. A culture note is its own culture, and states the field only as itself. |
|
|
796
796
|
| `data.form` | string | The kind of work in its people's own terms — an epic, a saga, a praise-song, an elegy. Free text. |
|
|
797
797
|
| `data.subjects` | list of Addresses | The beings, places, gods and other notes the work concerns, and each event it concerns, named as `place-ironfells#sack`. |
|
|
798
|
-
| `data.language` | an Address | The tongue the note is in, as a language skill note: the tongue a work is composed in, a culture's people speak, or an affiliation keeps as its trade, temple or order tongue. `package-build npc new` draws a culture's names from its language's `data.names`. A `literature` or `culture` lore note, or an affiliation, only. |
|
|
799
798
|
| `data.events` | list of event entries | What happened to or at this subject — each entry one dated, attributed event: a founding, a war, a fall, or an occasion that recurs. |
|
|
800
799
|
| `data.appearanceProfile` | an appearance profile | What this culture's people look like, for `package-build npc new`: `frame`, `eye_color`, `hair_color`, `skin_color` and `complexion` each map words of that field's vocabulary to positive weights, and `height` maps a gender to a `{min, max}` range, `female` and `male` required. A culture note only. See [appearance profiles](format-details.md#appearance-profiles). |
|
|
801
800
|
| `data.glossary` | list of `{ name, name_sort?, common?, definition?, pronunciation?, language? }` | `culture` only: Its own glossary entries: common vocabulary, dictionary words with a contextual meaning, and phrases, compounds or terms of art. A named dictionary word supplies its senses and class unless `definition` supplies the sense. Common marks belong to this owner. Every entry resolves to a language. Empty when unstated. See [glossary terms](format-details.md#glossary-terms). |
|
|
@@ -875,7 +874,7 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
|
|
|
875
874
|
| `data.glossary[].common` | `true` or `false` | no | Core vocabulary for this glossary's owner; `false` when unstated. |
|
|
876
875
|
| `data.glossary[].definition` | a string | no | The sense text, overriding the word's or governance sources' senses. Unstated, a named dictionary word supplies all its senses and class; a named rank or office supplies its descriptions. |
|
|
877
876
|
| `data.glossary[].pronunciation` | a respelling such as `sol-MEH-reh` | no | A respelling, held to the same rule as a word's; the dictionary word's when unstated. |
|
|
878
|
-
| `data.glossary[].language` | a language skill Address | no | The language the term is in. Unstated, it is the
|
|
877
|
+
| `data.glossary[].language` | a language skill Address | no | The language the term is in. Unstated, it is the note of the language the note is in, its `data.lang` as the content index derives it. |
|
|
879
878
|
|
|
880
879
|
### map
|
|
881
880
|
|
|
@@ -12,29 +12,28 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Turn Changesets' output for the newest release into its final shape.
|
|
16
16
|
*
|
|
17
17
|
* `@heroiclands/package-build/changelog` (`changelog.cjs`) writes each
|
|
18
|
-
* changeset's summary into `CHANGELOG.md`
|
|
19
|
-
* `###
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* model `changelog check` reads a block's label from — rather than parsing
|
|
32
|
-
* the markdown a second way.
|
|
18
|
+
* changeset's summary into `CHANGELOG.md` verbatim, under a
|
|
19
|
+
* `### Major Changes`, `### Minor Changes` or `### Patch Changes` heading,
|
|
20
|
+
* one block per changeset in whatever order Changesets read the files. A
|
|
21
|
+
* release is read by subject, not by bump, so `groupChangelogText` removes
|
|
22
|
+
* every one of those headings, merges the blocks that share a label into one,
|
|
23
|
+
* and orders the labels as the changelog `labels` declares them. A label the list
|
|
24
|
+
* does not declare follows the declared ones, in order of first appearance,
|
|
25
|
+
* and `Miscellaneous`, which every repository has without declaring it, is
|
|
26
|
+
* always last.
|
|
27
|
+
*
|
|
28
|
+
* It reuses {@link module:engine/changelog-lint.topLevelBlocks}, the block
|
|
29
|
+
* model `changelog check` reads a block's label from, rather than parsing the
|
|
30
|
+
* markdown a second way.
|
|
33
31
|
*
|
|
34
32
|
* @module
|
|
35
33
|
*/
|
|
36
34
|
|
|
37
35
|
import {
|
|
36
|
+
MISCELLANEOUS,
|
|
38
37
|
codeLineSet,
|
|
39
38
|
lineColOf,
|
|
40
39
|
releaseSectionRange,
|
|
@@ -43,43 +42,13 @@ import {
|
|
|
43
42
|
} from "./changelog-lint.mjs";
|
|
44
43
|
|
|
45
44
|
/** A `### <Bump> Changes` heading — the generated scaffold, never authored. */
|
|
46
|
-
const CHANGES_HEADING_RE = /^### (?:Major|Minor|Patch) Changes
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* A label, its complete opening paragraph and its remaining items.
|
|
50
|
-
*
|
|
51
|
-
* @param {string} blockText - One block, as {@link topLevelBlocks} collects
|
|
52
|
-
* it — its first line is the label line for a labelled block.
|
|
53
|
-
* @returns {{labelLine: string, introduction: string, rest: string}} The
|
|
54
|
-
* introduction retains hard wrapping and stops before a bullet or code.
|
|
55
|
-
*/
|
|
56
|
-
function splitLabelBlock(blockText) {
|
|
57
|
-
const lines = blockText.replace(/\s+$/, "").split("\n");
|
|
58
|
-
const labelLine = /^\*\*[^*]+\*\*/.exec(lines[0])[0];
|
|
59
|
-
const inline = lines[0].slice(labelLine.length).trimStart();
|
|
60
|
-
const introduction = inline ? [inline] : [];
|
|
61
|
-
const codeLines = codeLineSet(blockText);
|
|
62
|
-
let cursor = 1;
|
|
63
|
-
while (
|
|
64
|
-
cursor < lines.length &&
|
|
65
|
-
lines[cursor].trim() !== "" &&
|
|
66
|
-
!/^-\s/.test(lines[cursor]) &&
|
|
67
|
-
!codeLines.has(cursor + 1)
|
|
68
|
-
)
|
|
69
|
-
introduction.push(lines[cursor++]);
|
|
70
|
-
return {
|
|
71
|
-
labelLine,
|
|
72
|
-
introduction: introduction.join("\n"),
|
|
73
|
-
rest: lines.slice(cursor).join("\n").replace(/^\n+/, ""),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
45
|
+
const CHANGES_HEADING_RE = /^### (?:Major|Minor|Patch) Changes\s*$/;
|
|
76
46
|
|
|
77
47
|
/**
|
|
78
48
|
* A block's top-level bullets and the prose or code between them, in source
|
|
79
49
|
* order. A bullet retains its indented continuation and nested items.
|
|
80
50
|
*
|
|
81
|
-
* @param {string} text - A block's content, label line already stripped
|
|
82
|
-
* a lead block's whole text).
|
|
51
|
+
* @param {string} text - A block's content, label line already stripped.
|
|
83
52
|
* @returns {string[]}
|
|
84
53
|
*/
|
|
85
54
|
function itemsOf(text) {
|
|
@@ -111,7 +80,7 @@ function itemsOf(text) {
|
|
|
111
80
|
|
|
112
81
|
/**
|
|
113
82
|
* The first occurrence of each item, exact-duplicate text dropped, order
|
|
114
|
-
* preserved
|
|
83
|
+
* preserved.
|
|
115
84
|
*
|
|
116
85
|
* @param {string[]} items
|
|
117
86
|
* @returns {string[]}
|
|
@@ -129,66 +98,43 @@ function dedupeItems(items) {
|
|
|
129
98
|
}
|
|
130
99
|
|
|
131
100
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* already uses.
|
|
136
|
-
*
|
|
137
|
-
* @param {string[]} items
|
|
138
|
-
* @returns {string}
|
|
139
|
-
*/
|
|
140
|
-
function joinItems(items) {
|
|
141
|
-
if (items.length === 0) return "";
|
|
142
|
-
const allBullets = items.every((item) => /^-\s/.test(item));
|
|
143
|
-
return items.join(allBullets ? "\n" : "\n\n");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* One label's blocks (or every unlabelled one), folded into the single
|
|
148
|
-
* block `group` writes for it.
|
|
149
|
-
*
|
|
150
|
-
* A single block with no duplicate items retains its own spacing. Merged
|
|
151
|
-
* opening paragraphs form one hard-wrapped introduction before the items,
|
|
152
|
-
* so paragraph boundaries cannot turn labelled text into unlabelled lead.
|
|
101
|
+
* One label's blocks, folded into the single block `group` writes for it:
|
|
102
|
+
* the label line, a blank line, then every bullet in file order. Bullets are
|
|
103
|
+
* adjacent; any other item is separated by a blank line.
|
|
153
104
|
*
|
|
154
105
|
* @param {Array<{label: string|null, text: string}>} blocks - Every block
|
|
155
|
-
* sharing one label, in file order.
|
|
106
|
+
* sharing one label, in file order.
|
|
156
107
|
* @returns {string}
|
|
157
108
|
*/
|
|
158
109
|
function mergeGroup(blocks) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return
|
|
110
|
+
const labelled = blocks[0].label !== null;
|
|
111
|
+
const items = dedupeItems(
|
|
112
|
+
blocks.flatMap((block) => {
|
|
113
|
+
if (!labelled) return itemsOf(block.text);
|
|
114
|
+
const [, ...rest] = block.text.replace(/\s+$/, "").split("\n");
|
|
115
|
+
return itemsOf(rest.join("\n"));
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
const joined = items.join(items.every((item) => /^-\s/.test(item)) ? "\n" : "\n\n");
|
|
119
|
+
if (!labelled) return joined;
|
|
120
|
+
const labelLine = blocks[0].text.split("\n")[0].trimEnd();
|
|
121
|
+
return joined ? `${labelLine}\n\n${joined}` : labelLine;
|
|
171
122
|
}
|
|
172
123
|
|
|
173
124
|
/**
|
|
174
|
-
* Where a group sorts
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* appeared in.
|
|
183
|
-
*
|
|
184
|
-
* @param {string|null} key - A group's label, or `null` for the lead group.
|
|
185
|
-
* @param {string[]} appearanceOrder - Every key, in the order its first
|
|
186
|
-
* block appeared in the section.
|
|
187
|
-
* @param {readonly string[]|null} labels - `changelog.labels`, or `null`.
|
|
125
|
+
* Where a group sorts in its release: an unlabelled block first, then the
|
|
126
|
+
* declared labels in their declared order, then any other label in order of
|
|
127
|
+
* first appearance, and `Miscellaneous` last. With no vocabulary declared,
|
|
128
|
+
* every label but `Miscellaneous` keeps its order of first appearance.
|
|
129
|
+
*
|
|
130
|
+
* @param {string|null} key - A group's label, or `null` for an unlabelled one.
|
|
131
|
+
* @param {string[]} appearanceOrder - Every key, in order of first appearance.
|
|
132
|
+
* @param {readonly string[]|null} labels - the changelog `labels`, or `null`.
|
|
188
133
|
* @returns {[number, number]} `[tier, rank]`, compared tier first.
|
|
189
134
|
*/
|
|
190
135
|
function groupRank(key, appearanceOrder, labels) {
|
|
191
136
|
if (key === null) return [-1, 0];
|
|
137
|
+
if (key === MISCELLANEOUS) return [2, 0];
|
|
192
138
|
if (labels) {
|
|
193
139
|
const configured = labels.indexOf(key);
|
|
194
140
|
return configured === -1 ? [1, appearanceOrder.indexOf(key)] : [0, configured];
|
|
@@ -197,26 +143,40 @@ function groupRank(key, appearanceOrder, labels) {
|
|
|
197
143
|
}
|
|
198
144
|
|
|
199
145
|
/**
|
|
200
|
-
*
|
|
146
|
+
* `changelog group`: give the newest release its final shape.
|
|
147
|
+
*
|
|
148
|
+
* Every earlier release section is untouched, byte for byte. Running this on
|
|
149
|
+
* its own output is a no-op.
|
|
201
150
|
*
|
|
202
|
-
* @param {string}
|
|
203
|
-
*
|
|
204
|
-
* @param {readonly string[]|null} labels - `
|
|
205
|
-
* order
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* first-occurring line within `bodyText` — empty when `labels` is `null`,
|
|
210
|
-
* since nothing is "unknown" against no vocabulary.
|
|
151
|
+
* @param {string} text - The changelog's full contents.
|
|
152
|
+
* @param {object} [opts]
|
|
153
|
+
* @param {readonly string[]|null} [opts.labels] - the changelog `labels`, in
|
|
154
|
+
* display order. `null`/absent orders by first appearance and reports
|
|
155
|
+
* nothing as undeclared.
|
|
156
|
+
* @returns {{text: string, findings: Array<{line: number,
|
|
157
|
+
* severity: "warning", message: string}>}}
|
|
211
158
|
*/
|
|
212
|
-
function
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
if (blocks.length === 0) return { text: "", unknown: [] };
|
|
159
|
+
export function groupChangelogText(text, { labels = null } = {}) {
|
|
160
|
+
const range = releaseSectionRange(text);
|
|
161
|
+
if (!range) return { text, findings: [] };
|
|
216
162
|
|
|
217
|
-
|
|
163
|
+
const section = text.slice(range.start, range.end);
|
|
164
|
+
const headingEnd = section.indexOf("\n");
|
|
165
|
+
if (headingEnd === -1) return { text, findings: [] };
|
|
166
|
+
const heading = section.slice(0, headingEnd);
|
|
167
|
+
|
|
168
|
+
// The scaffold headings become blank lines, so every line keeps its number.
|
|
169
|
+
const codeLines = codeLineSet(section.slice(headingEnd + 1));
|
|
170
|
+
const body = section
|
|
171
|
+
.slice(headingEnd + 1)
|
|
172
|
+
.split("\n")
|
|
173
|
+
.map((line, i) => (!codeLines.has(i + 1) && CHANGES_HEADING_RE.test(line) ? "" : line))
|
|
174
|
+
.join("\n");
|
|
175
|
+
|
|
176
|
+
const blocks = topLevelBlocks(body, codeLines);
|
|
177
|
+
/** @type {Array<string|null>} */
|
|
218
178
|
const appearanceOrder = [];
|
|
219
|
-
/** @type {Map<string|null, Array<{label: string|null, text: string}>>} */
|
|
179
|
+
/** @type {Map<string|null, Array<{startLine: number, label: string|null, text: string}>>} */
|
|
220
180
|
const groups = new Map();
|
|
221
181
|
for (const block of blocks) {
|
|
222
182
|
if (!groups.has(block.label)) {
|
|
@@ -225,77 +185,28 @@ function groupSection(bodyText, labels) {
|
|
|
225
185
|
}
|
|
226
186
|
groups.get(block.label).push(block);
|
|
227
187
|
}
|
|
228
|
-
|
|
229
188
|
const sortedKeys = [...appearanceOrder].sort((a, b) => {
|
|
230
189
|
const [aTier, aRank] = groupRank(a, appearanceOrder, labels);
|
|
231
190
|
const [bTier, bRank] = groupRank(b, appearanceOrder, labels);
|
|
232
191
|
return aTier - bTier || aRank - bRank;
|
|
233
192
|
});
|
|
234
193
|
|
|
235
|
-
const
|
|
236
|
-
labels === null ?
|
|
237
|
-
[]
|
|
238
|
-
: sortedKeys
|
|
239
|
-
.filter((key) => key !== null && !labels.includes(key))
|
|
240
|
-
.map((key) => ({ label: key, startLine: groups.get(key)[0].startLine }));
|
|
241
|
-
|
|
242
|
-
const text = sortedKeys.map((key) => mergeGroup(groups.get(key))).join("\n\n");
|
|
243
|
-
return { text, unknown };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* `changelog group`: fold the newest release's changeset blocks together by
|
|
248
|
-
* their bold label, order them, and file an undeclared one last.
|
|
249
|
-
*
|
|
250
|
-
* Every earlier release section is untouched, byte for byte — only the
|
|
251
|
-
* first `## <version>` section's `### <Bump> Changes` bodies are rewritten,
|
|
252
|
-
* each independently (a label groups within its own bump level, never
|
|
253
|
-
* across one). Running this on its own output is a no-op: a release already
|
|
254
|
-
* in label order, with each label merged to one block, groups to itself.
|
|
255
|
-
*
|
|
256
|
-
* @param {string} text - The changelog's full contents.
|
|
257
|
-
* @param {object} [opts]
|
|
258
|
-
* @param {readonly string[]|null} [opts.labels] - `changelog.labels`, in
|
|
259
|
-
* display order. `null`/absent orders every group by first appearance
|
|
260
|
-
* instead, lead paragraph first, and files nothing as unknown.
|
|
261
|
-
* @returns {{text: string, findings: Array<{line: number,
|
|
262
|
-
* severity: "warning", message: string}>}}
|
|
263
|
-
*/
|
|
264
|
-
export function groupChangelogText(text, { labels = null } = {}) {
|
|
265
|
-
const range = releaseSectionRange(text);
|
|
266
|
-
if (!range) return { text, findings: [] };
|
|
267
|
-
|
|
268
|
-
const section = text.slice(range.start, range.end);
|
|
269
|
-
const codeLines = codeLineSet(section);
|
|
270
|
-
const headings = [...section.matchAll(CHANGES_HEADING_RE)].filter(
|
|
271
|
-
(heading) => !codeLines.has(lineColOf(section, heading.index).line),
|
|
272
|
-
);
|
|
273
|
-
if (headings.length === 0) return { text, findings: [] };
|
|
274
|
-
|
|
275
|
-
let rebuilt = section.slice(0, headings[0].index);
|
|
276
|
-
/** @type {Array<{line: number, severity: "warning", message: string}>} */
|
|
194
|
+
const { line: bodyFirstLine } = lineColOf(text, range.start + headingEnd + 1);
|
|
277
195
|
const findings = [];
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const bodyStart = heading.index + heading[0].length;
|
|
282
|
-
const bodyEnd = i + 1 < headings.length ? headings[i + 1].index : section.length;
|
|
283
|
-
const { text: grouped, unknown } = groupSection(section.slice(bodyStart, bodyEnd), labels);
|
|
284
|
-
|
|
285
|
-
const isVeryLast = i === headings.length - 1 && range.end === text.length;
|
|
286
|
-
rebuilt += heading[0] + (grouped ? `\n\n${grouped}` : "") + (isVeryLast ? "\n" : "\n\n");
|
|
287
|
-
|
|
288
|
-
const { line: bodyFirstLine } = lineColOf(text, range.start + bodyStart);
|
|
289
|
-
for (const { label, startLine } of unknown) {
|
|
196
|
+
if (labels)
|
|
197
|
+
for (const key of sortedKeys) {
|
|
198
|
+
if (key === null || key === MISCELLANEOUS || labels.includes(key)) continue;
|
|
290
199
|
findings.push({
|
|
291
|
-
line: bodyFirstLine + (startLine - 1),
|
|
292
|
-
severity: "warning",
|
|
200
|
+
line: bodyFirstLine + (groups.get(key)[0].startLine - 1),
|
|
201
|
+
severity: /** @type {const} */ ("warning"),
|
|
293
202
|
message:
|
|
294
|
-
`changelog-group/unknown-label "${
|
|
295
|
-
"`
|
|
203
|
+
`changelog-group/unknown-label "${key}" is not declared in ` +
|
|
204
|
+
"the changelog `labels` — filed after the declared labels, in order of first appearance",
|
|
296
205
|
});
|
|
297
206
|
}
|
|
298
|
-
}
|
|
299
207
|
|
|
208
|
+
const grouped = sortedKeys.map((key) => mergeGroup(groups.get(key))).join("\n\n");
|
|
209
|
+
const rebuilt =
|
|
210
|
+
heading + (grouped ? `\n\n${grouped}` : "") + (range.end === text.length ? "\n" : "\n\n");
|
|
300
211
|
return { text: text.slice(0, range.start) + rebuilt + text.slice(range.end), findings };
|
|
301
212
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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 labels a repository's changesets group under.
|
|
16
|
+
*
|
|
17
|
+
* They are the options of the changelog entry in `.changeset/config.json`,
|
|
18
|
+
* beside the generator they configure:
|
|
19
|
+
*
|
|
20
|
+
* ```json
|
|
21
|
+
* "changelog": ["@heroiclands/package-build/changelog", { "labels": ["Verse", "Rendering"] }]
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* `changelog check` accepts a label only if it is listed there or is
|
|
25
|
+
* `Miscellaneous`, which every repository has and never declares.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
|
|
33
|
+
import { MISCELLANEOUS } from "./changelog-lint.mjs";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read `labels` from the changelog options in `.changeset/config.json`.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} [cwd] - The repository root.
|
|
39
|
+
* @returns {readonly string[]|null} The labels in display order, or `null`
|
|
40
|
+
* when the file, the options or `labels` is absent.
|
|
41
|
+
* @throws {Error} When `config.json` is not JSON, or `labels` is not an array
|
|
42
|
+
* of distinct non-empty strings or names `Miscellaneous`.
|
|
43
|
+
*/
|
|
44
|
+
export function readChangelogLabels(cwd = process.cwd()) {
|
|
45
|
+
const file = path.join(cwd, ".changeset", "config.json");
|
|
46
|
+
if (!fs.existsSync(file)) return null;
|
|
47
|
+
const where = path.relative(cwd, file);
|
|
48
|
+
let config;
|
|
49
|
+
try {
|
|
50
|
+
config = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new Error(`${where}: is not valid JSON (${error.message})`);
|
|
53
|
+
}
|
|
54
|
+
const entry = config?.changelog;
|
|
55
|
+
const options = Array.isArray(entry) ? entry[1] : undefined;
|
|
56
|
+
if (options === undefined || options === null) return null;
|
|
57
|
+
if (typeof options !== "object" || Array.isArray(options))
|
|
58
|
+
throw new Error(`${where}: the changelog options must be an object`);
|
|
59
|
+
if (options.labels === undefined) return null;
|
|
60
|
+
const { labels } = options;
|
|
61
|
+
const bad = (problem) => new Error(`${where}: changelog \`labels\` ${problem}`);
|
|
62
|
+
if (!Array.isArray(labels)) throw bad("must be an array of strings");
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
for (const label of labels) {
|
|
65
|
+
if (typeof label !== "string" || label.trim() === "")
|
|
66
|
+
throw bad("must hold only non-empty strings");
|
|
67
|
+
if (label === MISCELLANEOUS)
|
|
68
|
+
throw bad(`declares \`${MISCELLANEOUS}\`, which is always available and always last`);
|
|
69
|
+
if (seen.has(label)) throw bad(`declares \`${label}\` more than once`);
|
|
70
|
+
seen.add(label);
|
|
71
|
+
}
|
|
72
|
+
return Object.freeze([...labels]);
|
|
73
|
+
}
|