@ampless/plugin-youtube 1.0.0-beta.15 → 1.0.0-beta.17
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/chunk-53IJFCTF.js +77 -0
- package/dist/editor.d.ts +30 -30
- package/dist/editor.js +5 -35
- package/dist/index.js +6 -4
- package/package.json +2 -2
- package/dist/chunk-67ETNQMN.js +0 -39
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// src/shared.tsx
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
var YOUTUBE_URL = /^https:\/\/(?:www\.)?(?:youtube\.com\/watch\?v=([\w-]{11})|youtu\.be\/([\w-]{11}))(?:[?&#]\S*)?$/;
|
|
4
|
+
var VIDEO_ID = /^[\w-]{11}$/;
|
|
5
|
+
function YouTubeEmbed({
|
|
6
|
+
videoId,
|
|
7
|
+
start,
|
|
8
|
+
title
|
|
9
|
+
}) {
|
|
10
|
+
if (typeof videoId !== "string" || !VIDEO_ID.test(videoId)) return null;
|
|
11
|
+
const query = typeof start === "number" && Number.isFinite(start) && start > 0 ? `?start=${Math.floor(start)}` : "";
|
|
12
|
+
const src = `https://www.youtube-nocookie.com/embed/${videoId}${query}`;
|
|
13
|
+
return /* @__PURE__ */ jsx(
|
|
14
|
+
"iframe",
|
|
15
|
+
{
|
|
16
|
+
src,
|
|
17
|
+
title: title ?? `YouTube video ${videoId}`,
|
|
18
|
+
loading: "lazy",
|
|
19
|
+
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
20
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
21
|
+
allowFullScreen: true,
|
|
22
|
+
width: 560,
|
|
23
|
+
height: 315,
|
|
24
|
+
style: { width: "100%", aspectRatio: "16 / 9", height: "auto", border: 0 }
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
function parseYoutubeUrl(url) {
|
|
29
|
+
const trimmed = url.trim();
|
|
30
|
+
const m = trimmed.match(YOUTUBE_URL);
|
|
31
|
+
if (!m) return null;
|
|
32
|
+
return m[1] ?? m[2] ?? null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/adapters.ts
|
|
36
|
+
function placeholderAttrs(attrs) {
|
|
37
|
+
const out = {
|
|
38
|
+
"data-ampless-youtube": "",
|
|
39
|
+
"data-video-id": String(attrs.videoId ?? ""),
|
|
40
|
+
class: "ampless-youtube-placeholder"
|
|
41
|
+
};
|
|
42
|
+
if (attrs.start != null && Number.isFinite(Number(attrs.start))) {
|
|
43
|
+
out["data-start"] = String(attrs.start);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function escapeAttr(v) {
|
|
48
|
+
return v.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
49
|
+
}
|
|
50
|
+
function attrsToHtmlString(attrs) {
|
|
51
|
+
return Object.entries(attrs).map(([k, v]) => v === "" ? k : `${k}="${escapeAttr(v)}"`).join(" ");
|
|
52
|
+
}
|
|
53
|
+
var tiptapNodeToMarkdown = {
|
|
54
|
+
amplessYoutube: (node) => {
|
|
55
|
+
const videoId = String(node.attrs?.videoId ?? "").trim();
|
|
56
|
+
if (!videoId) return null;
|
|
57
|
+
return `https://youtu.be/${videoId}`;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var tiptapNodeToHtml = {
|
|
61
|
+
amplessYoutube: (node) => {
|
|
62
|
+
const videoId = String(node.attrs?.videoId ?? "").trim();
|
|
63
|
+
if (!videoId) return null;
|
|
64
|
+
const attrs = placeholderAttrs(node.attrs ?? {});
|
|
65
|
+
const url = `https://youtu.be/${videoId}`;
|
|
66
|
+
return `<div ${attrsToHtmlString(attrs)}><a href="${escapeAttr(url)}">${escapeAttr(url)}</a></div>`;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
YOUTUBE_URL,
|
|
72
|
+
YouTubeEmbed,
|
|
73
|
+
parseYoutubeUrl,
|
|
74
|
+
placeholderAttrs,
|
|
75
|
+
tiptapNodeToMarkdown,
|
|
76
|
+
tiptapNodeToHtml
|
|
77
|
+
};
|
package/dist/editor.d.ts
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import { Node } from '@tiptap/core';
|
|
2
2
|
import { TiptapNodeHtmlAdapters, TiptapNodeMarkdownAdapters } from 'ampless';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* tiptap → markdown adapter map. Serialises `amplessYoutube` nodes back
|
|
6
|
+
* to a bare `https://youtu.be/<videoId>` URL line so the admin's
|
|
7
|
+
* "format: tiptap → markdown" body switch is lossless. The reverse
|
|
8
|
+
* direction (URL → embed node on paste) is handled by the existing paste
|
|
9
|
+
* rule + `extractSingleUrl` path in the runtime.
|
|
10
|
+
*
|
|
11
|
+
* `update-ampless` reads this export (via namespace import `* as`) and
|
|
12
|
+
* wires it into `installAdminTiptapNodeMarkdown`.
|
|
13
|
+
*/
|
|
14
|
+
declare const tiptapNodeToMarkdown: TiptapNodeMarkdownAdapters;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* tiptap → html adapter map. Serialises `amplessYoutube` nodes to the
|
|
18
|
+
* canonical placeholder div `<div data-ampless-youtube data-video-id="..."
|
|
19
|
+
* class="ampless-youtube-placeholder">…</div>` so the admin's
|
|
20
|
+
* "format: tiptap → html" body switch is lossless. The div is what
|
|
21
|
+
* `Node.parseHTML`'s `tag: 'div[data-ampless-youtube]'` rule restores
|
|
22
|
+
* from. It is an admin format-switch interchange form; public rendering
|
|
23
|
+
* expands YouTube embeds from the `tiptap` / `markdown` walkers, while
|
|
24
|
+
* `format: 'html'` preserves the div literally.
|
|
25
|
+
*
|
|
26
|
+
* `markdown → html` is a 2-hop via `generateJSON` (in admin format-switch);
|
|
27
|
+
* this adapter is reused by that path — no duplicate logic needed.
|
|
28
|
+
*
|
|
29
|
+
* `update-ampless` reads this export (via namespace import `* as`) and
|
|
30
|
+
* wires it into `installAdminTiptapNodeHtml`.
|
|
31
|
+
*/
|
|
32
|
+
declare const tiptapNodeToHtml: TiptapNodeHtmlAdapters;
|
|
33
|
+
|
|
4
34
|
declare module '@tiptap/core' {
|
|
5
35
|
interface Commands<ReturnType> {
|
|
6
36
|
amplessYoutube: {
|
|
@@ -36,34 +66,4 @@ declare const youtubeEditor: {
|
|
|
36
66
|
*/
|
|
37
67
|
declare const editorExtension: Node<any, any>;
|
|
38
68
|
|
|
39
|
-
/**
|
|
40
|
-
* tiptap → markdown adapter map. Serialises `amplessYoutube` nodes back
|
|
41
|
-
* to a bare `https://youtu.be/<videoId>` URL line so the admin's
|
|
42
|
-
* "format: tiptap → markdown" body switch is lossless. The reverse
|
|
43
|
-
* direction (URL → embed node on paste) is handled by the existing paste
|
|
44
|
-
* rule + `extractSingleUrl` path in the runtime.
|
|
45
|
-
*
|
|
46
|
-
* `update-ampless` reads this export (via namespace import `* as`) and
|
|
47
|
-
* wires it into `installAdminTiptapNodeMarkdown`.
|
|
48
|
-
*/
|
|
49
|
-
declare const tiptapNodeToMarkdown: TiptapNodeMarkdownAdapters;
|
|
50
|
-
|
|
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
69
|
export { AmplessYoutubeNode, editorExtension, tiptapNodeToHtml, tiptapNodeToMarkdown, youtubeEditor };
|
package/dist/editor.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import {
|
|
3
3
|
YOUTUBE_URL,
|
|
4
|
-
parseYoutubeUrl
|
|
5
|
-
|
|
4
|
+
parseYoutubeUrl,
|
|
5
|
+
placeholderAttrs,
|
|
6
|
+
tiptapNodeToHtml,
|
|
7
|
+
tiptapNodeToMarkdown
|
|
8
|
+
} from "./chunk-53IJFCTF.js";
|
|
6
9
|
|
|
7
10
|
// src/editor.tsx
|
|
8
11
|
import { Node, mergeAttributes } from "@tiptap/core";
|
|
@@ -23,23 +26,6 @@ function getBareUrlLinkHref(el) {
|
|
|
23
26
|
if (el.textContent?.trim() !== linkText) return null;
|
|
24
27
|
return href;
|
|
25
28
|
}
|
|
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
|
-
}
|
|
43
29
|
var AmplessYoutubeNode = Node.create({
|
|
44
30
|
name: "amplessYoutube",
|
|
45
31
|
group: "block",
|
|
@@ -178,22 +164,6 @@ var youtubeEditor = {
|
|
|
178
164
|
extension: AmplessYoutubeNode
|
|
179
165
|
};
|
|
180
166
|
var editorExtension = AmplessYoutubeNode;
|
|
181
|
-
var tiptapNodeToMarkdown = {
|
|
182
|
-
amplessYoutube: (node) => {
|
|
183
|
-
const videoId = String(node.attrs?.videoId ?? "").trim();
|
|
184
|
-
if (!videoId) return null;
|
|
185
|
-
return `https://youtu.be/${videoId}`;
|
|
186
|
-
}
|
|
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
|
-
const url = `https://youtu.be/${videoId}`;
|
|
194
|
-
return `<div ${attrsToHtmlString(attrs)}><a href="${escapeAttr(url)}">${escapeAttr(url)}</a></div>`;
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
167
|
export {
|
|
198
168
|
AmplessYoutubeNode,
|
|
199
169
|
editorExtension,
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
YOUTUBE_URL,
|
|
3
3
|
YouTubeEmbed,
|
|
4
|
-
parseYoutubeUrl
|
|
5
|
-
|
|
4
|
+
parseYoutubeUrl,
|
|
5
|
+
tiptapNodeToMarkdown
|
|
6
|
+
} from "./chunk-53IJFCTF.js";
|
|
6
7
|
|
|
7
8
|
// src/index.tsx
|
|
8
9
|
import { definePlugin } from "ampless";
|
|
@@ -29,7 +30,7 @@ function youtubePlugin(opts = {}) {
|
|
|
29
30
|
// the canonical placeholder div (emitted by the admin's tiptap→html
|
|
30
31
|
// switch) into the same `<YouTubeEmbed>` the tiptap / markdown
|
|
31
32
|
// walkers render. Attribute names match `placeholderAttrs()` in
|
|
32
|
-
// ./
|
|
33
|
+
// ./adapters.ts (the canonical definition site).
|
|
33
34
|
htmlPlaceholder: {
|
|
34
35
|
flagAttr: "data-ampless-youtube",
|
|
35
36
|
attrsFromElement: (attribs) => {
|
|
@@ -49,7 +50,8 @@ function youtubePlugin(opts = {}) {
|
|
|
49
50
|
return /* @__PURE__ */ jsx(YouTubeEmbed, { videoId });
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
|
-
]
|
|
53
|
+
],
|
|
54
|
+
tiptapNodeToMarkdown
|
|
53
55
|
});
|
|
54
56
|
}
|
|
55
57
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/plugin-youtube",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.17",
|
|
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-beta.
|
|
35
|
+
"ampless": "^1.0.0-beta.56"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@tiptap/core": "^3.23.6",
|
package/dist/chunk-67ETNQMN.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// src/shared.tsx
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
var YOUTUBE_URL = /^https:\/\/(?:www\.)?(?:youtube\.com\/watch\?v=([\w-]{11})|youtu\.be\/([\w-]{11}))(?:[?&#]\S*)?$/;
|
|
4
|
-
var VIDEO_ID = /^[\w-]{11}$/;
|
|
5
|
-
function YouTubeEmbed({
|
|
6
|
-
videoId,
|
|
7
|
-
start,
|
|
8
|
-
title
|
|
9
|
-
}) {
|
|
10
|
-
if (typeof videoId !== "string" || !VIDEO_ID.test(videoId)) return null;
|
|
11
|
-
const query = typeof start === "number" && Number.isFinite(start) && start > 0 ? `?start=${Math.floor(start)}` : "";
|
|
12
|
-
const src = `https://www.youtube-nocookie.com/embed/${videoId}${query}`;
|
|
13
|
-
return /* @__PURE__ */ jsx(
|
|
14
|
-
"iframe",
|
|
15
|
-
{
|
|
16
|
-
src,
|
|
17
|
-
title: title ?? `YouTube video ${videoId}`,
|
|
18
|
-
loading: "lazy",
|
|
19
|
-
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
20
|
-
referrerPolicy: "strict-origin-when-cross-origin",
|
|
21
|
-
allowFullScreen: true,
|
|
22
|
-
width: 560,
|
|
23
|
-
height: 315,
|
|
24
|
-
style: { width: "100%", aspectRatio: "16 / 9", height: "auto", border: 0 }
|
|
25
|
-
}
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
function parseYoutubeUrl(url) {
|
|
29
|
-
const trimmed = url.trim();
|
|
30
|
-
const m = trimmed.match(YOUTUBE_URL);
|
|
31
|
-
if (!m) return null;
|
|
32
|
-
return m[1] ?? m[2] ?? null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export {
|
|
36
|
-
YOUTUBE_URL,
|
|
37
|
-
YouTubeEmbed,
|
|
38
|
-
parseYoutubeUrl
|
|
39
|
-
};
|