From 84f6efec808c8e73f89711b409123522214be0a8 Mon Sep 17 00:00:00 2001 From: developomp Date: Wed, 4 Aug 2021 11:57:32 +0900 Subject: [PATCH] - added code block styling - added mathematical expression - combined recursive parsers --- README.md | 3 +- source/generate.ts | 533 ++++++++++++++--------------- source/markdown/posts/test post.md | 10 +- source/package.json | 5 + source/src/App.tsx | 32 ++ source/yarn.lock | 67 ++++ 6 files changed, 365 insertions(+), 285 deletions(-) diff --git a/README.md b/README.md index b6e638c..4d02ad6 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,11 @@ Tools / Frameworks / Packages used: | [AWS](https://aws.amazon.com) | Domain register | | [Firebase](https://firebase.google.com) | Static site hosting | | [react](https://reactjs.org) | Front end framework | -| [gray-matter](https://github.com/jonschlinkert/gray-matter) | Parsing markdown files | | [react-tooltip](https://github.com/wwayne/react-tooltip) | Tooltips | | [react-date-range](https://github.com/hypeserver/react-date-range) | Date picker for search page | | [elasticlunr](https://github.com/weixsong/elasticlunr.js) | Search engine | +| [gray-matter](https://github.com/jonschlinkert/gray-matter) | Markdown parsing | +| [markdown-it](https://github.com/markdown-it/markdown-it) | Markdown rendering | # Setup diff --git a/source/generate.ts b/source/generate.ts index 540856a..3cd0d31 100644 --- a/source/generate.ts +++ b/source/generate.ts @@ -10,7 +10,11 @@ import fs from "fs" // read and write files import path from "path" // get relative path import elasticlunr from "elasticlunr" // search index generation import matter from "gray-matter" // parse markdown metadata +import markdownIt from "markdown-it" // rendering markdown +import hljs from "highlight.js" // code block highlighting import toc from "markdown-toc" // table of contents generation +import tm from "markdown-it-texmath" // rendering mathematical expression +import katex from "katex" // rendering mathematical expression const markdownPath = "./markdown" // where it will look for markdown documents const outPath = "./src/data" // path to the json database @@ -95,6 +99,24 @@ const index = elasticlunr(function () { this.setRef("url" as never) }) +const md = markdownIt({ + highlight: function (str, lang) { + if (lang && hljs.getLanguage(lang)) { + try { + return hljs.highlight(str, { language: lang }).value + // eslint-disable-next-line no-empty + } catch (error) {} + } + + return "" // use external default escaping + }, + html: true, +}).use(tm, { + engine: katex, + delimiters: "dollars", + katexOptions: { macros: { "\\RR": "\\mathbb{R}" } }, +}) + // converts file path to url function path2URL(pathToConvert: string): string { return `/${path.relative(markdownPath, pathToConvert)}` @@ -112,193 +134,51 @@ function path2FileOrFolderName(inputPath: string): string { return inputPath.slice(inputPath.lastIndexOf("/") + 1) } -// A recursive function that calls itself for every files and directories that it finds -function recursiveParsePosts(fileOrFolderPath: string) { - // get string after the last slash character - const fileOrFolderName = path2FileOrFolderName(fileOrFolderPath) +// gets the nth occurance of a pattern in string +// returns -1 if nothing is found +// https://stackoverflow.com/a/14482123/12979111 +function nthIndex(str: string, pat: string, n: number) { + let i = -1 - // ignore if file or directory name starts with a underscore - if (fileOrFolderName.startsWith("_")) return - - // get data about the given path - const stats = fs.lstatSync(fileOrFolderPath) - - // if it's a directory, call this function to every files/directories in it - // if it's a file, parse it and then save it to file - if (stats.isDirectory()) { - fs.readdirSync(fileOrFolderPath).map((childPath) => { - recursiveParsePosts(`${fileOrFolderPath}/${childPath}`) - }) - } else if (stats.isFile()) { - // skip if it is not a markdown file - if (!fileOrFolderName.endsWith(".md")) { - console.log(`Ignoring non markdown file at: ${fileOrFolderPath}`) - return - } - - // path that will be used as site url - const urlPath = path2URL(fileOrFolderPath) - - // parse markdown metadata - const parsedMarkdown = matter(fs.readFileSync(fileOrFolderPath, "utf8")) - - if (!parsedMarkdown.data.title) { - throw Error(`Title is not defined in file: ${fileOrFolderPath}`) - } - - if (!parsedMarkdown.data.date) { - throw Error(`Date is not defined in file: ${fileOrFolderPath}`) - } - - // urlPath starts with a slash - const contentFilePath = `${contentDirectoryPath}${urlPath}.json` - - // create directory to put json content files - fs.mkdirSync( - contentFilePath.slice(0, contentFilePath.lastIndexOf("/")), - { recursive: true } - ) - - // write content to json file - fs.writeFileSync( - contentFilePath, - JSON.stringify({ - content: parsedMarkdown.content.trim(), - }) - ) - - // Parse data that will be written to map.js - const postData = { - title: parsedMarkdown.data.title, - preview: "", - date: "", - tags: [], - toc: toc(parsedMarkdown.content).content, - } - - // content preview - // parsedMarkdown.excerpt is intentionally not used - // todo: fix potential improper closing of html tag - const slicedContent = parsedMarkdown.content.split(" ") - if (slicedContent.length > 19) { - postData.preview = slicedContent.slice(0, 19).join(" ") + " ..." - } else { - postData.preview = parsedMarkdown.content - } - - // date - const postDate = new Date(parsedMarkdown.data.date) - postData.date = postDate.toLocaleString("default", { - month: "short", - day: "numeric", - year: "numeric", - }) - - const YYYY_MM_DD = postDate.toISOString().split("T")[0] - if (map.date[YYYY_MM_DD]) { - map.date[YYYY_MM_DD].push(urlPath) - } else { - map.date[YYYY_MM_DD] = [urlPath] - } - - //tags - postData.tags = parsedMarkdown.data.tags - if (postData.tags) { - postData.tags.forEach((tag) => { - if (map.tags[tag]) { - map.tags[tag].push(urlPath) - } else { - map.tags[tag] = [urlPath] - } - }) - } - - map.posts[urlPath] = postData - index.addDoc({ - title: parsedMarkdown.data.title, - body: parsedMarkdown.content, - url: urlPath, - }) + while (n-- && i++ < str.length) { + i = str.indexOf(pat, i) + if (i < 0) break } + + return i } -function recursiveParseUnsearchable(fileOrFolderPath: string) { - // get string after the last slash character - const fileOrFolderName = path2FileOrFolderName(fileOrFolderPath) +function writeToJSON(JSONFilePath: string, dataToWrite: string) { + // create directory to put json content files + fs.mkdirSync(JSONFilePath.slice(0, JSONFilePath.lastIndexOf("/")), { + recursive: true, + }) - // ignore if file or directory name starts with a underscore - if (fileOrFolderName.startsWith("_")) return - - // illegal names - if ( - fileOrFolderPath == "./markdown/unsearchable/posts" || - fileOrFolderPath == "./markdown/unsearchable/series" + // write content to json file + fs.writeFileSync( + JSONFilePath, + JSON.stringify({ + content: dataToWrite.trim(), + }) ) - throw Error( - `Illegal name (posts/series) in path: "${fileOrFolderPath}".` - ) - - // get data about the given path - const stats = fs.lstatSync(fileOrFolderPath) - - // if it's a directory, call this function to every files/directories in it - // if it's a file, parse it and then save it to file - if (stats.isDirectory()) { - fs.readdirSync(fileOrFolderPath).map((childPath) => { - recursiveParseUnsearchable(`${fileOrFolderPath}/${childPath}`) - }) - } else if (stats.isFile()) { - // skip if it is not a markdown file - if (!fileOrFolderName.endsWith(".md")) { - console.log(`Ignoring non markdown file at: ${fileOrFolderPath}`) - return - } - - const _urlPath = path2URL(fileOrFolderPath) - const urlPath = _urlPath.slice( - _urlPath - .slice(1) // ignore the first slash - .indexOf("/") + 1 - ) - - // parse markdown metadata - const parsedMarkdown = matter(fs.readFileSync(fileOrFolderPath, "utf8")) - - if (!parsedMarkdown.data.title) { - throw Error(`Title is not defined in file: ${fileOrFolderPath}`) - } - - // urlPath starts with a slash - const contentFilePath = `${contentDirectoryPath}/unsearchable${urlPath}.json` - - // create directory to put json content files - fs.mkdirSync( - contentFilePath.slice(0, contentFilePath.lastIndexOf("/")), - { recursive: true } - ) - - // write content to json file - fs.writeFileSync( - contentFilePath, - JSON.stringify({ - content: parsedMarkdown.content.trim(), - }) - ) - - // Parse data that will be written to map.js - map.unsearchable[urlPath] = { - title: parsedMarkdown.data.title, - } - - index.addDoc({ - title: parsedMarkdown.data.title, - body: parsedMarkdown.content, - url: urlPath, - }) - } } -function recursiveParseSeries(fileOrFolderPath: string) { +// A recursive function that calls itself for every files and directories that it finds +function recursiveParse( + mode: "posts" | "series" | "unsearchable", + fileOrFolderPath: string +) { + if (mode == "unsearchable") { + // illegal names + if ( + fileOrFolderPath == "./markdown/unsearchable/posts" || + fileOrFolderPath == "./markdown/unsearchable/series" + ) + throw Error( + `Illegal name (posts/series) in path: "${fileOrFolderPath}".` + ) + } + // get string after the last slash character const fileOrFolderName = path2FileOrFolderName(fileOrFolderPath) @@ -312,7 +192,7 @@ function recursiveParseSeries(fileOrFolderPath: string) { // if it's a file, parse it and then save it to file if (stats.isDirectory()) { fs.readdirSync(fileOrFolderPath).map((childPath) => { - recursiveParseSeries(`${fileOrFolderPath}/${childPath}`) + recursiveParse(mode, `${fileOrFolderPath}/${childPath}`) }) } else if (stats.isFile()) { // skip if it is not a markdown file @@ -321,126 +201,217 @@ function recursiveParseSeries(fileOrFolderPath: string) { return } - if ( - !fileOrFolderName.includes("_") && - !fileOrFolderName.startsWith("0") - ) - throw Error(`Invalid series post file name at: ${fileOrFolderPath}`) + // read markdown file + const markdownRaw = fs.readFileSync(fileOrFolderPath, "utf8") // parse markdown metadata - const parsedMarkdown = matter(fs.readFileSync(fileOrFolderPath, "utf8")) + const markdownData = matter( + markdownRaw.slice(0, nthIndex(markdownRaw, "---", 2) + 3) + ).data - if (!parsedMarkdown.data.title) { + if (!markdownData.title) throw Error(`Title is not defined in file: ${fileOrFolderPath}`) - } - if (!parsedMarkdown.data.date) { - throw Error(`Date is not defined in file: ${fileOrFolderPath}`) - } + markdownData.content = + md.render(markdownRaw.slice(nthIndex(markdownRaw, "---", 2) + 3)) || + "" - // path that will be used as site url - let urlPath = path2URL(fileOrFolderPath) - urlPath = urlPath.slice(0, urlPath.lastIndexOf("_")) - urlPath = urlPath.replace(/\/$/, "") // remove trailing slash + if (mode == "posts") { + if (!markdownData.date) { + throw Error(`Date is not defined in file: ${fileOrFolderPath}`) + } - // urlPath starts with a slash - const contentFilePath = `${contentDirectoryPath}${urlPath}.json` + // path that will be used as site url (starts with a slash) + const urlPath = path2URL(fileOrFolderPath) - // create directory to put json content files - fs.mkdirSync( - contentFilePath.slice(0, contentFilePath.lastIndexOf("/")), - { recursive: true } - ) + writeToJSON( + `${contentDirectoryPath}${urlPath}.json`, + markdownData.content + ) - // write content to json file - fs.writeFileSync( - contentFilePath, - JSON.stringify({ - content: parsedMarkdown.content.trim(), + // Parse data that will be written to map.js + const postData = { + title: markdownData.title, + preview: "", + date: "", + tags: [], + toc: toc(markdownData.content).content, + } + + // content preview + // parsedMarkdown.excerpt is intentionally not used + // todo: fix potential improper closing of html tag + const slicedContent = markdownData.content.split(" ") + if (slicedContent.length > 19) { + postData.preview = slicedContent.slice(0, 19).join(" ") + " ..." + } else { + postData.preview = markdownData.content + } + + // date + const postDate = new Date(markdownData.date) + postData.date = postDate.toLocaleString("default", { + month: "short", + day: "numeric", + year: "numeric", }) - ) - // Parse data that will be written to map.js - const postData = { - title: parsedMarkdown.data.title, - preview: "", - date: "", - tags: [], - toc: toc(parsedMarkdown.content).content, - } + const YYYY_MM_DD = postDate.toISOString().split("T")[0] + if (map.date[YYYY_MM_DD]) { + map.date[YYYY_MM_DD].push(urlPath) + } else { + map.date[YYYY_MM_DD] = [urlPath] + } - // content preview - // parsedMarkdown.excerpt is intentionally not used - // todo: fix potential improper closing of html tag - const slicedContent = parsedMarkdown.content.split(" ") - if (slicedContent.length > 19) { - postData.preview = slicedContent.slice(0, 19).join(" ") + " ..." - } else { - postData.preview = parsedMarkdown.content - } + //tags + postData.tags = markdownData.tags + if (postData.tags) { + postData.tags.forEach((tag) => { + if (map.tags[tag]) { + map.tags[tag].push(urlPath) + } else { + map.tags[tag] = [urlPath] + } + }) + } - // date - const postDate = new Date(parsedMarkdown.data.date) - postData.date = postDate.toLocaleString("default", { - month: "short", - day: "numeric", - year: "numeric", - }) - - const YYYY_MM_DD = postDate.toISOString().split("T")[0] - if (map.date[YYYY_MM_DD]) { - map.date[YYYY_MM_DD].push(urlPath) - } else { - map.date[YYYY_MM_DD] = [urlPath] - } - - //tags - postData.tags = parsedMarkdown.data.tags - if (postData.tags) { - postData.tags.forEach((tag) => { - if (map.tags[tag]) { - map.tags[tag].push(urlPath) - } else { - map.tags[tag] = [urlPath] - } - }) - } - - if (fileOrFolderName.startsWith("0")) { - map.series[urlPath] = { ...postData, order: [], length: 0 } - } else { map.posts[urlPath] = postData index.addDoc({ - title: parsedMarkdown.data.title, - body: parsedMarkdown.content, + title: markdownData.title, + body: markdownData.content, url: urlPath, }) - for (const key of Object.keys(map.series)) { - if (urlPath.slice(0, urlPath.lastIndexOf("/")).includes(key)) { - const index = parseInt( - fileOrFolderName.slice( - 0, - fileOrFolderName.lastIndexOf("_") - ) - ) + } else if (mode == "unsearchable") { + // path that will be used as site url (starts with a slash) + const _urlPath = path2URL(fileOrFolderPath) + const urlPath = _urlPath.slice( + _urlPath + .slice(1) // ignore the first slash + .indexOf("/") + 1 + ) - if (isNaN(index)) { - throw Error( - `Invalid series index at: ${fileOrFolderPath}` - ) - } + writeToJSON( + `${contentDirectoryPath}/unsearchable${urlPath}.json`, + markdownData.content + ) - const itemToPush = { - index: index, - url: urlPath, - } + // Parse data that will be written to map.js + map.unsearchable[urlPath] = { + title: markdownData.title, + } - if (seriesMap[key]) { - seriesMap[key].push(itemToPush) + index.addDoc({ + title: markdownData.title, + body: markdownData.content, + url: urlPath, + }) + } else if (mode == "series") { + if ( + !fileOrFolderName.includes("_") && + !fileOrFolderName.startsWith("0") + ) + throw Error( + `Invalid series post file name at: ${fileOrFolderPath}` + ) + + if (!markdownData.date) { + throw Error(`Date is not defined in file: ${fileOrFolderPath}`) + } + + // path that will be used as site url (starts with a slash) + let urlPath = path2URL(fileOrFolderPath) + urlPath = urlPath.slice(0, urlPath.lastIndexOf("_")) + urlPath = urlPath.replace(/\/$/, "") // remove trailing slash + + writeToJSON( + `${contentDirectoryPath}${urlPath}.json`, + markdownData.content + ) + + // Parse data that will be written to map.js + const postData = { + title: markdownData.title, + preview: "", + date: "", + tags: [], + toc: toc(markdownData.content).content, + } + + // content preview + // parsedMarkdown.excerpt is intentionally not used + // todo: fix potential improper closing of html tag + const slicedContent = markdownData.content.split(" ") + if (slicedContent.length > 19) { + postData.preview = slicedContent.slice(0, 19).join(" ") + " ..." + } else { + postData.preview = markdownData.content + } + + // date + const postDate = new Date(markdownData.date) + postData.date = postDate.toLocaleString("default", { + month: "short", + day: "numeric", + year: "numeric", + }) + + const YYYY_MM_DD = postDate.toISOString().split("T")[0] + if (map.date[YYYY_MM_DD]) { + map.date[YYYY_MM_DD].push(urlPath) + } else { + map.date[YYYY_MM_DD] = [urlPath] + } + + //tags + postData.tags = markdownData.tags + if (postData.tags) { + postData.tags.forEach((tag) => { + if (map.tags[tag]) { + map.tags[tag].push(urlPath) } else { - seriesMap[key] = [itemToPush] + map.tags[tag] = [urlPath] + } + }) + } + + if (fileOrFolderName.startsWith("0")) { + map.series[urlPath] = { ...postData, order: [], length: 0 } + } else { + map.posts[urlPath] = postData + index.addDoc({ + title: markdownData.title, + body: markdownData.content, + url: urlPath, + }) + for (const key of Object.keys(map.series)) { + if ( + urlPath.slice(0, urlPath.lastIndexOf("/")).includes(key) + ) { + const index = parseInt( + fileOrFolderName.slice( + 0, + fileOrFolderName.lastIndexOf("_") + ) + ) + + if (isNaN(index)) { + throw Error( + `Invalid series index at: ${fileOrFolderPath}` + ) + } + + const itemToPush = { + index: index, + url: urlPath, + } + + if (seriesMap[key]) { + seriesMap[key].push(itemToPush) + } else { + seriesMap[key] = [itemToPush] + } + break } - break } } } @@ -472,9 +443,9 @@ if (!fs.lstatSync(markdownPath + "/unsearchable").isDirectory()) if (!fs.lstatSync(markdownPath + "/series").isDirectory()) throw Error(`Cannot find directory: ${markdownPath + "/posts"}`) -recursiveParsePosts(markdownPath + "/posts") -recursiveParseUnsearchable(markdownPath + "/unsearchable") -recursiveParseSeries(markdownPath + "/series") +recursiveParse("posts", markdownPath + "/posts") +recursiveParse("unsearchable", markdownPath + "/unsearchable") +recursiveParse("series", markdownPath + "/series") // sort dates let dateKeys: string[] = [] diff --git a/source/markdown/posts/test post.md b/source/markdown/posts/test post.md index 25b659b..e574d90 100644 --- a/source/markdown/posts/test post.md +++ b/source/markdown/posts/test post.md @@ -41,11 +41,11 @@ A post have title, post date, tags, and content. ## Code -`code`
+Here's a `code`. ```python if __name__ == "__main__": - print("code block") + print("And here's a code block") # with comments! ``` ## Etc @@ -58,7 +58,7 @@ if __name__ == "__main__": _italic_
~~strikethrough~~ -## HTML +## Styling

centered paragraph @@ -67,3 +67,7 @@ _italic_
## Key Ctrl+C + +## Mathematical expression + +$e=mc^2$ is actually $e^2=(mc^2)^2 + (pc)^2$. diff --git a/source/package.json b/source/package.json index ac959fd..2450f2a 100644 --- a/source/package.json +++ b/source/package.json @@ -22,10 +22,15 @@ "@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/react-fontawesome": "^0.1.14", "@types/elasticlunr": "^0.9.2", + "@types/highlight.js": "^10.1.0", "date-fns": "^2.23.0", "elasticlunr": "^0.9.5", "gray-matter": "^4.0.3", + "highlight.js": "^11.2.0", + "katex": "^0.13.13", "local-storage-fallback": "^4.1.2", + "markdown-it": "^12.2.0", + "markdown-it-texmath": "^0.9.1", "markdown-toc": "^1.2.0", "marked": "^2.1.3", "query-string": "^7.0.1", diff --git a/source/src/App.tsx b/source/src/App.tsx index e434843..8df405c 100644 --- a/source/src/App.tsx +++ b/source/src/App.tsx @@ -5,6 +5,9 @@ import { HelmetProvider } from "react-helmet-async" import storage from "local-storage-fallback" import { isIE } from "react-device-detect" +import "highlight.js/styles/github-dark-dimmed.css" +import "katex/dist/katex.min.css" + import theming from "./theming" import Spinner from "./components/Spinner" @@ -70,6 +73,35 @@ body::-webkit-scrollbar-thumb { code { font-family: ${theming.font.code}; + color: ${(props) => + theming.theme(props.theme.currentTheme, { + light: theming.light.color1, + dark: theming.dark.color1, + })}; + background-color: ${(props) => + theming.theme(props.theme.currentTheme, { + light: "#eee", + dark: "#555", + })}; + border-radius: 3px; + padding: 0 3px; +} + +/* https://stackoverflow.com/a/48694906/12979111 */ +pre > code { + font-family: ${theming.font.code}; + color: #adbac7; + background-color: #22272e; + border: 1px solid #ddd; + page-break-inside: avoid; + font-size: 15px; + line-height: 1.6; + margin-bottom: 1.6em; + max-width: 100%; + overflow: auto; + padding: 1em 1.5em; + display: block; + word-wrap: break-word; } /* https://www.rgagnon.com/jsdetails/js-nice-effect-the-KBD-tag.html */ diff --git a/source/yarn.lock b/source/yarn.lock index 7096af2..4a9972a 100644 --- a/source/yarn.lock +++ b/source/yarn.lock @@ -1833,6 +1833,13 @@ dependencies: "@types/node" "*" +"@types/highlight.js@^10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-10.1.0.tgz#89bb0c202997d7a90a07bd2ec1f7d00c56bb90b4" + integrity sha512-77hF2dGBsOgnvZll1vymYiNUtqJ8cJfXPD6GG/2M0aLRc29PkvB7Au6sIDjIEFcSICBhCh2+Pyq6WSRS7LUm6A== + dependencies: + highlight.js "*" + "@types/hoist-non-react-statics@*": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" @@ -2545,6 +2552,11 @@ argparse@^1.0.10, argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" @@ -3633,6 +3645,11 @@ commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -4608,6 +4625,11 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -5913,6 +5935,11 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +highlight.js@*, highlight.js@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.2.0.tgz#a7e3b8c1fdc4f0538b93b2dc2ddd53a40c6ab0f0" + integrity sha512-JOySjtOEcyG8s4MLR2MNbLUyaXqUunmSnL2kdV/KuGJOmHZuAR5xC54Ko7goAXBWNhf09Vy3B+U7vR62UZ/0iw== + history@^4.9.0: version "4.10.1" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" @@ -7296,6 +7323,13 @@ jsprim@^1.2.2: array-includes "^3.1.2" object.assign "^4.1.2" +katex@^0.13.13: + version "0.13.13" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.13.tgz#15a796e95516869bc6d483443b58b2df872ee40f" + integrity sha512-cCMcil4jwMm7behpXGiQfXJA29sko/Gd/26iCsr53Dv5Jn2iHbHyEb14dm9uVrIijUXx6Zz1WhlFhHE6DckvkQ== + dependencies: + commander "^6.0.0" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -7388,6 +7422,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +linkify-it@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8" + integrity sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ== + dependencies: + uc.micro "^1.0.1" + list-item@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/list-item/-/list-item-1.1.1.tgz#0c65d00e287cb663ccb3cb3849a77e89ec268a56" @@ -7604,6 +7645,22 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markdown-it-texmath@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/markdown-it-texmath/-/markdown-it-texmath-0.9.1.tgz#fdf1442afbca474e9170b9707b6155125384bae2" + integrity sha512-hRA5KQcgBJf5q3qDBcui4s/VkzjGC/l1ILviLNrdW22cT9JXHKdEA2ZTk1RRtFuiif2AbrbiMEoEwV6tBFwiEw== + +markdown-it@^12.2.0: + version "12.2.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.2.0.tgz#091f720fd5db206f80de7a8d1f1a7035fd0d38db" + integrity sha512-Wjws+uCrVQRqOoJvze4HCqkKl1AsSh95iFAeQDwnyfxM09divCBSXlDR1uTvyUP3Grzpn4Ru8GeCxYPM8vkCQg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + markdown-link@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/markdown-link/-/markdown-link-0.1.1.tgz#32c5c65199a6457316322d1e4229d13407c8c7cf" @@ -7656,6 +7713,11 @@ mdn-data@2.0.4: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -11547,6 +11609,11 @@ ua-parser-js@^0.7.24: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + unbox-primitive@^1.0.0, unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"