contentbit 0.2.0 → 0.3.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/README.md +7 -1
- package/dist/bin.js +766 -43
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +55 -9
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +47 -7
- package/dist/commands/links.d.ts +3 -0
- package/dist/commands/links.d.ts.map +1 -0
- package/dist/commands/links.js +97 -0
- package/dist/commands/render.d.ts.map +1 -1
- package/dist/commands/render.js +2 -2
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +20 -1
- package/dist/link-options.d.ts +10 -0
- package/dist/link-options.d.ts.map +1 -0
- package/dist/link-options.js +31 -0
- package/dist/links-io.d.ts +3 -0
- package/dist/links-io.d.ts.map +1 -0
- package/dist/links-io.js +14 -0
- package/dist/run.d.ts +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +766 -43
- package/package.json +5 -5
package/dist/run.js
CHANGED
|
@@ -371,12 +371,58 @@ function parseValue(value, indented) {
|
|
|
371
371
|
if (indented.length === 0)
|
|
372
372
|
return null;
|
|
373
373
|
const items = dedent(indented);
|
|
374
|
-
|
|
375
|
-
|
|
374
|
+
const list = parseDashList(items);
|
|
375
|
+
if (list)
|
|
376
|
+
return list;
|
|
377
|
+
const mapping = parseNestedMapping(items);
|
|
378
|
+
if (mapping)
|
|
379
|
+
return mapping;
|
|
376
380
|
return items.join("\n");
|
|
377
381
|
}
|
|
378
382
|
return parseScalar(value);
|
|
379
383
|
}
|
|
384
|
+
function parseNestedMapping(items) {
|
|
385
|
+
const out = {};
|
|
386
|
+
for (const line of items) {
|
|
387
|
+
if (/^[ \t]/.test(line))
|
|
388
|
+
return null;
|
|
389
|
+
const m = line.match(KEY_RE2);
|
|
390
|
+
if (!m)
|
|
391
|
+
return null;
|
|
392
|
+
const [, key, rawValue] = m;
|
|
393
|
+
const v = rawValue.trim();
|
|
394
|
+
if (v === "")
|
|
395
|
+
return null;
|
|
396
|
+
out[key] = parseScalar(v);
|
|
397
|
+
}
|
|
398
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
399
|
+
}
|
|
400
|
+
function parseDashList(items) {
|
|
401
|
+
const groups = [];
|
|
402
|
+
let current = null;
|
|
403
|
+
for (const line of items) {
|
|
404
|
+
if (line.startsWith("- ")) {
|
|
405
|
+
if (current)
|
|
406
|
+
groups.push(current);
|
|
407
|
+
current = [line.slice(2)];
|
|
408
|
+
} else if (current && /^[ \t]/.test(line)) {
|
|
409
|
+
current.push(line);
|
|
410
|
+
} else {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (current)
|
|
415
|
+
groups.push(current);
|
|
416
|
+
return groups.map((group) => parseDashItem(group));
|
|
417
|
+
}
|
|
418
|
+
function parseDashItem(lines) {
|
|
419
|
+
const first = lines[0].trim();
|
|
420
|
+
if (lines.length === 1)
|
|
421
|
+
return parseNestedMapping([first]) ?? parseScalar(first);
|
|
422
|
+
const rest = dedent(lines.slice(1));
|
|
423
|
+
const mapping = parseNestedMapping([first, ...rest]);
|
|
424
|
+
return mapping ?? lines.join("\n");
|
|
425
|
+
}
|
|
380
426
|
function dedent(lines) {
|
|
381
427
|
const indent = Math.min(...lines.map((l) => l.match(/^[ \t]*/)[0].length));
|
|
382
428
|
return lines.map((l) => l.slice(indent));
|
|
@@ -408,6 +454,20 @@ function parseScalar(value) {
|
|
|
408
454
|
return [];
|
|
409
455
|
return splitInlineItems(inner).map((item) => parseScalar(item.trim()));
|
|
410
456
|
}
|
|
457
|
+
if (value.startsWith("{") && value.endsWith("}")) {
|
|
458
|
+
const inner = value.slice(1, -1).trim();
|
|
459
|
+
if (inner === "")
|
|
460
|
+
return {};
|
|
461
|
+
const out = {};
|
|
462
|
+
for (const item of splitInlineItems(inner)) {
|
|
463
|
+
const m = item.trim().match(KEY_RE2);
|
|
464
|
+
if (!m)
|
|
465
|
+
return value;
|
|
466
|
+
const [, key, rawValue] = m;
|
|
467
|
+
out[key] = parseScalar(rawValue.trim());
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
411
471
|
return value;
|
|
412
472
|
}
|
|
413
473
|
function splitInlineItems(inner) {
|
|
@@ -1096,21 +1156,6 @@ var init_render_markdown = __esm({
|
|
|
1096
1156
|
}
|
|
1097
1157
|
});
|
|
1098
1158
|
|
|
1099
|
-
// ../core/dist/index.js
|
|
1100
|
-
var init_dist = __esm({
|
|
1101
|
-
"../core/dist/index.js"() {
|
|
1102
|
-
"use strict";
|
|
1103
|
-
init_diagnostics();
|
|
1104
|
-
init_parser();
|
|
1105
|
-
init_frontmatter();
|
|
1106
|
-
init_analyze();
|
|
1107
|
-
init_registry();
|
|
1108
|
-
init_content_models();
|
|
1109
|
-
init_validate();
|
|
1110
|
-
init_render_markdown();
|
|
1111
|
-
}
|
|
1112
|
-
});
|
|
1113
|
-
|
|
1114
1159
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
|
|
1115
1160
|
// @__NO_SIDE_EFFECTS__
|
|
1116
1161
|
function $constructor(name, initializer3, params) {
|
|
@@ -1175,7 +1220,7 @@ var init_core = __esm({
|
|
|
1175
1220
|
NEVER = /* @__PURE__ */ Object.freeze({
|
|
1176
1221
|
status: "aborted"
|
|
1177
1222
|
});
|
|
1178
|
-
$brand = Symbol("zod_brand");
|
|
1223
|
+
$brand = /* @__PURE__ */ Symbol("zod_brand");
|
|
1179
1224
|
$ZodAsyncError = class extends Error {
|
|
1180
1225
|
constructor() {
|
|
1181
1226
|
super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);
|
|
@@ -11344,8 +11389,8 @@ function registry() {
|
|
|
11344
11389
|
var _a2, $output, $input, $ZodRegistry, globalRegistry;
|
|
11345
11390
|
var init_registries = __esm({
|
|
11346
11391
|
"../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/registries.js"() {
|
|
11347
|
-
$output = Symbol("ZodOutput");
|
|
11348
|
-
$input = Symbol("ZodInput");
|
|
11392
|
+
$output = /* @__PURE__ */ Symbol("ZodOutput");
|
|
11393
|
+
$input = /* @__PURE__ */ Symbol("ZodInput");
|
|
11349
11394
|
$ZodRegistry = class {
|
|
11350
11395
|
constructor() {
|
|
11351
11396
|
this._map = /* @__PURE__ */ new WeakMap();
|
|
@@ -16192,6 +16237,435 @@ var init_zod = __esm({
|
|
|
16192
16237
|
}
|
|
16193
16238
|
});
|
|
16194
16239
|
|
|
16240
|
+
// ../core/dist/links.js
|
|
16241
|
+
function parseLinkFrontmatter(data, options = {}) {
|
|
16242
|
+
const normalized = normalizeFrontmatter(data, options);
|
|
16243
|
+
if (!("slug" in normalized))
|
|
16244
|
+
return { ok: true, value: null };
|
|
16245
|
+
const parsed = LinkFrontmatter.safeParse(normalized);
|
|
16246
|
+
if (parsed.success)
|
|
16247
|
+
return { ok: true, value: parsed.data };
|
|
16248
|
+
return { ok: false, errors: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`) };
|
|
16249
|
+
}
|
|
16250
|
+
function buildLinkIndex(inputs, options = {}) {
|
|
16251
|
+
const resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
16252
|
+
const pages = /* @__PURE__ */ new Map();
|
|
16253
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
16254
|
+
const aliasEntries = [];
|
|
16255
|
+
for (const { path, data } of inputs) {
|
|
16256
|
+
const parsed = parseLinkFrontmatter(data, resolvedOptions);
|
|
16257
|
+
if (!parsed.ok || parsed.value === null)
|
|
16258
|
+
continue;
|
|
16259
|
+
const fm = parsed.value;
|
|
16260
|
+
const page = {
|
|
16261
|
+
slug: fm.slug,
|
|
16262
|
+
key: fm.key,
|
|
16263
|
+
locale: effectiveLocale(fm, resolvedOptions),
|
|
16264
|
+
path,
|
|
16265
|
+
title: fm.title,
|
|
16266
|
+
keywords: fm.keywords,
|
|
16267
|
+
linksTo: [],
|
|
16268
|
+
linkedFrom: [],
|
|
16269
|
+
aliases: fm.aliases ?? [],
|
|
16270
|
+
linkRefs: [],
|
|
16271
|
+
linkedFromRefs: []
|
|
16272
|
+
};
|
|
16273
|
+
pages.set(pageMapKey(page, resolvedOptions), page);
|
|
16274
|
+
}
|
|
16275
|
+
for (const page of pages.values()) {
|
|
16276
|
+
for (const alias of page.aliases) {
|
|
16277
|
+
const replacement = replacementFor(page, resolvedOptions);
|
|
16278
|
+
const key = aliasMapKey(alias, page.locale, resolvedOptions);
|
|
16279
|
+
aliases.set(key, replacement);
|
|
16280
|
+
aliasEntries.push({ alias, locale: page.locale, replacement, page });
|
|
16281
|
+
}
|
|
16282
|
+
}
|
|
16283
|
+
const lookup = buildLookup(pages, resolvedOptions);
|
|
16284
|
+
for (const page of pages.values()) {
|
|
16285
|
+
const source = parseLinkFrontmatter(inputForPage(page, inputs, resolvedOptions), resolvedOptions);
|
|
16286
|
+
if (!source.ok || source.value === null)
|
|
16287
|
+
continue;
|
|
16288
|
+
for (const rawTarget of source.value.linksTo ?? []) {
|
|
16289
|
+
const resolved = resolveTarget(rawTarget, page, lookup, resolvedOptions);
|
|
16290
|
+
if (!resolved.page) {
|
|
16291
|
+
page.linksTo.push(resolved.target);
|
|
16292
|
+
continue;
|
|
16293
|
+
}
|
|
16294
|
+
page.linksTo.push(replacementFor(resolved.page, resolvedOptions));
|
|
16295
|
+
page.linkRefs.push(referenceFor(resolved.page, resolved.target));
|
|
16296
|
+
if (resolved.page === page)
|
|
16297
|
+
continue;
|
|
16298
|
+
const from = replacementFor(page, resolvedOptions);
|
|
16299
|
+
if (!resolved.page.linkedFrom.includes(from))
|
|
16300
|
+
resolved.page.linkedFrom.push(from);
|
|
16301
|
+
if (!resolved.page.linkedFromRefs.some((r) => sameReference(r, page))) {
|
|
16302
|
+
resolved.page.linkedFromRefs.push(referenceFor(page));
|
|
16303
|
+
}
|
|
16304
|
+
}
|
|
16305
|
+
}
|
|
16306
|
+
return { pages, aliases, aliasEntries, options: resolvedOptions };
|
|
16307
|
+
}
|
|
16308
|
+
function serializeLinkIndex(index) {
|
|
16309
|
+
const scoped = [...index.pages.values()].some((page) => page.locale || page.key);
|
|
16310
|
+
const pages = [...index.pages.values()].map((p) => {
|
|
16311
|
+
const base = {
|
|
16312
|
+
slug: p.slug,
|
|
16313
|
+
...p.key ? { key: p.key } : {},
|
|
16314
|
+
...p.locale ? { locale: p.locale } : {},
|
|
16315
|
+
path: p.path,
|
|
16316
|
+
...p.title ? { title: p.title } : {},
|
|
16317
|
+
...p.keywords ? { keywords: p.keywords } : {},
|
|
16318
|
+
linksTo: scoped ? p.linkRefs : [...p.linksTo],
|
|
16319
|
+
linkedFrom: scoped ? sortedRefs(p.linkedFromRefs) : [...p.linkedFrom].sort(),
|
|
16320
|
+
aliases: [...p.aliases]
|
|
16321
|
+
};
|
|
16322
|
+
return base;
|
|
16323
|
+
}).sort((a, b) => sortIdentity(a.locale, a.slug).localeCompare(sortIdentity(b.locale, b.slug)));
|
|
16324
|
+
const aliases = {};
|
|
16325
|
+
if (scoped) {
|
|
16326
|
+
for (const entry of [...index.aliasEntries].sort((a, b) => sortIdentity(a.locale, a.alias).localeCompare(sortIdentity(b.locale, b.alias)))) {
|
|
16327
|
+
aliases[aliasMapKey(entry.alias, entry.locale, index.options)] = referenceFor(entry.page);
|
|
16328
|
+
}
|
|
16329
|
+
} else {
|
|
16330
|
+
for (const key of [...index.aliases.keys()].sort())
|
|
16331
|
+
aliases[key] = index.aliases.get(key);
|
|
16332
|
+
}
|
|
16333
|
+
return { pages, aliases };
|
|
16334
|
+
}
|
|
16335
|
+
function aliasReplacementsForPage(index, data) {
|
|
16336
|
+
const parsed = parseLinkFrontmatter(data, index.options);
|
|
16337
|
+
const out = /* @__PURE__ */ new Map();
|
|
16338
|
+
if (!parsed.ok || parsed.value === null)
|
|
16339
|
+
return out;
|
|
16340
|
+
const locale = effectiveLocale(parsed.value, index.options);
|
|
16341
|
+
for (const entry of index.aliasEntries) {
|
|
16342
|
+
if (index.options.resolve === "global-slug" || entry.locale === locale) {
|
|
16343
|
+
out.set(entry.alias, entry.replacement);
|
|
16344
|
+
}
|
|
16345
|
+
}
|
|
16346
|
+
return out;
|
|
16347
|
+
}
|
|
16348
|
+
function validateLinks(inputs, options = {}) {
|
|
16349
|
+
const resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
16350
|
+
const out = [];
|
|
16351
|
+
const validInputs = [];
|
|
16352
|
+
const seenSlug = /* @__PURE__ */ new Map();
|
|
16353
|
+
const seenKey = /* @__PURE__ */ new Map();
|
|
16354
|
+
const seenAlias = /* @__PURE__ */ new Map();
|
|
16355
|
+
for (const { path, data } of inputs) {
|
|
16356
|
+
const parsed = parseLinkFrontmatter(data, resolvedOptions);
|
|
16357
|
+
if (!parsed.ok) {
|
|
16358
|
+
for (const e of parsed.errors)
|
|
16359
|
+
out.push(diag(path, "CB_LINK_SHAPE", "error", `invalid link frontmatter: ${e}`));
|
|
16360
|
+
continue;
|
|
16361
|
+
}
|
|
16362
|
+
if (parsed.value === null)
|
|
16363
|
+
continue;
|
|
16364
|
+
const fm = parsed.value;
|
|
16365
|
+
const locale = effectiveLocale(fm, resolvedOptions);
|
|
16366
|
+
validInputs.push({ path, fm });
|
|
16367
|
+
const slugKey = scopedKey(fm.slug, locale, resolvedOptions);
|
|
16368
|
+
const prior = seenSlug.get(slugKey);
|
|
16369
|
+
if (prior)
|
|
16370
|
+
out.push(diag(path, "CB_SLUG_DUPLICATE", "error", `slug "${fm.slug}" also used by ${prior}`));
|
|
16371
|
+
else
|
|
16372
|
+
seenSlug.set(slugKey, path);
|
|
16373
|
+
if (usesKeyResolution(resolvedOptions) && !fm.key) {
|
|
16374
|
+
out.push(diag(path, "CB_KEY_MISSING", "error", `page "${fm.slug}" is missing key`));
|
|
16375
|
+
}
|
|
16376
|
+
if (fm.key) {
|
|
16377
|
+
const keyKey = scopedKey(fm.key, locale, resolvedOptions);
|
|
16378
|
+
const priorKey = seenKey.get(keyKey);
|
|
16379
|
+
if (priorKey)
|
|
16380
|
+
out.push(diag(path, "CB_KEY_DUPLICATE", "error", `key "${fm.key}" also used by ${priorKey}`));
|
|
16381
|
+
else
|
|
16382
|
+
seenKey.set(keyKey, path);
|
|
16383
|
+
}
|
|
16384
|
+
for (const alias of fm.aliases ?? []) {
|
|
16385
|
+
const aliasKey = scopedKey(alias, locale, resolvedOptions);
|
|
16386
|
+
if (seenAlias.has(aliasKey))
|
|
16387
|
+
out.push(diag(path, "CB_ALIAS_CONFLICT", "error", `alias "${alias}" already declared by ${seenAlias.get(aliasKey)}`));
|
|
16388
|
+
else
|
|
16389
|
+
seenAlias.set(aliasKey, path);
|
|
16390
|
+
}
|
|
16391
|
+
}
|
|
16392
|
+
const index = buildLinkIndex(inputs, resolvedOptions);
|
|
16393
|
+
const lookup = buildLookup(index.pages, resolvedOptions);
|
|
16394
|
+
for (const { path, fm } of validInputs) {
|
|
16395
|
+
const page = index.pages.get(pageMapKey(frontmatterIdentity(fm, resolvedOptions), resolvedOptions));
|
|
16396
|
+
if (!page)
|
|
16397
|
+
continue;
|
|
16398
|
+
for (const alias of page.aliases) {
|
|
16399
|
+
if (collidesWithPageIdentity(alias, page.locale, lookup, resolvedOptions))
|
|
16400
|
+
out.push(diag(page.path, "CB_ALIAS_CONFLICT", "error", `alias "${alias}" collides with an existing page identity`));
|
|
16401
|
+
}
|
|
16402
|
+
for (const target of fm.linksTo ?? []) {
|
|
16403
|
+
const resolved = resolveTarget(target, page, lookup, resolvedOptions);
|
|
16404
|
+
if (resolved.page === page) {
|
|
16405
|
+
out.push(diag(page.path, "CB_LINK_SELF", "warning", `page "${page.slug}" links to itself`));
|
|
16406
|
+
continue;
|
|
16407
|
+
}
|
|
16408
|
+
if (resolved.page && resolved.crossLocale) {
|
|
16409
|
+
out.push(diag(page.path, "CB_LINK_CROSS_LOCALE", "warning", `linksTo "${resolved.target}" resolves to locale "${resolved.page.locale}"`));
|
|
16410
|
+
continue;
|
|
16411
|
+
}
|
|
16412
|
+
if (!resolved.page) {
|
|
16413
|
+
if (targetExistsOutsideLocale(resolved.target, page.locale, lookup, resolvedOptions)) {
|
|
16414
|
+
out.push(diag(page.path, "CB_LINK_LOCALE_MISSING", "error", `linksTo "${resolved.target}" exists in another locale but not "${page.locale ?? "default"}"`));
|
|
16415
|
+
continue;
|
|
16416
|
+
}
|
|
16417
|
+
const hint = closest(resolved.target, candidatesFor(page.locale, lookup, resolvedOptions));
|
|
16418
|
+
out.push(diag(page.path, "CB_LINK_UNRESOLVED", "error", `linksTo "${resolved.target}" does not resolve to any page`, hint ? `Did you mean "${hint}"?` : void 0));
|
|
16419
|
+
}
|
|
16420
|
+
}
|
|
16421
|
+
if (page.linkedFrom.length === 0)
|
|
16422
|
+
out.push(diag(page.path, "CB_LINK_ORPHAN", "warning", `page "${page.slug}" has no inbound links`));
|
|
16423
|
+
}
|
|
16424
|
+
return out;
|
|
16425
|
+
}
|
|
16426
|
+
function normalizeFrontmatter(data, options) {
|
|
16427
|
+
const out = { ...data };
|
|
16428
|
+
copyConfiguredField(out, data, options.slugField, "slug");
|
|
16429
|
+
copyConfiguredField(out, data, options.keyField, "key");
|
|
16430
|
+
copyConfiguredField(out, data, options.localeField, "locale");
|
|
16431
|
+
return out;
|
|
16432
|
+
}
|
|
16433
|
+
function copyConfiguredField(out, data, from, to) {
|
|
16434
|
+
if (!from || from === to || !(from in data) || to in out)
|
|
16435
|
+
return;
|
|
16436
|
+
out[to] = data[from];
|
|
16437
|
+
}
|
|
16438
|
+
function effectiveLocale(fm, options) {
|
|
16439
|
+
return fm.locale ?? options.defaultLocale;
|
|
16440
|
+
}
|
|
16441
|
+
function frontmatterIdentity(fm, options) {
|
|
16442
|
+
return { slug: fm.slug, key: fm.key, locale: effectiveLocale(fm, options) };
|
|
16443
|
+
}
|
|
16444
|
+
function pageMapKey(page, options) {
|
|
16445
|
+
if (options.resolve === "global-slug")
|
|
16446
|
+
return page.slug;
|
|
16447
|
+
return scopedKey(page.slug, page.locale, options);
|
|
16448
|
+
}
|
|
16449
|
+
function scopedKey(value, locale, options) {
|
|
16450
|
+
if (options.resolve === "global-slug")
|
|
16451
|
+
return value;
|
|
16452
|
+
return `${locale ?? ""}\0${value}`;
|
|
16453
|
+
}
|
|
16454
|
+
function aliasMapKey(alias, locale, options) {
|
|
16455
|
+
return options.resolve === "global-slug" ? alias : `${locale ?? ""}:${alias}`;
|
|
16456
|
+
}
|
|
16457
|
+
function replacementFor(page, options) {
|
|
16458
|
+
if (options.resolve === "same-locale-key")
|
|
16459
|
+
return page.key ?? page.slug;
|
|
16460
|
+
if (options.resolve === "prefer-same-locale-key-fallback-slug")
|
|
16461
|
+
return page.key ?? page.slug;
|
|
16462
|
+
return page.slug;
|
|
16463
|
+
}
|
|
16464
|
+
function referenceFor(page, target) {
|
|
16465
|
+
return {
|
|
16466
|
+
...target ? { target } : {},
|
|
16467
|
+
...page.locale ? { locale: page.locale } : {},
|
|
16468
|
+
slug: page.slug,
|
|
16469
|
+
...page.key ? { key: page.key } : {}
|
|
16470
|
+
};
|
|
16471
|
+
}
|
|
16472
|
+
function sameReference(ref, page) {
|
|
16473
|
+
return ref.slug === page.slug && ref.locale === page.locale && ref.key === page.key;
|
|
16474
|
+
}
|
|
16475
|
+
function sortedRefs(refs) {
|
|
16476
|
+
return [...refs].sort((a, b) => sortIdentity(a.locale, a.key ?? a.slug).localeCompare(sortIdentity(b.locale, b.key ?? b.slug)));
|
|
16477
|
+
}
|
|
16478
|
+
function sortIdentity(locale, value) {
|
|
16479
|
+
return `${locale ?? ""}\0${value}`;
|
|
16480
|
+
}
|
|
16481
|
+
function buildLookup(pages, options) {
|
|
16482
|
+
const lookup = {
|
|
16483
|
+
bySlug: /* @__PURE__ */ new Map(),
|
|
16484
|
+
byScopedSlug: /* @__PURE__ */ new Map(),
|
|
16485
|
+
byKey: /* @__PURE__ */ new Map(),
|
|
16486
|
+
byScopedKey: /* @__PURE__ */ new Map(),
|
|
16487
|
+
aliasBySlug: /* @__PURE__ */ new Map(),
|
|
16488
|
+
aliasByScopedSlug: /* @__PURE__ */ new Map(),
|
|
16489
|
+
aliasByKey: /* @__PURE__ */ new Map(),
|
|
16490
|
+
aliasByScopedKey: /* @__PURE__ */ new Map()
|
|
16491
|
+
};
|
|
16492
|
+
for (const page of pages.values()) {
|
|
16493
|
+
pushMulti(lookup.bySlug, page.slug, page);
|
|
16494
|
+
lookup.byScopedSlug.set(scopedKey(page.slug, page.locale, options), page);
|
|
16495
|
+
if (page.key) {
|
|
16496
|
+
pushMulti(lookup.byKey, page.key, page);
|
|
16497
|
+
lookup.byScopedKey.set(scopedKey(page.key, page.locale, options), page);
|
|
16498
|
+
}
|
|
16499
|
+
for (const alias of page.aliases) {
|
|
16500
|
+
lookup.aliasBySlug.set(alias, page);
|
|
16501
|
+
lookup.aliasByScopedSlug.set(scopedKey(alias, page.locale, options), page);
|
|
16502
|
+
if (page.key) {
|
|
16503
|
+
lookup.aliasByKey.set(alias, page);
|
|
16504
|
+
lookup.aliasByScopedKey.set(scopedKey(alias, page.locale, options), page);
|
|
16505
|
+
}
|
|
16506
|
+
}
|
|
16507
|
+
}
|
|
16508
|
+
return lookup;
|
|
16509
|
+
}
|
|
16510
|
+
function pushMulti(map2, key, page) {
|
|
16511
|
+
const existing = map2.get(key);
|
|
16512
|
+
if (existing)
|
|
16513
|
+
existing.push(page);
|
|
16514
|
+
else
|
|
16515
|
+
map2.set(key, [page]);
|
|
16516
|
+
}
|
|
16517
|
+
function resolveTarget(rawTarget, source, lookup, options) {
|
|
16518
|
+
if (typeof rawTarget !== "string") {
|
|
16519
|
+
const locale2 = rawTarget.locale ?? source.locale;
|
|
16520
|
+
const page2 = rawTarget.key ? lookup.byScopedKey.get(scopedKey(rawTarget.key, locale2, options)) : rawTarget.slug ? lookup.byScopedSlug.get(scopedKey(rawTarget.slug, locale2, options)) : void 0;
|
|
16521
|
+
const target = rawTarget.key ?? rawTarget.slug ?? "";
|
|
16522
|
+
return {
|
|
16523
|
+
page: page2,
|
|
16524
|
+
target,
|
|
16525
|
+
explicitLocale: rawTarget.locale,
|
|
16526
|
+
crossLocale: Boolean(page2 && rawTarget.locale && rawTarget.locale !== source.locale),
|
|
16527
|
+
matchedBy: rawTarget.key ? "key" : "slug"
|
|
16528
|
+
};
|
|
16529
|
+
}
|
|
16530
|
+
const locale = source.locale;
|
|
16531
|
+
if (options.resolve === "global-slug") {
|
|
16532
|
+
const page2 = lookup.aliasBySlug.get(rawTarget) ?? single(lookup.bySlug.get(rawTarget));
|
|
16533
|
+
return { page: page2, target: rawTarget, crossLocale: false, matchedBy: page2 ? "slug" : void 0 };
|
|
16534
|
+
}
|
|
16535
|
+
if (options.resolve === "same-locale-key") {
|
|
16536
|
+
const scoped2 = scopedKey(rawTarget, locale, options);
|
|
16537
|
+
const page2 = lookup.byScopedKey.get(scoped2) ?? lookup.aliasByScopedKey.get(scoped2);
|
|
16538
|
+
return { page: page2, target: rawTarget, crossLocale: false, matchedBy: page2 ? "key" : void 0 };
|
|
16539
|
+
}
|
|
16540
|
+
if (options.resolve === "prefer-same-locale-key-fallback-slug") {
|
|
16541
|
+
const scoped2 = scopedKey(rawTarget, locale, options);
|
|
16542
|
+
const page2 = lookup.byScopedKey.get(scoped2) ?? lookup.aliasByScopedKey.get(scoped2) ?? lookup.byScopedSlug.get(scoped2) ?? lookup.aliasByScopedSlug.get(scoped2);
|
|
16543
|
+
return { page: page2, target: rawTarget, crossLocale: false, matchedBy: page2 ? "key" : void 0 };
|
|
16544
|
+
}
|
|
16545
|
+
const scoped = scopedKey(rawTarget, locale, options);
|
|
16546
|
+
const page = lookup.byScopedSlug.get(scoped) ?? lookup.aliasByScopedSlug.get(scoped);
|
|
16547
|
+
return { page, target: rawTarget, crossLocale: false, matchedBy: page ? "slug" : void 0 };
|
|
16548
|
+
}
|
|
16549
|
+
function single(values) {
|
|
16550
|
+
return values?.length === 1 ? values[0] : void 0;
|
|
16551
|
+
}
|
|
16552
|
+
function usesKeyResolution(options) {
|
|
16553
|
+
return options.resolve === "same-locale-key";
|
|
16554
|
+
}
|
|
16555
|
+
function collidesWithPageIdentity(alias, locale, lookup, options) {
|
|
16556
|
+
if (options.resolve === "global-slug")
|
|
16557
|
+
return lookup.bySlug.has(alias);
|
|
16558
|
+
const scoped = scopedKey(alias, locale, options);
|
|
16559
|
+
return lookup.byScopedSlug.has(scoped) || usesKeyResolution(options) && lookup.byScopedKey.has(scoped);
|
|
16560
|
+
}
|
|
16561
|
+
function targetExistsOutsideLocale(target, locale, lookup, options) {
|
|
16562
|
+
if (options.resolve === "global-slug")
|
|
16563
|
+
return false;
|
|
16564
|
+
const byIdentity = usesKeyResolution(options) ? lookup.byKey : lookup.bySlug;
|
|
16565
|
+
return (byIdentity.get(target) ?? []).some((page) => page.locale !== locale);
|
|
16566
|
+
}
|
|
16567
|
+
function candidatesFor(locale, lookup, options) {
|
|
16568
|
+
if (options.resolve === "global-slug")
|
|
16569
|
+
return [...lookup.bySlug.keys()];
|
|
16570
|
+
const out = [];
|
|
16571
|
+
for (const page of lookup.byScopedSlug.values()) {
|
|
16572
|
+
if (page.locale === locale)
|
|
16573
|
+
out.push(usesKeyResolution(options) && page.key ? page.key : page.slug);
|
|
16574
|
+
}
|
|
16575
|
+
return out;
|
|
16576
|
+
}
|
|
16577
|
+
function inputForPage(page, inputs, options) {
|
|
16578
|
+
for (const input of inputs) {
|
|
16579
|
+
const parsed = parseLinkFrontmatter(input.data, options);
|
|
16580
|
+
if (!parsed.ok || parsed.value === null)
|
|
16581
|
+
continue;
|
|
16582
|
+
const identity = frontmatterIdentity(parsed.value, options);
|
|
16583
|
+
if (pageMapKey(identity, options) === pageMapKey(page, options))
|
|
16584
|
+
return input.data;
|
|
16585
|
+
}
|
|
16586
|
+
return {};
|
|
16587
|
+
}
|
|
16588
|
+
function diag(file2, code, severity, message, hint) {
|
|
16589
|
+
return { file: file2, diagnostic: { code, severity, message, hint, position: FM_POSITION } };
|
|
16590
|
+
}
|
|
16591
|
+
function editDistance(a, b) {
|
|
16592
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
16593
|
+
for (let j = 0; j <= b.length; j++)
|
|
16594
|
+
dp[0][j] = j;
|
|
16595
|
+
for (let i = 1; i <= a.length; i++) {
|
|
16596
|
+
for (let j = 1; j <= b.length; j++) {
|
|
16597
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
16598
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
16599
|
+
}
|
|
16600
|
+
}
|
|
16601
|
+
return dp[a.length][b.length];
|
|
16602
|
+
}
|
|
16603
|
+
function closest(target, candidates) {
|
|
16604
|
+
let best;
|
|
16605
|
+
let bestD = Infinity;
|
|
16606
|
+
for (const c of candidates) {
|
|
16607
|
+
const d = editDistance(target, c);
|
|
16608
|
+
if (d < bestD) {
|
|
16609
|
+
bestD = d;
|
|
16610
|
+
best = c;
|
|
16611
|
+
}
|
|
16612
|
+
}
|
|
16613
|
+
return best && bestD <= Math.max(2, Math.floor(target.length / 3)) ? best : void 0;
|
|
16614
|
+
}
|
|
16615
|
+
var Keywords, LinkTarget, LinkFrontmatter, DEFAULT_OPTIONS, FM_POSITION;
|
|
16616
|
+
var init_links = __esm({
|
|
16617
|
+
"../core/dist/links.js"() {
|
|
16618
|
+
"use strict";
|
|
16619
|
+
init_zod();
|
|
16620
|
+
Keywords = external_exports.object({
|
|
16621
|
+
primary: external_exports.string().optional(),
|
|
16622
|
+
secondary: external_exports.array(external_exports.string()).optional()
|
|
16623
|
+
});
|
|
16624
|
+
LinkTarget = external_exports.union([
|
|
16625
|
+
external_exports.string(),
|
|
16626
|
+
external_exports.object({
|
|
16627
|
+
slug: external_exports.string().min(1).optional(),
|
|
16628
|
+
key: external_exports.string().min(1).optional(),
|
|
16629
|
+
locale: external_exports.string().min(1).optional()
|
|
16630
|
+
}).refine((target) => target.slug || target.key, {
|
|
16631
|
+
message: "object linksTo targets must include slug or key"
|
|
16632
|
+
})
|
|
16633
|
+
]);
|
|
16634
|
+
LinkFrontmatter = external_exports.object({
|
|
16635
|
+
slug: external_exports.string().min(1),
|
|
16636
|
+
key: external_exports.string().min(1).optional(),
|
|
16637
|
+
locale: external_exports.string().min(1).optional(),
|
|
16638
|
+
title: external_exports.string().optional(),
|
|
16639
|
+
linksTo: external_exports.array(LinkTarget).optional(),
|
|
16640
|
+
aliases: external_exports.array(external_exports.string()).optional(),
|
|
16641
|
+
keywords: Keywords.optional()
|
|
16642
|
+
});
|
|
16643
|
+
DEFAULT_OPTIONS = {
|
|
16644
|
+
resolve: "global-slug"
|
|
16645
|
+
};
|
|
16646
|
+
FM_POSITION = {
|
|
16647
|
+
start: { line: 1, column: 1, offset: 0 },
|
|
16648
|
+
end: { line: 1, column: 1, offset: 0 }
|
|
16649
|
+
};
|
|
16650
|
+
}
|
|
16651
|
+
});
|
|
16652
|
+
|
|
16653
|
+
// ../core/dist/index.js
|
|
16654
|
+
var init_dist = __esm({
|
|
16655
|
+
"../core/dist/index.js"() {
|
|
16656
|
+
"use strict";
|
|
16657
|
+
init_diagnostics();
|
|
16658
|
+
init_parser();
|
|
16659
|
+
init_frontmatter();
|
|
16660
|
+
init_analyze();
|
|
16661
|
+
init_registry();
|
|
16662
|
+
init_content_models();
|
|
16663
|
+
init_validate();
|
|
16664
|
+
init_render_markdown();
|
|
16665
|
+
init_links();
|
|
16666
|
+
}
|
|
16667
|
+
});
|
|
16668
|
+
|
|
16195
16669
|
// ../blocks/dist/blocks/callout.js
|
|
16196
16670
|
var calloutBlock, calloutMarkdown;
|
|
16197
16671
|
var init_callout = __esm({
|
|
@@ -16616,7 +17090,7 @@ var TEMPLATE_VERSION, AUTHOR_SKILL, AUDIT_SKILL, AGENTS_MD_BLOCK, START, END;
|
|
|
16616
17090
|
var init_agents = __esm({
|
|
16617
17091
|
"src/commands/agents.ts"() {
|
|
16618
17092
|
"use strict";
|
|
16619
|
-
TEMPLATE_VERSION =
|
|
17093
|
+
TEMPLATE_VERSION = 2;
|
|
16620
17094
|
AUTHOR_SKILL = `---
|
|
16621
17095
|
name: contentbit-author
|
|
16622
17096
|
description: |
|
|
@@ -16639,6 +17113,8 @@ Check \`package.json\` for a \`content:check\` script. It holds the canonical
|
|
|
16639
17113
|
validate invocation for this project: the content glob and, if present, the
|
|
16640
17114
|
\`--registry <path>\` flag pointing at custom block definitions. Reuse both
|
|
16641
17115
|
below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
|
|
17116
|
+
If the project has a \`content:links\` script, use it for the internal-link
|
|
17117
|
+
index; otherwise run \`contentbit links <content glob>\` directly.
|
|
16642
17118
|
|
|
16643
17119
|
## The loop
|
|
16644
17120
|
|
|
@@ -16653,7 +17129,19 @@ below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
|
|
|
16653
17129
|
|
|
16654
17130
|
2. **Write the document.** Plain Markdown everywhere; blocks only where the
|
|
16655
17131
|
guide's use-when guidance fits. Keep frontmatter consistent with sibling
|
|
16656
|
-
documents in the same folder.
|
|
17132
|
+
documents in the same folder. If sibling documents use \`slug\`, \`linksTo\`,
|
|
17133
|
+
\`aliases\`, or \`keywords\`, run the link index first:
|
|
17134
|
+
|
|
17135
|
+
\`\`\`sh
|
|
17136
|
+
contentbit links <content glob>
|
|
17137
|
+
\`\`\`
|
|
17138
|
+
|
|
17139
|
+
Read \`.contentbit/link-index.json\` to pick existing slugs and related
|
|
17140
|
+
pages. Author only \`slug\`, \`linksTo\`, \`aliases\`, and \`keywords\` in
|
|
17141
|
+
frontmatter; never write derived \`linkedFrom\` into source files. When
|
|
17142
|
+
creating a linked page, include \`keywords.primary\` and
|
|
17143
|
+
\`keywords.secondary\` with search-intent phrases that would help future
|
|
17144
|
+
agents choose this page as a \`linksTo\` target.
|
|
16657
17145
|
|
|
16658
17146
|
3. **Validate and fix until clean:**
|
|
16659
17147
|
|
|
@@ -16663,8 +17151,19 @@ below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
|
|
|
16663
17151
|
|
|
16664
17152
|
Diagnostics print to stderr as \`file:line:col severity CODE message\`, often
|
|
16665
17153
|
with a \`hint:\` line suggesting the fix. Exit 0 means clean; exit 1 means
|
|
16666
|
-
errors remain.
|
|
16667
|
-
|
|
17154
|
+
errors remain. If the document has link frontmatter, validate the full
|
|
17155
|
+
content glob so cross-file links are checked against the whole graph. Fix
|
|
17156
|
+
every diagnostic and re-run. Never finish with a failing validate.
|
|
17157
|
+
|
|
17158
|
+
4. **Refresh internal links when present:**
|
|
17159
|
+
|
|
17160
|
+
\`\`\`sh
|
|
17161
|
+
contentbit links <content glob> --fix
|
|
17162
|
+
\`\`\`
|
|
17163
|
+
|
|
17164
|
+
\`--fix\` only rewrites \`linksTo\` values that point at known aliases. It
|
|
17165
|
+
does not invent links, remove aliases, or write backlinks. Re-run validate
|
|
17166
|
+
after it changes files.
|
|
16668
17167
|
|
|
16669
17168
|
## Failure modes
|
|
16670
17169
|
|
|
@@ -16686,6 +17185,8 @@ version: ${TEMPLATE_VERSION}
|
|
|
16686
17185
|
|
|
16687
17186
|
\`contentbit stats\` analyzes documents and prints JSON to stdout. It is a read
|
|
16688
17187
|
tool: it always exits 0, even when documents have validation errors.
|
|
17188
|
+
\`contentbit links\` builds the frontmatter-authored internal-link graph and
|
|
17189
|
+
prints link diagnostics.
|
|
16689
17190
|
|
|
16690
17191
|
## Gather
|
|
16691
17192
|
|
|
@@ -16694,23 +17195,31 @@ content glob and \`--registry\` flag, then:
|
|
|
16694
17195
|
|
|
16695
17196
|
\`\`\`sh
|
|
16696
17197
|
contentbit stats "content/**/*.md" [--registry <path>]
|
|
17198
|
+
contentbit links "content/**/*.md"
|
|
16697
17199
|
\`\`\`
|
|
16698
17200
|
|
|
16699
17201
|
One matched file prints a single stats object; multiple files print an array.
|
|
16700
17202
|
Each entry includes the file path, frontmatter data, a heading \`outline\` with
|
|
16701
17203
|
per-section word counts, \`blocks.byName\` usage counts, \`links.domains\`, and
|
|
16702
17204
|
a \`validation\` summary (\`errors\`/\`warnings\`).
|
|
17205
|
+
\`contentbit links\` also writes \`.contentbit/link-index.json\`, whose pages
|
|
17206
|
+
contain \`slug\`, resolved \`linksTo\`, derived \`linkedFrom\`, \`aliases\`, and
|
|
17207
|
+
\`keywords\`.
|
|
16703
17208
|
|
|
16704
17209
|
## Interpret
|
|
16705
17210
|
|
|
16706
17211
|
Prioritize findings in this order:
|
|
16707
17212
|
|
|
16708
17213
|
1. **Validation errors and warnings** \u2014 broken content ships broken pages.
|
|
16709
|
-
2. **
|
|
16710
|
-
|
|
17214
|
+
2. **Internal-link errors** \u2014 unresolved links, duplicate slugs, and alias
|
|
17215
|
+
conflicts from \`contentbit links\`.
|
|
17216
|
+
3. **Orphans and self-links** \u2014 link warnings that point to isolated or noisy
|
|
17217
|
+
pages.
|
|
17218
|
+
4. **Thin documents** \u2014 outline sections with very low word counts.
|
|
17219
|
+
5. **Block-less documents** \u2014 \`blocks.byName\` empty where sibling documents
|
|
16711
17220
|
use blocks; structure (steps, callouts, comparisons, faq) may be missing.
|
|
16712
|
-
|
|
16713
|
-
|
|
17221
|
+
6. **Missing or inconsistent frontmatter** compared to sibling documents.
|
|
17222
|
+
7. **Structural imbalance** \u2014 skipped heading levels, single-section walls of text.
|
|
16714
17223
|
|
|
16715
17224
|
## Report
|
|
16716
17225
|
|
|
@@ -16726,14 +17235,22 @@ This project validates Markdown content with contentbit. Documents are plain
|
|
|
16726
17235
|
Markdown plus directive blocks (\`:::name{props} ... :::\`), each with a schema.
|
|
16727
17236
|
The \`content:check\` script in package.json holds the canonical validate
|
|
16728
17237
|
command \u2014 the content glob and the \`--registry\` flag \u2014 reuse its arguments.
|
|
17238
|
+
If the project has a \`content:links\` script, use it to build the internal-link
|
|
17239
|
+
index; otherwise run \`contentbit links <content glob>\`.
|
|
16729
17240
|
|
|
16730
17241
|
When writing or editing content:
|
|
16731
17242
|
|
|
16732
17243
|
1. Fetch the live authoring guide first \u2014 never guess block syntax:
|
|
16733
17244
|
\`contentbit instructions --audience llm [--registry <path>]\`
|
|
16734
17245
|
2. Write plain Markdown; use blocks where the guide's use-when guidance fits.
|
|
16735
|
-
3.
|
|
17246
|
+
3. If sibling documents use \`slug\` / \`linksTo\`, read
|
|
17247
|
+
\`.contentbit/link-index.json\` from \`contentbit links <content glob>\` and
|
|
17248
|
+
author frontmatter links with existing slugs. When creating a linked page,
|
|
17249
|
+
include \`keywords.primary\` and \`keywords.secondary\` with search-intent
|
|
17250
|
+
phrases future agents can use to choose related pages.
|
|
17251
|
+
4. Validate until clean (exit 0): \`contentbit validate <file> [--registry <path>]\`.
|
|
16736
17252
|
Diagnostics print as \`file:line:col severity CODE message\` with fix hints.
|
|
17253
|
+
For link frontmatter, validate the full content glob so cross-file checks run.
|
|
16737
17254
|
|
|
16738
17255
|
When auditing content health:
|
|
16739
17256
|
|
|
@@ -16741,6 +17258,9 @@ When auditing content health:
|
|
|
16741
17258
|
and always exits 0: outline word counts, block usage, link domains, and
|
|
16742
17259
|
validation error/warning counts. Flag validation issues, thin documents, and
|
|
16743
17260
|
block-less pages first.
|
|
17261
|
+
- \`contentbit links "content/**/*.md" [--fix]\` builds
|
|
17262
|
+
\`.contentbit/link-index.json\`, reports dangling links/orphans, and rewrites
|
|
17263
|
+
alias references in \`linksTo\` when \`--fix\` is used.
|
|
16744
17264
|
|
|
16745
17265
|
If \`contentbit\` is unavailable, suggest \`npx contentbit@latest init\` instead
|
|
16746
17266
|
of inventing block syntax.
|
|
@@ -16808,7 +17328,7 @@ import { ContentRenderer } from '@/components/content-blocks/content-renderer'`
|
|
|
16808
17328
|
return `'use client'
|
|
16809
17329
|
|
|
16810
17330
|
import { genericBlocks } from '@contentbit/blocks'
|
|
16811
|
-
import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
|
|
17331
|
+
import { createBlockRegistry, parseDocument, stripFrontmatter, validateDocument } from '@contentbit/core'
|
|
16812
17332
|
${reactImport}${mdImport}${rendererImport}
|
|
16813
17333
|
// Everything block-related lives in the blocks/ folder: definitions in
|
|
16814
17334
|
// registry.ts (shared with the validate CLI), components in components.tsx.
|
|
@@ -16818,7 +17338,7 @@ import { blockComponents } from '${blocksImport}/components'
|
|
|
16818
17338
|
const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
|
|
16819
17339
|
|
|
16820
17340
|
export function Content({ source }: { source: string }) {
|
|
16821
|
-
const result = validateDocument(parseDocument(source), registry)
|
|
17341
|
+
const result = validateDocument(parseDocument(stripFrontmatter(source)), registry)
|
|
16822
17342
|
return (
|
|
16823
17343
|
<${renderer}
|
|
16824
17344
|
document={result.document}
|
|
@@ -16838,14 +17358,14 @@ const renderMarkdown = (md) => mdIt.render(md)` : `// TODO: plug a Markdown libr
|
|
|
16838
17358
|
const renderMarkdown = undefined`;
|
|
16839
17359
|
return `// Render content/example.md to example.html. Run: node scripts/render-example.mjs
|
|
16840
17360
|
import { genericBlocks } from '@contentbit/blocks'
|
|
16841
|
-
import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
|
|
17361
|
+
import { createBlockRegistry, parseDocument, stripFrontmatter, validateDocument } from '@contentbit/core'
|
|
16842
17362
|
import { renderToHtml } from '@contentbit/html'
|
|
16843
17363
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
16844
17364
|
${wiring}
|
|
16845
17365
|
|
|
16846
17366
|
const source = await readFile('content/example.md', 'utf8')
|
|
16847
17367
|
const registry = createBlockRegistry().use(genericBlocks())
|
|
16848
|
-
const result = validateDocument(parseDocument(source), registry)
|
|
17368
|
+
const result = validateDocument(parseDocument(stripFrontmatter(source)), registry)
|
|
16849
17369
|
const html = renderToHtml(result.document, { renderMarkdown })
|
|
16850
17370
|
await writeFile('example.html', html, 'utf8')
|
|
16851
17371
|
console.log('wrote example.html')
|
|
@@ -17058,7 +17578,8 @@ async function initCommand(args, io) {
|
|
|
17058
17578
|
}
|
|
17059
17579
|
const files = [
|
|
17060
17580
|
["blocks/registry.ts", REGISTRY_TEMPLATE],
|
|
17061
|
-
["content/example.md", EXAMPLE_CONTENT]
|
|
17581
|
+
["content/example.md", EXAMPLE_CONTENT],
|
|
17582
|
+
["content/related.md", RELATED_CONTENT]
|
|
17062
17583
|
];
|
|
17063
17584
|
const layout = detectFramework(cwd, { ...pkg.dependencies, ...pkg.devDependencies });
|
|
17064
17585
|
let styled = false;
|
|
@@ -17112,9 +17633,15 @@ async function initCommand(args, io) {
|
|
|
17112
17633
|
fresh.scripts ??= {};
|
|
17113
17634
|
if (!fresh.scripts["content:check"]) {
|
|
17114
17635
|
fresh.scripts["content:check"] = 'contentbit validate "content/**/*.md" --registry ./blocks/registry.ts';
|
|
17636
|
+
io.stdout("added script: content:check");
|
|
17637
|
+
}
|
|
17638
|
+
if (!fresh.scripts["content:links"]) {
|
|
17639
|
+
fresh.scripts["content:links"] = 'contentbit links "content/**/*.md"';
|
|
17640
|
+
io.stdout("added script: content:links");
|
|
17641
|
+
}
|
|
17642
|
+
if (!pkg.scripts?.["content:check"] || !pkg.scripts?.["content:links"]) {
|
|
17115
17643
|
await writeFile2(pkgPath, `${JSON.stringify(fresh, null, 2)}
|
|
17116
17644
|
`, "utf8");
|
|
17117
|
-
io.stdout("added script: content:check");
|
|
17118
17645
|
}
|
|
17119
17646
|
let registry2;
|
|
17120
17647
|
try {
|
|
@@ -17133,6 +17660,7 @@ async function initCommand(args, io) {
|
|
|
17133
17660
|
io.stdout("");
|
|
17134
17661
|
io.stdout("Done. Next steps:");
|
|
17135
17662
|
io.stdout(` 1. Validate the starter content: ${detectPackageManager(cwd)} run content:check`);
|
|
17663
|
+
io.stdout(` Build the link index: ${detectPackageManager(cwd)} run content:links`);
|
|
17136
17664
|
if (target === "react") {
|
|
17137
17665
|
if (!values["no-page"] && layout.pagePath) {
|
|
17138
17666
|
io.stdout(" 2. Start the dev server and open /example to see the article rendered.");
|
|
@@ -17152,7 +17680,7 @@ async function initCommand(args, io) {
|
|
|
17152
17680
|
io.stdout(" Docs: https://contentbit.dev/docs");
|
|
17153
17681
|
return 0;
|
|
17154
17682
|
}
|
|
17155
|
-
var TARGETS, MD_CHOICES, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, TANSTACK_PAGE, NEXT_PAGE, ASTRO_CONTENT_CONFIG, ASTRO_QUOTE_BLOCK;
|
|
17683
|
+
var TARGETS, MD_CHOICES, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, RELATED_CONTENT, TANSTACK_PAGE, NEXT_PAGE, ASTRO_CONTENT_CONFIG, ASTRO_QUOTE_BLOCK;
|
|
17156
17684
|
var init_init = __esm({
|
|
17157
17685
|
"src/commands/init.ts"() {
|
|
17158
17686
|
"use strict";
|
|
@@ -17194,7 +17722,18 @@ export const quote = defineBlock({
|
|
|
17194
17722
|
|
|
17195
17723
|
export default [quote] satisfies BlockDefinition<unknown>[]
|
|
17196
17724
|
`;
|
|
17197
|
-
EXAMPLE_CONTENT =
|
|
17725
|
+
EXAMPLE_CONTENT = `---
|
|
17726
|
+
slug: hello-content-blocks
|
|
17727
|
+
linksTo:
|
|
17728
|
+
- related-contentbit-workflows
|
|
17729
|
+
aliases:
|
|
17730
|
+
- getting-started-contentbit
|
|
17731
|
+
keywords:
|
|
17732
|
+
primary: validated Markdown blocks
|
|
17733
|
+
secondary: [content workflow, agent writing]
|
|
17734
|
+
---
|
|
17735
|
+
|
|
17736
|
+
# Hello, Content Blocks
|
|
17198
17737
|
|
|
17199
17738
|
Regular Markdown works everywhere. Blocks add validated structure:
|
|
17200
17739
|
|
|
@@ -17215,6 +17754,27 @@ by the \`QuoteBlock\` component, in about twenty lines:
|
|
|
17215
17754
|
The Analytical Engine weaves algebraic patterns just as the Jacquard loom
|
|
17216
17755
|
weaves flowers and leaves.
|
|
17217
17756
|
:::
|
|
17757
|
+
`;
|
|
17758
|
+
RELATED_CONTENT = `---
|
|
17759
|
+
slug: related-contentbit-workflows
|
|
17760
|
+
linksTo:
|
|
17761
|
+
- hello-content-blocks
|
|
17762
|
+
keywords:
|
|
17763
|
+
primary: contentbit workflow
|
|
17764
|
+
secondary: [validation loop, internal links]
|
|
17765
|
+
---
|
|
17766
|
+
|
|
17767
|
+
# Related contentbit workflows
|
|
17768
|
+
|
|
17769
|
+
This supporting page exists to show internal links in frontmatter. The link
|
|
17770
|
+
graph is authored once with \`slug\` and \`linksTo\`, then contentbit derives
|
|
17771
|
+
\`linkedFrom\` in \`.contentbit/link-index.json\`.
|
|
17772
|
+
|
|
17773
|
+
:::callout{type="note"}
|
|
17774
|
+
Run \`contentbit links "content/**/*.md" --fix\` after renaming a page. Alias
|
|
17775
|
+
references in \`linksTo\` are rewritten to the current slug, while \`aliases\`
|
|
17776
|
+
stays as the rename record.
|
|
17777
|
+
:::
|
|
17218
17778
|
`;
|
|
17219
17779
|
TANSTACK_PAGE = `import { createFileRoute } from '@tanstack/react-router'
|
|
17220
17780
|
|
|
@@ -17279,6 +17839,36 @@ const { author, role } = Astro.props
|
|
|
17279
17839
|
}
|
|
17280
17840
|
});
|
|
17281
17841
|
|
|
17842
|
+
// src/link-options.ts
|
|
17843
|
+
function linkResolverOptions(values) {
|
|
17844
|
+
const out = {};
|
|
17845
|
+
const resolve = stringValue(values["link-resolve"]);
|
|
17846
|
+
if (resolve) {
|
|
17847
|
+
if (!isResolveMode(resolve)) throw new Error(`invalid --link-resolve ${resolve}`);
|
|
17848
|
+
out.resolve = resolve;
|
|
17849
|
+
}
|
|
17850
|
+
const localeField = stringValue(values["locale-field"]);
|
|
17851
|
+
const slugField = stringValue(values["slug-field"]);
|
|
17852
|
+
const keyField = stringValue(values["key-field"]);
|
|
17853
|
+
const defaultLocale = stringValue(values["default-locale"]);
|
|
17854
|
+
if (localeField) out.localeField = localeField;
|
|
17855
|
+
if (slugField) out.slugField = slugField;
|
|
17856
|
+
if (keyField) out.keyField = keyField;
|
|
17857
|
+
if (defaultLocale) out.defaultLocale = defaultLocale;
|
|
17858
|
+
return out;
|
|
17859
|
+
}
|
|
17860
|
+
function stringValue(value) {
|
|
17861
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
17862
|
+
}
|
|
17863
|
+
function isResolveMode(value) {
|
|
17864
|
+
return value === "global-slug" || value === "same-locale-slug" || value === "same-locale-key" || value === "prefer-same-locale-key-fallback-slug";
|
|
17865
|
+
}
|
|
17866
|
+
var init_link_options = __esm({
|
|
17867
|
+
"src/link-options.ts"() {
|
|
17868
|
+
"use strict";
|
|
17869
|
+
}
|
|
17870
|
+
});
|
|
17871
|
+
|
|
17282
17872
|
// src/commands/validate.ts
|
|
17283
17873
|
var validate_exports = {};
|
|
17284
17874
|
__export(validate_exports, {
|
|
@@ -17293,7 +17883,12 @@ async function validateCommand(args, io) {
|
|
|
17293
17883
|
allowPositionals: true,
|
|
17294
17884
|
options: {
|
|
17295
17885
|
registry: { type: "string" },
|
|
17296
|
-
"strict-warnings": { type: "boolean", default: false }
|
|
17886
|
+
"strict-warnings": { type: "boolean", default: false },
|
|
17887
|
+
"link-resolve": { type: "string" },
|
|
17888
|
+
"locale-field": { type: "string" },
|
|
17889
|
+
"slug-field": { type: "string" },
|
|
17890
|
+
"key-field": { type: "string" },
|
|
17891
|
+
"default-locale": { type: "string" }
|
|
17297
17892
|
}
|
|
17298
17893
|
});
|
|
17299
17894
|
if (positionals.length === 0) {
|
|
@@ -17306,10 +17901,13 @@ async function validateCommand(args, io) {
|
|
|
17306
17901
|
return 2;
|
|
17307
17902
|
}
|
|
17308
17903
|
const registry2 = await loadRegistry(values.registry);
|
|
17904
|
+
const linkOptions = linkResolverOptions(values);
|
|
17309
17905
|
let errors = 0;
|
|
17310
17906
|
let warnings = 0;
|
|
17907
|
+
const linkInputs = [];
|
|
17311
17908
|
for (const file2 of files.sort()) {
|
|
17312
17909
|
const source = await readFile3(file2, "utf8");
|
|
17910
|
+
linkInputs.push({ path: file2, data: extractFrontmatter(source)?.data ?? {} });
|
|
17313
17911
|
const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
|
|
17314
17912
|
for (const d of result.diagnostics) {
|
|
17315
17913
|
io.stderr(formatDiagnostic(d, file2));
|
|
@@ -17317,6 +17915,13 @@ async function validateCommand(args, io) {
|
|
|
17317
17915
|
else if (d.severity === "warning") warnings++;
|
|
17318
17916
|
}
|
|
17319
17917
|
}
|
|
17918
|
+
if (linkInputs.some((i) => "slug" in i.data)) {
|
|
17919
|
+
for (const { file: file2, diagnostic } of validateLinks(linkInputs, linkOptions)) {
|
|
17920
|
+
io.stderr(formatDiagnostic(diagnostic, file2));
|
|
17921
|
+
if (diagnostic.severity === "error") errors++;
|
|
17922
|
+
else if (diagnostic.severity === "warning") warnings++;
|
|
17923
|
+
}
|
|
17924
|
+
}
|
|
17320
17925
|
io.stdout(`${files.length} file(s): ${errors} errors, ${warnings} warnings`);
|
|
17321
17926
|
if (errors > 0) return 1;
|
|
17322
17927
|
if (warnings > 0 && values["strict-warnings"]) return 1;
|
|
@@ -17326,6 +17931,7 @@ var init_validate2 = __esm({
|
|
|
17326
17931
|
"src/commands/validate.ts"() {
|
|
17327
17932
|
"use strict";
|
|
17328
17933
|
init_dist();
|
|
17934
|
+
init_link_options();
|
|
17329
17935
|
init_load_registry();
|
|
17330
17936
|
}
|
|
17331
17937
|
});
|
|
@@ -17543,7 +18149,7 @@ async function renderCommand(args, io) {
|
|
|
17543
18149
|
}
|
|
17544
18150
|
const registry2 = await loadRegistry(values.registry);
|
|
17545
18151
|
const source = await readFile5(file2, "utf8");
|
|
17546
|
-
const result = validateDocument(parseDocument(source), registry2);
|
|
18152
|
+
const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
|
|
17547
18153
|
if (!result.ok) {
|
|
17548
18154
|
for (const d of result.diagnostics) io.stderr(formatDiagnostic(d, file2));
|
|
17549
18155
|
return 1;
|
|
@@ -17622,17 +18228,133 @@ var init_docs = __esm({
|
|
|
17622
18228
|
}
|
|
17623
18229
|
});
|
|
17624
18230
|
|
|
18231
|
+
// src/links-io.ts
|
|
18232
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
18233
|
+
async function collectLinkInputs(files) {
|
|
18234
|
+
const inputs = [];
|
|
18235
|
+
for (const path of files) {
|
|
18236
|
+
const source = await readFile6(path, "utf8");
|
|
18237
|
+
const fm = extractFrontmatter(source);
|
|
18238
|
+
inputs.push({ path, data: fm?.data ?? {} });
|
|
18239
|
+
}
|
|
18240
|
+
return inputs;
|
|
18241
|
+
}
|
|
18242
|
+
var init_links_io = __esm({
|
|
18243
|
+
"src/links-io.ts"() {
|
|
18244
|
+
"use strict";
|
|
18245
|
+
init_dist();
|
|
18246
|
+
}
|
|
18247
|
+
});
|
|
18248
|
+
|
|
18249
|
+
// src/commands/links.ts
|
|
18250
|
+
var links_exports = {};
|
|
18251
|
+
__export(links_exports, {
|
|
18252
|
+
linksCommand: () => linksCommand
|
|
18253
|
+
});
|
|
18254
|
+
import { mkdir as mkdir3, readFile as readFile7 } from "node:fs/promises";
|
|
18255
|
+
import { dirname, join as join3 } from "node:path";
|
|
18256
|
+
import { parseArgs as parseArgs8 } from "node:util";
|
|
18257
|
+
import { glob as glob3 } from "tinyglobby";
|
|
18258
|
+
async function linksCommand(args, io) {
|
|
18259
|
+
const { values, positionals } = parseArgs8({
|
|
18260
|
+
args,
|
|
18261
|
+
allowPositionals: true,
|
|
18262
|
+
options: {
|
|
18263
|
+
out: { type: "string" },
|
|
18264
|
+
fix: { type: "boolean", default: false },
|
|
18265
|
+
"link-resolve": { type: "string" },
|
|
18266
|
+
"locale-field": { type: "string" },
|
|
18267
|
+
"slug-field": { type: "string" },
|
|
18268
|
+
"key-field": { type: "string" },
|
|
18269
|
+
"default-locale": { type: "string" }
|
|
18270
|
+
}
|
|
18271
|
+
});
|
|
18272
|
+
if (positionals.length === 0) {
|
|
18273
|
+
io.stderr("links: provide at least one file or glob.");
|
|
18274
|
+
return 2;
|
|
18275
|
+
}
|
|
18276
|
+
const files = (await glob3(positionals, { absolute: true })).sort();
|
|
18277
|
+
if (files.length === 0) {
|
|
18278
|
+
io.stderr(`links: no files matched ${positionals.join(" ")}`);
|
|
18279
|
+
return 2;
|
|
18280
|
+
}
|
|
18281
|
+
const inputs = await collectLinkInputs(files);
|
|
18282
|
+
const linkOptions = linkResolverOptions(values);
|
|
18283
|
+
let errors = 0;
|
|
18284
|
+
let warnings = 0;
|
|
18285
|
+
for (const { file: file2, diagnostic } of validateLinks(inputs, linkOptions)) {
|
|
18286
|
+
io.stderr(formatDiagnostic(diagnostic, file2));
|
|
18287
|
+
if (diagnostic.severity === "error") errors++;
|
|
18288
|
+
else if (diagnostic.severity === "warning") warnings++;
|
|
18289
|
+
}
|
|
18290
|
+
const index = buildLinkIndex(inputs, linkOptions);
|
|
18291
|
+
if (values.fix && errors > 0) {
|
|
18292
|
+
io.stderr("links: --fix skipped because link errors must be resolved first.");
|
|
18293
|
+
} else if (values.fix && index.aliases.size > 0) {
|
|
18294
|
+
for (const file2 of files) {
|
|
18295
|
+
const source = await readFile7(file2, "utf8");
|
|
18296
|
+
const fm = extractFrontmatter(source);
|
|
18297
|
+
if (!fm) continue;
|
|
18298
|
+
const lines = source.split("\n");
|
|
18299
|
+
let changed = false;
|
|
18300
|
+
let inLinksTo = false;
|
|
18301
|
+
for (let i = 0; i < fm.lines.end && i < lines.length; i++) {
|
|
18302
|
+
const line = lines[i];
|
|
18303
|
+
const topKey = line.match(/^([A-Za-z0-9_.-]+):(.*)$/);
|
|
18304
|
+
if (topKey) inLinksTo = topKey[1] === "linksTo";
|
|
18305
|
+
if (!inLinksTo) continue;
|
|
18306
|
+
let next = line;
|
|
18307
|
+
for (const [alias, current] of aliasReplacementsForPage(index, fm.data)) {
|
|
18308
|
+
const re = new RegExp(`(^|[\\s\\[,'"-])${escapeRe(alias)}($|[\\s\\],'"])`, "g");
|
|
18309
|
+
next = next.replace(re, (_m, p1, p2) => `${p1}${current}${p2}`);
|
|
18310
|
+
}
|
|
18311
|
+
if (next !== line) {
|
|
18312
|
+
lines[i] = next;
|
|
18313
|
+
changed = true;
|
|
18314
|
+
}
|
|
18315
|
+
}
|
|
18316
|
+
if (changed) {
|
|
18317
|
+
await io.writeFile(file2, lines.join("\n"));
|
|
18318
|
+
io.stdout(`fixed alias references in ${file2}`);
|
|
18319
|
+
}
|
|
18320
|
+
}
|
|
18321
|
+
}
|
|
18322
|
+
const outPath = values.out ?? join3(process.cwd(), ".contentbit", "link-index.json");
|
|
18323
|
+
await mkdir3(dirname(outPath), { recursive: true });
|
|
18324
|
+
await io.writeFile(outPath, JSON.stringify(serializeLinkIndex(index), null, 2) + "\n");
|
|
18325
|
+
let edges = 0;
|
|
18326
|
+
for (const p of index.pages.values()) edges += p.linksTo.length;
|
|
18327
|
+
const orphans = [...index.pages.values()].filter((p) => p.linkedFrom.length === 0).length;
|
|
18328
|
+
io.stdout(
|
|
18329
|
+
`${index.pages.size} page(s), ${edges} link(s), ${orphans} orphan(s): ${errors} errors, ${warnings} warnings`
|
|
18330
|
+
);
|
|
18331
|
+
io.stdout(`index written to ${outPath}`);
|
|
18332
|
+
return errors > 0 ? 1 : 0;
|
|
18333
|
+
}
|
|
18334
|
+
function escapeRe(s) {
|
|
18335
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
18336
|
+
}
|
|
18337
|
+
var init_links2 = __esm({
|
|
18338
|
+
"src/commands/links.ts"() {
|
|
18339
|
+
"use strict";
|
|
18340
|
+
init_dist();
|
|
18341
|
+
init_link_options();
|
|
18342
|
+
init_links_io();
|
|
18343
|
+
}
|
|
18344
|
+
});
|
|
18345
|
+
|
|
17625
18346
|
// src/run.ts
|
|
17626
|
-
var USAGE = `Usage: contentbit <init|validate|stats|render|instructions|docs|agents> [options]
|
|
18347
|
+
var USAGE = `Usage: contentbit <init|validate|stats|render|instructions|docs|agents|links> [options]
|
|
17627
18348
|
|
|
17628
18349
|
init [-t react|html|markdown|astro] [--md ...] [-y] [--no-install] [--no-page] [--no-agents]
|
|
17629
18350
|
agents [--claude] [--no-agents-md]
|
|
17630
18351
|
|
|
17631
|
-
validate <globs...> [--registry <module.mjs>] [--strict-warnings]
|
|
18352
|
+
validate <globs...> [--registry <module.mjs>] [--strict-warnings] [--link-resolve <mode>]
|
|
17632
18353
|
stats <globs...> [--registry <module.mjs>] [--no-validate]
|
|
17633
18354
|
render <file> --target html|markdown [--registry <module.mjs>] [--out <file>]
|
|
17634
18355
|
instructions [--audience llm|human] [--no-examples] [--registry <module.mjs>] [--out <file>]
|
|
17635
|
-
docs [--registry <module.mjs>] [--out <file>]
|
|
18356
|
+
docs [--registry <module.mjs>] [--out <file>]
|
|
18357
|
+
links <globs...> [--fix] [--out <file>] [--link-resolve <mode>]`;
|
|
17636
18358
|
var commands = {
|
|
17637
18359
|
init: async () => (await Promise.resolve().then(() => (init_init(), init_exports))).initCommand,
|
|
17638
18360
|
validate: async () => (await Promise.resolve().then(() => (init_validate2(), validate_exports))).validateCommand,
|
|
@@ -17640,7 +18362,8 @@ var commands = {
|
|
|
17640
18362
|
render: async () => (await Promise.resolve().then(() => (init_render2(), render_exports))).renderCommand,
|
|
17641
18363
|
instructions: async () => (await Promise.resolve().then(() => (init_instructions(), instructions_exports))).instructionsCommand,
|
|
17642
18364
|
docs: async () => (await Promise.resolve().then(() => (init_docs(), docs_exports))).docsCommand,
|
|
17643
|
-
agents: async () => (await Promise.resolve().then(() => (init_agents(), agents_exports))).agentsCommand
|
|
18365
|
+
agents: async () => (await Promise.resolve().then(() => (init_agents(), agents_exports))).agentsCommand,
|
|
18366
|
+
links: async () => (await Promise.resolve().then(() => (init_links2(), links_exports))).linksCommand
|
|
17644
18367
|
};
|
|
17645
18368
|
async function run(argv, io) {
|
|
17646
18369
|
const [name, ...rest] = argv;
|