diff options
Diffstat (limited to 'src/lib/Markdown')
| -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 | 
8 files changed, 289 insertions, 0 deletions
| 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[]; +} + | 
