Skip to content

Commit

Permalink
feat: youtube and twitter embeds for articles (#500)
Browse files Browse the repository at this point in the history
  • Loading branch information
alfonsobries authored Nov 27, 2023
1 parent 4e67f1c commit 1140b1f
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 178 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
"tailwind-merge": "^1.14.0",
"tippy.js": "^6.3.7",
"unified": "^11.0.4",
"unist-util-visit": "^5.0.0",
"wavesurfer.js": "^7.4.5"
}
}
263 changes: 102 additions & 161 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

35 changes: 33 additions & 2 deletions resources/css/_article.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,36 @@
.article-content > img,
.article-content > figure,
.article-content > pre,
.article-content > code {
.article-content > code,
.article-content > iframe,
.article-content > .twitter-embed-wrapper {
@apply my-4;
}

.article-content > .twitter-embed-wrapper > blockquote {
@apply hidden;
}

.article-content > .twitter-embed-wrapper {
@apply flex justify-center;
}

.article-content > .twitter-embed-wrapper-dark {
@apply hidden;
}

.article-content > .twitter-embed-wrapper iframe {
@apply !max-w-full;
}

.dark .twitter-embed-wrapper {
@apply !hidden;
}

.dark .twitter-embed-wrapper.twitter-embed-wrapper-dark {
@apply !flex;
}

.article-content > blockquote {
@apply my-6;
}
Expand All @@ -56,10 +82,15 @@
@apply mb-3 mt-6;
}

