diff options
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/Benzin/Benzin.tsx (renamed from src/lib/BenzinThemeProvider/BenzinThemeProvider.tsx) | 10 | ||||
-rw-r--r-- | src/lib/ContentSection/ContentSection.tsx | 13 | ||||
-rw-r--r-- | src/lib/Markdown/CodeBlock.tsx | 27 | ||||
-rw-r--r-- | src/lib/Markdown/Content.tsx | 77 | ||||
-rw-r--r-- | src/lib/Markdown/Markdown.tsx | 24 | ||||
-rw-r--r-- | src/lib/Markdown/Section.tsx | 46 | ||||
-rw-r--r-- | src/lib/Markdown/SyntacticSpan.tsx | 96 | ||||
-rw-r--r-- | src/lib/Markdown/Text.tsx | 13 | ||||
-rw-r--r-- | src/lib/Markdown/emojilib.d.ts | 2 | ||||
-rw-r--r-- | src/lib/Markdown/types.ts | 4 | ||||
-rw-r--r-- | src/lib/index.ts | 3 |
11 files changed, 307 insertions, 8 deletions
diff --git a/src/lib/BenzinThemeProvider/BenzinThemeProvider.tsx b/src/lib/Benzin/Benzin.tsx index efb4f86..83ed0b0 100644 --- a/src/lib/BenzinThemeProvider/BenzinThemeProvider.tsx +++ b/src/lib/Benzin/Benzin.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import orange from '@material-ui/core/colors/orange'; +import purple from '@material-ui/core/colors/purple'; import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; import { CssBaseline } from '@material-ui/core'; import 'typeface-roboto'; @@ -17,10 +19,10 @@ const benzinTheme = createMuiTheme({ palette: { type: 'dark', primary: { - main: '#ffa726', + main: orange[400], }, secondary: { - main: '#9c27b0', + main: purple[500], }, background: { default: '#121212', @@ -37,7 +39,7 @@ const benzinTheme = createMuiTheme({ }); -const BenzinThemeProvider: React.FC = ({ children }) => ( +const Benzin: React.FC = ({ children }) => ( <ThemeProvider theme={benzinTheme}> <CssBaseline /> {children} @@ -45,5 +47,5 @@ const BenzinThemeProvider: React.FC = ({ children }) => ( ); -export default BenzinThemeProvider; +export default Benzin; 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 a41dd39..0f31104 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -2,5 +2,6 @@ export { default as Window } from './Window/Window'; export { default as Header } from './Header/Header'; export { default as ContentSection } from './ContentSection/ContentSection'; export { default as SmartList } from './SmartList/SmartList'; -export { default as BenzinThemeProvider } from './BenzinThemeProvider/BenzinThemeProvider'; +export { default as Benzin } from './Benzin/Benzin'; export { default as Button } from './Button/Button'; +export { default as Markdown } from './Markdown/Markdown'; |