├── .editorconfig ├── .github ├── funding.yml ├── issue_template.md ├── pull_request_template.md └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── example ├── .env.example ├── .gitignore ├── .vscode │ └── settings.json ├── app │ ├── layout.tsx │ └── page.tsx ├── components │ ├── GitHubShareButton.tsx │ ├── RandomTweet.tsx │ ├── TweetPage.tsx │ ├── anchor.tsx │ └── styles.module.css ├── demo.jpg ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── [tweetId].tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── get-tweet-ast │ │ │ └── [tweetId].ts │ │ └── tweets.ts │ └── dynamic │ │ └── [tweetId].tsx ├── public │ ├── favicon.png │ └── robots.txt ├── readme.md ├── styles │ └── globals.css ├── tsconfig.json └── yarn.lock ├── lerna.json ├── license ├── package.json ├── packages ├── react-static-tweets │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── client.ts │ │ ├── format-number.ts │ │ ├── html │ │ │ ├── handlers.tsx │ │ │ └── node.tsx │ │ ├── index.ts │ │ ├── tweet-client.tsx │ │ ├── tweet.tsx │ │ ├── twitter-layout │ │ │ ├── components │ │ │ │ ├── anchor.tsx │ │ │ │ ├── code.tsx │ │ │ │ ├── containers.tsx │ │ │ │ ├── embedded-tweet.tsx │ │ │ │ ├── headings.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── lists.tsx │ │ │ │ ├── media.tsx │ │ │ │ ├── table.tsx │ │ │ │ ├── text.tsx │ │ │ │ ├── tweet │ │ │ │ │ ├── tweet-header.module.css │ │ │ │ │ ├── tweet-header.tsx │ │ │ │ │ ├── tweet-info.tsx │ │ │ │ │ └── tweet.tsx │ │ │ │ └── twitter.tsx │ │ │ ├── skeleton.tsx │ │ │ └── tweet-skeleton.tsx │ │ └── twitter.tsx │ ├── styles.css │ └── tsconfig.json └── static-tweets │ ├── package.json │ ├── readme.md │ ├── src │ ├── fetchTweetAst.ts │ ├── index.ts │ ├── markdown │ │ ├── htmlToAst.ts │ │ ├── markdownToAst.ts │ │ ├── rehype-minify.ts │ │ ├── rehype-tweet.ts │ │ └── schema.ts │ └── twitter │ │ ├── api.ts │ │ ├── embed │ │ └── tweet-html.ts │ │ ├── getTweetHtml.ts │ │ └── tweet-html.ts │ └── tsconfig.json ├── readme.md ├── tsconfig.base.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: [transitive-bullshit] 2 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node-version: [14.x, 16.x, 18.x, 19.x] 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Use Node.js ${{ matrix.node-version }} 13 | uses: actions/setup-node@v2 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | cache: yarn 17 | 18 | - run: yarn install --frozen-lockfile 19 | - run: yarn build 20 | - run: yarn test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # builds 7 | build 8 | dist 9 | 10 | # misc 11 | .DS_Store 12 | .env 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | .cache 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | .vscode/settings.json 23 | 24 | .next 25 | .vercel 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .snapshots/ 2 | .next/ 3 | .vercel/ 4 | build/ 5 | docs/ 6 | node_modules/ 7 | lerna.json 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "useTabs": false, 6 | "tabWidth": 2, 7 | "bracketSpacing": true, 8 | "bracketSameLine": false, 9 | "arrowParens": "always", 10 | "trailingComma": "none" 11 | } 12 | -------------------------------------------------------------------------------- /example/.env.example: -------------------------------------------------------------------------------- 1 | # this is only used for the random tweet fetching and isn't necessary to test the demo normally 2 | #TWITTER_API_TOKEN= -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | -------------------------------------------------------------------------------- /example/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /example/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | // core styles shared by all of react-static-tweets (required) 4 | import 'react-static-tweets/styles.css' 5 | 6 | export default function RootLayout({ 7 | // Layouts must accept a children prop. 8 | // This will be populated with nested layouts or pages 9 | children 10 | }: { 11 | children: React.ReactNode 12 | }) { 13 | return ( 14 | 15 | {children} 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /example/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { fetchTweetAst } from 'static-tweets' 2 | import { ExampleTweetPage } from 'components/TweetPage' 3 | import { notFound } from 'next/navigation' 4 | 5 | // default tweet to show on the homepage 6 | const tweetId = '1352687755621351425' 7 | 8 | export default async function HomePage() { 9 | const tweetAst = await fetchTweetAst(tweetId) 10 | if (!tweetAst) { 11 | console.warn('tweet not found', tweetId) 12 | return notFound() 13 | } 14 | 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /example/components/GitHubShareButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './styles.module.css' 4 | 5 | export const GitHubShareButton: React.FC = () => { 6 | return ( 7 | 15 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /example/components/RandomTweet.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React from 'react' 4 | import { useState } from 'react' 5 | import Link from 'next/link' 6 | 7 | import styles from './styles.module.css' 8 | 9 | const APP_URL = 'https://react-static-tweets.vercel.app' 10 | const cn = (arr) => arr.filter(Boolean).join(' ') 11 | 12 | function getRandomId(id, tweets) { 13 | let i = 0 14 | while (true) { 15 | const randomId = tweets[Math.floor(Math.random() * tweets.length)] 16 | if (randomId !== id) return randomId 17 | // Make sure to not create an infinite loop 18 | i++ 19 | if (i >= tweets.length) return id 20 | } 21 | } 22 | 23 | async function getError(res) { 24 | if (res.headers.get('Content-Type').includes('application/json')) { 25 | const data = await res.json() 26 | return data.errors[0] 27 | } 28 | return { message: (await res.text()) || res.statusText } 29 | } 30 | 31 | export function RandomTweet({ initialId }) { 32 | const [{ id, loading, error, success }, setState] = useState({ 33 | id: initialId, 34 | loading: false 35 | } as any) 36 | 37 | const fetchTweet = async (e) => { 38 | e.preventDefault() 39 | setState({ id, loading: true }) 40 | 41 | const res = await fetch('/api/tweets') 42 | 43 | if (res.ok) { 44 | const { tweets } = await res.json() 45 | return setState({ 46 | id: getRandomId(id, tweets), 47 | loading: false, 48 | success: true 49 | }) 50 | } 51 | 52 | const error = await getError(res) 53 | 54 | setState({ id, loading: false, error }) 55 | } 56 | 57 | return ( 58 |
59 | 60 | {APP_URL}/{id} 61 | 62 | 63 |
64 | 80 | 81 | {error && ⚠️ Error: {error.message}. Please try again} 82 |
83 |
84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /example/components/TweetPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tweet } from 'react-static-tweets' 3 | import { GitHubShareButton } from './GitHubShareButton' 4 | import { RandomTweet } from './RandomTweet' 5 | import styles from './styles.module.css' 6 | 7 | const defaultRandomTweet = '1358199505280262150' 8 | 9 | export const ExampleTweetPage: React.FC<{ 10 | tweetId: string 11 | tweetAst: any 12 | }> = ({ tweetAst }) => { 13 | return ( 14 | <> 15 |
16 |

React Static Tweets Demo

17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /example/components/anchor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { forwardRef } from 'react' 3 | import styles from './styles.module.css' 4 | 5 | export const A = forwardRef( 6 | ({ children, href, title, blank = true, onClick }: any, ref: any) => ( 7 | 16 | {blank ? <>{children} » : children} 17 | 18 | ) 19 | ) 20 | -------------------------------------------------------------------------------- /example/components/styles.module.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 420px) { 2 | .githubCorner { 3 | display: none; 4 | } 5 | } 6 | 7 | .githubCorner:hover .octoArm { 8 | animation: octocat-wave 560ms ease-in-out; 9 | } 10 | 11 | @keyframes octocat-wave { 12 | 0%, 13 | 100% { 14 | transform: rotate(0); 15 | } 16 | 20%, 17 | 60% { 18 | transform: rotate(-25deg); 19 | } 20 | 40%, 21 | 80% { 22 | transform: rotate(10deg); 23 | } 24 | } 25 | 26 | .anchor { 27 | --tweet-link-color: #2b7bb9; 28 | color: var(--tweet-link-color); 29 | } 30 | 31 | .anchor:hover { 32 | --tweet-link-color-hover: #3b94d9; 33 | text-decoration: none; 34 | color: var(--tweet-link-color-hover); 35 | } 36 | 37 | .title { 38 | text-align: center; 39 | margin-bottom: 1em; 40 | } 41 | 42 | .random-tweet-container { 43 | padding: 2em 0; 44 | } 45 | 46 | .random-tweet { 47 | font-size: 0.875rem; 48 | } 49 | 50 | .random-tweet > span { 51 | margin-left: 0.5rem; 52 | color: #ff1a1a; 53 | animation: changed 0.5s; 54 | } 55 | 56 | .generate-tweet-button { 57 | --accents-5: #666666; 58 | height: 1rem; 59 | font-size: 0.875rem; 60 | text-decoration: underline; 61 | color: var(--accents-5); 62 | background: transparent; 63 | cursor: pointer; 64 | border: none; 65 | outline: none; 66 | margin-top: 0.25rem; 67 | } 68 | 69 | .generate-tweet-button:hover { 70 | text-decoration: none; 71 | } 72 | 73 | .generate-tweet-button.tweet-loading { 74 | cursor: wait; 75 | text-decoration: none; 76 | animation: changed 1s; 77 | } 78 | 79 | .id { 80 | animation: updated 1.7s; 81 | } 82 | 83 | @keyframes updated { 84 | 30% { 85 | background-color: yellow; 86 | } 87 | 100% { 88 | background-color: transparent; 89 | } 90 | } 91 | 92 | @keyframes changed { 93 | 0% { 94 | opacity: 0; 95 | } 96 | 100% { 97 | opacity: 100; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /example/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/react-static-tweets/e2184d706c60c5e6ee8f63fb576bc3da832c11e6/example/demo.jpg -------------------------------------------------------------------------------- /example/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /example/next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: process.env.ANALYZE === 'true' 3 | }) 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = withBundleAnalyzer({ 7 | experimental: { 8 | appDir: true 9 | }, 10 | images: { 11 | domains: ['pbs.twimg.com'], 12 | formats: ['image/avif', 'image/webp'] 13 | } 14 | }) 15 | 16 | module.exports = nextConfig 17 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-static-tweets-example", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "analyze": "cross-env ANALYZE=true next build", 10 | "analyze:server": "cross-env BUNDLE_ANALYZE=server next build", 11 | "analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build", 12 | "deploy": "vercel --prod" 13 | }, 14 | "dependencies": { 15 | "@vercel/fetch": "^6.2.0", 16 | "cors": "^2.8.5", 17 | "date-fns": "^2.29.3", 18 | "next": "^13.1.1", 19 | "node-fetch": "^2.6.1", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-static-tweets": "^2.0.0", 23 | "static-tweets": "^2.0.0", 24 | "swr": "^2.0.0" 25 | }, 26 | "devDependencies": { 27 | "@next/bundle-analyzer": "^13.1.1", 28 | "@types/node": "^18.11.18", 29 | "@types/react": "^18.0.26", 30 | "cross-env": "^7.0.2", 31 | "typescript": "^4.9.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/pages/[tweetId].tsx: -------------------------------------------------------------------------------- 1 | import { GetStaticProps } from 'next' 2 | import { fetchTweetAst } from 'static-tweets' 3 | import { ExampleTweetPage } from 'components/TweetPage' 4 | 5 | export const getStaticProps: GetStaticProps = async (context) => { 6 | const tweetId = context.params.tweetId as string 7 | 8 | try { 9 | const tweetAst = await fetchTweetAst(tweetId) 10 | if (!tweetAst) { 11 | console.warn('tweet not found', tweetId) 12 | return { 13 | notFound: true 14 | } 15 | } 16 | 17 | return { 18 | props: { 19 | tweetId, 20 | tweetAst 21 | }, 22 | revalidate: 30 23 | } 24 | } catch (err) { 25 | console.error('error fetching tweet info', tweetId, err) 26 | 27 | throw err 28 | } 29 | } 30 | 31 | export async function getStaticPaths() { 32 | return { 33 | paths: ['/1352687755621351425', '/1358199505280262150'], 34 | fallback: true 35 | } 36 | } 37 | 38 | export default ExampleTweetPage 39 | -------------------------------------------------------------------------------- /example/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | 4 | import '../styles/globals.css' 5 | 6 | // core styles shared by all of react-static-tweets (required) 7 | import 'react-static-tweets/styles.css' 8 | 9 | export default function App({ Component, pageProps }) { 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | React Static Tweets Demo 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /example/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Document, { Html, Head, Main, NextScript } from 'next/document' 3 | 4 | export default class MyDocument extends Document { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/pages/api/get-tweet-ast/[tweetId].ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next' 2 | import { fetchTweetAst } from 'static-tweets' 3 | import Cors from 'cors' 4 | 5 | const cors = initMiddleware( 6 | Cors({ 7 | methods: ['GET', 'OPTIONS'] 8 | }) 9 | ) 10 | 11 | export default async ( 12 | req: NextApiRequest, 13 | res: NextApiResponse 14 | ): Promise => { 15 | await cors(req, res) 16 | 17 | if (req.method === 'OPTIONS') { 18 | return res.status(200).send({}) 19 | } 20 | 21 | if (req.method !== 'GET') { 22 | return res.status(405).send({ error: 'method not allowed' }) 23 | } 24 | 25 | const tweetId = req.query.tweetId as string 26 | 27 | if (!tweetId) { 28 | return res 29 | .status(400) 30 | .send({ error: 'missing required parameter "tweetId"' }) 31 | } 32 | 33 | console.log('getTweetAst', tweetId) 34 | const tweetAst = await fetchTweetAst(tweetId) 35 | console.log('tweetAst', tweetId, tweetAst) 36 | 37 | res.status(200).json(tweetAst) 38 | } 39 | 40 | // Helper method to wait for a middleware to execute before continuing 41 | // And to throw an error when an error happens in a middleware 42 | function initMiddleware(middleware) { 43 | return (req, res) => 44 | new Promise((resolve, reject) => { 45 | middleware(req, res, (result) => { 46 | if (result instanceof Error) { 47 | return reject(result) 48 | } 49 | return resolve(result) 50 | }) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /example/pages/api/tweets.ts: -------------------------------------------------------------------------------- 1 | import vercelFetch from '@vercel/fetch' 2 | 3 | const fetch = vercelFetch() 4 | 5 | const QUERY = 'javascript' 6 | const LANG = 'en' 7 | 8 | export default async (req, res) => { 9 | if (req.method !== 'GET') { 10 | res.setHeader('Allow', 'GET') 11 | return res.status(405).end() 12 | } 13 | 14 | if (!process.env.TWITTER_API_TOKEN) { 15 | return res.status(401).json({ 16 | errors: [ 17 | { message: 'A Twitter API token is required to execute this request' } 18 | ] 19 | }) 20 | } 21 | 22 | const response = await fetch( 23 | `https://api.twitter.com/1.1/search/tweets.json?q=${QUERY}&lang=${LANG}&count=50`, 24 | { 25 | headers: { 26 | authorization: `Bearer ${process.env.TWITTER_API_TOKEN}` 27 | } 28 | } 29 | ) 30 | 31 | if (response.ok) { 32 | const { statuses } = await response.json() 33 | // Cache the Twitter response for 3 seconds, to avoid hitting the Twitter API limits 34 | // of 450 requests every 15 minutes (with app auth) 35 | res.setHeader('Cache-Control', 's-maxage=3, stale-while-revalidate') 36 | res.status(200).json({ tweets: statuses.map((tweet) => tweet.id_str) }) 37 | } else { 38 | res.status(400).json({ 39 | errors: [ 40 | { 41 | message: `Fetch to the Twitter API failed with code: ${response.status}` 42 | } 43 | ] 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/pages/dynamic/[tweetId].tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import useSWR from 'swr' 3 | import { useRouter } from 'next/router' 4 | import { Tweet } from 'react-static-tweets' 5 | 6 | /** 7 | * This example shows how you can use react-static-tweets to render dynamic 8 | * content on the client-side by wrapping `fetchTweetAst` in an API route. 9 | * 10 | * See `api/get-tweet-ast/[tweetId].tsx` for the corresponding API. 11 | */ 12 | 13 | // default dynamic tweet 14 | const defaultTweetId = '1352687755621351425' 15 | 16 | const fetcher = (id: string) => 17 | fetch(`/api/get-tweet-ast/${id}`).then((r) => r.json()) 18 | 19 | const DynamicTweet: React.FC<{ tweetId: string }> = ({ tweetId }) => { 20 | const { data: tweetAst } = useSWR(tweetId, fetcher) 21 | if (!tweetAst) return null 22 | 23 | return 24 | } 25 | 26 | export default () => { 27 | const router = useRouter() 28 | const tweetId = (router.query.tweetId as string) ?? defaultTweetId 29 | 30 | return ( 31 |
32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /example/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitive-bullshit/react-static-tweets/e2184d706c60c5e6ee8f63fb576bc3da832c11e6/example/public/favicon.png -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /example/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | React Static Tweets 4 | 5 |

6 | 7 | # React Static Tweets Next.js Demo 8 | 9 | > Simple demo showing how you can render tweets via SSR with Next.js and `react-static-tweets`. 10 | 11 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 12 | 13 | ## Getting Started 14 | 15 | First, run the development server: 16 | 17 | ```bash 18 | npm run dev 19 | # or 20 | yarn dev 21 | ``` 22 | 23 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 24 | 25 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 26 | 27 | ## License 28 | 29 | MIT © [Travis Fischer](https://transitivebullsh.it) 30 | 31 | Support my OSS work by following me on twitter twitter 32 | -------------------------------------------------------------------------------- /example/styles/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | a { 6 | color: inherit; 7 | text-decoration: none; 8 | } 9 | 10 | body { 11 | padding: 0; 12 | margin: 0; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | max-width: 100vw; 18 | min-height: 100vh; 19 | overflow-x: hidden; 20 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 21 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 22 | } 23 | 24 | .static-tweet { 25 | min-width: calc(min(100vw, 550px)) !important; 26 | } 27 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "lib": ["dom", "esnext"], 5 | "allowJs": false, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "experimentalDecorators": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "baseUrl": ".", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ] 24 | }, 25 | "exclude": ["node_modules"], 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /example/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@mapbox/rehype-prism@^0.5.0": 6 | version "0.5.0" 7 | resolved "https://registry.yarnpkg.com/@mapbox/rehype-prism/-/rehype-prism-0.5.0.tgz#b756308ebf3af8f92a6359cd78010a7770453e85" 8 | integrity sha512-sE5EetmSR6At7AU2s3N2rFUUqm8BpvxUcGcesgfTZgqF7bQoekqsKxLX8gunIDjZs34acZJ6fgPFHepEWnYKCQ== 9 | dependencies: 10 | hast-util-to-string "^1.0.3" 11 | refractor "^3.0.0" 12 | unist-util-visit "^2.0.2" 13 | 14 | "@next/bundle-analyzer@^13.1.1": 15 | version "13.1.1" 16 | resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-13.1.1.tgz#f36108dcb953ea518253df5eb9e175642f78b04a" 17 | integrity sha512-zxC/MOj7gDjvQffHT4QZqcPe1Ny+e6o3wethCZn3liSElMA+kxgEopbziTUXdrvJcd/porq+3Itc8P+gxE/xog== 18 | dependencies: 19 | webpack-bundle-analyzer "4.7.0" 20 | 21 | "@next/env@13.1.1": 22 | version "13.1.1" 23 | resolved "https://registry.yarnpkg.com/@next/env/-/env-13.1.1.tgz#6ff26488dc7674ef2bfdd1ca28fe43eed1113bea" 24 | integrity sha512-vFMyXtPjSAiOXOywMojxfKIqE3VWN5RCAx+tT3AS3pcKjMLFTCJFUWsKv8hC+87Z1F4W3r68qTwDFZIFmd5Xkw== 25 | 26 | "@next/swc-android-arm-eabi@13.1.1": 27 | version "13.1.1" 28 | resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.1.tgz#b5c3cd1f79d5c7e6a3b3562785d4e5ac3555b9e1" 29 | integrity sha512-qnFCx1kT3JTWhWve4VkeWuZiyjG0b5T6J2iWuin74lORCupdrNukxkq9Pm+Z7PsatxuwVJMhjUoYz7H4cWzx2A== 30 | 31 | "@next/swc-android-arm64@13.1.1": 32 | version "13.1.1" 33 | resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.1.1.tgz#e2ca9ccbba9ef770cb19fbe96d1ac00fe4cb330d" 34 | integrity sha512-eCiZhTzjySubNqUnNkQCjU3Fh+ep3C6b5DCM5FKzsTH/3Gr/4Y7EiaPZKILbvnXmhWtKPIdcY6Zjx51t4VeTfA== 35 | 36 | "@next/swc-darwin-arm64@13.1.1": 37 | version "13.1.1" 38 | resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.1.tgz#4af00877332231bbd5a3703435fdd0b011e74767" 39 | integrity sha512-9zRJSSIwER5tu9ADDkPw5rIZ+Np44HTXpYMr0rkM656IvssowPxmhK0rTreC1gpUCYwFsRbxarUJnJsTWiutPg== 40 | 41 | "@next/swc-darwin-x64@13.1.1": 42 | version "13.1.1" 43 | resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.1.tgz#bf4cb09e7e6ec6d91e031118dde2dd17078bcbbc" 44 | integrity sha512-qWr9qEn5nrnlhB0rtjSdR00RRZEtxg4EGvicIipqZWEyayPxhUu6NwKiG8wZiYZCLfJ5KWr66PGSNeDMGlNaiA== 45 | 46 | "@next/swc-freebsd-x64@13.1.1": 47 | version "13.1.1" 48 | resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.1.tgz#6933ea1264328e8523e28818f912cd53824382d4" 49 | integrity sha512-UwP4w/NcQ7V/VJEj3tGVszgb4pyUCt3lzJfUhjDMUmQbzG9LDvgiZgAGMYH6L21MoyAATJQPDGiAMWAPKsmumA== 50 | 51 | "@next/swc-linux-arm-gnueabihf@13.1.1": 52 | version "13.1.1" 53 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.1.tgz#b5896967aaba3873d809c3ad2e2039e89acde419" 54 | integrity sha512-CnsxmKHco9sosBs1XcvCXP845Db+Wx1G0qouV5+Gr+HT/ZlDYEWKoHVDgnJXLVEQzq4FmHddBNGbXvgqM1Gfkg== 55 | 56 | "@next/swc-linux-arm64-gnu@13.1.1": 57 | version "13.1.1" 58 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.1.tgz#91b3e9ea8575b1ded421c0ea0739b7bccf228469" 59 | integrity sha512-JfDq1eri5Dif+VDpTkONRd083780nsMCOKoFG87wA0sa4xL8LGcXIBAkUGIC1uVy9SMsr2scA9CySLD/i+Oqiw== 60 | 61 | "@next/swc-linux-arm64-musl@13.1.1": 62 | version "13.1.1" 63 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.1.tgz#83149ea05d7d55f3664d608dbe004c0d125f9147" 64 | integrity sha512-GA67ZbDq2AW0CY07zzGt07M5b5Yaq5qUpFIoW3UFfjOPgb0Sqf3DAW7GtFMK1sF4ROHsRDMGQ9rnT0VM2dVfKA== 65 | 66 | "@next/swc-linux-x64-gnu@13.1.1": 67 | version "13.1.1" 68 | resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.1.tgz#d7d0777b56de0dd82b78055772e13e18594a15ca" 69 | integrity sha512-nnjuBrbzvqaOJaV+XgT8/+lmXrSCOt1YYZn/irbDb2fR2QprL6Q7WJNgwsZNxiLSfLdv+2RJGGegBx9sLBEzGA== 70 | 71 | "@next/swc-linux-x64-musl@13.1.1": 72 | version "13.1.1" 73 | resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.1.1.tgz#41655722b127133cd95ab5bc8ca1473e9ab6876f" 74 | integrity sha512-CM9xnAQNIZ8zf/igbIT/i3xWbQZYaF397H+JroF5VMOCUleElaMdQLL5riJml8wUfPoN3dtfn2s4peSr3azz/g== 75 | 76 | "@next/swc-win32-arm64-msvc@13.1.1": 77 | version "13.1.1" 78 | resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.1.tgz#f10da3dfc9b3c2bbd202f5d449a9b807af062292" 79 | integrity sha512-pzUHOGrbgfGgPlOMx9xk3QdPJoRPU+om84hqVoe6u+E0RdwOG0Ho/2UxCgDqmvpUrMab1Deltlt6RqcXFpnigQ== 80 | 81 | "@next/swc-win32-ia32-msvc@13.1.1": 82 | version "13.1.1" 83 | resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.1.tgz#4c0102b9b18ece15c818056d07e3917ee9dade78" 84 | integrity sha512-WeX8kVS46aobM9a7Xr/kEPcrTyiwJqQv/tbw6nhJ4fH9xNZ+cEcyPoQkwPo570dCOLz3Zo9S2q0E6lJ/EAUOBg== 85 | 86 | "@next/swc-win32-x64-msvc@13.1.1": 87 | version "13.1.1" 88 | resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.1.tgz#c209a37da13be27b722f9c40c40ab4b094866244" 89 | integrity sha512-mVF0/3/5QAc5EGVnb8ll31nNvf3BWpPY4pBb84tk+BfQglWLqc5AC9q1Ht/YMWiEgs8ALNKEQ3GQnbY0bJF2Gg== 90 | 91 | "@polka/url@^1.0.0-next.20": 92 | version "1.0.0-next.21" 93 | resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" 94 | integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== 95 | 96 | "@swc/helpers@0.4.14": 97 | version "0.4.14" 98 | resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74" 99 | integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw== 100 | dependencies: 101 | tslib "^2.4.0" 102 | 103 | "@types/async-retry@1.2.1": 104 | version "1.2.1" 105 | resolved "https://registry.yarnpkg.com/@types/async-retry/-/async-retry-1.2.1.tgz#fa9ac165907a8ee78f4924f4e393b656c65b5bb4" 106 | integrity sha512-yMQ6CVgICWtyFNBqJT3zqOc+TnqqEPLo4nKJNPFwcialiylil38Ie6q1ENeFTjvaLOkVim9K5LisHgAKJWidGQ== 107 | 108 | "@types/async-retry@^1.4.3": 109 | version "1.4.5" 110 | resolved "https://registry.yarnpkg.com/@types/async-retry/-/async-retry-1.4.5.tgz#02680eb366739a327550f6ad38bb898a5571a095" 111 | integrity sha512-YrdjSD+yQv7h6d5Ip+PMxh3H6ZxKyQk0Ts+PvaNRInxneG9PFVZjFg77ILAN+N6qYf7g4giSJ1l+ZjQ1zeegvA== 112 | dependencies: 113 | "@types/retry" "*" 114 | 115 | "@types/hast@^2.0.0": 116 | version "2.3.1" 117 | resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.1.tgz#b16872f2a6144c7025f296fb9636a667ebb79cd9" 118 | integrity sha512-viwwrB+6xGzw+G1eWpF9geV3fnsDgXqHG+cqgiHrvQfDUW5hzhCyV7Sy3UJxhfRFBsgky2SSW33qi/YrIkjX5Q== 119 | dependencies: 120 | "@types/unist" "*" 121 | 122 | "@types/lru-cache@4.1.1": 123 | version "4.1.1" 124 | resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-4.1.1.tgz#b2d87a5e3df8d4b18ca426c5105cd701c2306d40" 125 | integrity sha512-8mNEUG6diOrI6pMqOHrHPDBB1JsrpedeMK9AWGzVCQ7StRRribiT9BRvUmF8aUws9iBbVlgVekOT5Sgzc1MTKw== 126 | 127 | "@types/mdast@^3.0.0": 128 | version "3.0.3" 129 | resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.3.tgz#2d7d671b1cd1ea3deb306ea75036c2a0407d2deb" 130 | integrity sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw== 131 | dependencies: 132 | "@types/unist" "*" 133 | 134 | "@types/node-fetch@^2.6.1": 135 | version "2.6.2" 136 | resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" 137 | integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== 138 | dependencies: 139 | "@types/node" "*" 140 | form-data "^3.0.0" 141 | 142 | "@types/node@*": 143 | version "14.14.28" 144 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.28.tgz#cade4b64f8438f588951a6b35843ce536853f25b" 145 | integrity sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g== 146 | 147 | "@types/node@10.12.18": 148 | version "10.12.18" 149 | resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" 150 | integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ== 151 | 152 | "@types/node@^18.11.18": 153 | version "18.11.18" 154 | resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" 155 | integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== 156 | 157 | "@types/parse5@^5.0.0": 158 | version "5.0.3" 159 | resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109" 160 | integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw== 161 | 162 | "@types/prop-types@*": 163 | version "15.7.4" 164 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" 165 | integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== 166 | 167 | "@types/react@^18.0.26": 168 | version "18.0.26" 169 | resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.26.tgz#8ad59fc01fef8eaf5c74f4ea392621749f0b7917" 170 | integrity sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug== 171 | dependencies: 172 | "@types/prop-types" "*" 173 | "@types/scheduler" "*" 174 | csstype "^3.0.2" 175 | 176 | "@types/retry@*": 177 | version "0.12.2" 178 | resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" 179 | integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== 180 | 181 | "@types/scheduler@*": 182 | version "0.16.2" 183 | resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" 184 | integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== 185 | 186 | "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2": 187 | version "2.0.3" 188 | resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" 189 | integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ== 190 | 191 | "@types/unist@^2.0.3": 192 | version "2.0.6" 193 | resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" 194 | integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== 195 | 196 | "@vercel/fetch-cached-dns@^2.0.2": 197 | version "2.1.2" 198 | resolved "https://registry.yarnpkg.com/@vercel/fetch-cached-dns/-/fetch-cached-dns-2.1.2.tgz#747cfb12c378fbc8d5b3341e634303a7631b5bf9" 199 | integrity sha512-8JHBmYuXYGlyRupRgnyRML84NL1SBi3lVedYhPaWb+6HwMsYF5/PXfQ9M9GPrXsZ8bqKpLQH9tQcVpZmAUJzeQ== 200 | dependencies: 201 | "@types/node-fetch" "^2.6.1" 202 | "@zeit/dns-cached-resolve" "^2.1.2" 203 | 204 | "@vercel/fetch-retry@^5.0.3": 205 | version "5.1.3" 206 | resolved "https://registry.yarnpkg.com/@vercel/fetch-retry/-/fetch-retry-5.1.3.tgz#1bca6531d4a006e3961469472d726a70edc459f2" 207 | integrity sha512-UIbFc4VsEZHOr6dWuE+kxY4NxnOLXFMCWm0fSKRRHUEtrIzaJLzHpWk2QskCXTSzFgFvhkLAvSrBK2XZg7NSzg== 208 | dependencies: 209 | async-retry "^1.3.3" 210 | debug "^4.3.3" 211 | 212 | "@vercel/fetch@^6.2.0": 213 | version "6.2.0" 214 | resolved "https://registry.yarnpkg.com/@vercel/fetch/-/fetch-6.2.0.tgz#149b97dddeb2c98a23439665f42baa66d5aa3903" 215 | integrity sha512-MU+Mzh06NIAXxwdnyHmBFg+/lTKBbzDkCSNhAwWTFJ4rHuBc4pHc8E6XP+qnwqaWugjOBQgFfQCGDLnV820c9A== 216 | dependencies: 217 | "@types/async-retry" "^1.4.3" 218 | "@vercel/fetch-cached-dns" "^2.0.2" 219 | "@vercel/fetch-retry" "^5.0.3" 220 | agentkeepalive "^4.2.1" 221 | debug "^4.3.3" 222 | 223 | "@zeit/dns-cached-resolve@^2.1.2": 224 | version "2.1.2" 225 | resolved "https://registry.yarnpkg.com/@zeit/dns-cached-resolve/-/dns-cached-resolve-2.1.2.tgz#2c2e33d682d67f94341c9a06ac0e2a8f14ff035f" 226 | integrity sha512-A/5gbBskKPETTBqHwvlaW1Ri2orO62yqoFoXdxna1SQ7A/lXjpWgpJ1wdY3IQEcz5LydpS4sJ8SzI2gFyyLEhg== 227 | dependencies: 228 | "@types/async-retry" "1.2.1" 229 | "@types/lru-cache" "4.1.1" 230 | "@types/node" "10.12.18" 231 | async-retry "1.2.3" 232 | lru-cache "5.1.1" 233 | 234 | acorn-walk@^8.0.0: 235 | version "8.2.0" 236 | resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" 237 | integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== 238 | 239 | acorn@^8.0.4: 240 | version "8.8.1" 241 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" 242 | integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== 243 | 244 | agentkeepalive@^4.2.1: 245 | version "4.2.1" 246 | resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" 247 | integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== 248 | dependencies: 249 | debug "^4.1.0" 250 | depd "^1.1.2" 251 | humanize-ms "^1.2.1" 252 | 253 | ansi-styles@^4.1.0: 254 | version "4.3.0" 255 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 256 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 257 | dependencies: 258 | color-convert "^2.0.1" 259 | 260 | async-retry@1.2.3: 261 | version "1.2.3" 262 | resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.2.3.tgz#a6521f338358d322b1a0012b79030c6f411d1ce0" 263 | integrity sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q== 264 | dependencies: 265 | retry "0.12.0" 266 | 267 | async-retry@^1.3.3: 268 | version "1.3.3" 269 | resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" 270 | integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== 271 | dependencies: 272 | retry "0.13.1" 273 | 274 | asynckit@^0.4.0: 275 | version "0.4.0" 276 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 277 | integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== 278 | 279 | bail@^1.0.0: 280 | version "1.0.5" 281 | resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776" 282 | integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ== 283 | 284 | boolbase@^1.0.0: 285 | version "1.0.0" 286 | resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" 287 | integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= 288 | 289 | caniuse-lite@^1.0.30001406: 290 | version "1.0.30001426" 291 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001426.tgz#58da20446ccd0cb1dfebd11d2350c907ee7c2eaa" 292 | integrity sha512-n7cosrHLl8AWt0wwZw/PJZgUg3lV0gk9LMI7ikGJwhyhgsd2Nb65vKvmSexCqq/J7rbH3mFG6yZZiPR5dLPW5A== 293 | 294 | chalk@^4.1.0: 295 | version "4.1.2" 296 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 297 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 298 | dependencies: 299 | ansi-styles "^4.1.0" 300 | supports-color "^7.1.0" 301 | 302 | character-entities-legacy@^1.0.0: 303 | version "1.1.4" 304 | resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1" 305 | integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA== 306 | 307 | character-entities@^1.0.0: 308 | version "1.2.4" 309 | resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b" 310 | integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw== 311 | 312 | character-reference-invalid@^1.0.0: 313 | version "1.1.4" 314 | resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" 315 | integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== 316 | 317 | cheerio-select-tmp@^0.1.0: 318 | version "0.1.1" 319 | resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646" 320 | integrity sha512-YYs5JvbpU19VYJyj+F7oYrIE2BOll1/hRU7rEy/5+v9BzkSo3bK81iAeeQEMI92vRIxz677m72UmJUiVwwgjfQ== 321 | dependencies: 322 | css-select "^3.1.2" 323 | css-what "^4.0.0" 324 | domelementtype "^2.1.0" 325 | domhandler "^4.0.0" 326 | domutils "^2.4.4" 327 | 328 | cheerio@^1.0.0-rc.5: 329 | version "1.0.0-rc.5" 330 | resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.5.tgz#88907e1828674e8f9fee375188b27dadd4f0fa2f" 331 | integrity sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw== 332 | dependencies: 333 | cheerio-select-tmp "^0.1.0" 334 | dom-serializer "~1.2.0" 335 | domhandler "^4.0.0" 336 | entities "~2.1.0" 337 | htmlparser2 "^6.0.0" 338 | parse5 "^6.0.0" 339 | parse5-htmlparser2-tree-adapter "^6.0.0" 340 | 341 | client-only@0.0.1: 342 | version "0.0.1" 343 | resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" 344 | integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== 345 | 346 | clipboard@^2.0.0: 347 | version "2.0.6" 348 | resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376" 349 | integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg== 350 | dependencies: 351 | good-listener "^1.2.2" 352 | select "^1.1.2" 353 | tiny-emitter "^2.0.0" 354 | 355 | clsx@^1.1.1: 356 | version "1.2.1" 357 | resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" 358 | integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== 359 | 360 | color-convert@^2.0.1: 361 | version "2.0.1" 362 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 363 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 364 | dependencies: 365 | color-name "~1.1.4" 366 | 367 | color-name@~1.1.4: 368 | version "1.1.4" 369 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 370 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 371 | 372 | combined-stream@^1.0.8: 373 | version "1.0.8" 374 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 375 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 376 | dependencies: 377 | delayed-stream "~1.0.0" 378 | 379 | comma-separated-tokens@^1.0.0: 380 | version "1.0.8" 381 | resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" 382 | integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== 383 | 384 | commander@^7.2.0: 385 | version "7.2.0" 386 | resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" 387 | integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== 388 | 389 | cors@^2.8.5: 390 | version "2.8.5" 391 | resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" 392 | integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== 393 | dependencies: 394 | object-assign "^4" 395 | vary "^1" 396 | 397 | cross-env@^7.0.2: 398 | version "7.0.2" 399 | resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9" 400 | integrity sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw== 401 | dependencies: 402 | cross-spawn "^7.0.1" 403 | 404 | cross-spawn@^7.0.1: 405 | version "7.0.3" 406 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" 407 | integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== 408 | dependencies: 409 | path-key "^3.1.0" 410 | shebang-command "^2.0.0" 411 | which "^2.0.1" 412 | 413 | css-select@^3.1.2: 414 | version "3.1.2" 415 | resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" 416 | integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== 417 | dependencies: 418 | boolbase "^1.0.0" 419 | css-what "^4.0.0" 420 | domhandler "^4.0.0" 421 | domutils "^2.4.3" 422 | nth-check "^2.0.0" 423 | 424 | css-what@^4.0.0: 425 | version "4.0.0" 426 | resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" 427 | integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== 428 | 429 | csstype@^3.0.2: 430 | version "3.0.11" 431 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33" 432 | integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw== 433 | 434 | date-fns@^2.29.3: 435 | version "2.29.3" 436 | resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" 437 | integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== 438 | 439 | debug@^4.0.0, debug@^4.1.0, debug@^4.3.3: 440 | version "4.3.4" 441 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 442 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 443 | dependencies: 444 | ms "2.1.2" 445 | 446 | delayed-stream@~1.0.0: 447 | version "1.0.0" 448 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 449 | integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== 450 | 451 | delegate@^3.1.2: 452 | version "3.2.0" 453 | resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" 454 | integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== 455 | 456 | depd@^1.1.2: 457 | version "1.1.2" 458 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 459 | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= 460 | 461 | dom-serializer@^1.0.1: 462 | version "1.1.0" 463 | resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.1.0.tgz#5f7c828f1bfc44887dc2a315ab5c45691d544b58" 464 | integrity sha512-ox7bvGXt2n+uLWtCRLybYx60IrOlWL/aCebWJk1T0d4m3y2tzf4U3ij9wBMUb6YJZpz06HCCYuyCDveE2xXmzQ== 465 | dependencies: 466 | domelementtype "^2.0.1" 467 | domhandler "^3.0.0" 468 | entities "^2.0.0" 469 | 470 | dom-serializer@~1.2.0: 471 | version "1.2.0" 472 | resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" 473 | integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== 474 | dependencies: 475 | domelementtype "^2.0.1" 476 | domhandler "^4.0.0" 477 | entities "^2.0.0" 478 | 479 | domelementtype@^2.0.1: 480 | version "2.0.2" 481 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.2.tgz#f3b6e549201e46f588b59463dd77187131fe6971" 482 | integrity sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA== 483 | 484 | domelementtype@^2.1.0: 485 | version "2.1.0" 486 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" 487 | integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== 488 | 489 | domhandler@^3.0.0: 490 | version "3.3.0" 491 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a" 492 | integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== 493 | dependencies: 494 | domelementtype "^2.0.1" 495 | 496 | domhandler@^4.0.0: 497 | version "4.0.0" 498 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" 499 | integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== 500 | dependencies: 501 | domelementtype "^2.1.0" 502 | 503 | domutils@^2.4.3, domutils@^2.4.4: 504 | version "2.4.4" 505 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" 506 | integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== 507 | dependencies: 508 | dom-serializer "^1.0.1" 509 | domelementtype "^2.0.1" 510 | domhandler "^4.0.0" 511 | 512 | duplexer@^0.1.2: 513 | version "0.1.2" 514 | resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" 515 | integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== 516 | 517 | "emoji-regex@>=6.0.0 <=6.1.1": 518 | version "6.1.1" 519 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e" 520 | integrity sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4= 521 | 522 | entities@^2.0.0: 523 | version "2.0.3" 524 | resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" 525 | integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== 526 | 527 | entities@~2.1.0: 528 | version "2.1.0" 529 | resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" 530 | integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== 531 | 532 | extend@^3.0.0: 533 | version "3.0.2" 534 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 535 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 536 | 537 | form-data@^3.0.0: 538 | version "3.0.1" 539 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" 540 | integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== 541 | dependencies: 542 | asynckit "^0.4.0" 543 | combined-stream "^1.0.8" 544 | mime-types "^2.1.12" 545 | 546 | github-slugger@^1.3.0: 547 | version "1.3.0" 548 | resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.3.0.tgz#9bd0a95c5efdfc46005e82a906ef8e2a059124c9" 549 | integrity sha512-gwJScWVNhFYSRDvURk/8yhcFBee6aFjye2a7Lhb2bUyRulpIoek9p0I9Kt7PT67d/nUlZbFu8L9RLiA0woQN8Q== 550 | dependencies: 551 | emoji-regex ">=6.0.0 <=6.1.1" 552 | 553 | good-listener@^1.2.2: 554 | version "1.2.2" 555 | resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" 556 | integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA= 557 | dependencies: 558 | delegate "^3.1.2" 559 | 560 | gzip-size@^6.0.0: 561 | version "6.0.0" 562 | resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" 563 | integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== 564 | dependencies: 565 | duplexer "^0.1.2" 566 | 567 | has-flag@^4.0.0: 568 | version "4.0.0" 569 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 570 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 571 | 572 | hast-to-hyperscript@^9.0.0: 573 | version "9.0.1" 574 | resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d" 575 | integrity sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA== 576 | dependencies: 577 | "@types/unist" "^2.0.3" 578 | comma-separated-tokens "^1.0.0" 579 | property-information "^5.3.0" 580 | space-separated-tokens "^1.0.0" 581 | style-to-object "^0.3.0" 582 | unist-util-is "^4.0.0" 583 | web-namespaces "^1.0.0" 584 | 585 | hast-util-from-parse5@^6.0.0: 586 | version "6.0.1" 587 | resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz#554e34abdeea25ac76f5bd950a1f0180e0b3bc2a" 588 | integrity sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA== 589 | dependencies: 590 | "@types/parse5" "^5.0.0" 591 | hastscript "^6.0.0" 592 | property-information "^5.0.0" 593 | vfile "^4.0.0" 594 | vfile-location "^3.2.0" 595 | web-namespaces "^1.0.0" 596 | 597 | hast-util-parse-selector@^2.0.0: 598 | version "2.2.5" 599 | resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a" 600 | integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ== 601 | 602 | hast-util-raw@^6.1.0: 603 | version "6.1.0" 604 | resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-6.1.0.tgz#e16a3c2642f65cc7c480c165400a40d604ab75d0" 605 | integrity sha512-5FoZLDHBpka20OlZZ4I/+RBw5piVQ8iI1doEvffQhx5CbCyTtP8UCq8Tw6NmTAMtXgsQxmhW7Ly8OdFre5/YMQ== 606 | dependencies: 607 | "@types/hast" "^2.0.0" 608 | hast-util-from-parse5 "^6.0.0" 609 | hast-util-to-parse5 "^6.0.0" 610 | html-void-elements "^1.0.0" 611 | parse5 "^6.0.0" 612 | unist-util-position "^3.0.0" 613 | unist-util-visit "^2.0.0" 614 | vfile "^4.0.0" 615 | web-namespaces "^1.0.0" 616 | xtend "^4.0.0" 617 | zwitch "^1.0.0" 618 | 619 | hast-util-sanitize@^3.0.0: 620 | version "3.0.2" 621 | resolved "https://registry.yarnpkg.com/hast-util-sanitize/-/hast-util-sanitize-3.0.2.tgz#b0b783220af528ba8fe6999f092d138908678520" 622 | integrity sha512-+2I0x2ZCAyiZOO/sb4yNLFmdwPBnyJ4PBkVTUMKMqBwYNA+lXSgOmoRXlJFazoyid9QPogRRKgKhVEodv181sA== 623 | dependencies: 624 | xtend "^4.0.0" 625 | 626 | hast-util-to-parse5@^6.0.0: 627 | version "6.0.0" 628 | resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz#1ec44650b631d72952066cea9b1445df699f8479" 629 | integrity sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ== 630 | dependencies: 631 | hast-to-hyperscript "^9.0.0" 632 | property-information "^5.0.0" 633 | web-namespaces "^1.0.0" 634 | xtend "^4.0.0" 635 | zwitch "^1.0.0" 636 | 637 | hast-util-to-string@^1.0.3: 638 | version "1.0.4" 639 | resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-1.0.4.tgz#9b24c114866bdb9478927d7e9c36a485ac728378" 640 | integrity sha512-eK0MxRX47AV2eZ+Lyr18DCpQgodvaS3fAQO2+b9Two9F5HEoRPhiUMNzoXArMJfZi2yieFzUBMRl3HNJ3Jus3w== 641 | 642 | hastscript@^6.0.0: 643 | version "6.0.0" 644 | resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640" 645 | integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w== 646 | dependencies: 647 | "@types/hast" "^2.0.0" 648 | comma-separated-tokens "^1.0.0" 649 | hast-util-parse-selector "^2.0.0" 650 | property-information "^5.0.0" 651 | space-separated-tokens "^1.0.0" 652 | 653 | html-void-elements@^1.0.0: 654 | version "1.0.5" 655 | resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483" 656 | integrity sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w== 657 | 658 | htmlparser2@^6.0.0: 659 | version "6.0.0" 660 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01" 661 | integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw== 662 | dependencies: 663 | domelementtype "^2.0.1" 664 | domhandler "^4.0.0" 665 | domutils "^2.4.4" 666 | entities "^2.0.0" 667 | 668 | humanize-ms@^1.2.1: 669 | version "1.2.1" 670 | resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" 671 | integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= 672 | dependencies: 673 | ms "^2.0.0" 674 | 675 | inline-style-parser@0.1.1: 676 | version "0.1.1" 677 | resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" 678 | integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== 679 | 680 | is-alphabetical@^1.0.0: 681 | version "1.0.4" 682 | resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" 683 | integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== 684 | 685 | is-alphanumerical@^1.0.0: 686 | version "1.0.4" 687 | resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf" 688 | integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A== 689 | dependencies: 690 | is-alphabetical "^1.0.0" 691 | is-decimal "^1.0.0" 692 | 693 | is-buffer@^2.0.0: 694 | version "2.0.5" 695 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" 696 | integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== 697 | 698 | is-decimal@^1.0.0: 699 | version "1.0.4" 700 | resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" 701 | integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== 702 | 703 | is-hexadecimal@^1.0.0: 704 | version "1.0.4" 705 | resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" 706 | integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw== 707 | 708 | is-plain-obj@^2.0.0: 709 | version "2.1.0" 710 | resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" 711 | integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== 712 | 713 | isexe@^2.0.0: 714 | version "2.0.0" 715 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 716 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 717 | 718 | "js-tokens@^3.0.0 || ^4.0.0": 719 | version "4.0.0" 720 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 721 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 722 | 723 | lodash@^4.17.20: 724 | version "4.17.21" 725 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 726 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 727 | 728 | loose-envify@^1.1.0: 729 | version "1.4.0" 730 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 731 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 732 | dependencies: 733 | js-tokens "^3.0.0 || ^4.0.0" 734 | 735 | lru-cache@5.1.1: 736 | version "5.1.1" 737 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" 738 | integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== 739 | dependencies: 740 | yallist "^3.0.2" 741 | 742 | mdast-util-definitions@^4.0.0: 743 | version "4.0.0" 744 | resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2" 745 | integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ== 746 | dependencies: 747 | unist-util-visit "^2.0.0" 748 | 749 | mdast-util-from-markdown@^0.8.0: 750 | version "0.8.5" 751 | resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz#d1ef2ca42bc377ecb0463a987910dae89bd9a28c" 752 | integrity sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ== 753 | dependencies: 754 | "@types/mdast" "^3.0.0" 755 | mdast-util-to-string "^2.0.0" 756 | micromark "~2.11.0" 757 | parse-entities "^2.0.0" 758 | unist-util-stringify-position "^2.0.0" 759 | 760 | mdast-util-to-hast@^10.0.0: 761 | version "10.2.0" 762 | resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz#61875526a017d8857b71abc9333942700b2d3604" 763 | integrity sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ== 764 | dependencies: 765 | "@types/mdast" "^3.0.0" 766 | "@types/unist" "^2.0.0" 767 | mdast-util-definitions "^4.0.0" 768 | mdurl "^1.0.0" 769 | unist-builder "^2.0.0" 770 | unist-util-generated "^1.0.0" 771 | unist-util-position "^3.0.0" 772 | unist-util-visit "^2.0.0" 773 | 774 | mdast-util-to-string@^2.0.0: 775 | version "2.0.0" 776 | resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz#b8cfe6a713e1091cb5b728fc48885a4767f8b97b" 777 | integrity sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w== 778 | 779 | mdurl@^1.0.0: 780 | version "1.0.1" 781 | resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" 782 | integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= 783 | 784 | micromark@~2.11.0: 785 | version "2.11.4" 786 | resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a" 787 | integrity sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA== 788 | dependencies: 789 | debug "^4.0.0" 790 | parse-entities "^2.0.0" 791 | 792 | mime-db@1.52.0: 793 | version "1.52.0" 794 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 795 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 796 | 797 | mime-types@^2.1.12: 798 | version "2.1.35" 799 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 800 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 801 | dependencies: 802 | mime-db "1.52.0" 803 | 804 | mrmime@^1.0.0: 805 | version "1.0.1" 806 | resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" 807 | integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== 808 | 809 | ms@2.1.2: 810 | version "2.1.2" 811 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 812 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 813 | 814 | ms@^2.0.0: 815 | version "2.1.3" 816 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 817 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 818 | 819 | nanoid@^3.3.4: 820 | version "3.3.4" 821 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" 822 | integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== 823 | 824 | next@^13.1.1: 825 | version "13.1.1" 826 | resolved "https://registry.yarnpkg.com/next/-/next-13.1.1.tgz#42b825f650410649aff1017d203a088d77c80b5b" 827 | integrity sha512-R5eBAaIa3X7LJeYvv1bMdGnAVF4fVToEjim7MkflceFPuANY3YyvFxXee/A+acrSYwYPvOvf7f6v/BM/48ea5w== 828 | dependencies: 829 | "@next/env" "13.1.1" 830 | "@swc/helpers" "0.4.14" 831 | caniuse-lite "^1.0.30001406" 832 | postcss "8.4.14" 833 | styled-jsx "5.1.1" 834 | optionalDependencies: 835 | "@next/swc-android-arm-eabi" "13.1.1" 836 | "@next/swc-android-arm64" "13.1.1" 837 | "@next/swc-darwin-arm64" "13.1.1" 838 | "@next/swc-darwin-x64" "13.1.1" 839 | "@next/swc-freebsd-x64" "13.1.1" 840 | "@next/swc-linux-arm-gnueabihf" "13.1.1" 841 | "@next/swc-linux-arm64-gnu" "13.1.1" 842 | "@next/swc-linux-arm64-musl" "13.1.1" 843 | "@next/swc-linux-x64-gnu" "13.1.1" 844 | "@next/swc-linux-x64-musl" "13.1.1" 845 | "@next/swc-win32-arm64-msvc" "13.1.1" 846 | "@next/swc-win32-ia32-msvc" "13.1.1" 847 | "@next/swc-win32-x64-msvc" "13.1.1" 848 | 849 | node-fetch@2: 850 | version "2.6.7" 851 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" 852 | integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== 853 | dependencies: 854 | whatwg-url "^5.0.0" 855 | 856 | node-fetch@^2.6.1: 857 | version "2.6.1" 858 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" 859 | integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== 860 | 861 | nth-check@^2.0.0: 862 | version "2.0.0" 863 | resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" 864 | integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== 865 | dependencies: 866 | boolbase "^1.0.0" 867 | 868 | object-assign@^4: 869 | version "4.1.1" 870 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 871 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 872 | 873 | opener@^1.5.2: 874 | version "1.5.2" 875 | resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" 876 | integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== 877 | 878 | parse-entities@^2.0.0: 879 | version "2.0.0" 880 | resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8" 881 | integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ== 882 | dependencies: 883 | character-entities "^1.0.0" 884 | character-entities-legacy "^1.0.0" 885 | character-reference-invalid "^1.0.0" 886 | is-alphanumerical "^1.0.0" 887 | is-decimal "^1.0.0" 888 | is-hexadecimal "^1.0.0" 889 | 890 | parse5-htmlparser2-tree-adapter@^6.0.0: 891 | version "6.0.1" 892 | resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" 893 | integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== 894 | dependencies: 895 | parse5 "^6.0.1" 896 | 897 | parse5@^6.0.0, parse5@^6.0.1: 898 | version "6.0.1" 899 | resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" 900 | integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== 901 | 902 | path-key@^3.1.0: 903 | version "3.1.1" 904 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" 905 | integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== 906 | 907 | picocolors@^1.0.0: 908 | version "1.0.0" 909 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 910 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 911 | 912 | postcss@8.4.14: 913 | version "8.4.14" 914 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" 915 | integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== 916 | dependencies: 917 | nanoid "^3.3.4" 918 | picocolors "^1.0.0" 919 | source-map-js "^1.0.2" 920 | 921 | prismjs@~1.23.0: 922 | version "1.23.0" 923 | resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" 924 | integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA== 925 | optionalDependencies: 926 | clipboard "^2.0.0" 927 | 928 | property-information@^5.0.0, property-information@^5.3.0: 929 | version "5.6.0" 930 | resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" 931 | integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA== 932 | dependencies: 933 | xtend "^4.0.0" 934 | 935 | react-dom@^18.2.0: 936 | version "18.2.0" 937 | resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" 938 | integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== 939 | dependencies: 940 | loose-envify "^1.1.0" 941 | scheduler "^0.23.0" 942 | 943 | react-static-tweets@^1.0.0: 944 | version "1.0.0" 945 | resolved "https://registry.yarnpkg.com/react-static-tweets/-/react-static-tweets-1.0.0.tgz#cf151d4cb5562b4f3bbe5dc62a504e2982fb8e20" 946 | integrity sha512-a20G/ad+UD5j1tp6a/lSnsrAhZqROSCW6iJpn5ol07vyeeZ404BTuXBupQNGGJdJLIGmP3+Kld6AJNlEEIqwHA== 947 | dependencies: 948 | clsx "^1.1.1" 949 | 950 | react@^18.2.0: 951 | version "18.2.0" 952 | resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" 953 | integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== 954 | dependencies: 955 | loose-envify "^1.1.0" 956 | 957 | refractor@^3.0.0: 958 | version "3.3.1" 959 | resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.3.1.tgz#ebbc04b427ea81dc25ad333f7f67a0b5f4f0be3a" 960 | integrity sha512-vaN6R56kLMuBszHSWlwTpcZ8KTMG6aUCok4GrxYDT20UIOXxOc5o6oDc8tNTzSlH3m2sI+Eu9Jo2kVdDcUTWYw== 961 | dependencies: 962 | hastscript "^6.0.0" 963 | parse-entities "^2.0.0" 964 | prismjs "~1.23.0" 965 | 966 | rehype-parse@^7.0.0: 967 | version "7.0.1" 968 | resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-7.0.1.tgz#58900f6702b56767814afc2a9efa2d42b1c90c57" 969 | integrity sha512-fOiR9a9xH+Le19i4fGzIEowAbwG7idy2Jzs4mOrFWBSJ0sNUgy0ev871dwWnbOo371SjgjG4pwzrbgSVrKxecw== 970 | dependencies: 971 | hast-util-from-parse5 "^6.0.0" 972 | parse5 "^6.0.0" 973 | 974 | rehype-raw@^5.0.0: 975 | version "5.1.0" 976 | resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-5.1.0.tgz#66d5e8d7188ada2d31bc137bc19a1000cf2c6b7e" 977 | integrity sha512-MDvHAb/5mUnif2R+0IPCYJU8WjHa9UzGtM/F4AVy5GixPlDZ1z3HacYy4xojDU+uBa+0X/3PIfyQI26/2ljJNA== 978 | dependencies: 979 | hast-util-raw "^6.1.0" 980 | 981 | rehype-sanitize@^4.0.0: 982 | version "4.0.0" 983 | resolved "https://registry.yarnpkg.com/rehype-sanitize/-/rehype-sanitize-4.0.0.tgz#b5241cf66bcedc49cd4e924a5f7a252f00a151ad" 984 | integrity sha512-ZCr/iQRr4JeqPjun5i9CHHILVY7i45VnLu1CkkibDrSyFQ7dTLSvw8OIQpHhS4RSh9h/9GidxFw1bRb0LOxIag== 985 | dependencies: 986 | hast-util-sanitize "^3.0.0" 987 | 988 | remark-parse@^9.0.0: 989 | version "9.0.0" 990 | resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640" 991 | integrity sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw== 992 | dependencies: 993 | mdast-util-from-markdown "^0.8.0" 994 | 995 | remark-rehype@^8.0.0: 996 | version "8.0.0" 997 | resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-8.0.0.tgz#5a8afc8262a59d205fba21dafb27a673fb3b92fa" 998 | integrity sha512-gVvOH02TMFqXOWoL6iXU7NXMsDJguNkNuMrzfkQeA4V6WCyHQnOKptn+IQBVVPuIH2sMJBwo8hlrmtn1MLTh9w== 999 | dependencies: 1000 | mdast-util-to-hast "^10.0.0" 1001 | 1002 | retry@0.12.0: 1003 | version "0.12.0" 1004 | resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" 1005 | integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= 1006 | 1007 | retry@0.13.1: 1008 | version "0.13.1" 1009 | resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" 1010 | integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== 1011 | 1012 | scheduler@^0.23.0: 1013 | version "0.23.0" 1014 | resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" 1015 | integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== 1016 | dependencies: 1017 | loose-envify "^1.1.0" 1018 | 1019 | select@^1.1.2: 1020 | version "1.1.2" 1021 | resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" 1022 | integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= 1023 | 1024 | shebang-command@^2.0.0: 1025 | version "2.0.0" 1026 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" 1027 | integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== 1028 | dependencies: 1029 | shebang-regex "^3.0.0" 1030 | 1031 | shebang-regex@^3.0.0: 1032 | version "3.0.0" 1033 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" 1034 | integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 1035 | 1036 | sirv@^1.0.7: 1037 | version "1.0.19" 1038 | resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.19.tgz#1d73979b38c7fe91fcba49c85280daa9c2363b49" 1039 | integrity sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ== 1040 | dependencies: 1041 | "@polka/url" "^1.0.0-next.20" 1042 | mrmime "^1.0.0" 1043 | totalist "^1.0.0" 1044 | 1045 | source-map-js@^1.0.2: 1046 | version "1.0.2" 1047 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 1048 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 1049 | 1050 | space-separated-tokens@^1.0.0: 1051 | version "1.1.5" 1052 | resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" 1053 | integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA== 1054 | 1055 | static-tweets@^1.0.0: 1056 | version "1.0.0" 1057 | resolved "https://registry.yarnpkg.com/static-tweets/-/static-tweets-1.0.0.tgz#e4a47647866e835e40df4524c54a1b06d62694ab" 1058 | integrity sha512-WPBtV4o69r2VwgirVzzcN0M3FoEs+BnnRCBom9kOoKn+u9CLJ1Ok0845kQvshQsFUGdwsHMMO2tjTdkffSqFCg== 1059 | dependencies: 1060 | "@mapbox/rehype-prism" "^0.5.0" 1061 | cheerio "^1.0.0-rc.5" 1062 | github-slugger "^1.3.0" 1063 | node-fetch "2" 1064 | rehype-parse "^7.0.0" 1065 | rehype-raw "^5.0.0" 1066 | rehype-sanitize "^4.0.0" 1067 | remark-parse "^9.0.0" 1068 | remark-rehype "^8.0.0" 1069 | unified "^9.0.0" 1070 | 1071 | style-to-object@^0.3.0: 1072 | version "0.3.0" 1073 | resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" 1074 | integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA== 1075 | dependencies: 1076 | inline-style-parser "0.1.1" 1077 | 1078 | styled-jsx@5.1.1: 1079 | version "5.1.1" 1080 | resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" 1081 | integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== 1082 | dependencies: 1083 | client-only "0.0.1" 1084 | 1085 | supports-color@^7.1.0: 1086 | version "7.2.0" 1087 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 1088 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 1089 | dependencies: 1090 | has-flag "^4.0.0" 1091 | 1092 | swr@^2.0.0: 1093 | version "2.0.0" 1094 | resolved "https://registry.yarnpkg.com/swr/-/swr-2.0.0.tgz#91d999359e2be92de1a41f6b6711d72be20ffdbd" 1095 | integrity sha512-IhUx5yPkX+Fut3h0SqZycnaNLXLXsb2ECFq0Y29cxnK7d8r7auY2JWNbCW3IX+EqXUg3rwNJFlhrw5Ye/b6k7w== 1096 | dependencies: 1097 | use-sync-external-store "^1.2.0" 1098 | 1099 | tiny-emitter@^2.0.0: 1100 | version "2.1.0" 1101 | resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" 1102 | integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== 1103 | 1104 | totalist@^1.0.0: 1105 | version "1.1.0" 1106 | resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" 1107 | integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== 1108 | 1109 | tr46@~0.0.3: 1110 | version "0.0.3" 1111 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 1112 | integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== 1113 | 1114 | trough@^1.0.0: 1115 | version "1.0.5" 1116 | resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" 1117 | integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== 1118 | 1119 | tslib@^2.4.0: 1120 | version "2.4.0" 1121 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" 1122 | integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== 1123 | 1124 | typescript@^4.9.4: 1125 | version "4.9.4" 1126 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" 1127 | integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== 1128 | 1129 | unified@^9.0.0: 1130 | version "9.2.2" 1131 | resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975" 1132 | integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ== 1133 | dependencies: 1134 | bail "^1.0.0" 1135 | extend "^3.0.0" 1136 | is-buffer "^2.0.0" 1137 | is-plain-obj "^2.0.0" 1138 | trough "^1.0.0" 1139 | vfile "^4.0.0" 1140 | 1141 | unist-builder@^2.0.0: 1142 | version "2.0.3" 1143 | resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436" 1144 | integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw== 1145 | 1146 | unist-util-generated@^1.0.0: 1147 | version "1.1.6" 1148 | resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b" 1149 | integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg== 1150 | 1151 | unist-util-is@^4.0.0: 1152 | version "4.0.4" 1153 | resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.0.4.tgz#3e9e8de6af2eb0039a59f50c9b3e99698a924f50" 1154 | integrity sha512-3dF39j/u423v4BBQrk1AQ2Ve1FxY5W3JKwXxVFzBODQ6WEvccguhgp802qQLKSnxPODE6WuRZtV+ohlUg4meBA== 1155 | 1156 | unist-util-position@^3.0.0: 1157 | version "3.1.0" 1158 | resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47" 1159 | integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA== 1160 | 1161 | unist-util-stringify-position@^2.0.0: 1162 | version "2.0.3" 1163 | resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da" 1164 | integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g== 1165 | dependencies: 1166 | "@types/unist" "^2.0.2" 1167 | 1168 | unist-util-visit-parents@^3.0.0: 1169 | version "3.1.1" 1170 | resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6" 1171 | integrity sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg== 1172 | dependencies: 1173 | "@types/unist" "^2.0.0" 1174 | unist-util-is "^4.0.0" 1175 | 1176 | unist-util-visit@^2.0.0, unist-util-visit@^2.0.2: 1177 | version "2.0.3" 1178 | resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c" 1179 | integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== 1180 | dependencies: 1181 | "@types/unist" "^2.0.0" 1182 | unist-util-is "^4.0.0" 1183 | unist-util-visit-parents "^3.0.0" 1184 | 1185 | use-sync-external-store@^1.2.0: 1186 | version "1.2.0" 1187 | resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" 1188 | integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== 1189 | 1190 | vary@^1: 1191 | version "1.1.2" 1192 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1193 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 1194 | 1195 | vfile-location@^3.2.0: 1196 | version "3.2.0" 1197 | resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.2.0.tgz#d8e41fbcbd406063669ebf6c33d56ae8721d0f3c" 1198 | integrity sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA== 1199 | 1200 | vfile-message@^2.0.0: 1201 | version "2.0.4" 1202 | resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" 1203 | integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ== 1204 | dependencies: 1205 | "@types/unist" "^2.0.0" 1206 | unist-util-stringify-position "^2.0.0" 1207 | 1208 | vfile@^4.0.0: 1209 | version "4.2.1" 1210 | resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624" 1211 | integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA== 1212 | dependencies: 1213 | "@types/unist" "^2.0.0" 1214 | is-buffer "^2.0.0" 1215 | unist-util-stringify-position "^2.0.0" 1216 | vfile-message "^2.0.0" 1217 | 1218 | web-namespaces@^1.0.0: 1219 | version "1.1.4" 1220 | resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" 1221 | integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== 1222 | 1223 | webidl-conversions@^3.0.0: 1224 | version "3.0.1" 1225 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 1226 | integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== 1227 | 1228 | webpack-bundle-analyzer@4.7.0: 1229 | version "4.7.0" 1230 | resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz#33c1c485a7fcae8627c547b5c3328b46de733c66" 1231 | integrity sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg== 1232 | dependencies: 1233 | acorn "^8.0.4" 1234 | acorn-walk "^8.0.0" 1235 | chalk "^4.1.0" 1236 | commander "^7.2.0" 1237 | gzip-size "^6.0.0" 1238 | lodash "^4.17.20" 1239 | opener "^1.5.2" 1240 | sirv "^1.0.7" 1241 | ws "^7.3.1" 1242 | 1243 | whatwg-url@^5.0.0: 1244 | version "5.0.0" 1245 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 1246 | integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== 1247 | dependencies: 1248 | tr46 "~0.0.3" 1249 | webidl-conversions "^3.0.0" 1250 | 1251 | which@^2.0.1: 1252 | version "2.0.2" 1253 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 1254 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 1255 | dependencies: 1256 | isexe "^2.0.0" 1257 | 1258 | ws@^7.3.1: 1259 | version "7.5.9" 1260 | resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" 1261 | integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== 1262 | 1263 | xtend@^4.0.0: 1264 | version "4.0.2" 1265 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" 1266 | integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== 1267 | 1268 | yallist@^3.0.2: 1269 | version "3.1.1" 1270 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" 1271 | integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== 1272 | 1273 | zwitch@^1.0.0: 1274 | version "1.0.5" 1275 | resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" 1276 | integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw== 1277 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "packages": [ 6 | "packages/*", 7 | "example/" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Travis Fischer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-static-tweets", 3 | "private": true, 4 | "description": "Extremely fast static renderer for tweets.", 5 | "repository": "transitive-bullshit/react-static-tweets", 6 | "author": "Travis Fischer ", 7 | "license": "MIT", 8 | "engines": { 9 | "node": ">=14" 10 | }, 11 | "workspaces": [ 12 | "packages/*", 13 | "example/" 14 | ], 15 | "scripts": { 16 | "build": "lerna run build --parallel --no-private", 17 | "watch": "lerna run watch --parallel --no-private", 18 | "prebuild": "run-s clean", 19 | "prewatch": "run-s clean", 20 | "dev": "run-s watch", 21 | "start": "run-s watch", 22 | "clean": "del packages/*/build", 23 | "test": "run-s test:*", 24 | "test:prettier": "prettier '**/*.{js,jsx,json,ts,tsx}' --check", 25 | "publish": "lerna publish", 26 | "bootstrap": "lerna bootstrap", 27 | "postinstall": "run-s bootstrap" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^17.0.22", 31 | "del-cli": "^4.0.1", 32 | "lerna": "^4.0.0", 33 | "microbundle": "^0.15.1", 34 | "npm-run-all": "^4.1.5", 35 | "prettier": "^2.6.0", 36 | "typescript": "^4.6.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/react-static-tweets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-static-tweets", 3 | "version": "2.0.0", 4 | "description": "Extremely fast static renderer for tweets.", 5 | "repository": "transitive-bullshit/react-static-tweets", 6 | "author": "Travis Fischer ", 7 | "license": "MIT", 8 | "main": "./build/index.js", 9 | "module": "./build/index.module.js", 10 | "types": "./build/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "import": "./build/index.module.js", 14 | "require": "./build/index.module.js", 15 | "types": "./build/index.d.ts", 16 | "default": "./build/index.js" 17 | }, 18 | "./client": { 19 | "import": "./build/client.module.js", 20 | "require": "./build/client.module.js", 21 | "types": "./build/client.d.ts", 22 | "default": "./build/client.js" 23 | }, 24 | "./styles.css": "./styles.css" 25 | }, 26 | "sideEffects": false, 27 | "engines": { 28 | "node": ">=12" 29 | }, 30 | "scripts": { 31 | "build": "microbundle -f cjs,esm --no-compress src/index.ts src/client.ts", 32 | "watch": "microbundle -f cjs,esm --no-compress --watch src/index.ts src/client.ts" 33 | }, 34 | "dependencies": { 35 | "clsx": "^1.1.1" 36 | }, 37 | "devDependencies": { 38 | "@types/react": "^17.0.3", 39 | "date-fns": "^2.17.0", 40 | "next": "^13.1.1", 41 | "react": "^18.2.0", 42 | "react-dom": "^18.2.0" 43 | }, 44 | "peerDependencies": { 45 | "date-fns": ">=2", 46 | "next": ">=13", 47 | "react": ">=18", 48 | "react-dom": ">=18" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/react-static-tweets/readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | React Static Tweets 4 | 5 |

6 | 7 | # React Static Tweets 8 | 9 | > Extremely fast static renderer for tweets. 10 | 11 | [![NPM](https://img.shields.io/npm/v/react-static-tweets.svg)](https://www.npmjs.com/package/react-static-tweets) [![Build Status](https://github.com/transitive-bullshit/react-static-tweets/actions/workflows/test.yml/badge.svg)](https://github.com/transitive-bullshit/react-static-tweets/actions/workflows/test.yml) [![Prettier Code Formatting](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 12 | 13 | ## Docs 14 | 15 | See the [main docs](https://github.com/transitive-bullshit/react-static-tweets). 16 | 17 | ## License 18 | 19 | MIT © [Travis Fischer](https://transitivebullsh.it) 20 | 21 | Support my OSS work by following me on twitter twitter 22 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/client.ts: -------------------------------------------------------------------------------- 1 | export * from './twitter' 2 | export * from './tweet' 3 | export * from './tweet-client' 4 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/format-number.ts: -------------------------------------------------------------------------------- 1 | export default function formatNumber(n) { 2 | return n.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,') 3 | } 4 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/html/handlers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function getContainerClassName(dataType) { 4 | if (!dataType) return 5 | 6 | const [type, count] = dataType.split(' ') 7 | 8 | switch (type) { 9 | case 'image-container': 10 | return `image-container image-count-${count}` 11 | case 'gif-container': 12 | case 'video-container': 13 | return type 14 | } 15 | } 16 | 17 | export default { 18 | div(props, components, i) { 19 | const { data } = props 20 | const type = props.dataType || (data && data.type) 21 | 22 | if (type === 'tweet') { 23 | return ( 24 | 25 | {props.children} 26 | 27 | ) 28 | } 29 | 30 | if (type === 'poll-container') { 31 | return 32 | } 33 | 34 | const className = getContainerClassName(type) 35 | 36 | return ( 37 | 38 | {props.children} 39 | 40 | ) 41 | }, 42 | 43 | img({ dataType, ...props }, components, i) { 44 | if (dataType === 'emoji-for-text') { 45 | return 46 | } 47 | 48 | if (dataType === 'media-image') { 49 | return 50 | } 51 | 52 | return null 53 | }, 54 | 55 | a(props, components, i) { 56 | const type = props.dataType 57 | 58 | if (type === 'mention') { 59 | return ( 60 | 65 | ) 66 | } 67 | 68 | if (type === 'hashtag') { 69 | return ( 70 | 75 | ) 76 | } 77 | 78 | if (type === 'cashtag') { 79 | return ( 80 | 85 | ) 86 | } 87 | 88 | if (type === 'quote-tweet') { 89 | return 90 | } 91 | 92 | return ( 93 | 94 | {props.children} 95 | 96 | ) 97 | }, 98 | 99 | blockquote(props, components, i) { 100 | if (process.env.NEXT_PUBLIC_TWITTER_LOAD_WIDGETS === 'true') { 101 | const isEmbeddedTweet = props.className?.includes('twitter-tweet') 102 | 103 | if (isEmbeddedTweet) { 104 | return 105 | } 106 | } else { 107 | const ast = props.data?.ast 108 | 109 | if (ast) { 110 | return 111 | } 112 | } 113 | 114 | return 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/html/node.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import handlers from './handlers' 3 | 4 | const defaultHandler = (name) => (props, components) => { 5 | const Comp = components[name] 6 | return Comp ? : React.createElement(name, props) 7 | } 8 | 9 | function handleNode(node, components, i = undefined) { 10 | if (!node) { 11 | return null 12 | } 13 | 14 | if (typeof node === 'string') { 15 | return node 16 | } 17 | 18 | const handler = handlers[node.tag] || defaultHandler(node.tag) 19 | 20 | if (!handler) { 21 | console.error('tweet error missing handler for:', node) 22 | return null 23 | } 24 | 25 | const { nodes } = node 26 | const props = { ...node.props, key: i } 27 | 28 | // Always send className as a string 29 | if (props.className && Array.isArray(props.className)) { 30 | props.className = props.className.join(' ') 31 | } 32 | 33 | if (node.data) { 34 | props.data = node.data 35 | } 36 | 37 | if (nodes && Array.isArray(nodes)) { 38 | props.children = nodes.map((node, i) => handleNode(node, components, i)) 39 | } 40 | 41 | const element = handler(props, components, i, node) 42 | 43 | if (!element) { 44 | console.error('A handler returned null for:', node) 45 | } 46 | 47 | return element 48 | } 49 | 50 | export default function Node({ components, node }) { 51 | return handleNode(node, components) 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tweet' 2 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/tweet-client.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import cs from 'clsx' 3 | 4 | import { useTwitterContext } from './twitter' 5 | import Node from './html/node' 6 | import components from './twitter-layout/components' 7 | 8 | interface TweetProps { 9 | id: string 10 | ast?: any 11 | caption?: string 12 | className?: string 13 | // TODO: understand what br is used for 14 | // br?: string 15 | } 16 | 17 | const TweetClient = forwardRef( 18 | ({ id, ast, caption, className }: TweetProps, ref) => { 19 | const twitter = useTwitterContext() 20 | const tweetAst = ast || twitter.tweetAstMap[id] 21 | if (!tweetAst) { 22 | return null 23 | } 24 | 25 | return ( 26 |
27 | {tweetAst && ( 28 | <> 29 | 30 | 31 | {caption &&

{caption}

} 32 | 33 | )} 34 |
35 | ) 36 | } 37 | ) 38 | 39 | export { TweetClient } 40 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/tweet.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import cs from 'clsx' 3 | 4 | import Node from './html/node' 5 | import components from './twitter-layout/components' 6 | 7 | interface TweetProps { 8 | ast: any 9 | caption?: string 10 | className?: string 11 | // TODO: understand what br is used for 12 | // br?: string 13 | } 14 | 15 | const Tweet = forwardRef( 16 | ({ ast, caption, className }: TweetProps, ref) => { 17 | if (!ast?.length) { 18 | return null 19 | } 20 | 21 | return ( 22 |
23 | 24 | 25 | {caption &&

{caption}

} 26 |
27 | ) 28 | } 29 | ) 30 | 31 | export { Tweet } 32 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/anchor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cs from 'clsx' 3 | 4 | const PROTOCOL = /^(https?:|)\/\// 5 | 6 | const beautifyHref = (href) => { 7 | const text = href.replace(PROTOCOL, '') 8 | const i = text.indexOf('/') 9 | 10 | if (i === -1) return text 11 | // Remove trailing slash 12 | if (i === text.length - 1) { 13 | return text.substring(0, i) 14 | } 15 | 16 | const hostname = text.substring(0, i) 17 | const pathname = text.substring(i) 18 | 19 | // Hide large paths similarly to how twitter does it 20 | return pathname.length > 20 21 | ? `${hostname}${pathname.substring(0, 15)}...` 22 | : text 23 | } 24 | 25 | export const A = (p) => ( 26 | 33 | {p.children[0] === p.href ? beautifyHref(p.href) : p.children} 34 | 35 | ) 36 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/code.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Code = (p) => 4 | 5 | export const Pre = (p) =>
6 | 


