markdown-to-mfuns-json 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/README.md +457 -0
- package/dist/index.cjs +6 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +4 -0
- package/dist/parser.cjs +328 -0
- package/dist/parser.d.mts +21 -0
- package/dist/parser.d.ts +21 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.mjs +326 -0
- package/dist/types.cjs +1 -0
- package/dist/types.d.mts +98 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.mjs +1 -0
- package/dist/upload.cjs +226 -0
- package/dist/upload.d.mts +51 -0
- package/dist/upload.d.ts +51 -0
- package/dist/upload.d.ts.map +1 -0
- package/dist/upload.mjs +224 -0
- package/package.json +44 -0
package/dist/parser.cjs
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
const { processImageUrl, isBase64Image, isImageUrl } = require('./upload.cjs');
|
|
2
|
+
/**
|
|
3
|
+
* 解析 Markdown 行内格式(粗体、斜体、删除线、下划线、链接、行内代码、图片)
|
|
4
|
+
* 返回 Quill Delta ops 数组
|
|
5
|
+
*
|
|
6
|
+
* @param text - 当前行的文本
|
|
7
|
+
* @param collectImages - 是否收集图片 URL
|
|
8
|
+
* @returns 解析结果
|
|
9
|
+
*/
|
|
10
|
+
function parseInlineFormats(text, collectImages = false) {
|
|
11
|
+
const result = [];
|
|
12
|
+
const imageUrls = [];
|
|
13
|
+
const imagePositions = [];
|
|
14
|
+
let pos = 0;
|
|
15
|
+
while (pos < text.length) {
|
|
16
|
+
// 检查图片 
|
|
17
|
+
if (text.startsWith(';
|
|
19
|
+
if (endBracket !== -1) {
|
|
20
|
+
const endParen = text.indexOf(')', endBracket + 2);
|
|
21
|
+
if (endParen !== -1) {
|
|
22
|
+
const url = text.substring(endBracket + 2, endParen);
|
|
23
|
+
if (collectImages && (isBase64Image(url) || isImageUrl(url))) {
|
|
24
|
+
imagePositions.push(result.length);
|
|
25
|
+
imageUrls.push(url);
|
|
26
|
+
}
|
|
27
|
+
result.push({ insert: { image: url } });
|
|
28
|
+
pos = endParen + 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// 检查链接 [text](url)
|
|
34
|
+
if (text.startsWith('[', pos)) {
|
|
35
|
+
const endBracket = text.indexOf('](', pos);
|
|
36
|
+
if (endBracket !== -1) {
|
|
37
|
+
const endParen = text.indexOf(')', endBracket + 2);
|
|
38
|
+
if (endParen !== -1) {
|
|
39
|
+
const linkText = text.substring(pos + 1, endBracket);
|
|
40
|
+
const url = text.substring(endBracket + 2, endParen);
|
|
41
|
+
result.push({ insert: linkText, attributes: { link: url } });
|
|
42
|
+
pos = endParen + 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// 检查粗体 **text**
|
|
48
|
+
if (text.startsWith('**', pos)) {
|
|
49
|
+
const endPos = text.indexOf('**', pos + 2);
|
|
50
|
+
if (endPos !== -1) {
|
|
51
|
+
const boldText = text.substring(pos + 2, endPos);
|
|
52
|
+
if (boldText) {
|
|
53
|
+
result.push({ insert: boldText, attributes: { bold: true } });
|
|
54
|
+
}
|
|
55
|
+
pos = endPos + 2;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// 检查删除线 ~~text~~
|
|
60
|
+
if (text.startsWith('~~', pos)) {
|
|
61
|
+
const endPos = text.indexOf('~~', pos + 2);
|
|
62
|
+
if (endPos !== -1) {
|
|
63
|
+
const strikeText = text.substring(pos + 2, endPos);
|
|
64
|
+
if (strikeText) {
|
|
65
|
+
result.push({ insert: strikeText, attributes: { strike: true } });
|
|
66
|
+
}
|
|
67
|
+
pos = endPos + 2;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// 检查下划线 <u>text</u>
|
|
72
|
+
if (text.startsWith('<u>', pos)) {
|
|
73
|
+
const endPos = text.indexOf('</u>', pos + 3);
|
|
74
|
+
if (endPos !== -1) {
|
|
75
|
+
const underlineText = text.substring(pos + 3, endPos);
|
|
76
|
+
if (underlineText) {
|
|
77
|
+
result.push({
|
|
78
|
+
insert: underlineText,
|
|
79
|
+
attributes: { underline: true },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
pos = endPos + 4;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// 检查行内代码 `code`
|
|
87
|
+
if (text.startsWith('`', pos)) {
|
|
88
|
+
const endPos = text.indexOf('`', pos + 1);
|
|
89
|
+
if (endPos !== -1) {
|
|
90
|
+
const codeText = text.substring(pos + 1, endPos);
|
|
91
|
+
if (codeText) {
|
|
92
|
+
result.push({ insert: codeText, attributes: { code: true } });
|
|
93
|
+
}
|
|
94
|
+
pos = endPos + 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 检查斜体 *text*(注意:要避免匹配到 ** 的一部分)
|
|
99
|
+
if (text.startsWith('*', pos) && !text.startsWith('**', pos)) {
|
|
100
|
+
const endPos = text.indexOf('*', pos + 1);
|
|
101
|
+
if (endPos !== -1) {
|
|
102
|
+
const italicText = text.substring(pos + 1, endPos);
|
|
103
|
+
if (italicText) {
|
|
104
|
+
result.push({ insert: italicText, attributes: { italic: true } });
|
|
105
|
+
}
|
|
106
|
+
pos = endPos + 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 普通文本 - 找到下一个格式标记
|
|
111
|
+
let nextFormatPos = text.length;
|
|
112
|
+
const imagePos = text.indexOf('![', pos);
|
|
113
|
+
const linkPos = text.indexOf('[', pos);
|
|
114
|
+
const boldPos = text.indexOf('**', pos);
|
|
115
|
+
const strikePos = text.indexOf('~~', pos);
|
|
116
|
+
const underlinePos = text.indexOf('<u>', pos);
|
|
117
|
+
const codePos = text.indexOf('`', pos);
|
|
118
|
+
const italicPos = text.indexOf('*', pos);
|
|
119
|
+
if (imagePos !== -1 && imagePos > pos) {
|
|
120
|
+
nextFormatPos = Math.min(nextFormatPos, imagePos);
|
|
121
|
+
}
|
|
122
|
+
if (linkPos !== -1 && linkPos > pos) {
|
|
123
|
+
nextFormatPos = Math.min(nextFormatPos, linkPos);
|
|
124
|
+
}
|
|
125
|
+
if (boldPos !== -1 && boldPos > pos) {
|
|
126
|
+
nextFormatPos = Math.min(nextFormatPos, boldPos);
|
|
127
|
+
}
|
|
128
|
+
if (strikePos !== -1 && strikePos > pos) {
|
|
129
|
+
nextFormatPos = Math.min(nextFormatPos, strikePos);
|
|
130
|
+
}
|
|
131
|
+
if (underlinePos !== -1 && underlinePos > pos) {
|
|
132
|
+
nextFormatPos = Math.min(nextFormatPos, underlinePos);
|
|
133
|
+
}
|
|
134
|
+
if (codePos !== -1 && codePos > pos) {
|
|
135
|
+
nextFormatPos = Math.min(nextFormatPos, codePos);
|
|
136
|
+
}
|
|
137
|
+
if (italicPos !== -1 &&
|
|
138
|
+
italicPos > pos &&
|
|
139
|
+
!text.startsWith('**', italicPos)) {
|
|
140
|
+
nextFormatPos = Math.min(nextFormatPos, italicPos);
|
|
141
|
+
}
|
|
142
|
+
if (nextFormatPos > pos) {
|
|
143
|
+
const plainText = text.substring(pos, nextFormatPos);
|
|
144
|
+
if (plainText) {
|
|
145
|
+
result.push({ insert: plainText });
|
|
146
|
+
}
|
|
147
|
+
pos = nextFormatPos;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
pos++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (collectImages) {
|
|
154
|
+
return { ops: result, imageUrls, imagePositions };
|
|
155
|
+
}
|
|
156
|
+
return { ops: result };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 将 Markdown 转换为 Quill Delta JSON 格式
|
|
160
|
+
* 支持:二级标题、三级标题、有序列表、无序列表、加粗、斜体、删除线、下划线、链接、分割线、代码块、行内代码、图片
|
|
161
|
+
*
|
|
162
|
+
* @param markdown - Markdown 格式的文本
|
|
163
|
+
* @param options - 解析器配置选项
|
|
164
|
+
* @param uploadOptions - 图片上传配置(可选)
|
|
165
|
+
* @returns Quill Delta JSON 字符串
|
|
166
|
+
*/
|
|
167
|
+
export async function markdownToQuillDelta(markdown, options = {}, uploadOptions) {
|
|
168
|
+
const delta = await markdownToQuillDeltaObject(markdown, options, uploadOptions);
|
|
169
|
+
return JSON.stringify(delta);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 将 Markdown 转换为 Quill Delta 对象(非 JSON 字符串)
|
|
173
|
+
*
|
|
174
|
+
* @param markdown - Markdown 格式的文本
|
|
175
|
+
* @param options - 解析器配置选项
|
|
176
|
+
* @param uploadOptions - 图片上传配置(可选)
|
|
177
|
+
* @returns Quill Delta 对象
|
|
178
|
+
*/
|
|
179
|
+
export async function markdownToQuillDeltaObject(markdown, options = {}, uploadOptions) {
|
|
180
|
+
const { skipH1 = true, skipHighLevelHeaders = true, preserveEmptyLines = false, } = options;
|
|
181
|
+
const ops = [];
|
|
182
|
+
const lines = markdown.split('\n');
|
|
183
|
+
let i = 0;
|
|
184
|
+
let inCodeBlock = false;
|
|
185
|
+
let codeBlockContent = '';
|
|
186
|
+
// 收集所有需要处理的图片
|
|
187
|
+
const allImageUrls = [];
|
|
188
|
+
const allImagePositions = [];
|
|
189
|
+
// 第一遍解析:收集 ops 和图片位置
|
|
190
|
+
while (i < lines.length) {
|
|
191
|
+
const line = lines[i];
|
|
192
|
+
// 处理代码块
|
|
193
|
+
if (line.startsWith('```')) {
|
|
194
|
+
if (!inCodeBlock) {
|
|
195
|
+
inCodeBlock = true;
|
|
196
|
+
codeBlockContent = '';
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
inCodeBlock = false;
|
|
200
|
+
if (codeBlockContent) {
|
|
201
|
+
ops.push({
|
|
202
|
+
insert: codeBlockContent,
|
|
203
|
+
attributes: { 'code-block': true },
|
|
204
|
+
});
|
|
205
|
+
ops.push({ insert: '\n' });
|
|
206
|
+
}
|
|
207
|
+
codeBlockContent = '';
|
|
208
|
+
}
|
|
209
|
+
i++;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (inCodeBlock) {
|
|
213
|
+
codeBlockContent += line + '\n';
|
|
214
|
+
i++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
// 处理标题
|
|
218
|
+
if (line.startsWith('# ')) {
|
|
219
|
+
if (skipH1) {
|
|
220
|
+
i++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (line.startsWith('## ')) {
|
|
225
|
+
const { ops: lineOps } = parseInlineFormats(line.substring(3));
|
|
226
|
+
ops.push(...lineOps);
|
|
227
|
+
ops.push({ insert: '\n', attributes: { header: 2 } });
|
|
228
|
+
if (i + 1 < lines.length && lines[i + 1].trim() === '') {
|
|
229
|
+
i++;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else if (line.startsWith('### ')) {
|
|
233
|
+
const { ops: lineOps } = parseInlineFormats(line.substring(4));
|
|
234
|
+
ops.push(...lineOps);
|
|
235
|
+
ops.push({ insert: '\n', attributes: { header: 3 } });
|
|
236
|
+
if (i + 1 < lines.length && lines[i + 1].trim() === '') {
|
|
237
|
+
i++;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else if (skipHighLevelHeaders &&
|
|
241
|
+
(line.startsWith('#### ') ||
|
|
242
|
+
line.startsWith('##### ') ||
|
|
243
|
+
line.startsWith('###### '))) {
|
|
244
|
+
i++;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
else if (line.startsWith('- ')) {
|
|
248
|
+
const { ops: lineOps, imageUrls, imagePositions } = parseInlineFormats(line.substring(2), true);
|
|
249
|
+
imagePositions?.forEach(pos => {
|
|
250
|
+
allImagePositions.push({ index: ops.length + pos, url: imageUrls[pos] });
|
|
251
|
+
});
|
|
252
|
+
allImageUrls.push(...(imageUrls || []));
|
|
253
|
+
ops.push(...lineOps);
|
|
254
|
+
ops.push({ insert: '\n', attributes: { list: 'bullet' } });
|
|
255
|
+
while (i + 1 < lines.length && lines[i + 1].trim() === '') {
|
|
256
|
+
i++;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if (/^\d+\./.test(line)) {
|
|
260
|
+
const match = line.match(/^\d+\.(.*)/);
|
|
261
|
+
if (match) {
|
|
262
|
+
const { ops: lineOps, imageUrls, imagePositions } = parseInlineFormats(match[1], true);
|
|
263
|
+
imagePositions?.forEach(pos => {
|
|
264
|
+
allImagePositions.push({ index: ops.length + pos, url: imageUrls[pos] });
|
|
265
|
+
});
|
|
266
|
+
allImageUrls.push(...(imageUrls || []));
|
|
267
|
+
ops.push(...lineOps);
|
|
268
|
+
ops.push({ insert: '\n', attributes: { list: 'ordered' } });
|
|
269
|
+
}
|
|
270
|
+
while (i + 1 < lines.length && lines[i + 1].trim() === '') {
|
|
271
|
+
i++;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else if (line === '---' || line === '***') {
|
|
275
|
+
ops.push({ insert: { divider: true } });
|
|
276
|
+
ops.push({ insert: '\n' });
|
|
277
|
+
}
|
|
278
|
+
else if (line.trim() === '') {
|
|
279
|
+
if (preserveEmptyLines) {
|
|
280
|
+
ops.push({ insert: '\n' });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
const { ops: lineOps, imageUrls, imagePositions } = parseInlineFormats(line, true);
|
|
285
|
+
imagePositions?.forEach(pos => {
|
|
286
|
+
allImagePositions.push({ index: ops.length + pos, url: imageUrls[pos] });
|
|
287
|
+
});
|
|
288
|
+
allImageUrls.push(...(imageUrls || []));
|
|
289
|
+
ops.push(...lineOps);
|
|
290
|
+
ops.push({ insert: '\n' });
|
|
291
|
+
}
|
|
292
|
+
i++;
|
|
293
|
+
}
|
|
294
|
+
// 第二遍:处理图片上传
|
|
295
|
+
if (allImageUrls.length > 0) {
|
|
296
|
+
// 去重,避免重复上传相同 URL
|
|
297
|
+
const uniqueUrls = [...new Set(allImageUrls)];
|
|
298
|
+
// 检查是否有 base64 但没有 token
|
|
299
|
+
if (!uploadOptions?.token) {
|
|
300
|
+
const hasBase64 = uniqueUrls.some(url => isBase64Image(url));
|
|
301
|
+
if (hasBase64) {
|
|
302
|
+
throw new Error('Base64 images must be uploaded to Mfuns server. ' +
|
|
303
|
+
'Please provide a token in uploadOptions.');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// 有 token 才上传
|
|
307
|
+
if (uploadOptions?.token) {
|
|
308
|
+
// 并发上传
|
|
309
|
+
const uploadResults = new Map();
|
|
310
|
+
const { ConcurrentProcessor } = await import('./upload.js');
|
|
311
|
+
const processor = new ConcurrentProcessor(async (url) => processImageUrl(url, uploadOptions), 3);
|
|
312
|
+
const results = await processor.process(uniqueUrls);
|
|
313
|
+
uniqueUrls.forEach((url, index) => {
|
|
314
|
+
uploadResults.set(url, results[index]);
|
|
315
|
+
});
|
|
316
|
+
// 替换所有图片 URL
|
|
317
|
+
allImagePositions.forEach(({ index, url }) => {
|
|
318
|
+
const newUrl = uploadResults.get(url);
|
|
319
|
+
if (newUrl && ops[index] && typeof ops[index].insert === 'object') {
|
|
320
|
+
ops[index].insert.image = newUrl;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return { ops };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = { parseInlineFormats, markdownToQuillDelta, markdownToQuillDeltaObject };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ParserOptions, QuillDelta, UploadOptions } from './types.mjs';
|
|
2
|
+
/**
|
|
3
|
+
* 将 Markdown 转换为 Quill Delta JSON 格式
|
|
4
|
+
* 支持:二级标题、三级标题、有序列表、无序列表、加粗、斜体、删除线、下划线、链接、分割线、代码块、行内代码、图片
|
|
5
|
+
*
|
|
6
|
+
* @param markdown - Markdown 格式的文本
|
|
7
|
+
* @param options - 解析器配置选项
|
|
8
|
+
* @param uploadOptions - 图片上传配置(可选)
|
|
9
|
+
* @returns Quill Delta JSON 字符串
|
|
10
|
+
*/
|
|
11
|
+
export declare function markdownToQuillDelta(markdown: string, options?: ParserOptions, uploadOptions?: UploadOptions): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* 将 Markdown 转换为 Quill Delta 对象(非 JSON 字符串)
|
|
14
|
+
*
|
|
15
|
+
* @param markdown - Markdown 格式的文本
|
|
16
|
+
* @param options - 解析器配置选项
|
|
17
|
+
* @param uploadOptions - 图片上传配置(可选)
|
|
18
|
+
* @returns Quill Delta 对象
|
|
19
|
+
*/
|
|
20
|
+
export declare function markdownToQuillDeltaObject(markdown: string, options?: ParserOptions, uploadOptions?: UploadOptions): Promise<QuillDelta>;
|
|
21
|
+
//# sourceMappingURL=parser.d.ts.map
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ParserOptions, QuillDelta, UploadOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* 将 Markdown 转换为 Quill Delta JSON 格式
|
|
4
|
+
* 支持:二级标题、三级标题、有序列表、无序列表、加粗、斜体、删除线、下划线、链接、分割线、代码块、行内代码、图片
|
|
5
|
+
*
|
|
6
|
+
* @param markdown - Markdown 格式的文本
|
|
7
|
+
* @param options - 解析器配置选项
|
|
8
|
+
* @param uploadOptions - 图片上传配置(可选)
|
|
9
|
+
* @returns Quill Delta JSON 字符串
|
|
10
|
+
*/
|
|
11
|
+
export declare function markdownToQuillDelta(markdown: string, options?: ParserOptions, uploadOptions?: UploadOptions): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* 将 Markdown 转换为 Quill Delta 对象(非 JSON 字符串)
|
|
14
|
+
*
|
|
15
|
+
* @param markdown - Markdown 格式的文本
|
|
16
|
+
* @param options - 解析器配置选项
|
|
17
|
+
* @param uploadOptions - 图片上传配置(可选)
|
|
18
|
+
* @returns Quill Delta 对象
|
|
19
|
+
*/
|
|
20
|
+
export declare function markdownToQuillDeltaObject(markdown: string, options?: ParserOptions, uploadOptions?: UploadOptions): Promise<QuillDelta>;
|
|
21
|
+
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,aAAa,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAuLpF;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,aAAkB,EAC3B,aAAa,CAAC,EAAE,aAAa,GAC5B,OAAO,CAAC,MAAM,CAAC,CAGjB;AAED;;;;;;;GAOG;AACH,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,aAAkB,EAC3B,aAAa,CAAC,EAAE,aAAa,GAC5B,OAAO,CAAC,UAAU,CAAC,CA0KrB"}
|