import styles from './text.module.css'
import ReactMarkdown from 'react-markdown'
import gfm from 'remark-gfm'
import dynamic from 'next/dynamic'
import React, { useState, memo, useRef, useCallback, useMemo, useEffect } from 'react'
import MediaOrLink from './media-or-link'
import { IMGPROXY_URL_REGEXP, decodeProxyUrl } from '@/lib/url'
import reactStringReplace from 'react-string-replace'
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 SubPopover from './sub-popover'
import UserPopover from './user-popover'
import ItemPopover from './item-popover'
import classNames from 'classnames'
import { CarouselProvider, useCarousel } from './carousel'
import rehypeSN from '@/lib/rehype-sn'
import remarkUnicode from '@/lib/remark-unicode'
import Embed from './embed'
import remarkMath from 'remark-math'
const rehypeSNStyled = () => rehypeSN({
stylers: [{
startTag: '',
endTag: '',
className: styles.superscript
}, {
startTag: '',
endTag: '',
className: styles.subscript
}]
})
const remarkPlugins = [gfm, remarkUnicode, [remarkMath, { singleDollarTextMath: false }]]
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 = UNKNOWN_LINK_REL, imgproxyUrls, children, tab, itemId, outlawed, topLevel }) {
const [overflowing, setOverflowing] = useState(false)
const router = useRouter()
const [show, setShow] = useState(false)
const containerRef = useRef(null)
const [mathJaxPlugin, setMathJaxPlugin] = useState(null)
// we only need mathjax if there's math content between $$ tags
useEffect(() => {
if (/\$\$(.|\n)+\$\$/g.test(children)) {
import('rehype-mathjax').then(mod => {
setMathJaxPlugin(() => mod.default)
}).catch(err => {
console.error('error loading mathjax', err)
setMathJaxPlugin(null)
})
}
}, [children])
// if we are navigating to a hash, show the full text
useEffect(() => {
setShow(router.asPath.includes('#') && !router.asPath.includes('#itemfn-'))
const handleRouteChange = (url, { shallow }) => {
setShow(url.includes('#') && !url.includes('#itemfn-'))
}
router.events.on('hashChangeStart', handleRouteChange)
return () => {
router.events.off('hashChangeStart', handleRouteChange)
}
}, [router.asPath, router.events])
// clip item and give it a`show full text` button if we are overflowing
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 TextMediaOrLink = useCallback(props => {
return
},
[outlawed, imgproxyUrls, topLevel, rel])
const components = useMemo(() => ({
h1: ({ node, id, ...props }) => ,
h2: ({ node, id, ...props }) => ,
h3: ({ node, id, ...props }) => ,
h4: ({ node, id, ...props }) => ,
h5: ({ node, id, ...props }) => ,
h6: ({ node, id, ...props }) => ,
table: Table,
p: P,
code: Code,
mention: Mention,
sub: Sub,
item: Item,
footnote: Footnote,
headlink: ({ node, href, ...props }) => ,
autolink: ({ href, ...props }) => ,
a: ({ node, href, children, ...props }) => {
// if outlawed, render the link as text
if (outlawed) {
return href
}
// eslint-disable-next-line
return {children}
},
img: TextMediaOrLink,
embed: Embed
}), [outlawed, rel, TextMediaOrLink, topLevel])
const carousel = useCarousel()
const markdownContent = useMemo(() => (
{children}
), [components, remarkPlugins, mathJaxPlugin, children, itemId])
const showOverflow = useCallback(() => setShow(true), [setShow])
return (
{
carousel && tab !== 'preview'
? markdownContent
: {markdownContent}
}
{overflowing && !show && (
)}
)
}, isEqual)
function Mention ({ children, node, href, name, id }) {
return (
{children}
)
}
function Sub ({ children, node, href, name, ...props }) {
return (
{children}
)
}
function Item ({ children, node, href, id }) {
return (
{children}
)
}
function Footnote ({ children, node, ...props }) {
return (
{children}
)
}
function MediaLink ({
node, src, outlawed, imgproxyUrls, rel = UNKNOWN_LINK_REL, ...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
}
function Table ({ node, ...props }) {
return (
)
}
// prevent layout shifting when the code block is loading
function CodeSkeleton ({ className, children, ...props }) {
return (
{children}
)
}
function Code ({ node, inline, className, children, style, ...props }) {
const [ReactSyntaxHighlighter, setReactSyntaxHighlighter] = useState(null)
const [syntaxTheme, setSyntaxTheme] = useState(null)
const language = className?.match(/language-(\w+)/)?.[1] || 'text'
const loadHighlighter = useCallback(() =>
Promise.all([
dynamic(() => import('react-syntax-highlighter').then(mod => mod.LightAsync), {
ssr: false,
loading: () => {children}
}),
import('react-syntax-highlighter/dist/cjs/styles/hljs/atom-one-dark').then(mod => mod.default)
]), []
)
useEffect(() => {
if (!inline && language !== 'math') { // MathJax should handle math
// loading the syntax highlighter and theme only when needed
loadHighlighter().then(([highlighter, theme]) => {
setReactSyntaxHighlighter(() => highlighter)
setSyntaxTheme(() => theme)
})
}
}, [inline])
if (inline || !ReactSyntaxHighlighter) { // inline code doesn't have a border radius
return (
{children}
)
}
return (
<>
{ReactSyntaxHighlighter && syntaxTheme && (
{children}
)}
>
)
}
function P ({ children, node, onlyImages, somethingBefore, somethingAfter, ...props }) {
return (
{children}
)
}