Skip to content

Commit 5cac571

Browse files
committed
feat: 更宽松的非标语法(图片连写)
1 parent 28eaf21 commit 5cac571

6 files changed

Lines changed: 360 additions & 25 deletions

File tree

src/core/parser/index.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ const BLOCK_PATTERNS = {
148148
html_block_start: /^<([a-zA-Z][a-zA-Z0-9]*)/, // 以 < 开头后跟标签名
149149
};
150150

151+
const IMAGE_TOKEN_PATTERNS = {
152+
linked: /\[!\[([^\]]*)\]\((.+?)(?:\s+"([^"]*)")?\)\]\((.+?)(?:\s+"([^"]*)")?\)/y,
153+
normal: /!\[([^\]]*)\]\((.+?)(?:\s+"([^"]*)")?\)/y,
154+
};
155+
156+
function generateConsecutiveImageGroupId(): string {
157+
return `cig_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
158+
}
159+
151160
/** 行内元素集合 - 这些标签不应被解析为块级 html_block */
152161
const INLINE_ELEMENTS = new Set([
153162
"a",
@@ -284,6 +293,14 @@ export class MarkdownParser {
284293
continue;
285294
}
286295

296+
// 连续图片(非标准 Markdown,但主流编辑器普遍支持)
297+
const consecutiveImages = this.parseConsecutiveImages(line);
298+
if (consecutiveImages) {
299+
blocks.push(...consecutiveImages);
300+
i++;
301+
continue;
302+
}
303+
287304
// 链接图片 [![alt](src)](href)
288305
const linkedImageMatch = line.match(BLOCK_PATTERNS.linked_image);
289306
if (linkedImageMatch) {
@@ -418,6 +435,60 @@ export class MarkdownParser {
418435
return this.schema.node("image", { src, alt, title, linkHref, linkTitle });
419436
}
420437

438+
/**
439+
* 解析同一行上的连续图片
440+
* 例如:![a](1.png)![b](2.png)
441+
*/
442+
private parseConsecutiveImages(line: string): Node[] | null {
443+
const images: Node[] = [];
444+
let index = 0;
445+
const groupId = generateConsecutiveImageGroupId();
446+
447+
while (index < line.length) {
448+
while (index < line.length && /\s/.test(line[index])) {
449+
index++;
450+
}
451+
452+
if (index >= line.length) break;
453+
454+
IMAGE_TOKEN_PATTERNS.linked.lastIndex = index;
455+
let match = IMAGE_TOKEN_PATTERNS.linked.exec(line);
456+
if (match) {
457+
images.push(
458+
this.schema.node("image", {
459+
alt: match[1] || "",
460+
src: match[2] || "",
461+
title: match[3] || "",
462+
linkHref: match[4] || "",
463+
linkTitle: match[5] || "",
464+
consecutiveGroup: groupId,
465+
})
466+
);
467+
index = IMAGE_TOKEN_PATTERNS.linked.lastIndex;
468+
continue;
469+
}
470+
471+
IMAGE_TOKEN_PATTERNS.normal.lastIndex = index;
472+
match = IMAGE_TOKEN_PATTERNS.normal.exec(line);
473+
if (match) {
474+
images.push(
475+
this.schema.node("image", {
476+
alt: match[1] || "",
477+
src: match[2] || "",
478+
title: match[3] || "",
479+
consecutiveGroup: groupId,
480+
})
481+
);
482+
index = IMAGE_TOKEN_PATTERNS.normal.lastIndex;
483+
continue;
484+
}
485+
486+
return null;
487+
}
488+
489+
return images.length >= 2 ? images : null;
490+
}
491+
421492
/**
422493
* 解析段落
423494
*/

src/core/plugins/source-view-transform.ts

Lines changed: 165 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,32 @@ function generateHtmlBlockId(): string {
3737
return `hb_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
3838
}
3939

