karma-lang 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/TAGS_GUIDE.md +601 -0
- package/about.karma +4 -0
- package/components/card.karma +6 -0
- package/home.karma +5 -0
- package/karma-ai.js +454 -0
- package/karma-cli.js +41 -0
- package/karma-compiler.js +721 -0
- package/karma-runtime.js +44 -0
- package/karma-v1.0.0.zip +0 -0
- package/karma-validator.js +470 -0
- package/package.json +8 -0
- package/test.karma +4 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
// Karma — compiler (v1.6)
|
|
2
|
+
// Build one: node karma-compiler.js test.karma
|
|
3
|
+
// Build all: node karma-compiler.js --all
|
|
4
|
+
// Pro layouts: node karma-compiler.js test.karma --pro (or KARMA_PRO=1)
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const arg = process.argv[2];
|
|
14
|
+
const suggest = process.argv.includes("--suggest");
|
|
15
|
+
const buildAll = process.argv.includes("--all");
|
|
16
|
+
const isPro = process.argv.includes("--pro") || process.env.KARMA_PRO === "1";
|
|
17
|
+
|
|
18
|
+
const baseDir = process.cwd();
|
|
19
|
+
|
|
20
|
+
if (!arg && !buildAll) {
|
|
21
|
+
console.error('Usage: node karma-compiler.js <file.karma> [--suggest] [--pro] OR node karma-compiler.js --all [--suggest] [--pro]');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function compileOne(input) {
|
|
26
|
+
let src = fs.readFileSync(input, "utf8");
|
|
27
|
+
|
|
28
|
+
// ---------- helpers ----------
|
|
29
|
+
function escapeHTML(s) {
|
|
30
|
+
return String(s)
|
|
31
|
+
.replaceAll("&", "&")
|
|
32
|
+
.replaceAll("<", "<")
|
|
33
|
+
.replaceAll(">", ">")
|
|
34
|
+
.replaceAll('"', """);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getAttr(attrStr, name) {
|
|
38
|
+
const re = new RegExp(`${name}\\s*=\\s*"([^"]*)"`, "i");
|
|
39
|
+
const m = attrStr.match(re);
|
|
40
|
+
return m ? m[1] : "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function clampInt(v, min, max, fallback) {
|
|
44
|
+
const n = parseInt(v, 10);
|
|
45
|
+
if (Number.isNaN(n)) return fallback;
|
|
46
|
+
return Math.max(min, Math.min(max, n));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function karmaToHtmlHref(hrefRaw) {
|
|
50
|
+
if (!hrefRaw) return "#";
|
|
51
|
+
return hrefRaw.toLowerCase().endsWith(".karma")
|
|
52
|
+
? hrefRaw.replace(/\.karma$/i, ".html")
|
|
53
|
+
: hrefRaw;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Layouts resolve relative to the compiler file (NOT the shell)
|
|
57
|
+
function readLayoutTemplate(layoutName) {
|
|
58
|
+
const p = path.resolve(__dirname, "layouts", `${layoutName}.html`);
|
|
59
|
+
if (!fs.existsSync(p)) return null;
|
|
60
|
+
return fs.readFileSync(p, "utf8");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parse all PAGE attrs into variables for layouts: {{BRAND}}, {{TAGLINE}}, etc.
|
|
64
|
+
function extractPageVars(pageVars, layoutName, pageTitle, baseCSS, bodyHtml) {
|
|
65
|
+
const extra = {};
|
|
66
|
+
for (const key of Object.keys(pageVars)) {
|
|
67
|
+
if (key === "TITLE" || key === "LAYOUT") continue;
|
|
68
|
+
extra[key] = pageVars[key];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
title: pageTitle,
|
|
73
|
+
style: baseCSS,
|
|
74
|
+
content: bodyHtml,
|
|
75
|
+
year: String(new Date().getFullYear()),
|
|
76
|
+
layout: layoutName || "",
|
|
77
|
+
extra
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function applyLayout(templateHtml, vars) {
|
|
82
|
+
let out = templateHtml;
|
|
83
|
+
|
|
84
|
+
// core placeholders
|
|
85
|
+
out = out.replaceAll("{{title}}", vars.title || "");
|
|
86
|
+
out = out.replaceAll("{{style}}", vars.style || "");
|
|
87
|
+
out = out.replaceAll("{{content}}", vars.content || "");
|
|
88
|
+
out = out.replaceAll("{{year}}", vars.year || "");
|
|
89
|
+
out = out.replaceAll("{{layout}}", vars.layout || "");
|
|
90
|
+
|
|
91
|
+
// extra placeholders from PAGE attrs
|
|
92
|
+
for (const k of Object.keys(vars.extra || {})) {
|
|
93
|
+
out = out.replaceAll(`{{${k}}}`, String(vars.extra[k] ?? ""));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If template forgot {{style}}, inject it
|
|
97
|
+
if (!templateHtml.includes("{{style}}")) {
|
|
98
|
+
if (out.includes("</head>")) out = out.replace("</head>", `<style>${vars.style || ""}</style></head>`);
|
|
99
|
+
else out = `<style>${vars.style || ""}</style>\n${out}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------- components (optional) ----------
|
|
106
|
+
const components = {};
|
|
107
|
+
|
|
108
|
+
function loadComponents(filePath) {
|
|
109
|
+
const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
112
|
+
const line = lines[i].trim();
|
|
113
|
+
|
|
114
|
+
if (line.startsWith("component ")) {
|
|
115
|
+
const head = line.match(/^component\s+(\w+)\(([^)]*)\)\s*\{$/);
|
|
116
|
+
if (!head) continue;
|
|
117
|
+
|
|
118
|
+
const name = head[1];
|
|
119
|
+
const args = head[2].split(",").map(a => a.trim()).filter(Boolean);
|
|
120
|
+
const bodyLines = [];
|
|
121
|
+
|
|
122
|
+
i++;
|
|
123
|
+
while (i < lines.length && lines[i].trim() !== "}") {
|
|
124
|
+
bodyLines.push(lines[i]);
|
|
125
|
+
i++;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
components[name] = { args, body: bodyLines.join("\n") };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------- imports ----------
|
|
134
|
+
const importRe = /import\s+"([^"]+)"/g;
|
|
135
|
+
let im;
|
|
136
|
+
while ((im = importRe.exec(src))) {
|
|
137
|
+
const importedPath = path.resolve(baseDir, im[1]);
|
|
138
|
+
loadComponents(importedPath);
|
|
139
|
+
src = src.replace(im[0], "");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------- PAGE ----------
|
|
143
|
+
const pageMatch = src.match(/<PAGE\s+([^>]*)>([\s\S]*?)<\/PAGE>/i);
|
|
144
|
+
if (!pageMatch) {
|
|
145
|
+
console.error(`Karma error in ${input}: <PAGE TITLE="..."> ... </PAGE> is required.`);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const pageAttrs = pageMatch[1] || "";
|
|
150
|
+
// extract all PAGE attributes into a map
|
|
151
|
+
const pageVars = {};
|
|
152
|
+
const attrRe = /([A-Z_]+)\s*=\s*"([^"]*)"/gi;
|
|
153
|
+
let m;
|
|
154
|
+
while ((m = attrRe.exec(pageAttrs))) {
|
|
155
|
+
pageVars[m[1].toUpperCase()] = m[2];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let body = pageMatch[2] || "";
|
|
159
|
+
|
|
160
|
+
const pageTitle = escapeHTML(pageVars["TITLE"] || "");
|
|
161
|
+
|
|
162
|
+
// LAYOUT name from PAGE attr or <LAYOUT NAME="..."/>
|
|
163
|
+
let layoutName = pageVars["LAYOUT"] || "";
|
|
164
|
+
const layoutTagRe = /<LAYOUT([^\/>]*)\/>/i;
|
|
165
|
+
const lm = body.match(layoutTagRe);
|
|
166
|
+
if (!layoutName && lm) {
|
|
167
|
+
layoutName = getAttr(lm[1] || "", "NAME") || getAttr(lm[1] || "", "name") || "";
|
|
168
|
+
}
|
|
169
|
+
body = body.replace(/<LAYOUT([^\/>]*)\/>/gi, "");
|
|
170
|
+
|
|
171
|
+
// Expand components
|
|
172
|
+
for (const name in components) {
|
|
173
|
+
const { args, body: template } = components[name];
|
|
174
|
+
const re = new RegExp(`<${name}([^>]*)>([\\s\\S]*?)<\\/${name}>`, "g");
|
|
175
|
+
|
|
176
|
+
body = body.replace(re, (_, attrStr, inner) => {
|
|
177
|
+
let out = template.replaceAll("<slot/>", inner);
|
|
178
|
+
args.forEach(arg => {
|
|
179
|
+
const val = getAttr(attrStr, arg) || getAttr(attrStr, arg.toUpperCase());
|
|
180
|
+
if (!val) {
|
|
181
|
+
console.warn(`Warning: component <${name}> requires argument '${arg}' but it was not provided`);
|
|
182
|
+
}
|
|
183
|
+
out = out.replaceAll(`{{${arg}}}`, val || "");
|
|
184
|
+
});
|
|
185
|
+
return out;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function applyKarmaBlocks(html) {
|
|
190
|
+
// HEADING
|
|
191
|
+
html = html.replace(/<HEADING([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
192
|
+
const text = getAttr(attrStr, "TEXT") || getAttr(attrStr, "text");
|
|
193
|
+
const level = clampInt(getAttr(attrStr, "LEVEL") || getAttr(attrStr, "level") || "2", 1, 6, 2);
|
|
194
|
+
const size = getAttr(attrStr, "SIZE") || getAttr(attrStr, "size") || "";
|
|
195
|
+
|
|
196
|
+
if (!text) {
|
|
197
|
+
return `<div class="k-block k-placeholder">Add TEXT="Your heading"</div>`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const sizeClass = size ? ` k-heading-${escapeHTML(size)}` : "";
|
|
201
|
+
return `<h${level} class="k-heading k-heading-${level}${sizeClass}">${escapeHTML(text)}</h${level}>`;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// PARA
|
|
205
|
+
html = html.replace(/<PARA([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
206
|
+
const text = getAttr(attrStr, "TEXT") || getAttr(attrStr, "text");
|
|
207
|
+
if (!text) {
|
|
208
|
+
return `<div class="k-block k-paragraph"><p class="k-placeholder">Write your paragraph here…</p></div>`;
|
|
209
|
+
}
|
|
210
|
+
return `<div class="k-block k-paragraph"><p>${escapeHTML(text)}</p></div>`;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// IMAGE
|
|
214
|
+
html = html.replace(/<IMAGE([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
215
|
+
const src2 = getAttr(attrStr, "SRC") || getAttr(attrStr, "src");
|
|
216
|
+
const alt = getAttr(attrStr, "ALT") || getAttr(attrStr, "alt");
|
|
217
|
+
const title = getAttr(attrStr, "TITLE") || getAttr(attrStr, "title");
|
|
218
|
+
const caption = getAttr(attrStr, "CAPTION") || getAttr(attrStr, "caption");
|
|
219
|
+
const variant = (getAttr(attrStr, "VARIANT") || getAttr(attrStr, "variant") || "card").toLowerCase();
|
|
220
|
+
const align = (getAttr(attrStr, "POSITION") || getAttr(attrStr, "align") || "left").toLowerCase();
|
|
221
|
+
const linkRaw = getAttr(attrStr, "LINK") || getAttr(attrStr, "link");
|
|
222
|
+
|
|
223
|
+
const head = title ? `<div class="k-block-title">${escapeHTML(title)}</div>` : "";
|
|
224
|
+
|
|
225
|
+
const wrapMaybeLink = (innerHtml) => {
|
|
226
|
+
if (!linkRaw) return innerHtml;
|
|
227
|
+
const href = karmaToHtmlHref(linkRaw);
|
|
228
|
+
return `<a class="k-media-link" href="${escapeHTML(href)}">${innerHtml}</a>`;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (!src2) {
|
|
232
|
+
return `<div class="k-block k-image k-variant-${escapeHTML(variant)}">${head}
|
|
233
|
+
<div class="k-image-placeholder"><div class="k-image-icon">🖼️</div>
|
|
234
|
+
<div class="k-image-text">Add SRC="path/to/image.jpg"</div></div></div>`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const safeAlt = alt ? escapeHTML(alt) : "";
|
|
238
|
+
const cap = caption ? `<figcaption class="k-caption">${escapeHTML(caption)}</figcaption>` : "";
|
|
239
|
+
|
|
240
|
+
if (variant === "split") {
|
|
241
|
+
const cls = align === "right" ? "k-split k-split-right" : "k-split";
|
|
242
|
+
const img = `<img class="k-img" src="${escapeHTML(src2)}" alt="${safeAlt}">`;
|
|
243
|
+
return `<div class="k-block ${cls}">
|
|
244
|
+
<div class="k-split-media">${wrapMaybeLink(img)}</div>
|
|
245
|
+
<div class="k-split-content">${head || `<div class="k-block-title">Section</div>`}${cap || ""}</div>
|
|
246
|
+
</div>`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (variant === "hero") {
|
|
250
|
+
const hero = `<img class="k-hero-img" src="${escapeHTML(src2)}" alt="${safeAlt}">`;
|
|
251
|
+
return `<section class="k-hero">${wrapMaybeLink(hero)}
|
|
252
|
+
<div class="k-hero-overlay">${head || `<div class="k-block-title">Hero</div>`}${cap || ""}</div>
|
|
253
|
+
</section>`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const img = `<img class="k-img" src="${escapeHTML(src2)}" alt="${safeAlt}">`;
|
|
257
|
+
return `<div class="k-block k-image k-variant-card">${head}
|
|
258
|
+
<figure class="k-figure">${wrapMaybeLink(img)}${cap}</figure>
|
|
259
|
+
</div>`;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// LINK
|
|
263
|
+
html = html.replace(/<LINK([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
264
|
+
const text = getAttr(attrStr, "TEXT") || getAttr(attrStr, "text") || "Click here";
|
|
265
|
+
const hrefRaw = getAttr(attrStr, "HREF") || getAttr(attrStr, "href") || "#";
|
|
266
|
+
const variant = (getAttr(attrStr, "VARIANT") || getAttr(attrStr, "variant") || "primary").toLowerCase();
|
|
267
|
+
const href = karmaToHtmlHref(hrefRaw);
|
|
268
|
+
|
|
269
|
+
return `<div class="k-block k-link"><a class="k-btn k-btn-${escapeHTML(variant)}" href="${escapeHTML(href)}">${escapeHTML(text)}</a></div>`;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// NAV
|
|
273
|
+
html = html.replace(/<NAV([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
274
|
+
const variant = (getAttr(attrStr, "TYPE") || getAttr(attrStr, "variant") || "bar").toLowerCase();
|
|
275
|
+
const position = (getAttr(attrStr, "POSITION") || getAttr(attrStr, "position") || "top").toLowerCase();
|
|
276
|
+
const title = getAttr(attrStr, "TITLE") || getAttr(attrStr, "title");
|
|
277
|
+
const linksRaw = getAttr(attrStr, "LINKS") || getAttr(attrStr, "links");
|
|
278
|
+
|
|
279
|
+
const items = (linksRaw || "")
|
|
280
|
+
.split(",")
|
|
281
|
+
.map(s => s.trim())
|
|
282
|
+
.filter(Boolean)
|
|
283
|
+
.map(pair => {
|
|
284
|
+
const [label, hrefRaw] = pair.split(":").map(x => (x || "").trim());
|
|
285
|
+
const href = karmaToHtmlHref(hrefRaw || "#");
|
|
286
|
+
return { label: label || "Link", href };
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const head = title ? `<div class="k-block-title">${escapeHTML(title)}</div>` : "";
|
|
290
|
+
const linksHtml = items.length
|
|
291
|
+
? items.map(it => `<a class="k-nav-link" href="${escapeHTML(it.href)}">${escapeHTML(it.label)}</a>`).join("")
|
|
292
|
+
: `<span class="k-muted">Add LINKS="Home:home.karma, About:about.karma"</span>`;
|
|
293
|
+
|
|
294
|
+
return `<div class="k-block k-nav k-nav-${escapeHTML(variant)} k-pos-${escapeHTML(position)}">${head}<nav class="k-nav-inner">${linksHtml}</nav></div>`;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// TABLE
|
|
298
|
+
html = html.replace(/<TABLE([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
299
|
+
const title = getAttr(attrStr, "TITLE") || getAttr(attrStr, "title") || "Table";
|
|
300
|
+
const cols = clampInt(getAttr(attrStr, "COLS") || getAttr(attrStr, "cols") || "3", 2, 6, 3);
|
|
301
|
+
const rows = clampInt(getAttr(attrStr, "ROWS") || getAttr(attrStr, "rows") || "2", 2, 10, 2);
|
|
302
|
+
|
|
303
|
+
const th = Array.from({ length: cols }, (_, i) => `<th>Column ${i + 1}</th>`).join("");
|
|
304
|
+
const tr = Array.from({ length: rows }, () => `<tr>${Array.from({ length: cols }, () => `<td>Edit</td>`).join("")}</tr>`).join("");
|
|
305
|
+
|
|
306
|
+
return `<div class="k-block k-table"><div class="k-block-title">${escapeHTML(title)}</div>
|
|
307
|
+
<table class="k-table-el"><thead><tr>${th}</tr></thead><tbody>${tr}</tbody></table></div>`;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// FORM
|
|
311
|
+
html = html.replace(/<FORM([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
312
|
+
const title = getAttr(attrStr, "TITLE") || getAttr(attrStr, "title") || "Form";
|
|
313
|
+
const fieldsRaw = getAttr(attrStr, "FIELDS") || getAttr(attrStr, "fields") || "name,email,message";
|
|
314
|
+
const submitText = getAttr(attrStr, "SUBMIT") || getAttr(attrStr, "submit") || "Submit";
|
|
315
|
+
|
|
316
|
+
const fields = fieldsRaw.split(",").map(s => s.trim()).filter(Boolean);
|
|
317
|
+
const inputs = fields.map(f => {
|
|
318
|
+
const id = `k_${f.replace(/\W+/g, "_")}`;
|
|
319
|
+
const label = f.charAt(0).toUpperCase() + f.slice(1);
|
|
320
|
+
const isMessage = f.toLowerCase() === "message" || f.toLowerCase() === "notes";
|
|
321
|
+
return isMessage
|
|
322
|
+
? `<label class="k-label" for="${id}">${escapeHTML(label)}</label><textarea class="k-input" id="${id}" placeholder="Type here..."></textarea>`
|
|
323
|
+
: `<label class="k-label" for="${id}">${escapeHTML(label)}</label><input class="k-input" id="${id}" placeholder="Type here..." />`;
|
|
324
|
+
}).join("\n");
|
|
325
|
+
|
|
326
|
+
return `<div class="k-block k-form"><div class="k-block-title">${escapeHTML(title)}</div>
|
|
327
|
+
<form class="k-form-inner" action="#" method="post">
|
|
328
|
+
${inputs}
|
|
329
|
+
<button class="k-btn k-btn-primary" type="button">${escapeHTML(submitText)}</button>
|
|
330
|
+
<div class="k-muted k-small">Placeholder form. Add action later.</div>
|
|
331
|
+
</form></div>`;
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// VIDEO
|
|
335
|
+
html = html.replace(/<VIDEO([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
336
|
+
const src2 = getAttr(attrStr, "SRC") || getAttr(attrStr, "src");
|
|
337
|
+
const url = getAttr(attrStr, "URL") || getAttr(attrStr, "url");
|
|
338
|
+
const title = getAttr(attrStr, "TITLE") || getAttr(attrStr, "title") || "Video";
|
|
339
|
+
|
|
340
|
+
if (!src2 && !url) {
|
|
341
|
+
return `<div class="k-block k-video"><div class="k-block-title">${escapeHTML(title)}</div><div class="k-muted">Add SRC or URL.</div></div>`;
|
|
342
|
+
}
|
|
343
|
+
if (src2) {
|
|
344
|
+
return `<div class="k-block k-video"><div class="k-block-title">${escapeHTML(title)}</div><video class="k-video-el" controls src="${escapeHTML(src2)}"></video></div>`;
|
|
345
|
+
}
|
|
346
|
+
return `<div class="k-block k-video"><div class="k-block-title">${escapeHTML(title)}</div>
|
|
347
|
+
<div class="k-iframe-wrap"><iframe class="k-iframe" src="${escapeHTML(url)}" title="${escapeHTML(title)}" allowfullscreen></iframe></div></div>`;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// EMAIL
|
|
351
|
+
html = html.replace(/<EMAIL([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
352
|
+
const to = getAttr(attrStr, "TO") || getAttr(attrStr, "to");
|
|
353
|
+
const text = getAttr(attrStr, "TEXT") || getAttr(attrStr, "text") || "Email";
|
|
354
|
+
const subject = getAttr(attrStr, "SUBJECT") || getAttr(attrStr, "subject");
|
|
355
|
+
const body2 = getAttr(attrStr, "BODY") || getAttr(attrStr, "body");
|
|
356
|
+
|
|
357
|
+
if (!to) return `<div class="k-block k-link"><span class="k-muted">Add TO="name@domain.com"</span></div>`;
|
|
358
|
+
|
|
359
|
+
const params = [];
|
|
360
|
+
if (subject) params.push(`subject=${encodeURIComponent(subject)}`);
|
|
361
|
+
if (body2) params.push(`body=${encodeURIComponent(body2)}`);
|
|
362
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
363
|
+
|
|
364
|
+
return `<div class="k-block k-link"><a class="k-btn k-btn-ghost" href="mailto:${escapeHTML(to)}${qs}">${escapeHTML(text)}</a></div>`;
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// PHONE
|
|
368
|
+
html = html.replace(/<PHONE([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
369
|
+
const number = getAttr(attrStr, "NUMBER") || getAttr(attrStr, "number");
|
|
370
|
+
const text = getAttr(attrStr, "TEXT") || getAttr(attrStr, "text") || "Call";
|
|
371
|
+
if (!number) return `<div class="k-block k-link"><span class="k-muted">Add NUMBER="+1206..."</span></div>`;
|
|
372
|
+
const digits = number.replace(/[^\d+]/g, "");
|
|
373
|
+
if (!digits || digits === "+") return `<div class="k-block k-link"><span class="k-muted">Invalid phone number</span></div>`;
|
|
374
|
+
return `<div class="k-block k-link"><a class="k-btn k-btn-ghost" href="tel:${escapeHTML(digits)}">${escapeHTML(text)}</a></div>`;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// SECTION - Balanced paired wrapper tags
|
|
378
|
+
html = html.replace(/<SECTION([^>]*)>([\s\S]*?)<\/SECTION>/gi, (_, attrStr, inner) => {
|
|
379
|
+
const title = getAttr(attrStr, "TITLE") || getAttr(attrStr, "title");
|
|
380
|
+
const bg = (getAttr(attrStr, "BG") || getAttr(attrStr, "bg") || "default").toLowerCase();
|
|
381
|
+
const padding = getAttr(attrStr, "PADDING") || getAttr(attrStr, "padding") || "16px";
|
|
382
|
+
|
|
383
|
+
const bgClass = bg === "dark" ? "k-section-dark" : bg === "light" ? "k-section-light" : "";
|
|
384
|
+
const head = title ? `<div class="k-block-title">${escapeHTML(title)}</div>` : "";
|
|
385
|
+
|
|
386
|
+
return `<section class="k-section ${bgClass}" style="padding:${escapeHTML(padding)}">
|
|
387
|
+
${head}${inner}
|
|
388
|
+
</section>`;
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// GRID - Multi-column block container
|
|
392
|
+
html = html.replace(/<GRID([^>]*)>([\s\S]*?)<\/GRID>/gi, (_, attrStr, inner) => {
|
|
393
|
+
const cols = clampInt(getAttr(attrStr, "COLS") || getAttr(attrStr, "cols") || "2", 1, 4, 2);
|
|
394
|
+
const gap = getAttr(attrStr, "GAP") || getAttr(attrStr, "gap") || "14px";
|
|
395
|
+
|
|
396
|
+
return `<div class="k-grid k-grid-${cols}" style="gap:${escapeHTML(gap)}">${inner}</div>`;
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// BADGE - Small info labels
|
|
400
|
+
html = html.replace(/<BADGE([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
401
|
+
const text = getAttr(attrStr, "TEXT") || getAttr(attrStr, "text") || "Badge";
|
|
402
|
+
const type = (getAttr(attrStr, "TYPE") || getAttr(attrStr, "type") || "default").toLowerCase();
|
|
403
|
+
|
|
404
|
+
return `<span class="k-badge k-badge-${escapeHTML(type)}">${escapeHTML(text)}</span>`;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// TAGS - Display multiple tags
|
|
408
|
+
html = html.replace(/<TAGS([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
409
|
+
const tagsRaw = getAttr(attrStr, "LIST") || getAttr(attrStr, "list") || "tag1,tag2,tag3";
|
|
410
|
+
const tags = tagsRaw.split(",").map(t => t.trim()).filter(Boolean);
|
|
411
|
+
|
|
412
|
+
return `<div class="k-tags">${tags.map(t =>
|
|
413
|
+
`<span class="k-tag">${escapeHTML(t)}</span>`).join("")}</div>`;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// ALERT - Highlighted callout messages
|
|
417
|
+
html = html.replace(/<ALERT([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
418
|
+
const text = getAttr(attrStr, "TEXT") || getAttr(attrStr, "text");
|
|
419
|
+
const type = (getAttr(attrStr, "TYPE") || getAttr(attrStr, "type") || "info").toLowerCase();
|
|
420
|
+
|
|
421
|
+
const icons = {
|
|
422
|
+
info: "ℹ️",
|
|
423
|
+
success: "✅",
|
|
424
|
+
warning: "⚠️",
|
|
425
|
+
error: "❌"
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
if (!text) return `<div class="k-block k-placeholder">Add TEXT="Alert message"</div>`;
|
|
429
|
+
|
|
430
|
+
return `<div class="k-alert k-alert-${escapeHTML(type)}">
|
|
431
|
+
<span class="k-alert-icon">${icons[type] || icons.info}</span>
|
|
432
|
+
<span class="k-alert-text">${escapeHTML(text)}</span>
|
|
433
|
+
</div>`;
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// CODE - Display code snippets
|
|
437
|
+
html = html.replace(/<CODE([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
438
|
+
const code = getAttr(attrStr, "TEXT") || getAttr(attrStr, "text");
|
|
439
|
+
const lang = getAttr(attrStr, "LANG") || getAttr(attrStr, "lang") || "plaintext";
|
|
440
|
+
const title = getAttr(attrStr, "TITLE") || getAttr(attrStr, "title");
|
|
441
|
+
|
|
442
|
+
if (!code) return `<div class="k-block k-placeholder">Add TEXT="code here"</div>`;
|
|
443
|
+
|
|
444
|
+
const head = title ? `<div class="k-block-title">${escapeHTML(title)}</div>` : "";
|
|
445
|
+
|
|
446
|
+
return `<div class="k-block k-code-block">
|
|
447
|
+
${head}
|
|
448
|
+
<pre class="k-code"><code class="k-code-${escapeHTML(lang)}">${escapeHTML(code)}</code></pre>
|
|
449
|
+
</div>`;
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// COLLAPSE - Expandable container structures
|
|
453
|
+
html = html.replace(/<COLLAPSE([^>]*)>([\s\S]*?)<\/COLLAPSE>/gi, (_, attrStr, inner) => {
|
|
454
|
+
const title = getAttr(attrStr, "TITLE") || getAttr(attrStr, "title") || "Expand";
|
|
455
|
+
const open = getAttr(attrStr, "OPEN") || getAttr(attrStr, "open");
|
|
456
|
+
const isOpen = open && (open.toLowerCase() === "true" || open === "1");
|
|
457
|
+
|
|
458
|
+
return `<details class="k-collapse" ${isOpen ? "open" : ""}>
|
|
459
|
+
<summary class="k-collapse-title">${escapeHTML(title)}</summary>
|
|
460
|
+
<div class="k-collapse-content">${inner}</div>
|
|
461
|
+
</details>`;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// TIMELINE - Chronological event wrapper shell
|
|
465
|
+
html = html.replace(/<TIMELINE([^>]*)>([\s\S]*?)<\/TIMELINE>/gi, (_, attrStr, inner) => {
|
|
466
|
+
const title = getAttr(attrStr, "TITLE") || getAttr(attrStr, "title");
|
|
467
|
+
const head = title ? `<div class="k-block-title">${escapeHTML(title)}</div>` : "";
|
|
468
|
+
|
|
469
|
+
return `<div class="k-block k-timeline">
|
|
470
|
+
${head}
|
|
471
|
+
<div class="k-timeline-items">${inner}</div>
|
|
472
|
+
</div>`;
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// TIMELINE-ITEM - Individual timeline entries
|
|
476
|
+
html = html.replace(/<TIMELINE-ITEM([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
477
|
+
const date = getAttr(attrStr, "DATE") || getAttr(attrStr, "date");
|
|
478
|
+
const title = getAttr(attrStr, "TITLE") || getAttr(attrStr, "title");
|
|
479
|
+
const text = getAttr(attrStr, "TEXT") || getAttr(attrStr, "text");
|
|
480
|
+
|
|
481
|
+
return `<div class="k-timeline-item">
|
|
482
|
+
<div class="k-timeline-dot"></div>
|
|
483
|
+
<div class="k-timeline-content">
|
|
484
|
+
<div class="k-timeline-date">${escapeHTML(date)}</div>
|
|
485
|
+
<div class="k-timeline-item-title">${escapeHTML(title)}</div>
|
|
486
|
+
<p>${escapeHTML(text)}</p>
|
|
487
|
+
</div>
|
|
488
|
+
</div>`;
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// STAT - Dashboard stat boxes
|
|
492
|
+
html = html.replace(/<STAT([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
493
|
+
const value = getAttr(attrStr, "VALUE") || getAttr(attrStr, "value");
|
|
494
|
+
const label = getAttr(attrStr, "LABEL") || getAttr(attrStr, "label");
|
|
495
|
+
const icon = getAttr(attrStr, "ICON") || getAttr(attrStr, "icon");
|
|
496
|
+
|
|
497
|
+
const iconHtml = icon ? `<div class="k-stat-icon">${escapeHTML(icon)}</div>` : "";
|
|
498
|
+
|
|
499
|
+
return `<div class="k-stat">
|
|
500
|
+
${iconHtml}
|
|
501
|
+
<div class="k-stat-value">${escapeHTML(value)}</div>
|
|
502
|
+
<div class="k-stat-label">${escapeHTML(label)}</div>
|
|
503
|
+
</div>`;
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// QUOTE - Blockquotes with author
|
|
507
|
+
html = html.replace(/<QUOTE([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
508
|
+
const text = getAttr(attrStr, "TEXT") || getAttr(attrStr, "text");
|
|
509
|
+
const author = getAttr(attrStr, "AUTHOR") || getAttr(attrStr, "author");
|
|
510
|
+
const role = getAttr(attrStr, "ROLE") || getAttr(attrStr, "role");
|
|
511
|
+
|
|
512
|
+
if (!text) return `<div class="k-block k-placeholder">Add TEXT="Quote here"</div>`;
|
|
513
|
+
|
|
514
|
+
const authorHtml = author ? `<strong>${escapeHTML(author)}</strong>` : "";
|
|
515
|
+
const roleHtml = role ? `<span class="k-quote-role">${escapeHTML(role)}</span>` : "";
|
|
516
|
+
|
|
517
|
+
return `<blockquote class="k-quote">
|
|
518
|
+
<p class="k-quote-text">"${escapeHTML(text)}"</p>
|
|
519
|
+
<footer class="k-quote-footer">
|
|
520
|
+
${authorHtml}
|
|
521
|
+
${roleHtml}
|
|
522
|
+
</footer>
|
|
523
|
+
</blockquote>`;
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// PROGRESS - Progress bars
|
|
527
|
+
html = html.replace(/<PROGRESS([^\/>]*)\/>/gi, (_, attrStr) => {
|
|
528
|
+
const value = clampInt(getAttr(attrStr, "VALUE") || getAttr(attrStr, "value") || "50", 0, 100, 50);
|
|
529
|
+
const label = getAttr(attrStr, "LABEL") || getAttr(attrStr, "label") || `${value}%`;
|
|
530
|
+
const color = getAttr(attrStr, "COLOR") || getAttr(attrStr, "color") || "blue";
|
|
531
|
+
|
|
532
|
+
return `<div class="k-progress-block">
|
|
533
|
+
<div class="k-progress-label">${escapeHTML(label)}</div>
|
|
534
|
+
<div class="k-progress">
|
|
535
|
+
<div class="k-progress-bar k-progress-${escapeHTML(color)}" style="width:${value}%"></div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>`;
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
return html;
|
|
541
|
+
}
|
|
542
|
+
function suggestNext(raw) {
|
|
543
|
+
if (!suggest) return;
|
|
544
|
+
const hasLayout = /LAYOUT\s*=/i.test(raw) || /<LAYOUT\b/i.test(raw);
|
|
545
|
+
if (!hasLayout) {
|
|
546
|
+
process.stderr.write('\nTip: try layout (Pro): <PAGE TITLE="..." LAYOUT="main"> ... </PAGE>\n\n');
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
suggestNext(src);
|
|
551
|
+
|
|
552
|
+
body = applyKarmaBlocks(body);
|
|
553
|
+
|
|
554
|
+
const baseCSS = `
|
|
555
|
+
:root{--k-bg:#0b0f14;--k-card:rgba(17,24,39,.55);--k-border:rgba(255,255,255,.12);--k-text:#e5e7eb;--k-muted:rgba(229,231,235,.72);--k-radius:16px;}
|
|
556
|
+
*{box-sizing:border-box}
|
|
557
|
+
body{margin:0;padding:28px;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;background:var(--k-bg);color:var(--k-text);}
|
|
558
|
+
.k-block{border:1px solid var(--k-border);background:var(--k-card);border-radius:var(--k-radius);padding:16px;margin:14px 0;}
|
|
559
|
+
.k-block-title{font-weight:900;margin-bottom:10px}
|
|
560
|
+
.k-placeholder{color:var(--k-muted);margin:0}
|
|
561
|
+
.k-muted{color:var(--k-muted)}
|
|
562
|
+
.k-small{font-size:.9rem}
|
|
563
|
+
.k-btn{display:inline-flex;align-items:center;justify-content:center;padding:10px 14px;border-radius:12px;border:1px solid var(--k-border);text-decoration:none;font-weight:900;background:rgba(255,255,255,.08);color:var(--k-text);cursor:pointer}
|
|
564
|
+
.k-btn-primary{background:rgba(255,255,255,.12)}
|
|
565
|
+
.k-btn-ghost{background:transparent}
|
|
566
|
+
.k-nav-inner{display:flex;gap:10px;flex-wrap:wrap}
|
|
567
|
+
.k-nav-link{padding:9px 12px;border-radius:999px;border:1px solid var(--k-border);background:rgba(255,255,255,.06);text-decoration:none}
|
|
568
|
+
.k-image-placeholder{display:flex;gap:12px;align-items:center;padding:18px;border:1px dashed var(--k-border);border-radius:14px}
|
|
569
|
+
.k-image-icon{font-size:22px}
|
|
570
|
+
.k-figure{margin:0}
|
|
571
|
+
.k-img{max-width:100%;height:auto;border-radius:14px;border:1px solid var(--k-border)}
|
|
572
|
+
.k-caption{margin-top:8px;color:var(--k-muted);font-size:.95rem}
|
|
573
|
+
.k-hero{position:relative;border-radius:20px;overflow:hidden;border:1px solid var(--k-border);margin:14px 0;}
|
|
574
|
+
.k-hero-img{width:100%;height:320px;object-fit:cover;display:block}
|
|
575
|
+
.k-hero-overlay{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:flex-end;padding:18px;background:linear-gradient(transparent, rgba(0,0,0,.65));}
|
|
576
|
+
.k-split{display:grid;grid-template-columns:1fr 1fr;gap:14px;align-items:center}
|
|
577
|
+
@media (max-width:820px){.k-split{grid-template-columns:1fr}}
|
|
578
|
+
.k-table-el{width:100%;border-collapse:collapse;overflow:hidden;border-radius:12px}
|
|
579
|
+
.k-table-el th,.k-table-el td{border-bottom:1px solid var(--k-border);padding:10px;text-align:left}
|
|
580
|
+
.k-form-inner{display:grid;gap:10px}
|
|
581
|
+
.k-label{font-weight:900}
|
|
582
|
+
.k-input{width:100%;padding:10px 12px;border-radius:12px;border:1px solid var(--k-border);background:rgba(255,255,255,.04);color:var(--k-text);outline:none}
|
|
583
|
+
.k-input::placeholder{color:rgba(229,231,235,.45)}
|
|
584
|
+
textarea.k-input{min-height:110px;resize:vertical}
|
|
585
|
+
.k-video-el{width:100%;border-radius:14px;border:1px solid var(--k-border)}
|
|
586
|
+
.k-iframe-wrap{position:relative;padding-top:56.25%;border-radius:14px;overflow:hidden;border:1px solid var(--k-border)}
|
|
587
|
+
.k-iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
|
|
588
|
+
.k-pro-notice{border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.06);border-radius:14px;padding:12px 14px;margin:14px 0;}
|
|
589
|
+
.k-pro-notice strong{font-weight:900}
|
|
590
|
+
.k-heading{margin:16px 0 10px 0;font-weight:900}
|
|
591
|
+
.k-heading-1{font-size:2.4rem}
|
|
592
|
+
.k-heading-2{font-size:1.8rem}
|
|
593
|
+
.k-heading-3{font-size:1.4rem}
|
|
594
|
+
.k-heading-4{font-size:1.1rem}
|
|
595
|
+
.k-heading-large{font-size:3rem}
|
|
596
|
+
.k-section{border:1px solid var(--k-border);border-radius:var(--k-radius);margin:14px 0;}
|
|
597
|
+
.k-section-dark{background:rgba(0,0,0,.3)}
|
|
598
|
+
.k-section-light{background:rgba(255,255,255,.06)}
|
|
599
|
+
.k-grid{display:grid}
|
|
600
|
+
.k-grid-1{grid-template-columns:1fr}
|
|
601
|
+
.k-grid-2{grid-template-columns:repeat(2,1fr)}
|
|
602
|
+
.k-grid-3{grid-template-columns:repeat(3,1fr)}
|
|
603
|
+
.k-grid-4{grid-template-columns:repeat(4,1fr)}
|
|
604
|
+
@media (max-width:768px){.k-grid-3,.k-grid-4{grid-template-columns:repeat(2,1fr)}}
|
|
605
|
+
@media (max-width:520px){.k-grid-2,.k-grid-3,.k-grid-4{grid-template-columns:1fr}}
|
|
606
|
+
.k-badge{display:inline-block;padding:4px 8px;border-radius:12px;font-size:.85rem;font-weight:900;background:rgba(255,255,255,.12);color:var(--k-text);margin-right:6px}
|
|
607
|
+
.k-badge-success{background:rgba(34,197,94,.4);color:#22c55e}
|
|
608
|
+
.k-badge-warning{background:rgba(234,179,8,.4);color:#eab308}
|
|
609
|
+
.k-badge-error{background:rgba(239,68,68,.4);color:#ef4444}
|
|
610
|
+
.k-tags{display:flex;gap:6px;flex-wrap:wrap;margin:14px 0;}
|
|
611
|
+
.k-tag{padding:6px 10px;background:rgba(255,255,255,.08);border-radius:6px;font-size:.9rem}
|
|
612
|
+
.k-alert{display:flex;gap:10px;align-items:flex-start;padding:12px;border-radius:12px;margin:14px 0;border-left:4px solid}
|
|
613
|
+
.k-alert-info{background:rgba(59,130,246,.15);border-left-color:#3b82f6}
|
|
614
|
+
.k-alert-success{background:rgba(34,197,94,.15);border-left-color:#22c55e}
|
|
615
|
+
.k-alert-warning{background:rgba(234,179,8,.15);border-left-color:#eab308}
|
|
616
|
+
.k-alert-error{background:rgba(239,68,68,.15);border-left-color:#ef4444}.k-alert-icon{font-size:1.2rem}
|
|
617
|
+
.k-alert-text{flex:1}
|
|
618
|
+
.k-code-block{background:rgba(0,0,0,.4);padding:0;margin:14px 0;}
|
|
619
|
+
.k-code{margin:0;overflow-x:auto;font-family:'Courier New',monospace;font-size:.85rem;padding:16px;background:rgba(0,0,0,.6);border-radius:8px}
|
|
620
|
+
.k-code code{color:var(--k-text)}
|
|
621
|
+
.k-collapse{border:1px solid var(--k-border);border-radius:var(--k-radius);margin:14px 0;background:var(--k-card)}
|
|
622
|
+
.k-collapse-title{padding:12px 16px;cursor:pointer;font-weight:900;user-select:none}
|
|
623
|
+
.k-collapse-title:hover{background:rgba(255,255,255,.06)}
|
|
624
|
+
.k-collapse-content{padding:16px;border-top:1px solid var(--k-border)}
|
|
625
|
+
.k-timeline{position:relative;padding:20px 0}
|
|
626
|
+
.k-timeline-items{border-left:2px solid var(--k-border);padding-left:20px}
|
|
627
|
+
.k-timeline-item{position:relative;margin-bottom:24px}
|
|
628
|
+
.k-timeline-dot{position:absolute;width:12px;height:12px;background:#3b82f6;border-radius:50%;left:-26px;top:4px}
|
|
629
|
+
.k-timeline-date{font-size:.85rem;color:var(--k-muted);margin-bottom:4px}
|
|
630
|
+
.k-timeline-item-title{font-weight:900;margin-bottom:4px}
|
|
631
|
+
.k-stat{text-align:center;padding:16px}
|
|
632
|
+
.k-stat-icon{font-size:2rem;margin-bottom:8px}
|
|
633
|
+
.k-stat-value{font-size:2.2rem;font-weight:900;color:#3b82f6}
|
|
634
|
+
.k-stat-label{font-size:.9rem;color:var(--k-muted);margin-top:4px}
|
|
635
|
+
.k-quote{border-left:4px solid #3b82f6;padding:16px 20px;background:rgba(59,130,246,.1);border-radius:8px;margin:16px 0;font-style:italic}
|
|
636
|
+
.k-quote-text{margin:0;font-size:1.1rem}
|
|
637
|
+
.k-quote-footer{margin-top:12px;font-style:normal;font-size:.9rem;color:var(--k-muted)}
|
|
638
|
+
.k-quote-role{display:block;margin-top:4px;font-size:.85rem}
|
|
639
|
+
.k-progress-block{margin:16px 0}
|
|
640
|
+
.k-progress-label{font-size:.9rem;margin-bottom:6px;font-weight:900}
|
|
641
|
+
.k-progress{width:100%;height:8px;background:rgba(255,255,255,.1);border-radius:4px;overflow:hidden}
|
|
642
|
+
.k-progress-bar{height:100%;background:rgba(59,130,246,.6);transition:width 0.3s}
|
|
643
|
+
.k-progress-blue{background:rgba(59,130,246,.6)}
|
|
644
|
+
.k-progress-green{background:rgba(34,197,94,.6)}
|
|
645
|
+
.k-progress-red{background:rgba(239,68,68,.6)}
|
|
646
|
+
`;
|
|
647
|
+
|
|
648
|
+
const standardHtml = `<!doctype html>
|
|
649
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
650
|
+
<title>${pageTitle}</title><style>${baseCSS}</style></head>
|
|
651
|
+
<body>${body}</body></html>`;
|
|
652
|
+
|
|
653
|
+
let out = standardHtml;
|
|
654
|
+
|
|
655
|
+
if (layoutName) {
|
|
656
|
+
if (!isPro) {
|
|
657
|
+
const notice = `<div class="k-pro-notice"><strong>Layout "${escapeHTML(layoutName)}"</strong> is a Karma Pro feature. Page built without layout.</div>`;
|
|
658
|
+
out = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
659
|
+
<title>${pageTitle}</title><style>${baseCSS}</style></head><body>${notice}${body}</body></html>`;
|
|
660
|
+
} else {
|
|
661
|
+
const tpl = readLayoutTemplate(layoutName);
|
|
662
|
+
if (!tpl) {
|
|
663
|
+
const notice = `<div class="k-pro-notice"><strong>Layout missing:</strong> layouts/${escapeHTML(layoutName)}.html not found. Page built without layout.</div>`;
|
|
664
|
+
out = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
665
|
+
<title>${pageTitle}</title><style>${baseCSS}</style></head><body>${notice}${body}</body></html>`;
|
|
666
|
+
} else {
|
|
667
|
+
const vars = extractPageVars(pageVars, layoutName, pageTitle, baseCSS, body);
|
|
668
|
+
out = applyLayout(tpl, vars);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const outPath = input.toLowerCase().endsWith(".karma")
|
|
674
|
+
? input.replace(/\.karma$/i, ".html")
|
|
675
|
+
: `${input}.html`;
|
|
676
|
+
|
|
677
|
+
fs.writeFileSync(outPath, out, "utf8");
|
|
678
|
+
console.log(`✅ Built: ${outPath}`);
|
|
679
|
+
|
|
680
|
+
return outPath;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// entry
|
|
684
|
+
if (buildAll) {
|
|
685
|
+
const files = fs.readdirSync(baseDir).filter(f => f.toLowerCase().endsWith(".karma"));
|
|
686
|
+
|
|
687
|
+
if (!files.length) {
|
|
688
|
+
console.error("No .karma files found in:", baseDir);
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
let ok = 0;
|
|
693
|
+
let failed = 0;
|
|
694
|
+
|
|
695
|
+
for (const f of files) {
|
|
696
|
+
try {
|
|
697
|
+
const full = path.resolve(baseDir, f);
|
|
698
|
+
const outPath = compileOne(full);
|
|
699
|
+
if (outPath) ok++;
|
|
700
|
+
else failed++;
|
|
701
|
+
} catch (e) {
|
|
702
|
+
failed++;
|
|
703
|
+
console.error(`❌ Failed: ${f}`);
|
|
704
|
+
console.error(e?.stack || e);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
console.error(`\nDone. ✅ ${ok} built, ❌ ${failed} failed.`);
|
|
709
|
+
process.exit(failed ? 1 : 0);
|
|
710
|
+
} else {
|
|
711
|
+
// single file mode
|
|
712
|
+
const inputPath = path.resolve(baseDir, arg);
|
|
713
|
+
|
|
714
|
+
if (!fs.existsSync(inputPath)) {
|
|
715
|
+
console.error("File not found:", inputPath);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const outPath = compileOne(inputPath);
|
|
720
|
+
process.exit(outPath ? 0 : 1);
|
|
721
|
+
}
|