@heroiclands/package-build 23.9.0 → 23.10.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 +26 -0
- package/README.md +2 -0
- package/bin/content-commands.mjs +197 -68
- package/docs/api.md +336 -68
- package/docs/authoring/links-and-markup.md +114 -47
- package/docs/commands.md +92 -3
- package/docs/content-format.md +6 -1
- package/docs/reference/format-details.md +129 -14
- package/docs/reference/note-types.md +4 -4
- package/engine/anchors.mjs +8 -6
- package/engine/base-compiler.mjs +6 -4
- package/engine/changelog-group.mjs +58 -34
- package/engine/changelog-lint.mjs +6 -2
- package/engine/compendiums.mjs +15 -1
- package/engine/compile-corpus.mjs +14 -1
- package/engine/content-charset.mjs +13 -1
- package/engine/content-html.mjs +18 -4
- package/engine/content-icons.mjs +15 -1
- package/engine/content-images.mjs +28 -9
- package/engine/content-index.mjs +102 -23
- package/engine/content-links.mjs +27 -12
- package/engine/content-session.mjs +247 -0
- package/engine/content-sources.mjs +110 -0
- package/engine/content-spans.mjs +87 -8
- package/engine/content-tables.mjs +1 -0
- package/engine/content-tasklists.mjs +20 -4
- package/engine/diagnostics.mjs +17 -6
- package/engine/extension-attributes.mjs +28 -5
- package/engine/first-use.mjs +371 -0
- package/engine/foundry-entries.mjs +12 -9
- package/engine/generate.mjs +13 -4
- package/engine/helpers.mjs +43 -14
- package/engine/index-records.mjs +1 -0
- package/engine/index.mjs +3 -0
- package/engine/journals.mjs +9 -5
- package/engine/language-data.mjs +2 -2
- package/engine/language-rows.mjs +4 -4
- package/engine/language-terms.mjs +4 -4
- package/engine/lexical-sections.mjs +5 -5
- package/engine/markup-validation.mjs +7 -2
- package/engine/metadata-index.mjs +1 -0
- package/engine/name-markup.mjs +114 -0
- package/engine/name-rows.mjs +70 -0
- package/engine/names.mjs +460 -0
- package/engine/note-addresses.mjs +25 -4
- package/engine/path-charset.mjs +24 -1
- package/engine/pdf-render.mjs +15 -1
- package/engine/site-index.mjs +5 -1
- package/engine/sql-tables.mjs +76 -15
- package/engine/stub-lint.mjs +4 -3
- package/engine/yaml-lint.mjs +3 -5
- package/hugo-theme/static/css/style.css +4 -0
- package/package.json +1 -1
- package/types/engine/anchors.d.mts +4 -2
- package/types/engine/compendiums.d.mts +5 -1
- package/types/engine/compile-corpus.d.mts +5 -2
- package/types/engine/content-charset.d.mts +5 -1
- package/types/engine/content-html.d.mts +5 -1
- package/types/engine/content-icons.d.mts +5 -1
- package/types/engine/content-images.d.mts +5 -1
- package/types/engine/content-index.d.mts +39 -19
- package/types/engine/content-links.d.mts +22 -8
- package/types/engine/content-session.d.mts +108 -0
- package/types/engine/content-sources.d.mts +32 -0
- package/types/engine/content-spans.d.mts +3 -5
- package/types/engine/content-tasklists.d.mts +5 -1
- package/types/engine/diagnostics.d.mts +10 -0
- package/types/engine/extension-attributes.d.mts +16 -2
- package/types/engine/first-use.d.mts +31 -0
- package/types/engine/foundry-entries.d.mts +11 -4
- package/types/engine/generate.d.mts +5 -1
- package/types/engine/helpers.d.mts +30 -7
- package/types/engine/index.d.mts +1 -0
- package/types/engine/journals.d.mts +5 -2
- package/types/engine/lexical-sections.d.mts +3 -2
- package/types/engine/markup-validation.d.mts +2 -1
- package/types/engine/name-markup.d.mts +176 -0
- package/types/engine/name-rows.d.mts +35 -0
- package/types/engine/names.d.mts +356 -0
- package/types/engine/note-addresses.d.mts +3 -3
- package/types/engine/path-charset.d.mts +16 -1
- package/types/engine/site-build.d.mts +2 -2
- package/types/engine/site-index.d.mts +3 -1
- package/types/engine/sql-tables.d.mts +7 -2
- package/types/engine/stub-lint.d.mts +3 -1
package/engine/helpers.mjs
CHANGED
|
@@ -78,7 +78,7 @@ import { dictionaryEntryPlugin } from "./dictionary-layout.mjs";
|
|
|
78
78
|
import { renderBlocks } from "./content-blocks.mjs";
|
|
79
79
|
import { headingAttributesPlugin } from "./heading-attributes.mjs";
|
|
80
80
|
import { renderFigureBlocks, scanFigures } from "./content-figures.mjs";
|
|
81
|
-
import { positionInBody } from "./diagnostics.mjs";
|
|
81
|
+
import { positionInBody, splitFrontmatter } from "./diagnostics.mjs";
|
|
82
82
|
// The pure `sohl:` frontmatter readers live in a leaf module so the item-type
|
|
83
83
|
// registry can import them without reaching back through this one.
|
|
84
84
|
// Re-exported here so every existing importer keeps its single import path.
|
|
@@ -233,7 +233,7 @@ export function renderFoundryMarkdown(body, figures, footnoteNumbers, docId, res
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/**
|
|
236
|
-
* Parses a markdown file with YAML frontmatter.
|
|
236
|
+
* Parses a markdown file with LF or CRLF YAML frontmatter delimiters.
|
|
237
237
|
*
|
|
238
238
|
* Returns `{ frontmatter, body, description, bodyLine, bodyColumn, raw }` where
|
|
239
239
|
* `body` is the trimmed raw markdown after the frontmatter block, and
|
|
@@ -245,20 +245,27 @@ export function renderFoundryMarkdown(body, figures, footnoteNumbers, docId, res
|
|
|
245
245
|
* `{ frontmatter: null, body: "", description: "", raw }` and no
|
|
246
246
|
* position: there is no body to have one.
|
|
247
247
|
* @param {string} filePath - Markdown file to read.
|
|
248
|
-
* @param {
|
|
248
|
+
* @param {object} [opts]
|
|
249
|
+
* @param {object} [opts.addressContext] - Address defaults for decoded fields.
|
|
250
|
+
* @param {string} [opts.raw] - Exact captured source; omitted, reads the file.
|
|
251
|
+
* @param {boolean} [opts.renderDescription=true] - Render the body's HTML description.
|
|
252
|
+
* False leaves `description` empty for readers that only use authored text.
|
|
249
253
|
* @returns {{frontmatter: object|null, body: string, description: string, raw: string,
|
|
250
254
|
* bodyLine?: number, bodyColumn?: number, parseError?: {message: string,
|
|
251
255
|
* line?: number, column?: number}}} Parsed note and body position.
|
|
252
256
|
*/
|
|
253
|
-
export function parseMarkdownFile(
|
|
254
|
-
|
|
255
|
-
|
|
257
|
+
export function parseMarkdownFile(
|
|
258
|
+
filePath,
|
|
259
|
+
{ addressContext, raw: capturedRaw, renderDescription = true } = {},
|
|
260
|
+
) {
|
|
261
|
+
const content = capturedRaw ?? fs.readFileSync(filePath, "utf8");
|
|
262
|
+
const fmMatch = splitFrontmatter(content);
|
|
256
263
|
if (!fmMatch) {
|
|
257
264
|
return { frontmatter: null, body: "", description: "", raw: content };
|
|
258
265
|
}
|
|
259
266
|
let frontmatter;
|
|
260
267
|
try {
|
|
261
|
-
frontmatter = yaml.parse(fmMatch
|
|
268
|
+
frontmatter = yaml.parse(fmMatch.yaml) || {};
|
|
262
269
|
} catch (err) {
|
|
263
270
|
const firstLine = String(err.message).split("\n", 1)[0];
|
|
264
271
|
const message = firstLine.replace(/ at line \d+, column \d+.*$/, "");
|
|
@@ -279,7 +286,7 @@ export function parseMarkdownFile(filePath, { addressContext } = {}) {
|
|
|
279
286
|
try {
|
|
280
287
|
decodeNoteAddresses(frontmatter, addressContext);
|
|
281
288
|
} catch (error) {
|
|
282
|
-
const position = positionOfYamlPath(fmMatch
|
|
289
|
+
const position = positionOfYamlPath(fmMatch.yaml, error.keyPath ?? [], {
|
|
283
290
|
key: error.addressKey,
|
|
284
291
|
});
|
|
285
292
|
if (position.line) position.line++;
|
|
@@ -288,9 +295,9 @@ export function parseMarkdownFile(filePath, { addressContext } = {}) {
|
|
|
288
295
|
throw error;
|
|
289
296
|
}
|
|
290
297
|
}
|
|
291
|
-
const raw = fmMatch
|
|
298
|
+
const raw = fmMatch.body;
|
|
292
299
|
const body = raw.trim();
|
|
293
|
-
const description = body ? renderFoundryMarkdown(body) : "";
|
|
300
|
+
const description = body && renderDescription ? renderFoundryMarkdown(body) : "";
|
|
294
301
|
// Where the trimmed body starts in the *file*, so an offset within it can
|
|
295
302
|
// be reported as a file position. The frontmatter's lines and the
|
|
296
303
|
// blank lines `trim()` removes both sit in between, and the trim can take
|
|
@@ -376,13 +383,19 @@ export function assertSuppliedCorpus(records, who) {
|
|
|
376
383
|
* Required: the scope is the caller's to state, so two passes cannot
|
|
377
384
|
* disagree about which files are the corpus.
|
|
378
385
|
* @param {object} [opts.addressContext] - Address resolution context.
|
|
386
|
+
* @param {Map<string, {raw: string}>} [opts.sources] - Selected sources in walk order.
|
|
387
|
+
* Supplied, the walk parses these strings instead of reading the filesystem.
|
|
388
|
+
* @param {boolean} [opts.renderDescription=true] - Render each body's description.
|
|
379
389
|
* @yields {{frontmatter: object|null, body: string, description: string,
|
|
380
390
|
* file: string, absPath: string, bodyLine?: number, bodyColumn?: number}}
|
|
381
391
|
* One entry per `.md` file found.
|
|
382
392
|
* @throws {Error} When `skipDirectories` is not stated — see
|
|
383
393
|
* {@link assertStatedScope}.
|
|
384
394
|
*/
|
|
385
|
-
export function* walkMarkdownTree(
|
|
395
|
+
export function* walkMarkdownTree(
|
|
396
|
+
rootDir,
|
|
397
|
+
{ skipDirectories, addressContext, sources, renderDescription = true } = {},
|
|
398
|
+
) {
|
|
386
399
|
// Stated by the caller, never resolved here. A default here
|
|
387
400
|
// — `loadPackConfig().skipDirectories` — read whichever configuration
|
|
388
401
|
// resolved from the working directory rather than the one the caller was
|
|
@@ -395,6 +408,20 @@ export function* walkMarkdownTree(rootDir, { skipDirectories, addressContext } =
|
|
|
395
408
|
// the ambient config rather than the passed one, which a fixture can pass
|
|
396
409
|
// on indefinitely.
|
|
397
410
|
assertStatedScope(skipDirectories, "walkMarkdownTree");
|
|
411
|
+
if (sources) {
|
|
412
|
+
for (const [absPath, source] of sources) {
|
|
413
|
+
yield {
|
|
414
|
+
...parseMarkdownFile(absPath, {
|
|
415
|
+
addressContext,
|
|
416
|
+
raw: source.raw,
|
|
417
|
+
renderDescription,
|
|
418
|
+
}),
|
|
419
|
+
file: path.basename(absPath),
|
|
420
|
+
absPath,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
398
425
|
if (!fs.existsSync(rootDir)) return;
|
|
399
426
|
const stack = [rootDir];
|
|
400
427
|
while (stack.length > 0) {
|
|
@@ -413,7 +440,7 @@ export function* walkMarkdownTree(rootDir, { skipDirectories, addressContext } =
|
|
|
413
440
|
stack.push(absPath);
|
|
414
441
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
415
442
|
yield {
|
|
416
|
-
...parseMarkdownFile(absPath, { addressContext }),
|
|
443
|
+
...parseMarkdownFile(absPath, { addressContext, renderDescription }),
|
|
417
444
|
file: entry.name,
|
|
418
445
|
absPath,
|
|
419
446
|
};
|
|
@@ -785,12 +812,14 @@ import { collectAnchors } from "./anchors.mjs";
|
|
|
785
812
|
* compile and handed in. Required: see {@link assertSuppliedCorpus}.
|
|
786
813
|
* @param {Map<string, object[]>} [opts.sqlTables] - Prepared directives,
|
|
787
814
|
* including the anchors and Foundry page maps of their expanded bodies.
|
|
815
|
+
* @param {ReturnType<import("./metadata-index.mjs").loadForeignIndexes>} [opts.foreign] -
|
|
816
|
+
* Captured dependency address and reference views for this context.
|
|
788
817
|
* @returns {{byShortcode: Map, types: Set}} From `buildWikilinkIndex`.
|
|
789
818
|
*/
|
|
790
819
|
export function buildContentLinkIndex(
|
|
791
820
|
contentBase,
|
|
792
821
|
router = packRouter(),
|
|
793
|
-
{ config, records, sqlTables } = {},
|
|
822
|
+
{ config, records, sqlTables, foreign: capturedForeign } = {},
|
|
794
823
|
) {
|
|
795
824
|
const docs = [];
|
|
796
825
|
/** The files this package ships, by canonical address. */
|
|
@@ -892,7 +921,7 @@ export function buildContentLinkIndex(
|
|
|
892
921
|
index: foreign,
|
|
893
922
|
references: foreignReferences,
|
|
894
923
|
stale,
|
|
895
|
-
} = loadForeignIndexes(resolved, [resolved.contentPackage], PACKAGE_BASE);
|
|
924
|
+
} = capturedForeign ?? loadForeignIndexes(resolved, [resolved.contentPackage], PACKAGE_BASE);
|
|
896
925
|
if (stale.length) {
|
|
897
926
|
for (const st of stale) {
|
|
898
927
|
log.error(`Unusable content index for "${st.package}": ${st.reason}`);
|
package/engine/index-records.mjs
CHANGED
package/engine/index.mjs
CHANGED
|
@@ -187,6 +187,9 @@ export * as foundryEntries from "./foundry-entries.mjs";
|
|
|
187
187
|
/** Publishing the note tree as a queryable JSON Lines index. */
|
|
188
188
|
export * as contentIndex from "./content-index.mjs";
|
|
189
189
|
|
|
190
|
+
/** One invocation's captured content and context-specific prepared views. */
|
|
191
|
+
export * as contentSession from "./content-session.mjs";
|
|
192
|
+
|
|
190
193
|
/** The asset types — `icon`, `image`, `audio` — and the roots they are walked from. */
|
|
191
194
|
export * as assetTypes from "./asset-types.mjs";
|
|
192
195
|
|
package/engine/journals.mjs
CHANGED
|
@@ -103,9 +103,9 @@ const WITHHELD_OWNERSHIP = 0;
|
|
|
103
103
|
* @param {string} markdown - A note's body, as {@link splitPages} receives it.
|
|
104
104
|
* @returns {Set<number>} 0-based line indices inside a named block.
|
|
105
105
|
*/
|
|
106
|
-
function namedBlockLines(markdown) {
|
|
106
|
+
function namedBlockLines(markdown, syntax) {
|
|
107
107
|
const lines = new Set();
|
|
108
|
-
for (const { start, end } of scanBlocks(markdown).blocks) {
|
|
108
|
+
for (const { start, end } of (syntax ? syntax(markdown).blocks : scanBlocks(markdown)).blocks) {
|
|
109
109
|
for (let i = start; i <= end; i++) lines.add(i);
|
|
110
110
|
}
|
|
111
111
|
return lines;
|
|
@@ -142,17 +142,18 @@ function namedBlockLines(markdown) {
|
|
|
142
142
|
* picture's address to the role its asset declares, so a figure page's
|
|
143
143
|
* name reads `Map 1` rather than `Figure 1` where it is due — see
|
|
144
144
|
* {@link module:engine/content-figures.scanFigures}.
|
|
145
|
+
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Session scanner projections.
|
|
145
146
|
* @returns {Array<{name: string, anchorSlug: string|null, level: number,
|
|
146
147
|
* classes: string[], figure: object|null, continues?: string,
|
|
147
148
|
* markdown: string}>} Pages in document order.
|
|
148
149
|
*/
|
|
149
|
-
export function splitPages(body, leadName = "Introduction", resolveRole) {
|
|
150
|
+
export function splitPages(body, leadName = "Introduction", resolveRole, syntax) {
|
|
150
151
|
const { markdown, definitions } = separateFootnotes(body);
|
|
151
152
|
const lines = markdown.split("\n");
|
|
152
153
|
const figureStarts = new Map(
|
|
153
154
|
scanFigures(markdown, { resolveRole }).figures.map((figure) => [figure.line - 1, figure]),
|
|
154
155
|
);
|
|
155
|
-
const blockLines = namedBlockLines(markdown);
|
|
156
|
+
const blockLines = namedBlockLines(markdown, syntax);
|
|
156
157
|
const openings = pageOpenings(markdown, blockLines);
|
|
157
158
|
const pages = [];
|
|
158
159
|
const beforeFirstH1 = [];
|
|
@@ -636,6 +637,7 @@ export function buildPages(rawPages, entryId, noteName, figures, resolveRole) {
|
|
|
636
637
|
* a picture's address to the role its asset declares, so a figure page's
|
|
637
638
|
* name reads `Map 1` rather than `Figure 1` where it is due — see
|
|
638
639
|
* {@link module:engine/content-figures.scanFigures}.
|
|
640
|
+
* @param {import("./content-session.mjs").ContentSyntax} [params.syntax] - Session scanner projections.
|
|
639
641
|
* @returns {object} The JournalEntry document, keyed for the pack.
|
|
640
642
|
*/
|
|
641
643
|
export function buildJournalEntry({
|
|
@@ -648,8 +650,9 @@ export function buildJournalEntry({
|
|
|
648
650
|
infoboxes = [],
|
|
649
651
|
notice = "",
|
|
650
652
|
resolveRole,
|
|
653
|
+
syntax,
|
|
651
654
|
}) {
|
|
652
|
-
const rawPages = splitPages(markdown, leadName, resolveRole);
|
|
655
|
+
const rawPages = splitPages(markdown, leadName, resolveRole, syntax);
|
|
653
656
|
const pages = buildPages(
|
|
654
657
|
rawPages,
|
|
655
658
|
id,
|
|
@@ -827,6 +830,7 @@ export class Journals extends BasePackCompiler {
|
|
|
827
830
|
const roles = pathnameRoles(this.linkIndex);
|
|
828
831
|
|
|
829
832
|
return buildJournalEntry({
|
|
833
|
+
syntax: this.corpus?.syntax,
|
|
830
834
|
id,
|
|
831
835
|
name,
|
|
832
836
|
markdown: [
|
package/engine/language-data.mjs
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* syllables, its consonants per vowel or both, each range's `min` no
|
|
45
45
|
* higher than its `max`; an edge stands once for each class of name; a kind
|
|
46
46
|
* of name has one set of draws; and a held-from row takes `elements` only
|
|
47
|
-
* where it takes an `element`, and is clipped or given a
|
|
47
|
+
* where it takes an `element`, and is clipped or given a joining ending only
|
|
48
48
|
* where it takes a name.
|
|
49
49
|
*
|
|
50
50
|
* @module
|
|
@@ -627,7 +627,7 @@ export function checkLanguageData(note, { index, types } = {}) {
|
|
|
627
627
|
),
|
|
628
628
|
);
|
|
629
629
|
if (!HELD_NAMES.includes(row.takes))
|
|
630
|
-
for (const key of ["clip", "
|
|
630
|
+
for (const key of ["clip", "joining"])
|
|
631
631
|
if (row[key] != null)
|
|
632
632
|
findings.push(
|
|
633
633
|
finding(
|
package/engine/language-rows.mjs
CHANGED
|
@@ -122,7 +122,7 @@ export const HELD_FROM_COLUMNS = Object.freeze([
|
|
|
122
122
|
"source",
|
|
123
123
|
"takes",
|
|
124
124
|
"clip",
|
|
125
|
-
"
|
|
125
|
+
"joining",
|
|
126
126
|
"elements",
|
|
127
127
|
]);
|
|
128
128
|
/** Every column of the `near_names` relation, in order. */
|
|
@@ -232,7 +232,7 @@ export const LANGUAGE_COLUMN_TYPES = Object.freeze({
|
|
|
232
232
|
},
|
|
233
233
|
name_edges: { position: "BIGINT", sounds: LIST, vowel: "BOOLEAN" },
|
|
234
234
|
name_draws: { position: "BIGINT", elements: LIST },
|
|
235
|
-
held_from: { position: "BIGINT",
|
|
235
|
+
held_from: { position: "BIGINT", joining: LIST, elements: LIST },
|
|
236
236
|
near_names: { position: "BIGINT", min_syllables: "BIGINT" },
|
|
237
237
|
stress: {},
|
|
238
238
|
finals: { position: "BIGINT" },
|
|
@@ -653,8 +653,8 @@ export function heldFromRows(records) {
|
|
|
653
653
|
return listRows(
|
|
654
654
|
records,
|
|
655
655
|
(data) => namingOf(data).heldFrom,
|
|
656
|
-
["source", "takes", "clip", "
|
|
657
|
-
{ lists: ["
|
|
656
|
+
["source", "takes", "clip", "joining", "elements"],
|
|
657
|
+
{ lists: ["joining", "elements"] },
|
|
658
658
|
);
|
|
659
659
|
}
|
|
660
660
|
|
|
@@ -1142,10 +1142,10 @@ export const ELEMENT_FIELDS = Object.freeze([
|
|
|
1142
1142
|
"ancestor.",
|
|
1143
1143
|
}),
|
|
1144
1144
|
Object.freeze({
|
|
1145
|
-
name: "
|
|
1145
|
+
name: "joining",
|
|
1146
1146
|
kind: "string",
|
|
1147
1147
|
shape: "the element as written, such as `-borgar`",
|
|
1148
|
-
describe: "The element
|
|
1148
|
+
describe: "The element's joining form, as written when it qualifies another word.",
|
|
1149
1149
|
}),
|
|
1150
1150
|
]);
|
|
1151
1151
|
|
|
@@ -1472,11 +1472,11 @@ export const HELD_FROM_FIELDS = Object.freeze([
|
|
|
1472
1472
|
describe: "How the name is clipped: `first`, to its first element; whole where absent.",
|
|
1473
1473
|
}),
|
|
1474
1474
|
Object.freeze({
|
|
1475
|
-
name: "
|
|
1475
|
+
name: "joining",
|
|
1476
1476
|
kind: "list",
|
|
1477
1477
|
shape: "a list of endings",
|
|
1478
1478
|
entries: STRINGS,
|
|
1479
|
-
describe: "The
|
|
1479
|
+
describe: "The joining endings, one of which stands at the seam after the name.",
|
|
1480
1480
|
}),
|
|
1481
1481
|
Object.freeze({
|
|
1482
1482
|
name: "elements",
|
|
@@ -162,8 +162,8 @@ export function lexicalSectionsFor(records, all = records) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
/** Authored section headings outside literal content and named containers. */
|
|
165
|
-
function headings(markdown) {
|
|
166
|
-
const containers = scanBlocks(markdown).blocks;
|
|
165
|
+
function headings(markdown, syntax) {
|
|
166
|
+
const containers = (syntax ? syntax(markdown).blocks : scanBlocks(markdown)).blocks;
|
|
167
167
|
return scanHeadingAttributes(markdown)
|
|
168
168
|
.headings.filter(
|
|
169
169
|
(heading) =>
|
|
@@ -181,7 +181,7 @@ function headings(markdown) {
|
|
|
181
181
|
*
|
|
182
182
|
* @param {string} markdown - The body with authored directives expanded.
|
|
183
183
|
* @param {LexicalSection[]} [sections=[]] - From {@link lexicalSectionsFor}.
|
|
184
|
-
* @param {{authored?: string, lineMap?: object[], generatedFrom?: number}} [context] -
|
|
184
|
+
* @param {{authored?: string, lineMap?: object[], generatedFrom?: number, syntax?: import("./content-session.mjs").ContentSyntax}} [context] -
|
|
185
185
|
* The original authored body, source mappings and the start of appended
|
|
186
186
|
* nonlexical generated sections. Without `authored`, `markdown` is the source.
|
|
187
187
|
* @returns {{markdown: string, lineMap: object[], generatedFrom?: number}}
|
|
@@ -189,7 +189,7 @@ function headings(markdown) {
|
|
|
189
189
|
export function insertLexicalSections(
|
|
190
190
|
markdown,
|
|
191
191
|
sections = [],
|
|
192
|
-
{ authored = markdown, lineMap, generatedFrom } = {},
|
|
192
|
+
{ authored = markdown, lineMap, generatedFrom, syntax } = {},
|
|
193
193
|
) {
|
|
194
194
|
const lines = String(markdown ?? "").split("\n");
|
|
195
195
|
const locations = lineMap ? [...lineMap] : lines.map((_, line) => ({ line, generated: false }));
|
|
@@ -200,7 +200,7 @@ export function insertLexicalSections(
|
|
|
200
200
|
...(generatedFrom === undefined ? {} : { generatedFrom }),
|
|
201
201
|
};
|
|
202
202
|
const edits = [];
|
|
203
|
-
const realHeadings = headings(String(authored ?? ""))
|
|
203
|
+
const realHeadings = headings(String(authored ?? ""), syntax)
|
|
204
204
|
.map((heading) => ({
|
|
205
205
|
...heading,
|
|
206
206
|
sourceLine: heading.line,
|
|
@@ -19,14 +19,19 @@ import { scanAlerts } from "./content-alerts.mjs";
|
|
|
19
19
|
* Frontmatter is masked so literal YAML values cannot become body markup.
|
|
20
20
|
* @param {string} source - Complete Markdown source, optionally with frontmatter.
|
|
21
21
|
* @param {string} file - Diagnostic filename.
|
|
22
|
+
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Exact-text scanner projections.
|
|
22
23
|
* @returns {Array<{file: string, line: number, column: number, severity: string, message: string}>} Syntax errors.
|
|
23
24
|
*/
|
|
24
|
-
export function markupFindings(source, file) {
|
|
25
|
+
export function markupFindings(source, file, syntax) {
|
|
25
26
|
const body = String(source ?? "").replace(
|
|
26
27
|
/^\uFEFF?---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/,
|
|
27
28
|
(frontmatter) => frontmatter.replace(/[^\r\n]/g, " "),
|
|
28
29
|
);
|
|
29
|
-
|
|
30
|
+
const scanned = syntax?.(body);
|
|
31
|
+
return [
|
|
32
|
+
...(scanned?.blocks ?? scanBlocks(body)).errors,
|
|
33
|
+
...(scanned?.alerts ?? scanAlerts(body)).errors,
|
|
34
|
+
].map((error) => ({
|
|
30
35
|
file,
|
|
31
36
|
...error,
|
|
32
37
|
severity: "error",
|
|
@@ -445,6 +445,7 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
|
|
|
445
445
|
const foundry = record.foundry?.[parts.system];
|
|
446
446
|
index.set(key, {
|
|
447
447
|
name: record.name?.full ?? record.name,
|
|
448
|
+
aliases: record.name?.aliases,
|
|
448
449
|
// Absent where the package publishes no page for it. A consumer
|
|
449
450
|
// must tolerate that rather than invent an href, exactly as it
|
|
450
451
|
// already tolerates an entry with no `uuid`.
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
/** Pure projections of parsed inline text and original marked-name positions. */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An original source range with its opening file position.
|
|
18
|
+
* @typedef {object} SpanSourceSite
|
|
19
|
+
* @property {number} start - Inclusive UTF-16 source offset.
|
|
20
|
+
* @property {number} end - Exclusive UTF-16 source offset.
|
|
21
|
+
* @property {number} line - Original 1-based source line.
|
|
22
|
+
* @property {number} column - Original 1-based UTF-16 source column.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The retained attribute key and value at their authored positions.
|
|
27
|
+
* @typedef {object} SpanAttributePosition
|
|
28
|
+
* @property {SpanSourceSite} key - Attribute name, without surrounding whitespace.
|
|
29
|
+
* @property {SpanSourceSite} value - Authored value, without its surrounding quotes.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One actual bracketed span and its renderer-derived visible text.
|
|
34
|
+
* @typedef {object} ContentSpan
|
|
35
|
+
* @property {string} id - Normalized anchor, or an empty string.
|
|
36
|
+
* @property {number} start - Inclusive UTF-16 offset of the opening bracket.
|
|
37
|
+
* @property {number} end - Exclusive UTF-16 offset after the complete span.
|
|
38
|
+
* @property {number} line - Original 1-based line of the opening bracket.
|
|
39
|
+
* @property {number} column - Original 1-based UTF-16 column of the bracket.
|
|
40
|
+
* @property {string} text - Inline label source passed to the existing parser.
|
|
41
|
+
* @property {number} contentEnd - Original UTF-16 offset of the closing label bracket.
|
|
42
|
+
* @property {string} visible - Visible text from the actual parsed children.
|
|
43
|
+
* @property {ReadonlyArray<readonly [string, string]>} attrs - Retained permitted HTML attributes.
|
|
44
|
+
* @property {Record<string, SpanAttributePosition>} attributePositions - Authored key/value sites by retained attribute name.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One located span-grammar or refused-attribute finding.
|
|
49
|
+
* @typedef {object} SpanFinding
|
|
50
|
+
* @property {number} line - Original 1-based source line.
|
|
51
|
+
* @property {number} column - Original 1-based UTF-16 source column.
|
|
52
|
+
* @property {string} message - The existing grammar or attribute diagnostic.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A captured authored span inventory.
|
|
57
|
+
* @typedef {object} SpanScan
|
|
58
|
+
* @property {ContentSpan[]} spans - All actual parsed bracketed spans in source order.
|
|
59
|
+
* @property {ContentSpan[]} names - The same span objects carrying the exact name class.
|
|
60
|
+
* @property {SpanFinding[]} errors - Located diagnostics in source order.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Visible name text from the renderer's actual parsed children. Footnote
|
|
65
|
+
* markers and images supply no name text; references have already resolved.
|
|
66
|
+
* @param {readonly object[]} tokens - Parsed inline tokens.
|
|
67
|
+
* @returns {string} Text without inline formatting or annotation markers.
|
|
68
|
+
*/
|
|
69
|
+
export function inlineText(tokens) {
|
|
70
|
+
return tokens
|
|
71
|
+
.map((token) => {
|
|
72
|
+
if (token.children && token.type !== "image") return inlineText(token.children);
|
|
73
|
+
if (token.type === "text" || token.type === "code_inline") return token.content;
|
|
74
|
+
if (token.type === "softbreak" || token.type === "hardbreak") return " ";
|
|
75
|
+
return "";
|
|
76
|
+
})
|
|
77
|
+
.join("");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Rebase an authored body's captured names to its original raw file.
|
|
82
|
+
* The inventory is read only; no source parsing or second marker scan occurs.
|
|
83
|
+
* @template {ContentSpan} Span
|
|
84
|
+
* @param {readonly Span[]} spans - The captured body's name spans.
|
|
85
|
+
* @param {{raw: string, bodyLine?: number, bodyColumn?: number}} source - Original body origin.
|
|
86
|
+
* @returns {Span[]} Copies retaining the span metadata with raw offsets and file coordinates.
|
|
87
|
+
*/
|
|
88
|
+
export function rebaseNameSpans(spans, { raw, bodyLine = 1, bodyColumn = 1 }) {
|
|
89
|
+
let origin = 0;
|
|
90
|
+
for (let line = 1; line < bodyLine; line++) origin = raw.indexOf("\n", origin) + 1;
|
|
91
|
+
origin += bodyColumn - 1;
|
|
92
|
+
/**
|
|
93
|
+
* @template {SpanSourceSite} Site
|
|
94
|
+
* @param {Site} position - Captured relative range and opening position.
|
|
95
|
+
* @returns {Site} The same metadata at original file coordinates.
|
|
96
|
+
*/
|
|
97
|
+
const rebase = (position) => ({
|
|
98
|
+
...position,
|
|
99
|
+
start: position.start + origin,
|
|
100
|
+
end: position.end + origin,
|
|
101
|
+
line: bodyLine + position.line - 1,
|
|
102
|
+
column: position.column + (position.line === 1 ? bodyColumn - 1 : 0),
|
|
103
|
+
});
|
|
104
|
+
return spans.map((span) => ({
|
|
105
|
+
...rebase(span),
|
|
106
|
+
contentEnd: span.contentEnd + origin,
|
|
107
|
+
attributePositions: Object.fromEntries(
|
|
108
|
+
Object.entries(span.attributePositions ?? {}).map(([key, positions]) => [
|
|
109
|
+
key,
|
|
110
|
+
{ key: rebase(positions.key), value: rebase(positions.value) },
|
|
111
|
+
]),
|
|
112
|
+
),
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
* Authored `.name` occurrences projected from own-note index records into the
|
|
16
|
+
* SQL `names` relation. Documentation records and assets contribute no rows.
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { canonicalKey, readCanonicalKey } from "./address.mjs";
|
|
21
|
+
import { encodeAddresses } from "./address-values.mjs";
|
|
22
|
+
import { isNoteRecord } from "./index-records.mjs";
|
|
23
|
+
|
|
24
|
+
/** Every column of the `names` relation, in order. */
|
|
25
|
+
export const NAME_COLUMNS = Object.freeze(["owner", "name", "type", "subType", "line", "column"]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} NameRow
|
|
29
|
+
* @property {string|null} owner - Containing authored page's canonical NOTE Address.
|
|
30
|
+
* @property {string} name - Rendered visible text of the occurrence.
|
|
31
|
+
* @property {string|null} type - Supplied note type, or null.
|
|
32
|
+
* @property {string|null} subType - Supplied subtype, or null.
|
|
33
|
+
* @property {number} line - Original 1-based source line at the opening bracket.
|
|
34
|
+
* @property {number} column - Original 1-based UTF-16 source column at that bracket.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The containing page's NOTE identity, including for a native Item source.
|
|
39
|
+
* @param {Record<string, any>} record - An own-note index record.
|
|
40
|
+
* @returns {string|null} The page Address, or null for an unaddressable record.
|
|
41
|
+
*/
|
|
42
|
+
function ownerOf(record) {
|
|
43
|
+
const parts = readCanonicalKey(encodeAddresses(record.address?.canonical));
|
|
44
|
+
return parts ? canonicalKey(parts.package, "note", parts.type, parts.shortcode) : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Project captured occurrences without reading sources or changing records.
|
|
49
|
+
* @param {Iterable<Record<string, any>>} records - Index records, already narrowed
|
|
50
|
+
* to the audience; canonical Addresses may be typed or encoded strings.
|
|
51
|
+
* @returns {NameRow[]} One row per occurrence, in record then source order.
|
|
52
|
+
*/
|
|
53
|
+
export function namesRows(records) {
|
|
54
|
+
const rows = [];
|
|
55
|
+
for (const record of records) {
|
|
56
|
+
if (!record || !isNoteRecord(record) || !Array.isArray(record.names)) continue;
|
|
57
|
+
const owner = ownerOf(record);
|
|
58
|
+
for (const entry of record.names) {
|
|
59
|
+
rows.push({
|
|
60
|
+
owner,
|
|
61
|
+
name: entry.name,
|
|
62
|
+
type: entry.type ?? null,
|
|
63
|
+
subType: entry.subType ?? null,
|
|
64
|
+
line: entry.line,
|
|
65
|
+
column: entry.column,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return rows;
|
|
70
|
+
}
|