Mercurial
comparison third_party/highlight/es/highlight.js @ 157:2db6253f355d
[ThirdParty] Added highlight library for better readability on blog.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Tue, 13 Jan 2026 19:18:47 -0800 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 156:cd35e600ae34 | 157:2db6253f355d |
|---|---|
| 1 /*! | |
| 2 Highlight.js v11.11.1 (git: 08cb242e7d) | |
| 3 (c) 2006-2025 Josh Goebel <[email protected]> and other contributors | |
| 4 License: BSD-3-Clause | |
| 5 */ | |
| 6 /* eslint-disable no-multi-assign */ | |
| 7 | |
| 8 function deepFreeze(obj) { | |
| 9 if (obj instanceof Map) { | |
| 10 obj.clear = | |
| 11 obj.delete = | |
| 12 obj.set = | |
| 13 function () { | |
| 14 throw new Error('map is read-only'); | |
| 15 }; | |
| 16 } else if (obj instanceof Set) { | |
| 17 obj.add = | |
| 18 obj.clear = | |
| 19 obj.delete = | |
| 20 function () { | |
| 21 throw new Error('set is read-only'); | |
| 22 }; | |
| 23 } | |
| 24 | |
| 25 // Freeze self | |
| 26 Object.freeze(obj); | |
| 27 | |
| 28 Object.getOwnPropertyNames(obj).forEach((name) => { | |
| 29 const prop = obj[name]; | |
| 30 const type = typeof prop; | |
| 31 | |
| 32 // Freeze prop if it is an object or function and also not already frozen | |
| 33 if ((type === 'object' || type === 'function') && !Object.isFrozen(prop)) { | |
| 34 deepFreeze(prop); | |
| 35 } | |
| 36 }); | |
| 37 | |
| 38 return obj; | |
| 39 } | |
| 40 | |
| 41 /** @typedef {import('highlight.js').CallbackResponse} CallbackResponse */ | |
| 42 /** @typedef {import('highlight.js').CompiledMode} CompiledMode */ | |
| 43 /** @implements CallbackResponse */ | |
| 44 | |
| 45 class Response { | |
| 46 /** | |
| 47 * @param {CompiledMode} mode | |
| 48 */ | |
| 49 constructor(mode) { | |
| 50 // eslint-disable-next-line no-undefined | |
| 51 if (mode.data === undefined) mode.data = {}; | |
| 52 | |
| 53 this.data = mode.data; | |
| 54 this.isMatchIgnored = false; | |
| 55 } | |
| 56 | |
| 57 ignoreMatch() { | |
| 58 this.isMatchIgnored = true; | |
| 59 } | |
| 60 } | |
| 61 | |
| 62 /** | |
| 63 * @param {string} value | |
| 64 * @returns {string} | |
| 65 */ | |
| 66 function escapeHTML(value) { | |
| 67 return value | |
| 68 .replace(/&/g, '&') | |
| 69 .replace(/</g, '<') | |
| 70 .replace(/>/g, '>') | |
| 71 .replace(/"/g, '"') | |
| 72 .replace(/'/g, '''); | |
| 73 } | |
| 74 | |
| 75 /** | |
| 76 * performs a shallow merge of multiple objects into one | |
| 77 * | |
| 78 * @template T | |
| 79 * @param {T} original | |
| 80 * @param {Record<string,any>[]} objects | |
| 81 * @returns {T} a single new object | |
| 82 */ | |
| 83 function inherit$1(original, ...objects) { | |
| 84 /** @type Record<string,any> */ | |
| 85 const result = Object.create(null); | |
| 86 | |
| 87 for (const key in original) { | |
| 88 result[key] = original[key]; | |
| 89 } | |
| 90 objects.forEach(function(obj) { | |
| 91 for (const key in obj) { | |
| 92 result[key] = obj[key]; | |
| 93 } | |
| 94 }); | |
| 95 return /** @type {T} */ (result); | |
| 96 } | |
| 97 | |
| 98 /** | |
| 99 * @typedef {object} Renderer | |
| 100 * @property {(text: string) => void} addText | |
| 101 * @property {(node: Node) => void} openNode | |
| 102 * @property {(node: Node) => void} closeNode | |
| 103 * @property {() => string} value | |
| 104 */ | |
| 105 | |
| 106 /** @typedef {{scope?: string, language?: string, sublanguage?: boolean}} Node */ | |
| 107 /** @typedef {{walk: (r: Renderer) => void}} Tree */ | |
| 108 /** */ | |
| 109 | |
| 110 const SPAN_CLOSE = '</span>'; | |
| 111 | |
| 112 /** | |
| 113 * Determines if a node needs to be wrapped in <span> | |
| 114 * | |
| 115 * @param {Node} node */ | |
| 116 const emitsWrappingTags = (node) => { | |
| 117 // rarely we can have a sublanguage where language is undefined | |
| 118 // TODO: track down why | |
| 119 return !!node.scope; | |
| 120 }; | |
| 121 | |
| 122 /** | |
| 123 * | |
| 124 * @param {string} name | |
| 125 * @param {{prefix:string}} options | |
| 126 */ | |
| 127 const scopeToCSSClass = (name, { prefix }) => { | |
| 128 // sub-language | |
| 129 if (name.startsWith("language:")) { | |
| 130 return name.replace("language:", "language-"); | |
| 131 } | |
| 132 // tiered scope: comment.line | |
| 133 if (name.includes(".")) { | |
| 134 const pieces = name.split("."); | |
| 135 return [ | |
| 136 `${prefix}${pieces.shift()}`, | |
| 137 ...(pieces.map((x, i) => `${x}${"_".repeat(i + 1)}`)) | |
| 138 ].join(" "); | |
| 139 } | |
| 140 // simple scope | |
| 141 return `${prefix}${name}`; | |
| 142 }; | |
| 143 | |
| 144 /** @type {Renderer} */ | |
| 145 class HTMLRenderer { | |
| 146 /** | |
| 147 * Creates a new HTMLRenderer | |
| 148 * | |
| 149 * @param {Tree} parseTree - the parse tree (must support `walk` API) | |
| 150 * @param {{classPrefix: string}} options | |
| 151 */ | |
| 152 constructor(parseTree, options) { | |
| 153 this.buffer = ""; | |
| 154 this.classPrefix = options.classPrefix; | |
| 155 parseTree.walk(this); | |
| 156 } | |
| 157 | |
| 158 /** | |
| 159 * Adds texts to the output stream | |
| 160 * | |
| 161 * @param {string} text */ | |
| 162 addText(text) { | |
| 163 this.buffer += escapeHTML(text); | |
| 164 } | |
| 165 | |
| 166 /** | |
| 167 * Adds a node open to the output stream (if needed) | |
| 168 * | |
| 169 * @param {Node} node */ | |
| 170 openNode(node) { | |
| 171 if (!emitsWrappingTags(node)) return; | |
| 172 | |
| 173 const className = scopeToCSSClass(node.scope, | |
| 174 { prefix: this.classPrefix }); | |
| 175 this.span(className); | |
| 176 } | |
| 177 | |
| 178 /** | |
| 179 * Adds a node close to the output stream (if needed) | |
| 180 * | |
| 181 * @param {Node} node */ | |
| 182 closeNode(node) { | |
| 183 if (!emitsWrappingTags(node)) return; | |
| 184 | |
| 185 this.buffer += SPAN_CLOSE; | |
| 186 } | |
| 187 | |
| 188 /** | |
| 189 * returns the accumulated buffer | |
| 190 */ | |
| 191 value() { | |
| 192 return this.buffer; | |
| 193 } | |
| 194 | |
| 195 // helpers | |
| 196 | |
| 197 /** | |
| 198 * Builds a span element | |
| 199 * | |
| 200 * @param {string} className */ | |
| 201 span(className) { | |
| 202 this.buffer += `<span class="${className}">`; | |
| 203 } | |
| 204 } | |
| 205 | |
| 206 /** @typedef {{scope?: string, language?: string, children: Node[]} | string} Node */ | |
| 207 /** @typedef {{scope?: string, language?: string, children: Node[]} } DataNode */ | |
| 208 /** @typedef {import('highlight.js').Emitter} Emitter */ | |
| 209 /** */ | |
| 210 | |
| 211 /** @returns {DataNode} */ | |
| 212 const newNode = (opts = {}) => { | |
| 213 /** @type DataNode */ | |
| 214 const result = { children: [] }; | |
| 215 Object.assign(result, opts); | |
| 216 return result; | |
| 217 }; | |
| 218 | |
| 219 class TokenTree { | |
| 220 constructor() { | |
| 221 /** @type DataNode */ | |
| 222 this.rootNode = newNode(); | |
| 223 this.stack = [this.rootNode]; | |
| 224 } | |
| 225 | |
| 226 get top() { | |
| 227 return this.stack[this.stack.length - 1]; | |
| 228 } | |
| 229 | |
| 230 get root() { return this.rootNode; } | |
| 231 | |
| 232 /** @param {Node} node */ | |
| 233 add(node) { | |
| 234 this.top.children.push(node); | |
| 235 } | |
| 236 | |
| 237 /** @param {string} scope */ | |
| 238 openNode(scope) { | |
| 239 /** @type Node */ | |
| 240 const node = newNode({ scope }); | |
| 241 this.add(node); | |
| 242 this.stack.push(node); | |
| 243 } | |
| 244 | |
| 245 closeNode() { | |
| 246 if (this.stack.length > 1) { | |
| 247 return this.stack.pop(); | |
| 248 } | |
| 249 // eslint-disable-next-line no-undefined | |
| 250 return undefined; | |
| 251 } | |
| 252 | |
| 253 closeAllNodes() { | |
| 254 while (this.closeNode()); | |
| 255 } | |
| 256 | |
| 257 toJSON() { | |
| 258 return JSON.stringify(this.rootNode, null, 4); | |
| 259 } | |
| 260 | |
| 261 /** | |
| 262 * @typedef { import("./html_renderer").Renderer } Renderer | |
| 263 * @param {Renderer} builder | |
| 264 */ | |
| 265 walk(builder) { | |
| 266 // this does not | |
| 267 return this.constructor._walk(builder, this.rootNode); | |
| 268 // this works | |
| 269 // return TokenTree._walk(builder, this.rootNode); | |
| 270 } | |
| 271 | |
| 272 /** | |
| 273 * @param {Renderer} builder | |
| 274 * @param {Node} node | |
| 275 */ | |
| 276 static _walk(builder, node) { | |
| 277 if (typeof node === "string") { | |
| 278 builder.addText(node); | |
| 279 } else if (node.children) { | |
| 280 builder.openNode(node); | |
| 281 node.children.forEach((child) => this._walk(builder, child)); | |
| 282 builder.closeNode(node); | |
| 283 } | |
| 284 return builder; | |
| 285 } | |
| 286 | |
| 287 /** | |
| 288 * @param {Node} node | |
| 289 */ | |
| 290 static _collapse(node) { | |
| 291 if (typeof node === "string") return; | |
| 292 if (!node.children) return; | |
| 293 | |
| 294 if (node.children.every(el => typeof el === "string")) { | |
| 295 // node.text = node.children.join(""); | |
| 296 // delete node.children; | |
| 297 node.children = [node.children.join("")]; | |
| 298 } else { | |
| 299 node.children.forEach((child) => { | |
| 300 TokenTree._collapse(child); | |
| 301 }); | |
| 302 } | |
| 303 } | |
| 304 } | |
| 305 | |
| 306 /** | |
| 307 Currently this is all private API, but this is the minimal API necessary | |
| 308 that an Emitter must implement to fully support the parser. | |
| 309 | |
| 310 Minimal interface: | |
| 311 | |
| 312 - addText(text) | |
| 313 - __addSublanguage(emitter, subLanguageName) | |
| 314 - startScope(scope) | |
| 315 - endScope() | |
| 316 - finalize() | |
| 317 - toHTML() | |
| 318 | |
| 319 */ | |
| 320 | |
| 321 /** | |
| 322 * @implements {Emitter} | |
| 323 */ | |
| 324 class TokenTreeEmitter extends TokenTree { | |
| 325 /** | |
| 326 * @param {*} options | |
| 327 */ | |
| 328 constructor(options) { | |
| 329 super(); | |
| 330 this.options = options; | |
| 331 } | |
| 332 | |
| 333 /** | |
| 334 * @param {string} text | |
| 335 */ | |
| 336 addText(text) { | |
| 337 if (text === "") { return; } | |
| 338 | |
| 339 this.add(text); | |
| 340 } | |
| 341 | |
| 342 /** @param {string} scope */ | |
| 343 startScope(scope) { | |
| 344 this.openNode(scope); | |
| 345 } | |
| 346 | |
| 347 endScope() { | |
| 348 this.closeNode(); | |
| 349 } | |
| 350 | |
| 351 /** | |
| 352 * @param {Emitter & {root: DataNode}} emitter | |
| 353 * @param {string} name | |
| 354 */ | |
| 355 __addSublanguage(emitter, name) { | |
| 356 /** @type DataNode */ | |
| 357 const node = emitter.root; | |
| 358 if (name) node.scope = `language:${name}`; | |
| 359 | |
| 360 this.add(node); | |
| 361 } | |
| 362 | |
| 363 toHTML() { | |
| 364 const renderer = new HTMLRenderer(this, this.options); | |
| 365 return renderer.value(); | |
| 366 } | |
| 367 | |
| 368 finalize() { | |
| 369 this.closeAllNodes(); | |
| 370 return true; | |
| 371 } | |
| 372 } | |
| 373 | |
| 374 /** | |
| 375 * @param {string} value | |
| 376 * @returns {RegExp} | |
| 377 * */ | |
| 378 | |
| 379 /** | |
| 380 * @param {RegExp | string } re | |
| 381 * @returns {string} | |
| 382 */ | |
| 383 function source(re) { | |
| 384 if (!re) return null; | |
| 385 if (typeof re === "string") return re; | |
| 386 | |
| 387 return re.source; | |
| 388 } | |
| 389 | |
| 390 /** | |
| 391 * @param {RegExp | string } re | |
| 392 * @returns {string} | |
| 393 */ | |
| 394 function lookahead(re) { | |
| 395 return concat('(?=', re, ')'); | |
| 396 } | |
| 397 | |
| 398 /** | |
| 399 * @param {RegExp | string } re | |
| 400 * @returns {string} | |
| 401 */ | |
| 402 function anyNumberOfTimes(re) { | |
| 403 return concat('(?:', re, ')*'); | |
| 404 } | |
| 405 | |
| 406 /** | |
| 407 * @param {RegExp | string } re | |
| 408 * @returns {string} | |
| 409 */ | |
| 410 function optional(re) { | |
| 411 return concat('(?:', re, ')?'); | |
| 412 } | |
| 413 | |
| 414 /** | |
| 415 * @param {...(RegExp | string) } args | |
| 416 * @returns {string} | |
| 417 */ | |
| 418 function concat(...args) { | |
| 419 const joined = args.map((x) => source(x)).join(""); | |
| 420 return joined; | |
| 421 } | |
| 422 | |
| 423 /** | |
| 424 * @param { Array<string | RegExp | Object> } args | |
| 425 * @returns {object} | |
| 426 */ | |
| 427 function stripOptionsFromArgs(args) { | |
| 428 const opts = args[args.length - 1]; | |
| 429 | |
| 430 if (typeof opts === 'object' && opts.constructor === Object) { | |
| 431 args.splice(args.length - 1, 1); | |
| 432 return opts; | |
| 433 } else { | |
| 434 return {}; | |
| 435 } | |
| 436 } | |
| 437 | |
| 438 /** @typedef { {capture?: boolean} } RegexEitherOptions */ | |
| 439 | |
| 440 /** | |
| 441 * Any of the passed expresssions may match | |
| 442 * | |
| 443 * Creates a huge this | this | that | that match | |
| 444 * @param {(RegExp | string)[] | [...(RegExp | string)[], RegexEitherOptions]} args | |
| 445 * @returns {string} | |
| 446 */ | |
| 447 function either(...args) { | |
| 448 /** @type { object & {capture?: boolean} } */ | |
| 449 const opts = stripOptionsFromArgs(args); | |
| 450 const joined = '(' | |
| 451 + (opts.capture ? "" : "?:") | |
| 452 + args.map((x) => source(x)).join("|") + ")"; | |
| 453 return joined; | |
| 454 } | |
| 455 | |
| 456 /** | |
| 457 * @param {RegExp | string} re | |
| 458 * @returns {number} | |
| 459 */ | |
| 460 function countMatchGroups(re) { | |
| 461 return (new RegExp(re.toString() + '|')).exec('').length - 1; | |
| 462 } | |
| 463 | |
| 464 /** | |
| 465 * Does lexeme start with a regular expression match at the beginning | |
| 466 * @param {RegExp} re | |
| 467 * @param {string} lexeme | |
| 468 */ | |
| 469 function startsWith(re, lexeme) { | |
| 470 const match = re && re.exec(lexeme); | |
| 471 return match && match.index === 0; | |
| 472 } | |
| 473 | |
| 474 // BACKREF_RE matches an open parenthesis or backreference. To avoid | |
| 475 // an incorrect parse, it additionally matches the following: | |
| 476 // - [...] elements, where the meaning of parentheses and escapes change | |
| 477 // - other escape sequences, so we do not misparse escape sequences as | |
| 478 // interesting elements | |
| 479 // - non-matching or lookahead parentheses, which do not capture. These | |
| 480 // follow the '(' with a '?'. | |
| 481 const BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./; | |
| 482 | |
| 483 // **INTERNAL** Not intended for outside usage | |
| 484 // join logically computes regexps.join(separator), but fixes the | |
| 485 // backreferences so they continue to match. | |
| 486 // it also places each individual regular expression into it's own | |
| 487 // match group, keeping track of the sequencing of those match groups | |
| 488 // is currently an exercise for the caller. :-) | |
| 489 /** | |
| 490 * @param {(string | RegExp)[]} regexps | |
| 491 * @param {{joinWith: string}} opts | |
| 492 * @returns {string} | |
| 493 */ | |
| 494 function _rewriteBackreferences(regexps, { joinWith }) { | |
| 495 let numCaptures = 0; | |
| 496 | |
| 497 return regexps.map((regex) => { | |
| 498 numCaptures += 1; | |
| 499 const offset = numCaptures; | |
| 500 let re = source(regex); | |
| 501 let out = ''; | |
| 502 | |
| 503 while (re.length > 0) { | |
| 504 const match = BACKREF_RE.exec(re); | |
| 505 if (!match) { | |
| 506 out += re; | |
| 507 break; | |
| 508 } | |
| 509 out += re.substring(0, match.index); | |
| 510 re = re.substring(match.index + match[0].length); | |
| 511 if (match[0][0] === '\\' && match[1]) { | |
| 512 // Adjust the backreference. | |
| 513 out += '\\' + String(Number(match[1]) + offset); | |
| 514 } else { | |
| 515 out += match[0]; | |
| 516 if (match[0] === '(') { | |
| 517 numCaptures++; | |
| 518 } | |
| 519 } | |
| 520 } | |
| 521 return out; | |
| 522 }).map(re => `(${re})`).join(joinWith); | |
| 523 } | |
| 524 | |
| 525 /** @typedef {import('highlight.js').Mode} Mode */ | |
| 526 /** @typedef {import('highlight.js').ModeCallback} ModeCallback */ | |
| 527 | |
| 528 // Common regexps | |
| 529 const MATCH_NOTHING_RE = /\b\B/; | |
| 530 const IDENT_RE = '[a-zA-Z]\\w*'; | |
| 531 const UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*'; | |
| 532 const NUMBER_RE = '\\b\\d+(\\.\\d+)?'; | |
| 533 const C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float | |
| 534 const BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b... | |
| 535 const RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~'; | |
| 536 | |
| 537 /** | |
| 538 * @param { Partial<Mode> & {binary?: string | RegExp} } opts | |
| 539 */ | |
| 540 const SHEBANG = (opts = {}) => { | |
| 541 const beginShebang = /^#![ ]*\//; | |
| 542 if (opts.binary) { | |
| 543 opts.begin = concat( | |
| 544 beginShebang, | |
| 545 /.*\b/, | |
| 546 opts.binary, | |
| 547 /\b.*/); | |
| 548 } | |
| 549 return inherit$1({ | |
| 550 scope: 'meta', | |
| 551 begin: beginShebang, | |
| 552 end: /$/, | |
| 553 relevance: 0, | |
| 554 /** @type {ModeCallback} */ | |
| 555 "on:begin": (m, resp) => { | |
| 556 if (m.index !== 0) resp.ignoreMatch(); | |
| 557 } | |
| 558 }, opts); | |
| 559 }; | |
| 560 | |
| 561 // Common modes | |
| 562 const BACKSLASH_ESCAPE = { | |
| 563 begin: '\\\\[\\s\\S]', relevance: 0 | |
| 564 }; | |
| 565 const APOS_STRING_MODE = { | |
| 566 scope: 'string', | |
| 567 begin: '\'', | |
| 568 end: '\'', | |
| 569 illegal: '\\n', | |
| 570 contains: [BACKSLASH_ESCAPE] | |
| 571 }; | |
| 572 const QUOTE_STRING_MODE = { | |
| 573 scope: 'string', | |
| 574 begin: '"', | |
| 575 end: '"', | |
| 576 illegal: '\\n', | |
| 577 contains: [BACKSLASH_ESCAPE] | |
| 578 }; | |
| 579 const PHRASAL_WORDS_MODE = { | |
| 580 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/ | |
| 581 }; | |
| 582 /** | |
| 583 * Creates a comment mode | |
| 584 * | |
| 585 * @param {string | RegExp} begin | |
| 586 * @param {string | RegExp} end | |
| 587 * @param {Mode | {}} [modeOptions] | |
| 588 * @returns {Partial<Mode>} | |
| 589 */ | |
| 590 const COMMENT = function(begin, end, modeOptions = {}) { | |
| 591 const mode = inherit$1( | |
| 592 { | |
| 593 scope: 'comment', | |
| 594 begin, | |
| 595 end, | |
| 596 contains: [] | |
| 597 }, | |
| 598 modeOptions | |
| 599 ); | |
| 600 mode.contains.push({ | |
| 601 scope: 'doctag', | |
| 602 // hack to avoid the space from being included. the space is necessary to | |
| 603 // match here to prevent the plain text rule below from gobbling up doctags | |
| 604 begin: '[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)', | |
| 605 end: /(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/, | |
| 606 excludeBegin: true, | |
| 607 relevance: 0 | |
| 608 }); | |
| 609 const ENGLISH_WORD = either( | |
| 610 // list of common 1 and 2 letter words in English | |
| 611 "I", | |
| 612 "a", | |
| 613 "is", | |
| 614 "so", | |
| 615 "us", | |
| 616 "to", | |
| 617 "at", | |
| 618 "if", | |
| 619 "in", | |
| 620 "it", | |
| 621 "on", | |
| 622 // note: this is not an exhaustive list of contractions, just popular ones | |
| 623 /[A-Za-z]+['](d|ve|re|ll|t|s|n)/, // contractions - can't we'd they're let's, etc | |
| 624 /[A-Za-z]+[-][a-z]+/, // `no-way`, etc. | |
| 625 /[A-Za-z][a-z]{2,}/ // allow capitalized words at beginning of sentences | |
| 626 ); | |
| 627 // looking like plain text, more likely to be a comment | |
| 628 mode.contains.push( | |
| 629 { | |
| 630 // TODO: how to include ", (, ) without breaking grammars that use these for | |
| 631 // comment delimiters? | |
| 632 // begin: /[ ]+([()"]?([A-Za-z'-]{3,}|is|a|I|so|us|[tT][oO]|at|if|in|it|on)[.]?[()":]?([.][ ]|[ ]|\))){3}/ | |
| 633 // --- | |
| 634 | |
| 635 // this tries to find sequences of 3 english words in a row (without any | |
| 636 // "programming" type syntax) this gives us a strong signal that we've | |
| 637 // TRULY found a comment - vs perhaps scanning with the wrong language. | |
| 638 // It's possible to find something that LOOKS like the start of the | |
| 639 // comment - but then if there is no readable text - good chance it is a | |
| 640 // false match and not a comment. | |
| 641 // | |
| 642 // for a visual example please see: | |
| 643 // https://github.com/highlightjs/highlight.js/issues/2827 | |
| 644 | |
| 645 begin: concat( | |
| 646 /[ ]+/, // necessary to prevent us gobbling up doctags like /* @author Bob Mcgill */ | |
| 647 '(', | |
| 648 ENGLISH_WORD, | |
| 649 /[.]?[:]?([.][ ]|[ ])/, | |
| 650 '){3}') // look for 3 words in a row | |
| 651 } | |
| 652 ); | |
| 653 return mode; | |
| 654 }; | |
| 655 const C_LINE_COMMENT_MODE = COMMENT('//', '$'); | |
| 656 const C_BLOCK_COMMENT_MODE = COMMENT('/\\*', '\\*/'); | |
| 657 const HASH_COMMENT_MODE = COMMENT('#', '$'); | |
| 658 const NUMBER_MODE = { | |
| 659 scope: 'number', | |
| 660 begin: NUMBER_RE, | |
| 661 relevance: 0 | |
| 662 }; | |
| 663 const C_NUMBER_MODE = { | |
| 664 scope: 'number', | |
| 665 begin: C_NUMBER_RE, | |
| 666 relevance: 0 | |
| 667 }; | |
| 668 const BINARY_NUMBER_MODE = { | |
| 669 scope: 'number', | |
| 670 begin: BINARY_NUMBER_RE, | |
| 671 relevance: 0 | |
| 672 }; | |
| 673 const REGEXP_MODE = { | |
| 674 scope: "regexp", | |
| 675 begin: /\/(?=[^/\n]*\/)/, | |
| 676 end: /\/[gimuy]*/, | |
| 677 contains: [ | |
| 678 BACKSLASH_ESCAPE, | |
| 679 { | |
| 680 begin: /\[/, | |
| 681 end: /\]/, | |
| 682 relevance: 0, | |
| 683 contains: [BACKSLASH_ESCAPE] | |
| 684 } | |
| 685 ] | |
| 686 }; | |
| 687 const TITLE_MODE = { | |
| 688 scope: 'title', | |
| 689 begin: IDENT_RE, | |
| 690 relevance: 0 | |
| 691 }; | |
| 692 const UNDERSCORE_TITLE_MODE = { | |
| 693 scope: 'title', | |
| 694 begin: UNDERSCORE_IDENT_RE, | |
| 695 relevance: 0 | |
| 696 }; | |
| 697 const METHOD_GUARD = { | |
| 698 // excludes method names from keyword processing | |
| 699 begin: '\\.\\s*' + UNDERSCORE_IDENT_RE, | |
| 700 relevance: 0 | |
| 701 }; | |
| 702 | |
| 703 /** | |
| 704 * Adds end same as begin mechanics to a mode | |
| 705 * | |
| 706 * Your mode must include at least a single () match group as that first match | |
| 707 * group is what is used for comparison | |
| 708 * @param {Partial<Mode>} mode | |
| 709 */ | |
| 710 const END_SAME_AS_BEGIN = function(mode) { | |
| 711 return Object.assign(mode, | |
| 712 { | |
| 713 /** @type {ModeCallback} */ | |
| 714 'on:begin': (m, resp) => { resp.data._beginMatch = m[1]; }, | |
| 715 /** @type {ModeCallback} */ | |
| 716 'on:end': (m, resp) => { if (resp.data._beginMatch !== m[1]) resp.ignoreMatch(); } | |
| 717 }); | |
| 718 }; | |
| 719 | |
| 720 var MODES = /*#__PURE__*/Object.freeze({ | |
| 721 __proto__: null, | |
| 722 APOS_STRING_MODE: APOS_STRING_MODE, | |
| 723 BACKSLASH_ESCAPE: BACKSLASH_ESCAPE, | |
| 724 BINARY_NUMBER_MODE: BINARY_NUMBER_MODE, | |
| 725 BINARY_NUMBER_RE: BINARY_NUMBER_RE, | |
| 726 COMMENT: COMMENT, | |
| 727 C_BLOCK_COMMENT_MODE: C_BLOCK_COMMENT_MODE, | |
| 728 C_LINE_COMMENT_MODE: C_LINE_COMMENT_MODE, | |
| 729 C_NUMBER_MODE: C_NUMBER_MODE, | |
| 730 C_NUMBER_RE: C_NUMBER_RE, | |
| 731 END_SAME_AS_BEGIN: END_SAME_AS_BEGIN, | |
| 732 HASH_COMMENT_MODE: HASH_COMMENT_MODE, | |
| 733 IDENT_RE: IDENT_RE, | |
| 734 MATCH_NOTHING_RE: MATCH_NOTHING_RE, | |
| 735 METHOD_GUARD: METHOD_GUARD, | |
| 736 NUMBER_MODE: NUMBER_MODE, | |
| 737 NUMBER_RE: NUMBER_RE, | |
| 738 PHRASAL_WORDS_MODE: PHRASAL_WORDS_MODE, | |
| 739 QUOTE_STRING_MODE: QUOTE_STRING_MODE, | |
| 740 REGEXP_MODE: REGEXP_MODE, | |
| 741 RE_STARTERS_RE: RE_STARTERS_RE, | |
| 742 SHEBANG: SHEBANG, | |
| 743 TITLE_MODE: TITLE_MODE, | |
| 744 UNDERSCORE_IDENT_RE: UNDERSCORE_IDENT_RE, | |
| 745 UNDERSCORE_TITLE_MODE: UNDERSCORE_TITLE_MODE | |
| 746 }); | |
| 747 | |
| 748 /** | |
| 749 @typedef {import('highlight.js').CallbackResponse} CallbackResponse | |
| 750 @typedef {import('highlight.js').CompilerExt} CompilerExt | |
| 751 */ | |
| 752 | |
| 753 // Grammar extensions / plugins | |
| 754 // See: https://github.com/highlightjs/highlight.js/issues/2833 | |
| 755 | |
| 756 // Grammar extensions allow "syntactic sugar" to be added to the grammar modes | |
| 757 // without requiring any underlying changes to the compiler internals. | |
| 758 | |
| 759 // `compileMatch` being the perfect small example of now allowing a grammar | |
| 760 // author to write `match` when they desire to match a single expression rather | |
| 761 // than being forced to use `begin`. The extension then just moves `match` into | |
| 762 // `begin` when it runs. Ie, no features have been added, but we've just made | |
| 763 // the experience of writing (and reading grammars) a little bit nicer. | |
| 764 | |
| 765 // ------ | |
| 766 | |
| 767 // TODO: We need negative look-behind support to do this properly | |
| 768 /** | |
| 769 * Skip a match if it has a preceding dot | |
| 770 * | |
| 771 * This is used for `beginKeywords` to prevent matching expressions such as | |
| 772 * `bob.keyword.do()`. The mode compiler automatically wires this up as a | |
| 773 * special _internal_ 'on:begin' callback for modes with `beginKeywords` | |
| 774 * @param {RegExpMatchArray} match | |
| 775 * @param {CallbackResponse} response | |
| 776 */ | |
| 777 function skipIfHasPrecedingDot(match, response) { | |
| 778 const before = match.input[match.index - 1]; | |
| 779 if (before === ".") { | |
| 780 response.ignoreMatch(); | |
| 781 } | |
| 782 } | |
| 783 | |
| 784 /** | |
| 785 * | |
| 786 * @type {CompilerExt} | |
| 787 */ | |
| 788 function scopeClassName(mode, _parent) { | |
| 789 // eslint-disable-next-line no-undefined | |
| 790 if (mode.className !== undefined) { | |
| 791 mode.scope = mode.className; | |
| 792 delete mode.className; | |
| 793 } | |
| 794 } | |
| 795 | |
| 796 /** | |
| 797 * `beginKeywords` syntactic sugar | |
| 798 * @type {CompilerExt} | |
| 799 */ | |
| 800 function beginKeywords(mode, parent) { | |
| 801 if (!parent) return; | |
| 802 if (!mode.beginKeywords) return; | |
| 803 | |
| 804 // for languages with keywords that include non-word characters checking for | |
| 805 // a word boundary is not sufficient, so instead we check for a word boundary | |
| 806 // or whitespace - this does no harm in any case since our keyword engine | |
| 807 // doesn't allow spaces in keywords anyways and we still check for the boundary | |
| 808 // first | |
| 809 mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')(?!\\.)(?=\\b|\\s)'; | |
| 810 mode.__beforeBegin = skipIfHasPrecedingDot; | |
| 811 mode.keywords = mode.keywords || mode.beginKeywords; | |
| 812 delete mode.beginKeywords; | |
| 813 | |
| 814 // prevents double relevance, the keywords themselves provide | |
| 815 // relevance, the mode doesn't need to double it | |
| 816 // eslint-disable-next-line no-undefined | |
| 817 if (mode.relevance === undefined) mode.relevance = 0; | |
| 818 } | |
| 819 | |
| 820 /** | |
| 821 * Allow `illegal` to contain an array of illegal values | |
| 822 * @type {CompilerExt} | |
| 823 */ | |
| 824 function compileIllegal(mode, _parent) { | |
| 825 if (!Array.isArray(mode.illegal)) return; | |
| 826 | |
| 827 mode.illegal = either(...mode.illegal); | |
| 828 } | |
| 829 | |
| 830 /** | |
| 831 * `match` to match a single expression for readability | |
| 832 * @type {CompilerExt} | |
| 833 */ | |
| 834 function compileMatch(mode, _parent) { | |
| 835 if (!mode.match) return; | |
| 836 if (mode.begin || mode.end) throw new Error("begin & end are not supported with match"); | |
| 837 | |
| 838 mode.begin = mode.match; | |
| 839 delete mode.match; | |
| 840 } | |
| 841 | |
| 842 /** | |
| 843 * provides the default 1 relevance to all modes | |
| 844 * @type {CompilerExt} | |
| 845 */ | |
| 846 function compileRelevance(mode, _parent) { | |
| 847 // eslint-disable-next-line no-undefined | |
| 848 if (mode.relevance === undefined) mode.relevance = 1; | |
| 849 } | |
| 850 | |
| 851 // allow beforeMatch to act as a "qualifier" for the match | |
| 852 // the full match begin must be [beforeMatch][begin] | |
| 853 const beforeMatchExt = (mode, parent) => { | |
| 854 if (!mode.beforeMatch) return; | |
| 855 // starts conflicts with endsParent which we need to make sure the child | |
| 856 // rule is not matched multiple times | |
| 857 if (mode.starts) throw new Error("beforeMatch cannot be used with starts"); | |
| 858 | |
| 859 const originalMode = Object.assign({}, mode); | |
| 860 Object.keys(mode).forEach((key) => { delete mode[key]; }); | |
| 861 | |
| 862 mode.keywords = originalMode.keywords; | |
| 863 mode.begin = concat(originalMode.beforeMatch, lookahead(originalMode.begin)); | |
| 864 mode.starts = { | |
| 865 relevance: 0, | |
| 866 contains: [ | |
| 867 Object.assign(originalMode, { endsParent: true }) | |
| 868 ] | |
| 869 }; | |
| 870 mode.relevance = 0; | |
| 871 | |
| 872 delete originalMode.beforeMatch; | |
| 873 }; | |
| 874 | |
| 875 // keywords that should have no default relevance value | |
| 876 const COMMON_KEYWORDS = [ | |
| 877 'of', | |
| 878 'and', | |
| 879 'for', | |
| 880 'in', | |
| 881 'not', | |
| 882 'or', | |
| 883 'if', | |
| 884 'then', | |
| 885 'parent', // common variable name | |
| 886 'list', // common variable name | |
| 887 'value' // common variable name | |
| 888 ]; | |
| 889 | |
| 890 const DEFAULT_KEYWORD_SCOPE = "keyword"; | |
| 891 | |
| 892 /** | |
| 893 * Given raw keywords from a language definition, compile them. | |
| 894 * | |
| 895 * @param {string | Record<string,string|string[]> | Array<string>} rawKeywords | |
| 896 * @param {boolean} caseInsensitive | |
| 897 */ | |
| 898 function compileKeywords(rawKeywords, caseInsensitive, scopeName = DEFAULT_KEYWORD_SCOPE) { | |
| 899 /** @type {import("highlight.js/private").KeywordDict} */ | |
| 900 const compiledKeywords = Object.create(null); | |
| 901 | |
| 902 // input can be a string of keywords, an array of keywords, or a object with | |
| 903 // named keys representing scopeName (which can then point to a string or array) | |
| 904 if (typeof rawKeywords === 'string') { | |
| 905 compileList(scopeName, rawKeywords.split(" ")); | |
| 906 } else if (Array.isArray(rawKeywords)) { | |
| 907 compileList(scopeName, rawKeywords); | |
| 908 } else { | |
| 909 Object.keys(rawKeywords).forEach(function(scopeName) { | |
| 910 // collapse all our objects back into the parent object | |
| 911 Object.assign( | |
| 912 compiledKeywords, | |
| 913 compileKeywords(rawKeywords[scopeName], caseInsensitive, scopeName) | |
| 914 ); | |
| 915 }); | |
| 916 } | |
| 917 return compiledKeywords; | |
| 918 | |
| 919 // --- | |
| 920 | |
| 921 /** | |
| 922 * Compiles an individual list of keywords | |
| 923 * | |
| 924 * Ex: "for if when while|5" | |
| 925 * | |
| 926 * @param {string} scopeName | |
| 927 * @param {Array<string>} keywordList | |
| 928 */ | |
| 929 function compileList(scopeName, keywordList) { | |
| 930 if (caseInsensitive) { | |
| 931 keywordList = keywordList.map(x => x.toLowerCase()); | |
| 932 } | |
| 933 keywordList.forEach(function(keyword) { | |
| 934 const pair = keyword.split('|'); | |
| 935 compiledKeywords[pair[0]] = [scopeName, scoreForKeyword(pair[0], pair[1])]; | |
| 936 }); | |
| 937 } | |
| 938 } | |
| 939 | |
| 940 /** | |
| 941 * Returns the proper score for a given keyword | |
| 942 * | |
| 943 * Also takes into account comment keywords, which will be scored 0 UNLESS | |
| 944 * another score has been manually assigned. | |
| 945 * @param {string} keyword | |
| 946 * @param {string} [providedScore] | |
| 947 */ | |
| 948 function scoreForKeyword(keyword, providedScore) { | |
| 949 // manual scores always win over common keywords | |
| 950 // so you can force a score of 1 if you really insist | |
| 951 if (providedScore) { | |
| 952 return Number(providedScore); | |
| 953 } | |
| 954 | |
| 955 return commonKeyword(keyword) ? 0 : 1; | |
| 956 } | |
| 957 | |
| 958 /** | |
| 959 * Determines if a given keyword is common or not | |
| 960 * | |
| 961 * @param {string} keyword */ | |
| 962 function commonKeyword(keyword) { | |
| 963 return COMMON_KEYWORDS.includes(keyword.toLowerCase()); | |
| 964 } | |
| 965 | |
| 966 /* | |
| 967 | |
| 968 For the reasoning behind this please see: | |
| 969 https://github.com/highlightjs/highlight.js/issues/2880#issuecomment-747275419 | |
| 970 | |
| 971 */ | |
| 972 | |
| 973 /** | |
| 974 * @type {Record<string, boolean>} | |
| 975 */ | |
| 976 const seenDeprecations = {}; | |
| 977 | |
| 978 /** | |
| 979 * @param {string} message | |
| 980 */ | |
| 981 const error = (message) => { | |
| 982 console.error(message); | |
| 983 }; | |
| 984 | |
| 985 /** | |
| 986 * @param {string} message | |
| 987 * @param {any} args | |
| 988 */ | |
| 989 const warn = (message, ...args) => { | |
| 990 console.log(`WARN: ${message}`, ...args); | |
| 991 }; | |
| 992 | |
| 993 /** | |
| 994 * @param {string} version | |
| 995 * @param {string} message | |
| 996 */ | |
| 997 const deprecated = (version, message) => { | |
| 998 if (seenDeprecations[`${version}/${message}`]) return; | |
| 999 | |
| 1000 console.log(`Deprecated as of ${version}. ${message}`); | |
| 1001 seenDeprecations[`${version}/${message}`] = true; | |
| 1002 }; | |
| 1003 | |
| 1004 /* eslint-disable no-throw-literal */ | |
| 1005 | |
| 1006 /** | |
| 1007 @typedef {import('highlight.js').CompiledMode} CompiledMode | |
| 1008 */ | |
| 1009 | |
| 1010 const MultiClassError = new Error(); | |
| 1011 | |
| 1012 /** | |
| 1013 * Renumbers labeled scope names to account for additional inner match | |
| 1014 * groups that otherwise would break everything. | |
| 1015 * | |
| 1016 * Lets say we 3 match scopes: | |
| 1017 * | |
| 1018 * { 1 => ..., 2 => ..., 3 => ... } | |
| 1019 * | |
| 1020 * So what we need is a clean match like this: | |
| 1021 * | |
| 1022 * (a)(b)(c) => [ "a", "b", "c" ] | |
| 1023 * | |
| 1024 * But this falls apart with inner match groups: | |
| 1025 * | |
| 1026 * (a)(((b)))(c) => ["a", "b", "b", "b", "c" ] | |
| 1027 * | |
| 1028 * Our scopes are now "out of alignment" and we're repeating `b` 3 times. | |
| 1029 * What needs to happen is the numbers are remapped: | |
| 1030 * | |
| 1031 * { 1 => ..., 2 => ..., 5 => ... } | |
| 1032 * | |
| 1033 * We also need to know that the ONLY groups that should be output | |
| 1034 * are 1, 2, and 5. This function handles this behavior. | |
| 1035 * | |
| 1036 * @param {CompiledMode} mode | |
| 1037 * @param {Array<RegExp | string>} regexes | |
| 1038 * @param {{key: "beginScope"|"endScope"}} opts | |
| 1039 */ | |
| 1040 function remapScopeNames(mode, regexes, { key }) { | |
| 1041 let offset = 0; | |
| 1042 const scopeNames = mode[key]; | |
| 1043 /** @type Record<number,boolean> */ | |
| 1044 const emit = {}; | |
| 1045 /** @type Record<number,string> */ | |
| 1046 const positions = {}; | |
| 1047 | |
| 1048 for (let i = 1; i <= regexes.length; i++) { | |
| 1049 positions[i + offset] = scopeNames[i]; | |
| 1050 emit[i + offset] = true; | |
| 1051 offset += countMatchGroups(regexes[i - 1]); | |
| 1052 } | |
| 1053 // we use _emit to keep track of which match groups are "top-level" to avoid double | |
| 1054 // output from inside match groups | |
| 1055 mode[key] = positions; | |
| 1056 mode[key]._emit = emit; | |
| 1057 mode[key]._multi = true; | |
| 1058 } | |
| 1059 | |
| 1060 /** | |
| 1061 * @param {CompiledMode} mode | |
| 1062 */ | |
| 1063 function beginMultiClass(mode) { | |
| 1064 if (!Array.isArray(mode.begin)) return; | |
| 1065 | |
| 1066 if (mode.skip || mode.excludeBegin || mode.returnBegin) { | |
| 1067 error("skip, excludeBegin, returnBegin not compatible with beginScope: {}"); | |
| 1068 throw MultiClassError; | |
| 1069 } | |
| 1070 | |
| 1071 if (typeof mode.beginScope !== "object" || mode.beginScope === null) { | |
| 1072 error("beginScope must be object"); | |
| 1073 throw MultiClassError; | |
| 1074 } | |
| 1075 | |
| 1076 remapScopeNames(mode, mode.begin, { key: "beginScope" }); | |
| 1077 mode.begin = _rewriteBackreferences(mode.begin, { joinWith: "" }); | |
| 1078 } | |
| 1079 | |
| 1080 /** | |
| 1081 * @param {CompiledMode} mode | |
| 1082 */ | |
| 1083 function endMultiClass(mode) { | |
| 1084 if (!Array.isArray(mode.end)) return; | |
| 1085 | |
| 1086 if (mode.skip || mode.excludeEnd || mode.returnEnd) { | |
| 1087 error("skip, excludeEnd, returnEnd not compatible with endScope: {}"); | |
| 1088 throw MultiClassError; | |
| 1089 } | |
| 1090 | |
| 1091 if (typeof mode.endScope !== "object" || mode.endScope === null) { | |
| 1092 error("endScope must be object"); | |
| 1093 throw MultiClassError; | |
| 1094 } | |
| 1095 | |
| 1096 remapScopeNames(mode, mode.end, { key: "endScope" }); | |
| 1097 mode.end = _rewriteBackreferences(mode.end, { joinWith: "" }); | |
| 1098 } | |
| 1099 | |
| 1100 /** | |
| 1101 * this exists only to allow `scope: {}` to be used beside `match:` | |
| 1102 * Otherwise `beginScope` would necessary and that would look weird | |
| 1103 | |
| 1104 { | |
| 1105 match: [ /def/, /\w+/ ] | |
| 1106 scope: { 1: "keyword" , 2: "title" } | |
| 1107 } | |
| 1108 | |
| 1109 * @param {CompiledMode} mode | |
| 1110 */ | |
| 1111 function scopeSugar(mode) { | |
| 1112 if (mode.scope && typeof mode.scope === "object" && mode.scope !== null) { | |
| 1113 mode.beginScope = mode.scope; | |
| 1114 delete mode.scope; | |
| 1115 } | |
| 1116 } | |
| 1117 | |
| 1118 /** | |
| 1119 * @param {CompiledMode} mode | |
| 1120 */ | |
| 1121 function MultiClass(mode) { | |
| 1122 scopeSugar(mode); | |
| 1123 | |
| 1124 if (typeof mode.beginScope === "string") { | |
| 1125 mode.beginScope = { _wrap: mode.beginScope }; | |
| 1126 } | |
| 1127 if (typeof mode.endScope === "string") { | |
| 1128 mode.endScope = { _wrap: mode.endScope }; | |
| 1129 } | |
| 1130 | |
| 1131 beginMultiClass(mode); | |
| 1132 endMultiClass(mode); | |
| 1133 } | |
| 1134 | |
| 1135 /** | |
| 1136 @typedef {import('highlight.js').Mode} Mode | |
| 1137 @typedef {import('highlight.js').CompiledMode} CompiledMode | |
| 1138 @typedef {import('highlight.js').Language} Language | |
| 1139 @typedef {import('highlight.js').HLJSPlugin} HLJSPlugin | |
| 1140 @typedef {import('highlight.js').CompiledLanguage} CompiledLanguage | |
| 1141 */ | |
| 1142 | |
| 1143 // compilation | |
| 1144 | |
| 1145 /** | |
| 1146 * Compiles a language definition result | |
| 1147 * | |
| 1148 * Given the raw result of a language definition (Language), compiles this so | |
| 1149 * that it is ready for highlighting code. | |
| 1150 * @param {Language} language | |
| 1151 * @returns {CompiledLanguage} | |
| 1152 */ | |
| 1153 function compileLanguage(language) { | |
| 1154 /** | |
| 1155 * Builds a regex with the case sensitivity of the current language | |
| 1156 * | |
| 1157 * @param {RegExp | string} value | |
| 1158 * @param {boolean} [global] | |
| 1159 */ | |
| 1160 function langRe(value, global) { | |
| 1161 return new RegExp( | |
| 1162 source(value), | |
| 1163 'm' | |
| 1164 + (language.case_insensitive ? 'i' : '') | |
| 1165 + (language.unicodeRegex ? 'u' : '') | |
| 1166 + (global ? 'g' : '') | |
| 1167 ); | |
| 1168 } | |
| 1169 | |
| 1170 /** | |
| 1171 Stores multiple regular expressions and allows you to quickly search for | |
| 1172 them all in a string simultaneously - returning the first match. It does | |
| 1173 this by creating a huge (a|b|c) regex - each individual item wrapped with () | |
| 1174 and joined by `|` - using match groups to track position. When a match is | |
| 1175 found checking which position in the array has content allows us to figure | |
| 1176 out which of the original regexes / match groups triggered the match. | |
| 1177 | |
| 1178 The match object itself (the result of `Regex.exec`) is returned but also | |
| 1179 enhanced by merging in any meta-data that was registered with the regex. | |
| 1180 This is how we keep track of which mode matched, and what type of rule | |
| 1181 (`illegal`, `begin`, end, etc). | |
| 1182 */ | |
| 1183 class MultiRegex { | |
| 1184 constructor() { | |
| 1185 this.matchIndexes = {}; | |
| 1186 // @ts-ignore | |
| 1187 this.regexes = []; | |
| 1188 this.matchAt = 1; | |
| 1189 this.position = 0; | |
| 1190 } | |
| 1191 | |
| 1192 // @ts-ignore | |
| 1193 addRule(re, opts) { | |
| 1194 opts.position = this.position++; | |
| 1195 // @ts-ignore | |
| 1196 this.matchIndexes[this.matchAt] = opts; | |
| 1197 this.regexes.push([opts, re]); | |
| 1198 this.matchAt += countMatchGroups(re) + 1; | |
| 1199 } | |
| 1200 | |
| 1201 compile() { | |
| 1202 if (this.regexes.length === 0) { | |
| 1203 // avoids the need to check length every time exec is called | |
| 1204 // @ts-ignore | |
| 1205 this.exec = () => null; | |
| 1206 } | |
| 1207 const terminators = this.regexes.map(el => el[1]); | |
| 1208 this.matcherRe = langRe(_rewriteBackreferences(terminators, { joinWith: '|' }), true); | |
| 1209 this.lastIndex = 0; | |
| 1210 } | |
| 1211 | |
| 1212 /** @param {string} s */ | |
| 1213 exec(s) { | |
| 1214 this.matcherRe.lastIndex = this.lastIndex; | |
| 1215 const match = this.matcherRe.exec(s); | |
| 1216 if (!match) { return null; } | |
| 1217 | |
| 1218 // eslint-disable-next-line no-undefined | |
| 1219 const i = match.findIndex((el, i) => i > 0 && el !== undefined); | |
| 1220 // @ts-ignore | |
| 1221 const matchData = this.matchIndexes[i]; | |
| 1222 // trim off any earlier non-relevant match groups (ie, the other regex | |
| 1223 // match groups that make up the multi-matcher) | |
| 1224 match.splice(0, i); | |
| 1225 | |
| 1226 return Object.assign(match, matchData); | |
| 1227 } | |
| 1228 } | |
| 1229 | |
| 1230 /* | |
| 1231 Created to solve the key deficiently with MultiRegex - there is no way to | |
| 1232 test for multiple matches at a single location. Why would we need to do | |
| 1233 that? In the future a more dynamic engine will allow certain matches to be | |
| 1234 ignored. An example: if we matched say the 3rd regex in a large group but | |
| 1235 decided to ignore it - we'd need to started testing again at the 4th | |
| 1236 regex... but MultiRegex itself gives us no real way to do that. | |
| 1237 | |
| 1238 So what this class creates MultiRegexs on the fly for whatever search | |
| 1239 position they are needed. | |
| 1240 | |
| 1241 NOTE: These additional MultiRegex objects are created dynamically. For most | |
| 1242 grammars most of the time we will never actually need anything more than the | |
| 1243 first MultiRegex - so this shouldn't have too much overhead. | |
| 1244 | |
| 1245 Say this is our search group, and we match regex3, but wish to ignore it. | |
| 1246 | |
| 1247 regex1 | regex2 | regex3 | regex4 | regex5 ' ie, startAt = 0 | |
| 1248 | |
| 1249 What we need is a new MultiRegex that only includes the remaining | |
| 1250 possibilities: | |
| 1251 | |
| 1252 regex4 | regex5 ' ie, startAt = 3 | |
| 1253 | |
| 1254 This class wraps all that complexity up in a simple API... `startAt` decides | |
| 1255 where in the array of expressions to start doing the matching. It | |
| 1256 auto-increments, so if a match is found at position 2, then startAt will be | |
| 1257 set to 3. If the end is reached startAt will return to 0. | |
| 1258 | |
| 1259 MOST of the time the parser will be setting startAt manually to 0. | |
| 1260 */ | |
| 1261 class ResumableMultiRegex { | |
| 1262 constructor() { | |
| 1263 // @ts-ignore | |
| 1264 this.rules = []; | |
| 1265 // @ts-ignore | |
| 1266 this.multiRegexes = []; | |
| 1267 this.count = 0; | |
| 1268 | |
| 1269 this.lastIndex = 0; | |
| 1270 this.regexIndex = 0; | |
| 1271 } | |
| 1272 | |
| 1273 // @ts-ignore | |
| 1274 getMatcher(index) { | |
| 1275 if (this.multiRegexes[index]) return this.multiRegexes[index]; | |
| 1276 | |
| 1277 const matcher = new MultiRegex(); | |
| 1278 this.rules.slice(index).forEach(([re, opts]) => matcher.addRule(re, opts)); | |
| 1279 matcher.compile(); | |
| 1280 this.multiRegexes[index] = matcher; | |
| 1281 return matcher; | |
| 1282 } | |
| 1283 | |
| 1284 resumingScanAtSamePosition() { | |
| 1285 return this.regexIndex !== 0; | |
| 1286 } | |
| 1287 | |
| 1288 considerAll() { | |
| 1289 this.regexIndex = 0; | |
| 1290 } | |
| 1291 | |
| 1292 // @ts-ignore | |
| 1293 addRule(re, opts) { | |
| 1294 this.rules.push([re, opts]); | |
| 1295 if (opts.type === "begin") this.count++; | |
| 1296 } | |
| 1297 | |
| 1298 /** @param {string} s */ | |
| 1299 exec(s) { | |
| 1300 const m = this.getMatcher(this.regexIndex); | |
| 1301 m.lastIndex = this.lastIndex; | |
| 1302 let result = m.exec(s); | |
| 1303 | |
| 1304 // The following is because we have no easy way to say "resume scanning at the | |
| 1305 // existing position but also skip the current rule ONLY". What happens is | |
| 1306 // all prior rules are also skipped which can result in matching the wrong | |
| 1307 // thing. Example of matching "booger": | |
| 1308 | |
| 1309 // our matcher is [string, "booger", number] | |
| 1310 // | |
| 1311 // ....booger.... | |
| 1312 | |
| 1313 // if "booger" is ignored then we'd really need a regex to scan from the | |
| 1314 // SAME position for only: [string, number] but ignoring "booger" (if it | |
| 1315 // was the first match), a simple resume would scan ahead who knows how | |
| 1316 // far looking only for "number", ignoring potential string matches (or | |
| 1317 // future "booger" matches that might be valid.) | |
| 1318 | |
| 1319 // So what we do: We execute two matchers, one resuming at the same | |
| 1320 // position, but the second full matcher starting at the position after: | |
| 1321 | |
| 1322 // /--- resume first regex match here (for [number]) | |
| 1323 // |/---- full match here for [string, "booger", number] | |
| 1324 // vv | |
| 1325 // ....booger.... | |
| 1326 | |
| 1327 // Which ever results in a match first is then used. So this 3-4 step | |
| 1328 // process essentially allows us to say "match at this position, excluding | |
| 1329 // a prior rule that was ignored". | |
| 1330 // | |
| 1331 // 1. Match "booger" first, ignore. Also proves that [string] does non match. | |
| 1332 // 2. Resume matching for [number] | |
| 1333 // 3. Match at index + 1 for [string, "booger", number] | |
| 1334 // 4. If #2 and #3 result in matches, which came first? | |
| 1335 if (this.resumingScanAtSamePosition()) { | |
| 1336 if (result && result.index === this.lastIndex) ; else { // use the second matcher result | |
| 1337 const m2 = this.getMatcher(0); | |
| 1338 m2.lastIndex = this.lastIndex + 1; | |
| 1339 result = m2.exec(s); | |
| 1340 } | |
| 1341 } | |
| 1342 | |
| 1343 if (result) { | |
| 1344 this.regexIndex += result.position + 1; | |
| 1345 if (this.regexIndex === this.count) { | |
| 1346 // wrap-around to considering all matches again | |
| 1347 this.considerAll(); | |
| 1348 } | |
| 1349 } | |
| 1350 | |
| 1351 return result; | |
| 1352 } | |
| 1353 } | |
| 1354 | |
| 1355 /** | |
| 1356 * Given a mode, builds a huge ResumableMultiRegex that can be used to walk | |
| 1357 * the content and find matches. | |
| 1358 * | |
| 1359 * @param {CompiledMode} mode | |
| 1360 * @returns {ResumableMultiRegex} | |
| 1361 */ | |
| 1362 function buildModeRegex(mode) { | |
| 1363 const mm = new ResumableMultiRegex(); | |
| 1364 | |
| 1365 mode.contains.forEach(term => mm.addRule(term.begin, { rule: term, type: "begin" })); | |
| 1366 | |
| 1367 if (mode.terminatorEnd) { | |
| 1368 mm.addRule(mode.terminatorEnd, { type: "end" }); | |
| 1369 } | |
| 1370 if (mode.illegal) { | |
| 1371 mm.addRule(mode.illegal, { type: "illegal" }); | |
| 1372 } | |
| 1373 | |
| 1374 return mm; | |
| 1375 } | |
| 1376 | |
| 1377 /** skip vs abort vs ignore | |
| 1378 * | |
| 1379 * @skip - The mode is still entered and exited normally (and contains rules apply), | |
| 1380 * but all content is held and added to the parent buffer rather than being | |
| 1381 * output when the mode ends. Mostly used with `sublanguage` to build up | |
| 1382 * a single large buffer than can be parsed by sublanguage. | |
| 1383 * | |
| 1384 * - The mode begin ands ends normally. | |
| 1385 * - Content matched is added to the parent mode buffer. | |
| 1386 * - The parser cursor is moved forward normally. | |
| 1387 * | |
| 1388 * @abort - A hack placeholder until we have ignore. Aborts the mode (as if it | |
| 1389 * never matched) but DOES NOT continue to match subsequent `contains` | |
| 1390 * modes. Abort is bad/suboptimal because it can result in modes | |
| 1391 * farther down not getting applied because an earlier rule eats the | |
| 1392 * content but then aborts. | |
| 1393 * | |
| 1394 * - The mode does not begin. | |
| 1395 * - Content matched by `begin` is added to the mode buffer. | |
| 1396 * - The parser cursor is moved forward accordingly. | |
| 1397 * | |
| 1398 * @ignore - Ignores the mode (as if it never matched) and continues to match any | |
| 1399 * subsequent `contains` modes. Ignore isn't technically possible with | |
| 1400 * the current parser implementation. | |
| 1401 * | |
| 1402 * - The mode does not begin. | |
| 1403 * - Content matched by `begin` is ignored. | |
| 1404 * - The parser cursor is not moved forward. | |
| 1405 */ | |
| 1406 | |
| 1407 /** | |
| 1408 * Compiles an individual mode | |
| 1409 * | |
| 1410 * This can raise an error if the mode contains certain detectable known logic | |
| 1411 * issues. | |
| 1412 * @param {Mode} mode | |
| 1413 * @param {CompiledMode | null} [parent] | |
| 1414 * @returns {CompiledMode | never} | |
| 1415 */ | |
| 1416 function compileMode(mode, parent) { | |
| 1417 const cmode = /** @type CompiledMode */ (mode); | |
| 1418 if (mode.isCompiled) return cmode; | |
| 1419 | |
| 1420 [ | |
| 1421 scopeClassName, | |
| 1422 // do this early so compiler extensions generally don't have to worry about | |
| 1423 // the distinction between match/begin | |
| 1424 compileMatch, | |
| 1425 MultiClass, | |
| 1426 beforeMatchExt | |
| 1427 ].forEach(ext => ext(mode, parent)); | |
| 1428 | |
| 1429 language.compilerExtensions.forEach(ext => ext(mode, parent)); | |
| 1430 | |
| 1431 // __beforeBegin is considered private API, internal use only | |
| 1432 mode.__beforeBegin = null; | |
| 1433 | |
| 1434 [ | |
| 1435 beginKeywords, | |
| 1436 // do this later so compiler extensions that come earlier have access to the | |
| 1437 // raw array if they wanted to perhaps manipulate it, etc. | |
| 1438 compileIllegal, | |
| 1439 // default to 1 relevance if not specified | |
| 1440 compileRelevance | |
| 1441 ].forEach(ext => ext(mode, parent)); | |
| 1442 | |
| 1443 mode.isCompiled = true; | |
| 1444 | |
| 1445 let keywordPattern = null; | |
| 1446 if (typeof mode.keywords === "object" && mode.keywords.$pattern) { | |
| 1447 // we need a copy because keywords might be compiled multiple times | |
| 1448 // so we can't go deleting $pattern from the original on the first | |
| 1449 // pass | |
| 1450 mode.keywords = Object.assign({}, mode.keywords); | |
| 1451 keywordPattern = mode.keywords.$pattern; | |
| 1452 delete mode.keywords.$pattern; | |
| 1453 } | |
| 1454 keywordPattern = keywordPattern || /\w+/; | |
| 1455 | |
| 1456 if (mode.keywords) { | |
| 1457 mode.keywords = compileKeywords(mode.keywords, language.case_insensitive); | |
| 1458 } | |
| 1459 | |
| 1460 cmode.keywordPatternRe = langRe(keywordPattern, true); | |
| 1461 | |
| 1462 if (parent) { | |
| 1463 if (!mode.begin) mode.begin = /\B|\b/; | |
| 1464 cmode.beginRe = langRe(cmode.begin); | |
| 1465 if (!mode.end && !mode.endsWithParent) mode.end = /\B|\b/; | |
| 1466 if (mode.end) cmode.endRe = langRe(cmode.end); | |
| 1467 cmode.terminatorEnd = source(cmode.end) || ''; | |
| 1468 if (mode.endsWithParent && parent.terminatorEnd) { | |
| 1469 cmode.terminatorEnd += (mode.end ? '|' : '') + parent.terminatorEnd; | |
| 1470 } | |
| 1471 } | |
| 1472 if (mode.illegal) cmode.illegalRe = langRe(/** @type {RegExp | string} */ (mode.illegal)); | |
| 1473 if (!mode.contains) mode.contains = []; | |
| 1474 | |
| 1475 mode.contains = [].concat(...mode.contains.map(function(c) { | |
| 1476 return expandOrCloneMode(c === 'self' ? mode : c); | |
| 1477 })); | |
| 1478 mode.contains.forEach(function(c) { compileMode(/** @type Mode */ (c), cmode); }); | |
| 1479 | |
| 1480 if (mode.starts) { | |
| 1481 compileMode(mode.starts, parent); | |
| 1482 } | |
| 1483 | |
| 1484 cmode.matcher = buildModeRegex(cmode); | |
| 1485 return cmode; | |
| 1486 } | |
| 1487 | |
| 1488 if (!language.compilerExtensions) language.compilerExtensions = []; | |
| 1489 | |
| 1490 // self is not valid at the top-level | |
| 1491 if (language.contains && language.contains.includes('self')) { | |
| 1492 throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation."); | |
| 1493 } | |
| 1494 | |
| 1495 // we need a null object, which inherit will guarantee | |
| 1496 language.classNameAliases = inherit$1(language.classNameAliases || {}); | |
| 1497 | |
| 1498 return compileMode(/** @type Mode */ (language)); | |
| 1499 } | |
| 1500 | |
| 1501 /** | |
| 1502 * Determines if a mode has a dependency on it's parent or not | |
| 1503 * | |
| 1504 * If a mode does have a parent dependency then often we need to clone it if | |
| 1505 * it's used in multiple places so that each copy points to the correct parent, | |
| 1506 * where-as modes without a parent can often safely be re-used at the bottom of | |
| 1507 * a mode chain. | |
| 1508 * | |
| 1509 * @param {Mode | null} mode | |
| 1510 * @returns {boolean} - is there a dependency on the parent? | |
| 1511 * */ | |
| 1512 function dependencyOnParent(mode) { | |
| 1513 if (!mode) return false; | |
| 1514 | |
| 1515 return mode.endsWithParent || dependencyOnParent(mode.starts); | |
| 1516 } | |
| 1517 | |
| 1518 /** | |
| 1519 * Expands a mode or clones it if necessary | |
| 1520 * | |
| 1521 * This is necessary for modes with parental dependenceis (see notes on | |
| 1522 * `dependencyOnParent`) and for nodes that have `variants` - which must then be | |
| 1523 * exploded into their own individual modes at compile time. | |
| 1524 * | |
| 1525 * @param {Mode} mode | |
| 1526 * @returns {Mode | Mode[]} | |
| 1527 * */ | |
| 1528 function expandOrCloneMode(mode) { | |
| 1529 if (mode.variants && !mode.cachedVariants) { | |
| 1530 mode.cachedVariants = mode.variants.map(function(variant) { | |
| 1531 return inherit$1(mode, { variants: null }, variant); | |
| 1532 }); | |
| 1533 } | |
| 1534 | |
| 1535 // EXPAND | |
| 1536 // if we have variants then essentially "replace" the mode with the variants | |
| 1537 // this happens in compileMode, where this function is called from | |
| 1538 if (mode.cachedVariants) { | |
| 1539 return mode.cachedVariants; | |
| 1540 } | |
| 1541 | |
| 1542 // CLONE | |
| 1543 // if we have dependencies on parents then we need a unique | |
| 1544 // instance of ourselves, so we can be reused with many | |
| 1545 // different parents without issue | |
| 1546 if (dependencyOnParent(mode)) { | |
| 1547 return inherit$1(mode, { starts: mode.starts ? inherit$1(mode.starts) : null }); | |
| 1548 } | |
| 1549 | |
| 1550 if (Object.isFrozen(mode)) { | |
| 1551 return inherit$1(mode); | |
| 1552 } | |
| 1553 | |
| 1554 // no special dependency issues, just return ourselves | |
| 1555 return mode; | |
| 1556 } | |
| 1557 | |
| 1558 var version = "11.11.1"; | |
| 1559 | |
| 1560 class HTMLInjectionError extends Error { | |
| 1561 constructor(reason, html) { | |
| 1562 super(reason); | |
| 1563 this.name = "HTMLInjectionError"; | |
| 1564 this.html = html; | |
| 1565 } | |
| 1566 } | |
| 1567 | |
| 1568 /* | |
| 1569 Syntax highlighting with language autodetection. | |
| 1570 https://highlightjs.org/ | |
| 1571 */ | |
| 1572 | |
| 1573 | |
| 1574 | |
| 1575 /** | |
| 1576 @typedef {import('highlight.js').Mode} Mode | |
| 1577 @typedef {import('highlight.js').CompiledMode} CompiledMode | |
| 1578 @typedef {import('highlight.js').CompiledScope} CompiledScope | |
| 1579 @typedef {import('highlight.js').Language} Language | |
| 1580 @typedef {import('highlight.js').HLJSApi} HLJSApi | |
| 1581 @typedef {import('highlight.js').HLJSPlugin} HLJSPlugin | |
| 1582 @typedef {import('highlight.js').PluginEvent} PluginEvent | |
| 1583 @typedef {import('highlight.js').HLJSOptions} HLJSOptions | |
| 1584 @typedef {import('highlight.js').LanguageFn} LanguageFn | |
| 1585 @typedef {import('highlight.js').HighlightedHTMLElement} HighlightedHTMLElement | |
| 1586 @typedef {import('highlight.js').BeforeHighlightContext} BeforeHighlightContext | |
| 1587 @typedef {import('highlight.js/private').MatchType} MatchType | |
| 1588 @typedef {import('highlight.js/private').KeywordData} KeywordData | |
| 1589 @typedef {import('highlight.js/private').EnhancedMatch} EnhancedMatch | |
| 1590 @typedef {import('highlight.js/private').AnnotatedError} AnnotatedError | |
| 1591 @typedef {import('highlight.js').AutoHighlightResult} AutoHighlightResult | |
| 1592 @typedef {import('highlight.js').HighlightOptions} HighlightOptions | |
| 1593 @typedef {import('highlight.js').HighlightResult} HighlightResult | |
| 1594 */ | |
| 1595 | |
| 1596 | |
| 1597 const escape = escapeHTML; | |
| 1598 const inherit = inherit$1; | |
| 1599 const NO_MATCH = Symbol("nomatch"); | |
| 1600 const MAX_KEYWORD_HITS = 7; | |
| 1601 | |
| 1602 /** | |
| 1603 * @param {any} hljs - object that is extended (legacy) | |
| 1604 * @returns {HLJSApi} | |
| 1605 */ | |
| 1606 const HLJS = function(hljs) { | |
| 1607 // Global internal variables used within the highlight.js library. | |
| 1608 /** @type {Record<string, Language>} */ | |
| 1609 const languages = Object.create(null); | |
| 1610 /** @type {Record<string, string>} */ | |
| 1611 const aliases = Object.create(null); | |
| 1612 /** @type {HLJSPlugin[]} */ | |
| 1613 const plugins = []; | |
| 1614 | |
| 1615 // safe/production mode - swallows more errors, tries to keep running | |
| 1616 // even if a single syntax or parse hits a fatal error | |
| 1617 let SAFE_MODE = true; | |
| 1618 const LANGUAGE_NOT_FOUND = "Could not find the language '{}', did you forget to load/include a language module?"; | |
| 1619 /** @type {Language} */ | |
| 1620 const PLAINTEXT_LANGUAGE = { disableAutodetect: true, name: 'Plain text', contains: [] }; | |
| 1621 | |
| 1622 // Global options used when within external APIs. This is modified when | |
| 1623 // calling the `hljs.configure` function. | |
| 1624 /** @type HLJSOptions */ | |
| 1625 let options = { | |
| 1626 ignoreUnescapedHTML: false, | |
| 1627 throwUnescapedHTML: false, | |
| 1628 noHighlightRe: /^(no-?highlight)$/i, | |
| 1629 languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, | |
| 1630 classPrefix: 'hljs-', | |
| 1631 cssSelector: 'pre code', | |
| 1632 languages: null, | |
| 1633 // beta configuration options, subject to change, welcome to discuss | |
| 1634 // https://github.com/highlightjs/highlight.js/issues/1086 | |
| 1635 __emitter: TokenTreeEmitter | |
| 1636 }; | |
| 1637 | |
| 1638 /* Utility functions */ | |
| 1639 | |
| 1640 /** | |
| 1641 * Tests a language name to see if highlighting should be skipped | |
| 1642 * @param {string} languageName | |
| 1643 */ | |
| 1644 function shouldNotHighlight(languageName) { | |
| 1645 return options.noHighlightRe.test(languageName); | |
| 1646 } | |
| 1647 | |
| 1648 /** | |
| 1649 * @param {HighlightedHTMLElement} block - the HTML element to determine language for | |
| 1650 */ | |
| 1651 function blockLanguage(block) { | |
| 1652 let classes = block.className + ' '; | |
| 1653 | |
| 1654 classes += block.parentNode ? block.parentNode.className : ''; | |
| 1655 | |
| 1656 // language-* takes precedence over non-prefixed class names. | |
| 1657 const match = options.languageDetectRe.exec(classes); | |
| 1658 if (match) { | |
| 1659 const language = getLanguage(match[1]); | |
| 1660 if (!language) { | |
| 1661 warn(LANGUAGE_NOT_FOUND.replace("{}", match[1])); | |
| 1662 warn("Falling back to no-highlight mode for this block.", block); | |
| 1663 } | |
| 1664 return language ? match[1] : 'no-highlight'; | |
| 1665 } | |
| 1666 | |
| 1667 return classes | |
| 1668 .split(/\s+/) | |
| 1669 .find((_class) => shouldNotHighlight(_class) || getLanguage(_class)); | |
| 1670 } | |
| 1671 | |
| 1672 /** | |
| 1673 * Core highlighting function. | |
| 1674 * | |
| 1675 * OLD API | |
| 1676 * highlight(lang, code, ignoreIllegals, continuation) | |
| 1677 * | |
| 1678 * NEW API | |
| 1679 * highlight(code, {lang, ignoreIllegals}) | |
| 1680 * | |
| 1681 * @param {string} codeOrLanguageName - the language to use for highlighting | |
| 1682 * @param {string | HighlightOptions} optionsOrCode - the code to highlight | |
| 1683 * @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail | |
| 1684 * | |
| 1685 * @returns {HighlightResult} Result - an object that represents the result | |
| 1686 * @property {string} language - the language name | |
| 1687 * @property {number} relevance - the relevance score | |
| 1688 * @property {string} value - the highlighted HTML code | |
| 1689 * @property {string} code - the original raw code | |
| 1690 * @property {CompiledMode} top - top of the current mode stack | |
| 1691 * @property {boolean} illegal - indicates whether any illegal matches were found | |
| 1692 */ | |
| 1693 function highlight(codeOrLanguageName, optionsOrCode, ignoreIllegals) { | |
| 1694 let code = ""; | |
| 1695 let languageName = ""; | |
| 1696 if (typeof optionsOrCode === "object") { | |
| 1697 code = codeOrLanguageName; | |
| 1698 ignoreIllegals = optionsOrCode.ignoreIllegals; | |
| 1699 languageName = optionsOrCode.language; | |
| 1700 } else { | |
| 1701 // old API | |
| 1702 deprecated("10.7.0", "highlight(lang, code, ...args) has been deprecated."); | |
| 1703 deprecated("10.7.0", "Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"); | |
| 1704 languageName = codeOrLanguageName; | |
| 1705 code = optionsOrCode; | |
| 1706 } | |
| 1707 | |
| 1708 // https://github.com/highlightjs/highlight.js/issues/3149 | |
| 1709 // eslint-disable-next-line no-undefined | |
| 1710 if (ignoreIllegals === undefined) { ignoreIllegals = true; } | |
| 1711 | |
| 1712 /** @type {BeforeHighlightContext} */ | |
| 1713 const context = { | |
| 1714 code, | |
| 1715 language: languageName | |
| 1716 }; | |
| 1717 // the plugin can change the desired language or the code to be highlighted | |
| 1718 // just be changing the object it was passed | |
| 1719 fire("before:highlight", context); | |
| 1720 | |
| 1721 // a before plugin can usurp the result completely by providing it's own | |
| 1722 // in which case we don't even need to call highlight | |
| 1723 const result = context.result | |
| 1724 ? context.result | |
| 1725 : _highlight(context.language, context.code, ignoreIllegals); | |
| 1726 | |
| 1727 result.code = context.code; | |
| 1728 // the plugin can change anything in result to suite it | |
| 1729 fire("after:highlight", result); | |
| 1730 | |
| 1731 return result; | |
| 1732 } | |
| 1733 | |
| 1734 /** | |
| 1735 * private highlight that's used internally and does not fire callbacks | |
| 1736 * | |
| 1737 * @param {string} languageName - the language to use for highlighting | |
| 1738 * @param {string} codeToHighlight - the code to highlight | |
| 1739 * @param {boolean?} [ignoreIllegals] - whether to ignore illegal matches, default is to bail | |
| 1740 * @param {CompiledMode?} [continuation] - current continuation mode, if any | |
| 1741 * @returns {HighlightResult} - result of the highlight operation | |
| 1742 */ | |
| 1743 function _highlight(languageName, codeToHighlight, ignoreIllegals, continuation) { | |
| 1744 const keywordHits = Object.create(null); | |
| 1745 | |
| 1746 /** | |
| 1747 * Return keyword data if a match is a keyword | |
| 1748 * @param {CompiledMode} mode - current mode | |
| 1749 * @param {string} matchText - the textual match | |
| 1750 * @returns {KeywordData | false} | |
| 1751 */ | |
| 1752 function keywordData(mode, matchText) { | |
| 1753 return mode.keywords[matchText]; | |
| 1754 } | |
| 1755 | |
| 1756 function processKeywords() { | |
| 1757 if (!top.keywords) { | |
| 1758 emitter.addText(modeBuffer); | |
| 1759 return; | |
| 1760 } | |
| 1761 | |
| 1762 let lastIndex = 0; | |
| 1763 top.keywordPatternRe.lastIndex = 0; | |
| 1764 let match = top.keywordPatternRe.exec(modeBuffer); | |
| 1765 let buf = ""; | |
| 1766 | |
| 1767 while (match) { | |
| 1768 buf += modeBuffer.substring(lastIndex, match.index); | |
| 1769 const word = language.case_insensitive ? match[0].toLowerCase() : match[0]; | |
| 1770 const data = keywordData(top, word); | |
| 1771 if (data) { | |
| 1772 const [kind, keywordRelevance] = data; | |
| 1773 emitter.addText(buf); | |
| 1774 buf = ""; | |
| 1775 | |
| 1776 keywordHits[word] = (keywordHits[word] || 0) + 1; | |
| 1777 if (keywordHits[word] <= MAX_KEYWORD_HITS) relevance += keywordRelevance; | |
| 1778 if (kind.startsWith("_")) { | |
| 1779 // _ implied for relevance only, do not highlight | |
| 1780 // by applying a class name | |
| 1781 buf += match[0]; | |
| 1782 } else { | |
| 1783 const cssClass = language.classNameAliases[kind] || kind; | |
| 1784 emitKeyword(match[0], cssClass); | |
| 1785 } | |
| 1786 } else { | |
| 1787 buf += match[0]; | |
| 1788 } | |
| 1789 lastIndex = top.keywordPatternRe.lastIndex; | |
| 1790 match = top.keywordPatternRe.exec(modeBuffer); | |
| 1791 } | |
| 1792 buf += modeBuffer.substring(lastIndex); | |
| 1793 emitter.addText(buf); | |
| 1794 } | |
| 1795 | |
| 1796 function processSubLanguage() { | |
| 1797 if (modeBuffer === "") return; | |
| 1798 /** @type HighlightResult */ | |
| 1799 let result = null; | |
| 1800 | |
| 1801 if (typeof top.subLanguage === 'string') { | |
| 1802 if (!languages[top.subLanguage]) { | |
| 1803 emitter.addText(modeBuffer); | |
| 1804 return; | |
| 1805 } | |
| 1806 result = _highlight(top.subLanguage, modeBuffer, true, continuations[top.subLanguage]); | |
| 1807 continuations[top.subLanguage] = /** @type {CompiledMode} */ (result._top); | |
| 1808 } else { | |
| 1809 result = highlightAuto(modeBuffer, top.subLanguage.length ? top.subLanguage : null); | |
| 1810 } | |
| 1811 | |
| 1812 // Counting embedded language score towards the host language may be disabled | |
| 1813 // with zeroing the containing mode relevance. Use case in point is Markdown that | |
| 1814 // allows XML everywhere and makes every XML snippet to have a much larger Markdown | |
| 1815 // score. | |
| 1816 if (top.relevance > 0) { | |
| 1817 relevance += result.relevance; | |
| 1818 } | |
| 1819 emitter.__addSublanguage(result._emitter, result.language); | |
| 1820 } | |
| 1821 | |
| 1822 function processBuffer() { | |
| 1823 if (top.subLanguage != null) { | |
| 1824 processSubLanguage(); | |
| 1825 } else { | |
| 1826 processKeywords(); | |
| 1827 } | |
| 1828 modeBuffer = ''; | |
| 1829 } | |
| 1830 | |
| 1831 /** | |
| 1832 * @param {string} text | |
| 1833 * @param {string} scope | |
| 1834 */ | |
| 1835 function emitKeyword(keyword, scope) { | |
| 1836 if (keyword === "") return; | |
| 1837 | |
| 1838 emitter.startScope(scope); | |
| 1839 emitter.addText(keyword); | |
| 1840 emitter.endScope(); | |
| 1841 } | |
| 1842 | |
| 1843 /** | |
| 1844 * @param {CompiledScope} scope | |
| 1845 * @param {RegExpMatchArray} match | |
| 1846 */ | |
| 1847 function emitMultiClass(scope, match) { | |
| 1848 let i = 1; | |
| 1849 const max = match.length - 1; | |
| 1850 while (i <= max) { | |
| 1851 if (!scope._emit[i]) { i++; continue; } | |
| 1852 const klass = language.classNameAliases[scope[i]] || scope[i]; | |
| 1853 const text = match[i]; | |
| 1854 if (klass) { | |
| 1855 emitKeyword(text, klass); | |
| 1856 } else { | |
| 1857 modeBuffer = text; | |
| 1858 processKeywords(); | |
| 1859 modeBuffer = ""; | |
| 1860 } | |
| 1861 i++; | |
| 1862 } | |
| 1863 } | |
| 1864 | |
| 1865 /** | |
| 1866 * @param {CompiledMode} mode - new mode to start | |
| 1867 * @param {RegExpMatchArray} match | |
| 1868 */ | |
| 1869 function startNewMode(mode, match) { | |
| 1870 if (mode.scope && typeof mode.scope === "string") { | |
| 1871 emitter.openNode(language.classNameAliases[mode.scope] || mode.scope); | |
| 1872 } | |
| 1873 if (mode.beginScope) { | |
| 1874 // beginScope just wraps the begin match itself in a scope | |
| 1875 if (mode.beginScope._wrap) { | |
| 1876 emitKeyword(modeBuffer, language.classNameAliases[mode.beginScope._wrap] || mode.beginScope._wrap); | |
| 1877 modeBuffer = ""; | |
| 1878 } else if (mode.beginScope._multi) { | |
| 1879 // at this point modeBuffer should just be the match | |
| 1880 emitMultiClass(mode.beginScope, match); | |
| 1881 modeBuffer = ""; | |
| 1882 } | |
| 1883 } | |
| 1884 | |
| 1885 top = Object.create(mode, { parent: { value: top } }); | |
| 1886 return top; | |
| 1887 } | |
| 1888 | |
| 1889 /** | |
| 1890 * @param {CompiledMode } mode - the mode to potentially end | |
| 1891 * @param {RegExpMatchArray} match - the latest match | |
| 1892 * @param {string} matchPlusRemainder - match plus remainder of content | |
| 1893 * @returns {CompiledMode | void} - the next mode, or if void continue on in current mode | |
| 1894 */ | |
| 1895 function endOfMode(mode, match, matchPlusRemainder) { | |
| 1896 let matched = startsWith(mode.endRe, matchPlusRemainder); | |
| 1897 | |
| 1898 if (matched) { | |
| 1899 if (mode["on:end"]) { | |
| 1900 const resp = new Response(mode); | |
| 1901 mode["on:end"](match, resp); | |
| 1902 if (resp.isMatchIgnored) matched = false; | |
| 1903 } | |
| 1904 | |
| 1905 if (matched) { | |
| 1906 while (mode.endsParent && mode.parent) { | |
| 1907 mode = mode.parent; | |
| 1908 } | |
| 1909 return mode; | |
| 1910 } | |
| 1911 } | |
| 1912 // even if on:end fires an `ignore` it's still possible | |
| 1913 // that we might trigger the end node because of a parent mode | |
| 1914 if (mode.endsWithParent) { | |
| 1915 return endOfMode(mode.parent, match, matchPlusRemainder); | |
| 1916 } | |
| 1917 } | |
| 1918 | |
| 1919 /** | |
| 1920 * Handle matching but then ignoring a sequence of text | |
| 1921 * | |
| 1922 * @param {string} lexeme - string containing full match text | |
| 1923 */ | |
| 1924 function doIgnore(lexeme) { | |
| 1925 if (top.matcher.regexIndex === 0) { | |
| 1926 // no more regexes to potentially match here, so we move the cursor forward one | |
| 1927 // space | |
| 1928 modeBuffer += lexeme[0]; | |
| 1929 return 1; | |
| 1930 } else { | |
| 1931 // no need to move the cursor, we still have additional regexes to try and | |
| 1932 // match at this very spot | |
| 1933 resumeScanAtSamePosition = true; | |
| 1934 return 0; | |
| 1935 } | |
| 1936 } | |
| 1937 | |
| 1938 /** | |
| 1939 * Handle the start of a new potential mode match | |
| 1940 * | |
| 1941 * @param {EnhancedMatch} match - the current match | |
| 1942 * @returns {number} how far to advance the parse cursor | |
| 1943 */ | |
| 1944 function doBeginMatch(match) { | |
| 1945 const lexeme = match[0]; | |
| 1946 const newMode = match.rule; | |
| 1947 | |
| 1948 const resp = new Response(newMode); | |
| 1949 // first internal before callbacks, then the public ones | |
| 1950 const beforeCallbacks = [newMode.__beforeBegin, newMode["on:begin"]]; | |
| 1951 for (const cb of beforeCallbacks) { | |
| 1952 if (!cb) continue; | |
| 1953 cb(match, resp); | |
| 1954 if (resp.isMatchIgnored) return doIgnore(lexeme); | |
| 1955 } | |
| 1956 | |
| 1957 if (newMode.skip) { | |
| 1958 modeBuffer += lexeme; | |
| 1959 } else { | |
| 1960 if (newMode.excludeBegin) { | |
| 1961 modeBuffer += lexeme; | |
| 1962 } | |
| 1963 processBuffer(); | |
| 1964 if (!newMode.returnBegin && !newMode.excludeBegin) { | |
| 1965 modeBuffer = lexeme; | |
| 1966 } | |
| 1967 } | |
| 1968 startNewMode(newMode, match); | |
| 1969 return newMode.returnBegin ? 0 : lexeme.length; | |
| 1970 } | |
| 1971 | |
| 1972 /** | |
| 1973 * Handle the potential end of mode | |
| 1974 * | |
| 1975 * @param {RegExpMatchArray} match - the current match | |
| 1976 */ | |
| 1977 function doEndMatch(match) { | |
| 1978 const lexeme = match[0]; | |
| 1979 const matchPlusRemainder = codeToHighlight.substring(match.index); | |
| 1980 | |
| 1981 const endMode = endOfMode(top, match, matchPlusRemainder); | |
| 1982 if (!endMode) { return NO_MATCH; } | |
| 1983 | |
| 1984 const origin = top; | |
| 1985 if (top.endScope && top.endScope._wrap) { | |
| 1986 processBuffer(); | |
| 1987 emitKeyword(lexeme, top.endScope._wrap); | |
| 1988 } else if (top.endScope && top.endScope._multi) { | |
| 1989 processBuffer(); | |
| 1990 emitMultiClass(top.endScope, match); | |
| 1991 } else if (origin.skip) { | |
| 1992 modeBuffer += lexeme; | |
| 1993 } else { | |
| 1994 if (!(origin.returnEnd || origin.excludeEnd)) { | |
| 1995 modeBuffer += lexeme; | |
| 1996 } | |
| 1997 processBuffer(); | |
| 1998 if (origin.excludeEnd) { | |
| 1999 modeBuffer = lexeme; | |
| 2000 } | |
| 2001 } | |
| 2002 do { | |
| 2003 if (top.scope) { | |
| 2004 emitter.closeNode(); | |
| 2005 } | |
| 2006 if (!top.skip && !top.subLanguage) { | |
| 2007 relevance += top.relevance; | |
| 2008 } | |
| 2009 top = top.parent; | |
| 2010 } while (top !== endMode.parent); | |
| 2011 if (endMode.starts) { | |
| 2012 startNewMode(endMode.starts, match); | |
| 2013 } | |
| 2014 return origin.returnEnd ? 0 : lexeme.length; | |
| 2015 } | |
| 2016 | |
| 2017 function processContinuations() { | |
| 2018 const list = []; | |
| 2019 for (let current = top; current !== language; current = current.parent) { | |
| 2020 if (current.scope) { | |
| 2021 list.unshift(current.scope); | |
| 2022 } | |
| 2023 } | |
| 2024 list.forEach(item => emitter.openNode(item)); | |
| 2025 } | |
| 2026 | |
| 2027 /** @type {{type?: MatchType, index?: number, rule?: Mode}}} */ | |
| 2028 let lastMatch = {}; | |
| 2029 | |
| 2030 /** | |
| 2031 * Process an individual match | |
| 2032 * | |
| 2033 * @param {string} textBeforeMatch - text preceding the match (since the last match) | |
| 2034 * @param {EnhancedMatch} [match] - the match itself | |
| 2035 */ | |
| 2036 function processLexeme(textBeforeMatch, match) { | |
| 2037 const lexeme = match && match[0]; | |
| 2038 | |
| 2039 // add non-matched text to the current mode buffer | |
| 2040 modeBuffer += textBeforeMatch; | |
| 2041 | |
| 2042 if (lexeme == null) { | |
| 2043 processBuffer(); | |
| 2044 return 0; | |
| 2045 } | |
| 2046 | |
| 2047 // we've found a 0 width match and we're stuck, so we need to advance | |
| 2048 // this happens when we have badly behaved rules that have optional matchers to the degree that | |
| 2049 // sometimes they can end up matching nothing at all | |
| 2050 // Ref: https://github.com/highlightjs/highlight.js/issues/2140 | |
| 2051 if (lastMatch.type === "begin" && match.type === "end" && lastMatch.index === match.index && lexeme === "") { | |
| 2052 // spit the "skipped" character that our regex choked on back into the output sequence | |
| 2053 modeBuffer += codeToHighlight.slice(match.index, match.index + 1); | |
| 2054 if (!SAFE_MODE) { | |
| 2055 /** @type {AnnotatedError} */ | |
| 2056 const err = new Error(`0 width match regex (${languageName})`); | |
| 2057 err.languageName = languageName; | |
| 2058 err.badRule = lastMatch.rule; | |
| 2059 throw err; | |
| 2060 } | |
| 2061 return 1; | |
| 2062 } | |
| 2063 lastMatch = match; | |
| 2064 | |
| 2065 if (match.type === "begin") { | |
| 2066 return doBeginMatch(match); | |
| 2067 } else if (match.type === "illegal" && !ignoreIllegals) { | |
| 2068 // illegal match, we do not continue processing | |
| 2069 /** @type {AnnotatedError} */ | |
| 2070 const err = new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.scope || '<unnamed>') + '"'); | |
| 2071 err.mode = top; | |
| 2072 throw err; | |
| 2073 } else if (match.type === "end") { | |
| 2074 const processed = doEndMatch(match); | |
| 2075 if (processed !== NO_MATCH) { | |
| 2076 return processed; | |
| 2077 } | |
| 2078 } | |
| 2079 | |
| 2080 // edge case for when illegal matches $ (end of line) which is technically | |
| 2081 // a 0 width match but not a begin/end match so it's not caught by the | |
| 2082 // first handler (when ignoreIllegals is true) | |
| 2083 if (match.type === "illegal" && lexeme === "") { | |
| 2084 // advance so we aren't stuck in an infinite loop | |
| 2085 modeBuffer += "\n"; | |
| 2086 return 1; | |
| 2087 } | |
| 2088 | |
| 2089 // infinite loops are BAD, this is a last ditch catch all. if we have a | |
| 2090 // decent number of iterations yet our index (cursor position in our | |
| 2091 // parsing) still 3x behind our index then something is very wrong | |
| 2092 // so we bail | |
| 2093 if (iterations > 100000 && iterations > match.index * 3) { | |
| 2094 const err = new Error('potential infinite loop, way more iterations than matches'); | |
| 2095 throw err; | |
| 2096 } | |
| 2097 | |
| 2098 /* | |
| 2099 Why might be find ourselves here? An potential end match that was | |
| 2100 triggered but could not be completed. IE, `doEndMatch` returned NO_MATCH. | |
| 2101 (this could be because a callback requests the match be ignored, etc) | |
| 2102 | |
| 2103 This causes no real harm other than stopping a few times too many. | |
| 2104 */ | |
| 2105 | |
| 2106 modeBuffer += lexeme; | |
| 2107 return lexeme.length; | |
| 2108 } | |
| 2109 | |
| 2110 const language = getLanguage(languageName); | |
| 2111 if (!language) { | |
| 2112 error(LANGUAGE_NOT_FOUND.replace("{}", languageName)); | |
| 2113 throw new Error('Unknown language: "' + languageName + '"'); | |
| 2114 } | |
| 2115 | |
| 2116 const md = compileLanguage(language); | |
| 2117 let result = ''; | |
| 2118 /** @type {CompiledMode} */ | |
| 2119 let top = continuation || md; | |
| 2120 /** @type Record<string,CompiledMode> */ | |
| 2121 const continuations = {}; // keep continuations for sub-languages | |
| 2122 const emitter = new options.__emitter(options); | |
| 2123 processContinuations(); | |
| 2124 let modeBuffer = ''; | |
| 2125 let relevance = 0; | |
| 2126 let index = 0; | |
| 2127 let iterations = 0; | |
| 2128 let resumeScanAtSamePosition = false; | |
| 2129 | |
| 2130 try { | |
| 2131 if (!language.__emitTokens) { | |
| 2132 top.matcher.considerAll(); | |
| 2133 | |
| 2134 for (;;) { | |
| 2135 iterations++; | |
| 2136 if (resumeScanAtSamePosition) { | |
| 2137 // only regexes not matched previously will now be | |
| 2138 // considered for a potential match | |
| 2139 resumeScanAtSamePosition = false; | |
| 2140 } else { | |
| 2141 top.matcher.considerAll(); | |
| 2142 } | |
| 2143 top.matcher.lastIndex = index; | |
| 2144 | |
| 2145 const match = top.matcher.exec(codeToHighlight); | |
| 2146 // console.log("match", match[0], match.rule && match.rule.begin) | |
| 2147 | |
| 2148 if (!match) break; | |
| 2149 | |
| 2150 const beforeMatch = codeToHighlight.substring(index, match.index); | |
| 2151 const processedCount = processLexeme(beforeMatch, match); | |
| 2152 index = match.index + processedCount; | |
| 2153 } | |
| 2154 processLexeme(codeToHighlight.substring(index)); | |
| 2155 } else { | |
| 2156 language.__emitTokens(codeToHighlight, emitter); | |
| 2157 } | |
| 2158 | |
| 2159 emitter.finalize(); | |
| 2160 result = emitter.toHTML(); | |
| 2161 | |
| 2162 return { | |
| 2163 language: languageName, | |
| 2164 value: result, | |
| 2165 relevance, | |
| 2166 illegal: false, | |
| 2167 _emitter: emitter, | |
| 2168 _top: top | |
| 2169 }; | |
| 2170 } catch (err) { | |
| 2171 if (err.message && err.message.includes('Illegal')) { | |
| 2172 return { | |
| 2173 language: languageName, | |
| 2174 value: escape(codeToHighlight), | |
| 2175 illegal: true, | |
| 2176 relevance: 0, | |
| 2177 _illegalBy: { | |
| 2178 message: err.message, | |
| 2179 index, | |
| 2180 context: codeToHighlight.slice(index - 100, index + 100), | |
| 2181 mode: err.mode, | |
| 2182 resultSoFar: result | |
| 2183 }, | |
| 2184 _emitter: emitter | |
| 2185 }; | |
| 2186 } else if (SAFE_MODE) { | |
| 2187 return { | |
| 2188 language: languageName, | |
| 2189 value: escape(codeToHighlight), | |
| 2190 illegal: false, | |
| 2191 relevance: 0, | |
| 2192 errorRaised: err, | |
| 2193 _emitter: emitter, | |
| 2194 _top: top | |
| 2195 }; | |
| 2196 } else { | |
| 2197 throw err; | |
| 2198 } | |
| 2199 } | |
| 2200 } | |
| 2201 | |
| 2202 /** | |
| 2203 * returns a valid highlight result, without actually doing any actual work, | |
| 2204 * auto highlight starts with this and it's possible for small snippets that | |
| 2205 * auto-detection may not find a better match | |
| 2206 * @param {string} code | |
| 2207 * @returns {HighlightResult} | |
| 2208 */ | |
| 2209 function justTextHighlightResult(code) { | |
| 2210 const result = { | |
| 2211 value: escape(code), | |
| 2212 illegal: false, | |
| 2213 relevance: 0, | |
| 2214 _top: PLAINTEXT_LANGUAGE, | |
| 2215 _emitter: new options.__emitter(options) | |
| 2216 }; | |
| 2217 result._emitter.addText(code); | |
| 2218 return result; | |
| 2219 } | |
| 2220 | |
| 2221 /** | |
| 2222 Highlighting with language detection. Accepts a string with the code to | |
| 2223 highlight. Returns an object with the following properties: | |
| 2224 | |
| 2225 - language (detected language) | |
| 2226 - relevance (int) | |
| 2227 - value (an HTML string with highlighting markup) | |
| 2228 - secondBest (object with the same structure for second-best heuristically | |
| 2229 detected language, may be absent) | |
| 2230 | |
| 2231 @param {string} code | |
| 2232 @param {Array<string>} [languageSubset] | |
| 2233 @returns {AutoHighlightResult} | |
| 2234 */ | |
| 2235 function highlightAuto(code, languageSubset) { | |
| 2236 languageSubset = languageSubset || options.languages || Object.keys(languages); | |
| 2237 const plaintext = justTextHighlightResult(code); | |
| 2238 | |
| 2239 const results = languageSubset.filter(getLanguage).filter(autoDetection).map(name => | |
| 2240 _highlight(name, code, false) | |
| 2241 ); | |
| 2242 results.unshift(plaintext); // plaintext is always an option | |
| 2243 | |
| 2244 const sorted = results.sort((a, b) => { | |
| 2245 // sort base on relevance | |
| 2246 if (a.relevance !== b.relevance) return b.relevance - a.relevance; | |
| 2247 | |
| 2248 // always award the tie to the base language | |
| 2249 // ie if C++ and Arduino are tied, it's more likely to be C++ | |
| 2250 if (a.language && b.language) { | |
| 2251 if (getLanguage(a.language).supersetOf === b.language) { | |
| 2252 return 1; | |
| 2253 } else if (getLanguage(b.language).supersetOf === a.language) { | |
| 2254 return -1; | |
| 2255 } | |
| 2256 } | |
| 2257 | |
| 2258 // otherwise say they are equal, which has the effect of sorting on | |
| 2259 // relevance while preserving the original ordering - which is how ties | |
| 2260 // have historically been settled, ie the language that comes first always | |
| 2261 // wins in the case of a tie | |
| 2262 return 0; | |
| 2263 }); | |
| 2264 | |
| 2265 const [best, secondBest] = sorted; | |
| 2266 | |
| 2267 /** @type {AutoHighlightResult} */ | |
| 2268 const result = best; | |
| 2269 result.secondBest = secondBest; | |
| 2270 | |
| 2271 return result; | |
| 2272 } | |
| 2273 | |
| 2274 /** | |
| 2275 * Builds new class name for block given the language name | |
| 2276 * | |
| 2277 * @param {HTMLElement} element | |
| 2278 * @param {string} [currentLang] | |
| 2279 * @param {string} [resultLang] | |
| 2280 */ | |
| 2281 function updateClassName(element, currentLang, resultLang) { | |
| 2282 const language = (currentLang && aliases[currentLang]) || resultLang; | |
| 2283 | |
| 2284 element.classList.add("hljs"); | |
| 2285 element.classList.add(`language-${language}`); | |
| 2286 } | |
| 2287 | |
| 2288 /** | |
| 2289 * Applies highlighting to a DOM node containing code. | |
| 2290 * | |
| 2291 * @param {HighlightedHTMLElement} element - the HTML element to highlight | |
| 2292 */ | |
| 2293 function highlightElement(element) { | |
| 2294 /** @type HTMLElement */ | |
| 2295 let node = null; | |
| 2296 const language = blockLanguage(element); | |
| 2297 | |
| 2298 if (shouldNotHighlight(language)) return; | |
| 2299 | |
| 2300 fire("before:highlightElement", | |
| 2301 { el: element, language }); | |
| 2302 | |
| 2303 if (element.dataset.highlighted) { | |
| 2304 console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.", element); | |
| 2305 return; | |
| 2306 } | |
| 2307 | |
| 2308 // we should be all text, no child nodes (unescaped HTML) - this is possibly | |
| 2309 // an HTML injection attack - it's likely too late if this is already in | |
| 2310 // production (the code has likely already done its damage by the time | |
| 2311 // we're seeing it)... but we yell loudly about this so that hopefully it's | |
| 2312 // more likely to be caught in development before making it to production | |
| 2313 if (element.children.length > 0) { | |
| 2314 if (!options.ignoreUnescapedHTML) { | |
| 2315 console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."); | |
| 2316 console.warn("https://github.com/highlightjs/highlight.js/wiki/security"); | |
| 2317 console.warn("The element with unescaped HTML:"); | |
| 2318 console.warn(element); | |
| 2319 } | |
| 2320 if (options.throwUnescapedHTML) { | |
| 2321 const err = new HTMLInjectionError( | |
| 2322 "One of your code blocks includes unescaped HTML.", | |
| 2323 element.innerHTML | |
| 2324 ); | |
| 2325 throw err; | |
| 2326 } | |
| 2327 } | |
| 2328 | |
| 2329 node = element; | |
| 2330 const text = node.textContent; | |
| 2331 const result = language ? highlight(text, { language, ignoreIllegals: true }) : highlightAuto(text); | |
| 2332 | |
| 2333 element.innerHTML = result.value; | |
| 2334 element.dataset.highlighted = "yes"; | |
| 2335 updateClassName(element, language, result.language); | |
| 2336 element.result = { | |
| 2337 language: result.language, | |
| 2338 // TODO: remove with version 11.0 | |
| 2339 re: result.relevance, | |
| 2340 relevance: result.relevance | |
| 2341 }; | |
| 2342 if (result.secondBest) { | |
| 2343 element.secondBest = { | |
| 2344 language: result.secondBest.language, | |
| 2345 relevance: result.secondBest.relevance | |
| 2346 }; | |
| 2347 } | |
| 2348 | |
| 2349 fire("after:highlightElement", { el: element, result, text }); | |
| 2350 } | |
| 2351 | |
| 2352 /** | |
| 2353 * Updates highlight.js global options with the passed options | |
| 2354 * | |
| 2355 * @param {Partial<HLJSOptions>} userOptions | |
| 2356 */ | |
| 2357 function configure(userOptions) { | |
| 2358 options = inherit(options, userOptions); | |
| 2359 } | |
| 2360 | |
| 2361 // TODO: remove v12, deprecated | |
| 2362 const initHighlighting = () => { | |
| 2363 highlightAll(); | |
| 2364 deprecated("10.6.0", "initHighlighting() deprecated. Use highlightAll() now."); | |
| 2365 }; | |
| 2366 | |
| 2367 // TODO: remove v12, deprecated | |
| 2368 function initHighlightingOnLoad() { | |
| 2369 highlightAll(); | |
| 2370 deprecated("10.6.0", "initHighlightingOnLoad() deprecated. Use highlightAll() now."); | |
| 2371 } | |
| 2372 | |
| 2373 let wantsHighlight = false; | |
| 2374 | |
| 2375 /** | |
| 2376 * auto-highlights all pre>code elements on the page | |
| 2377 */ | |
| 2378 function highlightAll() { | |
| 2379 function boot() { | |
| 2380 // if a highlight was requested before DOM was loaded, do now | |
| 2381 highlightAll(); | |
| 2382 } | |
| 2383 | |
| 2384 // if we are called too early in the loading process | |
| 2385 if (document.readyState === "loading") { | |
| 2386 // make sure the event listener is only added once | |
| 2387 if (!wantsHighlight) { | |
| 2388 window.addEventListener('DOMContentLoaded', boot, false); | |
| 2389 } | |
| 2390 wantsHighlight = true; | |
| 2391 return; | |
| 2392 } | |
| 2393 | |
| 2394 const blocks = document.querySelectorAll(options.cssSelector); | |
| 2395 blocks.forEach(highlightElement); | |
| 2396 } | |
| 2397 | |
| 2398 /** | |
| 2399 * Register a language grammar module | |
| 2400 * | |
| 2401 * @param {string} languageName | |
| 2402 * @param {LanguageFn} languageDefinition | |
| 2403 */ | |
| 2404 function registerLanguage(languageName, languageDefinition) { | |
| 2405 let lang = null; | |
| 2406 try { | |
| 2407 lang = languageDefinition(hljs); | |
| 2408 } catch (error$1) { | |
| 2409 error("Language definition for '{}' could not be registered.".replace("{}", languageName)); | |
| 2410 // hard or soft error | |
| 2411 if (!SAFE_MODE) { throw error$1; } else { error(error$1); } | |
| 2412 // languages that have serious errors are replaced with essentially a | |
| 2413 // "plaintext" stand-in so that the code blocks will still get normal | |
| 2414 // css classes applied to them - and one bad language won't break the | |
| 2415 // entire highlighter | |
| 2416 lang = PLAINTEXT_LANGUAGE; | |
| 2417 } | |
| 2418 // give it a temporary name if it doesn't have one in the meta-data | |
| 2419 if (!lang.name) lang.name = languageName; | |
| 2420 languages[languageName] = lang; | |
| 2421 lang.rawDefinition = languageDefinition.bind(null, hljs); | |
| 2422 | |
| 2423 if (lang.aliases) { | |
| 2424 registerAliases(lang.aliases, { languageName }); | |
| 2425 } | |
| 2426 } | |
| 2427 | |
| 2428 /** | |
| 2429 * Remove a language grammar module | |
| 2430 * | |
| 2431 * @param {string} languageName | |
| 2432 */ | |
| 2433 function unregisterLanguage(languageName) { | |
| 2434 delete languages[languageName]; | |
| 2435 for (const alias of Object.keys(aliases)) { | |
| 2436 if (aliases[alias] === languageName) { | |
| 2437 delete aliases[alias]; | |
| 2438 } | |
| 2439 } | |
| 2440 } | |
| 2441 | |
| 2442 /** | |
| 2443 * @returns {string[]} List of language internal names | |
| 2444 */ | |
| 2445 function listLanguages() { | |
| 2446 return Object.keys(languages); | |
| 2447 } | |
| 2448 | |
| 2449 /** | |
| 2450 * @param {string} name - name of the language to retrieve | |
| 2451 * @returns {Language | undefined} | |
| 2452 */ | |
| 2453 function getLanguage(name) { | |
| 2454 name = (name || '').toLowerCase(); | |
| 2455 return languages[name] || languages[aliases[name]]; | |
| 2456 } | |
| 2457 | |
| 2458 /** | |
| 2459 * | |
| 2460 * @param {string|string[]} aliasList - single alias or list of aliases | |
| 2461 * @param {{languageName: string}} opts | |
| 2462 */ | |
| 2463 function registerAliases(aliasList, { languageName }) { | |
| 2464 if (typeof aliasList === 'string') { | |
| 2465 aliasList = [aliasList]; | |
| 2466 } | |
| 2467 aliasList.forEach(alias => { aliases[alias.toLowerCase()] = languageName; }); | |
| 2468 } | |
| 2469 | |
| 2470 /** | |
| 2471 * Determines if a given language has auto-detection enabled | |
| 2472 * @param {string} name - name of the language | |
| 2473 */ | |
| 2474 function autoDetection(name) { | |
| 2475 const lang = getLanguage(name); | |
| 2476 return lang && !lang.disableAutodetect; | |
| 2477 } | |
| 2478 | |
| 2479 /** | |
| 2480 * Upgrades the old highlightBlock plugins to the new | |
| 2481 * highlightElement API | |
| 2482 * @param {HLJSPlugin} plugin | |
| 2483 */ | |
| 2484 function upgradePluginAPI(plugin) { | |
| 2485 // TODO: remove with v12 | |
| 2486 if (plugin["before:highlightBlock"] && !plugin["before:highlightElement"]) { | |
| 2487 plugin["before:highlightElement"] = (data) => { | |
| 2488 plugin["before:highlightBlock"]( | |
| 2489 Object.assign({ block: data.el }, data) | |
| 2490 ); | |
| 2491 }; | |
| 2492 } | |
| 2493 if (plugin["after:highlightBlock"] && !plugin["after:highlightElement"]) { | |
| 2494 plugin["after:highlightElement"] = (data) => { | |
| 2495 plugin["after:highlightBlock"]( | |
| 2496 Object.assign({ block: data.el }, data) | |
| 2497 ); | |
| 2498 }; | |
| 2499 } | |
| 2500 } | |
| 2501 | |
| 2502 /** | |
| 2503 * @param {HLJSPlugin} plugin | |
| 2504 */ | |
| 2505 function addPlugin(plugin) { | |
| 2506 upgradePluginAPI(plugin); | |
| 2507 plugins.push(plugin); | |
| 2508 } | |
| 2509 | |
| 2510 /** | |
| 2511 * @param {HLJSPlugin} plugin | |
| 2512 */ | |
| 2513 function removePlugin(plugin) { | |
| 2514 const index = plugins.indexOf(plugin); | |
| 2515 if (index !== -1) { | |
| 2516 plugins.splice(index, 1); | |
| 2517 } | |
| 2518 } | |
| 2519 | |
| 2520 /** | |
| 2521 * | |
| 2522 * @param {PluginEvent} event | |
| 2523 * @param {any} args | |
| 2524 */ | |
| 2525 function fire(event, args) { | |
| 2526 const cb = event; | |
| 2527 plugins.forEach(function(plugin) { | |
| 2528 if (plugin[cb]) { | |
| 2529 plugin[cb](args); | |
| 2530 } | |
| 2531 }); | |
| 2532 } | |
| 2533 | |
| 2534 /** | |
| 2535 * DEPRECATED | |
| 2536 * @param {HighlightedHTMLElement} el | |
| 2537 */ | |
| 2538 function deprecateHighlightBlock(el) { | |
| 2539 deprecated("10.7.0", "highlightBlock will be removed entirely in v12.0"); | |
| 2540 deprecated("10.7.0", "Please use highlightElement now."); | |
| 2541 | |
| 2542 return highlightElement(el); | |
| 2543 } | |
| 2544 | |
| 2545 /* Interface definition */ | |
| 2546 Object.assign(hljs, { | |
| 2547 highlight, | |
| 2548 highlightAuto, | |
| 2549 highlightAll, | |
| 2550 highlightElement, | |
| 2551 // TODO: Remove with v12 API | |
| 2552 highlightBlock: deprecateHighlightBlock, | |
| 2553 configure, | |
| 2554 initHighlighting, | |
| 2555 initHighlightingOnLoad, | |
| 2556 registerLanguage, | |
| 2557 unregisterLanguage, | |
| 2558 listLanguages, | |
| 2559 getLanguage, | |
| 2560 registerAliases, | |
| 2561 autoDetection, | |
| 2562 inherit, | |
| 2563 addPlugin, | |
| 2564 removePlugin | |
| 2565 }); | |
| 2566 | |
| 2567 hljs.debugMode = function() { SAFE_MODE = false; }; | |
| 2568 hljs.safeMode = function() { SAFE_MODE = true; }; | |
| 2569 hljs.versionString = version; | |
| 2570 | |
| 2571 hljs.regex = { | |
| 2572 concat: concat, | |
| 2573 lookahead: lookahead, | |
| 2574 either: either, | |
| 2575 optional: optional, | |
| 2576 anyNumberOfTimes: anyNumberOfTimes | |
| 2577 }; | |
| 2578 | |
| 2579 for (const key in MODES) { | |
| 2580 // @ts-ignore | |
| 2581 if (typeof MODES[key] === "object") { | |
| 2582 // @ts-ignore | |
| 2583 deepFreeze(MODES[key]); | |
| 2584 } | |
| 2585 } | |
| 2586 | |
| 2587 // merge all the modes/regexes into our main object | |
| 2588 Object.assign(hljs, MODES); | |
| 2589 | |
| 2590 return hljs; | |
| 2591 }; | |
| 2592 | |
| 2593 // Other names for the variable may break build script | |
| 2594 const highlight = HLJS({}); | |
| 2595 | |
| 2596 // returns a new instance of the highlighter to be used for extensions | |
| 2597 // check https://github.com/wooorm/lowlight/issues/47 | |
| 2598 highlight.newInstance = () => HLJS({}); | |
| 2599 | |
| 2600 export { highlight as default }; |