# HTML and inline styles

Render Markdown HTML and restrict its elements, destinations and inline styles.

When Markdown contains an HTML card or other markup, decide whether it should become elements. By default it remains visible source text. Enable `html` when you need sanitized HTML presentation.

## Enable and restrict HTML

With `html: true`, complete HTML islands pass through an
HTML parser and fixed allowlist sanitizer before becoming Vue nodes. Block and inline HTML
are supported; inline HTML can contain Markdown, while code stays literal. Event attributes,
scripts, dangerous URLs, iframe, `<style>` tags and arbitrary template expressions cannot be enabled.

```vue
<QuireMarkdown :content="text" :html="{
  allowedElements: ['div', 'span', 'strong', 'a'],
  allowLink: url => url.startsWith('https://example.com/'),
  allowImage: () => false
}" />
```

The configuration above permits only four listed tags, restricts HTML links to one site and disallows HTML images. Setting `html: true` without these restrictions uses the built-in allowlist.

These options only further restrict sanitized tags and HTML URLs; they do not change ordinary
Markdown link policy. HTML ids and fragment links are namespaced per reader. Document HTML
cannot register Vue components; use the component map or reading contributions instead.
Incomplete HTML, islands above 65,536 UTF-16 units, element depth above 32, or the parser node budget
retain source. HTML blocks do not reinterpret Markdown inside them. Block elements in a
paragraph and HTML anchors inside Markdown links retain source too, preventing browser
rearrangement from causing SSR hydration mismatches.

## Cards, layouts and inline styles

`html: true` strips `style` attributes by default. Explicitly use `html: { styles: 'safe' }`
for HTML cards with backgrounds, borders and spacing. This example uses wrapping Flex cards,
a table, nested native disclosures, an HTML fragment link and late Markdown references.
Toggle styles for comparison or replay the same source as a stream.

### reading/html.vue

```vue
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { QuireMarkdown } from "@quirekit/incremark/vue";
import { QuireButton } from "@quirekit/ui/vue";
import type { ExampleLocale } from "../shared/types";
import { complexHtmlSample } from "./html-source";

const props = withDefaults(defineProps<{ /** Tutorial language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const labels = computed(() => props.locale === "zh-CN"
  ? { stream: "流式回放", load: "一次性加载", stop: "停止", styles: "允许内联样式", region: "复杂 HTML 预览" }
  : { stream: "Stream HTML", load: "Load all", stop: "Stop", styles: "Allow inline styles", region: "Complex HTML preview" });
const source = computed(() => complexHtmlSample(props.locale));
const content = ref(source.value), receiving = ref(false), styles = ref(true), identity = ref(0);
const styledHtml = { styles: "safe" } as const;
let timer: ReturnType<typeof setTimeout> | undefined;
/** Cancels producer arrivals; received source remains authoritative. */
function stop(): void { if (timer !== undefined) clearTimeout(timer); timer = undefined; receiving.value = false; }
/** Restore a complete, independent document without stale producer continuations. */
function load(): void { stop(); identity.value++; content.value = source.value; }
/** Stream across HTML attributes and closing tags using the demo's real transport cadence. */
function stream(): void {
  stop(); identity.value++; content.value = ""; receiving.value = true;
  const text = source.value;
  /** Accept one exact source chunk without inserting synthetic closing HTML tags. */
  function append(): void {
    content.value = text.slice(0, content.value.length + 20);
    if (content.value.length < text.length) timer = setTimeout(append, 30); else stop();
  }
  append();
}
watch(source, load);
onBeforeUnmount(stop);
</script>
<template>
  <div class="grid gap-4 p-qk-surface">
    <div class="flex flex-wrap gap-2">
      <QuireButton size="sm" variant="outline" @click="stream">{{ labels.stream }}</QuireButton>
      <QuireButton size="sm" variant="ghost" @click="load">{{ labels.load }}</QuireButton>
      <QuireButton size="sm" variant="ghost" :disabled="!receiving" @click="stop">{{ labels.stop }}</QuireButton>
      <QuireButton size="sm" variant="ghost" :aria-pressed="styles" @click="styles = !styles">{{ labels.styles }}</QuireButton>
    </div>
    <section :aria-label="labels.region"><QuireMarkdown :content="content" :identity="String(identity)"
      :html="styles ? styledHtml : true" :locale="locale" :status="receiving ? 'receiving' : 'complete'" /></section>
  </div>
</template>

```

### shared/types.ts

```ts
/** Example host language; independent of the documentation router. */
export type ExampleLocale = "zh-CN" | "en";

```

### reading/html-source.ts

