🤖 Restructure SVG skill: extract references, add legibility rules
Extract inline SVG rules to references/inline-svg.md for progressive disclosure. Add references/layout-legibility.md with 7 rules for readable SVG output (text sizing, contrast, margins, chart patterns, palettes) based on concrete failure modes from issue #4. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# Inline SVG in Markdown — Per-Renderer Rules
|
||||
|
||||
Read this document only when the user explicitly asks for inline SVG (e.g., "embed it inline", "put the SVG directly in the markdown"). Pick the rule set that matches the target renderer. If the target is unknown, ask.
|
||||
|
||||
## Rule 1: Embed as a raw HTML block, never as a fenced code block
|
||||
|
||||
The two paths look similar but produce completely different output.
|
||||
|
||||
- **Embed SVG as a raw HTML block.** Place `<svg …>…</svg>` directly in the Markdown source, with a blank line before and after. CommonMark / GFM treat that as a raw HTML block and pass it through verbatim; the browser then parses what survives the sanitizer as real SVG.
|
||||
- **Never wrap renderable content in ` ```svg ` (or ` ```html `, ` ```xml `) fences.** Fenced code blocks are *by definition* "display this as text". The renderer wraps the contents in `<pre><code>`, HTML-escapes every `<`, `>`, and `&`, and hands the escaped text to a syntax highlighter. The browser sees a string, not markup — no SVG ever gets parsed.
|
||||
- **Don't assume non-standard "render this code block" extensions exist.** Some renderers (GitLab, certain Obsidian plugins, custom static-site setups) special-case ` ```mermaid ` or ` ```svg ` to render. This is non-standard. GitHub, VS Code's default preview, and CommonMark do not. Default to the raw-HTML-block path.
|
||||
- **Same rule applies to other inline-renderable HTML.** `<details>`/`<summary>`, `<picture>`, `<video>`, raw `<table>` with attributes Markdown can't express — all use the raw-HTML-block path, never a fence.
|
||||
|
||||
Diagnostic: "I see SVG source code as text" → fence problem. "I see blank space or default-styled shapes" → sanitizer/styling problem (see Rule 2).
|
||||
|
||||
## Rule 2: Per-renderer element / attribute rules
|
||||
|
||||
| Element / feature | GitHub inline `.md` | Gitea (default) | Obsidian | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `<svg>` | ❌ stripped | ✅ | ✅ | |
|
||||
| `<path>` | ❌ | ✅ | ✅ | The most portable shape primitive. |
|
||||
| `<rect>` | ❌ | ❌ | ✅ | Substitute with `<path>` rectangle on Gitea. |
|
||||
| `<circle>` | ❌ | ❌ | ✅ | Substitute with `<ellipse rx=R ry=R>` (Obsidian only) or `<path>` arc circle. |
|
||||
| `<ellipse>` | ❌ | ❌ | ✅ | |
|
||||
| `<line>`, `<polyline>`, `<polygon>` | ❌ | ❌ | ✅ | |
|
||||
| `<text>`, `<tspan>` | ❌ | ❌ | ✅ | |
|
||||
| `<g>` | ❌ | ❌ | ✅ | Group inheritance dies on Gitea — repeat presentation attrs per element if cross-renderer matters. |
|
||||
| `<defs>`, `<linearGradient>`, `<radialGradient>`, `<stop>` | ❌ | ❌ | ✅ | |
|
||||
| `<marker>` | ❌ | ❌ | ✅ | Arrowheads only work on Obsidian inline. |
|
||||
| `<use>` | ❌ | ❌ | ❌ (empirically confirmed) | Strip on Obsidian was unexpected; treat as not portable. |
|
||||
| `<style>` element | ❌ | ❌ | ❌ (empirically confirmed) | Don't use it anywhere. CSS rules don't have a clear scope boundary so sanitizers strip them as injection risk. |
|
||||
| `<title>` element | ❌ | ❌ | ❌ (empirically confirmed) | |
|
||||
| `<foreignObject>` | ❌ | ❌ | ❌ (empirically confirmed) | |
|
||||
| `<script>`, `on*` event handlers, `javascript:` URLs | ❌ | ❌ | ❌ | Always stripped, every renderer. Don't use. |
|
||||
| `class=` attribute (for styling) | ❌ | ❌ | ✅ | Even when class survives, the `<style>` block defining the rules doesn't, so styling is lost — use inline presentation attributes. |
|
||||
| Inline presentation attributes (`fill`, `stroke`, `stroke-width`, `font-size`, `font-family`, `text-anchor`, `stroke-dasharray`, etc.) | ❌ | ❌ | ✅ | These survive on Obsidian and any sanitizer that allows SVG at all. They never apply on GitHub-inline / Gitea because the elements themselves are stripped. |
|
||||
|
||||
Reading the table: an Obsidian-only target gives you most of SVG. A Gitea target restricts you to `<svg>` + `<path>` (no colors, no text, no groups). A GitHub-inline target gives you nothing — switch to the external-file recipe.
|
||||
|
||||
## Rule 3: Surviving the sanitizer where SVG is allowed
|
||||
|
||||
When the renderer allows SVG (Obsidian, VS Code preview, some self-hosted Gitea with extended `app.ini`, and the standalone-`.svg`-file path on GitHub), structure the SVG to survive its sanitizer:
|
||||
|
||||
- **Use SVG presentation attributes inline on every element.** `fill`, `stroke`, `stroke-width`, `stroke-dasharray`, `font-size`, `font-weight`, `font-family`, `text-anchor`. These can't escape the SVG and survive every sanitizer that allows SVG at all.
|
||||
- **Don't rely on `<style>` blocks or `class=` for styling.** `<style>` is stripped on every renderer's inline sanitizer (including Obsidian, empirically). Even if `class=` survives, the rules are gone, so styling is lost.
|
||||
- **Prefer longhand over CSS shorthand.** Use `font-size`, `font-weight`, `font-family` separately rather than the `font:` shorthand. The shorthand is CSS-only; longhand maps directly to SVG presentation attributes.
|
||||
- **`<defs>` and id references are fine** on renderers that allow them at all (Obsidian; not Gitea/GitHub-inline). `<marker>`, `<linearGradient>` referenced via `url(#id)` survive the Obsidian sanitizer.
|
||||
- **Group-level inheritance works for `font-family` and `fill`** (set on `<g>`, inherited by children) on Obsidian. Don't rely on it for cross-renderer; set the attribute on the leaf element.
|
||||
|
||||
The visible failure mode is silent: shapes still render, but with browser-default fill (usually black), no stroke, and default font sizing. On dark themes this looks like "nothing showed up"; on light themes like "a row of black blobs with no labels". There is no console error and no Markdown lint warning.
|
||||
|
||||
## Rule 4: No blank lines inside inline SVG
|
||||
|
||||
pulldown-cmark (and most CommonMark parsers) terminate an HTML block at a blank line. Any content after the blank line — even if it's still inside `<svg>...</svg>` — is parsed as Markdown, not HTML. Indented SVG elements become fenced code blocks (`<pre><code>`); unindented ones become paragraphs wrapping escaped angle brackets.
|
||||
|
||||
This means **the SVG source must be one unbroken block with no empty lines between `<svg>` and `</svg>`**. Use comments (`<!-- section: axes -->`) instead of blank lines if you need visual separation inside the SVG source.
|
||||
|
||||
Diagnostic: "Part of my SVG renders but the rest appears as code or escaped text" → blank-line-induced block break. Remove all blank lines between `<svg>` and `</svg>`.
|
||||
|
||||
## Minimal example (inline, Obsidian-friendly)
|
||||
|
||||
```markdown
|
||||
Some prose before the diagram.
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 80" width="200" height="80">
|
||||
<g font-family="sans-serif" font-size="12" fill="#222">
|
||||
<!-- inputs -->
|
||||
<rect x="10" y="20" width="80" height="40" fill="#e8f0fe" stroke="#1a73e8" stroke-width="2"/>
|
||||
<text x="50" y="45" text-anchor="middle">Input</text>
|
||||
<!-- arrow -->
|
||||
<line x1="90" y1="40" x2="130" y2="40" stroke="#1a73e8" stroke-width="2"/>
|
||||
<!-- outputs -->
|
||||
<rect x="130" y="20" width="60" height="40" fill="#fce8e6" stroke="#d93025" stroke-width="2"/>
|
||||
<text x="160" y="45" text-anchor="middle">Output</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
Prose after.
|
||||
```
|
||||
|
||||
Note: blank line before `<svg>`, blank line after `</svg>`, **no blank lines inside the SVG** (use `<!-- comments -->` for section separation), no fence, all styling via presentation attributes, font longhand on the group. This will render on Obsidian and similar permissive renderers. It will **not** render on GitHub `.md`. It will partially render on Gitea (the `<svg>` survives, the rest is stripped).
|
||||
|
||||
## Pre-commit checklist (inline SVG)
|
||||
|
||||
- [ ] Confirmed the target renderer allows SVG inline (not GitHub `.md`).
|
||||
- [ ] `<svg>` block has a blank line before and after, no surrounding ` ``` ` fence.
|
||||
- [ ] No blank lines between `<svg>` and `</svg>` (pulldown-cmark terminates the HTML block at blank lines).
|
||||
- [ ] No `<style>`, `<title>`, `<use>`, `<foreignObject>` elements (don't survive Obsidian).
|
||||
- [ ] No `class=` for styling, no CSS `font:` shorthand.
|
||||
- [ ] Every shape has explicit `fill` and (where applicable) `stroke` / `stroke-width` as inline attributes.
|
||||
- [ ] No `<script>`, `on*` handlers, or `javascript:` URLs.
|
||||
- [ ] `width` ≤ 750px unless asked otherwise.
|
||||
|
||||
If the target is Gitea specifically and inline is required:
|
||||
|
||||
- [ ] Only `<svg>` + `<path>` elements used.
|
||||
- [ ] No `fill`, `stroke`, `class`, `style`, `transform` attributes (stripped by default policy).
|
||||
- [ ] OR confirmed the Gitea instance has `[markup.sanitizer.N]` extensions in `app.ini` permitting more.
|
||||
Reference in New Issue
Block a user