@ampless/plugin-x-embed 1.0.0-alpha.5 → 1.0.0-alpha.7

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,4 +1,5 @@
1
1
  import { Node } from '@tiptap/core';
2
+ import { TiptapNodeMarkdownAdapters } from 'ampless';
2
3
 
3
4
  declare module '@tiptap/core' {
4
5
  interface Commands<ReturnType> {
@@ -32,4 +33,18 @@ declare const tweetEditor: {
32
33
  */
33
34
  declare const editorExtension: Node<any, any>;
34
35
 
35
- export { AmplessTweetNode, editorExtension, tweetEditor };
36
+ /**
37
+ * tiptap → markdown adapter map. Serialises `amplessTweet` nodes back
38
+ * to a bare `https://x.com/i/status/<tweetId>` URL line so the admin's
39
+ * "format: tiptap → markdown" body switch is lossless. The URL form
40
+ * uses `i` as the handle — confirmed to match the existing `TWEET_URL`
41
+ * regex (`/[A-Za-z0-9_]{1,15}/`). The reverse direction (URL → embed
42
+ * node on paste) is handled by the existing paste rule + `extractSingleUrl`
43
+ * path in the runtime.
44
+ *
45
+ * `update-ampless` reads this export (via namespace import `* as`) and
46
+ * wires it into `installAdminTiptapNodeMarkdown`.
47
+ */
48
+ declare const tiptapNodeToMarkdown: TiptapNodeMarkdownAdapters;
49
+
50
+ export { AmplessTweetNode, editorExtension, tiptapNodeToMarkdown, tweetEditor };
package/dist/editor.js CHANGED
@@ -6,6 +6,23 @@ 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
+ }
9
26
  var AmplessTweetNode = Node.create({
10
27
  name: "amplessTweet",
11
28
  group: "block",
@@ -16,7 +33,12 @@ var AmplessTweetNode = Node.create({
16
33
  return {
17
34
  tweetId: {
18
35
  default: "",
19
- parseHTML: (el) => el.getAttribute("data-tweet-id") ?? "",
36
+ parseHTML: (el) => {
37
+ const dataTweetId = el.getAttribute("data-tweet-id");
38
+ if (dataTweetId != null) return dataTweetId;
39
+ const href = getBareUrlLinkHref(el);
40
+ return href ? parseTweetUrl(href) ?? "" : "";
41
+ },
20
42
  renderHTML: (attrs) => ({
21
43
  "data-tweet-id": String(attrs.tweetId ?? "")
22
44
  })
@@ -27,6 +49,49 @@ var AmplessTweetNode = Node.create({
27
49
  return [
28
50
  {
29
51
  tag: "div[data-ampless-tweet]"
52
+ },
53
+ {
54
+ tag: "p",
55
+ priority: 100,
56
+ getAttrs: (el) => {
57
+ const href = getBareUrlLinkHref(el) ?? "";
58
+ const tweetId = parseTweetUrl(href);
59
+ if (!tweetId) return false;
60
+ return { tweetId };
61
+ }
62
+ },
63
+ {
64
+ // Defensive fallback for genuinely top-level bare-URL `<a>` tags
65
+ // (no parent block, or directly under `<body>` — = HTML fragments
66
+ // without a paragraph wrapper at all). The primary case is the
67
+ // markdown bare URL line `<p><a href=URL>URL</a></p>`, which is
68
+ // already handled by the `tag: 'p'` rule above (priority 100,
69
+ // `getBareUrlLinkHref`). This rule covers the rare standalone-link
70
+ // shape that bypasses that rule entirely.
71
+ //
72
+ // Scope is deliberately narrow:
73
+ // 1. Link text equals href — `<a href="URL">caption</a>` stays
74
+ // a captioned Link, not silently swallowed into an embed.
75
+ // 2. Parent is null or `<body>` only. Inside any content block
76
+ // (`<p>`, `<li>`, `<blockquote>`, `<div>`, …) the autolink
77
+ // stays an inline Link mark to preserve the surrounding
78
+ // structure. Promoting it would split `<li>` into an empty
79
+ // paragraph item plus a list-external embed, break
80
+ // blockquotes, etc.
81
+ tag: "a[href]",
82
+ priority: 100,
83
+ getAttrs: (el) => {
84
+ const link = el;
85
+ const parent = link.parentElement;
86
+ if (parent && parent.tagName.toLowerCase() !== "body") return false;
87
+ const href = link.getAttribute("href")?.trim() ?? "";
88
+ if (!href) return false;
89
+ const linkText = link.textContent?.trim() ?? "";
90
+ if (linkText !== href) return false;
91
+ const tweetId = parseTweetUrl(href);
92
+ if (!tweetId) return false;
93
+ return { tweetId };
94
+ }
30
95
  }
31
96
  ];
32
97
  },
@@ -86,8 +151,16 @@ var tweetEditor = {
86
151
  extension: AmplessTweetNode
87
152
  };
88
153
  var editorExtension = AmplessTweetNode;
154
+ var tiptapNodeToMarkdown = {
155
+ amplessTweet: (node) => {
156
+ const tweetId = String(node.attrs?.tweetId ?? "").trim();
157
+ if (!tweetId) return null;
158
+ return `https://x.com/i/status/${tweetId}`;
159
+ }
160
+ };
89
161
  export {
90
162
  AmplessTweetNode,
91
163
  editorExtension,
164
+ tiptapNodeToMarkdown,
92
165
  tweetEditor
93
166
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/plugin-x-embed",
3
- "version": "1.0.0-alpha.5",
3
+ "version": "1.0.0-alpha.7",
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.44"
35
+ "ampless": "1.0.0-alpha.46"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@tiptap/core": "^3.23.6",