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.
@@ -0,0 +1,44 @@
1
+ // karma-runtime.js
2
+ // Minimal Karma runtime renderer (v0.1)
3
+
4
+ export function renderKarma(ast) {
5
+ if (!ast || !ast.children) return "";
6
+
7
+ return ast.children.map(renderNode).join("");
8
+ }
9
+
10
+ function renderNode(node) {
11
+ if (node.type === "TEXT") {
12
+ return escapeHtml(node.value);
13
+ }
14
+
15
+ switch (node.type) {
16
+ case "PAGE":
17
+ return `<main>${node.children.map(renderNode).join("")}</main>`;
18
+
19
+ case "NAV":
20
+ return `<nav>Karma Nav</nav>`;
21
+
22
+ case "PARA":
23
+ return `<p>${node.children.map(renderNode).join("")}</p>`;
24
+
25
+ case "IMAGE":
26
+ return `<img src="${node.attrs?.SRC || ""}" />`;
27
+
28
+ case "BTN":
29
+ return `<button>${node.children.map(renderNode).join("")}</button>`;
30
+
31
+ default:
32
+ // unknown tags still render children
33
+ return node.children
34
+ ? node.children.map(renderNode).join("")
35
+ : "";
36
+ }
37
+ }
38
+
39
+ function escapeHtml(str = "") {
40
+ return str
41
+ .replaceAll("&", "&amp;")
42
+ .replaceAll("<", "&lt;")
43
+ .replaceAll(">", "&gt;");
44
+ }
Binary file
@@ -0,0 +1,470 @@
1
+ // Karma — validator & linter (v1.0)
2
+ // Validates .karma files and provides intelligent error reporting
3
+ // Usage: node karma-validator.js file.karma [--ai] [--fix]
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const arg = process.argv[2];
13
+ const useAI = process.argv.includes("--ai");
14
+ const autoFix = process.argv.includes("--fix");
15
+ const verbose = process.argv.includes("--verbose");
16
+
17
+ if (!arg) {
18
+ console.error('Usage: node karma-validator.js <file.karma> [--ai] [--fix] [--verbose]');
19
+ process.exit(1);
20
+ }
21
+
22
+ // ========== ERROR DEFINITIONS ==========
23
+ const ERRORS = {
24
+ MISSING_PAGE: {
25
+ code: "E001",
26
+ severity: "error",
27
+ message: "Missing required <PAGE> tag",
28
+ suggestion: "Every .karma file must have exactly one <PAGE>...</PAGE> block"
29
+ },
30
+ MISSING_TITLE: {
31
+ code: "E002",
32
+ severity: "error",
33
+ message: "PAGE tag missing TITLE attribute",
34
+ suggestion: 'Add TITLE="Page Title" to your <PAGE> tag'
35
+ },
36
+ UNCLOSED_TAG: {
37
+ code: "E003",
38
+ severity: "error",
39
+ message: "Unclosed tag detected",
40
+ suggestion: "Make sure all tags have proper closing tags or are self-closing"
41
+ },
42
+ INVALID_ATTRIBUTE: {
43
+ code: "E004",
44
+ severity: "warning",
45
+ message: "Invalid attribute for tag",
46
+ suggestion: "Check the TAGS_GUIDE.md for valid attributes"
47
+ },
48
+ EMPTY_TEXT: {
49
+ code: "E005",
50
+ severity: "warning",
51
+ message: "Empty TEXT attribute",
52
+ suggestion: "Add meaningful content to the TEXT attribute"
53
+ },
54
+ INVALID_TAG: {
55
+ code: "E006",
56
+ severity: "error",
57
+ message: "Unknown tag used",
58
+ suggestion: "Check the TAGS_GUIDE.md for a list of valid tags"
59
+ },
60
+ BROKEN_LINK: {
61
+ code: "E007",
62
+ severity: "warning",
63
+ message: "Link references non-existent file",
64
+ suggestion: "Make sure the linked .karma file exists"
65
+ },
66
+ MISSING_REQUIRED_ATTR: {
67
+ code: "E008",
68
+ severity: "error",
69
+ message: "Missing required attribute",
70
+ suggestion: "This tag requires specific attributes"
71
+ },
72
+ INVALID_GRID_COLS: {
73
+ code: "E009",
74
+ severity: "warning",
75
+ message: "GRID COLS value out of range",
76
+ suggestion: "COLS must be between 1 and 4"
77
+ },
78
+ INVALID_HEADING_LEVEL: {
79
+ code: "E010",
80
+ severity: "warning",
81
+ message: "HEADING LEVEL out of range",
82
+ suggestion: "LEVEL must be between 1 and 6"
83
+ }
84
+ };
85
+
86
+ // ========== VALID TAGS & ATTRIBUTES ==========
87
+ const VALID_TAGS = {
88
+ PAGE: { required: ["TITLE"], optional: ["LAYOUT", "BRAND", "TAGLINE"] },
89
+ HEADING: { required: ["TEXT"], optional: ["LEVEL", "SIZE"] },
90
+ PARA: { required: ["TEXT"], optional: [] },
91
+ IMAGE: { required: ["SRC"], optional: ["ALT", "TITLE", "CAPTION", "VARIANT", "POSITION", "LINK"] },
92
+ VIDEO: { required: [], optional: ["SRC", "URL", "TITLE"] },
93
+ LINK: { required: ["TEXT", "HREF"], optional: ["VARIANT"] },
94
+ NAV: { required: [], optional: ["LINKS", "TITLE", "TYPE", "POSITION"] },
95
+ TABLE: { required: [], optional: ["TITLE", "COLS", "ROWS"] },
96
+ FORM: { required: [], optional: ["TITLE", "FIELDS", "SUBMIT"] },
97
+ EMAIL: { required: ["TO"], optional: ["TEXT", "SUBJECT", "BODY"] },
98
+ PHONE: { required: ["NUMBER"], optional: ["TEXT"] },
99
+ SECTION: { required: [], optional: ["TITLE", "BG", "PADDING"] },
100
+ GRID: { required: [], optional: ["COLS", "GAP"] },
101
+ BADGE: { required: ["TEXT"], optional: ["TYPE"] },
102
+ TAGS: { required: [], optional: ["LIST"] },
103
+ ALERT: { required: ["TEXT"], optional: ["TYPE"] },
104
+ CODE: { required: ["TEXT"], optional: ["LANG", "TITLE"] },
105
+ COLLAPSE: { required: ["TITLE"], optional: ["OPEN"] },
106
+ TIMELINE: { required: [], optional: ["TITLE"] },
107
+ "TIMELINE-ITEM": { required: ["TITLE"], optional: ["DATE", "TEXT"] },
108
+ STAT: { required: ["VALUE", "LABEL"], optional: ["ICON"] },
109
+ QUOTE: { required: ["TEXT"], optional: ["AUTHOR", "ROLE"] },
110
+ PROGRESS: { required: ["VALUE"], optional: ["LABEL", "COLOR"] }
111
+ };
112
+
113
+ // ========== VALIDATOR CLASS ==========
114
+ class KarmaValidator {
115
+ constructor(filepath) {
116
+ this.filepath = filepath;
117
+ this.content = fs.readFileSync(filepath, "utf8");
118
+ this.lines = this.content.split(/\r?\n/);
119
+ this.errors = [];
120
+ this.warnings = [];
121
+ this.suggestions = [];
122
+ }
123
+
124
+ // Check if file has PAGE tag
125
+ checkPageTag() {
126
+ const hasPage = /<PAGE/i.test(this.content);
127
+ if (!hasPage) {
128
+ this.errors.push({
129
+ ...ERRORS.MISSING_PAGE,
130
+ line: 1,
131
+ fix: `<PAGE TITLE="Page Title">\n <!-- content -->\n</PAGE>`
132
+ });
133
+ return false;
134
+ }
135
+
136
+ const pageMatch = this.content.match(/<PAGE\s+([^>]*)>/i);
137
+ if (!pageMatch) return false;
138
+
139
+ const attrs = pageMatch[1];
140
+ if (!/TITLE\s*=/i.test(attrs)) {
141
+ this.errors.push({
142
+ ...ERRORS.MISSING_TITLE,
143
+ line: this.getLineNumber("<PAGE"),
144
+ fix: 'Add TITLE="Your Page Title"'
145
+ });
146
+ }
147
+
148
+ return true;
149
+ }
150
+
151
+ // Check for unclosed tags
152
+ checkUnclosedTags() {
153
+ const tagPattern = /<([A-Z\-]+)([^>]*)>(?!\/)/gi;
154
+ let match;
155
+
156
+ while ((match = tagPattern.exec(this.content))) {
157
+ const tagName = match[1].toUpperCase();
158
+
159
+ // Self-closing tags don't need closing tags
160
+ if (this.isSelfClosing(tagName)) continue;
161
+
162
+ const closePattern = new RegExp(`</${tagName}>`, "i");
163
+ if (!closePattern.test(this.content)) {
164
+ this.errors.push({
165
+ ...ERRORS.UNCLOSED_TAG,
166
+ line: this.getLineNumber(`<${tagName}`),
167
+ tag: tagName,
168
+ fix: `Add </${tagName}> before the end of the file`
169
+ });
170
+ }
171
+ }
172
+ }
173
+
174
+ // Check for unknown tags
175
+ checkUnknownTags() {
176
+ const tagPattern = /<([A-Z\-]+)/gi;
177
+ let match;
178
+
179
+ while ((match = tagPattern.exec(this.content))) {
180
+ const tagName = match[1].toUpperCase();
181
+ if (tagName !== "PAGE" && !VALID_TAGS[tagName]) {
182
+ this.errors.push({
183
+ ...ERRORS.INVALID_TAG,
184
+ line: this.getLineNumber(`<${tagName}`),
185
+ tag: tagName,
186
+ suggestion: `Did you mean one of these? ${Object.keys(VALID_TAGS).slice(0, 5).join(", ")}`
187
+ });
188
+ }
189
+ }
190
+ }
191
+
192
+ // Check required attributes
193
+ checkRequiredAttributes() {
194
+ for (const [tagName, attrs] of Object.entries(VALID_TAGS)) {
195
+ const pattern = new RegExp(`<${tagName}([^>]*)\\s*/?\\s*>`, "gi");
196
+ let match;
197
+
198
+ while ((match = pattern.exec(this.content))) {
199
+ const attrStr = match[1];
200
+
201
+ for (const required of attrs.required) {
202
+ if (!new RegExp(`${required}\\s*=`, "i").test(attrStr)) {
203
+ this.errors.push({
204
+ ...ERRORS.MISSING_REQUIRED_ATTR,
205
+ line: this.getLineNumber(`<${tagName}`),
206
+ tag: tagName,
207
+ attribute: required,
208
+ fix: `Add ${required}="value" to <${tagName}>`
209
+ });
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ // Check attribute values
217
+ checkAttributeValues() {
218
+ // Check GRID COLS
219
+ const gridColsPattern = /<GRID[^>]*COLS\s*=\s*"([^"]+)"/gi;
220
+ let match;
221
+ while ((match = gridColsPattern.exec(this.content))) {
222
+ const value = parseInt(match[1]);
223
+ if (value < 1 || value > 4) {
224
+ this.warnings.push({
225
+ ...ERRORS.INVALID_GRID_COLS,
226
+ line: this.getLineNumber(match[0]),
227
+ current: value,
228
+ fix: "Change COLS to a value between 1 and 4"
229
+ });
230
+ }
231
+ }
232
+
233
+ // Check HEADING LEVEL
234
+ const headingLevelPattern = /<HEADING[^>]*LEVEL\s*=\s*"([^"]+)"/gi;
235
+ while ((match = headingLevelPattern.exec(this.content))) {
236
+ const value = parseInt(match[1]);
237
+ if (value < 1 || value > 6) {
238
+ this.warnings.push({
239
+ ...ERRORS.INVALID_HEADING_LEVEL,
240
+ line: this.getLineNumber(match[0]),
241
+ current: value,
242
+ fix: "Change LEVEL to a value between 1 and 6"
243
+ });
244
+ }
245
+ }
246
+
247
+ // Check PROGRESS VALUE
248
+ const progressPattern = /<PROGRESS[^>]*VALUE\s*=\s*"([^"]+)"/gi;
249
+ while ((match = progressPattern.exec(this.content))) {
250
+ const value = parseInt(match[1]);
251
+ if (value < 0 || value > 100) {
252
+ this.warnings.push({
253
+ ...ERRORS.INVALID_GRID_COLS,
254
+ line: this.getLineNumber(match[0]),
255
+ message: "PROGRESS VALUE must be between 0 and 100",
256
+ current: value,
257
+ fix: "Change VALUE to a number between 0 and 100"
258
+ });
259
+ }
260
+ }
261
+ }
262
+
263
+ // Check for empty TEXT attributes
264
+ checkEmptyAttributes() {
265
+ const emptyPattern = /(TEXT|TITLE|LABEL|VALUE)\s*=\s*""/gi;
266
+ let match;
267
+
268
+ while ((match = emptyPattern.exec(this.content))) {
269
+ this.warnings.push({
270
+ ...ERRORS.EMPTY_TEXT,
271
+ line: this.getLineNumber(match[0]),
272
+ attribute: match[1],
273
+ fix: `Add a value to ${match[1]}="your content"`
274
+ });
275
+ }
276
+ }
277
+
278
+ // Check for broken links
279
+ checkBrokenLinks() {
280
+ const linkPattern = /HREF\s*=\s*"([^"]+\.karma)"/gi;
281
+ let match;
282
+
283
+ while ((match = linkPattern.exec(this.content))) {
284
+ const linkedFile = path.resolve(path.dirname(this.filepath), match[1]);
285
+ if (!fs.existsSync(linkedFile)) {
286
+ this.warnings.push({
287
+ ...ERRORS.BROKEN_LINK,
288
+ line: this.getLineNumber(match[0]),
289
+ file: match[1],
290
+ fix: `Make sure the file exists at: ${linkedFile}`
291
+ });
292
+ }
293
+ }
294
+ }
295
+
296
+ // Detect embedded prompts
297
+ detectPrompts() {
298
+ const promptPattern = /<!--\s*@ai\s*(.*?)\s*-->/gi;
299
+ let match;
300
+
301
+ while ((match = promptPattern.exec(this.content))) {
302
+ this.suggestions.push({
303
+ type: "prompt",
304
+ line: this.getLineNumber(match[0]),
305
+ prompt: match[1].trim(),
306
+ description: "Found embedded AI prompt"
307
+ });
308
+ }
309
+ }
310
+
311
+ // Helper to check if tag is self-closing
312
+ isSelfClosing(tag) {
313
+ const selfClosing = [
314
+ "PARA", "IMAGE", "VIDEO", "LINK", "NAV", "TABLE", "FORM",
315
+ "EMAIL", "PHONE", "BADGE", "TAGS", "ALERT", "CODE",
316
+ "STAT", "PROGRESS", "QUOTE", "COLLAPSE"
317
+ ];
318
+ return selfClosing.includes(tag);
319
+ }
320
+
321
+ // Get line number of content
322
+ getLineNumber(content) {
323
+ const index = this.content.indexOf(content);
324
+ return this.content.substring(0, index).split("\n").length;
325
+ }
326
+
327
+ // Run all checks
328
+ validate() {
329
+ this.checkPageTag();
330
+ this.checkUnclosedTags();
331
+ this.checkUnknownTags();
332
+ this.checkRequiredAttributes();
333
+ this.checkAttributeValues();
334
+ this.checkEmptyAttributes();
335
+ this.checkBrokenLinks();
336
+ this.detectPrompts();
337
+ }
338
+
339
+ // Generate suggestions
340
+ generateSuggestions() {
341
+ const suggestions = [];
342
+
343
+ // Suggest missing HEADING for pages without structure
344
+ if (!/HEADING|H[1-6]/i.test(this.content)) {
345
+ suggestions.push({
346
+ type: "structure",
347
+ message: "Consider adding a HEADING tag for better page structure",
348
+ example: '<HEADING TEXT="Welcome" LEVEL="1" />'
349
+ });
350
+ }
351
+
352
+ // Suggest adding META tags
353
+ if (!/META/i.test(this.content)) {
354
+ suggestions.push({
355
+ type: "seo",
356
+ message: "Consider adding metadata for better SEO"
357
+ });
358
+ }
359
+
360
+ // Suggest grid for better layout
361
+ if (!/GRID/i.test(this.content) && this.lines.length > 20) {
362
+ suggestions.push({
363
+ type: "layout",
364
+ message: "Consider using GRID for better layout organization",
365
+ example: '<GRID COLS="2"><Card>Item 1</Card><Card>Item 2</Card></GRID>'
366
+ });
367
+ }
368
+
369
+ return suggestions;
370
+ }
371
+
372
+ // Print report
373
+ printReport() {
374
+ console.log("\nšŸ” Karma Validator Report\n");
375
+ console.log(`šŸ“„ File: ${this.filepath}\n`);
376
+
377
+ if (this.errors.length === 0 && this.warnings.length === 0) {
378
+ console.log("āœ… No errors found!\n");
379
+ }
380
+
381
+ if (this.errors.length > 0) {
382
+ console.log(`āŒ Errors (${this.errors.length}):`);
383
+ this.errors.forEach(err => {
384
+ console.log(`\n [${err.code}] Line ${err.line}: ${err.message}`);
385
+ console.log(` šŸ’” ${err.suggestion}`);
386
+ if (err.fix) {
387
+ console.log(` šŸ”§ Fix: ${err.fix}`);
388
+ }
389
+ });
390
+ }
391
+
392
+ if (this.warnings.length > 0) {
393
+ console.log(`\nāš ļø Warnings (${this.warnings.length}):`);
394
+ this.warnings.forEach(warn => {
395
+ console.log(`\n [${warn.code}] Line ${warn.line}: ${warn.message}`);
396
+ console.log(` šŸ’” ${warn.suggestion}`);
397
+ if (warn.fix) {
398
+ console.log(` šŸ”§ Fix: ${warn.fix}`);
399
+ }
400
+ });
401
+ }
402
+
403
+ if (this.suggestions.length > 0) {
404
+ console.log(`\nšŸ’¬ AI Prompts Detected (${this.suggestions.length}):`);
405
+ this.suggestions.filter(s => s.type === "prompt").forEach(sugg => {
406
+ console.log(`\n Line ${sugg.line}: "${sugg.prompt}"`);
407
+ });
408
+ }
409
+
410
+ if (verbose) {
411
+ const autoSuggestions = this.generateSuggestions();
412
+ if (autoSuggestions.length > 0) {
413
+ console.log(`\nšŸš€ Suggestions:`);
414
+ autoSuggestions.forEach(sugg => {
415
+ console.log(`\n [${sugg.type.toUpperCase()}] ${sugg.message}`);
416
+ if (sugg.example) {
417
+ console.log(` Example: ${sugg.example}`);
418
+ }
419
+ });
420
+ }
421
+ }
422
+
423
+ console.log("\n" + "=".repeat(50) + "\n");
424
+
425
+ return {
426
+ errorCount: this.errors.length,
427
+ warningCount: this.warnings.length,
428
+ promptCount: this.suggestions.filter(s => s.type === "prompt").length
429
+ };
430
+ }
431
+
432
+ // Apply fixes
433
+ applyFixes() {
434
+ let fixed = this.content;
435
+
436
+ this.errors.forEach(err => {
437
+ if (err.code === "E001") {
438
+ fixed = `<PAGE TITLE="New Page">\n ${fixed}\n</PAGE>`;
439
+ }
440
+ });
441
+
442
+ return fixed;
443
+ }
444
+ }
445
+
446
+ // ========== MAIN ==========
447
+ function main() {
448
+ const inputPath = path.resolve(process.cwd(), arg);
449
+
450
+ if (!fs.existsSync(inputPath)) {
451
+ console.error(`āŒ File not found: ${inputPath}`);
452
+ process.exit(1);
453
+ }
454
+
455
+ const validator = new KarmaValidator(inputPath);
456
+ validator.validate();
457
+ const report = validator.printReport();
458
+
459
+ if (autoFix && report.errorCount > 0) {
460
+ console.log("šŸ”§ Applying auto-fixes...\n");
461
+ const fixed = validator.applyFixes();
462
+ const fixedPath = inputPath.replace(/\.karma$/, ".fixed.karma");
463
+ fs.writeFileSync(fixedPath, fixed, "utf8");
464
+ console.log(`āœ… Fixed file written to: ${fixedPath}\n`);
465
+ }
466
+
467
+ process.exit(report.errorCount > 0 ? 1 : 0);
468
+ }
469
+
470
+ main();
package/package.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "karma-lang",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "karma": "./karma-cli.js"
7
+ }
8
+ }
package/test.karma ADDED
@@ -0,0 +1,4 @@
1
+ <PAGE TITLE="Hello Karma" LAYOUT="main" BRAND="Karma" TAGLINE="Calm pages. Clear minds.">
2
+ <NAV LINKS="Home:test.karma, About:about.karma" />
3
+ <PARA TEXT="It works." />
4
+ </PAGE>