Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0d244e9
Add FastAPI web service with Next.js UI
jamesbconner Feb 21, 2026
415ab90
chore(api): improve type hints and code quality
jamesbconner Feb 21, 2026
ec5d09d
Use safe_path_join for file uploads and sanitize SVG content
jamesbconner Feb 21, 2026
23626d3
Include web dependencies in CI environment
jamesbconner Feb 21, 2026
991c1c0
Improve SVG sanitization with constants
jamesbconner Feb 22, 2026
20e15d7
Handle duplicate filenames and improve SVG sanitization logic
jamesbconner Feb 22, 2026
d307586
Extract SVG upload processing into shared service
jamesbconner Feb 22, 2026
2bcb150
Add API and processing test suite
jamesbconner Feb 22, 2026
d498e99
Format test files for consistency
jamesbconner Feb 22, 2026
e9dda07
Suppress mypy unused-ignores warning and add type ignore comments
jamesbconner Feb 22, 2026
915c05a
Return svg paths from process_svg_uploads
jamesbconner Feb 22, 2026
afebb6a
Add conflict error handling for duplicate icon names
jamesbconner Feb 22, 2026
616eaef
Preserve SVG namespace without ns0 prefixes
jamesbconner Feb 22, 2026
022c1e3
Handle ValueError explicitly in routers instead of globally
jamesbconner Feb 22, 2026
1393bf1
Enable CSS styling by default for SVG processing
jamesbconner Feb 22, 2026
bb72548
Remove unnecessary type ignore comment
jamesbconner Feb 22, 2026
6d9cfe7
Remove global ValueError handler
jamesbconner Feb 22, 2026
b13875e
Handle ValueError for invalid css_mode in add and create endpoints
jamesbconner Feb 22, 2026
bb9bfc5
Remove global ImportError handler and add data URI sanitization
jamesbconner Feb 22, 2026
c491ec5
Sanitize filenames before deduplication to prevent collisions
jamesbconner Feb 22, 2026
3849081
Validate icon_names parameter is JSON array and add web UI build to p…
jamesbconner Feb 22, 2026
309b177
Extract icon name parsing and add sizing validation
jamesbconner Feb 22, 2026
64e4e78
Validate all elements are strings in icon_names array
jamesbconner Feb 22, 2026
b1abaff
Refine data URI sanitization to allow safe image types
jamesbconner Feb 22, 2026
7231a46
Handle case-insensitive dangerous element matching
jamesbconner Feb 22, 2026
848aa5e
Extract library error handling to shared utility
jamesbconner Feb 22, 2026
5b69094
Handle invalid library format errors in rename endpoint
jamesbconner Feb 22, 2026
ea16510
Update branding and navigation link
jamesbconner Feb 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:

- name: Install dependencies
run: |
uv pip install --system -e ".[dev]"
uv pip install --system -e ".[dev,web]"

- name: Run ruff format check
run: ruff format --check src tests
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ jobs:
with:
python-version: "3.13"

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: web-ui/package-lock.json

- name: Install web UI dependencies
run: npm ci
working-directory: web-ui

- name: Build web UI and copy into package
run: make build-release

- name: Install build dependencies
run: |
uv pip install --system twine
Expand Down
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ debug/
/test_*.xml
/test_*.py

# Bundled web UI (built artifact — not source)
src/SVG2DrawIOLib/web/

# web-ui (Next.js)
web-ui/node_modules/
web-ui/.next/
web-ui/out/
web-ui/.env.local
web-ui/.env*.local

# api (FastAPI)
api/__pycache__/
api/**/__pycache__/
!web-ui/.env.production
4 changes: 3 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ repos:
- types-click
- types-setuptools
- pytest
args: [--strict]
- pydantic
- fastapi
args: [--strict, --no-warn-unused-ignores]
files: ^(src|tests)/

