Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,24 +152,24 @@ Both `.zpl` and `.json` round-trip cleanly. `.zpl` preserves all printable conte
## Coverage

<!-- coverage:start (generated from docs/zpl-roadmap.md by scripts/gen-coverage.mjs; run `pnpm coverage:gen`) -->
107 of the 204 ZPL II commands tracked in the [roadmap](docs/zpl-roadmap.md) are supported today. Categorical breakdown:
113 of the 225 ZPL II commands tracked in the [roadmap](docs/zpl-roadmap.md) are supported today. Categorical breakdown:

| Area | Supported |
|---|---|
| Layout & flow | 16 / 16 |
| Templates & variables | 4 / 4 |
| Barcodes | 28 / 28 |
| Fields | 17 / 20 |
| Fields | 18 / 20 |
| Serialisation | 2 / 2 |
| Encoding & language | 3 / 4 |
| Encoding & language | 3 / 3 |
| Clock & time | 3 / 3 |
| Identity & access | 3 / 3 |
| Graphics | 6 / 12 |
| Media & feed | 8 / 10 |
| Media & feed | 10 / 10 |
| Text & fonts | 7 / 14 |
| Print quality | 7 / 16 |
| Print quality | 10 / 18 |
| Configuration & persistence | 3 / 5 |
| Hardware / Host comm / RFID / Network | 0 / 67 |
| Hardware / Host comm / RFID / Network | 0 / 87 |
<!-- coverage:end -->

---
Expand Down
149 changes: 92 additions & 57 deletions docs/zpl-roadmap.md

Large diffs are not rendered by default.

18 changes: 15 additions & 3 deletions packages/core/src/lib/zplGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,14 @@ export function planFieldEmission(
return { headerLines, bodyLines: shifted.flatMap(emitLeaf) };
}

/** Positional-command slots with trailing empties dropped, so unset
* params fall back to the printer's own defaults on the wire. */
export function trimTrailingEmptySlots(slots: readonly string[]): string[] {
const trimmed = [...slots];
while (trimmed.length > 0 && trimmed[trimmed.length - 1] === "") trimmed.pop();
return trimmed;
}

