Skip to content

Feat/zpl fe templates#92

Merged
u8array merged 4 commits into
mainfrom
feat/zpl-fe-templates
May 24, 2026
Merged

Feat/zpl fe templates#92
u8array merged 4 commits into
mainfrom
feat/zpl-fe-templates

Conversation

@u8array
Copy link
Copy Markdown
Owner

@u8array u8array commented May 24, 2026

No description provided.

u8array added 3 commits May 24, 2026 12:23
…solve

Adds round-trip support for ZPL's `^FE` directive that lets a single
`^FD` payload reference multiple `^FN` slots inline. ZPL ground form:

  ^XA
  ^FN1^FDsku-default^FS
  ^FN2^FDprice-default^FS
  ^FO50,50^A0N,30,30^FD#1#-#2#^FS
  ^XZ

Stored in our model as `«name»` markers in `props.content` — same
syntax the canvas already uses for schema-mode variable placeholders,
so the marker reads naturally in the properties panel and the
single-bind + template paths compose at render time.

Three layers:

  - lib/fnTemplate.ts: marker primitives (extract/resolve/embeds-to-
    markers / markers-to-embeds / rename / pickEmbedChar). Fresh
    regex per call so the /g `lastIndex` state can't bleed across
    callers.
  - parser: tracks ^FE embedChar (default '#'), bootstraps Variables
    for unknown FN refs in `^FD` content via the SAME regex shape
    markersToEmbeds expects (`<e><n><e>` or `<e><n>,...<e>`) so a
    literal like `Item #5 special` does not phantom-bootstrap.
    Distinguishes bare `^FN<n>^FD<default>^FS` Variable declarations
    from implicit-text fields by skipping the FD→text promotion when
    pendingFn is set. Three former bootstrap paths (bare-decl,
    single-bind, embed-reference) funnel through one `bootstrapVariable`
    helper.
  - generator: pre-scan collects template-referenced fnNumbers,
    picks an embedChar that doesn't clash with any literal payload
    text, emits `^FE<char>` (only when ≠ '#'), emits one
    `^FN<n>^FD<default>^FS` header per referenced Variable that
    isn't already covered by an inline single-bind field. When
    pickEmbedChar finds NO safe candidate (pathological payload with
    every safe char taken) the template emit path is skipped and
    markers fall through as literal text. Whole header pass +
    emit-context is extracted to `planTemplateHeader` so generateZPL
    stays a sequence of label parts.

applyBindingToObject scans content for `«name»` markers and
substitutes the resolved Variable value (CSV-row-aware, schema-mode
aware). Single-bind resolves first, then template markers — so an
unusual single-bind value that itself contains markers also resolves.

Shared `getObjectStringContent(obj)` helper in variableBinding.ts
consolidates the five sites that previously cast
`(obj as { props?: { content?: unknown } })` to read content from a
heterogeneous LabelObject tree.

23 new unit tests (fnTemplate primitives + parser/generator
roundtrip + embedChar fallback to '@' when '#' literal in payload).
TemplateContentInput wraps the plain text input in the Text and 1D-
barcode properties panels with a small `{x}` button that opens a
dropdown of every defined Variable; picking one splices its
`«name»` marker into the input at the current cursor position.

Lets non-technical users compose multi-variable fields without
hand-typing the marker syntax. Templates resolve at render time via
the lib/fnTemplate + variableBinding chain from the previous commit.

Dropdown closes on outside-click and Esc. The barcode1d wiring
splits content at marker boundaries before sanitising literal
slices so the restricted-charset filter (e.g. numeric-only EAN13)
preserves `«…»` markers on every keystroke instead of stripping
them as out-of-charset bytes.

Locale: one new key (insertVariable) across all 32 locales for the
button title attribute.
VariablesPanel's per-row binding count now includes `«name»`
template references in addition to single-bind `variableId`
references, so the user sees real usage of each Variable across
both binding styles. De-duped per object — a field that carries
both `variableId === V` and `«V»` in its content counts as one
place, not two.

updateVariable rewrites every `«oldName»` marker in every
object's content to `«newName»` when a Variable is renamed —
without this ripple, renames would silently dangle the templates
(resolve to literal text). Identity-preserving at the leaf level
so React memoisation downstream stays effective for objects whose
content didn't carry the renamed name.

Reuses the shared `getObjectStringContent` helper from
variableBinding.ts so the content-reading shape check stays in
one place.

3 new tests cover the rename ripple (multiple markers, identity
preservation, no-op when name unchanged).
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for ZPL ^FE inline embeds, enabling multiple variables to be used within a single text or barcode field. It adds a TemplateContentInput component for variable insertion, a "rename ripple" mechanism to update markers when variables are renamed, and updates the ZPL parser and generator to handle the conversion between ZPL numeric embeds and human-readable markers. Feedback identifies accessibility gaps in the new UI component and suggests performance optimizations in the binding, generation, and parsing logic by replacing linear searches with map-based lookups.

Comment thread src/components/Properties/TemplateContentInput.tsx
Comment thread src/lib/variableBinding.ts
Comment thread src/lib/zplGenerator.ts
Comment thread src/lib/zplParser.ts
Address gemini's perf findings on PR #92:

- variableBinding.applyBindingToObject: build a name→Variable map
  once per call so a field with N markers resolves in O(N+V)
  instead of N linear scans across the V Variables.
- zplGenerator.planTemplateHeader: build id/name/fn maps once
  before the leaf walk so the per-leaf single-bind lookup +
  per-marker name lookup + per-fn header emit all stay constant-
  time. Cuts the worst case from O(N·V) to O(N+V).

Per-field Map rebuild inside zplParser.applyFnEmbeds (gemini's
4th finding) deferred — that path also bootstraps Variables so a
hoisted-cache invalidation strategy adds more complexity than the
inner-loop scan saves at current scale.

TemplateContentInput a11y findings (role=menuitem, keyboard nav)
deferred to a separate a11y pass alongside other Properties
components — keeping this PR scoped to the ^FE feature surface.
@u8array u8array merged commit be0e190 into main May 24, 2026
2 checks passed
@u8array u8array deleted the feat/zpl-fe-templates branch May 24, 2026 11:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant