@ckpack/markdown-wx 1.0.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 +164 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +215 -0
- package/dist/server/index.d.mts +20 -0
- package/dist/server/index.mjs +121 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ckvv
|
|
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,164 @@
|
|
|
1
|
+
@ckpack/markdown-wx
|
|
2
|
+
|
|
3
|
+
将 Markdown 转换为微信小程序富文本格式(https://developers.weixin.qq.com/miniprogram/dev/component/rich-text.html)
|
|
4
|
+
|
|
5
|
+
# client
|
|
6
|
+
通过 [markdown-exit](https://markdown-exit.pages.dev/guide/introduction.html) 将 markdown 字符串 转为 html
|
|
7
|
+
|
|
8
|
+
## 通过插件支持数学公式支持
|
|
9
|
+
行内公式 用 单个 $ 包起来:`$E = mc^2$`
|
|
10
|
+
块级公式 用 双 $$ 包起来:`$$E = mc^2$$`
|
|
11
|
+
通过插件请求 server 接口 渲染公式内容为 svg
|
|
12
|
+
|
|
13
|
+
# server
|
|
14
|
+
通过 [katex](https://katex.org/docs/node) 将 公式 转为 svg 图片
|
|
15
|
+
|
|
16
|
+
# API
|
|
17
|
+
|
|
18
|
+
## client
|
|
19
|
+
|
|
20
|
+
### createMarkdownWx(options?)
|
|
21
|
+
创建一个渲染器。
|
|
22
|
+
|
|
23
|
+
参数:
|
|
24
|
+
- options.markdown: MarkdownExitOptions,透传给 markdown-exit
|
|
25
|
+
- options.math: MathPluginOptions,开启数学公式插件
|
|
26
|
+
|
|
27
|
+
返回:
|
|
28
|
+
- { render: (src, env?) => Promise<string> }
|
|
29
|
+
|
|
30
|
+
示例:
|
|
31
|
+
```ts
|
|
32
|
+
import { createMarkdownWx } from '@ckpack/markdown-wx';
|
|
33
|
+
|
|
34
|
+
const renderer = createMarkdownWx({
|
|
35
|
+
math: { endpoint: 'http://localhost:3000/katex' }
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const html = await renderer.render('# Hello $E=mc^2$');
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### renderMarkdownToWx(src, options?)
|
|
42
|
+
一键渲染。
|
|
43
|
+
|
|
44
|
+
参数:
|
|
45
|
+
- src: markdown 字符串
|
|
46
|
+
- options: 同 createMarkdownWx
|
|
47
|
+
|
|
48
|
+
返回:
|
|
49
|
+
- Promise<string> (HTML)
|
|
50
|
+
|
|
51
|
+
示例:
|
|
52
|
+
```ts
|
|
53
|
+
import { renderMarkdownToWx } from '@ckpack/markdown-wx';
|
|
54
|
+
|
|
55
|
+
const html = await renderMarkdownToWx('$$E=mc^2$$', {
|
|
56
|
+
math: { endpoint: 'http://localhost:3000/katex' }
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### mathPlugin(options?)
|
|
61
|
+
数学公式插件。
|
|
62
|
+
|
|
63
|
+
MathPluginOptions:
|
|
64
|
+
- render?: (context) => Promise<string> | string
|
|
65
|
+
直接提供渲染函数,返回 svg 字符串
|
|
66
|
+
- endpoint?: string
|
|
67
|
+
公式渲染服务地址 (POST JSON 或 GET query)
|
|
68
|
+
- fetch?: FetchLike
|
|
69
|
+
自定义 fetch 实现 (微信小程序可用 createWxRequestFetchLike)
|
|
70
|
+
- requestInit?: FetchInit | ((context) => FetchInit)
|
|
71
|
+
自定义请求参数
|
|
72
|
+
- output?: 'img' | 'svg'
|
|
73
|
+
输出为 img(data uri) 或直接嵌入 svg,默认 'img'
|
|
74
|
+
- svgToDataUri?: (svg) => string
|
|
75
|
+
自定义 svg -> data uri
|
|
76
|
+
- inlineClassName?: string
|
|
77
|
+
行内公式 class
|
|
78
|
+
- blockClassName?: string
|
|
79
|
+
块级公式 class
|
|
80
|
+
|
|
81
|
+
MathRenderContext:
|
|
82
|
+
- formula: string
|
|
83
|
+
- displayMode: boolean
|
|
84
|
+
|
|
85
|
+
示例(自定义 render):
|
|
86
|
+
```ts
|
|
87
|
+
import { createMarkdownWx } from '@ckpack/markdown-wx';
|
|
88
|
+
|
|
89
|
+
const renderer = createMarkdownWx({
|
|
90
|
+
math: {
|
|
91
|
+
render: ({ formula, displayMode }) =>
|
|
92
|
+
`<svg xmlns="http://www.w3.org/2000/svg"><text>${displayMode ? 'block:' : 'inline:'}${formula}</text></svg>`
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### createWxRequestFetchLike(wxRequest?, options?)
|
|
98
|
+
将微信小程序的 wx.request 封装成 FetchLike。
|
|
99
|
+
|
|
100
|
+
参数:
|
|
101
|
+
- wxRequest?: WxRequest (默认使用全局 wx.request)
|
|
102
|
+
- options.dataType?: string (默认 'text')
|
|
103
|
+
- options.responseType?: string
|
|
104
|
+
- options.timeout?: number
|
|
105
|
+
|
|
106
|
+
返回:
|
|
107
|
+
- FetchLike
|
|
108
|
+
|
|
109
|
+
示例:
|
|
110
|
+
```ts
|
|
111
|
+
import { createMarkdownWx, createWxRequestFetchLike } from '@ckpack/markdown-wx';
|
|
112
|
+
|
|
113
|
+
const fetchLike = createWxRequestFetchLike();
|
|
114
|
+
|
|
115
|
+
const html = await createMarkdownWx({
|
|
116
|
+
math: {
|
|
117
|
+
endpoint: 'https://example.com/katex',
|
|
118
|
+
fetch: fetchLike
|
|
119
|
+
}
|
|
120
|
+
}).render('$E=mc^2$');
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## server
|
|
124
|
+
|
|
125
|
+
### renderFormulaToSvg(formula, options?)
|
|
126
|
+
将公式渲染为 svg 字符串。
|
|
127
|
+
|
|
128
|
+
参数:
|
|
129
|
+
- formula: string
|
|
130
|
+
- options: RenderFormulaOptions (katex 选项 + displayMode? + css?)
|
|
131
|
+
|
|
132
|
+
示例:
|
|
133
|
+
```ts
|
|
134
|
+
import { renderFormulaToSvg } from '@ckpack/markdown-wx';
|
|
135
|
+
|
|
136
|
+
const svg = renderFormulaToSvg('E = mc^2', { displayMode: true });
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### createMathSvgHandler(options?)
|
|
140
|
+
创建一个 Node HTTP handler,处理公式渲染请求。
|
|
141
|
+
|
|
142
|
+
参数:
|
|
143
|
+
- path?: string (默认 '/katex')
|
|
144
|
+
- cors?: boolean (默认 true)
|
|
145
|
+
- maxBodySize?: number (默认 1MB)
|
|
146
|
+
- 其余字段同 RenderFormulaOptions
|
|
147
|
+
|
|
148
|
+
请求格式:
|
|
149
|
+
- GET /katex?formula=...&displayMode=true
|
|
150
|
+
- POST /katex { "formula": "...", "displayMode": true }
|
|
151
|
+
|
|
152
|
+
### createMathSvgServer(options?)
|
|
153
|
+
快速创建 HTTP Server。
|
|
154
|
+
|
|
155
|
+
示例:
|
|
156
|
+
```ts
|
|
157
|
+
import { createMathSvgServer } from '@ckpack/markdown-wx/server';
|
|
158
|
+
|
|
159
|
+
const server = createMathSvgServer({ path: '/katex' });
|
|
160
|
+
server.listen(3000);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### getKatexCss()
|
|
164
|
+
返回内置的 KaTeX CSS 字符串,可复用或缓存。
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { MarkdownExit, MarkdownExitOptions } from "markdown-exit";
|
|
2
|
+
|
|
3
|
+
//#region src/math.d.ts
|
|
4
|
+
interface FetchInit {
|
|
5
|
+
method?: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
body?: string;
|
|
8
|
+
}
|
|
9
|
+
interface FetchResponseLike {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
status: number;
|
|
12
|
+
statusText: string;
|
|
13
|
+
text: () => Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
type FetchLike = (input: string | URL, init?: FetchInit) => Promise<FetchResponseLike>;
|
|
16
|
+
interface MathRenderContext {
|
|
17
|
+
formula: string;
|
|
18
|
+
displayMode: boolean;
|
|
19
|
+
}
|
|
20
|
+
type MathRenderer = (context: MathRenderContext) => Promise<string> | string;
|
|
21
|
+
interface MathPluginOptions {
|
|
22
|
+
render?: MathRenderer;
|
|
23
|
+
endpoint?: string;
|
|
24
|
+
fetch?: FetchLike;
|
|
25
|
+
requestInit?: FetchInit | ((context: MathRenderContext) => FetchInit);
|
|
26
|
+
output?: 'img' | 'svg';
|
|
27
|
+
svgToDataUri?: (svg: string) => string;
|
|
28
|
+
inlineClassName?: string;
|
|
29
|
+
blockClassName?: string;
|
|
30
|
+
}
|
|
31
|
+
declare function mathPlugin(options?: MathPluginOptions): (md: MarkdownExit) => void;
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/wx-fetch.d.ts
|
|
34
|
+
interface WxRequestSuccessResult {
|
|
35
|
+
data: unknown;
|
|
36
|
+
statusCode: number;
|
|
37
|
+
header: Record<string, string>;
|
|
38
|
+
cookies?: string[];
|
|
39
|
+
errMsg: string;
|
|
40
|
+
}
|
|
41
|
+
interface WxRequestFailResult {
|
|
42
|
+
errMsg: string;
|
|
43
|
+
}
|
|
44
|
+
interface WxRequestOptions {
|
|
45
|
+
url: string;
|
|
46
|
+
data?: unknown;
|
|
47
|
+
header?: Record<string, string>;
|
|
48
|
+
method?: string;
|
|
49
|
+
dataType?: string;
|
|
50
|
+
responseType?: string;
|
|
51
|
+
timeout?: number;
|
|
52
|
+
success?: (res: WxRequestSuccessResult) => void;
|
|
53
|
+
fail?: (res: WxRequestFailResult) => void;
|
|
54
|
+
complete?: (res: WxRequestSuccessResult | WxRequestFailResult) => void;
|
|
55
|
+
}
|
|
56
|
+
interface WxRequestTask {
|
|
57
|
+
abort: () => void;
|
|
58
|
+
}
|
|
59
|
+
type WxRequest = (options: WxRequestOptions) => WxRequestTask;
|
|
60
|
+
interface WxFetchOptions {
|
|
61
|
+
dataType?: string;
|
|
62
|
+
responseType?: string;
|
|
63
|
+
timeout?: number;
|
|
64
|
+
}
|
|
65
|
+
declare function createWxRequestFetchLike(wxRequest?: WxRequest | undefined, options?: WxFetchOptions): FetchLike;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/index.d.ts
|
|
68
|
+
interface MarkdownWxOptions {
|
|
69
|
+
markdown?: MarkdownExitOptions;
|
|
70
|
+
math?: MathPluginOptions;
|
|
71
|
+
}
|
|
72
|
+
interface MarkdownWxRenderer {
|
|
73
|
+
render: (src: string, env?: Record<string, unknown>) => Promise<string>;
|
|
74
|
+
}
|
|
75
|
+
declare function createMarkdownWx(options?: MarkdownWxOptions): MarkdownWxRenderer;
|
|
76
|
+
declare function renderMarkdownToWx(src: string, options?: MarkdownWxOptions): Promise<string>;
|
|
77
|
+
//#endregion
|
|
78
|
+
export { type FetchInit, type FetchLike, type FetchResponseLike, MarkdownWxOptions, MarkdownWxRenderer, type MathPluginOptions, type MathRenderContext, type MathRenderer, type WxFetchOptions, type WxRequest, createMarkdownWx, createWxRequestFetchLike, mathPlugin, renderMarkdownToWx };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { createMarkdownExit } from "markdown-exit";
|
|
2
|
+
|
|
3
|
+
//#region src/math.ts
|
|
4
|
+
function resolveRenderer(options) {
|
|
5
|
+
if (options.render) return options.render;
|
|
6
|
+
if (!options.endpoint) return async () => {
|
|
7
|
+
throw new Error("Math renderer is not configured. Provide render() or endpoint.");
|
|
8
|
+
};
|
|
9
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
10
|
+
if (!fetcher) return async () => {
|
|
11
|
+
throw new Error("fetch is not available. Provide a fetch implementation.");
|
|
12
|
+
};
|
|
13
|
+
return async (context) => {
|
|
14
|
+
const payload = {
|
|
15
|
+
formula: context.formula,
|
|
16
|
+
displayMode: context.displayMode
|
|
17
|
+
};
|
|
18
|
+
const init = typeof options.requestInit === "function" ? options.requestInit(context) : options.requestInit ?? {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "content-type": "application/json" },
|
|
21
|
+
body: JSON.stringify(payload)
|
|
22
|
+
};
|
|
23
|
+
const response = await fetcher(options.endpoint, init);
|
|
24
|
+
if (!response.ok) throw new Error(`Math render request failed: ${response.status} ${response.statusText}`);
|
|
25
|
+
return response.text();
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function svgToDataUri(svg) {
|
|
29
|
+
const cleaned = svg.replace(/[\n\r\t]+/g, " ").trim();
|
|
30
|
+
return `data:image/svg+xml;utf8,${encodeURIComponent(cleaned).replace(/%20/g, " ").replace(/%3D/g, "=").replace(/%3A/g, ":").replace(/%2F/g, "/")}`;
|
|
31
|
+
}
|
|
32
|
+
function normalizeFormula(content) {
|
|
33
|
+
return content.replace(/\s+$/g, "").replace(/^\s+/g, "");
|
|
34
|
+
}
|
|
35
|
+
function buildResolvedOptions(options) {
|
|
36
|
+
return {
|
|
37
|
+
render: resolveRenderer(options),
|
|
38
|
+
output: options.output ?? "img",
|
|
39
|
+
svgToDataUri: options.svgToDataUri ?? svgToDataUri,
|
|
40
|
+
inlineClassName: options.inlineClassName,
|
|
41
|
+
blockClassName: options.blockClassName
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function renderImg(svg, formula, displayMode, md, resolved) {
|
|
45
|
+
const dataUri = resolved.svgToDataUri(svg);
|
|
46
|
+
const alt = md.utils.escapeHtml(formula);
|
|
47
|
+
const className = displayMode ? resolved.blockClassName : resolved.inlineClassName;
|
|
48
|
+
return `<img${className ? ` class="${md.utils.escapeHtml(className)}"` : ""} data-math="${displayMode ? "block" : "inline"}" style="${displayMode ? "display:block;max-width:100%;margin:0.5em 0;" : "display:inline-block;vertical-align:middle;"}" alt="${alt}" src="${dataUri}">`;
|
|
49
|
+
}
|
|
50
|
+
function renderSvg(svg, displayMode, md, resolved) {
|
|
51
|
+
const className = displayMode ? resolved.blockClassName : resolved.inlineClassName;
|
|
52
|
+
const classAttr = className ? ` class="${md.utils.escapeHtml(className)}"` : "";
|
|
53
|
+
const wrapper = displayMode ? "div" : "span";
|
|
54
|
+
return `<${wrapper}${classAttr} data-math="${displayMode ? "block" : "inline"}">${svg}</${wrapper}>`;
|
|
55
|
+
}
|
|
56
|
+
function createRenderRule(md, resolved, displayMode) {
|
|
57
|
+
return async (tokens, idx, _options, _env) => {
|
|
58
|
+
const formula = normalizeFormula(tokens[idx].content);
|
|
59
|
+
const svg = await resolved.render({
|
|
60
|
+
formula,
|
|
61
|
+
displayMode
|
|
62
|
+
});
|
|
63
|
+
return resolved.output === "svg" ? renderSvg(svg, displayMode, md, resolved) : renderImg(svg, formula, displayMode, md, resolved);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function mathInlineRule(state, silent) {
|
|
67
|
+
const start = state.pos;
|
|
68
|
+
const max = state.posMax;
|
|
69
|
+
if (state.src.charCodeAt(start) !== 36) return false;
|
|
70
|
+
if (start + 1 < max && state.src.charCodeAt(start + 1) === 36) return false;
|
|
71
|
+
if (start > 0 && state.src.charCodeAt(start - 1) === 92) return false;
|
|
72
|
+
let pos = start + 1;
|
|
73
|
+
while (pos < max) {
|
|
74
|
+
const match = state.src.indexOf("$", pos);
|
|
75
|
+
if (match === -1) return false;
|
|
76
|
+
if (state.src.charCodeAt(match - 1) !== 92) {
|
|
77
|
+
pos = match;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
pos = match + 1;
|
|
81
|
+
}
|
|
82
|
+
if (pos >= max) return false;
|
|
83
|
+
if (pos === start + 1) return false;
|
|
84
|
+
if (!silent) {
|
|
85
|
+
const token = state.push("math_inline", "span", 0);
|
|
86
|
+
token.markup = "$";
|
|
87
|
+
token.content = state.src.slice(start + 1, pos);
|
|
88
|
+
}
|
|
89
|
+
state.pos = pos + 1;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function mathBlockRule(state, startLine, endLine, silent) {
|
|
93
|
+
let pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
94
|
+
let max = state.eMarks[startLine];
|
|
95
|
+
if (pos + 2 > max) return false;
|
|
96
|
+
if (state.src.slice(pos, pos + 2) !== "$$") return false;
|
|
97
|
+
if (silent) return true;
|
|
98
|
+
let nextLine = startLine;
|
|
99
|
+
let content = "";
|
|
100
|
+
let lineText = state.src.slice(pos + 2, max);
|
|
101
|
+
if (lineText.trim().endsWith("$$")) {
|
|
102
|
+
const lastIndex = lineText.lastIndexOf("$$");
|
|
103
|
+
content = lineText.slice(0, lastIndex).trim();
|
|
104
|
+
state.line = startLine + 1;
|
|
105
|
+
} else {
|
|
106
|
+
let found = false;
|
|
107
|
+
content += `${lineText}\n`;
|
|
108
|
+
while (true) {
|
|
109
|
+
nextLine += 1;
|
|
110
|
+
if (nextLine >= endLine) break;
|
|
111
|
+
pos = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
112
|
+
max = state.eMarks[nextLine];
|
|
113
|
+
lineText = state.src.slice(pos, max);
|
|
114
|
+
const endIndex = lineText.indexOf("$$");
|
|
115
|
+
if (endIndex !== -1) {
|
|
116
|
+
content += lineText.slice(0, endIndex);
|
|
117
|
+
state.line = nextLine + 1;
|
|
118
|
+
found = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
content += `${lineText}\n`;
|
|
122
|
+
}
|
|
123
|
+
if (!found) state.line = nextLine;
|
|
124
|
+
}
|
|
125
|
+
const token = state.push("math_block", "div", 0);
|
|
126
|
+
token.block = true;
|
|
127
|
+
token.content = content;
|
|
128
|
+
token.map = [startLine, state.line];
|
|
129
|
+
token.markup = "$$";
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
function mathPlugin(options = {}) {
|
|
133
|
+
return (md) => {
|
|
134
|
+
const resolved = buildResolvedOptions(options);
|
|
135
|
+
md.inline.ruler.after("escape", "math_inline", mathInlineRule);
|
|
136
|
+
md.block.ruler.after("blockquote", "math_block", mathBlockRule, { alt: [
|
|
137
|
+
"paragraph",
|
|
138
|
+
"reference",
|
|
139
|
+
"blockquote",
|
|
140
|
+
"list"
|
|
141
|
+
] });
|
|
142
|
+
md.renderer.rules.math_inline = createRenderRule(md, resolved, false);
|
|
143
|
+
md.renderer.rules.math_block = createRenderRule(md, resolved, true);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/wx-fetch.ts
|
|
149
|
+
function getDefaultWxRequest() {
|
|
150
|
+
return globalThis.wx?.request;
|
|
151
|
+
}
|
|
152
|
+
function normalizeText(data) {
|
|
153
|
+
if (typeof data === "string") return data;
|
|
154
|
+
try {
|
|
155
|
+
return JSON.stringify(data);
|
|
156
|
+
} catch {
|
|
157
|
+
return String(data);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function getHeaderValue(headers, name) {
|
|
161
|
+
const lower = name.toLowerCase();
|
|
162
|
+
for (const key of Object.keys(headers)) if (key.toLowerCase() === lower) return headers[key];
|
|
163
|
+
}
|
|
164
|
+
function createWxRequestFetchLike(wxRequest = getDefaultWxRequest(), options = {}) {
|
|
165
|
+
if (!wxRequest) return async () => {
|
|
166
|
+
throw new Error("wx.request is not available. Provide wxRequest explicitly.");
|
|
167
|
+
};
|
|
168
|
+
return (input, init) => {
|
|
169
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
170
|
+
const method = init?.method ?? "GET";
|
|
171
|
+
const headers = init?.headers ?? {};
|
|
172
|
+
let data = init?.body;
|
|
173
|
+
const contentType = getHeaderValue(headers, "content-type");
|
|
174
|
+
if (typeof data === "string" && contentType?.toLowerCase().includes("application/json")) try {
|
|
175
|
+
data = JSON.parse(data);
|
|
176
|
+
} catch {}
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
wxRequest({
|
|
179
|
+
url,
|
|
180
|
+
method,
|
|
181
|
+
data,
|
|
182
|
+
header: headers,
|
|
183
|
+
dataType: options.dataType ?? "text",
|
|
184
|
+
responseType: options.responseType,
|
|
185
|
+
timeout: options.timeout,
|
|
186
|
+
success: (res) => {
|
|
187
|
+
const status = res.statusCode ?? 0;
|
|
188
|
+
resolve({
|
|
189
|
+
ok: status >= 200 && status < 300,
|
|
190
|
+
status,
|
|
191
|
+
statusText: res.errMsg ?? "",
|
|
192
|
+
text: async () => normalizeText(res.data)
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
fail: (err) => {
|
|
196
|
+
reject(new Error(err?.errMsg ?? "wx.request failed"));
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/index.ts
|
|
205
|
+
function createMarkdownWx(options = {}) {
|
|
206
|
+
const md = createMarkdownExit(options.markdown);
|
|
207
|
+
if (options.math) md.use(mathPlugin(options.math));
|
|
208
|
+
return { render: (src, env) => md.renderAsync(src, env) };
|
|
209
|
+
}
|
|
210
|
+
async function renderMarkdownToWx(src, options = {}) {
|
|
211
|
+
return createMarkdownWx(options).render(src);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
export { createMarkdownWx, createWxRequestFetchLike, mathPlugin, renderMarkdownToWx };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as http from "node:http";
|
|
2
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import * as katex from "katex";
|
|
4
|
+
|
|
5
|
+
//#region src/server/index.d.ts
|
|
6
|
+
interface RenderFormulaOptions extends katex.KatexOptions {
|
|
7
|
+
displayMode?: boolean;
|
|
8
|
+
css?: string;
|
|
9
|
+
}
|
|
10
|
+
interface MathSvgServerOptions extends RenderFormulaOptions {
|
|
11
|
+
path?: string;
|
|
12
|
+
cors?: boolean;
|
|
13
|
+
maxBodySize?: number;
|
|
14
|
+
}
|
|
15
|
+
declare function getKatexCss(): string;
|
|
16
|
+
declare function renderFormulaToSvg(formula: string, options?: RenderFormulaOptions): string;
|
|
17
|
+
declare function createMathSvgHandler(options?: MathSvgServerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
18
|
+
declare function createMathSvgServer(options?: MathSvgServerOptions): http.Server;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { MathSvgServerOptions, RenderFormulaOptions, createMathSvgHandler, createMathSvgServer, getKatexCss, renderFormulaToSvg };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import * as http from "node:http";
|
|
5
|
+
import * as katex from "katex";
|
|
6
|
+
|
|
7
|
+
//#region src/server/index.ts
|
|
8
|
+
let cachedKatexCss = null;
|
|
9
|
+
function getKatexCss() {
|
|
10
|
+
if (cachedKatexCss) return cachedKatexCss;
|
|
11
|
+
cachedKatexCss = readFileSync(createRequire(import.meta.url).resolve("katex/dist/katex.css"), "utf8");
|
|
12
|
+
return cachedKatexCss;
|
|
13
|
+
}
|
|
14
|
+
function renderFormulaToSvg(formula, options = {}) {
|
|
15
|
+
const { displayMode = false, css = getKatexCss(), ...katexOptions } = options;
|
|
16
|
+
const html = katex.renderToString(formula, {
|
|
17
|
+
displayMode,
|
|
18
|
+
throwOnError: false,
|
|
19
|
+
...katexOptions
|
|
20
|
+
});
|
|
21
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
22
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" overflow="visible">
|
|
23
|
+
<foreignObject x="0" y="0" width="100%" height="100%">
|
|
24
|
+
${`
|
|
25
|
+
<div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;line-height:1;">
|
|
26
|
+
${css ? `<style>${css}</style>` : ""}
|
|
27
|
+
${html}
|
|
28
|
+
</div>`}
|
|
29
|
+
</foreignObject>
|
|
30
|
+
</svg>`;
|
|
31
|
+
}
|
|
32
|
+
function parseBoolean(value) {
|
|
33
|
+
if (!value) return false;
|
|
34
|
+
return value === "1" || value.toLowerCase() === "true";
|
|
35
|
+
}
|
|
36
|
+
async function readRequestBody(req, maxBodySize) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const chunks = [];
|
|
39
|
+
let size = 0;
|
|
40
|
+
req.on("data", (chunk) => {
|
|
41
|
+
size += chunk.length;
|
|
42
|
+
if (size > maxBodySize) {
|
|
43
|
+
reject(/* @__PURE__ */ new Error("Request body too large"));
|
|
44
|
+
req.destroy();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
chunks.push(chunk);
|
|
48
|
+
});
|
|
49
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
50
|
+
req.on("error", reject);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function createMathSvgHandler(options = {}) {
|
|
54
|
+
const { path = "/katex", cors = true, maxBodySize = 1024 * 1024, ...renderOptions } = options;
|
|
55
|
+
return async (req, res) => {
|
|
56
|
+
const host = req.headers.host ?? "localhost";
|
|
57
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
58
|
+
if (url.pathname !== path) {
|
|
59
|
+
res.statusCode = 404;
|
|
60
|
+
res.end("Not Found");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (cors) {
|
|
64
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
65
|
+
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
|
|
66
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
67
|
+
}
|
|
68
|
+
if (req.method === "OPTIONS") {
|
|
69
|
+
res.statusCode = 204;
|
|
70
|
+
res.end();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let formula = "";
|
|
74
|
+
let displayMode = false;
|
|
75
|
+
if (req.method === "GET") {
|
|
76
|
+
formula = url.searchParams.get("formula") ?? "";
|
|
77
|
+
displayMode = parseBoolean(url.searchParams.get("displayMode"));
|
|
78
|
+
} else if (req.method === "POST") {
|
|
79
|
+
const body = await readRequestBody(req, maxBodySize);
|
|
80
|
+
try {
|
|
81
|
+
const payload = JSON.parse(body);
|
|
82
|
+
formula = payload.formula ?? "";
|
|
83
|
+
displayMode = Boolean(payload.displayMode);
|
|
84
|
+
} catch {
|
|
85
|
+
res.statusCode = 400;
|
|
86
|
+
res.end("Invalid JSON payload");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
res.statusCode = 405;
|
|
91
|
+
res.end("Method Not Allowed");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!formula.trim()) {
|
|
95
|
+
res.statusCode = 400;
|
|
96
|
+
res.end("Missing formula");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const svg = renderFormulaToSvg(formula, {
|
|
101
|
+
displayMode,
|
|
102
|
+
...renderOptions
|
|
103
|
+
});
|
|
104
|
+
res.statusCode = 200;
|
|
105
|
+
res.setHeader("Content-Type", "image/svg+xml; charset=utf-8");
|
|
106
|
+
res.end(svg);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
res.statusCode = 500;
|
|
109
|
+
res.end(error instanceof Error ? error.message : "Render failed");
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function createMathSvgServer(options = {}) {
|
|
114
|
+
const handler = createMathSvgHandler(options);
|
|
115
|
+
return http.createServer((req, res) => {
|
|
116
|
+
handler(req, res);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
export { createMathSvgHandler, createMathSvgServer, getKatexCss, renderFormulaToSvg };
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ckpack/markdown-wx",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"private": false,
|
|
6
|
+
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac",
|
|
7
|
+
"description": "将 Markdown 转换为微信小程序富文本格式.",
|
|
8
|
+
"author": "ckvv",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": "https://github.com/ckpack/markdown-wx",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.js",
|
|
13
|
+
"./server": "./dist/server/index.mjs",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "node --import=tsx src/index.ts",
|
|
24
|
+
"build": "tsdown",
|
|
25
|
+
"test": "vitest",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint --fix",
|
|
28
|
+
"publish:npm": "npm run build && npm publish --registry https://registry.npmjs.org/ --access public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"katex": "^0.16.28",
|
|
32
|
+
"markdown-exit": "1.0.0-beta.7"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@antfu/eslint-config": "^7.2.0",
|
|
36
|
+
"@types/node": "^25.0.10",
|
|
37
|
+
"eslint": "^9.39.2",
|
|
38
|
+
"lint-staged": "^16.2.7",
|
|
39
|
+
"simple-git-hooks": "^2.13.1",
|
|
40
|
+
"tsdown": "^0.20.1",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
},
|
|
44
|
+
"simple-git-hooks": {
|
|
45
|
+
"pre-commit": "npx lint-staged"
|
|
46
|
+
},
|
|
47
|
+
"lint-staged": {
|
|
48
|
+
"*.{js,ts,mjs,cjs,tsx,jsx,vue,md}": [
|
|
49
|
+
"eslint --fix"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|