aboutsummaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/ContentSection/ContentSection.tsx13
-rw-r--r--src/lib/Markdown/CodeBlock.tsx27
-rw-r--r--src/lib/Markdown/Content.tsx77
-rw-r--r--src/lib/Markdown/Markdown.tsx24
-rw-r--r--src/lib/Markdown/Section.tsx46
-rw-r--r--src/lib/Markdown/SyntacticSpan.tsx96
-rw-r--r--src/lib/Markdown/Text.tsx13
-rw-r--r--src/lib/Markdown/emojilib.d.ts2
-rw-r--r--src/lib/Markdown/types.ts4
-rw-r--r--src/lib/index.ts1
10 files changed, 300 insertions, 3 deletions
diff --git a/src/lib/ContentSection/ContentSection.tsx b/src/lib/ContentSection/ContentSection.tsx
index 7ff47f9..ba8b882 100644
--- a/src/lib/ContentSection/ContentSection.tsx
+++ b/src/lib/ContentSection/ContentSection.tsx
@@ -9,11 +9,12 @@ import {
interface PropTypes {
sectionName: string;
+ level?: number;
}
const useStyles = makeStyles(theme => ({
content: {
- padding: theme.spacing(0, 2, 1, 2),
+ padding: theme.spacing(2, 2, 1, 3),
marginBottom: theme.spacing(1),
'& .MuiButton-root': {
@@ -22,12 +23,18 @@ const useStyles = makeStyles(theme => ({
},
}));
-const ContentSection: React.FC<PropTypes> = ({ sectionName, children }) => {
+const ContentSection: React.FC<PropTypes> = ({ sectionName, children, level = 0 }) => {
const classes = useStyles();
+ level += 2; // Make everything smaller
+ if (level > 6) level = 6;
+
+ type Variant = 'h3' | 'h4' | 'h5' | 'h6';
+ const variant: Variant = 'h' + level as Variant;
+
return (
<>
- <Typography variant="h4">{sectionName}</Typography>
+ <Typography variant={variant}>{sectionName}</Typography>
<Divider variant="middle"/>
<Typography component="div" className={classes.content}>
{children}
diff --git a/src/lib/Markdown/CodeBlock.tsx b/src/lib/Markdown/CodeBlock.tsx
new file mode 100644
index 0000000..5b8edec
--- /dev/null
+++ b/src/lib/Markdown/CodeBlock.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { ParserPropTypes } from './types';
+import { Paper } from '@material-ui/core';
+
+import { makeStyles } from '@material-ui/core/styles';
+
+const useStyles = makeStyles(theme => ({
+ root: {
+ background: theme.palette.background.default,
+ padding: theme.spacing(1),
+ overflowX: 'auto',
+ fontFamily: 'Monospace',
+ scrollbarColor: 'auto'
+ },
+}));
+
+const CodeBlock: React.FC<ParserPropTypes> = ({ rawLines }) => {
+ const classes = useStyles();
+ return (
+ <Paper variant="outlined" className={classes.root}>
+ {rawLines.map(line => <pre>{line}</pre>)}
+ </Paper>
+ );
+}
+
+export default CodeBlock;
+
diff --git a/src/lib/Markdown/Content.tsx b/src/lib/Markdown/Content.tsx
new file mode 100644
index 0000000..aaea100
--- /dev/null
+++ b/src/lib/Markdown/Content.tsx
@@ -0,0 +1,77 @@
+import React from 'react';
+
+import CodeBlock from './CodeBlock';
+import Text from './Text';
+import { ParserPropTypes } from './types';
+
+
+const denotesCodeBlock = (line: string): boolean => {
+ return line.match(/^```.*$/) !== 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 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(line => denotesCodeBlock(line));
+ const codeBlockLines = rawLines.splice(0, closeIndex + 1).slice(0, closeIndex);
+ buffer = <CodeBlock rawLines={codeBlockLines} />
+ } else if (denotesDottedList(line)) {
+ const closeIndex = rawLines.findIndex(line => !denotesDottedList(line));
+ 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 = rawLines.findIndex(line => denotesClosingHtml(line, tag));
+ const htmlLines = rawLines.splice(0, closeIndex + 1).slice(0, closeIndex);
+ htmlLines.unshift(line);
+ buffer = <div dangerouslySetInnerHTML={{ __html: htmlLines.join('\n') }}></div>;
+ } else if ((buffer = denotesSelfClosingHtml(line)) !== null) {
+ const match = buffer[0];
+ const [before, after] = line.split(match);
+ console.log({ line, match, before, after});
+ buffer = (
+ <>
+ <Text line={before} />
+ <div dangerouslySetInnerHTML={{ __html: match }}></div>
+ <Text line={after} />
+ </>
+ );
+ } else {
+ buffer = <p><Text line={line} /></p>
+ }
+
+ return (
+ <>
+ { buffer }
+ <Content rawLines={rawLines} />
+ </>
+ );
+}
+
+export default Content;
+
diff --git a/src/lib/Markdown/Markdown.tsx b/src/lib/Markdown/Markdown.tsx
new file mode 100644
index 0000000..09ad54a
--- /dev/null
+++ b/src/lib/Markdown/Markdown.tsx
@@ -0,0 +1,24 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+
+import Section from './Section';
+
+interface PropTypes {
+ data?: string;
+ url?: string;
+}
+
+const Markdown: React.FC<PropTypes> = ({ data, url }) => {
+ const [markdown, setMarkdown] = useState<string>(data || '');
+
+ useEffect(() => {
+ if (!url) setMarkdown(data || '');
+ }, [data, url]);
+
+ if (url) axios.get(url).then(response => setMarkdown(response.data));
+ return <Section rawLines={markdown.split('\n')} />
+};
+
+
+export default Markdown;
+
diff --git a/src/lib/Markdown/Section.tsx b/src/lib/Markdown/Section.tsx
new file mode 100644
index 0000000..5ce8954
--- /dev/null
+++ b/src/lib/Markdown/Section.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import ContentSection from '../ContentSection/ContentSection';
+import Content from './Content';
+import { ParserPropTypes } from './types';
+
+interface PropTypes extends ParserPropTypes {
+ level?: number;
+}
+
+const getHeaderLevel = (header: string): number => {
+ if (!header) return 0;
+ let level = 0;
+ while(header[level] === '#') level++;
+ return level;
+}
+
+const ChildrenSections: React.FC<PropTypes> = ({ rawLines, level = 0 }) => {
+ const childrenSectionLines = 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;
+ }, []);
+ const children = childrenSectionLines.map(sectionLines => <Section 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 <ChildrenSections rawLines={rawLines} level={getHeaderLevel(rawLines[0])}/>;
+
+ const sectionName = rawContent.splice(0, 1)[0].slice(level).trim();
+ const deeperLevel = getHeaderLevel(rawLines[0]);
+ return (
+ <ContentSection sectionName={sectionName} level={level}>
+ <Content rawLines={rawContent} />
+ <ChildrenSections rawLines={rawLines} level={deeperLevel} />
+ </ContentSection>
+ );
+}
+
+export default Section;
+
diff --git a/src/lib/Markdown/SyntacticSpan.tsx b/src/lib/Markdown/SyntacticSpan.tsx
new file mode 100644
index 0000000..299bf87
--- /dev/null
+++ b/src/lib/Markdown/SyntacticSpan.tsx
@@ -0,0 +1,96 @@
+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: {
+ 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 }));
+console.log({emojiList})
+
+const useStyles = makeStyles(theme => ({
+ code: {
+ background: theme.palette.background.default,
+ borderRadius: theme.spacing(.5),
+ padding: theme.spacing(.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]}>{matchConceal[1]}</Link>;
+ }
+
+ const matchEmoji = span.match(regex.emoji.local);
+ if (matchEmoji) {
+ const emoji = emojiList.find(emoji => emoji.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
new file mode 100644
index 0000000..e287dee
--- /dev/null
+++ b/src/lib/Markdown/Text.tsx
@@ -0,0 +1,13 @@
+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;
+
diff --git a/src/lib/Markdown/emojilib.d.ts b/src/lib/Markdown/emojilib.d.ts
new file mode 100644
index 0000000..cddfeea
--- /dev/null
+++ b/src/lib/Markdown/emojilib.d.ts
@@ -0,0 +1,2 @@
+declare module 'emojilib';
+
diff --git a/src/lib/Markdown/types.ts b/src/lib/Markdown/types.ts
new file mode 100644
index 0000000..0b6f4b6
--- /dev/null
+++ b/src/lib/Markdown/types.ts
@@ -0,0 +1,4 @@
+export interface ParserPropTypes {
+ rawLines: string[];
+}
+
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 1529443..0f31104 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -4,3 +4,4 @@ export { default as ContentSection } from './ContentSection/ContentSection';
export { default as SmartList } from './SmartList/SmartList';
export { default as Benzin } from './Benzin/Benzin';
export { default as Button } from './Button/Button';
+export { default as Markdown } from './Markdown/Markdown';