stacker.news/components/text.js

154 lines
6.2 KiB
JavaScript
Raw Normal View History

2021-04-14 23:56:29 +00:00
import styles from './text.module.css'
2021-06-27 22:28:30 +00:00
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'
2021-07-15 19:56:23 +00:00
import mention from '../lib/remark-mention'
2022-03-03 22:18:38 +00:00
import sub from '../lib/remark-sub'
2022-02-03 22:01:42 +00:00
import remarkDirective from 'remark-directive'
import { visit } from 'unist-util-visit'
import reactStringReplace from 'react-string-replace'
2023-10-01 23:03:52 +00:00
import React, { useState, memo } from 'react'
2022-07-17 15:33:55 +00:00
import GithubSlugger from 'github-slugger'
2022-10-04 17:43:18 +00:00
import LinkIcon from '../svgs/link.svg'
import Thumb from '../svgs/thumb-up-fill.svg'
2022-11-15 20:51:55 +00:00
import { toString } from 'mdast-util-to-string'
2022-10-04 17:43:18 +00:00
import copy from 'clipboard-copy'
2023-10-01 23:03:52 +00:00
import { useImgUrlCache, IMG_CACHE_STATES, ZoomableImage, decodeOriginalUrl } from './image'
import { IMGPROXY_URL_REGEXP } from '../lib/url'
2022-07-21 22:39:05 +00:00
function searchHighlighter () {
2022-02-03 22:01:42 +00:00
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 = {}
}
})
}
}
2021-04-14 23:56:29 +00:00
2022-07-17 15:33:55 +00:00
function Heading ({ h, slugger, noFragments, topLevel, children, node, ...props }) {
2022-10-04 17:43:18 +00:00
const [copied, setCopied] = useState(false)
const [id] = useState(noFragments ? undefined : slugger.slug(toString(node).replace(/[^\w\-\s]+/gi, '')))
const Icon = copied ? Thumb : LinkIcon
2022-07-17 15:33:55 +00:00
return (
<span className={styles.heading}>
2022-07-17 15:33:55 +00:00
{React.createElement(h, { id, ...props }, children)}
2022-10-04 17:43:18 +00:00
{!noFragments && topLevel &&
2022-11-15 20:51:55 +00:00
<a className={`${styles.headingLink} ${copied ? styles.copied : ''}`} href={`#${id}`}>
<Icon
onClick={() => {
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'
/>
</a>}
</span>
2022-07-17 15:33:55 +00:00
)
}
// this is one of the slowest components to render
2023-10-01 23:03:52 +00:00
export default memo(function Text ({ topLevel, noFragments, nofollow, imgproxyUrls, children }) {
// all the reactStringReplace calls are to facilitate search highlighting
2022-07-17 15:33:55 +00:00
const slugger = new GithubSlugger()
2022-11-15 20:51:55 +00:00
const HeadingWrapper = (props) => Heading({ topLevel, slugger, noFragments, ...props })
2023-10-01 23:03:52 +00:00
const imgUrlCache = useImgUrlCache(children, imgproxyUrls)
2021-04-14 23:56:29 +00:00
return (
2021-06-27 22:28:30 +00:00
<div className={styles.text}>
<ReactMarkdown
components={{
2022-07-17 15:33:55 +00:00
h1: (props) => 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 }),
2021-06-27 22:28:30 +00:00
table: ({ node, ...props }) =>
<span className='table-responsive'>
2021-06-27 22:28:30 +00:00
<table className='table table-bordered table-sm' {...props} />
</span>,
p: ({ children, ...props }) => <div className={styles.p} {...props}>{children}</div>,
2023-07-27 17:10:05 +00:00
code ({ node, inline, className, children, style, ...props }) {
2021-06-27 22:28:30 +00:00
return !inline
? (
2023-07-27 17:10:05 +00:00
<SyntaxHighlighter showLineNumbers style={atomDark} PreTag='div' {...props}>
{reactStringReplace(String(children).replace(/\n$/, ''), /:high\[([^\]]+)\]/g, (match, i) => {
return match
}).join('')}
2021-06-27 22:28:30 +00:00
</SyntaxHighlighter>
)
: (
<code className={className} style={atomDark} {...props}>
{reactStringReplace(String(children), /:high\[([^\]]+)\]/g, (match, i) => {
return <mark key={`mark-${match}`}>{match}</mark>
})}
2021-06-27 22:28:30 +00:00
</code>
)
},
a: ({ node, href, children, ...props }) => {
2022-08-10 15:06:31 +00:00
if (children?.some(e => e?.props?.node?.tagName === 'img')) {
return <>{children}</>
}
2023-10-01 23:03:52 +00:00
if (imgUrlCache[href] === IMG_CACHE_STATES.LOADED) {
const url = IMGPROXY_URL_REGEXP.test(href) ? decodeOriginalUrl(href) : href
// if `srcSet` is undefined, it means the image was not processed by worker yet
// if `srcSet` is null, image was processed but this specific url was not detected as an image by the worker
const srcSet = imgproxyUrls ? (imgproxyUrls[url] || null) : undefined
return <ZoomableImage topLevel={topLevel} srcSet={srcSet} {...props} src={href} />
}
2022-08-10 15:06:31 +00:00
// map: fix any highlighted links
2022-07-17 15:33:55 +00:00
children = children?.map(e =>
typeof e === 'string'
2022-11-15 20:51:55 +00:00
? reactStringReplace(e, /:high\[([^\]]+)\]/g, (match, i) => {
2023-07-25 14:14:45 +00:00
return <mark key={`mark-${match}-${i}`}>{match}</mark>
})
2022-11-15 20:51:55 +00:00
: e)
return (
/* eslint-disable-next-line */
<a
target='_blank' rel={nofollow ? 'nofollow' : 'noreferrer'}
href={reactStringReplace(href, /:high%5B([^%5D]+)%5D/g, (match, i) => {
return match
}).join('')} {...props}
>
{children}
</a>
)
},
2023-10-01 23:03:52 +00:00
img: ({ node, src, ...props }) => {
const url = IMGPROXY_URL_REGEXP.test(src) ? decodeOriginalUrl(src) : src
// if `srcSet` is undefined, it means the image was not processed by worker yet
// if `srcSet` is null, image was processed but this specific url was not detected as an image by the worker
const srcSet = imgproxyUrls ? (imgproxyUrls[url] || null) : undefined
return <ZoomableImage topLevel={topLevel} srcSet={srcSet} src={src} {...props} />
}
2021-06-27 22:28:30 +00:00
}}
remarkPlugins={[gfm, mention, sub, remarkDirective, searchHighlighter]}
2021-06-27 22:28:30 +00:00
>
{children}
</ReactMarkdown>
</div>
2021-04-14 23:56:29 +00:00
)
})