@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.
@@ -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
- if (matches.length === 0) return;
66
-
67
- const parent = textNode.parentNode;
68
- if (!parent) return;
69
-
70
- // 构建新的节点列表
71
- let lastIndex = 0;
72
- const fragments: Node[] = [];
73
-
74
- matches.forEach((m) => {
75
- // 添加匹配前的文本
76
- if (m.index > lastIndex) {
77
- fragments.push(document.createTextNode(text.slice(lastIndex, m.index)));
78
- }
79
-
80
- // 创建文档链接元素
81
- const span = document.createElement("span");
82
- span.className = "vditor-doclink";
83
- span.setAttribute("data-doc-id", m.id);
84
- span.setAttribute("data-doc-text", m.text);
85
- span.textContent = m.text;
86
-
87
- // 绑定点击事件
88
- span.onclick = (e) => {
89
- e.preventDefault();
90
- e.stopPropagation();
91
- handleDocLinkClick(m.id, m.text, vditor);
92
- };
93
- fragments.push(span);
94
-
95
- lastIndex = m.index + m.length;
96
- });
97
-
98
- // 添加剩余文本
99
- if (lastIndex < text.length) {
100
- fragments.push(document.createTextNode(text.slice(lastIndex)));
101
- }
102
-
103
- // 替换原节点
104
- fragments.forEach((frag) => {
105
- parent.insertBefore(frag, textNode);
106
- });
107
- parent.removeChild(textNode);
108
- });
109
- };
110
-
111
- /**
112
- * 处理文档链接点击事件
113
- */
114
- export const handleDocLinkClick = (docId: string, text: string, vditor?: any): void => {
115
- // 优先使用配置中的 click 回调
116
- if (vditor?.options?.citation?.click) {
117
- vditor.options.citation.click(docId, text);
118
- return;
119
- }
120
-
121
- // 使用全局回调(兼容旧版本)
122
- if (window.handleDocLinkClick) {
123
- window.handleDocLinkClick(docId);
124
- return;
125
- }
126
-
127
- // 配置了 URL,自动请求
128
- const url = vditor?.options?.citation?.url;
129
- if (url) {
130
- fetch(`${url}/${docId}`)
131
- .then(r => r.json())
132
- .then(data => {
133
- console.log('Citation data:', data);
134
- })
135
- .catch(err => {
136
- console.error('Failed to fetch citation:', err);
137
- });
138
- }
139
- };
140
-
141
- /**
142
- * 从 Markdown 文本中提取文档链接
143
- */
144
- export const extractDocLinks = (markdown: string): Array<{ id: string; text: string }> => {
145
- const links: Array<{ id: string; text: string }> = [];
146
- const regex = new RegExp(DOCLINK_REGEX.source, "g");
147
- let match: RegExpExecArray | null;
148
-
149
- while ((match = regex.exec(markdown)) !== null) {
150
- links.push({
151
- id: match[1],
152
- text: match[2]
153
- });
154
- }
155
-
156
- return links;
157
- };
158
-
159
- /**
160
- * 检查文本是否包含文档链接语法
161
- */
162
- export const hasDocLink = (text: string): boolean => {
163
- return DOCLINK_REGEX.test(text);
164
- };
165
-
166
- // 扩展 Window 接口
167
- declare global {
168
- interface Window {
169
- handleDocLinkClick?: (docId: string) => void;
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
- export const getMarkdown = (vditor: IVditor) => {
4
- if (vditor.currentMode === "sv") {
5
- return code160to32(`${vditor.sv.element.textContent}\n`.replace(/\n\n$/, "\n"));
6
- } else if (vditor.currentMode === "wysiwyg") {
7
- return vditor.lute.VditorDOM2Md(vditor.wysiwyg.element.innerHTML);
8
- } else if (vditor.currentMode === "ir") {
9
- return vditor.lute.VditorIRDOM2Md(vditor.ir.element.innerHTML);
10
- }
11
- return "";
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
+ };
@@ -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
  }