import { describe, expect, it } from "vitest"; import { extractMarkdownImageSources, MarkdownDocumentParseError, renderMarkdown } from "../src/render-markdown.js"; describe("renderMarkdown", () => { it("将 GFM Alert 渲染为与 Pandoc 一致的独立标题引用块", () => { const result = renderMarkdown( "> [!CAUTION]\n> **关键约束**:必须执行。", ); expect(result.bodyHtml).toContain( '
', ); expect(result.bodyHtml).toContain( '

Caution

\n

关键约束:必须执行。

', ); expect(result.bodyHtml).not.toContain("[!CAUTION]"); }); it("为含行内代码的普通段落标记分页保护类", () => { const result = renderMarkdown("段落前 `inline_code` 段落后"); expect(result.articleHtml).toContain( '

段落前 inline_code 段落后

', ); }); it("向下游暴露已剥离文首 Front Matter 的正文 Markdown", () => { const rendered = renderMarkdown( "---\ntitle: 测试标题\n---\n# 正文\n\n---\n\n后续内容" ); expect(rendered.markdownBody).toBe( "# 正文\n\n---\n\n后续内容" ); expect(rendered.metadata.title).toBe("测试标题"); }); it("渲染常用 Markdown 扩展并识别能力", () => { const result = renderMarkdown(` # 示例文档 | 名称 | 状态 | | --- | --- | | PDF | 可用 | - [x] 已完成 - [ ] 待处理 \`\`\`ts const answer = 42; \`\`\` 脚注引用[^note]。 [^note]: 脚注正文。 `); expect(result.articleHtml).toContain('id="write"'); expect(result.bodyHtml).toContain(""); expect(result.bodyHtml).toContain("md-task-list-item"); expect(result.bodyHtml).toContain("hljs-keyword"); expect(result.bodyHtml).toContain("footnote"); expect(result.metadata.title).toBe("示例文档"); expect(result.semanticDocument).toEqual({ schemaVersion: 1, titlePolicy: { metadataTitle: "suppress", firstBodyHeading: "keep" }, regions: [] }); expect(result.features).toEqual( expect.arrayContaining(["code", "table", "task-list", "footnote"]) ); }); it("为围栏代码块输出 Typora 主题兼容容器", () => { const result = renderMarkdown(` \`\`\` 无语言代码块 \`\`\` \`\`\`ts const answer = 42; \`\`\` `); expect(result.bodyHtml).toContain( '
无语言代码块\n
' ); expect(result.bodyHtml).toContain( '
'
    );
    expect(result.bodyHtml).toContain("hljs-keyword");
  });

  it("保留 Markdown 表格的列对齐语义", () => {
    const result = renderMarkdown(`
| 左对齐 | 居中 | 右对齐 |
| :--- | :---: | ---: |
| A | B | C |
`);

    expect(result.bodyHtml).toContain('
'); expect(result.bodyHtml).toContain(''); expect(result.bodyHtml).toContain(''); expect(result.bodyHtml).toContain(''); expect(result.bodyHtml).toContain(''); expect(result.bodyHtml).toContain(''); }); it("只在 GFM 表格单元格内解释无属性 br 换行", () => { const result = renderMarkdown(` | 场景 | 内容 | | --- | --- | | 标准 | 第一行
第二行
第三行
第四行 | 正文中的


保持文本。 | 转义与代码 | 内容 | | --- | --- | | 转义 | \\
与 <br> | | 代码 | \`
\` | | 属性 |
| \`\`\`html
\`\`\` `); expect(result.bodyHtml).toContain( "第一行
第二行
第三行
第四行" ); expect(result.bodyHtml).toContain( "正文中的 <br>、<br/> 和 <br /> 保持文本" ); expect(result.bodyHtml).toContain("<br> 与 <br>"); expect(result.bodyHtml).toContain("<br>"); expect(result.bodyHtml).toContain("<br class=\"unsafe\">"); expect(result.bodyHtml).toContain( '
'
    );
  });

  it("读取并规范化 Front Matter 元数据", () => {
    const result = renderMarkdown(`---
title: 项目报告
author: 张三
subject: 技术方案
keywords:
  - Markdown
  - PDF
lang: zh-CN
---

# 不作为标题
`);

    expect(result.metadata).toEqual({
      title: "项目报告",
      author: "张三",
      subject: "技术方案",
      keywords: ["Markdown", "PDF"],
      language: "zh-CN"
    });
  });

  it("使用稳定错误类型报告无效 Front Matter", () => {
    expect(() =>
      renderMarkdown(`---
title: [未闭合
---

# 文档
`)
    ).toThrow(MarkdownDocumentParseError);
  });

  it("生成政企公文的语义化版头、落款和版记", () => {
    const result = renderMarkdown(`---
title: 关于推进示范项目建设的通知
document:
  profile: official
  issuer: 示例市人民政府办公室
  number: 示例办发〔2026〕12号
  secrecy: 内部
  urgency: 加急
  signatory: 张三
  date: 2026年7月29日
  copyTo: 市发展改革委, 市财政局
  printingOffice: 示例市人民政府办公室
---

## 工作要求

正文内容。
`);

    expect(result.metadata.document).toEqual({
      profile: "official",
      issuer: "示例市人民政府办公室",
      number: "示例办发〔2026〕12号",
      secrecy: "内部",
      urgency: "加急",
      signatory: "张三",
      date: "2026年7月29日",
      copyTo: ["市发展改革委", "市财政局"],
      printingOffice: "示例市人民政府办公室"
    });
    expect(result.articleHtml).toContain(
      'data-document-profile="official"'
    );
    expect(result.articleHtml).toContain(
      '
' ); expect(result.articleHtml).toContain( '

关于推进示范项目建设的通知

' ); expect(result.articleHtml).toContain( '
' ); expect(result.articleHtml).toContain( '
' ); expect(result.semanticDocument.profile).toBe("official"); expect(result.semanticDocument.regions).toHaveLength(2); expect(result.bodyHtml).not.toContain("doc-masthead"); }); it("生成项目报告和标书封面的稳定语义节点", () => { const projectReport = renderMarkdown(`--- title: 可行性研究报告 document: profile: project-report projectName: 智慧园区建设项目 documentType: 可行性研究报告 owner: 示例建设集团 preparedBy: 示例咨询有限公司 version: V1.0 date: 2026年7月 --- # 编制说明 `); const tender = renderMarkdown(`--- title: 投标文件 document: profile: tender copyMark: 正本 projectName: 数据中心建设项目 projectNumber: ZB-2026-001 volume: 技术标 bidder: 示例科技有限公司 representative: 李四 date: 2026年7月29日 --- # 技术方案 `); expect(projectReport.articleHtml).toContain( 'class="doc-cover doc-cover-project-report"' ); expect(projectReport.articleHtml).toContain( 'data-semantic-region="cover"' ); expect(projectReport.articleHtml).toContain( 'data-semantic-break-after="next-page"' ); expect(tender.articleHtml).toContain( 'class="doc-cover doc-cover-tender"' ); expect(tender.articleHtml).toContain( '

正本

' ); }); it("拒绝未知结构字段并转义语义节点内容", () => { expect(() => renderMarkdown(`--- document: profile: official unknown: value --- 正文`) ).toThrow(MarkdownDocumentParseError); const result = renderMarkdown(`--- title: "" document: profile: briefing masthead: "" --- 正文`); expect(result.articleHtml).not.toContain(" [危险链接](javascript:alert("xss")) `); expect(result.bodyHtml).not.toContain(" { const result = renderMarkdown(` [锚点](#章节一) [网络](https://example.com/a) [协议相对](//example.com/a) [邮件](mailto:a@example.com) [电话](tel:+8612345) [上级目录](../docs/a.md) [绝对路径](C:/docs/a.md) [本地 URI]() [自定义协议](obsidian://open?vault=x) `); expect(result.bodyHtml).toContain('href="#%E7%AB%A0%E8%8A%82%E4%B8%80"'); expect(result.bodyHtml).toContain( 'href="https://example.com/a"' ); expect(result.bodyHtml).toContain('href="//example.com/a"'); expect(result.bodyHtml).toContain( 'href="mailto:a@example.com"' ); expect(result.bodyHtml).toContain('href="tel:+8612345"'); expect(result.bodyHtml).toContain('href="../docs/a.md"'); expect(result.bodyHtml).toContain('href="C:/docs/a.md"'); expect(result.bodyHtml).toContain( 'href="file:///C:/docs/a.md"' ); expect(result.bodyHtml).toContain( 'href="obsidian://open?vault=x"' ); }); it("提取图片引用并使用受控资源地址替换", () => { const source = "![本地](./文档.assets/a%20b.png)\n\n![远程](https://example.com/a.png)"; expect(extractMarkdownImageSources(source)).toEqual([ "./%E6%96%87%E6%A1%A3.assets/a%20b.png", "https://example.com/a.png" ]); const result = renderMarkdown(source, { imageSourceMap: new Map([ [ "./%E6%96%87%E6%A1%A3.assets/a%20b.png", "data:image/png;base64,iVBORw0KGgo=" ] ]), warnings: ["远程图片下载失败"] }); expect(result.bodyHtml).toContain( 'class="md-document-image"' ); expect(result.bodyHtml).toContain( '
' ); expect(result.bodyHtml).toContain( '
本地
' ); expect(result.bodyHtml).toContain( 'src="data:image/png;base64,iVBORw0KGgo="' ); expect(result.warnings).toContain("远程图片下载失败"); }); it("只为独立图片块生成可见标题,行内图片保持段落语义", () => { const result = renderMarkdown( "文字 ![行内图](inline.png) 继续\n\n![独立标题](block.png)" ); expect(result.bodyHtml).toContain( '

文字 行内图 继续

' ); expect(result.bodyHtml).not.toContain( "md-document-image-caption\">行内图" ); expect(result.bodyHtml).toContain( '
独立标题
' ); }); it("为重复中文标题生成唯一锚点", () => { const result = renderMarkdown(` # 章节 ## 章节 ## 章节 `); expect(result.bodyHtml).toContain('id="%E7%AB%A0%E8%8A%82"'); expect(result.bodyHtml).toContain('id="%E7%AB%A0%E8%8A%82-2"'); expect(result.bodyHtml).toContain('id="%E7%AB%A0%E8%8A%82-3"'); }); });
左对齐居中右对齐ABC