import styles from './text.module.css' import ReactMarkdown from 'react-markdown' import gfm from 'remark-gfm' import { LightAsync as SyntaxHighlighter } from 'react-syntax-highlighter' import atomDark from 'react-syntax-highlighter/dist/cjs/styles/prism/atom-dark' import mention from '@/lib/remark-mention' import sub from '@/lib/remark-sub' import React, { useState, memo, useRef, useCallback, useMemo, useEffect } from 'react' import { slug } from 'github-slugger' import LinkIcon from '@/svgs/link.svg' import Thumb from '@/svgs/thumb-up-fill.svg' import { toString } from 'mdast-util-to-string' import copy from 'clipboard-copy' import MediaOrLink from './media-or-link' import { IMGPROXY_URL_REGEXP, parseInternalLinks, decodeProxyUrl } from '@/lib/url' import reactStringReplace from 'react-string-replace' import { rehypeInlineCodeProperty, rehypeStyler } from '@/lib/md' import { Button } from 'react-bootstrap' import { useRouter } from 'next/router' import Link from 'next/link' import { UNKNOWN_LINK_REL } from '@/lib/constants' import isEqual from 'lodash/isEqual' import UserPopover from './user-popover' import ItemPopover from './item-popover' import classNames from 'classnames' // Explicitely defined start/end tags & which CSS class from text.module.css to apply export const rehypeSuperscript = () => rehypeStyler('', '', styles.superscript) export const rehypeSubscript = () => rehypeStyler('', '', styles.subscript) export function SearchText ({ text }) { return (

{reactStringReplace(text, /\*\*\*([^*]+)\*\*\*/g, (match, i) => { return {match} })}

) } // this is one of the slowest components to render export default memo(function Text ({ rel, imgproxyUrls, children, tab, itemId, outlawed, topLevel, noFragments }) { const [overflowing, setOverflowing] = useState(false) const router = useRouter() const [show, setShow] = useState(false) const containerRef = useRef(null) useEffect(() => { setShow(router.asPath.includes('#')) const handleRouteChange = (url, { shallow }) => { setShow(url.includes('#')) } router.events.on('hashChangeStart', handleRouteChange) return () => { router.events.off('hashChangeStart', handleRouteChange) } }, [router]) useEffect(() => { const container = containerRef.current if (!container || overflowing) return function checkOverflow () { setOverflowing(container.scrollHeight > window.innerHeight * 2) } let resizeObserver if (!overflowing && 'ResizeObserver' in window) { resizeObserver = new window.ResizeObserver(checkOverflow).observe(container) } window.addEventListener('resize', checkOverflow) checkOverflow() return () => { window.removeEventListener('resize', checkOverflow) resizeObserver?.disconnect() } }, [containerRef.current, setOverflowing]) const Heading = useCallback(({ children, node, ...props }) => { const [copied, setCopied] = useState(false) const nodeText = toString(node) const id = useMemo(() => noFragments ? undefined : slug(nodeText.replace(/[^\w\-\s]+/gi, '')), [nodeText, noFragments]) const h = useMemo(() => { if (topLevel) { return node?.TagName } const h = parseInt(node?.tagName?.replace('h', '') || 0) if (h < 4) return `h${h + 3}` return 'h6' }, [node, topLevel]) const Icon = copied ? Thumb : LinkIcon return ( {React.createElement(h || node?.tagName, { id, ...props }, children)} {!noFragments && topLevel && { const location = new URL(window.location) location.hash = `${id}` copy(location.href) setTimeout(() => setCopied(false), 1500) setCopied(true) }} width={18} height={18} className='fill-grey' /> } ) }, [topLevel, noFragments]) const Table = useCallback(({ node, ...props }) => , []) const Code = useCallback(({ node, inline, className, children, style, ...props }) => { return inline ? ( {children} ) : ( {children} ) }, []) const P = useCallback(({ children, node, ...props }) =>
{children}
, []) const TextMediaOrLink = useCallback(({ node, src, ...props }) => { const url = IMGPROXY_URL_REGEXP.test(src) ? decodeProxyUrl(src) : src // if outlawed, render the media link as text if (outlawed) { return url } const srcSet = imgproxyUrls?.[url] return }, [imgproxyUrls, topLevel, tab]) const components = useMemo(() => ({ h1: Heading, h2: Heading, h3: Heading, h4: Heading, h5: Heading, h6: Heading, table: Table, p: P, li: props => { return
  • }, code: Code, a: ({ node, href, children, ...props }) => { children = children ? Array.isArray(children) ? children : [children] : [] // don't allow zoomable images to be wrapped in links if (children.some(e => e?.props?.node?.tagName === 'img')) { return <>{children} } // if outlawed, render the link as text if (outlawed) { return href } // If [text](url) was parsed as and text is not empty and not a link itself, // we don't render it as an image since it was probably a conscious choice to include text. const text = children[0] let url try { url = !href.startsWith('/') && new URL(href) } catch { // ignore invalid URLs } const internalURL = process.env.NEXT_PUBLIC_URL if (!!text && !/^https?:\/\//.test(text)) { if (props['data-footnote-ref'] || typeof props['data-footnote-backref'] !== 'undefined') { return ( {text} ) } if (text.startsWith?.('@')) { // user mention might be within a markdown link like this: [@user foo bar](url) const name = text.replace('@', '').split(' ')[0] return ( {text} ) } else if (href.startsWith('/') || url?.origin === internalURL) { try { const { linkText } = parseInternalLinks(href) if (linkText) { return ( {text} ) } } catch { // ignore errors like invalid URLs } return ( {text} ) } return ( // eslint-disable-next-line {text} ) } try { const { linkText } = parseInternalLinks(href) if (linkText) { return ( {linkText} ) } } catch { // ignore errors like invalid URLs } // assume the link is an image which will fallback to link if it's not return {children} }, img: TextMediaOrLink }), [outlawed, rel, itemId, Code, P, Heading, Table, TextMediaOrLink]) const remarkPlugins = useMemo(() => [gfm, mention, sub], []) const rehypePlugins = useMemo(() => [rehypeInlineCodeProperty, rehypeSuperscript, rehypeSubscript], []) return (
    {children} {overflowing && !show && }
    ) }, isEqual)