export function generateZPL(
label: LabelConfig,
objects: LabelObject[],
Expand Down Expand Up @@ -531,6 +539,11 @@ export function generateZPL(
if (label.darkness !== undefined) lines.push(`^MD${label.darkness}`);
if (label.printOrientation) lines.push(`^PO${label.printOrientation}`);
if (label.mirror) lines.push(`^PM${label.mirror}`);
if (label.mapClear) lines.push(`^MC${label.mapClear}`);
// slewDotRows=0 is a valid explicit "no slew"; check undefined.
if (label.slewDotRows !== undefined) lines.push(`^PF${label.slewDotRows}`);
if (label.slewToHome) lines.push('^PH');
if (label.programmablePause) lines.push('^PP');
// ^LH / ^LT subtract below from per-field absolute (x,y) so emit matches editor.
const homeX = label.labelHomeX ?? 0;
const homeY = label.labelHomeY ?? 0;
Expand All @@ -552,12 +565,11 @@ export function generateZPL(
label.defaultFontHeight !== undefined ||
label.defaultFontWidth !== undefined
) {
const slots = [
const slots = trimTrailingEmptySlots([
label.defaultFontId ?? "",
label.defaultFontHeight !== undefined ? String(label.defaultFontHeight) : "",
label.defaultFontWidth !== undefined ? String(label.defaultFontWidth) : "",
];
while (slots.length > 1 && slots[slots.length - 1] === "") slots.pop();
]);
lines.push(`^CF${slots.join(",")}`);
}

Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/lib/zplParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,12 @@ export function parseZPL(
// ^DY font uploads are intentionally not flagged.
const replayRiskCodes = new Set(Object.keys(setupScriptHandlers));
const deviceActionCodes = new Set([
"JA", "JM", "JC", "JD", "JE", "JI", "JR", "PP",
"JA", "JM", "JC", "JD", "JE", "JI", "JR",
]);
// ^PH/^PP are modelled per-format settings, but their tilde twins are
// immediate device controls; the handler map keys have no prefix, so the
// split happens at dispatch via the token's source char.
const tildeDeviceCodes = new Set(["PH", "PP"]);
Object.assign(handlers, setupScriptHandlers);
Object.assign(handlers, createLabelConfigHandlers(s, dpmm));
Object.assign(handlers, createUnitsHandler(s, dpmm));
Expand Down Expand Up @@ -323,6 +327,12 @@ export function parseZPL(
const p = rest.split(s.format.delimiterChar);
// Flag printer-config commands: lossless replay re-emits them, so they run
// on the user's printer at print/export. Recorded by code (deduped later).
// ~PH/~PP: flagged as device actions AND skipped entirely, or they would
// fall through to the unknown bucket and surface twice in the summary.
if (tildeDeviceCodes.has(cmd) && zpl[start] === s.format.tildeChar) {
deviceAction.push(`~${cmd}`);
continue;
}
if (replayRiskCodes.has(cmd)) replayRisk.push(`^${cmd}`);
else if (deviceActionCodes.has(cmd)) deviceAction.push(`^${cmd}`);
const handler = handlers[cmd] ?? wildcards.find((w) => w.matches(cmd))?.handle;
Expand Down
13 changes: 5 additions & 8 deletions packages/core/src/lib/zplParser/handlers/barcodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,12 @@ export function createBarcodeHandlers(s: ParserState): Record<string, Handler> {
B0: handleAztec,
BO: handleAztec,

// ^BVo,{mode},{symbolNumber},{totalSymbols}: Maxicode (fixed
// physical size, no magnification). symbolNumber/totalSymbols
// describe structured-append composition; we don't expose that
// in the editor, so the params are read but the emitted form
// pins them to (1, 1).
// ^BV orientation slot is a firmware no-op; canonicalized on emit.
BV(p) {
// ^BDm,{symbolNumber},{totalSymbols}: Maxicode (spec p106; fixed
// physical size, no orientation slot). Structured-append params are
// read but pinned to (1,1) on emit.
BD(p) {
field.fieldType = "maxicode";
const m = int(p[1], 4);
const m = int(p[0], 4);
field.maxicodeMode = (m >= 2 && m <= 6 ? m : 4) as MaxicodeProps["mode"];
},

Expand Down
37 changes: 30 additions & 7 deletions packages/core/src/lib/zplParser/handlers/labelConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DARKNESS_INSTANT_RANGE, DARKNESS_PERMANENT_RANGE, MAX_LABEL_LENGTH_RANGE, SPEED_RANGE, isBackfeedSequence, isMediaFeedMode, isMediaMode, isMediaTracking, isMediaType, isPrintOrientation } from "../../../types/LabelConfig";
import { DARKNESS_INSTANT_RANGE, DARKNESS_PERMANENT_RANGE, MAX_LABEL_LENGTH_RANGE, SLEW_DOT_ROWS_RANGE, SPEED_RANGE, isBackfeedPercent, isBackfeedSequence, isMediaFeedMode, isMediaMode, isMediaTracking, isMediaType, isPrintOrientation } from "../../../types/LabelConfig";
import { parseIntOrUndef } from "../../inputParse";
import { isYesNo } from "../../../types/typeHelpers";
import type { ParserState } from "../context";
import { dotsFor, firstChar, inRange, int, strParam } from "../helpers";
import type { Handler } from "../types";
Expand Down Expand Up @@ -38,7 +39,7 @@ export function createLabelConfigHandlers(
}
if (p.length > 3) {
const o = (p[3] ?? "").toUpperCase();
if (o === "Y" || o === "N") labelConfig.overridePauseCount = o;
if (isYesNo(o)) labelConfig.overridePauseCount = o;
}
},
MM(_, rest) {
Expand All @@ -65,6 +66,22 @@ export function createLabelConfigHandlers(
const mt = firstChar(rest);
if (isMediaType(mt)) labelConfig.mediaType = mt;
},
MC(p) {
const v = strParam(p[0]);
if (isYesNo(v)) labelConfig.mapClear = v;
},
PF(p) {
const v = inRange(parseIntOrUndef(p[0]), SLEW_DOT_ROWS_RANGE);
if (v !== undefined) labelConfig.slewDotRows = v;
},
// ^PH/^PP take no parameters; the tilde forms never reach these handlers
// (dispatch routes them as device actions).
PH() {
labelConfig.slewToHome = true;
},
PP() {
labelConfig.programmablePause = true;
},
MN(p) {
// ^MNa,b: b is an optional black-mark offset for W/M modes,
// which we don't model. Reading p[0] instead of the raw rest
Expand Down Expand Up @@ -92,20 +109,26 @@ export function createLabelConfigHandlers(
},
PM(_, rest) {
const m = firstChar(rest);
if (m === "Y" || m === "N") labelConfig.mirror = m;
if (isYesNo(m)) labelConfig.mirror = m;
},
// ~SD: instant darkness set (00..30). Tilde-prefix, so the tokenizer
// drops the delimiter and this is the canonical SD handler.
SD(_, rest) {
const v = inRange(parseIntOrUndef(rest), DARKNESS_INSTANT_RANGE);
if (v !== undefined) labelConfig.instantDarkness = v;
},
// ~JS: change backfeed sequence. A/B/N/O modeled; percent forms
// (10-90) aren't, so track them as a partial-import loss.
// ~JS: change backfeed sequence. Percent forms round to the nearest
// ten like the printer does (~JS55 -> 60, p276).
JS(_, rest) {
const v = firstChar(rest);
if (isBackfeedSequence(v)) labelConfig.backfeedSequence = v;
else if (rest) s.result.partialCmds.add("~JS");
if (isBackfeedSequence(v)) {
labelConfig.backfeedSequence = v;
return;
}
const pct = inRange(parseIntOrUndef(rest), { min: 10, max: 90 });
if (pct === undefined) return;
const rounded = Math.round(pct / 10) * 10;
if (isBackfeedPercent(rounded)) labelConfig.backfeedSequence = rounded;
},
};
}
50 changes: 43 additions & 7 deletions packages/core/src/lib/zplParser/handlers/setupScript.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,55 @@
import { CLOCK_TOLERANCE_RANGE, FONT_LINKS_PATH_MAX_LEN, HEAD_CLEANING_INTERVAL_METERS, HEAD_TEST_INTERVAL_RANGE, JH_SLOT_F, JH_SLOT_G, MAINTENANCE_ALERT_DEFAULTS, MAINTENANCE_DISTANCE_MAX_BY_TYPE, MAINTENANCE_MESSAGE_MAX_LEN, PRINTER_NAME_MAX_LEN, PRINTER_PASSWORD_REGEX, TEAR_OFF_ADJUST_RANGE, isClockFormat, isClockLanguage, isConfigUpdateAction, isMaintenanceAlertPrint, isMaintenanceAlertType, isMaintenanceAlertUnit, isPrinterLocale, isZplMode, setupScriptUnsafeCharRegex } from "../../../types/PrinterProfile";
import { CLOCK_TOLERANCE_RANGE, FONT_CACHE_KB_RANGE, FONT_LINKS_PATH_MAX_LEN, HEAD_CLEANING_INTERVAL_METERS, HEAD_TEST_INTERVAL_RANGE, JH_SLOT_F, JH_SLOT_G, MAINTENANCE_ALERT_DEFAULTS, MAINTENANCE_DISTANCE_MAX_BY_TYPE, MAINTENANCE_MESSAGE_MAX_LEN, PRINTER_NAME_MAX_LEN, PRINTER_PASSWORD_REGEX, TEAR_OFF_ADJUST_RANGE, isClockFormat, isClockLanguage, isConfigUpdateAction, isFontCacheType, isMaintenanceAlertPrint, isMaintenanceAlertType, isMaintenanceAlertUnit, isModeProtection, isPrinterLocale, isZplMode, setupScriptUnsafeCharRegex } from "../../../types/PrinterProfile";
import { parseIntOrUndef } from "../../inputParse";
import { isYesNo } from "../../../types/typeHelpers";
import { parseRealtimeClock } from "../../realtimeClock";
import type { ParserState } from "../context";
import { inRange, strParam } from "../helpers";
import type { Handler } from "../types";

/** Setup-Script commands (^JZ, ^JT, ~TA, ^ST, ^KD, ^KL, ^SE, ^SZ,
* ^KN, ^SL, ^KP, ^MA, ^MI, ^MW, ^JH, ^JU): all write the shared
* `printerProfile` slice. */
/** Setup-Script command handlers: all write the shared `printerProfile` slice. */
export function createSetupScriptHandlers(s: ParserState): Record<string, Handler> {
const printerProfile = s.result.printerProfile;
return {
// Each ^CO is a complete command: an empty slot means the printer's own
// default, so it clears a value an earlier ^CO in the stream had set.
CO(p) {
const on = strParam(p[0]);
// A fully-empty ^CO would parse to nothing and vanish on re-emit;
// normalise it to its a-slot default (Y, cache on) instead.
if (on === "" && strParam(p[1]) === "" && strParam(p[2]) === "") {
printerProfile.fontCacheOn = "Y";
delete printerProfile.fontCacheAddKb;
delete printerProfile.fontCacheType;
return;
}
if (on === "") delete printerProfile.fontCacheOn;
else if (isYesNo(on)) printerProfile.fontCacheOn = on;
const rawKb = strParam(p[1]);
if (rawKb === "") delete printerProfile.fontCacheAddKb;
else {
const kb = inRange(parseIntOrUndef(rawKb), FONT_CACHE_KB_RANGE);
if (kb !== undefined) printerProfile.fontCacheAddKb = kb;
}
const type = strParam(p[2]);
if (type === "") delete printerProfile.fontCacheType;
else if (isFontCacheType(type)) printerProfile.fontCacheType = type;
},
MP(p) {
const v = strParam(p[0]);
// E re-enables all modes, wiping the protection set. It also anchors
// the set as absolute; without it the stream only ADDS modes and the
// emitter must not inject a resetting ^MPE baseline.
if (v === "E") {
printerProfile.modeProtection = [];
printerProfile.modeProtectionExplicit = true;
} else if (isModeProtection(v)) {
const set = printerProfile.modeProtection ?? [];
if (!set.includes(v)) printerProfile.modeProtection = [...set, v];
}
},
JZ(p) {
const v = strParam(p[0]);
if (v === "Y" || v === "N") printerProfile.reprintAfterError = v;
if (isYesNo(v)) printerProfile.reprintAfterError = v;
},
JT(p) {
const v = inRange(parseIntOrUndef(p[0]), HEAD_TEST_INTERVAL_RANGE);
Expand Down Expand Up @@ -130,11 +166,11 @@ export function createSetupScriptHandlers(s: ParserState): Record<string, Handle
},
MW(_, rest) {
const v = rest.trim().toUpperCase();
if (v === "Y" || v === "N") printerProfile.headColdWarning = v;
if (isYesNo(v)) printerProfile.headColdWarning = v;
},
CV(_, rest) {
const v = rest.trim().toUpperCase();
if (v === "Y" || v === "N") printerProfile.codeValidation = v;
if (isYesNo(v)) printerProfile.codeValidation = v;
},
PA(p) {
// ^PAa,b,c,d each 0/1; missing slot = 0 (printer default).
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/lib/zplParser/handlers/unsupported.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export function createUnsupportedHandlers(
JE: noop,
JI: noop,
JR: noop,
PP: noop,
// Browser-limit factories, surface as "not loaded" findings.
HT: mkBrowserLimit("HT"),
LF: mkBrowserLimit("LF"),
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/registry/maxicode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const ALL_MODES = [2, 3, 4, 5, 6] as const;
/** Mode 4 = standard symbol, only mode without UPS-domain SCM requirement. */
const MAXICODE_DEFAULT_MODE = 4 as const;

// No rotation prop: ^BV orientation slot is a firmware no-op (^BD has none).
// No rotation prop: ^BD has no orientation slot (spec p106).
export interface MaxicodeProps {
content: string;
mode: 2 | 3 | 4 | 5 | 6;
Expand All @@ -18,7 +18,7 @@ export interface MaxicodeProps {
export const maxicode: ObjectTypeCore<MaxicodeProps> = {
label: "Maxicode",
icon: "⬡",
zplCmd: "^BV",
zplCmd: "^BD",
group: "code-2d",
bindable: true,
defaultProps: {
Expand All @@ -32,10 +32,10 @@ export const maxicode: ObjectTypeCore<MaxicodeProps> = {

toZPL: (obj, ctx) => {
const p = obj.props;
// ^BV; structured-append slots fixed at (1,1) since unexposed.
// Structured-append slots fixed at (1,1) since unexposed.
return [
fieldPosZ(obj),
`^BVN,${p.mode},1,1`,
`^BD${p.mode},1,1`,
fdFieldFor(p.content, ctx),
].join("");
},
Expand Down
Loading
Loading