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