comparison third_party/highlight/es/core.js @ 173:827c6ac504cd hg-web

Merged in default here.
author MrJuneJune <me@mrjunejune.com>
date Mon, 19 Jan 2026 18:59:10 -0800
parents 2db6253f355d
children
comparison
equal deleted inserted replaced
151:c033667da5f9 173:827c6ac504cd
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, '&amp;')
69 .replace(/</g, '&lt;')
70 .replace(/>/g, '&gt;')
71 .replace(/"/g, '&quot;')
72 .replace(/'/g, '&#x27;');
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 };