kitfly 0.2.1 → 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 +79 -0
- package/README.md +38 -21
- package/VERSION +1 -1
- package/dist/_raw/content/guide/branding.md +146 -0
- package/dist/_raw/content/guide/data-driven-content.md +204 -0
- package/dist/_raw/content/reference/configuration.md +145 -7
- package/dist/_raw/content/reference/environment-variables.md +26 -1
- package/dist/_raw/content/reference/gantt-widget.md +468 -0
- package/dist/_raw/content/reference/glossary.md +25 -1
- package/dist/_raw/content/reference/key-concepts.md +30 -2
- package/dist/_raw/content/reference/plugins.md +170 -1
- package/dist/_raw/docs/decisions/ADR-0006-data-driven-content.md +350 -0
- package/dist/content/deployment/preflight.html +11 -8
- package/dist/content/deployment/recipes/aws-s3.html +11 -8
- package/dist/content/deployment/recipes/cloudflare-pages.html +11 -8
- package/dist/content/deployment/recipes/cloudflare-r2.html +11 -8
- package/dist/content/deployment/recipes/fly-io.html +11 -8
- package/dist/content/deployment/recipes/github-pages.html +11 -8
- package/dist/content/deployment/recipes/netlify.html +11 -8
- package/dist/content/deployment/recipes/vercel.html +11 -8
- package/dist/content/deployment/secrets-and-env-vars.html +11 -8
- package/dist/content/deployment.html +11 -8
- package/dist/content/guide/approaches.html +11 -8
- package/dist/content/guide/branding.html +509 -0
- package/dist/content/guide/data-driven-content.html +542 -0
- package/dist/content/guide/features.html +11 -8
- package/dist/content/guide/getting-started.html +11 -8
- package/dist/content/guide/kitfly-overview.html +11 -8
- package/dist/content/reference/configuration.html +136 -11
- package/dist/content/reference/design-catalog.html +11 -8
- package/dist/content/reference/environment-variables.html +51 -10
- package/dist/content/reference/gantt-widget.html +899 -0
- package/dist/content/reference/glossary.html +25 -10
- package/dist/content/reference/key-concepts.html +34 -11
- package/dist/content/reference/plugins.html +261 -10
- package/dist/content/reference/slides-authoring-guidelines.html +11 -8
- package/dist/content/reference/structure.html +11 -8
- package/dist/content/reference.html +11 -8
- package/dist/content/templates/crucible.html +11 -8
- package/dist/content/templates/handbook.html +11 -8
- package/dist/content/templates/minimal.html +11 -8
- package/dist/content/templates/overview.html +11 -8
- package/dist/content/templates/pipeline.html +11 -8
- package/dist/content/templates/productbook.html +11 -8
- package/dist/content/templates/runbook.html +11 -8
- package/dist/content/templates/servicebook.html +11 -8
- package/dist/content-index.json +37 -2
- package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +11 -8
- package/dist/docs/decisions/ADR-0002-ai-accessibility.html +11 -8
- package/dist/docs/decisions/ADR-0003-single-file-bundle.html +11 -8
- package/dist/docs/decisions/ADR-0004-bun-runtime.html +11 -8
- package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +11 -8
- package/dist/docs/decisions/ADR-0006-data-driven-content.html +751 -0
- package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +11 -8
- package/dist/docs/decisions/DDR-0002-theme-system.html +11 -8
- package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +11 -8
- package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +11 -8
- package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +11 -8
- package/dist/docs/userguide/cli/build.html +11 -8
- package/dist/docs/userguide/cli/bundle.html +11 -8
- package/dist/docs/userguide/cli/dev.html +11 -8
- package/dist/docs/userguide/cli/init.html +11 -8
- package/dist/docs/userguide/cli/servers.html +11 -8
- package/dist/docs/userguide/cli/stop.html +11 -8
- package/dist/docs/userguide/cli/update.html +11 -8
- package/dist/docs/userguide/cli/version.html +11 -8
- package/dist/docs/userguide/cli.html +11 -8
- package/dist/docs/userguide/sharing.html +11 -8
- package/dist/index.html +11 -8
- 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 +11 -8
- package/dist/schemas/plugin-schemas-notes.html +11 -8
- package/dist/schemas/plugin.schema.html +11 -8
- package/dist/schemas/plugins.schema.html +11 -8
- package/dist/schemas/v0/common.schema.html +15 -12
- package/dist/schemas/v0/plugin-registry.schema.html +14 -11
- package/dist/schemas/v0/plugin.schema.html +14 -11
- package/dist/schemas/v0/plugins.schema.html +14 -11
- package/dist/schemas/v0/site.schema.html +68 -9
- package/dist/schemas/v0/theme.schema.html +22 -19
- package/dist/schemas.html +11 -8
- package/dist/styles.css +39 -4
- package/package.json +1 -1
- package/plugins-dist/latex-runtime.js +140 -0
- package/plugins-dist/latex.js +178 -0
- package/plugins-dist/planning-visuals.css +261 -0
- package/plugins-dist/planning-visuals.js +669 -0
- package/plugins-dist/slides-charts-lite-runtime.js +179 -0
- package/plugins-dist/slides-charts-lite.js +198 -0
- package/registry/plugins.yaml +40 -1
- package/schemas/v0/site.schema.json +56 -0
- package/scripts/build-all.ts +5 -0
- package/scripts/build.ts +264 -80
- package/scripts/bundle.ts +188 -17
- package/scripts/dev.ts +294 -171
- package/scripts/embed-docs.ts +119 -0
- package/src/__tests__/brief.test.ts +151 -0
- package/src/__tests__/build.test.ts +293 -1
- package/src/__tests__/bundle.test.ts +195 -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__/init.test.ts +51 -2
- package/src/__tests__/latex-runtime.bun.test.ts +35 -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 +719 -1
- package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
- package/src/cli.ts +124 -22
- package/src/commands/docs.ts +71 -0
- package/src/commands/init.ts +1 -1
- package/src/generated/embedded-docs.ts +2384 -0
- package/src/server-registry.ts +50 -10
- package/src/shared.ts +1174 -43
- package/src/site/styles.css +39 -4
- package/src/site/template.html +5 -2
- package/src/templates/brief.ts +486 -0
- package/src/templates/deck.ts +59 -0
- package/src/templates/driver.ts +46 -13
- package/src/templates/handbook.ts +32 -0
- package/src/templates/runbook.ts +32 -0
|
@@ -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();
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Brief template definition
|
|
3
|
+
*
|
|
4
|
+
* Covers: src/templates/brief.ts
|
|
5
|
+
* Strategy: import the template directly, verify structure and generated content
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { brief } from "../templates/brief.ts";
|
|
10
|
+
import type { BrandingConfig, TemplateContext, TemplateFile } from "../templates/schema.ts";
|
|
11
|
+
|
|
12
|
+
function makeCtx(overrides?: Partial<TemplateContext>): TemplateContext {
|
|
13
|
+
const branding: BrandingConfig = {
|
|
14
|
+
siteName: "Test Brief",
|
|
15
|
+
brandName: "Acme Corp",
|
|
16
|
+
brandUrl: "https://acme.example.com",
|
|
17
|
+
primaryColor: "#2563eb",
|
|
18
|
+
footerText: "Footer text",
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
name: "test-brief",
|
|
22
|
+
branding,
|
|
23
|
+
template: brief,
|
|
24
|
+
year: 2026,
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveContent(file: TemplateFile, ctx: TemplateContext): string {
|
|
30
|
+
return typeof file.content === "function" ? file.content(ctx) : file.content;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findFile(path: string): TemplateFile {
|
|
34
|
+
const f = brief.files.find((entry) => entry.path === path);
|
|
35
|
+
if (!f) throw new Error(`Template file not found: ${path}`);
|
|
36
|
+
return f;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("brief template definition", () => {
|
|
40
|
+
it("has correct identity metadata", () => {
|
|
41
|
+
expect(brief.id).toBe("brief");
|
|
42
|
+
expect(brief.name).toBe("Brief");
|
|
43
|
+
expect(brief.version).toBe(1);
|
|
44
|
+
expect(brief.extends).toBe("minimal");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("defines exactly four sections", () => {
|
|
48
|
+
expect(brief.sections).toHaveLength(4);
|
|
49
|
+
expect(brief.sections.map((s) => s.name)).toEqual([
|
|
50
|
+
"Product",
|
|
51
|
+
"Use Cases",
|
|
52
|
+
"Getting Started",
|
|
53
|
+
"Reference",
|
|
54
|
+
]);
|
|
55
|
+
expect(brief.sections.map((s) => s.path)).toEqual([
|
|
56
|
+
"content/product",
|
|
57
|
+
"content/use-cases",
|
|
58
|
+
"content/getting-started",
|
|
59
|
+
"content/reference",
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("defines unique file paths", () => {
|
|
64
|
+
const paths = brief.files.map((f) => f.path);
|
|
65
|
+
expect(new Set(paths).size).toBe(paths.length);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("brief site.yaml", () => {
|
|
70
|
+
const ctx = makeCtx();
|
|
71
|
+
const file = findFile("site.yaml");
|
|
72
|
+
|
|
73
|
+
it("includes branding and all four section paths", () => {
|
|
74
|
+
const content = resolveContent(file, ctx);
|
|
75
|
+
expect(content).toContain('title: "Test Brief"');
|
|
76
|
+
expect(content).toContain('name: "Acme Corp"');
|
|
77
|
+
expect(content).toContain('url: "https://acme.example.com"');
|
|
78
|
+
expect(content).toContain('"content/product"');
|
|
79
|
+
expect(content).toContain('"content/use-cases"');
|
|
80
|
+
expect(content).toContain('"content/getting-started"');
|
|
81
|
+
expect(content).toContain('"content/reference"');
|
|
82
|
+
expect(content).toContain('home: "index.md"');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("brief starter files", () => {
|
|
87
|
+
const ctx = makeCtx();
|
|
88
|
+
const starterFiles = [
|
|
89
|
+
"index.md",
|
|
90
|
+
"content/product/overview.md",
|
|
91
|
+
"content/product/capabilities.md",
|
|
92
|
+
"content/use-cases/overview.md",
|
|
93
|
+
"content/use-cases/example-use-case.md",
|
|
94
|
+
"content/getting-started/overview.md",
|
|
95
|
+
"content/getting-started/requirements.md",
|
|
96
|
+
"content/reference/architecture.md",
|
|
97
|
+
"content/reference/faq.md",
|
|
98
|
+
"content/reference/contacts.md",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
it("ships all 10 starter files", () => {
|
|
102
|
+
expect(starterFiles).toHaveLength(10);
|
|
103
|
+
for (const path of starterFiles) {
|
|
104
|
+
expect(brief.files.some((f) => f.path === path)).toBe(true);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("includes frontmatter in markdown starter files", () => {
|
|
109
|
+
for (const path of starterFiles) {
|
|
110
|
+
const content = resolveContent(findFile(path), ctx);
|
|
111
|
+
expect(content).toMatch(/^---\n/);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("includes customize markers in starter files", () => {
|
|
116
|
+
for (const path of starterFiles) {
|
|
117
|
+
const content = resolveContent(findFile(path), ctx);
|
|
118
|
+
expect(content).toContain("<!-- ← CUSTOMIZE");
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("uses professional framing in index and use cases", () => {
|
|
123
|
+
const index = resolveContent(findFile("index.md"), ctx);
|
|
124
|
+
expect(index).toContain("At a Glance");
|
|
125
|
+
expect(index).toContain("[Use Cases](/content/use-cases/overview)");
|
|
126
|
+
|
|
127
|
+
const useCases = resolveContent(findFile("content/use-cases/example-use-case.md"), ctx);
|
|
128
|
+
expect(useCases).toContain("## Scenario");
|
|
129
|
+
expect(useCases).toContain("## Outcome");
|
|
130
|
+
expect(useCases).toContain("| Metric | Before | After |");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("brief CUSTOMIZING.md", () => {
|
|
135
|
+
const ctx = makeCtx();
|
|
136
|
+
const file = findFile("CUSTOMIZING.md");
|
|
137
|
+
|
|
138
|
+
it("documents template identity and external-audience tone", () => {
|
|
139
|
+
const content = resolveContent(file, ctx);
|
|
140
|
+
expect(content).toContain("template: brief");
|
|
141
|
+
expect(content).toContain("clients, prospects, and partners");
|
|
142
|
+
expect(content).toContain("Informational and professional");
|
|
143
|
+
expect(content).toContain("Not sales copy");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("guides use-case duplication workflow", () => {
|
|
147
|
+
const content = resolveContent(file, ctx);
|
|
148
|
+
expect(content).toContain("Duplicate `content/use-cases/example-use-case.md`");
|
|
149
|
+
expect(content).toContain("problem -> solution -> outcome");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -35,7 +35,8 @@ async function writeSiteYaml(dir: string, extra: Record<string, unknown> = {}):
|
|
|
35
35
|
const mode = extra.mode ? `mode: ${extra.mode}\n` : "";
|
|
36
36
|
const aspect = extra.aspect ? `aspect: ${extra.aspect}\n` : "";
|
|
37
37
|
const home = extra.home ? `home: ${extra.home}\n` : "";
|
|
38
|
-
const
|
|
38
|
+
const footer = extra.footer ? `footer:\n${extra.footer}\n` : "";
|
|
39
|
+
const yaml = `title: ${title}\n${version}${mode}${aspect}brand:\n${brand}\n${home}${footer}sections:\n${sections}\n`;
|
|
39
40
|
await writeFile(join(dir, "site.yaml"), yaml);
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -216,6 +217,45 @@ describe("build", () => {
|
|
|
216
217
|
expect(html).toContain("unversioned");
|
|
217
218
|
});
|
|
218
219
|
|
|
220
|
+
it("renders footer logo with static path prefix in built output", async () => {
|
|
221
|
+
const siteDir = await makeTempDir();
|
|
222
|
+
const outDir = "out";
|
|
223
|
+
await writeSiteYaml(siteDir, {
|
|
224
|
+
footer: ' logo: "assets/brand/footer-logo.png"\n logoAlt: "Footer Brand"\n logoHeight: 24',
|
|
225
|
+
});
|
|
226
|
+
await writeMd(siteDir, "docs/page.md", "# Page");
|
|
227
|
+
|
|
228
|
+
await build({ folder: siteDir, out: outDir });
|
|
229
|
+
|
|
230
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
231
|
+
expect(html).toContain('class="footer-logo-img"');
|
|
232
|
+
expect(html).toContain('src="./assets/brand/footer-logo.png"');
|
|
233
|
+
expect(html).toContain('alt="Footer Brand"');
|
|
234
|
+
expect(html).toContain("max-height: 24px");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("renders light/dark variants for header and footer logos when configured", async () => {
|
|
238
|
+
const siteDir = await makeTempDir();
|
|
239
|
+
const outDir = "out";
|
|
240
|
+
await writeSiteYaml(siteDir, {
|
|
241
|
+
brand:
|
|
242
|
+
' name: Test\n url: /\n logo: "assets/brand/logo.png"\n logoDark: "assets/brand/logo-dark.png"',
|
|
243
|
+
footer:
|
|
244
|
+
' logo: "assets/brand/footer-logo.png"\n logoDark: "assets/brand/footer-logo-dark.png"',
|
|
245
|
+
});
|
|
246
|
+
await writeMd(siteDir, "docs/page.md", "# Page");
|
|
247
|
+
|
|
248
|
+
await build({ folder: siteDir, out: outDir });
|
|
249
|
+
|
|
250
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
251
|
+
expect(html).toContain("logo-img logo-light");
|
|
252
|
+
expect(html).toContain("logo-img logo-dark");
|
|
253
|
+
expect(html).toContain("./assets/brand/logo-dark.png");
|
|
254
|
+
expect(html).toContain("footer-logo-img logo-light");
|
|
255
|
+
expect(html).toContain("footer-logo-img logo-dark");
|
|
256
|
+
expect(html).toContain("./assets/brand/footer-logo-dark.png");
|
|
257
|
+
});
|
|
258
|
+
|
|
219
259
|
it("builds a single-page hash-routed deck when mode is slides", async () => {
|
|
220
260
|
const siteDir = await makeTempDir();
|
|
221
261
|
const outDir = "out";
|
|
@@ -332,6 +372,87 @@ plugins:
|
|
|
332
372
|
expect(html).toContain(js);
|
|
333
373
|
});
|
|
334
374
|
|
|
375
|
+
it("preserves dollar-sign replacement patterns in injected plugin JS", async () => {
|
|
376
|
+
const siteDir = await makeTempDir();
|
|
377
|
+
const outDir = "out";
|
|
378
|
+
await writeSiteYaml(siteDir);
|
|
379
|
+
await writeMd(siteDir, "docs/page.md", "# Test");
|
|
380
|
+
|
|
381
|
+
const js = "const expr='x'; const sample = `$${expr}$`; const marker = '$`';";
|
|
382
|
+
await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
|
|
383
|
+
await writeFile(join(siteDir, "plugins-dist", "literal.js"), js, "utf-8");
|
|
384
|
+
|
|
385
|
+
await mkdir(join(siteDir, "registry"), { recursive: true });
|
|
386
|
+
await writeFile(
|
|
387
|
+
join(siteDir, "registry", "plugins.yaml"),
|
|
388
|
+
`version: 1
|
|
389
|
+
updated: "2026-02-16"
|
|
390
|
+
baseUrl: ""
|
|
391
|
+
plugins:
|
|
392
|
+
literal:
|
|
393
|
+
name: "Literal"
|
|
394
|
+
description: "Replacement pattern regression"
|
|
395
|
+
version: "1.0.0"
|
|
396
|
+
contract: "1"
|
|
397
|
+
kitfly: ">=0.2.0 <1.0.0"
|
|
398
|
+
license: MIT
|
|
399
|
+
verified: true
|
|
400
|
+
assets:
|
|
401
|
+
js: "plugins-dist/literal.js"
|
|
402
|
+
assetSha256:
|
|
403
|
+
js: "sha256:${sha256Hex(js)}"
|
|
404
|
+
`,
|
|
405
|
+
"utf-8",
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - literal@1.0.0\n", "utf-8");
|
|
409
|
+
|
|
410
|
+
await build({ folder: siteDir, out: outDir });
|
|
411
|
+
|
|
412
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
413
|
+
expect(html).toContain("const sample = `$${expr}$`;");
|
|
414
|
+
expect(html).toContain("const marker = '$`';");
|
|
415
|
+
expect(html).not.toContain("<!DOCTYPE html><html");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("preserves dollar-sign replacement patterns in markdown content", async () => {
|
|
419
|
+
const siteDir = await makeTempDir();
|
|
420
|
+
const outDir = "out";
|
|
421
|
+
await writeSiteYaml(siteDir);
|
|
422
|
+
await writeMd(
|
|
423
|
+
siteDir,
|
|
424
|
+
"docs/dollars.md",
|
|
425
|
+
"# Dollars\n\nLiteral dollars: $$ and $` and $' and $&\n\nMath style: $$x^2$$",
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
await build({ folder: siteDir, out: outDir });
|
|
429
|
+
|
|
430
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
431
|
+
expect(html).toContain("Literal dollars: $$ and $` and $' and $&");
|
|
432
|
+
expect(html).toContain("Math style: $$x^2$$");
|
|
433
|
+
expect(html.match(/<!DOCTYPE html>/g)?.length ?? 0).toBe(1);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("injects latex plugin in docs mode", async () => {
|
|
437
|
+
const siteDir = await makeTempDir();
|
|
438
|
+
const outDir = "out";
|
|
439
|
+
await writeSiteYaml(siteDir);
|
|
440
|
+
await writeMd(
|
|
441
|
+
siteDir,
|
|
442
|
+
"docs/math.md",
|
|
443
|
+
"# Math\n\nInline: $x^2$.\n\n$$\n\\\\int_0^1 x^2 dx\n$$\n\n```math\n\\\\sum_{i=1}^{n} i\n```",
|
|
444
|
+
);
|
|
445
|
+
await writeFile(join(siteDir, "kitfly.plugins.yaml"), "plugins:\n - latex@0.2.2\n", "utf-8");
|
|
446
|
+
|
|
447
|
+
await build({ folder: siteDir, out: outDir });
|
|
448
|
+
|
|
449
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
450
|
+
expect(html).toContain('data-kitfly-plugin="latex@0.2.2"');
|
|
451
|
+
expect(html).toContain("const KATEX_JS_URL =");
|
|
452
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js");
|
|
453
|
+
expect(html).toContain("cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css");
|
|
454
|
+
});
|
|
455
|
+
|
|
335
456
|
it("injects slides-only plugins when mode=slides", async () => {
|
|
336
457
|
const siteDir = await makeTempDir();
|
|
337
458
|
const outDir = "out";
|
|
@@ -395,6 +516,53 @@ plugins:
|
|
|
395
516
|
expect(html).toContain(js);
|
|
396
517
|
});
|
|
397
518
|
|
|
519
|
+
it("injects slides-charts-lite plugin in slides mode", async () => {
|
|
520
|
+
const siteDir = await makeTempDir();
|
|
521
|
+
const outDir = "out";
|
|
522
|
+
await writeSiteYaml(siteDir, { mode: "slides" });
|
|
523
|
+
await writeMd(
|
|
524
|
+
siteDir,
|
|
525
|
+
"docs/deck.md",
|
|
526
|
+
`# Chart
|
|
527
|
+
|
|
528
|
+
\`\`\`chart
|
|
529
|
+
kind: bar
|
|
530
|
+
title: Revenue
|
|
531
|
+
labels: ["Q1", "Q2"]
|
|
532
|
+
data: [10, 12]
|
|
533
|
+
\`\`\`
|
|
534
|
+
`,
|
|
535
|
+
);
|
|
536
|
+
await writeFile(
|
|
537
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
538
|
+
"plugins:\n - slides-charts-lite@0.2.2\n",
|
|
539
|
+
"utf-8",
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
await build({ folder: siteDir, out: outDir });
|
|
543
|
+
|
|
544
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
545
|
+
expect(html).toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
|
|
546
|
+
expect(html).toContain("kitfly-chart-wrapper");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("does not inject slides-charts-lite in docs mode", async () => {
|
|
550
|
+
const siteDir = await makeTempDir();
|
|
551
|
+
const outDir = "out";
|
|
552
|
+
await writeSiteYaml(siteDir, { mode: "docs" });
|
|
553
|
+
await writeMd(siteDir, "docs/page.md", "# Page");
|
|
554
|
+
await writeFile(
|
|
555
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
556
|
+
"plugins:\n - slides-charts-lite@0.2.2\n",
|
|
557
|
+
"utf-8",
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
await build({ folder: siteDir, out: outDir });
|
|
561
|
+
|
|
562
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
563
|
+
expect(html).not.toContain('data-kitfly-plugin="slides-charts-lite@0.2.2"');
|
|
564
|
+
});
|
|
565
|
+
|
|
398
566
|
it("ignores unknown slides-visuals block types while enforcing known contracts", async () => {
|
|
399
567
|
const siteDir = await makeTempDir();
|
|
400
568
|
const outDir = "out";
|
|
@@ -456,4 +624,128 @@ plugins:
|
|
|
456
624
|
expect(html).toContain('data-kitfly-plugin="slides-visuals@0.2.1"');
|
|
457
625
|
expect(html).toContain("future-thing");
|
|
458
626
|
});
|
|
627
|
+
|
|
628
|
+
it("enforces planning-visuals fence contract in docs mode", async () => {
|
|
629
|
+
const siteDir = await makeTempDir();
|
|
630
|
+
const outDir = "out";
|
|
631
|
+
await writeSiteYaml(siteDir, { mode: "docs" });
|
|
632
|
+
await writeMd(
|
|
633
|
+
siteDir,
|
|
634
|
+
"docs/plan.md",
|
|
635
|
+
`# Plan
|
|
636
|
+
|
|
637
|
+
:::gantt
|
|
638
|
+
time-unit: week
|
|
639
|
+
time-start: 2026-W10
|
|
640
|
+
time-end: 2026-W12
|
|
641
|
+
:::
|
|
642
|
+
`,
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
const js = "console.log('planning visuals');";
|
|
646
|
+
const css = ".kitfly-planning-gantt{border:1px solid red;}";
|
|
647
|
+
await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
|
|
648
|
+
await writeFile(join(siteDir, "plugins-dist", "planning-visuals.js"), js, "utf-8");
|
|
649
|
+
await writeFile(join(siteDir, "plugins-dist", "planning-visuals.css"), css, "utf-8");
|
|
650
|
+
await mkdir(join(siteDir, "registry"), { recursive: true });
|
|
651
|
+
await writeFile(
|
|
652
|
+
join(siteDir, "registry", "plugins.yaml"),
|
|
653
|
+
`version: 1
|
|
654
|
+
updated: "2026-03-03"
|
|
655
|
+
baseUrl: ""
|
|
656
|
+
plugins:
|
|
657
|
+
planning-visuals:
|
|
658
|
+
name: "Planning Visuals"
|
|
659
|
+
description: "Test planning visuals"
|
|
660
|
+
version: "0.2.4"
|
|
661
|
+
contract: "1"
|
|
662
|
+
kitfly: ">=0.2.4 <1.0.0"
|
|
663
|
+
license: MIT
|
|
664
|
+
verified: true
|
|
665
|
+
assets:
|
|
666
|
+
js: "plugins-dist/planning-visuals.js"
|
|
667
|
+
css: "plugins-dist/planning-visuals.css"
|
|
668
|
+
assetSha256:
|
|
669
|
+
js: "sha256:${sha256Hex(js)}"
|
|
670
|
+
css: "sha256:${sha256Hex(css)}"
|
|
671
|
+
`,
|
|
672
|
+
"utf-8",
|
|
673
|
+
);
|
|
674
|
+
await writeFile(
|
|
675
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
676
|
+
"plugins:\n - planning-visuals@0.2.4\n",
|
|
677
|
+
"utf-8",
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
await expect(build({ folder: siteDir, out: outDir })).rejects.toThrow(
|
|
681
|
+
/planning-visuals fence contract violations/i,
|
|
682
|
+
);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("ignores unknown planning-visuals block types while enforcing known contracts", async () => {
|
|
686
|
+
const siteDir = await makeTempDir();
|
|
687
|
+
const outDir = "out";
|
|
688
|
+
await writeSiteYaml(siteDir, { mode: "docs" });
|
|
689
|
+
await writeMd(
|
|
690
|
+
siteDir,
|
|
691
|
+
"docs/plan.md",
|
|
692
|
+
`# Plan
|
|
693
|
+
|
|
694
|
+
:::future-plan
|
|
695
|
+
note: pass-through
|
|
696
|
+
:::
|
|
697
|
+
|
|
698
|
+
:::gantt
|
|
699
|
+
time-unit: month
|
|
700
|
+
time-start: 2026-04
|
|
701
|
+
time-end: 2026-08
|
|
702
|
+
tracks:
|
|
703
|
+
- label: Wave 1
|
|
704
|
+
depth: 1
|
|
705
|
+
start: 2026-04
|
|
706
|
+
end: 2026-06
|
|
707
|
+
:::
|
|
708
|
+
`,
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
const js = "console.log('planning visuals');";
|
|
712
|
+
const css = ".kitfly-planning-gantt{border:1px solid red;}";
|
|
713
|
+
await mkdir(join(siteDir, "plugins-dist"), { recursive: true });
|
|
714
|
+
await writeFile(join(siteDir, "plugins-dist", "planning-visuals.js"), js, "utf-8");
|
|
715
|
+
await writeFile(join(siteDir, "plugins-dist", "planning-visuals.css"), css, "utf-8");
|
|
716
|
+
await mkdir(join(siteDir, "registry"), { recursive: true });
|
|
717
|
+
await writeFile(
|
|
718
|
+
join(siteDir, "registry", "plugins.yaml"),
|
|
719
|
+
`version: 1
|
|
720
|
+
updated: "2026-03-03"
|
|
721
|
+
baseUrl: ""
|
|
722
|
+
plugins:
|
|
723
|
+
planning-visuals:
|
|
724
|
+
name: "Planning Visuals"
|
|
725
|
+
description: "Test planning visuals"
|
|
726
|
+
version: "0.2.4"
|
|
727
|
+
contract: "1"
|
|
728
|
+
kitfly: ">=0.2.4 <1.0.0"
|
|
729
|
+
license: MIT
|
|
730
|
+
verified: true
|
|
731
|
+
assets:
|
|
732
|
+
js: "plugins-dist/planning-visuals.js"
|
|
733
|
+
css: "plugins-dist/planning-visuals.css"
|
|
734
|
+
assetSha256:
|
|
735
|
+
js: "sha256:${sha256Hex(js)}"
|
|
736
|
+
css: "sha256:${sha256Hex(css)}"
|
|
737
|
+
`,
|
|
738
|
+
"utf-8",
|
|
739
|
+
);
|
|
740
|
+
await writeFile(
|
|
741
|
+
join(siteDir, "kitfly.plugins.yaml"),
|
|
742
|
+
"plugins:\n - planning-visuals@0.2.4\n",
|
|
743
|
+
"utf-8",
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
await expect(build({ folder: siteDir, out: outDir })).resolves.toBeUndefined();
|
|
747
|
+
const html = await readFile(join(siteDir, outDir, "index.html"), "utf-8");
|
|
748
|
+
expect(html).toContain('data-kitfly-plugin="planning-visuals@0.2.4"');
|
|
749
|
+
expect(html).toContain("future-plan");
|
|
750
|
+
});
|
|
459
751
|
});
|