- repo: https://github.com/PyCQA/bandit
Expand Down
82 changes: 69 additions & 13 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,41 @@ src/SVG2DrawIOLib/
├── icon_analyzer.py # Icon extraction and analysis service
├── library_validator.py # Library validation service
├── path_splitter.py # SVG path splitting logic
└── cli/ # Modular CLI with dynamic loading
├── __init__.py # CLI entry point with dynamic command discovery
├── helpers.py # Shared CLI utilities
├── create.py # Create command
├── create_helpers.py # Business logic for create command
├── add.py # Add command
├── remove.py # Remove command
├── list.py # List command
├── extract.py # Extract command
├── rename.py # Rename command
├── inspect.py # Inspect command
├── validate.py # Validate command
└── split_paths.py # Split paths command
├── cli/ # Modular CLI with dynamic loading
│ ├── __init__.py # CLI entry point with dynamic command discovery
│ ├── helpers.py # Shared CLI utilities
│ ├── create.py # Create command
│ ├── create_helpers.py # Business logic for create command
│ ├── add.py # Add command
│ ├── remove.py # Remove command
│ ├── list.py # List command
│ ├── extract.py # Extract command
│ ├── rename.py # Rename command
│ ├── inspect.py # Inspect command
│ ├── validate.py # Validate command
│ ├── split_paths.py # Split paths command
│ └── web.py # Web UI launch command
├── api/ # FastAPI sidecar (optional web dependency)
│ ├── main.py # FastAPI app, CORS, router registration, StaticFiles mount
│ ├── dependencies.py # get_temp_dir() async generator (auto-cleanup)
│ ├── exceptions.py # Exception → HTTP response handlers
│ ├── models/
│ │ └── responses.py # Pydantic response models
│ ├── routers/ # One file per endpoint
│ │ ├── create.py # POST /api/create
│ │ ├── add.py # POST /api/add
│ │ ├── remove.py # POST /api/remove
│ │ ├── rename.py # POST /api/rename
│ │ ├── list.py # POST /api/list
│ │ ├── extract.py # POST /api/extract
│ │ ├── inspect.py # POST /api/inspect
│ │ ├── validate.py # POST /api/validate
│ │ └── split_paths.py # POST /api/split-paths
│ └── services/
│ └── processing.py # sanitize_svg_upload(), build_processing_options()
└── web/ # Pre-built Next.js static export (gitignored)
# Generated by `make build-release`; bundled into wheel
# via pyproject.toml hatchling artifacts
```

## Core Components
Expand Down Expand Up @@ -272,6 +294,40 @@ for filepath in COMMAND_DIR.iterdir():
- Shared helpers module
- Consistent error handling

### 8. FastAPI API Sidecar (`api/`)

Optional browser-based UI, installed with `pip install "SVG2DrawIOLib[web]"`.

**Request lifecycle:**
```
Browser (multipart/form-data POST)
FastAPI router (e.g. routers/create.py)
get_temp_dir() dependency — creates tmp dir, yields Path, cleans up in finally
sanitize_svg_upload() — strips scripts/event-handlers, enforces 10 MB limit
Same Python services as CLI (SVGProcessor, LibraryManager, etc.)
Response(content=out_path.read_bytes(), ...) — file read into memory before cleanup
Browser receives XML/SVG/ZIP blob
```

**Key design decisions:**

- All imports use absolute package paths (`from SVG2DrawIOLib.api.dependencies import ...`), never relative imports. Required because uvicorn addresses the app by dotted path (`SVG2DrawIOLib.api.main:app`).
- `Response(content=bytes)` instead of `FileResponse(path)` for all file-returning endpoints. `FileResponse` streams lazily and races with the `get_temp_dir()` finally-block cleanup on Windows, causing "Failed to fetch" with no server error logged.
- The StaticFiles mount (`/`) is registered **after** all API routers so `/api/*` routes always take priority.
- UI path resolution order: `SVG2DRAWIO_UI_DIR` env var → `src/SVG2DrawIOLib/web/` (bundled) → `web-ui/out/` (dev checkout).
- CORS allows `http://localhost:3000` by default (Next.js dev server). The bundled UI is served from the same origin as the API, so CORS is not needed in production — the frontend uses relative URLs (`API_BASE = ""`).

**Static export bundling:**
The `make build-release` target builds `web-ui/` with Next.js and copies the output to `src/SVG2DrawIOLib/web/`. This directory is gitignored but included in the wheel via `pyproject.toml` hatchling artifacts. After copying, a post-processing step renames Next.js RSC page payload files from subdirectory form (`create/__next.create/__PAGE__.txt`) to flat dot-separated form (`create/__next.create.__PAGE__.txt`) so Starlette's StaticFiles can serve them correctly.

---

## CLI Commands

### Library Management Commands
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.0] - 2026-02-21

### Added

- **Browser-based Web UI**: New optional web interface that exposes all CLI commands as a browser UI. Install with `pip install "SVG2DrawIOLib[web]"` and launch with `svg2drawio web`. The server automatically opens the browser and serves both the API and frontend from a single process on port 8000.
- **Six tabs**: Create (convert SVGs to a new library), Manage (add icons, remove icons, rename icons), Extract (save icons as SVG files), Inspect (view icon details and SVG previews), Validate (structural integrity report), Split Paths (split compound paths for per-path color control)
- **FastAPI sidecar** (`src/SVG2DrawIOLib/api/`): Nine REST endpoints under `/api/*` wrapping the same Python services used by the CLI. Reuses `process_svg_files()`, `determine_sizing_strategy()`, `sanitize_filename()`, and `safe_path_join()` directly — no logic duplication.
- **SVG sanitization**: Uploaded SVG files are stripped of `<script>`, `<foreignObject>`, `javascript:` hrefs, and event-handler attributes before processing. Enforces a 10 MB per-file size limit.
- **Bundled static export**: The pre-built Next.js UI is copied into `src/SVG2DrawIOLib/web/` at release time and included in the Python wheel via hatchling artifacts — no Node.js required at runtime.
- **`[web]` optional dependency group**: `fastapi>=0.115.0`, `uvicorn[standard]>=0.32.0`, `python-multipart>=0.0.12`.
- **`svg2drawio` entry point**: Added as a shorter alias alongside `SVG2DrawIOLib` and `svg2drawiolib`.
- **Web UI Makefile targets**: `make dev-api` (FastAPI with hot-reload), `make dev-web` (Next.js dev server), `make dev-all` (both together), `make build-web` (Next.js static export), `make build-release` (build + copy into package), `make start-web` (build then launch).

### Fixed

- **Windows `FileResponse` race condition** (`api/routers/`): All file-returning endpoints (`create`, `add`, `remove`, `rename`, `split-paths`) now use `Response(content=path.read_bytes())` instead of `FileResponse(path)`. `FileResponse` streams lazily — on Windows the `get_temp_dir()` dependency's `finally` cleanup block could delete the temp directory before the file body was streamed, causing "Failed to fetch" in the browser with no server-side error logged.
- **Static build CORS mismatch** (`web-ui/src/lib/api.ts`): Changed the `API_BASE` fallback from `"http://localhost:8000"` to `""` so the static export uses relative URLs (`/api/create` instead of `http://localhost:8000/api/create`). This fixes "Failed to fetch" when the browser opens to `http://127.0.0.1:8000` (the CLI's default bind address) while the hardcoded URL pointed to `http://localhost:8000` — a cross-origin mismatch the CORS policy rejected.
- **Dev-mode API routing** (`web-ui/next.config.ts`): Added a conditional dev-mode proxy rewrite that forwards `/api/*` to the FastAPI server (`http://localhost:8000`) when running `next dev`, so `make dev-all` works without a `.env.local` file. The rewrite is omitted during `next build` (static export does not support rewrites).
- **Next.js RSC `__PAGE__.txt` 404s**: `make build-release` now renames RSC page payload files from their on-disk subdirectory form (`create/__next.create/__PAGE__.txt`) to the flat dot-separated form (`create/__next.create.__PAGE__.txt`) that the browser requests. Starlette's `StaticFiles` serves paths literally, so the mismatched filenames caused 404s that silently broke client-side navigation.
- **Cross-platform `build-release` target**: Replaced Unix-only `rm -rf` and `cp -r` shell commands in the Makefile with a Python one-liner using `shutil.rmtree` and `shutil.copytree`, which works on Windows without Git Bash or WSL.

### Changed

- **`pyproject.toml`**: Added `[web]` optional dependency group; added `svg2drawio` script entry point; added `[tool.hatch.build.targets.wheel] artifacts` to include the gitignored `src/SVG2DrawIOLib/web/` directory in the built wheel.
- **`.gitignore`**: Added `src/SVG2DrawIOLib/web/` (generated build artifact); removed stale `api/__pycache__/` entries from the old top-level `api/` directory.
- **Documentation**: Updated `README.md`, `QUICKSTART.md`, and `ARCHITECTURE.md` to document the web UI feature, installation, tab-to-command mapping, developer build workflow, FastAPI sidecar architecture, and bundled static export path resolution.

## [1.2.1] - 2026-02-09

### Changed
Expand Down Expand Up @@ -203,5 +230,11 @@ The project follows SOLID principles with clear module boundaries:
- `library_manager.py`: Library file management
- `cli/`: Modular CLI with dynamic command loading

[1.3.0]: https://github.com/jamesbconner/SVG2DrawIOLib/compare/v1.2.1...v1.3.0
[1.2.1]: https://github.com/jamesbconner/SVG2DrawIOLib/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/jamesbconner/SVG2DrawIOLib/compare/v1.1.2...v1.2.0
[1.1.2]: https://github.com/jamesbconner/SVG2DrawIOLib/compare/v1.1.1...v1.1.2
[1.1.1]: https://github.com/jamesbconner/SVG2DrawIOLib/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/jamesbconner/SVG2DrawIOLib/compare/v1.0.1...v1.1.0
[1.0.1]: https://github.com/jamesbconner/SVG2DrawIOLib/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/jamesbconner/SVG2DrawIOLib/releases/tag/v1.0.0
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ include QUICKSTART.md
include pyproject.toml

recursive-include src *.py
recursive-include src/SVG2DrawIOLib/web *
recursive-include tests *.py

exclude .gitignore
Expand Down
22 changes: 21 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help install dev test lint format type security cov build check-dist clean clean-win zip all pre-commit run
.PHONY: help install dev test lint format type security cov build check-dist clean clean-win zip all pre-commit run dev-api dev-web build-web start-web build-release

help: ## Show this help message
@echo "Available targets:"
Expand Down Expand Up @@ -64,3 +64,23 @@ zip: ## Create a source archive respecting .gitignore

run: ## Run the CLI (use ARGS="..." to pass arguments)
SVG2DrawIOLib $(ARGS)

dev-api: ## Start FastAPI API server (from repo root)
uv pip install -e ".[web]"
uv run uvicorn SVG2DrawIOLib.api.main:app --reload --port 8000

dev-web: ## Start Next.js UI dev server
cd web-ui && npm run dev

dev-all: ## Start FastAPI API and Next.js UI (Unix only)
uv run uvicorn SVG2DrawIOLib.api.main:app --reload --port 8000 &
cd web-ui && npm run dev

build-web: ## Build Next.js static export into web-ui/out/ (required before svg2drawio web)
cd web-ui && npm run build

build-release: build-web ## Build Next.js UI and copy into the Python package for distribution
uv run python -c "import shutil,pathlib; w=pathlib.Path('src/SVG2DrawIOLib/web'); shutil.rmtree(w,ignore_errors=True); shutil.copytree('web-ui/out',w); [(p.rename(p.parent.parent/(p.parent.name+'.__PAGE__.txt')),p.parent.rmdir()) for p in list(w.rglob('__PAGE__.txt')) if p.parent.name.startswith('__next.')]"

start-web: build-web ## Build then launch the web UI via the CLI (opens browser)
svg2drawio web
53 changes: 53 additions & 0 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,41 @@ Options:
-q, --quiet Suppress output except errors
```

## Web UI

### Installation
```bash
pip install "SVG2DrawIOLib[web]"
```

### Launch
```bash
svg2drawio web
# Opens http://127.0.0.1:8000 in your browser automatically
```

### Options
```bash
svg2drawio web --port 9000 # Use a different port
svg2drawio web --host 0.0.0.0 # Listen on all interfaces
svg2drawio web --no-browser # Don't auto-open the browser
svg2drawio web --ui-dir /path/to/ui # Use a custom pre-built UI directory
```

### Tabs
| Tab | Equivalent CLI Command |
|-----|----------------------|
| Create | `svg2drawio create` |
| Manage → Add Icons | `svg2drawio add` |
| Manage → Remove Icons | `svg2drawio remove` |
| Manage → Rename Icon | `svg2drawio rename` |
| Extract | `svg2drawio extract` |
| Inspect | `svg2drawio inspect` |
| Validate | `svg2drawio validate` |
| Split Paths | `svg2drawio split-paths` |

---

## For Developers

### Setup
Expand Down Expand Up @@ -267,6 +302,24 @@ make all # Run all checks
make clean # Clean build artifacts
```

### Web UI Development
```bash
# Install web dependencies and start FastAPI + Next.js dev servers
make dev-api # FastAPI only on port 8000 (with --reload)
make dev-web # Next.js dev server only (port 3000)

# Build the static export and copy into the Python package
make build-release # Runs npm build + copies output to src/SVG2DrawIOLib/web/

# Build and launch the web UI from the source tree
make start-web
```

The `web-ui/` directory contains the Next.js frontend source. The built static export
is copied to `src/SVG2DrawIOLib/web/` (gitignored — generated artifact) for bundling
into the Python wheel via `make build-release`. The FastAPI server at
`SVG2DrawIOLib.api.main:app` serves both the API (`/api/*`) and the static UI (`/`).

### Running Tests
```bash
# All tests
Expand Down
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Convert SVG files into DrawIO/diagrams.net shape libraries with support for colo
- 🎨 **Color Customization**: Inject CSS classes to enable color editing directly in DrawIO's interface
- 📏 **Flexible Sizing**: Proportional scaling with aspect ratio preservation, or fixed dimensions
- 📚 **Library Management**: Create new libraries, add/remove icons, and list contents
- 🌐 **Browser-based Web UI**: All commands available in a local web interface — no command line required
- 🚀 **Modern CLI**: Beautiful, colorful output with rich-click
- 🔧 **Modern Python Stack**: Built with ruff, mypy, bandit, pytest, and pre-commit hooks

Expand Down Expand Up @@ -58,6 +59,54 @@ SVG2DrawIOLib create icons/ --css -o colorable-icons.xml
SVG2DrawIOLib create icons/ --max-size 64 -o large-icons.xml -R
```

## Web UI

All CLI commands are available as a browser-based interface. Install with the `web` optional dependencies and launch with a single command:

```bash
pip install 'SVG2DrawIOLib[web]'
svg2drawio web
```

This starts a local FastAPI server and opens the UI automatically in your default browser at `http://127.0.0.1:8000`.

**Available tabs:**

| Tab | Equivalent CLI command |
|-----|------------------------|
| Create | `svg2drawio create` |
| Manage (Add / Remove / Rename) | `svg2drawio add` / `remove` / `rename` |
| Extract | `svg2drawio extract` |
| Inspect | `svg2drawio inspect` |
| Validate | `svg2drawio validate` |
| Split Paths | `svg2drawio split-paths` |

**Options:**

```bash
svg2drawio web --host 0.0.0.0 # Listen on all interfaces
svg2drawio web --port 9000 # Custom port
svg2drawio web --no-browser # Don't open browser automatically
```

### Building from Source

If you are working from a source checkout, build the Next.js frontend first, then launch:

```bash
make build-release # builds web-ui/ and copies output into the Python package
svg2drawio web
```

Or run the frontend and API separately during development:

```bash
make dev-api # FastAPI on :8000 (terminal 1)
make dev-web # Next.js on :3000 (terminal 2)
```

---

## Documentation

- [Quick Start Guide](QUICKSTART.md) - Get started quickly for users and developers
Expand Down
Loading