Skip to content

Commit 64baf9b

Browse files
feat: add Validate tab to web playground — paste, parse, preview, warnings
New Validate tab: - Paste any printer commands (TSC, ZPL, EPL, CPCL, DPL, SBPL, IPL) - Select language from dropdown - Real-time parsing with live preview - Parse Result: shows all commands, stats (total, unknown, elements, label size) - Warnings: unknown/unrecognized commands highlighted in yellow - Copy button for parse results - Side-by-side layout: preview (left) + results/warnings (right) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c2b7aa7 commit 64baf9b

2 files changed

Lines changed: 223 additions & 0 deletions

File tree

web/app/app.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
separator,
55
renderPreview,
66
parseTSC,
7+
parseTSPL,
78
parseZPL,
89
parseEPL,
910
parseCPCL,
@@ -463,6 +464,141 @@ export function setupApp(): void {
463464
}
464465
});
465466

467+
// Validate tab
468+
function runValidate(): void {
469+
const code = ($("#v-code") as HTMLTextAreaElement).value;
470+
const lang = val("#v-lang");
471+
if (!code.trim()) {
472+
$("#v-preview").innerHTML = "";
473+
$("#v-result").textContent = "";
474+
$("#v-warnings").textContent = "";
475+
return;
476+
}
477+
478+
try {
479+
let commands: any[] = [];
480+
let elements: any[] = [];
481+
let warnings: string[] = [];
482+
let widthDots = 320;
483+
let heightDots = 240;
484+
485+
if (lang === "tsc") {
486+
const r = parseTSPL(code);
487+
commands = r.commands;
488+
elements = r.elements;
489+
warnings = r.warnings;
490+
widthDots = r.widthDots;
491+
heightDots = r.heightDots;
492+
} else if (lang === "zpl") {
493+
const r = parseZPL(code);
494+
commands = r.commands;
495+
elements = r.elements;
496+
warnings = r.warnings;
497+
widthDots = r.widthDots;
498+
heightDots = r.heightDots;
499+
} else if (lang === "epl") {
500+
const r = parseEPL(code);
501+
commands = r.commands;
502+
elements = r.elements;
503+
warnings = r.warnings;
504+
widthDots = r.widthDots;
505+
heightDots = r.heightDots || 240;
506+
} else if (lang === "cpcl") {
507+
const r = parseCPCL(code);
508+
commands = r.commands;
509+
elements = r.elements;
510+
warnings = r.warnings;
511+
widthDots = r.widthDots;
512+
heightDots = r.heightDots;
513+
} else if (lang === "dpl") {
514+
const r = parseDPL(code);
515+
commands = r.commands;
516+
elements = r.elements;
517+
warnings = r.warnings;
518+
widthDots = r.widthDots;
519+
} else if (lang === "sbpl") {
520+
const r = parseSBPL(code);
521+
commands = r.commands;
522+
elements = r.elements;
523+
warnings = r.warnings;
524+
} else if (lang === "ipl") {
525+
const r = parseIPL(code);
526+
commands = r.commands;
527+
elements = r.elements;
528+
warnings = r.warnings;
529+
widthDots = r.widthDots;
530+
heightDots = r.heightDots;
531+
}
532+
533+
// Count stats
534+
const unknowns = commands.filter(
535+
(c: any) => c.cmd === "UNKNOWN" || c.type === "UNKNOWN",
536+
).length;
537+
const total = commands.length;
538+
539+
// Build result summary
540+
let result = `Language: ${lang.toUpperCase()}\n`;
541+
result += `Commands: ${total}\n`;
542+
result += `Elements: ${elements.length}\n`;
543+
result += `Label: ${widthDots}×${heightDots} dots\n`;
544+
if (unknowns > 0) {
545+
result += `Unknown: ${unknowns} (${Math.round((unknowns / total) * 100)}%)\n`;
546+
}
547+
result += `\n--- Commands ---\n`;
548+
for (const cmd of commands) {
549+
const name = cmd.cmd ?? cmd.code ?? cmd.type ?? cmd.name ?? "?";
550+
result += `${name}`;
551+
if (cmd.content) result += ` "${cmd.content}"`;
552+
if (cmd.rawParams) result += ` ${cmd.rawParams}`;
553+
if (cmd.params && typeof cmd.params === "string") result += ` ${cmd.params}`;
554+
if (cmd.raw) result += ` (raw: ${cmd.raw.slice(0, 50)})`;
555+
result += "\n";
556+
}
557+
558+
$("#v-result").textContent = result;
559+
560+
// Warnings
561+
let warnText = "";
562+
if (unknowns > 0) {
563+
warnText += `${unknowns} unknown/unrecognized command(s)\n`;
564+
}
565+
for (const w of warnings) {
566+
warnText += `${w}\n`;
567+
}
568+
if (!warnText) warnText = "No warnings — all commands recognized.";
569+
$("#v-warnings").textContent = warnText;
570+
571+
// Preview
572+
const resolved: ResolvedLabel = {
573+
widthDots,
574+
heightDots,
575+
dpi: 203,
576+
gapDots: 24,
577+
speed: 4,
578+
density: 8,
579+
direction: 0,
580+
copies: 1,
581+
elements,
582+
};
583+
$("#v-preview").innerHTML = renderPreview(resolved);
584+
} catch (e: any) {
585+
$("#v-result").textContent = `Parse error: ${e.message}`;
586+
$("#v-warnings").textContent = "";
587+
$("#v-preview").innerHTML = "";
588+
}
589+
}
590+
591+
$("#v-code").addEventListener("input", runValidate);
592+
$("#v-lang").addEventListener("change", runValidate);
593+
594+
$("#btn-copy-validate").addEventListener("click", () => {
595+
const text = $("#v-result").textContent ?? "";
596+
navigator.clipboard.writeText(text);
597+
const btn = $("#btn-copy-validate");
598+
btn.textContent = "Copied!";
599+
setTimeout(() => (btn.textContent = "Copy"), 1500);
600+
});
601+
466602
generate();
467603
generateReceipt();
468604
}

