aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2023-08-23 22:59:02 +0100
committerHayleigh Thompson <me@hayleigh.dev>2023-08-23 22:59:02 +0100
commit21d59712dd56909293b3804b1d89320216f6906a (patch)
tree06300d54c0e5c2804f80167967c40f5371011401
parent83962575c255cc3b58677d18262a1d7b18e8239d (diff)
downloadlustre-21d59712dd56909293b3804b1d89320216f6906a.tar.gz
lustre-21d59712dd56909293b3804b1d89320216f6906a.zip
:ambulance: Update third-party lustre_animation package to new api.
-rw-r--r--compat/lustre_animation/.gitignore4
-rw-r--r--compat/lustre_animation/README.md52
-rw-r--r--compat/lustre_animation/gleam.toml14
-rw-r--r--compat/lustre_animation/highlight.js5008
-rw-r--r--compat/lustre_animation/index.html95
-rw-r--r--compat/lustre_animation/lustre_animation.css90
-rw-r--r--compat/lustre_animation/manifest.toml13
-rw-r--r--compat/lustre_animation/package-lock.json790
-rw-r--r--compat/lustre_animation/package.json9
-rw-r--r--compat/lustre_animation/run_highlight.js3
-rw-r--r--compat/lustre_animation/src/ffi.mjs2
-rw-r--r--compat/lustre_animation/src/lustre/animation.gleam170
-rw-r--r--compat/lustre_animation/test/example_drops.gleam120
-rw-r--r--compat/lustre_animation/test/example_two_independent.gleam103
-rw-r--r--compat/lustre_animation/test/info.mjs2
-rw-r--r--compat/lustre_animation/test/lustre/animation_test.gleam39
-rw-r--r--compat/lustre_animation/test/lustre_animation_test.gleam5
-rw-r--r--compat/lustre_animation/test/start_examples.mjs5
18 files changed, 6524 insertions, 0 deletions
diff --git a/compat/lustre_animation/.gitignore b/compat/lustre_animation/.gitignore
new file mode 100644
index 0000000..be616fc
--- /dev/null
+++ b/compat/lustre_animation/.gitignore
@@ -0,0 +1,4 @@
+build
+node_modules
+dist/main.js
+dist/assets/
diff --git a/compat/lustre_animation/README.md b/compat/lustre_animation/README.md
new file mode 100644
index 0000000..338e9c1
--- /dev/null
+++ b/compat/lustre_animation/README.md
@@ -0,0 +1,52 @@
+# Lustre Animation
+
+The basic usage is
+ * keep 1 `Animations` value
+ * add animations by some trigger
+ * call `animation.cmd()` on your value and let your lustre `update()` return it.
+ This will cause a dispatch on the next JS animation frame, unless all animations have finished (and auto-removed)
+ * update the animations with the new time.
+ * evaluate for each animation you are interested in.
+
+e.g. like this:
+```gleam
+import lustre/animation as a
+
+pub type Msg {
+ Trigger
+ Tick(Float)
+}
+
+pub fn update(model: Model, msg: Msg) {
+ let m = case msg of {
+ Trigger -> {
+ let new_anim = a.add(model.animations, "id", 1.0, 2.0, 0.250)
+ Model(1.0, animations: new_anim)
+ }
+ Tick(t) -> {
+ let new_anim = a.tick(model.animations, t)
+ let new_value = a.value(new_anim, "id", model.value)
+ Model(new_value, new_anim)
+ }
+ }
+ #(m, animation.cmd(m.animations, Tick))
+}
+```
+
+In the above `type Model`, `init` and `render` have been omitted.
+
+There are fully functional examples animations in the `test/` directory,
+which you can build by
+```bash
+gleam test
+npx vite
+````
+
+and then pointing your browser to the URL that vite indicates.
+
+## TODO
+* `every(Minute)`, etc
+* `after(seconds: 2.5)`
+* quadratic, cubic, etc interpolators
+* Compose animations
+ `from("anim", 1.0) |> after(0.250) |> cubic(3.0, 0.250) |> after(0.250) |> quadr(2.0, 0.250)` \ No newline at end of file
diff --git a/compat/lustre_animation/gleam.toml b/compat/lustre_animation/gleam.toml
new file mode 100644
index 0000000..182203e
--- /dev/null
+++ b/compat/lustre_animation/gleam.toml
@@ -0,0 +1,14 @@
+name = "lustre_animation"
+version = "0.2.0"
+description = "Animations for lustre, utilizing JS requestAnimationFrame"
+target = "javascript"
+licenses = ["MIT"]
+repository = { type = "custom", url = "https://git.chmeee.org/lustre_animation" }
+
+[dependencies]
+gleam_stdlib = "~> 0.30"
+lustre = { path = "../../lib" }
+
+
+[dev-dependencies]
+gleeunit = "~> 0.10"
diff --git a/compat/lustre_animation/highlight.js b/compat/lustre_animation/highlight.js
new file mode 100644
index 0000000..aa0170b
--- /dev/null
+++ b/compat/lustre_animation/highlight.js
@@ -0,0 +1,5008 @@
+/*!
+ Highlight.js v11.7.0 (git: 2f3d5ff18e)
+ (c) 2006-2023 undefined and other contributors
+ License: BSD-3-Clause
+ */
+var hljs = (function () {
+ 'use strict';
+
+ var deepFreezeEs6 = {exports: {}};
+
+ 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(function (name) {
+ var prop = obj[name];
+
+ // Freeze prop if it is an object
+ if (typeof prop == 'object' && !Object.isFrozen(prop)) {
+ deepFreeze(prop);
+ }
+ });
+
+ return obj;
+ }
+
+ deepFreezeEs6.exports = deepFreeze;
+ deepFreezeEs6.exports.default = deepFreeze;
+
+ /** @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, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#x27;');
+ }
+
+ /**
+ * 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 || (node.sublanguage && node.language);
+ };
+
+ /**
+ *
+ * @param {string} name
+ * @param {{prefix:string}} options
+ */
+ const scopeToCSSClass = (name, { prefix }) => {
+ if (name.includes(".")) {
+ const pieces = name.split(".");
+ return [
+ `${prefix}${pieces.shift()}`,
+ ...(pieces.map((x, i) => `${x}${"_".repeat(i + 1)}`))
+ ].join(" ");
+ }
+ 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;
+
+ let className = "";
+ if (node.sublanguage) {
+ className = `language-${node.language}`;
+ } else {
+ 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, sublanguage?: boolean, children: Node[]} | string} Node */
+ /** @typedef {{scope?: string, language?: string, sublanguage?: boolean, 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:
+
+ - addKeyword(text, scope)
+ - addText(text)
+ - addSublanguage(emitter, subLanguageName)
+ - finalize()
+ - openNode(scope)
+ - closeNode()
+ - closeAllNodes()
+ - toHTML()
+
+ */
+
+ /**
+ * @implements {Emitter}
+ */
+ class TokenTreeEmitter extends TokenTree {
+ /**
+ * @param {*} options
+ */
+ constructor(options) {
+ super();
+ this.options = options;
+ }
+
+ /**
+ * @param {string} text
+ * @param {string} scope
+ */
+ addKeyword(text, scope) {
+ if (text === "") { return; }
+
+ this.openNode(scope);
+ this.addText(text);
+ this.closeNode();
+ }
+
+ /**
+ * @param {string} text
+ */
+ addText(text) {
+ if (text === "") { return; }
+
+ this.add(text);
+ }
+
+ /**
+ * @param {Emitter & {root: DataNode}} emitter
+ * @param {string} name
+ */
+ addSublanguage(emitter, name) {
+ /** @type DataNode */
+ const node = emitter.root;
+ node.sublanguage = true;
+ node.language = name;
+ this.add(node);
+ }
+
+ toHTML() {
+ const renderer = new HTMLRenderer(this, this.options);
+ return renderer.value();
+ }
+
+ finalize() {
+ 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$1 = '[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 = {
+ // this outer rule makes sure we actually have a WHOLE regex and not simply
+ // an expression such as:
+ //
+ // 3 / something
+ //
+ // (which will then blow up when regex's `illegal` sees the newline)
+ begin: /(?=\/[^/\n]*\/)/,
+ contains: [{
+ scope: 'regexp',
+ begin: /\//,
+ end: /\/[gimuy]*/,
+ illegal: /\n/,
+ contains: [
+ BACKSLASH_ESCAPE,
+ {
+ begin: /\[/,
+ end: /\]/,
+ relevance: 0,
+ contains: [BACKSLASH_ESCAPE]
+ }
+ ]
+ }]
+ };
+ const TITLE_MODE = {
+ scope: 'title',
+ begin: IDENT_RE$1,
+ 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$1 = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ MATCH_NOTHING_RE: MATCH_NOTHING_RE,
+ IDENT_RE: IDENT_RE$1,
+ UNDERSCORE_IDENT_RE: UNDERSCORE_IDENT_RE,
+ NUMBER_RE: NUMBER_RE,
+ C_NUMBER_RE: C_NUMBER_RE,
+ BINARY_NUMBER_RE: BINARY_NUMBER_RE,
+ RE_STARTERS_RE: RE_STARTERS_RE,
+ SHEBANG: SHEBANG,
+ BACKSLASH_ESCAPE: BACKSLASH_ESCAPE,
+ APOS_STRING_MODE: APOS_STRING_MODE,
+ QUOTE_STRING_MODE: QUOTE_STRING_MODE,
+ PHRASAL_WORDS_MODE: PHRASAL_WORDS_MODE,
+ COMMENT: COMMENT,
+ C_LINE_COMMENT_MODE: C_LINE_COMMENT_MODE,
+ C_BLOCK_COMMENT_MODE: C_BLOCK_COMMENT_MODE,
+ HASH_COMMENT_MODE: HASH_COMMENT_MODE,
+ NUMBER_MODE: NUMBER_MODE,
+ C_NUMBER_MODE: C_NUMBER_MODE,
+ BINARY_NUMBER_MODE: BINARY_NUMBER_MODE,
+ REGEXP_MODE: REGEXP_MODE,
+ TITLE_MODE: TITLE_MODE,
+ UNDERSCORE_TITLE_MODE: UNDERSCORE_TITLE_MODE,
+ METHOD_GUARD: METHOD_GUARD,
+ END_SAME_AS_BEGIN: END_SAME_AS_BEGIN
+ });
+
+ /**
+ @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 MultiClassError;
+ }
+
+ remapScopeNames(mode, mode.begin, { key: "beginScope" });
+ mode.begin = _rewriteBackreferences(mode.begin, { joinWith: "" });
+ }
+
+ /**
+ * @param {CompiledMode} mode
+ */
+ function endMultiClass(mode) {
+ if (!Array.isArray(mode.end)) return;
+
+ if (mode.skip || mode.excludeEnd || mode.returnEnd) {
+ error("skip, excludeEnd, returnEnd not compatible with endScope: {}");
+ throw MultiClassError;
+ }
+
+ if (typeof mode.endScope !== "object" || mode.endScope === null) {
+ error("endScope must be object");
+ throw MultiClassError;
+ }
+
+ remapScopeNames(mode, mode.end, { key: "endScope" });
+ mode.end = _rewriteBackreferences(mode.end, { joinWith: "" });
+ }
+
+ /**
+ * this exists only to allow `scope: {}` to be used beside `match:`
+ * Otherwise `beginScope` would necessary and that would look weird
+
+ {
+ match: [ /def/, /\w+/ ]
+ scope: { 1: "keyword" , 2: "title" }
+ }
+
+ * @param {CompiledMode} mode
+ */
+ function scopeSugar(mode) {
+ if (mode.scope && typeof mode.scope === "object" && mode.scope !== null) {
+ mode.beginScope = mode.scope;
+ delete mode.scope;
+ }
+ }
+
+ /**
+ * @param {CompiledMode} mode
+ */
+ function MultiClass(mode) {
+ scopeSugar(mode);
+
+ if (typeof mode.beginScope === "string") {
+ mode.beginScope = { _wrap: mode.beginScope };
+ }
+ if (typeof mode.endScope === "string") {
+ mode.endScope = { _wrap: mode.endScope };
+ }
+
+ beginMultiClass(mode);
+ endMultiClass(mode);
+ }
+
+ /**
+ @typedef {import('highlight.js').Mode} Mode
+ @typedef {import('highlight.js').CompiledMode} CompiledMode
+ @typedef {import('highlight.js').Language} Language
+ @typedef {import('highlight.js').HLJSPlugin} HLJSPlugin
+ @typedef {import('highlight.js').CompiledLanguage} CompiledLanguage
+ */
+
+ // compilation
+
+ /**
+ * Compiles a language definition result
+ *
+ * Given the raw result of a language definition (Language), compiles this so
+ * that it is ready for highlighting code.
+ * @param {Language} language
+ * @returns {CompiledLanguage}
+ */
+ function compileLanguage(language) {
+ /**
+ * Builds a regex with the case sensitivity of the current language
+ *
+ * @param {RegExp | string} value
+ * @param {boolean} [global]
+ */
+ function langRe(value, global) {
+ return new RegExp(
+ source(value),
+ 'm'
+ + (language.case_insensitive ? 'i' : '')
+ + (language.unicodeRegex ? 'u' : '')
+ + (global ? 'g' : '')
+ );
+ }
+
+ /**
+ Stores multiple regular expressions and allows you to quickly search for
+ them all in a string simultaneously - returning the first match. It does
+ this by creating a huge (a|b|c) regex - each individual item wrapped with ()
+ and joined by `|` - using match groups to track position. When a match is
+ found checking which position in the array has content allows us to figure
+ out which of the original regexes / match groups triggered the match.
+
+ The match object itself (the result of `Regex.exec`) is returned but also
+ enhanced by merging in any meta-data that was registered with the regex.
+ This is how we keep track of which mode matched, and what type of rule
+ (`illegal`, `begin`, end, etc).
+ */
+ class MultiRegex {
+ constructor() {
+ this.matchIndexes = {};
+ // @ts-ignore
+ this.regexes = [];
+ this.matchAt = 1;
+ this.position = 0;
+ }
+
+ // @ts-ignore
+ addRule(re, opts) {
+ opts.position = this.position++;
+ // @ts-ignore
+ this.matchIndexes[this.matchAt] = opts;
+ this.regexes.push([opts, re]);
+ this.matchAt += countMatchGroups(re) + 1;
+ }
+
+ compile() {
+ if (this.regexes.length === 0) {
+ // avoids the need to check length every time exec is called
+ // @ts-ignore
+ this.exec = () => null;
+ }
+ const terminators = this.regexes.map(el => el[1]);
+ this.matcherRe = langRe(_rewriteBackreferences(terminators, { joinWith: '|' }), true);
+ this.lastIndex = 0;
+ }
+
+ /** @param {string} s */
+ exec(s) {
+ this.matcherRe.lastIndex = this.lastIndex;
+ const match = this.matcherRe.exec(s);
+ if (!match) { return null; }
+
+ // eslint-disable-next-line no-undefined
+ const i = match.findIndex((el, i) => i > 0 && el !== undefined);
+ // @ts-ignore
+ const matchData = this.matchIndexes[i];
+ // trim off any earlier non-relevant match groups (ie, the other regex
+ // match groups that make up the multi-matcher)
+ match.splice(0, i);
+
+ return Object.assign(match, matchData);
+ }
+ }
+
+ /*
+ Created to solve the key deficiently with MultiRegex - there is no way to
+ test for multiple matches at a single location. Why would we need to do
+ that? In the future a more dynamic engine will allow certain matches to be
+ ignored. An example: if we matched say the 3rd regex in a large group but
+ decided to ignore it - we'd need to started testing again at the 4th
+ regex... but MultiRegex itself gives us no real way to do that.
+
+ So what this class creates MultiRegexs on the fly for whatever search
+ position they are needed.
+
+ NOTE: These additional MultiRegex objects are created dynamically. For most
+ grammars most of the time we will never actually need anything more than the
+ first MultiRegex - so this shouldn't have too much overhead.
+
+ Say this is our search group, and we match regex3, but wish to ignore it.
+
+ regex1 | regex2 | regex3 | regex4 | regex5 ' ie, startAt = 0
+
+ What we need is a new MultiRegex that only includes the remaining
+ possibilities:
+
+ regex4 | regex5 ' ie, startAt = 3
+
+ This class wraps all that complexity up in a simple API... `startAt` decides
+ where in the array of expressions to start doing the matching. It
+ auto-increments, so if a match is found at position 2, then startAt will be
+ set to 3. If the end is reached startAt will return to 0.
+
+ MOST of the time the parser will be setting startAt manually to 0.
+ */
+ class ResumableMultiRegex {
+ constructor() {
+ // @ts-ignore
+ this.rules = [];
+ // @ts-ignore
+ this.multiRegexes = [];
+ this.count = 0;
+
+ this.lastIndex = 0;
+ this.regexIndex = 0;
+ }
+
+ // @ts-ignore
+ getMatcher(index) {
+ if (this.multiRegexes[index]) return this.multiRegexes[index];
+
+ const matcher = new MultiRegex();
+ this.rules.slice(index).forEach(([re, opts]) => matcher.addRule(re, opts));
+ matcher.compile();
+ this.multiRegexes[index] = matcher;
+ return matcher;
+ }
+
+ resumingScanAtSamePosition() {
+ return this.regexIndex !== 0;
+ }
+
+ considerAll() {
+ this.regexIndex = 0;
+ }
+
+ // @ts-ignore
+ addRule(re, opts) {
+ this.rules.push([re, opts]);
+ if (opts.type === "begin") this.count++;
+ }
+
+ /** @param {string} s */
+ exec(s) {
+ const m = this.getMatcher(this.regexIndex);
+ m.lastIndex = this.lastIndex;
+ let result = m.exec(s);
+
+ // The following is because we have no easy way to say "resume scanning at the
+ // existing position but also skip the current rule ONLY". What happens is
+ // all prior rules are also skipped which can result in matching the wrong
+ // thing. Example of matching "booger":
+
+ // our matcher is [string, "booger", number]
+ //
+ // ....booger....
+
+ // if "booger" is ignored then we'd really need a regex to scan from the
+ // SAME position for only: [string, number] but ignoring "booger" (if it
+ // was the first match), a simple resume would scan ahead who knows how
+ // far looking only for "number", ignoring potential string matches (or
+ // future "booger" matches that might be valid.)
+
+ // So what we do: We execute two matchers, one resuming at the same
+ // position, but the second full matcher starting at the position after:
+
+ // /--- resume first regex match here (for [number])
+ // |/---- full match here for [string, "booger", number]
+ // vv
+ // ....booger....
+
+ // Which ever results in a match first is then used. So this 3-4 step
+ // process essentially allows us to say "match at this position, excluding
+ // a prior rule that was ignored".
+ //
+ // 1. Match "booger" first, ignore. Also proves that [string] does non match.
+ // 2. Resume matching for [number]
+ // 3. Match at index + 1 for [string, "booger", number]
+ // 4. If #2 and #3 result in matches, which came first?
+ if (this.resumingScanAtSamePosition()) {
+ if (result && result.index === this.lastIndex) ; else { // use the second matcher result
+ const m2 = this.getMatcher(0);
+ m2.lastIndex = this.lastIndex + 1;
+ result = m2.exec(s);
+ }
+ }
+
+ if (result) {
+ this.regexIndex += result.position + 1;
+ if (this.regexIndex === this.count) {
+ // wrap-around to considering all matches again
+ this.considerAll();
+ }
+ }
+
+ return result;
+ }
+ }
+
+ /**
+ * Given a mode, builds a huge ResumableMultiRegex that can be used to walk
+ * the content and find matches.
+ *
+ * @param {CompiledMode} mode
+ * @returns {ResumableMultiRegex}
+ */
+ function buildModeRegex(mode) {
+ const mm = new ResumableMultiRegex();
+
+ mode.contains.forEach(term => mm.addRule(term.begin, { rule: term, type: "begin" }));
+
+ if (mode.terminatorEnd) {
+ mm.addRule(mode.terminatorEnd, { type: "end" });
+ }
+ if (mode.illegal) {
+ mm.addRule(mode.illegal, { type: "illegal" });
+ }
+
+ return mm;
+ }
+
+ /** skip vs abort vs ignore
+ *
+ * @skip - The mode is still entered and exited normally (and contains rules apply),
+ * but all content is held and added to the parent buffer rather than being
+ * output when the mode ends. Mostly used with `sublanguage` to build up
+ * a single large buffer than can be parsed by sublanguage.
+ *
+ * - The mode begin ands ends normally.
+ * - Content matched is added to the parent mode buffer.
+ * - The parser cursor is moved forward normally.
+ *
+ * @abort - A hack placeholder until we have ignore. Aborts the mode (as if it
+ * never matched) but DOES NOT continue to match subsequent `contains`
+ * modes. Abort is bad/suboptimal because it can result in modes
+ * farther down not getting applied because an earlier rule eats the
+ * content but then aborts.
+ *
+ * - The mode does not begin.
+ * - Content matched by `begin` is added to the mode buffer.
+ * - The parser cursor is moved forward accordingly.
+ *
+ * @ignore - Ignores the mode (as if it never matched) and continues to match any
+ * subsequent `contains` modes. Ignore isn't technically possible with
+ * the current parser implementation.
+ *
+ * - The mode does not begin.
+ * - Content matched by `begin` is ignored.
+ * - The parser cursor is not moved forward.
+ */
+
+ /**
+ * Compiles an individual mode
+ *
+ * This can raise an error if the mode contains certain detectable known logic
+ * issues.
+ * @param {Mode} mode
+ * @param {CompiledMode | null} [parent]
+ * @returns {CompiledMode | never}
+ */
+ function compileMode(mode, parent) {
+ const cmode = /** @type CompiledMode */ (mode);
+ if (mode.isCompiled) return cmode;
+
+ [
+ scopeClassName,
+ // do this early so compiler extensions generally don't have to worry about
+ // the distinction between match/begin
+ compileMatch,
+ MultiClass,
+ beforeMatchExt
+ ].forEach(ext => ext(mode, parent));
+
+ language.compilerExtensions.forEach(ext => ext(mode, parent));
+
+ // __beforeBegin is considered private API, internal use only
+ mode.__beforeBegin = null;
+
+ [
+ beginKeywords,
+ // do this later so compiler extensions that come earlier have access to the
+ // raw array if they wanted to perhaps manipulate it, etc.
+ compileIllegal,
+ // default to 1 relevance if not specified
+ compileRelevance
+ ].forEach(ext => ext(mode, parent));
+
+ mode.isCompiled = true;
+
+ let keywordPattern = null;
+ if (typeof mode.keywords === "object" && mode.keywords.$pattern) {
+ // we need a copy because keywords might be compiled multiple times
+ // so we can't go deleting $pattern from the original on the first
+ // pass
+ mode.keywords = Object.assign({}, mode.keywords);
+ keywordPattern = mode.keywords.$pattern;
+ delete mode.keywords.$pattern;
+ }
+ keywordPattern = keywordPattern || /\w+/;
+
+ if (mode.keywords) {
+ mode.keywords = compileKeywords(mode.keywords, language.case_insensitive);
+ }
+
+ cmode.keywordPatternRe = langRe(keywordPattern, true);
+
+ if (parent) {
+ if (!mode.begin) mode.begin = /\B|\b/;
+ cmode.beginRe = langRe(cmode.begin);
+ if (!mode.end && !mode.endsWithParent) mode.end = /\B|\b/;
+ if (mode.end) cmode.endRe = langRe(cmode.end);
+ cmode.terminatorEnd = source(cmode.end) || '';
+ if (mode.endsWithParent && parent.terminatorEnd) {
+ cmode.terminatorEnd += (mode.end ? '|' : '') + parent.terminatorEnd;
+ }
+ }
+ if (mode.illegal) cmode.illegalRe = langRe(/** @type {RegExp | string} */ (mode.illegal));
+ if (!mode.contains) mode.contains = [];
+
+ mode.contains = [].concat(...mode.contains.map(function(c) {
+ return expandOrCloneMode(c === 'self' ? mode : c);
+ }));
+ mode.contains.forEach(function(c) { compileMode(/** @type Mode */ (c), cmode); });
+
+ if (mode.starts) {
+ compileMode(mode.starts, parent);
+ }
+
+ cmode.matcher = buildModeRegex(cmode);
+ return cmode;
+ }
+
+ if (!language.compilerExtensions) language.compilerExtensions = [];
+
+ // self is not valid at the top-level
+ if (language.contains && language.contains.includes('self')) {
+ throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");
+ }
+
+ // we need a null object, which inherit will guarantee
+ language.classNameAliases = inherit$1(language.classNameAliases || {});
+
+ return compileMode(/** @type Mode */ (language));
+ }
+
+ /**
+ * Determines if a mode has a dependency on it's parent or not
+ *
+ * If a mode does have a parent dependency then often we need to clone it if
+ * it's used in multiple places so that each copy points to the correct parent,
+ * where-as modes without a parent can often safely be re-used at the bottom of
+ * a mode chain.
+ *
+ * @param {Mode | null} mode
+ * @returns {boolean} - is there a dependency on the parent?
+ * */
+ function dependencyOnParent(mode) {
+ if (!mode) return false;
+
+ return mode.endsWithParent || dependencyOnParent(mode.starts);
+ }
+
+ /**
+ * Expands a mode or clones it if necessary
+ *
+ * This is necessary for modes with parental dependenceis (see notes on
+ * `dependencyOnParent`) and for nodes that have `variants` - which must then be
+ * exploded into their own individual modes at compile time.
+ *
+ * @param {Mode} mode
+ * @returns {Mode | Mode[]}
+ * */
+ function expandOrCloneMode(mode) {
+ if (mode.variants && !mode.cachedVariants) {
+ mode.cachedVariants = mode.variants.map(function(variant) {
+ return inherit$1(mode, { variants: null }, variant);
+ });
+ }
+
+ // EXPAND
+ // if we have variants then essentially "replace" the mode with the variants
+ // this happens in compileMode, where this function is called from
+ if (mode.cachedVariants) {
+ return mode.cachedVariants;
+ }
+
+ // CLONE
+ // if we have dependencies on parents then we need a unique
+ // instance of ourselves, so we can be reused with many
+ // different parents without issue
+ if (dependencyOnParent(mode)) {
+ return inherit$1(mode, { starts: mode.starts ? inherit$1(mode.starts) : null });
+ }
+
+ if (Object.isFrozen(mode)) {
+ return inherit$1(mode);
+ }
+
+ // no special dependency issues, just return ourselves
+ return mode;
+ }
+
+ var version = "11.7.0";
+
+ class HTMLInjectionError extends Error {
+ constructor(reason, html) {
+ super(reason);
+ this.name = "HTMLInjectionError";
+ this.html = html;
+ }
+ }
+
+ /*
+ Syntax highlighting with language autodetection.
+ https://highlightjs.org/
+ */
+
+ /**
+ @typedef {import('highlight.js').Mode} Mode
+ @typedef {import('highlight.js').CompiledMode} CompiledMode
+ @typedef {import('highlight.js').CompiledScope} CompiledScope
+ @typedef {import('highlight.js').Language} Language
+ @typedef {import('highlight.js').HLJSApi} HLJSApi
+ @typedef {import('highlight.js').HLJSPlugin} HLJSPlugin
+ @typedef {import('highlight.js').PluginEvent} PluginEvent
+ @typedef {import('highlight.js').HLJSOptions} HLJSOptions
+ @typedef {import('highlight.js').LanguageFn} LanguageFn
+ @typedef {import('highlight.js').HighlightedHTMLElement} HighlightedHTMLElement
+ @typedef {import('highlight.js').BeforeHighlightContext} BeforeHighlightContext
+ @typedef {import('highlight.js/private').MatchType} MatchType
+ @typedef {import('highlight.js/private').KeywordData} KeywordData
+ @typedef {import('highlight.js/private').EnhancedMatch} EnhancedMatch
+ @typedef {import('highlight.js/private').AnnotatedError} AnnotatedError
+ @typedef {import('highlight.js').AutoHighlightResult} AutoHighlightResult
+ @typedef {import('highlight.js').HighlightOptions} HighlightOptions
+ @typedef {import('highlight.js').HighlightResult} HighlightResult
+ */
+
+
+ const escape = escapeHTML;
+ const inherit = inherit$1;
+ const NO_MATCH = Symbol("nomatch");
+ const MAX_KEYWORD_HITS = 7;
+
+ /**
+ * @param {any} hljs - object that is extended (legacy)
+ * @returns {HLJSApi}
+ */
+ const HLJS = function(hljs) {
+ // Global internal variables used within the highlight.js library.
+ /** @type {Record<string, Language>} */
+ const languages = Object.create(null);
+ /** @type {Record<string, string>} */
+ const aliases = Object.create(null);
+ /** @type {HLJSPlugin[]} */
+ const plugins = [];
+
+ // safe/production mode - swallows more errors, tries to keep running
+ // even if a single syntax or parse hits a fatal error
+ let SAFE_MODE = true;
+ const LANGUAGE_NOT_FOUND = "Could not find the language '{}', did you forget to load/include a language module?";
+ /** @type {Language} */
+ const PLAINTEXT_LANGUAGE = { disableAutodetect: true, name: 'Plain text', contains: [] };
+
+ // Global options used when within external APIs. This is modified when
+ // calling the `hljs.configure` function.
+ /** @type HLJSOptions */
+ let options = {
+ ignoreUnescapedHTML: false,
+ throwUnescapedHTML: false,
+ noHighlightRe: /^(no-?highlight)$/i,
+ languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i,
+ classPrefix: 'hljs-',
+ cssSelector: 'pre code',
+ languages: null,
+ // beta configuration options, subject to change, welcome to discuss
+ // https://github.com/highlightjs/highlight.js/issues/1086
+ __emitter: TokenTreeEmitter
+ };
+
+ /* Utility functions */
+
+ /**
+ * Tests a language name to see if highlighting should be skipped
+ * @param {string} languageName
+ */
+ function shouldNotHighlight(languageName) {
+ return options.noHighlightRe.test(languageName);
+ }
+
+ /**
+ * @param {HighlightedHTMLElement} block - the HTML element to determine language for
+ */
+ function blockLanguage(block) {
+ let classes = block.className + ' ';
+
+ classes += block.parentNode ? block.parentNode.className : '';
+
+ // language-* takes precedence over non-prefixed class names.
+ const match = options.languageDetectRe.exec(classes);
+ if (match) {
+ const language = getLanguage(match[1]);
+ if (!language) {
+ warn(LANGUAGE_NOT_FOUND.replace("{}", match[1]));
+ warn("Falling back to no-highlight mode for this block.", block);
+ }
+ return language ? match[1] : 'no-highlight';
+ }
+
+ return classes
+ .split(/\s+/)
+ .find((_class) => shouldNotHighlight(_class) || getLanguage(_class));
+ }
+
+ /**
+ * Core highlighting function.
+ *
+ * OLD API
+ * highlight(lang, code, ignoreIllegals, continuation)
+ *
+ * NEW API
+ * highlight(code, {lang, ignoreIllegals})
+ *
+ * @param {string} codeOrLanguageName - the language to use for highlighting
+ * @param {string | HighlightOptions} optionsOrCode - the code to highlight
+ * @param {boolean} [ignoreIllegals] - whether to ignore illegal matches, default is to bail
+ *
+ * @returns {HighlightResult} Result - an object that represents the result
+ * @property {string} language - the language name
+ * @property {number} relevance - the relevance score
+ * @property {string} value - the highlighted HTML code
+ * @property {string} code - the original raw code
+ * @property {CompiledMode} top - top of the current mode stack
+ * @property {boolean} illegal - indicates whether any illegal matches were found
+ */
+ function highlight(codeOrLanguageName, optionsOrCode, ignoreIllegals) {
+ let code = "";
+ let languageName = "";
+ if (typeof optionsOrCode === "object") {
+ code = codeOrLanguageName;
+ ignoreIllegals = optionsOrCode.ignoreIllegals;
+ languageName = optionsOrCode.language;
+ } else {
+ // old API
+ deprecated("10.7.0", "highlight(lang, code, ...args) has been deprecated.");
+ deprecated("10.7.0", "Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277");
+ languageName = codeOrLanguageName;
+ code = optionsOrCode;
+ }
+
+ // https://github.com/highlightjs/highlight.js/issues/3149
+ // eslint-disable-next-line no-undefined
+ if (ignoreIllegals === undefined) { ignoreIllegals = true; }
+
+ /** @type {BeforeHighlightContext} */
+ const context = {
+ code,
+ language: languageName
+ };
+ // the plugin can change the desired language or the code to be highlighted
+ // just be changing the object it was passed
+ fire("before:highlight", context);
+
+ // a before plugin can usurp the result completely by providing it's own
+ // in which case we don't even need to call highlight
+ const result = context.result
+ ? context.result
+ : _highlight(context.language, context.code, ignoreIllegals);
+
+ result.code = context.code;
+ // the plugin can change anything in result to suite it
+ fire("after:highlight", result);
+
+ return result;
+ }
+
+ /**
+ * private highlight that's used internally and does not fire callbacks
+ *
+ * @param {string} languageName - the language to use for highlighting
+ * @param {string} codeToHighlight - the code to highlight
+ * @param {boolean?} [ignoreIllegals] - whether to ignore illegal matches, default is to bail
+ * @param {CompiledMode?} [continuation] - current continuation mode, if any
+ * @returns {HighlightResult} - result of the highlight operation
+ */
+ function _highlight(languageName, codeToHighlight, ignoreIllegals, continuation) {
+ const keywordHits = Object.create(null);
+
+ /**
+ * Return keyword data if a match is a keyword
+ * @param {CompiledMode} mode - current mode
+ * @param {string} matchText - the textual match
+ * @returns {KeywordData | false}
+ */
+ function keywordData(mode, matchText) {
+ return mode.keywords[matchText];
+ }
+
+ function processKeywords() {
+ if (!top.keywords) {
+ emitter.addText(modeBuffer);
+ return;
+ }
+
+ let lastIndex = 0;
+ top.keywordPatternRe.lastIndex = 0;
+ let match = top.keywordPatternRe.exec(modeBuffer);
+ let buf = "";
+
+ while (match) {
+ buf += modeBuffer.substring(lastIndex, match.index);
+ const word = language.case_insensitive ? match[0].toLowerCase() : match[0];
+ const data = keywordData(top, word);
+ if (data) {
+ const [kind, keywordRelevance] = data;
+ emitter.addText(buf);
+ buf = "";
+
+ keywordHits[word] = (keywordHits[word] || 0) + 1;
+ if (keywordHits[word] <= MAX_KEYWORD_HITS) relevance += keywordRelevance;
+ if (kind.startsWith("_")) {
+ // _ implied for relevance only, do not highlight
+ // by applying a class name
+ buf += match[0];
+ } else {
+ const cssClass = language.classNameAliases[kind] || kind;
+ emitter.addKeyword(match[0], cssClass);
+ }
+ } else {
+ buf += match[0];
+ }
+ lastIndex = top.keywordPatternRe.lastIndex;
+ match = top.keywordPatternRe.exec(modeBuffer);
+ }
+ buf += modeBuffer.substring(lastIndex);
+ emitter.addText(buf);
+ }
+
+ function processSubLanguage() {
+ if (modeBuffer === "") return;
+ /** @type HighlightResult */
+ let result = null;
+
+ if (typeof top.subLanguage === 'string') {
+ if (!languages[top.subLanguage]) {
+ emitter.addText(modeBuffer);
+ return;
+ }
+ result = _highlight(top.subLanguage, modeBuffer, true, continuations[top.subLanguage]);
+ continuations[top.subLanguage] = /** @type {CompiledMode} */ (result._top);
+ } else {
+ result = highlightAuto(modeBuffer, top.subLanguage.length ? top.subLanguage : null);
+ }
+
+ // Counting embedded language score towards the host language may be disabled
+ // with zeroing the containing mode relevance. Use case in point is Markdown that
+ // allows XML everywhere and makes every XML snippet to have a much larger Markdown
+ // score.
+ if (top.relevance > 0) {
+ relevance += result.relevance;
+ }
+ emitter.addSublanguage(result._emitter, result.language);
+ }
+
+ function processBuffer() {
+ if (top.subLanguage != null) {
+ processSubLanguage();
+ } else {
+ processKeywords();
+ }
+ modeBuffer = '';
+ }
+
+ /**
+ * @param {CompiledScope} scope
+ * @param {RegExpMatchArray} match
+ */
+ function emitMultiClass(scope, match) {
+ let i = 1;
+ const max = match.length - 1;
+ while (i <= max) {
+ if (!scope._emit[i]) { i++; continue; }
+ const klass = language.classNameAliases[scope[i]] || scope[i];
+ const text = match[i];
+ if (klass) {
+ emitter.addKeyword(text, klass);
+ } else {
+ modeBuffer = text;
+ processKeywords();
+ modeBuffer = "";
+ }
+ i++;
+ }
+ }
+
+ /**
+ * @param {CompiledMode} mode - new mode to start
+ * @param {RegExpMatchArray} match
+ */
+ function startNewMode(mode, match) {
+ if (mode.scope && typeof mode.scope === "string") {
+ emitter.openNode(language.classNameAliases[mode.scope] || mode.scope);
+ }
+ if (mode.beginScope) {
+ // beginScope just wraps the begin match itself in a scope
+ if (mode.beginScope._wrap) {
+ emitter.addKeyword(modeBuffer, language.classNameAliases[mode.beginScope._wrap] || mode.beginScope._wrap);
+ modeBuffer = "";
+ } else if (mode.beginScope._multi) {
+ // at this point modeBuffer should just be the match
+ emitMultiClass(mode.beginScope, match);
+ modeBuffer = "";
+ }
+ }
+
+ top = Object.create(mode, { parent: { value: top } });
+ return top;
+ }
+
+ /**
+ * @param {CompiledMode } mode - the mode to potentially end
+ * @param {RegExpMatchArray} match - the latest match
+ * @param {string} matchPlusRemainder - match plus remainder of content
+ * @returns {CompiledMode | void} - the next mode, or if void continue on in current mode
+ */
+ function endOfMode(mode, match, matchPlusRemainder) {
+ let matched = startsWith(mode.endRe, matchPlusRemainder);
+
+ if (matched) {
+ if (mode["on:end"]) {
+ const resp = new Response(mode);
+ mode["on:end"](match, resp);
+ if (resp.isMatchIgnored) matched = false;
+ }
+
+ if (matched) {
+ while (mode.endsParent && mode.parent) {
+ mode = mode.parent;
+ }
+ return mode;
+ }
+ }
+ // even if on:end fires an `ignore` it's still possible
+ // that we might trigger the end node because of a parent mode
+ if (mode.endsWithParent) {
+ return endOfMode(mode.parent, match, matchPlusRemainder);
+ }
+ }
+
+ /**
+ * Handle matching but then ignoring a sequence of text
+ *
+ * @param {string} lexeme - string containing full match text
+ */
+ function doIgnore(lexeme) {
+ if (top.matcher.regexIndex === 0) {
+ // no more regexes to potentially match here, so we move the cursor forward one
+ // space
+ modeBuffer += lexeme[0];
+ return 1;
+ } else {
+ // no need to move the cursor, we still have additional regexes to try and
+ // match at this very spot
+ resumeScanAtSamePosition = true;
+ return 0;
+ }
+ }
+
+ /**
+ * Handle the start of a new potential mode match
+ *
+ * @param {EnhancedMatch} match - the current match
+ * @returns {number} how far to advance the parse cursor
+ */
+ function doBeginMatch(match) {
+ const lexeme = match[0];
+ const newMode = match.rule;
+
+ const resp = new Response(newMode);
+ // first internal before callbacks, then the public ones
+ const beforeCallbacks = [newMode.__beforeBegin, newMode["on:begin"]];
+ for (const cb of beforeCallbacks) {
+ if (!cb) continue;
+ cb(match, resp);
+ if (resp.isMatchIgnored) return doIgnore(lexeme);
+ }
+
+ if (newMode.skip) {
+ modeBuffer += lexeme;
+ } else {
+ if (newMode.excludeBegin) {
+ modeBuffer += lexeme;
+ }
+ processBuffer();
+ if (!newMode.returnBegin && !newMode.excludeBegin) {
+ modeBuffer = lexeme;
+ }
+ }
+ startNewMode(newMode, match);
+ return newMode.returnBegin ? 0 : lexeme.length;
+ }
+
+ /**
+ * Handle the potential end of mode
+ *
+ * @param {RegExpMatchArray} match - the current match
+ */
+ function doEndMatch(match) {
+ const lexeme = match[0];
+ const matchPlusRemainder = codeToHighlight.substring(match.index);
+
+ const endMode = endOfMode(top, match, matchPlusRemainder);
+ if (!endMode) { return NO_MATCH; }
+
+ const origin = top;
+ if (top.endScope && top.endScope._wrap) {
+ processBuffer();
+ emitter.addKeyword(lexeme, top.endScope._wrap);
+ } else if (top.endScope && top.endScope._multi) {
+ processBuffer();
+ emitMultiClass(top.endScope, match);
+ } else if (origin.skip) {
+ modeBuffer += lexeme;
+ } else {
+ if (!(origin.returnEnd || origin.excludeEnd)) {
+ modeBuffer += lexeme;
+ }
+ processBuffer();
+ if (origin.excludeEnd) {
+ modeBuffer = lexeme;
+ }
+ }
+ do {
+ if (top.scope) {
+ emitter.closeNode();
+ }
+ if (!top.skip && !top.subLanguage) {
+ relevance += top.relevance;
+ }
+ top = top.parent;
+ } while (top !== endMode.parent);
+ if (endMode.starts) {
+ startNewMode(endMode.starts, match);
+ }
+ return origin.returnEnd ? 0 : lexeme.length;
+ }
+
+ function processContinuations() {
+ const list = [];
+ for (let current = top; current !== language; current = current.parent) {
+ if (current.scope) {
+ list.unshift(current.scope);
+ }
+ }
+ list.forEach(item => emitter.openNode(item));
+ }
+
+ /** @type {{type?: MatchType, index?: number, rule?: Mode}}} */
+ let lastMatch = {};
+
+ /**
+ * Process an individual match
+ *
+ * @param {string} textBeforeMatch - text preceding the match (since the last match)
+ * @param {EnhancedMatch} [match] - the match itself
+ */
+ function processLexeme(textBeforeMatch, match) {
+ const lexeme = match && match[0];
+
+ // add non-matched text to the current mode buffer
+ modeBuffer += textBeforeMatch;
+
+ if (lexeme == null) {
+ processBuffer();
+ return 0;
+ }
+
+ // we've found a 0 width match and we're stuck, so we need to advance
+ // this happens when we have badly behaved rules that have optional matchers to the degree that
+ // sometimes they can end up matching nothing at all
+ // Ref: https://github.com/highlightjs/highlight.js/issues/2140
+ if (lastMatch.type === "begin" && match.type === "end" && lastMatch.index === match.index && lexeme === "") {
+ // spit the "skipped" character that our regex choked on back into the output sequence
+ modeBuffer += codeToHighlight.slice(match.index, match.index + 1);
+ if (!SAFE_MODE) {
+ /** @type {AnnotatedError} */
+ const err = new Error(`0 width match regex (${languageName})`);
+ err.languageName = languageName;
+ err.badRule = lastMatch.rule;
+ throw err;
+ }
+ return 1;
+ }
+ lastMatch = match;
+
+ if (match.type === "begin") {
+ return doBeginMatch(match);
+ } else if (match.type === "illegal" && !ignoreIllegals) {
+ // illegal match, we do not continue processing
+ /** @type {AnnotatedError} */
+ const err = new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.scope || '<unnamed>') + '"');
+ err.mode = top;
+ throw err;
+ } else if (match.type === "end") {
+ const processed = doEndMatch(match);
+ if (processed !== NO_MATCH) {
+ return processed;
+ }
+ }
+
+ // edge case for when illegal matches $ (end of line) which is technically
+ // a 0 width match but not a begin/end match so it's not caught by the
+ // first handler (when ignoreIllegals is true)
+ if (match.type === "illegal" && lexeme === "") {
+ // advance so we aren't stuck in an infinite loop
+ return 1;
+ }
+
+ // infinite loops are BAD, this is a last ditch catch all. if we have a
+ // decent number of iterations yet our index (cursor position in our
+ // parsing) still 3x behind our index then something is very wrong
+ // so we bail
+ if (iterations > 100000 && iterations > match.index * 3) {
+ const err = new Error('potential infinite loop, way more iterations than matches');
+ throw err;
+ }
+
+ /*
+ Why might be find ourselves here? An potential end match that was
+ triggered but could not be completed. IE, `doEndMatch` returned NO_MATCH.
+ (this could be because a callback requests the match be ignored, etc)
+
+ This causes no real harm other than stopping a few times too many.
+ */
+
+ modeBuffer += lexeme;
+ return lexeme.length;
+ }
+
+ const language = getLanguage(languageName);
+ if (!language) {
+ error(LANGUAGE_NOT_FOUND.replace("{}", languageName));
+ throw new Error('Unknown language: "' + languageName + '"');
+ }
+
+ const md = compileLanguage(language);
+ let result = '';
+ /** @type {CompiledMode} */
+ let top = continuation || md;
+ /** @type Record<string,CompiledMode> */
+ const continuations = {}; // keep continuations for sub-languages
+ const emitter = new options.__emitter(options);
+ processContinuations();
+ let modeBuffer = '';
+ let relevance = 0;
+ let index = 0;
+ let iterations = 0;
+ let resumeScanAtSamePosition = false;
+
+ try {
+ top.matcher.considerAll();
+
+ for (;;) {
+ iterations++;
+ if (resumeScanAtSamePosition) {
+ // only regexes not matched previously will now be
+ // considered for a potential match
+ resumeScanAtSamePosition = false;
+ } else {
+ top.matcher.considerAll();
+ }
+ top.matcher.lastIndex = index;
+
+ const match = top.matcher.exec(codeToHighlight);
+ // console.log("match", match[0], match.rule && match.rule.begin)
+
+ if (!match) break;
+
+ const beforeMatch = codeToHighlight.substring(index, match.index);
+ const processedCount = processLexeme(beforeMatch, match);
+ index = match.index + processedCount;
+ }
+ processLexeme(codeToHighlight.substring(index));
+ emitter.closeAllNodes();
+ emitter.finalize();
+ result = emitter.toHTML();
+
+ return {
+ language: languageName,
+ value: result,
+ relevance: relevance,
+ illegal: false,
+ _emitter: emitter,
+ _top: top
+ };
+ } catch (err) {
+ if (err.message && err.message.includes('Illegal')) {
+ return {
+ language: languageName,
+ value: escape(codeToHighlight),
+ illegal: true,
+ relevance: 0,
+ _illegalBy: {
+ message: err.message,
+ index: index,
+ context: codeToHighlight.slice(index - 100, index + 100),
+ mode: err.mode,
+ resultSoFar: result
+ },
+ _emitter: emitter
+ };
+ } else if (SAFE_MODE) {
+ return {
+ language: languageName,
+ value: escape(codeToHighlight),
+ illegal: false,
+ relevance: 0,
+ errorRaised: err,
+ _emitter: emitter,
+ _top: top
+ };
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ /**
+ * returns a valid highlight result, without actually doing any actual work,
+ * auto highlight starts with this and it's possible for small snippets that
+ * auto-detection may not find a better match
+ * @param {string} code
+ * @returns {HighlightResult}
+ */
+ function justTextHighlightResult(code) {
+ const result = {
+ value: escape(code),
+ illegal: false,
+ relevance: 0,
+ _top: PLAINTEXT_LANGUAGE,
+ _emitter: new options.__emitter(options)
+ };
+ result._emitter.addText(code);
+ return result;
+ }
+
+ /**
+ Highlighting with language detection. Accepts a string with the code to
+ highlight. Returns an object with the following properties:
+
+ - language (detected language)
+ - relevance (int)
+ - value (an HTML string with highlighting markup)
+ - secondBest (object with the same structure for second-best heuristically
+ detected language, may be absent)
+
+ @param {string} code
+ @param {Array<string>} [languageSubset]
+ @returns {AutoHighlightResult}
+ */
+ function highlightAuto(code, languageSubset) {
+ languageSubset = languageSubset || options.languages || Object.keys(languages);
+ const plaintext = justTextHighlightResult(code);
+
+ const results = languageSubset.filter(getLanguage).filter(autoDetection).map(name =>
+ _highlight(name, code, false)
+ );
+ results.unshift(plaintext); // plaintext is always an option
+
+ const sorted = results.sort((a, b) => {
+ // sort base on relevance
+ if (a.relevance !== b.relevance) return b.relevance - a.relevance;
+
+ // always award the tie to the base language
+ // ie if C++ and Arduino are tied, it's more likely to be C++
+ if (a.language && b.language) {
+ if (getLanguage(a.language).supersetOf === b.language) {
+ return 1;
+ } else if (getLanguage(b.language).supersetOf === a.language) {
+ return -1;
+ }
+ }
+
+ // otherwise say they are equal, which has the effect of sorting on
+ // relevance while preserving the original ordering - which is how ties
+ // have historically been settled, ie the language that comes first always
+ // wins in the case of a tie
+ return 0;
+ });
+
+ const [best, secondBest] = sorted;
+
+ /** @type {AutoHighlightResult} */
+ const result = best;
+ result.secondBest = secondBest;
+
+ return result;
+ }
+
+ /**
+ * Builds new class name for block given the language name
+ *
+ * @param {HTMLElement} element
+ * @param {string} [currentLang]
+ * @param {string} [resultLang]
+ */
+ function updateClassName(element, currentLang, resultLang) {
+ const language = (currentLang && aliases[currentLang]) || resultLang;
+
+ element.classList.add("hljs");
+ element.classList.add(`language-${language}`);
+ }
+
+ /**
+ * Applies highlighting to a DOM node containing code.
+ *
+ * @param {HighlightedHTMLElement} element - the HTML element to highlight
+ */
+ function highlightElement(element) {
+ /** @type HTMLElement */
+ let node = null;
+ const language = blockLanguage(element);
+
+ if (shouldNotHighlight(language)) return;
+
+ fire("before:highlightElement",
+ { el: element, language: language });
+
+ // we should be all text, no child nodes (unescaped HTML) - this is possibly
+ // an HTML injection attack - it's likely too late if this is already in
+ // production (the code has likely already done its damage by the time
+ // we're seeing it)... but we yell loudly about this so that hopefully it's
+ // more likely to be caught in development before making it to production
+ if (element.children.length > 0) {
+ if (!options.ignoreUnescapedHTML) {
+ console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk.");
+ console.warn("https://github.com/highlightjs/highlight.js/wiki/security");
+ console.warn("The element with unescaped HTML:");
+ console.warn(element);
+ }
+ if (options.throwUnescapedHTML) {
+ const err = new HTMLInjectionError(
+ "One of your code blocks includes unescaped HTML.",
+ element.innerHTML
+ );
+ throw err;
+ }
+ }
+
+ node = element;
+ const text = node.textContent;
+ const result = language ? highlight(text, { language, ignoreIllegals: true }) : highlightAuto(text);
+
+ element.innerHTML = result.value;
+ updateClassName(element, language, result.language);
+ element.result = {
+ language: result.language,
+ // TODO: remove with version 11.0
+ re: result.relevance,
+ relevance: result.relevance
+ };
+ if (result.secondBest) {
+ element.secondBest = {
+ language: result.secondBest.language,
+ relevance: result.secondBest.relevance
+ };
+ }
+
+ fire("after:highlightElement", { el: element, result, text });
+ }
+
+ /**
+ * Updates highlight.js global options with the passed options
+ *
+ * @param {Partial<HLJSOptions>} userOptions
+ */
+ function configure(userOptions) {
+ options = inherit(options, userOptions);
+ }
+
+ // TODO: remove v12, deprecated
+ const initHighlighting = () => {
+ highlightAll();
+ deprecated("10.6.0", "initHighlighting() deprecated. Use highlightAll() now.");
+ };
+
+ // TODO: remove v12, deprecated
+ function initHighlightingOnLoad() {
+ highlightAll();
+ deprecated("10.6.0", "initHighlightingOnLoad() deprecated. Use highlightAll() now.");
+ }
+
+ let wantsHighlight = false;
+
+ /**
+ * auto-highlights all pre>code elements on the page
+ */
+ function highlightAll() {
+ // if we are called too early in the loading process
+ if (document.readyState === "loading") {
+ wantsHighlight = true;
+ return;
+ }
+
+ const blocks = document.querySelectorAll(options.cssSelector);
+ blocks.forEach(highlightElement);
+ }
+
+ function boot() {
+ // if a highlight was requested before DOM was loaded, do now
+ if (wantsHighlight) highlightAll();
+ }
+
+ // make sure we are in the browser environment
+ if (typeof window !== 'undefined' && window.addEventListener) {
+ window.addEventListener('DOMContentLoaded', boot, false);
+ }
+
+ /**
+ * Register a language grammar module
+ *
+ * @param {string} languageName
+ * @param {LanguageFn} languageDefinition
+ */
+ function registerLanguage(languageName, languageDefinition) {
+ let lang = null;
+ try {
+ lang = languageDefinition(hljs);
+ } catch (error$1) {
+ error("Language definition for '{}' could not be registered.".replace("{}", languageName));
+ // hard or soft error
+ if (!SAFE_MODE) { throw error$1; } else { error(error$1); }
+ // languages that have serious errors are replaced with essentially a
+ // "plaintext" stand-in so that the code blocks will still get normal
+ // css classes applied to them - and one bad language won't break the
+ // entire highlighter
+ lang = PLAINTEXT_LANGUAGE;
+ }
+ // give it a temporary name if it doesn't have one in the meta-data
+ if (!lang.name) lang.name = languageName;
+ languages[languageName] = lang;
+ lang.rawDefinition = languageDefinition.bind(null, hljs);
+
+ if (lang.aliases) {
+ registerAliases(lang.aliases, { languageName });
+ }
+ }
+
+ /**
+ * Remove a language grammar module
+ *
+ * @param {string} languageName
+ */
+ function unregisterLanguage(languageName) {
+ delete languages[languageName];
+ for (const alias of Object.keys(aliases)) {
+ if (aliases[alias] === languageName) {
+ delete aliases[alias];
+ }
+ }
+ }
+
+ /**
+ * @returns {string[]} List of language internal names
+ */
+ function listLanguages() {
+ return Object.keys(languages);
+ }
+
+ /**
+ * @param {string} name - name of the language to retrieve
+ * @returns {Language | undefined}
+ */
+ function getLanguage(name) {
+ name = (name || '').toLowerCase();
+ return languages[name] || languages[aliases[name]];
+ }
+
+ /**
+ *
+ * @param {string|string[]} aliasList - single alias or list of aliases
+ * @param {{languageName: string}} opts
+ */
+ function registerAliases(aliasList, { languageName }) {
+ if (typeof aliasList === 'string') {
+ aliasList = [aliasList];
+ }
+ aliasList.forEach(alias => { aliases[alias.toLowerCase()] = languageName; });
+ }
+
+ /**
+ * Determines if a given language has auto-detection enabled
+ * @param {string} name - name of the language
+ */
+ function autoDetection(name) {
+ const lang = getLanguage(name);
+ return lang && !lang.disableAutodetect;
+ }
+
+ /**
+ * Upgrades the old highlightBlock plugins to the new
+ * highlightElement API
+ * @param {HLJSPlugin} plugin
+ */
+ function upgradePluginAPI(plugin) {
+ // TODO: remove with v12
+ if (plugin["before:highlightBlock"] && !plugin["before:highlightElement"]) {
+ plugin["before:highlightElement"] = (data) => {
+ plugin["before:highlightBlock"](
+ Object.assign({ block: data.el }, data)
+ );
+ };
+ }
+ if (plugin["after:highlightBlock"] && !plugin["after:highlightElement"]) {
+ plugin["after:highlightElement"] = (data) => {
+ plugin["after:highlightBlock"](
+ Object.assign({ block: data.el }, data)
+ );
+ };
+ }
+ }
+
+ /**
+ * @param {HLJSPlugin} plugin
+ */
+ function addPlugin(plugin) {
+ upgradePluginAPI(plugin);
+ plugins.push(plugin);
+ }
+
+ /**
+ *
+ * @param {PluginEvent} event
+ * @param {any} args
+ */
+ function fire(event, args) {
+ const cb = event;
+ plugins.forEach(function(plugin) {
+ if (plugin[cb]) {
+ plugin[cb](args);
+ }
+ });
+ }
+
+ /**
+ * DEPRECATED
+ * @param {HighlightedHTMLElement} el
+ */
+ function deprecateHighlightBlock(el) {
+ deprecated("10.7.0", "highlightBlock will be removed entirely in v12.0");
+ deprecated("10.7.0", "Please use highlightElement now.");
+
+ return highlightElement(el);
+ }
+
+ /* Interface definition */
+ Object.assign(hljs, {
+ highlight,
+ highlightAuto,
+ highlightAll,
+ highlightElement,
+ // TODO: Remove with v12 API
+ highlightBlock: deprecateHighlightBlock,
+ configure,
+ initHighlighting,
+ initHighlightingOnLoad,
+ registerLanguage,
+ unregisterLanguage,
+ listLanguages,
+ getLanguage,
+ registerAliases,
+ autoDetection,
+ inherit,
+ addPlugin
+ });
+
+ hljs.debugMode = function() { SAFE_MODE = false; };
+ hljs.safeMode = function() { SAFE_MODE = true; };
+ hljs.versionString = version;
+
+ hljs.regex = {
+ concat: concat,
+ lookahead: lookahead,
+ either: either,
+ optional: optional,
+ anyNumberOfTimes: anyNumberOfTimes
+ };
+
+ for (const key in MODES$1) {
+ // @ts-ignore
+ if (typeof MODES$1[key] === "object") {
+ // @ts-ignore
+ deepFreezeEs6.exports(MODES$1[key]);
+ }
+ }
+
+ // merge all the modes/regexes into our main object
+ Object.assign(hljs, MODES$1);
+
+ return hljs;
+ };
+
+ // export an "instance" of the highlighter
+ var HighlightJS = HLJS({});
+
+ /*
+ Language: Gleam
+ Description: Gleam is a general-purpose functional language, which is compiled to both Erlang and JS
+ Category: functional
+ */
+
+ /** @type LanguageFn */
+ function gleam (hljs) {
+ const KEYWORDS =
+ "as assert case const external fn if import let " +
+ "use opaque pub todo try tuple type";
+ const STRING = {
+ className: "string",
+ variants: [{ begin: /"/, end: /"/ }],
+ contains: [hljs.BACKSLASH_ESCAPE],
+ relevance: 0,
+ };
+ const NAME = {
+ className: "variable",
+ begin: "\\b[a-z][a-z0-9_]*\\b",
+ relevance: 0,
+ };
+ const DISCARD_NAME = {
+ className: "comment",
+ begin: "\\b_[a-z][a-z0-9_]*\\b",
+ relevance: 0,
+ };
+ const NUMBER = {
+ className: "number",
+ variants: [
+ {
+ // binary
+ begin: "\\b0[bB](?:_?[01]+)+",
+ },
+ {
+ // octal
+ begin: "\\b0[oO](?:_?[0-7]+)+",
+ },
+ {
+ // hex
+ begin: "\\b0[xX](?:_?[0-9a-fA-F]+)+",
+ },
+ {
+ // dec, float
+ begin: "\\b\\d(?:_?\\d+)*(?:\\.(?:\\d(?:_?\\d+)*)*)?",
+ },
+ ],
+ relevance: 0,
+ };
+
+ return {
+ name: "Gleam",
+ aliases: ["gleam"],
+ contains: [
+ hljs.C_LINE_COMMENT_MODE,
+ STRING,
+ {
+ // bit string
+ begin: "&lt;&lt;",
+ end: ">>",
+ contains: [
+ {
+ className: "keyword",
+ beginKeywords:
+ "binary bytes int float bit_string bits utf8 utf16 utf32 " +
+ "utf8_codepoint utf16_codepoint utf32_codepoint signed unsigned " +
+ "big little native unit size",
+ },
+ KEYWORDS,
+ STRING,
+ NAME,
+ DISCARD_NAME,
+ NUMBER,
+ ],
+ relevance: 10,
+ },
+ {
+ className: "function",
+ beginKeywords: "fn",
+ end: "\\(",
+ excludeEnd: true,
+ contains: [
+ {
+ className: "title",
+ begin: "[a-z][a-z0-9_]*\\w*",
+ relevance: 0,
+ },
+ ],
+ },
+ {
+ className: "keyword",
+ beginKeywords: KEYWORDS,
+ },
+ {
+ // Type names and constructors
+ className: "title",
+ begin: "\\b[A-Z][A-Za-z0-9]*\\b",
+ relevance: 0,
+ },
+ {
+ className: "operator",
+ begin: "[+\\-*/%!=<>&|.]+",
+ relevance: 0,
+ },
+ NAME,
+ DISCARD_NAME,
+ NUMBER,
+ ],
+ };
+ }
+
+ /*
+ Language: Erlang
+ Description: Erlang is a general-purpose functional language, with strict evaluation, single assignment, and dynamic typing.
+ Author: Nikolay Zakharov <nikolay.desh@gmail.com>, Dmitry Kovega <arhibot@gmail.com>
+ Website: https://www.erlang.org
+ Category: functional
+ */
+
+ /** @type LanguageFn */
+ function erlang(hljs) {
+ const BASIC_ATOM_RE = '[a-z\'][a-zA-Z0-9_\']*';
+ const FUNCTION_NAME_RE = '(' + BASIC_ATOM_RE + ':' + BASIC_ATOM_RE + '|' + BASIC_ATOM_RE + ')';
+ const ERLANG_RESERVED = {
+ keyword:
+ 'after and andalso|10 band begin bnot bor bsl bzr bxor case catch cond div end fun if '
+ + 'let not of orelse|10 query receive rem try when xor',
+ literal:
+ 'false true'
+ };
+
+ const COMMENT = hljs.COMMENT('%', '$');
+ const NUMBER = {
+ className: 'number',
+ begin: '\\b(\\d+(_\\d+)*#[a-fA-F0-9]+(_[a-fA-F0-9]+)*|\\d+(_\\d+)*(\\.\\d+(_\\d+)*)?([eE][-+]?\\d+)?)',
+ relevance: 0
+ };
+ const NAMED_FUN = { begin: 'fun\\s+' + BASIC_ATOM_RE + '/\\d+' };
+ const FUNCTION_CALL = {
+ begin: FUNCTION_NAME_RE + '\\(',
+ end: '\\)',
+ returnBegin: true,
+ relevance: 0,
+ contains: [
+ {
+ begin: FUNCTION_NAME_RE,
+ relevance: 0
+ },
+ {
+ begin: '\\(',
+ end: '\\)',
+ endsWithParent: true,
+ returnEnd: true,
+ relevance: 0
+ // "contains" defined later
+ }
+ ]
+ };
+ const TUPLE = {
+ begin: /\{/,
+ end: /\}/,
+ relevance: 0
+ // "contains" defined later
+ };
+ const VAR1 = {
+ begin: '\\b_([A-Z][A-Za-z0-9_]*)?',
+ relevance: 0
+ };
+ const VAR2 = {
+ begin: '[A-Z][a-zA-Z0-9_]*',
+ relevance: 0
+ };
+ const RECORD_ACCESS = {
+ begin: '#' + hljs.UNDERSCORE_IDENT_RE,
+ relevance: 0,
+ returnBegin: true,
+ contains: [
+ {
+ begin: '#' + hljs.UNDERSCORE_IDENT_RE,
+ relevance: 0
+ },
+ {
+ begin: /\{/,
+ end: /\}/,
+ relevance: 0
+ // "contains" defined later
+ }
+ ]
+ };
+
+ const BLOCK_STATEMENTS = {
+ beginKeywords: 'fun receive if try case',
+ end: 'end',
+ keywords: ERLANG_RESERVED
+ };
+ BLOCK_STATEMENTS.contains = [
+ COMMENT,
+ NAMED_FUN,
+ hljs.inherit(hljs.APOS_STRING_MODE, { className: '' }),
+ BLOCK_STATEMENTS,
+ FUNCTION_CALL,
+ hljs.QUOTE_STRING_MODE,
+ NUMBER,
+ TUPLE,
+ VAR1,
+ VAR2,
+ RECORD_ACCESS
+ ];
+
+ const BASIC_MODES = [
+ COMMENT,
+ NAMED_FUN,
+ BLOCK_STATEMENTS,
+ FUNCTION_CALL,
+ hljs.QUOTE_STRING_MODE,
+ NUMBER,
+ TUPLE,
+ VAR1,
+ VAR2,
+ RECORD_ACCESS
+ ];
+ FUNCTION_CALL.contains[1].contains = BASIC_MODES;
+ TUPLE.contains = BASIC_MODES;
+ RECORD_ACCESS.contains[1].contains = BASIC_MODES;
+
+ const DIRECTIVES = [
+ "-module",
+ "-record",
+ "-undef",
+ "-export",
+ "-ifdef",
+ "-ifndef",
+ "-author",
+ "-copyright",
+ "-doc",
+ "-vsn",
+ "-import",
+ "-include",
+ "-include_lib",
+ "-compile",
+ "-define",
+ "-else",
+ "-endif",
+ "-file",
+ "-behaviour",
+ "-behavior",
+ "-spec"
+ ];
+
+ const PARAMS = {
+ className: 'params',
+ begin: '\\(',
+ end: '\\)',
+ contains: BASIC_MODES
+ };
+ return {
+ name: 'Erlang',
+ aliases: [ 'erl' ],
+ keywords: ERLANG_RESERVED,
+ illegal: '(</|\\*=|\\+=|-=|/\\*|\\*/|\\(\\*|\\*\\))',
+ contains: [
+ {
+ className: 'function',
+ begin: '^' + BASIC_ATOM_RE + '\\s*\\(',
+ end: '->',
+ returnBegin: true,
+ illegal: '\\(|#|//|/\\*|\\\\|:|;',
+ contains: [
+ PARAMS,
+ hljs.inherit(hljs.TITLE_MODE, { begin: BASIC_ATOM_RE })
+ ],
+ starts: {
+ end: ';|\\.',
+ keywords: ERLANG_RESERVED,
+ contains: BASIC_MODES
+ }
+ },
+ COMMENT,
+ {
+ begin: '^-',
+ end: '\\.',
+ relevance: 0,
+ excludeEnd: true,
+ returnBegin: true,
+ keywords: {
+ $pattern: '-' + hljs.IDENT_RE,
+ keyword: DIRECTIVES.map(x => `${x}|1.5`).join(" ")
+ },
+ contains: [ PARAMS ]
+ },
+ NUMBER,
+ hljs.QUOTE_STRING_MODE,
+ RECORD_ACCESS,
+ VAR1,
+ VAR2,
+ TUPLE,
+ { begin: /\.$/ } // relevance booster
+ ]
+ };
+ }
+
+ /*
+ Language: HTML, XML
+ Website: https://www.w3.org/XML/
+ Category: common, web
+ Audit: 2020
+ */
+
+ /** @type LanguageFn */
+ function xml(hljs) {
+ const regex = hljs.regex;
+ // XML names can have the following additional letters: https://www.w3.org/TR/xml/#NT-NameChar
+ // OTHER_NAME_CHARS = /[:\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]/;
+ // Element names start with NAME_START_CHAR followed by optional other Unicode letters, ASCII digits, hyphens, underscores, and periods
+ // const TAG_NAME_RE = regex.concat(/[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/, regex.optional(/[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*:/), /[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*/);;
+ // const XML_IDENT_RE = /[A-Z_a-z:\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]+/;
+ // const TAG_NAME_RE = regex.concat(/[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/, regex.optional(/[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*:/), /[A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*/);
+ // however, to cater for performance and more Unicode support rely simply on the Unicode letter class
+ const TAG_NAME_RE = regex.concat(/[\p{L}_]/u, regex.optional(/[\p{L}0-9_.-]*:/u), /[\p{L}0-9_.-]*/u);
+ const XML_IDENT_RE = /[\p{L}0-9._:-]+/u;
+ const XML_ENTITIES = {
+ className: 'symbol',
+ begin: /&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/
+ };
+ const XML_META_KEYWORDS = {
+ begin: /\s/,
+ contains: [
+ {
+ className: 'keyword',
+ begin: /#?[a-z_][a-z1-9_-]+/,
+ illegal: /\n/
+ }
+ ]
+ };
+ const XML_META_PAR_KEYWORDS = hljs.inherit(XML_META_KEYWORDS, {
+ begin: /\(/,
+ end: /\)/
+ });
+ const APOS_META_STRING_MODE = hljs.inherit(hljs.APOS_STRING_MODE, { className: 'string' });
+ const QUOTE_META_STRING_MODE = hljs.inherit(hljs.QUOTE_STRING_MODE, { className: 'string' });
+ const TAG_INTERNALS = {
+ endsWithParent: true,
+ illegal: /</,
+ relevance: 0,
+ contains: [
+ {
+ className: 'attr',
+ begin: XML_IDENT_RE,
+ relevance: 0
+ },
+ {
+ begin: /=\s*/,
+ relevance: 0,
+ contains: [
+ {
+ className: 'string',
+ endsParent: true,
+ variants: [
+ {
+ begin: /"/,
+ end: /"/,
+ contains: [ XML_ENTITIES ]
+ },
+ {
+ begin: /'/,
+ end: /'/,
+ contains: [ XML_ENTITIES ]
+ },
+ { begin: /[^\s"'=<>`]+/ }
+ ]
+ }
+ ]
+ }
+ ]
+ };
+ return {
+ name: 'HTML, XML',
+ aliases: [
+ 'html',
+ 'xhtml',
+ 'rss',
+ 'atom',
+ 'xjb',
+ 'xsd',
+ 'xsl',
+ 'plist',
+ 'wsf',
+ 'svg'
+ ],
+ case_insensitive: true,
+ unicodeRegex: true,
+ contains: [
+ {
+ className: 'meta',
+ begin: /<![a-z]/,
+ end: />/,
+ relevance: 10,
+ contains: [
+ XML_META_KEYWORDS,
+ QUOTE_META_STRING_MODE,
+ APOS_META_STRING_MODE,
+ XML_META_PAR_KEYWORDS,
+ {
+ begin: /\[/,
+ end: /\]/,
+ contains: [
+ {
+ className: 'meta',
+ begin: /<![a-z]/,
+ end: />/,
+ contains: [
+ XML_META_KEYWORDS,
+ XML_META_PAR_KEYWORDS,
+ QUOTE_META_STRING_MODE,
+ APOS_META_STRING_MODE
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ hljs.COMMENT(
+ /<!--/,
+ /-->/,
+ { relevance: 10 }
+ ),
+ {
+ begin: /<!\[CDATA\[/,
+ end: /\]\]>/,
+ relevance: 10
+ },
+ XML_ENTITIES,
+ // xml processing instructions
+ {
+ className: 'meta',
+ end: /\?>/,
+ variants: [
+ {
+ begin: /<\?xml/,
+ relevance: 10,
+ contains: [
+ QUOTE_META_STRING_MODE
+ ]
+ },
+ {
+ begin: /<\?[a-z][a-z0-9]+/,
+ }
+ ]
+
+ },
+ {
+ className: 'tag',
+ /*
+ The lookahead pattern (?=...) ensures that 'begin' only matches
+ '<style' as a single word, followed by a whitespace or an
+ ending bracket.
+ */
+ begin: /<style(?=\s|>)/,
+ end: />/,
+ keywords: { name: 'style' },
+ contains: [ TAG_INTERNALS ],
+ starts: {
+ end: /<\/style>/,
+ returnEnd: true,
+ subLanguage: [
+ 'css',
+ 'xml'
+ ]
+ }
+ },
+ {
+ className: 'tag',
+ // See the comment in the <style tag about the lookahead pattern
+ begin: /<script(?=\s|>)/,
+ end: />/,
+ keywords: { name: 'script' },
+ contains: [ TAG_INTERNALS ],
+ starts: {
+ end: /<\/script>/,
+ returnEnd: true,
+ subLanguage: [
+ 'javascript',
+ 'handlebars',
+ 'xml'
+ ]
+ }
+ },
+ // we need this for now for jSX
+ {
+ className: 'tag',
+ begin: /<>|<\/>/
+ },
+ // open tag
+ {
+ className: 'tag',
+ begin: regex.concat(
+ /</,
+ regex.lookahead(regex.concat(
+ TAG_NAME_RE,
+ // <tag/>
+ // <tag>
+ // <tag ...
+ regex.either(/\/>/, />/, /\s/)
+ ))
+ ),
+ end: /\/?>/,
+ contains: [
+ {
+ className: 'name',
+ begin: TAG_NAME_RE,
+ relevance: 0,
+ starts: TAG_INTERNALS
+ }
+ ]
+ },
+ // close tag
+ {
+ className: 'tag',
+ begin: regex.concat(
+ /<\//,
+ regex.lookahead(regex.concat(
+ TAG_NAME_RE, />/
+ ))
+ ),
+ contains: [
+ {
+ className: 'name',
+ begin: TAG_NAME_RE,
+ relevance: 0
+ },
+ {
+ begin: />/,
+ relevance: 0,
+ endsParent: true
+ }
+ ]
+ }
+ ]
+ };
+ }
+
+ const MODES = (hljs) => {
+ return {
+ IMPORTANT: {
+ scope: 'meta',
+ begin: '!important'
+ },
+ BLOCK_COMMENT: hljs.C_BLOCK_COMMENT_MODE,
+ HEXCOLOR: {
+ scope: 'number',
+ begin: /#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/
+ },
+ FUNCTION_DISPATCH: {
+ className: "built_in",
+ begin: /[\w-]+(?=\()/
+ },
+ ATTRIBUTE_SELECTOR_MODE: {
+ scope: 'selector-attr',
+ begin: /\[/,
+ end: /\]/,
+ illegal: '$',
+ contains: [
+ hljs.APOS_STRING_MODE,
+ hljs.QUOTE_STRING_MODE
+ ]
+ },
+ CSS_NUMBER_MODE: {
+ scope: 'number',
+ begin: hljs.NUMBER_RE + '(' +
+ '%|em|ex|ch|rem' +
+ '|vw|vh|vmin|vmax' +
+ '|cm|mm|in|pt|pc|px' +
+ '|deg|grad|rad|turn' +
+ '|s|ms' +
+ '|Hz|kHz' +
+ '|dpi|dpcm|dppx' +
+ ')?',
+ relevance: 0
+ },
+ CSS_VARIABLE: {
+ className: "attr",
+ begin: /--[A-Za-z][A-Za-z0-9_-]*/
+ }
+ };
+ };
+
+ const TAGS = [
+ 'a',
+ 'abbr',
+ 'address',
+ 'article',
+ 'aside',
+ 'audio',
+ 'b',
+ 'blockquote',
+ 'body',
+ 'button',
+ 'canvas',
+ 'caption',
+ 'cite',
+ 'code',
+ 'dd',
+ 'del',
+ 'details',
+ 'dfn',
+ 'div',
+ 'dl',
+ 'dt',
+ 'em',
+ 'fieldset',
+ 'figcaption',
+ 'figure',
+ 'footer',
+ 'form',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'header',
+ 'hgroup',
+ 'html',
+ 'i',
+ 'iframe',
+ 'img',
+ 'input',
+ 'ins',
+ 'kbd',
+ 'label',
+ 'legend',
+ 'li',
+ 'main',
+ 'mark',
+ 'menu',
+ 'nav',
+ 'object',
+ 'ol',
+ 'p',
+ 'q',
+ 'quote',
+ 'samp',
+ 'section',
+ 'span',
+ 'strong',
+ 'summary',
+ 'sup',
+ 'table',
+ 'tbody',
+ 'td',
+ 'textarea',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'time',
+ 'tr',
+ 'ul',
+ 'var',
+ 'video'
+ ];
+
+ const MEDIA_FEATURES = [
+ 'any-hover',
+ 'any-pointer',
+ 'aspect-ratio',
+ 'color',
+ 'color-gamut',
+ 'color-index',
+ 'device-aspect-ratio',
+ 'device-height',
+ 'device-width',
+ 'display-mode',
+ 'forced-colors',
+ 'grid',
+ 'height',
+ 'hover',
+ 'inverted-colors',
+ 'monochrome',
+ 'orientation',
+ 'overflow-block',
+ 'overflow-inline',
+ 'pointer',
+ 'prefers-color-scheme',
+ 'prefers-contrast',
+ 'prefers-reduced-motion',
+ 'prefers-reduced-transparency',
+ 'resolution',
+ 'scan',
+ 'scripting',
+ 'update',
+ 'width',
+ // TODO: find a better solution?
+ 'min-width',
+ 'max-width',
+ 'min-height',
+ 'max-height'
+ ];
+
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes
+ const PSEUDO_CLASSES = [
+ 'active',
+ 'any-link',
+ 'blank',
+ 'checked',
+ 'current',
+ 'default',
+ 'defined',
+ 'dir', // dir()
+ 'disabled',
+ 'drop',
+ 'empty',
+ 'enabled',
+ 'first',
+ 'first-child',
+ 'first-of-type',
+ 'fullscreen',
+ 'future',
+ 'focus',
+ 'focus-visible',
+ 'focus-within',
+ 'has', // has()
+ 'host', // host or host()
+ 'host-context', // host-context()
+ 'hover',
+ 'indeterminate',
+ 'in-range',
+ 'invalid',
+ 'is', // is()
+ 'lang', // lang()
+ 'last-child',
+ 'last-of-type',
+ 'left',
+ 'link',
+ 'local-link',
+ 'not', // not()
+ 'nth-child', // nth-child()
+ 'nth-col', // nth-col()
+ 'nth-last-child', // nth-last-child()
+ 'nth-last-col', // nth-last-col()
+ 'nth-last-of-type', //nth-last-of-type()
+ 'nth-of-type', //nth-of-type()
+ 'only-child',
+ 'only-of-type',
+ 'optional',
+ 'out-of-range',
+ 'past',
+ 'placeholder-shown',
+ 'read-only',
+ 'read-write',
+ 'required',
+ 'right',
+ 'root',
+ 'scope',
+ 'target',
+ 'target-within',
+ 'user-invalid',
+ 'valid',
+ 'visited',
+ 'where' // where()
+ ];
+
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
+ const PSEUDO_ELEMENTS = [
+ 'after',
+ 'backdrop',
+ 'before',
+ 'cue',
+ 'cue-region',
+ 'first-letter',
+ 'first-line',
+ 'grammar-error',
+ 'marker',
+ 'part',
+ 'placeholder',
+ 'selection',
+ 'slotted',
+ 'spelling-error'
+ ];
+
+ const ATTRIBUTES = [
+ 'align-content',
+ 'align-items',
+ 'align-self',
+ 'all',
+ 'animation',
+ 'animation-delay',
+ 'animation-direction',
+ 'animation-duration',
+ 'animation-fill-mode',
+ 'animation-iteration-count',
+ 'animation-name',
+ 'animation-play-state',
+ 'animation-timing-function',
+ 'backface-visibility',
+ 'background',
+ 'background-attachment',
+ 'background-blend-mode',
+ 'background-clip',
+ 'background-color',
+ 'background-image',
+ 'background-origin',
+ 'background-position',
+ 'background-repeat',
+ 'background-size',
+ 'block-size',
+ 'border',
+ 'border-block',
+ 'border-block-color',
+ 'border-block-end',
+ 'border-block-end-color',
+ 'border-block-end-style',
+ 'border-block-end-width',
+ 'border-block-start',
+ 'border-block-start-color',
+ 'border-block-start-style',
+ 'border-block-start-width',
+ 'border-block-style',
+ 'border-block-width',
+ 'border-bottom',
+ 'border-bottom-color',
+ 'border-bottom-left-radius',
+ 'border-bottom-right-radius',
+ 'border-bottom-style',
+ 'border-bottom-width',
+ 'border-collapse',
+ 'border-color',
+ 'border-image',
+ 'border-image-outset',
+ 'border-image-repeat',
+ 'border-image-slice',
+ 'border-image-source',
+ 'border-image-width',
+ 'border-inline',
+ 'border-inline-color',
+ 'border-inline-end',
+ 'border-inline-end-color',
+ 'border-inline-end-style',
+ 'border-inline-end-width',
+ 'border-inline-start',
+ 'border-inline-start-color',
+ 'border-inline-start-style',
+ 'border-inline-start-width',
+ 'border-inline-style',
+ 'border-inline-width',
+ 'border-left',
+ 'border-left-color',
+ 'border-left-style',
+ 'border-left-width',
+ 'border-radius',
+ 'border-right',
+ 'border-right-color',
+ 'border-right-style',
+ 'border-right-width',
+ 'border-spacing',
+ 'border-style',
+ 'border-top',
+ 'border-top-color',
+ 'border-top-left-radius',
+ 'border-top-right-radius',
+ 'border-top-style',
+ 'border-top-width',
+ 'border-width',
+ 'bottom',
+ 'box-decoration-break',
+ 'box-shadow',
+ 'box-sizing',
+ 'break-after',
+ 'break-before',
+ 'break-inside',
+ 'caption-side',
+ 'caret-color',
+ 'clear',
+ 'clip',
+ 'clip-path',
+ 'clip-rule',
+ 'color',
+ 'column-count',
+ 'column-fill',
+ 'column-gap',
+ 'column-rule',
+ 'column-rule-color',
+ 'column-rule-style',
+ 'column-rule-width',
+ 'column-span',
+ 'column-width',
+ 'columns',
+ 'contain',
+ 'content',
+ 'content-visibility',
+ 'counter-increment',
+ 'counter-reset',
+ 'cue',
+ 'cue-after',
+ 'cue-before',
+ 'cursor',
+ 'direction',
+ 'display',
+ 'empty-cells',
+ 'filter',
+ 'flex',
+ 'flex-basis',
+ 'flex-direction',
+ 'flex-flow',
+ 'flex-grow',
+ 'flex-shrink',
+ 'flex-wrap',
+ 'float',
+ 'flow',
+ 'font',
+ 'font-display',
+ 'font-family',
+ 'font-feature-settings',
+ 'font-kerning',
+ 'font-language-override',
+ 'font-size',
+ 'font-size-adjust',
+ 'font-smoothing',
+ 'font-stretch',
+ 'font-style',
+ 'font-synthesis',
+ 'font-variant',
+ 'font-variant-caps',
+ 'font-variant-east-asian',
+ 'font-variant-ligatures',
+ 'font-variant-numeric',
+ 'font-variant-position',
+ 'font-variation-settings',
+ 'font-weight',
+ 'gap',
+ 'glyph-orientation-vertical',
+ 'grid',
+ 'grid-area',
+ 'grid-auto-columns',
+ 'grid-auto-flow',
+ 'grid-auto-rows',
+ 'grid-column',
+ 'grid-column-end',
+ 'grid-column-start',
+ 'grid-gap',
+ 'grid-row',
+ 'grid-row-end',
+ 'grid-row-start',
+ 'grid-template',
+ 'grid-template-areas',
+ 'grid-template-columns',
+ 'grid-template-rows',
+ 'hanging-punctuation',
+ 'height',
+ 'hyphens',
+ 'icon',
+ 'image-orientation',
+ 'image-rendering',
+ 'image-resolution',
+ 'ime-mode',
+ 'inline-size',
+ 'isolation',
+ 'justify-content',
+ 'left',
+ 'letter-spacing',
+ 'line-break',
+ 'line-height',
+ 'list-style',
+ 'list-style-image',
+ 'list-style-position',
+ 'list-style-type',
+ 'margin',
+ 'margin-block',
+ 'margin-block-end',
+ 'margin-block-start',
+ 'margin-bottom',
+ 'margin-inline',
+ 'margin-inline-end',
+ 'margin-inline-start',
+ 'margin-left',
+ 'margin-right',
+ 'margin-top',
+ 'marks',
+ 'mask',
+ 'mask-border',
+ 'mask-border-mode',
+ 'mask-border-outset',
+ 'mask-border-repeat',
+ 'mask-border-slice',
+ 'mask-border-source',
+ 'mask-border-width',
+ 'mask-clip',
+ 'mask-composite',
+ 'mask-image',
+ 'mask-mode',
+ 'mask-origin',
+ 'mask-position',
+ 'mask-repeat',
+ 'mask-size',
+ 'mask-type',
+ 'max-block-size',
+ 'max-height',
+ 'max-inline-size',
+ 'max-width',
+ 'min-block-size',
+ 'min-height',
+ 'min-inline-size',
+ 'min-width',
+ 'mix-blend-mode',
+ 'nav-down',
+ 'nav-index',
+ 'nav-left',
+ 'nav-right',
+ 'nav-up',
+ 'none',
+ 'normal',
+ 'object-fit',
+ 'object-position',
+ 'opacity',
+ 'order',
+ 'orphans',
+ 'outline',
+ 'outline-color',
+ 'outline-offset',
+ 'outline-style',
+ 'outline-width',
+ 'overflow',
+ 'overflow-wrap',
+ 'overflow-x',
+ 'overflow-y',
+ 'padding',
+ 'padding-block',
+ 'padding-block-end',
+ 'padding-block-start',
+ 'padding-bottom',
+ 'padding-inline',
+ 'padding-inline-end',
+ 'padding-inline-start',
+ 'padding-left',
+ 'padding-right',
+ 'padding-top',
+ 'page-break-after',
+ 'page-break-before',
+ 'page-break-inside',
+ 'pause',
+ 'pause-after',
+ 'pause-before',
+ 'perspective',
+ 'perspective-origin',
+ 'pointer-events',
+ 'position',
+ 'quotes',
+ 'resize',
+ 'rest',
+ 'rest-after',
+ 'rest-before',
+ 'right',
+ 'row-gap',
+ 'scroll-margin',
+ 'scroll-margin-block',
+ 'scroll-margin-block-end',
+ 'scroll-margin-block-start',
+ 'scroll-margin-bottom',
+ 'scroll-margin-inline',
+ 'scroll-margin-inline-end',
+ 'scroll-margin-inline-start',
+ 'scroll-margin-left',
+ 'scroll-margin-right',
+ 'scroll-margin-top',
+ 'scroll-padding',
+ 'scroll-padding-block',
+ 'scroll-padding-block-end',
+ 'scroll-padding-block-start',
+ 'scroll-padding-bottom',
+ 'scroll-padding-inline',
+ 'scroll-padding-inline-end',
+ 'scroll-padding-inline-start',
+ 'scroll-padding-left',
+ 'scroll-padding-right',
+ 'scroll-padding-top',
+ 'scroll-snap-align',
+ 'scroll-snap-stop',
+ 'scroll-snap-type',
+ 'scrollbar-color',
+ 'scrollbar-gutter',
+ 'scrollbar-width',
+ 'shape-image-threshold',
+ 'shape-margin',
+ 'shape-outside',
+ 'speak',
+ 'speak-as',
+ 'src', // @font-face
+ 'tab-size',
+ 'table-layout',
+ 'text-align',
+ 'text-align-all',
+ 'text-align-last',
+ 'text-combine-upright',
+ 'text-decoration',
+ 'text-decoration-color',
+ 'text-decoration-line',
+ 'text-decoration-style',
+ 'text-emphasis',
+ 'text-emphasis-color',
+ 'text-emphasis-position',
+ 'text-emphasis-style',
+ 'text-indent',
+ 'text-justify',
+ 'text-orientation',
+ 'text-overflow',
+ 'text-rendering',
+ 'text-shadow',
+ 'text-transform',
+ 'text-underline-position',
+ 'top',
+ 'transform',
+ 'transform-box',
+ 'transform-origin',
+ 'transform-style',
+ 'transition',
+ 'transition-delay',
+ 'transition-duration',
+ 'transition-property',
+ 'transition-timing-function',
+ 'unicode-bidi',
+ 'vertical-align',
+ 'visibility',
+ 'voice-balance',
+ 'voice-duration',
+ 'voice-family',
+ 'voice-pitch',
+ 'voice-range',
+ 'voice-rate',
+ 'voice-stress',
+ 'voice-volume',
+ 'white-space',
+ 'widows',
+ 'width',
+ 'will-change',
+ 'word-break',
+ 'word-spacing',
+ 'word-wrap',
+ 'writing-mode',
+ 'z-index'
+ // reverse makes sure longer attributes `font-weight` are matched fully
+ // instead of getting false positives on say `font`
+ ].reverse();
+
+ /*
+ Language: CSS
+ Category: common, css, web
+ Website: https://developer.mozilla.org/en-US/docs/Web/CSS
+ */
+
+ /** @type LanguageFn */
+ function css(hljs) {
+ const regex = hljs.regex;
+ const modes = MODES(hljs);
+ const VENDOR_PREFIX = { begin: /-(webkit|moz|ms|o)-(?=[a-z])/ };
+ const AT_MODIFIERS = "and or not only";
+ const AT_PROPERTY_RE = /@-?\w[\w]*(-\w+)*/; // @-webkit-keyframes
+ const IDENT_RE = '[a-zA-Z-][a-zA-Z0-9_-]*';
+ const STRINGS = [
+ hljs.APOS_STRING_MODE,
+ hljs.QUOTE_STRING_MODE
+ ];
+
+ return {
+ name: 'CSS',
+ case_insensitive: true,
+ illegal: /[=|'\$]/,
+ keywords: { keyframePosition: "from to" },
+ classNameAliases: {
+ // for visual continuity with `tag {}` and because we
+ // don't have a great class for this?
+ keyframePosition: "selector-tag" },
+ contains: [
+ modes.BLOCK_COMMENT,
+ VENDOR_PREFIX,
+ // to recognize keyframe 40% etc which are outside the scope of our
+ // attribute value mode
+ modes.CSS_NUMBER_MODE,
+ {
+ className: 'selector-id',
+ begin: /#[A-Za-z0-9_-]+/,
+ relevance: 0
+ },
+ {
+ className: 'selector-class',
+ begin: '\\.' + IDENT_RE,
+ relevance: 0
+ },
+ modes.ATTRIBUTE_SELECTOR_MODE,
+ {
+ className: 'selector-pseudo',
+ variants: [
+ { begin: ':(' + PSEUDO_CLASSES.join('|') + ')' },
+ { begin: ':(:)?(' + PSEUDO_ELEMENTS.join('|') + ')' }
+ ]
+ },
+ // we may actually need this (12/2020)
+ // { // pseudo-selector params
+ // begin: /\(/,
+ // end: /\)/,
+ // contains: [ hljs.CSS_NUMBER_MODE ]
+ // },
+ modes.CSS_VARIABLE,
+ {
+ className: 'attribute',
+ begin: '\\b(' + ATTRIBUTES.join('|') + ')\\b'
+ },
+ // attribute values
+ {
+ begin: /:/,
+ end: /[;}{]/,
+ contains: [
+ modes.BLOCK_COMMENT,
+ modes.HEXCOLOR,
+ modes.IMPORTANT,
+ modes.CSS_NUMBER_MODE,
+ ...STRINGS,
+ // needed to highlight these as strings and to avoid issues with
+ // illegal characters that might be inside urls that would tigger the
+ // languages illegal stack
+ {
+ begin: /(url|data-uri)\(/,
+ end: /\)/,
+ relevance: 0, // from keywords
+ keywords: { built_in: "url data-uri" },
+ contains: [
+ ...STRINGS,
+ {
+ className: "string",
+ // any character other than `)` as in `url()` will be the start
+ // of a string, which ends with `)` (from the parent mode)
+ begin: /[^)]/,
+ endsWithParent: true,
+ excludeEnd: true
+ }
+ ]
+ },
+ modes.FUNCTION_DISPATCH
+ ]
+ },
+ {
+ begin: regex.lookahead(/@/),
+ end: '[{;]',
+ relevance: 0,
+ illegal: /:/, // break on Less variables @var: ...
+ contains: [
+ {
+ className: 'keyword',
+ begin: AT_PROPERTY_RE
+ },
+ {
+ begin: /\s/,
+ endsWithParent: true,
+ excludeEnd: true,
+ relevance: 0,
+ keywords: {
+ $pattern: /[a-z-]+/,
+ keyword: AT_MODIFIERS,
+ attribute: MEDIA_FEATURES.join(" ")
+ },
+ contains: [
+ {
+ begin: /[a-z-]+(?=:)/,
+ className: "attribute"
+ },
+ ...STRINGS,
+ modes.CSS_NUMBER_MODE
+ ]
+ }
+ ]
+ },
+ {
+ className: 'selector-tag',
+ begin: '\\b(' + TAGS.join('|') + ')\\b'
+ }
+ ]
+ };
+ }
+
+ const IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*';
+ const KEYWORDS = [
+ "as", // for exports
+ "in",
+ "of",
+ "if",
+ "for",
+ "while",
+ "finally",
+ "var",
+ "new",
+ "function",
+ "do",
+ "return",
+ "void",
+ "else",
+ "break",
+ "catch",
+ "instanceof",
+ "with",
+ "throw",
+ "case",
+ "default",
+ "try",
+ "switch",
+ "continue",
+ "typeof",
+ "delete",
+ "let",
+ "yield",
+ "const",
+ "class",
+ // JS handles these with a special rule
+ // "get",
+ // "set",
+ "debugger",
+ "async",
+ "await",
+ "static",
+ "import",
+ "from",
+ "export",
+ "extends"
+ ];
+ const LITERALS = [
+ "true",
+ "false",
+ "null",
+ "undefined",
+ "NaN",
+ "Infinity"
+ ];
+
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
+ const TYPES = [
+ // Fundamental objects
+ "Object",
+ "Function",
+ "Boolean",
+ "Symbol",
+ // numbers and dates
+ "Math",
+ "Date",
+ "Number",
+ "BigInt",
+ // text
+ "String",
+ "RegExp",
+ // Indexed collections
+ "Array",
+ "Float32Array",
+ "Float64Array",
+ "Int8Array",
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Int16Array",
+ "Int32Array",
+ "Uint16Array",
+ "Uint32Array",
+ "BigInt64Array",
+ "BigUint64Array",
+ // Keyed collections
+ "Set",
+ "Map",
+ "WeakSet",
+ "WeakMap",
+ // Structured data
+ "ArrayBuffer",
+ "SharedArrayBuffer",
+ "Atomics",
+ "DataView",
+ "JSON",
+ // Control abstraction objects
+ "Promise",
+ "Generator",
+ "GeneratorFunction",
+ "AsyncFunction",
+ // Reflection
+ "Reflect",
+ "Proxy",
+ // Internationalization
+ "Intl",
+ // WebAssembly
+ "WebAssembly"
+ ];
+
+ const ERROR_TYPES = [
+ "Error",
+ "EvalError",
+ "InternalError",
+ "RangeError",
+ "ReferenceError",
+ "SyntaxError",
+ "TypeError",
+ "URIError"
+ ];
+
+ const BUILT_IN_GLOBALS = [
+ "setInterval",
+ "setTimeout",
+ "clearInterval",
+ "clearTimeout",
+
+ "require",
+ "exports",
+
+ "eval",
+ "isFinite",
+ "isNaN",
+ "parseFloat",
+ "parseInt",
+ "decodeURI",
+ "decodeURIComponent",
+ "encodeURI",
+ "encodeURIComponent",
+ "escape",
+ "unescape"
+ ];
+
+ const BUILT_IN_VARIABLES = [
+ "arguments",
+ "this",
+ "super",
+ "console",
+ "window",
+ "document",
+ "localStorage",
+ "sessionStorage",
+ "module",
+ "global" // Node.js
+ ];
+
+ const BUILT_INS = [].concat(
+ BUILT_IN_GLOBALS,
+ TYPES,
+ ERROR_TYPES
+ );
+
+ /*
+ Language: JavaScript
+ Description: JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions.
+ Category: common, scripting, web
+ Website: https://developer.mozilla.org/en-US/docs/Web/JavaScript
+ */
+
+ /** @type LanguageFn */
+ function javascript(hljs) {
+ const regex = hljs.regex;
+ /**
+ * Takes a string like "<Booger" and checks to see
+ * if we can find a matching "</Booger" later in the
+ * content.
+ * @param {RegExpMatchArray} match
+ * @param {{after:number}} param1
+ */
+ const hasClosingTag = (match, { after }) => {
+ const tag = "</" + match[0].slice(1);
+ const pos = match.input.indexOf(tag, after);
+ return pos !== -1;
+ };
+
+ const IDENT_RE$1 = IDENT_RE;
+ const FRAGMENT = {
+ begin: '<>',
+ end: '</>'
+ };
+ // to avoid some special cases inside isTrulyOpeningTag
+ const XML_SELF_CLOSING = /<[A-Za-z0-9\\._:-]+\s*\/>/;
+ const XML_TAG = {
+ begin: /<[A-Za-z0-9\\._:-]+/,
+ end: /\/[A-Za-z0-9\\._:-]+>|\/>/,
+ /**
+ * @param {RegExpMatchArray} match
+ * @param {CallbackResponse} response
+ */
+ isTrulyOpeningTag: (match, response) => {
+ const afterMatchIndex = match[0].length + match.index;
+ const nextChar = match.input[afterMatchIndex];
+ if (
+ // HTML should not include another raw `<` inside a tag
+ // nested type?
+ // `<Array<Array<number>>`, etc.
+ nextChar === "<" ||
+ // the , gives away that this is not HTML
+ // `<T, A extends keyof T, V>`
+ nextChar === ","
+ ) {
+ response.ignoreMatch();
+ return;
+ }
+
+ // `<something>`
+ // Quite possibly a tag, lets look for a matching closing tag...
+ if (nextChar === ">") {
+ // if we cannot find a matching closing tag, then we
+ // will ignore it
+ if (!hasClosingTag(match, { after: afterMatchIndex })) {
+ response.ignoreMatch();
+ }
+ }
+
+ // `<blah />` (self-closing)
+ // handled by simpleSelfClosing rule
+
+ let m;
+ const afterMatch = match.input.substring(afterMatchIndex);
+
+ // some more template typing stuff
+ // <T = any>(key?: string) => Modify<
+ if ((m = afterMatch.match(/^\s*=/))) {
+ response.ignoreMatch();
+ return;
+ }
+
+ // `<From extends string>`
+ // technically this could be HTML, but it smells like a type
+ // NOTE: This is ugh, but added specifically for https://github.com/highlightjs/highlight.js/issues/3276
+ if ((m = afterMatch.match(/^\s+extends\s+/))) {
+ if (m.index === 0) {
+ response.ignoreMatch();
+ // eslint-disable-next-line no-useless-return
+ return;
+ }
+ }
+ }
+ };
+ const KEYWORDS$1 = {
+ $pattern: IDENT_RE,
+ keyword: KEYWORDS,
+ literal: LITERALS,
+ built_in: BUILT_INS,
+ "variable.language": BUILT_IN_VARIABLES
+ };
+
+ // https://tc39.es/ecma262/#sec-literals-numeric-literals
+ const decimalDigits = '[0-9](_?[0-9])*';
+ const frac = `\\.(${decimalDigits})`;
+ // DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral
+ // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
+ const decimalInteger = `0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*`;
+ const NUMBER = {
+ className: 'number',
+ variants: [
+ // DecimalLiteral
+ { begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` +
+ `[eE][+-]?(${decimalDigits})\\b` },
+ { begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` },
+
+ // DecimalBigIntegerLiteral
+ { begin: `\\b(0|[1-9](_?[0-9])*)n\\b` },
+
+ // NonDecimalIntegerLiteral
+ { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b" },
+ { begin: "\\b0[bB][0-1](_?[0-1])*n?\\b" },
+ { begin: "\\b0[oO][0-7](_?[0-7])*n?\\b" },
+
+ // LegacyOctalIntegerLiteral (does not include underscore separators)
+ // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
+ { begin: "\\b0[0-7]+n?\\b" },
+ ],
+ relevance: 0
+ };
+
+ const SUBST = {
+ className: 'subst',
+ begin: '\\$\\{',
+ end: '\\}',
+ keywords: KEYWORDS$1,
+ contains: [] // defined later
+ };
+ const HTML_TEMPLATE = {
+ begin: 'html`',
+ end: '',
+ starts: {
+ end: '`',
+ returnEnd: false,
+ contains: [
+ hljs.BACKSLASH_ESCAPE,
+ SUBST
+ ],
+ subLanguage: 'xml'
+ }
+ };
+ const CSS_TEMPLATE = {
+ begin: 'css`',
+ end: '',
+ starts: {
+ end: '`',
+ returnEnd: false,
+ contains: [
+ hljs.BACKSLASH_ESCAPE,
+ SUBST
+ ],
+ subLanguage: 'css'
+ }
+ };
+ const TEMPLATE_STRING = {
+ className: 'string',
+ begin: '`',
+ end: '`',
+ contains: [
+ hljs.BACKSLASH_ESCAPE,
+ SUBST
+ ]
+ };
+ const JSDOC_COMMENT = hljs.COMMENT(
+ /\/\*\*(?!\/)/,
+ '\\*/',
+ {
+ relevance: 0,
+ contains: [
+ {
+ begin: '(?=@[A-Za-z]+)',
+ relevance: 0,
+ contains: [
+ {
+ className: 'doctag',
+ begin: '@[A-Za-z]+'
+ },
+ {
+ className: 'type',
+ begin: '\\{',
+ end: '\\}',
+ excludeEnd: true,
+ excludeBegin: true,
+ relevance: 0
+ },
+ {
+ className: 'variable',
+ begin: IDENT_RE$1 + '(?=\\s*(-)|$)',
+ endsParent: true,
+ relevance: 0
+ },
+ // eat spaces (not newlines) so we can find
+ // types or variables
+ {
+ begin: /(?=[^\n])\s/,
+ relevance: 0
+ }
+ ]
+ }
+ ]
+ }
+ );
+ const COMMENT = {
+ className: "comment",
+ variants: [
+ JSDOC_COMMENT,
+ hljs.C_BLOCK_COMMENT_MODE,
+ hljs.C_LINE_COMMENT_MODE
+ ]
+ };
+ const SUBST_INTERNALS = [
+ hljs.APOS_STRING_MODE,
+ hljs.QUOTE_STRING_MODE,
+ HTML_TEMPLATE,
+ CSS_TEMPLATE,
+ TEMPLATE_STRING,
+ // Skip numbers when they are part of a variable name
+ { match: /\$\d+/ },
+ NUMBER,
+ // This is intentional:
+ // See https://github.com/highlightjs/highlight.js/issues/3288
+ // hljs.REGEXP_MODE
+ ];
+ SUBST.contains = SUBST_INTERNALS
+ .concat({
+ // we need to pair up {} inside our subst to prevent
+ // it from ending too early by matching another }
+ begin: /\{/,
+ end: /\}/,
+ keywords: KEYWORDS$1,
+ contains: [
+ "self"
+ ].concat(SUBST_INTERNALS)
+ });
+ const SUBST_AND_COMMENTS = [].concat(COMMENT, SUBST.contains);
+ const PARAMS_CONTAINS = SUBST_AND_COMMENTS.concat([
+ // eat recursive parens in sub expressions
+ {
+ begin: /\(/,
+ end: /\)/,
+ keywords: KEYWORDS$1,
+ contains: ["self"].concat(SUBST_AND_COMMENTS)
+ }
+ ]);
+ const PARAMS = {
+ className: 'params',
+ begin: /\(/,
+ end: /\)/,
+ excludeBegin: true,
+ excludeEnd: true,
+ keywords: KEYWORDS$1,
+ contains: PARAMS_CONTAINS
+ };
+
+ // ES6 classes
+ const CLASS_OR_EXTENDS = {
+ variants: [
+ // class Car extends vehicle
+ {
+ match: [
+ /class/,
+ /\s+/,
+ IDENT_RE$1,
+ /\s+/,
+ /extends/,
+ /\s+/,
+ regex.concat(IDENT_RE$1, "(", regex.concat(/\./, IDENT_RE$1), ")*")
+ ],
+ scope: {
+ 1: "keyword",
+ 3: "title.class",
+ 5: "keyword",
+ 7: "title.class.inherited"
+ }
+ },
+ // class Car
+ {
+ match: [
+ /class/,
+ /\s+/,
+ IDENT_RE$1
+ ],
+ scope: {
+ 1: "keyword",
+ 3: "title.class"
+ }
+ },
+
+ ]
+ };
+
+ const CLASS_REFERENCE = {
+ relevance: 0,
+ match:
+ regex.either(
+ // Hard coded exceptions
+ /\bJSON/,
+ // Float32Array, OutT
+ /\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/,
+ // CSSFactory, CSSFactoryT
+ /\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/,
+ // FPs, FPsT
+ /\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/,
+ // P
+ // single letters are not highlighted
+ // BLAH
+ // this will be flagged as a UPPER_CASE_CONSTANT instead
+ ),
+ className: "title.class",
+ keywords: {
+ _: [
+ // se we still get relevance credit for JS library classes
+ ...TYPES,
+ ...ERROR_TYPES
+ ]
+ }
+ };
+
+ const USE_STRICT = {
+ label: "use_strict",
+ className: 'meta',
+ relevance: 10,
+ begin: /^\s*['"]use (strict|asm)['"]/
+ };
+
+ const FUNCTION_DEFINITION = {
+ variants: [
+ {
+ match: [
+ /function/,
+ /\s+/,
+ IDENT_RE$1,
+ /(?=\s*\()/
+ ]
+ },
+ // anonymous function
+ {
+ match: [
+ /function/,
+ /\s*(?=\()/
+ ]
+ }
+ ],
+ className: {
+ 1: "keyword",
+ 3: "title.function"
+ },
+ label: "func.def",
+ contains: [ PARAMS ],
+ illegal: /%/
+ };
+
+ const UPPER_CASE_CONSTANT = {
+ relevance: 0,
+ match: /\b[A-Z][A-Z_0-9]+\b/,
+ className: "variable.constant"
+ };
+
+ function noneOf(list) {
+ return regex.concat("(?!", list.join("|"), ")");
+ }
+
+ const FUNCTION_CALL = {
+ match: regex.concat(
+ /\b/,
+ noneOf([
+ ...BUILT_IN_GLOBALS,
+ "super",
+ "import"
+ ]),
+ IDENT_RE$1, regex.lookahead(/\(/)),
+ className: "title.function",
+ relevance: 0
+ };
+
+ const PROPERTY_ACCESS = {
+ begin: regex.concat(/\./, regex.lookahead(
+ regex.concat(IDENT_RE$1, /(?![0-9A-Za-z$_(])/)
+ )),
+ end: IDENT_RE$1,
+ excludeBegin: true,
+ keywords: "prototype",
+ className: "property",
+ relevance: 0
+ };
+
+ const GETTER_OR_SETTER = {
+ match: [
+ /get|set/,
+ /\s+/,
+ IDENT_RE$1,
+ /(?=\()/
+ ],
+ className: {
+ 1: "keyword",
+ 3: "title.function"
+ },
+ contains: [
+ { // eat to avoid empty params
+ begin: /\(\)/
+ },
+ PARAMS
+ ]
+ };
+
+ const FUNC_LEAD_IN_RE = '(\\(' +
+ '[^()]*(\\(' +
+ '[^()]*(\\(' +
+ '[^()]*' +
+ '\\)[^()]*)*' +
+ '\\)[^()]*)*' +
+ '\\)|' + hljs.UNDERSCORE_IDENT_RE + ')\\s*=>';
+
+ const FUNCTION_VARIABLE = {
+ match: [
+ /const|var|let/, /\s+/,
+ IDENT_RE$1, /\s*/,
+ /=\s*/,
+ /(async\s*)?/, // async is optional
+ regex.lookahead(FUNC_LEAD_IN_RE)
+ ],
+ keywords: "async",
+ className: {
+ 1: "keyword",
+ 3: "title.function"
+ },
+ contains: [
+ PARAMS
+ ]
+ };
+
+ return {
+ name: 'Javascript',
+ aliases: ['js', 'jsx', 'mjs', 'cjs'],
+ keywords: KEYWORDS$1,
+ // this will be extended by TypeScript
+ exports: { PARAMS_CONTAINS, CLASS_REFERENCE },
+ illegal: /#(?![$_A-z])/,
+ contains: [
+ hljs.SHEBANG({
+ label: "shebang",
+ binary: "node",
+ relevance: 5
+ }),
+ USE_STRICT,
+ hljs.APOS_STRING_MODE,
+ hljs.QUOTE_STRING_MODE,
+ HTML_TEMPLATE,
+ CSS_TEMPLATE,
+ TEMPLATE_STRING,
+ COMMENT,
+ // Skip numbers when they are part of a variable name
+ { match: /\$\d+/ },
+ NUMBER,
+ CLASS_REFERENCE,
+ {
+ className: 'attr',
+ begin: IDENT_RE$1 + regex.lookahead(':'),
+ relevance: 0
+ },
+ FUNCTION_VARIABLE,
+ { // "value" container
+ begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*',
+ keywords: 'return throw case',
+ relevance: 0,
+ contains: [
+ COMMENT,
+ hljs.REGEXP_MODE,
+ {
+ className: 'function',
+ // we have to count the parens to make sure we actually have the
+ // correct bounding ( ) before the =>. There could be any number of
+ // sub-expressions inside also surrounded by parens.
+ begin: FUNC_LEAD_IN_RE,
+ returnBegin: true,
+ end: '\\s*=>',
+ contains: [
+ {
+ className: 'params',
+ variants: [
+ {
+ begin: hljs.UNDERSCORE_IDENT_RE,
+ relevance: 0
+ },
+ {
+ className: null,
+ begin: /\(\s*\)/,
+ skip: true
+ },
+ {
+ begin: /\(/,
+ end: /\)/,
+ excludeBegin: true,
+ excludeEnd: true,
+ keywords: KEYWORDS$1,
+ contains: PARAMS_CONTAINS
+ }
+ ]
+ }
+ ]
+ },
+ { // could be a comma delimited list of params to a function call
+ begin: /,/,
+ relevance: 0
+ },
+ {
+ match: /\s+/,
+ relevance: 0
+ },
+ { // JSX
+ variants: [
+ { begin: FRAGMENT.begin, end: FRAGMENT.end },
+ { match: XML_SELF_CLOSING },
+ {
+ begin: XML_TAG.begin,
+ // we carefully check the opening tag to see if it truly
+ // is a tag and not a false positive
+ 'on:begin': XML_TAG.isTrulyOpeningTag,
+ end: XML_TAG.end
+ }
+ ],
+ subLanguage: 'xml',
+ contains: [
+ {
+ begin: XML_TAG.begin,
+ end: XML_TAG.end,
+ skip: true,
+ contains: ['self']
+ }
+ ]
+ }
+ ],
+ },
+ FUNCTION_DEFINITION,
+ {
+ // prevent this from getting swallowed up by function
+ // since they appear "function like"
+ beginKeywords: "while if switch catch for"
+ },
+ {
+ // we have to count the parens to make sure we actually have the correct
+ // bounding ( ). There could be any number of sub-expressions inside
+ // also surrounded by parens.
+ begin: '\\b(?!function)' + hljs.UNDERSCORE_IDENT_RE +
+ '\\(' + // first parens
+ '[^()]*(\\(' +
+ '[^()]*(\\(' +
+ '[^()]*' +
+ '\\)[^()]*)*' +
+ '\\)[^()]*)*' +
+ '\\)\\s*\\{', // end parens
+ returnBegin:true,
+ label: "func.def",
+ contains: [
+ PARAMS,
+ hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE$1, className: "title.function" })
+ ]
+ },
+ // catch ... so it won't trigger the property rule below
+ {
+ match: /\.\.\./,
+ relevance: 0
+ },
+ PROPERTY_ACCESS,
+ // hack: prevents detection of keywords in some circumstances
+ // .keyword()
+ // $keyword = x
+ {
+ match: '\\$' + IDENT_RE$1,
+ relevance: 0
+ },
+ {
+ match: [ /\bconstructor(?=\s*\()/ ],
+ className: { 1: "title.function" },
+ contains: [ PARAMS ]
+ },
+ FUNCTION_CALL,
+ UPPER_CASE_CONSTANT,
+ CLASS_OR_EXTENDS,
+ GETTER_OR_SETTER,
+ {
+ match: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something`
+ }
+ ]
+ };
+ }
+
+ /*
+ Language: Bash
+ Author: vah <vahtenberg@gmail.com>
+ Contributrors: Benjamin Pannell <contact@sierrasoftworks.com>
+ Website: https://www.gnu.org/software/bash/
+ Category: common
+ */
+
+ /** @type LanguageFn */
+ function bash(hljs) {
+ const regex = hljs.regex;
+ const VAR = {};
+ const BRACED_VAR = {
+ begin: /\$\{/,
+ end: /\}/,
+ contains: [
+ "self",
+ {
+ begin: /:-/,
+ contains: [ VAR ]
+ } // default values
+ ]
+ };
+ Object.assign(VAR, {
+ className: 'variable',
+ variants: [
+ { begin: regex.concat(/\$[\w\d#@][\w\d_]*/,
+ // negative look-ahead tries to avoid matching patterns that are not
+ // Perl at all like $ident$, @ident@, etc.
+ `(?![\\w\\d])(?![$])`) },
+ BRACED_VAR
+ ]
+ });
+
+ const SUBST = {
+ className: 'subst',
+ begin: /\$\(/,
+ end: /\)/,
+ contains: [ hljs.BACKSLASH_ESCAPE ]
+ };
+ const HERE_DOC = {
+ begin: /<<-?\s*(?=\w+)/,
+ starts: { contains: [
+ hljs.END_SAME_AS_BEGIN({
+ begin: /(\w+)/,
+ end: /(\w+)/,
+ className: 'string'
+ })
+ ] }
+ };
+ const QUOTE_STRING = {
+ className: 'string',
+ begin: /"/,
+ end: /"/,
+ contains: [
+ hljs.BACKSLASH_ESCAPE,
+ VAR,
+ SUBST
+ ]
+ };
+ SUBST.contains.push(QUOTE_STRING);
+ const ESCAPED_QUOTE = {
+ className: '',
+ begin: /\\"/
+
+ };
+ const APOS_STRING = {
+ className: 'string',
+ begin: /'/,
+ end: /'/
+ };
+ const ARITHMETIC = {
+ begin: /\$?\(\(/,
+ end: /\)\)/,
+ contains: [
+ {
+ begin: /\d+#[0-9a-f]+/,
+ className: "number"
+ },
+ hljs.NUMBER_MODE,
+ VAR
+ ]
+ };
+ const SH_LIKE_SHELLS = [
+ "fish",
+ "bash",
+ "zsh",
+ "sh",
+ "csh",
+ "ksh",
+ "tcsh",
+ "dash",
+ "scsh",
+ ];
+ const KNOWN_SHEBANG = hljs.SHEBANG({
+ binary: `(${SH_LIKE_SHELLS.join("|")})`,
+ relevance: 10
+ });
+ const FUNCTION = {
+ className: 'function',
+ begin: /\w[\w\d_]*\s*\(\s*\)\s*\{/,
+ returnBegin: true,
+ contains: [ hljs.inherit(hljs.TITLE_MODE, { begin: /\w[\w\d_]*/ }) ],
+ relevance: 0
+ };
+
+ const KEYWORDS = [
+ "if",
+ "then",
+ "else",
+ "elif",
+ "fi",
+ "for",
+ "while",
+ "in",
+ "do",
+ "done",
+ "case",
+ "esac",
+ "function"
+ ];
+
+ const LITERALS = [
+ "true",
+ "false"
+ ];
+
+ // to consume paths to prevent keyword matches inside them
+ const PATH_MODE = { match: /(\/[a-z._-]+)+/ };
+
+ // http://www.gnu.org/software/bash/manual/html_node/Shell-Builtin-Commands.html
+ const SHELL_BUILT_INS = [
+ "break",
+ "cd",
+ "continue",
+ "eval",
+ "exec",
+ "exit",
+ "export",
+ "getopts",
+ "hash",
+ "pwd",
+ "readonly",
+ "return",
+ "shift",
+ "test",
+ "times",
+ "trap",
+ "umask",
+ "unset"
+ ];
+
+ const BASH_BUILT_INS = [
+ "alias",
+ "bind",
+ "builtin",
+ "caller",
+ "command",
+ "declare",
+ "echo",
+ "enable",
+ "help",
+ "let",
+ "local",
+ "logout",
+ "mapfile",
+ "printf",
+ "read",
+ "readarray",
+ "source",
+ "type",
+ "typeset",
+ "ulimit",
+ "unalias"
+ ];
+
+ const ZSH_BUILT_INS = [
+ "autoload",
+ "bg",
+ "bindkey",
+ "bye",
+ "cap",
+ "chdir",
+ "clone",
+ "comparguments",
+ "compcall",
+ "compctl",
+ "compdescribe",
+ "compfiles",
+ "compgroups",
+ "compquote",
+ "comptags",
+ "comptry",
+ "compvalues",
+ "dirs",
+ "disable",
+ "disown",
+ "echotc",
+ "echoti",
+ "emulate",
+ "fc",
+ "fg",
+ "float",
+ "functions",
+ "getcap",
+ "getln",
+ "history",
+ "integer",
+ "jobs",
+ "kill",
+ "limit",
+ "log",
+ "noglob",
+ "popd",
+ "print",
+ "pushd",
+ "pushln",
+ "rehash",
+ "sched",
+ "setcap",
+ "setopt",
+ "stat",
+ "suspend",
+ "ttyctl",
+ "unfunction",
+ "unhash",
+ "unlimit",
+ "unsetopt",
+ "vared",
+ "wait",
+ "whence",
+ "where",
+ "which",
+ "zcompile",
+ "zformat",
+ "zftp",
+ "zle",
+ "zmodload",
+ "zparseopts",
+ "zprof",
+ "zpty",
+ "zregexparse",
+ "zsocket",
+ "zstyle",
+ "ztcp"
+ ];
+
+ const GNU_CORE_UTILS = [
+ "chcon",
+ "chgrp",
+ "chown",
+ "chmod",
+ "cp",
+ "dd",
+ "df",
+ "dir",
+ "dircolors",
+ "ln",
+ "ls",
+ "mkdir",
+ "mkfifo",
+ "mknod",
+ "mktemp",
+ "mv",
+ "realpath",
+ "rm",
+ "rmdir",
+ "shred",
+ "sync",
+ "touch",
+ "truncate",
+ "vdir",
+ "b2sum",
+ "base32",
+ "base64",
+ "cat",
+ "cksum",
+ "comm",
+ "csplit",
+ "cut",
+ "expand",
+ "fmt",
+ "fold",
+ "head",
+ "join",
+ "md5sum",
+ "nl",
+ "numfmt",
+ "od",
+ "paste",
+ "ptx",
+ "pr",
+ "sha1sum",
+ "sha224sum",
+ "sha256sum",
+ "sha384sum",
+ "sha512sum",
+ "shuf",
+ "sort",
+ "split",
+ "sum",
+ "tac",
+ "tail",
+ "tr",
+ "tsort",
+ "unexpand",
+ "uniq",
+ "wc",
+ "arch",
+ "basename",
+ "chroot",
+ "date",
+ "dirname",
+ "du",
+ "echo",
+ "env",
+ "expr",
+ "factor",
+ // "false", // keyword literal already
+ "groups",
+ "hostid",
+ "id",
+ "link",
+ "logname",
+ "nice",
+ "nohup",
+ "nproc",
+ "pathchk",
+ "pinky",
+ "printenv",
+ "printf",
+ "pwd",
+ "readlink",
+ "runcon",
+ "seq",
+ "sleep",
+ "stat",
+ "stdbuf",
+ "stty",
+ "tee",
+ "test",
+ "timeout",
+ // "true", // keyword literal already
+ "tty",
+ "uname",
+ "unlink",
+ "uptime",
+ "users",
+ "who",
+ "whoami",
+ "yes"
+ ];
+
+ return {
+ name: 'Bash',
+ aliases: [ 'sh' ],
+ keywords: {
+ $pattern: /\b[a-z][a-z0-9._-]+\b/,
+ keyword: KEYWORDS,
+ literal: LITERALS,
+ built_in: [
+ ...SHELL_BUILT_INS,
+ ...BASH_BUILT_INS,
+ // Shell modifiers
+ "set",
+ "shopt",
+ ...ZSH_BUILT_INS,
+ ...GNU_CORE_UTILS
+ ]
+ },
+ contains: [
+ KNOWN_SHEBANG, // to catch known shells and boost relevancy
+ hljs.SHEBANG(), // to catch unknown shells but still highlight the shebang
+ FUNCTION,
+ ARITHMETIC,
+ hljs.HASH_COMMENT_MODE,
+ HERE_DOC,
+ PATH_MODE,
+ QUOTE_STRING,
+ ESCAPED_QUOTE,
+ APOS_STRING,
+ VAR
+ ]
+ };
+ }
+
+ var builtIns = /*#__PURE__*/Object.freeze({
+ __proto__: null,
+ grmr_gleam: gleam,
+ grmr_erlang: erlang,
+ grmr_xml: xml,
+ grmr_css: css,
+ grmr_javascript: javascript,
+ grmr_bash: bash
+ });
+
+ const hljs = HighlightJS;
+
+ for (const key of Object.keys(builtIns)) {
+ // our builtInLanguages Rollup plugin has to use `_` to allow identifiers to be
+ // compatible with `export` naming conventions, so we need to convert the
+ // identifiers back into the more typical dash style that we use for language
+ // naming via the API
+ const languageName = key.replace("grmr_", "").replace("_", "-");
+ hljs.registerLanguage(languageName, builtIns[key]);
+ }
+
+ return hljs;
+
+})();
+if (typeof exports === 'object' && typeof module !== 'undefined') { module.exports = hljs; }
diff --git a/compat/lustre_animation/index.html b/compat/lustre_animation/index.html
new file mode 100644
index 0000000..5d15471
--- /dev/null
+++ b/compat/lustre_animation/index.html
@@ -0,0 +1,95 @@
+<html>
+
+<head>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Lustre Animation Example</title>
+ <style>
+ head,
+ body {
+ margin: 0;
+ padding: 1em;
+ background: linear-gradient(to bottom, #ffaff3, white);
+ }
+ </style>
+ <link rel="stylesheet" href="lustre_animation.css" />
+</head>
+
+<body>
+ <script type="text/javascript" src="highlight.js"></script>
+ <h1>Lustre Animation</h1>
+ You can use this package within Gleam with
+ <pre><code class="language-bash">gleam add lustre_animation</code></pre>
+ You can get the repository with
+ <pre><code class="no-highlight">git clone https://git.chmeee.org/lustre_animation</code></pre>
+ <h1>Usage</h1>
+ The basic usage is
+ <ol>
+ <li>keep 1 `Animations` value</li>
+ <li>add animations by some trigger</li>
+ <li>call `animation.cmd()` on your value and let your lustre `update()` return it.
+ This will cause a dispatch on the next JS animation frame, unless all animations have finished (and
+ auto-removed).</li>
+ <li>update the animations with the new time.</li>
+ <li>evaluate for each animation you are interested in.</li>
+ </ol>
+ e.g. like this:
+ <pre><code class="language-gleam">import lustre/animation as a
+
+pub type Msg {
+ Trigger
+ Tick(Float)
+}
+
+pub fn update(model: Model, msg: Msg) {
+ let m = case msg of {
+ Trigger -> {
+ let new_anim = a.add(model.animations, "id", 1.0, 2.0, 0.250)
+ Model(1.0, animations: new_anim)
+ }
+ Tick(t) -> {
+ let new_anim = a.tick(model.animations, t)
+ let new_value = a.value(new_anim, "id", model.value)
+ Model(new_value, new_anim)
+ }
+ }
+ #(m, animation.cmd(m.animations, Tick))
+}</code></pre>
+
+ In the above <code>type Model</code>, <code>init</code> and <code>render</code> have been omitted.
+
+ <h1>Examples</h1>
+ Below are two working examples, of which the code is in the git repository.
+ <div style="display: flex; flex-wrap: wrap; overflow-y: auto; overflow-x: hidden;">
+ <div style="width: 320; margin-right: 2em;">
+ <h2>1. Two independent animations</h2>
+ <p>Click on the four buttons to manipulate the text horizontally,
+ and vertically.</p>
+ <p>See <code>./test/example_move_around.gleam</code></p>
+ <div id="root"
+ style="width: calc(320px - 2em); height: 480; border: 2px solid grey; padding: 0 1em 1em 1em;">
+ <h1>Lustre Animation Example</h1>
+ Please wait for the JavaScript (compiled Gleam) to take over.
+ <noscript>
+ <p>This site needs JavaScript to run, please enable/allow it.</p>
+ </noscript>
+ </div>
+ </div>
+
+ <div style="width: 320;">
+ <h2>2. Many animations that are all 'the same'</h2>
+ <p>Each drop behaves in the same way. Click in the area to generate them.</p>
+ <p>See <code>./test/example_drops.gleam</code></p>
+ <div id="drops" style="width: calc(320px - 2em); height: 480; border: 2px solid grey; padding: 1em;">
+ <h1>Lustre Animation Example</h1>
+ Please wait for the JavaScript (compiled Gleam) to take over.
+ <noscript>
+ <p>This site needs JavaScript to run, please enable/allow it.</p>
+ </noscript>
+ </div>
+ </div>
+ </div>
+ <script type="text/javascript" src="run_highlight.js"></script>
+ <script src="./test/start_examples.mjs" type="module"></script>
+</body>
+
+</html>
diff --git a/compat/lustre_animation/lustre_animation.css b/compat/lustre_animation/lustre_animation.css
new file mode 100644
index 0000000..3e6bc47
--- /dev/null
+++ b/compat/lustre_animation/lustre_animation.css
@@ -0,0 +1,90 @@
+code {
+ color: #333;
+ border: 1px solid #847;
+ border-radius: 0.5ex;
+ padding: 0 0.5ex;
+}
+
+pre code {
+ border: 0;
+ padding: 0;
+}
+
+pre {
+ background: linear-gradient(to bottom, white, #ffaff3);
+ overflow-x: auto;
+ border: 1px solid #847;
+ border-radius: 1ex;
+ padding: 0.5ex 1.5ex;
+ margin: 1ex 0;
+}
+
+.hljs-comment,
+.hljs-quote {
+ color: #a0a1a7;
+ font-style: italic
+}
+
+.hljs-doctag,
+.hljs-formula,
+.hljs-keyword {
+ color: #a626a4
+}
+
+.hljs-deletion,
+.hljs-name,
+.hljs-section,
+.hljs-selector-tag,
+.hljs-subst {
+ color: #e45649
+}
+
+.hljs-literal {
+ color: #0184bb
+}
+
+.hljs-addition,
+.hljs-attribute,
+.hljs-meta .hljs-string,
+.hljs-regexp,
+.hljs-string {
+ color: #50a14f
+}
+
+.hljs-attr,
+.hljs-number,
+.hljs-selector-attr,
+.hljs-selector-class,
+.hljs-selector-pseudo,
+.hljs-template-variable,
+.hljs-type,
+.hljs-variable {
+ color: #986801
+}
+
+.hljs-bullet,
+.hljs-link,
+.hljs-meta,
+.hljs-selector-id,
+.hljs-symbol,
+.hljs-title {
+ color: #4078f2
+}
+
+.hljs-built_in,
+.hljs-class .hljs-title,
+.hljs-title.class_ {
+ color: #c18401
+}
+
+.hljs-emphasis {
+ font-style: italic
+}
+
+.hljs-strong {
+ font-weight: 700
+}
+
+.hljs-link {
+ text-decoration: underline
+}
diff --git a/compat/lustre_animation/manifest.toml b/compat/lustre_animation/manifest.toml
new file mode 100644
index 0000000..d420871
--- /dev/null
+++ b/compat/lustre_animation/manifest.toml
@@ -0,0 +1,13 @@
+# This file was generated by Gleam
+# You typically do not need to edit this file
+
+packages = [
+ { name = "gleam_stdlib", version = "0.30.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "704258528887F95075FFED7AAE1CCF836A9B88E3AADA2F69F9DA15815F94A4F9" },
+ { name = "gleeunit", version = "0.10.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "ECEA2DE4BE6528D36AFE74F42A21CDF99966EC36D7F25DEB34D47DD0F7977BAF" },
+ { name = "lustre", version = "3.0.0-rc.8", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "/Users/hayleigh/dev/gleam/lustre/lib" },
+]
+
+[requirements]
+gleam_stdlib = { version = "~> 0.30" }
+gleeunit = { version = "~> 0.10" }
+lustre = { path = "../../lib" }
diff --git a/compat/lustre_animation/package-lock.json b/compat/lustre_animation/package-lock.json
new file mode 100644
index 0000000..5c43ad9
--- /dev/null
+++ b/compat/lustre_animation/package-lock.json
@@ -0,0 +1,790 @@
+{
+ "name": "lustre_animation",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "vite": "^4.4.9"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+ "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+ "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+ "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+ "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+ "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+ "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+ "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+ "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+ "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+ "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+ "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+ "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+ "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+ "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+ "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+ "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+ "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+ "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+ "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+ "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.18.20",
+ "@esbuild/android-arm64": "0.18.20",
+ "@esbuild/android-x64": "0.18.20",
+ "@esbuild/darwin-arm64": "0.18.20",
+ "@esbuild/darwin-x64": "0.18.20",
+ "@esbuild/freebsd-arm64": "0.18.20",
+ "@esbuild/freebsd-x64": "0.18.20",
+ "@esbuild/linux-arm": "0.18.20",
+ "@esbuild/linux-arm64": "0.18.20",
+ "@esbuild/linux-ia32": "0.18.20",
+ "@esbuild/linux-loong64": "0.18.20",
+ "@esbuild/linux-mips64el": "0.18.20",
+ "@esbuild/linux-ppc64": "0.18.20",
+ "@esbuild/linux-riscv64": "0.18.20",
+ "@esbuild/linux-s390x": "0.18.20",
+ "@esbuild/linux-x64": "0.18.20",
+ "@esbuild/netbsd-x64": "0.18.20",
+ "@esbuild/openbsd-x64": "0.18.20",
+ "@esbuild/sunos-x64": "0.18.20",
+ "@esbuild/win32-arm64": "0.18.20",
+ "@esbuild/win32-ia32": "0.18.20",
+ "@esbuild/win32-x64": "0.18.20"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "node_modules/postcss": {
+ "version": "8.4.28",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz",
+ "integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "3.28.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz",
+ "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==",
+ "dev": true,
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "4.4.9",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
+ "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.18.10",
+ "postcss": "^8.4.27",
+ "rollup": "^3.27.1"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 14",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ }
+ },
+ "dependencies": {
+ "@esbuild/android-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+ "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/android-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+ "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/android-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+ "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/darwin-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+ "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/darwin-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+ "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/freebsd-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+ "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/freebsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+ "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+ "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+ "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+ "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-loong64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+ "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-mips64el": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+ "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-ppc64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+ "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-riscv64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+ "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-s390x": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+ "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/linux-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+ "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/netbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/openbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/sunos-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+ "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+ "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+ "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+ "dev": true,
+ "optional": true
+ },
+ "@esbuild/win32-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+ "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+ "dev": true,
+ "optional": true
+ },
+ "esbuild": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+ "dev": true,
+ "requires": {
+ "@esbuild/android-arm": "0.18.20",
+ "@esbuild/android-arm64": "0.18.20",
+ "@esbuild/android-x64": "0.18.20",
+ "@esbuild/darwin-arm64": "0.18.20",
+ "@esbuild/darwin-x64": "0.18.20",
+ "@esbuild/freebsd-arm64": "0.18.20",
+ "@esbuild/freebsd-x64": "0.18.20",
+ "@esbuild/linux-arm": "0.18.20",
+ "@esbuild/linux-arm64": "0.18.20",
+ "@esbuild/linux-ia32": "0.18.20",
+ "@esbuild/linux-loong64": "0.18.20",
+ "@esbuild/linux-mips64el": "0.18.20",
+ "@esbuild/linux-ppc64": "0.18.20",
+ "@esbuild/linux-riscv64": "0.18.20",
+ "@esbuild/linux-s390x": "0.18.20",
+ "@esbuild/linux-x64": "0.18.20",
+ "@esbuild/netbsd-x64": "0.18.20",
+ "@esbuild/openbsd-x64": "0.18.20",
+ "@esbuild/sunos-x64": "0.18.20",
+ "@esbuild/win32-arm64": "0.18.20",
+ "@esbuild/win32-ia32": "0.18.20",
+ "@esbuild/win32-x64": "0.18.20"
+ }
+ },
+ "fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "optional": true
+ },
+ "nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "dev": true
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "postcss": {
+ "version": "8.4.28",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.28.tgz",
+ "integrity": "sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==",
+ "dev": true,
+ "requires": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "rollup": {
+ "version": "3.28.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz",
+ "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==",
+ "dev": true,
+ "requires": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "dev": true
+ },
+ "vite": {
+ "version": "4.4.9",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
+ "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
+ "dev": true,
+ "requires": {
+ "esbuild": "^0.18.10",
+ "fsevents": "~2.3.2",
+ "postcss": "^8.4.27",
+ "rollup": "^3.27.1"
+ }
+ }
+ }
+}
diff --git a/compat/lustre_animation/package.json b/compat/lustre_animation/package.json
new file mode 100644
index 0000000..a0831d3
--- /dev/null
+++ b/compat/lustre_animation/package.json
@@ -0,0 +1,9 @@
+{
+ "scripts": {
+ "dev": "gleam build && vite",
+ "build": "gleam build && vite build"
+ },
+ "devDependencies": {
+ "vite": "^4.4.9"
+ }
+}
diff --git a/compat/lustre_animation/run_highlight.js b/compat/lustre_animation/run_highlight.js
new file mode 100644
index 0000000..d73c817
--- /dev/null
+++ b/compat/lustre_animation/run_highlight.js
@@ -0,0 +1,3 @@
+document.querySelectorAll('code').forEach((el) => {
+ hljs.highlightElement(el);
+});
diff --git a/compat/lustre_animation/src/ffi.mjs b/compat/lustre_animation/src/ffi.mjs
new file mode 100644
index 0000000..fd5d9d3
--- /dev/null
+++ b/compat/lustre_animation/src/ffi.mjs
@@ -0,0 +1,2 @@
+export const request_animation_frame = f => requestAnimationFrame(f)
+export const cancel_animation_frame = id => cancelAnimationFrame(id) \ No newline at end of file
diff --git a/compat/lustre_animation/src/lustre/animation.gleam b/compat/lustre_animation/src/lustre/animation.gleam
new file mode 100644
index 0000000..8f3312e
--- /dev/null
+++ b/compat/lustre_animation/src/lustre/animation.gleam
@@ -0,0 +1,170 @@
+import lustre/effect.{Effect}
+import gleam/list.{filter, find, map}
+
+/// A singleton holding all your animations, and a timestamp
+///
+/// Hayleigh note: since the stdlib got some improvmements a while ago, I think
+/// you could use a `Map(String, Animation)` now instead of `List.find`ing all
+/// over the place.
+pub opaque type Animations {
+ Animations(t: Float, List(Animation))
+}
+
+type Animation {
+ Animation(name: String, range: #(Float, Float), state: AnimationState)
+}
+
+// InParallel(Animations)
+// InSequence(Animations)
+// Repeat(Animation)
+// Cancelled(Animation)
+
+// The State Done will guarantee the `stop` value is returned, before the Animation is removed from the list
+type AnimationState {
+ NotStarted(seconds: Float)
+ Running(seconds: Float, since: Float)
+ Done
+}
+
+pub fn new() {
+ Animations(0.0, [])
+}
+
+/// Add an animation (linear interpolation) from `start` to `stop`, for `seconds` duration.
+/// The `name` should be unique, so the animation can be retrieved and evaluated by `value()` later.
+/// The animation is started when a subsequent command from `effect()` is returned by your
+/// lustre `update()` function; followed by a call to `tick()`.
+pub fn add(
+ animations: Animations,
+ name,
+ start: Float,
+ stop: Float,
+ seconds: Float,
+) -> Animations {
+ use list <- change_list(animations)
+ [Animation(name, #(start, stop), NotStarted(seconds)), ..list]
+}
+
+/// Remove an animation if it should stop before it is finished.
+/// If an animation runs to its end normally, you do not have to `remove()` it manually,
+/// will be automatically removed by `tick()`.
+pub fn remove(animations: Animations, name) -> Animations {
+ use list <- change_list(animations)
+ filter(list, does_not_have_name(_, name))
+ // Would not filter, but replace w/ Cancelled and filtered in the next call, see comments at bottom of this file
+}
+
+fn change_list(
+ animations: Animations,
+ f: fn(List(Animation)) -> List(Animation),
+) -> Animations {
+ let assert Animations(t, list) = animations
+ Animations(t, f(list))
+}
+
+fn does_not_have_name(animation: Animation, name: String) {
+ let assert Animation(n, _, _) = animation
+ n != name
+}
+
+/// When called for the first time on an animation, its start time is
+/// set to time-Offset. Its stop time then is the start time plus the
+/// duration.
+///
+/// When called for the *first* time for animations that have finished,
+/// they will be marked as such in the result. `value()` will return the `stop` value
+/// that you passed with `add()`
+///
+/// When called the *second* time for animations that have finished, they will be absent
+/// from the returned value.
+pub fn tick(animations: Animations, time_offset) -> Animations {
+ let assert Animations(_, list) = animations
+ let new_list =
+ list
+ |> filter(not_done)
+ |> map(tick_animation(_, time_offset))
+ Animations(time_offset, new_list)
+}
+
+fn not_done(animation: Animation) -> Bool {
+ case animation {
+ Animation(_, _, Done) -> False
+ _ -> True
+ }
+}
+
+fn tick_animation(animation: Animation, time: Float) -> Animation {
+ let assert Animation(name, range, state) = animation
+ let new_state = case state {
+ NotStarted(seconds) -> Running(seconds, since: time)
+ Running(seconds, since) ->
+ case time -. since >=. seconds *. 1000.0 {
+ True -> Done
+ False -> state
+ }
+ Done -> Done
+ }
+ Animation(name, range, new_state)
+}
+
+/// Returns `effect.none()` if none of the animations is running.
+/// Otherwise returns an opaque `Effect` that will cause `msg(time)` to
+/// be dispatched on a JavaScript `requestAnimationFrame`
+///
+/// Hayleigh note: Maybe this could have a better name like `next_tick` or
+/// `request_tick`?
+pub fn effect(animations: Animations, msg: fn(Float) -> m) -> Effect(m) {
+ case animations {
+ Animations(_, []) -> effect.none()
+ _ -> request_animation_frame(msg)
+ }
+}
+
+/// If the animation specified by `which` is not found, returns `default`.
+/// Otherwise, the interpolated value for the `time` passed to `tick()` is returned.
+pub fn value(animations: Animations, which: String, default: Float) -> Float {
+ let assert Animations(t, list) = animations
+ case find(list, has_name(_, which)) {
+ Ok(animation) -> evaluate(animation, t)
+ Error(Nil) -> default
+ }
+}
+
+fn has_name(animation: Animation, name: String) {
+ let assert Animation(n, _, _) = animation
+ n == name
+}
+
+fn evaluate(animation: Animation, time: Float) -> Float {
+ let assert Animation(_, #(start, stop), state) = animation
+ case state {
+ NotStarted(_) -> start
+ Running(seconds, since) -> {
+ let dt = time -. since
+ let delta = dt /. { seconds *. 1000.0 }
+ start +. { stop -. start } *. delta
+ }
+ Done -> stop
+ }
+}
+
+pub fn request_animation_frame(msg: fn(Float) -> m) -> Effect(m) {
+ effect.from(fn(dispatch) {
+ js_request_animation_frame(fn(time_offset: Float) {
+ dispatch(msg(time_offset))
+ })
+ Nil
+ })
+}
+
+pub type RequestedFrame
+
+// The returned 'long' can be passed to 'cancelAnimationFrame' - except we do not have any means to
+// TODO Push the 'long' down into JS land, with the Animation, so we can
+// make a mapping from Animation to RequestFrame and a `cancelFrame(Animation, msg) -> Effect(m)`
+// that (again in JS land) *can* cancel the appropriate request frame.
+external fn js_request_animation_frame(f) -> RequestedFrame =
+ "../ffi.mjs" "request_animation_frame"
+
+pub external fn cancel_animation_frame(frame: RequestedFrame) -> Nil =
+ "../ffi.mjs" "cancel_animation_frame"
diff --git a/compat/lustre_animation/test/example_drops.gleam b/compat/lustre_animation/test/example_drops.gleam
new file mode 100644
index 0000000..dd69f5c
--- /dev/null
+++ b/compat/lustre_animation/test/example_drops.gleam
@@ -0,0 +1,120 @@
+import lustre
+import lustre/animation.{Animations}
+import lustre/attribute.{id, style}
+import lustre/effect.{Effect}
+import lustre/element.{Element, text}
+import lustre/element/html.{div, h3}
+import lustre/event.{on}
+import gleam/float
+import gleam/int
+import gleam/list.{filter, map}
+import gleam/dynamic.{Dynamic} as d
+import gleam/option.{Some}
+
+pub type Msg {
+ Click(x: Float, y: Float)
+ Tick(time_offset: Float)
+}
+
+pub type Drop {
+ Drop(id: String, x: Float, y: Float, r: Float)
+}
+
+pub type Model {
+ Model(counter: Int, drops: List(Drop), animations: Animations)
+}
+
+pub fn main() {
+ lustre.application(init, update, render)
+ |> lustre.start("#drops", Nil)
+}
+
+fn init(_) {
+ #(Model(0, [], animation.new()), effect.none())
+}
+
+const to_s = int.to_string
+
+pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
+ let m = case msg {
+ Click(x, y) -> {
+ let id = "drop" <> to_s(model.counter)
+ let new_animations = animation.add(model.animations, id, 0.0, 1.0, 1.5)
+ let new_drop = Drop(id, x, y, 0.0)
+ Model(model.counter + 1, [new_drop, ..model.drops], new_animations)
+ }
+ Tick(time_offset) -> {
+ let new_animations = animation.tick(model.animations, time_offset)
+ let new_drops =
+ model.drops
+ |> filter(fn(drop) { drop.r != 1.0 })
+ |> map(fn(drop) {
+ let r = animation.value(new_animations, drop.id, drop.r)
+ Drop(..drop, r: r)
+ })
+ Model(..model, drops: new_drops, animations: new_animations)
+ }
+ }
+ #(m, animation.effect(m.animations, Tick))
+}
+
+pub fn render(model: Model) -> Element(Msg) {
+ div(
+ [
+ style([
+ #("width", "100%"),
+ #("height", "100%"),
+ #("display", "grid"),
+ #("grid-template-rows", "auto 1fr"),
+ ]),
+ ],
+ [
+ h3([style([#("text-align", "center")])], [text("Click to make drops")]),
+ div(
+ [
+ id("pond"),
+ style([#("position", "relative")]),
+ on(
+ "mouseDown",
+ fn(event) {
+ let assert Ok(x) = d.field("clientX", d.float)(event)
+ let assert Ok(y) = d.field("clientY", d.float)(event)
+ let rect = bounding_client_rect("pond")
+ let assert Ok(top) = d.field("top", d.float)(rect)
+ let assert Ok(left) = d.field("left", d.float)(rect)
+
+ Some(Click(x -. left, y -. top))
+ },
+ ),
+ ],
+ map(model.drops, render_drop),
+ ),
+ ],
+ )
+}
+
+fn render_drop(drop: Drop) {
+ let r = drop.r *. 50.0
+ let rad = float.to_string(r *. 2.0)
+ let rw = float.to_string(drop.r *. 2.5)
+ let alpha =
+ 1.0 -. drop.r
+ |> float.to_string
+ div(
+ [
+ style([
+ #("position", "absolute"),
+ #("left", float.to_string(drop.x -. r) <> "px"),
+ #("top", float.to_string(drop.y -. r) <> "px"),
+ #("width", rad <> "px"),
+ #("height", rad <> "px"),
+ #("border", rw <> "px solid rgba(0, 0, 0, " <> alpha <> ")"),
+ #("border-radius", "50%"),
+ ]),
+ ],
+ [],
+ )
+}
+
+external fn bounding_client_rect(String) -> Dynamic =
+ "./info.mjs" "bounding_client_rect"
diff --git a/compat/lustre_animation/test/example_two_independent.gleam b/compat/lustre_animation/test/example_two_independent.gleam
new file mode 100644
index 0000000..61e687d
--- /dev/null
+++ b/compat/lustre_animation/test/example_two_independent.gleam
@@ -0,0 +1,103 @@
+import lustre
+import lustre/animation.{Animations}
+import lustre/attribute.{style}
+import lustre/effect.{Effect}
+import lustre/element.{text}
+import lustre/element/html.{button, div, h3, span}
+import lustre/event.{on_click}
+import gleam/float
+
+pub type Msg {
+ Left
+ Right
+ Top
+ Bottom
+ Tick(time_offset: Float)
+}
+
+pub type Model {
+ Model(x: Float, y: Float, animations: Animations)
+}
+
+pub fn main() {
+ lustre.application(init, update, render)
+ |> lustre.start("#root", Nil)
+}
+
+fn init(_) {
+ #(Model(0.5, 0.5, animation.new()), effect.none())
+}
+
+pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
+ let m = case msg {
+ Left -> {
+ let new_animations =
+ animation.add(model.animations, "x", model.x, 0.0, 2.5)
+ Model(..model, animations: new_animations)
+ }
+ Right -> {
+ let new_animations =
+ animation.add(model.animations, "x", model.x, 1.0, 2.5)
+ Model(..model, animations: new_animations)
+ }
+ Top -> {
+ let new_animations =
+ animation.add(model.animations, "y", model.y, 0.0, 2.5)
+ Model(..model, animations: new_animations)
+ }
+ Bottom -> {
+ let new_animations =
+ animation.add(model.animations, "y", model.y, 1.0, 2.5)
+ Model(..model, animations: new_animations)
+ }
+ Tick(time_offset) -> {
+ let new_animations = animation.tick(model.animations, time_offset)
+ let x = animation.value(new_animations, "x", model.x)
+ let y = animation.value(new_animations, "y", model.y)
+ Model(x: x, y: y, animations: new_animations)
+ }
+ }
+ #(m, animation.effect(m.animations, Tick))
+}
+
+pub fn render(model: Model) {
+ let sp = span([], [])
+ let to_s = float.to_string
+ div(
+ [
+ style([
+ #("display", "grid"),
+ #("grid-template-rows", "1fr auto"),
+ #("width", "100%"),
+ #("height", "100%"),
+ ]),
+ ],
+ [
+ div(
+ [
+ style([
+ #("display", "grid"),
+ #(
+ "grid-template-rows",
+ to_s(model.y) <> "fr auto " <> to_s(1.0 -. model.y) <> "fr",
+ ),
+ #(
+ "grid-template-columns",
+ to_s(model.x) <> "fr auto " <> to_s(1.0 -. model.x) <> "fr",
+ ),
+ ]),
+ ],
+ [sp, sp, sp, sp, h3([], [text("Move me around")]), sp, sp, sp, sp],
+ ),
+ div(
+ [],
+ [
+ button([on_click(Left)], [text("Move to the Left")]),
+ button([on_click(Right)], [text("Move to the Right")]),
+ button([on_click(Top)], [text("Move to the Top")]),
+ button([on_click(Bottom)], [text("Move to the Bottom")]),
+ ],
+ ),
+ ],
+ )
+}
diff --git a/compat/lustre_animation/test/info.mjs b/compat/lustre_animation/test/info.mjs
new file mode 100644
index 0000000..d58e440
--- /dev/null
+++ b/compat/lustre_animation/test/info.mjs
@@ -0,0 +1,2 @@
+export const bounding_client_rect = id => document.getElementById(id).getBoundingClientRect()
+
diff --git a/compat/lustre_animation/test/lustre/animation_test.gleam b/compat/lustre_animation/test/lustre/animation_test.gleam
new file mode 100644
index 0000000..98ded63
--- /dev/null
+++ b/compat/lustre_animation/test/lustre/animation_test.gleam
@@ -0,0 +1,39 @@
+import gleeunit/should
+import lustre/animation.{add, new, tick, value}
+
+pub fn evaluate_test() {
+ new()
+ |> add("a", 7.0, 42.0, 2.0)
+ |> value("a", 0.0)
+ |> should.equal(7.0)
+}
+
+pub fn evaluate_start_test() {
+ new()
+ |> add("a", 7.0, 42.0, 2.0)
+ |> tick(1000.0)
+ |> value("a", 0.0)
+ |> should.equal(7.0)
+}
+
+pub fn evaluate_done_test() {
+ new()
+ |> add("a", 7.0, 42.0, 2.0)
+ |> tick(1000.0)
+ |> tick(3000.0)
+ |> value("a", 0.0)
+ |> should.equal(42.0)
+}
+
+pub fn evaluate_gone_test() {
+ new()
+ |> add("a", 7.0, 42.0, 2.0)
+ // start
+ |> tick(1000.0)
+ // Done
+ |> tick(3000.0001)
+ // remove
+ |> tick(3000.1)
+ |> value("a", 0.0)
+ |> should.equal(0.0)
+}
diff --git a/compat/lustre_animation/test/lustre_animation_test.gleam b/compat/lustre_animation/test/lustre_animation_test.gleam
new file mode 100644
index 0000000..ecd12ad
--- /dev/null
+++ b/compat/lustre_animation/test/lustre_animation_test.gleam
@@ -0,0 +1,5 @@
+import gleeunit
+
+pub fn main() {
+ gleeunit.main()
+}
diff --git a/compat/lustre_animation/test/start_examples.mjs b/compat/lustre_animation/test/start_examples.mjs
new file mode 100644
index 0000000..6d792c7
--- /dev/null
+++ b/compat/lustre_animation/test/start_examples.mjs
@@ -0,0 +1,5 @@
+import * as two from "../build/dev/javascript/lustre_animation/example_two_independent.mjs";
+import * as drops from "../build/dev/javascript/lustre_animation/example_drops.mjs";
+
+two.main();
+drops.main();