@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.
@@ -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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
- } from "./chunk-67ETNQMN.js";
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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
- } from "./chunk-67ETNQMN.js";
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
- // ./editor.tsx (the canonical definition site).
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.15",
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.54"
35
+ "ampless": "^1.0.0-beta.56"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@tiptap/core": "^3.23.6",
@@ -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
- };