@pactor-app/markdown 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/index.cjs +208 -0
- package/dist/index.d.cts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +165 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pactor Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @pactor-app/markdown
|
|
2
|
+
|
|
3
|
+
[Pactor](https://github.com/426-330/pactor) 的 Markdown 模块:安全的 Markdown 渲染器(`MarkdownRenderer`)与 DSL Markdown 组件适配(`type: Markdown`)。Chat 消息与 Page/Overlay DSL 共用同一渲染器。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @pactor-app/markdown react
|
|
9
|
+
# 或
|
|
10
|
+
pnpm add @pactor-app/markdown react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`react >= 18` 为 peer dependency。开发基准:react-markdown v10(^10.1.0)+ remark-gfm v4(^4.0.1)。
|
|
14
|
+
|
|
15
|
+
## 安全边界
|
|
16
|
+
|
|
17
|
+
- **不启用 raw HTML**:基于 react-markdown 的 AST 渲染,Markdown 文本中的原始 HTML(`<script>`、事件属性、任意标签)不会渲染为元素。**切勿引入 `rehype-raw`**——该约定由测试锁定(script / onerror 注入场景)。
|
|
18
|
+
- **协议白名单**:链接 href 仅允许 `http:` / `https:` / `mailto:` 与相对路径;图片 src 仅允许 `http:` / `https:` 与相对路径。`javascript:` 等危险协议的链接渲染为纯文本,图片不渲染。
|
|
19
|
+
- **外链安全属性**:http(s) 链接自动带 `target="_blank" rel="noreferrer"`。
|
|
20
|
+
|
|
21
|
+
## 独立使用
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { MarkdownRenderer } from '@pactor-app/markdown';
|
|
25
|
+
|
|
26
|
+
<MarkdownRenderer content={'### 标题\n\n- 列表项\n\n| 表 | 格 |\n| - | - |\n| 1 | 2 |'} />;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
支持:标题 / 段落 / 粗体 / 斜体 / 有序与无序列表 / GFM 表格与删除线 / 围栏代码块(`<pre><code class="language-xxx">`,供后续语法高亮扩展)与行内代码 / 引用 / 链接 / 图片。容器带基础 class `@pactor-app/markdown`,样式由宿主主题决定。
|
|
30
|
+
|
|
31
|
+
## DSL 使用
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { createPageRuntime, PageRuntimeView } from '@pactor-app/runtime';
|
|
35
|
+
import { registerMarkdownComponents } from '@pactor-app/markdown';
|
|
36
|
+
|
|
37
|
+
const runtime = createPageRuntime(pageDsl);
|
|
38
|
+
registerMarkdownComponents(runtime.context.components); // 注册 DSL 类型 'Markdown'
|
|
39
|
+
|
|
40
|
+
await runtime.initialized;
|
|
41
|
+
<PageRuntimeView runtime={runtime} />;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
type: Markdown
|
|
46
|
+
props:
|
|
47
|
+
content: '当前筛选:**${state.keyword}**'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`props.content` 中的表达式(`${data.xxx}` 等)由 Renderer 在下发前解析,组件拿到的已是最终文本。Chat 的 Safe Registry 场景可复用同一 `registerMarkdownComponents`。
|
|
51
|
+
|
|
52
|
+
## 文档
|
|
53
|
+
|
|
54
|
+
完整文档见 <https://github.com/426-330/pactor/tree/main/docs>(`pnpm docs:dev` 本地启动文档站)。
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
[MIT](../../LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
CodeBlock: () => CodeBlock,
|
|
34
|
+
MARKDOWN_DSL_TYPE: () => MARKDOWN_DSL_TYPE,
|
|
35
|
+
MarkdownDslComponent: () => MarkdownDslComponent,
|
|
36
|
+
MarkdownRenderer: () => MarkdownRenderer,
|
|
37
|
+
isSafeUrl: () => isSafeUrl,
|
|
38
|
+
registerCodeLanguages: () => registerCodeLanguages,
|
|
39
|
+
registerMarkdownComponents: () => registerMarkdownComponents
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
|
|
43
|
+
// src/renderer.tsx
|
|
44
|
+
var import_react_markdown = __toESM(require("react-markdown"), 1);
|
|
45
|
+
var import_rehype_katex = __toESM(require("rehype-katex"), 1);
|
|
46
|
+
var import_remark_gfm = __toESM(require("remark-gfm"), 1);
|
|
47
|
+
var import_remark_math = __toESM(require("remark-math"), 1);
|
|
48
|
+
|
|
49
|
+
// src/code-block.tsx
|
|
50
|
+
var import_react = require("react");
|
|
51
|
+
var import_react_syntax_highlighter = require("react-syntax-highlighter");
|
|
52
|
+
var import_bash = __toESM(require("react-syntax-highlighter/dist/esm/languages/prism/bash"), 1);
|
|
53
|
+
var import_css = __toESM(require("react-syntax-highlighter/dist/esm/languages/prism/css"), 1);
|
|
54
|
+
var import_javascript = __toESM(require("react-syntax-highlighter/dist/esm/languages/prism/javascript"), 1);
|
|
55
|
+
var import_json = __toESM(require("react-syntax-highlighter/dist/esm/languages/prism/json"), 1);
|
|
56
|
+
var import_jsx = __toESM(require("react-syntax-highlighter/dist/esm/languages/prism/jsx"), 1);
|
|
57
|
+
var import_markdown = __toESM(require("react-syntax-highlighter/dist/esm/languages/prism/markdown"), 1);
|
|
58
|
+
var import_python = __toESM(require("react-syntax-highlighter/dist/esm/languages/prism/python"), 1);
|
|
59
|
+
var import_tsx = __toESM(require("react-syntax-highlighter/dist/esm/languages/prism/tsx"), 1);
|
|
60
|
+
var import_typescript = __toESM(require("react-syntax-highlighter/dist/esm/languages/prism/typescript"), 1);
|
|
61
|
+
var import_yaml = __toESM(require("react-syntax-highlighter/dist/esm/languages/prism/yaml"), 1);
|
|
62
|
+
var import_one_light = __toESM(require("react-syntax-highlighter/dist/esm/styles/prism/one-light"), 1);
|
|
63
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
64
|
+
var LANGUAGES = {
|
|
65
|
+
bash: import_bash.default,
|
|
66
|
+
css: import_css.default,
|
|
67
|
+
javascript: import_javascript.default,
|
|
68
|
+
js: import_javascript.default,
|
|
69
|
+
json: import_json.default,
|
|
70
|
+
jsx: import_jsx.default,
|
|
71
|
+
markdown: import_markdown.default,
|
|
72
|
+
md: import_markdown.default,
|
|
73
|
+
python: import_python.default,
|
|
74
|
+
py: import_python.default,
|
|
75
|
+
tsx: import_tsx.default,
|
|
76
|
+
typescript: import_typescript.default,
|
|
77
|
+
ts: import_typescript.default,
|
|
78
|
+
yaml: import_yaml.default,
|
|
79
|
+
yml: import_yaml.default
|
|
80
|
+
};
|
|
81
|
+
var registered = false;
|
|
82
|
+
function registerCodeLanguages() {
|
|
83
|
+
if (registered) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
registered = true;
|
|
87
|
+
for (const [name, language] of Object.entries(LANGUAGES)) {
|
|
88
|
+
import_react_syntax_highlighter.PrismLight.registerLanguage(name, language);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isCodeLanguageRegistered(language) {
|
|
92
|
+
return language.toLowerCase() in LANGUAGES;
|
|
93
|
+
}
|
|
94
|
+
function CodeBlock({ language, children }) {
|
|
95
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
96
|
+
import_react_syntax_highlighter.PrismLight,
|
|
97
|
+
{
|
|
98
|
+
language: language.toLowerCase(),
|
|
99
|
+
style: import_one_light.default,
|
|
100
|
+
customStyle: { margin: 0, background: "transparent", fontSize: "0.85em" },
|
|
101
|
+
codeTagProps: { className: `language-${language.toLowerCase()}` },
|
|
102
|
+
children: children.replace(/\n$/, "")
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
function extractFencedCode(preChildren) {
|
|
107
|
+
if (!(0, import_react.isValidElement)(preChildren)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const { className, children } = preChildren.props;
|
|
111
|
+
const match = /language-([\w-]+)/.exec(className ?? "");
|
|
112
|
+
if (!match) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return { language: match[1], code: String(children ?? "") };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/renderer.tsx
|
|
119
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
120
|
+
var LINK_PROTOCOL_WHITELIST = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
|
|
121
|
+
var IMAGE_PROTOCOL_WHITELIST = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
122
|
+
function extractProtocol(url) {
|
|
123
|
+
const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(url.trim());
|
|
124
|
+
return match ? `${match[1].toLowerCase()}:` : null;
|
|
125
|
+
}
|
|
126
|
+
function isSafeUrl(url, kind) {
|
|
127
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const protocol = extractProtocol(url);
|
|
131
|
+
if (protocol === null) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
const whitelist = kind === "link" ? LINK_PROTOCOL_WHITELIST : IMAGE_PROTOCOL_WHITELIST;
|
|
135
|
+
return whitelist.has(protocol);
|
|
136
|
+
}
|
|
137
|
+
var SafeLink = ({ href, children }) => {
|
|
138
|
+
if (!isSafeUrl(href, "link")) {
|
|
139
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
|
|
140
|
+
}
|
|
141
|
+
const protocol = extractProtocol(href);
|
|
142
|
+
if (protocol === "http:" || protocol === "https:") {
|
|
143
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href, target: "_blank", rel: "noreferrer", children });
|
|
144
|
+
}
|
|
145
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href, children });
|
|
146
|
+
};
|
|
147
|
+
var SafeImage = ({ src, alt }) => {
|
|
148
|
+
if (!isSafeUrl(src, "image")) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("img", { src, alt: alt ?? "" });
|
|
152
|
+
};
|
|
153
|
+
var baseComponents = {
|
|
154
|
+
a: SafeLink,
|
|
155
|
+
img: SafeImage
|
|
156
|
+
};
|
|
157
|
+
var highlightingComponents = {
|
|
158
|
+
...baseComponents,
|
|
159
|
+
pre: ({ children }) => {
|
|
160
|
+
const fenced = extractFencedCode(children);
|
|
161
|
+
if (fenced && isCodeLanguageRegistered(fenced.language)) {
|
|
162
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CodeBlock, { language: fenced.language, children: fenced.code });
|
|
163
|
+
}
|
|
164
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("pre", { children });
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
function MarkdownRenderer({
|
|
168
|
+
content = "",
|
|
169
|
+
className,
|
|
170
|
+
math = true,
|
|
171
|
+
codeHighlight = true
|
|
172
|
+
}) {
|
|
173
|
+
if (codeHighlight) {
|
|
174
|
+
registerCodeLanguages();
|
|
175
|
+
}
|
|
176
|
+
const containerClass = className ? `pactor-markdown ${className}` : "pactor-markdown";
|
|
177
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: containerClass, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
178
|
+
import_react_markdown.default,
|
|
179
|
+
{
|
|
180
|
+
remarkPlugins: math ? [import_remark_gfm.default, import_remark_math.default] : [import_remark_gfm.default],
|
|
181
|
+
rehypePlugins: math ? [import_rehype_katex.default] : [],
|
|
182
|
+
components: codeHighlight ? highlightingComponents : baseComponents,
|
|
183
|
+
children: content
|
|
184
|
+
}
|
|
185
|
+
) });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/dsl-component.tsx
|
|
189
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
190
|
+
function MarkdownDslComponent({ content }) {
|
|
191
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MarkdownRenderer, { content: content ?? "" });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/register.ts
|
|
195
|
+
var MARKDOWN_DSL_TYPE = "Markdown";
|
|
196
|
+
function registerMarkdownComponents(registry) {
|
|
197
|
+
registry.register(MARKDOWN_DSL_TYPE, MarkdownDslComponent);
|
|
198
|
+
}
|
|
199
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
200
|
+
0 && (module.exports = {
|
|
201
|
+
CodeBlock,
|
|
202
|
+
MARKDOWN_DSL_TYPE,
|
|
203
|
+
MarkdownDslComponent,
|
|
204
|
+
MarkdownRenderer,
|
|
205
|
+
isSafeUrl,
|
|
206
|
+
registerCodeLanguages,
|
|
207
|
+
registerMarkdownComponents
|
|
208
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Registry } from '@pactor-app/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 协议白名单检查:相对路径(无协议)放行;
|
|
6
|
+
* 带协议的 URL 必须在白名单内,否则拒绝。
|
|
7
|
+
* 空串同样拒绝——react-markdown 默认 urlTransform 会把
|
|
8
|
+
* javascript: 等危险协议置为空串,空串即视为已被拒绝。
|
|
9
|
+
*/
|
|
10
|
+
declare function isSafeUrl(url: string | undefined, kind: 'link' | 'image'): boolean;
|
|
11
|
+
interface MarkdownRendererProps {
|
|
12
|
+
/** Markdown 源文本(DSL 场景下表达式已由 Renderer 解析后传入) */
|
|
13
|
+
content?: string;
|
|
14
|
+
/** 附加 className,与基础 class `@pactor-app/markdown` 并存 */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** 数学公式(remark-math + rehype-katex),默认 true;需自行引入 katex css */
|
|
17
|
+
math?: boolean;
|
|
18
|
+
/** fenced 代码块语法高亮,默认 true */
|
|
19
|
+
codeHighlight?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Markdown 渲染器:标题 / 列表 / 表格(GFM)/ 代码块 / 引用 / 链接 / 图片 /
|
|
23
|
+
* 数学公式(KaTeX)。容器带基础 class `@pactor-app/markdown`,样式由宿主主题决定。
|
|
24
|
+
* 技术选型参考 DeepTutor web:react-markdown + remark-gfm + remark-math +
|
|
25
|
+
* rehype-katex + react-syntax-highlighter(Light 构建)。
|
|
26
|
+
*
|
|
27
|
+
* 注意:启用 math 时需在使用方引入 `katex/dist/katex.min.css`。
|
|
28
|
+
*/
|
|
29
|
+
declare function MarkdownRenderer({ content, className, math, codeHighlight, }: MarkdownRendererProps): react.JSX.Element;
|
|
30
|
+
|
|
31
|
+
interface MarkdownDslComponentProps {
|
|
32
|
+
/**
|
|
33
|
+
* Markdown 内容(DSL props.content)。
|
|
34
|
+
* `${data.xxx}` 等表达式已由 Renderer 在 props 下发前解析(设计决策 D3)。
|
|
35
|
+
*/
|
|
36
|
+
content?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* DSL 组件适配:`{ type: 'Markdown', props: { content } }`。
|
|
40
|
+
* 仅做 props 透传,渲染与安全边界全部委托给 MarkdownRenderer。
|
|
41
|
+
*/
|
|
42
|
+
declare function MarkdownDslComponent({ content }: MarkdownDslComponentProps): react.JSX.Element;
|
|
43
|
+
|
|
44
|
+
/** Markdown 组件的 DSL 类型名 */
|
|
45
|
+
declare const MARKDOWN_DSL_TYPE = "Markdown";
|
|
46
|
+
/**
|
|
47
|
+
* 把 Markdown DSL 组件注册到给定的 Component Registry
|
|
48
|
+
* (@pactor-app/runtime 的 `ComponentRegistry` 即 `Registry<unknown>`,
|
|
49
|
+
* @pactor-app/chat 的 Safe Registry 场景亦可复用)。
|
|
50
|
+
*/
|
|
51
|
+
declare function registerMarkdownComponents(registry: Registry<unknown>): void;
|
|
52
|
+
|
|
53
|
+
/** 幂等注册内置语言表(模块加载时由 MarkdownRenderer 触发一次) */
|
|
54
|
+
declare function registerCodeLanguages(): void;
|
|
55
|
+
interface CodeBlockProps {
|
|
56
|
+
language: string;
|
|
57
|
+
children: string;
|
|
58
|
+
}
|
|
59
|
+
/** fenced 代码块高亮渲染(Prism Light + one-light 主题) */
|
|
60
|
+
declare function CodeBlock({ language, children }: CodeBlockProps): react.JSX.Element;
|
|
61
|
+
|
|
62
|
+
export { CodeBlock, type CodeBlockProps, MARKDOWN_DSL_TYPE, MarkdownDslComponent, type MarkdownDslComponentProps, MarkdownRenderer, type MarkdownRendererProps, isSafeUrl, registerCodeLanguages, registerMarkdownComponents };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { Registry } from '@pactor-app/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 协议白名单检查:相对路径(无协议)放行;
|
|
6
|
+
* 带协议的 URL 必须在白名单内,否则拒绝。
|
|
7
|
+
* 空串同样拒绝——react-markdown 默认 urlTransform 会把
|
|
8
|
+
* javascript: 等危险协议置为空串,空串即视为已被拒绝。
|
|
9
|
+
*/
|
|
10
|
+
declare function isSafeUrl(url: string | undefined, kind: 'link' | 'image'): boolean;
|
|
11
|
+
interface MarkdownRendererProps {
|
|
12
|
+
/** Markdown 源文本(DSL 场景下表达式已由 Renderer 解析后传入) */
|
|
13
|
+
content?: string;
|
|
14
|
+
/** 附加 className,与基础 class `@pactor-app/markdown` 并存 */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** 数学公式(remark-math + rehype-katex),默认 true;需自行引入 katex css */
|
|
17
|
+
math?: boolean;
|
|
18
|
+
/** fenced 代码块语法高亮,默认 true */
|
|
19
|
+
codeHighlight?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Markdown 渲染器:标题 / 列表 / 表格(GFM)/ 代码块 / 引用 / 链接 / 图片 /
|
|
23
|
+
* 数学公式(KaTeX)。容器带基础 class `@pactor-app/markdown`,样式由宿主主题决定。
|
|
24
|
+
* 技术选型参考 DeepTutor web:react-markdown + remark-gfm + remark-math +
|
|
25
|
+
* rehype-katex + react-syntax-highlighter(Light 构建)。
|
|
26
|
+
*
|
|
27
|
+
* 注意:启用 math 时需在使用方引入 `katex/dist/katex.min.css`。
|
|
28
|
+
*/
|
|
29
|
+
declare function MarkdownRenderer({ content, className, math, codeHighlight, }: MarkdownRendererProps): react.JSX.Element;
|
|
30
|
+
|
|
31
|
+
interface MarkdownDslComponentProps {
|
|
32
|
+
/**
|
|
33
|
+
* Markdown 内容(DSL props.content)。
|
|
34
|
+
* `${data.xxx}` 等表达式已由 Renderer 在 props 下发前解析(设计决策 D3)。
|
|
35
|
+
*/
|
|
36
|
+
content?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* DSL 组件适配:`{ type: 'Markdown', props: { content } }`。
|
|
40
|
+
* 仅做 props 透传,渲染与安全边界全部委托给 MarkdownRenderer。
|
|
41
|
+
*/
|
|
42
|
+
declare function MarkdownDslComponent({ content }: MarkdownDslComponentProps): react.JSX.Element;
|
|
43
|
+
|
|
44
|
+
/** Markdown 组件的 DSL 类型名 */
|
|
45
|
+
declare const MARKDOWN_DSL_TYPE = "Markdown";
|
|
46
|
+
/**
|
|
47
|
+
* 把 Markdown DSL 组件注册到给定的 Component Registry
|
|
48
|
+
* (@pactor-app/runtime 的 `ComponentRegistry` 即 `Registry<unknown>`,
|
|
49
|
+
* @pactor-app/chat 的 Safe Registry 场景亦可复用)。
|
|
50
|
+
*/
|
|
51
|
+
declare function registerMarkdownComponents(registry: Registry<unknown>): void;
|
|
52
|
+
|
|
53
|
+
/** 幂等注册内置语言表(模块加载时由 MarkdownRenderer 触发一次) */
|
|
54
|
+
declare function registerCodeLanguages(): void;
|
|
55
|
+
interface CodeBlockProps {
|
|
56
|
+
language: string;
|
|
57
|
+
children: string;
|
|
58
|
+
}
|
|
59
|
+
/** fenced 代码块高亮渲染(Prism Light + one-light 主题) */
|
|
60
|
+
declare function CodeBlock({ language, children }: CodeBlockProps): react.JSX.Element;
|
|
61
|
+
|
|
62
|
+
export { CodeBlock, type CodeBlockProps, MARKDOWN_DSL_TYPE, MarkdownDslComponent, type MarkdownDslComponentProps, MarkdownRenderer, type MarkdownRendererProps, isSafeUrl, registerCodeLanguages, registerMarkdownComponents };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// src/renderer.tsx
|
|
2
|
+
import Markdown from "react-markdown";
|
|
3
|
+
import rehypeKatex from "rehype-katex";
|
|
4
|
+
import remarkGfm from "remark-gfm";
|
|
5
|
+
import remarkMath from "remark-math";
|
|
6
|
+
|
|
7
|
+
// src/code-block.tsx
|
|
8
|
+
import { isValidElement } from "react";
|
|
9
|
+
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
10
|
+
import bash from "react-syntax-highlighter/dist/esm/languages/prism/bash";
|
|
11
|
+
import css from "react-syntax-highlighter/dist/esm/languages/prism/css";
|
|
12
|
+
import javascript from "react-syntax-highlighter/dist/esm/languages/prism/javascript";
|
|
13
|
+
import json from "react-syntax-highlighter/dist/esm/languages/prism/json";
|
|
14
|
+
import jsx from "react-syntax-highlighter/dist/esm/languages/prism/jsx";
|
|
15
|
+
import markdown from "react-syntax-highlighter/dist/esm/languages/prism/markdown";
|
|
16
|
+
import python from "react-syntax-highlighter/dist/esm/languages/prism/python";
|
|
17
|
+
import tsx from "react-syntax-highlighter/dist/esm/languages/prism/tsx";
|
|
18
|
+
import typescript from "react-syntax-highlighter/dist/esm/languages/prism/typescript";
|
|
19
|
+
import yaml from "react-syntax-highlighter/dist/esm/languages/prism/yaml";
|
|
20
|
+
import oneLight from "react-syntax-highlighter/dist/esm/styles/prism/one-light";
|
|
21
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
22
|
+
var LANGUAGES = {
|
|
23
|
+
bash,
|
|
24
|
+
css,
|
|
25
|
+
javascript,
|
|
26
|
+
js: javascript,
|
|
27
|
+
json,
|
|
28
|
+
jsx,
|
|
29
|
+
markdown,
|
|
30
|
+
md: markdown,
|
|
31
|
+
python,
|
|
32
|
+
py: python,
|
|
33
|
+
tsx,
|
|
34
|
+
typescript,
|
|
35
|
+
ts: typescript,
|
|
36
|
+
yaml,
|
|
37
|
+
yml: yaml
|
|
38
|
+
};
|
|
39
|
+
var registered = false;
|
|
40
|
+
function registerCodeLanguages() {
|
|
41
|
+
if (registered) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
registered = true;
|
|
45
|
+
for (const [name, language] of Object.entries(LANGUAGES)) {
|
|
46
|
+
SyntaxHighlighter.registerLanguage(name, language);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function isCodeLanguageRegistered(language) {
|
|
50
|
+
return language.toLowerCase() in LANGUAGES;
|
|
51
|
+
}
|
|
52
|
+
function CodeBlock({ language, children }) {
|
|
53
|
+
return /* @__PURE__ */ jsx2(
|
|
54
|
+
SyntaxHighlighter,
|
|
55
|
+
{
|
|
56
|
+
language: language.toLowerCase(),
|
|
57
|
+
style: oneLight,
|
|
58
|
+
customStyle: { margin: 0, background: "transparent", fontSize: "0.85em" },
|
|
59
|
+
codeTagProps: { className: `language-${language.toLowerCase()}` },
|
|
60
|
+
children: children.replace(/\n$/, "")
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
function extractFencedCode(preChildren) {
|
|
65
|
+
if (!isValidElement(preChildren)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const { className, children } = preChildren.props;
|
|
69
|
+
const match = /language-([\w-]+)/.exec(className ?? "");
|
|
70
|
+
if (!match) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return { language: match[1], code: String(children ?? "") };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/renderer.tsx
|
|
77
|
+
import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
|
|
78
|
+
var LINK_PROTOCOL_WHITELIST = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
|
|
79
|
+
var IMAGE_PROTOCOL_WHITELIST = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
80
|
+
function extractProtocol(url) {
|
|
81
|
+
const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(url.trim());
|
|
82
|
+
return match ? `${match[1].toLowerCase()}:` : null;
|
|
83
|
+
}
|
|
84
|
+
function isSafeUrl(url, kind) {
|
|
85
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const protocol = extractProtocol(url);
|
|
89
|
+
if (protocol === null) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
const whitelist = kind === "link" ? LINK_PROTOCOL_WHITELIST : IMAGE_PROTOCOL_WHITELIST;
|
|
93
|
+
return whitelist.has(protocol);
|
|
94
|
+
}
|
|
95
|
+
var SafeLink = ({ href, children }) => {
|
|
96
|
+
if (!isSafeUrl(href, "link")) {
|
|
97
|
+
return /* @__PURE__ */ jsx3(Fragment, { children });
|
|
98
|
+
}
|
|
99
|
+
const protocol = extractProtocol(href);
|
|
100
|
+
if (protocol === "http:" || protocol === "https:") {
|
|
101
|
+
return /* @__PURE__ */ jsx3("a", { href, target: "_blank", rel: "noreferrer", children });
|
|
102
|
+
}
|
|
103
|
+
return /* @__PURE__ */ jsx3("a", { href, children });
|
|
104
|
+
};
|
|
105
|
+
var SafeImage = ({ src, alt }) => {
|
|
106
|
+
if (!isSafeUrl(src, "image")) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return /* @__PURE__ */ jsx3("img", { src, alt: alt ?? "" });
|
|
110
|
+
};
|
|
111
|
+
var baseComponents = {
|
|
112
|
+
a: SafeLink,
|
|
113
|
+
img: SafeImage
|
|
114
|
+
};
|
|
115
|
+
var highlightingComponents = {
|
|
116
|
+
...baseComponents,
|
|
117
|
+
pre: ({ children }) => {
|
|
118
|
+
const fenced = extractFencedCode(children);
|
|
119
|
+
if (fenced && isCodeLanguageRegistered(fenced.language)) {
|
|
120
|
+
return /* @__PURE__ */ jsx3(CodeBlock, { language: fenced.language, children: fenced.code });
|
|
121
|
+
}
|
|
122
|
+
return /* @__PURE__ */ jsx3("pre", { children });
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
function MarkdownRenderer({
|
|
126
|
+
content = "",
|
|
127
|
+
className,
|
|
128
|
+
math = true,
|
|
129
|
+
codeHighlight = true
|
|
130
|
+
}) {
|
|
131
|
+
if (codeHighlight) {
|
|
132
|
+
registerCodeLanguages();
|
|
133
|
+
}
|
|
134
|
+
const containerClass = className ? `pactor-markdown ${className}` : "pactor-markdown";
|
|
135
|
+
return /* @__PURE__ */ jsx3("div", { className: containerClass, children: /* @__PURE__ */ jsx3(
|
|
136
|
+
Markdown,
|
|
137
|
+
{
|
|
138
|
+
remarkPlugins: math ? [remarkGfm, remarkMath] : [remarkGfm],
|
|
139
|
+
rehypePlugins: math ? [rehypeKatex] : [],
|
|
140
|
+
components: codeHighlight ? highlightingComponents : baseComponents,
|
|
141
|
+
children: content
|
|
142
|
+
}
|
|
143
|
+
) });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/dsl-component.tsx
|
|
147
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
148
|
+
function MarkdownDslComponent({ content }) {
|
|
149
|
+
return /* @__PURE__ */ jsx4(MarkdownRenderer, { content: content ?? "" });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/register.ts
|
|
153
|
+
var MARKDOWN_DSL_TYPE = "Markdown";
|
|
154
|
+
function registerMarkdownComponents(registry) {
|
|
155
|
+
registry.register(MARKDOWN_DSL_TYPE, MarkdownDslComponent);
|
|
156
|
+
}
|
|
157
|
+
export {
|
|
158
|
+
CodeBlock,
|
|
159
|
+
MARKDOWN_DSL_TYPE,
|
|
160
|
+
MarkdownDslComponent,
|
|
161
|
+
MarkdownRenderer,
|
|
162
|
+
isSafeUrl,
|
|
163
|
+
registerCodeLanguages,
|
|
164
|
+
registerMarkdownComponents
|
|
165
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pactor-app/markdown",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pactor Markdown module: safe Markdown renderer (react-markdown AST, no raw HTML) and DSL Markdown component adapter",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pactor",
|
|
7
|
+
"dsl",
|
|
8
|
+
"server-driven-ui",
|
|
9
|
+
"markdown",
|
|
10
|
+
"react"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/426-330/pactor.git",
|
|
16
|
+
"directory": "packages/markdown"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/426-330/pactor/tree/main/packages/markdown",
|
|
19
|
+
"bugs": "https://github.com/426-330/pactor/issues",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "./dist/index.cjs",
|
|
22
|
+
"module": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"sideEffects": false,
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"katex": "^0.18.4",
|
|
48
|
+
"react-markdown": "^10.1.0",
|
|
49
|
+
"react-syntax-highlighter": "^16.1.1",
|
|
50
|
+
"rehype-katex": "^7.0.1",
|
|
51
|
+
"remark-gfm": "^4.0.1",
|
|
52
|
+
"remark-math": "^6.0.0",
|
|
53
|
+
"@pactor-app/core": "0.1.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"react": ">=18"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@testing-library/dom": "^10.4.1",
|
|
60
|
+
"@testing-library/react": "^16.3.2",
|
|
61
|
+
"@types/react": "^19.2.18",
|
|
62
|
+
"@types/react-dom": "^19.2.4",
|
|
63
|
+
"@types/react-syntax-highlighter": "^15.5.13",
|
|
64
|
+
"jsdom": "^30.0.1",
|
|
65
|
+
"react": "^19.2.8",
|
|
66
|
+
"react-dom": "^19.2.8",
|
|
67
|
+
"@pactor-app/runtime": "0.1.0"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"build": "tsup",
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"typecheck": "tsc --noEmit"
|
|
73
|
+
}
|
|
74
|
+
}
|