| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| import { useEffect, useState, ReactElement, isValidElement } from 'react'; | |
| import { createRoot } from 'react-dom/client'; | |
| import { | |
| GithubGistEmbed, | |
| CodePenEmbed, | |
| YouTubeEmbed, | |
| JSFiddleEmbed, | |
| FigmaEmbed, | |
| ExcalidrawEmbed, | |
| LoomEmbed, | |
| DropboxEmbed, | |
| TwitterEmbed, | |
| } from './components'; | |
| import { Request } from './types'; | |
| interface Config { | |
| platform: string; | |
| enable: boolean; | |
| } | |
| const get = async (url: string) => { | |
| const response = await fetch(url); | |
| const { data } = await response.json(); | |
| return data; | |
| }; | |
| const useRenderEmbed = ( | |
| element, | |
| request: Request = { | |
| get, | |
| }, | |
| ) => { | |
| const [configs, setConfigs] = useState<Config[] | null>(null); | |
| const embeds = [ | |
| { | |
| name: 'YouTube', | |
| regexs: [ | |
| /https:\/\/youtu\.be\/([a-zA-Z0-9_-]{11})/, | |
| /https:\/\/www\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/, | |
| /https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/, | |
| ], | |
| embed: (videoId: string) => { | |
| return <YouTubeEmbed videoId={videoId} />; | |
| }, | |
| }, | |
| { | |
| name: 'Twitter', | |
| regexs: [ | |
| /https:\/\/twitter\.com\/[a-zA-Z0-9_]+\/status\/([a-zA-Z0-9_]+)/, | |
| /https:\/\/x\.com\/[a-zA-Z0-9_]+\/status\/([a-zA-Z0-9_]+)/, | |
| ], | |
| embed: (_, url, title = '') => { | |
| const blockquoteElement = document.createElement('blockquote'); | |
| blockquoteElement.classList.add('twitter-tweet'); | |
| const anchorElement = document.createElement('a'); | |
| anchorElement.href = url.replace('x.com', 'twitter.com'); | |
| anchorElement.textContent = title; | |
| blockquoteElement.appendChild(anchorElement); | |
| const scriptElement = document.createElement('script'); | |
| scriptElement.src = 'https://platform.twitter.com/widgets.js'; | |
| scriptElement.async = true; | |
| const styleElement = document.createElement('style'); | |
| styleElement.innerHTML = ` | |
| .twitter-tweet { | |
| display: block; | |
| margin: 0 auto; | |
| } | |
| `; | |
| return ( | |
| <TwitterEmbed | |
| url={url.replace('x.com', 'twitter.com')} | |
| title={title} | |
| /> | |
| ); | |
| }, | |
| }, | |
| { | |
| name: 'CodePen', | |
| regexs: [ | |
| /https:\/\/codepen\.io\/[a-zA-Z0-9_]+\/pen\/([a-zA-Z0-9_]+)/, | |
| /https:\/\/codepen\.io\/[a-zA-Z0-9_]+\/full\/([a-zA-Z0-9_]+)/, | |
| ], | |
| embed: (penId) => { | |
| return <CodePenEmbed penId={penId} />; | |
| }, | |
| }, | |
| { | |
| name: 'JSFiddle', | |
| regexs: [ | |
| /https:\/\/jsfiddle\.net\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)/, | |
| /https:\/\/jsfiddle\.net\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)\/embed/, | |
| ], | |
| embed: (fiddleId: string) => { | |
| return <JSFiddleEmbed fiddleId={fiddleId} />; | |
| }, | |
| }, | |
| { | |
| name: 'GithubGist', | |
| regexs: [ | |
| /https:\/\/gist\.github\.com\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)/, | |
| /https:\/\/gist\.github\.com\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)\.js/, | |
| ], | |
| embed: (_, url) => { | |
| const scriptUrl = url.indexOf('.js') > -1 ? url : `${url}.js`; | |
| return <GithubGistEmbed scriptUrl={scriptUrl} />; | |
| }, | |
| }, | |
| { | |
| name: 'Figma', | |
| regexs: [ | |
| /https:\/\/www\.figma\.com\/design\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)/, | |
| /https:\/\/www\.figma\.com\/file\/[a-zA-Z0-9_]+\/([a-zA-Z0-9_]+)/, | |
| ], | |
| embed: (_, url) => { | |
| return <FigmaEmbed url={url} />; | |
| }, | |
| }, | |
| { | |
| name: 'Excalidraw', | |
| regexs: [ | |
| /https:\/\/excalidraw\.com\/#json=([a-zA-Z0-9_,-]+)/, | |
| /https:\/\/excalidraw\.com\/([a-zA-Z0-9_,-]+)/, | |
| ], | |
| embed: (excalidrawId: string) => { | |
| return <ExcalidrawEmbed excalidrawId={excalidrawId} />; | |
| }, | |
| }, | |
| { | |
| name: 'Loom', | |
| regexs: [ | |
| /https:\/\/www\.loom\.com\/embed\/([a-zA-Z0-9_]+)/, | |
| /https:\/\/www\.loom\.com\/share\/([a-zA-Z0-9_]+)/, | |
| ], | |
| embed: (loomId: string) => { | |
| return <LoomEmbed loomId={loomId} />; | |
| }, | |
| }, | |
| { | |
| name: 'Dropbox', | |
| regexs: [ | |
| /https:\/\/www\.dropbox\.com\/s\/([a-zA-Z0-9_]+)\/[a-zA-Z0-9_]+/, | |
| ], | |
| embed: (dropboxId: string) => { | |
| return <DropboxEmbed dropboxId={dropboxId} />; | |
| }, | |
| }, | |
| ]; | |
| const filteredEmbeds = embeds.filter((embed) => { | |
| const finded = configs?.find( | |
| (config) => config.platform === embed.name && config.enable, | |
| ); | |
| return finded; | |
| }); | |
| const renderEmbed = ( | |
| url: string, | |
| title: string, | |
| ): string | HTMLElement | HTMLElement[] => { | |
| let html: string | HTMLElement | HTMLElement[] | ReactElement = ''; | |
| filteredEmbeds.forEach((embed) => { | |
| if (html) return; | |
| embed.regexs.forEach((regex) => { | |
| if (html) return; | |
| const match = url.match(regex); | |
| if (match) { | |
| html = embed.embed(match[1], url, title); | |
| } | |
| }); | |
| }); | |
| return html; | |
| }; | |
| const render = (targetElement) => { | |
| if (!element) { | |
| return; | |
| } | |
| const links = targetElement.querySelectorAll('a'); | |
| let hasDefaultStyle = false; | |
| links.forEach((link) => { | |
| const url = link.getAttribute('href') || ''; | |
| const title = link.getAttribute('title') || ''; | |
| if (!url) { | |
| return; | |
| } | |
| if (title !== '@embed') { | |
| return; | |
| } | |
| const embed = renderEmbed(url, link?.textContent || ''); | |
| if (isValidElement(embed)) { | |
| const parentElement = link.parentElement as HTMLElement; | |
| parentElement.classList.add('position-relative'); | |
| parentElement.style.height = '128px'; | |
| createRoot(parentElement).render(embed); | |
| } else { | |
| hasDefaultStyle = true; | |
| link.innerHTML = ` | |
| <div class="card embed-light"> | |
| <div class="card-body"> | |
| <div class="text-secondary small mb-1">${url}</div> | |
| <div class="text-body fw-bold">${link.textContent}</div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| }); | |
| // default card style add embed-ligh class for hover bg-light | |
| let styleElement = document.querySelector('style#embed-style'); | |
| if (!styleElement) { | |
| styleElement = document.createElement('style'); | |
| styleElement.id = 'embed-style'; | |
| if (hasDefaultStyle) { | |
| styleElement.textContent = ` | |
| .embed-light:hover { | |
| --bs-bg-opacity: 1; | |
| background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important; | |
| } | |
| `; | |
| const head = document.querySelector('head') as HTMLElement; | |
| head.appendChild(styleElement); | |
| } | |
| } | |
| }; | |
| const getConfig = () => { | |
| request | |
| .get('/answer/api/v1/embed/config') | |
| .then((result) => setConfigs(result)); | |
| }; | |
| useEffect(() => { | |
| getConfig(); | |
| return () => { | |
| const styleEle = document.querySelector('style#embed-style'); | |
| const head = document.querySelector('head') as HTMLElement; | |
| if (styleEle) { | |
| head.removeChild(styleEle); | |
| } | |
| }; | |
| }, []); | |
| useEffect(() => { | |
| if (!element) { | |
| return; | |
| } | |
| if (!configs) { | |
| return; | |
| } | |
| let targetElement; | |
| if (element instanceof HTMLElement) { | |
| targetElement = element; | |
| } else { | |
| targetElement = element.current; | |
| } | |
| render(targetElement); | |
| const observer = new MutationObserver(() => { | |
| render(targetElement); | |
| }); | |
| observer.observe(targetElement, { | |
| childList: true, | |
| }); | |
| return () => { | |
| observer.disconnect(); | |
| }; | |
| }, [element, configs]); | |
| }; | |
| export { useRenderEmbed }; |