@xvml/cli 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,351 @@
1
+ export function createRenderState(flags) {
2
+ return {
3
+ idCounter: { n: 0 },
4
+ noScripts: flags.has('no-scripts'),
5
+ rtl: flags.has('rtl'),
6
+ };
7
+ }
8
+ // ── Helpers ──────────────────────────────────────────────────────────────────
9
+ function esc(s) {
10
+ return s
11
+ .replace(/&/g, '&')
12
+ .replace(/</g, '&lt;')
13
+ .replace(/>/g, '&gt;')
14
+ .replace(/"/g, '&quot;');
15
+ }
16
+ function cls(...parts) {
17
+ return parts.filter(Boolean).join(' ');
18
+ }
19
+ function nextId(state, prefix) {
20
+ return `xvml-${prefix}-${state.idCounter.n++}`;
21
+ }
22
+ // First string arg
23
+ function firstStr(args, fallback = '') {
24
+ return args.find(a => a.type === 'string')?.value ?? fallback;
25
+ }
26
+ // Nth string arg (0-indexed among strings only)
27
+ function nthStr(args, n, fallback = '') {
28
+ let seen = 0;
29
+ for (const a of args) {
30
+ if (a.type === 'string') {
31
+ if (seen === n)
32
+ return a.value;
33
+ seen++;
34
+ }
35
+ }
36
+ return fallback;
37
+ }
38
+ function hasKw(args, ...kws) {
39
+ return args.some(a => a.type === 'keyword' && kws.includes(a.value));
40
+ }
41
+ function findKw(args, options) {
42
+ return args.find((a) => a.type === 'keyword' && options.includes(a.value))?.value;
43
+ }
44
+ function findKV(args, key) {
45
+ return args.find((a) => a.type === 'keyvalue' && a.key === key)?.value;
46
+ }
47
+ // ── Render tree ───────────────────────────────────────────────────────────────
48
+ // @layout on a line consumes all subsequent siblings in that block.
49
+ export function renderChildren(nodes, state) {
50
+ let html = '';
51
+ let i = 0;
52
+ while (i < nodes.length) {
53
+ const node = nodes[i];
54
+ if (node.command === 'layout') {
55
+ const mode = node.args[0]?.type === 'keyword' ? node.args[0].value : 'stack';
56
+ const inner = nodes.slice(i + 1).map(n => renderNode(n, state)).join('');
57
+ html += `<div class="xvml-layout${mode !== 'stack' ? ` xvml-layout--${mode}` : ''}">${inner}</div>`;
58
+ break;
59
+ }
60
+ html += renderNode(node, state);
61
+ i++;
62
+ }
63
+ return html;
64
+ }
65
+ export function renderNode(node, state) {
66
+ switch (node.command) {
67
+ case 'card': return renderCard(node, state);
68
+ case 'section': return renderSection(node, state);
69
+ case 'cols': return renderCols(node, state);
70
+ case 'stat-row': return renderStatRow(node, state);
71
+ case 'stats': return renderStats(node, state);
72
+ case 'nav': return renderNav(node);
73
+ case 'avatar': return renderAvatar(node);
74
+ case 'title': return renderTitle(node);
75
+ case 'subtitle': return renderSubtitle(node);
76
+ case 'text': return renderText(node);
77
+ case 'divider': return renderDivider(node);
78
+ case 'badge': return renderBadge(node);
79
+ case 'field': return renderField(node, state);
80
+ case 'button': return renderButton(node);
81
+ case 'checkbox': return renderCheckbox(node, state);
82
+ case 'select': return renderSelect(node, state);
83
+ case 'link': return renderLink(node);
84
+ case 'table': return renderTable(node);
85
+ case 'stat': return renderStat(node);
86
+ case 'progress': return renderProgress(node);
87
+ case 'list': return renderList(node);
88
+ case 'codeblock': return renderCodeblock(node);
89
+ case 'constraint': return renderConstraint(node);
90
+ case 'alert': return renderAlert(node);
91
+ case 'import':
92
+ case 'layout':
93
+ case 'row':
94
+ case 'item':
95
+ return '';
96
+ default:
97
+ return '';
98
+ }
99
+ }
100
+ // ── Layout ────────────────────────────────────────────────────────────────────
101
+ function renderCard(node, state) {
102
+ const label = node.args.find(a => a.type === 'string')?.value ?? '';
103
+ const mod = findKw(node.args, ['flat', 'outlined', 'compact']);
104
+ const labelHtml = label ? `<h2 class="xvml-card__label">${esc(label)}</h2>` : '';
105
+ const body = renderChildren(node.children, state);
106
+ return `<section class="${cls('xvml-card', mod && `xvml-card--${mod}`)}">${labelHtml}<div class="xvml-card__body">${body}</div></section>`;
107
+ }
108
+ function renderSection(node, state) {
109
+ const label = firstStr(node.args);
110
+ const mod = findKw(node.args, ['divided', 'collapsible']);
111
+ const body = renderChildren(node.children, state);
112
+ return (`<div class="${cls('xvml-section', mod && `xvml-section--${mod}`)}">` +
113
+ `<h3 class="xvml-section__label">${esc(label)}</h3>` +
114
+ `<div class="xvml-section__body">${body}</div>` +
115
+ `</div>`);
116
+ }
117
+ function renderCols(node, state) {
118
+ const count = node.args.find(a => a.type === 'number')?.value ?? 2;
119
+ const wrapped = node.children
120
+ .map(c => `<div class="xvml-col">${renderNode(c, state)}</div>`)
121
+ .join('');
122
+ return `<div class="xvml-cols xvml-cols--${count}">${wrapped}</div>`;
123
+ }
124
+ function renderStatRow(node, state) {
125
+ return `<div class="xvml-stat-row">${node.children.map(c => renderNode(c, state)).join('')}</div>`;
126
+ }
127
+ function renderStats(node, state) {
128
+ return `<div class="xvml-stats">${node.children.map(c => renderNode(c, state)).join('')}</div>`;
129
+ }
130
+ // ── Navigation / Presentation ─────────────────────────────────────────────────
131
+ function renderNav(node) {
132
+ // Args: keyword items separated by "|" keyword, e.g. Home | Projects | Settings
133
+ const items = node.args
134
+ .filter((a) => a.type === 'keyword' && a.value !== '|')
135
+ .map(a => a.value);
136
+ const links = items
137
+ .map(item => `<li><a class="xvml-nav__link" href="#">${esc(item)}</a></li>`)
138
+ .join('');
139
+ return `<nav class="xvml-nav"><ul class="xvml-nav__links">${links}</ul></nav>`;
140
+ }
141
+ function renderAvatar(node) {
142
+ const initials = firstStr(node.args);
143
+ return `<div class="xvml-avatar">${esc(initials)}</div>`;
144
+ }
145
+ // ── Content ───────────────────────────────────────────────────────────────────
146
+ function renderTitle(node) {
147
+ const text = firstStr(node.args);
148
+ const size = findKw(node.args, ['xl', 'lg', 'md', 'sm']) ?? 'lg';
149
+ return `<h1 class="xvml-title xvml-title--${size}">${esc(text)}</h1>`;
150
+ }
151
+ function renderSubtitle(node) {
152
+ const text = firstStr(node.args);
153
+ const muted = hasKw(node.args, 'muted');
154
+ return `<p class="${cls('xvml-subtitle', muted && 'xvml-subtitle--muted')}">${esc(text)}</p>`;
155
+ }
156
+ function renderText(node) {
157
+ const content = firstStr(node.args);
158
+ const mod = findKw(node.args, ['sm', 'muted', 'bold', 'mono', 'error', 'success']);
159
+ return `<p class="${cls('xvml-text', mod && `xvml-text--${mod}`)}">${esc(content)}</p>`;
160
+ }
161
+ function renderDivider(node) {
162
+ const text = node.args.find(a => a.type === 'string')?.value;
163
+ if (text) {
164
+ return (`<div class="xvml-divider xvml-divider--text">` +
165
+ `<span class="xvml-divider__line"></span>` +
166
+ `<span class="xvml-divider__text">${esc(text)}</span>` +
167
+ `<span class="xvml-divider__line"></span>` +
168
+ `</div>`);
169
+ }
170
+ const mod = findKw(node.args, ['dashed', 'thick', 'spacious']);
171
+ return `<hr class="${cls('xvml-divider', mod && `xvml-divider--${mod}`)}" />`;
172
+ }
173
+ function renderBadge(node) {
174
+ const label = firstStr(node.args);
175
+ const variant = findKw(node.args, ['neutral', 'success', 'warning', 'error', 'info']) ?? 'neutral';
176
+ return `<span class="xvml-badge xvml-badge--${variant}">${esc(label)}</span>`;
177
+ }
178
+ // ── Form ──────────────────────────────────────────────────────────────────────
179
+ // Syntax: @field <type-kw> "Label" [required] [secret] [value="..."] [placeholder="..."]
180
+ // OR legacy: @field "Label" <type-kw> [modifiers...]
181
+ const HTML_INPUT_TYPES = new Set([
182
+ 'email', 'password', 'number', 'tel', 'url', 'date', 'textarea', 'search', 'time', 'text',
183
+ ]);
184
+ function renderField(node, state) {
185
+ const label = firstStr(node.args);
186
+ // Find type keyword (first keyword that isn't a modifier)
187
+ const MODIFIERS = new Set(['required', 'secret', 'disabled', 'placeholder', 'value']);
188
+ const typeKw = node.args.find((a) => a.type === 'keyword' && !MODIFIERS.has(a.value));
189
+ const rawType = typeKw?.value ?? 'text';
190
+ const htmlType = HTML_INPUT_TYPES.has(rawType) ? rawType : 'text';
191
+ const isSecret = hasKw(node.args, 'secret') || htmlType === 'password';
192
+ const required = hasKw(node.args, 'required');
193
+ const disabled = hasKw(node.args, 'disabled');
194
+ // Named values from key=value args (e.g. value="Kumar S" placeholder="Enter email")
195
+ const defaultValue = findKV(node.args, 'value') ?? '';
196
+ const placeholder = findKV(node.args, 'placeholder') ?? '';
197
+ const actualType = isSecret ? 'password' : htmlType;
198
+ const id = nextId(state, 'field');
199
+ const reqAttr = required ? ' required' : '';
200
+ const disAttr = disabled ? ' disabled' : '';
201
+ const phAttr = placeholder ? ` placeholder="${esc(placeholder)}"` : '';
202
+ const valAttr = defaultValue ? ` value="${esc(defaultValue)}"` : '';
203
+ const input = actualType === 'textarea'
204
+ ? `<textarea id="${id}" class="xvml-field__textarea"${reqAttr}${disAttr}${phAttr}>${esc(defaultValue)}</textarea>`
205
+ : `<input id="${id}" class="xvml-field__input" type="${actualType}"${reqAttr}${disAttr}${phAttr}${valAttr} />`;
206
+ return `<div class="xvml-field"><label class="xvml-field__label" for="${id}">${esc(label)}</label>${input}</div>`;
207
+ }
208
+ function renderButton(node) {
209
+ const label = firstStr(node.args);
210
+ const variant = findKw(node.args, ['default', 'primary', 'secondary', 'danger', 'ghost', 'link']) ?? 'default';
211
+ const size = findKw(node.args, ['sm', 'md', 'lg']) ?? 'md';
212
+ const full = hasKw(node.args, 'full');
213
+ const disabled = hasKw(node.args, 'disabled');
214
+ return (`<button class="${cls('xvml-button', `xvml-button--${variant}`, size !== 'md' && `xvml-button--${size}`, full && 'xvml-button--full')}" ` +
215
+ `type="button"${disabled ? ' disabled' : ''}>${esc(label)}</button>`);
216
+ }
217
+ function renderCheckbox(node, state) {
218
+ const label = firstStr(node.args);
219
+ const checked = hasKw(node.args, 'checked');
220
+ const disabled = hasKw(node.args, 'disabled');
221
+ const id = nextId(state, 'checkbox');
222
+ return (`<label class="xvml-checkbox" for="${id}">` +
223
+ `<input id="${id}" class="xvml-checkbox__input" type="checkbox"${checked ? ' checked' : ''}${disabled ? ' disabled' : ''} />` +
224
+ `<span class="xvml-checkbox__label">${esc(label)}</span>` +
225
+ `</label>`);
226
+ }
227
+ function renderSelect(node, state) {
228
+ const label = nthStr(node.args, 0);
229
+ const required = hasKw(node.args, 'required');
230
+ const id = nextId(state, 'select');
231
+ // Options come from second string arg (pipe-delimited) OR additional string args
232
+ const allStrings = node.args.filter(a => a.type === 'string').map(a => a.value);
233
+ const [, ...rest] = allStrings;
234
+ const options = rest.length === 1 && rest[0].includes('|')
235
+ ? rest[0].split('|').map(s => s.trim()).filter(Boolean)
236
+ : rest;
237
+ const optionsHtml = options
238
+ .map(o => `<option value="${esc(o.toLowerCase().replace(/\s+/g, '-'))}">${esc(o)}</option>`)
239
+ .join('');
240
+ return (`<div class="xvml-select">` +
241
+ `<label class="xvml-select__label" for="${id}">${esc(label)}</label>` +
242
+ `<select id="${id}" class="xvml-select__input"${required ? ' required' : ''}>${optionsHtml}</select>` +
243
+ `</div>`);
244
+ }
245
+ function renderLink(node) {
246
+ const label = nthStr(node.args, 0);
247
+ const href = nthStr(node.args, 1, '#');
248
+ const blank = hasKw(node.args, 'blank');
249
+ return `<a class="xvml-link" href="${esc(href)}"${blank ? ' target="_blank" rel="noreferrer"' : ''}>${esc(label)}</a>`;
250
+ }
251
+ // ── Data ──────────────────────────────────────────────────────────────────────
252
+ function renderTable(node) {
253
+ const mod = findKw(node.args, ['striped', 'compact', 'bordered']);
254
+ const rows = node.children.filter(c => c.command === 'row');
255
+ if (rows.length === 0) {
256
+ return `<div class="xvml-table-wrapper"><table class="xvml-table"></table></div>`;
257
+ }
258
+ const [header, ...body] = rows;
259
+ const thead = `<thead><tr>${(header?.args ?? []).map(a => `<th>${esc(String(a.value))}</th>`).join('')}</tr></thead>`;
260
+ const tbody = `<tbody>${body.map(r => `<tr>${r.args.map(a => `<td>${esc(String(a.value))}</td>`).join('')}</tr>`).join('')}</tbody>`;
261
+ return `<div class="xvml-table-wrapper"><table class="${cls('xvml-table', mod && `xvml-table--${mod}`)}">${thead}${tbody}</table></div>`;
262
+ }
263
+ // Syntax: @stat "Value" "Label" [trend-kw]
264
+ // Value is displayed large; Label is the descriptor below it.
265
+ function renderStat(node) {
266
+ const value = nthStr(node.args, 0);
267
+ const label = nthStr(node.args, 1);
268
+ const trend = findKw(node.args, ['up', 'down', 'neutral']);
269
+ const trendIcon = { up: '↑', down: '↓', neutral: '→' };
270
+ const trendHtml = trend
271
+ ? `<span class="xvml-stat__trend xvml-stat__trend--${trend}">${trendIcon[trend]}</span>`
272
+ : '';
273
+ return (`<div class="xvml-stat">` +
274
+ `<span class="xvml-stat__value">${esc(value)}${trendHtml}</span>` +
275
+ `<span class="xvml-stat__label">${esc(label)}</span>` +
276
+ `</div>`);
277
+ }
278
+ function renderProgress(node) {
279
+ const label = firstStr(node.args);
280
+ const nums = node.args.filter((a) => a.type === 'number');
281
+ const value = nums[0]?.value ?? 0;
282
+ const max = nums[1]?.value ?? 100;
283
+ const variant = findKw(node.args, ['default', 'success', 'warning', 'error']);
284
+ const pct = max > 0 ? Math.round((value / max) * 100) : 0;
285
+ const fillClass = cls('xvml-progress__fill', variant && variant !== 'default' && `xvml-progress__fill--${variant}`);
286
+ return (`<div class="xvml-progress">` +
287
+ `<div class="xvml-progress__header">` +
288
+ `<span class="xvml-progress__label">${esc(label)}</span>` +
289
+ `<span class="xvml-progress__value">${pct}%</span>` +
290
+ `</div>` +
291
+ `<div class="xvml-progress__track"><div class="${fillClass}" style="width:${pct}%"></div></div>` +
292
+ `</div>`);
293
+ }
294
+ function renderList(node) {
295
+ const mod = findKw(node.args, ['ordered', 'unordered', 'check']) ?? 'unordered';
296
+ const tag = mod === 'ordered' ? 'ol' : 'ul';
297
+ const items = node.children
298
+ .filter(c => c.command === 'item')
299
+ .map(c => `<li class="xvml-list__item">${esc(firstStr(c.args))}</li>`)
300
+ .join('');
301
+ return `<${tag} class="xvml-list xvml-list--${mod}">${items}</${tag}>`;
302
+ }
303
+ // ── Code ──────────────────────────────────────────────────────────────────────
304
+ function renderCodeblock(node) {
305
+ const lang = findKw(node.args, [
306
+ 'ts', 'js', 'json', 'bash', 'html', 'css', 'xvml', 'sh',
307
+ 'py', 'go', 'rust', 'yaml', 'toml', 'sql', 'md', 'text',
308
+ ]) ?? 'text';
309
+ const filename = node.args.find(a => a.type === 'string')?.value ?? '';
310
+ const code = node.rawLines.join('\n');
311
+ const header = lang !== 'text' || filename
312
+ ? `<div class="xvml-codeblock__header">` +
313
+ `<span class="xvml-codeblock__lang">${esc(lang)}</span>` +
314
+ (filename ? `<span class="xvml-codeblock__filename">${esc(filename)}</span>` : '') +
315
+ `</div>`
316
+ : '';
317
+ return (`<div class="xvml-codeblock">${header}` +
318
+ `<pre class="xvml-codeblock__pre">` +
319
+ `<code class="xvml-codeblock__code language-${esc(lang)}">${esc(code)}</code>` +
320
+ `</pre></div>`);
321
+ }
322
+ function renderConstraint(node) {
323
+ const name = nthStr(node.args, 0);
324
+ const desc = nthStr(node.args, 1);
325
+ const severity = findKw(node.args, ['must', 'should', 'may']) ?? 'must';
326
+ return (`<div class="xvml-constraint xvml-constraint--${severity}">` +
327
+ `<span class="xvml-constraint__severity">${severity.toUpperCase()}</span>` +
328
+ `<span class="xvml-constraint__name">${esc(name)}</span>` +
329
+ `<p class="xvml-constraint__desc">${esc(desc)}</p>` +
330
+ `</div>`);
331
+ }
332
+ // Syntax: @alert <variant-kw> "Message"
333
+ // Variant keyword comes BEFORE the message string.
334
+ const ALERT_VARIANT_MAP = {
335
+ info: 'info', success: 'success', ok: 'success',
336
+ warn: 'warning', warning: 'warning',
337
+ error: 'error', err: 'error', danger: 'error',
338
+ };
339
+ const ALERT_ICONS = {
340
+ info: 'ℹ', success: '✓', warning: '⚠', error: '✕',
341
+ };
342
+ function renderAlert(node) {
343
+ const rawVariant = node.args.find(a => a.type === 'keyword' && a.value in ALERT_VARIANT_MAP)?.value ?? 'info';
344
+ const variant = ALERT_VARIANT_MAP[rawVariant] ?? 'info';
345
+ const message = firstStr(node.args);
346
+ const icon = ALERT_ICONS[variant] ?? 'ℹ';
347
+ return (`<div class="xvml-alert xvml-alert--${variant}" role="alert">` +
348
+ `<span class="xvml-alert__icon">${icon}</span>` +
349
+ `<span class="xvml-alert__message">${esc(message)}</span>` +
350
+ `</div>`);
351
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@xvml/cli",
3
+ "version": "0.1.0",
4
+ "description": "XVML — Visual Markup Language CLI renderer",
5
+ "type": "module",
6
+ "bin": {
7
+ "xvml": "dist/bin/xvml.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "XVML_SPEC.md",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc && cp XVML_SPEC.md dist/ && chmod +x dist/bin/xvml.js",
16
+ "xvml": "node --loader ts-node/esm --no-warnings bin/xvml.ts",
17
+ "check": "tsc --noEmit",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest"
20
+ },
21
+ "keywords": [
22
+ "xvml",
23
+ "markdown",
24
+ "html",
25
+ "cli",
26
+ "renderer"
27
+ ],
28
+ "author": "Kumar Raj <kumar.raj@codvo.ai>",
29
+ "license": "ISC",
30
+ "homepage": "https://github.com/kumarrajdevops/xvml#readme",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/kumarrajdevops/xvml.git"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "dependencies": {
39
+ "@anthropic-ai/sdk": "^0.104.1",
40
+ "chalk": "^5.6.2",
41
+ "commander": "^15.0.0",
42
+ "fs-extra": "^11.3.5"
43
+ },
44
+ "devDependencies": {
45
+ "@types/fs-extra": "^11.0.4",
46
+ "@types/node": "^25.9.2",
47
+ "ts-node": "^10.9.2",
48
+ "typescript": "^6.0.3",
49
+ "vitest": "^3.2.6"
50
+ }
51
+ }