.article-content img {
.article-content img,
.article-content iframe {
@apply mx-auto rounded-lg;
}

.article-content iframe {
@apply aspect-video h-auto w-full;
}

.article-content h2,
.article-content h3,
.article-content h4 {
Expand Down
15 changes: 14 additions & 1 deletion resources/js/Pages/Articles/Components/ArticleContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import tippy, { inlinePositioning } from "tippy.js";
import { useDarkModeContext } from "@/Contexts/DarkModeContext";
import { extractDomain } from "@/Utils/extract-domain";
import { remarkFigurePlugin } from "@/Utils/Remark/remarkFigurePlugin";
import { remarkTwitterEmbedPlugin } from "@/Utils/Remark/remarkTwitterEmbedPlugin";
import { remarkYoutubeEmbedPlugin } from "@/Utils/Remark/remarkYoutubeEmbedPlugin";

interface Properties {
article: App.Data.Articles.ArticleData;
Expand Down Expand Up @@ -57,11 +59,22 @@ export const ArticleContent = ({ article }: Properties): JSX.Element => {
};
}, [isDark]);

useEffect(() => {
const script = document.createElement("script");
script.src = "https://platform.twitter.com/widgets.js";
script.async = true;
document.body.appendChild(script);

return () => {
document.body.removeChild(script);
};
}, []);

return (
<div className="article-content">
<Markdown
rehypePlugins={[rehypeRaw, [rehypeExternalLinks, { target: "_blank" }]]}
remarkPlugins={[remarkFigurePlugin, remarkGfm]}
remarkPlugins={[remarkFigurePlugin, remarkGfm, remarkYoutubeEmbedPlugin, remarkTwitterEmbedPlugin]}
components={{
table: ({ children }) => (
<div className="w-fit overflow-hidden rounded-xl border border-theme-secondary-300 dark:border-theme-dark-700">
Expand Down
3 changes: 2 additions & 1 deletion resources/js/Utils/Remark/remarkFigurePlugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type NodeWithChildren, remarkFigurePlugin } from "./remarkFigurePlugin";
import { remarkFigurePlugin } from "./remarkFigurePlugin";
import { type NodeWithChildren } from "./remarkPlugins.contract";

describe("remarkFigurePlugin", () => {
it("should transform tree correctly", () => {
Expand Down
14 changes: 1 addition & 13 deletions resources/js/Utils/Remark/remarkFigurePlugin.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
import html from "rehype-stringify";
import remark2rehype from "remark-rehype";
import { unified } from "unified";
import { type Node } from "unist";

export interface NodeWithChildren extends Node {
children: NodeWithChildren[];
alt?: string;
url?: string;
value?: string;
}

interface NodeOptionalChildren extends Omit<Node, "children"> {
children?: NodeWithChildren[];
value?: string;
}
import { type NodeOptionalChildren, type NodeWithChildren } from "./remarkPlugins.contract";

const transformTree = (tree: NodeWithChildren): void => {
const nodesToReplace: Array<{
Expand Down
13 changes: 13 additions & 0 deletions resources/js/Utils/Remark/remarkPlugins.contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { type Node } from "unist";

export interface NodeWithChildren extends Node {
children: NodeWithChildren[];
alt?: string;
url?: string;
value?: string;
}

export interface NodeOptionalChildren extends Omit<Node, "children"> {
children?: NodeWithChildren[];
value?: string;
}
82 changes: 82 additions & 0 deletions resources/js/Utils/Remark/remarkTwitterEmbedPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { type NodeWithChildren } from "./remarkPlugins.contract";
import { remarkTwitterEmbedPlugin } from "./remarkTwitterEmbedPlugin";

describe("remarkTwitterEmbedPlugin", () => {
it("transforms Twitter links into embeds", () => {
const tree: NodeWithChildren = {
type: "root",
children: [
{
type: "paragraph",
children: [
{
type: "image",
url: "twitter:bobconfer/status/1575764493795479552",
children: [],
},
],
},
],
};

remarkTwitterEmbedPlugin()(tree);

expect(tree.children[0].type).toBe("html");
expect(tree.children[0].value).toContain("twitter.com/bobconfer/status/1575764493795479552");
expect(tree.children[0].value).toContain("twitter-tweet");
});

it("does not transform non-Twitter links", () => {
const tree: NodeWithChildren = {
type: "root",
children: [
{
type: "paragraph",
children: [
{
type: "image",
url: "https://example.com/image.jpg",
children: [],
},
],
},
],
};

const originalTree = JSON.parse(JSON.stringify(tree));

remarkTwitterEmbedPlugin()(tree);

expect(tree).toEqual(originalTree);
});

it("does not transform Twitter links if not the only child in a paragraph", () => {
const tree: NodeWithChildren = {
type: "root",
children: [
{
type: "paragraph",
children: [
{
type: "text",
value: "Random text",
children: [],
},
{
type: "image",
url: "twitter:bobconfer/status/1575764493795479552",
children: [],
},
],
},
],
};

const originalTree = JSON.parse(JSON.stringify(tree));

remarkTwitterEmbedPlugin()(tree);

expect(tree).toEqual(originalTree);
});
});
23 changes: 23 additions & 0 deletions resources/js/Utils/Remark/remarkTwitterEmbedPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { visit } from "unist-util-visit";
import { type NodeWithChildren } from "./remarkPlugins.contract";

const transformTwitterNodes = (tree: NodeWithChildren): void => {
visit(tree, "paragraph", (node: NodeWithChildren, index, parent) => {
if (node.children.length === 1 && node.children[0].type === "image") {
const imageNode = node.children[0];

if (typeof imageNode.url === "string" && imageNode.url.startsWith("twitter:")) {
const twitterPath = imageNode.url.replace("twitter:", "");

parent!.children.splice(index!, 1, {
type: "html",
value: `<div class="twitter-embed-wrapper"><blockquote class="twitter-tweet"><a href="https://twitter.com/${twitterPath}"></a></blockquote></div><div class="twitter-embed-wrapper twitter-embed-wrapper-dark"><blockquote class="twitter-tweet" data-theme="dark"><a href="https://twitter.com/${twitterPath}"></a></blockquote></div>`,
children: [],
});
}
}
});
};

export const remarkTwitterEmbedPlugin = (): ((tree: NodeWithChildren) => void) => transformTwitterNodes;
54 changes: 54 additions & 0 deletions resources/js/Utils/Remark/remarkYoutubeEmbedPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { type NodeWithChildren } from "./remarkPlugins.contract";
import { remarkYoutubeEmbedPlugin } from "./remarkYoutubeEmbedPlugin";

describe("remarkYoutubeEmbedPlugin", () => {
it("transforms YouTube links into iframe embeds", () => {
const tree: NodeWithChildren = {
type: "root",
children: [
{
type: "paragraph",
children: [
{
type: "image",
url: "youtube:bH1lHCirCGI",
children: [],
},
],
},
],
};

remarkYoutubeEmbedPlugin()(tree);

expect(tree.children[0].children[0].type).toBe("html");
expect(tree.children[0].children[0].value).toBe(
'<iframe src="https://www.youtube.com/embed/bH1lHCirCGI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
);
});

it("does not transform non-YouTube links", () => {
const tree: NodeWithChildren = {
type: "root",
children: [
{
type: "paragraph",
children: [
{
type: "image",
url: "https://example.com/image.jpg",
children: [],
},
],
},
],
};

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const originalTree = JSON.parse(JSON.stringify(tree));

remarkYoutubeEmbedPlugin()(tree);

expect(tree).toEqual(originalTree);
});
});
16 changes: 16 additions & 0 deletions resources/js/Utils/Remark/remarkYoutubeEmbedPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { visit } from "unist-util-visit";
import { type NodeWithChildren } from "./remarkPlugins.contract";

const transformYouTubeNodes = (tree: NodeWithChildren): void => {
visit(tree, "image", (node: NodeWithChildren) => {
if (typeof node.url === "string" && node.url.startsWith("youtube:")) {
const videoId = node.url.replace("youtube:", "");

// Convert the node to an HTML node with the iframe embed
node.type = "html";
node.value = `<iframe src="https://www.youtube.com/embed/${videoId}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
}
});
};

export const remarkYoutubeEmbedPlugin = (): ((tree: NodeWithChildren) => void) => transformYouTubeNodes;

0 comments on commit 1140b1f

Please sign in to comment.