@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 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 字符串,可复用或缓存。
@@ -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
+ }