web/index.html

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ <h1><span class="accent">portakal</span></h1>
2727
<nav class="tabs" role="tablist">
2828
<button class="tab active" data-tab="label" role="tab">Label</button>
2929
<button class="tab" data-tab="receipt" role="tab">Receipt</button>
30+
<button class="tab" data-tab="validate" role="tab">Validate</button>
3031
</nav>
3132

3233
<section class="panel active" data-panel="label">
@@ -508,6 +509,92 @@ <h3>Footer</h3>
508509
</div>
509510
</div>
510511
</section>
512+
<!-- ═══════ VALIDATE ═══════ -->
513+
<section class="panel" data-panel="validate">
514+
<div class="split">
515+
<div class="controls" style="min-width: 100%">
516+
<div class="ctrl-section">
517+
<div class="ctrl-header">
518+
<h3>Paste Printer Commands</h3>
519+
</div>
520+
<div class="ctrl-body" style="display: block">
521+
<div class="field full">
522+
<label for="v-lang">Language</label>
523+
<select id="v-lang">
524+
<option value="tsc" selected>TSC/TSPL2</option>
525+
<option value="zpl">ZPL II</option>
526+
<option value="epl">EPL2</option>
527+
<option value="cpcl">CPCL</option>
528+
<option value="dpl">DPL</option>
529+
<option value="sbpl">SBPL</option>
530+
<option value="ipl">IPL</option>
531+
</select>
532+
</div>
533+
<div class="field full">
534+
<label for="v-code">Commands</label>
535+
<textarea
536+
id="v-code"
537+
rows="12"
538+
spellcheck="false"
539+
placeholder="Paste your TSC, ZPL, EPL, CPCL, DPL, SBPL, or IPL commands here..."
540+
></textarea>
541+
</div>
542+
</div>
543+
</div>
544+
</div>
545+
</div>
546+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1rem">
547+
<div class="output">
548+
<div class="preview-section">
549+
<div class="preview-header"><h3>Preview</h3></div>
550+
<div id="v-preview" class="preview-canvas" style="min-height: 150px"></div>
551+
</div>
552+
</div>
553+
<div class="output">
554+
<div class="code-section">
555+
<div class="output-header">
556+
<h3
557+
style="
558+
font-size: 0.65rem;
559+
color: var(--text-dim);
560+
text-transform: uppercase;
561+
letter-spacing: 0.08em;
562+
font-weight: 700;
563+
"
564+
>
565+
Parse Result
566+
</h3>
567+
<button id="btn-copy-validate" class="btn">Copy</button>
568+
</div>
569+
<pre
570+
id="v-result"
571+
class="output-code"
572+
style="min-height: 200px; max-height: 400px"
573+
></pre>
574+
</div>
575+
<div class="code-section">
576+
<div class="output-header">
577+
<h3
578+
style="
579+
font-size: 0.65rem;
580+
color: var(--text-dim);
581+
text-transform: uppercase;
582+
letter-spacing: 0.08em;
583+
font-weight: 700;
584+
"
585+
>
586+
Warnings
587+
</h3>
588+
</div>
589+
<pre
590+
id="v-warnings"
591+
class="output-code"
592+
style="min-height: 40px; max-height: 120px; color: #fbbf24"
593+
></pre>
594+
</div>
595+
</div>
596+
</div>
597+
</section>
511598
</main>
512599
<footer>
513600
<div class="footer-links">

0 commit comments

Comments
 (0)