aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authoreug-vs <eug-vs@keemail.me>2021-01-07 13:22:21 +0300
committereug-vs <eug-vs@keemail.me>2021-01-07 15:17:43 +0300
commitf5c21698a09d9214f57ce6dab2595f3d61e1ff11 (patch)
tree900b605f147a5a024377b42be16a6f677a10a387 /src
parentafa7e4abe02959798665949193d832907d97153a (diff)
downloadreact-benzin-f5c21698a09d9214f57ce6dab2595f3d61e1ff11.tar.gz
feat!: use react-markdown
Diffstat (limited to 'src')
-rw-r--r--src/lib/Markdown/CodeBlock.tsx13
-rw-r--r--src/lib/Markdown/Content.tsx90
-rw-r--r--src/lib/Markdown/Heading.tsx33
-rw-r--r--src/lib/Markdown/Image.tsx12
-rw-r--r--src/lib/Markdown/InlineCode.tsx18
-rw-r--r--src/lib/Markdown/Markdown.tsx27
-rw-r--r--src/lib/Markdown/Section.tsx61
-rw-r--r--src/lib/Markdown/SyntacticSpan.tsx96
-rw-r--r--src/lib/Markdown/Text.tsx13
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;
-