2 Highlight.js v11.11.1 (git: 08cb242e7d)
3 (c) 2006-2025 Josh Goebel <hello@joshgoebel.com> and other contributors
6 /* eslint-disable no-multi-assign */
8 function deepFreeze(obj
) {
9 if (obj
instanceof Map
) {
14 throw new Error('map is read-only');
16 } else if (obj
instanceof Set
) {
21 throw new Error('set is read-only');
28 Object
.getOwnPropertyNames(obj
).forEach((name
) => {
29 const prop
= obj
[name
];
30 const type
= typeof prop
;
32 // Freeze prop if it is an object or function and also not already frozen
33 if ((type
=== 'object' || type
=== 'function') && !Object
.isFrozen(prop
)) {
41 /** @typedef {import('highlight.js').CallbackResponse} CallbackResponse */
42 /** @typedef {import('highlight.js').CompiledMode} CompiledMode */
43 /** @implements CallbackResponse */
47 * @param {CompiledMode} mode
50 // eslint-disable-next-line no-undefined
51 if (mode
.data
=== undefined) mode
.data
= {};
53 this.data
= mode
.data
;
54 this.isMatchIgnored
= false;
58 this.isMatchIgnored
= true;
63 * @param {string} value
66 function escapeHTML(value
) {
68 .replace(/&/g
, '&')
69 .replace(/</g
, '<')
70 .replace(/>/g
, '>')
71 .replace(/"/g, '"')
72 .replace(/'/g, ''');
76 * performs a shallow merge of multiple objects into one
80 * @param {Record<string,any>[]} objects
81 * @returns {T} a single new object
83 function inherit$1(original, ...objects) {
84 /** @type Record<string,any> */
85 const result = Object.create(null);
87 for (const key in original) {
88 result[key] = original[key];
90 objects.forEach(function(obj) {
91 for (const key in obj) {
92 result[key] = obj[key];
95 return /** @type {T} */ (result);
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
106 /** @typedef {{scope?: string, language?: string, sublanguage?: boolean}} Node */
107 /** @typedef {{walk: (r: Renderer) => void}} Tree */
110 const SPAN_CLOSE = '</span>';
113 * Determines if a node needs to be wrapped in <span>
115 * @param {Node} node */
116 const emitsWrappingTags = (node) => {
117 // rarely we can have a sublanguage where language is undefined
118 // TODO: track down why
124 * @param {string} name
125 * @param {{prefix:string}} options
127 const scopeToCSSClass = (name, { prefix }) => {
129 if (name.startsWith("language:")) {
130 return name.replace("language:", "language
-");
132 // tiered scope: comment.line
133 if (name.includes(".")) {
134 const pieces = name.split(".");
136 `${prefix}${pieces.shift()}`,
137 ...(pieces.map((x, i) => `${x}${"_".repeat(i + 1)}`))
141 return `${prefix}${name}`;
144 /** @type {Renderer} */
147 * Creates a new HTMLRenderer
149 * @param {Tree} parseTree - the parse tree (must support `walk` API)
150 * @param {{classPrefix: string}} options
152 constructor(parseTree, options) {
154 this.classPrefix = options.classPrefix;
155 parseTree.walk(this);
159 * Adds texts to the output stream
161 * @param {string} text */
163 this.buffer += escapeHTML(text);
167 * Adds a node open to the output stream (if needed)
169 * @param {Node} node */
171 if (!emitsWrappingTags(node)) return;
173 const className = scopeToCSSClass(node.scope,
174 { prefix: this.classPrefix });
175 this.span(className);
179 * Adds a node close to the output stream (if needed)
181 * @param {Node} node */
183 if (!emitsWrappingTags(node)) return;
185 this.buffer += SPAN_CLOSE;
189 * returns the accumulated buffer
198 * Builds a span element
200 * @param {string} className */
202 this.buffer += `<span class="${className}
">`;
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 */
211 /** @returns {DataNode} */
212 const newNode = (opts = {}) => {
213 /** @type DataNode */
214 const result = { children: [] };
215 Object.assign(result, opts);
221 /** @type DataNode */
222 this.rootNode = newNode();
223 this.stack = [this.rootNode];
227 return this.stack[this.stack.length - 1];
230 get root() { return this.rootNode; }
232 /** @param {Node} node */
234 this.top.children.push(node);
237 /** @param {string} scope */
240 const node = newNode({ scope });
242 this.stack.push(node);
246 if (this.stack.length > 1) {
247 return this.stack.pop();
249 // eslint-disable-next-line no-undefined
254 while (this.closeNode());
258 return JSON.stringify(this.rootNode, null, 4);
262 * @typedef { import("./html_renderer
").Renderer } Renderer
263 * @param {Renderer} builder
267 return this.constructor._walk(builder, this.rootNode);
269 // return TokenTree._walk(builder, this.rootNode);
273 * @param {Renderer} builder
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);
290 static _collapse(node) {
291 if (typeof node === "string
") return;
292 if (!node.children) return;
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("")];
299 node.children.forEach((child) => {
300 TokenTree._collapse(child);
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.
313 - __addSublanguage(emitter, subLanguageName)
322 * @implements {Emitter}
324 class TokenTreeEmitter extends TokenTree {
328 constructor(options) {
330 this.options = options;
334 * @param {string} text
337 if (text === "") { return; }
342 /** @param {string} scope */
344 this.openNode(scope);
352 * @param {Emitter & {root: DataNode}} emitter
353 * @param {string} name
355 __addSublanguage(emitter, name) {
356 /** @type DataNode */
357 const node = emitter.root;
358 if (name) node.scope = `language:${name}`;
364 const renderer = new HTMLRenderer(this, this.options);
365 return renderer.value();
369 this.closeAllNodes();
375 * @param {string} value
380 * @param {RegExp | string } re
383 function source(re) {
384 if (!re) return null;
385 if (typeof re === "string
") return re;
391 * @param {RegExp | string } re
394 function lookahead(re) {
395 return concat('(?=', re, ')');
399 * @param {RegExp | string } re
402 function anyNumberOfTimes(re) {
403 return concat('(?:', re, ')*');
407 * @param {RegExp | string } re
410 function optional(re) {
411 return concat('(?:', re, ')?');
415 * @param {...(RegExp | string) } args
418 function concat(...args) {
419 const joined = args.map((x) => source(x)).join("");
424 * @param { Array<string | RegExp | Object> } args
427 function stripOptionsFromArgs(args) {
428 const opts = args[args.length - 1];
430 if (typeof opts === 'object' && opts.constructor === Object) {
431 args.splice(args.length - 1, 1);
438 /** @typedef { {capture?: boolean} } RegexEitherOptions */
441 * Any of the passed expresssions may match
443 * Creates a huge this | this | that | that match
444 * @param {(RegExp | string)[] | [...(RegExp | string)[], RegexEitherOptions]} args
447 function either(...args) {
448 /** @type { object & {capture?: boolean} } */
449 const opts = stripOptionsFromArgs(args);
451 + (opts.capture ? "" : "?:")
452 + args.map((x) => source(x)).join("|") + ")";
457 * @param {RegExp | string} re
460 function countMatchGroups(re) {
461 return (new RegExp(re.toString() + '|')).exec('').length - 1;
465 * Does lexeme start with a regular expression match at the beginning
467 * @param {string} lexeme
469 function startsWith(re, lexeme) {
470 const match = re && re.exec(lexeme);
471 return match && match.index === 0;
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]*)|\\./;
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. :-)
490 * @param {(string | RegExp)[]} regexps
491 * @param {{joinWith: string}} opts
494 function _rewriteBackreferences(regexps, { joinWith }) {
497 return regexps.map((regex) => {
499 const offset = numCaptures;
500 let re = source(regex);
503 while (re.length > 0) {
504 const match = BACKREF_RE.exec(re);
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);
516 if (match[0] === '(') {
522 }).map(re => `(${re})`).join(joinWith);
525 /** @typedef {import('highlight.js').Mode} Mode */
526 /** @typedef {import('highlight.js').ModeCallback} ModeCallback */
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 = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~';
538 * @param { Partial<Mode> & {binary?: string | RegExp} } opts
540 const SHEBANG = (opts = {}) => {
541 const beginShebang = /^#![ ]*\//;
554 /** @type {ModeCallback} */
555 "on:begin
": (m, resp) => {
556 if (m.index !== 0) resp.ignoreMatch();
562 const BACKSLASH_ESCAPE = {
563 begin: '\\\\[\\s\\S]', relevance: 0
565 const APOS_STRING_MODE = {
570 contains: [BACKSLASH_ESCAPE]
572 const QUOTE_STRING_MODE = {
577 contains: [BACKSLASH_ESCAPE]
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/
583 * Creates a comment mode
585 * @param {string | RegExp} begin
586 * @param {string | RegExp} end
587 * @param {Mode | {}} [modeOptions]
588 * @returns {Partial<Mode>}
590 const COMMENT = function(begin, end, modeOptions = {}) {
591 const mode = inherit$1(
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):/,
609 const ENGLISH_WORD = either(
610 // list of common 1 and 2 letter words in English
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
627 // looking like plain text, more likely to be a comment
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}/
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.
642 // for a visual example please see:
643 // https://github.com/highlightjs/highlight.js/issues/2827
646 /[ ]+/, // necessary to prevent us gobbling up doctags like
/* @author Bob Mcgill */
649 /[.]?[:]?([.][ ]|[ ])/,
650 '){3}') // look for 3 words in a row
655 const C_LINE_COMMENT_MODE
= COMMENT('//', '$');
656 const C_BLOCK_COMMENT_MODE
= COMMENT('/\\*', '\\*/');
657 const HASH_COMMENT_MODE
= COMMENT('#', '$');
658 const NUMBER_MODE
= {
663 const C_NUMBER_MODE
= {
668 const BINARY_NUMBER_MODE
= {
670 begin: BINARY_NUMBER_RE
,
673 const REGEXP_MODE
= {
675 begin: /\/(?=[^/\n]*\/)/,
683 contains: [BACKSLASH_ESCAPE
]
692 const UNDERSCORE_TITLE_MODE
= {
694 begin: UNDERSCORE_IDENT_RE
,
697 const METHOD_GUARD
= {
698 // excludes method names from keyword processing
699 begin: '\\.\\s*' + UNDERSCORE_IDENT_RE
,
704 * Adds end same as begin mechanics to a mode
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
710 const END_SAME_AS_BEGIN = function(mode
) {
711 return Object
.assign(mode
,
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(); }
720 var MODES
= /*#__PURE__*/Object
.freeze({
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
,
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
,
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
,
743 TITLE_MODE: TITLE_MODE
,
744 UNDERSCORE_IDENT_RE: UNDERSCORE_IDENT_RE
,
745 UNDERSCORE_TITLE_MODE: UNDERSCORE_TITLE_MODE
749 @typedef {import('highlight.js').CallbackResponse} CallbackResponse
750 @typedef {import('highlight.js').CompilerExt} CompilerExt
753 // Grammar extensions / plugins
754 // See: https://github.com/highlightjs/highlight.js/issues/2833
756 // Grammar extensions allow "syntactic sugar" to be added to the grammar modes
757 // without requiring any underlying changes to the compiler internals.
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.
767 // TODO: We need negative look-behind support to do this properly
769 * Skip a match if it has a preceding dot
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
777 function skipIfHasPrecedingDot(match
, response
) {
778 const before
= match
.input
[match
.index
- 1];
779 if (before
=== ".") {
780 response
.ignoreMatch();
786 * @type {CompilerExt}
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
;
797 * `beginKeywords` syntactic sugar
798 * @type {CompilerExt}
800 function beginKeywords(mode
, parent
) {
802 if (!mode
.beginKeywords
) return;
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
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
;
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;
821 * Allow `illegal` to contain an array of illegal values
822 * @type {CompilerExt}
824 function compileIllegal(mode
, _parent
) {
825 if (!Array
.isArray(mode
.illegal
)) return;
827 mode
.illegal
= either(...mode
.illegal
);
831 * `match` to match a single expression for readability
832 * @type {CompilerExt}
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");
838 mode
.begin
= mode
.match
;
843 * provides the default 1 relevance to all modes
844 * @type {CompilerExt}
846 function compileRelevance(mode
, _parent
) {
847 // eslint-disable-next-line no-undefined
848 if (mode
.relevance
=== undefined) mode
.relevance
= 1;
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");
859 const originalMode
= Object
.assign({}, mode
);
860 Object
.keys(mode
).forEach((key
) => { delete mode
[key
]; });
862 mode
.keywords
= originalMode
.keywords
;
863 mode
.begin
= concat(originalMode
.beforeMatch
, lookahead(originalMode
.begin
));
867 Object
.assign(originalMode
, { endsParent: true })
872 delete originalMode
.beforeMatch
;
875 // keywords that should have no default relevance value
876 const COMMON_KEYWORDS
= [
885 'parent', // common variable name
886 'list', // common variable name
887 'value' // common variable name
890 const DEFAULT_KEYWORD_SCOPE
= "keyword";
893 * Given raw keywords from a language definition, compile them.
895 * @param {string | Record<string,string|string[]> | Array<string>} rawKeywords
896 * @param {boolean} caseInsensitive
898 function compileKeywords(rawKeywords
, caseInsensitive
, scopeName
= DEFAULT_KEYWORD_SCOPE
) {
899 /** @type {import("highlight.js/private").KeywordDict} */
900 const compiledKeywords
= Object
.create(null);
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
);
909 Object
.keys(rawKeywords
).forEach(function(scopeName
) {
910 // collapse all our objects back into the parent object
913 compileKeywords(rawKeywords
[scopeName
], caseInsensitive
, scopeName
)
917 return compiledKeywords
;
922 * Compiles an individual list of keywords
924 * Ex: "for if when while|5"
926 * @param {string} scopeName
927 * @param {Array<string>} keywordList
929 function compileList(scopeName
, keywordList
) {
930 if (caseInsensitive
) {
931 keywordList
= keywordList
.map(x
=> x
.toLowerCase());
933 keywordList
.forEach(function(keyword
) {
934 const pair
= keyword
.split('|');
935 compiledKeywords
[pair
[0]] = [scopeName
, scoreForKeyword(pair
[0], pair
[1])];
941 * Returns the proper score for a given keyword
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]
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
952 return Number(providedScore
);
955 return commonKeyword(keyword
) ? 0 : 1;
959 * Determines if a given keyword is common or not
961 * @param {string} keyword */
962 function commonKeyword(keyword
) {
963 return COMMON_KEYWORDS
.includes(keyword
.toLowerCase());
968 For the reasoning behind this please see:
969 https://github.com/highlightjs/highlight.js/issues/2880#issuecomment-747275419
974 * @type {Record<string, boolean>}
976 const seenDeprecations
= {};
979 * @param {string} message
981 const error
= (message
) => {
982 console
.error(message
);
986 * @param {string} message
989 const warn
= (message
, ...args
) => {
990 console
.log(`WARN: ${message}`, ...args
);
994 * @param {string} version
995 * @param {string} message
997 const deprecated
= (version
, message
) => {
998 if (seenDeprecations
[`${version}/${message}`]) return;
1000 console
.log(`Deprecated as of ${version}. ${message}`);
1001 seenDeprecations
[`${version}/${message}`] = true;
1004 /* eslint-disable no-throw-literal */
1007 @typedef {import('highlight.js').CompiledMode} CompiledMode
1010 const MultiClassError
= new Error();
1013 * Renumbers labeled scope names to account for additional inner match
1014 * groups that otherwise would break everything.
1016 * Lets say we 3 match scopes:
1018 * { 1 => ..., 2 => ..., 3 => ... }
1020 * So what we need is a clean match like this:
1022 * (a)(b)(c) => [ "a", "b", "c" ]
1024 * But this falls apart with inner match groups:
1026 * (a)(((b)))(c) => ["a", "b", "b", "b", "c" ]
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:
1031 * { 1 => ..., 2 => ..., 5 => ... }
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.
1036 * @param {CompiledMode} mode
1037 * @param {Array<RegExp | string>} regexes
1038 * @param {{key: "beginScope"|"endScope"}} opts
1040 function remapScopeNames(mode
, regexes
, { key
}) {
1042 const scopeNames
= mode
[key
];
1043 /** @type Record<number,boolean> */
1045 /** @type Record<number,string> */
1046 const positions
= {};
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]);
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;
1061 * @param {CompiledMode} mode
1063 function beginMultiClass(mode
) {
1064 if (!Array
.isArray(mode
.begin
)) return;
1066 if (mode
.skip
|| mode
.excludeBegin
|| mode
.returnBegin
) {
1067 error("skip, excludeBegin, returnBegin not compatible with beginScope: {}");
1068 throw MultiClassError
;
1071 if (typeof mode
.beginScope
!== "object" || mode
.beginScope
=== null) {
1072 error("beginScope must be object");
1073 throw MultiClassError
;
1076 remapScopeNames(mode
, mode
.begin
, { key: "beginScope" });
1077 mode
.begin
= _rewriteBackreferences(mode
.begin
, { joinWith: "" });
1081 * @param {CompiledMode} mode
1083 function endMultiClass(mode
) {
1084 if (!Array
.isArray(mode
.end
)) return;
1086 if (mode
.skip
|| mode
.excludeEnd
|| mode
.returnEnd
) {
1087 error("skip, excludeEnd, returnEnd not compatible with endScope: {}");
1088 throw MultiClassError
;
1091 if (typeof mode
.endScope
!== "object" || mode
.endScope
=== null) {
1092 error("endScope must be object");
1093 throw MultiClassError
;
1096 remapScopeNames(mode
, mode
.end
, { key: "endScope" });
1097 mode
.end
= _rewriteBackreferences(mode
.end
, { joinWith: "" });
1101 * this exists only to allow `scope: {}` to be used beside `match:`
1102 * Otherwise `beginScope` would necessary and that would look weird
1105 match: [ /def/, /\w+/ ]
1106 scope: { 1: "keyword" , 2: "title" }
1109 * @param {CompiledMode} mode
1111 function scopeSugar(mode
) {
1112 if (mode
.scope
&& typeof mode
.scope
=== "object" && mode
.scope
!== null) {
1113 mode
.beginScope
= mode
.scope
;
1119 * @param {CompiledMode} mode
1121 function MultiClass(mode
) {
1124 if (typeof mode
.beginScope
=== "string") {
1125 mode
.beginScope
= { _wrap: mode
.beginScope
};
1127 if (typeof mode
.endScope
=== "string") {
1128 mode
.endScope
= { _wrap: mode
.endScope
};
1131 beginMultiClass(mode
);
1132 endMultiClass(mode
);
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
1146 * Compiles a language definition result
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}
1153 function compileLanguage(language
) {
1155 * Builds a regex with the case sensitivity of the current language
1157 * @param {RegExp | string} value
1158 * @param {boolean} [global]
1160 function langRe(value
, global
) {
1164 + (language
.case_insensitive
? 'i' : '')
1165 + (language
.unicodeRegex
? 'u' : '')
1166 + (global
? 'g' : '')
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.
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).
1185 this.matchIndexes
= {};
1194 opts
.position
= this.position
++;
1196 this.matchIndexes
[this.matchAt
] = opts
;
1197 this.regexes
.push([opts
, re
]);
1198 this.matchAt
+= countMatchGroups(re
) + 1;
1202 if (this.regexes
.length
=== 0) {
1203 // avoids the need to check length every time exec is called
1205 this.exec
= () => null;
1207 const terminators
= this.regexes
.map(el
=> el
[1]);
1208 this.matcherRe
= langRe(_rewriteBackreferences(terminators
, { joinWith: '|' }), true);
1212 /** @param {string} s */
1214 this.matcherRe
.lastIndex
= this.lastIndex
;
1215 const match
= this.matcherRe
.exec(s
);
1216 if (!match
) { return null; }
1218 // eslint-disable-next-line no-undefined
1219 const i
= match
.findIndex((el
, i
) => i
> 0 && el
!== undefined);
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)
1226 return Object
.assign(match
, matchData
);
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.
1238 So what this class creates MultiRegexs on the fly for whatever search
1239 position they are needed.
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.
1245 Say this is our search group, and we match regex3, but wish to ignore it.
1247 regex1 | regex2 | regex3 | regex4 | regex5 ' ie, startAt = 0
1249 What we need is a new MultiRegex that only includes the remaining
1252 regex4 | regex5 ' ie, startAt = 3
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.
1259 MOST of the time the parser will be setting startAt manually to 0.
1261 class ResumableMultiRegex
{
1266 this.multiRegexes
= [];
1270 this.regexIndex
= 0;
1275 if (this.multiRegexes
[index
]) return this.multiRegexes
[index
];
1277 const matcher
= new MultiRegex();
1278 this.rules
.slice(index
).forEach(([re
, opts
]) => matcher
.addRule(re
, opts
));
1280 this.multiRegexes
[index
] = matcher
;
1284 resumingScanAtSamePosition() {
1285 return this.regexIndex
!== 0;
1289 this.regexIndex
= 0;
1294 this.rules
.push([re
, opts
]);
1295 if (opts
.type
=== "begin") this.count
++;
1298 /** @param {string} s */
1300 const m
= this.getMatcher(this.regexIndex
);
1301 m
.lastIndex
= this.lastIndex
;
1302 let result
= m
.exec(s
);
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":
1309 // our matcher is [string, "booger", number]
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.)
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:
1322 // /--- resume first regex match here (for [number])
1323 // |/---- full match here for [string, "booger", number]
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".
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
);
1344 this.regexIndex
+= result
.position
+ 1;
1345 if (this.regexIndex
=== this.count
) {
1346 // wrap-around to considering all matches again
1356 * Given a mode, builds a huge ResumableMultiRegex that can be used to walk
1357 * the content and find matches.
1359 * @param {CompiledMode} mode
1360 * @returns {ResumableMultiRegex}
1362 function buildModeRegex(mode
) {
1363 const mm
= new ResumableMultiRegex();
1365 mode
.contains
.forEach(term
=> mm
.addRule(term
.begin
, { rule: term
, type: "begin" }));
1367 if (mode
.terminatorEnd
) {
1368 mm
.addRule(mode
.terminatorEnd
, { type: "end" });
1371 mm
.addRule(mode
.illegal
, { type: "illegal" });
1377 /** skip vs abort vs ignore
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.
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.
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.
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.
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.
1402 * - The mode does not begin.
1403 * - Content matched by `begin` is ignored.
1404 * - The parser cursor is not moved forward.
1408 * Compiles an individual mode
1410 * This can raise an error if the mode contains certain detectable known logic
1412 * @param {Mode} mode
1413 * @param {CompiledMode | null} [parent]
1414 * @returns {CompiledMode | never}
1416 function compileMode(mode
, parent
) {
1417 const cmode
= /** @type CompiledMode */ (mode
);
1418 if (mode
.isCompiled
) return cmode
;
1422 // do this early so compiler extensions generally don't have to worry about
1423 // the distinction between match/begin
1427 ].forEach(ext
=> ext(mode
, parent
));
1429 language
.compilerExtensions
.forEach(ext
=> ext(mode
, parent
));
1431 // __beforeBegin is considered private API, internal use only
1432 mode
.__beforeBegin
= null;
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.
1439 // default to 1 relevance if not specified
1441 ].forEach(ext
=> ext(mode
, parent
));
1443 mode
.isCompiled
= true;
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
1450 mode
.keywords
= Object
.assign({}, mode
.keywords
);
1451 keywordPattern
= mode
.keywords
.$pattern
;
1452 delete mode
.keywords
.$pattern
;
1454 keywordPattern
= keywordPattern
|| /\w+/;
1456 if (mode
.keywords
) {
1457 mode
.keywords
= compileKeywords(mode
.keywords
, language
.case_insensitive
);
1460 cmode
.keywordPatternRe
= langRe(keywordPattern
, true);
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
;
1472 if (mode
.illegal
) cmode
.illegalRe
= langRe(/** @type {RegExp | string} */ (mode
.illegal
));
1473 if (!mode
.contains
) mode
.contains
= [];
1475 mode
.contains
= [].concat(...mode
.contains
.map(function(c
) {
1476 return expandOrCloneMode(c
=== 'self' ? mode : c
);
1478 mode
.contains
.forEach(function(c
) { compileMode(/** @type Mode */ (c
), cmode
); });
1481 compileMode(mode
.starts
, parent
);
1484 cmode
.matcher
= buildModeRegex(cmode
);
1488 if (!language
.compilerExtensions
) language
.compilerExtensions
= [];
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.");
1495 // we need a null object, which inherit will guarantee
1496 language
.classNameAliases
= inherit
$1(language
.classNameAliases
|| {});
1498 return compileMode(/** @type Mode */ (language
));
1502 * Determines if a mode has a dependency on it's parent or not
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
1509 * @param {Mode | null} mode
1510 * @returns {boolean} - is there a dependency on the parent?
1512 function dependencyOnParent(mode
) {
1513 if (!mode
) return false;
1515 return mode
.endsWithParent
|| dependencyOnParent(mode
.starts
);
1519 * Expands a mode or clones it if necessary
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.
1525 * @param {Mode} mode
1526 * @returns {Mode | Mode[]}
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
);
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
;
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 });
1550 if (Object
.isFrozen(mode
)) {
1551 return inherit
$1(mode
);
1554 // no special dependency issues, just return ourselves
1558 var version
= "11.11.1";
1560 class HTMLInjectionError
extends Error
{
1561 constructor(reason
, html
) {
1563 this.name
= "HTMLInjectionError";
1569 Syntax highlighting with language autodetection.
1570 https://highlightjs.org/
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
1597 const escape
= escapeHTML
;
1598 const inherit
= inherit
$1;
1599 const NO_MATCH
= Symbol("nomatch");
1600 const MAX_KEYWORD_HITS
= 7;
1603 * @param {any} hljs - object that is extended (legacy)
1604 * @returns {HLJSApi}
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[]} */
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: [] };
1622 // Global options used when within external APIs. This is modified when
1623 // calling the `hljs.configure` function.
1624 /** @type HLJSOptions */
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',
1633 // beta configuration options, subject to change, welcome to discuss
1634 // https://github.com/highlightjs/highlight.js/issues/1086
1635 __emitter: TokenTreeEmitter
1638 /* Utility functions */
1641 * Tests a language name to see if highlighting should be skipped
1642 * @param {string} languageName
1644 function shouldNotHighlight(languageName
) {
1645 return options
.noHighlightRe
.test(languageName
);
1649 * @param {HighlightedHTMLElement} block - the HTML element to determine language for
1651 function blockLanguage(block
) {
1652 let classes
= block
.className
+ ' ';
1654 classes
+= block
.parentNode
? block
.parentNode
.className : '';
1656 // language-* takes precedence over non-prefixed class names.
1657 const match
= options
.languageDetectRe
.exec(classes
);
1659 const language
= getLanguage(match
[1]);
1661 warn(LANGUAGE_NOT_FOUND
.replace("{}", match
[1]));
1662 warn("Falling back to no-highlight mode for this block.", block
);
1664 return language
? match
[1] : 'no-highlight';
1669 .find((_class
) => shouldNotHighlight(_class
) || getLanguage(_class
));
1673 * Core highlighting function.
1676 * highlight(lang, code, ignoreIllegals, continuation)
1679 * highlight(code, {lang, ignoreIllegals})
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
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
1693 function highlight(codeOrLanguageName
, optionsOrCode
, ignoreIllegals
) {
1695 let languageName
= "";
1696 if (typeof optionsOrCode
=== "object") {
1697 code
= codeOrLanguageName
;
1698 ignoreIllegals
= optionsOrCode
.ignoreIllegals
;
1699 languageName
= optionsOrCode
.language
;
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
;
1708 // https://github.com/highlightjs/highlight.js/issues/3149
1709 // eslint-disable-next-line no-undefined
1710 if (ignoreIllegals
=== undefined) { ignoreIllegals
= true; }
1712 /** @type {BeforeHighlightContext} */
1715 language: languageName
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
);
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
1725 : _highlight(context
.language
, context
.code
, ignoreIllegals
);
1727 result
.code
= context
.code
;
1728 // the plugin can change anything in result to suite it
1729 fire("after:highlight", result
);
1735 * private highlight that's used internally and does not fire callbacks
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
1743 function _highlight(languageName
, codeToHighlight
, ignoreIllegals
, continuation
) {
1744 const keywordHits
= Object
.create(null);
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}
1752 function keywordData(mode
, matchText
) {
1753 return mode
.keywords
[matchText
];
1756 function processKeywords() {
1757 if (!top
.keywords
) {
1758 emitter
.addText(modeBuffer
);
1763 top
.keywordPatternRe
.lastIndex
= 0;
1764 let match
= top
.keywordPatternRe
.exec(modeBuffer
);
1768 buf
+= modeBuffer
.substring(lastIndex
, match
.index
);
1769 const word
= language
.case_insensitive
? match
[0].toLowerCase() : match
[0];
1770 const data
= keywordData(top
, word
);
1772 const [kind
, keywordRelevance
] = data
;
1773 emitter
.addText(buf
);
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
1783 const cssClass
= language
.classNameAliases
[kind
] || kind
;
1784 emitKeyword(match
[0], cssClass
);
1789 lastIndex
= top
.keywordPatternRe
.lastIndex
;
1790 match
= top
.keywordPatternRe
.exec(modeBuffer
);
1792 buf
+= modeBuffer
.substring(lastIndex
);
1793 emitter
.addText(buf
);
1796 function processSubLanguage() {
1797 if (modeBuffer
=== "") return;
1798 /** @type HighlightResult */
1801 if (typeof top
.subLanguage
=== 'string') {
1802 if (!languages
[top
.subLanguage
]) {
1803 emitter
.addText(modeBuffer
);
1806 result
= _highlight(top
.subLanguage
, modeBuffer
, true, continuations
[top
.subLanguage
]);
1807 continuations
[top
.subLanguage
] = /** @type {CompiledMode} */ (result
._top
);
1809 result
= highlightAuto(modeBuffer
, top
.subLanguage
.length
? top
.subLanguage : null);
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
1816 if (top
.relevance
> 0) {
1817 relevance
+= result
.relevance
;
1819 emitter
.__addSublanguage(result
._emitter
, result
.language
);
1822 function processBuffer() {
1823 if (top
.subLanguage
!= null) {
1824 processSubLanguage();
1832 * @param {string} text
1833 * @param {string} scope
1835 function emitKeyword(keyword
, scope
) {
1836 if (keyword
=== "") return;
1838 emitter
.startScope(scope
);
1839 emitter
.addText(keyword
);
1844 * @param {CompiledScope} scope
1845 * @param {RegExpMatchArray} match
1847 function emitMultiClass(scope
, match
) {
1849 const max
= match
.length
- 1;
1851 if (!scope
._emit
[i
]) { i
++; continue; }
1852 const klass
= language
.classNameAliases
[scope
[i
]] || scope
[i
];
1853 const text
= match
[i
];
1855 emitKeyword(text
, klass
);
1866 * @param {CompiledMode} mode - new mode to start
1867 * @param {RegExpMatchArray} match
1869 function startNewMode(mode
, match
) {
1870 if (mode
.scope
&& typeof mode
.scope
=== "string") {
1871 emitter
.openNode(language
.classNameAliases
[mode
.scope
] || mode
.scope
);
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
);
1878 } else if (mode
.beginScope
._multi
) {
1879 // at this point modeBuffer should just be the match
1880 emitMultiClass(mode
.beginScope
, match
);
1885 top
= Object
.create(mode
, { parent: { value: top
} });
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
1895 function endOfMode(mode
, match
, matchPlusRemainder
) {
1896 let matched
= startsWith(mode
.endRe
, matchPlusRemainder
);
1899 if (mode
["on:end"]) {
1900 const resp
= new Response(mode
);
1901 mode
["on:end"](match
, resp
);
1902 if (resp
.isMatchIgnored
) matched
= false;
1906 while (mode
.endsParent
&& mode
.parent
) {
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
);
1920 * Handle matching but then ignoring a sequence of text
1922 * @param {string} lexeme - string containing full match text
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
1928 modeBuffer
+= lexeme
[0];
1931 // no need to move the cursor, we still have additional regexes to try and
1932 // match at this very spot
1933 resumeScanAtSamePosition
= true;
1939 * Handle the start of a new potential mode match
1941 * @param {EnhancedMatch} match - the current match
1942 * @returns {number} how far to advance the parse cursor
1944 function doBeginMatch(match
) {
1945 const lexeme
= match
[0];
1946 const newMode
= match
.rule
;
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
) {
1954 if (resp
.isMatchIgnored
) return doIgnore(lexeme
);
1958 modeBuffer
+= lexeme
;
1960 if (newMode
.excludeBegin
) {
1961 modeBuffer
+= lexeme
;
1964 if (!newMode
.returnBegin
&& !newMode
.excludeBegin
) {
1965 modeBuffer
= lexeme
;
1968 startNewMode(newMode
, match
);
1969 return newMode
.returnBegin
? 0 : lexeme
.length
;
1973 * Handle the potential end of mode
1975 * @param {RegExpMatchArray} match - the current match
1977 function doEndMatch(match
) {
1978 const lexeme
= match
[0];
1979 const matchPlusRemainder
= codeToHighlight
.substring(match
.index
);
1981 const endMode
= endOfMode(top
, match
, matchPlusRemainder
);
1982 if (!endMode
) { return NO_MATCH
; }
1985 if (top
.endScope
&& top
.endScope
._wrap
) {
1987 emitKeyword(lexeme
, top
.endScope
._wrap
);
1988 } else if (top
.endScope
&& top
.endScope
._multi
) {
1990 emitMultiClass(top
.endScope
, match
);
1991 } else if (origin
.skip
) {
1992 modeBuffer
+= lexeme
;
1994 if (!(origin
.returnEnd
|| origin
.excludeEnd
)) {
1995 modeBuffer
+= lexeme
;
1998 if (origin
.excludeEnd
) {
1999 modeBuffer
= lexeme
;
2004 emitter
.closeNode();
2006 if (!top
.skip
&& !top
.subLanguage
) {
2007 relevance
+= top
.relevance
;
2010 } while (top
!== endMode
.parent
);
2011 if (endMode
.starts
) {
2012 startNewMode(endMode
.starts
, match
);
2014 return origin
.returnEnd
? 0 : lexeme
.length
;
2017 function processContinuations() {
2019 for (let current
= top
; current
!== language
; current
= current
.parent
) {
2020 if (current
.scope
) {
2021 list
.unshift(current
.scope
);
2024 list
.forEach(item
=> emitter
.openNode(item
));
2027 /** @type {{type?: MatchType, index?: number, rule?: Mode}}} */
2031 * Process an individual match
2033 * @param {string} textBeforeMatch - text preceding the match (since the last match)
2034 * @param {EnhancedMatch} [match] - the match itself
2036 function processLexeme(textBeforeMatch
, match
) {
2037 const lexeme
= match
&& match
[0];
2039 // add non-matched text to the current mode buffer
2040 modeBuffer
+= textBeforeMatch
;
2042 if (lexeme
== null) {
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);
2055 /** @type {AnnotatedError} */
2056 const err
= new Error(`0 width match regex (${languageName})`);
2057 err
.languageName
= languageName
;
2058 err
.badRule
= lastMatch
.rule
;
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>') + '"');
2073 } else if (match
.type
=== "end") {
2074 const processed
= doEndMatch(match
);
2075 if (processed
!== NO_MATCH
) {
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
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
2093 if (iterations
> 100000 && iterations
> match
.index
* 3) {
2094 const err
= new Error('potential infinite loop, way more iterations than matches');
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)
2103 This causes no real harm other than stopping a few times too many.
2106 modeBuffer
+= lexeme
;
2107 return lexeme
.length
;
2110 const language
= getLanguage(languageName
);
2112 error(LANGUAGE_NOT_FOUND
.replace("{}", languageName
));
2113 throw new Error('Unknown language: "' + languageName
+ '"');
2116 const md
= compileLanguage(language
);
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
= '';
2128 let resumeScanAtSamePosition
= false;
2131 if (!language
.__emitTokens
) {
2132 top
.matcher
.considerAll();
2136 if (resumeScanAtSamePosition
) {
2137 // only regexes not matched previously will now be
2138 // considered for a potential match
2139 resumeScanAtSamePosition
= false;
2141 top
.matcher
.considerAll();
2143 top
.matcher
.lastIndex
= index
;
2145 const match
= top
.matcher
.exec(codeToHighlight
);
2146 // console.log("match", match[0], match.rule && match.rule.begin)
2150 const beforeMatch
= codeToHighlight
.substring(index
, match
.index
);
2151 const processedCount
= processLexeme(beforeMatch
, match
);
2152 index
= match
.index
+ processedCount
;
2154 processLexeme(codeToHighlight
.substring(index
));
2156 language
.__emitTokens(codeToHighlight
, emitter
);
2160 result
= emitter
.toHTML();
2163 language: languageName
,
2171 if (err
.message
&& err
.message
.includes('Illegal')) {
2173 language: languageName
,
2174 value: escape(codeToHighlight
),
2178 message: err
.message
,
2180 context: codeToHighlight
.slice(index
- 100, index
+ 100),
2186 } else if (SAFE_MODE
) {
2188 language: languageName
,
2189 value: escape(codeToHighlight
),
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}
2209 function justTextHighlightResult(code
) {
2211 value: escape(code
),
2214 _top: PLAINTEXT_LANGUAGE
,
2215 _emitter: new options
.__emitter(options
)
2217 result
._emitter
.addText(code
);
2222 Highlighting with language detection. Accepts a string with the code to
2223 highlight. Returns an object with the following properties:
2225 - language (detected language)
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)
2231 @param {string} code
2232 @param {Array<string>} [languageSubset]
2233 @returns {AutoHighlightResult}
2235 function highlightAuto(code
, languageSubset
) {
2236 languageSubset
= languageSubset
|| options
.languages
|| Object
.keys(languages
);
2237 const plaintext
= justTextHighlightResult(code
);
2239 const results
= languageSubset
.filter(getLanguage
).filter(autoDetection
).map(name
=>
2240 _highlight(name
, code
, false)
2242 results
.unshift(plaintext
); // plaintext is always an option
2244 const sorted
= results
.sort((a
, b
) => {
2245 // sort base on relevance
2246 if (a
.relevance
!== b
.relevance
) return b
.relevance
- a
.relevance
;
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
) {
2253 } else if (getLanguage(b
.language
).supersetOf
=== a
.language
) {
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
2265 const [best
, secondBest
] = sorted
;
2267 /** @type {AutoHighlightResult} */
2268 const result
= best
;
2269 result
.secondBest
= secondBest
;
2275 * Builds new class name for block given the language name
2277 * @param {HTMLElement} element
2278 * @param {string} [currentLang]
2279 * @param {string} [resultLang]
2281 function updateClassName(element
, currentLang
, resultLang
) {
2282 const language
= (currentLang
&& aliases
[currentLang
]) || resultLang
;
2284 element
.classList
.add("hljs");
2285 element
.classList
.add(`language-${language}`);
2289 * Applies highlighting to a DOM node containing code.
2291 * @param {HighlightedHTMLElement} element - the HTML element to highlight
2293 function highlightElement(element
) {
2294 /** @type HTMLElement */
2296 const language
= blockLanguage(element
);
2298 if (shouldNotHighlight(language
)) return;
2300 fire("before:highlightElement",
2301 { el: element
, language
});
2303 if (element
.dataset
.highlighted
) {
2304 console
.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.", element
);
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
);
2320 if (options
.throwUnescapedHTML
) {
2321 const err
= new HTMLInjectionError(
2322 "One of your code blocks includes unescaped HTML.",
2330 const text
= node
.textContent
;
2331 const result
= language
? highlight(text
, { language
, ignoreIllegals: true }) : highlightAuto(text
);
2333 element
.innerHTML
= result
.value
;
2334 element
.dataset
.highlighted
= "yes";
2335 updateClassName(element
, language
, result
.language
);
2337 language: result
.language
,
2338 // TODO: remove with version 11.0
2339 re: result
.relevance
,
2340 relevance: result
.relevance
2342 if (result
.secondBest
) {
2343 element
.secondBest
= {
2344 language: result
.secondBest
.language
,
2345 relevance: result
.secondBest
.relevance
2349 fire("after:highlightElement", { el: element
, result
, text
});
2353 * Updates highlight.js global options with the passed options
2355 * @param {Partial<HLJSOptions>} userOptions
2357 function configure(userOptions
) {
2358 options
= inherit(options
, userOptions
);
2361 // TODO: remove v12, deprecated
2362 const initHighlighting
= () => {
2364 deprecated("10.6.0", "initHighlighting() deprecated. Use highlightAll() now.");
2367 // TODO: remove v12, deprecated
2368 function initHighlightingOnLoad() {
2370 deprecated("10.6.0", "initHighlightingOnLoad() deprecated. Use highlightAll() now.");
2373 let wantsHighlight
= false;
2376 * auto-highlights all pre>code elements on the page
2378 function highlightAll() {
2380 // if a highlight was requested before DOM was loaded, do now
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);
2390 wantsHighlight
= true;
2394 const blocks
= document
.querySelectorAll(options
.cssSelector
);
2395 blocks
.forEach(highlightElement
);
2399 * Register a language grammar module
2401 * @param {string} languageName
2402 * @param {LanguageFn} languageDefinition
2404 function registerLanguage(languageName
, languageDefinition
) {
2407 lang
= languageDefinition(hljs
);
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
;
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
);
2424 registerAliases(lang
.aliases
, { languageName
});
2429 * Remove a language grammar module
2431 * @param {string} languageName
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
];
2443 * @returns {string[]} List of language internal names
2445 function listLanguages() {
2446 return Object
.keys(languages
);
2450 * @param {string} name - name of the language to retrieve
2451 * @returns {Language | undefined}
2453 function getLanguage(name
) {
2454 name
= (name
|| '').toLowerCase();
2455 return languages
[name
] || languages
[aliases
[name
]];
2460 * @param {string|string[]} aliasList - single alias or list of aliases
2461 * @param {{languageName: string}} opts
2463 function registerAliases(aliasList
, { languageName
}) {
2464 if (typeof aliasList
=== 'string') {
2465 aliasList
= [aliasList
];
2467 aliasList
.forEach(alias
=> { aliases
[alias
.toLowerCase()] = languageName
; });
2471 * Determines if a given language has auto-detection enabled
2472 * @param {string} name - name of the language
2474 function autoDetection(name
) {
2475 const lang
= getLanguage(name
);
2476 return lang
&& !lang
.disableAutodetect
;
2480 * Upgrades the old highlightBlock plugins to the new
2481 * highlightElement API
2482 * @param {HLJSPlugin} plugin
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
)
2493 if (plugin
["after:highlightBlock"] && !plugin
["after:highlightElement"]) {
2494 plugin
["after:highlightElement"] = (data
) => {
2495 plugin
["after:highlightBlock"](
2496 Object
.assign({ block: data
.el
}, data
)
2503 * @param {HLJSPlugin} plugin
2505 function addPlugin(plugin
) {
2506 upgradePluginAPI(plugin
);
2507 plugins
.push(plugin
);
2511 * @param {HLJSPlugin} plugin
2513 function removePlugin(plugin
) {
2514 const index
= plugins
.indexOf(plugin
);
2516 plugins
.splice(index
, 1);
2522 * @param {PluginEvent} event
2525 function fire(event
, args
) {
2527 plugins
.forEach(function(plugin
) {
2536 * @param {HighlightedHTMLElement} el
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.");
2542 return highlightElement(el
);
2545 /* Interface definition */
2546 Object
.assign(hljs
, {
2551 // TODO: Remove with v12 API
2552 highlightBlock: deprecateHighlightBlock
,
2555 initHighlightingOnLoad
,
2567 hljs
.debugMode = function() { SAFE_MODE
= false; };
2568 hljs
.safeMode = function() { SAFE_MODE
= true; };
2569 hljs
.versionString
= version
;
2573 lookahead: lookahead
,
2576 anyNumberOfTimes: anyNumberOfTimes
2579 for (const key
in MODES
) {
2581 if (typeof MODES
[key
] === "object") {
2583 deepFreeze(MODES
[key
]);
2587 // merge all the modes/regexes into our main object
2588 Object
.assign(hljs
, MODES
);
2593 // Other names for the variable may break build script
2594 const highlight
= HLJS({});
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({});
2600 export { highlight as
default };