@heroiclands/package-build 22.20.0 → 22.22.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-content-build.md +0 -8
- package/CHANGELOG.md +204 -0
- package/assets/styles/content-base.css +162 -0
- package/bin/content-commands.mjs +26 -3
- package/bin/package-build.mjs +15 -2
- package/ci/content-format-reference.mjs +68 -1
- package/config.mjs +26 -0
- package/content-config.mjs +73 -4
- package/docs/api.md +46 -28
- package/docs/authoring/dates-and-calendars.md +4 -2
- package/docs/authoring/frontmatter.md +2 -2
- package/docs/authoring/links-and-markup.md +237 -28
- package/docs/configuration.md +27 -1
- package/docs/content-format.md +3 -1
- package/docs/guides/reference-pages.md +15 -0
- package/docs/reference/format-details.md +265 -42
- package/docs/reference/note-types.md +45 -28
- package/engine/actor-compiler.mjs +23 -0
- package/engine/anchored-sections.mjs +35 -4
- package/engine/anchors.mjs +10 -20
- package/engine/base-compiler.mjs +86 -21
- package/engine/base-styles.mjs +149 -0
- package/engine/bundles.mjs +0 -1
- package/engine/compendiums.mjs +8 -1
- package/engine/content-blocks.mjs +410 -0
- package/engine/content-captions.mjs +57 -8
- package/engine/content-footnotes.mjs +189 -0
- package/engine/content-format-check.mjs +15 -12
- package/engine/content-index.mjs +2 -2
- package/engine/content-links.mjs +14 -6
- package/engine/content-tables.mjs +45 -4
- package/engine/content-tasklists.mjs +189 -0
- package/engine/docs-checks.mjs +4 -4
- package/engine/extension-attributes.mjs +48 -2
- package/engine/frontmatter-lint.mjs +62 -66
- package/engine/frontmatter.mjs +3 -3
- package/engine/heading-attributes.mjs +308 -0
- package/engine/helpers.mjs +19 -13
- package/engine/homepage.mjs +8 -8
- package/engine/index.mjs +3 -0
- package/engine/infobox-render.mjs +21 -4
- package/engine/infobox.mjs +167 -7
- package/engine/journals.mjs +36 -17
- package/engine/macros.mjs +1 -1
- package/engine/markdown-expressions.mjs +178 -53
- package/engine/note-addresses.mjs +17 -8
- package/engine/note-dates.mjs +49 -0
- package/engine/note-frontmatter.mjs +7 -16
- package/engine/note-vocabulary.mjs +216 -42
- package/engine/page-lists.mjs +358 -0
- package/engine/pdf-build.mjs +76 -42
- package/engine/pdf-render.mjs +422 -49
- package/engine/rank-ladder.mjs +198 -0
- package/engine/reckoning-markers.mjs +1 -1
- package/engine/reference-targets.mjs +13 -1
- package/engine/retired-fields.mjs +8 -13
- package/engine/scenes.mjs +10 -3
- package/engine/site-build.mjs +41 -12
- package/engine/site-config.mjs +10 -1
- package/engine/site-index.mjs +39 -4
- package/engine/site-preview.mjs +18 -7
- package/engine/social-ties.mjs +52 -14
- package/engine/sql-tables.mjs +66 -9
- package/engine/standing-terms.mjs +19 -0
- package/engine/standings.mjs +538 -0
- package/engine/svg-theme.mjs +15 -4
- package/engine/system-block.mjs +12 -14
- package/engine/web-wikilinks.mjs +48 -1
- package/engine/wikilinks.mjs +3 -1
- package/hm3/actors.mjs +6 -3
- package/manifest.mjs +13 -0
- package/package.json +1 -1
- package/sohl/actors.mjs +82 -103
- package/sohl/item-fields.mjs +4 -5
- package/sohl/items.mjs +1 -1
- package/sohl/note-schemas.mjs +11 -21
- package/types/config.d.mts +9 -0
- package/types/engine/actor-compiler.d.mts +11 -0
- package/types/engine/anchored-sections.d.mts +24 -0
- package/types/engine/base-compiler.d.mts +19 -0
- package/types/engine/base-styles.d.mts +93 -0
- package/types/engine/content-blocks.d.mts +87 -0
- package/types/engine/content-captions.d.mts +14 -1
- package/types/engine/content-footnotes.d.mts +73 -0
- package/types/engine/content-format-check.d.mts +8 -6
- package/types/engine/content-tables.d.mts +12 -4
- package/types/engine/content-tasklists.d.mts +72 -0
- package/types/engine/extension-attributes.d.mts +22 -1
- package/types/engine/frontmatter.d.mts +1 -1
- package/types/engine/heading-attributes.d.mts +174 -0
- package/types/engine/helpers.d.mts +7 -4
- package/types/engine/homepage.d.mts +7 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/infobox-render.d.mts +6 -1
- package/types/engine/journals.d.mts +14 -7
- package/types/engine/markdown-expressions.d.mts +23 -0
- package/types/engine/note-frontmatter.d.mts +1 -4
- package/types/engine/note-vocabulary.d.mts +98 -8
- package/types/engine/page-lists.d.mts +126 -0
- package/types/engine/pdf-render.d.mts +72 -2
- package/types/engine/rank-ladder.d.mts +32 -0
- package/types/engine/retired-fields.d.mts +4 -6
- package/types/engine/site-build.d.mts +13 -1
- package/types/engine/site-config.d.mts +9 -0
- package/types/engine/site-index.d.mts +13 -1
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +31 -7
- package/types/engine/standing-terms.d.mts +16 -0
- package/types/engine/standings.d.mts +147 -0
- package/types/engine/system-block.d.mts +11 -12
- package/types/engine/web-wikilinks.d.mts +3 -1
- package/types/engine/wikilinks.d.mts +2 -1
- package/types/hm3/actors.d.mts +3 -3
- package/types/sohl/actors.d.mts +25 -7
- package/engine/content-admonitions.mjs +0 -114
- package/engine/content-secrets.mjs +0 -112
- package/types/engine/content-admonitions.d.mts +0 -24
- package/types/engine/content-secrets.d.mts +0 -17
package/engine/social-ties.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { acceptsType, parseAddress, renderAddress } from "./address.mjs";
|
|
12
|
+
import { AddressEntries } from "./address-values.mjs";
|
|
12
13
|
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
13
14
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
14
15
|
|
|
@@ -16,14 +17,51 @@ export { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES };
|
|
|
16
17
|
|
|
17
18
|
const TERMS = new Set(SOCIAL_TIES.map(({ term }) => term));
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* A being's defining ties, in one shape whichever shape they reach here in.
|
|
22
|
+
*
|
|
23
|
+
* Two spellings reach this check, and both read the same entries from here
|
|
24
|
+
* on:
|
|
25
|
+
*
|
|
26
|
+
* - an **{@link AddressEntries}** — the map after the note boundary decoded
|
|
27
|
+
* its keys into typed Addresses, the shape every compile sees;
|
|
28
|
+
* - a **plain map**, the shape a caller reading raw YAML sees.
|
|
29
|
+
*
|
|
30
|
+
* `sourceKey` is always the authored key text, so a finding locates on what
|
|
31
|
+
* the file actually says even when `key` already carries a parsed Address.
|
|
32
|
+
*
|
|
33
|
+
* @param {unknown} value - The authored `data.socialTies`.
|
|
34
|
+
* @returns {{form: "absent"|"map"|"malformed",
|
|
35
|
+
* entries: Array<{key: unknown, term: unknown, sourceKey: string}>}}
|
|
36
|
+
*/
|
|
37
|
+
function readSocialTies(value) {
|
|
38
|
+
if (value === undefined || value === null) return { form: "absent", entries: [] };
|
|
39
|
+
if (value instanceof AddressEntries) {
|
|
40
|
+
return {
|
|
41
|
+
form: "map",
|
|
42
|
+
entries: value.entries.map((entry) => ({
|
|
43
|
+
key: entry.target,
|
|
44
|
+
term: entry.value,
|
|
45
|
+
sourceKey: entry.sourceKey,
|
|
46
|
+
})),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
50
|
+
return { form: "malformed", entries: [] };
|
|
51
|
+
return {
|
|
52
|
+
form: "map",
|
|
53
|
+
entries: Object.entries(value).map(([key, term]) => ({ key, term, sourceKey: key })),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
19
57
|
/**
|
|
20
58
|
* Validate a being's map of defining relationships.
|
|
21
59
|
* @param {object} note
|
|
22
60
|
* @param {{index?: object}} [options]
|
|
23
61
|
*/
|
|
24
62
|
export function checkSocialTies(note, { index } = {}) {
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
63
|
+
const { form, entries } = readSocialTies(note.fm?.data?.socialTies);
|
|
64
|
+
if (form === "absent") return [];
|
|
27
65
|
const at = (key) => ({
|
|
28
66
|
file: note.file,
|
|
29
67
|
...positionOfFrontmatterPath(
|
|
@@ -33,7 +71,7 @@ export function checkSocialTies(note, { index } = {}) {
|
|
|
33
71
|
),
|
|
34
72
|
severity: "error",
|
|
35
73
|
});
|
|
36
|
-
if (
|
|
74
|
+
if (form === "malformed")
|
|
37
75
|
return [
|
|
38
76
|
{
|
|
39
77
|
...at(),
|
|
@@ -42,11 +80,11 @@ export function checkSocialTies(note, { index } = {}) {
|
|
|
42
80
|
];
|
|
43
81
|
const findings = [];
|
|
44
82
|
const seen = new Map();
|
|
45
|
-
for (const
|
|
83
|
+
for (const { key, term, sourceKey } of entries) {
|
|
46
84
|
const tuple = parseAddress(
|
|
47
85
|
key,
|
|
48
86
|
{
|
|
49
|
-
package: index?.contentPackage
|
|
87
|
+
package: index?.contentPackage,
|
|
50
88
|
system: "note",
|
|
51
89
|
types: index?.types,
|
|
52
90
|
packages: index?.packages,
|
|
@@ -55,43 +93,43 @@ export function checkSocialTies(note, { index } = {}) {
|
|
|
55
93
|
);
|
|
56
94
|
if (tuple.reason) {
|
|
57
95
|
findings.push({
|
|
58
|
-
...at(
|
|
59
|
-
message: `data.socialTies key ${JSON.stringify(
|
|
96
|
+
...at(sourceKey),
|
|
97
|
+
message: `data.socialTies key ${JSON.stringify(sourceKey)} is not a complete Address (${tuple.reason}); write being or affiliation as its type`,
|
|
60
98
|
});
|
|
61
99
|
} else if (!acceptsType(tuple, SOCIAL_TIE_TARGET_TYPES)) {
|
|
62
100
|
findings.push({
|
|
63
|
-
...at(
|
|
101
|
+
...at(sourceKey),
|
|
64
102
|
message: `data.socialTies accepts being or affiliation, not ${tuple.type}`,
|
|
65
103
|
});
|
|
66
104
|
} else {
|
|
67
105
|
const target = renderAddress(tuple);
|
|
68
106
|
if (seen.has(target)) {
|
|
69
107
|
findings.push({
|
|
70
|
-
...at(
|
|
108
|
+
...at(sourceKey),
|
|
71
109
|
message: `data.socialTies names the same target ${target} as ${JSON.stringify(seen.get(target))}; keep one tie per target`,
|
|
72
110
|
});
|
|
73
111
|
} else {
|
|
74
|
-
seen.set(target,
|
|
112
|
+
seen.set(target, sourceKey);
|
|
75
113
|
}
|
|
76
114
|
if (
|
|
77
|
-
tuple.package ===
|
|
115
|
+
tuple.package === index?.contentPackage &&
|
|
78
116
|
tuple.type === "being" &&
|
|
79
117
|
tuple.shortcode === note.fm?.shortcode
|
|
80
118
|
) {
|
|
81
119
|
findings.push({
|
|
82
|
-
...at(
|
|
120
|
+
...at(sourceKey),
|
|
83
121
|
message: `data.socialTies cannot address this being itself`,
|
|
84
122
|
});
|
|
85
123
|
} else if (index?.addressHit && !index.addressHit(target)) {
|
|
86
124
|
findings.push({
|
|
87
|
-
...at(
|
|
125
|
+
...at(sourceKey),
|
|
88
126
|
message: `data.socialTies target ${target} does not resolve`,
|
|
89
127
|
});
|
|
90
128
|
}
|
|
91
129
|
}
|
|
92
130
|
if (!TERMS.has(term))
|
|
93
131
|
findings.push({
|
|
94
|
-
...at(
|
|
132
|
+
...at(sourceKey),
|
|
95
133
|
message: `data.socialTies value ${JSON.stringify(term)} must be one of ${SOCIAL_TIES.map(({ term: value }) => value).join(", ")}`,
|
|
96
134
|
});
|
|
97
135
|
}
|
package/engine/sql-tables.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import path from "node:path";
|
|
|
35
35
|
|
|
36
36
|
import { FENCE_LINE, parseHeaderArgs } from "./code-fences.mjs";
|
|
37
37
|
import { booleanAttribute } from "./extension-attributes.mjs";
|
|
38
|
+
import { findPageListBlocks, preparePageLists } from "./page-lists.mjs";
|
|
38
39
|
import { MARKET_CLASSES } from "./market-class.mjs";
|
|
39
40
|
import { parseMarkdownFile } from "./helpers.mjs";
|
|
40
41
|
import { sqlQueriesInMarkdown } from "./markdown-expressions.mjs";
|
|
@@ -53,6 +54,31 @@ const EMPTY_CELL = "—";
|
|
|
53
54
|
*/
|
|
54
55
|
export const RENDER_ALIASES = Object.freeze({ ref: "_ref", section: "_section" });
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The attributes an SQL fence takes, with what each one is for.
|
|
59
|
+
*
|
|
60
|
+
* **This is the one list.** The fence is validated against it and the authoring
|
|
61
|
+
* document is checked against it, so an attribute added here is one the document
|
|
62
|
+
* must describe, and anything else an author writes is reported by name.
|
|
63
|
+
*
|
|
64
|
+
* @type {Readonly<Record<string, Readonly<{value: string, default: string, summary: string}>>>}
|
|
65
|
+
*/
|
|
66
|
+
export const SQL_FENCE_ATTRIBUTES = Object.freeze({
|
|
67
|
+
"allow-empty": Object.freeze({
|
|
68
|
+
value: "`true` or `false`",
|
|
69
|
+
default: "`false`",
|
|
70
|
+
summary:
|
|
71
|
+
"Whether a query selecting no rows is allowed. A zero-row result is a finding " +
|
|
72
|
+
"otherwise, because a table that silently prints nothing is the commonest way a " +
|
|
73
|
+
"query goes wrong unnoticed.",
|
|
74
|
+
}),
|
|
75
|
+
"section-level": Object.freeze({
|
|
76
|
+
value: "an integer from 1 through 6",
|
|
77
|
+
default: "`2`",
|
|
78
|
+
summary: "The heading level given to each group a `_section` column creates.",
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
|
|
56
82
|
/** A table cell may not carry a raw `|` or a line break. */
|
|
57
83
|
const escapeCell = (text) =>
|
|
58
84
|
String(text)
|
|
@@ -92,7 +118,7 @@ export function findSqlBlocks(markdown) {
|
|
|
92
118
|
const level = Number(args["section-level"] ?? 2);
|
|
93
119
|
const errors = [...problems];
|
|
94
120
|
for (const key of Object.keys(args)) {
|
|
95
|
-
if (!
|
|
121
|
+
if (!Object.hasOwn(SQL_FENCE_ATTRIBUTES, key))
|
|
96
122
|
errors.push(`${key} is not an SQL fence attribute`);
|
|
97
123
|
}
|
|
98
124
|
let allowEmpty = false;
|
|
@@ -569,6 +595,10 @@ export function renderSqlTable(
|
|
|
569
595
|
* @param {object} [opts]
|
|
570
596
|
* @param {(ref: string) => boolean} [opts.linkable] - Passed to
|
|
571
597
|
* {@link renderSqlTable}.
|
|
598
|
+
* @param {object[]} [opts.records] - Content-index records, narrowed to the
|
|
599
|
+
* audience this surface publishes to. Given them, the result also carries
|
|
600
|
+
* `pageLists`, so one prepared object answers every corpus-reading directive
|
|
601
|
+
* a body can hold and a caller looks results up in one place.
|
|
572
602
|
* **Keyed by note, then by the directive's ordinal within it** — not by its
|
|
573
603
|
* line. The passes do not agree on what a body is: `walkMarkdownTree` trims it,
|
|
574
604
|
* while the link checker strips the frontmatter fence and leaves the newlines
|
|
@@ -578,8 +608,9 @@ export function renderSqlTable(
|
|
|
578
608
|
* @returns {Promise<Map<string, object[]>>} Note to results, in document order,
|
|
579
609
|
* each carrying either a rendered `markdown` and its `rows`, or a `reason`.
|
|
580
610
|
*/
|
|
581
|
-
export async function prepareSqlTables(db, sources, { linkable } = {}) {
|
|
611
|
+
export async function prepareSqlTables(db, sources, { linkable, records } = {}) {
|
|
582
612
|
const prepared = new Map();
|
|
613
|
+
if (records) prepared.pageLists = preparePageLists(records, sources);
|
|
583
614
|
for (const { source, markdown } of sources) {
|
|
584
615
|
const blocks = findSqlBlocks(markdown);
|
|
585
616
|
if (!blocks.length) continue;
|
|
@@ -658,10 +689,14 @@ export async function prepareInlineSqlExpressions(db, sources) {
|
|
|
658
689
|
* shared index
|
|
659
690
|
* describes, where N passes each derive the corpus their own way.
|
|
660
691
|
*
|
|
661
|
-
* **
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
692
|
+
* **Every corpus-reading directive is answered here**, which is why one call
|
|
693
|
+
* precedes every pass: the `sql` tables, the inline scalar queries, and the
|
|
694
|
+
* page lists, whose results ride on the same object as `pageLists`.
|
|
695
|
+
*
|
|
696
|
+
* **Nothing is opened for a tree that asks no query.** A tree with no `sql`
|
|
697
|
+
* directive costs one walk and no database at all, and so does one whose only
|
|
698
|
+
* directive is a page list — a page list is a filter over the records. That is
|
|
699
|
+
* what lets every pass call this unconditionally.
|
|
665
700
|
*
|
|
666
701
|
* @param {string} contentBase - Root of the content tree.
|
|
667
702
|
* @param {object} [opts]
|
|
@@ -671,8 +706,9 @@ export async function prepareInlineSqlExpressions(db, sources) {
|
|
|
671
706
|
* A command that also builds a link index holds them already, and deriving
|
|
672
707
|
* them twice is the duplicated-corpus failure this closes.
|
|
673
708
|
* @param {"all"|"public"} [opts.audience] - Whether to exclude GM notes.
|
|
674
|
-
* @returns {Promise<Map<string, object[]>|undefined>} Results by note path,
|
|
675
|
-
* nothing when the tree
|
|
709
|
+
* @returns {Promise<Map<string, object[]>|undefined>} Results by note path,
|
|
710
|
+
* carrying `inline` and `pageLists` beside them, or nothing when the tree
|
|
711
|
+
* holds no such directive.
|
|
676
712
|
*/
|
|
677
713
|
export async function prepareTreeSqlTables(
|
|
678
714
|
contentBase,
|
|
@@ -701,10 +737,30 @@ export async function prepareTreeSqlTables(
|
|
|
701
737
|
if (!isNoteRecord(record)) continue;
|
|
702
738
|
const absPath = noteFile(contentBase, record);
|
|
703
739
|
const { body, frontmatter } = parseMarkdownFile(absPath);
|
|
704
|
-
if (
|
|
740
|
+
if (
|
|
741
|
+
body &&
|
|
742
|
+
(findSqlBlocks(body).length ||
|
|
743
|
+
findPageListBlocks(body).length ||
|
|
744
|
+
sqlQueriesInMarkdown(body, frontmatter).length)
|
|
745
|
+
)
|
|
705
746
|
sources.push({ source: absPath, markdown: body, frontmatter });
|
|
706
747
|
}
|
|
707
748
|
if (!sources.length) return undefined;
|
|
749
|
+
|
|
750
|
+
// A page list is a filter over the records, so it needs no database. A tree
|
|
751
|
+
// whose only corpus-reading directive is one therefore opens none — which
|
|
752
|
+
// is the same bargain the early return above makes for a tree with no
|
|
753
|
+
// directive at all.
|
|
754
|
+
const needsDatabase = sources.some(
|
|
755
|
+
({ markdown, frontmatter }) =>
|
|
756
|
+
findSqlBlocks(markdown).length || sqlQueriesInMarkdown(markdown, frontmatter).length,
|
|
757
|
+
);
|
|
758
|
+
if (!needsDatabase) {
|
|
759
|
+
const pageOnly = new Map();
|
|
760
|
+
pageOnly.inline = new Map();
|
|
761
|
+
pageOnly.pageLists = preparePageLists(indexRecords, sources);
|
|
762
|
+
return pageOnly;
|
|
763
|
+
}
|
|
708
764
|
// A cell links only where the address it would emit resolves, so a table
|
|
709
765
|
// never ships a link the wikilink pass will then report dead.
|
|
710
766
|
//
|
|
@@ -736,6 +792,7 @@ export async function prepareTreeSqlTables(
|
|
|
736
792
|
try {
|
|
737
793
|
const prepared = await prepareSqlTables(db, sources, {
|
|
738
794
|
linkable: (ref) => addresses.has(renderAddress(ref)),
|
|
795
|
+
records: indexRecords,
|
|
739
796
|
});
|
|
740
797
|
prepared.inline = await prepareInlineSqlExpressions(db, sources);
|
|
741
798
|
return prepared;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The note type a standing is held in.
|
|
5
|
+
*
|
|
6
|
+
* A standing is always held in a body: the entry is keyed by that body's
|
|
7
|
+
* Address, which is what lets the rung and the post be checked against what
|
|
8
|
+
* the body itself declares.
|
|
9
|
+
*/
|
|
10
|
+
export const STANDING_BODY_TYPES = Object.freeze(["affiliation"]);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The two keys one standing carries.
|
|
14
|
+
*
|
|
15
|
+
* `rank` is the level on the body's own `governance.ranks` ladder, as a number.
|
|
16
|
+
* `office` is a key of its `governance.offices` map, as a string. Both are
|
|
17
|
+
* optional — a membership with neither is the ordinary shape.
|
|
18
|
+
*/
|
|
19
|
+
export const STANDING_KEYS = Object.freeze(["rank", "office"]);
|