# HTML 与内联样式

显示 Markdown 中的 HTML，按需限制标签、地址和内联样式。

当 Markdown 包含 `<div>` 卡片或其他 HTML 时，先决定是否需要把它显示成元素。默认会显示原始 HTML 文字；需要显示经过净化的元素时，再开启 `html`。

## 开启并限制 HTML

`html` 为 `true` 时，完整 HTML 片段经 HTML parser 与固定
白名单净化后生成 Vue 节点。支持块级和行内 HTML；行内 HTML 可包含 Markdown，代码内容
保持字面值。事件属性、脚本、危险 URL、iframe、`<style>` 标签和任意模板表达式不能开启。

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

上面的配置只允许列出的四种标签，HTML 链接只接受指定站点，HTML 图片不显示。省略这些限制并设置 `html: true` 时，使用内置白名单。

这些选项只能进一步限制净化后的标签和 HTML URL，不改变普通 Markdown 链接策略。
HTML 的 id 与片段链接使用阅读实例命名空间；文档中的 HTML 不能自行注册 Vue 组件。
自定义组件通过上面的 `components` 或阅读贡献接入。未闭合、超过 65,536 个 UTF-16 单元、超过 32 层
元素或超出解析节点预算的 HTML 片段保留源码。HTML 块内不重新解释 Markdown。段落中
混入块元素、Markdown 链接内嵌 HTML 链接也保留源码，避免浏览器重排导致 SSR 水合不一致。

## 提示框、布局与内联样式

`html: true` 默认移除 `style` 属性。需要呈现带背景、边框、间距的 HTML 卡片时，显式
使用 `html: { styles: 'safe' }`。下面的同一份 HTML 包含可换行的 Flex 卡片、表格、两层
原生折叠面板、页内链接和后置引用；可以关闭样式对照，也可以流式回放。

### 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' }" />
```

这是一组固定样式子集：纯色背景/文字、边框、非负间距与尺寸、基本文字排版、Flex 与
Grid 布局。颜色接受十六进制和基础名称；Grid 列可使用 `1fr 2fr`，不接受 `repeat()` 等
函数。数值长度上限为 2048px、128em/rem 或 100%，字体另有范围限制；单个 style 最多
4096 个 UTF-16 单元、64 项声明。无效声明单项丢弃，超预算丢弃整个 style。

不接收 URL、CSS 函数、变量、转义、注释、`!important`、定位、z-index、动画、透明度、
`display: none` 或外部样式表。原始 class 仍受固定 HTML 净化规则约束。规则在服务端和
客户端一致，样式策略变化会使块缓存失效；未闭合 HTML 继续保留源码，闭合后显示样式。
更复杂的视觉能力通过宿主提供的 `components` 接入。
