|
|
|
|
|
|
|
|
|
|
|
var hljs = (function () { |
|
'use strict'; |
|
|
|
var deepFreezeEs6 = {exports: {}}; |
|
|
|
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'); |
|
}; |
|
} |
|
|
|
|
|
Object.freeze(obj); |
|
|
|
Object.getOwnPropertyNames(obj).forEach(function (name) { |
|
var prop = obj[name]; |
|
|
|
|
|
if (typeof prop == 'object' && !Object.isFrozen(prop)) { |
|
deepFreeze(prop); |
|
} |
|
}); |
|
|
|
return obj; |
|
} |
|
|
|
deepFreezeEs6.exports = deepFreeze; |
|
deepFreezeEs6.exports.default = deepFreeze; |
|
|
|
|
|
|
|
|
|
|
|
class Response { |
|
|
|
|
|
|
|
constructor(mode) { |
|
|
|
if (mode.data === undefined) mode.data = {}; |
|
|
|
this.data = mode.data; |
|
this.isMatchIgnored = false; |
|
} |
|
|
|
ignoreMatch() { |
|
this.isMatchIgnored = true; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function escapeHTML(value) { |
|
return value |
|
.replace(/&/g, '&') |
|
.replace(/</g, '<') |
|
.replace(/>/g, '>') |
|
.replace(/"/g, '"') |
|
.replace(/'/g, '''); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function inherit$1(original, ...objects) { |
|
|
|
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 (result); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const SPAN_CLOSE = '</span>'; |
|
|
|
|
|
|
|
|
|
|
|
const emitsWrappingTags = (node) => { |
|
|
|
|
|
return !!node.scope || (node.sublanguage && node.language); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
const scopeToCSSClass = (name, { prefix }) => { |
|
if (name.includes(".")) { |
|
const pieces = name.split("."); |
|
return [ |
|
`${prefix}${pieces.shift()}`, |
|
...(pieces.map((x, i) => `${x}${"_".repeat(i + 1)}`)) |
|
].join(" "); |
|
} |
|
return `${prefix}${name}`; |
|
}; |
|
|
|
|
|
class HTMLRenderer { |
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(parseTree, options) { |
|
this.buffer = ""; |
|
this.classPrefix = options.classPrefix; |
|
parseTree.walk(this); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
addText(text) { |
|
this.buffer += escapeHTML(text); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
openNode(node) { |
|
if (!emitsWrappingTags(node)) return; |
|
|
|
let className = ""; |
|
if (node.sublanguage) { |
|
className = `language-${node.language}`; |
|
} else { |
|
className = scopeToCSSClass(node.scope, { prefix: this.classPrefix }); |
|
} |
|
this.span(className); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
closeNode(node) { |
|
if (!emitsWrappingTags(node)) return; |
|
|
|
this.buffer += SPAN_CLOSE; |
|
} |
|
|
|
|
|
|
|
|
|
value() { |
|
return this.buffer; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
span(className) { |
|
this.buffer += `<span class="${className}">`; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const newNode = (opts = {}) => { |
|
|
|
const result = { children: [] }; |
|
Object.assign(result, opts); |
|
return result; |
|
}; |
|
|
|
class TokenTree { |
|
constructor() { |
|
|
|
this.rootNode = newNode(); |
|
this.stack = [this.rootNode]; |
|
} |
|
|
|
get top() { |
|
return this.stack[this.stack.length - 1]; |
|
} |
|
|
|
get root() { return this.rootNode; } |
|
|
|
|
|
add(node) { |
|
this.top.children.push(node); |
|
} |
|
|
|
|
|
openNode(scope) { |
|
|
|
const node = newNode({ scope }); |
|
this.add(node); |
|
this.stack.push(node); |
|
} |
|
|
|
closeNode() { |
|
if (this.stack.length > 1) { |
|
return this.stack.pop(); |
|
} |
|
|
|
return undefined; |
|
} |
|
|
|
closeAllNodes() { |
|
while (this.closeNode()); |
|
} |
|
|
|
toJSON() { |
|
return JSON.stringify(this.rootNode, null, 4); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
walk(builder) { |
|
|
|
return this.constructor._walk(builder, this.rootNode); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
static _collapse(node) { |
|
if (typeof node === "string") return; |
|
if (!node.children) return; |
|
|
|
if (node.children.every(el => typeof el === "string")) { |
|
|
|
|
|
node.children = [node.children.join("")]; |
|
} else { |
|
node.children.forEach((child) => { |
|
TokenTree._collapse(child); |
|
}); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TokenTreeEmitter extends TokenTree { |
|
|
|
|
|
|
|
constructor(options) { |
|
super(); |
|
this.options = options; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
addKeyword(text, scope) { |
|
if (text === "") { return; } |
|
|
|
this.openNode(scope); |
|
this.addText(text); |
|
this.closeNode(); |
|
} |
|
|
|
|
|
|
|
|
|
addText(text) { |
|
if (text === "") { return; } |
|
|
|
this.add(text); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
addSublanguage(emitter, name) { |
|
|
|
const node = emitter.root; |
|
node.sublanguage = true; |
|
node.language = name; |
|
this.add(node); |
|
} |
|
|
|
toHTML() { |
|
const renderer = new HTMLRenderer(this, this.options); |
|
return renderer.value(); |
|
} |
|
|
|
finalize() { |
|
return true; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function source(re) { |
|
if (!re) return null; |
|
if (typeof re === "string") return re; |
|
|
|
return re.source; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function lookahead(re) { |
|
return concat('(?=', re, ')'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function anyNumberOfTimes(re) { |
|
return concat('(?:', re, ')*'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function optional(re) { |
|
return concat('(?:', re, ')?'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function concat(...args) { |
|
const joined = args.map((x) => source(x)).join(""); |
|
return joined; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function stripOptionsFromArgs(args) { |
|
const opts = args[args.length - 1]; |
|
|
|
if (typeof opts === 'object' && opts.constructor === Object) { |
|
args.splice(args.length - 1, 1); |
|
return opts; |
|
} else { |
|
return {}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function either(...args) { |
|
|
|
const opts = stripOptionsFromArgs(args); |
|
const joined = '(' |
|
+ (opts.capture ? "" : "?:") |
|
+ args.map((x) => source(x)).join("|") + ")"; |
|
return joined; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function countMatchGroups(re) { |
|
return (new RegExp(re.toString() + '|')).exec('').length - 1; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function startsWith(re, lexeme) { |
|
const match = re && re.exec(lexeme); |
|
return match && match.index === 0; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function _rewriteBackreferences(regexps, { joinWith }) { |
|
let numCaptures = 0; |
|
|
|
return regexps.map((regex) => { |
|
numCaptures += 1; |
|
const offset = numCaptures; |
|
let re = source(regex); |
|
let out = ''; |
|
|
|
while (re.length > 0) { |
|
const match = BACKREF_RE.exec(re); |
|
if (!match) { |
|
out += re; |
|
break; |
|
} |
|
out += re.substring(0, match.index); |
|
re = re.substring(match.index + match[0].length); |
|
if (match[0][0] === '\\' && match[1]) { |
|
|
|
out += '\\' + String(Number(match[1]) + offset); |
|
} else { |
|
out += match[0]; |
|
if (match[0] === '(') { |
|
numCaptures++; |
|
} |
|
} |
|
} |
|
return out; |
|
}).map(re => `(${re})`).join(joinWith); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const MATCH_NOTHING_RE = /\b\B/; |
|
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+)?)'; |
|
const BINARY_NUMBER_RE = '\\b(0b[01]+)'; |
|
const RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~'; |
|
|
|
|
|
|
|
|
|
const SHEBANG = (opts = {}) => { |
|
const beginShebang = /^#![ ]*\//; |
|
if (opts.binary) { |
|
opts.begin = concat( |
|
beginShebang, |
|
/.*\b/, |
|
opts.binary, |
|
/\b.*/); |
|
} |
|
return inherit$1({ |
|
scope: 'meta', |
|
begin: beginShebang, |
|
end: /$/, |
|
relevance: 0, |
|
|
|
"on:begin": (m, resp) => { |
|
if (m.index !== 0) resp.ignoreMatch(); |
|
} |
|
}, opts); |
|
}; |
|
|
|
|
|
const BACKSLASH_ESCAPE = { |
|
begin: '\\\\[\\s\\S]', relevance: 0 |
|
}; |
|
const APOS_STRING_MODE = { |
|
scope: 'string', |
|
begin: '\'', |
|
end: '\'', |
|
illegal: '\\n', |
|
contains: [BACKSLASH_ESCAPE] |
|
}; |
|
const QUOTE_STRING_MODE = { |
|
scope: '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/ |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const COMMENT = function(begin, end, modeOptions = {}) { |
|
const mode = inherit$1( |
|
{ |
|
scope: 'comment', |
|
begin, |
|
end, |
|
contains: [] |
|
}, |
|
modeOptions |
|
); |
|
mode.contains.push({ |
|
scope: 'doctag', |
|
|
|
|
|
begin: '[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)', |
|
end: /(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/, |
|
excludeBegin: true, |
|
relevance: 0 |
|
}); |
|
const ENGLISH_WORD = either( |
|
|
|
"I", |
|
"a", |
|
"is", |
|
"so", |
|
"us", |
|
"to", |
|
"at", |
|
"if", |
|
"in", |
|
"it", |
|
"on", |
|
|
|
/[A-Za-z]+['](d|ve|re|ll|t|s|n)/, |
|
/[A-Za-z]+[-][a-z]+/, |
|
/[A-Za-z][a-z]{2,}/ |
|
); |
|
|
|
mode.contains.push( |
|
{ |
|
// TODO: how to include ", (, ) without breaking grammars that use these for |
|
// comment delimiters? |
|
// begin: /[ ]+([()"]?([A-Za-z'-]{3,}|is|a|I|so|us|[tT][oO]|at|if|in|it|on)[.]?[()":]?([.][ ]|[ ]|\))){3}/ |
|
// --- |
|
|
|
// this tries to find sequences of 3 english words in a row (without any |
|
// "programming" type syntax) this gives us a strong signal that we've |
|
// TRULY found a comment - vs perhaps scanning with the wrong language. |
|
// It's possible to find something that LOOKS like the start of the |
|
// comment - but then if there is no readable text - good chance it is a |
|
// false match and not a comment. |
|
// |
|
// for a visual example please see: |
|
// https://github.com/highlightjs/highlight.js/issues/2827 |
|
|
|
begin: concat( |
|
/[ ]+/, // necessary to prevent us gobbling up doctags like /* @author Bob Mcgill */ |
|
'(', |
|
ENGLISH_WORD, |
|
/[.]?[:]?([.][ ]|[ ])/, |
|
'){3}') // look for 3 words in a row |
|
} |
|
); |
|
return mode; |
|
}; |
|
const C_LINE_COMMENT_MODE = COMMENT('//', '$'); |
|
const C_BLOCK_COMMENT_MODE = COMMENT('/\\*', '\\*/'); |
|
const HASH_COMMENT_MODE = COMMENT('#', '$'); |
|
const NUMBER_MODE = { |
|
scope: 'number', |
|
begin: NUMBER_RE, |
|
relevance: 0 |
|
}; |
|
const C_NUMBER_MODE = { |
|
scope: 'number', |
|
begin: C_NUMBER_RE, |
|
relevance: 0 |
|
}; |
|
const BINARY_NUMBER_MODE = { |
|
scope: 'number', |
|
begin: BINARY_NUMBER_RE, |
|
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: [{ |
|
scope: 'regexp', |
|
begin: /\//, |
|
end: /\/[gimuy]*/, |
|
illegal: /\n/, |
|
contains: [ |
|
BACKSLASH_ESCAPE, |
|
{ |
|
begin: /\[/, |
|
end: /\]/, |
|
relevance: 0, |
|
contains: [BACKSLASH_ESCAPE] |
|
} |
|
] |
|
}] |
|
}; |
|
const TITLE_MODE = { |
|
scope: 'title', |
|
begin: IDENT_RE, |
|
relevance: 0 |
|
}; |
|
const UNDERSCORE_TITLE_MODE = { |
|
scope: 'title', |
|
begin: UNDERSCORE_IDENT_RE, |
|
relevance: 0 |
|
}; |
|
const METHOD_GUARD = { |
|
// excludes method names from keyword processing |
|
begin: '\\.\\s*' + UNDERSCORE_IDENT_RE, |
|
relevance: 0 |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = Object.freeze({ |
|
__proto__: null, |
|
MATCH_NOTHING_RE: MATCH_NOTHING_RE, |
|
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, |
|
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 |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function skipIfHasPrecedingDot(match, response) { |
|
const before = match.input[match.index - 1]; |
|
if (before === ".") { |
|
response.ignoreMatch(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function scopeClassName(mode, _parent) { |
|
// eslint-disable-next-line no-undefined |
|
if (mode.className !== undefined) { |
|
mode.scope = mode.className; |
|
delete mode.className; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
// prevents double relevance, the keywords themselves provide |
|
// relevance, the mode doesn't need to double it |
|
// eslint-disable-next-line no-undefined |
|
if (mode.relevance === undefined) mode.relevance = 0; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function compileIllegal(mode, _parent) { |
|
if (!Array.isArray(mode.illegal)) return; |
|
|
|
mode.illegal = either(...mode.illegal); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function compileRelevance(mode, _parent) { |
|
// eslint-disable-next-line no-undefined |
|
if (mode.relevance === undefined) mode.relevance = 1; |
|
} |
|
|
|
|
|
|
|
const beforeMatchExt = (mode, parent) => { |
|
if (!mode.beforeMatch) return; |
|
// starts conflicts with endsParent which we need to make sure the child |
|
// rule is not matched multiple times |
|
if (mode.starts) throw new Error("beforeMatch cannot be used with starts"); |
|
|
|
const originalMode = Object.assign({}, mode); |
|
Object.keys(mode).forEach((key) => { delete mode[key]; }); |
|
|
|
mode.keywords = originalMode.keywords; |
|
mode.begin = concat(originalMode.beforeMatch, lookahead(originalMode.begin)); |
|
mode.starts = { |
|
relevance: 0, |
|
contains: [ |
|
Object.assign(originalMode, { endsParent: true }) |
|
] |
|
}; |
|
mode.relevance = 0; |
|
|
|
delete originalMode.beforeMatch; |
|
}; |
|
|
|
|
|
const COMMON_KEYWORDS = [ |
|
'of', |
|
'and', |
|
'for', |
|
'in', |
|
'not', |
|
'or', |
|
'if', |
|
'then', |
|
'parent', |
|
'list', |
|
'value' |
|
]; |
|
|
|
const DEFAULT_KEYWORD_SCOPE = "keyword"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function compileKeywords(rawKeywords, caseInsensitive, scopeName = DEFAULT_KEYWORD_SCOPE) { |
|
/** @type {import("highlight.js/private").KeywordDict} */ |
|
const compiledKeywords = Object.create(null); |
|
|
|
// input can be a string of keywords, an array of keywords, or a object with |
|
// named keys representing scopeName (which can then point to a string or array) |
|
if (typeof rawKeywords === 'string') { |
|
compileList(scopeName, rawKeywords.split(" ")); |
|
} else if (Array.isArray(rawKeywords)) { |
|
compileList(scopeName, rawKeywords); |
|
} else { |
|
Object.keys(rawKeywords).forEach(function(scopeName) { |
|
// collapse all our objects back into the parent object |
|
Object.assign( |
|
compiledKeywords, |
|
compileKeywords(rawKeywords[scopeName], caseInsensitive, scopeName) |
|
); |
|
}); |
|
} |
|
return compiledKeywords; |
|
|
|
// --- |
|
|
|
/** |
|
* Compiles an individual list of keywords |
|
* |
|
* Ex: "for if when while|5" |
|
* |
|
* @param {string} scopeName |
|
* @param {Array<string>} keywordList |
|
*/ |
|
function compileList(scopeName, keywordList) { |
|
if (caseInsensitive) { |
|
keywordList = keywordList.map(x => x.toLowerCase()); |
|
} |
|
keywordList.forEach(function(keyword) { |
|
const pair = keyword.split('|'); |
|
compiledKeywords[pair[0]] = [scopeName, scoreForKeyword(pair[0], pair[1])]; |
|
}); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function commonKeyword(keyword) { |
|
return COMMON_KEYWORDS.includes(keyword.toLowerCase()); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const seenDeprecations = {}; |
|
|
|
|
|
|
|
|
|
const error = (message) => { |
|
console.error(message); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const warn = (message, ...args) => { |
|
console.log(`WARN: ${message}`, ...args); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const deprecated = (version, message) => { |
|
if (seenDeprecations[`${version}/${message}`]) return; |
|
|
|
console.log(`Deprecated as of ${version}. ${message}`); |
|
seenDeprecations[`${version}/${message}`] = true; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const MultiClassError = new Error(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function remapScopeNames(mode, regexes, { key }) { |
|
let offset = 0; |
|
const scopeNames = mode[key]; |
|
/** @type Record<number,boolean> */ |
|
const emit = {}; |
|
/** @type Record<number,string> */ |
|
const positions = {}; |
|
|
|
for (let i = 1; i <= regexes.length; i++) { |
|
positions[i + offset] = scopeNames[i]; |
|
emit[i + offset] = true; |
|
offset += countMatchGroups(regexes[i - 1]); |
|
} |
|
// we use _emit to keep track of which match groups are "top-level" to avoid double |
|
// output from inside match groups |
|
mode[key] = positions; |
|
mode[key]._emit = emit; |
|
mode[key]._multi = true; |
|
} |
|
|
|
|
|
|
|
|
|
function beginMultiClass(mode) { |
|
if (!Array.isArray(mode.begin)) return; |
|
|
|
if (mode.skip || mode.excludeBegin || mode.returnBegin) { |
|
error("skip, excludeBegin, returnBegin not compatible with beginScope: {}"); |
|
throw MultiClassError; |
|
} |
|
|
|
if (typeof mode.beginScope !== "object" || mode.beginScope === null) { |
|
error("beginScope must be object"); |
|
throw MultiClassError; |
|
} |
|
|
|
remapScopeNames(mode, mode.begin, { key: "beginScope" }); |
|
mode.begin = _rewriteBackreferences(mode.begin, { joinWith: "" }); |
|
} |
|
|
|
|
|
|
|
|
|
function endMultiClass(mode) { |
|
if (!Array.isArray(mode.end)) return; |
|
|
|
if (mode.skip || mode.excludeEnd || mode.returnEnd) { |
|
error("skip, excludeEnd, returnEnd not compatible with endScope: {}"); |
|
throw MultiClassError; |
|
} |
|
|
|
if (typeof mode.endScope !== "object" || mode.endScope === null) { |
|
error("endScope must be object"); |
|
throw MultiClassError; |
|
} |
|
|
|
remapScopeNames(mode, mode.end, { key: "endScope" }); |
|
mode.end = _rewriteBackreferences(mode.end, { joinWith: "" }); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function scopeSugar(mode) { |
|
if (mode.scope && typeof mode.scope === "object" && mode.scope !== null) { |
|
mode.beginScope = mode.scope; |
|
delete mode.scope; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function MultiClass(mode) { |
|
scopeSugar(mode); |
|
|
|
if (typeof mode.beginScope === "string") { |
|
mode.beginScope = { _wrap: mode.beginScope }; |
|
} |
|
if (typeof mode.endScope === "string") { |
|
mode.endScope = { _wrap: mode.endScope }; |
|
} |
|
|
|
beginMultiClass(mode); |
|
endMultiClass(mode); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function compileLanguage(language) { |
|
/** |
|
* Builds a regex with the case sensitivity 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' : '') |
|
+ (language.unicodeRegex ? 'u' : '') |
|
+ (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(_rewriteBackreferences(terminators, { joinWith: '|' }), 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.isCompiled) return cmode; |
|
|
|
[ |
|
scopeClassName, |
|
// do this early so compiler extensions generally don't have to worry about |
|
// the distinction between match/begin |
|
compileMatch, |
|
MultiClass, |
|
beforeMatchExt |
|
].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.isCompiled = true; |
|
|
|
let keywordPattern = null; |
|
if (typeof mode.keywords === "object" && mode.keywords.$pattern) { |
|
// we need a copy because keywords might be compiled multiple times |
|
// so we can't go deleting $pattern from the original on the first |
|
// pass |
|
mode.keywords = Object.assign({}, mode.keywords); |
|
keywordPattern = mode.keywords.$pattern; |
|
delete mode.keywords.$pattern; |
|
} |
|
keywordPattern = keywordPattern || /\w+/; |
|
|
|
if (mode.keywords) { |
|
mode.keywords = compileKeywords(mode.keywords, language.case_insensitive); |
|
} |
|
|
|
cmode.keywordPatternRe = langRe(keywordPattern, true); |
|
|
|
if (parent) { |
|
if (!mode.begin) mode.begin = /\B|\b/; |
|
cmode.beginRe = langRe(cmode.begin); |
|
if (!mode.end && !mode.endsWithParent) mode.end = /\B|\b/; |
|
if (mode.end) cmode.endRe = langRe(cmode.end); |
|
cmode.terminatorEnd = source(cmode.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$1(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$1(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$1(mode, { starts: mode.starts ? inherit$1(mode.starts) : null }); |
|
} |
|
|
|
if (Object.isFrozen(mode)) { |
|
return inherit$1(mode); |
|
} |
|
|
|
// no special dependency issues, just return ourselves |
|
return mode; |
|
} |
|
|
|
var version = "11.7.0"; |
|
|
|
class HTMLInjectionError extends Error { |
|
constructor(reason, html) { |
|
super(reason); |
|
this.name = "HTMLInjectionError"; |
|
this.html = html; |
|
} |
|
} |
|
|
|
/* |
|
Syntax highlighting with language autodetection. |
|
https://highlightjs.org/ |
|
*/ |
|
|
|
/** |
|
@typedef {import('highlight.js').Mode} Mode |
|
@typedef {import('highlight.js').CompiledMode} CompiledMode |
|
@typedef {import('highlight.js').CompiledScope} CompiledScope |
|
@typedef {import('highlight.js').Language} Language |
|
@typedef {import('highlight.js').HLJSApi} HLJSApi |
|
@typedef {import('highlight.js').HLJSPlugin} HLJSPlugin |
|
@typedef {import('highlight.js').PluginEvent} PluginEvent |
|
@typedef {import('highlight.js').HLJSOptions} HLJSOptions |
|
@typedef {import('highlight.js').LanguageFn} LanguageFn |
|
@typedef {import('highlight.js').HighlightedHTMLElement} HighlightedHTMLElement |
|
@typedef {import('highlight.js').BeforeHighlightContext} BeforeHighlightContext |
|
@typedef {import('highlight.js/private').MatchType} MatchType |
|
@typedef {import('highlight.js/private').KeywordData} KeywordData |
|
@typedef {import('highlight.js/private').EnhancedMatch} EnhancedMatch |
|
@typedef {import('highlight.js/private').AnnotatedError} AnnotatedError |
|
@typedef {import('highlight.js').AutoHighlightResult} AutoHighlightResult |
|
@typedef {import('highlight.js').HighlightOptions} HighlightOptions |
|
@typedef {import('highlight.js').HighlightResult} HighlightResult |
|
*/ |
|
|
|
|
|
const escape = escapeHTML; |
|
const inherit = inherit$1; |
|
const NO_MATCH = Symbol("nomatch"); |
|
const MAX_KEYWORD_HITS = 7; |
|
|
|
/** |
|
* @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<string, Language>} */ |
|
const languages = Object.create(null); |
|
/** @type {Record<string, string>} */ |
|
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 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 = { |
|
ignoreUnescapedHTML: false, |
|
throwUnescapedHTML: false, |
|
noHighlightRe: /^(no-?highlight)$/i, |
|
languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, |
|
classPrefix: 'hljs-', |
|
cssSelector: 'pre code', |
|
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. |
|
* |
|
* OLD API |
|
* highlight(lang, code, ignoreIllegals, continuation) |
|
* |
|
* NEW API |
|
* highlight(code, {lang, ignoreIllegals}) |
|
* |
|
* @param {string} codeOrLanguageName - the language to use for highlighting |
|
* @param {string | HighlightOptions} optionsOrCode - the code to highlight |
|
* @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail |
|
* |
|
* @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(codeOrLanguageName, optionsOrCode, ignoreIllegals) { |
|
let code = ""; |
|
let languageName = ""; |
|
if (typeof optionsOrCode === "object") { |
|
code = codeOrLanguageName; |
|
ignoreIllegals = optionsOrCode.ignoreIllegals; |
|
languageName = optionsOrCode.language; |
|
} else { |
|
// old API |
|
deprecated("10.7.0", "highlight(lang, code, ...args) has been deprecated."); |
|
deprecated("10.7.0", "Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"); |
|
languageName = codeOrLanguageName; |
|
code = optionsOrCode; |
|
} |
|
|
|
// https://github.com/highlightjs/highlight.js/issues/3149 |
|
// eslint-disable-next-line no-undefined |
|
if (ignoreIllegals === undefined) { ignoreIllegals = true; } |
|
|
|
/** @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); |
|
|
|
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} codeToHighlight - 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, codeToHighlight, ignoreIllegals, continuation) { |
|
const keywordHits = Object.create(null); |
|
|
|
/** |
|
* Return keyword data if a match is a keyword |
|
* @param {CompiledMode} mode - current mode |
|
* @param {string} matchText - the textual match |
|
* @returns {KeywordData | false} |
|
*/ |
|
function keywordData(mode, matchText) { |
|
return 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 word = language.case_insensitive ? match[0].toLowerCase() : match[0]; |
|
const data = keywordData(top, word); |
|
if (data) { |
|
const [kind, keywordRelevance] = data; |
|
emitter.addText(buf); |
|
buf = ""; |
|
|
|
keywordHits[word] = (keywordHits[word] || 0) + 1; |
|
if (keywordHits[word] <= MAX_KEYWORD_HITS) relevance += keywordRelevance; |
|
if (kind.startsWith("_")) { |
|
// _ implied for relevance only, do not highlight |
|
// by applying a class name |
|
buf += match[0]; |
|
} else { |
|
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.substring(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 {CompiledScope} scope |
|
* @param {RegExpMatchArray} match |
|
*/ |
|
function emitMultiClass(scope, match) { |
|
let i = 1; |
|
const max = match.length - 1; |
|
while (i <= max) { |
|
if (!scope._emit[i]) { i++; continue; } |
|
const klass = language.classNameAliases[scope[i]] || scope[i]; |
|
const text = match[i]; |
|
if (klass) { |
|
emitter.addKeyword(text, klass); |
|
} else { |
|
modeBuffer = text; |
|
processKeywords(); |
|
modeBuffer = ""; |
|
} |
|
i++; |
|
} |
|
} |
|
|
|
/** |
|
* @param {CompiledMode} mode - new mode to start |
|
* @param {RegExpMatchArray} match |
|
*/ |
|
function startNewMode(mode, match) { |
|
if (mode.scope && typeof mode.scope === "string") { |
|
emitter.openNode(language.classNameAliases[mode.scope] || mode.scope); |
|
} |
|
if (mode.beginScope) { |
|
// beginScope just wraps the begin match itself in a scope |
|
if (mode.beginScope._wrap) { |
|
emitter.addKeyword(modeBuffer, language.classNameAliases[mode.beginScope._wrap] || mode.beginScope._wrap); |
|
modeBuffer = ""; |
|
} else if (mode.beginScope._multi) { |
|
// at this point modeBuffer should just be the match |
|
emitMultiClass(mode.beginScope, match); |
|
modeBuffer = ""; |
|
} |
|
} |
|
|
|
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.isMatchIgnored) 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 regexes 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.isMatchIgnored) return doIgnore(lexeme); |
|
} |
|
|
|
if (newMode.skip) { |
|
modeBuffer += lexeme; |
|
} else { |
|
if (newMode.excludeBegin) { |
|
modeBuffer += lexeme; |
|
} |
|
processBuffer(); |
|
if (!newMode.returnBegin && !newMode.excludeBegin) { |
|
modeBuffer = lexeme; |
|
} |
|
} |
|
startNewMode(newMode, match); |
|
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.substring(match.index); |
|
|
|
const endMode = endOfMode(top, match, matchPlusRemainder); |
|
if (!endMode) { return NO_MATCH; } |
|
|
|
const origin = top; |
|
if (top.endScope && top.endScope._wrap) { |
|
processBuffer(); |
|
emitter.addKeyword(lexeme, top.endScope._wrap); |
|
} else if (top.endScope && top.endScope._multi) { |
|
processBuffer(); |
|
emitMultiClass(top.endScope, match); |
|
} else if (origin.skip) { |
|
modeBuffer += lexeme; |
|
} else { |
|
if (!(origin.returnEnd || origin.excludeEnd)) { |
|
modeBuffer += lexeme; |
|
} |
|
processBuffer(); |
|
if (origin.excludeEnd) { |
|
modeBuffer = lexeme; |
|
} |
|
} |
|
do { |
|
if (top.scope) { |
|
emitter.closeNode(); |
|
} |
|
if (!top.skip && !top.subLanguage) { |
|
relevance += top.relevance; |
|
} |
|
top = top.parent; |
|
} while (top !== endMode.parent); |
|
if (endMode.starts) { |
|
startNewMode(endMode.starts, match); |
|
} |
|
return origin.returnEnd ? 0 : lexeme.length; |
|
} |
|
|
|
function processContinuations() { |
|
const list = []; |
|
for (let current = top; current !== language; current = current.parent) { |
|
if (current.scope) { |
|
list.unshift(current.scope); |
|
} |
|
} |
|
list.forEach(item => emitter.openNode(item)); |
|
} |
|
|
|
/** @type {{type?: MatchType, index?: number, rule?: Mode}}} */ |
|
let lastMatch = {}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 (${languageName})`); |
|
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.scope || '<unnamed>') + '"'); |
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (iterations > 100000 && iterations > match.index * 3) { |
|
const err = new Error('potential infinite loop, way more iterations than matches'); |
|
throw err; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
let result = ''; |
|
|
|
let top = continuation || md; |
|
|
|
const continuations = {}; |
|
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.substring(index)); |
|
emitter.closeAllNodes(); |
|
emitter.finalize(); |
|
result = emitter.toHTML(); |
|
|
|
return { |
|
language: languageName, |
|
value: result, |
|
relevance: relevance, |
|
illegal: false, |
|
_emitter: emitter, |
|
_top: top |
|
}; |
|
} catch (err) { |
|
if (err.message && err.message.includes('Illegal')) { |
|
return { |
|
language: languageName, |
|
value: escape(codeToHighlight), |
|
illegal: true, |
|
relevance: 0, |
|
_illegalBy: { |
|
message: err.message, |
|
index: index, |
|
context: codeToHighlight.slice(index - 100, index + 100), |
|
mode: err.mode, |
|
resultSoFar: result |
|
}, |
|
_emitter: emitter |
|
}; |
|
} else if (SAFE_MODE) { |
|
return { |
|
language: languageName, |
|
value: escape(codeToHighlight), |
|
illegal: false, |
|
relevance: 0, |
|
errorRaised: err, |
|
_emitter: emitter, |
|
_top: top |
|
}; |
|
} else { |
|
throw err; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function justTextHighlightResult(code) { |
|
const result = { |
|
value: escape(code), |
|
illegal: false, |
|
relevance: 0, |
|
_top: PLAINTEXT_LANGUAGE, |
|
_emitter: new options.__emitter(options) |
|
}; |
|
result._emitter.addText(code); |
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.secondBest = secondBest; |
|
|
|
return result; |
|
} |
|
|
|
/** |
|
* 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"); |
|
element.classList.add(`language-${language}`); |
|
} |
|
|
|
/** |
|
* Applies highlighting to a DOM node containing code. |
|
* |
|
* @param {HighlightedHTMLElement} element - the HTML element to highlight |
|
*/ |
|
function highlightElement(element) { |
|
/** @type HTMLElement */ |
|
let node = null; |
|
const language = blockLanguage(element); |
|
|
|
if (shouldNotHighlight(language)) return; |
|
|
|
fire("before:highlightElement", |
|
{ el: element, language: language }); |
|
|
|
// we should be all text, no child nodes (unescaped HTML) - this is possibly |
|
// an HTML injection attack - it's likely too late if this is already in |
|
// production (the code has likely already done its damage by the time |
|
// we're seeing it)... but we yell loudly about this so that hopefully it's |
|
// more likely to be caught in development before making it to production |
|
if (element.children.length > 0) { |
|
if (!options.ignoreUnescapedHTML) { |
|
console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."); |
|
console.warn("https://github.com/highlightjs/highlight.js/wiki/security"); |
|
console.warn("The element with unescaped HTML:"); |
|
console.warn(element); |
|
} |
|
if (options.throwUnescapedHTML) { |
|
const err = new HTMLInjectionError( |
|
"One of your code blocks includes unescaped HTML.", |
|
element.innerHTML |
|
); |
|
throw err; |
|
} |
|
} |
|
|
|
node = element; |
|
const text = node.textContent; |
|
const result = language ? highlight(text, { language, ignoreIllegals: true }) : highlightAuto(text); |
|
|
|
element.innerHTML = result.value; |
|
updateClassName(element, language, result.language); |
|
element.result = { |
|
language: result.language, |
|
// TODO: remove with version 11.0 |
|
re: result.relevance, |
|
relevance: result.relevance |
|
}; |
|
if (result.secondBest) { |
|
element.secondBest = { |
|
language: result.secondBest.language, |
|
relevance: result.secondBest.relevance |
|
}; |
|
} |
|
|
|
fire("after:highlightElement", { el: element, result, text }); |
|
} |
|
|
|
/** |
|
* Updates highlight.js global options with the passed options |
|
* |
|
* @param {Partial<HLJSOptions>} userOptions |
|
*/ |
|
function configure(userOptions) { |
|
options = inherit(options, userOptions); |
|
} |
|
|
|
// TODO: remove v12, deprecated |
|
const initHighlighting = () => { |
|
highlightAll(); |
|
deprecated("10.6.0", "initHighlighting() deprecated. Use highlightAll() now."); |
|
}; |
|
|
|
// TODO: remove v12, deprecated |
|
function initHighlightingOnLoad() { |
|
highlightAll(); |
|
deprecated("10.6.0", "initHighlightingOnLoad() deprecated. Use highlightAll() now."); |
|
} |
|
|
|
let wantsHighlight = false; |
|
|
|
/** |
|
* auto-highlights all pre>code elements on the page |
|
*/ |
|
function highlightAll() { |
|
// if we are called too early in the loading process |
|
if (document.readyState === "loading") { |
|
wantsHighlight = true; |
|
return; |
|
} |
|
|
|
const blocks = document.querySelectorAll(options.cssSelector); |
|
blocks.forEach(highlightElement); |
|
} |
|
|
|
function boot() { |
|
// if a highlight was requested before DOM was loaded, do now |
|
if (wantsHighlight) highlightAll(); |
|
} |
|
|
|
// make sure we are in the browser environment |
|
if (typeof window !== 'undefined' && window.addEventListener) { |
|
window.addEventListener('DOMContentLoaded', boot, 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 }); |
|
} |
|
} |
|
|
|
/** |
|
* Remove a language grammar module |
|
* |
|
* @param {string} languageName |
|
*/ |
|
function unregisterLanguage(languageName) { |
|
delete languages[languageName]; |
|
for (const alias of Object.keys(aliases)) { |
|
if (aliases[alias] === languageName) { |
|
delete aliases[alias]; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* @returns {string[]} List of language internal names |
|
*/ |
|
function listLanguages() { |
|
return Object.keys(languages); |
|
} |
|
|
|
/** |
|
* @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.toLowerCase()] = 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; |
|
} |
|
|
|
/** |
|
* Upgrades the old highlightBlock plugins to the new |
|
* highlightElement API |
|
* @param {HLJSPlugin} plugin |
|
*/ |
|
function upgradePluginAPI(plugin) { |
|
// TODO: remove with v12 |
|
if (plugin["before:highlightBlock"] && !plugin["before:highlightElement"]) { |
|
plugin["before:highlightElement"] = (data) => { |
|
plugin["before:highlightBlock"]( |
|
Object.assign({ block: data.el }, data) |
|
); |
|
}; |
|
} |
|
if (plugin["after:highlightBlock"] && !plugin["after:highlightElement"]) { |
|
plugin["after:highlightElement"] = (data) => { |
|
plugin["after:highlightBlock"]( |
|
Object.assign({ block: data.el }, data) |
|
); |
|
}; |
|
} |
|
} |
|
|
|
/** |
|
* @param {HLJSPlugin} plugin |
|
*/ |
|
function addPlugin(plugin) { |
|
upgradePluginAPI(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); |
|
} |
|
}); |
|
} |
|
|
|
/** |
|
* DEPRECATED |
|
* @param {HighlightedHTMLElement} el |
|
*/ |
|
function deprecateHighlightBlock(el) { |
|
deprecated("10.7.0", "highlightBlock will be removed entirely in v12.0"); |
|
deprecated("10.7.0", "Please use highlightElement now."); |
|
|
|
return highlightElement(el); |
|
} |
|
|
|
/* Interface definition */ |
|
Object.assign(hljs, { |
|
highlight, |
|
highlightAuto, |
|
highlightAll, |
|
highlightElement, |
|
// TODO: Remove with v12 API |
|
highlightBlock: deprecateHighlightBlock, |
|
configure, |
|
initHighlighting, |
|
initHighlightingOnLoad, |
|
registerLanguage, |
|
unregisterLanguage, |
|
listLanguages, |
|
getLanguage, |
|
registerAliases, |
|
autoDetection, |
|
inherit, |
|
addPlugin |
|
}); |
|
|
|
hljs.debugMode = function() { SAFE_MODE = false; }; |
|
hljs.safeMode = function() { SAFE_MODE = true; }; |
|
hljs.versionString = version; |
|
|
|
hljs.regex = { |
|
concat: concat, |
|
lookahead: lookahead, |
|
either: either, |
|
optional: optional, |
|
anyNumberOfTimes: anyNumberOfTimes |
|
}; |
|
|
|
for (const key in MODES) { |
|
// @ts-ignore |
|
if (typeof MODES[key] === "object") { |
|
// @ts-ignore |
|
deepFreezeEs6.exports(MODES[key]); |
|
} |
|
} |
|
|
|
// merge all the modes/regexes into our main object |
|
Object.assign(hljs, MODES); |
|
|
|
return hljs; |
|
}; |
|
|
|
// export an "instance" of the highlighter |
|
var highlight = HLJS({}); |
|
|
|
return highlight; |
|
|
|
})(); |
|
if (typeof exports === 'object' && typeof module !== 'undefined') { module.exports = hljs; } |
|
|