feat: introduce local-first SVG workbench
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# Accessibility audit
|
||||
|
||||
The audit is evidence-based and intentionally narrower than a browser or
|
||||
assistive-technology accessible-name computation.
|
||||
|
||||
Implemented checks include:
|
||||
|
||||
- root accessible name evidence from direct `title`, `aria-label` or a uniquely
|
||||
resolved `aria-labelledby` target;
|
||||
- missing root description (`desc`/`aria-describedby` evidence);
|
||||
- missing, duplicate or ambiguous ARIA ID references;
|
||||
- focusable graphical/link elements without local naming evidence;
|
||||
- selected fill/stroke contrast evidence where simple literal colours can be
|
||||
evaluated;
|
||||
- reference diagnostics that can invalidate accessible relationships.
|
||||
|
||||
Findings identify severity, rule, evidence, limitation and a suggested fix.
|
||||
Safe automatic fixes insert a root `title` or `desc` through an exact source
|
||||
patch, preserve surrounding source and are undoable.
|
||||
|
||||
Limitations: the audit does not compute CSS cascade, rendered visibility,
|
||||
language, reading order, browser/AT mappings, text contrast over arbitrary
|
||||
backgrounds, keyboard interaction quality or author intent. It does not certify
|
||||
WCAG conformance. Generated placeholder title/description text must be replaced
|
||||
with an author-appropriate description and tested with target browsers and
|
||||
assistive technology.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Animation
|
||||
|
||||
Native SVG animation elements are discovered/countable in source but removed
|
||||
from every rendered editing projection, so imported SMIL and event-triggered
|
||||
animation cannot execute automatically. Imported scripts are never supported.
|
||||
|
||||
Version 0.1.0 implements a deliberately small app-owned animation slice:
|
||||
|
||||
- add an opacity animation to the selected element;
|
||||
- enable/disable/remove definitions;
|
||||
- preview generated CSS only in the opaque-origin canvas;
|
||||
- persist definitions in an explicit `.svgtools.json` project;
|
||||
- apply validated CSS plus stable target IDs to source in one transaction.
|
||||
|
||||
One shared validator is used for project import, preview and Apply. It bounds
|
||||
definition/keyframe counts and strings, requires finite ordered offsets and
|
||||
bounded timing/iteration values, allowlists property/kind/direction/fill mode,
|
||||
accepts only safe easing grammar and parses every CSS value with css-tree. URLs,
|
||||
resource functions, structural CSS/XML characters and parser raw nodes are
|
||||
rejected even for disabled definitions. Selectors and keyframe identifiers are
|
||||
escaped/generated by the app.
|
||||
|
||||
The preview has play/pause as an enable toggle but no timeline, seek or native
|
||||
SMIL editing. Duration/keyframes are not yet exposed as a general UI. Transform,
|
||||
colour, motion-path and compatible path-morph authoring, animation discovery
|
||||
panels and conversion between CSS/SMIL remain future milestones. Applying CSS
|
||||
changes the SVG source intentionally and may affect downstream consumers that
|
||||
do not support the same CSS animation features.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Architecture
|
||||
|
||||
## Boundaries
|
||||
|
||||
SVG Tools is a static Vite application. React owns controls and projections,
|
||||
not the document. Canonical SVG source is the only persistent document
|
||||
representation. Normal sessions stay in memory; explicit downloads are the
|
||||
only persistence boundary.
|
||||
|
||||
```text
|
||||
canonical source
|
||||
├─ ranged XML scan + DOM parse → semantic document → tree/inspectors
|
||||
├─ security policy + DOMPurify → editing projection → opaque iframe
|
||||
└─ source patches/transactions ← visual commands and explicit workflows
|
||||
```
|
||||
|
||||
The last valid semantic document and projection remain available while current
|
||||
source is invalid. Revision checks reject stale visual, worker or dialog results.
|
||||
|
||||
## Representations
|
||||
|
||||
1. **Source document:** exact user text, including comments, quotes and
|
||||
whitespace.
|
||||
2. **Semantic document:** SVG elements, attributes, source ranges, in-memory
|
||||
keys, relationships, preferences, metrics and diagnostics.
|
||||
3. **Editing projection:** cloned, sanitized SVG with temporary mapping
|
||||
attributes. It is never exported as current SVG.
|
||||
4. **Preview:** the projection in a sandboxed opaque-origin iframe, plus a
|
||||
parent-owned overlay for bounds and path controls.
|
||||
|
||||
Imported source is never rendered through React or inserted into the main DOM.
|
||||
The iframe controller is a fixed packaged asset, accepts only its parent and a
|
||||
random per-instance channel, and removes its script element after startup.
|
||||
|
||||
## Commands and workers
|
||||
|
||||
Every accepted edit is a transaction containing exact before/after source,
|
||||
patches, selection and revision. Undo validates the current source before
|
||||
restoring exact text. Fast consecutive source/key nudge edits may merge under a
|
||||
bounded merge key.
|
||||
|
||||
SVGO executes in a dedicated module worker. Jobs have IDs, timeout,
|
||||
supersession, AbortSignal cancellation and immediate deterministic rejection.
|
||||
Parsing currently uses bounded synchronous browser XML facilities because its
|
||||
`XMLDocument` projection is not transferable; large inputs are throttled and
|
||||
hard-limited. Rasterization uses a sanitized Blob URL and Canvas 2D.
|
||||
|
||||
## Release boundary
|
||||
|
||||
`base: './'` keeps every asset relocatable. The independent ZIP is built and
|
||||
checked here. Toolbox Portal consumes the immutable ZIP and checksum through a
|
||||
release lock; it never compiles SVG Tools source. AppShell reads optional
|
||||
Toolbox context but standalone operation is the default fallback.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Document model
|
||||
|
||||
## Source ranges
|
||||
|
||||
The lexical scan records exact half-open offsets for each opening/full element,
|
||||
name and quoted attribute. CodeMirror's Lezer XML parser supplies ranged syntax
|
||||
errors; a separate stack scan rejects structures browser DOM parsers might
|
||||
repair. The DOM parse supplies namespace and decoded semantic values. Element
|
||||
order must match the lexical tokens or parsing fails with a source-map mismatch.
|
||||
|
||||
DOCTYPE text remains in canonical source but is removed before DOM parsing.
|
||||
Entity declarations are rejected for the semantic model. External entities are
|
||||
never resolved.
|
||||
|
||||
## Stable keys
|
||||
|
||||
A unique explicit SVG `id` becomes `id:<id>`. Otherwise a key hashes structural
|
||||
path, source offset, local name and optional ID; collision suffixes are
|
||||
in-memory. Reparsing retains explicit-ID identity and best-effort structural
|
||||
identity. Keys appear only as temporary `data-svg-tools-node` attributes in the
|
||||
editing projection and are stripped from exports.
|
||||
|
||||
## Semantic nodes
|
||||
|
||||
Nodes retain namespace/name, parent and children, depth, ID/classes, decoded
|
||||
attributes, exact attribute ranges, render status and leaf text. The document
|
||||
also records indentation/newline/quote preference, ordered keys, diagnostics
|
||||
and bounded metrics.
|
||||
|
||||
Unknown well-formed source remains canonical even when there is no inspector.
|
||||
A targeted command patches only its attribute/tag range. Whole-source
|
||||
serialization occurs only for explicit format, sanitize or optimize workflows,
|
||||
each with a source diff.
|
||||
|
||||
## Invalid state
|
||||
|
||||
Every source update gets a monotonically increasing revision. If current source
|
||||
is invalid, diagnostics refer to that text while the canvas/tree use the last
|
||||
valid snapshot and all visual controls are disabled. Once the same current
|
||||
revision parses and sanitizes successfully it atomically replaces the snapshot.
|
||||
|
||||
Resource limits can make otherwise well-formed source invalid for this app.
|
||||
Limits cover bytes, structure, path/CSS/reference/animation/filter counts,
|
||||
embedded data and total text. This prevents unbounded model construction but is
|
||||
not a promise that every file below a limit is inexpensive on every device.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Optimization
|
||||
|
||||
SVG Tools ships SVGO 4.0.2 (`gitHead`
|
||||
`b2309cf541aee11634eb653157b0ff86ab326e98`) inside a module worker. The exact
|
||||
version and active plugin list are displayed with every result.
|
||||
|
||||
## Profiles
|
||||
|
||||
- **Conservative:** cleanup and safe numeric/style normalization while retaining
|
||||
IDs, metadata, viewBox, dimensions and accessibility content.
|
||||
- **Balanced:** additional structural cleanup with protected identifiers and
|
||||
accessibility-sensitive content.
|
||||
- **Aggressive:** explicitly lossy/structural choices for users who accept the
|
||||
disclosed risk.
|
||||
|
||||
Optional plugins are selected explicitly in the dialog; plugin names are mapped
|
||||
to an application allowlist rather than passed through from imported data. See
|
||||
`src/optimization/profiles.ts` for the authoritative ordered lists.
|
||||
|
||||
The worker returns source, exact before/after UTF-8 byte counts, version and
|
||||
plugins. The main thread rejects stale job IDs. Closing/canceling, a superseding
|
||||
request or AbortSignal immediately rejects the old promise, removes listeners,
|
||||
clears its timer and terminates its worker. A 30-second safety timeout is the
|
||||
final boundary.
|
||||
|
||||
Optimization never mutates source automatically. A result remains a candidate
|
||||
with size summary, source diff and side-by-side isolated visual comparison until
|
||||
Apply. Applying checks the source revision and produces one undoable transaction.
|
||||
|
||||
Even conservative SVGO passes can change insignificant lexical form and browser
|
||||
edge behavior. Aggressive plugins can change IDs, metadata, editorability or
|
||||
rendering. Visual comparison cannot prove semantic equivalence, especially for
|
||||
fonts, filters, animation and external consumer behavior. Keep original source
|
||||
and use project/domain fixtures for important artwork.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Path editor
|
||||
|
||||
The project-authored path core accepts every SVG path command: `M/m`, `L/l`,
|
||||
`H/h`, `V/v`, `C/c`, `S/s`, `Q/q`, `T/t`, `A/a` and `Z/z`, including repeated
|
||||
parameter groups, implicit lines after move, packed arc flags, exponents,
|
||||
relative forms and multiple subpaths. It rejects non-finite values, malformed
|
||||
arity/flags and paths beyond the command limit.
|
||||
|
||||
Segments resolve to absolute geometry while retaining source command/form and
|
||||
fragment offsets for the command table. Serialization intentionally normalizes
|
||||
edited geometry; no editor metadata is emitted.
|
||||
|
||||
## Controls
|
||||
|
||||
- Every segment endpoint is an anchor.
|
||||
- Cubics show both controls. `S` reflects the previous cubic control and marks
|
||||
the resulting handle as derived/dashed until edited.
|
||||
- Quadratics show their one control. `T` reflects the previous quadratic
|
||||
control and marks it derived.
|
||||
- Arcs expose endpoint, radii and derived center controls and show rotation,
|
||||
large-arc and sweep flags in the command panel.
|
||||
- Handle radii and strokes use screen-oriented overlay styling so they remain
|
||||
usable across zoom levels.
|
||||
|
||||
The full selected-element/ancestor transform chain maps local path geometry into
|
||||
root space. Pointer coordinates use its inverse, including an active preview
|
||||
matrix; non-invertible chains disable handles. Dragging commits one transaction
|
||||
on release; arrow keys use 1, Shift+10 or Alt+0.1 units and merge safely.
|
||||
|
||||
Segment splitting uses the mathematical midpoint: linear interpolation for
|
||||
lines, de Casteljau for quadratic/cubic curves, and arc subdivision preserving
|
||||
the ellipse. Path reversal reverses every supported segment/subpath and adjusts
|
||||
controls/sweep where required.
|
||||
|
||||
## Current limits
|
||||
|
||||
The 0.1.0 UI supplies drag/nudge, split at 50%, reverse and a read-only numeric
|
||||
command table. It does not yet supply node modes, multi-selection, deletion,
|
||||
conversion, open/close, join/break/combine, subpath-start changes, arbitrary
|
||||
split parameter, pen creation, snapping, simplification or boolean operations.
|
||||
Visual editing expands shorthand/relative commands to normalized absolute data;
|
||||
the dialog/help discloses that source-form change.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Security model
|
||||
|
||||
SVG is active content. Threats include script/event execution, navigation,
|
||||
external fetches and tracking, CSS resource resolution, `foreignObject`, native
|
||||
animation triggers, namespace confusion, entity/DOCTYPE processing, parser
|
||||
differentials, oversized compressed/data content and algorithmic complexity.
|
||||
|
||||
## Source and projection
|
||||
|
||||
Canonical source is inert text and remains available even when unsafe. A
|
||||
separate projection is built from a cloned semantic document. DOMPurify's SVG
|
||||
profiles are followed by an application policy that:
|
||||
|
||||
- removes `script`, `foreignObject`, `animate`, `animateMotion`,
|
||||
`animateTransform`, `set`, foreign namespaces and all `on*` attributes;
|
||||
- disables links/navigation and executable or external URL attributes;
|
||||
- allows only unique local fragment references and bounded base64 raster image
|
||||
data (`png`, `jpeg`, `gif`, `webp`, `avif`);
|
||||
- parses style declarations/stylesheets with css-tree and rejects `@import`,
|
||||
unsafe legacy properties, parse failures, non-local URLs, `expression()` and
|
||||
resource-producing functions such as `image-set()` and `paint()`;
|
||||
- reports findings with source ranges wherever a semantic node/range exists.
|
||||
|
||||
DOCTYPE is excluded before DOM parsing; entity declarations invalidate the
|
||||
semantic projection. Sanitized export removes temporary mapping attributes.
|
||||
Sanitize is never applied to canonical source without preview and acceptance.
|
||||
|
||||
## Isolated preview
|
||||
|
||||
The projection is an iframe `srcdoc` with `sandbox="allow-scripts"` and no
|
||||
`allow-same-origin`, giving it an opaque origin. Its child CSP is:
|
||||
|
||||
```text
|
||||
default-src 'none'; connect-src 'none'; object-src 'none'; frame-src 'none';
|
||||
base-uri 'none'; form-action 'none'; img-src data: blob:;
|
||||
style-src 'unsafe-inline'; script-src <controller-origin>
|
||||
```
|
||||
|
||||
The sole script is the packaged `canvas-frame-controller.js`. It is loaded with
|
||||
anonymous CORS, accepts messages only from `parent` with a random channel,
|
||||
validates message fields and removes its script element. The parent validates
|
||||
both `event.source` and channel.
|
||||
|
||||
Because a sandbox without same-origin treats the script request as cross-origin,
|
||||
serve this one app-relative asset with:
|
||||
|
||||
```text
|
||||
Access-Control-Allow-Origin: *
|
||||
Cross-Origin-Resource-Policy: cross-origin
|
||||
```
|
||||
|
||||
Keep `Cross-Origin-Resource-Policy: same-origin` for other files. Use an Nginx
|
||||
`map`/header value, not a nested `location` that would drop inherited security
|
||||
headers. Toolbox Portal 0.10.0 implements and tests this exception.
|
||||
|
||||
## Limits and tests
|
||||
|
||||
Hard limits are defined in `src/app/limits.ts`. SVGZ uses streaming
|
||||
decompression and stops once output crosses the source limit. Workers are
|
||||
revisioned, timed out and cancelable. Tree/diagnostic rendering is capped.
|
||||
|
||||
Project-authored tests cover script, handlers, links, external image/use/filter,
|
||||
CSS imports/URLs/parser failures/resource functions, native animation,
|
||||
namespaces, duplicate IDs/cycles, malformed entities/XML, depth/attribute/path
|
||||
and data limits. Browser tests assert no script callback and no request to the
|
||||
hostile domain.
|
||||
|
||||
## Limitations and reporting
|
||||
|
||||
This policy protects this application's projections and exports; it is not a
|
||||
general guarantee for arbitrary downstream embedding. Browser SVG/CSS parsers
|
||||
and DOMPurify remain dependency/security boundaries. Bounded data URLs may still
|
||||
decode expensive images within the raster-pixel limits. CSP header regressions
|
||||
can break the fixed controller even when the content remains inert.
|
||||
|
||||
Report vulnerabilities privately through the repository owner/contact before
|
||||
opening a public issue when disclosure could expose users. Include the SVG,
|
||||
browser, deployment headers and observed network/execution behavior without
|
||||
sensitive user files.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Transform model
|
||||
|
||||
SVG transform functions are parsed without changing order: `matrix`,
|
||||
`translate`, `scale`, `rotate` (with optional center), `skewX` and `skewY`.
|
||||
The inspector shows each selected/ancestor source transform, accumulated matrix,
|
||||
determinant/invertibility diagnostics and the candidate matrix.
|
||||
|
||||
A draft combines translate, origin, rotate, scale and skew. It affects only the
|
||||
iframe/overlay until Apply. The proposed source and rendering are shown before
|
||||
one revision-checked transaction; undo restores the exact pre-apply text.
|
||||
|
||||
## Bake support
|
||||
|
||||
| Element | Supported result |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `path` | all segment points transformed; arcs retained for safe conformal cases or converted/disclosed when required |
|
||||
| `line` | transformed endpoints, remains `line` |
|
||||
| `polyline`, `polygon` | transformed point list, same element |
|
||||
| `rect` | remains `rect` for axis-aligned safe cases; otherwise candidate path |
|
||||
| `circle` | remains circle under conformal scale; otherwise explicit ellipse/path as required |
|
||||
| `ellipse` | remains ellipse for safe axis-aligned cases; otherwise path |
|
||||
|
||||
Numbers remain full precision internally and are serialized with stable bounded
|
||||
precision only at the explicit operation boundary. Invalid/non-finite geometry,
|
||||
negative radii and unsupported element types fail before patch creation.
|
||||
|
||||
## Policies and limits
|
||||
|
||||
- Non-conformal transforms with a visible stroke produce a warning; stroke
|
||||
width/vector-effect is not silently rewritten.
|
||||
- General arc conversion is disclosed in the preview.
|
||||
- Shared gradients, patterns, markers, filters, masks and clips are indexed but
|
||||
not cloned or remapped during bake.
|
||||
- Text transforms remain attributes; text-to-path is not implemented.
|
||||
- Group flattening and partial multi-child baking are not implemented.
|
||||
- Consolidation is available through an appended matrix candidate; a full
|
||||
editable transform-list UI/decomposition is future work.
|
||||
|
||||
Browser rendering, particularly text and filters, is the visual oracle; the
|
||||
unit suite checks matrix and geometry invariants but does not claim pixel-perfect
|
||||
equivalence for every renderer.
|
||||
Reference in New Issue
Block a user