@ampless/plugin-youtube 1.0.0-alpha.6 → 1.0.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editor.d.ts +20 -2
- package/dist/editor.js +96 -5
- package/package.json +2 -2
package/dist/editor.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Node } from '@tiptap/core';
|
|
2
|
-
import { TiptapNodeMarkdownAdapters } from 'ampless';
|
|
2
|
+
import { TiptapNodeHtmlAdapters, TiptapNodeMarkdownAdapters } from 'ampless';
|
|
3
3
|
|
|
4
4
|
declare module '@tiptap/core' {
|
|
5
5
|
interface Commands<ReturnType> {
|
|
@@ -48,4 +48,22 @@ declare const editorExtension: Node<any, any>;
|
|
|
48
48
|
*/
|
|
49
49
|
declare const tiptapNodeToMarkdown: TiptapNodeMarkdownAdapters;
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* tiptap → html adapter map. Serialises `amplessYoutube` nodes to the
|
|
53
|
+
* canonical placeholder div `<div data-ampless-youtube data-video-id="..."
|
|
54
|
+
* class="ampless-youtube-placeholder">…</div>` so the admin's
|
|
55
|
+
* "format: tiptap → html" body switch is lossless. The div is what
|
|
56
|
+
* `Node.parseHTML`'s `tag: 'div[data-ampless-youtube]'` rule restores
|
|
57
|
+
* from. It is an admin format-switch interchange form; public rendering
|
|
58
|
+
* expands YouTube embeds from the `tiptap` / `markdown` walkers, while
|
|
59
|
+
* `format: 'html'` preserves the div literally.
|
|
60
|
+
*
|
|
61
|
+
* `markdown → html` is a 2-hop via `generateJSON` (in admin format-switch);
|
|
62
|
+
* this adapter is reused by that path — no duplicate logic needed.
|
|
63
|
+
*
|
|
64
|
+
* `update-ampless` reads this export (via namespace import `* as`) and
|
|
65
|
+
* wires it into `installAdminTiptapNodeHtml`.
|
|
66
|
+
*/
|
|
67
|
+
declare const tiptapNodeToHtml: TiptapNodeHtmlAdapters;
|
|
68
|
+
|
|
69
|
+
export { AmplessYoutubeNode, editorExtension, tiptapNodeToHtml, tiptapNodeToMarkdown, youtubeEditor };
|
package/dist/editor.js
CHANGED
|
@@ -6,6 +6,40 @@ import {
|
|
|
6
6
|
|
|
7
7
|
// src/editor.tsx
|
|
8
8
|
import { Node, mergeAttributes } from "@tiptap/core";
|
|
9
|
+
function getBareUrlLinkHref(el) {
|
|
10
|
+
if (el.tagName.toLowerCase() === "a") {
|
|
11
|
+
return el.getAttribute("href");
|
|
12
|
+
}
|
|
13
|
+
if (el.tagName.toLowerCase() !== "p") return null;
|
|
14
|
+
const children = Array.from(el.children);
|
|
15
|
+
if (children.length !== 1) return null;
|
|
16
|
+
const link = children[0];
|
|
17
|
+
if (!(link instanceof HTMLElement)) return null;
|
|
18
|
+
if (link.tagName.toLowerCase() !== "a") return null;
|
|
19
|
+
const href = link.getAttribute("href")?.trim();
|
|
20
|
+
if (!href) return null;
|
|
21
|
+
const linkText = link.textContent?.trim();
|
|
22
|
+
if (linkText !== href) return null;
|
|
23
|
+
if (el.textContent?.trim() !== linkText) return null;
|
|
24
|
+
return href;
|
|
25
|
+
}
|
|
26
|
+
function placeholderAttrs(attrs) {
|
|
27
|
+
const out = {
|
|
28
|
+
"data-ampless-youtube": "",
|
|
29
|
+
"data-video-id": String(attrs.videoId ?? ""),
|
|
30
|
+
class: "ampless-youtube-placeholder"
|
|
31
|
+
};
|
|
32
|
+
if (attrs.start != null && Number.isFinite(Number(attrs.start))) {
|
|
33
|
+
out["data-start"] = String(attrs.start);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function escapeAttr(v) {
|
|
38
|
+
return v.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
39
|
+
}
|
|
40
|
+
function attrsToHtmlString(attrs) {
|
|
41
|
+
return Object.entries(attrs).map(([k, v]) => v === "" ? k : `${k}="${escapeAttr(v)}"`).join(" ");
|
|
42
|
+
}
|
|
9
43
|
var AmplessYoutubeNode = Node.create({
|
|
10
44
|
name: "amplessYoutube",
|
|
11
45
|
group: "block",
|
|
@@ -16,7 +50,12 @@ var AmplessYoutubeNode = Node.create({
|
|
|
16
50
|
return {
|
|
17
51
|
videoId: {
|
|
18
52
|
default: "",
|
|
19
|
-
parseHTML: (el) =>
|
|
53
|
+
parseHTML: (el) => {
|
|
54
|
+
const dataVideoId = el.getAttribute("data-video-id");
|
|
55
|
+
if (dataVideoId != null) return dataVideoId;
|
|
56
|
+
const href = getBareUrlLinkHref(el);
|
|
57
|
+
return href ? parseYoutubeUrl(href) ?? "" : "";
|
|
58
|
+
},
|
|
20
59
|
renderHTML: (attrs) => ({
|
|
21
60
|
"data-video-id": String(attrs.videoId ?? "")
|
|
22
61
|
})
|
|
@@ -37,16 +76,59 @@ var AmplessYoutubeNode = Node.create({
|
|
|
37
76
|
return [
|
|
38
77
|
{
|
|
39
78
|
tag: "div[data-ampless-youtube]"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
tag: "p",
|
|
82
|
+
priority: 100,
|
|
83
|
+
getAttrs: (el) => {
|
|
84
|
+
const href = getBareUrlLinkHref(el) ?? "";
|
|
85
|
+
const videoId = parseYoutubeUrl(href);
|
|
86
|
+
if (!videoId) return false;
|
|
87
|
+
return { videoId, start: null };
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
// Defensive fallback for genuinely top-level bare-URL `<a>` tags
|
|
92
|
+
// (no parent block, or directly under `<body>` — = HTML fragments
|
|
93
|
+
// without a paragraph wrapper at all). The primary case is the
|
|
94
|
+
// markdown bare URL line `<p><a href=URL>URL</a></p>`, which is
|
|
95
|
+
// already handled by the `tag: 'p'` rule above (priority 100,
|
|
96
|
+
// `getBareUrlLinkHref`). This rule covers the rare standalone-link
|
|
97
|
+
// shape that bypasses that rule entirely.
|
|
98
|
+
//
|
|
99
|
+
// Scope is deliberately narrow:
|
|
100
|
+
// 1. Link text equals href — `<a href="URL">caption</a>` stays
|
|
101
|
+
// a captioned Link, not silently swallowed into an embed.
|
|
102
|
+
// 2. Parent is null or `<body>` only. Inside any content block
|
|
103
|
+
// (`<p>`, `<li>`, `<blockquote>`, `<div>`, …) the autolink
|
|
104
|
+
// stays an inline Link mark to preserve the surrounding
|
|
105
|
+
// structure. Promoting it would split `<li>` into an empty
|
|
106
|
+
// paragraph item plus a list-external embed, break
|
|
107
|
+
// blockquotes, etc.
|
|
108
|
+
tag: "a[href]",
|
|
109
|
+
priority: 100,
|
|
110
|
+
getAttrs: (el) => {
|
|
111
|
+
const link = el;
|
|
112
|
+
const parent = link.parentElement;
|
|
113
|
+
if (parent && parent.tagName.toLowerCase() !== "body") return false;
|
|
114
|
+
const href = link.getAttribute("href")?.trim() ?? "";
|
|
115
|
+
if (!href) return false;
|
|
116
|
+
const linkText = link.textContent?.trim() ?? "";
|
|
117
|
+
if (linkText !== href) return false;
|
|
118
|
+
const videoId = parseYoutubeUrl(href);
|
|
119
|
+
if (!videoId) return false;
|
|
120
|
+
return { videoId, start: null };
|
|
121
|
+
}
|
|
40
122
|
}
|
|
41
123
|
];
|
|
42
124
|
},
|
|
43
125
|
renderHTML({ HTMLAttributes }) {
|
|
44
126
|
return [
|
|
45
127
|
"div",
|
|
46
|
-
mergeAttributes(HTMLAttributes, {
|
|
47
|
-
"data-
|
|
48
|
-
|
|
49
|
-
}),
|
|
128
|
+
mergeAttributes(HTMLAttributes, placeholderAttrs({
|
|
129
|
+
videoId: HTMLAttributes["data-video-id"],
|
|
130
|
+
start: HTMLAttributes["data-start"]
|
|
131
|
+
})),
|
|
50
132
|
["span", {}, `YouTube: ${HTMLAttributes["data-video-id"] ?? ""}`]
|
|
51
133
|
];
|
|
52
134
|
},
|
|
@@ -103,9 +185,18 @@ var tiptapNodeToMarkdown = {
|
|
|
103
185
|
return `https://youtu.be/${videoId}`;
|
|
104
186
|
}
|
|
105
187
|
};
|
|
188
|
+
var tiptapNodeToHtml = {
|
|
189
|
+
amplessYoutube: (node) => {
|
|
190
|
+
const videoId = String(node.attrs?.videoId ?? "").trim();
|
|
191
|
+
if (!videoId) return null;
|
|
192
|
+
const attrs = placeholderAttrs(node.attrs ?? {});
|
|
193
|
+
return `<div ${attrsToHtmlString(attrs)}><span>YouTube: ${escapeAttr(videoId)}</span></div>`;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
106
196
|
export {
|
|
107
197
|
AmplessYoutubeNode,
|
|
108
198
|
editorExtension,
|
|
199
|
+
tiptapNodeToHtml,
|
|
109
200
|
tiptapNodeToMarkdown,
|
|
110
201
|
youtubeEditor
|
|
111
202
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/plugin-youtube",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.8",
|
|
4
4
|
"description": "YouTube embed plugin for ampless — renders youtube.com / youtu.be URLs inline as iframes in tiptap + markdown posts",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@tiptap/core": "^3",
|
|
34
34
|
"react": "^18 || ^19",
|
|
35
|
-
"ampless": "1.0.0-alpha.
|
|
35
|
+
"ampless": "1.0.0-alpha.47"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@tiptap/core": "^3.23.6",
|