kitfly 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +13 -11
- package/VERSION +1 -1
- package/dist/_raw/content/reference/gantt-widget.md +468 -0
- package/dist/_raw/content/reference/plugins.md +157 -2
- package/dist/content/deployment/preflight.html +5 -6
- package/dist/content/deployment/recipes/aws-s3.html +5 -6
- package/dist/content/deployment/recipes/cloudflare-pages.html +5 -6
- package/dist/content/deployment/recipes/cloudflare-r2.html +5 -6
- package/dist/content/deployment/recipes/fly-io.html +5 -6
- package/dist/content/deployment/recipes/github-pages.html +5 -6
- package/dist/content/deployment/recipes/netlify.html +5 -6
- package/dist/content/deployment/recipes/vercel.html +5 -6
- package/dist/content/deployment/secrets-and-env-vars.html +5 -6
- package/dist/content/deployment.html +5 -6
- package/dist/content/guide/approaches.html +5 -6
- package/dist/content/guide/branding.html +5 -6
- package/dist/content/guide/data-driven-content.html +5 -6
- package/dist/content/guide/features.html +5 -6
- package/dist/content/guide/getting-started.html +5 -6
- package/dist/content/guide/kitfly-overview.html +5 -6
- package/dist/content/reference/configuration.html +5 -6
- package/dist/content/reference/design-catalog.html +5 -6
- package/dist/content/reference/environment-variables.html +5 -6
- package/dist/content/reference/gantt-widget.html +899 -0
- package/dist/content/reference/glossary.html +5 -6
- package/dist/content/reference/key-concepts.html +5 -6
- package/dist/content/reference/plugins.html +245 -9
- package/dist/content/reference/slides-authoring-guidelines.html +5 -6
- package/dist/content/reference/structure.html +5 -6
- package/dist/content/reference.html +5 -6
- package/dist/content/templates/crucible.html +5 -6
- package/dist/content/templates/handbook.html +5 -6
- package/dist/content/templates/minimal.html +5 -6
- package/dist/content/templates/overview.html +5 -6
- package/dist/content/templates/pipeline.html +5 -6
- package/dist/content/templates/productbook.html +5 -6
- package/dist/content/templates/runbook.html +5 -6
- package/dist/content/templates/servicebook.html +5 -6
- package/dist/content-index.json +10 -2
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +5 -6
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +5 -6
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +5 -6
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +5 -6
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +5 -6
- package/dist/docs/decisions/ADR-0006-data-driven-content.html +5 -6
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +5 -6
- package/dist/docs/decisions/DDR-0002-theme-system.html +5 -6
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +5 -6
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +5 -6
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +5 -6
- package/dist/docs/userguide/cli/build.html +5 -6
- package/dist/docs/userguide/cli/bundle.html +5 -6
- package/dist/docs/userguide/cli/dev.html +5 -6
- package/dist/docs/userguide/cli/init.html +5 -6
- package/dist/docs/userguide/cli/servers.html +5 -6
- package/dist/docs/userguide/cli/stop.html +5 -6
- package/dist/docs/userguide/cli/update.html +5 -6
- package/dist/docs/userguide/cli/version.html +5 -6
- package/dist/docs/userguide/cli.html +5 -6
- package/dist/docs/userguide/sharing.html +5 -6
- package/dist/index.html +5 -6
- package/dist/llms.txt +3 -3
- package/dist/provenance.json +4 -5
- package/dist/reports/license-inventory.csv +199 -0
- package/dist/schemas/plugin-registry.schema.html +5 -6
- package/dist/schemas/plugin-schemas-notes.html +5 -6
- package/dist/schemas/plugin.schema.html +5 -6
- package/dist/schemas/plugins.schema.html +5 -6
- package/dist/schemas/v0/common.schema.html +5 -6
- package/dist/schemas/v0/plugin-registry.schema.html +5 -6
- package/dist/schemas/v0/plugin.schema.html +5 -6
- package/dist/schemas/v0/plugins.schema.html +5 -6
- package/dist/schemas/v0/site.schema.html +5 -6
- package/dist/schemas/v0/theme.schema.html +5 -6
- package/dist/schemas.html +5 -6
- package/package.json +1 -1
- package/plugins-dist/planning-visuals.css +261 -0
- package/plugins-dist/planning-visuals.js +669 -0
- package/registry/plugins.yaml +15 -1
- package/scripts/build-all.ts +5 -0
- package/scripts/build.ts +73 -11
- package/scripts/bundle.ts +73 -10
- package/scripts/dev.ts +49 -5
- package/scripts/embed-docs.ts +119 -0
- package/src/__tests__/build.test.ts +124 -0
- package/src/__tests__/bundle.test.ts +61 -0
- package/src/__tests__/docs.test.ts +117 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/bad-month-format.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/marker-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/milestone-format-mismatch.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/missing-tracks.md +5 -0
- package/src/__tests__/fixtures/fences/planning-visuals/invalid/track-reversed.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-basic.md +15 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-no-milestones.md +13 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/month-basic.md +16 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/no-milestones.md +10 -0
- package/src/__tests__/fixtures/fences/planning-visuals/valid/week-basic.md +20 -0
- package/src/__tests__/planning-visuals-fence-contract.test.ts +28 -0
- package/src/__tests__/planning-visuals-runtime-regressions.bun.test.ts +68 -0
- package/src/__tests__/planning-visuals-runtime.bun.test.ts +192 -0
- package/src/__tests__/shared.test.ts +121 -0
- package/src/cli.ts +113 -18
- package/src/commands/docs.ts +71 -0
- package/src/generated/embedded-docs.ts +2384 -0
- package/src/server-registry.ts +50 -10
- package/src/shared.ts +449 -25
package/scripts/build-all.ts
CHANGED
|
@@ -70,6 +70,11 @@ function getVersion(): string {
|
|
|
70
70
|
function main(): void {
|
|
71
71
|
const version = getVersion();
|
|
72
72
|
|
|
73
|
+
// Generate embedded docs before compiling binaries
|
|
74
|
+
console.log("Generating embedded documentation...");
|
|
75
|
+
execSync("bun scripts/embed-docs.ts", { stdio: "inherit" });
|
|
76
|
+
console.log();
|
|
77
|
+
|
|
73
78
|
console.log(`Building ${TARGETS.length} binaries for '${BINARY_NAME}' v${version}`);
|
|
74
79
|
console.log(` Entry point: ${ENTRY_POINT}`);
|
|
75
80
|
console.log(` Output dir: ${OUT_DIR}`);
|
package/scripts/build.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type ContentFile,
|
|
30
30
|
collectFiles,
|
|
31
31
|
// Navigation/template building
|
|
32
|
+
collectPlanningVisualsContainmentWarnings,
|
|
32
33
|
collectSlides,
|
|
33
34
|
envBool,
|
|
34
35
|
// Config helpers
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
// File utilities
|
|
39
40
|
exists,
|
|
40
41
|
filterByProfile,
|
|
42
|
+
filterUnknownPlanningVisualsTypeDiagnostics,
|
|
41
43
|
filterUnknownSlidesVisualsTypeDiagnostics,
|
|
42
44
|
// Provenance
|
|
43
45
|
generateProvenance,
|
|
@@ -59,6 +61,7 @@ import {
|
|
|
59
61
|
type SiteConfig,
|
|
60
62
|
slugify,
|
|
61
63
|
validatePath,
|
|
64
|
+
validatePlanningVisualsFences,
|
|
62
65
|
validateSlidesVisualsFences,
|
|
63
66
|
} from "../src/shared.ts";
|
|
64
67
|
import { generateThemeCSS, getPrismUrls, loadTheme, type Theme } from "../src/theme.ts";
|
|
@@ -154,6 +157,27 @@ async function resolveSiteAssetsDir(siteRoot: string): Promise<string | null> {
|
|
|
154
157
|
return null;
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
type FenceValidationFlags = {
|
|
161
|
+
slidesVisuals: boolean;
|
|
162
|
+
planningVisuals: boolean;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
async function getFenceValidationFlags(root: string): Promise<FenceValidationFlags> {
|
|
166
|
+
try {
|
|
167
|
+
const raw = await readFile(join(root, "kitfly.plugins.yaml"), "utf-8");
|
|
168
|
+
const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
|
|
169
|
+
const enabled = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
|
|
170
|
+
return {
|
|
171
|
+
slidesVisuals: enabled.some((p) => typeof p === "string" && p.startsWith("slides-visuals@")),
|
|
172
|
+
planningVisuals: enabled.some(
|
|
173
|
+
(p) => typeof p === "string" && p.startsWith("planning-visuals@"),
|
|
174
|
+
),
|
|
175
|
+
};
|
|
176
|
+
} catch {
|
|
177
|
+
return { slidesVisuals: false, planningVisuals: false };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
157
181
|
function computePathPrefix(urlKey: string): string {
|
|
158
182
|
const clean = urlKey.replace(/^\/+/, "").replace(/\.html$/, "");
|
|
159
183
|
if (!clean) return "./";
|
|
@@ -217,6 +241,7 @@ async function renderFile(
|
|
|
217
241
|
config: SiteConfig,
|
|
218
242
|
theme: Theme,
|
|
219
243
|
plugins: PluginInjections,
|
|
244
|
+
fenceValidation: FenceValidationFlags,
|
|
220
245
|
): Promise<string> {
|
|
221
246
|
const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
|
|
222
247
|
const content = await readFile(filePath, "utf-8");
|
|
@@ -242,6 +267,22 @@ async function renderFile(
|
|
|
242
267
|
title = frontmatter.title as string;
|
|
243
268
|
}
|
|
244
269
|
pageMeta = buildPageMeta(frontmatter);
|
|
270
|
+
if (fenceValidation.planningVisuals) {
|
|
271
|
+
const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
|
|
272
|
+
validatePlanningVisualsFences(body),
|
|
273
|
+
);
|
|
274
|
+
if (diagnostics.length) {
|
|
275
|
+
const msg = diagnostics
|
|
276
|
+
.slice(0, 12)
|
|
277
|
+
.map((d) => ` - ${filePath}:${d.line} ${d.message}`)
|
|
278
|
+
.join("\n");
|
|
279
|
+
throw new Error(`planning-visuals fence contract violations:\n${msg}`);
|
|
280
|
+
}
|
|
281
|
+
const warnings = collectPlanningVisualsContainmentWarnings(body);
|
|
282
|
+
for (const warning of warnings.slice(0, 12)) {
|
|
283
|
+
console.warn(` ⚠ ${filePath}:${warning.line} ${warning.message}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
245
286
|
htmlContent = marked.parse(body) as string;
|
|
246
287
|
}
|
|
247
288
|
|
|
@@ -394,26 +435,18 @@ async function renderSlidesIndex(
|
|
|
394
435
|
config: SiteConfig,
|
|
395
436
|
theme: Theme,
|
|
396
437
|
plugins: PluginInjections,
|
|
438
|
+
fenceValidation: FenceValidationFlags,
|
|
397
439
|
): Promise<string> {
|
|
398
440
|
const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
|
|
399
441
|
const pathPrefix = "./";
|
|
400
442
|
const slides = await collectSlides(files, {
|
|
401
443
|
markdownTransform: (raw, file) => applyDataBindingsForSlides(raw, file.path, config),
|
|
402
444
|
});
|
|
403
|
-
let validateFences = false;
|
|
404
|
-
try {
|
|
405
|
-
const raw = await readFile(join(ROOT, "kitfly.plugins.yaml"), "utf-8");
|
|
406
|
-
const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
|
|
407
|
-
const enabled = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
|
|
408
|
-
validateFences = enabled.some((p) => typeof p === "string" && p.startsWith("slides-visuals@"));
|
|
409
|
-
} catch {
|
|
410
|
-
// no config, skip
|
|
411
|
-
}
|
|
412
445
|
const renderedSlides = await Promise.all(
|
|
413
446
|
slides.map(async (slide, i) => {
|
|
414
447
|
let inner = "";
|
|
415
448
|
if (slide.kind === "markdown") {
|
|
416
|
-
if (
|
|
449
|
+
if (fenceValidation.slidesVisuals) {
|
|
417
450
|
const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
|
|
418
451
|
validateSlidesVisualsFences(slide.body),
|
|
419
452
|
);
|
|
@@ -425,6 +458,22 @@ async function renderSlidesIndex(
|
|
|
425
458
|
throw new Error(`slides-visuals fence contract violations:\n${msg}`);
|
|
426
459
|
}
|
|
427
460
|
}
|
|
461
|
+
if (fenceValidation.planningVisuals) {
|
|
462
|
+
const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
|
|
463
|
+
validatePlanningVisualsFences(slide.body),
|
|
464
|
+
);
|
|
465
|
+
if (diagnostics.length) {
|
|
466
|
+
const msg = diagnostics
|
|
467
|
+
.slice(0, 12)
|
|
468
|
+
.map((d) => ` - ${slide.sourcePath}:${d.line} ${d.message}`)
|
|
469
|
+
.join("\n");
|
|
470
|
+
throw new Error(`planning-visuals fence contract violations:\n${msg}`);
|
|
471
|
+
}
|
|
472
|
+
const warnings = collectPlanningVisualsContainmentWarnings(slide.body);
|
|
473
|
+
for (const warning of warnings.slice(0, 12)) {
|
|
474
|
+
console.warn(` ⚠ ${slide.sourcePath}:${warning.line} ${warning.message}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
428
477
|
inner = marked.parse(slide.body) as string;
|
|
429
478
|
} else if (slide.kind === "yaml") {
|
|
430
479
|
inner = `<pre><code class="language-yaml">${escapeHtml(slide.body)}</code></pre>`;
|
|
@@ -581,6 +630,7 @@ async function buildSite() {
|
|
|
581
630
|
root: ROOT,
|
|
582
631
|
mode: config.mode === "slides" ? "slides" : "docs",
|
|
583
632
|
});
|
|
633
|
+
const fenceValidation = await getFenceValidationFlags(ROOT);
|
|
584
634
|
|
|
585
635
|
// Copy CSS
|
|
586
636
|
const css = await readFile(await resolveStylesPath(ROOT), "utf-8");
|
|
@@ -631,7 +681,15 @@ async function buildSite() {
|
|
|
631
681
|
}
|
|
632
682
|
|
|
633
683
|
if (config.mode === "slides") {
|
|
634
|
-
const html = await renderSlidesIndex(
|
|
684
|
+
const html = await renderSlidesIndex(
|
|
685
|
+
template,
|
|
686
|
+
files,
|
|
687
|
+
provenance,
|
|
688
|
+
config,
|
|
689
|
+
theme,
|
|
690
|
+
plugins,
|
|
691
|
+
fenceValidation,
|
|
692
|
+
);
|
|
635
693
|
await writeFile(join(DIST, "index.html"), html);
|
|
636
694
|
console.log(` ✓ index.html (slides mode, ${files.length} source files)`);
|
|
637
695
|
await generateAIAccessibility(DIST, files, config, provenance);
|
|
@@ -650,6 +708,7 @@ async function buildSite() {
|
|
|
650
708
|
config,
|
|
651
709
|
theme,
|
|
652
710
|
plugins,
|
|
711
|
+
fenceValidation,
|
|
653
712
|
);
|
|
654
713
|
|
|
655
714
|
// Create output path
|
|
@@ -676,6 +735,7 @@ async function buildSite() {
|
|
|
676
735
|
config,
|
|
677
736
|
theme,
|
|
678
737
|
plugins,
|
|
738
|
+
fenceValidation,
|
|
679
739
|
);
|
|
680
740
|
await writeFile(join(DIST, "index.html"), homeHtml);
|
|
681
741
|
console.log(` ✓ index.html (from ${config.home})`);
|
|
@@ -691,6 +751,7 @@ async function buildSite() {
|
|
|
691
751
|
config,
|
|
692
752
|
theme,
|
|
693
753
|
plugins,
|
|
754
|
+
fenceValidation,
|
|
694
755
|
);
|
|
695
756
|
await writeFile(join(DIST, "index.html"), indexHtml);
|
|
696
757
|
console.log(" ✓ index.html");
|
|
@@ -708,6 +769,7 @@ async function buildSite() {
|
|
|
708
769
|
config,
|
|
709
770
|
theme,
|
|
710
771
|
plugins,
|
|
772
|
+
fenceValidation,
|
|
711
773
|
);
|
|
712
774
|
await writeFile(join(DIST, "index.html"), indexHtml);
|
|
713
775
|
console.log(" ✓ index.html");
|
package/scripts/bundle.ts
CHANGED
|
@@ -26,9 +26,10 @@ import {
|
|
|
26
26
|
buildSectionNav,
|
|
27
27
|
// Navigation/template building
|
|
28
28
|
buildSlideNavHierarchical,
|
|
29
|
-
// Types
|
|
30
29
|
type ContentFile,
|
|
31
30
|
collectFiles,
|
|
31
|
+
// Types
|
|
32
|
+
collectPlanningVisualsContainmentWarnings,
|
|
32
33
|
collectSlides,
|
|
33
34
|
envBool,
|
|
34
35
|
// Config helpers
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
// Formatting
|
|
37
38
|
escapeHtml,
|
|
38
39
|
filterByProfile,
|
|
40
|
+
filterUnknownPlanningVisualsTypeDiagnostics,
|
|
39
41
|
filterUnknownSlidesVisualsTypeDiagnostics,
|
|
40
42
|
// YAML/Config parsing
|
|
41
43
|
loadDataBindings,
|
|
@@ -52,6 +54,7 @@ import {
|
|
|
52
54
|
type SiteConfig,
|
|
53
55
|
slugify,
|
|
54
56
|
validatePath,
|
|
57
|
+
validatePlanningVisualsFences,
|
|
55
58
|
validateSlidesVisualsFences,
|
|
56
59
|
} from "../src/shared.ts";
|
|
57
60
|
import { generateThemeCSS, getPrismUrls, loadTheme } from "../src/theme.ts";
|
|
@@ -354,24 +357,35 @@ function buildBundleNav(files: ContentFile[], config: SiteConfig): string {
|
|
|
354
357
|
return html;
|
|
355
358
|
}
|
|
356
359
|
|
|
357
|
-
async function
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
let validateFences = false;
|
|
360
|
+
async function getFenceValidationFlags(root: string): Promise<{
|
|
361
|
+
slidesVisuals: boolean;
|
|
362
|
+
planningVisuals: boolean;
|
|
363
|
+
}> {
|
|
362
364
|
try {
|
|
363
|
-
const raw = await readFile(join(
|
|
365
|
+
const raw = await readFile(join(root, "kitfly.plugins.yaml"), "utf-8");
|
|
364
366
|
const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
|
|
365
367
|
const enabled = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
|
|
366
|
-
|
|
368
|
+
return {
|
|
369
|
+
slidesVisuals: enabled.some((p) => typeof p === "string" && p.startsWith("slides-visuals@")),
|
|
370
|
+
planningVisuals: enabled.some(
|
|
371
|
+
(p) => typeof p === "string" && p.startsWith("planning-visuals@"),
|
|
372
|
+
),
|
|
373
|
+
};
|
|
367
374
|
} catch {
|
|
368
|
-
|
|
375
|
+
return { slidesVisuals: false, planningVisuals: false };
|
|
369
376
|
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function buildSlidesBundleContent(files: ContentFile[], config: SiteConfig): Promise<string> {
|
|
380
|
+
const slides = await collectSlides(files, {
|
|
381
|
+
markdownTransform: (raw, file) => applyDataBindingsForSlides(raw, file.path, config),
|
|
382
|
+
});
|
|
383
|
+
const fenceValidation = await getFenceValidationFlags(ROOT);
|
|
370
384
|
const renderedSlides = await Promise.all(
|
|
371
385
|
slides.map(async (slide, i) => {
|
|
372
386
|
let inner = "";
|
|
373
387
|
if (slide.kind === "markdown") {
|
|
374
|
-
if (
|
|
388
|
+
if (fenceValidation.slidesVisuals) {
|
|
375
389
|
const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
|
|
376
390
|
validateSlidesVisualsFences(slide.body),
|
|
377
391
|
);
|
|
@@ -383,6 +397,22 @@ async function buildSlidesBundleContent(files: ContentFile[], config: SiteConfig
|
|
|
383
397
|
throw new Error(`slides-visuals fence contract violations:\n${msg}`);
|
|
384
398
|
}
|
|
385
399
|
}
|
|
400
|
+
if (fenceValidation.planningVisuals) {
|
|
401
|
+
const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
|
|
402
|
+
validatePlanningVisualsFences(slide.body),
|
|
403
|
+
);
|
|
404
|
+
if (diagnostics.length) {
|
|
405
|
+
const msg = diagnostics
|
|
406
|
+
.slice(0, 12)
|
|
407
|
+
.map((d) => ` - ${slide.sourcePath}:${d.line} ${d.message}`)
|
|
408
|
+
.join("\n");
|
|
409
|
+
throw new Error(`planning-visuals fence contract violations:\n${msg}`);
|
|
410
|
+
}
|
|
411
|
+
const warnings = collectPlanningVisualsContainmentWarnings(slide.body);
|
|
412
|
+
for (const warning of warnings.slice(0, 12)) {
|
|
413
|
+
console.warn(` ⚠ ${slide.sourcePath}:${warning.line} ${warning.message}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
386
416
|
inner = marked.parse(slide.body) as string;
|
|
387
417
|
} else if (slide.kind === "yaml") {
|
|
388
418
|
inner = `<pre><code class="language-yaml">${escapeHtml(slide.body)}</code></pre>`;
|
|
@@ -602,6 +632,7 @@ async function bundle() {
|
|
|
602
632
|
navHtml = buildSlideNavHierarchical(slides, config, "slide-1");
|
|
603
633
|
contentHtml = await buildSlidesBundleContent(files, config);
|
|
604
634
|
} else {
|
|
635
|
+
const fenceValidation = await getFenceValidationFlags(ROOT);
|
|
605
636
|
// Build navigation and content sections
|
|
606
637
|
const sections: Map<string, { id: string; title: string; html: string }[]> = new Map();
|
|
607
638
|
|
|
@@ -617,6 +648,22 @@ async function bundle() {
|
|
|
617
648
|
homePath,
|
|
618
649
|
config,
|
|
619
650
|
);
|
|
651
|
+
if (fenceValidation.planningVisuals) {
|
|
652
|
+
const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
|
|
653
|
+
validatePlanningVisualsFences(body),
|
|
654
|
+
);
|
|
655
|
+
if (diagnostics.length) {
|
|
656
|
+
const msg = diagnostics
|
|
657
|
+
.slice(0, 12)
|
|
658
|
+
.map((d) => ` - ${homePath}:${d.line} ${d.message}`)
|
|
659
|
+
.join("\n");
|
|
660
|
+
throw new Error(`planning-visuals fence contract violations:\n${msg}`);
|
|
661
|
+
}
|
|
662
|
+
const warnings = collectPlanningVisualsContainmentWarnings(body);
|
|
663
|
+
for (const warning of warnings.slice(0, 12)) {
|
|
664
|
+
console.warn(` ⚠ ${homePath}:${warning.line} ${warning.message}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
620
667
|
const title = (frontmatter.title as string) || "Home";
|
|
621
668
|
let htmlContent = marked.parse(body) as string;
|
|
622
669
|
htmlContent = await inlineLocalImages(htmlContent, config);
|
|
@@ -654,6 +701,22 @@ async function bundle() {
|
|
|
654
701
|
if (frontmatter.description) {
|
|
655
702
|
description = frontmatter.description as string;
|
|
656
703
|
}
|
|
704
|
+
if (fenceValidation.planningVisuals) {
|
|
705
|
+
const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
|
|
706
|
+
validatePlanningVisualsFences(body),
|
|
707
|
+
);
|
|
708
|
+
if (diagnostics.length) {
|
|
709
|
+
const msg = diagnostics
|
|
710
|
+
.slice(0, 12)
|
|
711
|
+
.map((d) => ` - ${file.path}:${d.line} ${d.message}`)
|
|
712
|
+
.join("\n");
|
|
713
|
+
throw new Error(`planning-visuals fence contract violations:\n${msg}`);
|
|
714
|
+
}
|
|
715
|
+
const warnings = collectPlanningVisualsContainmentWarnings(body);
|
|
716
|
+
for (const warning of warnings.slice(0, 12)) {
|
|
717
|
+
console.warn(` ⚠ ${file.path}:${warning.line} ${warning.message}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
657
720
|
htmlContent = marked.parse(body) as string;
|
|
658
721
|
|
|
659
722
|
// Collect raw markdown for AI accessibility
|
package/scripts/dev.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
checkPortOrExit,
|
|
41
41
|
// Navigation/template building
|
|
42
42
|
collectFiles,
|
|
43
|
+
collectPlanningVisualsContainmentWarnings,
|
|
43
44
|
collectSlides,
|
|
44
45
|
envBool,
|
|
45
46
|
envInt,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
// Formatting
|
|
49
50
|
escapeHtml,
|
|
50
51
|
filterByProfile,
|
|
52
|
+
filterUnknownPlanningVisualsTypeDiagnostics,
|
|
51
53
|
filterUnknownSlidesVisualsTypeDiagnostics,
|
|
52
54
|
// Provenance
|
|
53
55
|
generateProvenance,
|
|
@@ -68,6 +70,7 @@ import {
|
|
|
68
70
|
type SiteConfig,
|
|
69
71
|
slugify,
|
|
70
72
|
validatePath,
|
|
73
|
+
validatePlanningVisualsFences,
|
|
71
74
|
validateSlidesVisualsFences,
|
|
72
75
|
} from "../src/shared.ts";
|
|
73
76
|
import { generateThemeCSS, getPrismUrls, loadTheme, type Theme } from "../src/theme.ts";
|
|
@@ -302,15 +305,23 @@ const clients: Set<ReadableStreamDefaultController> = new Set();
|
|
|
302
305
|
|
|
303
306
|
let pluginCache: { key: string; head: string; bodyEnd: string } | null = null;
|
|
304
307
|
|
|
305
|
-
async function
|
|
308
|
+
async function getFenceValidationFlags(): Promise<{
|
|
309
|
+
slidesVisuals: boolean;
|
|
310
|
+
planningVisuals: boolean;
|
|
311
|
+
}> {
|
|
306
312
|
const configPath = join(ROOT, "kitfly.plugins.yaml");
|
|
307
313
|
try {
|
|
308
314
|
const raw = await readFile(configPath, "utf-8");
|
|
309
315
|
const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
|
|
310
316
|
const plugins = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
|
|
311
|
-
return
|
|
317
|
+
return {
|
|
318
|
+
slidesVisuals: plugins.some((p) => typeof p === "string" && p.startsWith("slides-visuals@")),
|
|
319
|
+
planningVisuals: plugins.some(
|
|
320
|
+
(p) => typeof p === "string" && p.startsWith("planning-visuals@"),
|
|
321
|
+
),
|
|
322
|
+
};
|
|
312
323
|
} catch {
|
|
313
|
-
return false;
|
|
324
|
+
return { slidesVisuals: false, planningVisuals: false };
|
|
314
325
|
}
|
|
315
326
|
}
|
|
316
327
|
|
|
@@ -400,6 +411,23 @@ async function renderPage(
|
|
|
400
411
|
title = frontmatter.title as string;
|
|
401
412
|
}
|
|
402
413
|
pageMeta = buildPageMeta(frontmatter);
|
|
414
|
+
const fenceValidation = await getFenceValidationFlags();
|
|
415
|
+
if (fenceValidation.planningVisuals) {
|
|
416
|
+
const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
|
|
417
|
+
validatePlanningVisualsFences(body),
|
|
418
|
+
);
|
|
419
|
+
if (diagnostics.length) {
|
|
420
|
+
const msg = diagnostics
|
|
421
|
+
.slice(0, 12)
|
|
422
|
+
.map((d) => ` - ${filePath}:${d.line} ${d.message}`)
|
|
423
|
+
.join("\n");
|
|
424
|
+
throw new Error(`planning-visuals fence contract violations:\n${msg}`);
|
|
425
|
+
}
|
|
426
|
+
const warnings = collectPlanningVisualsContainmentWarnings(body);
|
|
427
|
+
for (const warning of warnings.slice(0, 12)) {
|
|
428
|
+
logWarn(`${filePath}:${warning.line} ${warning.message}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
403
431
|
htmlContent = marked.parse(body) as string;
|
|
404
432
|
}
|
|
405
433
|
|
|
@@ -487,13 +515,13 @@ async function renderSlidesPage(
|
|
|
487
515
|
return renderGettingStarted(provenance, config, theme);
|
|
488
516
|
}
|
|
489
517
|
const pathPrefix = "/";
|
|
490
|
-
const
|
|
518
|
+
const fenceValidation = await getFenceValidationFlags();
|
|
491
519
|
|
|
492
520
|
const sections = await Promise.all(
|
|
493
521
|
slides.map(async (slide, i) => {
|
|
494
522
|
let inner = "";
|
|
495
523
|
if (slide.kind === "markdown") {
|
|
496
|
-
if (
|
|
524
|
+
if (fenceValidation.slidesVisuals) {
|
|
497
525
|
const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
|
|
498
526
|
validateSlidesVisualsFences(slide.body),
|
|
499
527
|
);
|
|
@@ -505,6 +533,22 @@ async function renderSlidesPage(
|
|
|
505
533
|
throw new Error(`slides-visuals fence contract violations:\n${msg}`);
|
|
506
534
|
}
|
|
507
535
|
}
|
|
536
|
+
if (fenceValidation.planningVisuals) {
|
|
537
|
+
const diagnostics = filterUnknownPlanningVisualsTypeDiagnostics(
|
|
538
|
+
validatePlanningVisualsFences(slide.body),
|
|
539
|
+
);
|
|
540
|
+
if (diagnostics.length) {
|
|
541
|
+
const msg = diagnostics
|
|
542
|
+
.slice(0, 12)
|
|
543
|
+
.map((d) => ` - ${slide.sourcePath}:${d.line} ${d.message}`)
|
|
544
|
+
.join("\n");
|
|
545
|
+
throw new Error(`planning-visuals fence contract violations:\n${msg}`);
|
|
546
|
+
}
|
|
547
|
+
const warnings = collectPlanningVisualsContainmentWarnings(slide.body);
|
|
548
|
+
for (const warning of warnings.slice(0, 12)) {
|
|
549
|
+
logWarn(`${slide.sourcePath}:${warning.line} ${warning.message}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
508
552
|
inner = marked.parse(slide.body) as string;
|
|
509
553
|
} else if (slide.kind === "yaml") {
|
|
510
554
|
inner = `<pre><code class="language-yaml">${escapeHtml(slide.body)}</code></pre>`;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build-time codegen: reads docs/embed-manifest.yaml, resolves globs,
|
|
5
|
+
* and generates src/generated/embedded-docs.ts with all matched content.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* bun scripts/embed-docs.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { basename, dirname } from "node:path";
|
|
13
|
+
import { Glob } from "bun";
|
|
14
|
+
import { parseFrontmatter, parseYaml } from "../src/shared.ts";
|
|
15
|
+
|
|
16
|
+
const MANIFEST_PATH = "docs/embed-manifest.yaml";
|
|
17
|
+
const OUTPUT_PATH = "src/generated/embedded-docs.ts";
|
|
18
|
+
|
|
19
|
+
function toSlug(filePath: string): string {
|
|
20
|
+
// Normalize path separators first (Windows compat: Bun.Glob returns backslashes)
|
|
21
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
22
|
+
// Strip first segment (docs/ or content/) and .md extension
|
|
23
|
+
const withoutPrefix = normalized.replace(/^[^/]+\//, "");
|
|
24
|
+
const withoutExt = withoutPrefix.replace(/\.md$/, "");
|
|
25
|
+
const base = basename(withoutExt).toLowerCase();
|
|
26
|
+
if (base === "readme" || base === "index") {
|
|
27
|
+
return dirname(withoutPrefix).replace(/\\/g, "/");
|
|
28
|
+
}
|
|
29
|
+
return withoutExt;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractTitle(body: string): string {
|
|
33
|
+
for (const line of body.split("\n")) {
|
|
34
|
+
const m = line.match(/^#\s+(.+)/);
|
|
35
|
+
if (m) return m[1].trim();
|
|
36
|
+
}
|
|
37
|
+
return "(untitled)";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function resolveGlobs(includes: string[], excludes: string[]): Promise<string[]> {
|
|
41
|
+
const matched = new Set<string>();
|
|
42
|
+
|
|
43
|
+
for (const pattern of includes) {
|
|
44
|
+
const glob = new Glob(pattern);
|
|
45
|
+
for await (const path of glob.scan({ cwd: ".", dot: false })) {
|
|
46
|
+
matched.add(path);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const pattern of excludes) {
|
|
51
|
+
const glob = new Glob(pattern);
|
|
52
|
+
for await (const path of glob.scan({ cwd: ".", dot: false })) {
|
|
53
|
+
matched.delete(path);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [...matched].sort();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function escapeForTemplate(s: string): string {
|
|
61
|
+
return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function main(): Promise<void> {
|
|
65
|
+
const raw = readFileSync(MANIFEST_PATH, "utf-8");
|
|
66
|
+
const manifest = parseYaml(raw);
|
|
67
|
+
const includes = manifest.include as string[];
|
|
68
|
+
const excludes = (manifest.exclude as string[]) ?? [];
|
|
69
|
+
|
|
70
|
+
if (!includes?.length) {
|
|
71
|
+
console.error(`Error: no include patterns in ${MANIFEST_PATH}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const files = await resolveGlobs(includes, excludes);
|
|
76
|
+
|
|
77
|
+
if (files.length === 0) {
|
|
78
|
+
console.error("Error: no files matched the manifest patterns");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const entries: Array<[string, string, string]> = [];
|
|
83
|
+
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
const content = readFileSync(file, "utf-8");
|
|
86
|
+
const { body } = parseFrontmatter(content);
|
|
87
|
+
const trimmed = body.trim();
|
|
88
|
+
const slug = toSlug(file);
|
|
89
|
+
const title = extractTitle(trimmed);
|
|
90
|
+
entries.push([slug, title, trimmed]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Sort by slug for deterministic output
|
|
94
|
+
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
95
|
+
|
|
96
|
+
const lines = [
|
|
97
|
+
"// AUTO-GENERATED by scripts/embed-docs.ts — do not edit",
|
|
98
|
+
"export const EMBEDDED_DOCS: ReadonlyArray<readonly [string, string, string]> = [",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
for (const [slug, title, content] of entries) {
|
|
102
|
+
lines.push(
|
|
103
|
+
`\t[${JSON.stringify(slug)}, ${JSON.stringify(title)}, \`${escapeForTemplate(content)}\`],`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
lines.push("];");
|
|
108
|
+
lines.push("");
|
|
109
|
+
|
|
110
|
+
mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
|
|
111
|
+
writeFileSync(OUTPUT_PATH, lines.join("\n"));
|
|
112
|
+
|
|
113
|
+
console.log(`Embedded ${entries.length} docs into ${OUTPUT_PATH}`);
|
|
114
|
+
for (const [slug, title] of entries) {
|
|
115
|
+
console.log(` ${slug} — ${title}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
main();
|