irgen 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +21 -0
  3. package/README.md +161 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +312 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/dsl/aggregator.d.ts +8 -0
  9. package/dist/dsl/aggregator.d.ts.map +1 -0
  10. package/dist/dsl/aggregator.js +64 -0
  11. package/dist/dsl/aggregator.js.map +1 -0
  12. package/dist/dsl/frontend-runtime.d.ts +486 -0
  13. package/dist/dsl/frontend-runtime.d.ts.map +1 -0
  14. package/dist/dsl/frontend-runtime.js +232 -0
  15. package/dist/dsl/frontend-runtime.js.map +1 -0
  16. package/dist/dsl/runtime.d.ts +33 -0
  17. package/dist/dsl/runtime.d.ts.map +1 -0
  18. package/dist/dsl/runtime.js +120 -0
  19. package/dist/dsl/runtime.js.map +1 -0
  20. package/dist/emit/backend/adapters.d.ts +11 -0
  21. package/dist/emit/backend/adapters.d.ts.map +1 -0
  22. package/dist/emit/backend/adapters.js +374 -0
  23. package/dist/emit/backend/adapters.js.map +1 -0
  24. package/dist/emit/backend/backend-tsmorph.d.ts +5 -0
  25. package/dist/emit/backend/backend-tsmorph.d.ts.map +1 -0
  26. package/dist/emit/backend/backend-tsmorph.js +858 -0
  27. package/dist/emit/backend/backend-tsmorph.js.map +1 -0
  28. package/dist/emit/backend/fake-backend.d.ts +2 -0
  29. package/dist/emit/backend/fake-backend.d.ts.map +1 -0
  30. package/dist/emit/backend/fake-backend.js +19 -0
  31. package/dist/emit/backend/fake-backend.js.map +1 -0
  32. package/dist/emit/backend/packaging.d.ts +3 -0
  33. package/dist/emit/backend/packaging.d.ts.map +1 -0
  34. package/dist/emit/backend/packaging.js +71 -0
  35. package/dist/emit/backend/packaging.js.map +1 -0
  36. package/dist/emit/backend/server.d.ts +4 -0
  37. package/dist/emit/backend/server.d.ts.map +1 -0
  38. package/dist/emit/backend/server.js +169 -0
  39. package/dist/emit/backend/server.js.map +1 -0
  40. package/dist/emit/cli/cli-fake.d.ts +2 -0
  41. package/dist/emit/cli/cli-fake.d.ts.map +1 -0
  42. package/dist/emit/cli/cli-fake.js +33 -0
  43. package/dist/emit/cli/cli-fake.js.map +1 -0
  44. package/dist/emit/electron/electron-shell.d.ts +3 -0
  45. package/dist/emit/electron/electron-shell.d.ts.map +1 -0
  46. package/dist/emit/electron/electron-shell.js +454 -0
  47. package/dist/emit/electron/electron-shell.js.map +1 -0
  48. package/dist/emit/engine.d.ts +14 -0
  49. package/dist/emit/engine.d.ts.map +1 -0
  50. package/dist/emit/engine.js +25 -0
  51. package/dist/emit/engine.js.map +1 -0
  52. package/dist/emit/format.d.ts +2 -0
  53. package/dist/emit/format.d.ts.map +1 -0
  54. package/dist/emit/format.js +23 -0
  55. package/dist/emit/format.js.map +1 -0
  56. package/dist/emit/frontend/frontend-react.d.ts +4 -0
  57. package/dist/emit/frontend/frontend-react.d.ts.map +1 -0
  58. package/dist/emit/frontend/frontend-react.js +2021 -0
  59. package/dist/emit/frontend/frontend-react.js.map +1 -0
  60. package/dist/emit/frontend/registry.d.ts +20 -0
  61. package/dist/emit/frontend/registry.d.ts.map +1 -0
  62. package/dist/emit/frontend/registry.js +46 -0
  63. package/dist/emit/frontend/registry.js.map +1 -0
  64. package/dist/emit/frontend/runtime-emitter.d.ts +4 -0
  65. package/dist/emit/frontend/runtime-emitter.d.ts.map +1 -0
  66. package/dist/emit/frontend/runtime-emitter.js +435 -0
  67. package/dist/emit/frontend/runtime-emitter.js.map +1 -0
  68. package/dist/emit/frontend/runtime-template.d.ts +28 -0
  69. package/dist/emit/frontend/runtime-template.d.ts.map +1 -0
  70. package/dist/emit/frontend/runtime-template.js +218 -0
  71. package/dist/emit/frontend/runtime-template.js.map +1 -0
  72. package/dist/emit/frontend/ssg.d.ts +8 -0
  73. package/dist/emit/frontend/ssg.d.ts.map +1 -0
  74. package/dist/emit/frontend/ssg.js +219 -0
  75. package/dist/emit/frontend/ssg.js.map +1 -0
  76. package/dist/emit/registry.d.ts +17 -0
  77. package/dist/emit/registry.d.ts.map +1 -0
  78. package/dist/emit/registry.js +38 -0
  79. package/dist/emit/registry.js.map +1 -0
  80. package/dist/emit/static-site/css.d.ts +5 -0
  81. package/dist/emit/static-site/css.d.ts.map +1 -0
  82. package/dist/emit/static-site/css.js +872 -0
  83. package/dist/emit/static-site/css.js.map +1 -0
  84. package/dist/emit/static-site/enhancements.d.ts +11 -0
  85. package/dist/emit/static-site/enhancements.d.ts.map +1 -0
  86. package/dist/emit/static-site/enhancements.js +266 -0
  87. package/dist/emit/static-site/enhancements.js.map +1 -0
  88. package/dist/emit/static-site/static-site-html.d.ts +3 -0
  89. package/dist/emit/static-site/static-site-html.d.ts.map +1 -0
  90. package/dist/emit/static-site/static-site-html.js +1172 -0
  91. package/dist/emit/static-site/static-site-html.js.map +1 -0
  92. package/dist/emit/utils/sdk.d.ts +15 -0
  93. package/dist/emit/utils/sdk.d.ts.map +1 -0
  94. package/dist/emit/utils/sdk.js +34 -0
  95. package/dist/emit/utils/sdk.js.map +1 -0
  96. package/dist/extensions/context.d.ts +23 -0
  97. package/dist/extensions/context.d.ts.map +1 -0
  98. package/dist/extensions/context.js +43 -0
  99. package/dist/extensions/context.js.map +1 -0
  100. package/dist/index.d.ts +32 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +135 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/ir/decl/backend.raw.schema.d.ts +128 -0
  105. package/dist/ir/decl/backend.raw.schema.d.ts.map +1 -0
  106. package/dist/ir/decl/backend.raw.schema.js +24 -0
  107. package/dist/ir/decl/backend.raw.schema.js.map +1 -0
  108. package/dist/ir/decl/bundle.d.ts +15 -0
  109. package/dist/ir/decl/bundle.d.ts.map +1 -0
  110. package/dist/ir/decl/bundle.js +8 -0
  111. package/dist/ir/decl/bundle.js.map +1 -0
  112. package/dist/ir/decl/cli.raw.schema.d.ts +133 -0
  113. package/dist/ir/decl/cli.raw.schema.d.ts.map +1 -0
  114. package/dist/ir/decl/cli.raw.schema.js +20 -0
  115. package/dist/ir/decl/cli.raw.schema.js.map +1 -0
  116. package/dist/ir/decl/frontend.raw.schema.d.ts +6631 -0
  117. package/dist/ir/decl/frontend.raw.schema.d.ts.map +1 -0
  118. package/dist/ir/decl/frontend.raw.schema.js +272 -0
  119. package/dist/ir/decl/frontend.raw.schema.js.map +1 -0
  120. package/dist/ir/decl/index.d.ts +6 -0
  121. package/dist/ir/decl/index.d.ts.map +1 -0
  122. package/dist/ir/decl/index.js +6 -0
  123. package/dist/ir/decl/index.js.map +1 -0
  124. package/dist/ir/decl/normalize.schema.d.ts +9154 -0
  125. package/dist/ir/decl/normalize.schema.d.ts.map +1 -0
  126. package/dist/ir/decl/normalize.schema.js +71 -0
  127. package/dist/ir/decl/normalize.schema.js.map +1 -0
  128. package/dist/ir/domain/backend.d.ts +19 -0
  129. package/dist/ir/domain/backend.d.ts.map +1 -0
  130. package/dist/ir/domain/backend.js +3 -0
  131. package/dist/ir/domain/backend.js.map +1 -0
  132. package/dist/ir/domain/cli.d.ts +18 -0
  133. package/dist/ir/domain/cli.d.ts.map +1 -0
  134. package/dist/ir/domain/cli.js +2 -0
  135. package/dist/ir/domain/cli.js.map +1 -0
  136. package/dist/ir/domain/frontend/index.d.ts +190 -0
  137. package/dist/ir/domain/frontend/index.d.ts.map +1 -0
  138. package/dist/ir/domain/frontend/index.js +2 -0
  139. package/dist/ir/domain/frontend/index.js.map +1 -0
  140. package/dist/ir/domain/frontend.d.ts +2 -0
  141. package/dist/ir/domain/frontend.d.ts.map +1 -0
  142. package/dist/ir/domain/frontend.js +3 -0
  143. package/dist/ir/domain/frontend.js.map +1 -0
  144. package/dist/ir/frontend-contract.d.ts +187 -0
  145. package/dist/ir/frontend-contract.d.ts.map +1 -0
  146. package/dist/ir/frontend-contract.js +6 -0
  147. package/dist/ir/frontend-contract.js.map +1 -0
  148. package/dist/ir/target/backend.d.ts +11 -0
  149. package/dist/ir/target/backend.d.ts.map +1 -0
  150. package/dist/ir/target/backend.js +2 -0
  151. package/dist/ir/target/backend.js.map +1 -0
  152. package/dist/ir/target/backend.policy.d.ts +896 -0
  153. package/dist/ir/target/backend.policy.d.ts.map +1 -0
  154. package/dist/ir/target/backend.policy.js +106 -0
  155. package/dist/ir/target/backend.policy.js.map +1 -0
  156. package/dist/ir/target/cli.d.ts +3 -0
  157. package/dist/ir/target/cli.d.ts.map +1 -0
  158. package/dist/ir/target/cli.js +2 -0
  159. package/dist/ir/target/cli.js.map +1 -0
  160. package/dist/ir/target/electron.d.ts +99 -0
  161. package/dist/ir/target/electron.d.ts.map +1 -0
  162. package/dist/ir/target/electron.js +2 -0
  163. package/dist/ir/target/electron.js.map +1 -0
  164. package/dist/ir/target/electron.policy.d.ts +7015 -0
  165. package/dist/ir/target/electron.policy.d.ts.map +1 -0
  166. package/dist/ir/target/electron.policy.js +119 -0
  167. package/dist/ir/target/electron.policy.js.map +1 -0
  168. package/dist/ir/target/frontend.d.ts +12 -0
  169. package/dist/ir/target/frontend.d.ts.map +1 -0
  170. package/dist/ir/target/frontend.js +2 -0
  171. package/dist/ir/target/frontend.js.map +1 -0
  172. package/dist/ir/target/frontend.policy.d.ts +268 -0
  173. package/dist/ir/target/frontend.policy.d.ts.map +1 -0
  174. package/dist/ir/target/frontend.policy.js +33 -0
  175. package/dist/ir/target/frontend.policy.js.map +1 -0
  176. package/dist/ir/target/index.d.ts +6 -0
  177. package/dist/ir/target/index.d.ts.map +1 -0
  178. package/dist/ir/target/index.js +6 -0
  179. package/dist/ir/target/index.js.map +1 -0
  180. package/dist/ir/target/static-site.d.ts +18 -0
  181. package/dist/ir/target/static-site.d.ts.map +1 -0
  182. package/dist/ir/target/static-site.js +2 -0
  183. package/dist/ir/target/static-site.js.map +1 -0
  184. package/dist/ir/target/static-site.policy.d.ts +2911 -0
  185. package/dist/ir/target/static-site.policy.d.ts.map +1 -0
  186. package/dist/ir/target/static-site.policy.js +127 -0
  187. package/dist/ir/target/static-site.policy.js.map +1 -0
  188. package/dist/lowering/backend.d.ts +4 -0
  189. package/dist/lowering/backend.d.ts.map +1 -0
  190. package/dist/lowering/backend.js +57 -0
  191. package/dist/lowering/backend.js.map +1 -0
  192. package/dist/lowering/cli.d.ts +4 -0
  193. package/dist/lowering/cli.d.ts.map +1 -0
  194. package/dist/lowering/cli.js +22 -0
  195. package/dist/lowering/cli.js.map +1 -0
  196. package/dist/lowering/engine.d.ts +18 -0
  197. package/dist/lowering/engine.d.ts.map +1 -0
  198. package/dist/lowering/engine.js +47 -0
  199. package/dist/lowering/engine.js.map +1 -0
  200. package/dist/lowering/frontend.d.ts +9 -0
  201. package/dist/lowering/frontend.d.ts.map +1 -0
  202. package/dist/lowering/frontend.js +246 -0
  203. package/dist/lowering/frontend.js.map +1 -0
  204. package/dist/lowering/targets/to-backend.d.ts +9 -0
  205. package/dist/lowering/targets/to-backend.d.ts.map +1 -0
  206. package/dist/lowering/targets/to-backend.js +55 -0
  207. package/dist/lowering/targets/to-backend.js.map +1 -0
  208. package/dist/lowering/targets/to-cli.d.ts +4 -0
  209. package/dist/lowering/targets/to-cli.d.ts.map +1 -0
  210. package/dist/lowering/targets/to-cli.js +11 -0
  211. package/dist/lowering/targets/to-cli.js.map +1 -0
  212. package/dist/lowering/targets/to-electron.d.ts +30 -0
  213. package/dist/lowering/targets/to-electron.d.ts.map +1 -0
  214. package/dist/lowering/targets/to-electron.js +87 -0
  215. package/dist/lowering/targets/to-electron.js.map +1 -0
  216. package/dist/lowering/targets/to-frontend.d.ts +4 -0
  217. package/dist/lowering/targets/to-frontend.d.ts.map +1 -0
  218. package/dist/lowering/targets/to-frontend.js +30 -0
  219. package/dist/lowering/targets/to-frontend.js.map +1 -0
  220. package/dist/lowering/targets/to-static-site.d.ts +16 -0
  221. package/dist/lowering/targets/to-static-site.d.ts.map +1 -0
  222. package/dist/lowering/targets/to-static-site.js +30 -0
  223. package/dist/lowering/targets/to-static-site.js.map +1 -0
  224. package/dist/mappers/index.d.ts +12 -0
  225. package/dist/mappers/index.d.ts.map +1 -0
  226. package/dist/mappers/index.js +60 -0
  227. package/dist/mappers/index.js.map +1 -0
  228. package/dist/types/extension.d.ts +3 -0
  229. package/dist/types/extension.d.ts.map +1 -0
  230. package/dist/types/extension.js +2 -0
  231. package/dist/types/extension.js.map +1 -0
  232. package/dist/utils/array.d.ts +2 -0
  233. package/dist/utils/array.d.ts.map +1 -0
  234. package/dist/utils/array.js +4 -0
  235. package/dist/utils/array.js.map +1 -0
  236. package/dist/utils/index.d.ts +3 -0
  237. package/dist/utils/index.d.ts.map +1 -0
  238. package/dist/utils/index.js +3 -0
  239. package/dist/utils/index.js.map +1 -0
  240. package/dist/utils/string.d.ts +13 -0
  241. package/dist/utils/string.d.ts.map +1 -0
  242. package/dist/utils/string.js +56 -0
  243. package/dist/utils/string.js.map +1 -0
  244. package/package.json +112 -0
