highlight-it
Version:
A lightweight syntax highlighting library with themes, line numbers, and copy functionality
1,114 lines (987 loc) • 2.06 MB
JavaScript
/*!
* HighlightIt v0.2.7
* https://github.com/TN3W/highlight-it
* (c) 2025 TN3W
* Released under the Apache 2.0 License
*/
/*!
* highlight.js v11.11.1
* Copyright (c) 2006, Ivan Sagalaev
* Licensed under the BSD 3-Clause License
* https://github.com/highlightjs/highlight.js/blob/main/LICENSE
*/
(function (global) {
const STYLES_CSS =
':root{--hl-background:#1e1e1e;--hl-border:#2d2d2d;--hl-header-bg:#2d2d2d;--hl-text:#d4d4d4;--hl-badge-bg:#3d3d3d;--hl-hover-bg:#3d3d3d;--hl-copied-color:#4ec9b0;--hl-keyword:#569cd6;--hl-string:#ce9178;--hl-comment:#6a9955;--hl-function:#dcdcaa;--hl-number:#b5cea8;--hl-class:#4ec9b0;--hl-params:#9cdcfe;--hl-built-in:#4ec9b0;--hl-tag:#569cd6;--hl-attr:#9cdcfe;--hl-variable:#9cdcfe;--hl-property:#9cdcfe;--hl-operator:#d4d4d4;--hl-punctuation:#d4d4d4;--hl-regexp:#d16969;--hl-doctype:grey;--hl-meta:grey;--hl-name:#569cd6;--hl-selector:#d7ba7d;--hl-attr-value:#ce9178;--hl-constant:#4fc1ff;--hl-symbol:#b5cea8;--hl-important:#569cd6;--hl-deleted:#ce9178;--hl-inserted:#b5cea8;--hl-type:#4ec9b0;--hl-literal:#569cd6;--hl-link:#9cdcfe;--hl-title-function:#dcdcaa;--hl-title-class:#4ec9b0;--hl-title-namespace:#4ec9b0;--hl-text-rgb:212,212,212}.highlightit-theme-light{--hl-background:#fff;--hl-border:#ccc;--hl-header-bg:#e0e0e0;--hl-text:#333;--hl-badge-bg:silver;--hl-hover-bg:#d0d0d0;--hl-copied-color:#0b8043;--hl-keyword:#00f;--hl-string:#a31515;--hl-comment:green;--hl-function:#795e26;--hl-number:#098658;--hl-class:#267f99;--hl-params:#001080;--hl-built-in:#0070c1;--hl-tag:maroon;--hl-attr:red;--hl-variable:#001080;--hl-property:#001080;--hl-operator:#000;--hl-punctuation:#000;--hl-regexp:#811f3f;--hl-doctype:grey;--hl-meta:grey;--hl-name:maroon;--hl-selector:maroon;--hl-attr-value:#00f;--hl-constant:#0070c1;--hl-symbol:#000;--hl-important:#00f;--hl-deleted:#a31515;--hl-inserted:#098658;--hl-type:#267f99;--hl-literal:#00f;--hl-link:#00f;--hl-title-function:#795e26;--hl-title-class:#267f99;--hl-title-namespace:#267f99;--hl-text-rgb:51,51,51}@media (prefers-color-scheme:light){:root.highlightit-theme-auto{--hl-background:#fff;--hl-border:#ccc;--hl-header-bg:#e0e0e0;--hl-text:#333;--hl-badge-bg:silver;--hl-hover-bg:#d0d0d0;--hl-copied-color:#0b8043;--hl-keyword:#00f;--hl-string:#a31515;--hl-comment:green;--hl-function:#795e26;--hl-number:#098658;--hl-class:#267f99;--hl-params:#001080;--hl-built-in:#0070c1;--hl-tag:maroon;--hl-attr:red;--hl-variable:#001080;--hl-property:#001080;--hl-operator:#000;--hl-punctuation:#000;--hl-regexp:#811f3f;--hl-doctype:grey;--hl-meta:grey;--hl-name:maroon;--hl-selector:maroon;--hl-attr-value:#00f;--hl-constant:#0070c1;--hl-symbol:#000;--hl-important:#00f;--hl-deleted:#a31515;--hl-inserted:#098658;--hl-type:#267f99;--hl-literal:#00f;--hl-link:#00f;--hl-title-function:#795e26;--hl-title-class:#267f99;--hl-title-namespace:#267f99;--hl-text-rgb:51,51,51}}.highlightit-container{border:1px solid var(--hl-border);border-radius:6px;overflow:hidden;position:relative}.highlightit-container code[data-theme=light],.highlightit-container pre[data-theme=light],.highlightit-container[data-theme=light]{--hl-background:#fff!important;--hl-border:#ccc!important;--hl-header-bg:#e0e0e0!important;--hl-text:#333!important;--hl-badge-bg:silver!important;--hl-hover-bg:#d0d0d0!important;--hl-copied-color:#0b8043!important;--hl-keyword:#00f!important;--hl-string:#a31515!important;--hl-comment:green!important;--hl-function:#795e26!important;--hl-number:#098658!important;--hl-class:#267f99!important;--hl-params:#001080!important;--hl-built-in:#0070c1!important;--hl-tag:maroon!important;--hl-attr:red!important;--hl-variable:#001080!important;--hl-property:#001080!important;--hl-operator:#000!important;--hl-punctuation:#000!important;--hl-regexp:#811f3f!important;--hl-doctype:grey!important;--hl-meta:grey!important;--hl-name:maroon!important;--hl-selector:maroon!important;--hl-attr-value:#00f!important;--hl-constant:#0070c1!important;--hl-symbol:#000!important;--hl-important:#00f!important;--hl-deleted:#a31515!important;--hl-inserted:#098658!important;--hl-type:#267f99!important;--hl-literal:#00f!important;--hl-link:#00f!important;--hl-title-function:#795e26!important;--hl-title-class:#267f99!important;--hl-title-namespace:#267f99!important}.highlightit-container code[data-theme=dark],.highlightit-container pre[data-theme=dark],.highlightit-container[data-theme=dark]{--hl-background:#1e1e1e!important;--hl-border:#2d2d2d!important;--hl-header-bg:#2d2d2d!important;--hl-text:#d4d4d4!important;--hl-badge-bg:#3d3d3d!important;--hl-hover-bg:#3d3d3d!important;--hl-copied-color:#4ec9b0!important;--hl-keyword:#569cd6!important;--hl-string:#ce9178!important;--hl-comment:#6a9955!important;--hl-function:#dcdcaa!important;--hl-number:#b5cea8!important;--hl-class:#4ec9b0!important;--hl-params:#9cdcfe!important;--hl-built-in:#4ec9b0!important;--hl-tag:#569cd6!important;--hl-attr:#9cdcfe!important;--hl-variable:#9cdcfe!important;--hl-property:#9cdcfe!important;--hl-operator:#d4d4d4!important;--hl-punctuation:#d4d4d4!important;--hl-regexp:#d16969!important;--hl-doctype:grey!important;--hl-meta:grey!important;--hl-name:#569cd6!important;--hl-selector:#d7ba7d!important;--hl-attr-value:#ce9178!important;--hl-constant:#4fc1ff!important;--hl-symbol:#b5cea8!important;--hl-important:#569cd6!important;--hl-deleted:#ce9178!important;--hl-inserted:#b5cea8!important;--hl-type:#4ec9b0!important;--hl-literal:#569cd6!important;--hl-link:#9cdcfe!important;--hl-title-function:#dcdcaa!important;--hl-title-class:#4ec9b0!important;--hl-title-namespace:#4ec9b0!important}.highlightit-header{align-items:center;background:var(--hl-header-bg);color:var(--hl-text);display:flex;font-size:14px;font-weight:600;justify-content:space-between;padding:8px 15px}.highlightit-language{background:var(--hl-badge-bg);border-radius:4px;color:var(--hl-text);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:12px;padding:2px 8px}.highlightit-button{align-items:center;background:transparent;border:none;border-radius:4px;color:var(--hl-text);cursor:pointer;display:flex;height:28px;justify-content:center;padding:4px 6px;transition:all .2s ease}.highlightit-button:hover{background:var(--hl-hover-bg)}.highlightit-button.copied{color:var(--hl-copied-color)}.highlightit-check-icon,.highlightit-copy-icon,.highlightit-download-icon,.highlightit-share-icon{height:16px;vertical-align:middle;width:16px}.highlightit-original,[data-with-reload][style*="display: none"]{border:0!important;height:0!important;margin:0!important;opacity:0!important;overflow:hidden!important;padding:0!important;pointer-events:none!important;position:absolute!important;visibility:hidden!important;width:0!important;z-index:-9999!important;clip:rect(1px,1px,1px,1px)!important;clip-path:inset(50%)!important}.highlightit-floating-buttons{display:flex;gap:4px;opacity:0;position:absolute;right:8px;top:8px;transition:opacity .2s ease;z-index:10}.highlightit-button.highlightit-floating{background-color:rgba(45,45,45,.8);transition:background-color .2s ease,transform .2s ease}.highlightit-container[data-theme=light] .highlightit-button.highlightit-floating,.highlightit-theme-light .highlightit-button.highlightit-floating,:root.highlightit-theme-auto.system-light-theme .highlightit-button.highlightit-floating{background-color:hsla(0,0%,78%,.8)}.highlightit-no-header:hover .highlightit-floating-buttons,:root.highlightit-touch-device .highlightit-floating-buttons{opacity:1}.highlightit-button.highlightit-floating:hover{background-color:rgba(65,65,65,.9);transform:scale(1.05)}.highlightit-container[data-theme=light] .highlightit-button.highlightit-floating:hover,.highlightit-theme-light .highlightit-button.highlightit-floating:hover,:root.highlightit-theme-auto.system-light-theme .highlightit-button.highlightit-floating:hover{background-color:hsla(0,0%,67%,.9);transform:scale(1.05)}.highlightit-container pre{background-color:var(--hl-background)!important;border-radius:0 0 6px 6px;margin:0;overflow:auto;padding:16px}.highlightit-container pre::-webkit-scrollbar{height:10px;width:10px}.highlightit-theme-dark .highlightit-container pre::-webkit-scrollbar-track,:root:not(.highlightit-theme-light) .highlightit-container pre::-webkit-scrollbar-track{background:#2d2d2d;border-radius:4px}.highlightit-theme-dark .highlightit-container pre::-webkit-scrollbar-thumb,:root:not(.highlightit-theme-light) .highlightit-container pre::-webkit-scrollbar-thumb{background:#3d3d3d;border-radius:4px}.highlightit-theme-dark .highlightit-container pre::-webkit-scrollbar-thumb:hover,:root:not(.highlightit-theme-light) .highlightit-container pre::-webkit-scrollbar-thumb:hover{background:#4d4d4d}:root.highlightit-theme-light .highlightit-container pre::-webkit-scrollbar-track{background:#e0e0e0;border-radius:4px}:root.highlightit-theme-light .highlightit-container pre::-webkit-scrollbar-thumb{background:silver;border-radius:4px}.highlightit-container[data-theme=light] pre::-webkit-scrollbar-thumb:hover,:root.highlightit-theme-light .highlightit-container pre::-webkit-scrollbar-thumb:hover{background:#a0a0a0}.highlightit-theme-dark .highlightit-container pre,:root:not(.highlightit-theme-light) .highlightit-container pre{scrollbar-color:#3d3d3d #2d2d2d;scrollbar-width:thin}:root.highlightit-theme-light .highlightit-container pre{scrollbar-color:silver #e0e0e0;scrollbar-width:thin}html .highlightit-container[data-theme=dark] pre::-webkit-scrollbar-track{background:#2d2d2d!important;border-radius:4px}html .highlightit-container[data-theme=dark] pre::-webkit-scrollbar-thumb{background:#3d3d3d!important;border-radius:4px}html .highlightit-container[data-theme=dark] pre::-webkit-scrollbar-thumb:hover{background:#4d4d4d!important}html .highlightit-container[data-theme=dark] pre{scrollbar-color:#3d3d3d #2d2d2d!important;scrollbar-width:thin!important}html .highlightit-container[data-theme=light] pre::-webkit-scrollbar-track{background:#e0e0e0!important;border-radius:4px}html .highlightit-container[data-theme=light] pre::-webkit-scrollbar-thumb{background:silver!important;border-radius:4px}html .highlightit-container[data-theme=light] pre::-webkit-scrollbar-thumb:hover{background:#a0a0a0!important}html .highlightit-container[data-theme=light] pre{scrollbar-color:silver #e0e0e0!important;scrollbar-width:thin!important}.highlightit-no-header pre{border-radius:6px}.highlightit-container code{color:var(--hl-text);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:14px;line-height:1.5;margin-left:0;padding:0!important;tab-size:4;-moz-tab-size:4;white-space:pre}.highlightit-has-line-numbers code{margin-left:1em}.highlightit-anchor-highlight{animation:highlightit-anchor-pulse 2s ease-in-out}@keyframes highlightit-anchor-pulse{0%{box-shadow:0 0 0 0 rgba(var(--hl-text-rgb),.2)}70%{box-shadow:0 0 0 10px rgba(var(--hl-text-rgb),0)}to{box-shadow:0 0 0 0 rgba(var(--hl-text-rgb),0)}}.hljs-keyword{color:var(--hl-keyword)}.hljs-string{color:var(--hl-string)}.hljs-comment{color:var(--hl-comment)}.hljs-function{color:var(--hl-function)}.hljs-number{color:var(--hl-number)}.hljs-class,.hljs-title{color:var(--hl-class)}.hljs-params{color:var(--hl-params)}.hljs-built_in{color:var(--hl-built-in)}.hljs-tag{color:var(--hl-tag)}.hljs-name{color:var(--hl-name)}.hljs-attribute{color:var(--hl-attr)}.hljs-attr-value{color:var(--hl-attr-value)}.hljs-doctype{font-style:italic}.hljs-doctag,.hljs-doctype{color:var(--hl-doctype)}.hljs-variable{color:var(--hl-variable)}.hljs-property{color:var(--hl-property)}.hljs-operator{color:var(--hl-operator)}.hljs-punctuation{color:var(--hl-punctuation)}.hljs-regexp{color:var(--hl-regexp)}.hljs-meta{color:var(--hl-meta);font-style:italic}.hljs-selector{color:var(--hl-selector)}.hljs-constant{color:var(--hl-constant);font-weight:700}.hljs-symbol{color:var(--hl-symbol)}.hljs-important{color:var(--hl-important);font-weight:700}.hljs-type{color:var(--hl-type)}.hljs-literal{color:var(--hl-literal)}.hljs-link{color:var(--hl-link);text-decoration:underline}.hljs-deleted{background-color:rgba(255,0,0,.1);color:var(--hl-deleted)}.hljs-inserted{background-color:rgba(0,255,0,.1);color:var(--hl-inserted)}.hljs-title.function_{color:var(--hl-title-function)}.hljs-title.class_{color:var(--hl-title-class)}.hljs-title.namespace{color:var(--hl-title-namespace)}.hljs-selector-tag{color:var(--hl-tag)}.hljs-selector-class,.hljs-selector-id{color:var(--hl-selector)}.hljs-selector-attr{color:var(--hl-attr)}.hljs-selector-pseudo{color:var(--hl-selector);font-style:italic}.hljs-template-variable{font-style:italic}.hljs-subst,.hljs-template-variable{color:var(--hl-variable)}.hljs-section{color:var(--hl-keyword);font-weight:700}.hljs-bullet{color:var(--hl-operator)}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-quote{color:var(--hl-string);font-style:italic}.hljs-code{background-color:rgba(0,0,0,.05);border-radius:3px;color:var(--hl-class);padding:.1em .2em}.hljs-keyword.operator{color:var(--hl-operator)}.hljs-attr{color:var(--hl-attr)}.hljs-variable.language_{color:var(--hl-params);font-style:italic}.hljs-built_in.shell{color:var(--hl-keyword)}.hljs-symbol.instance{color:var(--hl-symbol)}.hljs-symbol.class_{color:var(--hl-class)}.language-html .hljs-tag,.language-xml .hljs-tag{color:var(--hl-tag)}.language-css .hljs-property{color:var(--hl-property)}.language-javascript .hljs-keyword,.language-js .hljs-keyword{color:var(--hl-keyword)}.language-ts .hljs-type,.language-typescript .hljs-type{color:var(--hl-type)}.language-php .hljs-variable{color:var(--hl-variable)}.language-diff .hljs-deletion{background-color:rgba(255,0,0,.1);color:var(--hl-deleted)}.language-diff .hljs-addition{background-color:rgba(0,255,0,.1);color:var(--hl-inserted)}.highlightit-has-line-numbers{display:flex;isolation:isolate;overflow:hidden;position:relative}.highlightit-line-numbers{border-right:1px solid var(--hl-border);color:var(--hl-text);display:flex;flex-direction:column;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:14px;line-height:1.5;opacity:.7;padding-right:.5em;position:relative;text-align:right;user-select:none;-webkit-user-select:none;-moz-user-select:none}.highlightit-line-numbers:after{background-color:var(--hl-header-bg);bottom:0;content:"";left:0;opacity:.05;pointer-events:none;position:absolute;right:0;top:0;z-index:-1}.highlightit-has-line-numbers pre{border-radius:0!important;flex:1;margin:0;padding-left:.5em;position:relative}.highlightit-container[data-theme=light] .highlightit-line-numbers,.highlightit-theme-light .highlightit-line-numbers,:root.highlightit-theme-auto.system-light-theme .highlightit-line-numbers{border-right-color:#ccc!important}.highlightit-visually-hidden{height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;clip:rect(0,0,0,0)!important;border:0!important;opacity:.01!important;pointer-events:none!important;white-space:nowrap!important}.highlightit-line-number-container,.highlightit-with-lines .highlightit-line-number{align-items:center;display:flex;height:1.5em;justify-content:flex-end;margin-bottom:0;position:relative}.highlightit-with-lines .highlightit-line-number{padding-right:1em;transition:opacity .2s ease;z-index:1}.highlightit-line-number-shareable{cursor:pointer}.highlightit-container[data-with-share] .highlightit-line-number-container:hover .highlightit-line-number,[data-with-share] .highlightit-line-number-container:hover .highlightit-line-number{opacity:0}.highlightit-line-share{align-items:center;background:transparent;border:none;border-radius:3px;color:var(--hl-text);cursor:pointer;display:flex;height:auto;justify-content:center;margin-right:1em;opacity:0;padding:4px;position:absolute;right:0;top:50%;transform:translateY(-50%);transition:opacity .2s ease;width:auto;z-index:2}.highlightit-line-number-container:hover .highlightit-line-share{opacity:.7}.highlightit-line-share:hover{background:var(--hl-hover-bg);opacity:1!important}.highlightit-line-share.copied{background:var(--hl-hover-bg);color:var(--hl-copied-color);opacity:1}.highlightit-line-number-container:has(.highlightit-line-share.copied) .highlightit-line-number{opacity:.3}.highlightit-line-number-container.has-copied-button .highlightit-line-number{opacity:.3}.highlightit-line-share svg{height:14px;width:14px}.highlightit-line-highlight{position:relative}.highlightit-line-highlight-overlay{animation:highlightit-line-pulse 2s ease-in-out;background-color:rgba(var(--hl-text-rgb),.1);bottom:0;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:1}@keyframes highlightit-line-pulse{0%{background-color:rgba(var(--hl-text-rgb),.2)}70%{background-color:rgba(var(--hl-text-rgb),.1)}to{background-color:rgba(var(--hl-text-rgb),.1)}}';
function injectCSS(css) {
const style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
}
var hljs = (function () {
'use strict';
function getDefaultExportFromCjs(x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default')
? x['default']
: x;
}
/* eslint-disable no-multi-assign */
var core;
var hasRequiredCore;
function requireCore() {
if (hasRequiredCore) return core;
hasRequiredCore = 1;
function deepFreeze(obj) {
if (obj instanceof Map) {
obj.clear =
obj.delete =
obj.set =
function () {
throw new Error('map is read-only');
};
} else if (obj instanceof Set) {
obj.add =
obj.clear =
obj.delete =
function () {
throw new Error('set is read-only');
};
}
// Freeze self
Object.freeze(obj);
Object.getOwnPropertyNames(obj).forEach((name) => {
const prop = obj[name];
const type = typeof prop;
// Freeze prop if it is an object or function and also not already frozen
if ((type === 'object' || type === 'function') && !Object.isFrozen(prop)) {
deepFreeze(prop);
}
});
return obj;
}
/** @typedef {import('highlight.js').CallbackResponse} CallbackResponse */
/** @typedef {import('highlight.js').CompiledMode} CompiledMode */
/** @implements CallbackResponse */
class Response {
/**
* @param {CompiledMode} mode
*/
constructor(mode) {
// eslint-disable-next-line no-undefined
if (mode.data === undefined) mode.data = {};
this.data = mode.data;
this.isMatchIgnored = false;
}
ignoreMatch() {
this.isMatchIgnored = true;
}
}
/**
* @param {string} value
* @returns {string}
*/
function escapeHTML(value) {
return value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* performs a shallow merge of multiple objects into one
*
* @template T
* @param {T} original
* @param {Record<string,any>[]} objects
* @returns {T} a single new object
*/
function inherit$1(original, ...objects) {
/** @type Record<string,any> */
const result = Object.create(null);
for (const key in original) {
result[key] = original[key];
}
objects.forEach(function (obj) {
for (const key in obj) {
result[key] = obj[key];
}
});
return /** @type {T} */ (result);
}
/**
* @typedef {object} Renderer
* @property {(text: string) => void} addText
* @property {(node: Node) => void} openNode
* @property {(node: Node) => void} closeNode
* @property {() => string} value
*/
/** @typedef {{scope?: string, language?: string, sublanguage?: boolean}} Node */
/** @typedef {{walk: (r: Renderer) => void}} Tree */
/** */
const SPAN_CLOSE = '</span>';
/**
* Determines if a node needs to be wrapped in <span>
*
* @param {Node} node */
const emitsWrappingTags = (node) => {
// rarely we can have a sublanguage where language is undefined
// TODO: track down why
return !!node.scope;
};
/**
*
* @param {string} name
* @param {{prefix:string}} options
*/
const scopeToCSSClass = (name, { prefix }) => {
// sub-language
if (name.startsWith('language:')) {
return name.replace('language:', 'language-');
}
// tiered scope: comment.line
if (name.includes('.')) {
const pieces = name.split('.');
return [
`${prefix}${pieces.shift()}`,
...pieces.map((x, i) => `${x}${'_'.repeat(i + 1)}`),
].join(' ');
}
// simple scope
return `${prefix}${name}`;
};
/** @type {Renderer} */
class HTMLRenderer {
/**
* Creates a new HTMLRenderer
*
* @param {Tree} parseTree - the parse tree (must support `walk` API)
* @param {{classPrefix: string}} options
*/
constructor(parseTree, options) {
this.buffer = '';
this.classPrefix = options.classPrefix;
parseTree.walk(this);
}
/**
* Adds texts to the output stream
*
* @param {string} text */
addText(text) {
this.buffer += escapeHTML(text);
}
/**
* Adds a node open to the output stream (if needed)
*
* @param {Node} node */
openNode(node) {
if (!emitsWrappingTags(node)) return;
const className = scopeToCSSClass(node.scope, { prefix: this.classPrefix });
this.span(className);
}
/**
* Adds a node close to the output stream (if needed)
*
* @param {Node} node */
closeNode(node) {
if (!emitsWrappingTags(node)) return;
this.buffer += SPAN_CLOSE;
}
/**
* returns the accumulated buffer
*/
value() {
return this.buffer;
}
// helpers
/**
* Builds a span element
*
* @param {string} className */
span(className) {
this.buffer += `<span class="${className}">`;
}
}
/** @typedef {{scope?: string, language?: string, children: Node[]} | string} Node */
/** @typedef {{scope?: string, language?: string, children: Node[]} } DataNode */
/** @typedef {import('highlight.js').Emitter} Emitter */
/** */
/** @returns {DataNode} */
const newNode = (opts = {}) => {
/** @type DataNode */
const result = { children: [] };
Object.assign(result, opts);
return result;
};
class TokenTree {
constructor() {
/** @type DataNode */
this.rootNode = newNode();
this.stack = [this.rootNode];
}
get top() {
return this.stack[this.stack.length - 1];
}
get root() {
return this.rootNode;
}
/** @param {Node} node */
add(node) {
this.top.children.push(node);
}
/** @param {string} scope */
openNode(scope) {
/** @type Node */
const node = newNode({ scope });
this.add(node);
this.stack.push(node);
}
closeNode() {
if (this.stack.length > 1) {
return this.stack.pop();
}
// eslint-disable-next-line no-undefined
return undefined;
}
closeAllNodes() {
while (this.closeNode());
}
toJSON() {
return JSON.stringify(this.rootNode, null, 4);
}
/**
* @typedef { import("./html_renderer").Renderer } Renderer
* @param {Renderer} builder
*/
walk(builder) {
// this does not
return this.constructor._walk(builder, this.rootNode);
// this works
// return TokenTree._walk(builder, this.rootNode);
}
/**
* @param {Renderer} builder
* @param {Node} node
*/
static _walk(builder, node) {
if (typeof node === 'string') {
builder.addText(node);
} else if (node.children) {
builder.openNode(node);
node.children.forEach((child) => this._walk(builder, child));
builder.closeNode(node);
}
return builder;
}
/**
* @param {Node} node
*/
static _collapse(node) {
if (typeof node === 'string') return;
if (!node.children) return;
if (node.children.every((el) => typeof el === 'string')) {
// node.text = node.children.join("");
// delete node.children;
node.children = [node.children.join('')];
} else {
node.children.forEach((child) => {
TokenTree._collapse(child);
});
}
}
}
/**
Currently this is all private API, but this is the minimal API necessary
that an Emitter must implement to fully support the parser.
Minimal interface:
- addText(text)
- __addSublanguage(emitter, subLanguageName)
- startScope(scope)
- endScope()
- finalize()
- toHTML()
*/
/**
* @implements {Emitter}
*/
class TokenTreeEmitter extends TokenTree {
/**
* @param {*} options
*/
constructor(options) {
super();
this.options = options;
}
/**
* @param {string} text
*/
addText(text) {
if (text === '') {
return;
}
this.add(text);
}
/** @param {string} scope */
startScope(scope) {
this.openNode(scope);
}
endScope() {
this.closeNode();
}
/**
* @param {Emitter & {root: DataNode}} emitter
* @param {string} name
*/
__addSublanguage(emitter, name) {
/** @type DataNode */
const node = emitter.root;
if (name) node.scope = `language:${name}`;
this.add(node);
}
toHTML() {
const renderer = new HTMLRenderer(this, this.options);
return renderer.value();
}
finalize() {
this.closeAllNodes();
return true;
}
}
/**
* @param {string} value
* @returns {RegExp}
* */
/**
* @param {RegExp | string } re
* @returns {string}
*/
function source(re) {
if (!re) return null;
if (typeof re === 'string') return re;
return re.source;
}
/**
* @param {RegExp | string } re
* @returns {string}
*/
function lookahead(re) {
return concat('(?=', re, ')');
}
/**
* @param {RegExp | string } re
* @returns {string}
*/
function anyNumberOfTimes(re) {
return concat('(?:', re, ')*');
}
/**
* @param {RegExp | string } re
* @returns {string}
*/
function optional(re) {
return concat('(?:', re, ')?');
}
/**
* @param {...(RegExp | string) } args
* @returns {string}
*/
function concat(...args) {
const joined = args.map((x) => source(x)).join('');
return joined;
}
/**
* @param { Array<string | RegExp | Object> } args
* @returns {object}
*/
function stripOptionsFromArgs(args) {
const opts = args[args.length - 1];
if (typeof opts === 'object' && opts.constructor === Object) {
args.splice(args.length - 1, 1);
return opts;
} else {
return {};
}
}
/** @typedef { {capture?: boolean} } RegexEitherOptions */
/**
* Any of the passed expresssions may match
*
* Creates a huge this | this | that | that match
* @param {(RegExp | string)[] | [...(RegExp | string)[], RegexEitherOptions]} args
* @returns {string}
*/
function either(...args) {
/** @type { object & {capture?: boolean} } */
const opts = stripOptionsFromArgs(args);
const joined =
'(' + (opts.capture ? '' : '?:') + args.map((x) => source(x)).join('|') + ')';
return joined;
}
/**
* @param {RegExp | string} re
* @returns {number}
*/
function countMatchGroups(re) {
return new RegExp(re.toString() + '|').exec('').length - 1;
}
/**
* Does lexeme start with a regular expression match at the beginning
* @param {RegExp} re
* @param {string} lexeme
*/
function startsWith(re, lexeme) {
const match = re && re.exec(lexeme);
return match && match.index === 0;
}
// BACKREF_RE matches an open parenthesis or backreference. To avoid
// an incorrect parse, it additionally matches the following:
// - [...] elements, where the meaning of parentheses and escapes change
// - other escape sequences, so we do not misparse escape sequences as
// interesting elements
// - non-matching or lookahead parentheses, which do not capture. These
// follow the '(' with a '?'.
const BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
// **INTERNAL** Not intended for outside usage
// join logically computes regexps.join(separator), but fixes the
// backreferences so they continue to match.
// it also places each individual regular expression into it's own
// match group, keeping track of the sequencing of those match groups
// is currently an exercise for the caller. :-)
/**
* @param {(string | RegExp)[]} regexps
* @param {{joinWith: string}} opts
* @returns {string}
*/
function _rewriteBackreferences(regexps, { joinWith }) {
let numCaptures = 0;
return regexps
.map((regex) => {
numCaptures += 1;
const offset = numCaptures;
let re = source(regex);
let out = '';
while (re.length > 0) {
const match = BACKREF_RE.exec(re);
if (!match) {
out += re;
break;
}
out += re.substring(0, match.index);
re = re.substring(match.index + match[0].length);
if (match[0][0] === '\\' && match[1]) {
// Adjust the backreference.
out += '\\' + String(Number(match[1]) + offset);
} else {
out += match[0];
if (match[0] === '(') {
numCaptures++;
}
}
}
return out;
})
.map((re) => `(${re})`)
.join(joinWith);
}
/** @typedef {import('highlight.js').Mode} Mode */
/** @typedef {import('highlight.js').ModeCallback} ModeCallback */
// Common regexps
const MATCH_NOTHING_RE = /\b\B/;
const IDENT_RE = '[a-zA-Z]\\w*';
const UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*';
const NUMBER_RE = '\\b\\d+(\\.\\d+)?';
const C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float
const BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b...
const RE_STARTERS_RE =
'!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~';
/**
* @param { Partial<Mode> & {binary?: string | RegExp} } opts
*/
const SHEBANG = (opts = {}) => {
const beginShebang = /^#![ ]*\//;
if (opts.binary) {
opts.begin = concat(beginShebang, /.*\b/, opts.binary, /\b.*/);
}
return inherit$1(
{
scope: 'meta',
begin: beginShebang,
end: /$/,
relevance: 0,
/** @type {ModeCallback} */
'on:begin': (m, resp) => {
if (m.index !== 0) resp.ignoreMatch();
},
},
opts
);
};
// Common modes
const BACKSLASH_ESCAPE = {
begin: '\\\\[\\s\\S]',
relevance: 0,
};
const APOS_STRING_MODE = {
scope: 'string',
begin: "'",
end: "'",
illegal: '\\n',
contains: [BACKSLASH_ESCAPE],
};
const QUOTE_STRING_MODE = {
scope: 'string',
begin: '"',
end: '"',
illegal: '\\n',
contains: [BACKSLASH_ESCAPE],
};
const PHRASAL_WORDS_MODE = {
begin:
/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/,
};
/**
* Creates a comment mode
*
* @param {string | RegExp} begin
* @param {string | RegExp} end
* @param {Mode | {}} [modeOptions]
* @returns {Partial<Mode>}
*/
const COMMENT = function (begin, end, modeOptions = {}) {
const mode = inherit$1(
{
scope: 'comment',
begin,
end,
contains: [],
},
modeOptions
);
mode.contains.push({
scope: 'doctag',
// hack to avoid the space from being included. the space is necessary to
// match here to prevent the plain text rule below from gobbling up doctags
begin: '[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)',
end: /(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,
excludeBegin: true,
relevance: 0,
});
const ENGLISH_WORD = either(
// list of common 1 and 2 letter words in English
'I',
'a',
'is',
'so',
'us',
'to',
'at',
'if',
'in',
'it',
'on',
// note: this is not an exhaustive list of contractions, just popular ones
/[A-Za-z]+['](d|ve|re|ll|t|s|n)/, // contractions - can't we'd they're let's, etc
/[A-Za-z]+[-][a-z]+/, // `no-way`, etc.
/[A-Za-z][a-z]{2,}/ // allow capitalized words at beginning of sentences
);
// looking like plain text, more likely to be a comment
mode.contains.push({
// TODO: how to include ", (, ) without breaking grammars that use these for
// comment delimiters?
// begin: /[ ]+([()"]?([A-Za-z'-]{3,}|is|a|I|so|us|[tT][oO]|at|if|in|it|on)[.]?[()":]?([.][ ]|[ ]|\))){3}/
// ---
// this tries to find sequences of 3 english words in a row (without any
// "programming" type syntax) this gives us a strong signal that we've
// TRULY found a comment - vs perhaps scanning with the wrong language.
// It's possible to find something that LOOKS like the start of the
// comment - but then if there is no readable text - good chance it is a
// false match and not a comment.
//
// for a visual example please see:
// https://github.com/highlightjs/highlight.js/issues/2827
begin: concat(
/[ ]+/, // necessary to prevent us gobbling up doctags like /* @author Bob Mcgill */
'(',
ENGLISH_WORD,
/[.]?[:]?([.][ ]|[ ])/,
'){3}'
), // look for 3 words in a row
});
return mode;
};
const C_LINE_COMMENT_MODE = COMMENT('//', '$');
const C_BLOCK_COMMENT_MODE = COMMENT('/\\*', '\\*/');
const HASH_COMMENT_MODE = COMMENT('#', '$');
const NUMBER_MODE = {
scope: 'number',
begin: NUMBER_RE,
relevance: 0,
};
const C_NUMBER_MODE = {
scope: 'number',
begin: C_NUMBER_RE,
relevance: 0,
};
const BINARY_NUMBER_MODE = {
scope: 'number',
begin: BINARY_NUMBER_RE,
relevance: 0,
};
const REGEXP_MODE = {
scope: 'regexp',
begin: /\/(?=[^/\n]*\/)/,
end: /\/[gimuy]*/,
contains: [
BACKSLASH_ESCAPE,
{
begin: /\[/,
end: /\]/,
relevance: 0,
contains: [BACKSLASH_ESCAPE],
},
],
};
const TITLE_MODE = {
scope: 'title',
begin: IDENT_RE,
relevance: 0,
};
const UNDERSCORE_TITLE_MODE = {
scope: 'title',
begin: UNDERSCORE_IDENT_RE,
relevance: 0,
};
const METHOD_GUARD = {
// excludes method names from keyword processing
begin: '\\.\\s*' + UNDERSCORE_IDENT_RE,
relevance: 0,
};
/**
* Adds end same as begin mechanics to a mode
*
* Your mode must include at least a single () match group as that first match
* group is what is used for comparison
* @param {Partial<Mode>} mode
*/
const END_SAME_AS_BEGIN = function (mode) {
return Object.assign(mode, {
/** @type {ModeCallback} */
'on:begin': (m, resp) => {
resp.data._beginMatch = m[1];
},
/** @type {ModeCallback} */
'on:end': (m, resp) => {
if (resp.data._beginMatch !== m[1]) resp.ignoreMatch();
},
});
};
var MODES = /*#__PURE__*/ Object.freeze({
__proto__: null,
APOS_STRING_MODE: APOS_STRING_MODE,
BACKSLASH_ESCAPE: BACKSLASH_ESCAPE,
BINARY_NUMBER_MODE: BINARY_NUMBER_MODE,
BINARY_NUMBER_RE: BINARY_NUMBER_RE,
COMMENT: COMMENT,
C_BLOCK_COMMENT_MODE: C_BLOCK_COMMENT_MODE,
C_LINE_COMMENT_MODE: C_LINE_COMMENT_MODE,
C_NUMBER_MODE: C_NUMBER_MODE,
C_NUMBER_RE: C_NUMBER_RE,
END_SAME_AS_BEGIN: END_SAME_AS_BEGIN,
HASH_COMMENT_MODE: HASH_COMMENT_MODE,
IDENT_RE: IDENT_RE,
MATCH_NOTHING_RE: MATCH_NOTHING_RE,
METHOD_GUARD: METHOD_GUARD,
NUMBER_MODE: NUMBER_MODE,
NUMBER_RE: NUMBER_RE,
PHRASAL_WORDS_MODE: PHRASAL_WORDS_MODE,
QUOTE_STRING_MODE: QUOTE_STRING_MODE,
REGEXP_MODE: REGEXP_MODE,
RE_STARTERS_RE: RE_STARTERS_RE,
SHEBANG: SHEBANG,
TITLE_MODE: TITLE_MODE,
UNDERSCORE_IDENT_RE: UNDERSCORE_IDENT_RE,
UNDERSCORE_TITLE_MODE: UNDERSCORE_TITLE_MODE,
});
/**
@typedef {import('highlight.js').CallbackResponse} CallbackResponse
@typedef {import('highlight.js').CompilerExt} CompilerExt
*/
// Grammar extensions / plugins
// See: https://github.com/highlightjs/highlight.js/issues/2833
// Grammar extensions allow "syntactic sugar" to be added to the grammar modes
// without requiring any underlying changes to the compiler internals.
// `compileMatch` being the perfect small example of now allowing a grammar
// author to write `match` when they desire to match a single expression rather
// than being forced to use `begin`. The extension then just moves `match` into
// `begin` when it runs. Ie, no features have been added, but we've just made
// the experience of writing (and reading grammars) a little bit nicer.
// ------
// TODO: We need negative look-behind support to do this properly
/**
* Skip a match if it has a preceding dot
*
* This is used for `beginKeywords` to prevent matching expressions such as
* `bob.keyword.do()`. The mode compiler automatically wires this up as a
* special _internal_ 'on:begin' callback for modes with `beginKeywords`
* @param {RegExpMatchArray} match
* @param {CallbackResponse} response
*/
function skipIfHasPrecedingDot(match, response) {
const before = match.input[match.index - 1];
if (before === '.') {
response.ignoreMatch();
}
}
/**
*
* @type {CompilerExt}
*/
function scopeClassName(mode, _parent) {
// eslint-disable-next-line no-undefined
if (mode.className !== undefined) {
mode.scope = mode.className;
delete mode.className;
}
}
/**
* `beginKeywords` syntactic sugar
* @type {CompilerExt}
*/
function beginKeywords(mode, parent) {
if (!parent) return;
if (!mode.beginKeywords) return;
// for languages with keywords that include non-word characters checking for
// a word boundary is not sufficient, so instead we check for a word boundary
// or whitespace - this does no harm in any case since our keyword engine
// doesn't allow spaces in keywords anyways and we still check for the boundary
// first
mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')(?!\\.)(?=\\b|\\s)';
mode.__beforeBegin = skipIfHasPrecedingDot;
mode.keywords = mode.keywords || mode.beginKeywords;
delete mode.beginKeywords;
// prevents double relevance, the keywords themselves provide
// relevance, the mode doesn't need to double it
// eslint-disable-next-line no-undefined
if (mode.relevance === undefined) mode.relevance = 0;
}
/**
* Allow `illegal` to contain an array of illegal values
* @type {CompilerExt}
*/
function compileIllegal(mode, _parent) {
if (!Array.isArray(mode.illegal)) return;
mode.illegal = either(...mode.illegal);
}
/**
* `match` to match a single expression for readability
* @type {CompilerExt}
*/
function compileMatch(mode, _parent) {
if (!mode.match) return;
if (mode.begin || mode.end) throw new Error('begin & end are not supported with match');
mode.begin = mode.match;
delete mode.match;
}
/**
* provides the default 1 relevance to all modes
* @type {CompilerExt}
*/
function compileRelevance(mode, _parent) {
// eslint-disable-next-line no-undefined
if (mode.relevance === undefined) mode.relevance = 1;
}
// allow beforeMatch to act as a "qualifier" for the match
// the full match begin must be [beforeMatch][begin]
const beforeMatchExt = (mode, parent) => {
if (!mode.beforeMatch) return;
// starts conflicts with endsParent which we need to make sure the child
// rule is not matched multiple times
if (mode.starts) throw new Error('beforeMatch cannot be used with starts');
const originalMode = Object.assign({}, mode);
Object.keys(mode).forEach((key) => {
delete mode[key];
});
mode.keywords = originalMode.keywords;
mode.begin = concat(originalMode.beforeMatch, lookahead(originalMode.begin));
mode.starts = {
relevance: 0,
contains: [Object.assign(originalMode, { endsParent: true })],
};
mode.relevance = 0;
delete originalMode.beforeMatch;
};
// keywords that should have no default relevance value
const COMMON_KEYWORDS = [
'of',
'and',
'for',
'in',
'not',
'or',
'if',
'then',
'parent', // common variable name
'list', // common variable name
'value', // common variable name
];
const DEFAULT_KEYWORD_SCOPE = 'keyword';
/**
* Given raw keywords from a language definition, compile them.
*
* @param {string | Record<string,string|string[]> | Array<string>} rawKeywords
* @param {boolean} caseInsensitive
*/
function compileKeywords(rawKeywords, caseInsensitive, scopeName = DEFAULT_KEYWORD_SCOPE) {
/** @type {import("highlight.js/private").KeywordDict} */
const compiledKeywords = Object.create(null);
// input can be a string of keywords, an array of keywords, or a object with
// named keys representing scopeName (which can then point to a string or array)
if (typeof rawKeywords === 'string') {
compileList(scopeName, rawKeywords.split(' '));
} else if (Array.isArray(rawKeywords)) {
compileList(scopeName, rawKeywords);
} else {
Object.keys(rawKeywords).forEach(function (scopeName) {
// collapse all our objects back into the parent object
Object.assign(
compiledKeywords,
compileKeywords(rawKeywords[scopeName], caseInsensitive, scopeName)
);
});
}
return compiledKeywords;
// ---
/**
* Compiles an individual list of keywords
*
* Ex: "for if when while|5"
*
* @param {string} scopeName
* @param {Array<string>} keywordList
*/
function compileList(scopeName, keywordList) {
if (caseInsensitive) {
keywordList = keywordList.map((x) => x.toLowerCase());
}
keywordList.forEach(function (keyword) {
const pair = keyword.split('|');
compiledKeywords[pair[0]] = [scopeName, scoreForKeyword(pair[0], pair[1])];
});
}
}
/**
* Returns the proper score for a given keyword
*
* Also takes into account comment keywords, which will be scored 0 UNLESS
* another score has been manually assigned.
* @param {string} keyword
* @param {string} [providedScore]
*/
function scoreForKeyword(keyword, providedScore) {
// manual scores always win over common keywords
// so you can force a score of 1 if you really insist
if (providedScore) {
return Number(providedScore);
}
return commonKeyword(keyword) ? 0 : 1;
}
/**
* Determines if a given keyword is common or not
*
* @param {string} keyword */
function commonKeyword(keyword) {
return COMMON_KEYWORDS.includes(keyword.toLowerCase());
}
/*
For the reasoning behind this please see:
https://github.com/highlightjs/highlight.js/issues/2880#issuecomment-747275419
*/
/**
* @type {Record<string, boolean>}
*/
const seenDeprecations = {};
/**
* @param {string} message
*/
const error = (message) => {
console.error(message);
};
/**
* @param {string} message
* @param {any} args
*/
const warn = (message, ...args) => {
console.log(`WARN: ${message}`, ...args);
};
/**
* @param {string} version
* @param {string} message
*/
const deprecated = (version, message) => {
if (seenDeprecations[`${version}/${message}`]) return;
console.log(`Deprecated as of ${version}. ${message}`);
seenDeprecations[`${version}/${message}`] = true;
};
/* eslint-disable no-throw-literal */
/**
@typedef {import('highlight.js').CompiledMode} CompiledMode
*/
const MultiClassError = new Error();
/**
* Renumbers labeled scope names to account for additional inner match
* groups that otherwise would break everything.
*
* Lets say we 3 match scopes:
*
* { 1 => ..., 2 => ..., 3 => ... }
*
* So what we need is a clean match like this:
*
* (a)(b)(c) => [ "a", "b", "c" ]
*
* But this falls apart with inner match groups:
*
* (a)(((b)))(c) => ["a", "b", "b", "b", "c" ]
*
* Our scopes are now "out of alignment" and we're repeating `b` 3 times.
* What needs to happen is the numbers are remapped:
*
* { 1 => ..., 2 => ..., 5 => ... }
*
* We also need to know that the ONLY groups that should be output
* are 1, 2, and 5. This function handles this behavior.
*
* @param {CompiledMode} mode
* @param {Array<RegExp | string>} regexes
* @param {{key: "beginScope"|"endScope"}} opts
*/
function remapScopeNames(mode, regexes, { key }) {
let offset = 0;
const scopeNames = mode[key];
/** @type Record<number,boolean> */
const emit = {};
/** @type Record<number,string> */
const positions = {};
for (let i = 1; i <= regexes.length; i++) {
positions[i + offset] = scopeNames[i];
emit[i + offset] = true;
offset += countMatchGroups(regexes[i - 1]);
}
// we use _emit to keep track of which match groups are "top-level" to avoid double
// output from inside match groups
mode[key] = positions;
mode[key]._emit = emit;
mode[key]._multi = true;
}
/**
* @param {CompiledMode} mode
*/
function beginMultiClass(mode) {
if (!Array.isArray(mode.begin)) return;
if (mode.skip || mode.excludeBegin || mode.returnBegin) {
error('skip, excludeBegin, returnBegin not compatible with beginScope: {}');
throw MultiClassError;
}
if (typeof mode.beginScope !== 'object' || mode.beginScope === null) {
error('beginScope must be object');
throw MultiClassErr