feat: 实现 Markdown 渲染核心
This commit is contained in:
@@ -10,10 +10,11 @@
|
|||||||
- Fastify 后端;
|
- Fastify 后端;
|
||||||
- 共享导出配置模型;
|
- 共享导出配置模型;
|
||||||
- 版本化主题清单接口;
|
- 版本化主题清单接口;
|
||||||
|
- Markdown、Front Matter、安全过滤和扩展语法渲染核心;
|
||||||
- 内置 Typora 风格主题目录;
|
- 内置 Typora 风格主题目录;
|
||||||
- Docker 与测试目录预留。
|
- Docker 与测试目录预留。
|
||||||
|
|
||||||
Markdown 渲染和 PDF 生成将在后续阶段实现。
|
网页预览和 PDF 生成将在后续阶段实现。
|
||||||
|
|
||||||
## 本地开发
|
## 本地开发
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@md-to-pdf/core": "0.1.0",
|
"@md-to-pdf/core": "0.1.0",
|
||||||
|
"@md-to-pdf/renderer": "0.1.0",
|
||||||
"fastify": "^5.6.2"
|
"fastify": "^5.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
defaultExportConfig,
|
defaultExportConfig,
|
||||||
supportedPaperFormats
|
supportedPaperFormats
|
||||||
} from "@md-to-pdf/core";
|
} from "@md-to-pdf/core";
|
||||||
|
import { RENDERER_VERSION } from "@md-to-pdf/renderer";
|
||||||
|
|
||||||
const port = Number.parseInt(process.env.PORT ?? "3001", 10);
|
const port = Number.parseInt(process.env.PORT ?? "3001", 10);
|
||||||
const host = process.env.HOST ?? "0.0.0.0";
|
const host = process.env.HOST ?? "0.0.0.0";
|
||||||
@@ -15,14 +16,20 @@ const app = Fastify({
|
|||||||
app.get("/api/health", async () => ({
|
app.get("/api/health", async () => ({
|
||||||
status: "ok",
|
status: "ok",
|
||||||
service: "md-to-pdf",
|
service: "md-to-pdf",
|
||||||
configVersion: EXPORT_CONFIG_VERSION
|
configVersion: EXPORT_CONFIG_VERSION,
|
||||||
|
rendererVersion: RENDERER_VERSION
|
||||||
}));
|
}));
|
||||||
|
|
||||||
app.get("/api/capabilities", async () => ({
|
app.get("/api/capabilities", async () => ({
|
||||||
defaultExportConfig,
|
defaultExportConfig,
|
||||||
supportedPaperFormats,
|
supportedPaperFormats,
|
||||||
implemented: ["project-skeleton", "export-config", "theme-manifest"],
|
implemented: [
|
||||||
planned: ["markdown-render", "html-preview", "pdf-export"]
|
"project-skeleton",
|
||||||
|
"export-config",
|
||||||
|
"theme-manifest",
|
||||||
|
"markdown-render"
|
||||||
|
],
|
||||||
|
planned: ["html-preview", "pdf-export"]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
|
|||||||
Generated
+1041
-4
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -8,12 +8,14 @@
|
|||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/web && npm run build -w @md-to-pdf/server",
|
"build": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/web && npm run build -w @md-to-pdf/server",
|
||||||
"dev": "npm run build -w @md-to-pdf/core && concurrently -k -n core,server,web \"npm:dev:core\" \"npm:dev:server\" \"npm:dev:web\"",
|
"dev": "npm run build -w @md-to-pdf/core && npm run build -w @md-to-pdf/renderer && concurrently -k -n core,renderer,server,web \"npm:dev:core\" \"npm:dev:renderer\" \"npm:dev:server\" \"npm:dev:web\"",
|
||||||
"dev:core": "npm run dev -w @md-to-pdf/core",
|
"dev:core": "npm run dev -w @md-to-pdf/core",
|
||||||
|
"dev:renderer": "npm run dev -w @md-to-pdf/renderer",
|
||||||
"dev:server": "npm run dev -w @md-to-pdf/server",
|
"dev:server": "npm run dev -w @md-to-pdf/server",
|
||||||
"dev:web": "npm run dev -w @md-to-pdf/web",
|
"dev:web": "npm run dev -w @md-to-pdf/web",
|
||||||
"typecheck": "npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server"
|
"test": "npm run test -w @md-to-pdf/renderer",
|
||||||
|
"typecheck": "npm run typecheck -w @md-to-pdf/core && npm run build -w @md-to-pdf/core && npm run typecheck -w @md-to-pdf/renderer && npm run build -w @md-to-pdf/renderer && npm run typecheck -w @md-to-pdf/web && npm run typecheck -w @md-to-pdf/server"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@md-to-pdf/renderer",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"test": "vitest run",
|
||||||
|
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mdit/plugin-anchor": "^1.1.1",
|
||||||
|
"@mdit/plugin-footnote": "^1.0.1",
|
||||||
|
"@mdit/plugin-katex": "^1.0.1",
|
||||||
|
"@mdit/plugin-tasklist": "^1.0.1",
|
||||||
|
"@md-to-pdf/core": "0.1.0",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
|
"markdown-it": "^14.3.0",
|
||||||
|
"sanitize-html": "^2.17.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"@types/sanitize-html": "^2.16.0",
|
||||||
|
"vitest": "^4.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./render-markdown.js";
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
import { anchor } from "@mdit/plugin-anchor";
|
||||||
|
import { footnote } from "@mdit/plugin-footnote";
|
||||||
|
import { katex } from "@mdit/plugin-katex";
|
||||||
|
import { tasklist } from "@mdit/plugin-tasklist";
|
||||||
|
import type { ThemeFeature } from "@md-to-pdf/core";
|
||||||
|
import matter from "gray-matter";
|
||||||
|
import hljs from "highlight.js";
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
|
import type { Options as MarkdownItOptions } from "markdown-it";
|
||||||
|
import type Renderer from "markdown-it/lib/renderer.mjs";
|
||||||
|
import type Token from "markdown-it/lib/token.mjs";
|
||||||
|
import sanitizeHtml from "sanitize-html";
|
||||||
|
|
||||||
|
export const RENDERER_VERSION = 1;
|
||||||
|
|
||||||
|
export interface MarkdownDocumentMetadata {
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
subject: string;
|
||||||
|
keywords: string[];
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderMarkdownOptions {
|
||||||
|
language?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderedMarkdown {
|
||||||
|
rendererVersion: typeof RENDERER_VERSION;
|
||||||
|
articleHtml: string;
|
||||||
|
bodyHtml: string;
|
||||||
|
metadata: MarkdownDocumentMetadata;
|
||||||
|
features: ThemeFeature[];
|
||||||
|
warnings: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const markdownOptions: MarkdownItOptions = {
|
||||||
|
html: false,
|
||||||
|
breaks: false,
|
||||||
|
linkify: true,
|
||||||
|
typographer: false,
|
||||||
|
highlight(code, language) {
|
||||||
|
const normalizedLanguage = language.trim().toLowerCase();
|
||||||
|
|
||||||
|
if (normalizedLanguage && hljs.getLanguage(normalizedLanguage)) {
|
||||||
|
return hljs.highlight(code, {
|
||||||
|
language: normalizedLanguage,
|
||||||
|
ignoreIllegals: true
|
||||||
|
}).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MarkdownIt().utils.escapeHtml(code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const markdown = new MarkdownIt(markdownOptions)
|
||||||
|
.use(anchor, {
|
||||||
|
level: 1,
|
||||||
|
uniqueSlugStartIndex: 2,
|
||||||
|
tabIndex: false
|
||||||
|
})
|
||||||
|
.use(footnote)
|
||||||
|
.use(tasklist, {
|
||||||
|
disabled: true,
|
||||||
|
label: true,
|
||||||
|
containerClass: "contains-task-list",
|
||||||
|
itemClass: "md-task-list-item",
|
||||||
|
checkboxClass: "task-list-item-checkbox",
|
||||||
|
labelClass: "task-list-item-label"
|
||||||
|
})
|
||||||
|
.use(katex, {
|
||||||
|
delimiters: "all",
|
||||||
|
output: "html",
|
||||||
|
throwOnError: false,
|
||||||
|
strict: "warn",
|
||||||
|
trust: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultFenceRenderer = markdown.renderer.rules.fence;
|
||||||
|
const lengthStylePattern = /^-?\d+(?:\.\d+)?(?:em|ex|px|%)$/u;
|
||||||
|
|
||||||
|
markdown.renderer.rules.fence = (
|
||||||
|
tokens,
|
||||||
|
index,
|
||||||
|
options,
|
||||||
|
environment,
|
||||||
|
renderer
|
||||||
|
) => {
|
||||||
|
const token = tokens[index];
|
||||||
|
const language = token?.info.trim().split(/\s+/u)[0]?.toLowerCase();
|
||||||
|
|
||||||
|
if (token && language === "mermaid") {
|
||||||
|
return `<div class="mermaid" data-mermaid-pending="true">${markdown.utils.escapeHtml(token.content)}</div>\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultFenceRenderer) {
|
||||||
|
return defaultFenceRenderer(
|
||||||
|
tokens,
|
||||||
|
index,
|
||||||
|
options,
|
||||||
|
environment,
|
||||||
|
renderer
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderFallbackFence(tokens, index, renderer);
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeHtmlOptions: sanitizeHtml.IOptions = {
|
||||||
|
allowedTags: [
|
||||||
|
...sanitizeHtml.defaults.allowedTags,
|
||||||
|
"article",
|
||||||
|
"div",
|
||||||
|
"figure",
|
||||||
|
"figcaption",
|
||||||
|
"img",
|
||||||
|
"input",
|
||||||
|
"mark",
|
||||||
|
"section",
|
||||||
|
"span",
|
||||||
|
"sub",
|
||||||
|
"summary",
|
||||||
|
"sup"
|
||||||
|
],
|
||||||
|
allowedAttributes: {
|
||||||
|
"*": ["class", "id", "title", "aria-*", "role"],
|
||||||
|
a: ["href", "name", "target", "rel"],
|
||||||
|
div: ["class", "data-mermaid-pending"],
|
||||||
|
img: ["src", "alt", "title", "width", "height"],
|
||||||
|
input: ["type", "checked", "disabled", "class"],
|
||||||
|
li: ["class", "value"],
|
||||||
|
ol: ["class", "start"],
|
||||||
|
span: ["class", "style"],
|
||||||
|
td: ["colspan", "rowspan"],
|
||||||
|
th: ["colspan", "rowspan", "scope"]
|
||||||
|
},
|
||||||
|
allowedSchemes: ["http", "https", "mailto"],
|
||||||
|
allowedSchemesByTag: {
|
||||||
|
img: ["http", "https", "data"]
|
||||||
|
},
|
||||||
|
allowedStyles: {
|
||||||
|
span: {
|
||||||
|
height: [lengthStylePattern],
|
||||||
|
top: [lengthStylePattern],
|
||||||
|
width: [lengthStylePattern],
|
||||||
|
"min-width": [lengthStylePattern],
|
||||||
|
"margin-left": [lengthStylePattern],
|
||||||
|
"margin-right": [lengthStylePattern],
|
||||||
|
"padding-left": [lengthStylePattern],
|
||||||
|
"vertical-align": [lengthStylePattern],
|
||||||
|
"border-bottom-width": [lengthStylePattern]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transformTags: {
|
||||||
|
a: sanitizeHtml.simpleTransform("a", {
|
||||||
|
rel: "noopener noreferrer"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderMarkdown(
|
||||||
|
source: string,
|
||||||
|
options: RenderMarkdownOptions = {}
|
||||||
|
): RenderedMarkdown {
|
||||||
|
const warnings: string[] = [];
|
||||||
|
const parsed = parseFrontMatter(source);
|
||||||
|
const environment = {
|
||||||
|
warnings
|
||||||
|
};
|
||||||
|
const rendered = markdown.render(parsed.content, environment);
|
||||||
|
const bodyHtml = sanitizeHtml(rendered, safeHtmlOptions);
|
||||||
|
const metadata = normalizeMetadata(
|
||||||
|
parsed.data,
|
||||||
|
options.language ?? "zh-CN",
|
||||||
|
extractFirstHeading(parsed.content)
|
||||||
|
);
|
||||||
|
const articleHtml = `<article id="write" class="markdown-body" lang="${escapeAttribute(metadata.language)}">${bodyHtml}</article>`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
rendererVersion: RENDERER_VERSION,
|
||||||
|
articleHtml,
|
||||||
|
bodyHtml,
|
||||||
|
metadata,
|
||||||
|
features: detectFeatures(bodyHtml),
|
||||||
|
warnings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFrontMatter(source: string) {
|
||||||
|
try {
|
||||||
|
return matter(source);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "未知错误";
|
||||||
|
throw new Error(`无法解析 Markdown Front Matter:${message}`, {
|
||||||
|
cause: error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMetadata(
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
fallbackLanguage: string,
|
||||||
|
fallbackTitle: string
|
||||||
|
): MarkdownDocumentMetadata {
|
||||||
|
return {
|
||||||
|
title: readString(data.title) || fallbackTitle,
|
||||||
|
author: readString(data.author),
|
||||||
|
subject: readString(data.subject) || readString(data.description),
|
||||||
|
keywords: readStringList(data.keywords ?? data.tags),
|
||||||
|
language: readString(data.language ?? data.lang) || fallbackLanguage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readString(value: unknown): string {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "number" || typeof value === "boolean") {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStringList(value: unknown): string[] {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(readString).filter(Boolean).slice(0, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFirstHeading(source: string): string {
|
||||||
|
const heading = source.match(/^#\s+(.+?)\s*#*\s*$/mu);
|
||||||
|
return heading?.[1]?.trim() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectFeatures(html: string): ThemeFeature[] {
|
||||||
|
const featureChecks: Array<[ThemeFeature, RegExp]> = [
|
||||||
|
["code", /<pre\b/u],
|
||||||
|
["table", /<table\b/u],
|
||||||
|
["task-list", /\bmd-task-list-item\b/u],
|
||||||
|
["footnote", /\bfootnote/u],
|
||||||
|
["katex", /\bkatex\b/u],
|
||||||
|
["mermaid", /\bmermaid\b/u]
|
||||||
|
];
|
||||||
|
|
||||||
|
return featureChecks
|
||||||
|
.filter(([, pattern]) => pattern.test(html))
|
||||||
|
.map(([feature]) => feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFallbackFence(
|
||||||
|
tokens: Token[],
|
||||||
|
index: number,
|
||||||
|
renderer: Renderer
|
||||||
|
): string {
|
||||||
|
const token = tokens[index];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<pre${renderer.renderAttrs(token)}><code>${markdown.utils.escapeHtml(token.content)}</code></pre>\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeAttribute(value: string): string {
|
||||||
|
return markdown.utils.escapeHtml(value);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { renderMarkdown } from "../src/render-markdown.js";
|
||||||
|
|
||||||
|
describe("renderMarkdown", () => {
|
||||||
|
it("渲染常用 Markdown 扩展并识别能力", () => {
|
||||||
|
const result = renderMarkdown(`
|
||||||
|
# 示例文档
|
||||||
|
|
||||||
|
| 名称 | 状态 |
|
||||||
|
| --- | --- |
|
||||||
|
| PDF | 可用 |
|
||||||
|
|
||||||
|
- [x] 已完成
|
||||||
|
- [ ] 待处理
|
||||||
|
|
||||||
|
\`\`\`ts
|
||||||
|
const answer = 42;
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
脚注引用[^note]。
|
||||||
|
|
||||||
|
[^note]: 脚注正文。
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(result.articleHtml).toContain('id="write"');
|
||||||
|
expect(result.bodyHtml).toContain("<table>");
|
||||||
|
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.features).toEqual(
|
||||||
|
expect.arrayContaining(["code", "table", "task-list", "footnote"])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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("服务端渲染公式并保留 Mermaid 安全占位", () => {
|
||||||
|
const result = renderMarkdown(`
|
||||||
|
内联公式 $E=mc^2$。
|
||||||
|
|
||||||
|
\`\`\`mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[开始] --> B[结束]
|
||||||
|
\`\`\`
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(result.bodyHtml).toContain("katex");
|
||||||
|
expect(result.bodyHtml).toContain('class="mermaid"');
|
||||||
|
expect(result.bodyHtml).toContain("data-mermaid-pending");
|
||||||
|
expect(result.features).toEqual(
|
||||||
|
expect.arrayContaining(["katex", "mermaid"])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("阻止原始 HTML 和危险链接形成可执行内容", () => {
|
||||||
|
const result = renderMarkdown(`
|
||||||
|
<script>alert("xss")</script>
|
||||||
|
|
||||||
|
[危险链接](javascript:alert("xss"))
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(result.bodyHtml).not.toContain("<script");
|
||||||
|
expect(result.bodyHtml).not.toMatch(/href=["']javascript:/u);
|
||||||
|
expect(result.bodyHtml).toContain("<script>");
|
||||||
|
});
|
||||||
|
|
||||||
|
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"');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -83,6 +83,27 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#write .hljs-keyword,
|
||||||
|
#write .hljs-selector-tag,
|
||||||
|
#write .hljs-literal {
|
||||||
|
color: #9a3412;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write .hljs-string,
|
||||||
|
#write .hljs-attr {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write .hljs-title,
|
||||||
|
#write .hljs-function {
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write .hljs-comment {
|
||||||
|
color: #7c838a;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
#write table {
|
#write table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 1.3em 0;
|
margin: 1.3em 0;
|
||||||
@@ -106,3 +127,37 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#write .contains-task-list {
|
||||||
|
padding-left: 0.25em;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write .md-task-list-item {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write .task-list-item-checkbox {
|
||||||
|
margin: 0 0.55em 0.2em 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write .footnotes {
|
||||||
|
margin-top: 2.5em;
|
||||||
|
padding-top: 1em;
|
||||||
|
border-top: 1px solid var(--md-border);
|
||||||
|
color: var(--md-muted);
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write .katex-display {
|
||||||
|
margin: 1.4em 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#write .mermaid {
|
||||||
|
margin: 1.5em auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user