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.
Files changed (132) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +38 -21
  3. package/VERSION +1 -1
  4. package/dist/_raw/content/guide/branding.md +146 -0
  5. package/dist/_raw/content/guide/data-driven-content.md +204 -0
  6. package/dist/_raw/content/reference/configuration.md +145 -7
  7. package/dist/_raw/content/reference/environment-variables.md +26 -1
  8. package/dist/_raw/content/reference/gantt-widget.md +468 -0
  9. package/dist/_raw/content/reference/glossary.md +25 -1
  10. package/dist/_raw/content/reference/key-concepts.md +30 -2
  11. package/dist/_raw/content/reference/plugins.md +170 -1
  12. package/dist/_raw/docs/decisions/ADR-0006-data-driven-content.md +350 -0
  13. package/dist/content/deployment/preflight.html +11 -8
  14. package/dist/content/deployment/recipes/aws-s3.html +11 -8
  15. package/dist/content/deployment/recipes/cloudflare-pages.html +11 -8
  16. package/dist/content/deployment/recipes/cloudflare-r2.html +11 -8
  17. package/dist/content/deployment/recipes/fly-io.html +11 -8
  18. package/dist/content/deployment/recipes/github-pages.html +11 -8
  19. package/dist/content/deployment/recipes/netlify.html +11 -8
  20. package/dist/content/deployment/recipes/vercel.html +11 -8
  21. package/dist/content/deployment/secrets-and-env-vars.html +11 -8
  22. package/dist/content/deployment.html +11 -8
  23. package/dist/content/guide/approaches.html +11 -8
  24. package/dist/content/guide/branding.html +509 -0
  25. package/dist/content/guide/data-driven-content.html +542 -0
  26. package/dist/content/guide/features.html +11 -8
  27. package/dist/content/guide/getting-started.html +11 -8
  28. package/dist/content/guide/kitfly-overview.html +11 -8
  29. package/dist/content/reference/configuration.html +136 -11
  30. package/dist/content/reference/design-catalog.html +11 -8
  31. package/dist/content/reference/environment-variables.html +51 -10
  32. package/dist/content/reference/gantt-widget.html +899 -0
  33. package/dist/content/reference/glossary.html +25 -10
  34. package/dist/content/reference/key-concepts.html +34 -11
  35. package/dist/content/reference/plugins.html +261 -10
  36. package/dist/content/reference/slides-authoring-guidelines.html +11 -8
  37. package/dist/content/reference/structure.html +11 -8
  38. package/dist/content/reference.html +11 -8
  39. package/dist/content/templates/crucible.html +11 -8
  40. package/dist/content/templates/handbook.html +11 -8
  41. package/dist/content/templates/minimal.html +11 -8
  42. package/dist/content/templates/overview.html +11 -8
  43. package/dist/content/templates/pipeline.html +11 -8
  44. package/dist/content/templates/productbook.html +11 -8
  45. package/dist/content/templates/runbook.html +11 -8
  46. package/dist/content/templates/servicebook.html +11 -8
  47. package/dist/content-index.json +37 -2
  48. package/dist/docs/decisions/ADR-0001-minimalist-site-code.html +11 -8
  49. package/dist/docs/decisions/ADR-0002-ai-accessibility.html +11 -8
  50. package/dist/docs/decisions/ADR-0003-single-file-bundle.html +11 -8
  51. package/dist/docs/decisions/ADR-0004-bun-runtime.html +11 -8
  52. package/dist/docs/decisions/ADR-0005-plugin-contract-and-distribution.html +11 -8
  53. package/dist/docs/decisions/ADR-0006-data-driven-content.html +751 -0
  54. package/dist/docs/decisions/DDR-0001-viewport-locked-layout.html +11 -8
  55. package/dist/docs/decisions/DDR-0002-theme-system.html +11 -8
  56. package/dist/docs/decisions/DDR-0003-bounded-logo-slot.html +11 -8
  57. package/dist/docs/decisions/DDR-0004-slides-rendering-model.html +11 -8
  58. package/dist/docs/decisions/DDR-0005-deterministic-layout-boundary.html +11 -8
  59. package/dist/docs/userguide/cli/build.html +11 -8
  60. package/dist/docs/userguide/cli/bundle.html +11 -8
  61. package/dist/docs/userguide/cli/dev.html +11 -8
  62. package/dist/docs/userguide/cli/init.html +11 -8
  63. package/dist/docs/userguide/cli/servers.html +11 -8
  64. package/dist/docs/userguide/cli/stop.html +11 -8
  65. package/dist/docs/userguide/cli/update.html +11 -8
  66. package/dist/docs/userguide/cli/version.html +11 -8
  67. package/dist/docs/userguide/cli.html +11 -8
  68. package/dist/docs/userguide/sharing.html +11 -8
  69. package/dist/index.html +11 -8
  70. package/dist/llms.txt +3 -3
  71. package/dist/provenance.json +4 -5
  72. package/dist/reports/license-inventory.csv +199 -0
  73. package/dist/schemas/plugin-registry.schema.html +11 -8
  74. package/dist/schemas/plugin-schemas-notes.html +11 -8
  75. package/dist/schemas/plugin.schema.html +11 -8
  76. package/dist/schemas/plugins.schema.html +11 -8
  77. package/dist/schemas/v0/common.schema.html +15 -12
  78. package/dist/schemas/v0/plugin-registry.schema.html +14 -11
  79. package/dist/schemas/v0/plugin.schema.html +14 -11
  80. package/dist/schemas/v0/plugins.schema.html +14 -11
  81. package/dist/schemas/v0/site.schema.html +68 -9
  82. package/dist/schemas/v0/theme.schema.html +22 -19
  83. package/dist/schemas.html +11 -8
  84. package/dist/styles.css +39 -4
  85. package/package.json +1 -1
  86. package/plugins-dist/latex-runtime.js +140 -0
  87. package/plugins-dist/latex.js +178 -0
  88. package/plugins-dist/planning-visuals.css +261 -0
  89. package/plugins-dist/planning-visuals.js +669 -0
  90. package/plugins-dist/slides-charts-lite-runtime.js +179 -0
  91. package/plugins-dist/slides-charts-lite.js +198 -0
  92. package/registry/plugins.yaml +40 -1
  93. package/schemas/v0/site.schema.json +56 -0
  94. package/scripts/build-all.ts +5 -0
  95. package/scripts/build.ts +264 -80
  96. package/scripts/bundle.ts +188 -17
  97. package/scripts/dev.ts +294 -171
  98. package/scripts/embed-docs.ts +119 -0
  99. package/src/__tests__/brief.test.ts +151 -0
  100. package/src/__tests__/build.test.ts +293 -1
  101. package/src/__tests__/bundle.test.ts +195 -0
  102. package/src/__tests__/docs.test.ts +117 -0
  103. package/src/__tests__/fixtures/fences/planning-visuals/invalid/bad-month-format.md +10 -0
  104. package/src/__tests__/fixtures/fences/planning-visuals/invalid/marker-format-mismatch.md +13 -0
  105. package/src/__tests__/fixtures/fences/planning-visuals/invalid/milestone-format-mismatch.md +13 -0
  106. package/src/__tests__/fixtures/fences/planning-visuals/invalid/missing-tracks.md +5 -0
  107. package/src/__tests__/fixtures/fences/planning-visuals/invalid/track-reversed.md +10 -0
  108. package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-basic.md +15 -0
  109. package/src/__tests__/fixtures/fences/planning-visuals/valid/markers-no-milestones.md +13 -0
  110. package/src/__tests__/fixtures/fences/planning-visuals/valid/month-basic.md +16 -0
  111. package/src/__tests__/fixtures/fences/planning-visuals/valid/no-milestones.md +10 -0
  112. package/src/__tests__/fixtures/fences/planning-visuals/valid/week-basic.md +20 -0
  113. package/src/__tests__/init.test.ts +51 -2
  114. package/src/__tests__/latex-runtime.bun.test.ts +35 -0
  115. package/src/__tests__/planning-visuals-fence-contract.test.ts +28 -0
  116. package/src/__tests__/planning-visuals-runtime-regressions.bun.test.ts +68 -0
  117. package/src/__tests__/planning-visuals-runtime.bun.test.ts +192 -0
  118. package/src/__tests__/shared.test.ts +719 -1
  119. package/src/__tests__/slides-charts-lite-runtime.bun.test.ts +45 -0
  120. package/src/cli.ts +124 -22
  121. package/src/commands/docs.ts +71 -0
  122. package/src/commands/init.ts +1 -1
  123. package/src/generated/embedded-docs.ts +2384 -0
  124. package/src/server-registry.ts +50 -10
  125. package/src/shared.ts +1174 -43
  126. package/src/site/styles.css +39 -4
  127. package/src/site/template.html +5 -2
  128. package/src/templates/brief.ts +486 -0
  129. package/src/templates/deck.ts +59 -0
  130. package/src/templates/driver.ts +46 -13
  131. package/src/templates/handbook.ts +32 -0
  132. package/src/templates/runbook.ts +32 -0
