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
package/karma-runtime.js
ADDED
|
@@ -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("&", "&")
|
|
42
|
+
.replaceAll("<", "<")
|
|
43
|
+
.replaceAll(">", ">");
|
|
44
|
+
}
|
package/karma-v1.0.0.zip
ADDED
|
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
package/test.karma
ADDED