diff --git a/next.config.js b/next.config.js index 5a9aa8b3..83e120a4 100644 --- a/next.config.js +++ b/next.config.js @@ -92,7 +92,11 @@ module.exports = withBundleAnalyzer( dangerouslyAllowSVG: true, contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox; style-src 'unsafe-inline';", - remotePatterns: [{ hostname: "**" }], + remotePatterns: [ + { hostname: "**" }, + { protocol: "https", hostname: "pbs.twimg.com" }, + { protocol: "https", hostname: "abs.twimg.com" }, + ], }, async generateBuildId() { diff --git a/package.json b/package.json index 201dd2e8..5fdf10f1 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "react-popper": "^2.3.0", "react-sortablejs": "6.1.4", "react-string-replace": "^1.1.0", + "react-tweet": "^2.0.1", "react-virtuoso": "4.3.6", "refractor": "4.8.1", "rehype-autolink-headings": "6.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6538063..5e609351 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,6 +232,9 @@ dependencies: react-string-replace: specifier: ^1.1.0 version: 1.1.0 + react-tweet: + specifier: ^2.0.1 + version: 2.0.1(react-dom@18.2.0)(react@18.2.0) react-virtuoso: specifier: 4.3.6 version: 4.3.6(react-dom@18.2.0)(react@18.2.0) @@ -4322,6 +4325,12 @@ packages: string.prototype.matchall: 4.0.8 dev: true + /@swc/helpers@0.4.14: + resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==} + dependencies: + tslib: 2.5.0 + dev: false + /@swc/helpers@0.5.1: resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==} dependencies: @@ -6909,6 +6918,13 @@ packages: whatwg-url: 12.0.1 dev: false + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.21.5 + dev: false + /dayjs@1.11.7: resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==} dev: false @@ -11605,6 +11621,20 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-tweet@2.0.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-r8Yrv+cW8ajpCcT2AJCIji9crbbVxiaYSagqU4IW4Btsm3b3Ui65pFOV9gZmnt2xorn8L98kqj8REEuGxwnaeA==} + peerDependencies: + react: '>= 18.0.0' + react-dom: '>= 18.0.0' + dependencies: + '@swc/helpers': 0.4.14 + clsx: 1.2.1 + date-fns: 2.30.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + swr: 2.1.5(react@18.2.0) + dev: false + /react-virtuoso@4.3.6(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-t2yzA9QKwMrcECLytVfZcBZMusuQ9Ahgt62SL8uC/sYm9Sk/h14sYTJC0MVO6CAtl3BzKiLKSC79Vp+W3KOXMg==} engines: {node: '>=10'} @@ -12673,6 +12703,15 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /swr@2.1.5(react@18.2.0): + resolution: {integrity: sha512-/OhfZMcEpuz77KavXST5q6XE9nrOBOVcBLWjMT+oAE/kQHyE3PASrevXCtQDZ8aamntOfFkbVJp7Il9tNBQWrw==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: false diff --git a/src/components/ui/Tweet.tsx b/src/components/ui/Tweet.tsx new file mode 100644 index 00000000..3c7e703e --- /dev/null +++ b/src/components/ui/Tweet.tsx @@ -0,0 +1,12 @@ +import Image from "next/image" +import { Tweet as ReactTweet } from "react-tweet" +import type { TweetComponents } from "react-tweet" + +const components: TweetComponents = { + AvatarImg: (props) => , + MediaImg: (props) => , +} + +export function Tweet({ id }: { id: string }) { + return +} diff --git a/src/markdown/index.ts b/src/markdown/index.ts index 1b898923..e4d9cb1c 100644 --- a/src/markdown/index.ts +++ b/src/markdown/index.ts @@ -32,6 +32,7 @@ import { APlayer } from "~/components/ui/APlayer" import { ZoomedImage } from "~/components/ui/Image" import { Mention } from "~/components/ui/Mention" import { Mermaid } from "~/components/ui/Mermaid" +import { Tweet } from "~/components/ui/Tweet" import { rehypeAudio } from "./rehype-audio" import { @@ -41,6 +42,7 @@ import { } from "./rehype-custom-wrapper" import { rehypeImage } from "./rehype-image" import { rehypeTable } from "./rehype-table" +import { rehypeTweet } from "./rehype-tweet" import { rehypeWrapCode } from "./rehype-wrap-code" import { rehypeExternalLink } from "./rehyper-external-link" import { remarkCallout } from "./remark-callout" @@ -165,6 +167,7 @@ export const renderPageContent = ( .use(rehypeExternalLink) .use(rehypeWrapCode) .use(rehypeInferDescriptionMeta) + .use(rehypeTweet) .use(rehypeRewrite, { selector: "p, li", rewrite: (node: any) => { @@ -213,6 +216,7 @@ export const renderPageContent = ( mention: Mention, mermaid: Mermaid, audio: APlayer, + tweet: Tweet, style: Style, } as any, }) diff --git a/src/markdown/rehype-tweet.ts b/src/markdown/rehype-tweet.ts new file mode 100644 index 00000000..6cbe30d0 --- /dev/null +++ b/src/markdown/rehype-tweet.ts @@ -0,0 +1,31 @@ +import { Root } from "rehype-raw" +import { Plugin } from "unified" +import { visit } from "unist-util-visit" + +export const rehypeTweet: Plugin, Root> = () => (tree) => { + visit(tree, { tagName: "a" }, (node, i, parent) => { + if ( + !parent || + !("tagName" in parent) || + parent.tagName !== "p" || + parent.children.length > 1 + ) + return + + const tweetRegex = + /^https?:\/\/(?:www\.)?twitter\.com\/\w+\/status\/(\d+)(?:\?.*)?(?:#.*)?$/ + + const href = node.properties?.href + + if (typeof href === "string") { + const match = href.match(tweetRegex) + if (match && match[0] === href) { + const tweetId = match[1] + node.tagName = "tweet" + node.properties = { + id: tweetId, + } + } + } + }) +}