first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
---
name: sp-compress-svg
description: SVG optimisation and minification with pure-TypeScript parser, plugin pipeline, and SVGO-compatible presets.
---
# Compress SVG for Suaveplan
Pure-TypeScript SVG minifier with no DOM dependencies. Parses SVG strings into an AST, runs a plugin pipeline with optional multipass, and serializes back to compact markup. Ships with 8 built-in plugins and 3 presets, plus OpenTelemetry instrumentation.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `compressSvg` | Orchestrates parse → plugin pipeline → multipass loop → serialize, returning `CompressResult` with size metrics |
| `parseSvg` | Hand-written push parser that tokenizes raw SVG into `SvgNode` AST (elements, text, comments, CDATA, DOCTYPE) |
| `serializeSvg` | Compact serializer that renders `SvgNode` back to XML, collapsing empty elements to self-closing tags |
| `applyPlugins` | Recursively walks the AST depth-first, passing each node through every plugin in order |
| `SvgPlugin` | Contract object `{ name, apply(node, options) }` — returns mutated node, `null` to remove |
| `cleanupAttrs` | Strips redundant default attributes (e.g. `fill="black"`, `opacity="1"`) and empty values |
| `convertPathData` | Rounds numeric coordinates in `path@d` to `floatPrecision` decimal places |
| `convertTransforms` | Rounds numeric values in `transform` attributes to `transformPrecision` decimal places |
| `collapseGroups` | Removes single-child `<g>` elements by merging their attributes onto the child (skips if `id` present or attr conflicts) |
| `mangleIds` / `createMangleIds` | Shortens `id` attributes to generated short IDs and rewrites `href` / `xlink:href` fragment references |
| `removeComments` | Drops `<!-- … -->` comment nodes from the AST |
| `removeMetadata` | Drops `<metadata>` element nodes |
| `removeDoctype` | Drops `<!DOCTYPE …>` doctype nodes |
## File Structure
```
devtools/docs/compress-svg/
src/core/compress-svg/ # compressSvg entry point and CompressResult types
src/parser/svg/ # parseSvg / serializeSvg push-parser and compact serializer
src/plugins/*/ # 8 built-in plugins (cleanup-attrs, collapse-groups, convert-path-data, convert-transforms, mangle-ids, remove-comments, remove-doctype, remove-metadata)
src/presets/ # PRESET_DEFAULT, PRESET_SAFE, PRESET_AGGRESSIVE
src/types/ # SvgNode, SvgPlugin, SvgPluginOptions, CompressOptions, CompressResult, Zod schemas
src/error-codes/ # SVG_COMPRESS_CODES and SvgCompressError via @suaveplan/error
src/constants/ # DEFAULT_FLOAT_PRECISION (3), DEFAULT_TRANSFORM_PRECISION (5), MAX_MULTIPASS_ITERATIONS (10), default SVG attribute map
```
Sibling packages:
- `@suaveplan/pdf` — Server-tier PDF 1.4 generation toolkit for document output
- `@suaveplan/sitemap` — Sitemap.xml generation and parsing per sitemaps.org protocol
- `@suaveplan/docs` — Markdown documentation toolkit with monorepo discovery and search
## Patterns
### Quick Start
```typescript
import { compressSvg } from '@suaveplan/compress-svg';
const result = compressSvg(svgString);
console.log(result.data); // optimised SVG
console.log(result.ratio); // e.g. 0.78
console.log(result.passCount); // multipass iterations used
```
### Using Presets
```typescript
import { compressSvg, PRESET_DEFAULT, PRESET_SAFE, PRESET_AGGRESSIVE } from '@suaveplan/compress-svg';
// Default: removeComments + metadata + doctype + collapseGroups + cleanupAttrs + convertPathData
compressSvg(svg, { plugins: PRESET_DEFAULT });
// Safe: no ID mangling, no transform conversion
compressSvg(svg, { plugins: PRESET_SAFE });
// Aggressive: all plugins including mangleIds and convertTransforms
compressSvg(svg, { plugins: PRESET_AGGRESSIVE });
```
### Custom Plugin
```typescript
import type { SvgPlugin } from '@suaveplan/compress-svg';
const removeTitle: SvgPlugin = {
name: 'removeTitle',
apply(node) {
return node.type === 'element' && node.name === 'title' ? null : node;
},
};
compressSvg(svg, { plugins: [removeTitle, ...PRESET_DEFAULT] });
```
### Standalone Parser / Serializer
```typescript
import { parseSvg, serializeSvg } from '@suaveplan/compress-svg/parser/svg';
const node = parseSvg('<svg><circle r="5"/></svg>');
const serialized = serializeSvg(node);
```
### Subpath Export for a Single Plugin
```typescript
import { convertPathData } from '@suaveplan/compress-svg/plugins/convert-path-data';
import { mangleIds, createMangleIds } from '@suaveplan/compress-svg/plugins/mangle-ids';
// createMangleIds returns a fresh plugin instance with isolated ID mapping
const freshMangle = createMangleIds();
```
## Constraints
- **Pure TypeScript — no DOMParser or external SVG libraries.** The parser is hand-written and runs in Bun, Node.js, browsers, and edge runtimes.
- **Plugins must be pure functions.** A plugin's `apply` must not mutate the input node in place; it should return a new object or `null` to remove the node.
- **`compressSvg` defaults to multipass with a hard ceiling of 10 iterations.** Passes stop early when serialized byte size stabilizes.
- **`mangleIds` is stateful per instance.** Reuse the same `mangleIds` export across multiple SVGs and IDs will collide; use `createMangleIds()` for isolated mappings.
- **`collapseGroups` never collapses a `<g>` with an `id` attribute** or when group and child have conflicting attributes, to preserve selector targets and rendering.
- **Default attribute allowlist is fixed.** `cleanupAttrs` only strips the 11 hard-coded defaults (e.g. `fill="black"`, `opacity="1"`); custom defaults are not configurable.
- **Error codes are namespaced via `defineCodes`.** Always assert against `SVG_COMPRESS_CODES.PARSE_FAILED`, `INVALID_SVG`, or `PLUGIN_FAILED` — never raw strings.
## Checklist
1. Import `compressSvg` from the root or `@suaveplan/compress-svg/core/compress-svg` subpath.
2. Decide on a preset (DEFAULT / SAFE / AGGRESSIVE) or build a custom `SvgPlugin[]` pipeline.
3. If using `mangleIds` across multiple unrelated SVGs, instantiate via `createMangleIds()` instead of the shared export.
4. Validate inputs are non-empty strings; `parseSvg` throws `SVG_COMPRESS_CODES.PARSE_FAILED` on empty or malformed XML.
5. Check that `CompressResult.ratio` is computed as `compressedSize / originalSize` (not a percentage).
6. If adding a custom plugin, ensure it returns `null` to prune nodes and does not mutate the input node in place.
7. Run `bunx turbo lint test typecheck build --filter=@suaveplan/compress-svg` before declaring changes complete.