UNPKG

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