import styles from './text.module.css' import ReactMarkdown from 'react-markdown' import gfm from 'remark-gfm' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism' import mention from '../lib/remark-mention' import sub from '../lib/remark-sub' import remarkDirective from 'remark-directive' import { visit } from 'unist-util-visit' import reactStringReplace from 'react-string-replace' import React, { useEffect, useState } from 'react' import GithubSlugger 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' function myRemarkPlugin () { return (tree) => { visit(tree, (node) => { if ( node.type === 'textDirective' || node.type === 'leafDirective' ) { if (node.name !== 'high') return const data = node.data || (node.data = {}) data.hName = 'mark' data.hProperties = {} } }) } } function Heading ({ h, slugger, noFragments, topLevel, children, node, ...props }) { const [copied, setCopied] = useState(false) const [id] = useState(noFragments ? undefined : slugger.slug(toString(node).replace(/[^\w\-\s]+/gi, ''))) const Icon = copied ? Thumb : LinkIcon return (
{React.createElement(h, { 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' /> }
) } export default function Text ({ topLevel, noFragments, nofollow, children }) { // all the reactStringReplace calls are to facilitate search highlighting const slugger = new GithubSlugger() const HeadingWrapper = (props) => Heading({ topLevel, slugger, noFragments, ...props }) return (
HeadingWrapper({ h: topLevel ? 'h1' : 'h3', ...props }), h2: (props) => HeadingWrapper({ h: topLevel ? 'h2' : 'h4', ...props }), h3: (props) => HeadingWrapper({ h: topLevel ? 'h3' : 'h5', ...props }), h4: (props) => HeadingWrapper({ h: topLevel ? 'h4' : 'h6', ...props }), h5: (props) => HeadingWrapper({ h: topLevel ? 'h5' : 'h6', ...props }), h6: (props) => HeadingWrapper({ h: 'h6', ...props }), table: ({ node, ...props }) =>
, code ({ node, inline, className, children, ...props }) { const match = /language-(\w+)/.exec(className || '') return !inline ? ( {reactStringReplace(String(children).replace(/\n$/, ''), /:high\[([^\]]+)\]/g, (match, i) => { return match }).join('')} ) : ( {reactStringReplace(String(children), /:high\[([^\]]+)\]/g, (match, i) => { return {match} })} ) }, a: ({ node, href, children, ...props }) => { if (children?.some(e => e?.props?.node?.tagName === 'img')) { return <>{children} } // map: fix any highlighted links children = children?.map(e => typeof e === 'string' ? reactStringReplace(e, /:high\[([^\]]+)\]/g, (match, i) => { return {match} }) : e) return ( /* eslint-disable-next-line */ { return match }).join('')} {...props} > {children} ) }, img: ({ node, ...props }) => }} remarkPlugins={[gfm, mention, sub, remarkDirective, myRemarkPlugin]} > {children} ) } export function ZoomableImage ({ src, topLevel, ...props }) { if (!src) { return null } const defaultMediaStyle = { maxHeight: topLevel ? '75vh' : '25vh', cursor: 'zoom-in' } // if image changes we need to update state const [mediaStyle, setMediaStyle] = useState(defaultMediaStyle) useEffect(() => { setMediaStyle(defaultMediaStyle) }, [src]) const handleClick = () => { if (mediaStyle.cursor === 'zoom-in') { setMediaStyle({ width: '100%', cursor: 'zoom-out' }) } else { setMediaStyle(defaultMediaStyle) } } return ( ) }