@omniharmonic/openspace-cli 0.1.1 → 0.1.2
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/dist/bin.js +1 -1
- package/dist/{chunk-LTJEM3DN.js → chunk-R3SYHQ5J.js} +364 -105
- package/dist/chunk-R3SYHQ5J.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +7 -7
- package/dist/chunk-LTJEM3DN.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -7435,6 +7435,223 @@ var CliError = class extends Error {
|
|
|
7435
7435
|
import { readdir as readdir2, readFile as readFile2, stat } from "fs/promises";
|
|
7436
7436
|
import { join as join2 } from "path";
|
|
7437
7437
|
|
|
7438
|
+
// ../schema/src/links.ts
|
|
7439
|
+
var COLLECTION_SUBPATHS = /* @__PURE__ */ new Set(["page", "category", "categories", "tag", "tags"]);
|
|
7440
|
+
var MARKDOWN_LINK_RE = /\]\((\/[^)\s]*)\)|href="(\/[^"\s]*)"/g;
|
|
7441
|
+
var trimSlashes = (s) => s.replace(/^\/+|\/+$/g, "");
|
|
7442
|
+
var LinkTargets = class {
|
|
7443
|
+
constructor(doc) {
|
|
7444
|
+
this.doc = doc;
|
|
7445
|
+
this.slugs = new Set(Object.values(doc.pages).map((p) => p.slug));
|
|
7446
|
+
this.redirectFroms = new Set((doc.site.redirects ?? []).map((r) => trimSlashes(r.from)));
|
|
7447
|
+
}
|
|
7448
|
+
doc;
|
|
7449
|
+
slugs;
|
|
7450
|
+
redirectFroms;
|
|
7451
|
+
hasCollection(name) {
|
|
7452
|
+
return name in this.doc.collections || name in (this.doc.collectionConfigs ?? {});
|
|
7453
|
+
}
|
|
7454
|
+
/** Does a site-internal path (`/about/`, `/journal/hello`) lead somewhere? */
|
|
7455
|
+
pathResolves(raw) {
|
|
7456
|
+
const path = raw.split(/[?#]/)[0] ?? "";
|
|
7457
|
+
const slug = trimSlashes(path);
|
|
7458
|
+
const last = slug.split("/").pop() ?? "";
|
|
7459
|
+
if (/\.[A-Za-z0-9]+$/.test(last)) return true;
|
|
7460
|
+
if (slug === "404" || slug.startsWith("_os/")) return true;
|
|
7461
|
+
if (this.slugs.has(slug) || this.redirectFroms.has(slug)) return true;
|
|
7462
|
+
const [head, second, ...rest] = slug.split("/");
|
|
7463
|
+
if (head && this.hasCollection(head)) {
|
|
7464
|
+
if (second === void 0) return true;
|
|
7465
|
+
if (this.doc.collections[head]?.[second]) return rest.length === 0;
|
|
7466
|
+
return COLLECTION_SUBPATHS.has(second);
|
|
7467
|
+
}
|
|
7468
|
+
return false;
|
|
7469
|
+
}
|
|
7470
|
+
};
|
|
7471
|
+
var isInternalPath = (s) => /^\/(?!\/)/.test(s);
|
|
7472
|
+
var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7473
|
+
function findBrokenLinks(doc) {
|
|
7474
|
+
const targets = new LinkTargets(doc);
|
|
7475
|
+
const out = [];
|
|
7476
|
+
const walk5 = (value, scope, path, where) => {
|
|
7477
|
+
const add = (kind, target, message, severity = "error") => out.push({ scope, path, ...where, kind, target, severity, message });
|
|
7478
|
+
if (typeof value === "string") {
|
|
7479
|
+
if (path[path.length - 1] === "body")
|
|
7480
|
+
for (const m of value.matchAll(MARKDOWN_LINK_RE)) {
|
|
7481
|
+
const target = m[1] ?? m[2];
|
|
7482
|
+
if (target && !targets.pathResolves(target))
|
|
7483
|
+
add("markdown", target, `Link to ${target} does not lead to any page`);
|
|
7484
|
+
}
|
|
7485
|
+
return;
|
|
7486
|
+
}
|
|
7487
|
+
if (Array.isArray(value)) {
|
|
7488
|
+
for (const [i, v] of value.entries()) walk5(v, scope, [...path, i], where);
|
|
7489
|
+
return;
|
|
7490
|
+
}
|
|
7491
|
+
if (!isObject(value)) return;
|
|
7492
|
+
const o = value;
|
|
7493
|
+
if (o.type === "page" && typeof o.page === "string") {
|
|
7494
|
+
if (!(o.page in doc.pages))
|
|
7495
|
+
add("page", o.page, `Link to page ${o.page} but no such page exists`);
|
|
7496
|
+
} else if (o.type === "item" && typeof o.collection === "string" && typeof o.slug === "string") {
|
|
7497
|
+
if (!doc.collections[o.collection]?.[o.slug])
|
|
7498
|
+
add(
|
|
7499
|
+
"item",
|
|
7500
|
+
`${o.collection}/${o.slug}`,
|
|
7501
|
+
`Link to ${o.collection}/${o.slug} but no such item exists`
|
|
7502
|
+
);
|
|
7503
|
+
} else if (o.type === "collection" && typeof o.collection === "string") {
|
|
7504
|
+
if (!targets.hasCollection(o.collection))
|
|
7505
|
+
add(
|
|
7506
|
+
"collection",
|
|
7507
|
+
o.collection,
|
|
7508
|
+
`Navigation points at collection "${o.collection}" which does not exist`
|
|
7509
|
+
);
|
|
7510
|
+
} else if (o.type === "url" && typeof o.url === "string" && isInternalPath(o.url)) {
|
|
7511
|
+
if (!targets.pathResolves(o.url))
|
|
7512
|
+
add("url", o.url, `Link to ${o.url} does not lead to any page`);
|
|
7513
|
+
} else if (o.type === "link" && isObject(o.attrs) && typeof o.attrs.href === "string") {
|
|
7514
|
+
const href = o.attrs.href;
|
|
7515
|
+
if (isInternalPath(href) && !targets.pathResolves(href))
|
|
7516
|
+
add("richtext", href, `Text link to ${href} does not lead to any page`, "warning");
|
|
7517
|
+
}
|
|
7518
|
+
for (const [k, v] of Object.entries(o)) walk5(v, scope, [...path, k], where);
|
|
7519
|
+
};
|
|
7520
|
+
const walkSection = (section, scope, base, container2) => {
|
|
7521
|
+
for (const [k, v] of Object.entries(section))
|
|
7522
|
+
if (k !== "blocks") walk5(v, scope, [...base, k], { container: container2, section: section.id });
|
|
7523
|
+
section.blocks.forEach((b, j) => {
|
|
7524
|
+
const at = { container: container2, section: section.id, block: b.id };
|
|
7525
|
+
for (const [k, v] of Object.entries(b)) {
|
|
7526
|
+
if (k !== "fields") walk5(v, scope, [...base, "blocks", j, k], at);
|
|
7527
|
+
else if (isObject(v))
|
|
7528
|
+
for (const [field, fv] of Object.entries(v))
|
|
7529
|
+
walk5(fv, scope, [...base, "blocks", j, "fields", field], { ...at, field });
|
|
7530
|
+
}
|
|
7531
|
+
});
|
|
7532
|
+
};
|
|
7533
|
+
const site2 = { kind: "site" };
|
|
7534
|
+
for (const [k, v] of Object.entries(doc.site))
|
|
7535
|
+
if (k === "footer") walkSection(doc.site.footer, site2, ["footer"], "footer");
|
|
7536
|
+
else walk5(v, site2, [k], {});
|
|
7537
|
+
for (const page of Object.values(doc.pages)) {
|
|
7538
|
+
const scope = { kind: "page", page: page.id };
|
|
7539
|
+
for (const [k, v] of Object.entries(page)) if (k !== "sections") walk5(v, scope, [k], {});
|
|
7540
|
+
for (const [i, s] of page.sections.entries()) walkSection(s, scope, ["sections", i], page.id);
|
|
7541
|
+
}
|
|
7542
|
+
for (const [collection, items] of Object.entries(doc.collections))
|
|
7543
|
+
for (const item of Object.values(items))
|
|
7544
|
+
walk5(item, { kind: "item", collection, slug: item.slug }, [], {});
|
|
7545
|
+
return out;
|
|
7546
|
+
}
|
|
7547
|
+
var CONTACT_WORDS = [
|
|
7548
|
+
"contact",
|
|
7549
|
+
"book",
|
|
7550
|
+
"booking",
|
|
7551
|
+
"bookings",
|
|
7552
|
+
"reserve",
|
|
7553
|
+
"reservations",
|
|
7554
|
+
"enquire",
|
|
7555
|
+
"enquiry",
|
|
7556
|
+
"enquiries",
|
|
7557
|
+
"inquire",
|
|
7558
|
+
"inquiry",
|
|
7559
|
+
"get-in-touch",
|
|
7560
|
+
"hello",
|
|
7561
|
+
"appointments",
|
|
7562
|
+
"visit"
|
|
7563
|
+
];
|
|
7564
|
+
var words = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2);
|
|
7565
|
+
var related = (a, b) => {
|
|
7566
|
+
if (a === b) return true;
|
|
7567
|
+
const [s, l] = a.length < b.length ? [a, b] : [b, a];
|
|
7568
|
+
return s.length >= 4 && l.startsWith(s) && l.length - s.length <= 3;
|
|
7569
|
+
};
|
|
7570
|
+
var publicPages = (doc) => Object.values(doc.pages).filter(
|
|
7571
|
+
(p) => !p.access?.password && !p.access?.members && p.slug !== "404"
|
|
7572
|
+
);
|
|
7573
|
+
var pagePath = (p) => p.slug ? `/${p.slug}` : "/";
|
|
7574
|
+
function suggestLinkTarget(doc, target) {
|
|
7575
|
+
const wanted = words(trimSlashes(target.split(/[?#]/)[0] ?? ""));
|
|
7576
|
+
const pages = publicPages(doc);
|
|
7577
|
+
const matches = (p, list3) => {
|
|
7578
|
+
const own = [...words(p.slug), ...words(p.title)];
|
|
7579
|
+
return own.some((w) => list3.some((x) => related(w, x)));
|
|
7580
|
+
};
|
|
7581
|
+
const bySlugWord = pages.find((p) => p.slug && wanted.some((w) => words(p.slug).includes(w)));
|
|
7582
|
+
if (bySlugWord) return { page: bySlugWord, path: pagePath(bySlugWord) };
|
|
7583
|
+
const byWord = pages.find((p) => p.slug && matches(p, wanted));
|
|
7584
|
+
if (byWord) return { page: byWord, path: pagePath(byWord) };
|
|
7585
|
+
const collection = Object.keys(doc.collections).find(
|
|
7586
|
+
(c) => wanted.some((w) => related(w, c.toLowerCase()))
|
|
7587
|
+
);
|
|
7588
|
+
if (collection) return { path: `/${collection}/` };
|
|
7589
|
+
const contact = CONTACT_WORDS.map(
|
|
7590
|
+
(w) => pages.find((p) => p.slug && (words(p.slug).includes(w) || words(p.title).includes(w)))
|
|
7591
|
+
).find(Boolean);
|
|
7592
|
+
if (contact) return { page: contact, path: pagePath(contact) };
|
|
7593
|
+
const home = pages.find((p) => p.slug === "") ?? pages[0];
|
|
7594
|
+
return home ? { page: home, path: pagePath(home) } : void 0;
|
|
7595
|
+
}
|
|
7596
|
+
function suggestionLink(s, newTab) {
|
|
7597
|
+
if (s.page) return { type: "page", page: s.page.id };
|
|
7598
|
+
return { type: "url", url: s.path, ...newTab ? { newTab } : {} };
|
|
7599
|
+
}
|
|
7600
|
+
function resolveInternalLinks(value, doc) {
|
|
7601
|
+
const targets = new LinkTargets(doc);
|
|
7602
|
+
const changes = [];
|
|
7603
|
+
const cache = /* @__PURE__ */ new Map();
|
|
7604
|
+
const suggest = (target) => {
|
|
7605
|
+
if (!cache.has(target)) cache.set(target, suggestLinkTarget(doc, target));
|
|
7606
|
+
return cache.get(target);
|
|
7607
|
+
};
|
|
7608
|
+
const fix = (v, path) => {
|
|
7609
|
+
if (typeof v === "string") {
|
|
7610
|
+
if (path[path.length - 1] !== "body") return v;
|
|
7611
|
+
return v.replace(MARKDOWN_LINK_RE, (m, md, html) => {
|
|
7612
|
+
const target = md ?? html;
|
|
7613
|
+
if (!target || targets.pathResolves(target)) return m;
|
|
7614
|
+
const s = suggest(target);
|
|
7615
|
+
if (!s) return m;
|
|
7616
|
+
changes.push({ path, from: target, to: s.path });
|
|
7617
|
+
return md ? `](${s.path})` : `href="${s.path}"`;
|
|
7618
|
+
});
|
|
7619
|
+
}
|
|
7620
|
+
if (Array.isArray(v)) return v.map((x, i) => fix(x, [...path, i]));
|
|
7621
|
+
if (!isObject(v)) return v;
|
|
7622
|
+
if (v.type === "url" && typeof v.url === "string" && isInternalPath(v.url)) {
|
|
7623
|
+
if (targets.pathResolves(v.url)) return v;
|
|
7624
|
+
const s = suggest(v.url);
|
|
7625
|
+
if (!s) return v;
|
|
7626
|
+
changes.push({ path, from: v.url, to: s.path });
|
|
7627
|
+
return suggestionLink(s, v.newTab === true ? true : void 0);
|
|
7628
|
+
}
|
|
7629
|
+
if (v.type === "page" && typeof v.page === "string" && !(v.page in doc.pages)) {
|
|
7630
|
+
const home = suggest("/");
|
|
7631
|
+
if (!home?.page) return v;
|
|
7632
|
+
changes.push({ path, from: v.page, to: home.path });
|
|
7633
|
+
return { ...v, page: home.page.id };
|
|
7634
|
+
}
|
|
7635
|
+
if (v.type === "link" && isObject(v.attrs) && typeof v.attrs.href === "string") {
|
|
7636
|
+
const href = v.attrs.href;
|
|
7637
|
+
if (!isInternalPath(href) || targets.pathResolves(href)) return v;
|
|
7638
|
+
const s = suggest(href);
|
|
7639
|
+
if (!s) return v;
|
|
7640
|
+
changes.push({ path, from: href, to: s.path });
|
|
7641
|
+
return { ...v, attrs: { ...v.attrs, href: s.path } };
|
|
7642
|
+
}
|
|
7643
|
+
const out = {};
|
|
7644
|
+
for (const [k, x] of Object.entries(v)) out[k] = fix(x, [...path, k]);
|
|
7645
|
+
return out;
|
|
7646
|
+
};
|
|
7647
|
+
return { value: fix(value, []), changes };
|
|
7648
|
+
}
|
|
7649
|
+
function withPages(doc, pages) {
|
|
7650
|
+
const all = { ...doc.pages };
|
|
7651
|
+
for (const p of pages) all[p.id] = p;
|
|
7652
|
+
return { ...doc, pages: all };
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7438
7655
|
// ../schema/src/secrets.ts
|
|
7439
7656
|
function shannonEntropy(value) {
|
|
7440
7657
|
if (!value) return 0;
|
|
@@ -8151,39 +8368,6 @@ function fileDepth2(path) {
|
|
|
8151
8368
|
}
|
|
8152
8369
|
var IMAGE_EXT_RE = /\.(?:jpe?g|png|gif|webp|avif|svg)$/i;
|
|
8153
8370
|
var MEDIA_REF_RE = /(?<![\w/.:-])media\/[A-Za-z0-9_\-./]+\.[A-Za-z0-9]+/g;
|
|
8154
|
-
var MD_LINK_RE = /\]\((\/[^)\s]*)\)|href="(\/[^"\s]*)"/g;
|
|
8155
|
-
var COLLECTION_SUBPATHS = /* @__PURE__ */ new Set(["page", "category", "categories", "tag", "tags"]);
|
|
8156
|
-
var Resolver = class {
|
|
8157
|
-
constructor(doc) {
|
|
8158
|
-
this.doc = doc;
|
|
8159
|
-
this.slugs = new Set(Object.values(doc.pages).map((p) => p.slug));
|
|
8160
|
-
this.redirectFroms = new Set(
|
|
8161
|
-
(doc.site.redirects ?? []).map((r) => r.from.replace(/^\/+|\/+$/g, ""))
|
|
8162
|
-
);
|
|
8163
|
-
}
|
|
8164
|
-
doc;
|
|
8165
|
-
slugs;
|
|
8166
|
-
redirectFroms;
|
|
8167
|
-
hasCollection(name) {
|
|
8168
|
-
return name in this.doc.collections || name in (this.doc.collectionConfigs ?? {});
|
|
8169
|
-
}
|
|
8170
|
-
/** Does a site-internal path (`/about/`, `/journal/hello`) lead somewhere? */
|
|
8171
|
-
pathResolves(raw) {
|
|
8172
|
-
const path = raw.split(/[?#]/)[0] ?? "";
|
|
8173
|
-
const slug = path.replace(/^\/+|\/+$/g, "");
|
|
8174
|
-
const last = slug.split("/").pop() ?? "";
|
|
8175
|
-
if (/\.[A-Za-z0-9]+$/.test(last)) return true;
|
|
8176
|
-
if (slug === "404" || slug.startsWith("_os/")) return true;
|
|
8177
|
-
if (this.slugs.has(slug) || this.redirectFroms.has(slug)) return true;
|
|
8178
|
-
const [head, second, ...rest] = slug.split("/");
|
|
8179
|
-
if (head && this.hasCollection(head)) {
|
|
8180
|
-
if (second === void 0) return true;
|
|
8181
|
-
if (this.doc.collections[head]?.[second]) return rest.length === 0;
|
|
8182
|
-
return COLLECTION_SUBPATHS.has(second);
|
|
8183
|
-
}
|
|
8184
|
-
return false;
|
|
8185
|
-
}
|
|
8186
|
-
};
|
|
8187
8371
|
async function runCheck(root, options = {}) {
|
|
8188
8372
|
const budgets = { ...DEFAULT_BUDGETS, ...options.budgets };
|
|
8189
8373
|
const findings = [];
|
|
@@ -8321,7 +8505,7 @@ async function runCheck(root, options = {}) {
|
|
|
8321
8505
|
return { ok: errors2 === 0, errors: errors2, warnings, findings, stats: stats2 };
|
|
8322
8506
|
}
|
|
8323
8507
|
function checkReferences(doc, pageFiles2, mediaOnDisk, budgets, add) {
|
|
8324
|
-
const resolver = new
|
|
8508
|
+
const resolver = new LinkTargets(doc);
|
|
8325
8509
|
const reportedImages = /* @__PURE__ */ new Set();
|
|
8326
8510
|
const checkImage = (ref, file, path) => {
|
|
8327
8511
|
if (!isMediaPath(ref)) return;
|
|
@@ -8349,67 +8533,23 @@ function checkReferences(doc, pageFiles2, mediaOnDisk, budgets, add) {
|
|
|
8349
8533
|
if (typeof value === "string") {
|
|
8350
8534
|
const here = dotPath(path);
|
|
8351
8535
|
for (const m of value.matchAll(MEDIA_REF_RE)) checkImage(m[0], file, here);
|
|
8352
|
-
if (path[path.length - 1] === "body") {
|
|
8353
|
-
for (const m of value.matchAll(MD_LINK_RE)) {
|
|
8354
|
-
const target = m[1] ?? m[2];
|
|
8355
|
-
if (target && !resolver.pathResolves(target))
|
|
8356
|
-
add({
|
|
8357
|
-
severity: "error",
|
|
8358
|
-
rule: "link",
|
|
8359
|
-
file,
|
|
8360
|
-
path: here,
|
|
8361
|
-
message: `Link to ${target} does not lead to any page`
|
|
8362
|
-
});
|
|
8363
|
-
}
|
|
8364
|
-
}
|
|
8365
8536
|
return;
|
|
8366
8537
|
}
|
|
8367
8538
|
if (Array.isArray(value)) {
|
|
8368
8539
|
for (const [i, v] of value.entries()) walk5(v, file, [...path, i]);
|
|
8369
8540
|
return;
|
|
8370
8541
|
}
|
|
8371
|
-
if (value && typeof value === "object")
|
|
8372
|
-
const
|
|
8373
|
-
const here = dotPath(path);
|
|
8374
|
-
if (o.type === "page" && typeof o.page === "string" && !(o.page in doc.pages))
|
|
8375
|
-
add({
|
|
8376
|
-
severity: "error",
|
|
8377
|
-
rule: "link",
|
|
8378
|
-
file,
|
|
8379
|
-
path: here || void 0,
|
|
8380
|
-
message: `Link to page ${o.page} but no such page exists`
|
|
8381
|
-
});
|
|
8382
|
-
else if (o.type === "item" && typeof o.collection === "string" && typeof o.slug === "string") {
|
|
8383
|
-
if (!doc.collections[o.collection]?.[o.slug])
|
|
8384
|
-
add({
|
|
8385
|
-
severity: "error",
|
|
8386
|
-
rule: "link",
|
|
8387
|
-
file,
|
|
8388
|
-
path: here || void 0,
|
|
8389
|
-
message: `Link to ${o.collection}/${o.slug} but no such item exists`
|
|
8390
|
-
});
|
|
8391
|
-
} else if (o.type === "collection" && typeof o.collection === "string") {
|
|
8392
|
-
if (!resolver.hasCollection(o.collection))
|
|
8393
|
-
add({
|
|
8394
|
-
severity: "error",
|
|
8395
|
-
rule: "link",
|
|
8396
|
-
file,
|
|
8397
|
-
path: here || void 0,
|
|
8398
|
-
message: `Navigation points at collection "${o.collection}" which does not exist`
|
|
8399
|
-
});
|
|
8400
|
-
} else if (o.type === "url" && typeof o.url === "string" && o.url.startsWith("/")) {
|
|
8401
|
-
if (!resolver.pathResolves(o.url))
|
|
8402
|
-
add({
|
|
8403
|
-
severity: "error",
|
|
8404
|
-
rule: "link",
|
|
8405
|
-
file,
|
|
8406
|
-
path: here || void 0,
|
|
8407
|
-
message: `Link to ${o.url} does not lead to any page`
|
|
8408
|
-
});
|
|
8409
|
-
}
|
|
8410
|
-
for (const [k, v] of Object.entries(o)) walk5(v, file, [...path, k]);
|
|
8411
|
-
}
|
|
8542
|
+
if (value && typeof value === "object")
|
|
8543
|
+
for (const [k, v] of Object.entries(value)) walk5(v, file, [...path, k]);
|
|
8412
8544
|
};
|
|
8545
|
+
for (const l of findBrokenLinks(doc))
|
|
8546
|
+
add({
|
|
8547
|
+
severity: l.severity,
|
|
8548
|
+
rule: "link",
|
|
8549
|
+
file: l.scope.kind === "site" ? SITE_FILE : l.scope.kind === "page" ? pageFiles2[l.scope.page] ?? PAGES_DIR : `${COLLECTIONS_DIR}/${l.scope.collection}/${l.scope.slug}${ITEM_EXT}`,
|
|
8550
|
+
path: dotPath(l.path) || void 0,
|
|
8551
|
+
message: l.message
|
|
8552
|
+
});
|
|
8413
8553
|
walk5(doc.site, SITE_FILE, []);
|
|
8414
8554
|
for (const page of Object.values(doc.pages)) walk5(page, pageFiles2[page.id] ?? PAGES_DIR, []);
|
|
8415
8555
|
for (const [name, items] of Object.entries(doc.collections))
|
|
@@ -12472,6 +12612,104 @@ function sortIssues(issues) {
|
|
|
12472
12612
|
return issues.map((i, n) => [i, n]).sort(([a, na], [b, nb]) => rank[a.severity] - rank[b.severity] || na - nb).map(([i]) => i);
|
|
12473
12613
|
}
|
|
12474
12614
|
|
|
12615
|
+
// ../lint/src/links.ts
|
|
12616
|
+
var titleOf = (p) => p.title || (p.slug === "" ? "Home" : p.slug);
|
|
12617
|
+
function linkPlace(doc, link) {
|
|
12618
|
+
const s = link.scope;
|
|
12619
|
+
if (link.block) {
|
|
12620
|
+
const container2 = link.container === "footer" ? doc.site.footer : doc.pages[link.container]?.sections.find((x) => x.id === link.section);
|
|
12621
|
+
const block = container2?.blocks.find((b) => b.id === link.block);
|
|
12622
|
+
const label = block && blocksRegistry().get(block.type)?.meta.label || "block";
|
|
12623
|
+
const what = `A ${label.toLowerCase()}`;
|
|
12624
|
+
if (link.container === "footer") return `${what} in the footer`;
|
|
12625
|
+
const page = doc.pages[link.container];
|
|
12626
|
+
return page ? `${what} on ${titleOf(page)}` : what;
|
|
12627
|
+
}
|
|
12628
|
+
if (s.kind === "site") {
|
|
12629
|
+
const head = link.path[0];
|
|
12630
|
+
if (head === "nav") return "Your menu";
|
|
12631
|
+
if (head === "header") return "The header";
|
|
12632
|
+
if (head === "announcement") return "The announcement bar";
|
|
12633
|
+
if (head === "popup") return "The pop-up";
|
|
12634
|
+
return "Your site settings";
|
|
12635
|
+
}
|
|
12636
|
+
if (s.kind === "page") {
|
|
12637
|
+
const page = doc.pages[s.page];
|
|
12638
|
+
return page ? `The page ${titleOf(page)}` : "A page";
|
|
12639
|
+
}
|
|
12640
|
+
const item = doc.collections[s.collection]?.[s.slug];
|
|
12641
|
+
const title = item?.frontmatter?.title ?? s.slug;
|
|
12642
|
+
return `\u201C${title}\u201D in ${s.collection}`;
|
|
12643
|
+
}
|
|
12644
|
+
function describeBrokenLink(doc, link) {
|
|
12645
|
+
const where = linkPlace(doc, link);
|
|
12646
|
+
switch (link.kind) {
|
|
12647
|
+
case "page":
|
|
12648
|
+
return `${where} links to a page that no longer exists.`;
|
|
12649
|
+
case "item":
|
|
12650
|
+
return `${where} links to ${link.target}, which isn't on your site any more.`;
|
|
12651
|
+
case "collection":
|
|
12652
|
+
return `${where} points at the \u201C${link.target}\u201D collection, which doesn't exist.`;
|
|
12653
|
+
default:
|
|
12654
|
+
return `${where} links to ${link.target}, which isn't a page on your site.`;
|
|
12655
|
+
}
|
|
12656
|
+
}
|
|
12657
|
+
function linkFix(doc, link, meta) {
|
|
12658
|
+
const target = link.kind === "page" ? "/" : link.target;
|
|
12659
|
+
const s = suggestLinkTarget(doc, target);
|
|
12660
|
+
if (!s) return void 0;
|
|
12661
|
+
const label = `Link to ${s.page ? titleOf(s.page) : s.path} instead`;
|
|
12662
|
+
if (link.block && link.field) {
|
|
12663
|
+
const container2 = link.container === "footer" ? doc.site.footer : doc.pages[link.container]?.sections.find((x) => x.id === link.section);
|
|
12664
|
+
const block = container2?.blocks.find((b) => b.id === link.block);
|
|
12665
|
+
if (!block) return void 0;
|
|
12666
|
+
const { value, changes } = resolveInternalLinks(block.fields[link.field], doc);
|
|
12667
|
+
if (!changes.length) return void 0;
|
|
12668
|
+
return {
|
|
12669
|
+
label,
|
|
12670
|
+
ops: [
|
|
12671
|
+
{
|
|
12672
|
+
op: "block.update",
|
|
12673
|
+
...meta(),
|
|
12674
|
+
page: link.container,
|
|
12675
|
+
block: block.id,
|
|
12676
|
+
set: { [link.field]: value }
|
|
12677
|
+
}
|
|
12678
|
+
]
|
|
12679
|
+
};
|
|
12680
|
+
}
|
|
12681
|
+
if (link.scope.kind === "site" && link.path[0] === "nav") {
|
|
12682
|
+
const { value, changes } = resolveInternalLinks(doc.site.nav, doc);
|
|
12683
|
+
if (!changes.length) return void 0;
|
|
12684
|
+
return { label, ops: [{ op: "site.update", ...meta(), set: { nav: value } }] };
|
|
12685
|
+
}
|
|
12686
|
+
return void 0;
|
|
12687
|
+
}
|
|
12688
|
+
function linkIssues(doc, pageId, options = {}, meta = opMeta(options)) {
|
|
12689
|
+
const out = [];
|
|
12690
|
+
for (const link of findBrokenLinks(doc)) {
|
|
12691
|
+
const s = link.scope;
|
|
12692
|
+
if (pageId && s.kind === "page" && s.page !== pageId) continue;
|
|
12693
|
+
if (pageId && s.kind === "item") continue;
|
|
12694
|
+
const page = link.container ?? (s.kind === "page" ? s.page : void 0);
|
|
12695
|
+
const autofix = linkFix(doc, link, meta);
|
|
12696
|
+
out.push(
|
|
12697
|
+
issue({
|
|
12698
|
+
rule: "broken-link",
|
|
12699
|
+
severity: link.severity,
|
|
12700
|
+
...page ? { page } : {},
|
|
12701
|
+
...link.section ? { section: link.section } : {},
|
|
12702
|
+
...link.block ? { block: link.block } : {},
|
|
12703
|
+
key: `${s.kind === "item" ? `${s.collection}/${s.slug}:` : ""}${link.path.join(".")}`,
|
|
12704
|
+
message: describeBrokenLink(doc, link),
|
|
12705
|
+
fix: link.severity === "error" ? "Point the link at a page that exists, or add the missing page. The site can't be published until then." : "Point the link at a page that exists, or add the missing page.",
|
|
12706
|
+
...autofix ? { autofix } : {}
|
|
12707
|
+
})
|
|
12708
|
+
);
|
|
12709
|
+
}
|
|
12710
|
+
return out;
|
|
12711
|
+
}
|
|
12712
|
+
|
|
12475
12713
|
// ../lint/src/mobile.ts
|
|
12476
12714
|
var ROW_UNIT = { d: 24, m: 20 };
|
|
12477
12715
|
var MOBILE_WIDTH = 350;
|
|
@@ -12733,6 +12971,7 @@ function designCheck(doc, pageId, options = {}) {
|
|
|
12733
12971
|
if (pageId && pages.length === 0) throw new Error(`Page ${pageId} not found`);
|
|
12734
12972
|
for (const page of pages) ctx.page(page);
|
|
12735
12973
|
ctx.container(FOOTER, [doc.site.footer], { footer: true });
|
|
12974
|
+
ctx.issues.push(...linkIssues(doc, pageId, options, ctx.meta));
|
|
12736
12975
|
return sortIssues(ctx.issues);
|
|
12737
12976
|
}
|
|
12738
12977
|
var LIGHT = /* @__PURE__ */ new Set(["light", "light-accent"]);
|
|
@@ -13530,12 +13769,12 @@ function seoCheck(page, doc, options = {}) {
|
|
|
13530
13769
|
} : {}
|
|
13531
13770
|
});
|
|
13532
13771
|
}
|
|
13533
|
-
const
|
|
13534
|
-
if (!page.seo?.noindex &&
|
|
13772
|
+
const words2 = blocks.map(blockText).join(" ").split(/\s+/).filter(Boolean).length;
|
|
13773
|
+
if (!page.seo?.noindex && words2 < 80) {
|
|
13535
13774
|
add({
|
|
13536
13775
|
rule: "seo-thin-content",
|
|
13537
13776
|
severity: "info",
|
|
13538
|
-
message: `The page has about ${
|
|
13777
|
+
message: `The page has about ${words2} words of text; search engines have little to go on.`,
|
|
13539
13778
|
fix: "Add a short paragraph that explains what's on the page in your own words."
|
|
13540
13779
|
});
|
|
13541
13780
|
}
|
|
@@ -14237,10 +14476,10 @@ var listSectionPresets = defineTool({
|
|
|
14237
14476
|
let list3 = presetCatalog.slice();
|
|
14238
14477
|
if (input.category) list3 = list3.filter((p) => p.category === input.category);
|
|
14239
14478
|
if (input.query) {
|
|
14240
|
-
const
|
|
14479
|
+
const words2 = input.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
14241
14480
|
list3 = list3.filter((p) => {
|
|
14242
14481
|
const hay = `${p.id} ${p.label} ${p.description} ${p.tags.join(" ")} ${p.ai.whenToUse}`.toLowerCase();
|
|
14243
|
-
return
|
|
14482
|
+
return words2.every((w) => hay.includes(w));
|
|
14244
14483
|
});
|
|
14245
14484
|
}
|
|
14246
14485
|
const full = input.detail === "full";
|
|
@@ -14882,14 +15121,23 @@ var insertSection = defineTool({
|
|
|
14882
15121
|
if (input.preset) {
|
|
14883
15122
|
const preset = findPreset(input.preset);
|
|
14884
15123
|
const opts = { colorTheme: input.colorTheme, collection: input.collection, ...links };
|
|
15124
|
+
const linkNote = (changes2) => changes2.length ? {
|
|
15125
|
+
linksChanged: changes2.map((c) => ({ from: c.from, to: c.to })),
|
|
15126
|
+
linkNote: "Example links that lead nowhere on this site were pointed at existing pages; change them with update_block."
|
|
15127
|
+
} : {};
|
|
14885
15128
|
if (preset.target === "header") {
|
|
14886
|
-
const header = preset.build(opts);
|
|
15129
|
+
const { value: header, changes: changes2 } = resolveInternalLinks(preset.build(opts), doc);
|
|
14887
15130
|
await ctx.apply(siteId, [{ op: "site.update", set: { header } }]);
|
|
14888
15131
|
return {
|
|
14889
|
-
data: {
|
|
15132
|
+
data: {
|
|
15133
|
+
applied: preset.id,
|
|
15134
|
+
header,
|
|
15135
|
+
note: "The site header now uses this preset.",
|
|
15136
|
+
...linkNote(changes2)
|
|
15137
|
+
}
|
|
14890
15138
|
};
|
|
14891
15139
|
}
|
|
14892
|
-
const section2 = preset.build(opts);
|
|
15140
|
+
const { value: section2, changes } = resolveInternalLinks(preset.build(opts), doc);
|
|
14893
15141
|
if (input.label) section2.label = input.label;
|
|
14894
15142
|
if (preset.target === "footer") {
|
|
14895
15143
|
await ctx.apply(siteId, [{ op: "site.update", set: { footer: section2 } }]);
|
|
@@ -14897,7 +15145,8 @@ var insertSection = defineTool({
|
|
|
14897
15145
|
data: {
|
|
14898
15146
|
replaced: "footer",
|
|
14899
15147
|
section: sectionSummary(ctx.registry, section2),
|
|
14900
|
-
note: 'Edit footer blocks with page "footer".'
|
|
15148
|
+
note: 'Edit footer blocks with page "footer".',
|
|
15149
|
+
...linkNote(changes)
|
|
14901
15150
|
}
|
|
14902
15151
|
};
|
|
14903
15152
|
}
|
|
@@ -14912,7 +15161,8 @@ var insertSection = defineTool({
|
|
|
14912
15161
|
page: page2,
|
|
14913
15162
|
section: sectionSummary(ctx.registry, section2),
|
|
14914
15163
|
copyGuidance: preset.copy.map((c) => `${c.label}: ${c.words[0]}\u2013${c.words[1]} words`),
|
|
14915
|
-
next: "Replace the demo copy with the owner's content (update_block), then run design_check."
|
|
15164
|
+
next: "Replace the demo copy with the owner's content (update_block), then run design_check.",
|
|
15165
|
+
...linkNote(changes)
|
|
14916
15166
|
}
|
|
14917
15167
|
};
|
|
14918
15168
|
}
|
|
@@ -15017,7 +15267,10 @@ var insertBlock = defineTool({
|
|
|
15017
15267
|
const doc = await ctx.doc(siteId);
|
|
15018
15268
|
const page = container(doc, input.page);
|
|
15019
15269
|
requireSection2(doc, page, input.section);
|
|
15020
|
-
const
|
|
15270
|
+
const made = makeBlock(ctx.registry, input.block);
|
|
15271
|
+
const block = resolveInternalLinks(made, doc).value;
|
|
15272
|
+
for (const k of Object.keys(input.block.fields ?? {}))
|
|
15273
|
+
if (k in made.fields) block.fields[k] = made.fields[k];
|
|
15021
15274
|
const body = {
|
|
15022
15275
|
op: "block.insert",
|
|
15023
15276
|
page,
|
|
@@ -15263,7 +15516,6 @@ var createPage = defineTool({
|
|
|
15263
15516
|
const colorTheme = input.themes?.[i];
|
|
15264
15517
|
sections.push(preset.build(colorTheme ? { colorTheme } : {}));
|
|
15265
15518
|
});
|
|
15266
|
-
for (const raw of input.sections ?? []) sections.push(makeSection(ctx.registry, raw));
|
|
15267
15519
|
const page = {
|
|
15268
15520
|
id: newId("pg"),
|
|
15269
15521
|
title: input.title,
|
|
@@ -15271,6 +15523,9 @@ var createPage = defineTool({
|
|
|
15271
15523
|
seo: input.description ? { description: input.description } : {},
|
|
15272
15524
|
sections
|
|
15273
15525
|
};
|
|
15526
|
+
const linked = resolveInternalLinks(sections, withPages(doc, [page]));
|
|
15527
|
+
page.sections = linked.value;
|
|
15528
|
+
for (const raw of input.sections ?? []) page.sections.push(makeSection(ctx.registry, raw));
|
|
15274
15529
|
if (input.hidden) page.hidden = true;
|
|
15275
15530
|
const body = { op: "page.create", page };
|
|
15276
15531
|
if (input.addToNav !== false && !input.hidden)
|
|
@@ -15279,8 +15534,12 @@ var createPage = defineTool({
|
|
|
15279
15534
|
return {
|
|
15280
15535
|
data: {
|
|
15281
15536
|
page: { id: page.id, title: page.title, slug: page.slug, path: pagePathOf(page) },
|
|
15282
|
-
sections: sections.map((s) => sectionSummary(ctx.registry, s)),
|
|
15283
|
-
inNav: Boolean(body.nav)
|
|
15537
|
+
sections: page.sections.map((s) => sectionSummary(ctx.registry, s)),
|
|
15538
|
+
inNav: Boolean(body.nav),
|
|
15539
|
+
...linked.changes.length ? {
|
|
15540
|
+
linksChanged: linked.changes.map((c) => ({ from: c.from, to: c.to })),
|
|
15541
|
+
linkNote: "Example links that lead nowhere on this site were pointed at existing pages; change them with update_block."
|
|
15542
|
+
} : {}
|
|
15284
15543
|
}
|
|
15285
15544
|
};
|
|
15286
15545
|
}
|
|
@@ -16589,10 +16848,10 @@ Unpublished work saved as ${tag}`,
|
|
|
16589
16848
|
});
|
|
16590
16849
|
return this.preview;
|
|
16591
16850
|
}
|
|
16592
|
-
async previewUrl(siteId,
|
|
16851
|
+
async previewUrl(siteId, pagePath2 = "/") {
|
|
16593
16852
|
this.check(siteId);
|
|
16594
16853
|
const { url } = await this.previewServer();
|
|
16595
|
-
return `${url}${
|
|
16854
|
+
return `${url}${pagePath2.startsWith("/") ? pagePath2 : `/${pagePath2}`}`;
|
|
16596
16855
|
}
|
|
16597
16856
|
async uploadImage(siteId, input, ctx) {
|
|
16598
16857
|
this.check(siteId);
|
|
@@ -17158,4 +17417,4 @@ export {
|
|
|
17158
17417
|
createProgram,
|
|
17159
17418
|
run
|
|
17160
17419
|
};
|
|
17161
|
-
//# sourceMappingURL=chunk-
|
|
17420
|
+
//# sourceMappingURL=chunk-R3SYHQ5J.js.map
|