@ampless/plugin-x-embed 1.0.0-alpha.1 → 1.0.0-alpha.10
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-6SPJOY2C.js → chunk-ZB3Q7IQG.js} +1 -1
- package/dist/editor.d.ts +42 -1
- package/dist/editor.js +112 -7
- package/dist/index.js +14 -2
- package/package.json +5 -4
|
@@ -22,7 +22,7 @@ function hasTweetIn(post) {
|
|
|
22
22
|
}
|
|
23
23
|
if (post.format === "html") {
|
|
24
24
|
if (typeof post.body !== "string") return false;
|
|
25
|
-
return post.body.includes("twitter-tweet");
|
|
25
|
+
return post.body.includes("twitter-tweet") || post.body.toLowerCase().includes("data-ampless-tweet");
|
|
26
26
|
}
|
|
27
27
|
return false;
|
|
28
28
|
}
|
package/dist/editor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Node } from '@tiptap/core';
|
|
2
|
+
import { TiptapNodeHtmlAdapters, TiptapNodeMarkdownAdapters } from 'ampless';
|
|
2
3
|
|
|
3
4
|
declare module '@tiptap/core' {
|
|
4
5
|
interface Commands<ReturnType> {
|
|
@@ -23,5 +24,45 @@ declare const AmplessTweetNode: Node<any, any>;
|
|
|
23
24
|
declare const tweetEditor: {
|
|
24
25
|
extension: Node<any, any>;
|
|
25
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Canonical named export consumed by the codegen'd
|
|
29
|
+
* `_editor-bootstrap.tsx`. Plugins MUST export this symbol from
|
|
30
|
+
* their `./editor` module (= the subpath declared in
|
|
31
|
+
* `package.json#amplessPlugin.editorExports`) for the
|
|
32
|
+
* auto-wiring to find them.
|
|
33
|
+
*/
|
|
34
|
+
declare const editorExtension: Node<any, any>;
|
|
35
|
+
|
|
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
|
+
/**
|
|
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;
|
|
26
67
|
|
|
27
|
-
export { AmplessTweetNode, tweetEditor };
|
|
68
|
+
export { AmplessTweetNode, editorExtension, tiptapNodeToHtml, tiptapNodeToMarkdown, tweetEditor };
|
package/dist/editor.js
CHANGED
|
@@ -2,10 +2,40 @@
|
|
|
2
2
|
import {
|
|
3
3
|
TWEET_URL,
|
|
4
4
|
parseTweetUrl
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-ZB3Q7IQG.js";
|
|
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, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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) =>
|
|
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-
|
|
38
|
-
|
|
39
|
-
}),
|
|
114
|
+
mergeAttributes(HTMLAttributes, placeholderAttrs({
|
|
115
|
+
tweetId: HTMLAttributes["data-tweet-id"]
|
|
116
|
+
})),
|
|
40
117
|
["span", {}, `Tweet: ${HTMLAttributes["data-tweet-id"] ?? ""}`]
|
|
41
118
|
];
|
|
42
119
|
},
|
|
@@ -60,7 +137,15 @@ var AmplessTweetNode = Node.create({
|
|
|
60
137
|
return [
|
|
61
138
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
139
|
{
|
|
63
|
-
|
|
140
|
+
// tiptap's paste rule internally calls `String.prototype.matchAll`
|
|
141
|
+
// which throws when the regex doesn't carry the `g` flag. Clone
|
|
142
|
+
// the source regex (which is anchored `^...$` for use in the
|
|
143
|
+
// renderer / markdown extractor) into a global form here. Anchored
|
|
144
|
+
// global is fine — without the `m` flag, `^...$` only anchor at the
|
|
145
|
+
// start / end of the **entire input**, so this matches at most
|
|
146
|
+
// once for the pasted input as a whole (= exactly the single-URL
|
|
147
|
+
// paste case we care about).
|
|
148
|
+
find: new RegExp(TWEET_URL.source, "g"),
|
|
64
149
|
handler: ({ range, match, commands }) => {
|
|
65
150
|
const tweetId = match[1];
|
|
66
151
|
if (!tweetId) return;
|
|
@@ -77,7 +162,27 @@ var AmplessTweetNode = Node.create({
|
|
|
77
162
|
var tweetEditor = {
|
|
78
163
|
extension: AmplessTweetNode
|
|
79
164
|
};
|
|
165
|
+
var editorExtension = AmplessTweetNode;
|
|
166
|
+
var tiptapNodeToMarkdown = {
|
|
167
|
+
amplessTweet: (node) => {
|
|
168
|
+
const tweetId = String(node.attrs?.tweetId ?? "").trim();
|
|
169
|
+
if (!tweetId) return null;
|
|
170
|
+
return `https://x.com/i/status/${tweetId}`;
|
|
171
|
+
}
|
|
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
|
+
const url = `https://x.com/i/status/${tweetId}`;
|
|
179
|
+
return `<div ${attrsToHtmlString(attrs)}><a href="${escapeAttr(url)}">${escapeAttr(url)}</a></div>`;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
80
182
|
export {
|
|
81
183
|
AmplessTweetNode,
|
|
184
|
+
editorExtension,
|
|
185
|
+
tiptapNodeToHtml,
|
|
186
|
+
tiptapNodeToMarkdown,
|
|
82
187
|
tweetEditor
|
|
83
188
|
};
|
package/dist/index.js
CHANGED
|
@@ -3,14 +3,14 @@ import {
|
|
|
3
3
|
TweetEmbed,
|
|
4
4
|
hasTweetIn,
|
|
5
5
|
parseTweetUrl
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZB3Q7IQG.js";
|
|
7
7
|
|
|
8
8
|
// src/index.tsx
|
|
9
9
|
import { definePlugin } from "ampless";
|
|
10
10
|
import { jsx } from "react/jsx-runtime";
|
|
11
11
|
function xEmbedPlugin(opts = {}) {
|
|
12
12
|
return definePlugin({
|
|
13
|
-
name: opts.name ?? "
|
|
13
|
+
name: opts.name ?? "x-embed",
|
|
14
14
|
apiVersion: 1,
|
|
15
15
|
packageName: "@ampless/plugin-x-embed",
|
|
16
16
|
trust_level: "trusted",
|
|
@@ -23,6 +23,18 @@ function xEmbedPlugin(opts = {}) {
|
|
|
23
23
|
render: (node) => {
|
|
24
24
|
const tweetId = String(node.attrs?.tweetId ?? "");
|
|
25
25
|
return /* @__PURE__ */ jsx(TweetEmbed, { tweetId });
|
|
26
|
+
},
|
|
27
|
+
// Opt into the public html walker so `format: 'html'` posts expand
|
|
28
|
+
// the canonical placeholder div (emitted by the admin's tiptap→html
|
|
29
|
+
// switch) into the same `<TweetEmbed>` the tiptap / markdown walkers
|
|
30
|
+
// render. Attribute names match `placeholderAttrs()` in ./editor.tsx
|
|
31
|
+
// (the canonical definition site). The page-level widgets.js is
|
|
32
|
+
// injected via `publicPostScript` + the `hasTweetIn` html branch.
|
|
33
|
+
htmlPlaceholder: {
|
|
34
|
+
flagAttr: "data-ampless-tweet",
|
|
35
|
+
attrsFromElement: (attribs) => ({
|
|
36
|
+
tweetId: attribs["data-tweet-id"] ?? ""
|
|
37
|
+
})
|
|
26
38
|
}
|
|
27
39
|
},
|
|
28
40
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/plugin-x-embed",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.10",
|
|
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.
|
|
35
|
+
"ampless": "1.0.0-alpha.48"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@tiptap/core": "^3.23.6",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
],
|
|
53
53
|
"amplessPlugin": {
|
|
54
54
|
"apiVersion": 1,
|
|
55
|
-
"name": "
|
|
55
|
+
"name": "x-embed",
|
|
56
56
|
"trustLevel": "trusted",
|
|
57
57
|
"capabilities": [
|
|
58
58
|
"contentFields",
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"displayName": {
|
|
62
62
|
"en": "x.com (Twitter) embeds",
|
|
63
63
|
"ja": "x.com (Twitter) 埋め込み"
|
|
64
|
-
}
|
|
64
|
+
},
|
|
65
|
+
"editorExports": "./editor"
|
|
65
66
|
},
|
|
66
67
|
"scripts": {
|
|
67
68
|
"build": "tsup",
|