diff --git a/.babelrc b/.babelrc index ed55a7ce..7b400959 100644 --- a/.babelrc +++ b/.babelrc @@ -19,10 +19,11 @@ "@babel/plugin-proposal-numeric-separator", "@babel/plugin-proposal-throw-expressions", ["@babel/plugin-proposal-class-properties", { "loose": true }], + ["@babel/plugin-proposal-private-property-in-object", { "loose": true }], "@babel/proposal-object-rest-spread", "@babel/transform-react-constant-elements", "@babel/transform-react-inline-elements", "transform-react-remove-prop-types", - "transform-react-pure-class-to-function", + "transform-react-pure-class-to-function" ] } diff --git a/src/core/Markdown.js b/src/core/MarkdownParser.js similarity index 55% rename from src/core/Markdown.js rename to src/core/MarkdownParser.js index 307589eb..dd742311 100644 --- a/src/core/Markdown.js +++ b/src/core/MarkdownParser.js @@ -15,22 +15,36 @@ export default class MarkdownParser { this.parseLinks = opt && opt.parseLinks || false; this.tabWidth = opt && opt.tabWidth || 4; this.newlineBreaksArticles = opt && opt.newlineBreaksArticles || true; + this.keepQuoteArrows = opt && opt.keepQuoteArrows || true; } - parseText(text: string, level = 0, start = 0) { + parse(text: string) { + return this.parseText(text, 0, 0, '')[0]; + } + + parseText(text, headingLevel, start, quoteLevel) { let mdArray = []; let iter = start; while (iter < text.length) { - const [aMdArray, newIter] = this.parseArticle(text, iter, level); + const [aMdArray, newIter, breaking] = this.parseSection( + text, iter, headingLevel, quoteLevel, + ); iter = newIter; mdArray = mdArray.concat(aMdArray); + if (breaking) { + break; + } // either heading hit or article end - if (text[iter] === '#') { + const chr = text[iter]; + if (chr === '#') { let subLvl = 0; - for (;iter + subLvl <= text.length && text[iter + subLvl] === '#'; subLvl += 1) {} - if (subLvl <= level || level === 6) { + for (; + iter + subLvl <= text.length && text[iter + subLvl] === '#'; + subLvl += 1 + ) {} + if (subLvl <= headingLevel || headingLevel === 6) { // end of article - // encountered title with same level or lower + // encountered title with same headingLevel or lower break; } else { // child article @@ -39,11 +53,18 @@ export default class MarkdownParser { const title = text.slice(iter + subLvl, lineEnd).trimLeft(); subLvl = Math.min(subLvl, 6); const [subMdArray, newIter] = this.parseText( - text, subLvl, lineEnd + 1, + text, subLvl, lineEnd + 1, quoteLevel, ); mdArray.push(['a', subLvl, title, subMdArray]); iter = newIter; } + } else if (chr === '>' || chr === '<') { + // child quote + const [subMdArray, newIter] = this.parseText( + text, 0, iter - quoteLevel.length, quoteLevel + chr, + ); + mdArray.push([chr, subMdArray]); + iter = newIter; } else { break; } @@ -52,12 +73,10 @@ export default class MarkdownParser { return [mdArray, iter]; } - stoppingCondition(text: string, iter: number) { + static stoppingCondition(text: string, iter: number) { const chr = text[iter]; if (chr === '\n' || chr === '#' - || (chr === '`' && text[iter + 1] === '`' && text[iter + 2] === '`' - || (chr === '-' && text[iter + 1] === ' ')) ) { return true; } @@ -68,54 +87,149 @@ export default class MarkdownParser { * parses Articles (contains paragraphs, code-blocks, numeration, etc.) * @param text string of text * @param start number of position in text where to start - * @param level the number of heading levels we are in + * @param headingLevel the number of heading headingLevels we are in * @param indent ndentation that should be considered - * returns when encountering heading of <= level (iter is at # position) + * returns when encountering heading of <= headingLevel (iter is at # position) * or heading-cancel with three spaces (iter is past newlines) * or ident is smaller than given */ - parseArticle(text: string, start: number, level = 0, indent = 0) { + parseSection( + text: string, + start: number, + headingLevel = 0, + quoteLevel = '', + indent = 0, + ) { let iter = start; + /* + * breaking is currently only used when + * end of quote block is reached + */ + let breaking = false; const mdArray = []; let paraStart = iter; + let lineNr = 0; + + const addParagraph = (start, end) => { + let paraText = text.slice(start, end); + if (!this.keepQuoteArrows && quoteLevel) { + paraText = paraText.split('\n') + .map((t) => t.trim().slice(quoteLevel.length)) + .join('\n'); + } + mdArray.push(['p', paraText]); + } while (true) { if (iter >= text.length) { if (paraStart < text.length) { - mdArray.push(['p', text.slice(paraStart)]); + addParagraph(paraStart, text.length); } break; } - // beginning of line - const paraLineStart = iter; - if (indent && this.getIndent(text, paraLineStart) <= indent) { - // smaller indent occured + const paraLineStart = iter; + lineNr += 1; + + /* + * break when in or out of quote levels + */ + if (!indent || lineNr > 1) { + if (text.startsWith(quoteLevel, iter)) { + iter += quoteLevel.length; + const chr = text[iter]; + if (chr === '>' || chr === '<') { + if (iter - 1 > paraStart) { + addParagraph(paraStart, iter - quoteLevel.length - 1); + } + break; + } + } + else { + // quote ended aka less deep quotelevel + if (iter - 1 > paraStart) { + addParagraph(paraStart, iter - 1); + } + breaking = true; + break; + } + } else { + iter += quoteLevel.length; } - iter = this.skipSpaces(text, iter); - if (stoppingCondition(text, iter)) { - const chr = text[iter]; + + /* + * act on indent + */ + let curIndent; + [curIndent, iter] = this.getIndent(text, iter); + if (curIndent < indent && lineNr > 1) { + if (paraLineStart - 1 > paraStart) { + addParagraph(paraStart, paraLineStart - 1); + } + iter = paraLineStart; + break; + } + + const chr = text[iter]; + + if (chr === '-') { + if (paraLineStart - 1 > paraStart) { + addParagraph(paraStart, paraLineStart - 1); + } + let childMdArray; + [childMdArray, iter, breaking] = this.parseSection( + text, + iter + 1, + headingLevel, + quoteLevel, + curIndent + 1, + ); + childMdArray = ['-', childMdArray]; + // lists are encapsuled by 'ul' + if (!mdArray.length || mdArray[mdArray.length - 1][0] !== 'ul') { + mdArray.push(['ul', [childMdArray]]); + } + else { + mdArray[mdArray.length - 1][1].push(childMdArray); + } + if (breaking) { + return [mdArray, iter, breaking]; + } + paraStart = iter; + continue; + } + + /* + * code block + */ + if (chr === '`' && text[iter + 1] === '`' && text[iter + 2] === '`') { + if (paraLineStart - 1 > paraStart) { + addParagraph(paraStart, paraLineStart - 1); + } + const [cbArray, newIter] = this.parseCodeBlock(text, iter + 3); + mdArray.push(cbArray); + iter = newIter; + paraStart = iter; + continue; + } + + /* other stopping conditions */ + if (!indent && MarkdownParser.stoppingCondition(text, iter)) { // encountered something - save paragraph if (paraLineStart - 1 > paraStart) { - mdArray.push(['p', text.slice(paraStart, paraLineStart - 1)]); + addParagraph(paraStart, paraLineStart - 1); } + const chr = text[iter]; if (chr === '\n') { iter = this.skipSpaces(text, iter + 1); if (text[iter] === '\n') { - if (level > 0 && this.newlineBreaksArticles) { + if (headingLevel && this.newlineBreaksArticles) { break; } iter += 1; } - } else if (chr === '-') { - const [cbArray, newIter] = this.parseList(text, paraLineStart); } else if (chr === '#') { break; - } else if (chr === '`') { - // code block - const [cbArray, newIter] = this.parseCodeBlock(text, iter + 3); - mdArray.push(cbArray); - iter = newIter; } paraStart = iter; continue; @@ -125,7 +239,7 @@ export default class MarkdownParser { iter += 1; } - return [mdArray, iter]; + return [mdArray, iter, breaking]; } parseList(text: string, start: number) { @@ -153,7 +267,7 @@ export default class MarkdownParser { } iter += 1; } - return getIndent; + return [indent, iter]; } /* @@ -161,7 +275,7 @@ export default class MarkdownParser { * start is first character after the initializing ``` * we just parse till the ending occures */ - parseCodeBlock(text: string, start: number) { + parseCodeBlock(text, start) { const cbStart = this.skipSpaces(text, start, true); let iter = cbStart; while (true) { @@ -173,7 +287,7 @@ export default class MarkdownParser { && text[iter + 1] === '`' && text[iter + 2] === '`' ) { - const nextIter = this.skipSpaces(text, iter + 3, true); + const nextIter = iter + 3; return [['cb', text.slice(cbStart, iter)], nextIter]; } for (;iter < text.length && text[iter] !== '\n'; iter += 1) {} diff --git a/utils/markdown-test/Markdown.jsx b/utils/markdown-test/Markdown.jsx new file mode 100644 index 00000000..62d43e81 --- /dev/null +++ b/utils/markdown-test/Markdown.jsx @@ -0,0 +1,92 @@ +/* + * Renders Markdown that got parsed by core/MarkdownParser + */ +import React from 'react'; + +const Markdown = ({ mdArray }) => { + return mdArray.map((part) => { + const type = part[0]; + switch (type) { + /* Heading */ + case 'a': { + const level = Number(part[1]); + const heading = part[2]; + const children = part[3]; + let headingElem = []; + switch (level) { + case 1: + headingElem =

{heading}

; + break; + case 2: + headingElem =

{heading}

; + break; + case 3: + headingElem =

{heading}

; + break; + default: + headingElem =

{heading}

; + } + return [ + headingElem, +
+ +
+ ]; + } + /* Paragraph */ + case 'p': { + const text = part[1]; + return ( +

+ {text} +

+ ); + } + /* Code Block */ + case 'cb': { + /* + * using pe instead of code because pre + * keeps linebreaks + */ + const content = part[1]; + return
{content}
; + } + case '>': + case '<': { + const children = part[1]; + return ( +
') ? 'gt' : 'rt'} + > + +
+ ); + } + case 'ul': { + const children = part[1]; + return ( + + ); + } + case '-': { + const children = part[1]; + return ( +
  • + +
  • + ); + } + default: + return

    {part[1]}

    ; + } +})}; + +const MarkdownArticle = ({ mdArray }) => ( +
    + +
    +); + +export default React.memo(MarkdownArticle); diff --git a/utils/markdown-test/index.css b/utils/markdown-test/index.css new file mode 100644 index 00000000..a58a4c31 --- /dev/null +++ b/utils/markdown-test/index.css @@ -0,0 +1,25 @@ +article { + border-style: solid; +} + +.post-para { + white-space: pre-line; +} + +section { + padding-left: 10px; +} + +pre { + border-style: solid; + border-width: thin; + padding: 5px; +} + +.gt { + color: green; +} + +.rt { + color: red; +} diff --git a/utils/markdown-test/index.html b/utils/markdown-test/index.html index 056b125b..29c5dd1c 100644 --- a/utils/markdown-test/index.html +++ b/utils/markdown-test/index.html @@ -3,11 +3,10 @@ - Markdown Parser Test - + diff --git a/utils/markdown-test/mdtest.js b/utils/markdown-test/mdtest.js index 062fd869..a7c12e84 100644 --- a/utils/markdown-test/mdtest.js +++ b/utils/markdown-test/mdtest.js @@ -1,24 +1,26 @@ -import React from 'react'; +import React, { useState } from 'react'; import ReactDOM from 'react-dom'; - -import MarkdownParser from '../../src/core/Markdown'; import { Parser } from 'commonmark'; -const reader = new Parser({smart: true}); +import MarkdownParser from '../../src/core/MarkdownParser'; + +import Markdown from './Markdown'; + +const reader = new Parser({ smart: true }); const a = new MarkdownParser(); -function parseText(text, setDuration, setCmDuration) { +function parseText(text, setDuration, setCmDuration, setMd) { let startt = Date.now(); - const [arr] = a.parseText(text); + const arr = a.parse(text); setDuration(Date.now() - startt); startt = Date.now(); - const parsed = reader.parse(input.value); + reader.parse(text); setCmDuration(Date.now() - startt); - return JSON.stringify(arr, null, 2); + setMd(arr); } const App = () => { - const [text, setText] = useState(''); + const [md, setMd] = useState([]); const [duration, setDuration] = useState(''); const [cmDuration, setCmDuration] = useState(''); @@ -27,19 +29,23 @@ const App = () => {