diff --git a/.eslintignore b/.eslintignore index e454775f7c..14b2242beb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,6 +5,7 @@ src/generated/* src/server/chromium/protocol.ts src/server/firefox/protocol.ts src/server/webkit/protocol.ts +src/third_party/ /types/* /index.d.ts utils/generate_types/overrides.d.ts diff --git a/package-lock.json b/package-lock.json index 298593d44e..d733fb470e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4042,11 +4042,6 @@ "minimalistic-assert": "^1.0.1" } }, - "highlight.js": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.4.1.tgz", - "integrity": "sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==" - }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", diff --git a/package.json b/package.json index 594e352664..46f8fd1b20 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "commander": "^6.1.0", "debug": "^4.1.1", "extract-zip": "^2.0.1", - "highlight.js": "^10.1.2", "https-proxy-agent": "^5.0.0", "jpeg-js": "^0.4.2", "mime": "^2.4.6", diff --git a/src/cli/codegen/outputs.ts b/src/cli/codegen/outputs.ts index 4b2094ae03..5a899b36e6 100644 --- a/src/cli/codegen/outputs.ts +++ b/src/cli/codegen/outputs.ts @@ -17,7 +17,7 @@ import * as fs from 'fs'; import * as querystring from 'querystring'; import { Writable } from 'stream'; -import { highlight } from 'highlight.js'; +import * as hljs from '../../third_party/highlightjs/highlightjs'; import { CodeGeneratorOutput } from './codeGenerator'; export class OutputMultiplexer implements CodeGeneratorOutput { @@ -73,7 +73,7 @@ export class TerminalOutput implements CodeGeneratorOutput { } private _highlight(text: string) { - let highlightedCode = highlight(this._language, text).value; + let highlightedCode = hljs.highlight(this._language, text).value; highlightedCode = querystring.unescape(highlightedCode); highlightedCode = highlightedCode.replace(//g, '\x1b[38;5;205m'); highlightedCode = highlightedCode.replace(//g, '\x1b[38;5;220m'); diff --git a/src/third_party/highlightjs/README.md b/src/third_party/highlightjs/README.md new file mode 100644 index 0000000000..5f79f06cb2 --- /dev/null +++ b/src/third_party/highlightjs/README.md @@ -0,0 +1,4 @@ +# Highlight.js + +This is a small build of [highlight.js](https://github.com/highlightjs/highlight.js). +Pick a [stable release](https://github.com/highlightjs/highlight.js/releases) revision and use `roll.sh` to update. diff --git a/src/third_party/highlightjs/highlightjs/LICENSE b/src/third_party/highlightjs/highlightjs/LICENSE new file mode 100644 index 0000000000..2250cc7eca --- /dev/null +++ b/src/third_party/highlightjs/highlightjs/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2006, Ivan Sagalaev. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/third_party/highlightjs/highlightjs/core.js b/src/third_party/highlightjs/highlightjs/core.js new file mode 100644 index 0000000000..59d131b1f3 --- /dev/null +++ b/src/third_party/highlightjs/highlightjs/core.js @@ -0,0 +1,2378 @@ +function deepFreeze(obj) { + if (obj instanceof Map) { + obj.clear = obj.delete = obj.set = function () { + throw new Error('map is read-only'); + }; + } else if (obj instanceof Set) { + obj.add = obj.clear = obj.delete = function () { + throw new Error('set is read-only'); + }; + } + + // Freeze self + Object.freeze(obj); + + Object.getOwnPropertyNames(obj).forEach(function (name) { + var prop = obj[name]; + + // Freeze prop if it is an object + if (typeof prop == 'object' && !Object.isFrozen(prop)) { + deepFreeze(prop); + } + }); + + return obj; +} + +var deepFreezeEs6 = deepFreeze; +var _default = deepFreeze; +deepFreezeEs6.default = _default; + +class Response { + /** + * @param {CompiledMode} mode + */ + constructor(mode) { + // eslint-disable-next-line no-undefined + if (mode.data === undefined) mode.data = {}; + + this.data = mode.data; + } + + ignoreMatch() { + this.ignore = true; + } +} + +/** + * @param {string} value + * @returns {string} + */ +function escapeHTML(value) { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * performs a shallow merge of multiple objects into one + * + * @template T + * @param {T} original + * @param {Record[]} objects + * @returns {T} a single new object + */ +function inherit(original, ...objects) { + /** @type Record */ + const result = Object.create(null); + + for (const key in original) { + result[key] = original[key]; + } + objects.forEach(function(obj) { + for (const key in obj) { + result[key] = obj[key]; + } + }); + return /** @type {T} */ (result); +} + +/** + * @typedef {object} Renderer + * @property {(text: string) => void} addText + * @property {(node: Node) => void} openNode + * @property {(node: Node) => void} closeNode + * @property {() => string} value + */ + +/** @typedef {{kind?: string, sublanguage?: boolean}} Node */ +/** @typedef {{walk: (r: Renderer) => void}} Tree */ +/** */ + +const SPAN_CLOSE = ''; + +/** + * Determines if a node needs to be wrapped in + * + * @param {Node} node */ +const emitsWrappingTags = (node) => { + return !!node.kind; +}; + +/** @type {Renderer} */ +class HTMLRenderer { + /** + * Creates a new HTMLRenderer + * + * @param {Tree} parseTree - the parse tree (must support `walk` API) + * @param {{classPrefix: string}} options + */ + constructor(parseTree, options) { + this.buffer = ""; + this.classPrefix = options.classPrefix; + parseTree.walk(this); + } + + /** + * Adds texts to the output stream + * + * @param {string} text */ + addText(text) { + this.buffer += escapeHTML(text); + } + + /** + * Adds a node open to the output stream (if needed) + * + * @param {Node} node */ + openNode(node) { + if (!emitsWrappingTags(node)) return; + + let className = node.kind; + if (!node.sublanguage) { + className = `${this.classPrefix}${className}`; + } + this.span(className); + } + + /** + * Adds a node close to the output stream (if needed) + * + * @param {Node} node */ + closeNode(node) { + if (!emitsWrappingTags(node)) return; + + this.buffer += SPAN_CLOSE; + } + + /** + * returns the accumulated buffer + */ + value() { + return this.buffer; + } + + // helpers + + /** + * Builds a span element + * + * @param {string} className */ + span(className) { + this.buffer += ``; + } +} + +/** @typedef {{kind?: string, sublanguage?: boolean, children: Node[]} | string} Node */ +/** @typedef {{kind?: string, sublanguage?: boolean, children: Node[]} } DataNode */ +/** */ + +class TokenTree { + constructor() { + /** @type DataNode */ + this.rootNode = { children: [] }; + this.stack = [this.rootNode]; + } + + get top() { + return this.stack[this.stack.length - 1]; + } + + get root() { return this.rootNode; } + + /** @param {Node} node */ + add(node) { + this.top.children.push(node); + } + + /** @param {string} kind */ + openNode(kind) { + /** @type Node */ + const node = { kind, children: [] }; + this.add(node); + this.stack.push(node); + } + + closeNode() { + if (this.stack.length > 1) { + return this.stack.pop(); + } + // eslint-disable-next-line no-undefined + return undefined; + } + + closeAllNodes() { + while (this.closeNode()); + } + + toJSON() { + return JSON.stringify(this.rootNode, null, 4); + } + + /** + * @typedef { import("./html_renderer").Renderer } Renderer + * @param {Renderer} builder + */ + walk(builder) { + // this does not + return this.constructor._walk(builder, this.rootNode); + // this works + // return TokenTree._walk(builder, this.rootNode); + } + + /** + * @param {Renderer} builder + * @param {Node} node + */ + static _walk(builder, node) { + if (typeof node === "string") { + builder.addText(node); + } else if (node.children) { + builder.openNode(node); + node.children.forEach((child) => this._walk(builder, child)); + builder.closeNode(node); + } + return builder; + } + + /** + * @param {Node} node + */ + static _collapse(node) { + if (typeof node === "string") return; + if (!node.children) return; + + if (node.children.every(el => typeof el === "string")) { + // node.text = node.children.join(""); + // delete node.children; + node.children = [node.children.join("")]; + } else { + node.children.forEach((child) => { + TokenTree._collapse(child); + }); + } + } +} + +/** + Currently this is all private API, but this is the minimal API necessary + that an Emitter must implement to fully support the parser. + + Minimal interface: + + - addKeyword(text, kind) + - addText(text) + - addSublanguage(emitter, subLanguageName) + - finalize() + - openNode(kind) + - closeNode() + - closeAllNodes() + - toHTML() + +*/ + +/** + * @implements {Emitter} + */ +class TokenTreeEmitter extends TokenTree { + /** + * @param {*} options + */ + constructor(options) { + super(); + this.options = options; + } + + /** + * @param {string} text + * @param {string} kind + */ + addKeyword(text, kind) { + if (text === "") { return; } + + this.openNode(kind); + this.addText(text); + this.closeNode(); + } + + /** + * @param {string} text + */ + addText(text) { + if (text === "") { return; } + + this.add(text); + } + + /** + * @param {Emitter & {root: DataNode}} emitter + * @param {string} name + */ + addSublanguage(emitter, name) { + /** @type DataNode */ + const node = emitter.root; + node.kind = name; + node.sublanguage = true; + this.add(node); + } + + toHTML() { + const renderer = new HTMLRenderer(this, this.options); + return renderer.value(); + } + + finalize() { + return true; + } +} + +/** + * @param {string} value + * @returns {RegExp} + * */ +function escape(value) { + return new RegExp(value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'm'); +} + +/** + * @param {RegExp | string } re + * @returns {string} + */ +function source(re) { + if (!re) return null; + if (typeof re === "string") return re; + + return re.source; +} + +/** + * @param {...(RegExp | string) } args + * @returns {string} + */ +function concat(...args) { + const joined = args.map((x) => source(x)).join(""); + return joined; +} + +/** + * Any of the passed expresssions may match + * + * Creates a huge this | this | that | that match + * @param {(RegExp | string)[] } args + * @returns {string} + */ +function either(...args) { + const joined = '(' + args.map((x) => source(x)).join("|") + ")"; + return joined; +} + +/** + * @param {RegExp} re + * @returns {number} + */ +function countMatchGroups(re) { + return (new RegExp(re.toString() + '|')).exec('').length - 1; +} + +/** + * Does lexeme start with a regular expression match at the beginning + * @param {RegExp} re + * @param {string} lexeme + */ +function startsWith(re, lexeme) { + const match = re && re.exec(lexeme); + return match && match.index === 0; +} + +// join logically computes regexps.join(separator), but fixes the +// backreferences so they continue to match. +// it also places each individual regular expression into it's own +// match group, keeping track of the sequencing of those match groups +// is currently an exercise for the caller. :-) +/** + * @param {(string | RegExp)[]} regexps + * @param {string} separator + * @returns {string} + */ +function join(regexps, separator = "|") { + // backreferenceRe matches an open parenthesis or backreference. To avoid + // an incorrect parse, it additionally matches the following: + // - [...] elements, where the meaning of parentheses and escapes change + // - other escape sequences, so we do not misparse escape sequences as + // interesting elements + // - non-matching or lookahead parentheses, which do not capture. These + // follow the '(' with a '?'. + const backreferenceRe = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./; + let numCaptures = 0; + let ret = ''; + for (let i = 0; i < regexps.length; i++) { + numCaptures += 1; + const offset = numCaptures; + let re = source(regexps[i]); + if (i > 0) { + ret += separator; + } + ret += "("; + while (re.length > 0) { + const match = backreferenceRe.exec(re); + if (match == null) { + ret += re; + break; + } + ret += re.substring(0, match.index); + re = re.substring(match.index + match[0].length); + if (match[0][0] === '\\' && match[1]) { + // Adjust the backreference. + ret += '\\' + String(Number(match[1]) + offset); + } else { + ret += match[0]; + if (match[0] === '(') { + numCaptures++; + } + } + } + ret += ")"; + } + return ret; +} + +// Common regexps +const IDENT_RE = '[a-zA-Z]\\w*'; +const UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*'; +const NUMBER_RE = '\\b\\d+(\\.\\d+)?'; +const C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float +const BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b... +const RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~'; + +/** +* @param { Partial & {binary?: string | RegExp} } opts +*/ +const SHEBANG = (opts = {}) => { + const beginShebang = /^#![ ]*\//; + if (opts.binary) { + opts.begin = concat( + beginShebang, + /.*\b/, + opts.binary, + /\b.*/); + } + return inherit({ + className: 'meta', + begin: beginShebang, + end: /$/, + relevance: 0, + /** @type {ModeCallback} */ + "on:begin": (m, resp) => { + if (m.index !== 0) resp.ignoreMatch(); + } + }, opts); +}; + +// Common modes +const BACKSLASH_ESCAPE = { + begin: '\\\\[\\s\\S]', relevance: 0 +}; +const APOS_STRING_MODE = { + className: 'string', + begin: '\'', + end: '\'', + illegal: '\\n', + contains: [BACKSLASH_ESCAPE] +}; +const QUOTE_STRING_MODE = { + className: 'string', + begin: '"', + end: '"', + illegal: '\\n', + contains: [BACKSLASH_ESCAPE] +}; +const PHRASAL_WORDS_MODE = { + begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +}; +/** + * Creates a comment mode + * + * @param {string | RegExp} begin + * @param {string | RegExp} end + * @param {Mode | {}} [modeOptions] + * @returns {Partial} + */ +const COMMENT = function(begin, end, modeOptions = {}) { + const mode = inherit( + { + className: 'comment', + begin, + end, + contains: [] + }, + modeOptions + ); + mode.contains.push(PHRASAL_WORDS_MODE); + mode.contains.push({ + className: 'doctag', + begin: '(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):', + relevance: 0 + }); + return mode; +}; +const C_LINE_COMMENT_MODE = COMMENT('//', '$'); +const C_BLOCK_COMMENT_MODE = COMMENT('/\\*', '\\*/'); +const HASH_COMMENT_MODE = COMMENT('#', '$'); +const NUMBER_MODE = { + className: 'number', + begin: NUMBER_RE, + relevance: 0 +}; +const C_NUMBER_MODE = { + className: 'number', + begin: C_NUMBER_RE, + relevance: 0 +}; +const BINARY_NUMBER_MODE = { + className: 'number', + begin: BINARY_NUMBER_RE, + relevance: 0 +}; +const CSS_NUMBER_MODE = { + className: 'number', + begin: NUMBER_RE + '(' + + '%|em|ex|ch|rem' + + '|vw|vh|vmin|vmax' + + '|cm|mm|in|pt|pc|px' + + '|deg|grad|rad|turn' + + '|s|ms' + + '|Hz|kHz' + + '|dpi|dpcm|dppx' + + ')?', + relevance: 0 +}; +const REGEXP_MODE = { + // this outer rule makes sure we actually have a WHOLE regex and not simply + // an expression such as: + // + // 3 / something + // + // (which will then blow up when regex's `illegal` sees the newline) + begin: /(?=\/[^/\n]*\/)/, + contains: [{ + className: 'regexp', + begin: /\//, + end: /\/[gimuy]*/, + illegal: /\n/, + contains: [ + BACKSLASH_ESCAPE, + { + begin: /\[/, + end: /\]/, + relevance: 0, + contains: [BACKSLASH_ESCAPE] + } + ] + }] +}; +const TITLE_MODE = { + className: 'title', + begin: IDENT_RE, + relevance: 0 +}; +const UNDERSCORE_TITLE_MODE = { + className: 'title', + begin: UNDERSCORE_IDENT_RE, + relevance: 0 +}; +const METHOD_GUARD = { + // excludes method names from keyword processing + begin: '\\.\\s*' + UNDERSCORE_IDENT_RE, + relevance: 0 +}; + +/** + * Adds end same as begin mechanics to a mode + * + * Your mode must include at least a single () match group as that first match + * group is what is used for comparison + * @param {Partial} mode + */ +const END_SAME_AS_BEGIN = function(mode) { + return Object.assign(mode, + { + /** @type {ModeCallback} */ + 'on:begin': (m, resp) => { resp.data._beginMatch = m[1]; }, + /** @type {ModeCallback} */ + 'on:end': (m, resp) => { if (resp.data._beginMatch !== m[1]) resp.ignoreMatch(); } + }); +}; + +var MODES = /*#__PURE__*/Object.freeze({ + __proto__: null, + IDENT_RE: IDENT_RE, + UNDERSCORE_IDENT_RE: UNDERSCORE_IDENT_RE, + NUMBER_RE: NUMBER_RE, + C_NUMBER_RE: C_NUMBER_RE, + BINARY_NUMBER_RE: BINARY_NUMBER_RE, + RE_STARTERS_RE: RE_STARTERS_RE, + SHEBANG: SHEBANG, + BACKSLASH_ESCAPE: BACKSLASH_ESCAPE, + APOS_STRING_MODE: APOS_STRING_MODE, + QUOTE_STRING_MODE: QUOTE_STRING_MODE, + PHRASAL_WORDS_MODE: PHRASAL_WORDS_MODE, + COMMENT: COMMENT, + C_LINE_COMMENT_MODE: C_LINE_COMMENT_MODE, + C_BLOCK_COMMENT_MODE: C_BLOCK_COMMENT_MODE, + HASH_COMMENT_MODE: HASH_COMMENT_MODE, + NUMBER_MODE: NUMBER_MODE, + C_NUMBER_MODE: C_NUMBER_MODE, + BINARY_NUMBER_MODE: BINARY_NUMBER_MODE, + CSS_NUMBER_MODE: CSS_NUMBER_MODE, + REGEXP_MODE: REGEXP_MODE, + TITLE_MODE: TITLE_MODE, + UNDERSCORE_TITLE_MODE: UNDERSCORE_TITLE_MODE, + METHOD_GUARD: METHOD_GUARD, + END_SAME_AS_BEGIN: END_SAME_AS_BEGIN +}); + +// Grammar extensions / plugins +// See: https://github.com/highlightjs/highlight.js/issues/2833 + +// Grammar extensions allow "syntactic sugar" to be added to the grammar modes +// without requiring any underlying changes to the compiler internals. + +// `compileMatch` being the perfect small example of now allowing a grammar +// author to write `match` when they desire to match a single expression rather +// than being forced to use `begin`. The extension then just moves `match` into +// `begin` when it runs. Ie, no features have been added, but we've just made +// the experience of writing (and reading grammars) a little bit nicer. + +// ------ + +// TODO: We need negative look-behind support to do this properly +/** + * Skip a match if it has a preceding dot + * + * This is used for `beginKeywords` to prevent matching expressions such as + * `bob.keyword.do()`. The mode compiler automatically wires this up as a + * special _internal_ 'on:begin' callback for modes with `beginKeywords` + * @param {RegExpMatchArray} match + * @param {CallbackResponse} response + */ +function skipIfhasPrecedingDot(match, response) { + const before = match.input[match.index - 1]; + if (before === ".") { + response.ignoreMatch(); + } +} + + +/** + * `beginKeywords` syntactic sugar + * @type {CompilerExt} + */ +function beginKeywords(mode, parent) { + if (!parent) return; + if (!mode.beginKeywords) return; + + // for languages with keywords that include non-word characters checking for + // a word boundary is not sufficient, so instead we check for a word boundary + // or whitespace - this does no harm in any case since our keyword engine + // doesn't allow spaces in keywords anyways and we still check for the boundary + // first + mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')(?!\\.)(?=\\b|\\s)'; + mode.__beforeBegin = skipIfhasPrecedingDot; + mode.keywords = mode.keywords || mode.beginKeywords; + delete mode.beginKeywords; +} + +/** + * Allow `illegal` to contain an array of illegal values + * @type {CompilerExt} + */ +function compileIllegal(mode, _parent) { + if (!Array.isArray(mode.illegal)) return; + + mode.illegal = either(...mode.illegal); +} + +/** + * `match` to match a single expression for readability + * @type {CompilerExt} + */ +function compileMatch(mode, _parent) { + if (!mode.match) return; + if (mode.begin || mode.end) throw new Error("begin & end are not supported with match"); + + mode.begin = mode.match; + delete mode.match; +} + +/** + * provides the default 1 relevance to all modes + * @type {CompilerExt} + */ +function compileRelevance(mode, _parent) { + // eslint-disable-next-line no-undefined + if (mode.relevance === undefined) mode.relevance = 1; +} + +// keywords that should have no default relevance value +const COMMON_KEYWORDS = [ + 'of', + 'and', + 'for', + 'in', + 'not', + 'or', + 'if', + 'then', + 'parent', // common variable name + 'list', // common variable name + 'value' // common variable name +]; + +/** + * Given raw keywords from a language definition, compile them. + * + * @param {string | Record} rawKeywords + * @param {boolean} caseInsensitive + */ +function compileKeywords(rawKeywords, caseInsensitive) { + /** @type KeywordDict */ + const compiledKeywords = {}; + + if (typeof rawKeywords === 'string') { // string + splitAndCompile('keyword', rawKeywords); + } else { + Object.keys(rawKeywords).forEach(function(className) { + splitAndCompile(className, rawKeywords[className]); + }); + } + return compiledKeywords; + + // --- + + /** + * Compiles an individual list of keywords + * + * Ex: "for if when while|5" + * + * @param {string} className + * @param {string} keywordList + */ + function splitAndCompile(className, keywordList) { + if (caseInsensitive) { + keywordList = keywordList.toLowerCase(); + } + keywordList.split(' ').forEach(function(keyword) { + const pair = keyword.split('|'); + compiledKeywords[pair[0]] = [className, scoreForKeyword(pair[0], pair[1])]; + }); + } +} + +/** + * Returns the proper score for a given keyword + * + * Also takes into account comment keywords, which will be scored 0 UNLESS + * another score has been manually assigned. + * @param {string} keyword + * @param {string} [providedScore] + */ +function scoreForKeyword(keyword, providedScore) { + // manual scores always win over common keywords + // so you can force a score of 1 if you really insist + if (providedScore) { + return Number(providedScore); + } + + return commonKeyword(keyword) ? 0 : 1; +} + +/** + * Determines if a given keyword is common or not + * + * @param {string} keyword */ +function commonKeyword(keyword) { + return COMMON_KEYWORDS.includes(keyword.toLowerCase()); +} + +// compilation + +/** + * Compiles a language definition result + * + * Given the raw result of a language definition (Language), compiles this so + * that it is ready for highlighting code. + * @param {Language} language + * @param {{plugins: HLJSPlugin[]}} opts + * @returns {CompiledLanguage} + */ +function compileLanguage(language, { plugins }) { + /** + * Builds a regex with the case sensativility of the current language + * + * @param {RegExp | string} value + * @param {boolean} [global] + */ + function langRe(value, global) { + return new RegExp( + source(value), + 'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '') + ); + } + + /** + Stores multiple regular expressions and allows you to quickly search for + them all in a string simultaneously - returning the first match. It does + this by creating a huge (a|b|c) regex - each individual item wrapped with () + and joined by `|` - using match groups to track position. When a match is + found checking which position in the array has content allows us to figure + out which of the original regexes / match groups triggered the match. + + The match object itself (the result of `Regex.exec`) is returned but also + enhanced by merging in any meta-data that was registered with the regex. + This is how we keep track of which mode matched, and what type of rule + (`illegal`, `begin`, end, etc). + */ + class MultiRegex { + constructor() { + this.matchIndexes = {}; + // @ts-ignore + this.regexes = []; + this.matchAt = 1; + this.position = 0; + } + + // @ts-ignore + addRule(re, opts) { + opts.position = this.position++; + // @ts-ignore + this.matchIndexes[this.matchAt] = opts; + this.regexes.push([opts, re]); + this.matchAt += countMatchGroups(re) + 1; + } + + compile() { + if (this.regexes.length === 0) { + // avoids the need to check length every time exec is called + // @ts-ignore + this.exec = () => null; + } + const terminators = this.regexes.map(el => el[1]); + this.matcherRe = langRe(join(terminators), true); + this.lastIndex = 0; + } + + /** @param {string} s */ + exec(s) { + this.matcherRe.lastIndex = this.lastIndex; + const match = this.matcherRe.exec(s); + if (!match) { return null; } + + // eslint-disable-next-line no-undefined + const i = match.findIndex((el, i) => i > 0 && el !== undefined); + // @ts-ignore + const matchData = this.matchIndexes[i]; + // trim off any earlier non-relevant match groups (ie, the other regex + // match groups that make up the multi-matcher) + match.splice(0, i); + + return Object.assign(match, matchData); + } + } + + /* + Created to solve the key deficiently with MultiRegex - there is no way to + test for multiple matches at a single location. Why would we need to do + that? In the future a more dynamic engine will allow certain matches to be + ignored. An example: if we matched say the 3rd regex in a large group but + decided to ignore it - we'd need to started testing again at the 4th + regex... but MultiRegex itself gives us no real way to do that. + + So what this class creates MultiRegexs on the fly for whatever search + position they are needed. + + NOTE: These additional MultiRegex objects are created dynamically. For most + grammars most of the time we will never actually need anything more than the + first MultiRegex - so this shouldn't have too much overhead. + + Say this is our search group, and we match regex3, but wish to ignore it. + + regex1 | regex2 | regex3 | regex4 | regex5 ' ie, startAt = 0 + + What we need is a new MultiRegex that only includes the remaining + possibilities: + + regex4 | regex5 ' ie, startAt = 3 + + This class wraps all that complexity up in a simple API... `startAt` decides + where in the array of expressions to start doing the matching. It + auto-increments, so if a match is found at position 2, then startAt will be + set to 3. If the end is reached startAt will return to 0. + + MOST of the time the parser will be setting startAt manually to 0. + */ + class ResumableMultiRegex { + constructor() { + // @ts-ignore + this.rules = []; + // @ts-ignore + this.multiRegexes = []; + this.count = 0; + + this.lastIndex = 0; + this.regexIndex = 0; + } + + // @ts-ignore + getMatcher(index) { + if (this.multiRegexes[index]) return this.multiRegexes[index]; + + const matcher = new MultiRegex(); + this.rules.slice(index).forEach(([re, opts]) => matcher.addRule(re, opts)); + matcher.compile(); + this.multiRegexes[index] = matcher; + return matcher; + } + + resumingScanAtSamePosition() { + return this.regexIndex !== 0; + } + + considerAll() { + this.regexIndex = 0; + } + + // @ts-ignore + addRule(re, opts) { + this.rules.push([re, opts]); + if (opts.type === "begin") this.count++; + } + + /** @param {string} s */ + exec(s) { + const m = this.getMatcher(this.regexIndex); + m.lastIndex = this.lastIndex; + let result = m.exec(s); + + // The following is because we have no easy way to say "resume scanning at the + // existing position but also skip the current rule ONLY". What happens is + // all prior rules are also skipped which can result in matching the wrong + // thing. Example of matching "booger": + + // our matcher is [string, "booger", number] + // + // ....booger.... + + // if "booger" is ignored then we'd really need a regex to scan from the + // SAME position for only: [string, number] but ignoring "booger" (if it + // was the first match), a simple resume would scan ahead who knows how + // far looking only for "number", ignoring potential string matches (or + // future "booger" matches that might be valid.) + + // So what we do: We execute two matchers, one resuming at the same + // position, but the second full matcher starting at the position after: + + // /--- resume first regex match here (for [number]) + // |/---- full match here for [string, "booger", number] + // vv + // ....booger.... + + // Which ever results in a match first is then used. So this 3-4 step + // process essentially allows us to say "match at this position, excluding + // a prior rule that was ignored". + // + // 1. Match "booger" first, ignore. Also proves that [string] does non match. + // 2. Resume matching for [number] + // 3. Match at index + 1 for [string, "booger", number] + // 4. If #2 and #3 result in matches, which came first? + if (this.resumingScanAtSamePosition()) { + if (result && result.index === this.lastIndex) ; else { // use the second matcher result + const m2 = this.getMatcher(0); + m2.lastIndex = this.lastIndex + 1; + result = m2.exec(s); + } + } + + if (result) { + this.regexIndex += result.position + 1; + if (this.regexIndex === this.count) { + // wrap-around to considering all matches again + this.considerAll(); + } + } + + return result; + } + } + + /** + * Given a mode, builds a huge ResumableMultiRegex that can be used to walk + * the content and find matches. + * + * @param {CompiledMode} mode + * @returns {ResumableMultiRegex} + */ + function buildModeRegex(mode) { + const mm = new ResumableMultiRegex(); + + mode.contains.forEach(term => mm.addRule(term.begin, { rule: term, type: "begin" })); + + if (mode.terminatorEnd) { + mm.addRule(mode.terminatorEnd, { type: "end" }); + } + if (mode.illegal) { + mm.addRule(mode.illegal, { type: "illegal" }); + } + + return mm; + } + + /** skip vs abort vs ignore + * + * @skip - The mode is still entered and exited normally (and contains rules apply), + * but all content is held and added to the parent buffer rather than being + * output when the mode ends. Mostly used with `sublanguage` to build up + * a single large buffer than can be parsed by sublanguage. + * + * - The mode begin ands ends normally. + * - Content matched is added to the parent mode buffer. + * - The parser cursor is moved forward normally. + * + * @abort - A hack placeholder until we have ignore. Aborts the mode (as if it + * never matched) but DOES NOT continue to match subsequent `contains` + * modes. Abort is bad/suboptimal because it can result in modes + * farther down not getting applied because an earlier rule eats the + * content but then aborts. + * + * - The mode does not begin. + * - Content matched by `begin` is added to the mode buffer. + * - The parser cursor is moved forward accordingly. + * + * @ignore - Ignores the mode (as if it never matched) and continues to match any + * subsequent `contains` modes. Ignore isn't technically possible with + * the current parser implementation. + * + * - The mode does not begin. + * - Content matched by `begin` is ignored. + * - The parser cursor is not moved forward. + */ + + /** + * Compiles an individual mode + * + * This can raise an error if the mode contains certain detectable known logic + * issues. + * @param {Mode} mode + * @param {CompiledMode | null} [parent] + * @returns {CompiledMode | never} + */ + function compileMode(mode, parent) { + const cmode = /** @type CompiledMode */ (mode); + if (mode.compiled) return cmode; + + [ + // do this early so compiler extensions generally don't have to worry about + // the distinction between match/begin + compileMatch + ].forEach(ext => ext(mode, parent)); + + language.compilerExtensions.forEach(ext => ext(mode, parent)); + + // __beforeBegin is considered private API, internal use only + mode.__beforeBegin = null; + + [ + beginKeywords, + // do this later so compiler extensions that come earlier have access to the + // raw array if they wanted to perhaps manipulate it, etc. + compileIllegal, + // default to 1 relevance if not specified + compileRelevance + ].forEach(ext => ext(mode, parent)); + + mode.compiled = true; + + let keywordPattern = null; + if (typeof mode.keywords === "object") { + keywordPattern = mode.keywords.$pattern; + delete mode.keywords.$pattern; + } + + if (mode.keywords) { + mode.keywords = compileKeywords(mode.keywords, language.case_insensitive); + } + + // both are not allowed + if (mode.lexemes && keywordPattern) { + throw new Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) "); + } + + // `mode.lexemes` was the old standard before we added and now recommend + // using `keywords.$pattern` to pass the keyword pattern + keywordPattern = keywordPattern || mode.lexemes || /\w+/; + cmode.keywordPatternRe = langRe(keywordPattern, true); + + if (parent) { + if (!mode.begin) mode.begin = /\B|\b/; + cmode.beginRe = langRe(mode.begin); + if (mode.endSameAsBegin) mode.end = mode.begin; + if (!mode.end && !mode.endsWithParent) mode.end = /\B|\b/; + if (mode.end) cmode.endRe = langRe(mode.end); + cmode.terminatorEnd = source(mode.end) || ''; + if (mode.endsWithParent && parent.terminatorEnd) { + cmode.terminatorEnd += (mode.end ? '|' : '') + parent.terminatorEnd; + } + } + if (mode.illegal) cmode.illegalRe = langRe(/** @type {RegExp | string} */ (mode.illegal)); + if (!mode.contains) mode.contains = []; + + mode.contains = [].concat(...mode.contains.map(function(c) { + return expandOrCloneMode(c === 'self' ? mode : c); + })); + mode.contains.forEach(function(c) { compileMode(/** @type Mode */ (c), cmode); }); + + if (mode.starts) { + compileMode(mode.starts, parent); + } + + cmode.matcher = buildModeRegex(cmode); + return cmode; + } + + if (!language.compilerExtensions) language.compilerExtensions = []; + + // self is not valid at the top-level + if (language.contains && language.contains.includes('self')) { + throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation."); + } + + // we need a null object, which inherit will guarantee + language.classNameAliases = inherit(language.classNameAliases || {}); + + return compileMode(/** @type Mode */ (language)); +} + +/** + * Determines if a mode has a dependency on it's parent or not + * + * If a mode does have a parent dependency then often we need to clone it if + * it's used in multiple places so that each copy points to the correct parent, + * where-as modes without a parent can often safely be re-used at the bottom of + * a mode chain. + * + * @param {Mode | null} mode + * @returns {boolean} - is there a dependency on the parent? + * */ +function dependencyOnParent(mode) { + if (!mode) return false; + + return mode.endsWithParent || dependencyOnParent(mode.starts); +} + +/** + * Expands a mode or clones it if necessary + * + * This is necessary for modes with parental dependenceis (see notes on + * `dependencyOnParent`) and for nodes that have `variants` - which must then be + * exploded into their own individual modes at compile time. + * + * @param {Mode} mode + * @returns {Mode | Mode[]} + * */ +function expandOrCloneMode(mode) { + if (mode.variants && !mode.cachedVariants) { + mode.cachedVariants = mode.variants.map(function(variant) { + return inherit(mode, { variants: null }, variant); + }); + } + + // EXPAND + // if we have variants then essentially "replace" the mode with the variants + // this happens in compileMode, where this function is called from + if (mode.cachedVariants) { + return mode.cachedVariants; + } + + // CLONE + // if we have dependencies on parents then we need a unique + // instance of ourselves, so we can be reused with many + // different parents without issue + if (dependencyOnParent(mode)) { + return inherit(mode, { starts: mode.starts ? inherit(mode.starts) : null }); + } + + if (Object.isFrozen(mode)) { + return inherit(mode); + } + + // no special dependency issues, just return ourselves + return mode; +} + +var version = "10.5.0"; + +// @ts-nocheck + +function hasValueOrEmptyAttribute(value) { + return Boolean(value || value === ""); +} + +function BuildVuePlugin(hljs) { + const Component = { + props: ["language", "code", "autodetect"], + data: function() { + return { + detectedLanguage: "", + unknownLanguage: false + }; + }, + computed: { + className() { + if (this.unknownLanguage) return ""; + + return "hljs " + this.detectedLanguage; + }, + highlighted() { + // no idea what language to use, return raw code + if (!this.autoDetect && !hljs.getLanguage(this.language)) { + console.warn(`The language "${this.language}" you specified could not be found.`); + this.unknownLanguage = true; + return escapeHTML(this.code); + } + + let result = {}; + if (this.autoDetect) { + result = hljs.highlightAuto(this.code); + this.detectedLanguage = result.language; + } else { + result = hljs.highlight(this.language, this.code, this.ignoreIllegals); + this.detectedLanguage = this.language; + } + return result.value; + }, + autoDetect() { + return !this.language || hasValueOrEmptyAttribute(this.autodetect); + }, + ignoreIllegals() { + return true; + } + }, + // this avoids needing to use a whole Vue compilation pipeline just + // to build Highlight.js + render(createElement) { + return createElement("pre", {}, [ + createElement("code", { + class: this.className, + domProps: { innerHTML: this.highlighted } + }) + ]); + } + // template: `
` + }; + + const VuePlugin = { + install(Vue) { + Vue.component('highlightjs', Component); + } + }; + + return { Component, VuePlugin }; +} + +/* plugin itself */ + +/** @type {HLJSPlugin} */ +const mergeHTMLPlugin = { + "after:highlightBlock": ({ block, result, text }) => { + const originalStream = nodeStream(block); + if (!originalStream.length) return; + + const resultNode = document.createElement('div'); + resultNode.innerHTML = result.value; + result.value = mergeStreams(originalStream, nodeStream(resultNode), text); + } +}; + +/* Stream merging support functions */ + +/** + * @typedef Event + * @property {'start'|'stop'} event + * @property {number} offset + * @property {Node} node + */ + +/** + * @param {Node} node + */ +function tag(node) { + return node.nodeName.toLowerCase(); +} + +/** + * @param {Node} node + */ +function nodeStream(node) { + /** @type Event[] */ + const result = []; + (function _nodeStream(node, offset) { + for (let child = node.firstChild; child; child = child.nextSibling) { + if (child.nodeType === 3) { + offset += child.nodeValue.length; + } else if (child.nodeType === 1) { + result.push({ + event: 'start', + offset: offset, + node: child + }); + offset = _nodeStream(child, offset); + // Prevent void elements from having an end tag that would actually + // double them in the output. There are more void elements in HTML + // but we list only those realistically expected in code display. + if (!tag(child).match(/br|hr|img|input/)) { + result.push({ + event: 'stop', + offset: offset, + node: child + }); + } + } + } + return offset; + })(node, 0); + return result; +} + +/** + * @param {any} original - the original stream + * @param {any} highlighted - stream of the highlighted source + * @param {string} value - the original source itself + */ +function mergeStreams(original, highlighted, value) { + let processed = 0; + let result = ''; + const nodeStack = []; + + function selectStream() { + if (!original.length || !highlighted.length) { + return original.length ? original : highlighted; + } + if (original[0].offset !== highlighted[0].offset) { + return (original[0].offset < highlighted[0].offset) ? original : highlighted; + } + + /* + To avoid starting the stream just before it should stop the order is + ensured that original always starts first and closes last: + + if (event1 == 'start' && event2 == 'start') + return original; + if (event1 == 'start' && event2 == 'stop') + return highlighted; + if (event1 == 'stop' && event2 == 'start') + return original; + if (event1 == 'stop' && event2 == 'stop') + return highlighted; + + ... which is collapsed to: + */ + return highlighted[0].event === 'start' ? original : highlighted; + } + + /** + * @param {Node} node + */ + function open(node) { + /** @param {Attr} attr */ + function attributeString(attr) { + return ' ' + attr.nodeName + '="' + escapeHTML(attr.value) + '"'; + } + // @ts-ignore + result += '<' + tag(node) + [].map.call(node.attributes, attributeString).join('') + '>'; + } + + /** + * @param {Node} node + */ + function close(node) { + result += ''; + } + + /** + * @param {Event} event + */ + function render(event) { + (event.event === 'start' ? open : close)(event.node); + } + + while (original.length || highlighted.length) { + let stream = selectStream(); + result += escapeHTML(value.substring(processed, stream[0].offset)); + processed = stream[0].offset; + if (stream === original) { + /* + On any opening or closing tag of the original markup we first close + the entire highlighted node stack, then render the original tag along + with all the following original tags at the same offset and then + reopen all the tags on the highlighted stack. + */ + nodeStack.reverse().forEach(close); + do { + render(stream.splice(0, 1)[0]); + stream = selectStream(); + } while (stream === original && stream.length && stream[0].offset === processed); + nodeStack.reverse().forEach(open); + } else { + if (stream[0].event === 'start') { + nodeStack.push(stream[0].node); + } else { + nodeStack.pop(); + } + render(stream.splice(0, 1)[0]); + } + } + return result + escapeHTML(value.substr(processed)); +} + +/* + +For the reasoning behind this please see: +https://github.com/highlightjs/highlight.js/issues/2880#issuecomment-747275419 + +*/ + +/** + * @param {string} message + */ +const error = (message) => { + console.error(message); +}; + +/** + * @param {string} message + * @param {any} args + */ +const warn = (message, ...args) => { + console.log(`WARN: ${message}`, ...args); +}; + +/** + * @param {string} version + * @param {string} message + */ +const deprecated = (version, message) => { + console.log(`Deprecated as of ${version}. ${message}`); +}; + +/* +Syntax highlighting with language autodetection. +https://highlightjs.org/ +*/ + +const escape$1 = escapeHTML; +const inherit$1 = inherit; +const NO_MATCH = Symbol("nomatch"); + +/** + * @param {any} hljs - object that is extended (legacy) + * @returns {HLJSApi} + */ +const HLJS = function(hljs) { + // Global internal variables used within the highlight.js library. + /** @type {Record} */ + const languages = Object.create(null); + /** @type {Record} */ + const aliases = Object.create(null); + /** @type {HLJSPlugin[]} */ + const plugins = []; + + // safe/production mode - swallows more errors, tries to keep running + // even if a single syntax or parse hits a fatal error + let SAFE_MODE = true; + const fixMarkupRe = /(^(<[^>]+>|\t|)+|\n)/gm; + const LANGUAGE_NOT_FOUND = "Could not find the language '{}', did you forget to load/include a language module?"; + /** @type {Language} */ + const PLAINTEXT_LANGUAGE = { disableAutodetect: true, name: 'Plain text', contains: [] }; + + // Global options used when within external APIs. This is modified when + // calling the `hljs.configure` function. + /** @type HLJSOptions */ + let options = { + noHighlightRe: /^(no-?highlight)$/i, + languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, + classPrefix: 'hljs-', + tabReplace: null, + useBR: false, + languages: null, + // beta configuration options, subject to change, welcome to discuss + // https://github.com/highlightjs/highlight.js/issues/1086 + __emitter: TokenTreeEmitter + }; + + /* Utility functions */ + + /** + * Tests a language name to see if highlighting should be skipped + * @param {string} languageName + */ + function shouldNotHighlight(languageName) { + return options.noHighlightRe.test(languageName); + } + + /** + * @param {HighlightedHTMLElement} block - the HTML element to determine language for + */ + function blockLanguage(block) { + let classes = block.className + ' '; + + classes += block.parentNode ? block.parentNode.className : ''; + + // language-* takes precedence over non-prefixed class names. + const match = options.languageDetectRe.exec(classes); + if (match) { + const language = getLanguage(match[1]); + if (!language) { + warn(LANGUAGE_NOT_FOUND.replace("{}", match[1])); + warn("Falling back to no-highlight mode for this block.", block); + } + return language ? match[1] : 'no-highlight'; + } + + return classes + .split(/\s+/) + .find((_class) => shouldNotHighlight(_class) || getLanguage(_class)); + } + + /** + * Core highlighting function. + * + * @param {string} languageName - the language to use for highlighting + * @param {string} code - the code to highlight + * @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail + * @param {CompiledMode} [continuation] - current continuation mode, if any + * + * @returns {HighlightResult} Result - an object that represents the result + * @property {string} language - the language name + * @property {number} relevance - the relevance score + * @property {string} value - the highlighted HTML code + * @property {string} code - the original raw code + * @property {CompiledMode} top - top of the current mode stack + * @property {boolean} illegal - indicates whether any illegal matches were found + */ + function highlight(languageName, code, ignoreIllegals, continuation) { + /** @type {BeforeHighlightContext} */ + const context = { + code, + language: languageName + }; + // the plugin can change the desired language or the code to be highlighted + // just be changing the object it was passed + fire("before:highlight", context); + + // a before plugin can usurp the result completely by providing it's own + // in which case we don't even need to call highlight + const result = context.result ? + context.result : + _highlight(context.language, context.code, ignoreIllegals, continuation); + + result.code = context.code; + // the plugin can change anything in result to suite it + fire("after:highlight", result); + + return result; + } + + /** + * private highlight that's used internally and does not fire callbacks + * + * @param {string} languageName - the language to use for highlighting + * @param {string} code - the code to highlight + * @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail + * @param {CompiledMode} [continuation] - current continuation mode, if any + * @returns {HighlightResult} - result of the highlight operation + */ + function _highlight(languageName, code, ignoreIllegals, continuation) { + const codeToHighlight = code; + + /** + * Return keyword data if a match is a keyword + * @param {CompiledMode} mode - current mode + * @param {RegExpMatchArray} match - regexp match data + * @returns {KeywordData | false} + */ + function keywordData(mode, match) { + const matchText = language.case_insensitive ? match[0].toLowerCase() : match[0]; + return Object.prototype.hasOwnProperty.call(mode.keywords, matchText) && mode.keywords[matchText]; + } + + function processKeywords() { + if (!top.keywords) { + emitter.addText(modeBuffer); + return; + } + + let lastIndex = 0; + top.keywordPatternRe.lastIndex = 0; + let match = top.keywordPatternRe.exec(modeBuffer); + let buf = ""; + + while (match) { + buf += modeBuffer.substring(lastIndex, match.index); + const data = keywordData(top, match); + if (data) { + const [kind, keywordRelevance] = data; + emitter.addText(buf); + buf = ""; + + relevance += keywordRelevance; + const cssClass = language.classNameAliases[kind] || kind; + emitter.addKeyword(match[0], cssClass); + } else { + buf += match[0]; + } + lastIndex = top.keywordPatternRe.lastIndex; + match = top.keywordPatternRe.exec(modeBuffer); + } + buf += modeBuffer.substr(lastIndex); + emitter.addText(buf); + } + + function processSubLanguage() { + if (modeBuffer === "") return; + /** @type HighlightResult */ + let result = null; + + if (typeof top.subLanguage === 'string') { + if (!languages[top.subLanguage]) { + emitter.addText(modeBuffer); + return; + } + result = _highlight(top.subLanguage, modeBuffer, true, continuations[top.subLanguage]); + continuations[top.subLanguage] = /** @type {CompiledMode} */ (result.top); + } else { + result = highlightAuto(modeBuffer, top.subLanguage.length ? top.subLanguage : null); + } + + // Counting embedded language score towards the host language may be disabled + // with zeroing the containing mode relevance. Use case in point is Markdown that + // allows XML everywhere and makes every XML snippet to have a much larger Markdown + // score. + if (top.relevance > 0) { + relevance += result.relevance; + } + emitter.addSublanguage(result.emitter, result.language); + } + + function processBuffer() { + if (top.subLanguage != null) { + processSubLanguage(); + } else { + processKeywords(); + } + modeBuffer = ''; + } + + /** + * @param {Mode} mode - new mode to start + */ + function startNewMode(mode) { + if (mode.className) { + emitter.openNode(language.classNameAliases[mode.className] || mode.className); + } + top = Object.create(mode, { parent: { value: top } }); + return top; + } + + /** + * @param {CompiledMode } mode - the mode to potentially end + * @param {RegExpMatchArray} match - the latest match + * @param {string} matchPlusRemainder - match plus remainder of content + * @returns {CompiledMode | void} - the next mode, or if void continue on in current mode + */ + function endOfMode(mode, match, matchPlusRemainder) { + let matched = startsWith(mode.endRe, matchPlusRemainder); + + if (matched) { + if (mode["on:end"]) { + const resp = new Response(mode); + mode["on:end"](match, resp); + if (resp.ignore) matched = false; + } + + if (matched) { + while (mode.endsParent && mode.parent) { + mode = mode.parent; + } + return mode; + } + } + // even if on:end fires an `ignore` it's still possible + // that we might trigger the end node because of a parent mode + if (mode.endsWithParent) { + return endOfMode(mode.parent, match, matchPlusRemainder); + } + } + + /** + * Handle matching but then ignoring a sequence of text + * + * @param {string} lexeme - string containing full match text + */ + function doIgnore(lexeme) { + if (top.matcher.regexIndex === 0) { + // no more regexs to potentially match here, so we move the cursor forward one + // space + modeBuffer += lexeme[0]; + return 1; + } else { + // no need to move the cursor, we still have additional regexes to try and + // match at this very spot + resumeScanAtSamePosition = true; + return 0; + } + } + + /** + * Handle the start of a new potential mode match + * + * @param {EnhancedMatch} match - the current match + * @returns {number} how far to advance the parse cursor + */ + function doBeginMatch(match) { + const lexeme = match[0]; + const newMode = match.rule; + + const resp = new Response(newMode); + // first internal before callbacks, then the public ones + const beforeCallbacks = [newMode.__beforeBegin, newMode["on:begin"]]; + for (const cb of beforeCallbacks) { + if (!cb) continue; + cb(match, resp); + if (resp.ignore) return doIgnore(lexeme); + } + + if (newMode && newMode.endSameAsBegin) { + newMode.endRe = escape(lexeme); + } + + if (newMode.skip) { + modeBuffer += lexeme; + } else { + if (newMode.excludeBegin) { + modeBuffer += lexeme; + } + processBuffer(); + if (!newMode.returnBegin && !newMode.excludeBegin) { + modeBuffer = lexeme; + } + } + startNewMode(newMode); + // if (mode["after:begin"]) { + // let resp = new Response(mode); + // mode["after:begin"](match, resp); + // } + return newMode.returnBegin ? 0 : lexeme.length; + } + + /** + * Handle the potential end of mode + * + * @param {RegExpMatchArray} match - the current match + */ + function doEndMatch(match) { + const lexeme = match[0]; + const matchPlusRemainder = codeToHighlight.substr(match.index); + + const endMode = endOfMode(top, match, matchPlusRemainder); + if (!endMode) { return NO_MATCH; } + + const origin = top; + if (origin.skip) { + modeBuffer += lexeme; + } else { + if (!(origin.returnEnd || origin.excludeEnd)) { + modeBuffer += lexeme; + } + processBuffer(); + if (origin.excludeEnd) { + modeBuffer = lexeme; + } + } + do { + if (top.className) { + emitter.closeNode(); + } + if (!top.skip && !top.subLanguage) { + relevance += top.relevance; + } + top = top.parent; + } while (top !== endMode.parent); + if (endMode.starts) { + if (endMode.endSameAsBegin) { + endMode.starts.endRe = endMode.endRe; + } + startNewMode(endMode.starts); + } + return origin.returnEnd ? 0 : lexeme.length; + } + + function processContinuations() { + const list = []; + for (let current = top; current !== language; current = current.parent) { + if (current.className) { + list.unshift(current.className); + } + } + list.forEach(item => emitter.openNode(item)); + } + + /** @type {{type?: MatchType, index?: number, rule?: Mode}}} */ + let lastMatch = {}; + + /** + * Process an individual match + * + * @param {string} textBeforeMatch - text preceeding the match (since the last match) + * @param {EnhancedMatch} [match] - the match itself + */ + function processLexeme(textBeforeMatch, match) { + const lexeme = match && match[0]; + + // add non-matched text to the current mode buffer + modeBuffer += textBeforeMatch; + + if (lexeme == null) { + processBuffer(); + return 0; + } + + // we've found a 0 width match and we're stuck, so we need to advance + // this happens when we have badly behaved rules that have optional matchers to the degree that + // sometimes they can end up matching nothing at all + // Ref: https://github.com/highlightjs/highlight.js/issues/2140 + if (lastMatch.type === "begin" && match.type === "end" && lastMatch.index === match.index && lexeme === "") { + // spit the "skipped" character that our regex choked on back into the output sequence + modeBuffer += codeToHighlight.slice(match.index, match.index + 1); + if (!SAFE_MODE) { + /** @type {AnnotatedError} */ + const err = new Error('0 width match regex'); + err.languageName = languageName; + err.badRule = lastMatch.rule; + throw err; + } + return 1; + } + lastMatch = match; + + if (match.type === "begin") { + return doBeginMatch(match); + } else if (match.type === "illegal" && !ignoreIllegals) { + // illegal match, we do not continue processing + /** @type {AnnotatedError} */ + const err = new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.className || '') + '"'); + err.mode = top; + throw err; + } else if (match.type === "end") { + const processed = doEndMatch(match); + if (processed !== NO_MATCH) { + return processed; + } + } + + // edge case for when illegal matches $ (end of line) which is technically + // a 0 width match but not a begin/end match so it's not caught by the + // first handler (when ignoreIllegals is true) + if (match.type === "illegal" && lexeme === "") { + // advance so we aren't stuck in an infinite loop + return 1; + } + + // infinite loops are BAD, this is a last ditch catch all. if we have a + // decent number of iterations yet our index (cursor position in our + // parsing) still 3x behind our index then something is very wrong + // so we bail + if (iterations > 100000 && iterations > match.index * 3) { + const err = new Error('potential infinite loop, way more iterations than matches'); + throw err; + } + + /* + Why might be find ourselves here? Only one occasion now. An end match that was + triggered but could not be completed. When might this happen? When an `endSameasBegin` + rule sets the end rule to a specific match. Since the overall mode termination rule that's + being used to scan the text isn't recompiled that means that any match that LOOKS like + the end (but is not, because it is not an exact match to the beginning) will + end up here. A definite end match, but when `doEndMatch` tries to "reapply" + the end rule and fails to match, we wind up here, and just silently ignore the end. + + This causes no real harm other than stopping a few times too many. + */ + + modeBuffer += lexeme; + return lexeme.length; + } + + const language = getLanguage(languageName); + if (!language) { + error(LANGUAGE_NOT_FOUND.replace("{}", languageName)); + throw new Error('Unknown language: "' + languageName + '"'); + } + + const md = compileLanguage(language, { plugins }); + let result = ''; + /** @type {CompiledMode} */ + let top = continuation || md; + /** @type Record */ + const continuations = {}; // keep continuations for sub-languages + const emitter = new options.__emitter(options); + processContinuations(); + let modeBuffer = ''; + let relevance = 0; + let index = 0; + let iterations = 0; + let resumeScanAtSamePosition = false; + + try { + top.matcher.considerAll(); + + for (;;) { + iterations++; + if (resumeScanAtSamePosition) { + // only regexes not matched previously will now be + // considered for a potential match + resumeScanAtSamePosition = false; + } else { + top.matcher.considerAll(); + } + top.matcher.lastIndex = index; + + const match = top.matcher.exec(codeToHighlight); + // console.log("match", match[0], match.rule && match.rule.begin) + + if (!match) break; + + const beforeMatch = codeToHighlight.substring(index, match.index); + const processedCount = processLexeme(beforeMatch, match); + index = match.index + processedCount; + } + processLexeme(codeToHighlight.substr(index)); + emitter.closeAllNodes(); + emitter.finalize(); + result = emitter.toHTML(); + + return { + relevance: relevance, + value: result, + language: languageName, + illegal: false, + emitter: emitter, + top: top + }; + } catch (err) { + if (err.message && err.message.includes('Illegal')) { + return { + illegal: true, + illegalBy: { + msg: err.message, + context: codeToHighlight.slice(index - 100, index + 100), + mode: err.mode + }, + sofar: result, + relevance: 0, + value: escape$1(codeToHighlight), + emitter: emitter + }; + } else if (SAFE_MODE) { + return { + illegal: false, + relevance: 0, + value: escape$1(codeToHighlight), + emitter: emitter, + language: languageName, + top: top, + errorRaised: err + }; + } else { + throw err; + } + } + } + + /** + * returns a valid highlight result, without actually doing any actual work, + * auto highlight starts with this and it's possible for small snippets that + * auto-detection may not find a better match + * @param {string} code + * @returns {HighlightResult} + */ + function justTextHighlightResult(code) { + const result = { + relevance: 0, + emitter: new options.__emitter(options), + value: escape$1(code), + illegal: false, + top: PLAINTEXT_LANGUAGE + }; + result.emitter.addText(code); + return result; + } + + /** + Highlighting with language detection. Accepts a string with the code to + highlight. Returns an object with the following properties: + + - language (detected language) + - relevance (int) + - value (an HTML string with highlighting markup) + - second_best (object with the same structure for second-best heuristically + detected language, may be absent) + + @param {string} code + @param {Array} [languageSubset] + @returns {AutoHighlightResult} + */ + function highlightAuto(code, languageSubset) { + languageSubset = languageSubset || options.languages || Object.keys(languages); + const plaintext = justTextHighlightResult(code); + + const results = languageSubset.filter(getLanguage).filter(autoDetection).map(name => + _highlight(name, code, false) + ); + results.unshift(plaintext); // plaintext is always an option + + const sorted = results.sort((a, b) => { + // sort base on relevance + if (a.relevance !== b.relevance) return b.relevance - a.relevance; + + // always award the tie to the base language + // ie if C++ and Arduino are tied, it's more likely to be C++ + if (a.language && b.language) { + if (getLanguage(a.language).supersetOf === b.language) { + return 1; + } else if (getLanguage(b.language).supersetOf === a.language) { + return -1; + } + } + + // otherwise say they are equal, which has the effect of sorting on + // relevance while preserving the original ordering - which is how ties + // have historically been settled, ie the language that comes first always + // wins in the case of a tie + return 0; + }); + + const [best, secondBest] = sorted; + + /** @type {AutoHighlightResult} */ + const result = best; + result.second_best = secondBest; + + return result; + } + + /** + Post-processing of the highlighted markup: + + - replace TABs with something more useful + - replace real line-breaks with '
' for non-pre containers + + @param {string} html + @returns {string} + */ + function fixMarkup(html) { + if (!(options.tabReplace || options.useBR)) { + return html; + } + + return html.replace(fixMarkupRe, match => { + if (match === '\n') { + return options.useBR ? '
' : match; + } else if (options.tabReplace) { + return match.replace(/\t/g, options.tabReplace); + } + return match; + }); + } + + /** + * Builds new class name for block given the language name + * + * @param {HTMLElement} element + * @param {string} [currentLang] + * @param {string} [resultLang] + */ + function updateClassName(element, currentLang, resultLang) { + const language = currentLang ? aliases[currentLang] : resultLang; + + element.classList.add("hljs"); + if (language) element.classList.add(language); + } + + /** @type {HLJSPlugin} */ + const brPlugin = { + "before:highlightBlock": ({ block }) => { + if (options.useBR) { + block.innerHTML = block.innerHTML.replace(/\n/g, '').replace(//g, '\n'); + } + }, + "after:highlightBlock": ({ result }) => { + if (options.useBR) { + result.value = result.value.replace(/\n/g, "
"); + } + } + }; + + const TAB_REPLACE_RE = /^(<[^>]+>|\t)+/gm; + /** @type {HLJSPlugin} */ + const tabReplacePlugin = { + "after:highlightBlock": ({ result }) => { + if (options.tabReplace) { + result.value = result.value.replace(TAB_REPLACE_RE, (m) => + m.replace(/\t/g, options.tabReplace) + ); + } + } + }; + + /** + * Applies highlighting to a DOM node containing code. Accepts a DOM node and + * two optional parameters for fixMarkup. + * + * @param {HighlightedHTMLElement} element - the HTML element to highlight + */ + function highlightBlock(element) { + /** @type HTMLElement */ + let node = null; + const language = blockLanguage(element); + + if (shouldNotHighlight(language)) return; + + fire("before:highlightBlock", + { block: element, language: language }); + + node = element; + const text = node.textContent; + const result = language ? highlight(language, text, true) : highlightAuto(text); + + fire("after:highlightBlock", { block: element, result, text }); + + element.innerHTML = result.value; + updateClassName(element, language, result.language); + element.result = { + language: result.language, + // TODO: remove with version 11.0 + re: result.relevance, + relavance: result.relevance + }; + if (result.second_best) { + element.second_best = { + language: result.second_best.language, + // TODO: remove with version 11.0 + re: result.second_best.relevance, + relavance: result.second_best.relevance + }; + } + } + + /** + * Updates highlight.js global options with the passed options + * + * @param {Partial} userOptions + */ + function configure(userOptions) { + if (userOptions.useBR) { + deprecated("10.3.0", "'useBR' will be removed entirely in v11.0"); + deprecated("10.3.0", "Please see https://github.com/highlightjs/highlight.js/issues/2559"); + } + options = inherit$1(options, userOptions); + } + + /** + * Highlights to all
 blocks on a page
+   *
+   * @type {Function & {called?: boolean}}
+   */
+  const initHighlighting = () => {
+    if (initHighlighting.called) return;
+    initHighlighting.called = true;
+
+    const blocks = document.querySelectorAll('pre code');
+    blocks.forEach(highlightBlock);
+  };
+
+  // Higlights all when DOMContentLoaded fires
+  function initHighlightingOnLoad() {
+    // @ts-ignore
+    window.addEventListener('DOMContentLoaded', initHighlighting, false);
+  }
+
+  /**
+   * Register a language grammar module
+   *
+   * @param {string} languageName
+   * @param {LanguageFn} languageDefinition
+   */
+  function registerLanguage(languageName, languageDefinition) {
+    let lang = null;
+    try {
+      lang = languageDefinition(hljs);
+    } catch (error$1) {
+      error("Language definition for '{}' could not be registered.".replace("{}", languageName));
+      // hard or soft error
+      if (!SAFE_MODE) { throw error$1; } else { error(error$1); }
+      // languages that have serious errors are replaced with essentially a
+      // "plaintext" stand-in so that the code blocks will still get normal
+      // css classes applied to them - and one bad language won't break the
+      // entire highlighter
+      lang = PLAINTEXT_LANGUAGE;
+    }
+    // give it a temporary name if it doesn't have one in the meta-data
+    if (!lang.name) lang.name = languageName;
+    languages[languageName] = lang;
+    lang.rawDefinition = languageDefinition.bind(null, hljs);
+
+    if (lang.aliases) {
+      registerAliases(lang.aliases, { languageName });
+    }
+  }
+
+  /**
+   * @returns {string[]} List of language internal names
+   */
+  function listLanguages() {
+    return Object.keys(languages);
+  }
+
+  /**
+    intended usage: When one language truly requires another
+
+    Unlike `getLanguage`, this will throw when the requested language
+    is not available.
+
+    @param {string} name - name of the language to fetch/require
+    @returns {Language | never}
+  */
+  function requireLanguage(name) {
+    deprecated("10.4.0", "requireLanguage will be removed entirely in v11.");
+    deprecated("10.4.0", "Please see https://github.com/highlightjs/highlight.js/pull/2844");
+
+    const lang = getLanguage(name);
+    if (lang) { return lang; }
+
+    const err = new Error('The \'{}\' language is required, but not loaded.'.replace('{}', name));
+    throw err;
+  }
+
+  /**
+   * @param {string} name - name of the language to retrieve
+   * @returns {Language | undefined}
+   */
+  function getLanguage(name) {
+    name = (name || '').toLowerCase();
+    return languages[name] || languages[aliases[name]];
+  }
+
+  /**
+   *
+   * @param {string|string[]} aliasList - single alias or list of aliases
+   * @param {{languageName: string}} opts
+   */
+  function registerAliases(aliasList, { languageName }) {
+    if (typeof aliasList === 'string') {
+      aliasList = [aliasList];
+    }
+    aliasList.forEach(alias => { aliases[alias] = languageName; });
+  }
+
+  /**
+   * Determines if a given language has auto-detection enabled
+   * @param {string} name - name of the language
+   */
+  function autoDetection(name) {
+    const lang = getLanguage(name);
+    return lang && !lang.disableAutodetect;
+  }
+
+  /**
+   * @param {HLJSPlugin} plugin
+   */
+  function addPlugin(plugin) {
+    plugins.push(plugin);
+  }
+
+  /**
+   *
+   * @param {PluginEvent} event
+   * @param {any} args
+   */
+  function fire(event, args) {
+    const cb = event;
+    plugins.forEach(function(plugin) {
+      if (plugin[cb]) {
+        plugin[cb](args);
+      }
+    });
+  }
+
+  /**
+  Note: fixMarkup is deprecated and will be removed entirely in v11
+
+  @param {string} arg
+  @returns {string}
+  */
+  function deprecateFixMarkup(arg) {
+    deprecated("10.2.0", "fixMarkup will be removed entirely in v11.0");
+    deprecated("10.2.0", "Please see https://github.com/highlightjs/highlight.js/issues/2534");
+
+    return fixMarkup(arg);
+  }
+
+  /* Interface definition */
+  Object.assign(hljs, {
+    highlight,
+    highlightAuto,
+    fixMarkup: deprecateFixMarkup,
+    highlightBlock,
+    configure,
+    initHighlighting,
+    initHighlightingOnLoad,
+    registerLanguage,
+    listLanguages,
+    getLanguage,
+    registerAliases,
+    requireLanguage,
+    autoDetection,
+    inherit: inherit$1,
+    addPlugin,
+    // plugins for frameworks
+    vuePlugin: BuildVuePlugin(hljs).VuePlugin
+  });
+
+  hljs.debugMode = function() { SAFE_MODE = false; };
+  hljs.safeMode = function() { SAFE_MODE = true; };
+  hljs.versionString = version;
+
+  for (const key in MODES) {
+    // @ts-ignore
+    if (typeof MODES[key] === "object") {
+      // @ts-ignore
+      deepFreezeEs6(MODES[key]);
+    }
+  }
+
+  // merge all the modes/regexs into our main object
+  Object.assign(hljs, MODES);
+
+  // built-in plugins, likely to be moved out of core in the future
+  hljs.addPlugin(brPlugin); // slated to be removed in v11
+  hljs.addPlugin(mergeHTMLPlugin);
+  hljs.addPlugin(tabReplacePlugin);
+  return hljs;
+};
+
+// export an "instance" of the highlighter
+var highlight = HLJS({});
+
+module.exports = highlight;
diff --git a/src/third_party/highlightjs/highlightjs/highlight.js b/src/third_party/highlightjs/highlightjs/highlight.js
new file mode 100644
index 0000000000..44179572e8
--- /dev/null
+++ b/src/third_party/highlightjs/highlightjs/highlight.js
@@ -0,0 +1,2 @@
+// This file has been deprecated in favor of core.js
+var hljs = require('./core');
diff --git a/src/third_party/highlightjs/highlightjs/index.d.ts b/src/third_party/highlightjs/highlightjs/index.d.ts
new file mode 100644
index 0000000000..79ca2848b5
--- /dev/null
+++ b/src/third_party/highlightjs/highlightjs/index.d.ts
@@ -0,0 +1,251 @@
+/* eslint-disable no-unused-vars */
+/* eslint-disable no-use-before-define */
+// For TS consumers who use Node and don't have dom in their tsconfig lib, import the necessary types here.
+/// 
+
+/* Public API */
+
+// eslint-disable-next-line
+declare const hljs : HLJSApi;
+
+type HLJSApi = PublicApi & ModesAPI
+
+interface VuePlugin {
+    install: (vue: any) => void
+}
+
+interface PublicApi {
+    highlight: (languageName: string, code: string, ignoreIllegals?: boolean, continuation?: Mode) => HighlightResult
+    highlightAuto: (code: string, languageSubset?: string[]) => AutoHighlightResult
+    fixMarkup: (html: string) => string
+    highlightBlock: (element: HTMLElement) => void
+    configure: (options: Partial) => void
+    initHighlighting: () => void
+    initHighlightingOnLoad: () => void
+    registerLanguage: (languageName: string, language: LanguageFn) => void
+    listLanguages: () => string[]
+    registerAliases: (aliasList: string | string[], { languageName } : {languageName: string}) => void
+    getLanguage: (languageName: string) => Language | undefined
+    requireLanguage: (languageName: string) => Language | never
+    autoDetection: (languageName: string) => boolean
+    inherit: (original: T, ...args: Record[]) => T
+    addPlugin: (plugin: HLJSPlugin) => void
+    debugMode: () => void
+    safeMode: () => void
+    versionString: string
+    vuePlugin: () => VuePlugin
+}
+
+interface ModesAPI {
+    SHEBANG: (mode?: Partial & {binary?: string | RegExp}) => Mode
+    BACKSLASH_ESCAPE: Mode
+    QUOTE_STRING_MODE: Mode
+    APOS_STRING_MODE: Mode
+    PHRASAL_WORDS_MODE: Mode
+    COMMENT: (begin: string | RegExp, end: string | RegExp, modeOpts?: Mode | {}) => Mode
+    C_LINE_COMMENT_MODE: Mode
+    C_BLOCK_COMMENT_MODE: Mode
+    HASH_COMMENT_MODE: Mode
+    NUMBER_MODE: Mode
+    C_NUMBER_MODE: Mode
+    BINARY_NUMBER_MODE: Mode
+    CSS_NUMBER_MODE: Mode
+    REGEXP_MODE: Mode
+    TITLE_MODE: Mode
+    UNDERSCORE_TITLE_MODE: Mode
+    METHOD_GUARD: Mode
+    END_SAME_AS_BEGIN: (mode: Mode) => Mode
+    // built in regex
+    IDENT_RE: string
+    UNDERSCORE_IDENT_RE: string
+    NUMBER_RE: string
+    C_NUMBER_RE: string
+    BINARY_NUMBER_RE: string
+    RE_STARTERS_RE: string
+}
+
+type LanguageFn = (hljs?: HLJSApi) => Language
+type CompilerExt = (mode: Mode, parent: Mode | Language | null) => void
+
+interface HighlightResult {
+    relevance : number
+    value : string
+    language? : string
+    emitter : Emitter
+    illegal : boolean
+    top? : Language | CompiledMode
+    illegalBy? : illegalData
+    sofar? : string
+    errorRaised? : Error
+    // * for auto-highlight
+    second_best? : Omit
+    code?: string
+}
+interface AutoHighlightResult extends HighlightResult {}
+
+interface illegalData {
+    msg: string
+    context: string
+    mode: CompiledMode
+}
+
+type BeforeHighlightContext = {
+  code: string,
+  language: string,
+  result?: HighlightResult
+}
+type PluginEvent = keyof HLJSPlugin;
+type HLJSPlugin = {
+    'after:highlight'?: (result: HighlightResult) => void,
+    'before:highlight'?: (context: BeforeHighlightContext) => void,
+    'after:highlightBlock'?: (data: { result: HighlightResult}) => void,
+    'before:highlightBlock'?: (data: { block: Element, language: string}) => void,
+}
+
+interface EmitterConstructor {
+    new (opts: any): Emitter
+}
+
+interface HLJSOptions {
+   noHighlightRe: RegExp
+   languageDetectRe: RegExp
+   classPrefix: string
+   tabReplace?: string
+   useBR: boolean
+   languages?: string[]
+   __emitter: EmitterConstructor
+}
+
+interface CallbackResponse {
+    data: Record
+    ignoreMatch: () => void
+}
+
+/************
+ PRIVATE API
+ ************/
+
+/* for jsdoc annotations in the JS source files */
+
+type AnnotatedError = Error & {mode?: Mode | Language, languageName?: string, badRule?: Mode}
+
+type ModeCallback = (match: RegExpMatchArray, response: CallbackResponse) => void
+type HighlightedHTMLElement = HTMLElement & {result?: object, second_best?: object, parentNode: HTMLElement}
+type EnhancedMatch = RegExpMatchArray & {rule: CompiledMode, type: MatchType}
+type MatchType = "begin" | "end" | "illegal"
+
+ interface Emitter {
+    addKeyword(text: string, kind: string): void
+    addText(text: string): void
+    toHTML(): string
+    finalize(): void
+    closeAllNodes(): void
+    openNode(kind: string): void
+    closeNode(): void
+    addSublanguage(emitter: Emitter, subLanguageName: string): void
+ }
+
+/* modes */
+
+ interface ModeCallbacks {
+     "on:end"?: Function,
+     "on:begin"?: ModeCallback
+ }
+
+interface Mode extends ModeCallbacks, ModeDetails {
+
+}
+
+interface LanguageDetail {
+    name?: string
+    rawDefinition?: () => Language
+    aliases?: string[]
+    disableAutodetect?: boolean
+    contains: (Mode)[]
+    case_insensitive?: boolean
+    keywords?: Record | string
+    compiled?: boolean,
+    exports?: any,
+    classNameAliases?: Record
+    compilerExtensions?: CompilerExt[]
+    supersetOf?: string
+}
+
+type Language = LanguageDetail & Partial
+
+interface CompiledLanguage extends LanguageDetail, CompiledMode {
+    compiled: true
+    contains: CompiledMode[]
+    keywords: Record
+}
+
+type KeywordData = [string, number];
+type KeywordDict = Record
+
+type CompiledMode = Omit &
+    {
+        contains: CompiledMode[]
+        keywords: KeywordDict
+        data: Record
+        terminatorEnd: string
+        keywordPatternRe: RegExp
+        beginRe: RegExp
+        endRe: RegExp
+        illegalRe: RegExp
+        matcher: any
+        compiled: true
+        starts?: CompiledMode
+        parent?: CompiledMode
+    }
+
+interface ModeDetails {
+    begin?: RegExp | string
+    match?: RegExp | string
+    end?: RegExp | string
+    className?: string
+    contains?: ("self" | Mode)[]
+    endsParent?: boolean
+    endsWithParent?: boolean
+    endSameAsBegin?: boolean
+    skip?: boolean
+    excludeBegin?: boolean
+    excludeEnd?: boolean
+    returnBegin?: boolean
+    returnEnd?: boolean
+    __beforeBegin?: Function
+    parent?: Mode
+    starts?:Mode
+    lexemes?: string | RegExp
+    keywords?: Record | string
+    beginKeywords?: string
+    relevance?: number
+    illegal?: string | RegExp | Array
+    variants?: Mode[]
+    cachedVariants?: Mode[]
+    // parsed
+    subLanguage?: string | string[]
+    compiled?: boolean
+    label?: string
+}
+
+// deprecated API since v10
+// declare module 'highlight.js/lib/highlight.js';
+
+declare module 'highlight.js' {
+    export = hljs;
+}
+
+declare module 'highlight.js/lib/core' {
+    export = hljs;
+}
+
+declare module 'highlight.js/lib/core.js' {
+    export = hljs;
+}
+
+declare module 'highlight.js/lib/languages/*' {
+    export default function(hljs?: HLJSApi): LanguageDetail;
+}
+
+export = hljs;
+
diff --git a/src/third_party/highlightjs/highlightjs/index.js b/src/third_party/highlightjs/highlightjs/index.js
new file mode 100644
index 0000000000..af9e12dd38
--- /dev/null
+++ b/src/third_party/highlightjs/highlightjs/index.js
@@ -0,0 +1,7 @@
+var hljs = require('./core');
+
+hljs.registerLanguage('javascript', require('./languages/javascript'));
+hljs.registerLanguage('python', require('./languages/python'));
+hljs.registerLanguage('csharp', require('./languages/csharp'));
+
+module.exports = hljs;
\ No newline at end of file
diff --git a/src/third_party/highlightjs/highlightjs/languages/csharp.js b/src/third_party/highlightjs/highlightjs/languages/csharp.js
new file mode 100644
index 0000000000..6474f2e94d
--- /dev/null
+++ b/src/third_party/highlightjs/highlightjs/languages/csharp.js
@@ -0,0 +1,367 @@
+/*
+Language: C#
+Author: Jason Diamond 
+Contributor: Nicolas LLOBERA , Pieter Vantorre , David Pine 
+Website: https://docs.microsoft.com/en-us/dotnet/csharp/
+Category: common
+*/
+
+/** @type LanguageFn */
+function csharp(hljs) {
+  var BUILT_IN_KEYWORDS = [
+      'bool',
+      'byte',
+      'char',
+      'decimal',
+      'delegate',
+      'double',
+      'dynamic',
+      'enum',
+      'float',
+      'int',
+      'long',
+      'nint',
+      'nuint',
+      'object',
+      'sbyte',
+      'short',
+      'string',
+      'ulong',
+      'unit',
+      'ushort'
+  ];
+  var FUNCTION_MODIFIERS = [
+    'public',
+    'private',
+    'protected',
+    'static',
+    'internal',
+    'protected',
+    'abstract',
+    'async',
+    'extern',
+    'override',
+    'unsafe',
+    'virtual',
+    'new',
+    'sealed',
+    'partial'
+  ];
+  var LITERAL_KEYWORDS = [
+      'default',
+      'false',
+      'null',
+      'true'
+  ];
+  var NORMAL_KEYWORDS = [
+    'abstract',
+    'as',
+    'base',
+    'break',
+    'case',
+    'class',
+    'const',
+    'continue',
+    'do',
+    'else',
+    'event',
+    'explicit',
+    'extern',
+    'finally',
+    'fixed',
+    'for',
+    'foreach',
+    'goto',
+    'if',
+    'implicit',
+    'in',
+    'interface',
+    'internal',
+    'is',
+    'lock',
+    'namespace',
+    'new',
+    'operator',
+    'out',
+    'override',
+    'params',
+    'private',
+    'protected',
+    'public',
+    'readonly',
+    'record',
+    'ref',
+    'return',
+    'sealed',
+    'sizeof',
+    'stackalloc',
+    'static',
+    'struct',
+    'switch',
+    'this',
+    'throw',
+    'try',
+    'typeof',
+    'unchecked',
+    'unsafe',
+    'using',
+    'virtual',
+    'void',
+    'volatile',
+    'while'
+  ];
+  var CONTEXTUAL_KEYWORDS = [
+    'add',
+    'alias',
+    'and',
+    'ascending',
+    'async',
+    'await',
+    'by',
+    'descending',
+    'equals',
+    'from',
+    'get',
+    'global',
+    'group',
+    'init',
+    'into',
+    'join',
+    'let',
+    'nameof',
+    'not',
+    'notnull',
+    'on',
+    'or',
+    'orderby',
+    'partial',
+    'remove',
+    'select',
+    'set',
+    'unmanaged',
+    'value|0',
+    'var',
+    'when',
+    'where',
+    'with',
+    'yield'
+  ];
+
+  var KEYWORDS = {
+    keyword: NORMAL_KEYWORDS.concat(CONTEXTUAL_KEYWORDS).join(' '),
+    built_in: BUILT_IN_KEYWORDS.join(' '),
+    literal: LITERAL_KEYWORDS.join(' ')
+  };
+  var TITLE_MODE = hljs.inherit(hljs.TITLE_MODE, {begin: '[a-zA-Z](\\.?\\w)*'});
+  var NUMBERS = {
+    className: 'number',
+    variants: [
+      { begin: '\\b(0b[01\']+)' },
+      { begin: '(-?)\\b([\\d\']+(\\.[\\d\']*)?|\\.[\\d\']+)(u|U|l|L|ul|UL|f|F|b|B)' },
+      { begin: '(-?)(\\b0[xX][a-fA-F0-9\']+|(\\b[\\d\']+(\\.[\\d\']*)?|\\.[\\d\']+)([eE][-+]?[\\d\']+)?)' }
+    ],
+    relevance: 0
+  };
+  var VERBATIM_STRING = {
+    className: 'string',
+    begin: '@"', end: '"',
+    contains: [{begin: '""'}]
+  };
+  var VERBATIM_STRING_NO_LF = hljs.inherit(VERBATIM_STRING, {illegal: /\n/});
+  var SUBST = {
+    className: 'subst',
+    begin: /\{/, end: /\}/,
+    keywords: KEYWORDS
+  };
+  var SUBST_NO_LF = hljs.inherit(SUBST, {illegal: /\n/});
+  var INTERPOLATED_STRING = {
+    className: 'string',
+    begin: /\$"/, end: '"',
+    illegal: /\n/,
+    contains: [{begin: /\{\{/}, {begin: /\}\}/}, hljs.BACKSLASH_ESCAPE, SUBST_NO_LF]
+  };
+  var INTERPOLATED_VERBATIM_STRING = {
+    className: 'string',
+    begin: /\$@"/, end: '"',
+    contains: [{begin: /\{\{/}, {begin: /\}\}/}, {begin: '""'}, SUBST]
+  };
+  var INTERPOLATED_VERBATIM_STRING_NO_LF = hljs.inherit(INTERPOLATED_VERBATIM_STRING, {
+    illegal: /\n/,
+    contains: [{begin: /\{\{/}, {begin: /\}\}/}, {begin: '""'}, SUBST_NO_LF]
+  });
+  SUBST.contains = [
+    INTERPOLATED_VERBATIM_STRING,
+    INTERPOLATED_STRING,
+    VERBATIM_STRING,
+    hljs.APOS_STRING_MODE,
+    hljs.QUOTE_STRING_MODE,
+    NUMBERS,
+    hljs.C_BLOCK_COMMENT_MODE
+  ];
+  SUBST_NO_LF.contains = [
+    INTERPOLATED_VERBATIM_STRING_NO_LF,
+    INTERPOLATED_STRING,
+    VERBATIM_STRING_NO_LF,
+    hljs.APOS_STRING_MODE,
+    hljs.QUOTE_STRING_MODE,
+    NUMBERS,
+    hljs.inherit(hljs.C_BLOCK_COMMENT_MODE, {illegal: /\n/})
+  ];
+  var STRING = {
+    variants: [
+      INTERPOLATED_VERBATIM_STRING,
+      INTERPOLATED_STRING,
+      VERBATIM_STRING,
+      hljs.APOS_STRING_MODE,
+      hljs.QUOTE_STRING_MODE
+    ]
+  };
+
+  var GENERIC_MODIFIER = {
+    begin: "<",
+    end: ">",
+    contains: [
+      { beginKeywords: "in out"},
+      TITLE_MODE
+    ]
+  };
+  var TYPE_IDENT_RE = hljs.IDENT_RE + '(<' + hljs.IDENT_RE + '(\\s*,\\s*' + hljs.IDENT_RE + ')*>)?(\\[\\])?';
+  var AT_IDENTIFIER = {
+    // prevents expressions like `@class` from incorrect flagging
+    // `class` as a keyword
+    begin: "@" + hljs.IDENT_RE,
+    relevance: 0
+  };
+
+  return {
+    name: 'C#',
+    aliases: ['cs', 'c#'],
+    keywords: KEYWORDS,
+    illegal: /::/,
+    contains: [
+      hljs.COMMENT(
+        '///',
+        '$',
+        {
+          returnBegin: true,
+          contains: [
+            {
+              className: 'doctag',
+              variants: [
+                {
+                  begin: '///', relevance: 0
+                },
+                {
+                  begin: ''
+                },
+                {
+                  begin: ''
+                }
+              ]
+            }
+          ]
+        }
+      ),
+      hljs.C_LINE_COMMENT_MODE,
+      hljs.C_BLOCK_COMMENT_MODE,
+      {
+        className: 'meta',
+        begin: '#', end: '$',
+        keywords: {
+          'meta-keyword': 'if else elif endif define undef warning error line region endregion pragma checksum'
+        }
+      },
+      STRING,
+      NUMBERS,
+      {
+        beginKeywords: 'class interface',
+        relevance: 0,
+        end: /[{;=]/,
+        illegal: /[^\s:,]/,
+        contains: [
+          { beginKeywords: "where class" },
+          TITLE_MODE,
+          GENERIC_MODIFIER,
+          hljs.C_LINE_COMMENT_MODE,
+          hljs.C_BLOCK_COMMENT_MODE
+        ]
+      },
+      {
+        beginKeywords: 'namespace',
+        relevance: 0,
+        end: /[{;=]/,
+        illegal: /[^\s:]/,
+        contains: [
+          TITLE_MODE,
+          hljs.C_LINE_COMMENT_MODE,
+          hljs.C_BLOCK_COMMENT_MODE
+        ]
+      },
+      {
+        beginKeywords: 'record',
+        relevance: 0,
+        end: /[{;=]/,
+        illegal: /[^\s:]/,
+        contains: [
+          TITLE_MODE,
+          GENERIC_MODIFIER,
+          hljs.C_LINE_COMMENT_MODE,
+          hljs.C_BLOCK_COMMENT_MODE
+        ]
+      },
+      {
+        // [Attributes("")]
+        className: 'meta',
+        begin: '^\\s*\\[', excludeBegin: true, end: '\\]', excludeEnd: true,
+        contains: [
+          {className: 'meta-string', begin: /"/, end: /"/}
+        ]
+      },
+      {
+        // Expression keywords prevent 'keyword Name(...)' from being
+        // recognized as a function definition
+        beginKeywords: 'new return throw await else',
+        relevance: 0
+      },
+      {
+        className: 'function',
+        begin: '(' + TYPE_IDENT_RE + '\\s+)+' + hljs.IDENT_RE + '\\s*(<.+>\\s*)?\\(', returnBegin: true,
+        end: /\s*[{;=]/, excludeEnd: true,
+        keywords: KEYWORDS,
+        contains: [
+          // prevents these from being highlighted `title`
+          {
+            beginKeywords: FUNCTION_MODIFIERS.join(" "),
+            relevance: 0
+          },
+          {
+            begin: hljs.IDENT_RE + '\\s*(<.+>\\s*)?\\(', returnBegin: true,
+            contains: [
+              hljs.TITLE_MODE,
+              GENERIC_MODIFIER
+            ],
+            relevance: 0
+          },
+          {
+            className: 'params',
+            begin: /\(/, end: /\)/,
+            excludeBegin: true,
+            excludeEnd: true,
+            keywords: KEYWORDS,
+            relevance: 0,
+            contains: [
+              STRING,
+              NUMBERS,
+              hljs.C_BLOCK_COMMENT_MODE
+            ]
+          },
+          hljs.C_LINE_COMMENT_MODE,
+          hljs.C_BLOCK_COMMENT_MODE
+        ]
+      },
+      AT_IDENTIFIER
+    ]
+  };
+}
+
+module.exports = csharp;
diff --git a/src/third_party/highlightjs/highlightjs/languages/javascript.js b/src/third_party/highlightjs/highlightjs/languages/javascript.js
new file mode 100644
index 0000000000..f2a9e575fe
--- /dev/null
+++ b/src/third_party/highlightjs/highlightjs/languages/javascript.js
@@ -0,0 +1,601 @@
+const IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*';
+const KEYWORDS = [
+  "as", // for exports
+  "in",
+  "of",
+  "if",
+  "for",
+  "while",
+  "finally",
+  "var",
+  "new",
+  "function",
+  "do",
+  "return",
+  "void",
+  "else",
+  "break",
+  "catch",
+  "instanceof",
+  "with",
+  "throw",
+  "case",
+  "default",
+  "try",
+  "switch",
+  "continue",
+  "typeof",
+  "delete",
+  "let",
+  "yield",
+  "const",
+  "class",
+  // JS handles these with a special rule
+  // "get",
+  // "set",
+  "debugger",
+  "async",
+  "await",
+  "static",
+  "import",
+  "from",
+  "export",
+  "extends"
+];
+const LITERALS = [
+  "true",
+  "false",
+  "null",
+  "undefined",
+  "NaN",
+  "Infinity"
+];
+
+const TYPES = [
+  "Intl",
+  "DataView",
+  "Number",
+  "Math",
+  "Date",
+  "String",
+  "RegExp",
+  "Object",
+  "Function",
+  "Boolean",
+  "Error",
+  "Symbol",
+  "Set",
+  "Map",
+  "WeakSet",
+  "WeakMap",
+  "Proxy",
+  "Reflect",
+  "JSON",
+  "Promise",
+  "Float64Array",
+  "Int16Array",
+  "Int32Array",
+  "Int8Array",
+  "Uint16Array",
+  "Uint32Array",
+  "Float32Array",
+  "Array",
+  "Uint8Array",
+  "Uint8ClampedArray",
+  "ArrayBuffer"
+];
+
+const ERROR_TYPES = [
+  "EvalError",
+  "InternalError",
+  "RangeError",
+  "ReferenceError",
+  "SyntaxError",
+  "TypeError",
+  "URIError"
+];
+
+const BUILT_IN_GLOBALS = [
+  "setInterval",
+  "setTimeout",
+  "clearInterval",
+  "clearTimeout",
+
+  "require",
+  "exports",
+
+  "eval",
+  "isFinite",
+  "isNaN",
+  "parseFloat",
+  "parseInt",
+  "decodeURI",
+  "decodeURIComponent",
+  "encodeURI",
+  "encodeURIComponent",
+  "escape",
+  "unescape"
+];
+
+const BUILT_IN_VARIABLES = [
+  "arguments",
+  "this",
+  "super",
+  "console",
+  "window",
+  "document",
+  "localStorage",
+  "module",
+  "global" // Node.js
+];
+
+const BUILT_INS = [].concat(
+  BUILT_IN_GLOBALS,
+  BUILT_IN_VARIABLES,
+  TYPES,
+  ERROR_TYPES
+);
+
+/**
+ * @param {string} value
+ * @returns {RegExp}
+ * */
+
+/**
+ * @param {RegExp | string } re
+ * @returns {string}
+ */
+function source(re) {
+  if (!re) return null;
+  if (typeof re === "string") return re;
+
+  return re.source;
+}
+
+/**
+ * @param {RegExp | string } re
+ * @returns {string}
+ */
+function lookahead(re) {
+  return concat('(?=', re, ')');
+}
+
+/**
+ * @param {...(RegExp | string) } args
+ * @returns {string}
+ */
+function concat(...args) {
+  const joined = args.map((x) => source(x)).join("");
+  return joined;
+}
+
+/*
+Language: JavaScript
+Description: JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions.
+Category: common, scripting
+Website: https://developer.mozilla.org/en-US/docs/Web/JavaScript
+*/
+
+/** @type LanguageFn */
+function javascript(hljs) {
+  /**
+   * Takes a string like " {
+    const tag = "',
+    end: ''
+  };
+  const XML_TAG = {
+    begin: /<[A-Za-z0-9\\._:-]+/,
+    end: /\/[A-Za-z0-9\\._:-]+>|\/>/,
+    /**
+     * @param {RegExpMatchArray} match
+     * @param {CallbackResponse} response
+     */
+    isTrulyOpeningTag: (match, response) => {
+      const afterMatchIndex = match[0].length + match.index;
+      const nextChar = match.input[afterMatchIndex];
+      // nested type?
+      // HTML should not include another raw `<` inside a tag
+      // But a type might: `>`, etc.
+      if (nextChar === "<") {
+        response.ignoreMatch();
+        return;
+      }
+      // 
+      // This is now either a tag or a type.
+      if (nextChar === ">") {
+        // if we cannot find a matching closing tag, then we
+        // will ignore it
+        if (!hasClosingTag(match, { after: afterMatchIndex })) {
+          response.ignoreMatch();
+        }
+      }
+    }
+  };
+  const KEYWORDS$1 = {
+    $pattern: IDENT_RE,
+    keyword: KEYWORDS.join(" "),
+    literal: LITERALS.join(" "),
+    built_in: BUILT_INS.join(" ")
+  };
+
+  // https://tc39.es/ecma262/#sec-literals-numeric-literals
+  const decimalDigits = '[0-9](_?[0-9])*';
+  const frac = `\\.(${decimalDigits})`;
+  // DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral
+  // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
+  const decimalInteger = `0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*`;
+  const NUMBER = {
+    className: 'number',
+    variants: [
+      // DecimalLiteral
+      { begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` +
+        `[eE][+-]?(${decimalDigits})\\b` },
+      { begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` },
+
+      // DecimalBigIntegerLiteral
+      { begin: `\\b(0|[1-9](_?[0-9])*)n\\b` },
+
+      // NonDecimalIntegerLiteral
+      { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b" },
+      { begin: "\\b0[bB][0-1](_?[0-1])*n?\\b" },
+      { begin: "\\b0[oO][0-7](_?[0-7])*n?\\b" },
+
+      // LegacyOctalIntegerLiteral (does not include underscore separators)
+      // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
+      { begin: "\\b0[0-7]+n?\\b" },
+    ],
+    relevance: 0
+  };
+
+  const SUBST = {
+    className: 'subst',
+    begin: '\\$\\{',
+    end: '\\}',
+    keywords: KEYWORDS$1,
+    contains: [] // defined later
+  };
+  const HTML_TEMPLATE = {
+    begin: 'html`',
+    end: '',
+    starts: {
+      end: '`',
+      returnEnd: false,
+      contains: [
+        hljs.BACKSLASH_ESCAPE,
+        SUBST
+      ],
+      subLanguage: 'xml'
+    }
+  };
+  const CSS_TEMPLATE = {
+    begin: 'css`',
+    end: '',
+    starts: {
+      end: '`',
+      returnEnd: false,
+      contains: [
+        hljs.BACKSLASH_ESCAPE,
+        SUBST
+      ],
+      subLanguage: 'css'
+    }
+  };
+  const TEMPLATE_STRING = {
+    className: 'string',
+    begin: '`',
+    end: '`',
+    contains: [
+      hljs.BACKSLASH_ESCAPE,
+      SUBST
+    ]
+  };
+  const JSDOC_COMMENT = hljs.COMMENT(
+    /\/\*\*(?!\/)/,
+    '\\*/',
+    {
+      relevance: 0,
+      contains: [
+        {
+          className: 'doctag',
+          begin: '@[A-Za-z]+',
+          contains: [
+            {
+              className: 'type',
+              begin: '\\{',
+              end: '\\}',
+              relevance: 0
+            },
+            {
+              className: 'variable',
+              begin: IDENT_RE$1 + '(?=\\s*(-)|$)',
+              endsParent: true,
+              relevance: 0
+            },
+            // eat spaces (not newlines) so we can find
+            // types or variables
+            {
+              begin: /(?=[^\n])\s/,
+              relevance: 0
+            }
+          ]
+        }
+      ]
+    }
+  );
+  const COMMENT = {
+    className: "comment",
+    variants: [
+      JSDOC_COMMENT,
+      hljs.C_BLOCK_COMMENT_MODE,
+      hljs.C_LINE_COMMENT_MODE
+    ]
+  };
+  const SUBST_INTERNALS = [
+    hljs.APOS_STRING_MODE,
+    hljs.QUOTE_STRING_MODE,
+    HTML_TEMPLATE,
+    CSS_TEMPLATE,
+    TEMPLATE_STRING,
+    NUMBER,
+    hljs.REGEXP_MODE
+  ];
+  SUBST.contains = SUBST_INTERNALS
+    .concat({
+      // we need to pair up {} inside our subst to prevent
+      // it from ending too early by matching another }
+      begin: /\{/,
+      end: /\}/,
+      keywords: KEYWORDS$1,
+      contains: [
+        "self"
+      ].concat(SUBST_INTERNALS)
+    });
+  const SUBST_AND_COMMENTS = [].concat(COMMENT, SUBST.contains);
+  const PARAMS_CONTAINS = SUBST_AND_COMMENTS.concat([
+    // eat recursive parens in sub expressions
+    {
+      begin: /\(/,
+      end: /\)/,
+      keywords: KEYWORDS$1,
+      contains: ["self"].concat(SUBST_AND_COMMENTS)
+    }
+  ]);
+  const PARAMS = {
+    className: 'params',
+    begin: /\(/,
+    end: /\)/,
+    excludeBegin: true,
+    excludeEnd: true,
+    keywords: KEYWORDS$1,
+    contains: PARAMS_CONTAINS
+  };
+
+  return {
+    name: 'Javascript',
+    aliases: ['js', 'jsx', 'mjs', 'cjs'],
+    keywords: KEYWORDS$1,
+    // this will be extended by TypeScript
+    exports: { PARAMS_CONTAINS },
+    illegal: /#(?![$_A-z])/,
+    contains: [
+      hljs.SHEBANG({
+        label: "shebang",
+        binary: "node",
+        relevance: 5
+      }),
+      {
+        label: "use_strict",
+        className: 'meta',
+        relevance: 10,
+        begin: /^\s*['"]use (strict|asm)['"]/
+      },
+      hljs.APOS_STRING_MODE,
+      hljs.QUOTE_STRING_MODE,
+      HTML_TEMPLATE,
+      CSS_TEMPLATE,
+      TEMPLATE_STRING,
+      COMMENT,
+      NUMBER,
+      { // object attr container
+        begin: concat(/[{,\n]\s*/,
+          // we need to look ahead to make sure that we actually have an
+          // attribute coming up so we don't steal a comma from a potential
+          // "value" container
+          //
+          // NOTE: this might not work how you think.  We don't actually always
+          // enter this mode and stay.  Instead it might merely match `,
+          // ` and then immediately end after the , because it
+          // fails to find any actual attrs. But this still does the job because
+          // it prevents the value contain rule from grabbing this instead and
+          // prevening this rule from firing when we actually DO have keys.
+          lookahead(concat(
+            // we also need to allow for multiple possible comments inbetween
+            // the first key:value pairing
+            /(((\/\/.*$)|(\/\*(\*[^/]|[^*])*\*\/))\s*)*/,
+            IDENT_RE$1 + '\\s*:'))),
+        relevance: 0,
+        contains: [
+          {
+            className: 'attr',
+            begin: IDENT_RE$1 + lookahead('\\s*:'),
+            relevance: 0
+          }
+        ]
+      },
+      { // "value" container
+        begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*',
+        keywords: 'return throw case',
+        contains: [
+          COMMENT,
+          hljs.REGEXP_MODE,
+          {
+            className: 'function',
+            // we have to count the parens to make sure we actually have the
+            // correct bounding ( ) before the =>.  There could be any number of
+            // sub-expressions inside also surrounded by parens.
+            begin: '(\\(' +
+            '[^()]*(\\(' +
+            '[^()]*(\\(' +
+            '[^()]*' +
+            '\\)[^()]*)*' +
+            '\\)[^()]*)*' +
+            '\\)|' + hljs.UNDERSCORE_IDENT_RE + ')\\s*=>',
+            returnBegin: true,
+            end: '\\s*=>',
+            contains: [
+              {
+                className: 'params',
+                variants: [
+                  {
+                    begin: hljs.UNDERSCORE_IDENT_RE,
+                    relevance: 0
+                  },
+                  {
+                    className: null,
+                    begin: /\(\s*\)/,
+                    skip: true
+                  },
+                  {
+                    begin: /\(/,
+                    end: /\)/,
+                    excludeBegin: true,
+                    excludeEnd: true,
+                    keywords: KEYWORDS$1,
+                    contains: PARAMS_CONTAINS
+                  }
+                ]
+              }
+            ]
+          },
+          { // could be a comma delimited list of params to a function call
+            begin: /,/, relevance: 0
+          },
+          {
+            className: '',
+            begin: /\s/,
+            end: /\s*/,
+            skip: true
+          },
+          { // JSX
+            variants: [
+              { begin: FRAGMENT.begin, end: FRAGMENT.end },
+              {
+                begin: XML_TAG.begin,
+                // we carefully check the opening tag to see if it truly
+                // is a tag and not a false positive
+                'on:begin': XML_TAG.isTrulyOpeningTag,
+                end: XML_TAG.end
+              }
+            ],
+            subLanguage: 'xml',
+            contains: [
+              {
+                begin: XML_TAG.begin,
+                end: XML_TAG.end,
+                skip: true,
+                contains: ['self']
+              }
+            ]
+          }
+        ],
+        relevance: 0
+      },
+      {
+        className: 'function',
+        beginKeywords: 'function',
+        end: /[{;]/,
+        excludeEnd: true,
+        keywords: KEYWORDS$1,
+        contains: [
+          'self',
+          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE$1 }),
+          PARAMS
+        ],
+        illegal: /%/
+      },
+      {
+        // prevent this from getting swallowed up by function
+        // since they appear "function like"
+        beginKeywords: "while if switch catch for"
+      },
+      {
+        className: 'function',
+        // we have to count the parens to make sure we actually have the correct
+        // bounding ( ).  There could be any number of sub-expressions inside
+        // also surrounded by parens.
+        begin: hljs.UNDERSCORE_IDENT_RE +
+          '\\(' + // first parens
+          '[^()]*(\\(' +
+            '[^()]*(\\(' +
+              '[^()]*' +
+            '\\)[^()]*)*' +
+          '\\)[^()]*)*' +
+          '\\)\\s*\\{', // end parens
+        returnBegin:true,
+        contains: [
+          PARAMS,
+          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE$1 }),
+        ]
+      },
+      // hack: prevents detection of keywords in some circumstances
+      // .keyword()
+      // $keyword = x
+      {
+        variants: [
+          { begin: '\\.' + IDENT_RE$1 },
+          { begin: '\\$' + IDENT_RE$1 }
+        ],
+        relevance: 0
+      },
+      { // ES6 class
+        className: 'class',
+        beginKeywords: 'class',
+        end: /[{;=]/,
+        excludeEnd: true,
+        illegal: /[:"[\]]/,
+        contains: [
+          { beginKeywords: 'extends' },
+          hljs.UNDERSCORE_TITLE_MODE
+        ]
+      },
+      {
+        begin: /\b(?=constructor)/,
+        end: /[{;]/,
+        excludeEnd: true,
+        contains: [
+          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE$1 }),
+          'self',
+          PARAMS
+        ]
+      },
+      {
+        begin: '(get|set)\\s+(?=' + IDENT_RE$1 + '\\()',
+        end: /\{/,
+        keywords: "get set",
+        contains: [
+          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE$1 }),
+          { begin: /\(\)/ }, // eat to avoid empty params
+          PARAMS
+        ]
+      },
+      {
+        begin: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something`
+      }
+    ]
+  };
+}
+
+module.exports = javascript;
diff --git a/src/third_party/highlightjs/highlightjs/languages/python.js b/src/third_party/highlightjs/highlightjs/languages/python.js
new file mode 100644
index 0000000000..bc2b2213af
--- /dev/null
+++ b/src/third_party/highlightjs/highlightjs/languages/python.js
@@ -0,0 +1,289 @@
+/*
+Language: Python
+Description: Python is an interpreted, object-oriented, high-level programming language with dynamic semantics.
+Website: https://www.python.org
+Category: common
+*/
+
+function python(hljs) {
+  const RESERVED_WORDS = [
+    'and',
+    'as',
+    'assert',
+    'async',
+    'await',
+    'break',
+    'class',
+    'continue',
+    'def',
+    'del',
+    'elif',
+    'else',
+    'except',
+    'finally',
+    'for',
+    '',
+    'from',
+    'global',
+    'if',
+    'import',
+    'in',
+    'is',
+    'lambda',
+    'nonlocal|10',
+    'not',
+    'or',
+    'pass',
+    'raise',
+    'return',
+    'try',
+    'while',
+    'with',
+    'yield',
+  ];
+
+  const BUILT_INS = [
+    '__import__',
+    'abs',
+    'all',
+    'any',
+    'ascii',
+    'bin',
+    'bool',
+    'breakpoint',
+    'bytearray',
+    'bytes',
+    'callable',
+    'chr',
+    'classmethod',
+    'compile',
+    'complex',
+    'delattr',
+    'dict',
+    'dir',
+    'divmod',
+    'enumerate',
+    'eval',
+    'exec',
+    'filter',
+    'float',
+    'format',
+    'frozenset',
+    'getattr',
+    'globals',
+    'hasattr',
+    'hash',
+    'help',
+    'hex',
+    'id',
+    'input',
+    'int',
+    'isinstance',
+    'issubclass',
+    'iter',
+    'len',
+    'list',
+    'locals',
+    'map',
+    'max',
+    'memoryview',
+    'min',
+    'next',
+    'object',
+    'oct',
+    'open',
+    'ord',
+    'pow',
+    'print',
+    'property',
+    'range',
+    'repr',
+    'reversed',
+    'round',
+    'set',
+    'setattr',
+    'slice',
+    'sorted',
+    'staticmethod',
+    'str',
+    'sum',
+    'super',
+    'tuple',
+    'type',
+    'vars',
+    'zip',
+  ];
+
+  const LITERALS = [
+    '__debug__',
+    'Ellipsis',
+    'False',
+    'None',
+    'NotImplemented',
+    'True',
+  ];
+
+  const KEYWORDS = {
+    keyword: RESERVED_WORDS.join(' '),
+    built_in: BUILT_INS.join(' '),
+    literal: LITERALS.join(' ')
+  };
+
+  const PROMPT = {
+    className: 'meta',  begin: /^(>>>|\.\.\.) /
+  };
+
+  const SUBST = {
+    className: 'subst',
+    begin: /\{/, end: /\}/,
+    keywords: KEYWORDS,
+    illegal: /#/
+  };
+
+  const LITERAL_BRACKET = {
+    begin: /\{\{/,
+    relevance: 0
+  };
+
+  const STRING = {
+    className: 'string',
+    contains: [hljs.BACKSLASH_ESCAPE],
+    variants: [
+      {
+        begin: /([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/, end: /'''/,
+        contains: [hljs.BACKSLASH_ESCAPE, PROMPT],
+        relevance: 10
+      },
+      {
+        begin: /([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/, end: /"""/,
+        contains: [hljs.BACKSLASH_ESCAPE, PROMPT],
+        relevance: 10
+      },
+      {
+        begin: /([fF][rR]|[rR][fF]|[fF])'''/, end: /'''/,
+        contains: [hljs.BACKSLASH_ESCAPE, PROMPT, LITERAL_BRACKET, SUBST]
+      },
+      {
+        begin: /([fF][rR]|[rR][fF]|[fF])"""/, end: /"""/,
+        contains: [hljs.BACKSLASH_ESCAPE, PROMPT, LITERAL_BRACKET, SUBST]
+      },
+      {
+        begin: /([uU]|[rR])'/, end: /'/,
+        relevance: 10
+      },
+      {
+        begin: /([uU]|[rR])"/, end: /"/,
+        relevance: 10
+      },
+      {
+        begin: /([bB]|[bB][rR]|[rR][bB])'/, end: /'/
+      },
+      {
+        begin: /([bB]|[bB][rR]|[rR][bB])"/, end: /"/
+      },
+      {
+        begin: /([fF][rR]|[rR][fF]|[fF])'/, end: /'/,
+        contains: [hljs.BACKSLASH_ESCAPE, LITERAL_BRACKET, SUBST]
+      },
+      {
+        begin: /([fF][rR]|[rR][fF]|[fF])"/, end: /"/,
+        contains: [hljs.BACKSLASH_ESCAPE, LITERAL_BRACKET, SUBST]
+      },
+      hljs.APOS_STRING_MODE,
+      hljs.QUOTE_STRING_MODE
+    ]
+  };
+
+  // https://docs.python.org/3.9/reference/lexical_analysis.html#numeric-literals
+  const digitpart = '[0-9](_?[0-9])*';
+  const pointfloat = `(\\b(${digitpart}))?\\.(${digitpart})|\\b(${digitpart})\\.`;
+  const NUMBER = {
+    className: 'number', relevance: 0,
+    variants: [
+      // exponentfloat, pointfloat
+      // https://docs.python.org/3.9/reference/lexical_analysis.html#floating-point-literals
+      // optionally imaginary
+      // https://docs.python.org/3.9/reference/lexical_analysis.html#imaginary-literals
+      // Note: no leading \b because floats can start with a decimal point
+      // and we don't want to mishandle e.g. `fn(.5)`,
+      // no trailing \b for pointfloat because it can end with a decimal point
+      // and we don't want to mishandle e.g. `0..hex()`; this should be safe
+      // because both MUST contain a decimal point and so cannot be confused with
+      // the interior part of an identifier
+      { begin: `(\\b(${digitpart})|(${pointfloat}))[eE][+-]?(${digitpart})[jJ]?\\b` },
+      { begin: `(${pointfloat})[jJ]?` },
+
+      // decinteger, bininteger, octinteger, hexinteger
+      // https://docs.python.org/3.9/reference/lexical_analysis.html#integer-literals
+      // optionally "long" in Python 2
+      // https://docs.python.org/2.7/reference/lexical_analysis.html#integer-and-long-integer-literals
+      // decinteger is optionally imaginary
+      // https://docs.python.org/3.9/reference/lexical_analysis.html#imaginary-literals
+      { begin: '\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?\\b' },
+      { begin: '\\b0[bB](_?[01])+[lL]?\\b' },
+      { begin: '\\b0[oO](_?[0-7])+[lL]?\\b' },
+      { begin: '\\b0[xX](_?[0-9a-fA-F])+[lL]?\\b' },
+
+      // imagnumber (digitpart-based)
+      // https://docs.python.org/3.9/reference/lexical_analysis.html#imaginary-literals
+      { begin: `\\b(${digitpart})[jJ]\\b` },
+    ]
+  };
+
+  const PARAMS = {
+    className: 'params',
+    variants: [
+      // Exclude params at functions without params
+      {begin: /\(\s*\)/, skip: true, className: null },
+      {
+        begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true,
+        keywords: KEYWORDS,
+        contains: ['self', PROMPT, NUMBER, STRING, hljs.HASH_COMMENT_MODE],
+      },
+    ],
+  };
+  SUBST.contains = [STRING, NUMBER, PROMPT];
+
+  return {
+    name: 'Python',
+    aliases: ['py', 'gyp', 'ipython'],
+    keywords: KEYWORDS,
+    illegal: /(<\/|->|\?)|=>/,
+    contains: [
+      PROMPT,
+      NUMBER,
+      // eat "if" prior to string so that it won't accidentally be
+      // labeled as an f-string as in:
+      { begin: /\bself\b/, }, // very common convention
+      { beginKeywords: "if", relevance: 0 },
+      STRING,
+      hljs.HASH_COMMENT_MODE,
+      {
+        variants: [
+          {className: 'function', beginKeywords: 'def'},
+          {className: 'class', beginKeywords: 'class'}
+        ],
+        end: /:/,
+        illegal: /[${=;\n,]/,
+        contains: [
+          hljs.UNDERSCORE_TITLE_MODE,
+          PARAMS,
+          {
+            begin: /->/, endsWithParent: true,
+            keywords: 'None'
+          }
+        ]
+      },
+      {
+        className: 'meta',
+        begin: /^[\t ]*@/, end: /(?=#)|$/,
+        contains: [NUMBER, PARAMS, STRING]
+      },
+      {
+        begin: /\b(print|exec)\(/ // don’t highlight keywords-turned-functions in Python 3
+      }
+    ]
+  };
+}
+
+module.exports = python;
diff --git a/src/third_party/highlightjs/roll.sh b/src/third_party/highlightjs/roll.sh
new file mode 100755
index 0000000000..e8cceb87d4
--- /dev/null
+++ b/src/third_party/highlightjs/roll.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+set -e
+set +x
+
+# Pick a stable release revision from here:
+# https://github.com/highlightjs/highlight.js/releases
+RELEASE_REVISION="af20048d5c601d6e30016d8171317bfdf8a6c242"
+LANGUAGES="javascript python csharp"
+
+trap "cd $(pwd -P)" EXIT
+SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
+
+cd "$(dirname "$0")"
+rm -rf ./output
+mkdir -p ./output
+
+cd ./output
+git clone git@github.com:highlightjs/highlight.js.git
+cd ./highlight.js
+git checkout ${RELEASE_REVISION}
+npm install
+node tools/build.js -t node ${LANGUAGES}
+
+cd ../..
+rm -rf ./highlightjs
+mkdir -p ./highlightjs
+cp -R output/highlight.js/build/lib/* highlightjs/
+cp output/highlight.js/build/LICENSE highlightjs/
+cp output/highlight.js/build/types/index.d.ts highlightjs/
+echo $'\n'"export = hljs;"$'\n' >> highlightjs/index.d.ts
+rm -rf ./output
diff --git a/utils/check_deps.js b/utils/check_deps.js
index 26ca72c754..3ceaa043c6 100644
--- a/utils/check_deps.js
+++ b/utils/check_deps.js
@@ -62,6 +62,8 @@ async function checkDeps() {
   function allowImport(from, to) {
     if (!to.startsWith(src + path.sep))
       return true;
+    if (!fs.existsSync(to))
+      return true;
     from = path.relative(root, from).replace(/\\/g, '/');
     to = path.relative(root, to).replace(/\\/g, '/');
     const fromDirectory = from.substring(0, from.lastIndexOf('/') + 1);