--------------------------------------------------------------------------------
/packages/react-static-tweets/src/twitter-layout/components/containers.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | 
3 | export const Div = (p) => 
{p.children}
4 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/embedded-tweet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Node from '../../html/node' 4 | import components from './index' 5 | 6 | export default function EmbeddedTweet({ ast }) { 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/headings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Permalink = ({ children, id }) => ( 4 | 5 | 6 | {children} 7 | # 8 | 9 | ) 10 | 11 | export const H1 = (p) => ( 12 |

13 | {p.children} 14 |

15 | ) 16 | 17 | export const H2 = (p) => ( 18 |

19 | {p.children} 20 |

21 | ) 22 | 23 | export const H3 = (p) => ( 24 |

25 | {p.children} 26 |

27 | ) 28 | 29 | export const H4 = (p) => ( 30 |

31 | {p.children} 32 |

33 | ) 34 | 35 | export const H5 = (p) => ( 36 |
37 | {p.children} 38 |
39 | ) 40 | 41 | export const H6 = (p) => ( 42 |
43 | {p.children} 44 |
45 | ) 46 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/index.ts: -------------------------------------------------------------------------------- 1 | import { Div } from './containers' 2 | import { H1, H2, H3, H4, H5, H6 } from './headings' 3 | import { P, Blockquote, Hr } from './text' 4 | import { Code, Pre } from './code' 5 | import { A } from './anchor' 6 | import { Ul, Ol, Li } from './lists' 7 | import { Table, Th, Td } from './table' 8 | import { Img } from './media' 9 | import { Mention, Hashtag, Cashtag, Emoji, Poll } from './twitter' 10 | import Tweet from './tweet/tweet' 11 | import EmbeddedTweet from './embedded-tweet' 12 | 13 | export default { 14 | div: Div, 15 | 16 | h1: H1, 17 | h2: H2, 18 | h3: H3, 19 | h4: H4, 20 | h5: H5, 21 | h6: H6, 22 | 23 | p: P, 24 | blockquote: Blockquote, 25 | hr: Hr, 26 | 27 | code: Code, 28 | pre: Pre, 29 | 30 | a: A, 31 | 32 | ul: Ul, 33 | ol: Ol, 34 | li: Li, 35 | 36 | table: Table, 37 | th: Th, 38 | td: Td, 39 | 40 | img: Img, 41 | 42 | Mention, 43 | Hashtag, 44 | Cashtag, 45 | Emoji, 46 | Poll, 47 | 48 | Tweet, 49 | EmbeddedTweet 50 | } 51 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/lists.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Ul = (p) =>
    4 | 5 | export const Ol = (p) =>
      6 | 7 | export const Li = (p) =>
    1. 8 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/media.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Image from 'next/image' 3 | 4 | export const Img = ({ width, height, src, dataUrl, ...p }) => { 5 | const image = ( 6 | {`Tweet 17 | ) 18 | 19 | return ( 20 |
      21 | 27 | {dataUrl ? ( 28 | 34 | {image} 35 | 36 | ) : ( 37 |
      {image}
      38 | )} 39 |
      40 |
      41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/table.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Table = (p) => ( 4 |
      5 | 6 | 7 | ) 8 | 9 | export const Th = (p) =>
      10 | 11 | export const Td = (p) => 12 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/text.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cs from 'clsx' 3 | 4 | export const P = ({ className = undefined, ...p }) => ( 5 |

      6 | ) 7 | 8 | export const Blockquote = ({ className = undefined, ...p }) => ( 9 |

      10 | ) 11 | 12 | export const Hr = ({ className = undefined, ...p }) => ( 13 |
      14 | ) 15 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/tweet/tweet-header.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | } 4 | .avatar { 5 | height: 2.25rem; 6 | width: 2.25rem; 7 | margin-right: 0.625rem; 8 | } 9 | .author { 10 | display: flex; 11 | flex-direction: column; 12 | text-decoration: none; 13 | color: inherit; 14 | } 15 | @media (any-hover: hover) { 16 | .author:hover { 17 | color: var(--tweet-link-color-hover); 18 | } 19 | } 20 | .name, 21 | .username { 22 | line-height: 1.2; 23 | text-overflow: ellipsis; 24 | white-space: nowrap; 25 | overflow: hidden; 26 | color: black; 27 | } 28 | .name { 29 | font-weight: 700; 30 | } 31 | .username { 32 | color: var(--tweet-color-gray); 33 | font-size: 0.875rem; 34 | } 35 | .brand { 36 | margin-left: auto; 37 | } 38 | .icon-twitter { 39 | display: inline-block; 40 | height: 1.25em; 41 | vertical-align: text-bottom; 42 | background-size: contain; 43 | background-repeat: no-repeat; 44 | width: 1.25em; 45 | background-image: url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%3Cpath%20fill%3D%22none%22%20d%3D%22M0%200h72v72H0z%22%2F%3E%3Cpath%20class%3D%22icon%22%20fill%3D%22%231da1f2%22%20d%3D%22M68.812%2015.14c-2.348%201.04-4.87%201.744-7.52%202.06%202.704-1.62%204.78-4.186%205.757-7.243-2.53%201.5-5.33%202.592-8.314%203.176C56.35%2010.59%2052.948%209%2049.182%209c-7.23%200-13.092%205.86-13.092%2013.093%200%201.026.118%202.02.338%202.98C25.543%2024.527%2015.9%2019.318%209.44%2011.396c-1.125%201.936-1.77%204.184-1.77%206.58%200%204.543%202.312%208.552%205.824%2010.9-2.146-.07-4.165-.658-5.93-1.64-.002.056-.002.11-.002.163%200%206.345%204.513%2011.638%2010.504%2012.84-1.1.298-2.256.457-3.45.457-.845%200-1.666-.078-2.464-.23%201.667%205.2%206.5%208.985%2012.23%209.09-4.482%203.51-10.13%205.605-16.26%205.605-1.055%200-2.096-.06-3.122-.184%205.794%203.717%2012.676%205.882%2020.067%205.882%2024.083%200%2037.25-19.95%2037.25-37.25%200-.565-.013-1.133-.038-1.693%202.558-1.847%204.778-4.15%206.532-6.774z%22%2F%3E%3C%2Fsvg%3E); 46 | } 47 | .rounded { 48 | border-radius: 50%; 49 | } 50 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/tweet/tweet-header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Image from 'next/image' 3 | 4 | export default function TweetHeader({ tweet }) { 5 | const authorUrl = `https://twitter.com/${tweet.username}` 6 | const tweetUrl = `https://twitter.com/${tweet.username}/status/${tweet.id}` 7 | const avatar = tweet.avatar.normal 8 | 9 | return ( 10 |
      11 | 17 | {tweet.name} 24 | 25 | 26 | 32 | 33 | {tweet.name} 34 | 35 | 36 | 40 | @{tweet.username} 41 | 42 | 43 | 44 | 50 |
      55 | 56 |
      57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/tweet/tweet-info.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cs from 'clsx' 3 | import format from 'date-fns/format' 4 | 5 | import formatNumber from '../../../format-number' 6 | 7 | export default function TweetInfo({ tweet, className = undefined }) { 8 | const likeUrl = `https://twitter.com/intent/like?tweet_id=${tweet.id}` 9 | const tweetUrl = `https://twitter.com/${tweet.username}/status/${tweet.id}` 10 | const createdAt = new Date(tweet.createdAt) 11 | 12 | return ( 13 |
      14 | 21 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/tweet/tweet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TweetHeader from './tweet-header' 3 | import TweetInfo from './tweet-info' 4 | 5 | export default function Tweet({ children, data }) { 6 | return ( 7 |
      8 |
      9 | 10 | {children} 11 | 12 |
      13 |
      14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/components/twitter.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import cs from 'clsx' 3 | 4 | import formatDistanceStrict from 'date-fns/formatDistanceStrict' 5 | 6 | export const TwitterLink = (p) => ( 7 | 14 | {p.type} 15 | 16 | {p.children} 17 | 18 | ) 19 | 20 | export const Mention = (p) => ( 21 | 22 | {p.children[0].replace(/^@/, '')} 23 | 24 | ) 25 | 26 | export const Hashtag = (p) => ( 27 | 28 | {p.children[0].replace(/^\#/, '')} 29 | 30 | ) 31 | 32 | export const Cashtag = (p) => ( 33 | 34 | {p.children[0].replace(/^\$/, '')} 35 | 36 | ) 37 | 38 | export const Emoji = ({ className, ...p }) => ( 39 | 40 | ) 41 | 42 | // Note: Poll data is most likely cached, so ongoing polls will not be updated 43 | // until a revalidation happens 44 | export const Poll = ({ data }) => { 45 | const votesCount = data.options.reduce( 46 | (count, option) => count + option.votes, 47 | 0 48 | ) 49 | const endsAt = new Date(data.endsAt) 50 | const now = new Date() 51 | 52 | return ( 53 |
      54 |
      55 | {data.options.map((option) => { 56 | const per = Math.round((option.votes / votesCount) * 100) || 0 57 | const width = per || 1 + '%' 58 | const widthLabel = per + '%' 59 | 60 | return ( 61 | 62 | {option.label} 63 | 64 | {widthLabel} 65 | 66 | ) 67 | })} 68 |
      69 |
      70 |
      71 | {votesCount} votes 72 | 73 | {now > endsAt 74 | ? 'Final results' 75 | : `${formatDistanceStrict(endsAt, now)} left`} 76 | 77 |
      78 |
      79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cs from 'clsx' 3 | 4 | export const Skeleton: React.FC<{ 5 | children?: React.ReactNode 6 | className?: string 7 | style?: React.CSSProperties 8 | }> = ({ children, className, style }) => { 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter-layout/tweet-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cs from 'clsx' 3 | import { Skeleton } from './skeleton' 4 | 5 | export default function TweetSkeleton({ 6 | simple = false, 7 | className = undefined 8 | }) { 9 | return ( 10 |
      11 |
      12 | 13 | 14 | 15 |
      16 | 17 | {simple ? null : ( 18 |
      19 | 20 |
      21 | )} 22 |
      23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-static-tweets/src/twitter.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode, useContext } from 'react' 2 | 3 | // TODO: make this more specific 4 | export type TweetAst = Array 5 | 6 | export type TwitterContextValue = { 7 | // static tweet ast info 8 | tweetAstMap: TweetAstMap 9 | } 10 | 11 | export type TweetAstMap = { 12 | [tweetId: string]: TweetAst 13 | } 14 | 15 | export interface TwitterContextProviderProps { 16 | value: Partial 17 | children?: ReactNode 18 | } 19 | 20 | // Saves the tweets returned as props to the page 21 | const TwitterContext = createContext({ 22 | tweetAstMap: {} 23 | }) 24 | 25 | export function useTwitterContext() { 26 | return useContext(TwitterContext) 27 | } 28 | 29 | // allows partials that override outer providers 30 | export function TwitterContextProvider({ 31 | value, 32 | children 33 | }: TwitterContextProviderProps) { 34 | const currentContext = useContext(TwitterContext) 35 | const { tweetAstMap, ...rest } = value 36 | const mergedContext = { 37 | ...currentContext, 38 | ...rest, 39 | tweetAstMap: { 40 | ...currentContext.tweetAstMap, 41 | ...tweetAstMap 42 | } 43 | } 44 | 45 | return ( 46 | 47 | {children} 48 | 49 | ) 50 | } 51 | 52 | export const TwitterContextConsumer = TwitterContext.Consumer 53 | -------------------------------------------------------------------------------- /packages/react-static-tweets/styles.css: -------------------------------------------------------------------------------- 1 | .static-tweet { 2 | --colors-blue: #0c00ff; 3 | --colors-purple: #be00ff; 4 | 5 | --accents-1: #fafafa; 6 | --accents-2: #eaeaea; 7 | --accents-3: #999999; 8 | --accents-4: #888888; 9 | --accents-5: #666666; 10 | 11 | --bg-color: #fff; 12 | --link-color: var(--colors-blue); 13 | --poll-bar-color: var(--colors-blue); 14 | --inline-code-color: var(--colors-purple); 15 | --code-color: #9efeff; 16 | --code-bg-color: #1e1e3f; 17 | 18 | --text-margin: 1.25rem 0; 19 | --container-margin: 0.5rem 0; 20 | --poll-margin: 1.5rem 1rem; 21 | --heading-margin-top: 3.5rem; 22 | --heading-margin-bottom: 2rem; 23 | --li-margin: 0 0 0.5rem 0; 24 | 25 | /* Embedded tweet */ 26 | --tweet-font: normal normal 16px/1.4 Helvetica, Roboto, 'Segoe UI', Calibri, 27 | sans-serif; 28 | --tweet-font-color: #1c2022; 29 | --tweet-bg-color: #fff; 30 | --tweet-border: 1px solid #e1e8ed; 31 | --tweet-border-hover: 1px solid #ccd6dd; 32 | --tweet-link-color: #2b7bb9; 33 | --tweet-link-color-hover: #3b94d9; 34 | --tweet-color-gray: #697882; 35 | --tweet-color-red: #e02460; 36 | } 37 | 38 | .static-tweet { 39 | width: 100%; 40 | max-width: 550px; 41 | min-width: 220px; 42 | } 43 | 44 | .static-tweet-caption { 45 | font-size: 14px; 46 | color: #999; 47 | text-align: center; 48 | margin: 0; 49 | margin-top: 10px; 50 | padding: 0; 51 | } 52 | 53 | .static-tweet-anchor { 54 | color: var(--tweet-link-color); 55 | text-decoration: none; 56 | } 57 | 58 | @media (any-hover: hover) { 59 | .static-tweet-anchor:hover { 60 | text-decoration: underline; 61 | } 62 | } 63 | 64 | .static-tweet code { 65 | font-size: 14px; 66 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 67 | Bitstream Vera Sans Mono, Courier New, monospace, serif; 68 | } 69 | 70 | .static-tweet code.inline { 71 | color: var(--inline-code-color); 72 | font-size: 1rem; 73 | white-space: pre-wrap; 74 | } 75 | 76 | .static-tweet pre { 77 | color: var(--code-color); 78 | background: var(--code-bg-color); 79 | padding: 1.25rem; 80 | margin: var(--container-margin); 81 | white-space: pre; 82 | overflow: auto; 83 | -webkit-overflow-scrolling: touch; 84 | } 85 | 86 | .static-tweet .image-container { 87 | display: grid; 88 | grid-template-columns: repeat(auto-fit, minmax(50%, 1fr)); 89 | margin: var(--container-margin); 90 | } 91 | .static-tweet .image-count-3 > :global(:first-child) { 92 | grid-row-end: span 2; 93 | } 94 | .static-tweet .gif-container, 95 | .static-tweet .video-container { 96 | margin: var(--container-margin); 97 | } 98 | .static-tweet .gif-container > :global(video), 99 | .static-tweet .video-container > :global(video) { 100 | width: 100%; 101 | max-height: 500px; 102 | } 103 | 104 | .static-tweet-permalink span[id] { 105 | display: block; 106 | position: absolute; 107 | visibility: hidden; 108 | margin-top: calc(-1 * var(--heading-margin-top)); 109 | padding-top: var(--heading-margin-top); 110 | } 111 | 112 | .static-tweet-permalink a { 113 | color: inherit; 114 | text-decoration: none; 115 | } 116 | @media (any-hover: hover) { 117 | .static-tweet-permalink a:hover { 118 | color: inherit; 119 | border-bottom: 1px solid; 120 | } 121 | .static-tweet-permalink a:hover ~ .permalink { 122 | visibility: visible; 123 | } 124 | } 125 | .static-tweet-permalink .permalink { 126 | visibility: hidden; 127 | display: none; 128 | font-weight: 500; 129 | } 130 | @media screen and (min-width: 992px) { 131 | .static-tweet-permalink a { 132 | margin-right: 0.5rem; 133 | } 134 | .static-tweet-permalink .permalink { 135 | display: inline-block; 136 | } 137 | } 138 | 139 | .static-tweet-h1, 140 | .static-tweet-h2, 141 | .static-tweet-h3, 142 | .static-tweet-h4, 143 | .static-tweet-h5, 144 | .static-tweet-h6 { 145 | font-weight: 600; 146 | margin: var(--heading-margin-top) 0 var(--heading-margin-bottom) 0; 147 | } 148 | 149 | .static-tweet-h1 { 150 | font-size: 2rem; 151 | } 152 | 153 | .static-tweet-h2 { 154 | font-size: 1.75rem; 155 | } 156 | 157 | .static-tweet-h3 { 158 | font-size: 1.5rem; 159 | } 160 | 161 | .static-tweet-h4 { 162 | font-size: 1.25rem; 163 | } 164 | 165 | .static-tweet-h5 { 166 | font-size: 1rem; 167 | } 168 | 169 | .static-tweet-h6 { 170 | font-size: 0.875rem; 171 | } 172 | 173 | .static-tweet ul { 174 | margin: var(--text-margin); 175 | list-style-type: none; 176 | padding-left: 1rem; 177 | } 178 | 179 | .static-tweet ul li:before { 180 | content: '-'; 181 | color: var(--accents-3); 182 | position: absolute; 183 | margin-left: -1rem; 184 | } 185 | 186 | .static-tweet ol { 187 | margin: var(--text-margin); 188 | padding-left: 1rem; 189 | } 190 | 191 | .static-tweet li { 192 | padding-left: 0; 193 | margin: var(--li-margin); 194 | } 195 | 196 | .static-tweet-summary { 197 | position: relative; 198 | box-sizing: border-box; 199 | } 200 | 201 | .static-tweet-details { 202 | height: 100%; 203 | overflow: hidden; 204 | } 205 | 206 | .static-tweet-summary { 207 | position: relative; 208 | height: 100%; 209 | list-style: none; 210 | } 211 | 212 | .static-tweet-summary::marker { 213 | display: none; 214 | } 215 | 216 | .static-tweet-table-container { 217 | display: flex; 218 | justify-content: center; 219 | width: 100%; 220 | margin: var(--container-margin); 221 | } 222 | 223 | .static-tweet-table-container table { 224 | display: block; 225 | overflow: auto; 226 | border-collapse: collapse; 227 | } 228 | 229 | .static-tweet-table-container th { 230 | font-weight: 600; 231 | padding: 0.5rem 0.875rem; 232 | border: 1px solid var(--accents-2); 233 | } 234 | 235 | .static-tweet-table-container td { 236 | padding: 0.5rem 0.875rem; 237 | border: 1px solid var(--accents-2); 238 | } 239 | 240 | .static-tweet-p { 241 | margin: var(--text-margin); 242 | white-space: pre-wrap; 243 | word-wrap: break-word; 244 | } 245 | 246 | .static-tweet blockquote { 247 | margin: 0; 248 | margin-block-start: 0; 249 | margin-block-end: 0; 250 | margin-inline-start: 0; 251 | margin-inline-end: 0; 252 | } 253 | 254 | .static-tweet-blockquote { 255 | background: var(--accents-1); 256 | color: var(--accents-5); 257 | border: 1px solid var(--accents-2); 258 | margin: var(--container-margin); 259 | padding: 0 1.25rem; 260 | } 261 | 262 | .static-tweet-hr { 263 | border: 0; 264 | border-top: 1px solid var(--accents-2); 265 | margin: var(--text-margin); 266 | } 267 | 268 | .static-tweet-twitter-link, 269 | .static-tweet-twitter-link s { 270 | text-decoration: none; 271 | } 272 | 273 | @media (any-hover: hover) { 274 | .static-tweet-twitter-link:hover { 275 | text-decoration: underline; 276 | } 277 | } 278 | 279 | .static-tweet-emoji { 280 | height: 1.2em !important; 281 | width: 1.2em !important; 282 | margin: 0 2px; 283 | vertical-align: -3px; 284 | } 285 | 286 | .static-tweet-poll { 287 | margin: var(--poll-margin); 288 | } 289 | .static-tweet-options { 290 | display: grid; 291 | grid-template-columns: max-content 14rem max-content; 292 | align-items: center; 293 | grid-gap: 1rem; 294 | overflow: auto; 295 | } 296 | .static-tweet-label { 297 | overflow: auto; 298 | text-align: right; 299 | white-space: pre-wrap; 300 | word-wrap: break-word; 301 | } 302 | .static-tweet-chart { 303 | height: 100%; 304 | background: var(--poll-bar-color); 305 | } 306 | .static-tweet-poll hr { 307 | border: 0; 308 | border-top: 1px solid var(--accents-2); 309 | margin: 1rem 0 0.5rem 0; 310 | } 311 | .static-tweet-footer { 312 | display: flex; 313 | font-size: 0.875rem; 314 | color: var(--accents-4); 315 | } 316 | .static-tweet-votes-count { 317 | flex-grow: 1; 318 | } 319 | @media screen and (max-width: 450px) { 320 | .static-tweet-options { 321 | grid-template-columns: max-content 7rem max-content; 322 | } 323 | } 324 | 325 | .static-tweet-info a { 326 | text-decoration: none; 327 | } 328 | .static-tweet-info { 329 | font-size: 0.875rem; 330 | display: flex; 331 | } 332 | .static-tweet-like { 333 | display: flex; 334 | color: var(--tweet-color-gray); 335 | margin-right: 0.75rem; 336 | } 337 | .static-tweet-like:visited { 338 | color: var(--tweet-link-color); 339 | } 340 | @media (any-hover: hover) { 341 | .static-tweet-like:hover { 342 | color: var(--tweet-color-red); 343 | } 344 | .static-tweet-like:hover .static-tweet-icon-heart { 345 | background-image: url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20class%3D%22icon%22%20fill%3D%22%23E0245E%22%20d%3D%22M12%2021.638h-.014C9.403%2021.59%201.95%2014.856%201.95%208.478c0-3.064%202.525-5.754%205.403-5.754%202.29%200%203.83%201.58%204.646%202.73.813-1.148%202.353-2.73%204.644-2.73%202.88%200%205.404%202.69%205.404%205.755%200%206.375-7.454%2013.11-10.037%2013.156H12zM7.354%204.225c-2.08%200-3.903%201.988-3.903%204.255%200%205.74%207.035%2011.596%208.55%2011.658%201.52-.062%208.55-5.917%208.55-11.658%200-2.267-1.822-4.255-3.902-4.255-2.528%200-3.94%202.936-3.952%202.965-.23.562-1.156.562-1.387%200-.015-.03-1.426-2.965-3.955-2.965z%22%2F%3E%3C%2Fsvg%3E); 346 | } 347 | } 348 | .static-tweet-icon-heart { 349 | width: 1.25em; 350 | background-image: url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20class%3D%22icon%22%20fill%3D%22%23697882%22%20d%3D%22M12%2021.638h-.014C9.403%2021.59%201.95%2014.856%201.95%208.478c0-3.064%202.525-5.754%205.403-5.754%202.29%200%203.83%201.58%204.646%202.73.813-1.148%202.353-2.73%204.644-2.73%202.88%200%205.404%202.69%205.404%205.755%200%206.375-7.454%2013.11-10.037%2013.156H12zM7.354%204.225c-2.08%200-3.903%201.988-3.903%204.255%200%205.74%207.035%2011.596%208.55%2011.658%201.52-.062%208.55-5.917%208.55-11.658%200-2.267-1.822-4.255-3.902-4.255-2.528%200-3.94%202.936-3.952%202.965-.23.562-1.156.562-1.387%200-.015-.03-1.426-2.965-3.955-2.965z%22%2F%3E%3C%2Fsvg%3E); 351 | } 352 | .static-tweet-likes { 353 | margin-left: 0.25rem; 354 | } 355 | .static-tweet-time { 356 | display: block; 357 | flex: 1; 358 | color: var(--tweet-color-gray); 359 | } 360 | @media (any-hover: hover) { 361 | .static-tweet-time:hover, 362 | .static-tweet-time:focus { 363 | color: var(--tweet-link-color-hover); 364 | } 365 | .static-tweet-time:focus { 366 | text-decoration: underline; 367 | } 368 | } 369 | 370 | .static-tweet-body { 371 | color: var(--tweet-font-color); 372 | font: var(--tweet-font); 373 | overflow: hidden; 374 | background: var(--tweet-bg-color); 375 | border: var(--tweet-border); 376 | border-radius: 5px; 377 | } 378 | 379 | @media (any-hover: hover) { 380 | .static-tweet-body:hover { 381 | border: var(--tweet-border-hover); 382 | } 383 | } 384 | 385 | .static-tweet-body-blockquote { 386 | position: relative; 387 | padding: 1.25rem 1.25rem 0.625rem 1.25rem; 388 | } 389 | 390 | .static-tweet-icon { 391 | display: inline-block; 392 | height: 1.25em; 393 | vertical-align: text-bottom; 394 | background-size: contain; 395 | background-repeat: no-repeat; 396 | } 397 | 398 | .static-tweet-header { 399 | display: flex; 400 | } 401 | .static-tweet-header-avatar { 402 | height: 2.25rem; 403 | width: 2.25rem; 404 | margin-right: 0.625rem; 405 | } 406 | .static-tweet-header-author { 407 | display: flex; 408 | flex-direction: column; 409 | text-decoration: none; 410 | color: inherit; 411 | } 412 | @media (any-hover: hover) { 413 | .static-tweet-header-author:hover { 414 | color: var(--tweet-link-color-hover); 415 | } 416 | } 417 | .static-tweet-header-name, 418 | .static-tweet-header-username { 419 | line-height: 1.2; 420 | text-overflow: ellipsis; 421 | white-space: nowrap; 422 | overflow: hidden; 423 | color: black; 424 | } 425 | .static-tweet-header-name { 426 | font-weight: 700; 427 | } 428 | .static-tweet-header-username { 429 | color: var(--tweet-color-gray); 430 | font-size: 0.875rem; 431 | } 432 | .static-tweet-header-brand { 433 | margin-left: auto; 434 | } 435 | .static-tweet-header-icon-twitter { 436 | display: inline-block; 437 | height: 1.25em; 438 | vertical-align: text-bottom; 439 | background-size: contain; 440 | background-repeat: no-repeat; 441 | width: 1.25em; 442 | background-image: url(data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2072%2072%22%3E%3Cpath%20fill%3D%22none%22%20d%3D%22M0%200h72v72H0z%22%2F%3E%3Cpath%20class%3D%22icon%22%20fill%3D%22%231da1f2%22%20d%3D%22M68.812%2015.14c-2.348%201.04-4.87%201.744-7.52%202.06%202.704-1.62%204.78-4.186%205.757-7.243-2.53%201.5-5.33%202.592-8.314%203.176C56.35%2010.59%2052.948%209%2049.182%209c-7.23%200-13.092%205.86-13.092%2013.093%200%201.026.118%202.02.338%202.98C25.543%2024.527%2015.9%2019.318%209.44%2011.396c-1.125%201.936-1.77%204.184-1.77%206.58%200%204.543%202.312%208.552%205.824%2010.9-2.146-.07-4.165-.658-5.93-1.64-.002.056-.002.11-.002.163%200%206.345%204.513%2011.638%2010.504%2012.84-1.1.298-2.256.457-3.45.457-.845%200-1.666-.078-2.464-.23%201.667%205.2%206.5%208.985%2012.23%209.09-4.482%203.51-10.13%205.605-16.26%205.605-1.055%200-2.096-.06-3.122-.184%205.794%203.717%2012.676%205.882%2020.067%205.882%2024.083%200%2037.25-19.95%2037.25-37.25%200-.565-.013-1.133-.038-1.693%202.558-1.847%204.778-4.15%206.532-6.774z%22%2F%3E%3C%2Fsvg%3E); 443 | } 444 | .static-tweet-header-rounded { 445 | border-radius: 50%; 446 | } 447 | 448 | .static-tweet-skeleton { 449 | display: block; 450 | width: 100%; 451 | border-radius: 5px; 452 | 453 | background-image: linear-gradient( 454 | 270deg, 455 | var(--accents-1), 456 | var(--accents-2), 457 | var(--accents-2), 458 | var(--accents-1) 459 | ); 460 | background-size: 400% 100%; 461 | animation: static-tweet-skeleton-loading 8s ease-in-out infinite; 462 | } 463 | 464 | @keyframes static-tweet-skeleton-loading { 465 | 0% { 466 | background-position: 200% 0; 467 | } 468 | 100% { 469 | background-position: -200% 0; 470 | } 471 | } 472 | 473 | .static-tweet-skeleton-container { 474 | background: var(--tweet-bg-color); 475 | border: var(--tweet-border); 476 | border-radius: 5px; 477 | } 478 | .static-tweet-skeleton-content { 479 | padding: 1.25rem 1.25rem 0.625rem 1.25rem; 480 | } 481 | .static-tweet-skeleton-footer { 482 | height: 2.5rem; 483 | padding: 0.625rem 1.25rem; 484 | border-top: var(--tweet-border); 485 | } 486 | -------------------------------------------------------------------------------- /packages/react-static-tweets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base", 3 | "compilerOptions": { 4 | "composite": true, 5 | "rootDir": "src", 6 | "outDir": "build", 7 | "tsBuildInfoFile": "build/.tsbuildinfo", 8 | "lib": ["DOM"], 9 | "strict": false, 10 | "noImplicitReturns": false, 11 | "skipLibCheck": true, 12 | "jsx": "react", 13 | "jsxFactory": "React.createElement", 14 | "jsxFragmentFactory": "React.Fragment" 15 | }, 16 | "include": ["node_modules/microbundle/index.d.ts", "src"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/static-tweets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "static-tweets", 3 | "version": "2.0.0", 4 | "description": "Utilities for fetching and manipulating tweet ASTs.", 5 | "repository": "transitive-bullshit/react-static-tweets", 6 | "author": "Travis Fischer ", 7 | "license": "MIT", 8 | "main": "./build/index.js", 9 | "module": "./build/index.module.js", 10 | "types": "./build/index.d.ts", 11 | "source": "./src/index.ts", 12 | "sideEffects": false, 13 | "engines": { 14 | "node": ">=14" 15 | }, 16 | "scripts": { 17 | "build": "microbundle -f cjs,esm --no-compress --target node", 18 | "watch": "microbundle -f cjs,esm --no-compress --target node --watch" 19 | }, 20 | "dependencies": { 21 | "@mapbox/rehype-prism": "^0.5.0", 22 | "cheerio": "^1.0.0-rc.5", 23 | "github-slugger": "^1.3.0", 24 | "node-fetch": "2", 25 | "rehype-parse": "^7.0.0", 26 | "rehype-raw": "^5.0.0", 27 | "rehype-sanitize": "^4.0.0", 28 | "remark-parse": "^9.0.0", 29 | "remark-rehype": "^8.0.0", 30 | "unified": "^9.0.0" 31 | }, 32 | "devDependencies": { 33 | "ava": "^3.13.0", 34 | "hast-util-sanitize": "^4.0.0", 35 | "mdast-util-to-string": "^3.0.0", 36 | "unist-util-visit": "^3.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/static-tweets/readme.md: -------------------------------------------------------------------------------- 1 |

      2 | 3 | React Static Tweets 4 | 5 |

      6 | 7 | # static-tweets 8 | 9 | > Utilities for fetching and manipulating tweet ASTs. 10 | 11 | [![NPM](https://img.shields.io/npm/v/static-tweets.svg)](https://www.npmjs.com/package/static-tweets) [![Build Status](https://github.com/transitive-bullshit/react-static-tweets/actions/workflows/test.yml/badge.svg)](https://github.com/transitive-bullshit/react-static-tweets/actions/workflows/test.yml) [![Prettier Code Formatting](https://img.shields.io/badge/code_style-prettier-brightgreen.svg)](https://prettier.io) 12 | 13 | ## Install 14 | 15 | ```bash 16 | npm install static-tweets 17 | ``` 18 | 19 | This package is compatible with Node.js. 20 | 21 | ## Usage 22 | 23 | ```ts 24 | import { fetchTweetAst } from 'static-tweets' 25 | 26 | const tweetId = '1358199505280262150' 27 | const tweetAst = await fetchTweetAst(tweetId) 28 | 29 | // tweetAst is a JSON representation of this tweet's contents 30 | // which `react-static-tweets` can use to render 31 | ``` 32 | 33 | ## Client-Side 34 | 35 | This package is only meant to be used on the server. If you want to render tweets dynamically on the client-side, then you'll need to wrap this package in an API route. 36 | 37 | ## Docs 38 | 39 | See the [main docs](https://github.com/transitive-bullshit/react-static-tweets). 40 | 41 | ## License 42 | 43 | MIT © [Travis Fischer](https://transitivebullsh.it) 44 | 45 | Support my OSS work by following me on twitter twitter 46 | -------------------------------------------------------------------------------- /packages/static-tweets/src/fetchTweetAst.ts: -------------------------------------------------------------------------------- 1 | import GithubSlugger from 'github-slugger' 2 | import { fetchTweetHtml } from './twitter/api' 3 | import { getTweetData } from './twitter/embed/tweet-html' 4 | import getTweetHtml from './twitter/getTweetHtml' 5 | import htmlToAst from './markdown/htmlToAst' 6 | 7 | class Context { 8 | slugger = new GithubSlugger() 9 | map = [] 10 | 11 | get(id) { 12 | return this.map[Number(id)] 13 | } 14 | 15 | add(data, nodes) { 16 | return this.map.push({ data, nodes }) - 1 17 | } 18 | } 19 | 20 | export async function fetchTweetAst(tweetId: string): Promise { 21 | const tweetHtml = await fetchTweetHtml(tweetId) 22 | const tweet = tweetHtml && getTweetData(tweetHtml) 23 | 24 | if (!tweet) return null 25 | 26 | const context = new Context() 27 | const html = await getTweetHtml(tweet, context) 28 | const ast = await htmlToAst(html, context) 29 | 30 | return ast 31 | } 32 | -------------------------------------------------------------------------------- /packages/static-tweets/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fetchTweetAst' 2 | -------------------------------------------------------------------------------- /packages/static-tweets/src/markdown/htmlToAst.ts: -------------------------------------------------------------------------------- 1 | import unified from 'unified' 2 | import parse from 'rehype-parse' 3 | import sanitize from 'rehype-sanitize' 4 | import tweet from './rehype-tweet' 5 | import minify from './rehype-minify' 6 | import schema from './schema' 7 | 8 | // Create the processor, the order of the plugins is important 9 | const getProcessor = unified() 10 | .use(parse) 11 | // Sanitize the HTML 12 | .use<[typeof schema]>(sanitize, schema) 13 | .use(minify) 14 | .freeze() 15 | 16 | export default async function htmlToAst(html, context) { 17 | try { 18 | const processor = getProcessor().use(tweet, context) 19 | const file = await processor.process(html) 20 | return file.result 21 | } catch (error) { 22 | // eslint-disable-next-line no-console 23 | console.error(`HTML to AST error: ${error}`) 24 | throw error 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/static-tweets/src/markdown/markdownToAst.ts: -------------------------------------------------------------------------------- 1 | import unified from 'unified' 2 | import markdown from 'remark-parse' 3 | import remarkToRehype from 'remark-rehype' 4 | import raw from 'rehype-raw' 5 | import prism from '@mapbox/rehype-prism' 6 | 7 | const handlers = { 8 | // Add a className to inlineCode so we can differentiate between it and code fragments 9 | inlineCode(_: any, node) { 10 | return { 11 | ...node, 12 | type: 'element', 13 | tagName: 'code', 14 | properties: { className: 'inline' }, 15 | children: [ 16 | { 17 | type: 'text', 18 | value: node.value 19 | } 20 | ] 21 | } 22 | } 23 | } 24 | 25 | function toAst() { 26 | this.Compiler = (tree) => tree 27 | } 28 | 29 | // Create the processor, the order of the plugins is important 30 | const processor = unified() 31 | .use(markdown) 32 | .use(remarkToRehype, { handlers, allowDangerousHtml: true }) 33 | // Add custom HTML found in the tweet to the AST 34 | .use(raw) 35 | // Add syntax highlighting 36 | .use(prism) 37 | .use(toAst) 38 | 39 | export default async function markdownToAst(md) { 40 | try { 41 | const file = await processor.process(md) 42 | return file.result 43 | } catch (error) { 44 | // eslint-disable-next-line no-console 45 | console.error(`Markdown to AST error: ${error}`) 46 | throw error 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/static-tweets/src/markdown/rehype-minify.ts: -------------------------------------------------------------------------------- 1 | function minifyAst(ast) { 2 | if (Array.isArray(ast)) { 3 | return ast.reduce((nodes, node) => { 4 | const n = minifyAst(node) 5 | // Empty new lines aren't required 6 | const isNoise = n === '\n' && nodes[nodes.length - 1]?.tag !== 'span' 7 | 8 | if (!isNoise) nodes.push(n) 9 | 10 | return nodes 11 | }, []) 12 | } 13 | // Handle the root ast 14 | if (!ast.tagName && ast.children) { 15 | return minifyAst(ast.children) 16 | } 17 | if (ast.type === 'text') { 18 | return ast.value 19 | } 20 | if (ast.type === 'element') { 21 | const node: any = { tag: ast.tagName } 22 | const children = ast.children?.length ? minifyAst(ast.children) : [] 23 | 24 | if (ast.properties && Object.keys(ast.properties).length) { 25 | node.props = ast.properties 26 | } 27 | if (ast.data) { 28 | node.data = ast.data 29 | } 30 | if (children.length) { 31 | node.nodes = children 32 | } 33 | 34 | return node 35 | } 36 | 37 | throw new Error( 38 | `Unable to handle the following AST: ${JSON.stringify(ast, null, 2)}` 39 | ) 40 | } 41 | 42 | function rehypeMinify() { 43 | this.Compiler = (tree) => minifyAst(tree) 44 | } 45 | 46 | export default rehypeMinify 47 | -------------------------------------------------------------------------------- /packages/static-tweets/src/markdown/rehype-tweet.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'url' 2 | import { visit } from 'unist-util-visit' 3 | import { toString } from 'mdast-util-to-string' 4 | 5 | const TWITTER_URL = 'https://twitter.com' 6 | const ABSOLUTE_URL = /^https?:\/\/|^\/\//i 7 | const HEADINGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] 8 | 9 | function visitAnchor(node) { 10 | if (!node.properties) return 11 | 12 | const { href } = node.properties 13 | 14 | if (!href) return 15 | 16 | const isAbsoluteUrl = ABSOLUTE_URL.test(href) 17 | 18 | if (!isAbsoluteUrl) { 19 | node.properties.href = resolve(TWITTER_URL, href) 20 | } 21 | } 22 | 23 | export default function rehypeTweet(context) { 24 | // Nodes may have custom data required by the UI 25 | function visitData(node) { 26 | const ctx = context.get(node.properties.dataId) 27 | 28 | if (ctx?.data) node.data = ctx.data 29 | 30 | // Add markdown content to the tweet container 31 | if (ctx?.nodes) { 32 | node.children.unshift(...ctx.nodes) 33 | } 34 | 35 | delete node.properties.dataId 36 | } 37 | 38 | function visitHeading(node) { 39 | const text = toString(node) 40 | 41 | if (!text) return 42 | 43 | const id = context.slugger.slug(text) 44 | 45 | node.data = { id } 46 | } 47 | 48 | return function transformer(tree) { 49 | visit(tree, (node: any) => node.properties?.dataId, visitData) 50 | visit(tree, (node: any) => node.tagName === 'a', visitAnchor) 51 | visit(tree, (node: any) => HEADINGS.includes(node.tagName), visitHeading) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/static-tweets/src/markdown/schema.ts: -------------------------------------------------------------------------------- 1 | import { defaultSchema, Schema } from 'hast-util-sanitize' 2 | 3 | const githubSchema: Schema = defaultSchema 4 | 5 | githubSchema.tagNames.push('video', 'source') 6 | 7 | // Allow className for all elements 8 | githubSchema.attributes['*'].push('className') 9 | 10 | // Allow specific attributes that are required for the page to render properly 11 | githubSchema.attributes.div = ['dataType', 'dataId'] 12 | githubSchema.attributes.blockquote = ['dataId'] 13 | githubSchema.attributes.img = ['dataType', 'src', 'height', 'width', 'dataUrl'] 14 | githubSchema.attributes.video = [ 15 | 'poster', 16 | 'controls', 17 | 'preload', 18 | 'playsInline', 19 | 'autoPlay', 20 | 'muted', 21 | 'loop' 22 | ] 23 | githubSchema.attributes.source = ['src'] 24 | 25 | export default githubSchema 26 | -------------------------------------------------------------------------------- /packages/static-tweets/src/twitter/api.ts: -------------------------------------------------------------------------------- 1 | import https from 'node:https' 2 | import fetch from 'node-fetch' 3 | 4 | const API_URL = 'https://api.twitter.com' 5 | const SYNDICATION_URL = 'https://syndication.twitter.com' 6 | const agent = new https.Agent({ maxCachedSessions: 0 }) 7 | 8 | function twitterLabsEnabled(expansions) { 9 | if (process.env.TWITTER_LABS_ENABLED !== 'true') return false 10 | if (!expansions) return true 11 | 12 | const exp = process.env.TWITTER_LABS_EXPANSIONS || '' 13 | 14 | return exp.includes(expansions) 15 | } 16 | 17 | async function get(url: string, opts?: any): Promise { 18 | // twitter's syndication API has some weird bugs with TLS, so we're explicitly 19 | // disabling TLS session reuse as a workaround 20 | // @see https://github.com/transitive-bullshit/react-static-tweets/issues/43 21 | const res = await fetch(url, { 22 | ...opts, 23 | agent 24 | }) 25 | 26 | if (res.ok) { 27 | return res.json() 28 | } 29 | 30 | if (res.status === 404) { 31 | return {} 32 | } 33 | 34 | throw new Error(`Twitter fetch error ${res.status} ${res.statusText}`) 35 | } 36 | 37 | export async function fetchTweetsHtml(ids) { 38 | return get(`${SYNDICATION_URL}/tweets.json?ids=${ids}`) 39 | } 40 | 41 | export async function fetchTweetHtml(id) { 42 | const html = await fetchTweetsHtml(id) 43 | return html[id] 44 | } 45 | 46 | export async function fetchUserStatus(tweetId) { 47 | // If there isn't an API token don't do anything, this is only required for videos. 48 | if (!process.env.TWITTER_ACCESS_TOKEN) return null 49 | 50 | return get( 51 | `${API_URL}/1.1/statuses/show/${tweetId}.json?include_entities=true&tweet_mode=extended`, 52 | { 53 | headers: { 54 | authorization: `Bearer ${process.env.TWITTER_ACCESS_TOKEN}` 55 | } 56 | } 57 | ) 58 | } 59 | 60 | export async function fetchTweetWithPoll(tweetId) { 61 | const expansions = 'attachments.poll_ids' 62 | 63 | // If there isn't an API token or Twitter Labs is not enabled, don't do anything, 64 | // this is only required for Polls. 65 | if (!process.env.TWITTER_ACCESS_TOKEN || !twitterLabsEnabled(expansions)) 66 | return null 67 | 68 | return get( 69 | `${API_URL}/labs/1/tweets?format=compact&expansions=${expansions}&ids=${tweetId}`, 70 | { 71 | headers: { 72 | authorization: `Bearer ${process.env.TWITTER_ACCESS_TOKEN}` 73 | } 74 | } 75 | ) 76 | } 77 | 78 | export async function getEmbeddedTweetHtml(url) { 79 | return get(`https://publish.twitter.com/oembed?url=${url}`) 80 | } 81 | -------------------------------------------------------------------------------- /packages/static-tweets/src/twitter/embed/tweet-html.ts: -------------------------------------------------------------------------------- 1 | import cheerio from 'cheerio' 2 | 3 | function getTweetContent($) { 4 | const container = $('.EmbeddedTweet-tweetContainer') 5 | 6 | if (!container.length) return 7 | 8 | const meta: any = {} 9 | const content: any = { meta } 10 | 11 | // This is the blockquote with the tweet 12 | const subject = container.find('[data-scribe="section:subject"]') 13 | 14 | // Tweet header with the author info 15 | const header = subject.children('.Tweet-header') 16 | const avatar = header.find('[data-scribe="element:avatar"]') 17 | const author = header.find('[data-scribe="component:author"]') 18 | const name = author.find('[data-scribe="element:name"]') 19 | const screenName = author.find('[data-scribe="element:screen_name"]') 20 | 21 | // Tweet body 22 | const tweet = subject.children('[data-scribe="component:tweet"]') 23 | const tweetContent = tweet.children('p') 24 | const card = tweet.children('.Tweet-card') 25 | const tweetInfo = tweet.children('.TweetInfo') 26 | const fullTimestamp = tweetInfo.find('[data-scribe="element:full_timestamp"]') 27 | const heartCount = tweetInfo.find('[data-scribe="element:heart_count"]') 28 | 29 | // Tweet footer 30 | const callToAction = container.children( 31 | '[data-scribe="section:cta component:news"]' 32 | ) 33 | const profileText = callToAction.children( 34 | '[data-scribe="element:profile_text"]' 35 | ) 36 | const conversationText = callToAction.children( 37 | '[data-scribe="element:conversation_text"]' 38 | ) 39 | 40 | let quotedTweet 41 | let mediaHtml 42 | 43 | meta.id = subject.attr('data-tweet-id') 44 | meta.avatar = { 45 | normal: avatar.attr('data-src-1x') 46 | } 47 | meta.name = name.text() 48 | meta.username = screenName.text().substring(1) // Omit the initial @ 49 | meta.createdAt = new Date(fullTimestamp.attr('data-datetime')).getTime() 50 | meta.heartCount = heartCount.text() 51 | meta.ctaType = profileText.length ? 'profile' : 'conversation' 52 | const tweetUrl = `https://twitter.com/${meta.username}/status/${meta.id}` 53 | 54 | if (conversationText.length) { 55 | // Get the formatted count and skip the rest 56 | meta.ctaCount = conversationText.text().match(/^[^\s]+/)[0] 57 | } 58 | 59 | // If some text ends without a trailing space, it's missing a
      60 | tweetContent.contents().each(function () { 61 | const el = $(this) 62 | const type = el[0].type 63 | 64 | if (type !== 'text') return 65 | 66 | const text = el.text() 67 | 68 | if (text.length && text.trim() === '') { 69 | if (el.next().children().length) { 70 | el.after($('
      ')) 71 | } 72 | } else if ( 73 | !/\s$/.test(el.text()) && 74 | el.next().children().length && 75 | !/^[#@]/.test(el.next().text()) 76 | ) { 77 | el.after($('
      ')) 78 | } 79 | }) 80 | 81 | card.children().each(function () { 82 | const props = this.attribs 83 | const scribe = props['data-scribe'] 84 | const el = $(this) 85 | 86 | if (scribe === 'section:quote') { 87 | const tweetCard = el.children('a') 88 | const id = tweetCard.attr('data-tweet-id') 89 | const url = tweetCard.attr('href') 90 | 91 | quotedTweet = { id, url } 92 | return 93 | } 94 | 95 | const media = $('
      ') 96 | 97 | if (scribe === 'component:card') { 98 | const photo = el.children('[data-scribe="element:photo"]') 99 | const photoGrid = el.children('[data-scribe="element:photo_grid"]') 100 | const photos = photo.length ? photo : photoGrid 101 | 102 | if (photos.length) { 103 | const images = photos.find('img') 104 | 105 | images.each(function () { 106 | const img = $(this) 107 | const alt = img.attr('alt') 108 | const url = img.attr('data-image') 109 | const format = img.attr('data-image-format') 110 | const height = img.attr('height') 111 | const width = img.attr('width') 112 | 113 | this.attribs = { 114 | 'data-type': 'media-image', 115 | 'data-url': tweetUrl, 116 | src: `${url}?format=${format}`, 117 | height, 118 | width 119 | } 120 | if (alt) { 121 | this.attribs.alt = alt 122 | } 123 | // Move the media img to a new container 124 | media.append(img) 125 | }) 126 | media.attr('data-type', `image-container ${images.length}`) 127 | mediaHtml = $('
      ').append(media).html() 128 | } 129 | } 130 | }) 131 | 132 | tweetContent.children('img').each(function () { 133 | const props = this.attribs 134 | 135 | // Handle emojis inside the text 136 | if (props.class?.includes('Emoji--forText')) { 137 | this.attribs = { 138 | 'data-type': 'emoji-for-text', 139 | src: props.src, 140 | alt: props.alt 141 | } 142 | return 143 | } 144 | 145 | console.error( 146 | 'An image with the following props is not being handled:', 147 | props 148 | ) 149 | }) 150 | 151 | tweetContent.children('a').each(function () { 152 | const props = this.attribs 153 | const scribe = props['data-scribe'] 154 | const el = $(this) 155 | const asTwitterLink = (type) => { 156 | this.attribs = { 157 | 'data-type': type, 158 | href: props.href 159 | } 160 | // Replace custom tags inside the anchor with text 161 | el.text(el.text()) 162 | } 163 | 164 | // @mention 165 | if (scribe === 'element:mention') { 166 | return asTwitterLink('mention') 167 | } 168 | 169 | // #hashtag 170 | if (scribe === 'element:hashtag') { 171 | // A hashtag may be a $cashtag too 172 | const type = 173 | props['data-query-source'] === 'cashtag_click' ? 'cashtag' : 'hashtag' 174 | return asTwitterLink(type) 175 | } 176 | 177 | if (scribe === 'element:url') { 178 | const url = props['data-expanded-url'] 179 | // const quotedTweetId = props['data-tweet-id'] 180 | 181 | // Remove link to quoted tweet to leave the card only 182 | // if (quotedTweetId && quotedTweetId === quotedTweet?.id) { 183 | // el.remove(); 184 | // return; 185 | // } 186 | 187 | // Handle normal links 188 | const text = { type: 'text', data: url } 189 | // Replace the link with plain text and markdown will take care of it 190 | el.replaceWith(text) 191 | } 192 | }) 193 | 194 | content.html = tweetContent.html() 195 | 196 | if (quotedTweet) content.quotedTweet = quotedTweet 197 | if (mediaHtml) content.mediaHtml = mediaHtml 198 | 199 | return content 200 | } 201 | 202 | export function getTweetData(html) { 203 | const $ = cheerio.load(html, { 204 | decodeEntities: false, 205 | xmlMode: false 206 | }) 207 | const tweetContent = getTweetContent($) 208 | 209 | return tweetContent 210 | } 211 | -------------------------------------------------------------------------------- /packages/static-tweets/src/twitter/getTweetHtml.ts: -------------------------------------------------------------------------------- 1 | import { getVideo } from './tweet-html' 2 | import { 3 | fetchUserStatus, 4 | getEmbeddedTweetHtml, 5 | fetchTweetWithPoll 6 | } from './api' 7 | import { fetchTweetAst } from '../fetchTweetAst' 8 | import markdownToAst from '../markdown/markdownToAst' 9 | 10 | function getVideoData(userStatus) { 11 | const video = userStatus.extended_entities.media[0] 12 | const poster = video.media_url_https 13 | // Find the first mp4 video in the array, if the results are always properly sorted, then 14 | // it should always be the mp4 video with the lowest bitrate 15 | const mp4Video = video.video_info.variants.find( 16 | (v) => v.content_type === 'video/mp4' 17 | ) 18 | 19 | if (!mp4Video) return 20 | 21 | return { poster, ...mp4Video } 22 | } 23 | 24 | function getPollData(tweet) { 25 | const polls = tweet.includes && tweet.includes.polls 26 | return polls && polls[0] 27 | } 28 | 29 | async function getMediaHtml(tweet) { 30 | let media = tweet.mediaHtml 31 | 32 | if (tweet.hasVideo) { 33 | const userStatus = await fetchUserStatus(tweet.meta.id) 34 | const video = userStatus && getVideoData(userStatus) 35 | 36 | media = video ? getVideo(media, video) : null 37 | } 38 | 39 | return media 40 | } 41 | 42 | async function getQuotedTweetHtml({ quotedTweet }, context) { 43 | if (!quotedTweet) return 44 | 45 | if (process.env.NEXT_PUBLIC_TWITTER_LOAD_WIDGETS === 'true') { 46 | const data = await getEmbeddedTweetHtml(quotedTweet.url) 47 | return data?.html 48 | } else { 49 | const ast = await fetchTweetAst(quotedTweet.id) 50 | // The AST of embedded tweets is always sent as data 51 | return ast && `
      ` 52 | } 53 | } 54 | 55 | async function getPollHtml(tweet, context) { 56 | if (!tweet.hasPoll) return null 57 | 58 | const tweetData = await fetchTweetWithPoll(tweet.meta.id) 59 | const poll = tweetData && getPollData(tweetData) 60 | 61 | if (poll) { 62 | const meta = { 63 | type: 'poll-container', 64 | endsAt: poll.end_datetime, 65 | duration: poll.duration_minutes, 66 | status: poll.voting_status, 67 | options: poll.options 68 | } 69 | 70 | return `
      ` 71 | } 72 | 73 | return null 74 | } 75 | 76 | export default async function getTweetHtml(tweet, context) { 77 | const meta = { ...tweet.meta, type: 'tweet' } 78 | const md: any = await markdownToAst(tweet.html) 79 | 80 | const html = [ 81 | // md.children is the markdown content, which is later added as children to the container 82 | `
      `, 83 | (await getMediaHtml(tweet)) || '', 84 | (await getQuotedTweetHtml(tweet, context)) || '', 85 | (await getPollHtml(tweet, context)) || '', 86 | `
      ` 87 | ].join('') 88 | 89 | return html 90 | } 91 | -------------------------------------------------------------------------------- /packages/static-tweets/src/twitter/tweet-html.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import cheerio from 'cheerio' 3 | import { URL } from 'url' 4 | 5 | const TWEET_VIDEO_URL = 'https://video.twimg.com/tweet_video' 6 | 7 | // Could we use rehype directly and remove cheerio? 8 | function getTweetContent($, tweet, isMainTweet = true) { 9 | if (!tweet.length) return 10 | 11 | const meta: any = {} 12 | const content: any = { meta } 13 | const tweetContent = tweet.children('.js-tweet-text-container').children('p') 14 | const actions = tweet 15 | .children('.stream-item-footer') 16 | .children('.ProfileTweet-actionCountList') 17 | .children() 18 | const hasCard = 19 | tweet.children('.js-tweet-details-fixer').children('.card2').length > 0 20 | 21 | let quotedTweet 22 | let mediaHtml 23 | let hasVideo = false 24 | 25 | // Add the user avatar and date to the tweet only if it's the main tweet 26 | if (isMainTweet) { 27 | const avatar = tweet.find('.account-group').children('.avatar') 28 | const time = tweet.find('a.tweet-timestamp').children('span') 29 | 30 | meta.avatar = { bigger: avatar.attr('src') } 31 | meta.createdAt = Number(time.attr('data-time-ms')) 32 | } 33 | 34 | tweetContent.children('img').each(function () { 35 | const props = this.attribs 36 | 37 | // Handle emojis inside the text 38 | if (props.class && props.class.includes('Emoji--forText')) { 39 | this.attribs = { 40 | 'data-type': 'emoji-for-text', 41 | src: props.src, 42 | alt: props.alt 43 | } 44 | return 45 | } 46 | 47 | console.error( 48 | 'An image with the following props is not being handled:', 49 | props 50 | ) 51 | }) 52 | 53 | tweetContent.children('a').each(function () { 54 | const props = this.attribs 55 | const el = $(this) 56 | 57 | if (props['data-expanded-url']) { 58 | const url = props['data-expanded-url'] 59 | const quotedTweetPath = tweet 60 | .children('.QuoteTweet') 61 | .find('.QuoteTweet-link') 62 | .attr('href') 63 | 64 | // Embedded Tweet 65 | if (quotedTweetPath && url.endsWith(quotedTweetPath)) { 66 | quotedTweet = { url } 67 | el.remove() 68 | return 69 | } 70 | 71 | // If Twitter is hiding the link, it's because it's adding a card with a preview 72 | const isLinkPreview = props.class && props.class.includes('u-hidden') 73 | 74 | if (isLinkPreview) { 75 | // In the case of a preview we only add a line break between the link and the paragraph. 76 | // TODO: Add the preview HTML and remove the link 77 | el.before('
      ') 78 | } 79 | 80 | // Handle normal links 81 | const text = { type: 'text', data: url } 82 | // Replace the link with plain text and markdown will take care of it 83 | el.replaceWith(text) 84 | 85 | return 86 | } 87 | 88 | // Embedded media 89 | if (props['data-pre-embedded'] === 'true') { 90 | const adaptiveMedia = tweet 91 | .children('.AdaptiveMediaOuterContainer') 92 | .children('.AdaptiveMedia') 93 | const isVideo = adaptiveMedia.hasClass('is-video') 94 | const media = $('
      ') 95 | 96 | // Videos and gifs 97 | if (isVideo) { 98 | const img = adaptiveMedia 99 | .find('.PlayableMedia-player') 100 | .css('background-image') 101 | const url = new URL(img.slice(4, -1).replace(/['"]/g, '')) 102 | const fileName = path.basename(url.pathname) 103 | const ext = path.extname(fileName) 104 | const videoUrl = `${TWEET_VIDEO_URL}/${fileName.replace(ext, '.mp4')}` 105 | 106 | // Gifs 107 | if (url.pathname.startsWith('/tweet_video_thumb')) { 108 | const video = $( 109 | `