@slexkit/tiptap 0.3.3
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 +88 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +372 -0
- package/package.json +66 -0
- package/style.css +100 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) SlexKit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @slexkit/tiptap
|
|
2
|
+
|
|
3
|
+
Tiptap extension for explicit `slex` fenced UI blocks.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install slexkit @slexkit/theme-shadcn @slexkit/tiptap @tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/extension-code-block @tiptap/markdown
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Import the SlexKit theme once in your app entry:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import "@slexkit/theme-shadcn/style.css";
|
|
13
|
+
import "@slexkit/tiptap/style.css";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Minimal setup
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { Editor } from "@tiptap/core";
|
|
20
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
21
|
+
import { createSlexKitTiptapExtension } from "@slexkit/tiptap";
|
|
22
|
+
|
|
23
|
+
const editor = new Editor({
|
|
24
|
+
element: document.querySelector("#editor"),
|
|
25
|
+
extensions: [
|
|
26
|
+
StarterKit.configure({ codeBlock: false }),
|
|
27
|
+
createSlexKitTiptapExtension({
|
|
28
|
+
artifactId: "note-42",
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Only code blocks whose language is exactly `slex` are rendered by SlexKit. Other code blocks stay ordinary Tiptap code blocks.
|
|
35
|
+
|
|
36
|
+
## Markdown roundtrip
|
|
37
|
+
|
|
38
|
+
Add Tiptap's Markdown extension when loading or exporting Markdown:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { Markdown } from "@tiptap/markdown";
|
|
42
|
+
|
|
43
|
+
const editor = new Editor({
|
|
44
|
+
extensions: [
|
|
45
|
+
StarterKit.configure({ codeBlock: false }),
|
|
46
|
+
Markdown,
|
|
47
|
+
createSlexKitTiptapExtension({ artifactId: "doc" }),
|
|
48
|
+
],
|
|
49
|
+
content: markdown,
|
|
50
|
+
contentType: "markdown",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const nextMarkdown = editor.getMarkdown();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The adapter uses normal fenced code blocks instead of a custom tokenizer, so ` ```slex ` blocks remain readable in plain Markdown hosts.
|
|
57
|
+
|
|
58
|
+
## Shared state across fences
|
|
59
|
+
|
|
60
|
+
All `slex` blocks in one editor share the same artifact runtime by default. A state-only fence can seed later renderable fences:
|
|
61
|
+
|
|
62
|
+
````md
|
|
63
|
+
```slex
|
|
64
|
+
{
|
|
65
|
+
namespace: "calc",
|
|
66
|
+
g: { value: 21 }
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```slex
|
|
71
|
+
{
|
|
72
|
+
namespace: "calc",
|
|
73
|
+
layout: {
|
|
74
|
+
"text:answer": { "$text": "'answer: ' + (g.value * 2)" }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
````
|
|
79
|
+
|
|
80
|
+
## Runtime boundary
|
|
81
|
+
|
|
82
|
+
This adapter defaults to trusted runtime mode, intended for application-authored content, repository-maintained snippets, reviewed examples, and local document workflows. Use SlexKit secure runtime APIs for untrusted user Markdown or direct agent output.
|
|
83
|
+
|
|
84
|
+
## Documentation
|
|
85
|
+
|
|
86
|
+
- [Official Tiptap editor example](https://slexkit.app.simgor.cn/examples/tiptap-host)
|
|
87
|
+
- [Runnable example source](https://github.com/slexkit/slexkit/tree/main/examples/tiptap)
|
|
88
|
+
- [SlexKit Host integration](https://github.com/slexkit/slexkit/blob/main/site/content/reference/integration/en-US.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
import { type SlexKitMarkdownRuntimeHost, type ThemeMode } from "slexkit";
|
|
3
|
+
export type SlexKitTiptapOptions = {
|
|
4
|
+
artifactId?: string | ((ctx: {
|
|
5
|
+
editor: Editor;
|
|
6
|
+
}) => string);
|
|
7
|
+
runtimeHost?: SlexKitMarkdownRuntimeHost;
|
|
8
|
+
theme?: ThemeMode;
|
|
9
|
+
showSource?: boolean;
|
|
10
|
+
sourceLabel?: string;
|
|
11
|
+
previewLabel?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
onError?: (error: unknown, source: string) => void;
|
|
14
|
+
};
|
|
15
|
+
export type SlexKitTiptapNodeViewOptions = Required<Pick<SlexKitTiptapOptions, "showSource" | "sourceLabel" | "previewLabel">> & Pick<SlexKitTiptapOptions, "artifactId" | "className" | "onError" | "runtimeHost" | "theme">;
|
|
16
|
+
export declare function createSlexKitTiptapExtension(options?: SlexKitTiptapOptions): import("@tiptap/core").Node<import("@tiptap/extension-code-block").CodeBlockOptions, any>;
|
|
17
|
+
export declare const SlexKitTiptapExtension: import("@tiptap/core").Node<import("@tiptap/extension-code-block").CodeBlockOptions, any>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import CodeBlock from "@tiptap/extension-code-block";
|
|
2
|
+
import { createSlexKitMarkdownRuntimeHost, parseSlexSource, } from "slexkit";
|
|
3
|
+
const DEFAULT_OPTIONS = {
|
|
4
|
+
showSource: true,
|
|
5
|
+
sourceLabel: "Source",
|
|
6
|
+
previewLabel: "",
|
|
7
|
+
};
|
|
8
|
+
const PREVIEW_INTERACTIVE_SELECTOR = [
|
|
9
|
+
"a[href]",
|
|
10
|
+
"button",
|
|
11
|
+
"input",
|
|
12
|
+
"select",
|
|
13
|
+
"textarea",
|
|
14
|
+
"[tabindex]:not([tabindex='-1'])",
|
|
15
|
+
"[role='button']",
|
|
16
|
+
"[role='checkbox']",
|
|
17
|
+
"[role='radio']",
|
|
18
|
+
"[role='slider']",
|
|
19
|
+
"[role='switch']",
|
|
20
|
+
"[role='tab']",
|
|
21
|
+
"[role='textbox']",
|
|
22
|
+
].join(",");
|
|
23
|
+
const editorArtifactIds = new WeakMap();
|
|
24
|
+
let nextArtifactId = 0;
|
|
25
|
+
const sourceOpenStates = new Map();
|
|
26
|
+
const editorArtifactRuntimes = new WeakMap();
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
function isRenderableSource(value) {
|
|
31
|
+
if (!isRecord(value))
|
|
32
|
+
return false;
|
|
33
|
+
if ("layout" in value)
|
|
34
|
+
return isRecord(value.layout) && Object.keys(value.layout).length > 0;
|
|
35
|
+
if ("namespace" in value || "g" in value)
|
|
36
|
+
return false;
|
|
37
|
+
return Object.keys(value).some((key) => key.includes(":"));
|
|
38
|
+
}
|
|
39
|
+
function isStateOnlySource(value) {
|
|
40
|
+
return isRecord(value)
|
|
41
|
+
&& !isRenderableSource(value)
|
|
42
|
+
&& ("slex" in value || "namespace" in value || "g" in value);
|
|
43
|
+
}
|
|
44
|
+
function resolveArtifactId(editor, option) {
|
|
45
|
+
if (typeof option === "function")
|
|
46
|
+
return option({ editor });
|
|
47
|
+
if (typeof option === "string" && option.trim())
|
|
48
|
+
return option;
|
|
49
|
+
const existing = editorArtifactIds.get(editor);
|
|
50
|
+
if (existing)
|
|
51
|
+
return existing;
|
|
52
|
+
const artifactId = `tiptap-${++nextArtifactId}`;
|
|
53
|
+
editorArtifactIds.set(editor, artifactId);
|
|
54
|
+
return artifactId;
|
|
55
|
+
}
|
|
56
|
+
function codeBlockLanguage(node) {
|
|
57
|
+
return String(node.attrs.language ?? node.attrs.defaultLanguage ?? "").toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
function sourceOpenStateKey(artifactId, node) {
|
|
60
|
+
return `${artifactId}::${node.textContent}`;
|
|
61
|
+
}
|
|
62
|
+
function compareNodeViewOrder(a, b) {
|
|
63
|
+
if (a.dom === b.dom)
|
|
64
|
+
return 0;
|
|
65
|
+
const position = a.dom.compareDocumentPosition(b.dom);
|
|
66
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
67
|
+
return -1;
|
|
68
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING)
|
|
69
|
+
return 1;
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
function resolveArtifactRuntime(editor, artifactId, runtimeHost) {
|
|
73
|
+
let runtimes = editorArtifactRuntimes.get(editor);
|
|
74
|
+
if (!runtimes) {
|
|
75
|
+
runtimes = new Map();
|
|
76
|
+
editorArtifactRuntimes.set(editor, runtimes);
|
|
77
|
+
}
|
|
78
|
+
let runtime = runtimes.get(artifactId);
|
|
79
|
+
if (!runtime) {
|
|
80
|
+
runtime = {
|
|
81
|
+
artifactId,
|
|
82
|
+
runtimeHost: runtimeHost ?? createSlexKitMarkdownRuntimeHost(),
|
|
83
|
+
views: new Set(),
|
|
84
|
+
scheduled: false,
|
|
85
|
+
};
|
|
86
|
+
runtimes.set(artifactId, runtime);
|
|
87
|
+
}
|
|
88
|
+
return runtime;
|
|
89
|
+
}
|
|
90
|
+
function scheduleArtifactRender(runtime) {
|
|
91
|
+
if (runtime.scheduled)
|
|
92
|
+
return;
|
|
93
|
+
runtime.scheduled = true;
|
|
94
|
+
queueMicrotask(() => {
|
|
95
|
+
runtime.scheduled = false;
|
|
96
|
+
runtime.runtimeHost.disposeArtifact(runtime.artifactId);
|
|
97
|
+
for (const view of Array.from(runtime.views).sort(compareNodeViewOrder)) {
|
|
98
|
+
view.renderWithRuntime(runtime.runtimeHost);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function unregisterArtifactRuntime(editor, runtime, view) {
|
|
103
|
+
runtime.views.delete(view);
|
|
104
|
+
if (runtime.views.size > 0) {
|
|
105
|
+
scheduleArtifactRender(runtime);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
runtime.runtimeHost.disposeArtifact(runtime.artifactId);
|
|
109
|
+
editorArtifactRuntimes.get(editor)?.delete(runtime.artifactId);
|
|
110
|
+
}
|
|
111
|
+
function createElement(doc, tag, className) {
|
|
112
|
+
const element = doc.createElement(tag);
|
|
113
|
+
if (className)
|
|
114
|
+
element.className = className;
|
|
115
|
+
return element;
|
|
116
|
+
}
|
|
117
|
+
function stopPropagation(event) {
|
|
118
|
+
event.stopPropagation();
|
|
119
|
+
}
|
|
120
|
+
function stopProseMirrorEvents(element, events) {
|
|
121
|
+
for (const eventName of events)
|
|
122
|
+
element.addEventListener(eventName, stopPropagation);
|
|
123
|
+
}
|
|
124
|
+
function focusPreviewInteractiveTarget(event) {
|
|
125
|
+
const target = event.target;
|
|
126
|
+
const targetElement = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
|
127
|
+
const interactive = targetElement?.closest(PREVIEW_INTERACTIVE_SELECTOR);
|
|
128
|
+
if (interactive instanceof HTMLElement)
|
|
129
|
+
interactive.focus({ preventScroll: true });
|
|
130
|
+
}
|
|
131
|
+
function markPreviewInteractive(preview, force = false) {
|
|
132
|
+
if (preview.getAttribute("contenteditable") !== "false") {
|
|
133
|
+
preview.setAttribute("contenteditable", "false");
|
|
134
|
+
}
|
|
135
|
+
preview.querySelectorAll(PREVIEW_INTERACTIVE_SELECTOR).forEach((element) => {
|
|
136
|
+
if (element instanceof HTMLElement && (force || element.getAttribute("contenteditable") !== "true")) {
|
|
137
|
+
if (force)
|
|
138
|
+
element.removeAttribute("contenteditable");
|
|
139
|
+
element.setAttribute("contenteditable", "true");
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function schedulePreviewInteractiveMark(preview) {
|
|
144
|
+
markPreviewInteractive(preview);
|
|
145
|
+
queueMicrotask(() => markPreviewInteractive(preview, true));
|
|
146
|
+
const win = preview.ownerDocument.defaultView;
|
|
147
|
+
win?.setTimeout(() => markPreviewInteractive(preview, true), 0);
|
|
148
|
+
win?.requestAnimationFrame(() => markPreviewInteractive(preview, true));
|
|
149
|
+
}
|
|
150
|
+
function renderError(error, source, container) {
|
|
151
|
+
const doc = container.ownerDocument || document;
|
|
152
|
+
const alert = createElement(doc, "div", "slex-tiptap-error");
|
|
153
|
+
alert.setAttribute("role", "alert");
|
|
154
|
+
const title = createElement(doc, "div", "slex-tiptap-error-title");
|
|
155
|
+
title.textContent = "Failed to render SlexKit";
|
|
156
|
+
const message = createElement(doc, "div", "slex-tiptap-error-message");
|
|
157
|
+
message.textContent = error instanceof Error ? error.message : String(error);
|
|
158
|
+
alert.append(title, message);
|
|
159
|
+
if (source) {
|
|
160
|
+
const excerpt = doc.createElement("pre");
|
|
161
|
+
excerpt.className = "slex-tiptap-error-source";
|
|
162
|
+
excerpt.textContent = source;
|
|
163
|
+
alert.append(excerpt);
|
|
164
|
+
}
|
|
165
|
+
container.replaceChildren(alert);
|
|
166
|
+
}
|
|
167
|
+
class PlainCodeBlockNodeView {
|
|
168
|
+
dom;
|
|
169
|
+
contentDOM;
|
|
170
|
+
constructor(node, doc) {
|
|
171
|
+
const language = codeBlockLanguage(node);
|
|
172
|
+
this.dom = doc.createElement("pre");
|
|
173
|
+
this.dom.className = "slex-tiptap-code-block";
|
|
174
|
+
this.contentDOM = doc.createElement("code");
|
|
175
|
+
if (language)
|
|
176
|
+
this.contentDOM.className = `language-${language}`;
|
|
177
|
+
this.dom.append(this.contentDOM);
|
|
178
|
+
}
|
|
179
|
+
update(node) {
|
|
180
|
+
if (node.type.name !== "codeBlock" || codeBlockLanguage(node) === "slex")
|
|
181
|
+
return false;
|
|
182
|
+
const language = codeBlockLanguage(node);
|
|
183
|
+
this.contentDOM.className = language ? `language-${language}` : "";
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
class SlexKitCodeBlockNodeView {
|
|
188
|
+
node;
|
|
189
|
+
dom;
|
|
190
|
+
contentDOM;
|
|
191
|
+
artifactId;
|
|
192
|
+
editor;
|
|
193
|
+
options;
|
|
194
|
+
preview;
|
|
195
|
+
runtime;
|
|
196
|
+
interactiveObserver;
|
|
197
|
+
renderToken = 0;
|
|
198
|
+
constructor(node, props, options) {
|
|
199
|
+
this.node = node;
|
|
200
|
+
this.options = options;
|
|
201
|
+
this.editor = props.editor;
|
|
202
|
+
this.artifactId = resolveArtifactId(props.editor, options.artifactId);
|
|
203
|
+
this.runtime = resolveArtifactRuntime(props.editor, this.artifactId, options.runtimeHost);
|
|
204
|
+
this.runtime.views.add(this);
|
|
205
|
+
const doc = props.view.dom.ownerDocument || document;
|
|
206
|
+
this.dom = createElement(doc, "div", ["slex-tiptap-block", options.className].filter(Boolean).join(" "));
|
|
207
|
+
this.dom.dataset.language = "slex";
|
|
208
|
+
this.dom.setAttribute("data-slexkit-tiptap", "true");
|
|
209
|
+
const label = options.previewLabel.trim() ? createElement(doc, "div", "slex-tiptap-label") : undefined;
|
|
210
|
+
if (label)
|
|
211
|
+
label.textContent = options.previewLabel;
|
|
212
|
+
this.preview = createElement(doc, "div", "slex-tiptap-preview");
|
|
213
|
+
this.preview.setAttribute("contenteditable", "false");
|
|
214
|
+
this.preview.addEventListener("pointerdown", focusPreviewInteractiveTarget, { capture: true });
|
|
215
|
+
this.preview.addEventListener("mousedown", focusPreviewInteractiveTarget, { capture: true });
|
|
216
|
+
this.preview.addEventListener("click", focusPreviewInteractiveTarget, { capture: true });
|
|
217
|
+
if (typeof MutationObserver !== "undefined") {
|
|
218
|
+
this.interactiveObserver = new MutationObserver(() => schedulePreviewInteractiveMark(this.preview));
|
|
219
|
+
this.interactiveObserver.observe(this.preview, { childList: true, subtree: true });
|
|
220
|
+
}
|
|
221
|
+
this.contentDOM = doc.createElement("code");
|
|
222
|
+
this.contentDOM.className = "language-slex";
|
|
223
|
+
const pre = doc.createElement("pre");
|
|
224
|
+
pre.append(this.contentDOM);
|
|
225
|
+
if (options.showSource) {
|
|
226
|
+
const openStateKey = sourceOpenStateKey(this.artifactId, node);
|
|
227
|
+
const sourceShell = createElement(doc, "div", "slex-tiptap-source");
|
|
228
|
+
const sourceToggle = doc.createElement("button");
|
|
229
|
+
sourceToggle.type = "button";
|
|
230
|
+
sourceToggle.className = "slex-tiptap-source-toggle slex-tiptap-source-summary";
|
|
231
|
+
sourceToggle.textContent = options.sourceLabel;
|
|
232
|
+
sourceToggle.setAttribute("aria-expanded", "false");
|
|
233
|
+
let sourceOpen = sourceOpenStates.get(openStateKey) ?? false;
|
|
234
|
+
const setSourceOpen = (open) => {
|
|
235
|
+
sourceOpen = open;
|
|
236
|
+
sourceOpenStates.set(openStateKey, open);
|
|
237
|
+
pre.hidden = !open;
|
|
238
|
+
sourceShell.dataset.open = open ? "true" : "false";
|
|
239
|
+
sourceToggle.setAttribute("aria-expanded", String(open));
|
|
240
|
+
for (const delay of [0, 16, 50, 100, 200]) {
|
|
241
|
+
sourceShell.ownerDocument.defaultView?.setTimeout(() => {
|
|
242
|
+
pre.hidden = !sourceOpen;
|
|
243
|
+
sourceShell.dataset.open = sourceOpen ? "true" : "false";
|
|
244
|
+
sourceToggle.setAttribute("aria-expanded", String(sourceOpen));
|
|
245
|
+
}, delay);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
const enforceSourceOpen = (event) => {
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
event.stopPropagation();
|
|
251
|
+
setSourceOpen(!sourceOpen);
|
|
252
|
+
};
|
|
253
|
+
const stopSourceClick = (event) => {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
event.stopPropagation();
|
|
256
|
+
};
|
|
257
|
+
stopProseMirrorEvents(sourceToggle, ["mousedown", "keydown"]);
|
|
258
|
+
sourceToggle.addEventListener("pointerdown", enforceSourceOpen, { capture: true });
|
|
259
|
+
sourceToggle.addEventListener("click", stopSourceClick, { capture: true });
|
|
260
|
+
setSourceOpen(sourceOpen);
|
|
261
|
+
sourceShell.append(sourceToggle, pre);
|
|
262
|
+
this.dom.append(...[label, this.preview, sourceShell].filter((element) => !!element));
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const sourceShell = createElement(doc, "div", "slex-tiptap-source slex-tiptap-source-hidden");
|
|
266
|
+
sourceShell.hidden = true;
|
|
267
|
+
sourceShell.append(pre);
|
|
268
|
+
this.dom.append(...[label, this.preview, sourceShell].filter((element) => !!element));
|
|
269
|
+
}
|
|
270
|
+
scheduleArtifactRender(this.runtime);
|
|
271
|
+
}
|
|
272
|
+
update(node) {
|
|
273
|
+
if (node.type.name !== "codeBlock" || codeBlockLanguage(node) !== "slex")
|
|
274
|
+
return false;
|
|
275
|
+
this.node = node;
|
|
276
|
+
this.renderSoon();
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
destroy() {
|
|
280
|
+
this.interactiveObserver?.disconnect();
|
|
281
|
+
this.preview.replaceChildren();
|
|
282
|
+
unregisterArtifactRuntime(this.editor, this.runtime, this);
|
|
283
|
+
}
|
|
284
|
+
stopEvent(event) {
|
|
285
|
+
const target = event.target;
|
|
286
|
+
if (!(target instanceof Node))
|
|
287
|
+
return false;
|
|
288
|
+
if (this.preview.contains(target))
|
|
289
|
+
return true;
|
|
290
|
+
const targetElement = target instanceof HTMLElement ? target : target.parentElement;
|
|
291
|
+
if (targetElement?.closest(".slex-tiptap-source-summary")) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
ignoreMutation(mutation) {
|
|
297
|
+
const target = mutation.target;
|
|
298
|
+
if (this.preview.contains(target))
|
|
299
|
+
return true;
|
|
300
|
+
if (target === this.contentDOM || this.contentDOM.contains(target))
|
|
301
|
+
return false;
|
|
302
|
+
const targetElement = target instanceof Element ? target : target.parentElement;
|
|
303
|
+
if (targetElement?.closest(".slex-tiptap-source"))
|
|
304
|
+
return true;
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
source() {
|
|
308
|
+
return this.node.textContent;
|
|
309
|
+
}
|
|
310
|
+
renderSoon() {
|
|
311
|
+
const token = ++this.renderToken;
|
|
312
|
+
queueMicrotask(() => {
|
|
313
|
+
if (token !== this.renderToken)
|
|
314
|
+
return;
|
|
315
|
+
scheduleArtifactRender(this.runtime);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
renderWithRuntime(runtimeHost) {
|
|
319
|
+
const source = this.source();
|
|
320
|
+
this.dom.hidden = false;
|
|
321
|
+
this.preview.hidden = false;
|
|
322
|
+
this.preview.replaceChildren();
|
|
323
|
+
const parsed = parseSlexSource(source);
|
|
324
|
+
if (!parsed.ok) {
|
|
325
|
+
renderError(parsed.error, source, this.preview);
|
|
326
|
+
this.options.onError?.(parsed.error, source);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
runtimeHost.mountBlock({
|
|
331
|
+
artifactId: this.artifactId,
|
|
332
|
+
source,
|
|
333
|
+
container: this.preview,
|
|
334
|
+
theme: this.options.theme,
|
|
335
|
+
});
|
|
336
|
+
schedulePreviewInteractiveMark(this.preview);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
renderError(error, source, this.preview);
|
|
340
|
+
this.options.onError?.(error, source);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (isStateOnlySource(parsed.value) && !this.preview.querySelector(".slexkit-root")) {
|
|
344
|
+
if (!this.options.showSource) {
|
|
345
|
+
this.dom.hidden = true;
|
|
346
|
+
this.preview.hidden = true;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const placeholder = createElement(this.preview.ownerDocument || document, "div", "slex-tiptap-state");
|
|
350
|
+
placeholder.textContent = "State block";
|
|
351
|
+
this.preview.append(placeholder);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
export function createSlexKitTiptapExtension(options = {}) {
|
|
356
|
+
const resolvedOptions = {
|
|
357
|
+
...DEFAULT_OPTIONS,
|
|
358
|
+
...options,
|
|
359
|
+
};
|
|
360
|
+
return CodeBlock.extend({
|
|
361
|
+
addNodeView() {
|
|
362
|
+
return (props) => {
|
|
363
|
+
const doc = props.view.dom.ownerDocument || document;
|
|
364
|
+
if (codeBlockLanguage(props.node) !== "slex") {
|
|
365
|
+
return new PlainCodeBlockNodeView(props.node, doc);
|
|
366
|
+
}
|
|
367
|
+
return new SlexKitCodeBlockNodeView(props.node, props, resolvedOptions);
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
export const SlexKitTiptapExtension = createSlexKitTiptapExtension();
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@slexkit/tiptap",
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Tiptap extension for SlexKit fenced UI blocks.",
|
|
5
|
+
"author": "SlexKit contributors",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/slexkit/slexkit.git",
|
|
10
|
+
"directory": "packages/tiptap"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/slexkit/slexkit/tree/main/packages/tiptap#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/slexkit/slexkit/issues"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./style.css": "./style.css"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist/",
|
|
28
|
+
"style.css",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "bun run scripts/build.ts"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"slexkit",
|
|
37
|
+
"tiptap",
|
|
38
|
+
"prosemirror",
|
|
39
|
+
"markdown",
|
|
40
|
+
"editor"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@tiptap/core": "^3.0.0",
|
|
48
|
+
"@tiptap/extension-code-block": "^3.0.0",
|
|
49
|
+
"@tiptap/markdown": "^3.0.0",
|
|
50
|
+
"@tiptap/pm": "^3.0.0",
|
|
51
|
+
"slexkit": "^0.3.3"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@tiptap/markdown": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@tiptap/core": "^3.27.1",
|
|
60
|
+
"@tiptap/extension-code-block": "^3.27.1",
|
|
61
|
+
"@tiptap/markdown": "^3.27.1",
|
|
62
|
+
"@tiptap/pm": "^3.27.1",
|
|
63
|
+
"@tiptap/starter-kit": "^3.27.1",
|
|
64
|
+
"slexkit": "file:../.."
|
|
65
|
+
}
|
|
66
|
+
}
|
package/style.css
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
.slex-tiptap-block {
|
|
2
|
+
display: grid;
|
|
3
|
+
gap: 0.5rem;
|
|
4
|
+
margin: 1.25rem 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.slex-tiptap-label {
|
|
8
|
+
color: var(--muted-foreground, #64748b);
|
|
9
|
+
font-size: 0.8125rem;
|
|
10
|
+
font-weight: 650;
|
|
11
|
+
line-height: 1.3;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.slex-tiptap-preview[contenteditable="false"] {
|
|
15
|
+
outline: none;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.slex-tiptap-source {
|
|
19
|
+
display: grid;
|
|
20
|
+
justify-items: start;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.slex-tiptap-source-toggle {
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
min-height: 1.75rem;
|
|
27
|
+
border: 1px solid var(--border, #dbe3ef);
|
|
28
|
+
border-radius: calc(var(--radius, 0.5rem) - 2px);
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
padding: 0 0.625rem;
|
|
31
|
+
color: var(--muted-foreground, #64748b);
|
|
32
|
+
background: var(--background, #ffffff);
|
|
33
|
+
font-size: 0.75rem;
|
|
34
|
+
font-weight: 650;
|
|
35
|
+
font-family: inherit;
|
|
36
|
+
line-height: 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.slex-tiptap-source pre,
|
|
40
|
+
.slex-tiptap-code-block {
|
|
41
|
+
overflow: auto;
|
|
42
|
+
width: 100%;
|
|
43
|
+
margin: 0.5rem 0 0;
|
|
44
|
+
padding: 0.75rem;
|
|
45
|
+
border: 1px solid var(--border, #dbe3ef);
|
|
46
|
+
border-radius: var(--radius, 0.5rem);
|
|
47
|
+
background: var(--muted, #f1f5f9);
|
|
48
|
+
font: 0.875rem/1.55 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
49
|
+
white-space: pre;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.slex-tiptap-code-block {
|
|
53
|
+
margin: 1rem 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.slex-tiptap-state {
|
|
57
|
+
display: inline-flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
min-height: 1.5rem;
|
|
60
|
+
padding: 0 0.5rem;
|
|
61
|
+
border: 0;
|
|
62
|
+
border-radius: calc(var(--radius, 0.5rem) - 2px);
|
|
63
|
+
color: var(--muted-foreground, #64748b);
|
|
64
|
+
background: var(--muted, #f1f5f9);
|
|
65
|
+
font-size: 0.75rem;
|
|
66
|
+
font-weight: 650;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.slex-tiptap-error {
|
|
70
|
+
display: grid;
|
|
71
|
+
gap: 0.5rem;
|
|
72
|
+
padding: 0.75rem;
|
|
73
|
+
border: 1px solid var(--destructive, #dc2626);
|
|
74
|
+
border-radius: var(--radius, 0.5rem);
|
|
75
|
+
color: var(--destructive, #dc2626);
|
|
76
|
+
background: color-mix(in srgb, var(--destructive, #dc2626) 8%, transparent);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.slex-tiptap-error-title {
|
|
80
|
+
font-weight: 750;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.slex-tiptap-error-message {
|
|
84
|
+
color: inherit;
|
|
85
|
+
font-size: 0.875rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.slex-tiptap-error-source {
|
|
89
|
+
overflow: auto;
|
|
90
|
+
margin: 0;
|
|
91
|
+
color: inherit;
|
|
92
|
+
font: 0.8125rem/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
93
|
+
white-space: pre;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.slex-tiptap-block[hidden],
|
|
97
|
+
.slex-tiptap-preview[hidden],
|
|
98
|
+
.slex-tiptap-source[hidden] {
|
|
99
|
+
display: none;
|
|
100
|
+
}
|