mardora 1.2.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 +21 -0
- package/README.md +113 -0
- package/dist/chunk-3OCUX4OO.js +7690 -0
- package/dist/chunk-3OCUX4OO.js.map +1 -0
- package/dist/chunk-3ZOCCFDL.cjs +74 -0
- package/dist/chunk-3ZOCCFDL.cjs.map +1 -0
- package/dist/chunk-7JOEPNEV.cjs +7740 -0
- package/dist/chunk-7JOEPNEV.cjs.map +1 -0
- package/dist/chunk-BIKZQZ6W.js +33 -0
- package/dist/chunk-BIKZQZ6W.js.map +1 -0
- package/dist/chunk-EQJESPP2.js +234 -0
- package/dist/chunk-EQJESPP2.js.map +1 -0
- package/dist/chunk-G4SE26YY.js +70 -0
- package/dist/chunk-G4SE26YY.js.map +1 -0
- package/dist/chunk-KNDWF2DP.cjs +35 -0
- package/dist/chunk-KNDWF2DP.cjs.map +1 -0
- package/dist/chunk-MLBEBFHB.cjs +2971 -0
- package/dist/chunk-MLBEBFHB.cjs.map +1 -0
- package/dist/chunk-P7JFCYU3.js +905 -0
- package/dist/chunk-P7JFCYU3.js.map +1 -0
- package/dist/chunk-SWFUKJDO.cjs +243 -0
- package/dist/chunk-SWFUKJDO.cjs.map +1 -0
- package/dist/chunk-WFVCG4LD.cjs +926 -0
- package/dist/chunk-WFVCG4LD.cjs.map +1 -0
- package/dist/chunk-XL6WFGJT.js +2901 -0
- package/dist/chunk-XL6WFGJT.js.map +1 -0
- package/dist/editor/index.cjs +277 -0
- package/dist/editor/index.cjs.map +1 -0
- package/dist/editor/index.d.cts +186 -0
- package/dist/editor/index.d.ts +186 -0
- package/dist/editor/index.js +4 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index.cjs +405 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/index.cjs +12 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.d.cts +16 -0
- package/dist/lib/index.d.ts +16 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/mardora-DCwjomil.d.cts +640 -0
- package/dist/mardora-DCwjomil.d.ts +640 -0
- package/dist/plugins/index.cjs +104 -0
- package/dist/plugins/index.cjs.map +1 -0
- package/dist/plugins/index.d.cts +740 -0
- package/dist/plugins/index.d.ts +740 -0
- package/dist/plugins/index.js +7 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/preview/index.cjs +38 -0
- package/dist/preview/index.cjs.map +1 -0
- package/dist/preview/index.d.cts +101 -0
- package/dist/preview/index.d.ts +101 -0
- package/dist/preview/index.js +5 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/types-NBsaxl4d.d.cts +71 -0
- package/dist/types-Pw2SWWAR.d.ts +71 -0
- package/package.json +92 -0
- package/src/editor/attachments/extension.ts +181 -0
- package/src/editor/attachments/format.ts +63 -0
- package/src/editor/attachments/index.ts +3 -0
- package/src/editor/attachments/types.ts +37 -0
- package/src/editor/heading-fold/config.ts +25 -0
- package/src/editor/heading-fold/extension.ts +268 -0
- package/src/editor/heading-fold/extract.ts +88 -0
- package/src/editor/heading-fold/index.ts +5 -0
- package/src/editor/heading-fold/theme.ts +85 -0
- package/src/editor/heading-fold/types.ts +24 -0
- package/src/editor/i18n.ts +13 -0
- package/src/editor/icons/index.ts +367 -0
- package/src/editor/index.ts +16 -0
- package/src/editor/mardora.ts +257 -0
- package/src/editor/media-lightbox-theme.ts +146 -0
- package/src/editor/media-lightbox.ts +125 -0
- package/src/editor/plugin.ts +294 -0
- package/src/editor/selection-toolbar/activation.ts +123 -0
- package/src/editor/selection-toolbar/commands.ts +279 -0
- package/src/editor/selection-toolbar/extension.ts +564 -0
- package/src/editor/selection-toolbar/i18n.ts +164 -0
- package/src/editor/selection-toolbar/index.ts +7 -0
- package/src/editor/selection-toolbar/menu.ts +252 -0
- package/src/editor/selection-toolbar/position.ts +43 -0
- package/src/editor/selection-toolbar/theme.ts +195 -0
- package/src/editor/selection-toolbar/types.ts +155 -0
- package/src/editor/slash/default-commands.ts +190 -0
- package/src/editor/slash/extension.ts +319 -0
- package/src/editor/slash/index.ts +7 -0
- package/src/editor/slash/insertions.ts +26 -0
- package/src/editor/slash/menu.ts +123 -0
- package/src/editor/slash/position.ts +61 -0
- package/src/editor/slash/query.ts +33 -0
- package/src/editor/slash/theme.ts +113 -0
- package/src/editor/slash/types.ts +40 -0
- package/src/editor/table-of-contents/extension.ts +202 -0
- package/src/editor/table-of-contents/extract.ts +53 -0
- package/src/editor/table-of-contents/index.ts +7 -0
- package/src/editor/table-of-contents/panel.ts +83 -0
- package/src/editor/table-of-contents/slug.ts +50 -0
- package/src/editor/table-of-contents/storage.ts +35 -0
- package/src/editor/table-of-contents/theme.ts +153 -0
- package/src/editor/table-of-contents/types.ts +44 -0
- package/src/editor/theme.ts +72 -0
- package/src/editor/utils.ts +176 -0
- package/src/editor/view-plugin.ts +189 -0
- package/src/index.ts +5 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/input-handler.ts +47 -0
- package/src/plugins/code-plugin.theme.ts +545 -0
- package/src/plugins/code-plugin.ts +1892 -0
- package/src/plugins/emoji-plugin.ts +140 -0
- package/src/plugins/heading-plugin.ts +194 -0
- package/src/plugins/hr-plugin.ts +102 -0
- package/src/plugins/html-plugin.ts +353 -0
- package/src/plugins/image-plugin.ts +806 -0
- package/src/plugins/index.ts +71 -0
- package/src/plugins/inline-plugin.ts +311 -0
- package/src/plugins/link-plugin.ts +509 -0
- package/src/plugins/list-plugin.ts +492 -0
- package/src/plugins/math-plugin.ts +526 -0
- package/src/plugins/mermaid-plugin.ts +513 -0
- package/src/plugins/paragraph-plugin.ts +38 -0
- package/src/plugins/quote-plugin.ts +733 -0
- package/src/plugins/table-controls-theme.ts +126 -0
- package/src/plugins/table-controls.ts +423 -0
- package/src/plugins/table-model.ts +661 -0
- package/src/plugins/table-plugin.ts +2111 -0
- package/src/preview/context.ts +45 -0
- package/src/preview/css-generator.ts +64 -0
- package/src/preview/default-renderers.ts +29 -0
- package/src/preview/index.ts +29 -0
- package/src/preview/preview.ts +41 -0
- package/src/preview/renderer.ts +184 -0
- package/src/preview/syntax-theme.ts +112 -0
- package/src/preview/toc.ts +23 -0
- package/src/preview/types.ts +89 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { Decoration, EditorView, KeyBinding, WidgetType } from "@codemirror/view";
|
|
2
|
+
import { syntaxTree } from "@codemirror/language";
|
|
3
|
+
import { DecorationContext, DecorationPlugin } from "../editor/plugin";
|
|
4
|
+
import { createTheme } from "../editor";
|
|
5
|
+
import { SyntaxNode } from "@lezer/common";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Mark decorations for link syntax elements
|
|
9
|
+
*/
|
|
10
|
+
const linkMarkDecorations = {
|
|
11
|
+
"link-text": Decoration.mark({ class: "cm-mardora-link-text" }),
|
|
12
|
+
"link-marker": Decoration.mark({ class: "cm-mardora-link-marker" }),
|
|
13
|
+
"link-url": Decoration.mark({ class: "cm-mardora-link-url" }),
|
|
14
|
+
"link-hidden": Decoration.mark({ class: "cm-mardora-link-hidden" }),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse link markdown to extract text and URL
|
|
19
|
+
* Format: [text](url) or [text](url "title")
|
|
20
|
+
*/
|
|
21
|
+
function parseLinkMarkdown(content: string): { text: string; url: string; title?: string } | null {
|
|
22
|
+
// Regex to match: [text](url) or [text](url "title")
|
|
23
|
+
const match = content.match(/^\[([^\]]*)\]\(([^"\s)]+)(?:\s+"([^"]*)")?\s*\)$/);
|
|
24
|
+
if (!match) return null;
|
|
25
|
+
|
|
26
|
+
const result: { text: string; url: string; title?: string } = {
|
|
27
|
+
text: match[1] || "",
|
|
28
|
+
url: match[2]!,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (match[3] !== undefined) {
|
|
32
|
+
result.title = match[3];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Widget for displaying a tooltip with the link URL on hover
|
|
40
|
+
*/
|
|
41
|
+
class LinkTooltipWidget extends WidgetType {
|
|
42
|
+
constructor(
|
|
43
|
+
readonly url: string,
|
|
44
|
+
readonly from: number,
|
|
45
|
+
readonly to: number
|
|
46
|
+
) {
|
|
47
|
+
super();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
override eq(other: LinkTooltipWidget): boolean {
|
|
51
|
+
return other.url === this.url && other.from === this.from && other.to === this.to;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toDOM(view: EditorView) {
|
|
55
|
+
const wrapper = document.createElement("span");
|
|
56
|
+
wrapper.className = "cm-mardora-link-wrapper";
|
|
57
|
+
wrapper.style.cursor = "pointer";
|
|
58
|
+
|
|
59
|
+
// Tooltip element
|
|
60
|
+
const tooltip = document.createElement("span");
|
|
61
|
+
tooltip.className = "cm-mardora-link-tooltip";
|
|
62
|
+
tooltip.textContent = this.url;
|
|
63
|
+
wrapper.appendChild(tooltip);
|
|
64
|
+
|
|
65
|
+
// Show/hide tooltip on hover
|
|
66
|
+
wrapper.addEventListener("mouseenter", () => {
|
|
67
|
+
tooltip.classList.add("cm-mardora-link-tooltip-visible");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
wrapper.addEventListener("mouseleave", () => {
|
|
71
|
+
tooltip.classList.remove("cm-mardora-link-tooltip-visible");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Click handler - select the raw markdown
|
|
75
|
+
wrapper.addEventListener("click", (e) => {
|
|
76
|
+
if (e.ctrlKey || e.metaKey) {
|
|
77
|
+
// Ctrl+Click: open in new tab
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
e.stopPropagation();
|
|
80
|
+
window.open(this.url, "_blank", "noopener,noreferrer");
|
|
81
|
+
} else {
|
|
82
|
+
// Regular click: select raw markdown
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
view.dispatch({
|
|
86
|
+
selection: { anchor: this.from, head: this.to },
|
|
87
|
+
scrollIntoView: true,
|
|
88
|
+
});
|
|
89
|
+
view.focus();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return wrapper;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override ignoreEvent(event: Event): boolean {
|
|
97
|
+
// Allow click and mouse events to be handled by our handlers
|
|
98
|
+
return event.type !== "click" && event.type !== "mouseenter" && event.type !== "mouseleave";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* LinkPlugin - Decorates and provides interactivity for markdown links
|
|
104
|
+
*
|
|
105
|
+
* Supports the full link syntax: [text](url) and [text](url "title")
|
|
106
|
+
* - Click: reveals raw markdown (selects/focuses the link syntax)
|
|
107
|
+
* - Ctrl+Click: opens the link URL in a new browser tab
|
|
108
|
+
* - Hover: shows tooltip with the link URL
|
|
109
|
+
* - Hides the markdown syntax when cursor is not in range
|
|
110
|
+
* - Shows raw markdown when cursor is within the link range
|
|
111
|
+
*/
|
|
112
|
+
export class LinkPlugin extends DecorationPlugin {
|
|
113
|
+
readonly name = "link";
|
|
114
|
+
readonly version = "1.0.0";
|
|
115
|
+
override decorationPriority = 22;
|
|
116
|
+
override readonly requiredNodes = ["Link"] as const;
|
|
117
|
+
|
|
118
|
+
constructor() {
|
|
119
|
+
super();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Plugin theme
|
|
124
|
+
*/
|
|
125
|
+
override get theme() {
|
|
126
|
+
return theme;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Keyboard shortcuts for link formatting
|
|
131
|
+
*/
|
|
132
|
+
override getKeymap(): KeyBinding[] {
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
key: "Mod-k",
|
|
136
|
+
run: (view) => this.toggleLink(view),
|
|
137
|
+
preventDefault: true,
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* URL regex pattern
|
|
144
|
+
*/
|
|
145
|
+
private readonly urlPattern = /^(https?:\/\/|www\.)[^\s]+$/i;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Toggle link on selection
|
|
149
|
+
* - If text is selected and is a URL: [](url) with cursor in brackets
|
|
150
|
+
* - If text is selected (not URL): [text]() with cursor in parentheses
|
|
151
|
+
* - If nothing selected: []() with cursor in brackets
|
|
152
|
+
* - If already a link: remove syntax, leave plain text
|
|
153
|
+
*/
|
|
154
|
+
private toggleLink(view: EditorView): boolean {
|
|
155
|
+
const { state } = view;
|
|
156
|
+
const { from, to, empty } = state.selection.main;
|
|
157
|
+
const selectedText = state.sliceDoc(from, to);
|
|
158
|
+
|
|
159
|
+
// Check if selection is already a link [text](url)
|
|
160
|
+
const linkMatch = selectedText.match(/^\[([^\]]*)\]\(([^)]*)\)$/);
|
|
161
|
+
if (linkMatch) {
|
|
162
|
+
// Already a link - extract just the text and replace
|
|
163
|
+
const linkText = linkMatch[1] || "";
|
|
164
|
+
view.dispatch({
|
|
165
|
+
changes: { from, to, insert: linkText },
|
|
166
|
+
selection: { anchor: from, head: from + linkText.length },
|
|
167
|
+
});
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check if we're inside a link by looking at surrounding context
|
|
172
|
+
const lineStart = state.doc.lineAt(from).from;
|
|
173
|
+
const lineEnd = state.doc.lineAt(to).to;
|
|
174
|
+
const lineText = state.sliceDoc(lineStart, lineEnd);
|
|
175
|
+
|
|
176
|
+
// Find link pattern in line that contains the selection
|
|
177
|
+
const linkRegex = /\[([^\]]*)\]\(([^)]*)\)/g;
|
|
178
|
+
let match;
|
|
179
|
+
while ((match = linkRegex.exec(lineText)) !== null) {
|
|
180
|
+
const matchFrom = lineStart + match.index;
|
|
181
|
+
const matchTo = matchFrom + match[0].length;
|
|
182
|
+
|
|
183
|
+
// Check if selection is within this link
|
|
184
|
+
if (from >= matchFrom && to <= matchTo) {
|
|
185
|
+
// Remove the link syntax, leave plain text
|
|
186
|
+
const linkText = match[1] || "";
|
|
187
|
+
view.dispatch({
|
|
188
|
+
changes: { from: matchFrom, to: matchTo, insert: linkText },
|
|
189
|
+
selection: { anchor: matchFrom, head: matchFrom + linkText.length },
|
|
190
|
+
});
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (empty) {
|
|
196
|
+
// No selection - insert []() and place cursor in brackets
|
|
197
|
+
view.dispatch({
|
|
198
|
+
changes: { from, insert: "[]()" },
|
|
199
|
+
selection: { anchor: from + 1 },
|
|
200
|
+
});
|
|
201
|
+
} else if (this.urlPattern.test(selectedText)) {
|
|
202
|
+
// Selected text is a URL - put it in parentheses, cursor in brackets
|
|
203
|
+
const newText = `[](${selectedText})`;
|
|
204
|
+
view.dispatch({
|
|
205
|
+
changes: { from, to, insert: newText },
|
|
206
|
+
selection: { anchor: from + 1 },
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
// Selected text is not a URL - wrap as link text, cursor in parentheses
|
|
210
|
+
const newText = `[${selectedText}]()`;
|
|
211
|
+
view.dispatch({
|
|
212
|
+
changes: { from, to, insert: newText },
|
|
213
|
+
selection: { anchor: from + selectedText.length + 3 },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
buildDecorations(ctx: DecorationContext): void {
|
|
221
|
+
const { view, decorations } = ctx;
|
|
222
|
+
const tree = syntaxTree(view.state);
|
|
223
|
+
|
|
224
|
+
tree.iterate({
|
|
225
|
+
enter: (node) => {
|
|
226
|
+
const { from, to, name } = node;
|
|
227
|
+
|
|
228
|
+
// Handle Link nodes
|
|
229
|
+
if (name === "Link") {
|
|
230
|
+
const content = view.state.sliceDoc(from, to);
|
|
231
|
+
const parsed = parseLinkMarkdown(content);
|
|
232
|
+
|
|
233
|
+
if (!parsed) return;
|
|
234
|
+
|
|
235
|
+
const cursorInRange = ctx.selectionOverlapsRange(from, to);
|
|
236
|
+
|
|
237
|
+
if (cursorInRange) {
|
|
238
|
+
// Cursor in range: show raw markdown with styling
|
|
239
|
+
this.decorateRawLink(node.node, decorations, view);
|
|
240
|
+
} else {
|
|
241
|
+
// Cursor out of range: hide raw markdown, show styled link text
|
|
242
|
+
// Hide the entire markdown syntax
|
|
243
|
+
decorations.push(linkMarkDecorations["link-hidden"].range(from, to));
|
|
244
|
+
|
|
245
|
+
// Add styled link text with tooltip widget after the hidden markdown
|
|
246
|
+
decorations.push(
|
|
247
|
+
Decoration.widget({
|
|
248
|
+
widget: new LinkTooltipWidget(parsed.url, from, to),
|
|
249
|
+
side: 1,
|
|
250
|
+
}).range(to)
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Add replacement decoration to show styled link text
|
|
254
|
+
decorations.push(
|
|
255
|
+
Decoration.replace({
|
|
256
|
+
widget: new LinkTextWidget(parsed.text, parsed.url, from, to, parsed.title),
|
|
257
|
+
}).range(from, to)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Decorate raw link markdown when cursor is in range
|
|
267
|
+
*/
|
|
268
|
+
private decorateRawLink(
|
|
269
|
+
node: SyntaxNode,
|
|
270
|
+
decorations: import("@codemirror/state").Range<Decoration>[],
|
|
271
|
+
view: import("@codemirror/view").EditorView
|
|
272
|
+
): void {
|
|
273
|
+
const content = view.state.sliceDoc(node.from, node.to);
|
|
274
|
+
|
|
275
|
+
// Style the opening bracket [
|
|
276
|
+
decorations.push(linkMarkDecorations["link-marker"].range(node.from, node.from + 1));
|
|
277
|
+
|
|
278
|
+
// Find and style the link text and closing bracket + opening paren ](
|
|
279
|
+
const bracketParen = content.indexOf("](");
|
|
280
|
+
if (bracketParen !== -1) {
|
|
281
|
+
// Style link text
|
|
282
|
+
if (bracketParen > 1) {
|
|
283
|
+
decorations.push(linkMarkDecorations["link-text"].range(node.from + 1, node.from + bracketParen));
|
|
284
|
+
}
|
|
285
|
+
// Style ]( markers
|
|
286
|
+
decorations.push(
|
|
287
|
+
linkMarkDecorations["link-marker"].range(node.from + bracketParen, node.from + bracketParen + 2)
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Find and style the URL
|
|
291
|
+
const urlChild = node.getChild("URL");
|
|
292
|
+
if (urlChild) {
|
|
293
|
+
decorations.push(linkMarkDecorations["link-url"].range(urlChild.from, urlChild.to));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Style closing )
|
|
297
|
+
decorations.push(linkMarkDecorations["link-marker"].range(node.to - 1, node.to));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Render link to HTML for preview mode
|
|
303
|
+
*/
|
|
304
|
+
override renderToHTML(
|
|
305
|
+
node: SyntaxNode,
|
|
306
|
+
_children: string,
|
|
307
|
+
ctx: { sliceDoc(from: number, to: number): string; sanitize(html: string): string }
|
|
308
|
+
): string | null {
|
|
309
|
+
if (node.name !== "Link") return null;
|
|
310
|
+
|
|
311
|
+
const content = ctx.sliceDoc(node.from, node.to);
|
|
312
|
+
const parsed = parseLinkMarkdown(content);
|
|
313
|
+
if (!parsed) return null;
|
|
314
|
+
|
|
315
|
+
const textContent = ctx.sanitize(parsed.text);
|
|
316
|
+
const urlAttr = ctx.sanitize(parsed.url);
|
|
317
|
+
const titleAttr = parsed.title ? ` title="${ctx.sanitize(parsed.title)}"` : "";
|
|
318
|
+
|
|
319
|
+
return `<a class="cm-mardora-link" href="${urlAttr}"${titleAttr} target="_blank" rel="noopener noreferrer">${textContent}</a>`;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Widget to display the styled link text with interactivity
|
|
325
|
+
*/
|
|
326
|
+
class LinkTextWidget extends WidgetType {
|
|
327
|
+
constructor(
|
|
328
|
+
readonly text: string,
|
|
329
|
+
readonly url: string,
|
|
330
|
+
readonly from: number,
|
|
331
|
+
readonly to: number,
|
|
332
|
+
readonly title?: string
|
|
333
|
+
) {
|
|
334
|
+
super();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
override eq(other: LinkTextWidget): boolean {
|
|
338
|
+
return (
|
|
339
|
+
other.text === this.text &&
|
|
340
|
+
other.url === this.url &&
|
|
341
|
+
other.from === this.from &&
|
|
342
|
+
other.to === this.to &&
|
|
343
|
+
other.title === this.title
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
toDOM(view: EditorView) {
|
|
348
|
+
const span = document.createElement("span");
|
|
349
|
+
span.className = "cm-mardora-link-styled";
|
|
350
|
+
span.textContent = this.text;
|
|
351
|
+
span.style.cursor = "pointer";
|
|
352
|
+
|
|
353
|
+
if (this.title) {
|
|
354
|
+
span.title = this.title;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Tooltip element
|
|
358
|
+
const tooltip = document.createElement("span");
|
|
359
|
+
tooltip.className = "cm-mardora-link-tooltip";
|
|
360
|
+
tooltip.textContent = this.url;
|
|
361
|
+
span.appendChild(tooltip);
|
|
362
|
+
|
|
363
|
+
// Show/hide tooltip on hover
|
|
364
|
+
span.addEventListener("mouseenter", () => {
|
|
365
|
+
tooltip.classList.add("cm-mardora-link-tooltip-visible");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
span.addEventListener("mouseleave", () => {
|
|
369
|
+
tooltip.classList.remove("cm-mardora-link-tooltip-visible");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Click handler
|
|
373
|
+
span.addEventListener("click", (e) => {
|
|
374
|
+
if (e.ctrlKey || e.metaKey) {
|
|
375
|
+
// Ctrl+Click: open in new tab
|
|
376
|
+
e.preventDefault();
|
|
377
|
+
e.stopPropagation();
|
|
378
|
+
window.open(this.url, "_blank", "noopener,noreferrer");
|
|
379
|
+
} else {
|
|
380
|
+
// Regular click: select raw markdown
|
|
381
|
+
e.preventDefault();
|
|
382
|
+
e.stopPropagation();
|
|
383
|
+
view.dispatch({
|
|
384
|
+
selection: { anchor: this.from, head: this.to },
|
|
385
|
+
scrollIntoView: true,
|
|
386
|
+
});
|
|
387
|
+
view.focus();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return span;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
override ignoreEvent(event: Event): boolean {
|
|
395
|
+
// Allow click and mouse events to be handled by our handlers
|
|
396
|
+
return event.type !== "click" && event.type !== "mouseenter" && event.type !== "mouseleave";
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Theme for link styling
|
|
402
|
+
*/
|
|
403
|
+
const theme = createTheme({
|
|
404
|
+
default: {
|
|
405
|
+
// Link text
|
|
406
|
+
".cm-mardora-link-text": {
|
|
407
|
+
color: "#0366d6",
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
// Link markers ([ ] ( ))
|
|
411
|
+
".cm-mardora-link-marker": {
|
|
412
|
+
color: "#6a737d",
|
|
413
|
+
fontFamily: "var(--font-jetbrains-mono, monospace)",
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
// URL in raw markdown
|
|
417
|
+
".cm-mardora-link-url": {
|
|
418
|
+
color: "#6a737d",
|
|
419
|
+
fontStyle: "italic",
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
// Hidden markdown syntax
|
|
423
|
+
".cm-mardora-link-hidden": {
|
|
424
|
+
display: "none",
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
// Styled link when cursor is not in range
|
|
428
|
+
".cm-mardora-link-styled": {
|
|
429
|
+
color: "#0366d6",
|
|
430
|
+
textDecoration: "underline",
|
|
431
|
+
position: "relative",
|
|
432
|
+
cursor: "pointer",
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
".cm-mardora-link-styled:hover": {
|
|
436
|
+
color: "#0056b3",
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
// Preview link styling
|
|
440
|
+
".cm-mardora-link": {
|
|
441
|
+
color: "#0366d6",
|
|
442
|
+
textDecoration: "underline",
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
".cm-mardora-link:hover": {
|
|
446
|
+
color: "#0056b3",
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
// Tooltip styling
|
|
450
|
+
".cm-mardora-link-tooltip": {
|
|
451
|
+
display: "none",
|
|
452
|
+
position: "absolute",
|
|
453
|
+
bottom: "100%",
|
|
454
|
+
left: "50%",
|
|
455
|
+
transform: "translateX(-50%)",
|
|
456
|
+
backgroundColor: "#24292e",
|
|
457
|
+
color: "#ffffff",
|
|
458
|
+
padding: "4px 8px",
|
|
459
|
+
borderRadius: "4px",
|
|
460
|
+
fontSize: "12px",
|
|
461
|
+
whiteSpace: "nowrap",
|
|
462
|
+
zIndex: "1000",
|
|
463
|
+
pointerEvents: "none",
|
|
464
|
+
marginBottom: "4px",
|
|
465
|
+
maxWidth: "300px",
|
|
466
|
+
overflow: "hidden",
|
|
467
|
+
textOverflow: "ellipsis",
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
".cm-mardora-link-tooltip-visible": {
|
|
471
|
+
display: "block",
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
dark: {
|
|
476
|
+
".cm-mardora-link-text": {
|
|
477
|
+
color: "#58a6ff",
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
".cm-mardora-link-marker": {
|
|
481
|
+
color: "#8b949e",
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
".cm-mardora-link-url": {
|
|
485
|
+
color: "#8b949e",
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
".cm-mardora-link-styled": {
|
|
489
|
+
color: "#58a6ff",
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
".cm-mardora-link-styled:hover": {
|
|
493
|
+
color: "#79c0ff",
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
".cm-mardora-link": {
|
|
497
|
+
color: "#58a6ff",
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
".cm-mardora-link:hover": {
|
|
501
|
+
color: "#79c0ff",
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
".cm-mardora-link-tooltip": {
|
|
505
|
+
backgroundColor: "#30363d",
|
|
506
|
+
color: "#c9d1d9",
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
});
|