```ts
import type { ExampleLocale } from "../shared/types";

/** @param locale - Example language. @returns One canonical source shared by the tutorial and the local streaming comparison. */
export function complexHtmlSample(locale: ExampleLocale = "zh-CN"): string {
  const text = locale === "en" ? {
    title: "Complex HTML, streamed", intro: "Nested HTML keeps its structure and explicitly permitted inline styles.",
    card: "HTML element example", body: "This paragraph uses a colored background, a border and custom spacing.",
    cards: "Nested layout", first: "Accepted sections", second: "Pending sections", ready: "Ready", pending: "Receiving",
    table: "A table inside a disclosure", expand: "Open the delivery report", name: "Feature", result: "Result", detail: "Details",
    feature: "Nested cards", notes: "Notes and references", stable: "Source preserved", linked: "Definitions arrive later",
    more: "Open nested details", nested: "Lists, code, links and tables can be nested inside the same HTML island.",
    source: "Source stays authoritative", events: "Native disclosure needs no event handler", anchor: "Back to the card",
    tail: "This Markdown link is resolved at the end", report: "Report", footnote: "The definition arrives after the HTML.",
  } : {
    title: "复杂 HTML · 流式渲染", intro: "嵌套 HTML 保留结构，并显式启用允许的内联样式。",
    card: "HTML 元素示例", body: "这是一个使用 HTML 样式自定义的段落，包含背景、边框与间距。",
    cards: "嵌套布局", first: "已接收片段", second: "等待片段", ready: "已就绪", pending: "接收中",
    table: "折叠面板中的表格", expand: "展开交付报告", name: "能力", result: "结果", detail: "说明",
    feature: "嵌套卡片", notes: "脚注与引用", stable: "原文保全", linked: "定义后到仍更新",
    more: "展开嵌套说明", nested: "列表、代码、链接和表格可以嵌套在同一个 HTML 片段里。",
    source: "Markdown 原文始终是权威", events: "原生折叠交互不需要事件脚本", anchor: "返回上方提示框",
    tail: "这条 Markdown 链接在文末获得定义", report: "交付报告", footnote: "这条定义在 HTML 内容之后到达。",
  };
  // Deliberately keep each HTML island free of blank lines: Markdown, not the HTML
  // renderer, owns block boundaries. Attribute/tag boundaries are split by transport.
  return `# ${text.title}

${text.intro}

<div id="html-card" style="background: #f0f9ff; padding: 1rem; border-radius: 6px; border-left: 4px solid #3b82f6; margin: 1em 0">
  <p style="margin: 0; color: #1e40af; font-size: 20px"><strong>${text.card}</strong></p>
  <p style="margin: 0.5em 0 0 0; color: #1e40af">${text.body}</p>
</div>

## ${text.cards}

<div style="display: flex; flex-wrap: wrap; gap: 12px">
  <div style="flex: 1 1 240px; min-width: 0; max-width: 100%; padding: 16px; border: 1px solid #bfdbfe; border-radius: 12px; background: #eff6ff">
    <p style="margin: 0; color: #1e40af">${text.first}</p>
    <div style="display: flex; align-items: center; justify-content: space-between; gap: 8px">
      <strong style="font-size: 32px; color: #1e3a8a">128</strong>
      <span style="background: #dcfce7; color: #166534; padding: 4px 8px; border-radius: 6px; font-size: 12px">${text.ready}</span>
    </div>
  </div>
  <div style="flex: 1 1 240px; min-width: 0; max-width: 100%; padding: 16px; border: 1px solid #e2e8f0; border-radius: 12px; background: #f8fafc">
    <p style="margin: 0; color: #475569">${text.second}</p>
    <div style="display: flex; align-items: center; justify-content: space-between; gap: 8px">
      <strong style="font-size: 32px; color: #334155">3</strong>
      <span style="background: #fef3c7; color: #92400e; padding: 4px 8px; border-radius: 6px; font-size: 12px">${text.pending}</span>
    </div>
  </div>
</div>

## ${text.table}

<details open style="border: 1px solid #cbd5e1; border-radius: 12px; padding: 16px">
  <summary style="cursor: pointer; font-weight: 600; color: #334155">${text.expand}</summary>
  <table style="width: 100%; border-collapse: collapse; margin: 12px 0">
    <thead><tr style="background: #f1f5f9"><th>${text.name}</th><th>${text.result}</th><th>${text.detail}</th></tr></thead>
    <tbody>
      <tr><td><strong>${text.feature}</strong></td><td style="color: #166534">${text.ready}</td><td>${text.stable}</td></tr>
      <tr><td><strong>${text.notes}</strong></td><td style="color: #166534">${text.ready}</td><td>${text.linked}</td></tr>
    </tbody>
  </table>
  <details style="background: #f8fafc; border-radius: 8px; padding: 12px">
    <summary style="cursor: pointer; color: #475569">${text.more}</summary>
    <p>${text.nested}</p>
    <ul><li>${text.source}</li><li>${text.events}</li><li><code>&lt;div style="padding: 1rem"&gt;</code></li></ul>
    <a href="#html-card">${text.anchor}</a>
  </details>
</details>

${text.tail}：[${text.report}][report]。[^html-note]

[report]: https://example.com/report

[^html-note]: ${text.footnote}
`;
}

```

```vue
<QuireMarkdown :content="text" :html="{ styles: 'safe' }" />
```

The fixed subset includes solid background/text colors, borders, nonnegative spacing and sizes,
basic typography, Flex and Grid. Colors accept hexadecimal values and basic names; Grid columns
can use `1fr 2fr`, but not functions such as `repeat()`. Lengths are bounded to 2048px, 128em/rem
or 100%, with separate font-size limits. Each style attribute permits at most 4096 UTF-16 units
and 64 declarations. Invalid declarations are dropped individually; exceeding a budget drops
the entire style attribute.

URLs, CSS functions, variables, escapes, comments, `!important`, positioning, z-index,
animations, opacity, `display: none` and external stylesheets are not accepted. Source classes
still follow the fixed HTML sanitizer. The same rules run on the server and client; style policy
changes invalidate cached blocks. Incomplete HTML stays visible as source until its closing tag
arrives. Use host-provided `components` for richer presentation.
