Implement the SVG-to-shapes converter in src/svg.ts with unit tests #209
Labels
No labels
bug
chore
documentation
enhancement
feature
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
david/pi-extensions-and-skills#209
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implement
extensions/penpot/src/svg.ts: a bounded SVG reader plus a converter that turns the documented subset —rect,circle,path,text,g, fills, strokes and transforms — into native Penpot shapes, and marks every unsupported node forsvg-rawfallback instead of silently dropping it. Pure, unit-tested, no HTTP.Background
Depends on: #199
Code → design takes SVG as an input format only; the internal currency stays semantic primitives. Penpot's own SVG parser is front-end ClojureScript and is not exposed over RPC, so the converter is ours to build and maintain — which is exactly why its scope must be explicit and its gaps visible.
Scope decisions that this step must honour:
rect,circle,path(a documented command subset),text,g(as a group/parent),svg(the root, contributingviewBox/width/height).pathcommands:M,L,H,V,C,S,Q,T,Z, absolute and relative. Arcs (A) require arc→bézier conversion that is easy to get subtly wrong — mark such nodes unsupported and fall back rather than approximate silently. Record this as a limitation.fill(hex colours,none,currentColortreated as a documented default),fill-opacity,stroke,stroke-width, basicstroke-linecap/stroke-linejoinwhere Penpot has an equivalent.translate,scale,matrix— flattened into geometry by applying the accumulated matrix to the node's coordinates.rotate/skewon a shape whose geometry cannot be expressed axis-aligned after flattening must fall back tosvg-raw(document it); do not emit a shape whoseselrectdisagrees with its geometry, since Penpot's selection/geometry assumptions depend on that consistency.use,mask,filter,clipPath,linearGradientfills, dashed strokes,textPath) →svg-rawfallback, never a silent drop.DOMParser, so this includes writing a bounded XML parser for what SVG actually needs: elements, attributes, self-closing tags, text nodes, comments, CDATA and the standard entities. Do not attempt full XML-spec compliance; document the limits. If, after attempting it, a dependency proves genuinely necessary, raise that as an explicit decision in the issue rather than adding one silently.The output of the converter is a list of staged changes in the same shape as the composition primitives — the next step wraps it in a tool.
Documentation Required
A separate process downloads these into the listed folders before this issue is implemented. Check the folders for the actual reference material before starting.
docs/reference/svg/rect,circle,path,text,g,svg), including notes onviewBoxand coordinate systems.x,y,width,height,rx,cx,cy,r).dpath-data grammar for the supported command subset.fill,fill-opacity,stroke,stroke-width,stroke-linecap,stroke-linejoin,transform.docs/reference/penpot-api/contentsegment model (move-to/line-to/curve-to/close-path, absolute vs relative) and its required geometry fields. This is what the converter must emit forpathnodes.selrect,points,transform) that must stay consistent for every emitted shape.textnodes.<PENPOT_URL>/api/main/doc/openapi.json— theadd-objchange shape, including thesvg-rawtype the fallback uses.docs/reference/nodejs/node:teststructure for the table-driven tests this module needs.Implementation Details
Create
extensions/penpot/src/svg.ts(plussrc/xml.tsfor the bounded parser, and co-located tests) exporting:src/xml.tsparseXml(text)→ a minimal tree of{ name, attributes, children, text }, handling self-closing tags, attributes with single/double quotes, comments, CDATA, and the five standard entities (& < > " ').src/svg.tsparsePathData(d)→ an ordered list of commands with absolute coordinates resolved (relative commands are resolved against the current point). Emit Penpot path segments; reject arcs explicitly.matrixFrom(transformAttr)→ a 2D affine matrix fortranslate,scale,matrix;rotate/skeware detected and reported as unsupported.applyMatrixToRect/applyMatrixToCircle/applyMatrixToPath→ flat geometry plus a consistency check (selrect/pointsderived from the geometry, never independently).convertSvg(svgText, options)→{ shapes, skipped, report }where:shapesis an ordered list of converter-local shape descriptors (id, parent, type, geometry, style) ready for the next step to turn intoadd-objchanges;skippedlists every node routed tosvg-rawwith a reason (unsupported element "filter",arc path commands not supported,unsupported transform rotate);reportis a summary the tool can print: node counts by type, converted vs fallback, and the distinct reasons.groupToShapes— handlegby producing a Penpot group/frame parent with thetransformapplied to children, or by flattening children when a group cannot be represented.id/nameattributes as layer names where present (e.g.id="logo-mark"→ shape name), and generate stable names otherwise.Table-driven tests are the right shape here: one small fixture per supported node/attribute, one per fallback reason, plus malformed input.
Acceptance Criteria
parseXmlhandles self-closing tags, quoted attributes, comments, CDATA and standard entities, and errors clearly on malformed input with the offending position.rect,circle,path(M/L/H/V/C/S/Q/T/Z, absolute and relative),textandgconvert to shape descriptors with geometry consistent betweenselrect/pointsand the shape's own coordinates.pathnodes containingA/aare reported as unsupported (fallback), with the reason recorded.translate/scale/matrixare flattened into geometry;rotate/skeware reported as unsupported rather than emitted incorrectly.fill(hex/none),fill-opacity,strokeandstroke-widthmap onto Penpot fill/stroke records.skippedwith a specific reason — no node is ever dropped silently (asserted by a test that counts input vs output+skipped).id/nameattributes when present, with stable generated names otherwise.reportgives node counts, converted/fallback totals and the distinct fallback reasons.Test Plan
Fixture-driven checks:
skippedempty.pathwithC/Z→ converted to path shapes; compare segment counts against the fixture's command count.Acommand → the node is reported unsupported (not silently malformed).filter,mask,linearGradientand dashed strokes → every one of those nodes appears inskippedwith its own reason.<) → a clear parse error, not an empty result.pi-loop opened and merged a pull request for this issue: #242