package/scripts/build.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * Options:
7
7
  * -o, --out <dir> Output directory [env: KITFLY_BUILD_OUT] [default: dist]
8
+ * --profile <name> Active content profile [env: KITFLY_PROFILE]
8
9
  * --raw Include raw markdown files [env: KITFLY_BUILD_RAW] [default: true]
9
10
  * --no-raw Don't include raw markdown files
10
11
  * --help Show help message
@@ -20,13 +21,15 @@ import { loadPluginInjections, type PluginInjections } from "../src/plugin-loade
20
21
  import {
21
22
  buildBreadcrumbsStatic,
22
23
  buildFooter,
24
+ buildLogoImgHtml,
23
25
  buildNavStatic,
24
26
  buildPageMeta,
25
- buildSlideNav,
27
+ buildSlideNavHierarchical,
26
28
  buildToc,
27
29
  type ContentFile,
28
30
  collectFiles,
29
31
  // Navigation/template building
32
+ collectPlanningVisualsContainmentWarnings,
30
33
  collectSlides,
31
34
  envBool,
32
35
  // Config helpers
@@ -35,22 +38,30 @@ import {
35
38
  escapeHtml,
36
39
  // File utilities
37
40
  exists,
41
+ filterByProfile,
42
+ filterUnknownPlanningVisualsTypeDiagnostics,
38
43
  filterUnknownSlidesVisualsTypeDiagnostics,
39
44
  // Provenance
40
45
  generateProvenance,
46
+ loadDataBindings,
41
47
  // YAML/Config parsing
42
48
  loadSiteConfig,
49
+ mergeFrontmatterWithBody,
43
50
  type Provenance,
51
+ pagePathForData,
44
52
  // Markdown utilities
45
53
  parseFrontmatter,
46
54
  parseYaml,
55
+ resolveBindings,
47
56
  resolveStylesPath,
48
57
  resolveTemplatePath,
49
58
  rewriteRelativeAssetUrls,
59
+ runPrebuildHooks,
50
60
  // Types
51
61
  type SiteConfig,
52
62
  slugify,
53
63
  validatePath,
64
+ validatePlanningVisualsFences,
54
65
  validateSlidesVisualsFences,
55
66
  } from "../src/shared.ts";
56
67
  import { generateThemeCSS, getPrismUrls, loadTheme, type Theme } from "../src/theme.ts";
@@ -60,6 +71,41 @@ const DEFAULT_OUT = "dist";
60
71
 
61
72
  let ROOT = process.cwd();
62
73
  let OUT_DIR = DEFAULT_OUT;
74
+ let ACTIVE_PROFILE: string | undefined;
75
+
76
+ async function applyDataBindingsToMarkdown(
77
+ rawMarkdown: string,
78
+ filePath: string,
79
+ config: SiteConfig,
80
+ ): Promise<{ frontmatter: Record<string, unknown>; body: string }> {
81
+ const parsed = parseFrontmatter(rawMarkdown);
82
+ const dataRef = typeof parsed.frontmatter.data === "string" ? parsed.frontmatter.data.trim() : "";
83
+ if (!dataRef) return parsed;
84
+
85
+ const pagePath = pagePathForData(ROOT, config.docroot, filePath);
86
+ const bindings = await loadDataBindings(dataRef, pagePath, ROOT, config.docroot, config.dataroot);
87
+ return {
88
+ frontmatter: parsed.frontmatter,
89
+ body: resolveBindings(parsed.body, bindings, pagePath),
90
+ };
91
+ }
92
+
93
+ async function applyDataBindingsForSlides(
94
+ rawMarkdown: string,
95
+ filePath: string,
96
+ config: SiteConfig,
97
+ ): Promise<string> {
98
+ const resolved = await applyDataBindingsToMarkdown(rawMarkdown, filePath, config);
99
+ return mergeFrontmatterWithBody(rawMarkdown, resolved.body);
100
+ }
101
+
102
+ function normalizeMsysPath(p: string): string {
103
+ // Git Bash / MSYS-style paths: /c/Users/... -> C:\Users\...
104
+ if (process.platform !== "win32") return p;
105
+ const m = p.match(/^\/([a-zA-Z])\/(.*)$/);
106
+ if (!m) return p;
107
+ return `${m[1].toUpperCase()}:\\${m[2].replaceAll("/", "\\")}`;
108
+ }
63
109
 
64
110
  // ---------------------------------------------------------------------------
65
111
  // CLI argument parsing
@@ -69,6 +115,7 @@ interface ParsedArgs {
69
115
  folder?: string;
70
116
  out?: string;
71
117
  raw?: boolean;
118
+ profile?: string;
72
119
  }
73
120
 
74
121
  function parseArgs(argv: string[]): ParsedArgs {
@@ -80,6 +127,9 @@ function parseArgs(argv: string[]): ParsedArgs {
80
127
  if ((arg === "--out" || arg === "-o") && next && !next.startsWith("-")) {
81
128
  result.out = next;
82
129
  i++;
130
+ } else if (arg === "--profile" && next && !next.startsWith("-")) {
131
+ result.profile = next;
132
+ i++;
83
133
  } else if (arg === "--raw") {
84
134
  result.raw = true;
85
135
  } else if (arg === "--no-raw") {
@@ -91,12 +141,13 @@ function parseArgs(argv: string[]): ParsedArgs {
91
141
  return result;
92
142
  }
93
143
 
94
- function getConfig(): { folder?: string; out: string; raw: boolean } {
144
+ function getConfig(): { folder?: string; out: string; raw: boolean; profile?: string } {
95
145
  const args = parseArgs(process.argv.slice(2));
96
146
  return {
97
147
  folder: args.folder,
98
148
  out: args.out ?? envString("KITFLY_BUILD_OUT", DEFAULT_OUT),
99
149
  raw: args.raw ?? envBool("KITFLY_BUILD_RAW", true),
150
+ profile: args.profile ?? process.env.KITFLY_PROFILE,
100
151
  };
101
152
  }
102
153
 
@@ -106,6 +157,27 @@ async function resolveSiteAssetsDir(siteRoot: string): Promise<string | null> {
106
157
  return null;
107
158
  }
108
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
+
109
181
  function computePathPrefix(urlKey: string): string {
110
182
  const clean = urlKey.replace(/^\/+/, "").replace(/\.html$/, "");
111
183
  if (!clean) return "./";
@@ -169,6 +241,7 @@ async function renderFile(
169
241
  config: SiteConfig,
170
242
  theme: Theme,
171
243
  plugins: PluginInjections,
244
+ fenceValidation: FenceValidationFlags,
172
245
  ): Promise<string> {
173
246
  const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
174
247
  const content = await readFile(filePath, "utf-8");
@@ -189,17 +262,33 @@ async function renderFile(
189
262
  }
190
263
  htmlContent = `<h1>${title}</h1>\n<pre><code class="language-json">${escapeHtml(prettyJson)}</code></pre>`;
191
264
  } else {
192
- const { frontmatter, body } = parseFrontmatter(content);
265
+ const { frontmatter, body } = await applyDataBindingsToMarkdown(content, filePath, config);
193
266
  if (frontmatter.title) {
194
267
  title = frontmatter.title as string;
195
268
  }
196
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
+ }
197
286
  htmlContent = marked.parse(body) as string;
198
287
  }
199
288
 
200
289
  const pathPrefix = computePathPrefix(urlKey);
201
290
  const nav = buildNavStatic(files, urlKey, config, pathPrefix);
202
- const footer = buildFooter(provenance, config);
291
+ const footer = buildFooter(provenance, config, pathPrefix);
203
292
  const breadcrumbs = buildBreadcrumbsStatic(urlKey, pathPrefix, files, config);
204
293
  const toc = buildToc(htmlContent);
205
294
  const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
@@ -207,32 +296,50 @@ async function renderFile(
207
296
  const prismUrls = getPrismUrls(theme);
208
297
  const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
209
298
  const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
299
+ const mobileLogoHtml = buildLogoImgHtml({
300
+ logo: config.brand.logo || "assets/brand/logo.png",
301
+ logoDark: config.brand.logoDark,
302
+ alt: config.brand.name,
303
+ className: `logo-img ${logoClass}`,
304
+ pathPrefix,
305
+ onerrorFallback: true,
306
+ });
307
+ const sidebarLogoHtml = buildLogoImgHtml({
308
+ logo: config.brand.logo || "assets/brand/logo.png",
309
+ logoDark: config.brand.logoDark,
310
+ alt: config.brand.name,
311
+ className: "logo-img",
312
+ pathPrefix,
313
+ onerrorFallback: true,
314
+ });
210
315
 
211
316
  return template
212
317
  .replace("{{BODY_CLASS}}", "mode-docs")
213
- .replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
214
- .replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
215
- .replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
216
- .replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
217
- .replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
218
- .replace(/\{\{BRAND_LOGO\}\}/g, config.brand.logo || "assets/brand/logo.png")
219
- .replace(/\{\{BRAND_FAVICON\}\}/g, config.brand.favicon || "assets/brand/favicon.png")
220
- .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, logoClass)
221
- .replace(/\{\{SITE_TITLE\}\}/g, config.title)
222
- .replace("{{TITLE}}", title)
223
- .replace("{{VERSION}}", uiVersion)
224
- .replace("{{BRANCH}}", provenance.gitBranch)
225
- .replace("{{BREADCRUMBS}}", breadcrumbs)
226
- .replace("{{PAGE_META}}", pageMeta)
227
- .replace("{{NAV}}", nav)
228
- .replace("{{CONTENT}}", htmlContent)
229
- .replace("{{TOC}}", toc)
230
- .replace("{{FOOTER}}", footer)
231
- .replace("{{THEME_CSS}}", themeCSS)
232
- .replace("{{PLUGIN_HEAD}}", plugins.head)
233
- .replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
234
- .replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
235
- .replace("{{PRISM_DARK_URL}}", prismUrls.dark)
318
+ .replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
319
+ .replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
320
+ .replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
321
+ .replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
322
+ .replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
323
+ .replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
324
+ .replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
325
+ .replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
326
+ .replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
327
+ .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
328
+ .replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
329
+ .replace("{{TITLE}}", () => title)
330
+ .replace("{{VERSION}}", () => uiVersion)
331
+ .replace("{{BRANCH}}", () => provenance.gitBranch)
332
+ .replace("{{BREADCRUMBS}}", () => breadcrumbs)
333
+ .replace("{{PAGE_META}}", () => pageMeta)
334
+ .replace("{{NAV}}", () => nav)
335
+ .replace("{{CONTENT}}", () => htmlContent)
336
+ .replace("{{TOC}}", () => toc)
337
+ .replace("{{FOOTER}}", () => footer)
338
+ .replace("{{THEME_CSS}}", () => themeCSS)
339
+ .replace("{{PLUGIN_HEAD}}", () => plugins.head)
340
+ .replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
341
+ .replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
342
+ .replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
236
343
  .replace("{{HOT_RELOAD_SCRIPT}}", "");
237
344
  }
238
345
 
@@ -274,32 +381,50 @@ sections:
274
381
  const pathPrefix = "./";
275
382
  const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
276
383
  const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
384
+ const mobileLogoHtml = buildLogoImgHtml({
385
+ logo: config.brand.logo || "assets/brand/logo.png",
386
+ logoDark: config.brand.logoDark,
387
+ alt: config.brand.name,
388
+ className: `logo-img ${logoClass}`,
389
+ pathPrefix,
390
+ onerrorFallback: true,
391
+ });
392
+ const sidebarLogoHtml = buildLogoImgHtml({
393
+ logo: config.brand.logo || "assets/brand/logo.png",
394
+ logoDark: config.brand.logoDark,
395
+ alt: config.brand.name,
396
+ className: "logo-img",
397
+ pathPrefix,
398
+ onerrorFallback: true,
399
+ });
277
400
 
278
401
  return template
279
402
  .replace("{{BODY_CLASS}}", "mode-docs")
280
- .replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
281
- .replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
282
- .replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
283
- .replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
284
- .replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
285
- .replace(/\{\{BRAND_LOGO\}\}/g, config.brand.logo || "assets/brand/logo.png")
286
- .replace(/\{\{BRAND_FAVICON\}\}/g, config.brand.favicon || "assets/brand/favicon.png")
287
- .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, logoClass)
288
- .replace(/\{\{SITE_TITLE\}\}/g, config.title)
403
+ .replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
404
+ .replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
405
+ .replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
406
+ .replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
407
+ .replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
408
+ .replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
409
+ .replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
410
+ .replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
411
+ .replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
412
+ .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
413
+ .replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
289
414
  .replace("{{TITLE}}", "Getting Started")
290
- .replace("{{VERSION}}", uiVersion)
291
- .replace("{{BRANCH}}", provenance.gitBranch)
415
+ .replace("{{VERSION}}", () => uiVersion)
416
+ .replace("{{BRANCH}}", () => provenance.gitBranch)
292
417
  .replace("{{BREADCRUMBS}}", "")
293
418
  .replace("{{PAGE_META}}", "")
294
419
  .replace("{{NAV}}", "<ul></ul>")
295
- .replace("{{CONTENT}}", htmlContent)
420
+ .replace("{{CONTENT}}", () => htmlContent)
296
421
  .replace("{{TOC}}", "")
297
- .replace("{{FOOTER}}", buildFooter(provenance, config))
298
- .replace("{{THEME_CSS}}", themeCSS)
299
- .replace("{{PLUGIN_HEAD}}", plugins.head)
300
- .replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
301
- .replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
302
- .replace("{{PRISM_DARK_URL}}", prismUrls.dark)
422
+ .replace("{{FOOTER}}", () => buildFooter(provenance, config, pathPrefix))
423
+ .replace("{{THEME_CSS}}", () => themeCSS)
424
+ .replace("{{PLUGIN_HEAD}}", () => plugins.head)
425
+ .replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
426
+ .replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
427
+ .replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
303
428
  .replace("{{HOT_RELOAD_SCRIPT}}", "");
304
429
  }
305
430
 
@@ -310,24 +435,18 @@ async function renderSlidesIndex(
310
435
  config: SiteConfig,
311
436
  theme: Theme,
312
437
  plugins: PluginInjections,
438
+ fenceValidation: FenceValidationFlags,
313
439
  ): Promise<string> {
314
440
  const uiVersion = provenance.version ? `v${provenance.version}` : "unversioned";
315
441
  const pathPrefix = "./";
316
- const slides = await collectSlides(files);
317
- let validateFences = false;
318
- try {
319
- const raw = await readFile(join(ROOT, "kitfly.plugins.yaml"), "utf-8");
320
- const parsed = parseYaml(raw) as unknown as Record<string, unknown>;
321
- const enabled = Array.isArray(parsed?.plugins) ? (parsed.plugins as unknown[]) : [];
322
- validateFences = enabled.some((p) => typeof p === "string" && p.startsWith("slides-visuals@"));
323
- } catch {
324
- // no config, skip
325
- }
442
+ const slides = await collectSlides(files, {
443
+ markdownTransform: (raw, file) => applyDataBindingsForSlides(raw, file.path, config),
444
+ });
326
445
  const renderedSlides = await Promise.all(
327
446
  slides.map(async (slide, i) => {
328
447
  let inner = "";
329
448
  if (slide.kind === "markdown") {
330
- if (validateFences) {
449
+ if (fenceValidation.slidesVisuals) {
331
450
  const diagnostics = filterUnknownSlidesVisualsTypeDiagnostics(
332
451
  validateSlidesVisualsFences(slide.body),
333
452
  );
@@ -339,6 +458,22 @@ async function renderSlidesIndex(
339
458
  throw new Error(`slides-visuals fence contract violations:\n${msg}`);
340
459
  }
341
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
+ }
342
477
  inner = marked.parse(slide.body) as string;
343
478
  } else if (slide.kind === "yaml") {
344
479
  inner = `<pre><code class="language-yaml">${escapeHtml(slide.body)}</code></pre>`;
@@ -376,38 +511,56 @@ async function renderSlidesIndex(
376
511
  </div>
377
512
  </div>`;
378
513
 
379
- const nav = buildSlideNav(slides, config, "slide-1");
514
+ const nav = buildSlideNavHierarchical(slides, config, "slide-1");
380
515
  const brandTarget = config.brand.external ? ' target="_blank" rel="noopener"' : "";
381
516
  const logoClass = config.brand.logoType === "wordmark" ? "logo-wordmark" : "logo-icon";
382
517
  const themeCSS = generateThemeCSS(theme);
383
518
  const prismUrls = getPrismUrls(theme);
384
519
  const brandInitial = escapeHtml(config.brand.name.trim().charAt(0).toUpperCase() || "K");
520
+ const mobileLogoHtml = buildLogoImgHtml({
521
+ logo: config.brand.logo || "assets/brand/logo.png",
522
+ logoDark: config.brand.logoDark,
523
+ alt: config.brand.name,
524
+ className: `logo-img ${logoClass}`,
525
+ pathPrefix,
526
+ onerrorFallback: true,
527
+ });
528
+ const sidebarLogoHtml = buildLogoImgHtml({
529
+ logo: config.brand.logo || "assets/brand/logo.png",
530
+ logoDark: config.brand.logoDark,
531
+ alt: config.brand.name,
532
+ className: "logo-img",
533
+ pathPrefix,
534
+ onerrorFallback: true,
535
+ });
385
536
 
386
537
  return template
387
538
  .replace("{{BODY_CLASS}}", "mode-slides")
388
- .replace(/\{\{PATH_PREFIX\}\}/g, pathPrefix)
389
- .replace(/\{\{BRAND_URL\}\}/g, config.brand.url)
390
- .replace(/\{\{BRAND_TARGET\}\}/g, brandTarget)
391
- .replace(/\{\{BRAND_NAME\}\}/g, config.brand.name)
392
- .replace(/\{\{BRAND_INITIAL\}\}/g, brandInitial)
393
- .replace(/\{\{BRAND_LOGO\}\}/g, config.brand.logo || "assets/brand/logo.png")
394
- .replace(/\{\{BRAND_FAVICON\}\}/g, config.brand.favicon || "assets/brand/favicon.png")
395
- .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, logoClass)
396
- .replace(/\{\{SITE_TITLE\}\}/g, config.title)
397
- .replace("{{TITLE}}", config.title)
398
- .replace("{{VERSION}}", uiVersion)
399
- .replace("{{BRANCH}}", provenance.gitBranch)
539
+ .replace(/\{\{PATH_PREFIX\}\}/g, () => pathPrefix)
540
+ .replace(/\{\{BRAND_URL\}\}/g, () => config.brand.url)
541
+ .replace(/\{\{BRAND_TARGET\}\}/g, () => brandTarget)
542
+ .replace(/\{\{BRAND_NAME\}\}/g, () => config.brand.name)
543
+ .replace(/\{\{BRAND_INITIAL\}\}/g, () => brandInitial)
544
+ .replace("{{MOBILE_BRAND_LOGO_IMG}}", () => mobileLogoHtml)
545
+ .replace("{{SIDEBAR_BRAND_LOGO_IMG}}", () => sidebarLogoHtml)
546
+ .replace(/\{\{BRAND_LOGO\}\}/g, () => config.brand.logo || "assets/brand/logo.png")
547
+ .replace(/\{\{BRAND_FAVICON\}\}/g, () => config.brand.favicon || "assets/brand/favicon.png")
548
+ .replace(/\{\{BRAND_LOGO_CLASS\}\}/g, () => logoClass)
549
+ .replace(/\{\{SITE_TITLE\}\}/g, () => config.title)
550
+ .replace("{{TITLE}}", () => config.title)
551
+ .replace("{{VERSION}}", () => uiVersion)
552
+ .replace("{{BRANCH}}", () => provenance.gitBranch)
400
553
  .replace("{{BREADCRUMBS}}", "")
401
554
  .replace("{{PAGE_META}}", "")
402
- .replace("{{NAV}}", nav)
403
- .replace("{{CONTENT}}", htmlContent)
555
+ .replace("{{NAV}}", () => nav)
556
+ .replace("{{CONTENT}}", () => htmlContent)
404
557
  .replace("{{TOC}}", "")
405
- .replace("{{FOOTER}}", buildFooter(provenance, config))
406
- .replace("{{THEME_CSS}}", themeCSS)
407
- .replace("{{PLUGIN_HEAD}}", plugins.head)
408
- .replace("{{PLUGIN_BODY_END}}", plugins.bodyEnd)
409
- .replace("{{PRISM_LIGHT_URL}}", prismUrls.light)
410
- .replace("{{PRISM_DARK_URL}}", prismUrls.dark)
558
+ .replace("{{FOOTER}}", () => buildFooter(provenance, config, pathPrefix))
559
+ .replace("{{THEME_CSS}}", () => themeCSS)
560
+ .replace("{{PLUGIN_HEAD}}", () => plugins.head)
561
+ .replace("{{PLUGIN_BODY_END}}", () => plugins.bodyEnd)
562
+ .replace("{{PRISM_LIGHT_URL}}", () => prismUrls.light)
563
+ .replace("{{PRISM_DARK_URL}}", () => prismUrls.dark)
411
564
  .replace("{{HOT_RELOAD_SCRIPT}}", "");
412
565
  }
413
566
 
@@ -416,6 +569,7 @@ export interface BuildOptions {
416
569
  folder?: string;
417
570
  out?: string;
418
571
  raw?: boolean; // Include raw markdown files (default: true)
572
+ profile?: string;
419
573
  }
420
574
 
421
575
  let INCLUDE_RAW = true;
@@ -430,18 +584,29 @@ export async function build(options: BuildOptions = {}) {
430
584
  if (options.raw === false) {
431
585
  INCLUDE_RAW = false;
432
586
  }
587
+ ACTIVE_PROFILE = options.profile;
433
588
  await buildSite();
434
589
  }
435
590
 
436
591
  // Rename internal function
437
592
  async function buildSite() {
438
- const DIST = join(ROOT, OUT_DIR);
593
+ const DIST = resolve(ROOT, normalizeMsysPath(OUT_DIR));
439
594
 
440
595
  console.log("Building site...\n");
441
596
 
442
597
  // Load configuration
443
598
  const config = await loadSiteConfig(ROOT);
444
599
  console.log(` ✓ Loaded config: "${config.title}" (${config.sections.length} sections)`);
600
+ if (config.prebuild?.length) {
601
+ await runPrebuildHooks(
602
+ config.prebuild,
603
+ ROOT,
604
+ "build",
605
+ ACTIVE_PROFILE,
606
+ config.dataroot || "data",
607
+ );
608
+ console.log(` ✓ prebuild hooks (${config.prebuild.length})`);
609
+ }
445
610
 
446
611
  // Load theme
447
612
  const theme = await loadTheme(ROOT);
@@ -465,6 +630,7 @@ async function buildSite() {
465
630
  root: ROOT,
466
631
  mode: config.mode === "slides" ? "slides" : "docs",
467
632
  });
633
+ const fenceValidation = await getFenceValidationFlags(ROOT);
468
634
 
469
635
  // Copy CSS
470
636
  const css = await readFile(await resolveStylesPath(ROOT), "utf-8");
@@ -499,7 +665,11 @@ async function buildSite() {
499
665
  }
500
666
 
501
667
  // Collect and render all files
502
- const files = await collectFiles(ROOT, config);
668
+ const files = await filterByProfile(
669
+ await collectFiles(ROOT, config),
670
+ ACTIVE_PROFILE,
671
+ config.profiles,
672
+ );
503
673
 
504
674
  if (files.length === 0) {
505
675
  // No content - render Getting Started page
@@ -511,7 +681,15 @@ async function buildSite() {
511
681
  }
512
682
 
513
683
  if (config.mode === "slides") {
514
- const html = await renderSlidesIndex(template, files, provenance, config, theme, plugins);
684
+ const html = await renderSlidesIndex(
685
+ template,
686
+ files,
687
+ provenance,
688
+ config,
689
+ theme,
690
+ plugins,
691
+ fenceValidation,
692
+ );
515
693
  await writeFile(join(DIST, "index.html"), html);
516
694
  console.log(` ✓ index.html (slides mode, ${files.length} source files)`);
517
695
  await generateAIAccessibility(DIST, files, config, provenance);
@@ -530,6 +708,7 @@ async function buildSite() {
530
708
  config,
531
709
  theme,
532
710
  plugins,
711
+ fenceValidation,
533
712
  );
534
713
 
535
714
  // Create output path
@@ -556,6 +735,7 @@ async function buildSite() {
556
735
  config,
557
736
  theme,
558
737
  plugins,
738
+ fenceValidation,
559
739
  );
560
740
  await writeFile(join(DIST, "index.html"), homeHtml);
561
741
  console.log(` ✓ index.html (from ${config.home})`);
@@ -571,6 +751,7 @@ async function buildSite() {
571
751
  config,
572
752
  theme,
573
753
  plugins,
754
+ fenceValidation,
574
755
  );
575
756
  await writeFile(join(DIST, "index.html"), indexHtml);
576
757
  console.log(" ✓ index.html");
@@ -588,6 +769,7 @@ async function buildSite() {
588
769
  config,
589
770
  theme,
590
771
  plugins,
772
+ fenceValidation,
591
773
  );
592
774
  await writeFile(join(DIST, "index.html"), indexHtml);
593
775
  console.log(" ✓ index.html");
@@ -733,6 +915,7 @@ Usage: bun run build [folder] [options]
733
915
 
734
916
  Options:
735
917
  -o, --out <dir> Output directory [env: KITFLY_BUILD_OUT] [default: ${DEFAULT_OUT}]
918
+ --profile <name> Active content profile [env: KITFLY_PROFILE]
736
919
  --raw Include raw markdown files [env: KITFLY_BUILD_RAW] [default: true]
737
920
  --no-raw Don't include raw markdown files
738
921
  --help Show this help message
@@ -752,5 +935,6 @@ Examples:
752
935
  folder: cfg.folder,
753
936
  out: cfg.out,
754
937
  raw: cfg.raw,
938
+ profile: cfg.profile,
755
939
  }).catch(console.error);
756
940
  }