@heroiclands/package-build 22.9.1 → 22.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 +54 -0
- package/CONTENT.md +33 -0
- package/bin/content-build.mjs +3 -1
- package/docs/api.md +10 -6
- package/docs/content-format.md +384 -200
- package/docs/project-setup.md +23 -0
- package/engine/actor-compiler.mjs +20 -7
- package/engine/address-fields.mjs +404 -0
- package/engine/address-values.mjs +80 -0
- package/engine/address.mjs +769 -0
- package/engine/art-fields.mjs +112 -97
- package/engine/art-slots.mjs +81 -0
- package/engine/base-compiler.mjs +59 -7
- package/engine/bundle-notes.mjs +5 -2
- package/engine/bundles.mjs +17 -10
- package/engine/content-address.mjs +23 -186
- package/engine/content-embeds.mjs +18 -4
- package/engine/content-format-check.mjs +50 -22
- package/engine/content-index.mjs +85 -43
- package/engine/content-links.mjs +105 -104
- package/engine/content-lint.mjs +11 -1
- package/engine/content-tables.mjs +9 -1
- package/engine/field-spec.mjs +20 -4
- package/engine/folder-notes.mjs +44 -15
- package/engine/foundry-entries.mjs +3 -0
- package/engine/frontmatter-lint.mjs +144 -43
- package/engine/frontmatter.mjs +55 -10
- package/engine/generate.mjs +7 -1
- package/engine/helpers.mjs +37 -9
- package/engine/holdings.mjs +41 -12
- package/engine/index-records.mjs +2 -0
- package/engine/infobox-render.mjs +2 -1
- package/engine/infobox.mjs +26 -6
- package/engine/item-compiler.mjs +5 -0
- package/engine/item-docs.mjs +7 -44
- package/engine/map-dot.mjs +2 -0
- package/engine/map-notes.mjs +7 -2
- package/engine/map-places.mjs +42 -10
- package/engine/metadata-index.mjs +60 -5
- package/engine/note-addresses.mjs +310 -0
- package/engine/note-vocabulary.mjs +145 -26
- package/engine/pdf-build.mjs +1 -1
- package/engine/pdf-toc.mjs +17 -2
- package/engine/place-relations.mjs +160 -50
- package/engine/populations.mjs +17 -9
- package/engine/reference-targets.mjs +54 -0
- package/engine/scenes.mjs +5 -2
- package/engine/shortcode-references.mjs +68 -0
- package/engine/site-build.mjs +39 -15
- package/engine/site-index.mjs +57 -75
- package/engine/sql-tables.mjs +43 -9
- package/engine/system-block.mjs +21 -0
- package/engine/system-vocabulary.mjs +46 -0
- package/engine/web-wikilinks.mjs +11 -9
- package/engine/wikilink-syntax.mjs +21 -0
- package/engine/wikilinks.mjs +92 -306
- package/githooks/attribution.mjs +82 -0
- package/githooks/commit-message.mjs +26 -0
- package/githooks/commit-msg +3 -35
- package/hm3/actors.mjs +8 -24
- package/hm3/item-fields.mjs +15 -5
- package/package.json +1 -1
- package/sohl/infobox.mjs +80 -17
- package/sohl/item-fields.mjs +114 -41
- package/types/engine/address-fields.d.mts +90 -0
- package/types/engine/address-values.d.mts +34 -0
- package/types/engine/address.d.mts +452 -0
- package/types/engine/art-fields.d.mts +59 -53
- package/types/engine/art-slots.d.mts +40 -0
- package/types/engine/base-compiler.d.mts +24 -2
- package/types/engine/bundle-notes.d.mts +2 -2
- package/types/engine/content-address.d.mts +3 -139
- package/types/engine/content-embeds.d.mts +1 -1
- package/types/engine/content-format-check.d.mts +25 -0
- package/types/engine/content-index.d.mts +2 -2
- package/types/engine/content-links.d.mts +6 -5
- package/types/engine/field-spec.d.mts +44 -9
- package/types/engine/folder-notes.d.mts +11 -10
- package/types/engine/frontmatter-lint.d.mts +7 -97
- package/types/engine/frontmatter.d.mts +16 -1
- package/types/engine/helpers.d.mts +2 -2
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/item-docs.d.mts +1 -35
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-addresses.d.mts +41 -0
- package/types/engine/note-vocabulary.d.mts +20 -3
- package/types/engine/place-relations.d.mts +12 -1
- package/types/engine/populations.d.mts +17 -2
- package/types/engine/reference-targets.d.mts +9 -0
- package/types/engine/shortcode-references.d.mts +14 -0
- package/types/engine/site-build.d.mts +3 -3
- package/types/engine/site-index.d.mts +9 -30
- package/types/engine/sql-tables.d.mts +2 -2
- package/types/engine/system-block.d.mts +17 -0
- package/types/engine/system-vocabulary.d.mts +16 -0
- package/types/engine/wikilink-syntax.d.mts +7 -0
- package/types/engine/wikilinks.d.mts +6 -109
- package/types/hm3/item-fields.d.mts +5 -0
- package/types/sohl/infobox.d.mts +4 -5
- package/types/sohl/item-fields.d.mts +8 -0
package/engine/content-links.mjs
CHANGED
|
@@ -24,10 +24,9 @@
|
|
|
24
24
|
* dead-ends for the reader.
|
|
25
25
|
* 2. **A dead address.** Every link is an address, and one resolving to no note
|
|
26
26
|
* is a typo. So is a target that does not parse as an address at all. A
|
|
27
|
-
* written target
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* naming every candidate.
|
|
27
|
+
* written target expands to exactly one canonical address, from the
|
|
28
|
+
* position's own defaults — the package it omits is always this one — so a
|
|
29
|
+
* lookup finds one entry or none. There is no candidate set to disambiguate.
|
|
31
30
|
* 3. **An unlabelled link.** `[[x]]` addresses nothing: the alias namespace it
|
|
32
31
|
* once named is retired, and a shortcode is an address rather than
|
|
33
32
|
* prose, so the link has neither a resolvable target nor text to show. The
|
|
@@ -35,7 +34,7 @@
|
|
|
35
34
|
* 4. **A wikilink authored in frontmatter.** Both builds walk a note's *body*
|
|
36
35
|
* and copy frontmatter through verbatim, so a link written in a
|
|
37
36
|
* `description` is never resolved and publishes as literal `[[…]]` text.
|
|
38
|
-
* Frontmatter is data:
|
|
37
|
+
* Frontmatter is data: an `Address` field is parsed by the address grammar
|
|
39
38
|
* and a bracketed link there is a finding naming the note and the field.
|
|
40
39
|
*
|
|
41
40
|
* **This resolves links the way the builds do**, calling the same
|
|
@@ -54,6 +53,8 @@
|
|
|
54
53
|
* @module
|
|
55
54
|
*/
|
|
56
55
|
|
|
56
|
+
import { readWikilink } from "./wikilink-syntax.mjs";
|
|
57
|
+
import { encodeAddresses } from "./address-values.mjs";
|
|
57
58
|
import fs from "node:fs";
|
|
58
59
|
import path from "node:path";
|
|
59
60
|
|
|
@@ -208,7 +209,8 @@ export function buildLinkIndex(
|
|
|
208
209
|
// alongside the notes because `byKey` holds notes, and a caller that
|
|
209
210
|
// reaches for `.fm` or `.body` on one must not be handed a file.
|
|
210
211
|
if (isAssetRecord(record)) {
|
|
211
|
-
if (record.address?.canonical)
|
|
212
|
+
if (record.address?.canonical)
|
|
213
|
+
byAssetKey.set(encodeAddresses(record.address.canonical), record);
|
|
212
214
|
continue;
|
|
213
215
|
}
|
|
214
216
|
// A documentation journal has a record of its own but no file and no
|
|
@@ -247,7 +249,7 @@ export function buildLinkIndex(
|
|
|
247
249
|
// Taken from the record, which is where the address rule is applied
|
|
248
250
|
// once for the whole build.
|
|
249
251
|
const canonical =
|
|
250
|
-
record.address?.canonical ??
|
|
252
|
+
encodeAddresses(record.address?.canonical) ??
|
|
251
253
|
canonicalKey(pkg, systemOf(type, KNOWN_DOCUMENT_SUBTYPE_MAPS), type, fm.shortcode);
|
|
252
254
|
// A note with no address publishes no page, so its identity is
|
|
253
255
|
// filed where a link cannot reach it and a reference still can.
|
|
@@ -285,7 +287,12 @@ export function buildLinkIndex(
|
|
|
285
287
|
: { index: new Map(), packages: new Set(), stale: [] };
|
|
286
288
|
for (const v of foreign.index.values()) if (v.type) types.add(v.type);
|
|
287
289
|
|
|
288
|
-
|
|
290
|
+
// A stub counts. It publishes no page, so nothing may *link* to one, but its
|
|
291
|
+
// address is a name the tree holds and a reference reaches it — a border's
|
|
292
|
+
// far side is the case that matters. Leaving this package out of the set
|
|
293
|
+
// whenever every note it holds is a stub would make a fully qualified local
|
|
294
|
+
// address unreadable in precisely the tree whose relations need it.
|
|
295
|
+
const packages = new Set([...(byKey.size || byStub.size ? [pkg] : []), ...foreign.packages]);
|
|
289
296
|
// Packages declared `contentIndex: false` — a Foundry dependency only, with
|
|
290
297
|
// no fetched index. A link naming one is refused with a diagnostic that
|
|
291
298
|
// names the key, rather than reading as an undeclared package or a typo.
|
|
@@ -414,8 +421,15 @@ export function buildLinkIndex(
|
|
|
414
421
|
// Code is verbatim, so a `[[…]]` inside a fence, an indented block or an
|
|
415
422
|
// inline span is not a link — the compilers make none of it either.
|
|
416
423
|
for (const [all, rawInner] of matchAllOutsideCode(body, new RegExp(WIKILINK.source, "g"))) {
|
|
417
|
-
const parsed =
|
|
418
|
-
|
|
424
|
+
const parsed = readWikilink(rawInner, {
|
|
425
|
+
package: pkg,
|
|
426
|
+
system: "none",
|
|
427
|
+
types,
|
|
428
|
+
packages,
|
|
429
|
+
noIndexPackages,
|
|
430
|
+
});
|
|
431
|
+
const target = parsed.target.kind ? parsed.rawTarget : parsed.target;
|
|
432
|
+
const { anchor, rawTarget } = parsed;
|
|
419
433
|
const occurrence = (seen.get(all) ?? 0) + 1;
|
|
420
434
|
seen.set(all, occurrence);
|
|
421
435
|
// `text` is the link exactly as authored, which is what locates it
|
|
@@ -423,6 +437,7 @@ export function buildLinkIndex(
|
|
|
423
437
|
// so the search simply fails and a finding names the file.
|
|
424
438
|
out.push({
|
|
425
439
|
target,
|
|
440
|
+
rawTarget,
|
|
426
441
|
anchor,
|
|
427
442
|
text: all,
|
|
428
443
|
occurrence,
|
|
@@ -432,45 +447,6 @@ export function buildLinkIndex(
|
|
|
432
447
|
return out;
|
|
433
448
|
}
|
|
434
449
|
|
|
435
|
-
/**
|
|
436
|
-
* Every indexed entry an address names, matching only the segments it
|
|
437
|
-
* supplies.
|
|
438
|
-
*
|
|
439
|
-
* This is the whole resolution rule in one place: a written
|
|
440
|
-
* address is a *partial* one, unsupplied segments are wildcards, and the
|
|
441
|
-
* caller requires exactly one hit. Nothing here decides an ambiguity — zero
|
|
442
|
-
* and many are different findings with different fixes, so the count is
|
|
443
|
-
* returned rather than collapsed.
|
|
444
|
-
*
|
|
445
|
-
* The **system** is wildcarded unless stated. Defaulting it to `none` would
|
|
446
|
-
* exclude every link to an item, which is most of them.
|
|
447
|
-
*
|
|
448
|
-
* @param {Array<[string, any]>} pairs - Indexed `[canonicalKey, value]`.
|
|
449
|
-
* @param {object} q - The parsed qualifier.
|
|
450
|
-
* @returns {Array<[string, any]>} The matching pairs.
|
|
451
|
-
*/
|
|
452
|
-
function matchAddress(pairs, q) {
|
|
453
|
-
const type = String(q.type).toLowerCase();
|
|
454
|
-
const shortcode = String(q.shortcode).toLowerCase();
|
|
455
|
-
return pairs.filter(([k]) => {
|
|
456
|
-
const parts = readCanonicalKey(k);
|
|
457
|
-
if (!parts) return false;
|
|
458
|
-
if (q.package && parts.package !== String(q.package).toLowerCase()) return false;
|
|
459
|
-
if (q.system && parts.system !== String(q.system).toLowerCase()) return false;
|
|
460
|
-
return parts.type === type && parts.shortcode === shortcode;
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* The local notes an address names, by the same rule.
|
|
466
|
-
*
|
|
467
|
-
* @param {object} q - The parsed qualifier.
|
|
468
|
-
* @returns {object[]} The notes.
|
|
469
|
-
*/
|
|
470
|
-
function matchLocal(q) {
|
|
471
|
-
return matchAddress([...byKey], q).map(([, v]) => v);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
450
|
/**
|
|
475
451
|
* The note an **address** names, or `undefined`.
|
|
476
452
|
*
|
|
@@ -551,7 +527,7 @@ export function buildLinkIndex(
|
|
|
551
527
|
// saying so is the whole point: a link that resolved into `sohl` only
|
|
552
528
|
// because no local note claimed the address was resolving by accident,
|
|
553
529
|
// and would have retargeted silently the day one did.
|
|
554
|
-
if (!q.package) return [];
|
|
530
|
+
if (!q.package || q.package === pkg) return [];
|
|
555
531
|
const hit = foreign.index.get(
|
|
556
532
|
expandAddress(q, { package: q.package, system: blockSystem(keyPath) }),
|
|
557
533
|
);
|
|
@@ -573,38 +549,71 @@ export function buildLinkIndex(
|
|
|
573
549
|
}
|
|
574
550
|
|
|
575
551
|
/**
|
|
576
|
-
* The note or foreign entry
|
|
552
|
+
* The note, asset, stub or foreign entry an **address** names, or
|
|
553
|
+
* `undefined`.
|
|
577
554
|
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
555
|
+
* The same rule as {@link resolveAddress} — read with {@link readQualifier},
|
|
556
|
+
* expanded with {@link expandAddress} against this index's own package and
|
|
557
|
+
* the citing block's system — consulted against every source this index
|
|
558
|
+
* holds rather than only the notes a page link may reach: a stub the tree
|
|
559
|
+
* holds and a fetched foreign package both answer here, where neither
|
|
560
|
+
* answers a page link. A caller with its own position-specific default
|
|
561
|
+
* type — a place's `to`, whose default is `place` rather than anything
|
|
562
|
+
* this index states — parses and expands the value itself and hands this
|
|
563
|
+
* the canonical string it already computed; every segment that string
|
|
564
|
+
* states overrides the default that would otherwise apply, so it round-trips
|
|
565
|
+
* to itself and this is a plain keyed lookup either way.
|
|
584
566
|
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
567
|
+
* @param {string} target - The address, or a short form read against this
|
|
568
|
+
* index's own defaults.
|
|
569
|
+
* @param {string} [keyPath] - The dotted frontmatter key path the value
|
|
570
|
+
* sits under; body prose has none. Ignored once `target` is already
|
|
571
|
+
* canonical.
|
|
572
|
+
* @returns {object|undefined} The entry.
|
|
573
|
+
*/
|
|
574
|
+
function addressHit(target, keyPath) {
|
|
575
|
+
const qualified = readQualifier(target, types, packages, noIndexPackages);
|
|
576
|
+
if (!qualified || qualified.reason) return undefined;
|
|
577
|
+
const canonical = expandAddress(qualified, { package: pkg, system: blockSystem(keyPath) });
|
|
578
|
+
// Stubs and foreign entries answer here and nowhere else in this
|
|
579
|
+
// index: a reference names an item to stand beside rather than a
|
|
580
|
+
// document to point at, so a border with a stub — or a place in a
|
|
581
|
+
// dependency — on the far side is a valid statement about the world.
|
|
582
|
+
return (
|
|
583
|
+
byKey.get(canonical) ??
|
|
584
|
+
byAssetKey.get(canonical) ??
|
|
585
|
+
byStub.get(canonical) ??
|
|
586
|
+
foreign.index.get(canonical)
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* The note, asset, stub or foreign entry a **Shortcode** names, or `null`.
|
|
588
592
|
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
593
|
+
* A `code:` field's value is a Shortcode, not an address — one segment,
|
|
594
|
+
* persisted verbatim and resolved at runtime against the items embedded on
|
|
595
|
+
* one actor, where packages do not exist. So `type` and `shortcode` are
|
|
596
|
+
* searched for directly rather than assembled into a written form and read
|
|
597
|
+
* back through the address grammar: a Shortcode never reaches that grammar,
|
|
598
|
+
* by {@link module:engine/address}'s own contract. It resolves in any
|
|
599
|
+
* reachable package, because the value comes from every package the actor
|
|
600
|
+
* draws on — package and system are never part of the search, not defaulted
|
|
601
|
+
* out of it.
|
|
592
602
|
*
|
|
593
|
-
* @param {string}
|
|
594
|
-
* @
|
|
603
|
+
* @param {string} type - The field's declared `code:`.
|
|
604
|
+
* @param {string} shortcode - The value as authored, already checked
|
|
605
|
+
* against the shortcode charset.
|
|
606
|
+
* @returns {object|null} The first entry naming the pair, from local notes,
|
|
607
|
+
* local assets, local stubs, or a fetched foreign index, in that order.
|
|
595
608
|
*/
|
|
596
|
-
function
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
const local = matchAddress([...byKey, ...byAssetKey, ...byStub], q);
|
|
605
|
-
if (local.length) return local[0][1];
|
|
606
|
-
const abroad = matchAddress([...foreign.index], q);
|
|
607
|
-
return abroad.length ? abroad[0][1] : null;
|
|
609
|
+
function shortcodeHit(type, shortcode) {
|
|
610
|
+
const wantType = String(type).toLowerCase();
|
|
611
|
+
const wantCode = String(shortcode).toLowerCase();
|
|
612
|
+
for (const [key, value] of [...byKey, ...byAssetKey, ...byStub, ...foreign.index]) {
|
|
613
|
+
const parts = readCanonicalKey(key);
|
|
614
|
+
if (parts && parts.type === wantType && parts.shortcode === wantCode) return value;
|
|
615
|
+
}
|
|
616
|
+
return null;
|
|
608
617
|
}
|
|
609
618
|
|
|
610
619
|
return {
|
|
@@ -646,7 +655,8 @@ export function buildLinkIndex(
|
|
|
646
655
|
stubAt,
|
|
647
656
|
manifestHit,
|
|
648
657
|
foreignHits,
|
|
649
|
-
|
|
658
|
+
addressHit,
|
|
659
|
+
shortcodeHit,
|
|
650
660
|
/** Whether a target reads as a qualified address at all. */
|
|
651
661
|
isAddress: (target) => Boolean(readQualifier(target, types, packages, noIndexPackages)),
|
|
652
662
|
};
|
|
@@ -936,11 +946,12 @@ export function auditHomepageLinks(index) {
|
|
|
936
946
|
* unlabelledLinks: object[], frontmatterLinks: object[],
|
|
937
947
|
* homepageLinks: object[], usedManifest: Set<string>}} The findings, and
|
|
938
948
|
* which addresses a foreign manifest answered. Each `deadAddresses` entry
|
|
939
|
-
* carries a `reason` from {@link LINK_FINDING_REASONS} —
|
|
940
|
-
* `"
|
|
941
|
-
* `
|
|
942
|
-
*
|
|
943
|
-
*
|
|
949
|
+
* carries a `reason` from {@link LINK_FINDING_REASONS} — `"not-an-address"`,
|
|
950
|
+
* `"unknown-type"`, `"no-content-index"`, `"stub"` (with the stub's path) or
|
|
951
|
+
* `"unresolved"`, never `"ambiguous"`: every target here expands to one
|
|
952
|
+
* canonical address before anything is looked up, so there is no candidate
|
|
953
|
+
* set for two packages to claim — and every reason is an **error**: the
|
|
954
|
+
* three resolvers agree on severity for every class.
|
|
944
955
|
*/
|
|
945
956
|
export function auditLinks(index) {
|
|
946
957
|
const { notes, anchors, linksOf, embedsOf, resolve, manifestHit, isAddress } = index;
|
|
@@ -955,7 +966,7 @@ export function auditLinks(index) {
|
|
|
955
966
|
|
|
956
967
|
const deadAnchors = [];
|
|
957
968
|
for (const note of notes) {
|
|
958
|
-
for (const { target, anchor, text, occurrence, labelled } of linksOf(note)) {
|
|
969
|
+
for (const { target, rawTarget, anchor, text, occurrence, labelled } of linksOf(note)) {
|
|
959
970
|
if (!anchor || !labelled) continue;
|
|
960
971
|
const dest = target ? resolve(target) : note;
|
|
961
972
|
// An unresolvable target is reported by the pass below; its anchor
|
|
@@ -966,7 +977,7 @@ export function auditLinks(index) {
|
|
|
966
977
|
if (!(anchors.get(dest) ?? new Set()).has(slugify(anchor))) {
|
|
967
978
|
deadAnchors.push({
|
|
968
979
|
note,
|
|
969
|
-
link: `${
|
|
980
|
+
link: `${rawTarget}#${anchor}`,
|
|
970
981
|
dest,
|
|
971
982
|
text,
|
|
972
983
|
occurrence,
|
|
@@ -979,13 +990,13 @@ export function auditLinks(index) {
|
|
|
979
990
|
const unlabelledLinks = [];
|
|
980
991
|
const usedManifest = new Set();
|
|
981
992
|
for (const note of notes) {
|
|
982
|
-
for (const { target, anchor, text, occurrence, labelled } of linksOf(note)) {
|
|
993
|
+
for (const { target, rawTarget, anchor, text, occurrence, labelled } of linksOf(note)) {
|
|
983
994
|
// The label is required whatever the link part is, an anchor
|
|
984
995
|
// included — so this is tested before the same-page form.
|
|
985
996
|
if (!labelled) {
|
|
986
997
|
unlabelledLinks.push({
|
|
987
998
|
note,
|
|
988
|
-
target:
|
|
999
|
+
target: rawTarget || (anchor ? `#${anchor}` : ""),
|
|
989
1000
|
text,
|
|
990
1001
|
occurrence,
|
|
991
1002
|
// Carried like every other finding's, so a reporter reads
|
|
@@ -995,7 +1006,7 @@ export function auditLinks(index) {
|
|
|
995
1006
|
continue;
|
|
996
1007
|
}
|
|
997
1008
|
if (!target) continue; // a same-page `[[#anchor|Text]]`
|
|
998
|
-
const at = { note, target, text, occurrence };
|
|
1009
|
+
const at = { note, target: rawTarget, text, occurrence };
|
|
999
1010
|
|
|
1000
1011
|
if (!isAddress(target)) {
|
|
1001
1012
|
deadAddresses.push({ ...at, reason: "not-an-address" });
|
|
@@ -1012,22 +1023,12 @@ export function auditLinks(index) {
|
|
|
1012
1023
|
continue;
|
|
1013
1024
|
}
|
|
1014
1025
|
// A manifest answers with the target package's own build output
|
|
1015
|
-
// rather than a reviewed guess.
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (hits.length > 1) {
|
|
1022
|
-
// Two packages publish the short address, so it names neither.
|
|
1023
|
-
// Reported as its own class: "no document has that identity" is
|
|
1024
|
-
// false here — two do — and the fix is the qualified form
|
|
1025
|
-
// rather than a corrected shortcode.
|
|
1026
|
-
deadAddresses.push({
|
|
1027
|
-
...at,
|
|
1028
|
-
reason: "ambiguous",
|
|
1029
|
-
packages: hits.map((h) => h.package).filter(Boolean),
|
|
1030
|
-
});
|
|
1026
|
+
// rather than a reviewed guess. `manifestHit` is a keyed lookup —
|
|
1027
|
+
// a fully qualified target names one package, so there is nothing
|
|
1028
|
+
// left to disambiguate, and no ambiguity finding this resolver can
|
|
1029
|
+
// report.
|
|
1030
|
+
if (manifestHit(target)) {
|
|
1031
|
+
usedManifest.add(encodeAddresses(target).toLowerCase());
|
|
1031
1032
|
continue;
|
|
1032
1033
|
}
|
|
1033
1034
|
const read = readQualifier(target, index.types, index.packages, index.noIndexPackages);
|
package/engine/content-lint.mjs
CHANGED
|
@@ -160,6 +160,16 @@ function collectNotes(contentBase, { skipDirectories, config, records, problems
|
|
|
160
160
|
file: path.relative(process.cwd(), absPath),
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
|
+
for (let i = (problems?.length ?? 0) - 1; i >= 0; i--) {
|
|
164
|
+
const problem = problems[i];
|
|
165
|
+
if (!problem.identity) continue;
|
|
166
|
+
notes.push({
|
|
167
|
+
fm: problem.identity,
|
|
168
|
+
absPath: problem.file,
|
|
169
|
+
file: path.relative(process.cwd(), problem.file),
|
|
170
|
+
});
|
|
171
|
+
problems.splice(i, 1);
|
|
172
|
+
}
|
|
163
173
|
// The records already come in content-path order, so this only re-states
|
|
164
174
|
// the guarantee findings depend on: they read top to bottom.
|
|
165
175
|
notes.sort((a, b) => (a.absPath < b.absPath ? -1 : 1));
|
|
@@ -291,7 +301,7 @@ function checkRenamedFrom({ fm, file }, raw) {
|
|
|
291
301
|
*/
|
|
292
302
|
export function lintContentTree(
|
|
293
303
|
contentBase,
|
|
294
|
-
{ skipDirectories, contentPackage, config, records, problems } = {},
|
|
304
|
+
{ skipDirectories, contentPackage, config, records, problems = [] } = {},
|
|
295
305
|
) {
|
|
296
306
|
const findings = [];
|
|
297
307
|
const notes = collectNotes(contentBase, { skipDirectories, config, records, problems });
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { completeAddress, renderAddress, isAddressTuple } from "./address.mjs";
|
|
15
|
+
import { contentPackage } from "./content-package.mjs";
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* Generated content tables — Obsidian **Dataview** `TABLE` queries.
|
|
16
19
|
*
|
|
@@ -702,6 +705,7 @@ export function resolveField(doc, path) {
|
|
|
702
705
|
|
|
703
706
|
/** Render a value as the plain text a comparison or a cell works with. */
|
|
704
707
|
function asText(value) {
|
|
708
|
+
if (isAddressTuple(value)) return renderAddress(value);
|
|
705
709
|
if (value == null) return "";
|
|
706
710
|
if (isLink(value)) return value.display;
|
|
707
711
|
if (Array.isArray(value)) return value.map(asText).join(", ");
|
|
@@ -1035,7 +1039,11 @@ export function renderContentTable(spec, rows, linkable, self) {
|
|
|
1035
1039
|
// table cell; the resolvers unescape it before splitting, so the
|
|
1036
1040
|
// display text itself must not carry one.
|
|
1037
1041
|
const label = text.replace(/\\?\|/g, "/");
|
|
1038
|
-
|
|
1042
|
+
const target = completeAddress(
|
|
1043
|
+
{ type: doc.fm.type, shortcode: doc.fm.shortcode },
|
|
1044
|
+
{ package: doc.fm.package ?? contentPackage(), system: "none" },
|
|
1045
|
+
);
|
|
1046
|
+
return `[[${renderAddress(target)}\\|${label}]]`;
|
|
1039
1047
|
}),
|
|
1040
1048
|
);
|
|
1041
1049
|
|
package/engine/field-spec.mjs
CHANGED
|
@@ -138,10 +138,26 @@ export { legacyKeyOf, retiredTopLevelKey, setPath };
|
|
|
138
138
|
* several do — `weight` is coerced leniently but is still a number, and
|
|
139
139
|
* `weight: heavy` is an authoring mistake worth reporting where it was made.
|
|
140
140
|
* Absent means the lint makes no claim about the value.
|
|
141
|
-
* @property {string} [
|
|
142
|
-
* for the linter's dead-reference check.
|
|
143
|
-
*
|
|
144
|
-
*
|
|
141
|
+
* @property {string} [code] - The content type a value names by **Shortcode**,
|
|
142
|
+
* for the linter's dead-reference check. The authored value is one segment —
|
|
143
|
+
* held to {@link module:engine/address-charset.ADDRESS_SEGMENT_PATTERN} —
|
|
144
|
+
* persisted verbatim into the emitted field and resolved at runtime among
|
|
145
|
+
* the items embedded on one actor, where packages do not exist; the linter
|
|
146
|
+
* still forms the `type-shortcode` pair to check it against the index. Only
|
|
147
|
+
* for references to a **note**: `bodyLocationCode` names a part inside a
|
|
148
|
+
* being's own body structure, not a note, so it declares none. Distinct
|
|
149
|
+
* from a `DataFieldSpec`'s `ref` ({@link module:engine/note-vocabulary}),
|
|
150
|
+
* which names an **Address** instead.
|
|
151
|
+
* @property {import("./address-fields.mjs").AddressPosition} [address] - **The
|
|
152
|
+
* authored value is an Address and the emitted one is its Shortcode.** The
|
|
153
|
+
* statement carries the default type an omitted `<type>` segment takes, the
|
|
154
|
+
* set of types the position accepts — two separate things, which coincide at
|
|
155
|
+
* most positions — and where the Addresses sit: the value itself, every
|
|
156
|
+
* element of a list, or every key of a map.
|
|
157
|
+
* {@link module:engine/address-fields.reduceAddressFields} performs the
|
|
158
|
+
* reduction once the block is assembled, so a value authored at the
|
|
159
|
+
* destination is reduced along with one this field read. Distinct from `code`,
|
|
160
|
+
* which is a Shortcode at both ends and resolves nothing.
|
|
145
161
|
* @property {any|((fm: object) => any)} [value] - For a field with no `name`:
|
|
146
162
|
* the constant, or a function deriving it from the frontmatter.
|
|
147
163
|
* @property {boolean} [omitWhenAbsent] - **The key is left out entirely when
|
package/engine/folder-notes.mjs
CHANGED
|
@@ -53,7 +53,9 @@ import path from "node:path";
|
|
|
53
53
|
|
|
54
54
|
import log from "loglevel";
|
|
55
55
|
|
|
56
|
-
import {
|
|
56
|
+
import { isAddressTuple } from "./address.mjs";
|
|
57
|
+
import { NO_SYSTEM, canonicalKey, readCanonicalKey } from "./content-address.mjs";
|
|
58
|
+
import { parseAddress, renderAddress } from "./address.mjs";
|
|
57
59
|
import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./address-charset.mjs";
|
|
58
60
|
import { makeId } from "./ids.mjs";
|
|
59
61
|
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
@@ -65,6 +67,14 @@ import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
|
65
67
|
*/
|
|
66
68
|
export const FOLDER_TYPE = "folder";
|
|
67
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The one type a folder reference may name — {@link parseAddress}'s
|
|
72
|
+
* vocabulary for a position that accepts exactly one.
|
|
73
|
+
*
|
|
74
|
+
* @type {ReadonlySet<string>}
|
|
75
|
+
*/
|
|
76
|
+
const FOLDER_TYPES = Object.freeze(new Set([FOLDER_TYPE]));
|
|
77
|
+
|
|
68
78
|
/**
|
|
69
79
|
* The id namespace a derived folder id is hashed under.
|
|
70
80
|
*
|
|
@@ -135,6 +145,7 @@ function folderFields(fm) {
|
|
|
135
145
|
const read = (key) => data[key] ?? fm?.[key];
|
|
136
146
|
const text = (value) => {
|
|
137
147
|
if (value == null) return null;
|
|
148
|
+
if (isAddressTuple(value)) return value;
|
|
138
149
|
const trimmed = String(value).trim();
|
|
139
150
|
return trimmed === "" ? null : trimmed;
|
|
140
151
|
};
|
|
@@ -142,7 +153,12 @@ function folderFields(fm) {
|
|
|
142
153
|
const authored = read("parent");
|
|
143
154
|
/** @type {Record<string, string|null>} */
|
|
144
155
|
const parent = {};
|
|
145
|
-
if (
|
|
156
|
+
if (
|
|
157
|
+
authored != null &&
|
|
158
|
+
typeof authored === "object" &&
|
|
159
|
+
!Array.isArray(authored) &&
|
|
160
|
+
!isAddressTuple(authored)
|
|
161
|
+
) {
|
|
146
162
|
// An explicit `~` under a pack key means "at the root *there*", which
|
|
147
163
|
// is a different statement from saying nothing — so the key is kept
|
|
148
164
|
// with a null value rather than dropped.
|
|
@@ -157,8 +173,8 @@ function folderFields(fm) {
|
|
|
157
173
|
/**
|
|
158
174
|
* An authored `parent` with any wikilink brackets and label stripped.
|
|
159
175
|
*
|
|
160
|
-
* The specification types `parent` as
|
|
161
|
-
*
|
|
176
|
+
* The specification types `parent` as an `Address`, written bare — but
|
|
177
|
+
* `[[address]]` is what an author reaches for,
|
|
162
178
|
* and Obsidian wrote that form for years. Accepting both costs one regex and
|
|
163
179
|
* removes a failure whose message would have to explain the difference.
|
|
164
180
|
*
|
|
@@ -166,6 +182,7 @@ function folderFields(fm) {
|
|
|
166
182
|
* @returns {string|null} The bare address.
|
|
167
183
|
*/
|
|
168
184
|
export function bareAddress(value) {
|
|
185
|
+
if (isAddressTuple(value)) return value;
|
|
169
186
|
if (value == null) return null;
|
|
170
187
|
const text = String(value).trim();
|
|
171
188
|
if (!text) return null;
|
|
@@ -280,14 +297,15 @@ export function collectFolderNotes(notes, pkg) {
|
|
|
280
297
|
}
|
|
281
298
|
|
|
282
299
|
/**
|
|
283
|
-
* Index folder notes by
|
|
284
|
-
*
|
|
300
|
+
* Index folder notes by their canonical address, and check the invariants
|
|
301
|
+
* that make the index sound.
|
|
285
302
|
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
303
|
+
* A folder reference is resolved by {@link parseAddress}, taking `folder` as
|
|
304
|
+
* the type an omitted segment fills in — the same suffix ladder every other
|
|
305
|
+
* position climbs, so `cooking`, `folder-cooking` and the full address all
|
|
306
|
+
* expand to the one key this index holds. A `packFolder` or `parent` field
|
|
307
|
+
* supplies no type of its own; the position's default is what makes a bare
|
|
308
|
+
* shortcode a complete address there.
|
|
291
309
|
*
|
|
292
310
|
* @param {FolderNote[]} folders - From {@link collectFolderNotes}.
|
|
293
311
|
* @returns {{byKey: Map<string, FolderNote>, folders: FolderNote[],
|
|
@@ -303,6 +321,12 @@ export function buildFolderNoteIndex(folders) {
|
|
|
303
321
|
const byShortcode = new Map();
|
|
304
322
|
/** @type {Map<string, FolderNote>} */
|
|
305
323
|
const byId = new Map();
|
|
324
|
+
// Every folder in one content tree shares one package — a `Folder` is
|
|
325
|
+
// filed inside the module this build compiles, and nothing addresses one
|
|
326
|
+
// in another package. Read from the first folder's own canonical address
|
|
327
|
+
// rather than threaded through as a parameter, so a caller holding
|
|
328
|
+
// `FolderNote[]` never has to carry the package alongside it.
|
|
329
|
+
const pkg = folders.length ? readCanonicalKey(folders[0].address)?.package : undefined;
|
|
306
330
|
|
|
307
331
|
for (const folder of folders) {
|
|
308
332
|
const key = folder.shortcode.toLowerCase();
|
|
@@ -339,8 +363,6 @@ export function buildFolderNoteIndex(folders) {
|
|
|
339
363
|
byId.set(folder.id, folder);
|
|
340
364
|
|
|
341
365
|
byKey.set(folder.address.toLowerCase(), folder);
|
|
342
|
-
byKey.set(`${FOLDER_TYPE}-${folder.shortcode}`.toLowerCase(), folder);
|
|
343
|
-
byKey.set(key, folder);
|
|
344
366
|
}
|
|
345
367
|
|
|
346
368
|
/**
|
|
@@ -355,11 +377,18 @@ export function buildFolderNoteIndex(folders) {
|
|
|
355
377
|
if (!address) {
|
|
356
378
|
throw new Error("a folder reference is blank");
|
|
357
379
|
}
|
|
358
|
-
const
|
|
380
|
+
const tuple = parseAddress(address, {
|
|
381
|
+
package: pkg,
|
|
382
|
+
system: NO_SYSTEM,
|
|
383
|
+
type: FOLDER_TYPE,
|
|
384
|
+
types: FOLDER_TYPES,
|
|
385
|
+
packages: pkg ? new Set([pkg]) : undefined,
|
|
386
|
+
});
|
|
387
|
+
const hit = tuple.reason ? undefined : byKey.get(renderAddress(tuple).toLowerCase());
|
|
359
388
|
if (!hit) {
|
|
360
389
|
const known = [...byShortcode.values()].map((f) => f.shortcode).sort();
|
|
361
390
|
throw new Error(
|
|
362
|
-
`no folder note is addressed "${address}" — this package ` +
|
|
391
|
+
`no folder note is addressed "${isAddressTuple(address) ? renderAddress(address) : address}" — this package ` +
|
|
363
392
|
`declares ${known.length} folder(s)` +
|
|
364
393
|
(known.length ? `: ${known.join(", ")}` : ""),
|
|
365
394
|
);
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
import fs from "node:fs";
|
|
59
59
|
import path from "node:path";
|
|
60
60
|
|
|
61
|
+
import { noteAddressContext } from "./note-addresses.mjs";
|
|
61
62
|
import { canonicalKey, packageAddress } from "./content-address.mjs";
|
|
62
63
|
import { NO_SYSTEM, systemOf } from "./document-subtypes.mjs";
|
|
63
64
|
import {
|
|
@@ -320,6 +321,7 @@ export function collectFoundryEntries(contentBase, ctx) {
|
|
|
320
321
|
let notes = 0;
|
|
321
322
|
for (const { frontmatter: fm, body, absPath } of walkMarkdownTree(contentBase, {
|
|
322
323
|
skipDirectories: ctx.skipDirectories,
|
|
324
|
+
addressContext: ctx.addressContext ?? { package: ctx.contentPackage },
|
|
323
325
|
})) {
|
|
324
326
|
if (!fm) continue;
|
|
325
327
|
// Its authored pin, or the id derived from its canonical address.
|
|
@@ -376,6 +378,7 @@ export function collectFoundryEntries(contentBase, ctx) {
|
|
|
376
378
|
export function foundryIdentities(config = loadPackConfig()) {
|
|
377
379
|
return {
|
|
378
380
|
contentPackage: config.contentPackage,
|
|
381
|
+
addressContext: noteAddressContext(config),
|
|
379
382
|
foundryPackageId: config.foundryPackage,
|
|
380
383
|
packRouter: routerFor(config),
|
|
381
384
|
// Carried in the context rather than read from the global config at the
|