40+
function generateConsecutiveImageGroupId(): string {
41+
return `cig_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
42+
}
43+
44+
function buildImageMarkdown(image: ProseMirrorNode): string {
45+
const alt = image.attrs.alt || "";
46+
const src = image.attrs.src || "";
47+
const title = image.attrs.title || "";
48+
const linkHref = image.attrs.linkHref || "";
49+
const linkTitle = image.attrs.linkTitle || "";
50+
const titlePart = title ? ` "${title}"` : "";
51+
const imgMarkdown = `![${alt}](${src}${titlePart})`;
52+
53+
if (linkHref) {
54+
const linkTitlePart = linkTitle ? ` "${linkTitle}"` : "";
55+
return `[${imgMarkdown}](${linkHref}${linkTitlePart})`;
56+
}
57+
58+
return imgMarkdown;
59+
}
60+
61+
const IMAGE_TOKEN_PATTERNS = {
62+
linked: /\[!\[([^\]]*)\]\((.+?)(?:\s+"([^"]*)")?\)\]\((.+?)(?:\s+"([^"]*)")?\)/y,
63+
normal: /!\[([^\]]*)\]\((.+?)(?:\s+"([^"]*)")?\)/y,
64+
};
65+
4066
/**
4167
* 将代码块转换为多个段落节点
4268
*/
@@ -121,22 +147,22 @@ function transformImageToParagraph(image: ProseMirrorNode, schema: Schema): Pros
121147
const title = image.attrs.title || "";
122148
const linkHref = image.attrs.linkHref || "";
123149
const linkTitle = image.attrs.linkTitle || "";
124-
const titlePart = title ? ` "${title}"` : "";
125-
const imgMarkdown = `![${alt}](${src}${titlePart})`;
126-
let markdownText: string;
127-
if (linkHref) {
128-
const linkTitlePart = linkTitle ? ` "${linkTitle}"` : "";
129-
markdownText = `[${imgMarkdown}](${linkHref}${linkTitlePart})`;
130-
} else {
131-
markdownText = imgMarkdown;
132-
}
133-
134150
return schema.nodes.paragraph.create(
135151
{ imageAttrs: { src, alt, title, linkHref, linkTitle } },
136-
schema.text(markdownText)
152+
schema.text(buildImageMarkdown(image))
137153
);
138154
}
139155

156+
function transformImageGroupToParagraph(
157+
images: ProseMirrorNode[],
158+
schema: Schema
159+
): ProseMirrorNode | null {
160+
if (images.length === 0) return null;
161+
162+
const markdownText = images.map((image) => buildImageMarkdown(image)).join("");
163+
return schema.nodes.paragraph.create({ imageGroupSource: true }, schema.text(markdownText));
164+
}
165+
140166
/**
141167
* 将图片段落节点转换回图片节点
142168
*/
@@ -178,6 +204,62 @@ function transformParagraphToImage(
178204
return null;
179205
}
180206

207+
function transformParagraphToImages(
208+
paragraph: ProseMirrorNode,
209+
schema: Schema
210+
): ProseMirrorNode[] | null {
211+
if (!paragraph.attrs.imageGroupSource) return null;
212+
213+
const images: ProseMirrorNode[] = [];
214+
const text = paragraph.textContent;
215+
let index = 0;
216+
const groupId = generateConsecutiveImageGroupId();
217+
218+
while (index < text.length) {
219+
while (index < text.length && /\s/.test(text[index])) {
220+
index++;
221+
}
222+
223+
if (index >= text.length) break;
224+
225+
IMAGE_TOKEN_PATTERNS.linked.lastIndex = index;
226+
let match = IMAGE_TOKEN_PATTERNS.linked.exec(text);
227+
if (match) {
228+
images.push(
229+
schema.nodes.image.create({
230+
alt: match[1] || "",
231+
src: match[2] || "",
232+
title: match[3] || "",
233+
linkHref: match[4] || "",
234+
linkTitle: match[5] || "",
235+
consecutiveGroup: groupId,
236+
})
237+
);
238+
index = IMAGE_TOKEN_PATTERNS.linked.lastIndex;
239+
continue;
240+
}
241+
242+
IMAGE_TOKEN_PATTERNS.normal.lastIndex = index;
243+
match = IMAGE_TOKEN_PATTERNS.normal.exec(text);
244+
if (match) {
245+
images.push(
246+
schema.nodes.image.create({
247+
alt: match[1] || "",
248+
src: match[2] || "",
249+
title: match[3] || "",
250+
consecutiveGroup: groupId,
251+
})
252+
);
253+
index = IMAGE_TOKEN_PATTERNS.normal.lastIndex;
254+
continue;
255+
}
256+
257+
return null;
258+
}
259+
260+
return images.length > 0 ? images : null;
261+
}
262+
181263
/**
182264
* 将分割线节点转换为段落节点
183265
*/
@@ -522,9 +604,39 @@ function processNodeForSourceConversion(
522604
// 递归处理子节点
523605
if (node.content.size > 0) {
524606
const newChildren: ProseMirrorNode[] = [];
607+
let consecutiveImageGroup: ProseMirrorNode[] = [];
608+
let currentConsecutiveImageGroupId: string | null = null;
525609
let changed = false;
526610

611+
const flushConsecutiveImageGroup = () => {
612+
if (consecutiveImageGroup.length === 0) return;
613+
const paragraph = transformImageGroupToParagraph(consecutiveImageGroup, schema);
614+
if (paragraph) {
615+
newChildren.push(paragraph);
616+
changed = true;
617+
} else {
618+
consecutiveImageGroup.forEach((image) =>
619+
newChildren.push(transformImageToParagraph(image, schema))
620+
);
621+
changed = true;
622+
}
623+
consecutiveImageGroup = [];
624+
currentConsecutiveImageGroupId = null;
625+
};
626+
527627
node.content.forEach((child) => {
628+
if (child.type.name === "image" && child.attrs.consecutiveGroup) {
629+
const groupId = child.attrs.consecutiveGroup as string;
630+
if (currentConsecutiveImageGroupId && currentConsecutiveImageGroupId !== groupId) {
631+
flushConsecutiveImageGroup();
632+
}
633+
currentConsecutiveImageGroupId = groupId;
634+
consecutiveImageGroup.push(child);
635+
return;
636+
}
637+
638+
flushConsecutiveImageGroup();
639+
528640
const processed = processNodeForSourceConversion(child, schema);
529641
if (Array.isArray(processed)) {
530642
newChildren.push(...processed);
@@ -537,6 +649,8 @@ function processNodeForSourceConversion(
537649
}
538650
});
539651

652+
flushConsecutiveImageGroup();
653+
540654
if (changed) {
541655
return node.type.create(node.attrs, Fragment.from(newChildren), node.marks);
542656
}
@@ -553,9 +667,39 @@ export function convertBlocksToParagraphs(tr: Transaction): Transaction {
553667
const doc = tr.doc;
554668
const schema = doc.type.schema;
555669
const newContent: ProseMirrorNode[] = [];
670+
let consecutiveImageGroup: ProseMirrorNode[] = [];
671+
let currentConsecutiveImageGroupId: string | null = null;
556672
let changed = false;
557673

674+
const flushConsecutiveImageGroup = () => {
675+
if (consecutiveImageGroup.length === 0) return;
676+
const paragraph = transformImageGroupToParagraph(consecutiveImageGroup, schema);
677+
if (paragraph) {
678+
newContent.push(paragraph);
679+
changed = true;
680+
} else {
681+
consecutiveImageGroup.forEach((image) =>
682+
newContent.push(transformImageToParagraph(image, schema))
683+
);
684+
changed = true;
685+
}
686+
consecutiveImageGroup = [];
687+
currentConsecutiveImageGroupId = null;
688+
};
689+
558690
doc.forEach((node) => {
691+
if (node.type.name === "image" && node.attrs.consecutiveGroup) {
692+
const groupId = node.attrs.consecutiveGroup as string;
693+
if (currentConsecutiveImageGroupId && currentConsecutiveImageGroupId !== groupId) {
694+
flushConsecutiveImageGroup();
695+
}
696+
currentConsecutiveImageGroupId = groupId;
697+
consecutiveImageGroup.push(node);
698+
return;
699+
}
700+
701+
flushConsecutiveImageGroup();
702+
559703
const processed = processNodeForSourceConversion(node, schema);
560704
if (Array.isArray(processed)) {
561705
newContent.push(...processed);
@@ -566,6 +710,8 @@ export function convertBlocksToParagraphs(tr: Transaction): Transaction {
566710
}
567711
});
568712

713+
flushConsecutiveImageGroup();
714+
569715
if (changed && newContent.length > 0) {
570716
const step = new ReplaceStep(0, doc.content.size, new Slice(Fragment.from(newContent), 0, 0));
571717
tr.step(step);
@@ -745,7 +891,10 @@ function processNodeForBlockConversion(
745891
flushMathBlockGroup();
746892
flushListGroup();
747893

748-
if (child.attrs.imageAttrs) {
894+
if (child.attrs.imageGroupSource) {
895+
const images = transformParagraphToImages(child, schema);
896+
newChildren.push(...(images || [child]));
897+
} else if (child.attrs.imageAttrs) {
749898
const image = transformParagraphToImage(child, schema);
750899
newChildren.push(image || child);
751900
} else if (child.attrs.hrSource) {
@@ -963,7 +1112,10 @@ export function convertParagraphsToBlocks(tr: Transaction): Transaction {
9631112
flushMathBlockGroup();
9641113
flushListGroup();
9651114

966-
if (node.attrs.imageAttrs) {
1115+
if (node.attrs.imageGroupSource) {
1116+
const images = transformParagraphToImages(node, schema);
1117+
newContent.push(...(images || [node]));
1118+
} else if (node.attrs.imageAttrs) {
9671119
// 图片段落
9681120
const image = transformParagraphToImage(node, schema);
9691121
newContent.push(image || node);

0 commit comments

Comments
 (0)