@heroiclands/package-build 25.1.2 → 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 +36 -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/encoding-core.md +11 -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 +329 -187
- package/docs/reference/note-types.md +274 -274
- 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/encoding-lexicon.mjs +12 -2
- 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 +70 -10
- 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 +35 -13
- 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/language-names.d.mts +3 -0
- 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
|
@@ -607,73 +607,196 @@ function checkCodeLikeTokens(text, maskedRegions) {
|
|
|
607
607
|
}
|
|
608
608
|
|
|
609
609
|
/**
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
610
|
+
* The label every repository may use without declaring it, for the changes
|
|
611
|
+
* that fit no declared label. `changelog group` writes it last.
|
|
612
|
+
*/
|
|
613
|
+
export const MISCELLANEOUS = "Miscellaneous";
|
|
614
|
+
|
|
615
|
+
/** A label line that holds the label and nothing else. */
|
|
616
|
+
const LABEL_ONLY_RE = /^\*\*([^*]+)\*\*[ \t]*$/;
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Hold a changeset body, or a release section's body, to the house shape:
|
|
620
|
+
* one or more blocks, each a `**Label**` line on its own followed by bullets
|
|
621
|
+
* and nothing else.
|
|
622
|
+
*
|
|
623
|
+
* Findings: a `lead-paragraph` (prose before any label), `unlabelled` (a
|
|
624
|
+
* bullet before any label), `stray-prose` (prose after a label opens, between
|
|
625
|
+
* or after its bullets), `label-line` (text sharing the label's line),
|
|
626
|
+
* `empty-block` (a label with no bullet) and `unknown-label` (a label neither
|
|
627
|
+
* declared in the changelog `labels` nor {@link MISCELLANEOUS}). Nested lists,
|
|
628
|
+
* headings and bullet length are separate rules. Code regions are skipped; a
|
|
629
|
+
* fenced block has its own finding.
|
|
630
|
+
*
|
|
631
|
+
* @param {string} text - Section text.
|
|
632
|
+
* @param {Set<number>} codeLines - Lines inside a code region.
|
|
633
|
+
* @param {Set<number>} scaffoldLines - Generated lines, never authored.
|
|
634
|
+
* @param {readonly string[]|null|undefined} labels - the changelog `labels`, or
|
|
635
|
+
* `null`/`undefined` when the repository declares none.
|
|
623
636
|
* @returns {RelativeFinding[]}
|
|
624
637
|
*/
|
|
625
|
-
function
|
|
626
|
-
|
|
638
|
+
function checkShape(text, codeLines, scaffoldLines, labels) {
|
|
639
|
+
const lines = text.split("\n");
|
|
640
|
+
const declared = labels ?? [];
|
|
627
641
|
/** @type {RelativeFinding[]} */
|
|
628
642
|
const findings = [];
|
|
629
|
-
|
|
630
|
-
|
|
643
|
+
/** @type {{line: number, label: string, bullets: number}|null} */
|
|
644
|
+
let open = null;
|
|
645
|
+
let reportedLead = false;
|
|
646
|
+
const closeBlock = () => {
|
|
647
|
+
if (open && open.bullets === 0)
|
|
648
|
+
findings.push({
|
|
649
|
+
line: open.line,
|
|
650
|
+
column: 1,
|
|
651
|
+
severity: "error",
|
|
652
|
+
message:
|
|
653
|
+
`changelog-check/empty-block the block labelled "${open.label}" has no ` +
|
|
654
|
+
"bullets — a block is a label line, then bullets",
|
|
655
|
+
});
|
|
656
|
+
open = null;
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
for (let i = 0; i < lines.length; i++) {
|
|
660
|
+
const lineNo = i + 1;
|
|
661
|
+
const raw = lines[i];
|
|
662
|
+
if (codeLines.has(lineNo) || scaffoldLines.has(lineNo)) continue;
|
|
663
|
+
if (raw.trim() === "") continue;
|
|
664
|
+
const atColumnZero = !/^[ \t]/.test(raw);
|
|
665
|
+
if (!atColumnZero) continue; // a bullet's continuation, or a nested item
|
|
666
|
+
if (/^#{1,6}\s/.test(raw)) {
|
|
667
|
+
closeBlock();
|
|
668
|
+
continue; // reported by the heading rule
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const label = LABEL_LINE_RE.exec(raw)?.[1];
|
|
672
|
+
if (label !== undefined) {
|
|
673
|
+
closeBlock();
|
|
674
|
+
open = { line: lineNo, label, bullets: 0 };
|
|
675
|
+
if (!LABEL_ONLY_RE.test(raw))
|
|
676
|
+
findings.push({
|
|
677
|
+
line: lineNo,
|
|
678
|
+
column: 1,
|
|
679
|
+
severity: "error",
|
|
680
|
+
message:
|
|
681
|
+
"changelog-check/label-line the label stands on a line of its own, " +
|
|
682
|
+
"`**Label**`, with the bullets beneath it",
|
|
683
|
+
});
|
|
684
|
+
if (label !== MISCELLANEOUS && !declared.includes(label))
|
|
685
|
+
findings.push({
|
|
686
|
+
line: lineNo,
|
|
687
|
+
column: 3, // right after the opening `**`
|
|
688
|
+
severity: "error",
|
|
689
|
+
message:
|
|
690
|
+
`changelog-check/unknown-label "${label}" is not a declared label — ` +
|
|
691
|
+
(declared.length > 0 ?
|
|
692
|
+
`declared: ${declared.join(", ")}; ${MISCELLANEOUS} is always available`
|
|
693
|
+
: `declare \`labels\` in the changelog options of .changeset/config.json, ` +
|
|
694
|
+
`or use ${MISCELLANEOUS}`),
|
|
695
|
+
});
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (TOP_BULLET_RE.test(raw)) {
|
|
700
|
+
if (open) open.bullets++;
|
|
701
|
+
else
|
|
702
|
+
findings.push({
|
|
703
|
+
line: lineNo,
|
|
704
|
+
column: 1,
|
|
705
|
+
severity: "error",
|
|
706
|
+
message:
|
|
707
|
+
"changelog-check/unlabelled a bullet belongs under a `**Label**` line — " +
|
|
708
|
+
`use a declared label, or ${MISCELLANEOUS}`,
|
|
709
|
+
});
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (!open) {
|
|
714
|
+
if (!reportedLead)
|
|
715
|
+
findings.push({
|
|
716
|
+
line: lineNo,
|
|
717
|
+
column: 1,
|
|
718
|
+
severity: "error",
|
|
719
|
+
message:
|
|
720
|
+
"changelog-check/lead-paragraph a changeset is labelled blocks of " +
|
|
721
|
+
"bullets; a paragraph before the first `**Label**` has no place",
|
|
722
|
+
});
|
|
723
|
+
reportedLead = true;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
631
726
|
findings.push({
|
|
632
|
-
line:
|
|
633
|
-
column:
|
|
634
|
-
severity: "
|
|
727
|
+
line: lineNo,
|
|
728
|
+
column: 1,
|
|
729
|
+
severity: "error",
|
|
635
730
|
message:
|
|
636
|
-
|
|
637
|
-
"
|
|
638
|
-
`${labels.join(", ")}) — add it there, or correct the label`,
|
|
731
|
+
"changelog-check/stray-prose only bullets sit under a label — move this " +
|
|
732
|
+
"into a bullet, or drop it",
|
|
639
733
|
});
|
|
640
734
|
}
|
|
735
|
+
closeBlock();
|
|
641
736
|
return findings;
|
|
642
737
|
}
|
|
643
738
|
|
|
644
739
|
/**
|
|
645
740
|
* The 1-based lines a caller drops from the heading rule because they are the
|
|
646
|
-
* generated scaffold, never authored prose:
|
|
647
|
-
* opening
|
|
741
|
+
* generated scaffold, never authored prose: a release section's own
|
|
742
|
+
* `## <version>` opening. Only a release has one.
|
|
648
743
|
*
|
|
649
744
|
* @param {string} text - Section text.
|
|
745
|
+
* @param {boolean} release - Whether `text` is a release section.
|
|
650
746
|
* @returns {Set<number>}
|
|
651
747
|
*/
|
|
652
|
-
function scaffoldLineSet(text) {
|
|
653
|
-
const lines = text.split("\n");
|
|
748
|
+
function scaffoldLineSet(text, release) {
|
|
654
749
|
const set = new Set();
|
|
655
|
-
if (RELEASE_HEADING_RE.test(
|
|
656
|
-
for (let i = 0; i < lines.length; i++) {
|
|
657
|
-
if (CHANGES_HEADING_RE.test(lines[i])) set.add(i + 1);
|
|
658
|
-
}
|
|
750
|
+
if (release && RELEASE_HEADING_RE.test(text.split("\n")[0] ?? "")) set.add(1);
|
|
659
751
|
return set;
|
|
660
752
|
}
|
|
661
753
|
|
|
754
|
+
/**
|
|
755
|
+
* A `### <Bump> Changes` heading in a release section — the scaffold
|
|
756
|
+
* `changelog group` removes, so one that remains means group has not run.
|
|
757
|
+
*
|
|
758
|
+
* @param {string} text - Release section text.
|
|
759
|
+
* @param {Set<number>} codeLines
|
|
760
|
+
* @returns {RelativeFinding[]}
|
|
761
|
+
*/
|
|
762
|
+
function checkChangesHeadings(text, codeLines) {
|
|
763
|
+
/** @type {RelativeFinding[]} */
|
|
764
|
+
const findings = [];
|
|
765
|
+
text.split("\n").forEach((line, i) => {
|
|
766
|
+
if (codeLines.has(i + 1) || !CHANGES_HEADING_RE.test(line)) return;
|
|
767
|
+
findings.push({
|
|
768
|
+
line: i + 1,
|
|
769
|
+
column: 1,
|
|
770
|
+
severity: "error",
|
|
771
|
+
message:
|
|
772
|
+
"changelog-check/changes-heading a `### … Changes` heading survives in the " +
|
|
773
|
+
"release — run `package-build changelog group`, which removes it",
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
return findings;
|
|
777
|
+
}
|
|
778
|
+
|
|
662
779
|
/**
|
|
663
780
|
* Run every rule over one section of release prose.
|
|
664
781
|
*
|
|
665
782
|
* @param {string} text - The section, already isolated by the caller.
|
|
666
783
|
* @param {object} [opts]
|
|
667
|
-
* @param {readonly string[]|null} [opts.labels] - `
|
|
668
|
-
* {@link
|
|
784
|
+
* @param {readonly string[]|null} [opts.labels] - the changelog `labels`, for
|
|
785
|
+
* {@link checkShape}.
|
|
786
|
+
* @param {boolean} [opts.release] - `text` is a release section, whose
|
|
787
|
+
* `## <version>` line is scaffold and whose `### … Changes` headings are
|
|
788
|
+
* findings.
|
|
669
789
|
* @returns {RelativeFinding[]} Findings with line numbers relative to `text`.
|
|
670
790
|
*/
|
|
671
|
-
function lintSection(text, { labels = null } = {}) {
|
|
791
|
+
function lintSection(text, { labels = null, release = false } = {}) {
|
|
672
792
|
const codeLines = codeLineSet(text);
|
|
673
793
|
const maskedRegions = codeRegions(text, { spans: true });
|
|
674
|
-
const scaffoldLines = scaffoldLineSet(text);
|
|
794
|
+
const scaffoldLines = scaffoldLineSet(text, release);
|
|
795
|
+
if (release)
|
|
796
|
+
text.split("\n").forEach((line, i) => {
|
|
797
|
+
if (CHANGES_HEADING_RE.test(line)) scaffoldLines.add(i + 1);
|
|
798
|
+
});
|
|
675
799
|
const bullets = topLevelBullets(text, codeLines);
|
|
676
|
-
const blocks = topLevelBlocks(text, codeLines, scaffoldLines);
|
|
677
800
|
|
|
678
801
|
return [
|
|
679
802
|
...checkCommitHash(text, codeLines, scaffoldLines),
|
|
@@ -687,7 +810,8 @@ function lintSection(text, { labels = null } = {}) {
|
|
|
687
810
|
...checkTooManyBullets(bullets),
|
|
688
811
|
...checkTooManyLines(text),
|
|
689
812
|
...checkCodeLikeTokens(text, maskedRegions),
|
|
690
|
-
...
|
|
813
|
+
...checkShape(text, codeLines, scaffoldLines, labels),
|
|
814
|
+
...(release ? checkChangesHeadings(text, codeLines) : []),
|
|
691
815
|
].sort((a, b) => a.line - b.line || (a.column ?? 0) - (b.column ?? 0));
|
|
692
816
|
}
|
|
693
817
|
|
|
@@ -738,9 +862,9 @@ function extractReleaseSection(text) {
|
|
|
738
862
|
*
|
|
739
863
|
* @param {string} text - The file's full contents, frontmatter included.
|
|
740
864
|
* @param {object} [opts]
|
|
741
|
-
* @param {readonly string[]|null} [opts.labels] - `
|
|
742
|
-
* display order, or `null`/absent when the repository declares none
|
|
743
|
-
* {@link
|
|
865
|
+
* @param {readonly string[]|null} [opts.labels] - the changelog `labels`, in
|
|
866
|
+
* display order, or `null`/absent when the repository declares none, in
|
|
867
|
+
* which case only {@link MISCELLANEOUS} is accepted.
|
|
744
868
|
* @returns {{findings: Array<{line: number, column?: number,
|
|
745
869
|
* severity: "error"|"warning", message: string}>}}
|
|
746
870
|
*/
|
|
@@ -758,9 +882,9 @@ export function lintChangesetText(text, { labels = null } = {}) {
|
|
|
758
882
|
*
|
|
759
883
|
* @param {string} text - The changelog's full contents.
|
|
760
884
|
* @param {object} [opts]
|
|
761
|
-
* @param {readonly string[]|null} [opts.labels] - `
|
|
762
|
-
* display order, or `null`/absent when the repository declares none
|
|
763
|
-
* {@link
|
|
885
|
+
* @param {readonly string[]|null} [opts.labels] - the changelog `labels`, in
|
|
886
|
+
* display order, or `null`/absent when the repository declares none, in
|
|
887
|
+
* which case only {@link MISCELLANEOUS} is accepted.
|
|
764
888
|
* @returns {{findings: Array<{line?: number, column?: number,
|
|
765
889
|
* severity: "error"|"warning", message: string}>}}
|
|
766
890
|
*/
|
|
@@ -776,7 +900,7 @@ export function lintReleaseText(text, { labels = null } = {}) {
|
|
|
776
900
|
],
|
|
777
901
|
};
|
|
778
902
|
}
|
|
779
|
-
const findings = lintSection(section.body, { labels }).map((f) => ({
|
|
903
|
+
const findings = lintSection(section.body, { labels, release: true }).map((f) => ({
|
|
780
904
|
...f,
|
|
781
905
|
line: f.line + section.startLine - 1,
|
|
782
906
|
}));
|
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
* `[[#id]]` link read the label.
|
|
27
27
|
* 3. **Expressions.** `{{…}}` is evaluated — `sql`, `ref`, dates and the
|
|
28
28
|
* helpers — see {@link module:engine/markdown-expressions.renderMarkdownExpressions}.
|
|
29
|
-
* 4. **
|
|
29
|
+
* 4. **First uses.** The first use of each name in the author's prose is
|
|
30
|
+
* marked bold — see {@link module:engine/first-use.boldFirstUses}. The
|
|
31
|
+
* generated sections take no mark.
|
|
32
|
+
* 5. **Wikilinks.** Each `[[…]]` outside code is resolved against the index by
|
|
30
33
|
* {@link module:engine/wikilinks.resolveWikilinks}, and written in the
|
|
31
34
|
* target's form.
|
|
32
35
|
*
|
|
@@ -55,7 +58,8 @@ import { collectAnchors, eventAnchors } from "./anchors.mjs";
|
|
|
55
58
|
import { protectCode } from "./code-fences.mjs";
|
|
56
59
|
import { scanFigures } from "./content-figures.mjs";
|
|
57
60
|
import { expandContentTables } from "./content-tables.mjs";
|
|
58
|
-
import {
|
|
61
|
+
import { boldFirstUses } from "./first-use.mjs";
|
|
62
|
+
import { GENERATED_BOUNDARY, markGenerated, splitGenerated } from "./generated-sections.mjs";
|
|
59
63
|
import { convertNoteWikilinks } from "./helpers.mjs";
|
|
60
64
|
import { renderMarkdownExpressions } from "./markdown-expressions.mjs";
|
|
61
65
|
import { reckoningContext } from "./reckoning-markers.mjs";
|
|
@@ -239,35 +243,32 @@ export function expandContent(
|
|
|
239
243
|
]),
|
|
240
244
|
);
|
|
241
245
|
|
|
242
|
-
const expressions = renderMarkdownExpressions(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
: { get: (id) => figuresById.get(id), link: foundryRefLink },
|
|
269
|
-
},
|
|
270
|
-
);
|
|
246
|
+
const expressions = renderMarkdownExpressions(markGenerated(tables.markdown, generatedFrom), {
|
|
247
|
+
fm,
|
|
248
|
+
dates: web ? index.dateContext : reckoningContext(index),
|
|
249
|
+
file,
|
|
250
|
+
bodyLine,
|
|
251
|
+
sqlResults: prepared.inline,
|
|
252
|
+
events,
|
|
253
|
+
figures:
|
|
254
|
+
web ?
|
|
255
|
+
{
|
|
256
|
+
get: (id) => figuresById.get(id),
|
|
257
|
+
// A cross-note `ref` resolves the written address
|
|
258
|
+
// exactly as a wikilink does, against every page's
|
|
259
|
+
// figures scanned before any page's expressions render.
|
|
260
|
+
note: (address) =>
|
|
261
|
+
resolveCrossNoteFigures(address, {
|
|
262
|
+
contentTypes: index.contentTypes,
|
|
263
|
+
packages: index.packages,
|
|
264
|
+
noIndexPackages: index.noIndexPackages,
|
|
265
|
+
contentPackage: index.contentPackage,
|
|
266
|
+
siteIndexMap: index.index,
|
|
267
|
+
figuresByAddress,
|
|
268
|
+
}),
|
|
269
|
+
}
|
|
270
|
+
: { get: (id) => figuresById.get(id), link: foundryRefLink },
|
|
271
|
+
});
|
|
271
272
|
|
|
272
273
|
const findings = {
|
|
273
274
|
tables: tables.errors,
|
|
@@ -276,11 +277,24 @@ export function expandContent(
|
|
|
276
277
|
links: [],
|
|
277
278
|
};
|
|
278
279
|
|
|
280
|
+
// The first use of each name is counted in the author's prose alone.
|
|
281
|
+
const sections = splitGenerated(expressions.markdown);
|
|
282
|
+
const named = {
|
|
283
|
+
authored: boldFirstUses(sections.authored, {
|
|
284
|
+
types: web ? index.contentTypes : index.types,
|
|
285
|
+
packages: index.packages,
|
|
286
|
+
noIndexPackages: index.noIndexPackages,
|
|
287
|
+
contentPackage: index.contentPackage,
|
|
288
|
+
}),
|
|
289
|
+
generated: sections.generated,
|
|
290
|
+
};
|
|
291
|
+
|
|
279
292
|
if (!web) {
|
|
293
|
+
const marked = named.generated ? `${named.authored}\n${named.generated}` : named.authored;
|
|
280
294
|
let converted;
|
|
281
295
|
let linkError;
|
|
282
296
|
try {
|
|
283
|
-
converted = convertNoteWikilinks(
|
|
297
|
+
converted = convertNoteWikilinks(marked, {
|
|
284
298
|
type: fm.type,
|
|
285
299
|
id: fm.id,
|
|
286
300
|
file,
|
|
@@ -295,7 +309,7 @@ export function expandContent(
|
|
|
295
309
|
} catch (error) {
|
|
296
310
|
linkError = error;
|
|
297
311
|
}
|
|
298
|
-
const body = converted?.markdown ??
|
|
312
|
+
const body = converted?.markdown ?? marked;
|
|
299
313
|
return {
|
|
300
314
|
markdown: body,
|
|
301
315
|
authored: body,
|
|
@@ -338,7 +352,14 @@ export function expandContent(
|
|
|
338
352
|
// The book resolves the body whole, so a link's occurrence counts across
|
|
339
353
|
// the author's text and the generated sections it prints after it.
|
|
340
354
|
if (book) {
|
|
341
|
-
const split = splitGenerated(
|
|
355
|
+
const split = splitGenerated(
|
|
356
|
+
protectCode(
|
|
357
|
+
named.generated ?
|
|
358
|
+
`${named.authored}\n${GENERATED_BOUNDARY}\n${named.generated}`
|
|
359
|
+
: named.authored,
|
|
360
|
+
resolve,
|
|
361
|
+
),
|
|
362
|
+
);
|
|
342
363
|
return {
|
|
343
364
|
markdown: split.generated ? `${split.authored}\n${split.generated}` : split.authored,
|
|
344
365
|
authored: split.authored,
|
|
@@ -356,12 +377,11 @@ export function expandContent(
|
|
|
356
377
|
// The author's text and the generated sections are resolved apart, so a
|
|
357
378
|
// caller can tell the links the author wrote from the ones a section
|
|
358
379
|
// derives.
|
|
359
|
-
const
|
|
360
|
-
const authored = protectCode(split.authored, resolve);
|
|
380
|
+
const authored = protectCode(named.authored, resolve);
|
|
361
381
|
const authoredLinks = resolved.length;
|
|
362
|
-
const generated =
|
|
382
|
+
const generated = named.generated ? protectCode(named.generated, resolve) : "";
|
|
363
383
|
return {
|
|
364
|
-
markdown:
|
|
384
|
+
markdown: named.generated ? `${authored}\n${generated}` : authored,
|
|
365
385
|
authored,
|
|
366
386
|
generated,
|
|
367
387
|
lineMap,
|
package/engine/content-index.mjs
CHANGED
|
@@ -87,6 +87,8 @@
|
|
|
87
87
|
|
|
88
88
|
import { isAddressSegment } from "./address-charset.mjs";
|
|
89
89
|
import { configFileIn, resolveNoteCalendars } from "./note-calendars.mjs";
|
|
90
|
+
import { resolveNoteLanguages } from "./note-language.mjs";
|
|
91
|
+
import { nameFormLists } from "./name-forms.mjs";
|
|
90
92
|
import { cultureInheritance, reportCultureParents } from "./culture-parents.mjs";
|
|
91
93
|
import {
|
|
92
94
|
formatDiagnostic,
|
|
@@ -106,6 +108,7 @@ import { AddressLink } from "./address-values.mjs";
|
|
|
106
108
|
import { scanSpans } from "./content-spans.mjs";
|
|
107
109
|
import { rebaseNameSpans } from "./name-markup.mjs";
|
|
108
110
|
import { analyzeNameSpans } from "./names.mjs";
|
|
111
|
+
import { nameRefFindings } from "./name-refs.mjs";
|
|
109
112
|
import { fillEncodingRuns, recordRuns } from "./run-rows.mjs";
|
|
110
113
|
import fs from "node:fs";
|
|
111
114
|
import path from "node:path";
|
|
@@ -249,27 +252,31 @@ export function noteAddress(frontmatter, contentPackage) {
|
|
|
249
252
|
}
|
|
250
253
|
|
|
251
254
|
/**
|
|
252
|
-
* A note's
|
|
255
|
+
* A note's listed name forms reduced to printable 7-bit ASCII, by language.
|
|
253
256
|
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
257
|
+
* A listed form is a name a reader is at least as likely to reach for as the
|
|
258
|
+
* full one — `Killer Whale` for an orca, a people's own name for the people —
|
|
259
|
+
* so anything searching or completing over the index has to match them too,
|
|
260
|
+
* and needs the same keyboard-typeable form {@link asciiName} gives the full
|
|
261
|
+
* name.
|
|
259
262
|
*
|
|
260
|
-
*
|
|
261
|
-
* came from.
|
|
262
|
-
*
|
|
263
|
-
*
|
|
263
|
+
* Each language keeps its authored order, so a caller can pair an entry with
|
|
264
|
+
* the form it came from. A form that leaves nothing printable behind is
|
|
265
|
+
* dropped rather than left as a hole, and a language left with no form is
|
|
266
|
+
* left out.
|
|
264
267
|
*
|
|
265
|
-
* @param {unknown}
|
|
266
|
-
* @returns {
|
|
267
|
-
* has an empty
|
|
268
|
-
*
|
|
268
|
+
* @param {unknown} name - The note's `name`; may be absent or null.
|
|
269
|
+
* @returns {Record<string, string[]>} Possibly empty, never null: a note
|
|
270
|
+
* listing no form has an empty map, which a consumer iterating it need not
|
|
271
|
+
* check first.
|
|
269
272
|
*/
|
|
270
|
-
export function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
export function asciiForms(name) {
|
|
274
|
+
const out = {};
|
|
275
|
+
for (const [code, forms] of nameFormLists(name)) {
|
|
276
|
+
const folded = forms.map((form) => asciiName(form)).filter((form) => form !== null);
|
|
277
|
+
if (folded.length) out[code] = folded;
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
273
280
|
}
|
|
274
281
|
|
|
275
282
|
/**
|
|
@@ -423,7 +430,7 @@ function assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage, raw)
|
|
|
423
430
|
* Build one index record from a note's frontmatter and its place in the tree.
|
|
424
431
|
*
|
|
425
432
|
* The frontmatter as authored, plus what the index derives from it: the package
|
|
426
|
-
* it compiles as, its address, ASCII folds of its name and
|
|
433
|
+
* it compiles as, its address, ASCII folds of its name and listed forms, the anchors
|
|
427
434
|
* of its body, its Foundry block, the address of its documentation journal, and
|
|
428
435
|
* where the file sits within the tree.
|
|
429
436
|
*
|
|
@@ -482,10 +489,20 @@ export function buildIndexRecord({
|
|
|
482
489
|
assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage, raw);
|
|
483
490
|
|
|
484
491
|
const source = body ?? "";
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
492
|
+
const scannedSpans = syntax ? syntax(source).spans : scanSpans(source);
|
|
493
|
+
const origin = { raw: raw ?? source, bodyLine, bodyColumn };
|
|
494
|
+
const marked = analyzeNameSpans(rebaseNameSpans(scannedSpans.names, origin), {
|
|
495
|
+
file: absPath ?? relPath,
|
|
496
|
+
types: nameTypes,
|
|
497
|
+
});
|
|
498
|
+
marked.findings.push(
|
|
499
|
+
...nameRefFindings(
|
|
500
|
+
rebaseNameSpans(
|
|
501
|
+
scannedSpans.spans.filter((span) => span.attrs.some(([key]) => key === "ref")),
|
|
502
|
+
origin,
|
|
503
|
+
),
|
|
504
|
+
{ file: absPath ?? relPath },
|
|
505
|
+
),
|
|
489
506
|
);
|
|
490
507
|
if (marked.findings.length) {
|
|
491
508
|
const findings = marked.findings.map(({ line, column, ...finding }) => ({
|
|
@@ -548,7 +565,7 @@ export function buildIndexRecord({
|
|
|
548
565
|
runs: recordRuns(source, { context: runContext, bodyLine, bodyColumn, syntax }),
|
|
549
566
|
address: stub ? null : address,
|
|
550
567
|
nameAscii: asciiName(frontmatter?.name?.full),
|
|
551
|
-
|
|
568
|
+
formsAscii: asciiForms(frontmatter?.name),
|
|
552
569
|
// Each anchor carries the link that reaches it, so a section is
|
|
553
570
|
// addressable from the index without anyone re-deriving how an
|
|
554
571
|
// anchor is spelled — its file line, so an editor can jump
|
|
@@ -1284,6 +1301,15 @@ export function collectContentIndex(
|
|
|
1284
1301
|
problems,
|
|
1285
1302
|
parsed,
|
|
1286
1303
|
});
|
|
1304
|
+
// Languages read the culture a note states, as calendars do.
|
|
1305
|
+
resolveNoteLanguages(records, {
|
|
1306
|
+
contentPackage,
|
|
1307
|
+
languages,
|
|
1308
|
+
contentBase,
|
|
1309
|
+
foreignRecord,
|
|
1310
|
+
problems,
|
|
1311
|
+
parsed,
|
|
1312
|
+
});
|
|
1287
1313
|
resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems, parsed });
|
|
1288
1314
|
const cultureNotes = records
|
|
1289
1315
|
.filter((record) => isNoteRecord(record) && record.type === "lore")
|
package/engine/content-links.mjs
CHANGED
|
@@ -242,6 +242,10 @@ export function buildLinkIndex(
|
|
|
242
242
|
// The calendar the index resolved, which the reckoning context built
|
|
243
243
|
// over these notes hands to every date printed for this note.
|
|
244
244
|
if (record.resolvedCalendar) note.resolvedCalendar = record.resolvedCalendar;
|
|
245
|
+
// The language the index derived, and the code a language note is the
|
|
246
|
+
// note of, which the glossary and `npc new` read.
|
|
247
|
+
if (typeof record.lang === "string") note.lang = record.lang;
|
|
248
|
+
if (typeof record.languageCode === "string") note.languageCode = record.languageCode;
|
|
245
249
|
const hierarchyProblems = cultureRecordProblems(record);
|
|
246
250
|
note.inheritedCultureFields = cultureInheritedFields(record);
|
|
247
251
|
if (hierarchyProblems)
|
package/engine/content-lint.mjs
CHANGED
|
@@ -42,9 +42,8 @@
|
|
|
42
42
|
* **Nothing here writes.** A check reports and an author fixes.
|
|
43
43
|
*
|
|
44
44
|
* **There is deliberately no third rule** requiring every note to repeat its
|
|
45
|
-
* own `type-shortcode` address in
|
|
46
|
-
*
|
|
47
|
-
* qualifier themselves.
|
|
45
|
+
* own `type-shortcode` address in its frontmatter. It would serve no build
|
|
46
|
+
* reader: the resolver and the checker parse the hyphen qualifier themselves.
|
|
48
47
|
*
|
|
49
48
|
* **What is deliberately absent.** Corpus reachability — "every Rules document
|
|
50
49
|
* is reachable from the book's root" — is a statement about what one package
|
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
* | a heading's id | as authored | as authored, or its text's | as authored | as authored |
|
|
28
28
|
* | quotes, dashes and ellipses | as written | typographic | as written | as written, set by Typst |
|
|
29
29
|
* | raw HTML in a note | passed through | passed through | shown as text | shown as text |
|
|
30
|
+
* | a name's first use | bold | bold | bold, with an index | bold |
|
|
30
31
|
* | the output | HTML | HTML | HTML | the token stream, for the Typst stage |
|
|
31
32
|
*
|
|
32
33
|
* A target differs from the others only in that table. The shared plugins are
|
|
33
34
|
* footnotes, definition lists, dictionary rows, bracket spans, dictionary
|
|
34
|
-
* entries, heading attributes, icons and
|
|
35
|
-
* lists what its target installs.
|
|
35
|
+
* entries, heading attributes, icons, body images and first uses; each
|
|
36
|
+
* renderer's `plugins` lists what its target installs.
|
|
36
37
|
*
|
|
37
38
|
* ## The stages of a render
|
|
38
39
|
*
|
|
@@ -72,6 +73,7 @@ import { dictionaryListPlugin } from "./definition-list.mjs";
|
|
|
72
73
|
import { alertMarkdownPlugin, renderAlerts, scanAlerts } from "./content-alerts.mjs";
|
|
73
74
|
import { renderBlocks, renderWithheldSections } from "./content-blocks.mjs";
|
|
74
75
|
import { renderFigureBlocks } from "./content-figures.mjs";
|
|
76
|
+
import { firstUsePlugin } from "./first-use.mjs";
|
|
75
77
|
import { iconPlugin } from "./content-icons.mjs";
|
|
76
78
|
import { imagePlugin } from "./content-images.mjs";
|
|
77
79
|
import { renderRuns } from "./content-scripts.mjs";
|
|
@@ -390,6 +392,7 @@ export function createRenderer(target, options = {}) {
|
|
|
390
392
|
use("definition-lists", deflistPlugin);
|
|
391
393
|
use("icons", iconPlugin(options.icons));
|
|
392
394
|
use("images", imagePlugin());
|
|
395
|
+
use("first-use", firstUsePlugin);
|
|
393
396
|
} else {
|
|
394
397
|
use("cartouche-figures", cartoucheFigures);
|
|
395
398
|
use("definition-lists", deflistPlugin);
|
|
@@ -409,6 +412,7 @@ export function createRenderer(target, options = {}) {
|
|
|
409
412
|
// A buffer with no index has no registry to draw an icon from.
|
|
410
413
|
if (!source) use("icons", iconPlugin(options.icons));
|
|
411
414
|
use("images", imagePlugin(options.images?.resolveSrc, options.images?.lookupAsset));
|
|
415
|
+
use("first-use", firstUsePlugin);
|
|
412
416
|
footnoteMarkup(md);
|
|
413
417
|
if (preview)
|
|
414
418
|
use("preview-source", previewSourcePlugin, { ...options.sourceContext, source });
|
|
@@ -15,7 +15,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
15
15
|
import { canonicalKey, parseAddress, renderAddress } from "./address.mjs";
|
|
16
16
|
import { positionOfFrontmatterPath, formatDiagnostic } from "./diagnostics.mjs";
|
|
17
17
|
|
|
18
|
-
const fields = ["
|
|
18
|
+
const fields = ["appearanceProfile"];
|
|
19
19
|
const payload = (note) => note.fm ?? note;
|
|
20
20
|
const dataOf = (note) => payload(note).data ?? payload(note);
|
|
21
21
|
const culture = (note) => payload(note).type === "lore" && payload(note).subType === "culture";
|