@@ -0,0 +1,1172 @@
1
+ import path from "node:path";
2
+ import crypto from "node:crypto";
3
+ import { emitterEngine } from "../engine.js";
4
+ import { appendCustomCss, emitStaticSiteCss } from "./css.js";
5
+ import { emitEnhancements } from "./enhancements.js";
6
+ const FALLBACK_RULES = [
7
+ { component: "form", fallback: "Render static placeholder block (no inputs, no submission)." },
8
+ { component: "themeToggle", fallback: "Render static badge text, no interactivity." },
9
+ { component: "layout.tabs", fallback: "Render tabs as stacked sections with labels." },
10
+ ];
11
+ const highlighterCache = new Map();
12
+ let warnedHighlight = false;
13
+ function normalizeLang(input) {
14
+ const lang = input.trim().toLowerCase();
15
+ if (!lang)
16
+ return "text";
17
+ const map = {
18
+ ts: "typescript",
19
+ js: "javascript",
20
+ bash: "shellscript",
21
+ sh: "shellscript",
22
+ shell: "shellscript",
23
+ zsh: "shellscript",
24
+ };
25
+ return map[lang] ?? lang;
26
+ }
27
+ async function getHighlighter(theme, lang) {
28
+ const key = `${theme}::${lang}`;
29
+ if (highlighterCache.has(key))
30
+ return highlighterCache.get(key) ?? null;
31
+ const promise = (async () => {
32
+ try {
33
+ const shiki = await import("shiki/bundle/full");
34
+ return await shiki.getHighlighter({ themes: [theme], langs: [lang] });
35
+ }
36
+ catch (_) {
37
+ return null;
38
+ }
39
+ })();
40
+ highlighterCache.set(key, promise);
41
+ return promise;
42
+ }
43
+ async function highlightCode(snippet, language, theme, warnings) {
44
+ const normalized = normalizeLang(language);
45
+ const highlighter = await getHighlighter(theme, normalized);
46
+ if (!highlighter) {
47
+ if (!warnedHighlight) {
48
+ warnings.push({
49
+ code: "highlight_fallback",
50
+ message: "Shiki not available; falling back to plain code blocks. Run npm install if dependencies are missing.",
51
+ });
52
+ warnedHighlight = true;
53
+ }
54
+ return null;
55
+ }
56
+ try {
57
+ return highlighter.codeToHtml(snippet, { lang: normalized, theme });
58
+ }
59
+ catch (_) {
60
+ warnings.push({
61
+ code: "highlight_fallback",
62
+ message: `Failed to highlight code for language "${normalized}".`,
63
+ context: normalized,
64
+ });
65
+ return null;
66
+ }
67
+ }
68
+ function escapeHtml(input) {
69
+ return input
70
+ .replace(/&/g, "&")
71
+ .replace(/</g, "&lt;")
72
+ .replace(/>/g, "&gt;")
73
+ .replace(/"/g, "&quot;")
74
+ .replace(/'/g, "&#39;");
75
+ }
76
+ function escapeAttr(input) {
77
+ return escapeHtml(input);
78
+ }
79
+ function safeAttr(value) {
80
+ if (value === null || value === undefined)
81
+ return "";
82
+ return escapeAttr(String(value));
83
+ }
84
+ function normalizeBaseUrl(baseUrl) {
85
+ const normalized = baseUrl.startsWith("/") ? baseUrl : `/${baseUrl}`;
86
+ return normalized.endsWith("/") ? normalized : `${normalized}/`;
87
+ }
88
+ function isDynamicRoute(routePath) {
89
+ return /[:*]/.test(routePath);
90
+ }
91
+ function normalizeRoutePath(routePath) {
92
+ if (!routePath.startsWith("/"))
93
+ return `/${routePath}`;
94
+ return routePath;
95
+ }
96
+ function toFilePath(routePath, trailingSlash) {
97
+ const cleaned = normalizeRoutePath(routePath).replace(/\/+$/, "");
98
+ if (cleaned === "" || cleaned === "/")
99
+ return "index.html";
100
+ const relative = cleaned.replace(/^\//, "");
101
+ if (trailingSlash)
102
+ return path.join(relative, "index.html");
103
+ return `${relative}.html`;
104
+ }
105
+ function toHref(baseUrl, routePath, trailingSlash) {
106
+ const normalizedBase = normalizeBaseUrl(baseUrl);
107
+ const cleaned = normalizeRoutePath(routePath).replace(/\/+$/, "");
108
+ if (cleaned === "" || cleaned === "/")
109
+ return normalizedBase;
110
+ const suffix = trailingSlash ? `${cleaned}/` : cleaned;
111
+ return `${normalizedBase.replace(/\/+$/, "")}${suffix}`;
112
+ }
113
+ function isExternalUrl(url) {
114
+ return /^https?:\/\//i.test(url);
115
+ }
116
+ function buildExternalRel(security) {
117
+ const parts = [];
118
+ if (security?.externalLinks?.noopener)
119
+ parts.push("noopener");
120
+ if (security?.externalLinks?.noreferrer)
121
+ parts.push("noreferrer");
122
+ return parts.join(" ");
123
+ }
124
+ function renderWarningBox(message) {
125
+ return `<div class="irgen-warning">${escapeHtml(message)}</div>`;
126
+ }
127
+ function safeText(value) {
128
+ if (value === null || value === undefined)
129
+ return "";
130
+ return escapeHtml(String(value));
131
+ }
132
+ function slugify(input) {
133
+ return input
134
+ .toLowerCase()
135
+ .replace(/[^a-z0-9]+/g, "-")
136
+ .replace(/^-+|-+$/g, "");
137
+ }
138
+ function nextHeadingId(text, ctx) {
139
+ const base = slugify(text) || "section";
140
+ const existing = ctx.usedIds.get(base) ?? 0;
141
+ const next = existing + 1;
142
+ ctx.usedIds.set(base, next);
143
+ return next === 1 ? base : `${base}-${next}`;
144
+ }
145
+ function renderHeading(level, text, ctx, includeCopy = true) {
146
+ const id = nextHeadingId(text, ctx);
147
+ ctx.headings.push({ level, text, id });
148
+ const button = includeCopy
149
+ ? `<button class="irgen-heading-copy" type="button" data-irgen-copy-anchor="#${safeAttr(id)}" aria-label="Copy link to ${safeAttr(text)}">
150
+ <span class="irgen-icon" aria-hidden="true">
151
+ <svg viewBox="0 0 24 24" role="presentation"><path d="M10 13a5 5 0 0 0 7.07 0l2.83-2.83a5 5 0 1 0-7.07-7.07L10.5 4.1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M14 11a5 5 0 0 0-7.07 0L4.1 13.93a5 5 0 0 0 7.07 7.07L13.5 19.9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
152
+ </span>
153
+ </button>`
154
+ : "";
155
+ return `<h${level} id="${safeAttr(id)}" class="irgen-heading"><span class="irgen-heading-text">${safeText(text)}</span>${button}</h${level}>`;
156
+ }
157
+ function resolveMarkdownLink(raw, linkCtx) {
158
+ const trimmed = raw.trim();
159
+ if (/^(mailto:|tel:)/i.test(trimmed) || isExternalUrl(trimmed)) {
160
+ const rel = buildExternalRel(linkCtx.security);
161
+ return { href: trimmed, rel: rel || undefined };
162
+ }
163
+ if (trimmed.startsWith("#"))
164
+ return { href: trimmed };
165
+ let [pathPart, hash] = trimmed.split("#");
166
+ if (!pathPart)
167
+ pathPart = linkCtx.pagePath;
168
+ if (!pathPart.startsWith("/"))
169
+ pathPart = `/${pathPart}`;
170
+ if (pathPart.endsWith(".md"))
171
+ pathPart = pathPart.slice(0, -3);
172
+ const normalized = normalizeRoutePath(pathPart);
173
+ const targetPath = linkCtx.pagePaths.get(normalized) ?? normalized;
174
+ let href = toHref(linkCtx.baseUrl, targetPath, linkCtx.trailingSlash);
175
+ if (hash)
176
+ href += `#${hash}`;
177
+ return { href };
178
+ }
179
+ function renderInlineText(text, linkCtx) {
180
+ const parts = [];
181
+ const linkRe = /\[([^\]]+)\]\(([^)]+)\)/g;
182
+ let lastIndex = 0;
183
+ let match;
184
+ const renderNoLinks = (value) => {
185
+ const parts = [];
186
+ const codeRe = /`([^`]+)`/g;
187
+ let last = 0;
188
+ let codeMatch;
189
+ while ((codeMatch = codeRe.exec(value)) !== null) {
190
+ const plain = escapeHtml(value.slice(last, codeMatch.index))
191
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
192
+ .replace(/\*([^*]+)\*/g, "<em>$1</em>");
193
+ parts.push(plain);
194
+ parts.push(`<code>${escapeHtml(codeMatch[1])}</code>`);
195
+ last = codeMatch.index + codeMatch[0].length;
196
+ }
197
+ const tail = escapeHtml(value.slice(last))
198
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
199
+ .replace(/\*([^*]+)\*/g, "<em>$1</em>");
200
+ parts.push(tail);
201
+ return parts.join("");
202
+ };
203
+ while ((match = linkRe.exec(text)) !== null) {
204
+ parts.push(renderNoLinks(text.slice(lastIndex, match.index)));
205
+ const label = renderNoLinks(match[1]);
206
+ const { href, rel } = resolveMarkdownLink(match[2], linkCtx);
207
+ const relAttr = rel ? ` rel="${safeAttr(rel)}"` : "";
208
+ parts.push(`<a href="${safeAttr(href)}"${relAttr}>${label}</a>`);
209
+ lastIndex = match.index + match[0].length;
210
+ }
211
+ parts.push(renderNoLinks(text.slice(lastIndex)));
212
+ return parts.join("");
213
+ }
214
+ async function renderMarkdown(input, ir, ctx, warnings, linkCtx) {
215
+ const text = String(input ?? "");
216
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
217
+ const htmlParts = [];
218
+ let paragraph = [];
219
+ let listItems = [];
220
+ let listType = null;
221
+ let inCode = false;
222
+ let codeLang = "";
223
+ let codeLines = [];
224
+ const flushParagraph = () => {
225
+ if (!paragraph.length)
226
+ return;
227
+ const body = renderInlineText(paragraph.join(" "), linkCtx);
228
+ htmlParts.push(`<p>${body}</p>`);
229
+ paragraph = [];
230
+ };
231
+ const flushList = () => {
232
+ if (!listType || !listItems.length) {
233
+ listType = null;
234
+ listItems = [];
235
+ return;
236
+ }
237
+ const items = listItems.map((item) => `<li>${renderInlineText(item, linkCtx)}</li>`).join("");
238
+ htmlParts.push(`<${listType}>${items}</${listType}>`);
239
+ listType = null;
240
+ listItems = [];
241
+ };
242
+ for (const rawLine of lines) {
243
+ const line = rawLine.replace(/\s+$/, "");
244
+ const fenceMatch = line.match(/^```(\w+)?\s*$/);
245
+ if (inCode) {
246
+ if (fenceMatch) {
247
+ const snippet = codeLines.join("\n");
248
+ htmlParts.push(await renderCodeBlock(ir, { snippet, language: codeLang }, warnings));
249
+ inCode = false;
250
+ codeLang = "";
251
+ codeLines = [];
252
+ }
253
+ else {
254
+ codeLines.push(rawLine);
255
+ }
256
+ continue;
257
+ }
258
+ if (fenceMatch) {
259
+ flushParagraph();
260
+ flushList();
261
+ inCode = true;
262
+ codeLang = fenceMatch[1] ?? "text";
263
+ continue;
264
+ }
265
+ if (!line.trim()) {
266
+ flushParagraph();
267
+ flushList();
268
+ continue;
269
+ }
270
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
271
+ if (headingMatch) {
272
+ flushParagraph();
273
+ flushList();
274
+ const levelRaw = headingMatch[1].length;
275
+ const textValue = headingMatch[2].trim();
276
+ const level = levelRaw === 1 ? 2 : 3;
277
+ htmlParts.push(renderHeading(level, textValue, ctx));
278
+ continue;
279
+ }
280
+ const orderedMatch = line.match(/^\d+\.\s+(.*)$/);
281
+ const unorderedMatch = line.match(/^[-*]\s+(.*)$/);
282
+ if (orderedMatch || unorderedMatch) {
283
+ flushParagraph();
284
+ const nextType = orderedMatch ? "ol" : "ul";
285
+ if (listType && listType !== nextType)
286
+ flushList();
287
+ listType = nextType;
288
+ listItems.push((orderedMatch ?? unorderedMatch)[1].trim());
289
+ continue;
290
+ }
291
+ paragraph.push(line.trim());
292
+ }
293
+ if (inCode) {
294
+ const snippet = codeLines.join("\n");
295
+ htmlParts.push(await renderCodeBlock(ir, { snippet, language: codeLang || "text" }, warnings));
296
+ }
297
+ flushParagraph();
298
+ flushList();
299
+ return `<div class="irgen-markdown">${htmlParts.join("\n")}</div>`;
300
+ }
301
+ function renderActionLinks(actions, security) {
302
+ if (!Array.isArray(actions) || actions.length === 0)
303
+ return "";
304
+ const links = actions.map((a) => {
305
+ const href = a?.href ? safeAttr(a.href) : "#";
306
+ const label = safeText(a?.label ?? "Action");
307
+ const rel = isExternalUrl(String(a?.href ?? "")) ? buildExternalRel(security) : "";
308
+ const relAttr = rel ? ` rel="${safeAttr(rel)}"` : "";
309
+ return `<a class="irgen-action-link" href="${href}"${relAttr}>${label}</a>`;
310
+ }).join("");
311
+ return `<div class="irgen-actions">${links}</div>`;
312
+ }
313
+ function renderItemList(items, ordered) {
314
+ if (!Array.isArray(items) || items.length === 0)
315
+ return "";
316
+ const tag = ordered ? "ol" : "ul";
317
+ const entries = items.map((i) => {
318
+ const title = i?.title ? `<strong>${safeText(i.title)}</strong>` : "";
319
+ const desc = i?.description ? `<span>${safeText(i.description)}</span>` : "";
320
+ const value = i?.value ? `<span>${safeText(i.value)}</span>` : "";
321
+ const label = i?.label ? `<span>${safeText(i.label)}</span>` : "";
322
+ const body = [title, desc, value, label].filter(Boolean).join(" ");
323
+ return `<li>${body || safeText(i)}</li>`;
324
+ }).join("");
325
+ return `<${tag} class="irgen-list">${entries}</${tag}>`;
326
+ }
327
+ function renderStatsTable(items) {
328
+ if (!Array.isArray(items) || items.length === 0)
329
+ return "";
330
+ const rows = items.map((i) => {
331
+ const label = safeText(i?.label ?? i?.title ?? "");
332
+ const value = safeText(i?.value ?? "");
333
+ if (!label && !value)
334
+ return "";
335
+ return `<tr><th>${label}</th><td>${value}</td></tr>`;
336
+ }).filter(Boolean).join("");
337
+ if (!rows)
338
+ return "";
339
+ return `<table class="irgen-table"><tbody>${rows}</tbody></table>`;
340
+ }
341
+ function renderMarketing(marketing, ctx, security) {
342
+ const kind = marketing?.kind ?? "generic";
343
+ const title = marketing.title ? renderHeading(2, marketing.title, ctx, false) : "";
344
+ const subtitle = marketing.subtitle ? `<p>${safeText(marketing.subtitle)}</p>` : "";
345
+ const badge = marketing.badge ? `<span class="irgen-badge">${safeText(marketing.badge)}</span>` : "";
346
+ const actions = renderActionLinks(marketing.actions ?? [], security);
347
+ const items = marketing.items ?? [];
348
+ if (kind === "hero") {
349
+ return `<section class="irgen-marketing irgen-hero">${badge}${title}${subtitle}${actions}</section>`;
350
+ }
351
+ if (kind === "features" || kind === "logos" || kind === "testimonials") {
352
+ const list = renderItemList(items, false);
353
+ const align = marketing.align === "center" ? " irgen-align-center" : "";
354
+ return `<section class="irgen-marketing irgen-${kind}${align}">${title}${subtitle}${list}${actions}</section>`;
355
+ }
356
+ if (kind === "faq") {
357
+ const list = renderItemList(items, false);
358
+ return `<section class="irgen-marketing irgen-faq">${title}${subtitle}${list}</section>`;
359
+ }
360
+ if (kind === "timeline") {
361
+ const list = renderItemList(items, true);
362
+ return `<section class="irgen-marketing irgen-timeline">${title}${subtitle}${list}</section>`;
363
+ }
364
+ if (kind === "stats") {
365
+ const table = renderStatsTable(items);
366
+ return `<section class="irgen-marketing irgen-stats">${title}${subtitle}${table}</section>`;
367
+ }
368
+ if (kind === "cta") {
369
+ return `<section class="irgen-marketing irgen-callout-links">${actions}</section>`;
370
+ }
371
+ const list = renderItemList(items, false);
372
+ return `<section class="irgen-marketing">${badge}${title}${subtitle}${list}${actions}</section>`;
373
+ }
374
+ async function renderCodeBlock(ir, codeBlock, warnings) {
375
+ const mode = ir.policies.staticSite.codeHighlight?.mode ?? "pre";
376
+ const theme = ir.policies.staticSite.codeHighlight?.theme ?? "github-dark";
377
+ const addCopy = ir.policies.staticSite.codeHighlight?.addCopyButton ?? true;
378
+ const language = normalizeLang(codeBlock.language ?? "text");
379
+ const snippet = codeBlock.snippet ?? "";
380
+ const copyButton = addCopy
381
+ ? `<button class="irgen-copy-button irgen-icon-button" type="button" data-irgen-copy-code>
382
+ <span class="irgen-icon" aria-hidden="true">
383
+ <svg viewBox="0 0 24 24" role="presentation"><path d="M9 9a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-8a2 2 0 0 1-2-2V9z" stroke="currentColor" stroke-width="2" fill="none"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
384
+ </span>
385
+ <span class="irgen-button-label">Copy</span>
386
+ </button>`
387
+ : "";
388
+ if (language === "mermaid" && ir.policies.staticSite.enhancements?.features?.includes("mermaid")) {
389
+ return `<div class="irgen-code irgen-mermaid" data-irgen-code data-irgen-lang="mermaid">${copyButton}<pre class="mermaid">${escapeHtml(snippet)}</pre></div>`;
390
+ }
391
+ if (mode === "pre") {
392
+ const highlighted = await highlightCode(snippet, language, theme, warnings);
393
+ if (highlighted) {
394
+ return `<div class="irgen-code" data-irgen-code data-irgen-lang="${safeAttr(language)}">${copyButton}${highlighted}</div>`;
395
+ }
396
+ }
397
+ const langClass = language ? ` class="language-${safeAttr(language)}"` : "";
398
+ const attrs = mode === "client" ? ` data-irgen-highlight="client"` : "";
399
+ return `<div class="irgen-code" data-irgen-code data-irgen-lang="${safeAttr(language)}"${attrs}>${copyButton}<pre><code${langClass}>${escapeHtml(snippet)}</code></pre></div>`;
400
+ }
401
+ async function renderComponent(ir, component, componentsByName, ctx, warnings, linkCtx) {
402
+ if (component.html) {
403
+ throw new Error(`static-site emitter does not allow component.html (component: ${component.name}). Use markdown in component.content instead.`);
404
+ }
405
+ const renderContentParts = async () => {
406
+ const parts = [];
407
+ if (component.content)
408
+ parts.push(await renderMarkdown(component.content, ir, ctx, warnings, linkCtx));
409
+ if (component.codeBlock) {
410
+ parts.push(await renderCodeBlock(ir, component.codeBlock, warnings));
411
+ }
412
+ if (component.button?.label) {
413
+ parts.push(`<button type="button">${escapeHtml(component.button.label)}</button>`);
414
+ }
415
+ return parts.join("");
416
+ };
417
+ const props = component.props ?? {};
418
+ const hideTitle = props.hideTitle === "true" || props.hideTitle === "1";
419
+ const customTitle = typeof props.title === "string" && props.title.trim().length > 0 ? props.title.trim() : "";
420
+ const headingText = hideTitle ? "" : (customTitle || component.name);
421
+ const renderDefaultHeading = () => headingText ? renderHeading(2, headingText, ctx) : "";
422
+ if (component.form && component.form.fields?.length) {
423
+ warnings.push({
424
+ code: "component_fallback",
425
+ message: `Component "${component.name}" uses form; rendered as static placeholder.`,
426
+ context: component.name,
427
+ });
428
+ const heading = renderDefaultHeading();
429
+ return `<section class="irgen-component">${heading}${renderWarningBox("Form component rendered as static placeholder.")}</section>`;
430
+ }
431
+ if (component.themeToggle) {
432
+ warnings.push({
433
+ code: "component_fallback",
434
+ message: `Component "${component.name}" uses themeToggle; rendered as static placeholder.`,
435
+ context: component.name,
436
+ });
437
+ const heading = renderDefaultHeading();
438
+ return `<section class="irgen-component">${heading}${renderWarningBox("Theme toggle rendered as static placeholder.")}</section>`;
439
+ }
440
+ if (component.layout?.kind === "tabs") {
441
+ warnings.push({
442
+ code: "component_fallback",
443
+ message: `Component "${component.name}" uses layout.tabs; rendered as stacked sections.`,
444
+ context: component.name,
445
+ });
446
+ const tabs = component.layout.tabs ?? [];
447
+ const tabHtmlParts = [];
448
+ for (const t of tabs) {
449
+ const label = t.label ? renderHeading(3, t.label, ctx, false) : "";
450
+ const content = t.content ? await renderMarkdown(t.content, ir, ctx, warnings, linkCtx) : "";
451
+ let items = "";
452
+ if (Array.isArray(t.items)) {
453
+ const itemParts = [];
454
+ for (const n of t.items) {
455
+ const child = componentsByName.get(n);
456
+ itemParts.push(child ? await renderComponent(ir, child, componentsByName, ctx, warnings, linkCtx) : renderWarningBox(`Missing component: ${n}`));
457
+ }
458
+ items = itemParts.join("");
459
+ }
460
+ tabHtmlParts.push(`<section class="irgen-tab">${label}${content}${items}</section>`);
461
+ }
462
+ const tabHtml = tabHtmlParts.join("");
463
+ return `<section class="irgen-component">${tabHtml || renderWarningBox("Tabs rendered as stacked sections.")}</section>`;
464
+ }
465
+ if (component.layout) {
466
+ const title = component.layout.title ? renderHeading(2, component.layout.title, ctx) : "";
467
+ const contentParts = await renderContentParts();
468
+ const itemParts = [];
469
+ for (const n of component.layout.items ?? []) {
470
+ const child = componentsByName.get(n);
471
+ itemParts.push(child ? await renderComponent(ir, child, componentsByName, ctx, warnings, linkCtx) : renderWarningBox(`Missing component: ${n}`));
472
+ }
473
+ const items = itemParts.join("");
474
+ return `<section class="irgen-component irgen-layout">${title}${contentParts}${items}</section>`;
475
+ }
476
+ if (component.marketing) {
477
+ return `<section class="irgen-component">${renderMarketing(component.marketing, ctx, ir.policies.staticSite.security)}</section>`;
478
+ }
479
+ if (component.agentChat) {
480
+ const title = component.agentChat.title ?? "AI Copilot Integration";
481
+ const messages = Array.isArray(component.agentChat.messages) ? component.agentChat.messages : [];
482
+ const items = messages.map((msg) => {
483
+ const role = msg.role === "agent" ? "agent" : "user";
484
+ const label = msg.label ?? (role === "agent" ? "A" : "U");
485
+ const content = msg.content ? escapeHtml(String(msg.content)) : "";
486
+ return `<div class="irgen-chat-row irgen-chat-${role}"><div class="irgen-chat-avatar">${escapeHtml(String(label))}</div><div class="irgen-chat-bubble">${content}</div></div>`;
487
+ }).join("");
488
+ return `<section class="irgen-component"><div class="irgen-agent-chat"><div class="irgen-chat-title">${escapeHtml(String(title))}</div>${items}</div></section>`;
489
+ }
490
+ if (component.cliUsage) {
491
+ const title = component.cliUsage.title ?? "Standard Usage";
492
+ const command = component.cliUsage.command ? escapeHtml(String(component.cliUsage.command)) : "";
493
+ const options = Array.isArray(component.cliUsage.options) ? component.cliUsage.options : [];
494
+ const optionItems = options.map((opt) => {
495
+ const flag = opt?.flag ? escapeHtml(String(opt.flag)) : "";
496
+ const desc = opt?.description ? escapeHtml(String(opt.description)) : "";
497
+ return `<div class="irgen-cli-option"><div class="irgen-cli-flag">${flag}</div><div class="irgen-cli-desc">${desc}</div></div>`;
498
+ }).join("");
499
+ const optionsBlock = optionItems ? `<div class="irgen-cli-options">${optionItems}</div>` : "";
500
+ return `<section class="irgen-component"><div class="irgen-cli-usage"><h3>${escapeHtml(String(title))}</h3><pre class="irgen-cli-command"><code>${command}</code></pre>${optionsBlock}</div></section>`;
501
+ }
502
+ const partsHtml = await renderContentParts();
503
+ if (!partsHtml) {
504
+ warnings.push({
505
+ code: "component_fallback",
506
+ message: `Component "${component.name}" has no renderable content; rendered as placeholder.`,
507
+ context: component.name,
508
+ });
509
+ const heading = renderDefaultHeading();
510
+ return `<section class="irgen-component">${heading}${renderWarningBox("Component rendered as empty placeholder.")}</section>`;
511
+ }
512
+ const heading = renderDefaultHeading();
513
+ return `<section class="irgen-component">${heading}${partsHtml}</section>`;
514
+ }
515
+ function renderBreadcrumbs(baseUrl, routePath, trailingSlash, pageName) {
516
+ const cleaned = normalizeRoutePath(routePath).replace(/\/+$/, "");
517
+ if (cleaned === "" || cleaned === "/")
518
+ return "";
519
+ const parts = cleaned.replace(/^\//, "").split("/").filter(Boolean);
520
+ const crumbs = [];
521
+ let acc = "";
522
+ for (const part of parts.slice(0, -1)) {
523
+ acc += `/${part}`;
524
+ const href = toHref(baseUrl, acc, trailingSlash);
525
+ crumbs.push(`<li><a href="${safeAttr(href)}">${safeText(part)}</a></li>`);
526
+ }
527
+ crumbs.push(`<li><span>${safeText(pageName)}</span></li>`);
528
+ return `<nav class="irgen-breadcrumbs"><ol>${crumbs.join("")}</ol></nav>`;
529
+ }
530
+ function formatTitle(title, template) {
531
+ if (!template)
532
+ return title;
533
+ if (template.includes("%s"))
534
+ return template.replace("%s", title);
535
+ return `${title} ${template}`;
536
+ }
537
+ function escapeXml(input) {
538
+ return input
539
+ .replace(/&/g, "&amp;")
540
+ .replace(/</g, "&lt;")
541
+ .replace(/>/g, "&gt;")
542
+ .replace(/"/g, "&quot;")
543
+ .replace(/'/g, "&apos;");
544
+ }
545
+ function buildSitemapXml(urls) {
546
+ const entries = urls.map((u) => ` <url><loc>${escapeXml(u)}</loc></url>`).join("\n");
547
+ return [
548
+ '<?xml version="1.0" encoding="UTF-8"?>',
549
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
550
+ entries,
551
+ "</urlset>",
552
+ ].join("\n");
553
+ }
554
+ function buildRobotsTxt(sitemapUrl) {
555
+ const lines = ["User-agent: *", "Allow: /"];
556
+ if (sitemapUrl)
557
+ lines.push(`Sitemap: ${sitemapUrl}`);
558
+ return `${lines.join("\n")}\n`;
559
+ }
560
+ function buildToc(ctx) {
561
+ const entries = ctx.headings.filter((h) => h.level >= 2 && h.level <= 3);
562
+ if (entries.length < 2)
563
+ return "";
564
+ const items = entries.map((h) => {
565
+ return `<li class="irgen-toc-level-${h.level}"><a href="#${safeAttr(h.id)}" data-irgen-toc-link="${safeAttr(h.id)}">${safeText(h.text)}</a></li>`;
566
+ }).join("");
567
+ return `<nav class="irgen-toc" data-irgen-toc><h2>On this page</h2><ul>${items}</ul></nav>`;
568
+ }
569
+ function validateHeadings(ctx, warnings, pageName) {
570
+ const h1Count = ctx.headings.filter((h) => h.level === 1).length;
571
+ if (h1Count !== 1) {
572
+ warnings.push({
573
+ code: "component_fallback",
574
+ message: `Page "${pageName}" has ${h1Count} H1 headings; expected exactly 1.`,
575
+ context: pageName,
576
+ });
577
+ }
578
+ let prevLevel = 0;
579
+ for (const h of ctx.headings) {
580
+ if (prevLevel > 0 && h.level - prevLevel > 1) {
581
+ warnings.push({
582
+ code: "component_fallback",
583
+ message: `Heading level jump from H${prevLevel} to H${h.level} in page "${pageName}".`,
584
+ context: pageName,
585
+ });
586
+ break;
587
+ }
588
+ prevLevel = h.level;
589
+ }
590
+ }
591
+ function assetHref(filePath, assetName) {
592
+ const rel = path.posix.relative(path.posix.dirname(filePath), `assets/${assetName}`);
593
+ return rel || `assets/${assetName}`;
594
+ }
595
+ function relHref(fromFilePath, targetRelPath) {
596
+ const rel = path.posix.relative(path.posix.dirname(fromFilePath), targetRelPath);
597
+ return rel || targetRelPath;
598
+ }
599
+ async function renderPage(ir, page, pagesForNav, warnings, caps, emitAppJs, assets, fontAssets) {
600
+ const policy = ir.policies.staticSite;
601
+ const baseUrl = policy.baseUrl ?? "/";
602
+ const trailingSlash = policy.trailingSlash ?? true;
603
+ const filePath = toFilePath(page.path, trailingSlash);
604
+ const cssHref = assetHref(filePath, assets.styleCss);
605
+ const prismCssHref = assets.prismCss ? assetHref(filePath, assets.prismCss) : "";
606
+ const prismJsHref = assets.prismJs ? assetHref(filePath, assets.prismJs) : "";
607
+ const appJsHref = assets.appJs ? assetHref(filePath, assets.appJs) : "";
608
+ const searchJsHref = assets.searchJs ? assetHref(filePath, assets.searchJs) : "";
609
+ const rawTitle = page.name || policy.seo?.defaultTitle || ir.appName;
610
+ const title = formatTitle(rawTitle, policy.seo?.titleTemplate);
611
+ const description = page.description || policy.seo?.defaultDescription || "";
612
+ const themeMode = policy.theme?.mode ?? "auto";
613
+ const htmlAttrs = ['lang="en"'];
614
+ const themeScript = `
615
+ <script>
616
+ (function() {
617
+ try {
618
+ var pref = localStorage.getItem("irgen-theme");
619
+ var theme = pref || (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
620
+ document.documentElement.setAttribute("data-theme", theme);
621
+ } catch (_) {}
622
+ })();
623
+ </script>
624
+ `.trim();
625
+ if (themeMode === "light" || themeMode === "dark") {
626
+ htmlAttrs.push(`data-theme="${themeMode}"`);
627
+ }
628
+ const highlightMode = policy.codeHighlight?.mode ?? "pre";
629
+ const includeClientHighlight = highlightMode === "client";
630
+ const canonicalBase = policy.seo?.canonicalBaseUrl;
631
+ const canonicalUrl = canonicalBase ? toHref(canonicalBase, page.path, trailingSlash) : null;
632
+ const cspValue = policy.security?.csp?.enabled ? (policy.security?.csp?.value ?? "default-src 'self'; base-uri 'self'; object-src 'none'") : null;
633
+ const pageByPath = new Map();
634
+ const pageById = new Map();
635
+ for (const p of pagesForNav) {
636
+ const normalized = normalizeRoutePath(p.path);
637
+ pageByPath.set(normalized, p);
638
+ const id = normalized.replace(/^\/|\/$/g, "");
639
+ if (id)
640
+ pageById.set(id, p);
641
+ }
642
+ const renderNavItem = (p) => {
643
+ const href = toHref(baseUrl, p.path, trailingSlash);
644
+ return `<li><a href="${safeAttr(href)}">${escapeHtml(p.name)}</a></li>`;
645
+ };
646
+ const sidebarGroups = policy.sidebar?.groups ?? [];
647
+ const navItems = sidebarGroups.length
648
+ ? sidebarGroups.map((group) => {
649
+ const items = (group.items ?? []).map((item) => {
650
+ const normalized = normalizeRoutePath(item);
651
+ const byPath = pageByPath.get(normalized);
652
+ if (byPath)
653
+ return renderNavItem(byPath);
654
+ const byId = pageById.get(item.replace(/^\/|\/$/g, ""));
655
+ if (byId)
656
+ return renderNavItem(byId);
657
+ return "";
658
+ }).filter(Boolean).join("");
659
+ if (!items)
660
+ return "";
661
+ return `<li class="irgen-nav-group"><span>${escapeHtml(group.label)}</span><ul>${items}</ul></li>`;
662
+ }).filter(Boolean).join("")
663
+ : pagesForNav.map(renderNavItem).join("");
664
+ const breadcrumbs = renderBreadcrumbs(baseUrl, page.path, trailingSlash, page.name);
665
+ const searchIndexFile = policy.search?.indexFile ?? "assets/search-index.json";
666
+ const searchIndexHref = relHref(filePath, searchIndexFile);
667
+ const mermaidJsHref = assets.mermaidJs ? assetHref(filePath, assets.mermaidJs) : "";
668
+ const navbarLinks = policy.navbar?.links ?? [];
669
+ const headerNav = navbarLinks.length
670
+ ? `<nav class="irgen-header-nav"><ul>${navbarLinks.map((link) => {
671
+ const href = link?.href ? safeAttr(link.href) : "#";
672
+ const label = safeText(link?.label ?? "Link");
673
+ const rel = isExternalUrl(String(link?.href ?? "")) ? buildExternalRel(policy.security) : "";
674
+ const relAttr = rel ? ` rel="${safeAttr(rel)}"` : "";
675
+ return `<li><a href="${href}"${relAttr}>${label}</a></li>`;
676
+ }).join("")}</ul></nav>`
677
+ : "";
678
+ const searchBox = caps.search
679
+ ? `<div class="irgen-search" data-irgen-search data-irgen-search-index="${safeAttr(searchIndexHref)}">
680
+ <span class="irgen-search-icon" aria-hidden="true">
681
+ <svg viewBox="0 0 24 24" role="presentation"><circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" fill="none"/><path d="M20 20L16.5 16.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
682
+ </span>
683
+ <input type="search" placeholder="Search..." aria-label="Search" data-irgen-search-input />
684
+ <div class="irgen-search-results" data-irgen-search-results></div>
685
+ </div>`
686
+ : "";
687
+ const preloadFonts = fontAssets.map((asset) => {
688
+ const href = relHref(filePath, asset);
689
+ return ` <link rel="preload" href="${safeAttr(href)}" as="font" type="font/woff2" crossorigin />`;
690
+ }).join("\n");
691
+ const componentsByName = new Map();
692
+ for (const c of ir.components ?? [])
693
+ componentsByName.set(c.name, c);
694
+ const ctx = { headings: [], usedIds: new Map() };
695
+ const pagePaths = new Map();
696
+ for (const p of pagesForNav) {
697
+ const normalized = normalizeRoutePath(p.path);
698
+ pagePaths.set(normalized, p.path);
699
+ }
700
+ const linkCtx = {
701
+ baseUrl,
702
+ trailingSlash,
703
+ pagePath: page.path,
704
+ pagePaths,
705
+ security: policy.security,
706
+ };
707
+ const normalizeText = (input) => input.trim().toLowerCase();
708
+ const heroMatchesTitle = (page.components ?? []).some((component) => {
709
+ const hero = component?.marketing;
710
+ if (!hero || hero.kind !== "hero" || !hero.title)
711
+ return false;
712
+ return normalizeText(hero.title) === normalizeText(page.name ?? "");
713
+ });
714
+ const pageHeading = page.hideHeader || heroMatchesTitle ? "" : renderHeading(1, page.name, ctx);
715
+ if (!pageHeading && heroMatchesTitle) {
716
+ // Preserve a single H1 in the document outline when hero title replaces page heading.
717
+ renderHeading(1, page.name, ctx, false);
718
+ }
719
+ const bodyParts = [];
720
+ for (const c of page.components ?? []) {
721
+ bodyParts.push(await renderComponent(ir, c, componentsByName, ctx, warnings, linkCtx));
722
+ }
723
+ const bodyContent = bodyParts.join("");
724
+ const toc = buildToc(ctx);
725
+ validateHeadings(ctx, warnings, page.name);
726
+ return [
727
+ "<!DOCTYPE html>",
728
+ `<html ${htmlAttrs.join(" ")}>`,
729
+ "<head>",
730
+ ' <meta charset="UTF-8" />',
731
+ ' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
732
+ cspValue ? ` <meta http-equiv="Content-Security-Policy" content="${safeAttr(cspValue)}" />` : "",
733
+ description ? ` <meta name="description" content="${safeAttr(description)}" />` : "",
734
+ canonicalUrl ? ` <link rel="canonical" href="${safeAttr(canonicalUrl)}" />` : "",
735
+ ` <link rel="stylesheet" href="${safeAttr(cssHref)}" />`,
736
+ themeScript,
737
+ includeClientHighlight && assets.prismCss ? ` <link rel="stylesheet" href="${safeAttr(prismCssHref)}" />` : "",
738
+ preloadFonts,
739
+ ` <title>${escapeHtml(title)}</title>`,
740
+ policy.seo?.openGraph?.enabled ? ` <meta property="og:title" content="${safeAttr(title)}" />` : "",
741
+ policy.seo?.openGraph?.enabled && description ? ` <meta property="og:description" content="${safeAttr(description)}" />` : "",
742
+ policy.seo?.openGraph?.enabled ? ` <meta property="og:type" content="website" />` : "",
743
+ policy.seo?.openGraph?.enabled && canonicalUrl ? ` <meta property="og:url" content="${safeAttr(canonicalUrl)}" />` : "",
744
+ "</head>",
745
+ "<body>",
746
+ ` <a class="irgen-skip-link" href="#irgen-main">Skip to content</a>`,
747
+ ` <header class="irgen-header">`,
748
+ ` <div class="irgen-header-left">`,
749
+ ` <div class="irgen-site-title">${escapeHtml(ir.appName)}</div>`,
750
+ headerNav,
751
+ ` </div>`,
752
+ ` <div class="irgen-header-actions">`,
753
+ searchBox,
754
+ caps.sidebarToggle ? ` <button class="irgen-sidebar-toggle irgen-icon-button" type="button" data-irgen-sidebar-toggle aria-controls="irgen-sidebar" aria-expanded="true">
755
+ <span class="irgen-icon" aria-hidden="true">
756
+ <svg viewBox="0 0 24 24" role="presentation"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
757
+ </span>
758
+ <span class="irgen-button-label">Menu</span>
759
+ </button>` : "",
760
+ caps.themeToggle ? ` <button class="irgen-theme-toggle irgen-icon-button" type="button" data-irgen-theme-toggle aria-pressed="false">
761
+ <span class="irgen-icon" aria-hidden="true">
762
+ <svg viewBox="0 0 24 24" role="presentation"><path d="M12 3a1 1 0 0 1 1 1v1.5a1 1 0 1 1-2 0V4a1 1 0 0 1 1-1zM12 18.5a1 1 0 0 1 1 1V21a1 1 0 1 1-2 0v-1.5a1 1 0 0 1 1-1zM4 11a1 1 0 0 1 1 1h1.5a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1 1 1 0 0 1 1-1zm13.5 1a1 1 0 1 1 0-2H19a1 1 0 1 1 0 2h-1.5zM6.2 6.2a1 1 0 0 1 1.4 0l1.06 1.06a1 1 0 1 1-1.42 1.42L6.2 7.6a1 1 0 0 1 0-1.4zm10.6 10.6a1 1 0 0 1 1.4 0l1.06 1.06a1 1 0 1 1-1.42 1.42l-1.04-1.06a1 1 0 0 1 0-1.4zM6.2 17.8a1 1 0 0 1 0-1.4l1.06-1.06a1 1 0 1 1 1.42 1.42L7.6 17.8a1 1 0 0 1-1.4 0zm10.6-10.6a1 1 0 0 1 0-1.4l1.06-1.06a1 1 0 1 1 1.42 1.42l-1.04 1.04a1 1 0 0 1-1.4 0zM12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8z" fill="currentColor"/></svg>
763
+ </span>
764
+ <span class="irgen-button-label">Theme</span>
765
+ </button>` : "",
766
+ ` </div>`,
767
+ ` </header>`,
768
+ ` <div class="irgen-layout-grid">`,
769
+ ` <aside class="irgen-sidebar" id="irgen-sidebar"><nav class="irgen-nav"><ul>${navItems}</ul></nav></aside>`,
770
+ ` <main class="irgen-main" id="irgen-main">`,
771
+ ` ${breadcrumbs}`,
772
+ ` ${toc}`,
773
+ ` ${pageHeading}`,
774
+ ` ${bodyContent}`,
775
+ ` </main>`,
776
+ ` </div>`,
777
+ ` <footer class="irgen-footer"><small>Generated by irgen</small></footer>`,
778
+ includeClientHighlight && assets.prismJs ? ` <script defer src="${safeAttr(prismJsHref)}"></script>` : "",
779
+ caps.search && assets.searchJs ? ` <script defer src="${safeAttr(searchJsHref)}"></script>` : "",
780
+ emitAppJs && assets.appJs ? ` <script defer src="${safeAttr(appJsHref)}"></script>` : "",
781
+ caps.mermaid && assets.mermaidJs ? ` <script defer src="${safeAttr(mermaidJsHref)}"></script>` : "",
782
+ "</body>",
783
+ "</html>",
784
+ ].join("\n");
785
+ }
786
+ async function copyDirRecursive(srcDir, destDir) {
787
+ const fs = await import("node:fs/promises");
788
+ await fs.mkdir(destDir, { recursive: true });
789
+ const entries = await fs.readdir(srcDir, { withFileTypes: true });
790
+ for (const entry of entries) {
791
+ const srcPath = path.join(srcDir, entry.name);
792
+ const destPath = path.join(destDir, entry.name);
793
+ if (entry.isDirectory()) {
794
+ await copyDirRecursive(srcPath, destPath);
795
+ }
796
+ else if (entry.isFile()) {
797
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
798
+ try {
799
+ await fs.access(destPath);
800
+ continue;
801
+ }
802
+ catch (_) {
803
+ await fs.copyFile(srcPath, destPath);
804
+ }
805
+ }
806
+ }
807
+ }
808
+ async function copyPublicAssets(outDir, publicDir) {
809
+ if (!publicDir)
810
+ return;
811
+ const srcDir = path.isAbsolute(publicDir) ? publicDir : path.resolve(process.cwd(), publicDir);
812
+ try {
813
+ const fs = await import("node:fs/promises");
814
+ const stat = await fs.stat(srcDir);
815
+ if (!stat.isDirectory())
816
+ return;
817
+ await copyDirRecursive(srcDir, outDir);
818
+ }
819
+ catch (_) {
820
+ console.warn(`[static-site emitter] Public assets folder not found: ${srcDir}`);
821
+ }
822
+ }
823
+ async function collectFontAssets(outDir) {
824
+ const fs = await import("node:fs/promises");
825
+ const results = [];
826
+ async function walk(dir) {
827
+ const entries = await fs.readdir(dir, { withFileTypes: true });
828
+ for (const entry of entries) {
829
+ const full = path.join(dir, entry.name);
830
+ if (entry.isDirectory()) {
831
+ await walk(full);
832
+ }
833
+ else if (entry.isFile() && entry.name.endsWith(".woff2")) {
834
+ const rel = path.relative(outDir, full).split(path.sep).join(path.posix.sep);
835
+ results.push(rel);
836
+ }
837
+ }
838
+ }
839
+ try {
840
+ await walk(outDir);
841
+ }
842
+ catch (_) {
843
+ return [];
844
+ }
845
+ return results;
846
+ }
847
+ function stripTags(value) {
848
+ return value.replace(/<[^>]*>/g, " ");
849
+ }
850
+ function collectComponentText(component) {
851
+ const parts = [];
852
+ if (component.name)
853
+ parts.push(String(component.name));
854
+ if (component.content)
855
+ parts.push(String(component.content));
856
+ if (component.codeBlock?.snippet)
857
+ parts.push(String(component.codeBlock.snippet));
858
+ if (component.button?.label)
859
+ parts.push(String(component.button.label));
860
+ if (component.layout?.title)
861
+ parts.push(String(component.layout.title));
862
+ if (component.agentChat?.title)
863
+ parts.push(String(component.agentChat.title));
864
+ if (Array.isArray(component.agentChat?.messages)) {
865
+ for (const msg of component.agentChat.messages) {
866
+ if (msg?.content)
867
+ parts.push(String(msg.content));
868
+ }
869
+ }
870
+ if (component.cliUsage?.title)
871
+ parts.push(String(component.cliUsage.title));
872
+ if (component.cliUsage?.command)
873
+ parts.push(String(component.cliUsage.command));
874
+ if (Array.isArray(component.cliUsage?.options)) {
875
+ for (const opt of component.cliUsage.options) {
876
+ if (opt?.flag)
877
+ parts.push(String(opt.flag));
878
+ if (opt?.description)
879
+ parts.push(String(opt.description));
880
+ }
881
+ }
882
+ if (component.marketing?.title)
883
+ parts.push(String(component.marketing.title));
884
+ if (component.marketing?.subtitle)
885
+ parts.push(String(component.marketing.subtitle));
886
+ if (Array.isArray(component.marketing?.items)) {
887
+ for (const item of component.marketing.items) {
888
+ if (item?.title)
889
+ parts.push(String(item.title));
890
+ if (item?.description)
891
+ parts.push(String(item.description));
892
+ if (item?.value)
893
+ parts.push(String(item.value));
894
+ if (item?.label)
895
+ parts.push(String(item.label));
896
+ }
897
+ }
898
+ if (Array.isArray(component.marketing?.actions)) {
899
+ for (const action of component.marketing.actions) {
900
+ if (action?.label)
901
+ parts.push(String(action.label));
902
+ }
903
+ }
904
+ return parts.join(" ");
905
+ }
906
+ function collectPageText(page) {
907
+ const parts = [];
908
+ if (page.name)
909
+ parts.push(String(page.name));
910
+ if (page.description)
911
+ parts.push(String(page.description));
912
+ for (const c of page.components ?? []) {
913
+ parts.push(collectComponentText(c));
914
+ }
915
+ return parts.join(" ");
916
+ }
917
+ async function emitSearchIndex(outDir, pages, baseUrl, trailingSlash, indexFile) {
918
+ const fs = await import("node:fs/promises");
919
+ const items = pages.map((p, idx) => ({
920
+ id: idx + 1,
921
+ title: p.name,
922
+ description: p.description ?? "",
923
+ url: toHref(baseUrl, p.path, trailingSlash),
924
+ content: collectPageText(p),
925
+ }));
926
+ const abs = path.join(outDir, indexFile);
927
+ await fs.mkdir(path.dirname(abs), { recursive: true });
928
+ await fs.writeFile(abs, JSON.stringify({ items }, null, 2), "utf-8");
929
+ }
930
+ async function hashAndRenameAsset(outDir, assetName) {
931
+ const fs = await import("node:fs/promises");
932
+ const assetPath = path.join(outDir, "assets", assetName);
933
+ try {
934
+ const content = await fs.readFile(assetPath);
935
+ const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 8);
936
+ const ext = path.extname(assetName);
937
+ const base = path.basename(assetName, ext);
938
+ const hashedName = `${base}.${hash}${ext}`;
939
+ const hashedPath = path.join(outDir, "assets", hashedName);
940
+ await fs.rename(assetPath, hashedPath);
941
+ return hashedName;
942
+ }
943
+ catch (_) {
944
+ return assetName;
945
+ }
946
+ }
947
+ async function copySearchLibrary(outDir) {
948
+ try {
949
+ const { createRequire } = await import("node:module");
950
+ const require = createRequire(import.meta.url);
951
+ let pkgDir = null;
952
+ try {
953
+ const pkgPath = require.resolve("minisearch/package.json");
954
+ pkgDir = path.dirname(pkgPath);
955
+ }
956
+ catch (_) {
957
+ pkgDir = null;
958
+ }
959
+ const candidates = [
960
+ ...(pkgDir ? [
961
+ path.join(pkgDir, "dist", "umd", "index.min.js"),
962
+ path.join(pkgDir, "dist", "umd", "index.js"),
963
+ path.join(pkgDir, "dist", "minisearch.min.js"),
964
+ path.join(pkgDir, "dist", "minisearch.js"),
965
+ ] : []),
966
+ path.resolve(process.cwd(), "node_modules", "minisearch", "dist", "umd", "index.min.js"),
967
+ path.resolve(process.cwd(), "node_modules", "minisearch", "dist", "umd", "index.js"),
968
+ path.resolve(process.cwd(), "node_modules", "minisearch", "dist", "minisearch.min.js"),
969
+ path.resolve(process.cwd(), "node_modules", "minisearch", "dist", "minisearch.js"),
970
+ ];
971
+ let resolved = null;
972
+ for (const c of candidates) {
973
+ try {
974
+ await (await import("node:fs/promises")).access(c);
975
+ resolved = c;
976
+ break;
977
+ }
978
+ catch (_) {
979
+ // try next
980
+ }
981
+ }
982
+ if (!resolved)
983
+ return null;
984
+ const fs = await import("node:fs/promises");
985
+ const outPath = path.join(outDir, "assets", "minisearch.js");
986
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
987
+ await fs.copyFile(resolved, outPath);
988
+ return "minisearch.js";
989
+ }
990
+ catch (_) {
991
+ return null;
992
+ }
993
+ }
994
+ async function copyMermaidLibrary(outDir) {
995
+ const candidates = [
996
+ path.resolve(process.cwd(), "node_modules", "mermaid", "dist", "mermaid.min.js"),
997
+ ];
998
+ for (const candidate of candidates) {
999
+ try {
1000
+ const fs = await import("node:fs/promises");
1001
+ await fs.access(candidate);
1002
+ const outPath = path.join(outDir, "assets", "mermaid.min.js");
1003
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
1004
+ await fs.copyFile(candidate, outPath);
1005
+ return "mermaid.min.js";
1006
+ }
1007
+ catch (_) {
1008
+ // try next
1009
+ }
1010
+ }
1011
+ return null;
1012
+ }
1013
+ export async function emitStaticSite(ir, outDir) {
1014
+ const policy = ir.policies.staticSite;
1015
+ const policyOut = (policy.outDir ?? ".").trim();
1016
+ const finalOutDir = policyOut === "." || policyOut === "" ? outDir : path.join(outDir, policyOut);
1017
+ const warnings = [];
1018
+ const hasCode = (ir.pages ?? []).some((p) => (p.components ?? []).some((c) => c.codeBlock))
1019
+ || (ir.components ?? []).some((c) => c.codeBlock);
1020
+ const hasMermaid = (ir.pages ?? []).some((p) => (p.components ?? []).some((c) => {
1021
+ const lang = String(c?.codeBlock?.language ?? "").toLowerCase();
1022
+ return lang === "mermaid";
1023
+ })) || (ir.components ?? []).some((c) => {
1024
+ const lang = String(c?.codeBlock?.language ?? "").toLowerCase();
1025
+ return lang === "mermaid";
1026
+ });
1027
+ const baseUrl = policy.baseUrl ?? "/";
1028
+ const trailingSlash = policy.trailingSlash ?? true;
1029
+ const sitemapBase = policy.seo?.canonicalBaseUrl ?? baseUrl;
1030
+ const features = policy.enhancements?.features ?? [];
1031
+ const enhancementsEnabled = policy.enhancements?.enabled ?? true;
1032
+ const caps = {
1033
+ sidebarToggle: enhancementsEnabled && features.includes("sidebarToggle"),
1034
+ copyCode: enhancementsEnabled && features.includes("copyCode") && hasCode && (policy.codeHighlight?.addCopyButton ?? true),
1035
+ themeToggle: enhancementsEnabled && features.includes("themeToggle"),
1036
+ tocScrollSpy: enhancementsEnabled && features.includes("tocScrollSpy"),
1037
+ search: enhancementsEnabled && features.includes("search") && (policy.search?.mode ?? "none") === "client_index",
1038
+ mermaid: enhancementsEnabled && features.includes("mermaid") && hasMermaid,
1039
+ };
1040
+ const emitAppJs = caps.sidebarToggle || caps.copyCode || caps.themeToggle || caps.tocScrollSpy || caps.search || caps.mermaid;
1041
+ const fs = await import("node:fs/promises");
1042
+ await fs.mkdir(finalOutDir, { recursive: true });
1043
+ await emitStaticSiteCss(finalOutDir, { accentColor: policy.theme?.accentColor });
1044
+ await copyPublicAssets(finalOutDir, policy.assets?.publicDir);
1045
+ if (policy.customCssPath) {
1046
+ await appendCustomCss(finalOutDir, policy.customCssPath);
1047
+ }
1048
+ if (emitAppJs) {
1049
+ await emitEnhancements(finalOutDir, caps);
1050
+ }
1051
+ let prismCss = "prism.css";
1052
+ let prismJs = "prism.js";
1053
+ if ((policy.codeHighlight?.mode ?? "pre") === "client" && hasCode) {
1054
+ try {
1055
+ const { createRequire } = await import("node:module");
1056
+ const require = createRequire(import.meta.url);
1057
+ const prismJsPath = require.resolve("prismjs");
1058
+ const prismCssPath = require.resolve("prismjs/themes/prism.css");
1059
+ const prismJsOut = path.join(finalOutDir, "assets", "prism.js");
1060
+ const prismCssOut = path.join(finalOutDir, "assets", "prism.css");
1061
+ await fs.mkdir(path.dirname(prismJsOut), { recursive: true });
1062
+ await fs.copyFile(prismJsPath, prismJsOut);
1063
+ await fs.copyFile(prismCssPath, prismCssOut);
1064
+ }
1065
+ catch (err) {
1066
+ warnings.push({
1067
+ code: "highlight_fallback",
1068
+ message: "Failed to load Prism.js assets; client highlighting disabled.",
1069
+ });
1070
+ prismCss = "";
1071
+ prismJs = "";
1072
+ }
1073
+ }
1074
+ const assets = {
1075
+ styleCss: "style.css",
1076
+ prismCss: prismCss || undefined,
1077
+ prismJs: prismJs || undefined,
1078
+ appJs: emitAppJs ? "app.js" : undefined,
1079
+ searchJs: undefined,
1080
+ mermaidJs: undefined,
1081
+ };
1082
+ if (caps.search && policy.search?.mode === "client_index") {
1083
+ const searchLib = await copySearchLibrary(finalOutDir);
1084
+ if (searchLib) {
1085
+ assets.searchJs = searchLib;
1086
+ }
1087
+ else {
1088
+ warnings.push({
1089
+ code: "search_fallback",
1090
+ message: "MiniSearch asset not found; falling back to basic search. Run npm install if dependencies are missing.",
1091
+ });
1092
+ }
1093
+ }
1094
+ if (caps.mermaid) {
1095
+ const mermaidLib = await copyMermaidLibrary(finalOutDir);
1096
+ if (mermaidLib) {
1097
+ assets.mermaidJs = mermaidLib;
1098
+ }
1099
+ else {
1100
+ warnings.push({
1101
+ code: "component_fallback",
1102
+ message: "Mermaid asset not found; diagrams will render as plain code blocks.",
1103
+ });
1104
+ }
1105
+ }
1106
+ if (policy.assets?.hashing) {
1107
+ assets.styleCss = await hashAndRenameAsset(finalOutDir, assets.styleCss);
1108
+ if (assets.prismCss)
1109
+ assets.prismCss = await hashAndRenameAsset(finalOutDir, assets.prismCss);
1110
+ if (assets.prismJs)
1111
+ assets.prismJs = await hashAndRenameAsset(finalOutDir, assets.prismJs);
1112
+ if (assets.appJs)
1113
+ assets.appJs = await hashAndRenameAsset(finalOutDir, assets.appJs);
1114
+ if (assets.searchJs)
1115
+ assets.searchJs = await hashAndRenameAsset(finalOutDir, assets.searchJs);
1116
+ if (assets.mermaidJs)
1117
+ assets.mermaidJs = await hashAndRenameAsset(finalOutDir, assets.mermaidJs);
1118
+ }
1119
+ const fontAssets = await collectFontAssets(finalOutDir);
1120
+ const pages = (ir.pages ?? []).filter((p) => {
1121
+ if (isDynamicRoute(p.path)) {
1122
+ warnings.push({
1123
+ code: "route_skipped",
1124
+ message: `Skipping dynamic route "${p.path}" (static-site Phase 2).`,
1125
+ context: p.path,
1126
+ });
1127
+ return false;
1128
+ }
1129
+ return true;
1130
+ });
1131
+ if (caps.search && policy.search?.mode === "client_index") {
1132
+ const indexFile = policy.search?.indexFile ?? "assets/search-index.json";
1133
+ await emitSearchIndex(finalOutDir, pages, baseUrl, trailingSlash, indexFile);
1134
+ }
1135
+ for (const page of pages) {
1136
+ const filePath = toFilePath(page.path, policy.trailingSlash ?? true);
1137
+ const absPath = path.join(finalOutDir, filePath);
1138
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
1139
+ const html = await renderPage(ir, page, pages, warnings, caps, emitAppJs, assets, fontAssets);
1140
+ await fs.writeFile(absPath, html, "utf-8");
1141
+ }
1142
+ if (policy.seo?.sitemap?.enabled) {
1143
+ const urls = pages.map((p) => toHref(sitemapBase, p.path, trailingSlash));
1144
+ const sitemapXml = buildSitemapXml(urls);
1145
+ await fs.writeFile(path.join(finalOutDir, "sitemap.xml"), sitemapXml, "utf-8");
1146
+ }
1147
+ if (policy.seo?.robotsTxt?.enabled) {
1148
+ const sitemapUrl = policy.seo?.sitemap?.enabled
1149
+ ? toHref(sitemapBase, "/sitemap.xml", false)
1150
+ : null;
1151
+ const robotsTxt = buildRobotsTxt(sitemapUrl);
1152
+ await fs.writeFile(path.join(finalOutDir, "robots.txt"), robotsTxt, "utf-8");
1153
+ }
1154
+ if (warnings.length > 0) {
1155
+ console.warn("[static-site emitter] Warnings:");
1156
+ for (const w of warnings) {
1157
+ console.warn(`- ${w.code}: ${w.message}${w.context ? ` (${w.context})` : ""}`);
1158
+ }
1159
+ console.warn("[static-site emitter] Fallback rules:");
1160
+ for (const rule of FALLBACK_RULES) {
1161
+ console.warn(`- ${rule.component}: ${rule.fallback}`);
1162
+ }
1163
+ }
1164
+ }
1165
+ // Register emitter
1166
+ try {
1167
+ emitterEngine.registerEmitter("static-site-html", emitStaticSite);
1168
+ }
1169
+ catch (e) {
1170
+ // ignore if already registered
1171
+ }
1172
+ //# sourceMappingURL=static-site-html.js.map