first commit
This commit is contained in:
126
plugins/library-navigator/skills/sp-compress-svg/SKILL.md
Normal file
126
plugins/library-navigator/skills/sp-compress-svg/SKILL.md
Normal 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.
|
||||
Reference in New Issue
Block a user