@yltrcc/vditor 0.0.4 → 0.0.6
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/dist/index.css +2 -2
- package/dist/index.js +96 -13
- package/dist/index.min.js +1 -1
- package/dist/method.js +3 -3
- package/dist/method.min.js +1 -1
- package/dist/ts/markdown/docLink.d.ts +5 -0
- package/package.json +1 -1
- package/src/ts/markdown/docLink.ts +202 -171
- package/src/ts/markdown/getMarkdown.ts +60 -12
- package/src/ts/wysiwyg/index.ts +11 -0
|
@@ -1,171 +1,202 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 文档链接支持
|
|
3
|
-
* 将 ((文档ID '显示文本')) 语法渲染为可点击的文档链接
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// 文档链接正则表达式
|
|
7
|
-
const DOCLINK_REGEX = /\(\((\d+)\s+'([^']+)'\)\)/g;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 在 HTML 中渲染文档链接
|
|
11
|
-
* 将 ((id 'text')) 转换为可点击的 span 元素
|
|
12
|
-
*/
|
|
13
|
-
export const renderDocLink = (html: string, vditor?: any): string => {
|
|
14
|
-
// 如果禁用了 citation,直接返回原文
|
|
15
|
-
if (vditor?.options?.citation?.enable === false) {
|
|
16
|
-
return html;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return html.replace(
|
|
20
|
-
DOCLINK_REGEX,
|
|
21
|
-
'<span class="vditor-doclink" data-doc-id="$1" data-doc-text="$2">$2</span>'
|
|
22
|
-
);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 在 WYSIWYG 编辑器中处理文档链接的实时渲染
|
|
27
|
-
*/
|
|
28
|
-
export const processDocLinkInWYSIWYG = (element: HTMLElement, vditor?: any): void => {
|
|
29
|
-
// 如果禁用了 citation,不处理
|
|
30
|
-
if (vditor?.options?.citation?.enable === false) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const walker = document.createTreeWalker(
|
|
35
|
-
element,
|
|
36
|
-
NodeFilter.SHOW_TEXT,
|
|
37
|
-
null
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const textNodes: Text[] = [];
|
|
41
|
-
let node: Node | null;
|
|
42
|
-
while ((node = walker.nextNode())) {
|
|
43
|
-
// 排除已经在 vditor-doclink 中的文本节点
|
|
44
|
-
if (!(node.parentElement?.classList.contains("vditor-doclink"))) {
|
|
45
|
-
textNodes.push(node as Text);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
textNodes.forEach((textNode) => {
|
|
50
|
-
const text = textNode.textContent || "";
|
|
51
|
-
const matches: Array<{ index: number; length: number; id: string; text: string }> = [];
|
|
52
|
-
|
|
53
|
-
// 查找所有匹配
|
|
54
|
-
let match: RegExpExecArray | null;
|
|
55
|
-
const regex = new RegExp(DOCLINK_REGEX.source, "g");
|
|
56
|
-
while ((match = regex.exec(text)) !== null) {
|
|
57
|
-
matches.push({
|
|
58
|
-
index: match.index,
|
|
59
|
-
length: match[0].length,
|
|
60
|
-
id: match[1],
|
|
61
|
-
text: match[2]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 文档链接支持
|
|
3
|
+
* 将 ((文档ID '显示文本')) 语法渲染为可点击的文档链接
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// 文档链接正则表达式
|
|
7
|
+
const DOCLINK_REGEX = /\(\((\d+)\s+'([^']+)'\)\)/g;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 在 HTML 中渲染文档链接
|
|
11
|
+
* 将 ((id 'text')) 转换为可点击的 span 元素
|
|
12
|
+
*/
|
|
13
|
+
export const renderDocLink = (html: string, vditor?: any): string => {
|
|
14
|
+
// 如果禁用了 citation,直接返回原文
|
|
15
|
+
if (vditor?.options?.citation?.enable === false) {
|
|
16
|
+
return html;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return html.replace(
|
|
20
|
+
DOCLINK_REGEX,
|
|
21
|
+
'<span class="vditor-doclink" data-doc-id="$1" data-doc-text="$2" data-doc-markdown="(($1 \'$2\'))">$2</span>'
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 在 WYSIWYG 编辑器中处理文档链接的实时渲染
|
|
27
|
+
*/
|
|
28
|
+
export const processDocLinkInWYSIWYG = (element: HTMLElement, vditor?: any): void => {
|
|
29
|
+
// 如果禁用了 citation,不处理
|
|
30
|
+
if (vditor?.options?.citation?.enable === false) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const walker = document.createTreeWalker(
|
|
35
|
+
element,
|
|
36
|
+
NodeFilter.SHOW_TEXT,
|
|
37
|
+
null
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const textNodes: Text[] = [];
|
|
41
|
+
let node: Node | null;
|
|
42
|
+
while ((node = walker.nextNode())) {
|
|
43
|
+
// 排除已经在 vditor-doclink 中的文本节点
|
|
44
|
+
if (!(node.parentElement?.classList.contains("vditor-doclink"))) {
|
|
45
|
+
textNodes.push(node as Text);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
textNodes.forEach((textNode) => {
|
|
50
|
+
const text = textNode.textContent || "";
|
|
51
|
+
const matches: Array<{ index: number; length: number; id: string; text: string; fullMatch: string }> = [];
|
|
52
|
+
|
|
53
|
+
// 查找所有匹配
|
|
54
|
+
let match: RegExpExecArray | null;
|
|
55
|
+
const regex = new RegExp(DOCLINK_REGEX.source, "g");
|
|
56
|
+
while ((match = regex.exec(text)) !== null) {
|
|
57
|
+
matches.push({
|
|
58
|
+
index: match.index,
|
|
59
|
+
length: match[0].length,
|
|
60
|
+
id: match[1],
|
|
61
|
+
text: match[2],
|
|
62
|
+
fullMatch: match[0]
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (matches.length === 0) return;
|
|
67
|
+
|
|
68
|
+
const parent = textNode.parentNode;
|
|
69
|
+
if (!parent) return;
|
|
70
|
+
|
|
71
|
+
// 构建新的节点列表
|
|
72
|
+
let lastIndex = 0;
|
|
73
|
+
const fragments: Node[] = [];
|
|
74
|
+
|
|
75
|
+
matches.forEach((m) => {
|
|
76
|
+
// 添加匹配前的文本
|
|
77
|
+
if (m.index > lastIndex) {
|
|
78
|
+
fragments.push(document.createTextNode(text.slice(lastIndex, m.index)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 创建文档链接容器 - 使用 contenteditable="false" 防止编辑时破坏结构
|
|
82
|
+
const container = document.createElement("span");
|
|
83
|
+
container.className = "vditor-doclink-wrapper";
|
|
84
|
+
container.setAttribute("contenteditable", "false");
|
|
85
|
+
container.setAttribute("data-doc-id", m.id);
|
|
86
|
+
container.setAttribute("data-doc-text", m.text);
|
|
87
|
+
// 存储完整的 Markdown 语法,用于复制
|
|
88
|
+
container.setAttribute("data-doc-markdown", m.fullMatch);
|
|
89
|
+
|
|
90
|
+
// 创建显示文本的元素
|
|
91
|
+
const span = document.createElement("span");
|
|
92
|
+
span.className = "vditor-doclink";
|
|
93
|
+
span.textContent = m.text;
|
|
94
|
+
|
|
95
|
+
// 绑定点击事件
|
|
96
|
+
container.onclick = (e) => {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
handleDocLinkClick(m.id, m.text, vditor);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
container.appendChild(span);
|
|
103
|
+
fragments.push(container);
|
|
104
|
+
|
|
105
|
+
lastIndex = m.index + m.length;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// 添加剩余文本
|
|
109
|
+
if (lastIndex < text.length) {
|
|
110
|
+
fragments.push(document.createTextNode(text.slice(lastIndex)));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 替换原节点
|
|
114
|
+
fragments.forEach((frag) => {
|
|
115
|
+
parent.insertBefore(frag, textNode);
|
|
116
|
+
});
|
|
117
|
+
parent.removeChild(textNode);
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 处理文档链接点击事件
|
|
123
|
+
*/
|
|
124
|
+
export const handleDocLinkClick = (docId: string, text: string, vditor?: any): void => {
|
|
125
|
+
// 优先使用配置中的 click 回调
|
|
126
|
+
if (vditor?.options?.citation?.click) {
|
|
127
|
+
vditor.options.citation.click(docId, text);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 使用全局回调(兼容旧版本)
|
|
132
|
+
if (window.handleDocLinkClick) {
|
|
133
|
+
window.handleDocLinkClick(docId);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 配置了 URL,自动请求
|
|
138
|
+
const url = vditor?.options?.citation?.url;
|
|
139
|
+
if (url) {
|
|
140
|
+
fetch(`${url}/${docId}`)
|
|
141
|
+
.then(r => r.json())
|
|
142
|
+
.then(data => {
|
|
143
|
+
console.log('Citation data:', data);
|
|
144
|
+
})
|
|
145
|
+
.catch(err => {
|
|
146
|
+
console.error('Failed to fetch citation:', err);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 从 Markdown 文本中提取文档链接
|
|
153
|
+
*/
|
|
154
|
+
export const extractDocLinks = (markdown: string): Array<{ id: string; text: string }> => {
|
|
155
|
+
const links: Array<{ id: string; text: string }> = [];
|
|
156
|
+
const regex = new RegExp(DOCLINK_REGEX.source, "g");
|
|
157
|
+
let match: RegExpExecArray | null;
|
|
158
|
+
|
|
159
|
+
while ((match = regex.exec(markdown)) !== null) {
|
|
160
|
+
links.push({
|
|
161
|
+
id: match[1],
|
|
162
|
+
text: match[2]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return links;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 检查文本是否包含文档链接语法
|
|
171
|
+
*/
|
|
172
|
+
export const hasDocLink = (text: string): boolean => {
|
|
173
|
+
return DOCLINK_REGEX.test(text);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 获取文档链接的 Markdown 语法
|
|
178
|
+
* 用于复制操作时恢复原始语法
|
|
179
|
+
*/
|
|
180
|
+
export const getDocLinkMarkdown = (element: HTMLElement): string | null => {
|
|
181
|
+
// 优先从 data-doc-markdown 属性获取
|
|
182
|
+
const markdown = element.getAttribute("data-doc-markdown");
|
|
183
|
+
if (markdown) {
|
|
184
|
+
return markdown;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 从 data-doc-id 和 data-doc-text 重建
|
|
188
|
+
const id = element.getAttribute("data-doc-id");
|
|
189
|
+
const text = element.getAttribute("data-doc-text");
|
|
190
|
+
if (id && text) {
|
|
191
|
+
return `((${id} '${text}'))`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// 扩展 Window 接口
|
|
198
|
+
declare global {
|
|
199
|
+
interface Window {
|
|
200
|
+
handleDocLinkClick?: (docId: string) => void;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -1,12 +1,60 @@
|
|
|
1
|
-
import {code160to32} from "../util/code160to32";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import {code160to32} from "../util/code160to32";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 将 doclink DOM 元素转换回 Markdown 语法
|
|
5
|
+
* 处理 <span class="vditor-doclink-wrapper" data-doc-markdown="((id 'text'))">text</span>
|
|
6
|
+
*/
|
|
7
|
+
const processDocLinksInHTML = (html: string): string => {
|
|
8
|
+
// 创建临时 DOM 元素来解析 HTML
|
|
9
|
+
const tempDiv = document.createElement("div");
|
|
10
|
+
tempDiv.innerHTML = html;
|
|
11
|
+
|
|
12
|
+
// 查找所有 doclink wrapper 元素
|
|
13
|
+
const docLinkWrappers = tempDiv.querySelectorAll(".vditor-doclink-wrapper");
|
|
14
|
+
docLinkWrappers.forEach((wrapper) => {
|
|
15
|
+
const markdown = wrapper.getAttribute("data-doc-markdown");
|
|
16
|
+
if (markdown) {
|
|
17
|
+
// 用文本节点替换整个 wrapper,内容为 Markdown 语法
|
|
18
|
+
wrapper.replaceWith(document.createTextNode(markdown));
|
|
19
|
+
} else {
|
|
20
|
+
// 如果没有 data-doc-markdown,尝试从 data-doc-id 和 data-doc-text 重建
|
|
21
|
+
const id = wrapper.getAttribute("data-doc-id");
|
|
22
|
+
const text = wrapper.getAttribute("data-doc-text");
|
|
23
|
+
if (id && text) {
|
|
24
|
+
wrapper.replaceWith(document.createTextNode(`((${id} '${text}'))`));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// 也处理旧的 .vditor-doclink 元素(没有 wrapper 的情况)
|
|
30
|
+
const docLinks = tempDiv.querySelectorAll(".vditor-doclink");
|
|
31
|
+
docLinks.forEach((link) => {
|
|
32
|
+
const markdown = link.getAttribute("data-doc-markdown");
|
|
33
|
+
if (markdown) {
|
|
34
|
+
link.replaceWith(document.createTextNode(markdown));
|
|
35
|
+
} else {
|
|
36
|
+
const id = link.getAttribute("data-doc-id");
|
|
37
|
+
const text = link.getAttribute("data-doc-text");
|
|
38
|
+
if (id && text) {
|
|
39
|
+
link.replaceWith(document.createTextNode(`((${id} '${text}'))`));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return tempDiv.innerHTML;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const getMarkdown = (vditor: IVditor) => {
|
|
48
|
+
if (vditor.currentMode === "sv") {
|
|
49
|
+
return code160to32(`${vditor.sv.element.textContent}\n`.replace(/\n\n$/, "\n"));
|
|
50
|
+
} else if (vditor.currentMode === "wysiwyg") {
|
|
51
|
+
// 先处理 doclink 元素,将其转换回 Markdown 语法
|
|
52
|
+
const processedHTML = processDocLinksInHTML(vditor.wysiwyg.element.innerHTML);
|
|
53
|
+
return vditor.lute.VditorDOM2Md(processedHTML);
|
|
54
|
+
} else if (vditor.currentMode === "ir") {
|
|
55
|
+
// 同样处理 IR 模式
|
|
56
|
+
const processedHTML = processDocLinksInHTML(vditor.ir.element.innerHTML);
|
|
57
|
+
return vditor.lute.VditorIRDOM2Md(processedHTML);
|
|
58
|
+
}
|
|
59
|
+
return "";
|
|
60
|
+
};
|
package/src/ts/wysiwyg/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {Constants} from "../constants";
|
|
2
|
+
import {getDocLinkMarkdown} from "../markdown/docLink";
|
|
2
3
|
import {hidePanel} from "../toolbar/setToolbar";
|
|
3
4
|
import {isCtrl, isFirefox} from "../util/compatibility";
|
|
4
5
|
import {
|
|
@@ -239,6 +240,16 @@ class WYSIWYG {
|
|
|
239
240
|
const tempElement = document.createElement("div");
|
|
240
241
|
tempElement.appendChild(range.cloneContents());
|
|
241
242
|
|
|
243
|
+
// 处理文档链接的复制 - 将渲染后的元素替换为原始 Markdown 语法
|
|
244
|
+
const docLinkElements = tempElement.querySelectorAll(".vditor-doclink-wrapper, .vditor-doclink");
|
|
245
|
+
docLinkElements.forEach((el) => {
|
|
246
|
+
const markdown = getDocLinkMarkdown(el as HTMLElement);
|
|
247
|
+
if (markdown) {
|
|
248
|
+
// 用文本节点替换元素,内容为 Markdown 语法
|
|
249
|
+
el.replaceWith(document.createTextNode(markdown));
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
242
253
|
event.clipboardData.setData("text/plain", vditor.lute.VditorDOM2Md(tempElement.innerHTML).trim());
|
|
243
254
|
event.clipboardData.setData("text/html", "");
|
|
244
255
|
}
|