aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/index.tsx93
-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
11 files changed, 333 insertions, 63 deletions
diff --git a/src/index.tsx b/src/index.tsx
index 73536d7..b64b207 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,15 +1,13 @@
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
-import { makeStyles, Link } from '@material-ui/core';
+import { makeStyles } from '@material-ui/core';
import {
Benzin,
Header,
Window,
- ContentSection,
- SmartList,
- Button,
+ Markdown,
} from './lib';
import icon from './assets/icon.svg';
@@ -31,8 +29,18 @@ const Icon = <img src={icon} width="32px" height="37px" alt="logo"/>
const headerContents = {
home: null,
- page: null,
- 'another page': null,
+ space: null,
+ 'spacevim': null,
+ 'emoji': null,
+ 'material-ui': null,
+};
+
+const pageMap: Record<string, string> = {
+ home: 'https://raw.githubusercontent.com/eug-vs/react-benzin/develop/README.md',
+ space: 'https://raw.githubusercontent.com/eug-vs/space/master/docs/environment.md',
+ 'spacevim': 'https://raw.githubusercontent.com/spacevim/spacevim/master/README.md',
+ emoji: 'https://raw.githubusercontent.com/muan/emoji/gh-pages/README.md',
+ 'material-ui': 'https://raw.githubusercontent.com/mui-org/material-ui/master/README.md',
};
@@ -40,30 +48,20 @@ const App: React.FC = () => {
const classes = useStyles();
const [page, setPage] = useState('home');
- const renderItem: React.FC<RenderPropTypes> = ({ index, style}) => {
- return (
- <div style={style} className={classes.window}>
- <ContentSection sectionName={`Item ${index+1}`}>
- <p>
- Fusce commodo. Vestibulum convallis, lorem a tempus semper, dui dui euismod elit, vitae placerat urna tortor vitae lacus. Nullam libero mauris, consequat quis, varius et, dictum id, arcu. Mauris mollis tincidunt felis.
- </p>
- {(index % 2 === 0)?
- (
- <Button color="primary">
- primary
- </Button>
- )
- :
- (
- <Button color="secondary">
- secondary
- </Button>
- )
- }
- </ContentSection>
- </div>
- );
- };
+ const url = pageMap[page];
+ const fileName = url.slice(url.lastIndexOf('/') + 1);
+ const metadata = [
+ `## Markdown\n [Markdown file](${url}) *(...${fileName})* that you can see on the left was parsed and processed by **BENZIN**! :rocket:`,
+ 'Switch between tabs on the header to explore other markdown templates. :recycle: ',
+ 'Currently **only core features** of markdown function.',
+ 'Templates on the left are being loaded from the [GitHub](https://github.com), though this pane is generated from plaintext. :pen:',
+ '## How do I use this feature?',
+ '```',
+ 'import Markdown from \'react-benzin\';',
+ 'const data = \'# Header\\nHello, *world!*\';',
+ 'ReactDOM.render(<Markdown data={data}/>, document.getElementById(\'root\'));',
+ '```',
+ ].join('\n');
return (
<Benzin>
@@ -78,38 +76,13 @@ const App: React.FC = () => {
/>
<Window type="primary">
<div className={classes.window}>
- <ContentSection sectionName="Library preview">
- <p>
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pellentesque dapibus suscipit ligula. Donec posuere augue in quam. Etiam vel tortor sodales tellus ultricies commodo. Suspendisse potenti. Aenean in sem ac leo mollis blandit. Donec neque quam, dignissim in, mollis nec, sagittis eu, wisi. <Link href="#">Phasellus lacus.</Link> Etiam laoreet quam sed arcu. Phasellus at dui in ligula mollis ultricies. Integer placerat tristique nisl. Praesent augue. Fusce commodo.
- </p>
- <Button color="secondary">
- secondary
- </Button>
- <Button color="primary">
- primary
- </Button>
- </ContentSection>
- <ContentSection sectionName="Content section">
- <p>
- Fusce suscipit, wisi nec facilisis facilisis, est dui fermentum leo, quis tempor ligula erat quis odio. Nunc porta vulputate tellus. Nunc rutrum turpis sed pede. Sed bibendum. Aliquam posuere.
- </p>
- <p>
- <Link href="#">Link example</Link>
- </p>
- </ContentSection>
- <ContentSection sectionName="Content section">
- <p>
- Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec hendrerit tempor tellus. Donec pretium posuere tellus. Proin quam nisl, tincidunt et, mattis eget, convallis nec, purus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nulla posuere. Donec vitae dolor. Nullam tristique diam non turpis. Cras placerat accumsan <Link href="#">nulla</Link>. Nullam rutrum. Nam vestibulum accumsan nisl. Pellentesque dapibus suscipit ligula.
- </p>
- </ContentSection>
+ <Markdown url={url} />
</div>
</Window>
- <Window type="secondary" name="SmartList preview window">
- <SmartList
- itemSize={270}
- itemCount={100}
- renderItem={renderItem}
- />
+ <Window type="secondary" name="Feature preview">
+ <div className={classes.window}>
+ <Markdown data={metadata} />
+ </div>
</Window>
</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 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';