diff --git a/src/components/Properties/PropertiesPanel.tsx b/src/components/Properties/PropertiesPanel.tsx index 2c23f56..2d24124 100644 --- a/src/components/Properties/PropertiesPanel.tsx +++ b/src/components/Properties/PropertiesPanel.tsx @@ -1,5 +1,6 @@ import { useLabelStore, useCurrentObjects } from "../../store/labelStore"; import { ObjectRegistry } from "../../registry"; +import { stripZplCommandChars } from "../../registry/zplHelpers"; import { dotsToMm, mmToDots } from "../../lib/coordinates"; import { mmToUnit, @@ -137,7 +138,7 @@ export function PropertiesPanel() { rows={2} value={obj.comment ?? ""} onChange={(e) => - updateObject(obj.id, { comment: e.target.value || undefined }) + updateObject(obj.id, { comment: stripZplCommandChars(e.target.value) || undefined }) } /> diff --git a/src/lib/zplGenerator.ts b/src/lib/zplGenerator.ts index d210984..1018038 100644 --- a/src/lib/zplGenerator.ts +++ b/src/lib/zplGenerator.ts @@ -1,5 +1,6 @@ import { mmToDots } from './coordinates'; import { ObjectRegistry } from '../registry'; +import { stripZplCommandChars } from '../registry/zplHelpers'; import type { LabelConfig } from '../types/ObjectType'; import type { LabelObject } from '../registry'; import type { Page } from '../store/labelStore'; @@ -29,9 +30,7 @@ export function generateZPL(label: LabelConfig, objects: LabelObject[]): string lines.push(...objects.map((obj) => { const zpl = ObjectRegistry[obj.type]?.toZPL(obj) ?? ''; if (!obj.comment) return zpl; - // Strip ^ to prevent breaking ZPL structure inside the comment text - const safe = obj.comment.replace(/\^/g, ''); - return `^FX${safe}\n${zpl}`; + return `^FX${stripZplCommandChars(obj.comment)}\n${zpl}`; })); if (label.printQuantity && label.printQuantity > 1) { diff --git a/src/lib/zplParser.test.ts b/src/lib/zplParser.test.ts index 1c301df..a72bb70 100644 --- a/src/lib/zplParser.test.ts +++ b/src/lib/zplParser.test.ts @@ -192,6 +192,39 @@ describe('parseZPL — ^FX comment', () => { expect(objects).toHaveLength(1); expect(skipped.some((s) => s.startsWith('^FX'))).toBe(false); }); + + it('attaches a single ^FX to the next object as comment', () => { + const { objects } = parseZPL( + '^XA^FXTop section^FO10,20^A0N,30,0^FDText^FS^XZ', + 8, + ); + expect(objects[0]?.comment).toBe('Top section'); + }); + + it('joins consecutive ^FX lines with a newline', () => { + const { objects } = parseZPL( + '^XA^FXLine 1^FXLine 2^FO10,20^A0N,30,0^FDText^FS^XZ', + 8, + ); + expect(objects[0]?.comment).toBe('Line 1\nLine 2'); + }); + + it('does not bleed comments across ^XA boundaries', () => { + const { objects } = parseZPL( + '^XA^FXOnly first^XZ^XA^FO10,20^A0N,30,0^FDText^FS^XZ', + 8, + ); + expect(objects[0]?.comment).toBeUndefined(); + }); + + it('does not reattach a consumed comment to a later object', () => { + const { objects } = parseZPL( + '^XA^FXOnly first^FO10,20^A0N,30,0^FDFirst^FS^FO10,60^A0N,30,0^FDSecond^FS^XZ', + 8, + ); + expect(objects[0]?.comment).toBe('Only first'); + expect(objects[1]?.comment).toBeUndefined(); + }); }); // ── ^FH hex encoding ────────────────────────────────────────────────────────── diff --git a/src/lib/zplParser.ts b/src/lib/zplParser.ts index 53ce121..df5ba71 100644 --- a/src/lib/zplParser.ts +++ b/src/lib/zplParser.ts @@ -531,6 +531,14 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { // ── Command handler map ──────────────────────────────────────────────────── const noop: Handler = () => void 0; const resetComment: Handler = (_, rest) => { pendingComment = rest.trim() || undefined; }; + // Hand-written ZPL often splits a logical comment across several `^FX` lines + // before the field they describe. Accumulate them so each line survives on + // the imported object's comment field; XA/XZ still reset at label boundaries. + const appendComment: Handler = (_, rest) => { + const next = rest.trim(); + if (!next) return; + pendingComment = pendingComment ? `${pendingComment}\n${next}` : next; + }; const mkBrowserLimit = (prefix: string, delimiter = "^"): Handler => (_, rest) => { const tok = `${delimiter}${prefix}${rest}`; skipped.push(tok); @@ -1083,8 +1091,9 @@ export function parseZPL(zpl: string, dpmm = 8): ParsedZPL { // ^XA / ^XZ: label start/end — reset pending comment via empty rest XA: resetComment, XZ: resetComment, - // ^FX: comment field — store for attachment to the next field object - FX: resetComment, + // ^FX: comment field — accumulate across consecutive ^FX lines so the + // assembled text reaches the next field object as one multi-line comment. + FX: appendComment, // These commands carry no canvas-design information and are silently // discarded so they do not pollute importReport.unknown. diff --git a/src/registry/zplHelpers.ts b/src/registry/zplHelpers.ts index e000852..50d4b95 100644 --- a/src/registry/zplHelpers.ts +++ b/src/registry/zplHelpers.ts @@ -6,6 +6,16 @@ export function fieldPos(obj: LabelObjectBase): string { return `^${cmd}${obj.x},${obj.y}`; } +/** + * Remove ZPL command/format prefixes from free-form text. `^FX` (comment) and + * other text-only contexts have no `^FH` escape mechanism, so these chars + * cannot be encoded — strip them so a stray `^` or `~` cannot terminate the + * surrounding command. + */ +export function stripZplCommandChars(s: string): string { + return s.replace(/[\^~]/g, ''); +} + const FH_DELIM = '_'; const NEEDS_FH = /[\^~]/;