diff options
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/ContentSection/ContentSection.tsx | 51 | ||||
| -rw-r--r-- | src/lib/Markdown/CodeBlock.tsx | 13 | ||||
| -rw-r--r-- | src/lib/Markdown/Content.tsx | 90 | ||||
| -rw-r--r-- | src/lib/Markdown/Heading.tsx | 33 | ||||
| -rw-r--r-- | src/lib/Markdown/Image.tsx | 12 | ||||
| -rw-r--r-- | src/lib/Markdown/InlineCode.tsx | 18 | ||||
| -rw-r--r-- | src/lib/Markdown/Markdown.tsx | 69 | ||||
| -rw-r--r-- | src/lib/Markdown/Section.tsx | 61 | ||||
| -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.d.ts | 7 | ||||
| -rw-r--r-- | src/lib/Markdown/types.ts | 4 | ||||
| -rw-r--r-- | src/lib/index.ts | 4 | 
14 files changed, 140 insertions, 333 deletions
| diff --git a/src/lib/ContentSection/ContentSection.tsx b/src/lib/ContentSection/ContentSection.tsx deleted file mode 100644 index f18684c..0000000 --- a/src/lib/ContentSection/ContentSection.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; - -import { -  Typography, -  Divider, -  makeStyles, -  useMediaQuery, -  Theme, -} from '@material-ui/core'; - - -interface PropTypes { -  sectionName: string; -  level?: number; -} - -const useStyles = makeStyles(theme => ({ -  content: { -    [theme.breakpoints.up('md')]: { -      padding: theme.spacing(2, 2, 1, 3), -    }, -    [theme.breakpoints.down('sm')]: { -      padding: theme.spacing(2, 0), -    }, -    marginBottom: theme.spacing(1), -  }, -})); - -const ContentSection: React.FC<PropTypes> = ({ sectionName, children, level = 0 }) => { -  const classes = useStyles(); -  const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm')); - -  let adjustedLevel = level + 2; // Make everything smaller -  if (adjustedLevel > 6) adjustedLevel = 6; - -  type Variant = 'h3' | 'h4' | 'h5' | 'h6'; -  const variant: Variant = `h${adjustedLevel}` as Variant; - -  return ( -    <> -      <Typography variant={variant}>{sectionName}</Typography> -      <Divider variant={isMobile ? 'fullWidth' : 'middle'} /> -      <Typography component="div" className={classes.content}> -        {children} -      </Typography> -    </> -  ); -}; - - -export default ContentSection; 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..c0389dc 100644 --- a/src/lib/Markdown/Markdown.tsx +++ b/src/lib/Markdown/Markdown.tsx @@ -1,33 +1,82 @@  import React, { useState, useEffect } from 'react'; +import { Link, Typography } from '@material-ui/core';  import axios from 'axios'; +import ReactMarkdown from 'react-markdown'; +import emoji from 'remark-gemoji'; -import Section from './Section'; +import CodeBlock from './CodeBlock'; +import InlineCode from './InlineCode'; +import Heading from './Heading'; +import Image from './Image';  interface PropTypes { -  data?: string; +  source?: string;    url?: string; +  // eslint-disable-next-line @typescript-eslint/no-explicit-any +  context?: Record<string, any>; +  // eslint-disable-next-line @typescript-eslint/no-explicit-any +  plugins?: any[]  }  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 Markdown: React.FC<PropTypes> = ({ data, url }) => { -  const [markdown, setMarkdown] = useState<string>(data || ''); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const WrappedInlineCode = (context: Record<string, any>): React.FC => ({ children }) => { +  if (typeof children === 'string' && children?.startsWith('$')) { +    const symbol = children.slice(1); +    return context[symbol] || null; +  } +  return <InlineCode>{children}</InlineCode>; +}; + +const Markdown: React.FC<PropTypes> = ({ +  children, +  url, +  source, +  context = {}, +  plugins = [], +}) => { +  const [markdown, setMarkdown] = useState<string>(source || ''); -  if (url) axios.get(url).then(response => setMarkdown(response.data)); +  useEffect(() => { +    if (url) axios.get(url).then(response => setMarkdown(response.data)); +  }, [url]);    useEffect(() => { -    if (!url) setMarkdown(data || ''); -  }, [data, url]); +    if (source) setMarkdown(source); +  }, [source]); + +  useEffect(() => { +    if (children && typeof children === 'string') setMarkdown(children); +  }, [children]);    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); + +  const renderers = { +    heading: Heading, +    code: CodeBlock, +    link: Link, +    image: Image, +    inlineCode: WrappedInlineCode(context), +  }; + +  return ( +    <Typography> +      <ReactMarkdown +        source={sanitized} +        renderers={renderers} +        plugins={[emoji, ...plugins]} +        allowDangerousHtml +      /> +    </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; - diff --git a/src/lib/Markdown/emojilib.d.ts b/src/lib/Markdown/emojilib.d.ts deleted file mode 100644 index cddfeea..0000000 --- a/src/lib/Markdown/emojilib.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare module 'emojilib'; - diff --git a/src/lib/Markdown/types.d.ts b/src/lib/Markdown/types.d.ts new file mode 100644 index 0000000..f444cf1 --- /dev/null +++ b/src/lib/Markdown/types.d.ts @@ -0,0 +1,7 @@ +declare module 'remark-gemoji'; +declare module '*.md' { +  // eslint-disable-next-line import/no-mutable-exports +  let Markdown: string; +  export default Markdown; +} + diff --git a/src/lib/Markdown/types.ts b/src/lib/Markdown/types.ts deleted file mode 100644 index 0b6f4b6..0000000 --- a/src/lib/Markdown/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ParserPropTypes { -  rawLines: string[]; -} - diff --git a/src/lib/index.ts b/src/lib/index.ts index 545e6f8..2e6dd9e 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,3 +1,5 @@ -export { default as ContentSection } from './ContentSection/ContentSection';  export { default as Benzin } from './Benzin/Benzin';  export { default as Markdown } from './Markdown/Markdown'; +export { default as CodeBlock } from './Markdown/CodeBlock'; +export { default as InlineCode } from './Markdown/InlineCode'; +export { default as Heading } from './Markdown/Heading'; | 
