diff options
author | eug-vs <eug-vs@keemail.me> | 2021-01-07 13:22:21 +0300 |
---|---|---|
committer | eug-vs <eug-vs@keemail.me> | 2021-01-07 15:17:43 +0300 |
commit | f5c21698a09d9214f57ce6dab2595f3d61e1ff11 (patch) | |
tree | 900b605f147a5a024377b42be16a6f677a10a387 /src/lib | |
parent | afa7e4abe02959798665949193d832907d97153a (diff) | |
download | react-benzin-f5c21698a09d9214f57ce6dab2595f3d61e1ff11.tar.gz |
feat!: use react-markdown
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/Markdown/CodeBlock.tsx | 13 | ||||
-rw-r--r-- | src/lib/Markdown/Content.tsx | 90 | ||||
-rw-r--r-- | src/lib/Markdown/Heading.tsx | 33 | ||||
-rw-r--r-- | src/lib/Markdown/Image.tsx | 12 | ||||
-rw-r--r-- | src/lib/Markdown/InlineCode.tsx | 18 | ||||
-rw-r--r-- | src/lib/Markdown/Markdown.tsx | 27 | ||||
-rw-r--r-- | src/lib/Markdown/Section.tsx | 61 | ||||
-rw-r--r-- | src/lib/Markdown/SyntacticSpan.tsx | 96 | ||||
-rw-r--r-- | src/lib/Markdown/Text.tsx | 13 |
9 files changed, 94 insertions, 269 deletions
diff --git a/src/lib/Markdown/CodeBlock.tsx b/src/lib/Markdown/CodeBlock.tsx index 394458e..7431881 100644 --- a/src/lib/Markdown/CodeBlock.tsx +++ b/src/lib/Markdown/CodeBlock.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import { Paper } from '@material-ui/core'; +import { Paper, makeStyles } from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import { ParserPropTypes } from './types'; +interface PropTypes { + value: string; +} const useStyles = makeStyles(theme => ({ root: { @@ -14,11 +15,13 @@ const useStyles = makeStyles(theme => ({ }, })); -const CodeBlock: React.FC<ParserPropTypes> = ({ rawLines }) => { +const CodeBlock: React.FC<PropTypes> = ({ value }) => { const classes = useStyles(); return ( <Paper variant="outlined" className={classes.root}> - {rawLines.map(line => <pre>{line}</pre>)} + <pre> + {value} + </pre> </Paper> ); }; diff --git a/src/lib/Markdown/Content.tsx b/src/lib/Markdown/Content.tsx deleted file mode 100644 index 88409fa..0000000 --- a/src/lib/Markdown/Content.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; - -import CodeBlock from './CodeBlock'; -import Text from './Text'; -import { ParserPropTypes } from './types'; - - -const denotesCodeBlock = (line: string): boolean => { - return line.match(/^\s*```.*$/) !== null; -}; - -const denotesDottedList = (line: string): boolean => { - return line.match(/^ ?[-*] .*$/) !== null; -}; - -const denotesOpenHtml = (line: string): string => { - const regex = /<([^/\s]*)[^<]*[^/]>/g; - const match = regex.exec(line); - return match ? match[1] : ''; -}; - -const denotesClosingHtml = (line: string, tag: string): boolean => { - const regex = new RegExp(`</${tag}[^<]*>`); - return line.match(regex) !== null; -}; - -const denotesSelfClosingHtml = (line: string): string[] | null => { - const regex = /(<[^/\s]*[^<]*\/>)/g; - return line.match(regex); -}; - -const declaresNoLineBreak = (line: string): boolean => { - return line.match(/\\\|$/) !== null; -}; - -const Content: React.FC<ParserPropTypes> = ({ rawLines }) => { - if (!rawLines.length) return null; - - const line = rawLines.splice(0, 1)[0]; - - let buffer; - if (denotesCodeBlock(line)) { - const closeIndex = rawLines.findIndex(rawLine => denotesCodeBlock(rawLine)); - const codeBlockLines = rawLines.splice(0, closeIndex + 1).slice(0, closeIndex); - buffer = <CodeBlock rawLines={codeBlockLines} />; - } else if (denotesDottedList(line)) { - const closeIndex = rawLines.findIndex(rawLine => !denotesDottedList(rawLine)); - const dottedListLines = rawLines.splice(0, closeIndex).slice(0, closeIndex); - dottedListLines.unshift(line); - buffer = <ul>{dottedListLines.map(li => <li><Text line={li.slice(2)} /></li>)}</ul>; - } else if ((buffer = denotesOpenHtml(line))) { - const tag = buffer; - const closeIndex = denotesClosingHtml(line, tag) ? -1 : rawLines.findIndex( - rawLine => denotesClosingHtml(rawLine, tag), - ); - const htmlLines = rawLines.splice(0, closeIndex + 1); - htmlLines.unshift(line); - buffer = <div dangerouslySetInnerHTML={{ __html: htmlLines.join('\n') }} />; - } else if ((buffer = denotesSelfClosingHtml(line)) !== null) { - const match = buffer[0]; - const [before, after] = line.split(match); - buffer = ( - <> - <Text line={before} /> - <div dangerouslySetInnerHTML={{ __html: match }} /> - <Text line={after} /> - </> - ); - } else if (declaresNoLineBreak(line)) { - const closeIndex = rawLines.findIndex(rawLine => !declaresNoLineBreak(rawLine)); - const lineBreakLines = rawLines.splice(0, closeIndex).map(rawLine => rawLine.slice(0, -2)); - lineBreakLines.unshift(line.slice(0, -2)); - lineBreakLines.push(rawLines.splice(0, 1)[0]); - buffer = <p>{lineBreakLines.map(lineBreakLine => <Text line={lineBreakLine} />)}</p>; - } else if (denotesClosingHtml(line, '')) { - buffer = null; - } else { - buffer = <p><Text line={line} /></p>; - } - - return ( - <> - { buffer } - <Content rawLines={rawLines} /> - </> - ); -}; - -export default Content; - diff --git a/src/lib/Markdown/Heading.tsx b/src/lib/Markdown/Heading.tsx new file mode 100644 index 0000000..cc0b709 --- /dev/null +++ b/src/lib/Markdown/Heading.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Typography, Divider, makeStyles } from '@material-ui/core'; + +interface PropTypes { + level: number; +} + +type Variant = 'h3' | 'h4' | 'h5' | 'h6'; + +const useStyles = makeStyles(theme => ({ + root: { + padding: theme.spacing(2, 0, 1, 0), + }, +})); + +const Heading: React.FC<PropTypes> = ({ children, level }) => { + const classes = useStyles(); + + let adjustedLevel = level + 2; // Make everything smaller + if (adjustedLevel > 6) adjustedLevel = 6; + + const variant: Variant = `h${adjustedLevel}` as Variant; + + return ( + <div className={classes.root}> + <Typography variant={variant}>{children}</Typography> + <Divider variant="middle" /> + </div> + ); +}; + +export default Heading; + diff --git a/src/lib/Markdown/Image.tsx b/src/lib/Markdown/Image.tsx new file mode 100644 index 0000000..472d6f0 --- /dev/null +++ b/src/lib/Markdown/Image.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +interface PropTypes { + src: string; + alt: string; +} + +const Image: React.FC<PropTypes> = ({ src, alt }) => { + return <img src={src} alt={alt} style={{ maxWidth: '100%', maxHeight: '100%' }} />; +}; + +export default Image; diff --git a/src/lib/Markdown/InlineCode.tsx b/src/lib/Markdown/InlineCode.tsx new file mode 100644 index 0000000..c51f924 --- /dev/null +++ b/src/lib/Markdown/InlineCode.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles(theme => ({ + root: { + background: theme.palette.background.default, + borderRadius: theme.spacing(0.5), + padding: theme.spacing(0.5), + fontFamily: 'Monospace', + }, +})); + +const InlineCode: React.FC = ({ children }) => { + const classes = useStyles(); + return <span className={classes.root}>{children}</span>; +}; + +export default InlineCode; diff --git a/src/lib/Markdown/Markdown.tsx b/src/lib/Markdown/Markdown.tsx index cfcf117..01923f9 100644 --- a/src/lib/Markdown/Markdown.tsx +++ b/src/lib/Markdown/Markdown.tsx @@ -1,7 +1,12 @@ import React, { useState, useEffect } from 'react'; import axios from 'axios'; +import ReactMarkdown from 'react-markdown'; +import { Link, Typography } from '@material-ui/core'; -import Section from './Section'; +import CodeBlock from './CodeBlock'; +import InlineCode from './InlineCode'; +import Heading from './Heading'; +import Image from './Image'; interface PropTypes { data?: string; @@ -12,10 +17,18 @@ const resolveUrls = (line: string, baseUrl: string): string => line.replace( /src="(?!http)(.*)"[\s>]/, (match, url) => `src="${baseUrl}/${url}?sanitize=true"`, ).replace( - /\[(.*\]?.*)\]\((?!http)(.+?)\)/, + /\[(.*\]?.*)\]\((?!http)(.+?)\)/g, (match, text, url) => `[${text}](${baseUrl}/${url})`, ); +const renderers = { + heading: Heading, + inlineCode: InlineCode, + code: CodeBlock, + link: Link, + image: Image, +}; + const Markdown: React.FC<PropTypes> = ({ data, url }) => { const [markdown, setMarkdown] = useState<string>(data || ''); @@ -26,8 +39,14 @@ const Markdown: React.FC<PropTypes> = ({ data, url }) => { }, [data, url]); const baseUrl = url?.slice(0, url.lastIndexOf('/')) || ''; - const lines = markdown.split(/\r?\n/).map(line => resolveUrls(line, baseUrl)); - return <Section rawLines={lines} />; + + const sanitized = resolveUrls(markdown, baseUrl); + + return ( + <Typography> + <ReactMarkdown renderers={renderers} allowDangerousHtml>{sanitized}</ReactMarkdown> + </Typography> + ); }; diff --git a/src/lib/Markdown/Section.tsx b/src/lib/Markdown/Section.tsx deleted file mode 100644 index fb2933d..0000000 --- a/src/lib/Markdown/Section.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { Typography } from '@material-ui/core'; -import ContentSection from '../ContentSection/ContentSection'; -import Content from './Content'; -import { ParserPropTypes } from './types'; - -interface PropTypes extends ParserPropTypes { - level?: number; -} - -interface MapperPropTypes extends PropTypes { - SectionComponent: React.FC<PropTypes>; -} - -const getHeaderLevel = (header: string): number => { - if (!header) return 0; - let level = 0; - while (header[level] === '#') level += 1; - return level; -}; - -const SectionMapper: React.FC<MapperPropTypes> = ({ rawLines, level = 0, SectionComponent }) => { - const children = rawLines - .reduce((sections: string[][], line: string) => { - if (line) { - if (getHeaderLevel(line) === level) sections.push([]); - if (sections.length) sections[sections.length - 1].push(line); - } - return sections; - }, []) - .map(sectionLines => <SectionComponent rawLines={sectionLines} level={level} />); - - return <>{children}</>; -}; - - -const Section: React.FC<PropTypes> = ({ rawLines, level = 0 }) => { - const deeperLevelIndex = rawLines.findIndex(line => line.match(`^#{${level + 1},} .*$`)); - const rawContent = rawLines.splice(0, (deeperLevelIndex < 0) ? rawLines.length : deeperLevelIndex); - - if (!level) { - return ( - <> - <Typography><Content rawLines={rawContent} /></Typography> - <SectionMapper rawLines={rawLines} level={getHeaderLevel(rawLines[0])} SectionComponent={Section} /> - </> - ); - } - - const sectionName = rawContent.splice(0, 1)[0].slice(level).trim(); - const deeperLevel = getHeaderLevel(rawLines[0]); - return ( - <ContentSection sectionName={sectionName} level={level}> - <Content rawLines={rawContent} /> - <SectionMapper rawLines={rawLines} level={deeperLevel} SectionComponent={Section} /> - </ContentSection> - ); -}; - -export default Section; - diff --git a/src/lib/Markdown/SyntacticSpan.tsx b/src/lib/Markdown/SyntacticSpan.tsx deleted file mode 100644 index 11cc024..0000000 --- a/src/lib/Markdown/SyntacticSpan.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react'; -import { Link, makeStyles } from '@material-ui/core'; - -import { lib as emojiLib } from 'emojilib'; - -interface PropTypes { - span: string; -} - -interface RegexPair { - global: RegExp; - local: RegExp; -} - -interface Emoji { - name: string; - char: string; -} - -const enclosureRegex = (e: string): RegexPair => ({ - local: new RegExp(`${e}([^${e}]+)${e}`), - global: new RegExp(`(${e}[^${e}]+${e})`), -}); - -const regex: Record<string, RegexPair> = { - conceal: { - global: /(!?\[.+?\]\(.+?\))(?!])/g, - local: /!?\[(.*\]?.*)\]\((.+?)\)/, - }, - rawLink: { - // eslint-disable-next-line max-len - global: /((?:(?:[A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www\.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)(?:(?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[.!/\\\w]*))?)/, - local: /&^/, - }, - emoji: enclosureRegex(':'), - bold: enclosureRegex('\\*\\*'), - italic: enclosureRegex('\\*'), - code: enclosureRegex('`'), - strikeThrough: enclosureRegex('~~'), -}; - -const splitter = new RegExp(Object.values(regex).map(pair => pair.global.source).join('|')); - -const emojiList: Emoji[] = []; -Object.keys(emojiLib).forEach(name => emojiList.push({ name, char: emojiLib[name].char })); - -const useStyles = makeStyles(theme => ({ - code: { - background: theme.palette.background.default, - borderRadius: theme.spacing(0.5), - padding: theme.spacing(0.5), - fontFamily: 'Monospace', - }, - image: { - maxWidth: '100%', - maxHeight: '100%', - }, -})); - -const SyntacticSpan: React.FC<PropTypes> = ({ span }) => { - const classes = useStyles(); - if (!span) return null; - - const matchConceal = regex.conceal.local.exec(span); - if (matchConceal) { - if (span[0] === '!') return <img src={matchConceal[2]} alt={matchConceal[1]} className={classes.image} />; - return <Link href={matchConceal[2]}><SyntacticSpan span={matchConceal[1]} /></Link>; - } - - const matchEmoji = span.match(regex.emoji.local); - if (matchEmoji) { - const emoji = emojiList.find(e => e.name === matchEmoji[1]); - return <span>{emoji ? emoji.char : span}</span>; - } - - const matchCode = span.match(regex.code.local); - if (matchCode) return <span className={classes.code}>{matchCode[1]}</span>; - - const matchBold = span.match(regex.bold.local); - if (matchBold) return <b>{matchBold[1]}</b>; - - const matchItalic = span.match(regex.italic.local); - if (matchItalic) return <i>{matchItalic[1]}</i>; - - const matchStrikeThrough = span.match(regex.strikeThrough.local); - if (matchStrikeThrough) return <span style={{ textDecoration: 'line-through' }}>{matchStrikeThrough[1]}</span>; - - if (span.match(regex.rawLink.global)) return <Link href={span}>{span}</Link>; - - return <>{span}</>; -}; - - -export { splitter }; -export default SyntacticSpan; - diff --git a/src/lib/Markdown/Text.tsx b/src/lib/Markdown/Text.tsx deleted file mode 100644 index be715fd..0000000 --- a/src/lib/Markdown/Text.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import SyntacticSpan, { splitter } from './SyntacticSpan'; - -interface PropTypes { - line: string; -} - -const Text: React.FC<PropTypes> = ({ line }) => { - return <>{line.split(splitter).map(span => <SyntacticSpan span={span} />)}</>; -}; - -export default Text; - |