@ampless/plugin-x-embed 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 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> {
@@ -47,4 +47,22 @@ declare const editorExtension: Node<any, any>;
47
47
  */
48
48
  declare const tiptapNodeToMarkdown: TiptapNodeMarkdownAdapters;
49
49
 
50
- export { AmplessTweetNode, editorExtension, tiptapNodeToMarkdown, tweetEditor };
50
+ /**
51
+ * tiptap → html adapter map. Serialises `amplessTweet` nodes to the
52
+ * canonical placeholder div `<div data-ampless-tweet data-tweet-id="..."
53
+ * class="ampless-tweet-placeholder">…</div>` so the admin's
54
+ * "format: tiptap → html" body switch is lossless. The div is what
55
+ * `Node.parseHTML`'s `tag: 'div[data-ampless-tweet]'` rule restores
56
+ * from. It is an admin format-switch interchange form; public rendering
57
+ * expands tweet embeds from the `tiptap` / `markdown` walkers, while
58
+ * `format: 'html'` preserves the div literally.
59
+ *
60
+ * `markdown → html` is a 2-hop via `generateJSON` (in admin format-switch);
61
+ * this adapter is reused by that path — no duplicate logic needed.
62
+ *
63
+ * `update-ampless` reads this export (via namespace import `* as`) and
64
+ * wires it into `installAdminTiptapNodeHtml`.
65
+ */
66
+ declare const tiptapNodeToHtml: TiptapNodeHtmlAdapters;
67
+
68
+ export { AmplessTweetNode, editorExtension, tiptapNodeToHtml, tiptapNodeToMarkdown, tweetEditor };
package/dist/editor.js CHANGED
@@ -6,6 +6,36 @@ 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
+ return {
28
+ "data-ampless-tweet": "",
29
+ "data-tweet-id": String(attrs.tweetId ?? ""),
30
+ class: "ampless-tweet-placeholder"
31
+ };
32
+ }
33
+ function escapeAttr(v) {
34
+ return v.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
35
+ }
36
+ function attrsToHtmlString(attrs) {
37
+ return Object.entries(attrs).map(([k, v]) => v === "" ? k : `${k}="${escapeAttr(v)}"`).join(" ");
38
+ }
9
39
  var AmplessTweetNode = Node.create({
10
40
  name: "amplessTweet",
11
41
  group: "block",
@@ -16,7 +46,12 @@ var AmplessTweetNode = Node.create({
16
46
  return {
17
47
  tweetId: {
18
48
  default: "",
19
- parseHTML: (el) => el.getAttribute("data-tweet-id") ?? "",
49
+ parseHTML: (el) => {
50
+ const dataTweetId = el.getAttribute("data-tweet-id");
51
+ if (dataTweetId != null) return dataTweetId;
52
+ const href = getBareUrlLinkHref(el);
53
+ return href ? parseTweetUrl(href) ?? "" : "";
54
+ },
20
55
  renderHTML: (attrs) => ({
21
56
  "data-tweet-id": String(attrs.tweetId ?? "")
22
57
  })
@@ -27,16 +62,58 @@ var AmplessTweetNode = Node.create({
27
62
  return [
28
63
  {
29
64
  tag: "div[data-ampless-tweet]"
65
+ },
66
+ {
67
+ tag: "p",
68
+ priority: 100,
69
+ getAttrs: (el) => {
70
+ const href = getBareUrlLinkHref(el) ?? "";
71
+ const tweetId = parseTweetUrl(href);
72
+ if (!tweetId) return false;
73
+ return { tweetId };
74
+ }
75
+ },
76
+ {
77
+ // Defensive fallback for genuinely top-level bare-URL `<a>` tags
78
+ // (no parent block, or directly under `<body>` — = HTML fragments
79
+ // without a paragraph wrapper at all). The primary case is the
80
+ // markdown bare URL line `<p><a href=URL>URL</a></p>`, which is
81
+ // already handled by the `tag: 'p'` rule above (priority 100,
82
+ // `getBareUrlLinkHref`). This rule covers the rare standalone-link
83
+ // shape that bypasses that rule entirely.
84
+ //
85
+ // Scope is deliberately narrow:
86
+ // 1. Link text equals href — `<a href="URL">caption</a>` stays
87
+ // a captioned Link, not silently swallowed into an embed.
88
+ // 2. Parent is null or `<body>` only. Inside any content block
89
+ // (`<p>`, `<li>`, `<blockquote>`, `<div>`, …) the autolink
90
+ // stays an inline Link mark to preserve the surrounding
91
+ // structure. Promoting it would split `<li>` into an empty
92
+ // paragraph item plus a list-external embed, break
93
+ // blockquotes, etc.
94
+ tag: "a[href]",
95
+ priority: 100,
96
+ getAttrs: (el) => {
97
+ const link = el;
98
+ const parent = link.parentElement;
99
+ if (parent && parent.tagName.toLowerCase() !== "body") return false;
100
+ const href = link.getAttribute("href")?.trim() ?? "";
101
+ if (!href) return false;
102
+ const linkText = link.textContent?.trim() ?? "";
103
+ if (linkText !== href) return false;
104
+ const tweetId = parseTweetUrl(href);
105
+ if (!tweetId) return false;
106
+ return { tweetId };
107
+ }
30
108
  }
31
109
  ];
32
110
  },
33
111
  renderHTML({ HTMLAttributes }) {
34
112
  return [
35
113
  "div",
36
- mergeAttributes(HTMLAttributes, {
37
- "data-ampless-tweet": "",
38
- class: "ampless-tweet-placeholder"
39
- }),
114
+ mergeAttributes(HTMLAttributes, placeholderAttrs({
115
+ tweetId: HTMLAttributes["data-tweet-id"]
116
+ })),
40
117
  ["span", {}, `Tweet: ${HTMLAttributes["data-tweet-id"] ?? ""}`]
41
118
  ];
42
119
  },
@@ -93,9 +170,18 @@ var tiptapNodeToMarkdown = {
93
170
  return `https://x.com/i/status/${tweetId}`;
94
171
  }
95
172
  };
173
+ var tiptapNodeToHtml = {
174
+ amplessTweet: (node) => {
175
+ const tweetId = String(node.attrs?.tweetId ?? "").trim();
176
+ if (!tweetId) return null;
177
+ const attrs = placeholderAttrs(node.attrs ?? {});
178
+ return `<div ${attrsToHtmlString(attrs)}><span>Tweet: ${escapeAttr(tweetId)}</span></div>`;
179
+ }
180
+ };
96
181
  export {
97
182
  AmplessTweetNode,
98
183
  editorExtension,
184
+ tiptapNodeToHtml,
99
185
  tiptapNodeToMarkdown,
100
186
  tweetEditor
101
187
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/plugin-x-embed",
3
- "version": "1.0.0-alpha.6",
3
+ "version": "1.0.0-alpha.8",
4
4
  "description": "x.com (Twitter) embed plugin for ampless — renders tweet URLs inline as blockquotes hydrated by widgets.js",
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.45"
35
+ "ampless": "1.0.0-alpha.47"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@tiptap/core": "^3.23.6",