diff options
author | Jim Mason <jmason@ibinx.com> | 2020-06-12 16:47:37 +0100 |
---|---|---|
committer | Jim Mason <jmason@ibinx.com> | 2020-06-14 10:57:58 +0100 |
commit | 9cb2748867137b1e1fccc16fabca32427df075f1 (patch) | |
tree | 35a2b7638017f40c22b35582094235d72e9536d6 | |
parent | b584710b9e8af9f33ac1a7f993c90fb290b63dfd (diff) | |
download | epiphany-9cb2748867137b1e1fccc16fabca32427df075f1.tar.gz |
highlight.js: update to version 10.1.0
fixes #1230
-rw-r--r-- | third-party/highlightjs/highlight.js | 1403 |
1 files changed, 1043 insertions, 360 deletions
diff --git a/third-party/highlightjs/highlight.js b/third-party/highlightjs/highlight.js index c62a3082e..5d3214cb6 100644 --- a/third-party/highlightjs/highlight.js +++ b/third-party/highlightjs/highlight.js @@ -1,5 +1,5 @@ /* - Highlight.js 10.0.2 (e29f8f7d) + Highlight.js 10.1.0 (74de6eaa) License: BSD-3-Clause Copyright (c) 2006-2020, Ivan Sagalaev */ @@ -7,61 +7,106 @@ var hljs = (function () { 'use strict'; // https://github.com/substack/deep-freeze/blob/master/index.js - function deepFreeze (o) { - Object.freeze(o); + + function deepFreeze(obj) { + Object.freeze(obj); - var objIsFunction = typeof o === 'function'; + var objIsFunction = typeof obj === 'function'; - Object.getOwnPropertyNames(o).forEach(function (prop) { - if (o.hasOwnProperty(prop) - && o[prop] !== null - && (typeof o[prop] === "object" || typeof o[prop] === "function") + Object.getOwnPropertyNames(obj).forEach(function(prop) { + if (Object.hasOwnProperty.call(obj, prop) + && obj[prop] !== null + && (typeof obj[prop] === "object" || typeof obj[prop] === "function") // IE11 fix: https://github.com/highlightjs/highlight.js/issues/2318 // TODO: remove in the future && (objIsFunction ? prop !== 'caller' && prop !== 'callee' && prop !== 'arguments' : true) - && !Object.isFrozen(o[prop])) { - deepFreeze(o[prop]); + && !Object.isFrozen(obj[prop])) { + deepFreeze(obj[prop]); } }); - return o; + return obj; } - function escapeHTML(value) { - return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); + 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, '"') + .replace(/'/g, '''); + } - - function inherit(parent) { // inherit(parent, override_obj, override_obj, ...) - var key; + /** + * performs a shallow merge of multiple objects into one + * + * @template T + * @param {T} original + * @param {Record<string,any>[]} objects + * @returns {T} a single new object + */ + function inherit(original, ...objects) { + /** @type Record<string,any> */ var result = {}; - var objects = Array.prototype.slice.call(arguments, 1); - for (key in parent) - result[key] = parent[key]; + for (const key in original) { + result[key] = original[key]; + } objects.forEach(function(obj) { - for (key in obj) + for (const key in obj) { result[key] = obj[key]; + } }); - return result; + return /** @type {T} */ (result); } /* Stream merging */ + /** + * @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[] */ var result = []; (function _nodeStream(node, offset) { for (var child = node.firstChild; child; child = child.nextSibling) { - if (child.nodeType === 3) + if (child.nodeType === 3) { offset += child.nodeValue.length; - else if (child.nodeType === 1) { + } else if (child.nodeType === 1) { result.push({ event: 'start', offset: offset, @@ -85,6 +130,11 @@ var hljs = (function () { 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) { var processed = 0; var result = ''; @@ -116,17 +166,28 @@ var hljs = (function () { return highlighted[0].event === 'start' ? original : highlighted; } + /** + * @param {Node} node + */ function open(node) { - function attr_str(a) { - return ' ' + a.nodeName + '="' + escapeHTML(a.value).replace(/"/g, '"') + '"'; + /** @param {Attr} attr */ + function attr_str(attr) { + return ' ' + attr.nodeName + '="' + escapeHTML(attr.value) + '"'; } + // @ts-ignore result += '<' + tag(node) + [].map.call(node.attributes, attr_str).join('') + '>'; } + /** + * @param {Node} node + */ function close(node) { result += '</' + tag(node) + '>'; } + /** + * @param {Event} event + */ function render(event) { (event.event === 'start' ? open : close)(event.node); } @@ -168,76 +229,128 @@ var hljs = (function () { mergeStreams: mergeStreams }); + /** + * @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 = '</span>'; + /** + * Determines if a node needs to be wrapped in <span> + * + * @param {Node} node */ const emitsWrappingTags = (node) => { return !!node.kind; }; + /** @type {Renderer} */ class HTMLRenderer { - constructor(tree, options) { + /** + * 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; - tree.walk(this); + parseTree.walk(this); } - // renderer API - + /** + * 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) + 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 += `<span class="${className}">`; } - - value() { - return 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 ]; + this.stack = [this.rootNode]; } get top() { return this.stack[this.stack.length - 1]; } - get root() { return this.rootNode }; + get root() { return this.rootNode; } + /** @param {Node} node */ add(node) { this.top.children.push(node); } + /** @param {string} kind */ openNode(kind) { - let node = { kind, children: [] }; + /** @type Node */ + const node = { kind, children: [] }; this.add(node); this.stack.push(node); } closeNode() { - if (this.stack.length > 1) + if (this.stack.length > 1) { return this.stack.pop(); + } + // eslint-disable-next-line no-undefined + return undefined; } closeAllNodes() { @@ -248,10 +361,21 @@ var hljs = (function () { 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); @@ -263,16 +387,19 @@ var hljs = (function () { return builder; } + /** + * @param {Node} node + */ static _collapse(node) { - if (!node.children) { - return; - } + 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.text = node.children.join(""); + // delete node.children; + node.children = [node.children.join("")]; } else { node.children.forEach((child) => { - if (typeof child === "string") return; TokenTree._collapse(child); }); } @@ -287,7 +414,7 @@ var hljs = (function () { - addKeyword(text, kind) - addText(text) - - addSublanguage(emitter, subLangaugeName) + - addSublanguage(emitter, subLanguageName) - finalize() - openNode(kind) - closeNode() @@ -295,12 +422,23 @@ var hljs = (function () { - 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; } @@ -309,44 +447,78 @@ var hljs = (function () { this.closeNode(); } + /** + * @param {string} text + */ addText(text) { if (text === "") { return; } this.add(text); } + /** + * @param {Emitter & {root: DataNode}} emitter + * @param {string} name + */ addSublanguage(emitter, name) { - let node = emitter.root; + /** @type DataNode */ + const node = emitter.root; node.kind = name; node.sublanguage = true; this.add(node); } toHTML() { - let renderer = new HTMLRenderer(this, this.options); + const renderer = new HTMLRenderer(this, this.options); return renderer.value(); } finalize() { - return; + return true; } - } + /** + * @param {string} value + * @returns {RegExp} + * */ function escape(value) { - return new RegExp(value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'm'); + return new RegExp(value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'm'); } + /** + * @param {RegExp | string } re + * @returns {string} + */ function source(re) { - // if it's a regex get it's source, - // otherwise it's a string already so just return it - return (re && re.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; } + /** + * @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) { var match = re && re.exec(lexeme); return match && match.index === 0; @@ -357,7 +529,12 @@ var hljs = (function () { // 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. :-) - function join(regexps, separator) { + /** + * @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 @@ -384,12 +561,12 @@ var hljs = (function () { } ret += re.substring(0, match.index); re = re.substring(match.index + match[0].length); - if (match[0][0] == '\\' && match[1]) { + if (match[0][0] === '\\' && match[1]) { // Adjust the backreference. ret += '\\' + String(Number(match[1]) + offset); } else { ret += match[0]; - if (match[0] == '(') { + if (match[0] === '(') { numCaptures++; } } @@ -407,38 +584,73 @@ var hljs = (function () { const BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b... const RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~'; + /** + * @param { Partial<Mode> & {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: '\'', + begin: '\'', + end: '\'', illegal: '\\n', contains: [BACKSLASH_ESCAPE] }; const QUOTE_STRING_MODE = { className: 'string', - begin: '"', end: '"', + 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/ }; - const COMMENT = function (begin, end, inherits) { + /** + * Creates a comment mode + * + * @param {string | RegExp} begin + * @param {string | RegExp} end + * @param {Mode | {}} [modeOptions] + * @returns {Partial<Mode>} + */ + const COMMENT = function(begin, end, modeOptions = {}) { var mode = inherit( { className: 'comment', - begin: begin, end: end, + begin, + end, contains: [] }, - inherits || {} + modeOptions ); mode.contains.push(PHRASAL_WORDS_MODE); mode.contains.push({ className: 'doctag', - begin: '(?:TODO|FIXME|NOTE|BUG|XXX):', + begin: '(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):', relevance: 0 }); return mode; @@ -464,7 +676,7 @@ var hljs = (function () { const CSS_NUMBER_MODE = { className: 'number', begin: NUMBER_RE + '(' + - '%|em|ex|ch|rem' + + '%|em|ex|ch|rem' + '|vw|vh|vmin|vmax' + '|cm|mm|in|pt|pc|px' + '|deg|grad|rad|turn' + @@ -481,15 +693,17 @@ var hljs = (function () { // 3 / something // // (which will then blow up when regex's `illegal` sees the newline) - begin: /(?=\/[^\/\n]*\/)/, + begin: /(?=\/[^/\n]*\/)/, contains: [{ className: 'regexp', - begin: /\//, end: /\/[gimuy]*/, + begin: /\//, + end: /\/[gimuy]*/, illegal: /\n/, contains: [ BACKSLASH_ESCAPE, { - begin: /\[/, end: /\]/, + begin: /\[/, + end: /\]/, relevance: 0, contains: [BACKSLASH_ESCAPE] } @@ -512,6 +726,23 @@ var hljs = (function () { 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>} 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, @@ -520,6 +751,7 @@ var hljs = (function () { 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, @@ -535,7 +767,8 @@ var hljs = (function () { REGEXP_MODE: REGEXP_MODE, TITLE_MODE: TITLE_MODE, UNDERSCORE_TITLE_MODE: UNDERSCORE_TITLE_MODE, - METHOD_GUARD: METHOD_GUARD + METHOD_GUARD: METHOD_GUARD, + END_SAME_AS_BEGIN: END_SAME_AS_BEGIN }); // keywords that should have no default relevance value @@ -543,8 +776,21 @@ var hljs = (function () { // 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 + * @returns {CompiledLanguage} + */ function compileLanguage(language) { - + /** + * 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), @@ -568,13 +814,16 @@ var hljs = (function () { 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; @@ -583,20 +832,27 @@ var hljs = (function () { compile() { if (this.regexes.length === 0) { // avoids the need to check length every time exec is called + // @ts-ignore this.exec = () => null; } - let terminators = this.regexes.map(el => el[1]); - this.matcherRe = langRe(join(terminators, '|'), true); + 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; - let match = this.matcherRe.exec(s); + const match = this.matcherRe.exec(s); if (!match) { return null; } - let i = match.findIndex((el, i) => i>0 && el!=undefined); - let matchData = this.matchIndexes[i]; + // 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); } @@ -635,7 +891,9 @@ var hljs = (function () { */ class ResumableMultiRegex { constructor() { + // @ts-ignore this.rules = []; + // @ts-ignore this.multiRegexes = []; this.count = 0; @@ -643,11 +901,12 @@ var hljs = (function () { this.regexIndex = 0; } + // @ts-ignore getMatcher(index) { if (this.multiRegexes[index]) return this.multiRegexes[index]; - let matcher = new MultiRegex(); - this.rules.slice(index).forEach(([re, opts])=> matcher.addRule(re,opts)); + const matcher = new MultiRegex(); + this.rules.slice(index).forEach(([re, opts]) => matcher.addRule(re, opts)); matcher.compile(); this.multiRegexes[index] = matcher; return matcher; @@ -657,19 +916,22 @@ var hljs = (function () { this.regexIndex = 0; } + // @ts-ignore addRule(re, opts) { this.rules.push([re, opts]); - if (opts.type==="begin") this.count++; + if (opts.type === "begin") this.count++; } + /** @param {string} s */ exec(s) { - let m = this.getMatcher(this.regexIndex); + const m = this.getMatcher(this.regexIndex); m.lastIndex = this.lastIndex; - let result = m.exec(s); + const result = m.exec(s); if (result) { this.regexIndex += result.position + 1; - if (this.regexIndex === this.count) // wrap-around + if (this.regexIndex === this.count) { // wrap-around this.regexIndex = 0; + } } // this.regexIndex = 0; @@ -677,26 +939,43 @@ var hljs = (function () { } } + /** + * 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(); - let mm = new ResumableMultiRegex(); - - mode.contains.forEach(term => mm.addRule(term.begin, {rule: term, type: "begin" })); + mode.contains.forEach(term => mm.addRule(term.begin, { rule: term, type: "begin" })); - if (mode.terminator_end) - mm.addRule(mode.terminator_end, {type: "end"} ); - if (mode.illegal) - mm.addRule(mode.illegal, {type: "illegal"} ); + if (mode.terminator_end) { + mm.addRule(mode.terminator_end, { type: "end" }); + } + if (mode.illegal) { + mm.addRule(mode.illegal, { type: "illegal" }); + } return mm; } // TODO: We need negative look-behind support to do this properly - function skipIfhasPrecedingOrTrailingDot(match) { - let before = match.input[match.index-1]; - let after = match.input[match.index + match[0].length]; + /** + * Skip a match if it has a preceding or trailing 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 skipIfhasPrecedingOrTrailingDot(match, response) { + const before = match.input[match.index - 1]; + const after = match.input[match.index + match[0].length]; if (before === "." || after === ".") { - return {ignoreMatch: true }; + response.ignoreMatch(); } } @@ -730,19 +1009,43 @@ var hljs = (function () { * - 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) { - if (mode.compiled) - return; + const cmode = /** @type CompiledMode */ (mode); + if (mode.compiled) return cmode; mode.compiled = true; - // __onBegin is considered private API, internal use only - mode.__onBegin = null; + // __beforeBegin is considered private API, internal use only + mode.__beforeBegin = null; mode.keywords = mode.keywords || mode.beginKeywords; - if (mode.keywords) + + let kw_pattern = null; + if (typeof mode.keywords === "object") { + kw_pattern = 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 && kw_pattern) { + throw new Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) "); + } - mode.lexemesRe = langRe(mode.lexemes || /\w+/, true); + // `mode.lexemes` was the old standard before we added and now recommend + // using `keywords.$pattern` to pass the keyword pattern + cmode.keywordPatternRe = langRe(mode.lexemes || kw_pattern || /\w+/, true); if (parent) { if (mode.beginKeywords) { @@ -752,122 +1055,171 @@ var hljs = (function () { // 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.__onBegin = skipIfhasPrecedingOrTrailingDot; + mode.__beforeBegin = skipIfhasPrecedingOrTrailingDot; + } + 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.terminator_end = source(mode.end) || ''; + if (mode.endsWithParent && parent.terminator_end) { + cmode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end; } - if (!mode.begin) - mode.begin = /\B|\b/; - mode.beginRe = langRe(mode.begin); - if (mode.endSameAsBegin) - mode.end = mode.begin; - if (!mode.end && !mode.endsWithParent) - mode.end = /\B|\b/; - if (mode.end) - mode.endRe = langRe(mode.end); - mode.terminator_end = source(mode.end) || ''; - if (mode.endsWithParent && parent.terminator_end) - mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end; - } - if (mode.illegal) - mode.illegalRe = langRe(mode.illegal); - if (mode.relevance == null) - mode.relevance = 1; - if (!mode.contains) { - mode.contains = []; } + if (mode.illegal) cmode.illegalRe = langRe(mode.illegal); + // eslint-disable-next-line no-undefined + if (mode.relevance === undefined) mode.relevance = 1; + if (!mode.contains) mode.contains = []; + mode.contains = [].concat(...mode.contains.map(function(c) { return expand_or_clone_mode(c === 'self' ? mode : c); })); - mode.contains.forEach(function(c) {compileMode(c, mode);}); + mode.contains.forEach(function(c) { compileMode(/** @type Mode */ (c), cmode); }); if (mode.starts) { compileMode(mode.starts, parent); } - mode.matcher = buildModeRegex(mode); + cmode.matcher = buildModeRegex(cmode); + return cmode; } // 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.") + throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation."); } - compileMode(language); + 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 expand_or_clone_mode(mode) { if (mode.variants && !mode.cached_variants) { mode.cached_variants = mode.variants.map(function(variant) { - return inherit(mode, {variants: null}, 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.cached_variants) + if (mode.cached_variants) { return mode.cached_variants; + } // 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)) + if (dependencyOnParent(mode)) { return inherit(mode, { starts: mode.starts ? inherit(mode.starts) : null }); + } - if (Object.isFrozen(mode)) + if (Object.isFrozen(mode)) { return inherit(mode); + } // no special dependency issues, just return ourselves return mode; } + /*********************************************** + Keywords + ***********************************************/ - // keywords - + /** + * Given raw keywords from a language definition, compile them. + * + * @param {string | Record<string,string>} rawKeywords + * @param {boolean} case_insensitive + */ function compileKeywords(rawKeywords, case_insensitive) { + /** @type KeywordDict */ var compiled_keywords = {}; if (typeof rawKeywords === 'string') { // string splitAndCompile('keyword', rawKeywords); } else { - Object.keys(rawKeywords).forEach(function (className) { + Object.keys(rawKeywords).forEach(function(className) { splitAndCompile(className, rawKeywords[className]); }); } - return compiled_keywords; + return compiled_keywords; - // --- + // --- - function splitAndCompile(className, str) { - if (case_insensitive) { - str = str.toLowerCase(); + /** + * Compiles an individual list of keywords + * + * Ex: "for if when while|5" + * + * @param {string} className + * @param {string} keywordList + */ + function splitAndCompile(className, keywordList) { + if (case_insensitive) { + keywordList = keywordList.toLowerCase(); + } + keywordList.split(' ').forEach(function(keyword) { + var pair = keyword.split('|'); + compiled_keywords[pair[0]] = [className, scoreForKeyword(pair[0], pair[1])]; + }); } - str.split(' ').forEach(function(keyword) { - var pair = keyword.split('|'); - compiled_keywords[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); + // 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; + return commonKeyword(keyword) ? 0 : 1; } - function commonKeyword(word) { - return COMMON_KEYWORDS.includes(word.toLowerCase()); + /** + * Determines if a given keyword is common or not + * + * @param {string} keyword */ + function commonKeyword(keyword) { + return COMMON_KEYWORDS.includes(keyword.toLowerCase()); } - var version = "10.0.2"; + var version = "10.1.0"; /* Syntax highlighting with language autodetection. @@ -878,36 +1230,42 @@ var hljs = (function () { const inherit$1 = inherit; const { nodeStream: nodeStream$1, mergeStreams: mergeStreams$1 } = utils; + const NO_MATCH = Symbol("nomatch"); - + /** + * @param {any} hljs - object that is extended (legacy) + */ const HLJS = function(hljs) { - // Convenience variables for build-in objects + /** @type {unknown[]} */ var ArrayProto = []; // Global internal variables used within the highlight.js library. - var languages = {}, - aliases = {}, - plugins = []; + /** @type {Record<string, Language>} */ + var languages = {}; + /** @type {Record<string, string>} */ + var aliases = {}; + /** @type {HLJSPlugin[]} */ + var plugins = []; // safe/production mode - swallows more errors, tries to keep running // even if a single syntax or parse hits a fatal error var SAFE_MODE = true; - - // Regular expressions used throughout the highlight.js library. - var fixMarkupRe = /((^(<[^>]+>|\t|)+|(?:\n)))/gm; - + var fixMarkupRe = /(^(<[^>]+>|\t|)+|\n)/gm; var 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 */ var options = { noHighlightRe: /^(no-?highlight)$/i, languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, classPrefix: 'hljs-', tabReplace: null, useBR: false, - languages: undefined, + languages: null, // beta configuration options, subject to change, welcome to discuss // https://github.com/highlightjs/highlight.js/issues/1086 __emitter: TokenTreeEmitter @@ -915,18 +1273,24 @@ var hljs = (function () { /* Utility functions */ - function shouldNotHighlight(language) { - return options.noHighlightRe.test(language); + /** + * 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) { - var match; var classes = block.className + ' '; classes += block.parentNode ? block.parentNode.className : ''; // language-* takes precedence over non-prefixed class names. - match = options.languageDetectRe.exec(classes); + const match = options.languageDetectRe.exec(classes); if (match) { var language = getLanguage(match[1]); if (!language) { @@ -946,18 +1310,19 @@ var hljs = (function () { * * @param {string} languageName - the language to use for highlighting * @param {string} code - the code to highlight - * @param {boolean} ignore_illegals - whether to ignore illegal matches, default is to bail - * @param {array<mode>} continuation - array of continuation modes + * @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail + * @param {Mode} [continuation] - current continuation mode, if any * - * @returns an object that represents the result + * @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 {mode} top - top of the current mode stack + * @property {Mode} top - top of the current mode stack * @property {boolean} illegal - indicates whether any illegal matches were found */ - function highlight(languageName, code, ignore_illegals, continuation) { + function highlight(languageName, code, ignoreIllegals, continuation) { + /** @type {{ code: string, language: string, result?: any }} */ var context = { code, language: languageName @@ -970,7 +1335,7 @@ var hljs = (function () { // in which case we don't even need to call highlight var result = context.result ? context.result : - _highlight(context.language, context.code, ignore_illegals, continuation); + _highlight(context.language, context.code, ignoreIllegals, continuation); result.code = context.code; // the plugin can change anything in result to suite it @@ -979,56 +1344,54 @@ var hljs = (function () { return result; } - // private highlight that's used internally and does not fire callbacks - function _highlight(languageName, code, ignore_illegals, continuation) { + /** + * 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 {Mode} [continuation] - current continuation mode, if any + */ + function _highlight(languageName, code, ignoreIllegals, continuation) { var codeToHighlight = code; - function endOfMode(mode, lexeme) { - if (startsWith(mode.endRe, lexeme)) { - while (mode.endsParent && mode.parent) { - mode = mode.parent; - } - return mode; - } - if (mode.endsWithParent) { - return endOfMode(mode.parent, lexeme); - } - } - - function keywordMatch(mode, match) { - var match_str = language.case_insensitive ? match[0].toLowerCase() : match[0]; - return mode.keywords.hasOwnProperty(match_str) && mode.keywords[match_str]; + /** + * 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) { + var matchText = language.case_insensitive ? match[0].toLowerCase() : match[0]; + return Object.prototype.hasOwnProperty.call(mode.keywords, matchText) && mode.keywords[matchText]; } function processKeywords() { - var keyword_match, last_index, match, buf; - if (!top.keywords) { emitter.addText(mode_buffer); return; } - last_index = 0; - top.lexemesRe.lastIndex = 0; - match = top.lexemesRe.exec(mode_buffer); - buf = ""; + let last_index = 0; + top.keywordPatternRe.lastIndex = 0; + let match = top.keywordPatternRe.exec(mode_buffer); + let buf = ""; while (match) { buf += mode_buffer.substring(last_index, match.index); - keyword_match = keywordMatch(top, match); - var kind = null; - if (keyword_match) { + const data = keywordData(top, match); + if (data) { + const [kind, keywordRelevance] = data; emitter.addText(buf); buf = ""; - relevance += keyword_match[1]; - kind = keyword_match[0]; + relevance += keywordRelevance; emitter.addKeyword(match[0], kind); } else { buf += match[0]; } - last_index = top.lexemesRe.lastIndex; - match = top.lexemesRe.exec(mode_buffer); + last_index = top.keywordPatternRe.lastIndex; + match = top.keywordPatternRe.exec(mode_buffer); } buf += mode_buffer.substr(last_index); emitter.addText(buf); @@ -1036,18 +1399,20 @@ var hljs = (function () { function processSubLanguage() { if (mode_buffer === "") return; + /** @type HighlightResult */ + var result = null; - var explicit = typeof top.subLanguage === 'string'; - - if (explicit && !languages[top.subLanguage]) { - emitter.addText(mode_buffer); - return; + if (typeof top.subLanguage === 'string') { + if (!languages[top.subLanguage]) { + emitter.addText(mode_buffer); + return; + } + result = _highlight(top.subLanguage, mode_buffer, true, continuations[top.subLanguage]); + continuations[top.subLanguage] = result.top; + } else { + result = highlightAuto(mode_buffer, top.subLanguage.length ? top.subLanguage : null); } - var result = explicit ? - _highlight(top.subLanguage, mode_buffer, true, continuations[top.subLanguage]) : - highlightAuto(mode_buffer, top.subLanguage.length ? top.subLanguage : undefined); - // 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 @@ -1055,27 +1420,64 @@ var hljs = (function () { if (top.relevance > 0) { relevance += result.relevance; } - if (explicit) { - continuations[top.subLanguage] = result.top; - } emitter.addSublanguage(result.emitter, result.language); } function processBuffer() { - if (top.subLanguage != null) + if (top.subLanguage != null) { processSubLanguage(); - else + } else { processKeywords(); + } mode_buffer = ''; } + /** + * @param {Mode} mode - new mode to start + */ function startNewMode(mode) { if (mode.className) { emitter.openNode(mode.className); } - top = Object.create(mode, {parent: {value: top}}); + 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 @@ -1090,18 +1492,27 @@ var hljs = (function () { } } + /** + * 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) { var lexeme = match[0]; var new_mode = match.rule; - if (new_mode.__onBegin) { - let res = new_mode.__onBegin(match) || {}; - if (res.ignoreMatch) - return doIgnore(lexeme); + const resp = new Response(new_mode); + // first internal before callbacks, then the public ones + const beforeCallbacks = [new_mode.__beforeBegin, new_mode["on:begin"]]; + for (const cb of beforeCallbacks) { + if (!cb) continue; + cb(match, resp); + if (resp.ignore) return doIgnore(lexeme); } if (new_mode && new_mode.endSameAsBegin) { - new_mode.endRe = escape( lexeme ); + new_mode.endRe = escape(lexeme); } if (new_mode.skip) { @@ -1116,14 +1527,24 @@ var hljs = (function () { } } startNewMode(new_mode); + // if (mode["after:begin"]) { + // let resp = new Response(mode); + // mode["after:begin"](match, resp); + // } return new_mode.returnBegin ? 0 : lexeme.length; } + /** + * Handle the potential end of mode + * + * @param {RegExpMatchArray} match - the current match + */ function doEndMatch(match) { var lexeme = match[0]; var matchPlusRemainder = codeToHighlight.substr(match.index); - var end_mode = endOfMode(top, matchPlusRemainder); - if (!end_mode) { return; } + + var end_mode = endOfMode(top, match, matchPlusRemainder); + if (!end_mode) { return NO_MATCH; } var origin = top; if (origin.skip) { @@ -1157,7 +1578,7 @@ var hljs = (function () { function processContinuations() { var list = []; - for(var current = top; current !== language; current = current.parent) { + for (var current = top; current !== language; current = current.parent) { if (current.className) { list.unshift(current.className); } @@ -1165,50 +1586,57 @@ var hljs = (function () { list.forEach(item => emitter.openNode(item)); } + /** @type {{type?: MatchType, index?: number, rule?: Mode}}} */ var lastMatch = {}; - function processLexeme(text_before_match, match) { - var err; + /** + * 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) { var lexeme = match && match[0]; // add non-matched text to the current mode buffer - mode_buffer += text_before_match; + mode_buffer += 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 === "") { + 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 mode_buffer += codeToHighlight.slice(match.index, match.index + 1); if (!SAFE_MODE) { - err = new Error('0 width match regex'); + /** @type {AnnotatedError} */ + const err = new Error('0 width match regex'); err.languageName = languageName; err.badRule = lastMatch.rule; - throw(err); + throw err; } return 1; } lastMatch = match; - if (match.type==="begin") { + if (match.type === "begin") { return doBeginMatch(match); - } else if (match.type==="illegal" && !ignore_illegals) { + } else if (match.type === "illegal" && !ignoreIllegals) { // illegal match, we do not continue processing - err = new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.className || '<unnamed>') + '"'); + /** @type {AnnotatedError} */ + const err = new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.className || '<unnamed>') + '"'); err.mode = top; throw err; - } else if (match.type==="end") { + } else if (match.type === "end") { var processed = doEndMatch(match); - if (processed != undefined) + if (processed !== NO_MATCH) { return processed; + } } // edge case for when illegal matches $ (end of line) which is technically @@ -1250,16 +1678,16 @@ var hljs = (function () { throw new Error('Unknown language: "' + languageName + '"'); } - compileLanguage(language); - var top = continuation || language; + var md = compileLanguage(language); + var result = ''; + /** @type {CompiledMode} */ + var top = continuation || md; + /** @type Record<string,Mode> */ var continuations = {}; // keep continuations for sub-languages - var result; var emitter = new options.__emitter(options); processContinuations(); var mode_buffer = ''; var relevance = 0; - var match; - var processedCount; var index = 0; var iterations = 0; var continueScanAtSamePosition = false; @@ -1270,19 +1698,19 @@ var hljs = (function () { for (;;) { iterations++; if (continueScanAtSamePosition) { - continueScanAtSamePosition = false; // only regexes not matched previously will now be // considered for a potential match + continueScanAtSamePosition = false; } else { top.matcher.lastIndex = index; top.matcher.considerAll(); } - match = top.matcher.exec(codeToHighlight); + const match = top.matcher.exec(codeToHighlight); // console.log("match", match[0], match.rule && match.rule.begin) - if (!match) - break; - let beforeMatch = codeToHighlight.substring(index, match.index); - processedCount = processLexeme(beforeMatch, match); + if (!match) break; + + const beforeMatch = codeToHighlight.substring(index, match.index); + const processedCount = processLexeme(beforeMatch, match); index = match.index + processedCount; } processLexeme(codeToHighlight.substr(index)); @@ -1304,16 +1732,17 @@ var hljs = (function () { illegal: true, illegalBy: { msg: err.message, - context: codeToHighlight.slice(index-100,index+100), + context: codeToHighlight.slice(index - 100, index + 100), mode: err.mode }, sofar: result, relevance: 0, value: escape$1(codeToHighlight), - emitter: emitter, + emitter: emitter }; } else if (SAFE_MODE) { return { + illegal: false, relevance: 0, value: escape$1(codeToHighlight), emitter: emitter, @@ -1327,10 +1756,13 @@ var hljs = (function () { } } - // 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 + /** + * 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, @@ -1343,7 +1775,7 @@ var hljs = (function () { return result; } - /* + /** Highlighting with language detection. Accepts a string with the code to highlight. Returns an object with the following properties: @@ -1353,53 +1785,66 @@ var hljs = (function () { - second_best (object with the same structure for second-best heuristically detected language, may be absent) + @param {string} code + @param {Array<string>} [languageSubset] + @returns {AutoHighlightResult} */ function highlightAuto(code, languageSubset) { languageSubset = languageSubset || options.languages || Object.keys(languages); var result = justTextHighlightResult(code); - var second_best = result; + var secondBest = result; languageSubset.filter(getLanguage).filter(autoDetection).forEach(function(name) { var current = _highlight(name, code, false); current.language = name; - if (current.relevance > second_best.relevance) { - second_best = current; + if (current.relevance > secondBest.relevance) { + secondBest = current; } if (current.relevance > result.relevance) { - second_best = result; + secondBest = result; result = current; } }); - if (second_best.language) { - result.second_best = second_best; + if (secondBest.language) { + // second_best (with underscore) is the expected API + result.second_best = secondBest; } return result; } - /* + /** Post-processing of the highlighted markup: - replace TABs with something more useful - replace real line-breaks with '<br>' for non-pre containers + @param {string} html + @returns {string} */ - function fixMarkup(value) { + function fixMarkup(html) { if (!(options.tabReplace || options.useBR)) { - return value; + return html; } - return value.replace(fixMarkupRe, function(match, p1) { - if (options.useBR && match === '\n') { - return '<br>'; - } else if (options.tabReplace) { - return p1.replace(/\t/g, options.tabReplace); - } - return ''; + return html.replace(fixMarkupRe, match => { + if (match === '\n') { + return options.useBR ? '<br>' : 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 {string} prevClassName + * @param {string} [currentLang] + * @param {string} [resultLang] + */ function buildClassName(prevClassName, currentLang, resultLang) { - var language = currentLang ? aliases[currentLang] : resultLang, - result = [prevClassName.trim()]; + var language = currentLang ? aliases[currentLang] : resultLang; + var result = [prevClassName.trim()]; if (!prevClassName.match(/\bhljs\b/)) { result.push('hljs'); @@ -1412,86 +1857,99 @@ var hljs = (function () { return result.join(' ').trim(); } - /* - Applies highlighting to a DOM node containing code. Accepts a DOM node and - two optional parameters for fixMarkup. + /** + * 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(block) { - var node, originalStream, result, resultNode, text; - var language = blockLanguage(block); + function highlightBlock(element) { + /** @type HTMLElement */ + let node = null; + const language = blockLanguage(element); - if (shouldNotHighlight(language)) - return; + if (shouldNotHighlight(language)) return; fire("before:highlightBlock", - { block: block, language: language}); + { block: element, language: language }); if (options.useBR) { node = document.createElement('div'); - node.innerHTML = block.innerHTML.replace(/\n/g, '').replace(/<br[ \/]*>/g, '\n'); + node.innerHTML = element.innerHTML.replace(/\n/g, '').replace(/<br[ /]*>/g, '\n'); } else { - node = block; + node = element; } - text = node.textContent; - result = language ? highlight(language, text, true) : highlightAuto(text); + const text = node.textContent; + const result = language ? highlight(language, text, true) : highlightAuto(text); - originalStream = nodeStream$1(node); + const originalStream = nodeStream$1(node); if (originalStream.length) { - resultNode = document.createElement('div'); + const resultNode = document.createElement('div'); resultNode.innerHTML = result.value; result.value = mergeStreams$1(originalStream, nodeStream$1(resultNode), text); } result.value = fixMarkup(result.value); - fire("after:highlightBlock", { block: block, result: result}); + fire("after:highlightBlock", { block: element, result: result }); - block.innerHTML = result.value; - block.className = buildClassName(block.className, language, result.language); - block.result = { + element.innerHTML = result.value; + element.className = buildClassName(element.className, language, result.language); + element.result = { language: result.language, - re: result.relevance + // TODO: remove with version 11.0 + re: result.relevance, + relavance: result.relevance }; if (result.second_best) { - block.second_best = { + element.second_best = { language: result.second_best.language, - re: result.second_best.relevance + // TODO: remove with version 11.0 + re: result.second_best.relevance, + relavance: result.second_best.relevance }; } } - /* - Updates highlight.js global options with values passed in the form of an object. - */ - function configure(user_options) { - options = inherit$1(options, user_options); + /** + * Updates highlight.js global options with the passed options + * + * @param {{}} userOptions + */ + function configure(userOptions) { + options = inherit$1(options, userOptions); } - /* - Applies highlighting to all <pre><code>..</code></pre> blocks on a page. - */ - function initHighlighting() { - if (initHighlighting.called) - return; + /** + * Highlights to all <pre><code> blocks on a page + * + * @type {Function & {called?: boolean}} + */ + const initHighlighting = () => { + if (initHighlighting.called) return; initHighlighting.called = true; var blocks = document.querySelectorAll('pre code'); ArrayProto.forEach.call(blocks, highlightBlock); - } + }; - /* - Attaches highlighting to the page load event. - */ + // Higlights all when DOMContentLoaded fires function initHighlightingOnLoad() { + // @ts-ignore window.addEventListener('DOMContentLoaded', initHighlighting, false); } - const PLAINTEXT_LANGUAGE = { disableAutodetect: true, name: 'Plain text' }; - - function registerLanguage(name, language) { - var lang; - try { lang = language(hljs); } - catch (error) { - console.error("Language definition for '{}' could not be registered.".replace("{}", name)); + /** + * Register a language grammar module + * + * @param {string} languageName + * @param {LanguageFn} languageDefinition + */ + function registerLanguage(languageName, languageDefinition) { + var lang = null; + try { + lang = languageDefinition(hljs); + } catch (error) { + console.error("Language definition for '{}' could not be registered.".replace("{}", languageName)); // hard or soft error if (!SAFE_MODE) { throw error; } else { console.error(error); } // languages that have serious errors are replaced with essentially a @@ -1501,51 +1959,84 @@ var hljs = (function () { lang = PLAINTEXT_LANGUAGE; } // give it a temporary name if it doesn't have one in the meta-data - if (!lang.name) - lang.name = name; - languages[name] = lang; - lang.rawDefinition = language.bind(null,hljs); + if (!lang.name) lang.name = languageName; + languages[languageName] = lang; + lang.rawDefinition = languageDefinition.bind(null, hljs); if (lang.aliases) { - lang.aliases.forEach(function(alias) {aliases[alias] = name;}); + 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) { var lang = getLanguage(name); if (lang) { return lang; } - var err = new Error('The \'{}\' language is required, but not loaded.'.replace('{}',name)); + var 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) { var lang = getLanguage(name); return lang && !lang.disableAutodetect; } - function addPlugin(plugin, options) { + /** + * @param {HLJSPlugin} plugin + */ + function addPlugin(plugin) { plugins.push(plugin); } + /** + * + * @param {PluginEvent} event + * @param {any} args + */ function fire(event, args) { var cb = event; - plugins.forEach(function (plugin) { + plugins.forEach(function(plugin) { if (plugin[cb]) { plugin[cb](args); } @@ -1554,7 +2045,7 @@ var hljs = (function () { /* Interface definition */ - Object.assign(hljs,{ + Object.assign(hljs, { highlight, highlightAuto, fixMarkup, @@ -1565,6 +2056,7 @@ var hljs = (function () { registerLanguage, listLanguages, getLanguage, + registerAliases, requireLanguage, autoDetection, inherit: inherit$1, @@ -1576,8 +2068,11 @@ var hljs = (function () { hljs.versionString = version; for (const key in MODES) { - if (typeof MODES[key] === "object") + // @ts-ignore + if (typeof MODES[key] === "object") { + // @ts-ignore deepFreeze(MODES[key]); + } } // merge all the modes/regexs into our main object @@ -1603,6 +2098,7 @@ hljs.registerLanguage('css', function () { Website: https://developer.mozilla.org/en-US/docs/Web/CSS */ + /** @type LanguageFn */ function css(hljs) { var FUNCTION_LIKE = { begin: /[\w-]+\(/, returnBegin: true, @@ -1738,6 +2234,177 @@ hljs.registerLanguage('css', function () { hljs.registerLanguage('javascript', function () { 'use strict'; + 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. @@ -1746,6 +2413,7 @@ hljs.registerLanguage('javascript', function () { */ function javascript(hljs) { + var IDENT_RE$1 = IDENT_RE; var FRAGMENT = { begin: '<>', end: '</>' @@ -1754,26 +2422,11 @@ hljs.registerLanguage('javascript', function () { begin: /<[A-Za-z0-9\\._:-]+/, end: /\/[A-Za-z0-9\\._:-]+>|\/>/ }; - var IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*'; - var KEYWORDS = { - keyword: - 'in of if for while finally var new function do return void else break catch ' + - 'instanceof with throw case default try this switch continue typeof delete ' + - 'let yield const export super debugger as async await static ' + - // ECMAScript 6 modules import - 'import from as' - , - literal: - 'true false null undefined NaN Infinity', - built_in: - 'eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent ' + - 'encodeURI encodeURIComponent escape unescape Object Function Boolean Error ' + - 'EvalError InternalError RangeError ReferenceError StopIteration SyntaxError ' + - 'TypeError URIError Number Math Date String RegExp Array Float32Array ' + - 'Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array ' + - 'Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require ' + - 'module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect ' + - 'Promise' + var KEYWORDS$1 = { + $pattern: IDENT_RE, + keyword: KEYWORDS.join(" "), + literal: LITERALS.join(" "), + built_in: BUILT_INS.join(" ") }; var NUMBER = { className: 'number', @@ -1787,7 +2440,7 @@ hljs.registerLanguage('javascript', function () { var SUBST = { className: 'subst', begin: '\\$\\{', end: '\\}', - keywords: KEYWORDS, + keywords: KEYWORDS$1, contains: [] // defined later }; var HTML_TEMPLATE = { @@ -1830,6 +2483,10 @@ hljs.registerLanguage('javascript', function () { hljs.REGEXP_MODE ]; var PARAMS_CONTAINS = SUBST.contains.concat([ + // eat recursive parens in sub expressions + { begin: /\(/, end: /\)/, + contains: ["self"].concat(SUBST.contains, [hljs.C_BLOCK_COMMENT_MODE, hljs.C_LINE_COMMENT_MODE]) + }, hljs.C_BLOCK_COMMENT_MODE, hljs.C_LINE_COMMENT_MODE ]); @@ -1844,17 +2501,17 @@ hljs.registerLanguage('javascript', function () { return { name: 'JavaScript', aliases: ['js', 'jsx', 'mjs', 'cjs'], - keywords: KEYWORDS, + keywords: KEYWORDS$1, contains: [ + hljs.SHEBANG({ + binary: "node", + relevance: 5 + }), { className: 'meta', relevance: 10, begin: /^\s*['"]use (strict|asm)['"]/ }, - { - className: 'meta', - begin: /^#!/, end: /$/ - }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, HTML_TEMPLATE, @@ -1879,7 +2536,7 @@ hljs.registerLanguage('javascript', function () { }, { className: 'variable', - begin: IDENT_RE + '(?=\\s*(-)|$)', + begin: IDENT_RE$1 + '(?=\\s*(-)|$)', endsParent: true, relevance: 0 }, @@ -1897,13 +2554,29 @@ hljs.registerLanguage('javascript', function () { hljs.C_BLOCK_COMMENT_MODE, NUMBER, { // object attr container - begin: /[{,\n]\s*/, relevance: 0, + 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 `, + // <comments up next>` 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 + /(((\/\/.*)|(\/\*(.|\n)*\*\/))\s*)*/, + IDENT_RE$1 + '\\s*:'))), + relevance: 0, contains: [ { - begin: IDENT_RE + '\\s*:', returnBegin: true, + className: 'attr', + begin: IDENT_RE$1 + lookahead('\\s*:'), relevance: 0, - contains: [{className: 'attr', begin: IDENT_RE, relevance: 0}] - } + }, ] }, { // "value" container @@ -1915,22 +2588,32 @@ hljs.registerLanguage('javascript', function () { hljs.REGEXP_MODE, { className: 'function', - begin: '(\\(.*?\\)|' + IDENT_RE + ')\\s*=>', returnBegin: true, + // 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: IDENT_RE + begin: hljs.UNDERSCORE_IDENT_RE }, { + className: null, begin: /\(\s*\)/, + skip: true }, { begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, - keywords: KEYWORDS, + keywords: KEYWORDS$1, contains: PARAMS_CONTAINS } ] @@ -1966,7 +2649,7 @@ hljs.registerLanguage('javascript', function () { className: 'function', beginKeywords: 'function', end: /\{/, excludeEnd: true, contains: [ - hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE}), + hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE$1}), PARAMS ], illegal: /\[|%/ @@ -1989,11 +2672,11 @@ hljs.registerLanguage('javascript', function () { beginKeywords: 'constructor', end: /\{/, excludeEnd: true }, { - begin:'(get|set)\\s*(?=' + IDENT_RE+ '\\()', + begin: '(get|set)\\s+(?=' + IDENT_RE$1 + '\\()', end: /{/, keywords: "get set", contains: [ - hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE}), + hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE$1}), { begin: /\(\)/ }, // eat to avoid empty params PARAMS ] |