inkpen 0.7.1
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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.rubocop.yml +8 -0
- data/.yardopts +11 -0
- data/CLAUDE.md +141 -0
- data/README.md +409 -0
- data/Rakefile +19 -0
- data/app/assets/javascripts/inkpen/controllers/editor_controller.js +2050 -0
- data/app/assets/javascripts/inkpen/controllers/sticky_toolbar_controller.js +667 -0
- data/app/assets/javascripts/inkpen/controllers/toolbar_controller.js +693 -0
- data/app/assets/javascripts/inkpen/export/html.js +637 -0
- data/app/assets/javascripts/inkpen/export/index.js +30 -0
- data/app/assets/javascripts/inkpen/export/markdown.js +697 -0
- data/app/assets/javascripts/inkpen/export/pdf.js +372 -0
- data/app/assets/javascripts/inkpen/extensions/advanced_table.js +640 -0
- data/app/assets/javascripts/inkpen/extensions/block_commands.js +300 -0
- data/app/assets/javascripts/inkpen/extensions/block_gutter.js +338 -0
- data/app/assets/javascripts/inkpen/extensions/callout.js +303 -0
- data/app/assets/javascripts/inkpen/extensions/columns.js +403 -0
- data/app/assets/javascripts/inkpen/extensions/database.js +990 -0
- data/app/assets/javascripts/inkpen/extensions/document_section.js +352 -0
- data/app/assets/javascripts/inkpen/extensions/drag_handle.js +407 -0
- data/app/assets/javascripts/inkpen/extensions/embed.js +629 -0
- data/app/assets/javascripts/inkpen/extensions/enhanced_image.js +566 -0
- data/app/assets/javascripts/inkpen/extensions/export_commands.js +271 -0
- data/app/assets/javascripts/inkpen/extensions/file_attachment.js +593 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/index.js +58 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table.js +638 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_cell.js +100 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_header.js +100 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_constants.js +152 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_helpers.js +254 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_menu.js +282 -0
- data/app/assets/javascripts/inkpen/extensions/preformatted.js +239 -0
- data/app/assets/javascripts/inkpen/extensions/section.js +281 -0
- data/app/assets/javascripts/inkpen/extensions/section_title.js +126 -0
- data/app/assets/javascripts/inkpen/extensions/slash_commands.js +439 -0
- data/app/assets/javascripts/inkpen/extensions/table_of_contents.js +474 -0
- data/app/assets/javascripts/inkpen/extensions/toggle_block.js +332 -0
- data/app/assets/javascripts/inkpen/index.js +87 -0
- data/app/assets/stylesheets/inkpen/advanced_table.css +514 -0
- data/app/assets/stylesheets/inkpen/animations.css +626 -0
- data/app/assets/stylesheets/inkpen/block_gutter.css +265 -0
- data/app/assets/stylesheets/inkpen/callout.css +359 -0
- data/app/assets/stylesheets/inkpen/columns.css +314 -0
- data/app/assets/stylesheets/inkpen/database.css +658 -0
- data/app/assets/stylesheets/inkpen/document_section.css +305 -0
- data/app/assets/stylesheets/inkpen/drag_drop.css +220 -0
- data/app/assets/stylesheets/inkpen/editor.css +652 -0
- data/app/assets/stylesheets/inkpen/embed.css +468 -0
- data/app/assets/stylesheets/inkpen/enhanced_image.css +453 -0
- data/app/assets/stylesheets/inkpen/export.css +499 -0
- data/app/assets/stylesheets/inkpen/file_attachment.css +347 -0
- data/app/assets/stylesheets/inkpen/footnotes.css +136 -0
- data/app/assets/stylesheets/inkpen/inkpen_table.css +608 -0
- data/app/assets/stylesheets/inkpen/preformatted.css +215 -0
- data/app/assets/stylesheets/inkpen/search_replace.css +58 -0
- data/app/assets/stylesheets/inkpen/section.css +236 -0
- data/app/assets/stylesheets/inkpen/slash_menu.css +252 -0
- data/app/assets/stylesheets/inkpen/sticky_toolbar.css +314 -0
- data/app/assets/stylesheets/inkpen/toc.css +386 -0
- data/app/assets/stylesheets/inkpen/toggle.css +260 -0
- data/app/helpers/inkpen/editor_helper.rb +114 -0
- data/app/views/inkpen/_editor.html.erb +139 -0
- data/config/importmap.rb +170 -0
- data/docs/.DS_Store +0 -0
- data/docs/CHANGELOG.md +571 -0
- data/docs/FEATURES.md +436 -0
- data/docs/ROADMAP.md +3029 -0
- data/docs/VISION.md +235 -0
- data/docs/extensions/INKPEN_TABLE.md +482 -0
- data/docs/thinking/CORRECTED_NO_VUE.md +756 -0
- data/docs/thinking/EXECUTIVE_SUMMARY.md +403 -0
- data/docs/thinking/INKPEN_CODE_SAMPLES.md +1479 -0
- data/docs/thinking/INKPEN_MASTER_GUIDE.md +891 -0
- data/docs/thinking/README_START_HERE.md +341 -0
- data/lib/inkpen/configuration.rb +175 -0
- data/lib/inkpen/editor.rb +204 -0
- data/lib/inkpen/engine.rb +32 -0
- data/lib/inkpen/extensions/base.rb +109 -0
- data/lib/inkpen/extensions/code_block_syntax.rb +177 -0
- data/lib/inkpen/extensions/document_section.rb +111 -0
- data/lib/inkpen/extensions/forced_document.rb +183 -0
- data/lib/inkpen/extensions/mention.rb +155 -0
- data/lib/inkpen/extensions/preformatted.rb +111 -0
- data/lib/inkpen/extensions/section.rb +139 -0
- data/lib/inkpen/extensions/slash_commands.rb +100 -0
- data/lib/inkpen/extensions/table.rb +182 -0
- data/lib/inkpen/extensions/task_list.rb +145 -0
- data/lib/inkpen/sticky_toolbar.rb +157 -0
- data/lib/inkpen/toolbar.rb +145 -0
- data/lib/inkpen/version.rb +5 -0
- data/lib/inkpen.rb +101 -0
- data/sig/inkpen.rbs +4 -0
- metadata +165 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from "@tiptap/core"
|
|
2
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Social Embed Extension for TipTap
|
|
6
|
+
*
|
|
7
|
+
* Embed content from various platforms:
|
|
8
|
+
* - Twitter/X posts
|
|
9
|
+
* - Instagram posts
|
|
10
|
+
* - TikTok videos
|
|
11
|
+
* - Figma designs
|
|
12
|
+
* - Loom videos
|
|
13
|
+
* - CodePen pens
|
|
14
|
+
* - GitHub Gists
|
|
15
|
+
* - Spotify tracks/playlists
|
|
16
|
+
* - Generic link cards for unsupported URLs
|
|
17
|
+
*
|
|
18
|
+
* Privacy-aware: Shows preview until user clicks to load external content.
|
|
19
|
+
*
|
|
20
|
+
* @since 0.5.0
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const EMBED_KEY = new PluginKey("embed")
|
|
24
|
+
|
|
25
|
+
// Provider configurations
|
|
26
|
+
const PROVIDERS = {
|
|
27
|
+
youtube: {
|
|
28
|
+
name: "YouTube",
|
|
29
|
+
icon: "▶",
|
|
30
|
+
color: "#ff0000",
|
|
31
|
+
patterns: [
|
|
32
|
+
/(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)/,
|
|
33
|
+
/(?:https?:\/\/)?(?:www\.)?youtu\.be\/([a-zA-Z0-9_-]+)/,
|
|
34
|
+
/(?:https?:\/\/)?(?:www\.)?youtube\.com\/embed\/([a-zA-Z0-9_-]+)/
|
|
35
|
+
],
|
|
36
|
+
getEmbedUrl: (id) => `https://www.youtube.com/embed/${id}`,
|
|
37
|
+
aspectRatio: "16/9"
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
twitter: {
|
|
41
|
+
name: "Twitter",
|
|
42
|
+
icon: "𝕏",
|
|
43
|
+
color: "#000000",
|
|
44
|
+
patterns: [
|
|
45
|
+
/(?:https?:\/\/)?(?:www\.)?(twitter|x)\.com\/(\w+)\/status\/(\d+)/
|
|
46
|
+
],
|
|
47
|
+
getEmbedUrl: (match) => null, // Uses widget script
|
|
48
|
+
extractId: (match) => ({ user: match[2], id: match[3] }),
|
|
49
|
+
useWidget: true,
|
|
50
|
+
widgetScript: "https://platform.twitter.com/widgets.js"
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
instagram: {
|
|
54
|
+
name: "Instagram",
|
|
55
|
+
icon: "📷",
|
|
56
|
+
color: "#e4405f",
|
|
57
|
+
patterns: [
|
|
58
|
+
/(?:https?:\/\/)?(?:www\.)?instagram\.com\/(p|reel)\/([A-Za-z0-9_-]+)/
|
|
59
|
+
],
|
|
60
|
+
getEmbedUrl: (id) => null,
|
|
61
|
+
extractId: (match) => ({ type: match[1], id: match[2] }),
|
|
62
|
+
useWidget: true,
|
|
63
|
+
widgetScript: "https://www.instagram.com/embed.js"
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
tiktok: {
|
|
67
|
+
name: "TikTok",
|
|
68
|
+
icon: "♪",
|
|
69
|
+
color: "#000000",
|
|
70
|
+
patterns: [
|
|
71
|
+
/(?:https?:\/\/)?(?:www\.)?tiktok\.com\/@([^\/]+)\/video\/(\d+)/,
|
|
72
|
+
/(?:https?:\/\/)?(?:vm\.)?tiktok\.com\/([A-Za-z0-9]+)/
|
|
73
|
+
],
|
|
74
|
+
getEmbedUrl: (id) => `https://www.tiktok.com/embed/v2/${id}`,
|
|
75
|
+
aspectRatio: "9/16",
|
|
76
|
+
maxWidth: 325
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
figma: {
|
|
80
|
+
name: "Figma",
|
|
81
|
+
icon: "◈",
|
|
82
|
+
color: "#f24e1e",
|
|
83
|
+
patterns: [
|
|
84
|
+
/(?:https?:\/\/)?(?:www\.)?figma\.com\/(file|proto|design)\/([A-Za-z0-9]+)/
|
|
85
|
+
],
|
|
86
|
+
getEmbedUrl: (url) => `https://www.figma.com/embed?embed_host=inkpen&url=${encodeURIComponent(url)}`,
|
|
87
|
+
useFullUrl: true,
|
|
88
|
+
aspectRatio: "16/9"
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
loom: {
|
|
92
|
+
name: "Loom",
|
|
93
|
+
icon: "🎥",
|
|
94
|
+
color: "#625df5",
|
|
95
|
+
patterns: [
|
|
96
|
+
/(?:https?:\/\/)?(?:www\.)?loom\.com\/share\/([a-zA-Z0-9]+)/,
|
|
97
|
+
/(?:https?:\/\/)?(?:www\.)?loom\.com\/embed\/([a-zA-Z0-9]+)/
|
|
98
|
+
],
|
|
99
|
+
getEmbedUrl: (id) => `https://www.loom.com/embed/${id}`,
|
|
100
|
+
aspectRatio: "16/9"
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
codepen: {
|
|
104
|
+
name: "CodePen",
|
|
105
|
+
icon: "⌨",
|
|
106
|
+
color: "#000000",
|
|
107
|
+
patterns: [
|
|
108
|
+
/(?:https?:\/\/)?(?:www\.)?codepen\.io\/([^\/]+)\/pen\/([A-Za-z0-9]+)/,
|
|
109
|
+
/(?:https?:\/\/)?(?:www\.)?codepen\.io\/([^\/]+)\/full\/([A-Za-z0-9]+)/
|
|
110
|
+
],
|
|
111
|
+
getEmbedUrl: (match) => `https://codepen.io/${match[1]}/embed/${match[2]}?default-tab=result`,
|
|
112
|
+
extractId: (match) => ({ user: match[1], pen: match[2] }),
|
|
113
|
+
aspectRatio: "16/9"
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
gist: {
|
|
117
|
+
name: "GitHub Gist",
|
|
118
|
+
icon: "📋",
|
|
119
|
+
color: "#24292e",
|
|
120
|
+
patterns: [
|
|
121
|
+
/(?:https?:\/\/)?gist\.github\.com\/([^\/]+)\/([a-f0-9]+)/
|
|
122
|
+
],
|
|
123
|
+
getEmbedUrl: (match) => null,
|
|
124
|
+
extractId: (match) => ({ user: match[1], id: match[2] }),
|
|
125
|
+
useScript: true,
|
|
126
|
+
getScriptUrl: (match) => `https://gist.github.com/${match[1]}/${match[2]}.js`
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
spotify: {
|
|
130
|
+
name: "Spotify",
|
|
131
|
+
icon: "🎵",
|
|
132
|
+
color: "#1db954",
|
|
133
|
+
patterns: [
|
|
134
|
+
/(?:https?:\/\/)?open\.spotify\.com\/(track|album|playlist|episode|show)\/([A-Za-z0-9]+)/
|
|
135
|
+
],
|
|
136
|
+
getEmbedUrl: (match) => `https://open.spotify.com/embed/${match[1]}/${match[2]}`,
|
|
137
|
+
extractId: (match) => ({ type: match[1], id: match[2] }),
|
|
138
|
+
height: 352
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
vimeo: {
|
|
142
|
+
name: "Vimeo",
|
|
143
|
+
icon: "▶",
|
|
144
|
+
color: "#1ab7ea",
|
|
145
|
+
patterns: [
|
|
146
|
+
/(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/,
|
|
147
|
+
/(?:https?:\/\/)?player\.vimeo\.com\/video\/(\d+)/
|
|
148
|
+
],
|
|
149
|
+
getEmbedUrl: (id) => `https://player.vimeo.com/video/${id}`,
|
|
150
|
+
aspectRatio: "16/9"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Detect provider from URL
|
|
155
|
+
function detectProvider(url) {
|
|
156
|
+
for (const [key, provider] of Object.entries(PROVIDERS)) {
|
|
157
|
+
for (const pattern of provider.patterns) {
|
|
158
|
+
const match = url.match(pattern)
|
|
159
|
+
if (match) {
|
|
160
|
+
return { provider: key, config: provider, match }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Extract domain from URL
|
|
168
|
+
function extractDomain(url) {
|
|
169
|
+
try {
|
|
170
|
+
const urlObj = new URL(url)
|
|
171
|
+
return urlObj.hostname.replace("www.", "")
|
|
172
|
+
} catch {
|
|
173
|
+
return url
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const Embed = Node.create({
|
|
178
|
+
name: "embed",
|
|
179
|
+
|
|
180
|
+
group: "block",
|
|
181
|
+
|
|
182
|
+
atom: true,
|
|
183
|
+
|
|
184
|
+
draggable: true,
|
|
185
|
+
|
|
186
|
+
addOptions() {
|
|
187
|
+
return {
|
|
188
|
+
// Provider settings
|
|
189
|
+
providers: PROVIDERS,
|
|
190
|
+
allowedProviders: null, // null = all, or ["youtube", "twitter"]
|
|
191
|
+
// Privacy mode: show placeholder until clicked
|
|
192
|
+
privacyMode: true,
|
|
193
|
+
// Link card settings for unknown URLs
|
|
194
|
+
enableLinkCards: true,
|
|
195
|
+
linkCardFetcher: null, // Custom function to fetch Open Graph data
|
|
196
|
+
// Callbacks
|
|
197
|
+
onEmbed: null,
|
|
198
|
+
onLoad: null,
|
|
199
|
+
onError: null
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
addAttributes() {
|
|
204
|
+
return {
|
|
205
|
+
url: {
|
|
206
|
+
default: null
|
|
207
|
+
},
|
|
208
|
+
provider: {
|
|
209
|
+
default: null
|
|
210
|
+
},
|
|
211
|
+
embedData: {
|
|
212
|
+
default: null // Provider-specific data (id, user, etc.)
|
|
213
|
+
},
|
|
214
|
+
loaded: {
|
|
215
|
+
default: false // Has the embed been loaded (for privacy mode)
|
|
216
|
+
},
|
|
217
|
+
// Link card data (for generic URLs)
|
|
218
|
+
linkCard: {
|
|
219
|
+
default: null // { title, description, image, domain }
|
|
220
|
+
},
|
|
221
|
+
error: {
|
|
222
|
+
default: null
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
parseHTML() {
|
|
228
|
+
return [
|
|
229
|
+
{
|
|
230
|
+
tag: 'div[data-type="embed"]'
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
renderHTML({ HTMLAttributes, node }) {
|
|
236
|
+
return [
|
|
237
|
+
"div",
|
|
238
|
+
mergeAttributes(HTMLAttributes, {
|
|
239
|
+
"data-type": "embed",
|
|
240
|
+
"data-provider": node.attrs.provider,
|
|
241
|
+
class: "inkpen-embed"
|
|
242
|
+
}),
|
|
243
|
+
// Content rendered by NodeView
|
|
244
|
+
0
|
|
245
|
+
]
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
addNodeView() {
|
|
249
|
+
return ({ node, editor, getPos }) => {
|
|
250
|
+
const extension = this
|
|
251
|
+
|
|
252
|
+
// Create container
|
|
253
|
+
const container = document.createElement("div")
|
|
254
|
+
container.className = "inkpen-embed"
|
|
255
|
+
container.setAttribute("data-type", "embed")
|
|
256
|
+
container.setAttribute("data-provider", node.attrs.provider || "link")
|
|
257
|
+
|
|
258
|
+
const renderEmbed = () => {
|
|
259
|
+
container.innerHTML = ""
|
|
260
|
+
|
|
261
|
+
const provider = node.attrs.provider
|
|
262
|
+
const providerConfig = PROVIDERS[provider]
|
|
263
|
+
const isLoaded = node.attrs.loaded || !extension.options.privacyMode
|
|
264
|
+
const hasError = node.attrs.error
|
|
265
|
+
|
|
266
|
+
if (hasError) {
|
|
267
|
+
// Error state
|
|
268
|
+
container.innerHTML = `
|
|
269
|
+
<div class="inkpen-embed__error">
|
|
270
|
+
<span class="inkpen-embed__error-icon">⚠️</span>
|
|
271
|
+
<span class="inkpen-embed__error-text">${node.attrs.error}</span>
|
|
272
|
+
<button type="button" class="inkpen-embed__remove" title="Remove">×</button>
|
|
273
|
+
</div>
|
|
274
|
+
`
|
|
275
|
+
container.querySelector(".inkpen-embed__remove")?.addEventListener("click", () => {
|
|
276
|
+
if (typeof getPos === "function") {
|
|
277
|
+
editor.chain().focus().deleteRange({
|
|
278
|
+
from: getPos(),
|
|
279
|
+
to: getPos() + node.nodeSize
|
|
280
|
+
}).run()
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!provider || !providerConfig) {
|
|
287
|
+
// Link card for unknown URLs
|
|
288
|
+
renderLinkCard(container, node, editor, getPos)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!isLoaded) {
|
|
293
|
+
// Privacy placeholder
|
|
294
|
+
container.innerHTML = `
|
|
295
|
+
<div class="inkpen-embed__placeholder" style="--provider-color: ${providerConfig.color}">
|
|
296
|
+
<div class="inkpen-embed__placeholder-icon">${providerConfig.icon}</div>
|
|
297
|
+
<div class="inkpen-embed__placeholder-info">
|
|
298
|
+
<span class="inkpen-embed__placeholder-name">${providerConfig.name}</span>
|
|
299
|
+
<span class="inkpen-embed__placeholder-url">${extractDomain(node.attrs.url)}</span>
|
|
300
|
+
</div>
|
|
301
|
+
<button type="button" class="inkpen-embed__load-btn">Load ${providerConfig.name}</button>
|
|
302
|
+
</div>
|
|
303
|
+
`
|
|
304
|
+
container.querySelector(".inkpen-embed__load-btn")?.addEventListener("click", () => {
|
|
305
|
+
if (typeof getPos === "function") {
|
|
306
|
+
editor.chain().focus().updateAttributes("embed", { loaded: true }).run()
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Loaded embed
|
|
313
|
+
if (providerConfig.useWidget) {
|
|
314
|
+
renderWidget(container, node, providerConfig)
|
|
315
|
+
} else if (providerConfig.useScript) {
|
|
316
|
+
renderScript(container, node, providerConfig)
|
|
317
|
+
} else {
|
|
318
|
+
renderIframe(container, node, providerConfig)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
renderEmbed()
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
dom: container,
|
|
326
|
+
update: (updatedNode) => {
|
|
327
|
+
if (updatedNode.type.name !== "embed") return false
|
|
328
|
+
node = updatedNode
|
|
329
|
+
container.setAttribute("data-provider", node.attrs.provider || "link")
|
|
330
|
+
renderEmbed()
|
|
331
|
+
return true
|
|
332
|
+
},
|
|
333
|
+
selectNode: () => {
|
|
334
|
+
container.classList.add("is-selected")
|
|
335
|
+
},
|
|
336
|
+
deselectNode: () => {
|
|
337
|
+
container.classList.remove("is-selected")
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
addCommands() {
|
|
344
|
+
return {
|
|
345
|
+
insertEmbed: (url) => ({ commands, editor }) => {
|
|
346
|
+
const detected = detectProvider(url)
|
|
347
|
+
|
|
348
|
+
if (detected) {
|
|
349
|
+
const { provider, config, match } = detected
|
|
350
|
+
const embedData = config.extractId ? config.extractId(match) : match[1]
|
|
351
|
+
|
|
352
|
+
return commands.insertContent({
|
|
353
|
+
type: this.name,
|
|
354
|
+
attrs: {
|
|
355
|
+
url,
|
|
356
|
+
provider,
|
|
357
|
+
embedData,
|
|
358
|
+
loaded: !this.options.privacyMode
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Unknown URL - create link card
|
|
364
|
+
if (this.options.enableLinkCards) {
|
|
365
|
+
commands.insertContent({
|
|
366
|
+
type: this.name,
|
|
367
|
+
attrs: {
|
|
368
|
+
url,
|
|
369
|
+
provider: null,
|
|
370
|
+
linkCard: {
|
|
371
|
+
title: extractDomain(url),
|
|
372
|
+
description: null,
|
|
373
|
+
image: null,
|
|
374
|
+
domain: extractDomain(url)
|
|
375
|
+
},
|
|
376
|
+
loaded: true
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// Fetch Open Graph data if fetcher provided
|
|
381
|
+
if (this.options.linkCardFetcher) {
|
|
382
|
+
this.options.linkCardFetcher(url).then(data => {
|
|
383
|
+
// Update the node with fetched data
|
|
384
|
+
// This would need to find the node by URL
|
|
385
|
+
}).catch(() => {
|
|
386
|
+
// Keep basic link card
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return true
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return false
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
loadEmbed: () => ({ commands }) => {
|
|
397
|
+
return commands.updateAttributes(this.name, { loaded: true })
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
setEmbedError: (error) => ({ commands }) => {
|
|
401
|
+
return commands.updateAttributes(this.name, { error })
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
addKeyboardShortcuts() {
|
|
407
|
+
return {
|
|
408
|
+
Backspace: ({ editor }) => {
|
|
409
|
+
const { selection } = editor.state
|
|
410
|
+
const node = selection.node
|
|
411
|
+
|
|
412
|
+
if (node?.type.name === this.name) {
|
|
413
|
+
return editor.commands.deleteSelection()
|
|
414
|
+
}
|
|
415
|
+
return false
|
|
416
|
+
},
|
|
417
|
+
Delete: ({ editor }) => {
|
|
418
|
+
const { selection } = editor.state
|
|
419
|
+
const node = selection.node
|
|
420
|
+
|
|
421
|
+
if (node?.type.name === this.name) {
|
|
422
|
+
return editor.commands.deleteSelection()
|
|
423
|
+
}
|
|
424
|
+
return false
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
addPasteRules() {
|
|
430
|
+
return []
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
addProseMirrorPlugins() {
|
|
434
|
+
const extension = this
|
|
435
|
+
|
|
436
|
+
return [
|
|
437
|
+
new Plugin({
|
|
438
|
+
key: EMBED_KEY,
|
|
439
|
+
props: {
|
|
440
|
+
handlePaste(view, event) {
|
|
441
|
+
const text = event.clipboardData?.getData("text/plain")
|
|
442
|
+
if (!text) return false
|
|
443
|
+
|
|
444
|
+
// Check if it's a URL
|
|
445
|
+
const urlPattern = /^https?:\/\/[^\s]+$/
|
|
446
|
+
if (!urlPattern.test(text.trim())) return false
|
|
447
|
+
|
|
448
|
+
const url = text.trim()
|
|
449
|
+
const detected = detectProvider(url)
|
|
450
|
+
|
|
451
|
+
// Only handle if it's a known provider or link cards are enabled
|
|
452
|
+
if (detected || extension.options.enableLinkCards) {
|
|
453
|
+
event.preventDefault()
|
|
454
|
+
extension.editor.commands.insertEmbed(url)
|
|
455
|
+
return true
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return false
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
]
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
// Helper: Render iframe embed
|
|
467
|
+
function renderIframe(container, node, config) {
|
|
468
|
+
const { url, embedData } = node.attrs
|
|
469
|
+
let embedUrl
|
|
470
|
+
|
|
471
|
+
if (config.useFullUrl) {
|
|
472
|
+
embedUrl = config.getEmbedUrl(url)
|
|
473
|
+
} else if (config.extractId) {
|
|
474
|
+
const match = [null, ...Object.values(embedData)]
|
|
475
|
+
embedUrl = config.getEmbedUrl(match)
|
|
476
|
+
} else {
|
|
477
|
+
embedUrl = config.getEmbedUrl(embedData)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const wrapper = document.createElement("div")
|
|
481
|
+
wrapper.className = "inkpen-embed__wrapper"
|
|
482
|
+
|
|
483
|
+
if (config.aspectRatio) {
|
|
484
|
+
wrapper.style.aspectRatio = config.aspectRatio
|
|
485
|
+
}
|
|
486
|
+
if (config.maxWidth) {
|
|
487
|
+
wrapper.style.maxWidth = `${config.maxWidth}px`
|
|
488
|
+
}
|
|
489
|
+
if (config.height) {
|
|
490
|
+
wrapper.style.height = `${config.height}px`
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const iframe = document.createElement("iframe")
|
|
494
|
+
iframe.src = embedUrl
|
|
495
|
+
iframe.frameBorder = "0"
|
|
496
|
+
iframe.allowFullscreen = true
|
|
497
|
+
iframe.allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
498
|
+
iframe.loading = "lazy"
|
|
499
|
+
|
|
500
|
+
wrapper.appendChild(iframe)
|
|
501
|
+
container.appendChild(wrapper)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Helper: Render widget (Twitter, Instagram)
|
|
505
|
+
function renderWidget(container, node, config) {
|
|
506
|
+
const wrapper = document.createElement("div")
|
|
507
|
+
wrapper.className = "inkpen-embed__wrapper inkpen-embed__wrapper--widget"
|
|
508
|
+
|
|
509
|
+
if (config.name === "Twitter") {
|
|
510
|
+
const { user, id } = node.attrs.embedData
|
|
511
|
+
wrapper.innerHTML = `
|
|
512
|
+
<blockquote class="twitter-tweet" data-conversation="none">
|
|
513
|
+
<a href="https://twitter.com/${user}/status/${id}">Loading tweet...</a>
|
|
514
|
+
</blockquote>
|
|
515
|
+
`
|
|
516
|
+
loadScript(config.widgetScript, () => {
|
|
517
|
+
if (window.twttr?.widgets) {
|
|
518
|
+
window.twttr.widgets.load(wrapper)
|
|
519
|
+
}
|
|
520
|
+
})
|
|
521
|
+
} else if (config.name === "Instagram") {
|
|
522
|
+
const { type, id } = node.attrs.embedData
|
|
523
|
+
wrapper.innerHTML = `
|
|
524
|
+
<blockquote class="instagram-media" data-instgrm-captioned
|
|
525
|
+
data-instgrm-permalink="https://www.instagram.com/${type}/${id}/">
|
|
526
|
+
<a href="https://www.instagram.com/${type}/${id}/">Loading Instagram...</a>
|
|
527
|
+
</blockquote>
|
|
528
|
+
`
|
|
529
|
+
loadScript(config.widgetScript, () => {
|
|
530
|
+
if (window.instgrm?.Embeds) {
|
|
531
|
+
window.instgrm.Embeds.process(wrapper)
|
|
532
|
+
}
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
container.appendChild(wrapper)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Helper: Render script embed (Gist)
|
|
540
|
+
function renderScript(container, node, config) {
|
|
541
|
+
const wrapper = document.createElement("div")
|
|
542
|
+
wrapper.className = "inkpen-embed__wrapper inkpen-embed__wrapper--script"
|
|
543
|
+
|
|
544
|
+
const { user, id } = node.attrs.embedData
|
|
545
|
+
const scriptUrl = config.getScriptUrl([null, user, id])
|
|
546
|
+
|
|
547
|
+
// Create iframe to sandbox the gist
|
|
548
|
+
const iframe = document.createElement("iframe")
|
|
549
|
+
iframe.className = "inkpen-embed__gist-frame"
|
|
550
|
+
iframe.frameBorder = "0"
|
|
551
|
+
iframe.scrolling = "no"
|
|
552
|
+
|
|
553
|
+
iframe.onload = () => {
|
|
554
|
+
const doc = iframe.contentDocument
|
|
555
|
+
if (doc) {
|
|
556
|
+
doc.open()
|
|
557
|
+
doc.write(`
|
|
558
|
+
<!DOCTYPE html>
|
|
559
|
+
<html>
|
|
560
|
+
<head>
|
|
561
|
+
<base target="_blank">
|
|
562
|
+
<style>
|
|
563
|
+
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
|
564
|
+
.gist .gist-file { border: none !important; }
|
|
565
|
+
.gist .gist-meta { display: none !important; }
|
|
566
|
+
</style>
|
|
567
|
+
</head>
|
|
568
|
+
<body>
|
|
569
|
+
<script src="${scriptUrl}"></script>
|
|
570
|
+
</body>
|
|
571
|
+
</html>
|
|
572
|
+
`)
|
|
573
|
+
doc.close()
|
|
574
|
+
|
|
575
|
+
// Resize iframe to content
|
|
576
|
+
setTimeout(() => {
|
|
577
|
+
const height = doc.body.scrollHeight
|
|
578
|
+
iframe.style.height = `${height}px`
|
|
579
|
+
}, 500)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
wrapper.appendChild(iframe)
|
|
584
|
+
container.appendChild(wrapper)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Helper: Render link card
|
|
588
|
+
function renderLinkCard(container, node, editor, getPos) {
|
|
589
|
+
const { url, linkCard } = node.attrs
|
|
590
|
+
const data = linkCard || { domain: extractDomain(url) }
|
|
591
|
+
|
|
592
|
+
container.innerHTML = `
|
|
593
|
+
<a href="${url}" target="_blank" rel="noopener noreferrer" class="inkpen-link-card">
|
|
594
|
+
${data.image ? `<img src="${data.image}" alt="" class="inkpen-link-card__image">` : ""}
|
|
595
|
+
<div class="inkpen-link-card__content">
|
|
596
|
+
<span class="inkpen-link-card__title">${data.title || url}</span>
|
|
597
|
+
${data.description ? `<span class="inkpen-link-card__description">${data.description}</span>` : ""}
|
|
598
|
+
<span class="inkpen-link-card__domain">
|
|
599
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
600
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
|
601
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
|
|
602
|
+
</svg>
|
|
603
|
+
${data.domain}
|
|
604
|
+
</span>
|
|
605
|
+
</div>
|
|
606
|
+
</a>
|
|
607
|
+
`
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Helper: Load external script
|
|
611
|
+
const loadedScripts = new Set()
|
|
612
|
+
|
|
613
|
+
function loadScript(src, callback) {
|
|
614
|
+
if (loadedScripts.has(src)) {
|
|
615
|
+
callback?.()
|
|
616
|
+
return
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const script = document.createElement("script")
|
|
620
|
+
script.src = src
|
|
621
|
+
script.async = true
|
|
622
|
+
script.onload = () => {
|
|
623
|
+
loadedScripts.add(src)
|
|
624
|
+
callback?.()
|
|
625
|
+
}
|
|
626
|
+
document.body.appendChild(script)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export default Embed
|