From 4eea9697c6718552f7fbe1061b22cbd80dcacf8f Mon Sep 17 00:00:00 2001
From: Hannes Wallnoefer <hannes.wallnoefer@oracle.com>
Date: Wed, 2 Apr 2025 15:06:22 +0200
Subject: [PATCH 1/6] 8348282: Add option for syntax highlighting in javadoc
 snippets

---
 make/Docs.gmk                                 |    2 +-
 .../doclets/formats/html/HtmlDoclet.java      |    8 +-
 .../formats/html/HtmlDocletWriter.java        |    1 +
 .../doclets/formats/html/HtmlOptions.java     |   22 +-
 .../doclets/formats/html/markup/Head.java     |   16 +
 .../formats/html/resources/highlight.css      |   67 +
 .../formats/html/resources/highlight.js       | 3278 +++++++++++++++++
 .../formats/html/resources/script.js.template |    4 +
 .../html/resources/standard.properties        |    4 +
 .../formats/html/resources/stylesheet.css     |   17 +-
 .../doclets/toolkit/util/DocPaths.java        |    8 +-
 src/jdk.javadoc/share/man/javadoc.md          |   19 +-
 .../TestSyntaxHighlightOption.java            |   97 +
 13 files changed, 3528 insertions(+), 15 deletions(-)
 create mode 100644 src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css
 create mode 100644 src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.js
 create mode 100644 test/langtools/jdk/javadoc/doclet/testSyntaxHighlightOption/TestSyntaxHighlightOption.java

diff --git a/make/Docs.gmk b/make/Docs.gmk
index 49c97946f7531..fe8c9ec316cb6 100644
--- a/make/Docs.gmk
+++ b/make/Docs.gmk
@@ -100,7 +100,7 @@ JAVA_WARNINGS_ARE_ERRORS ?= -Werror
 JAVADOC_OPTIONS := -use -keywords -notimestamp \
     -serialwarn -encoding ISO-8859-1 -docencoding UTF-8 -breakiterator \
     -splitIndex --system none -javafx --expand-requires transitive \
-    --override-methods=summary
+    --override-methods=summary --syntax-highlight
 
 # The reference options must stay stable to allow for comparisons across the
 # development cycle.
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDoclet.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDoclet.java
index 46ae80e78bd25..9ca1fb32eb839 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDoclet.java
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDoclet.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -311,6 +311,12 @@ protected void generateOtherFiles(ClassTree classTree)
             copyFontResources();
         }
 
+        var syntaxHighlight = options.syntaxHighlight();
+        if (syntaxHighlight) {
+            copyResource(DocPaths.HIGHLIGHT_CSS, DocPaths.RESOURCE_FILES.resolve(DocPaths.HIGHLIGHT_CSS), true);
+            copyResource(DocPaths.HIGHLIGHT_JS, DocPaths.SCRIPT_FILES.resolve(DocPaths.HIGHLIGHT_JS), true);
+        }
+
         // If a stylesheet file is not specified, copy the default stylesheet
         // and replace newline with platform-specific newline.
         if (options.stylesheetFile().isEmpty()) {
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDocletWriter.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDocletWriter.java
index 448a68fd5a1f1..34ef40ed650a4 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDocletWriter.java
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlDocletWriter.java
@@ -511,6 +511,7 @@ public void printHtmlDocument(List<String> metakeywords,
                 .setStylesheets(configuration.getMainStylesheet(), additionalStylesheets, localStylesheets)
                 .setAdditionalScripts(configuration.getAdditionalScripts())
                 .setIndex(options.createIndex(), mainBodyScript)
+                .setSyntaxHighlight(options.syntaxHighlight())
                 .addContent(extraHeadContent);
 
         HtmlDocument htmlDocument = new HtmlDocument(
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlOptions.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlOptions.java
index 10483fb4a4c19..0d055eaa25443 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlOptions.java
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlOptions.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -198,6 +198,11 @@ public class HtmlOptions extends BaseOptions {
      */
     private String stylesheetFile = "";
 
+    /**
+     * Argument for command line option {@code --syntax-highlight}.
+     */
+    private boolean syntaxHighlight = false;
+
     /**
      * Argument for command-line option {@code -tagletpath}.
      * The path to Taglets
@@ -423,6 +428,14 @@ public boolean process(String opt, List<String> args) {
                     }
                 },
 
+                new Option(resources, "--syntax-highlight") {
+                    @Override
+                    public boolean process(String opt,  List<String> args) {
+                        syntaxHighlight = true;
+                        return true;
+                    }
+                },
+
                 new Option(resources, "-tag", 1) {
                     @Override
                     public boolean process(String opt, List<String> args) {
@@ -806,6 +819,13 @@ String stylesheetFile() {
         return stylesheetFile;
     }
 
+    /**
+     * Argument for command line option {@code --syntax-highlight}.
+     * True if command line option "--syntax-highlight" is used and syntax
+     * highlighting should be enabled. Default value is false.
+     */
+    public boolean syntaxHighlight() { return syntaxHighlight; }
+
     /**
      * Argument for command-line option {@code -tagletpath}.
      * The path to Taglets
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/markup/Head.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/markup/Head.java
index 7170c6bcc8a2e..0c54fc1e29c1a 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/markup/Head.java
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/markup/Head.java
@@ -70,6 +70,7 @@ public class Head extends Content {
     private final List<Content> extraContent;
     private boolean addDefaultScript = true;
     private DocPath canonicalLink;
+    private boolean syntaxHighlight = false;
 
     /**
      * Creates a {@code Head} object, for a given file and HTML version.
@@ -238,6 +239,16 @@ public void setCanonicalLink(DocPath link) {
         this.canonicalLink = link;
     }
 
+    /**
+     * Enables or disables support for syntax highlighting.
+     * @param value {@code true} to enable syntax highligting
+     * @return this object
+     */
+    public Head setSyntaxHighlight(boolean value) {
+        this.syntaxHighlight = value;
+        return this;
+    }
+
     /**
      * Adds additional content to be included in the HEAD element.
      *
@@ -339,6 +350,11 @@ private void addStylesheets(HtmlTree head) {
             addStylesheet(head, DocPaths.RESOURCE_FILES.resolve(path));
         }
 
+        if (syntaxHighlight) {
+            addStylesheet(head, DocPaths.RESOURCE_FILES.resolve(DocPaths.HIGHLIGHT_CSS));
+            addScriptElement(head, DocPaths.HIGHLIGHT_JS);
+        }
+
         for (DocPath path : localStylesheets) {
             // Local stylesheets are contained in doc-files, so omit resource-files prefix
             addStylesheet(head, path);
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css
new file mode 100644
index 0000000000000..138138dabc784
--- /dev/null
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
+ */
+/* Syntax highlight style sheet */
+.hljs-title.function_,
+.hljs-template-variable {
+    color: #00738F;
+}
+.hljs-code,
+.hljs-comment,
+.hljs-quote {
+    color: #707071;
+    font-style: italic;
+}
+.hljs-meta {
+    color: #836F00;
+}
+.hljs-symbol,
+.hljs-template-tag,
+.hljs-keyword,
+.hljs-literal,
+.hljs-name,
+.hljs-built_in,
+.hljs-char.escape_ {
+    color: #0C40C2;
+}
+.hljs-variable,
+.hljs-property,
+.hljs-attr,
+.hljs-section {
+    color: #7422a1;
+}
+.hljs-attribute {
+    color: #164ad9;
+}
+.hljs-regexp,
+.hljs-number {
+    color: #104BEB;
+}
+.hljs-link {
+    color: #47688a;
+}
+.hljs-string {
+    color: #008313;
+}
+.hljs-doctag {
+    text-decoration: underline;
+}
+.hljs-emphasis {
+    font-style: italic;
+}
+.hljs-strong {
+    font-weight: bold;
+}
+.hljs-subst,
+.hljs-title,
+.hljs-params,
+.hljs-bullet,
+.hljs-formula,
+.hljs-tag,
+.hljs-type {
+    /* ignored */
+}
+
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.js b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.js
new file mode 100644
index 0000000000000..5f5e03511725b
--- /dev/null
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.js
@@ -0,0 +1,3278 @@
+/*!
+  Highlight.js v11.11.1 (git: 08cb242e7d)
+  (c) 2006-2025 Josh Goebel <hello@joshgoebel.com> and other contributors
+  License: BSD-3-Clause
+ */
+var hljs = (function () {
+  'use strict';
+
+  /* eslint-disable no-multi-assign */
+
+  function deepFreeze(obj) {
+    if (obj instanceof Map) {
+      obj.clear =
+        obj.delete =
+        obj.set =
+          function () {
+            throw new Error('map is read-only');
+          };
+    } else if (obj instanceof Set) {
+      obj.add =
+        obj.clear =
+        obj.delete =
+          function () {
+            throw new Error('set is read-only');
+          };
+    }
+
+    // Freeze self
+    Object.freeze(obj);
+
+    Object.getOwnPropertyNames(obj).forEach((name) => {
+      const prop = obj[name];
+      const type = typeof prop;
+
+      // Freeze prop if it is an object or function and also not already frozen
+      if ((type === 'object' || type === 'function') && !Object.isFrozen(prop)) {
+        deepFreeze(prop);
+      }
+    });
+
+    return obj;
+  }
+
+  /** @typedef {import('highlight.js').CallbackResponse} CallbackResponse */
+  /** @typedef {import('highlight.js').CompiledMode} CompiledMode */
+  /** @implements CallbackResponse */
+
+  class Response {
+    /**
+     * @param {CompiledMode} mode
+     */
+    constructor(mode) {
+      // eslint-disable-next-line no-undefined
+      if (mode.data === undefined) mode.data = {};
+
+      this.data = mode.data;
+      this.isMatchIgnored = false;
+    }
+
+    ignoreMatch() {
+      this.isMatchIgnored = true;
+    }
+  }
+
+  /**
+   * @param {string} value
+   * @returns {string}
+   */
+  function escapeHTML(value) {
+    return value
+      .replace(/&/g, '&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;
+  };
+
+  /**
+   *
+   * @param {string} name
+   * @param {{prefix:string}} options
+   */
+  const scopeToCSSClass = (name, { prefix }) => {
+    // sub-language
+    if (name.startsWith("language:")) {
+      return name.replace("language:", "language-");
+    }
+    // tiered scope: comment.line
+    if (name.includes(".")) {
+      const pieces = name.split(".");
+      return [
+        `${prefix}${pieces.shift()}`,
+        ...(pieces.map((x, i) => `${x}${"_".repeat(i + 1)}`))
+      ].join(" ");
+    }
+    // simple scope
+    return `${prefix}${name}`;
+  };
+
+  /** @type {Renderer} */
+  class HTMLRenderer {
+    /**
+     * Creates a new HTMLRenderer
+     *
+     * @param {Tree} parseTree - the parse tree (must support `walk` API)
+     * @param {{classPrefix: string}} options
+     */
+    constructor(parseTree, options) {
+      this.buffer = "";
+      this.classPrefix = options.classPrefix;
+      parseTree.walk(this);
+    }
+
+    /**
+     * Adds texts to the output stream
+     *
+     * @param {string} text */
+    addText(text) {
+      this.buffer += escapeHTML(text);
+    }
+
+    /**
+     * Adds a node open to the output stream (if needed)
+     *
+     * @param {Node} node */
+    openNode(node) {
+      if (!emitsWrappingTags(node)) return;
+
+      const className = scopeToCSSClass(node.scope,
+        { prefix: this.classPrefix });
+      this.span(className);
+    }
+
+    /**
+     * Adds a node close to the output stream (if needed)
+     *
+     * @param {Node} node */
+    closeNode(node) {
+      if (!emitsWrappingTags(node)) return;
+
+      this.buffer += SPAN_CLOSE;
+    }
+
+    /**
+     * returns the accumulated buffer
+    */
+    value() {
+      return this.buffer;
+    }
+
+    // helpers
+
+    /**
+     * Builds a span element
+     *
+     * @param {string} className */
+    span(className) {
+      this.buffer += `<span class="${className}">`;
+    }
+  }
+
+  /** @typedef {{scope?: string, language?: string, children: Node[]} | string} Node */
+  /** @typedef {{scope?: string, language?: string, children: Node[]} } DataNode */
+  /** @typedef {import('highlight.js').Emitter} Emitter */
+  /**  */
+
+  /** @returns {DataNode} */
+  const newNode = (opts = {}) => {
+    /** @type DataNode */
+    const result = { children: [] };
+    Object.assign(result, opts);
+    return result;
+  };
+
+  class TokenTree {
+    constructor() {
+      /** @type DataNode */
+      this.rootNode = newNode();
+      this.stack = [this.rootNode];
+    }
+
+    get top() {
+      return this.stack[this.stack.length - 1];
+    }
+
+    get root() { return this.rootNode; }
+
+    /** @param {Node} node */
+    add(node) {
+      this.top.children.push(node);
+    }
+
+    /** @param {string} scope */
+    openNode(scope) {
+      /** @type Node */
+      const node = newNode({ scope });
+      this.add(node);
+      this.stack.push(node);
+    }
+
+    closeNode() {
+      if (this.stack.length > 1) {
+        return this.stack.pop();
+      }
+      // eslint-disable-next-line no-undefined
+      return undefined;
+    }
+
+    closeAllNodes() {
+      while (this.closeNode());
+    }
+
+    toJSON() {
+      return JSON.stringify(this.rootNode, null, 4);
+    }
+
+    /**
+     * @typedef { import("./html_renderer").Renderer } Renderer
+     * @param {Renderer} builder
+     */
+    walk(builder) {
+      // this does not
+      return this.constructor._walk(builder, this.rootNode);
+      // this works
+      // return TokenTree._walk(builder, this.rootNode);
+    }
+
+    /**
+     * @param {Renderer} builder
+     * @param {Node} node
+     */
+    static _walk(builder, node) {
+      if (typeof node === "string") {
+        builder.addText(node);
+      } else if (node.children) {
+        builder.openNode(node);
+        node.children.forEach((child) => this._walk(builder, child));
+        builder.closeNode(node);
+      }
+      return builder;
+    }
+
+    /**
+     * @param {Node} node
+     */
+    static _collapse(node) {
+      if (typeof node === "string") return;
+      if (!node.children) return;
+
+      if (node.children.every(el => typeof el === "string")) {
+        // node.text = node.children.join("");
+        // delete node.children;
+        node.children = [node.children.join("")];
+      } else {
+        node.children.forEach((child) => {
+          TokenTree._collapse(child);
+        });
+      }
+    }
+  }
+
+  /**
+    Currently this is all private API, but this is the minimal API necessary
+    that an Emitter must implement to fully support the parser.
+
+    Minimal interface:
+
+    - addText(text)
+    - __addSublanguage(emitter, subLanguageName)
+    - startScope(scope)
+    - endScope()
+    - finalize()
+    - toHTML()
+
+  */
+
+  /**
+   * @implements {Emitter}
+   */
+  class TokenTreeEmitter extends TokenTree {
+    /**
+     * @param {*} options
+     */
+    constructor(options) {
+      super();
+      this.options = options;
+    }
+
+    /**
+     * @param {string} text
+     */
+    addText(text) {
+      if (text === "") { return; }
+
+      this.add(text);
+    }
+
+    /** @param {string} scope */
+    startScope(scope) {
+      this.openNode(scope);
+    }
+
+    endScope() {
+      this.closeNode();
+    }
+
+    /**
+     * @param {Emitter & {root: DataNode}} emitter
+     * @param {string} name
+     */
+    __addSublanguage(emitter, name) {
+      /** @type DataNode */
+      const node = emitter.root;
+      if (name) node.scope = `language:${name}`;
+
+      this.add(node);
+    }
+
+    toHTML() {
+      const renderer = new HTMLRenderer(this, this.options);
+      return renderer.value();
+    }
+
+    finalize() {
+      this.closeAllNodes();
+      return true;
+    }
+  }
+
+  /**
+   * @param {string} value
+   * @returns {RegExp}
+   * */
+
+  /**
+   * @param {RegExp | string } re
+   * @returns {string}
+   */
+  function source(re) {
+    if (!re) return null;
+    if (typeof re === "string") return re;
+
+    return re.source;
+  }
+
+  /**
+   * @param {RegExp | string } re
+   * @returns {string}
+   */
+  function lookahead(re) {
+    return concat('(?=', re, ')');
+  }
+
+  /**
+   * @param {RegExp | string } re
+   * @returns {string}
+   */
+  function anyNumberOfTimes(re) {
+    return concat('(?:', re, ')*');
+  }
+
+  /**
+   * @param {RegExp | string } re
+   * @returns {string}
+   */
+  function optional(re) {
+    return concat('(?:', re, ')?');
+  }
+
+  /**
+   * @param {...(RegExp | string) } args
+   * @returns {string}
+   */
+  function concat(...args) {
+    const joined = args.map((x) => source(x)).join("");
+    return joined;
+  }
+
+  /**
+   * @param { Array<string | RegExp | Object> } args
+   * @returns {object}
+   */
+  function stripOptionsFromArgs(args) {
+    const opts = args[args.length - 1];
+
+    if (typeof opts === 'object' && opts.constructor === Object) {
+      args.splice(args.length - 1, 1);
+      return opts;
+    } else {
+      return {};
+    }
+  }
+
+  /** @typedef { {capture?: boolean} } RegexEitherOptions */
+
+  /**
+   * Any of the passed expresssions may match
+   *
+   * Creates a huge this | this | that | that match
+   * @param {(RegExp | string)[] | [...(RegExp | string)[], RegexEitherOptions]} args
+   * @returns {string}
+   */
+  function either(...args) {
+    /** @type { object & {capture?: boolean} }  */
+    const opts = stripOptionsFromArgs(args);
+    const joined = '('
+      + (opts.capture ? "" : "?:")
+      + args.map((x) => source(x)).join("|") + ")";
+    return joined;
+  }
+
+  /**
+   * @param {RegExp | string} re
+   * @returns {number}
+   */
+  function countMatchGroups(re) {
+    return (new RegExp(re.toString() + '|')).exec('').length - 1;
+  }
+
+  /**
+   * Does lexeme start with a regular expression match at the beginning
+   * @param {RegExp} re
+   * @param {string} lexeme
+   */
+  function startsWith(re, lexeme) {
+    const match = re && re.exec(lexeme);
+    return match && match.index === 0;
+  }
+
+  // BACKREF_RE matches an open parenthesis or backreference. To avoid
+  // an incorrect parse, it additionally matches the following:
+  // - [...] elements, where the meaning of parentheses and escapes change
+  // - other escape sequences, so we do not misparse escape sequences as
+  //   interesting elements
+  // - non-matching or lookahead parentheses, which do not capture. These
+  //   follow the '(' with a '?'.
+  const BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
+
+  // **INTERNAL** Not intended for outside usage
+  // join logically computes regexps.join(separator), but fixes the
+  // backreferences so they continue to match.
+  // it also places each individual regular expression into it's own
+  // match group, keeping track of the sequencing of those match groups
+  // is currently an exercise for the caller. :-)
+  /**
+   * @param {(string | RegExp)[]} regexps
+   * @param {{joinWith: string}} opts
+   * @returns {string}
+   */
+  function _rewriteBackreferences(regexps, { joinWith }) {
+    let numCaptures = 0;
+
+    return regexps.map((regex) => {
+      numCaptures += 1;
+      const offset = numCaptures;
+      let re = source(regex);
+      let out = '';
+
+      while (re.length > 0) {
+        const match = BACKREF_RE.exec(re);
+        if (!match) {
+          out += re;
+          break;
+        }
+        out += re.substring(0, match.index);
+        re = re.substring(match.index + match[0].length);
+        if (match[0][0] === '\\' && match[1]) {
+          // Adjust the backreference.
+          out += '\\' + String(Number(match[1]) + offset);
+        } else {
+          out += match[0];
+          if (match[0] === '(') {
+            numCaptures++;
+          }
+        }
+      }
+      return out;
+    }).map(re => `(${re})`).join(joinWith);
+  }
+
+  /** @typedef {import('highlight.js').Mode} Mode */
+  /** @typedef {import('highlight.js').ModeCallback} ModeCallback */
+
+  // Common regexps
+  const MATCH_NOTHING_RE = /\b\B/;
+  const IDENT_RE = '[a-zA-Z]\\w*';
+  const UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*';
+  const NUMBER_RE = '\\b\\d+(\\.\\d+)?';
+  const C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float
+  const BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b...
+  const RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~';
+
+  /**
+  * @param { Partial<Mode> & {binary?: string | RegExp} } opts
+  */
+  const SHEBANG = (opts = {}) => {
+    const beginShebang = /^#![ ]*\//;
+    if (opts.binary) {
+      opts.begin = concat(
+        beginShebang,
+        /.*\b/,
+        opts.binary,
+        /\b.*/);
+    }
+    return inherit$1({
+      scope: 'meta',
+      begin: beginShebang,
+      end: /$/,
+      relevance: 0,
+      /** @type {ModeCallback} */
+      "on:begin": (m, resp) => {
+        if (m.index !== 0) resp.ignoreMatch();
+      }
+    }, opts);
+  };
+
+  // Common modes
+  const BACKSLASH_ESCAPE = {
+    begin: '\\\\[\\s\\S]', relevance: 0
+  };
+  const APOS_STRING_MODE = {
+    scope: 'string',
+    begin: '\'',
+    end: '\'',
+    illegal: '\\n',
+    contains: [BACKSLASH_ESCAPE]
+  };
+  const QUOTE_STRING_MODE = {
+    scope: 'string',
+    begin: '"',
+    end: '"',
+    illegal: '\\n',
+    contains: [BACKSLASH_ESCAPE]
+  };
+  const PHRASAL_WORDS_MODE = {
+    begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
+  };
+  /**
+   * Creates a comment mode
+   *
+   * @param {string | RegExp} begin
+   * @param {string | RegExp} end
+   * @param {Mode | {}} [modeOptions]
+   * @returns {Partial<Mode>}
+   */
+  const COMMENT = function(begin, end, modeOptions = {}) {
+    const mode = inherit$1(
+      {
+        scope: 'comment',
+        begin,
+        end,
+        contains: []
+      },
+      modeOptions
+    );
+    mode.contains.push({
+      scope: 'doctag',
+      // hack to avoid the space from being included. the space is necessary to
+      // match here to prevent the plain text rule below from gobbling up doctags
+      begin: '[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)',
+      end: /(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,
+      excludeBegin: true,
+      relevance: 0
+    });
+    const ENGLISH_WORD = either(
+      // list of common 1 and 2 letter words in English
+      "I",
+      "a",
+      "is",
+      "so",
+      "us",
+      "to",
+      "at",
+      "if",
+      "in",
+      "it",
+      "on",
+      // note: this is not an exhaustive list of contractions, just popular ones
+      /[A-Za-z]+['](d|ve|re|ll|t|s|n)/, // contractions - can't we'd they're let's, etc
+      /[A-Za-z]+[-][a-z]+/, // `no-way`, etc.
+      /[A-Za-z][a-z]{2,}/ // allow capitalized words at beginning of sentences
+    );
+    // looking like plain text, more likely to be a comment
+    mode.contains.push(
+      {
+        // TODO: how to include ", (, ) without breaking grammars that use these for
+        // comment delimiters?
+        // begin: /[ ]+([()"]?([A-Za-z'-]{3,}|is|a|I|so|us|[tT][oO]|at|if|in|it|on)[.]?[()":]?([.][ ]|[ ]|\))){3}/
+        // ---
+
+        // this tries to find sequences of 3 english words in a row (without any
+        // "programming" type syntax) this gives us a strong signal that we've
+        // TRULY found a comment - vs perhaps scanning with the wrong language.
+        // It's possible to find something that LOOKS like the start of the
+        // comment - but then if there is no readable text - good chance it is a
+        // false match and not a comment.
+        //
+        // for a visual example please see:
+        // https://github.com/highlightjs/highlight.js/issues/2827
+
+        begin: concat(
+          /[ ]+/, // necessary to prevent us gobbling up doctags like /* @author Bob Mcgill */
+          '(',
+          ENGLISH_WORD,
+          /[.]?[:]?([.][ ]|[ ])/,
+          '){3}') // look for 3 words in a row
+      }
+    );
+    return mode;
+  };
+  const C_LINE_COMMENT_MODE = COMMENT('//', '$');
+  const C_BLOCK_COMMENT_MODE = COMMENT('/\\*', '\\*/');
+  const HASH_COMMENT_MODE = COMMENT('#', '$');
+  const NUMBER_MODE = {
+    scope: 'number',
+    begin: NUMBER_RE,
+    relevance: 0
+  };
+  const C_NUMBER_MODE = {
+    scope: 'number',
+    begin: C_NUMBER_RE,
+    relevance: 0
+  };
+  const BINARY_NUMBER_MODE = {
+    scope: 'number',
+    begin: BINARY_NUMBER_RE,
+    relevance: 0
+  };
+  const REGEXP_MODE = {
+    scope: "regexp",
+    begin: /\/(?=[^/\n]*\/)/,
+    end: /\/[gimuy]*/,
+    contains: [
+      BACKSLASH_ESCAPE,
+      {
+        begin: /\[/,
+        end: /\]/,
+        relevance: 0,
+        contains: [BACKSLASH_ESCAPE]
+      }
+    ]
+  };
+  const TITLE_MODE = {
+    scope: 'title',
+    begin: IDENT_RE,
+    relevance: 0
+  };
+  const UNDERSCORE_TITLE_MODE = {
+    scope: 'title',
+    begin: UNDERSCORE_IDENT_RE,
+    relevance: 0
+  };
+  const METHOD_GUARD = {
+    // excludes method names from keyword processing
+    begin: '\\.\\s*' + UNDERSCORE_IDENT_RE,
+    relevance: 0
+  };
+
+  /**
+   * Adds end same as begin mechanics to a mode
+   *
+   * Your mode must include at least a single () match group as that first match
+   * group is what is used for comparison
+   * @param {Partial<Mode>} mode
+   */
+  const END_SAME_AS_BEGIN = function(mode) {
+    return Object.assign(mode,
+      {
+        /** @type {ModeCallback} */
+        'on:begin': (m, resp) => { resp.data._beginMatch = m[1]; },
+        /** @type {ModeCallback} */
+        'on:end': (m, resp) => { if (resp.data._beginMatch !== m[1]) resp.ignoreMatch(); }
+      });
+  };
+
+  var MODES = /*#__PURE__*/Object.freeze({
+    __proto__: null,
+    APOS_STRING_MODE: APOS_STRING_MODE,
+    BACKSLASH_ESCAPE: BACKSLASH_ESCAPE,
+    BINARY_NUMBER_MODE: BINARY_NUMBER_MODE,
+    BINARY_NUMBER_RE: BINARY_NUMBER_RE,
+    COMMENT: COMMENT,
+    C_BLOCK_COMMENT_MODE: C_BLOCK_COMMENT_MODE,
+    C_LINE_COMMENT_MODE: C_LINE_COMMENT_MODE,
+    C_NUMBER_MODE: C_NUMBER_MODE,
+    C_NUMBER_RE: C_NUMBER_RE,
+    END_SAME_AS_BEGIN: END_SAME_AS_BEGIN,
+    HASH_COMMENT_MODE: HASH_COMMENT_MODE,
+    IDENT_RE: IDENT_RE,
+    MATCH_NOTHING_RE: MATCH_NOTHING_RE,
+    METHOD_GUARD: METHOD_GUARD,
+    NUMBER_MODE: NUMBER_MODE,
+    NUMBER_RE: NUMBER_RE,
+    PHRASAL_WORDS_MODE: PHRASAL_WORDS_MODE,
+    QUOTE_STRING_MODE: QUOTE_STRING_MODE,
+    REGEXP_MODE: REGEXP_MODE,
+    RE_STARTERS_RE: RE_STARTERS_RE,
+    SHEBANG: SHEBANG,
+    TITLE_MODE: TITLE_MODE,
+    UNDERSCORE_IDENT_RE: UNDERSCORE_IDENT_RE,
+    UNDERSCORE_TITLE_MODE: UNDERSCORE_TITLE_MODE
+  });
+
+  /**
+  @typedef {import('highlight.js').CallbackResponse} CallbackResponse
+  @typedef {import('highlight.js').CompilerExt} CompilerExt
+  */
+
+  // Grammar extensions / plugins
+  // See: https://github.com/highlightjs/highlight.js/issues/2833
+
+  // Grammar extensions allow "syntactic sugar" to be added to the grammar modes
+  // without requiring any underlying changes to the compiler internals.
+
+  // `compileMatch` being the perfect small example of now allowing a grammar
+  // author to write `match` when they desire to match a single expression rather
+  // than being forced to use `begin`.  The extension then just moves `match` into
+  // `begin` when it runs.  Ie, no features have been added, but we've just made
+  // the experience of writing (and reading grammars) a little bit nicer.
+
+  // ------
+
+  // TODO: We need negative look-behind support to do this properly
+  /**
+   * Skip a match if it has a preceding dot
+   *
+   * This is used for `beginKeywords` to prevent matching expressions such as
+   * `bob.keyword.do()`. The mode compiler automatically wires this up as a
+   * special _internal_ 'on:begin' callback for modes with `beginKeywords`
+   * @param {RegExpMatchArray} match
+   * @param {CallbackResponse} response
+   */
+  function skipIfHasPrecedingDot(match, response) {
+    const before = match.input[match.index - 1];
+    if (before === ".") {
+      response.ignoreMatch();
+    }
+  }
+
+  /**
+   *
+   * @type {CompilerExt}
+   */
+  function scopeClassName(mode, _parent) {
+    // eslint-disable-next-line no-undefined
+    if (mode.className !== undefined) {
+      mode.scope = mode.className;
+      delete mode.className;
+    }
+  }
+
+  /**
+   * `beginKeywords` syntactic sugar
+   * @type {CompilerExt}
+   */
+  function beginKeywords(mode, parent) {
+    if (!parent) return;
+    if (!mode.beginKeywords) return;
+
+    // for languages with keywords that include non-word characters checking for
+    // a word boundary is not sufficient, so instead we check for a word boundary
+    // or whitespace - this does no harm in any case since our keyword engine
+    // doesn't allow spaces in keywords anyways and we still check for the boundary
+    // first
+    mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')(?!\\.)(?=\\b|\\s)';
+    mode.__beforeBegin = skipIfHasPrecedingDot;
+    mode.keywords = mode.keywords || mode.beginKeywords;
+    delete mode.beginKeywords;
+
+    // prevents double relevance, the keywords themselves provide
+    // relevance, the mode doesn't need to double it
+    // eslint-disable-next-line no-undefined
+    if (mode.relevance === undefined) mode.relevance = 0;
+  }
+
+  /**
+   * Allow `illegal` to contain an array of illegal values
+   * @type {CompilerExt}
+   */
+  function compileIllegal(mode, _parent) {
+    if (!Array.isArray(mode.illegal)) return;
+
+    mode.illegal = either(...mode.illegal);
+  }
+
+  /**
+   * `match` to match a single expression for readability
+   * @type {CompilerExt}
+   */
+  function compileMatch(mode, _parent) {
+    if (!mode.match) return;
+    if (mode.begin || mode.end) throw new Error("begin & end are not supported with match");
+
+    mode.begin = mode.match;
+    delete mode.match;
+  }
+
+  /**
+   * provides the default 1 relevance to all modes
+   * @type {CompilerExt}
+   */
+  function compileRelevance(mode, _parent) {
+    // eslint-disable-next-line no-undefined
+    if (mode.relevance === undefined) mode.relevance = 1;
+  }
+
+  // allow beforeMatch to act as a "qualifier" for the match
+  // the full match begin must be [beforeMatch][begin]
+  const beforeMatchExt = (mode, parent) => {
+    if (!mode.beforeMatch) return;
+    // starts conflicts with endsParent which we need to make sure the child
+    // rule is not matched multiple times
+    if (mode.starts) throw new Error("beforeMatch cannot be used with starts");
+
+    const originalMode = Object.assign({}, mode);
+    Object.keys(mode).forEach((key) => { delete mode[key]; });
+
+    mode.keywords = originalMode.keywords;
+    mode.begin = concat(originalMode.beforeMatch, lookahead(originalMode.begin));
+    mode.starts = {
+      relevance: 0,
+      contains: [
+        Object.assign(originalMode, { endsParent: true })
+      ]
+    };
+    mode.relevance = 0;
+
+    delete originalMode.beforeMatch;
+  };
+
+  // keywords that should have no default relevance value
+  const COMMON_KEYWORDS = [
+    'of',
+    'and',
+    'for',
+    'in',
+    'not',
+    'or',
+    'if',
+    'then',
+    'parent', // common variable name
+    'list', // common variable name
+    'value' // common variable name
+  ];
+
+  const DEFAULT_KEYWORD_SCOPE = "keyword";
+
+  /**
+   * Given raw keywords from a language definition, compile them.
+   *
+   * @param {string | Record<string,string|string[]> | Array<string>} rawKeywords
+   * @param {boolean} caseInsensitive
+   */
+  function compileKeywords(rawKeywords, caseInsensitive, scopeName = DEFAULT_KEYWORD_SCOPE) {
+    /** @type {import("highlight.js/private").KeywordDict} */
+    const compiledKeywords = Object.create(null);
+
+    // input can be a string of keywords, an array of keywords, or a object with
+    // named keys representing scopeName (which can then point to a string or array)
+    if (typeof rawKeywords === 'string') {
+      compileList(scopeName, rawKeywords.split(" "));
+    } else if (Array.isArray(rawKeywords)) {
+      compileList(scopeName, rawKeywords);
+    } else {
+      Object.keys(rawKeywords).forEach(function(scopeName) {
+        // collapse all our objects back into the parent object
+        Object.assign(
+          compiledKeywords,
+          compileKeywords(rawKeywords[scopeName], caseInsensitive, scopeName)
+        );
+      });
+    }
+    return compiledKeywords;
+
+    // ---
+
+    /**
+     * Compiles an individual list of keywords
+     *
+     * Ex: "for if when while|5"
+     *
+     * @param {string} scopeName
+     * @param {Array<string>} keywordList
+     */
+    function compileList(scopeName, keywordList) {
+      if (caseInsensitive) {
+        keywordList = keywordList.map(x => x.toLowerCase());
+      }
+      keywordList.forEach(function(keyword) {
+        const pair = keyword.split('|');
+        compiledKeywords[pair[0]] = [scopeName, scoreForKeyword(pair[0], pair[1])];
+      });
+    }
+  }
+
+  /**
+   * Returns the proper score for a given keyword
+   *
+   * Also takes into account comment keywords, which will be scored 0 UNLESS
+   * another score has been manually assigned.
+   * @param {string} keyword
+   * @param {string} [providedScore]
+   */
+  function scoreForKeyword(keyword, providedScore) {
+    // manual scores always win over common keywords
+    // so you can force a score of 1 if you really insist
+    if (providedScore) {
+      return Number(providedScore);
+    }
+
+    return commonKeyword(keyword) ? 0 : 1;
+  }
+
+  /**
+   * Determines if a given keyword is common or not
+   *
+   * @param {string} keyword */
+  function commonKeyword(keyword) {
+    return COMMON_KEYWORDS.includes(keyword.toLowerCase());
+  }
+
+  /*
+
+  For the reasoning behind this please see:
+  https://github.com/highlightjs/highlight.js/issues/2880#issuecomment-747275419
+
+  */
+
+  /**
+   * @type {Record<string, boolean>}
+   */
+  const seenDeprecations = {};
+
+  /**
+   * @param {string} message
+   */
+  const error = (message) => {
+    console.error(message);
+  };
+
+  /**
+   * @param {string} message
+   * @param {any} args
+   */
+  const warn = (message, ...args) => {
+    console.log(`WARN: ${message}`, ...args);
+  };
+
+  /**
+   * @param {string} version
+   * @param {string} message
+   */
+  const deprecated = (version, message) => {
+    if (seenDeprecations[`${version}/${message}`]) return;
+
+    console.log(`Deprecated as of ${version}. ${message}`);
+    seenDeprecations[`${version}/${message}`] = true;
+  };
+
+  /* eslint-disable no-throw-literal */
+
+  /**
+  @typedef {import('highlight.js').CompiledMode} CompiledMode
+  */
+
+  const MultiClassError = new Error();
+
+  /**
+   * Renumbers labeled scope names to account for additional inner match
+   * groups that otherwise would break everything.
+   *
+   * Lets say we 3 match scopes:
+   *
+   *   { 1 => ..., 2 => ..., 3 => ... }
+   *
+   * So what we need is a clean match like this:
+   *
+   *   (a)(b)(c) => [ "a", "b", "c" ]
+   *
+   * But this falls apart with inner match groups:
+   *
+   * (a)(((b)))(c) => ["a", "b", "b", "b", "c" ]
+   *
+   * Our scopes are now "out of alignment" and we're repeating `b` 3 times.
+   * What needs to happen is the numbers are remapped:
+   *
+   *   { 1 => ..., 2 => ..., 5 => ... }
+   *
+   * We also need to know that the ONLY groups that should be output
+   * are 1, 2, and 5.  This function handles this behavior.
+   *
+   * @param {CompiledMode} mode
+   * @param {Array<RegExp | string>} regexes
+   * @param {{key: "beginScope"|"endScope"}} opts
+   */
+  function remapScopeNames(mode, regexes, { key }) {
+    let offset = 0;
+    const scopeNames = mode[key];
+    /** @type Record<number,boolean> */
+    const emit = {};
+    /** @type Record<number,string> */
+    const positions = {};
+
+    for (let i = 1; i <= regexes.length; i++) {
+      positions[i + offset] = scopeNames[i];
+      emit[i + offset] = true;
+      offset += countMatchGroups(regexes[i - 1]);
+    }
+    // we use _emit to keep track of which match groups are "top-level" to avoid double
+    // output from inside match groups
+    mode[key] = positions;
+    mode[key]._emit = emit;
+    mode[key]._multi = true;
+  }
+
+  /**
+   * @param {CompiledMode} mode
+   */
+  function beginMultiClass(mode) {
+    if (!Array.isArray(mode.begin)) return;
+
+    if (mode.skip || mode.excludeBegin || mode.returnBegin) {
+      error("skip, excludeBegin, returnBegin not compatible with beginScope: {}");
+      throw MultiClassError;
+    }
+
+    if (typeof mode.beginScope !== "object" || mode.beginScope === null) {
+      error("beginScope must be object");
+      throw 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.11.1";
+
+  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;
+              emitKeyword(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 {string} text
+       * @param {string} scope
+       */
+      function emitKeyword(keyword, scope) {
+        if (keyword === "") return;
+
+        emitter.startScope(scope);
+        emitter.addText(keyword);
+        emitter.endScope();
+      }
+
+      /**
+       * @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) {
+            emitKeyword(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) {
+            emitKeyword(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();
+          emitKeyword(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
+          modeBuffer += "\n";
+          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 {
+        if (!language.__emitTokens) {
+          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));
+        } else {
+          language.__emitTokens(codeToHighlight, emitter);
+        }
+
+        emitter.finalize();
+        result = emitter.toHTML();
+
+        return {
+          language: languageName,
+          value: result,
+          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,
+              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 });
+
+      if (element.dataset.highlighted) {
+        console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.", element);
+        return;
+      }
+
+      // 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;
+      element.dataset.highlighted = "yes";
+      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() {
+      function boot() {
+        // if a highlight was requested before DOM was loaded, do now
+        highlightAll();
+      }
+
+      // if we are called too early in the loading process
+      if (document.readyState === "loading") {
+        // make sure the event listener is only added once
+        if (!wantsHighlight) {
+          window.addEventListener('DOMContentLoaded', boot, false);
+        }
+        wantsHighlight = true;
+        return;
+      }
+
+      const blocks = document.querySelectorAll(options.cssSelector);
+      blocks.forEach(highlightElement);
+    }
+
+    /**
+     * 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 {HLJSPlugin} plugin
+     */
+    function removePlugin(plugin) {
+      const index = plugins.indexOf(plugin);
+      if (index !== -1) {
+        plugins.splice(index, 1);
+      }
+    }
+
+    /**
+     *
+     * @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,
+      removePlugin
+    });
+
+    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) {
+      // @ts-ignore
+      if (typeof MODES[key] === "object") {
+        // @ts-ignore
+        deepFreeze(MODES[key]);
+      }
+    }
+
+    // merge all the modes/regexes into our main object
+    Object.assign(hljs, MODES);
+
+    return hljs;
+  };
+
+  // Other names for the variable may break build script
+  const highlight = HLJS({});
+
+  // returns a new instance of the highlighter to be used for extensions
+  // check https://github.com/wooorm/lowlight/issues/47
+  highlight.newInstance = () => HLJS({});
+
+  // https://docs.oracle.com/javase/specs/jls/se15/html/jls-3.html#jls-3.10
+  var decimalDigits = '[0-9](_*[0-9])*';
+  var frac = `\\.(${decimalDigits})`;
+  var hexDigits = '[0-9a-fA-F](_*[0-9a-fA-F])*';
+  var NUMERIC = {
+    className: 'number',
+    variants: [
+      // DecimalFloatingPointLiteral
+      // including ExponentPart
+      { begin: `(\\b(${decimalDigits})((${frac})|\\.)?|(${frac}))` +
+        `[eE][+-]?(${decimalDigits})[fFdD]?\\b` },
+      // excluding ExponentPart
+      { begin: `\\b(${decimalDigits})((${frac})[fFdD]?\\b|\\.([fFdD]\\b)?)` },
+      { begin: `(${frac})[fFdD]?\\b` },
+      { begin: `\\b(${decimalDigits})[fFdD]\\b` },
+
+      // HexadecimalFloatingPointLiteral
+      { begin: `\\b0[xX]((${hexDigits})\\.?|(${hexDigits})?\\.(${hexDigits}))` +
+        `[pP][+-]?(${decimalDigits})[fFdD]?\\b` },
+
+      // DecimalIntegerLiteral
+      { begin: '\\b(0|[1-9](_*[0-9])*)[lL]?\\b' },
+
+      // HexIntegerLiteral
+      { begin: `\\b0[xX](${hexDigits})[lL]?\\b` },
+
+      // OctalIntegerLiteral
+      { begin: '\\b0(_*[0-7])*[lL]?\\b' },
+
+      // BinaryIntegerLiteral
+      { begin: '\\b0[bB][01](_*[01])*[lL]?\\b' },
+    ],
+    relevance: 0
+  };
+
+  /*
+  Language: Java
+  Author: Vsevolod Solovyov <vsevolod.solovyov@gmail.com>
+  Category: common, enterprise
+  Website: https://www.java.com/
+  */
+
+
+  /**
+   * Allows recursive regex expressions to a given depth
+   *
+   * ie: recurRegex("(abc~~~)", /~~~/g, 2) becomes:
+   * (abc(abc(abc)))
+   *
+   * @param {string} re
+   * @param {RegExp} substitution (should be a g mode regex)
+   * @param {number} depth
+   * @returns {string}``
+   */
+  function recurRegex(re, substitution, depth) {
+    if (depth === -1) return "";
+
+    return re.replace(substitution, _ => {
+      return recurRegex(re, substitution, depth - 1);
+    });
+  }
+
+  /** @type LanguageFn */
+  function java(hljs) {
+    const regex = hljs.regex;
+    const JAVA_IDENT_RE = '[\u00C0-\u02B8a-zA-Z_$][\u00C0-\u02B8a-zA-Z_$0-9]*';
+    const GENERIC_RE= recurRegex('(?:<(?:\\?(?:\\s+(?:extends|super)\\s+' + JAVA_IDENT_RE + ')?)|(?:'
+      + JAVA_IDENT_RE + ')~~~(?:\\s*,\\s*' + JAVA_IDENT_RE + '~~~)*>)?', /~~~/g, 2);
+    const ARRAY_RE = '(?:(?:\\[])+)?';
+    const MAIN_KEYWORDS = [
+      'synchronized',
+      'abstract',
+      'private',
+      'var',
+      'static',
+      'if',
+      'const ',
+      'for',
+      'while',
+      'strictfp',
+      'finally',
+      'protected',
+      'import',
+      'native',
+      'final',
+      'void',
+      'enum',
+      'else',
+      'break',
+      'transient',
+      'catch',
+      'instanceof',
+      'volatile',
+      'case',
+      'assert',
+      'package',
+      'default',
+      'public',
+      'try',
+      'switch',
+      'continue',
+      'throws',
+      'protected',
+      'public',
+      'private',
+      'module',
+      'requires',
+      'exports',
+      'do',
+      'sealed',
+      'yield',
+      'permits',
+      'goto',
+      'when'
+    ];
+
+    const BUILT_INS = [
+      'super',
+      'this'
+    ];
+
+    const LITERALS = [
+      'false',
+      'true',
+      'null'
+    ];
+
+    const TYPES = [
+      'char',
+      'boolean',
+      'long',
+      'float',
+      'int',
+      'byte',
+      'short',
+      'double'
+    ];
+
+    const KEYWORDS = {
+      keyword: MAIN_KEYWORDS,
+      literal: LITERALS,
+      type: TYPES,
+      built_in: BUILT_INS
+    };
+
+    const ANNOTATION = {
+      className: 'meta',
+      begin: '@' + JAVA_IDENT_RE,
+      contains: [
+        {
+          begin: /\(/,
+          end: /\)/,
+          contains: [ "self" ] // allow nested () inside our annotation
+        }
+      ]
+    };
+    const PARAMS = {
+      className: 'params',
+      begin: /\(/,
+      end: /\)/,
+      keywords: KEYWORDS,
+      relevance: 0,
+      contains: [ hljs.C_BLOCK_COMMENT_MODE ],
+      endsParent: true
+    };
+
+    return {
+      name: 'Java',
+      aliases: [ 'jsp' ],
+      keywords: KEYWORDS,
+      illegal: /<\/|#/,
+      contains: [
+        hljs.COMMENT(
+          '/\\*\\*',
+          '\\*/',
+          {
+            relevance: 0,
+            contains: [
+              {
+                // eat up @'s in emails to prevent them to be recognized as doctags
+                begin: /\w+@/,
+                relevance: 0
+              },
+              {
+                className: 'doctag',
+                begin: '@[A-Za-z]+'
+              }
+            ]
+          }
+        ),
+        // relevance boost
+        {
+          begin: /import java\.[a-z]+\./,
+          keywords: "import",
+          relevance: 2
+        },
+        hljs.C_LINE_COMMENT_MODE,
+        hljs.C_BLOCK_COMMENT_MODE,
+        {
+          begin: /"""/,
+          end: /"""/,
+          className: "string",
+          contains: [ hljs.BACKSLASH_ESCAPE ]
+        },
+        hljs.APOS_STRING_MODE,
+        hljs.QUOTE_STRING_MODE,
+        {
+          match: [
+            /\b(?:class|interface|enum|extends|implements|new)/,
+            /\s+/,
+            JAVA_IDENT_RE
+          ],
+          className: {
+            1: "keyword",
+            3: "title.class"
+          }
+        },
+        {
+          // Exceptions for hyphenated keywords
+          match: /non-sealed/,
+          scope: "keyword"
+        },
+        {
+          begin: [
+            regex.concat(/(?!else)/, JAVA_IDENT_RE),
+            GENERIC_RE,
+            ARRAY_RE,
+            /\s+/,
+            JAVA_IDENT_RE,
+            /\s+/,
+            /=(?!=)/
+          ],
+          className: {
+            1: "type",
+            5: "variable",
+            7: "operator"
+          }
+        },
+        {
+          begin: [
+            /record/,
+            /\s+/,
+            JAVA_IDENT_RE
+          ],
+          className: {
+            1: "keyword",
+            3: "title.class"
+          },
+          contains: [
+            PARAMS,
+            hljs.C_LINE_COMMENT_MODE,
+            hljs.C_BLOCK_COMMENT_MODE
+          ]
+        },
+        {
+          // Expression keywords prevent 'keyword Name(...)' from being
+          // recognized as a function definition
+          beginKeywords: 'new throw return else',
+          relevance: 0
+        },
+        {
+          begin: [
+            '(?:' + JAVA_IDENT_RE + GENERIC_RE + ARRAY_RE + '\\s+)',
+            hljs.UNDERSCORE_IDENT_RE,
+            /\s*(?=\()/
+          ],
+          className: { 2: "title.function" },
+          keywords: KEYWORDS,
+          contains: [
+            {
+              className: 'params',
+              begin: /\(/,
+              end: /\)/,
+              keywords: KEYWORDS,
+              relevance: 0,
+              contains: [
+                ANNOTATION,
+                hljs.APOS_STRING_MODE,
+                hljs.QUOTE_STRING_MODE,
+                NUMERIC,
+                hljs.C_BLOCK_COMMENT_MODE
+              ]
+            },
+            hljs.C_LINE_COMMENT_MODE,
+            hljs.C_BLOCK_COMMENT_MODE
+          ]
+        },
+        NUMERIC,
+        ANNOTATION
+      ]
+    };
+  }
+
+  /*
+  Language: .properties
+  Contributors: Valentin Aitken <valentin@nalisbg.com>, Egor Rogov <e.rogov@postgrespro.ru>
+  Website: https://en.wikipedia.org/wiki/.properties
+  Category: config
+  */
+
+  /** @type LanguageFn */
+  function properties(hljs) {
+    // whitespaces: space, tab, formfeed
+    const WS0 = '[ \\t\\f]*';
+    const WS1 = '[ \\t\\f]+';
+    // delimiter
+    const EQUAL_DELIM = WS0 + '[:=]' + WS0;
+    const WS_DELIM = WS1;
+    const DELIM = '(' + EQUAL_DELIM + '|' + WS_DELIM + ')';
+    const KEY = '([^\\\\:= \\t\\f\\n]|\\\\.)+';
+
+    const DELIM_AND_VALUE = {
+      // skip DELIM
+      end: DELIM,
+      relevance: 0,
+      starts: {
+        // value: everything until end of line (again, taking into account backslashes)
+        className: 'string',
+        end: /$/,
+        relevance: 0,
+        contains: [
+          { begin: '\\\\\\\\' },
+          { begin: '\\\\\\n' }
+        ]
+      }
+    };
+
+    return {
+      name: '.properties',
+      disableAutodetect: true,
+      case_insensitive: true,
+      illegal: /\S/,
+      contains: [
+        hljs.COMMENT('^\\s*[!#]', '$'),
+        // key: everything until whitespace or = or : (taking into account backslashes)
+        // case of a key-value pair
+        {
+          returnBegin: true,
+          variants: [
+            { begin: KEY + EQUAL_DELIM },
+            { begin: KEY + WS_DELIM }
+          ],
+          contains: [
+            {
+              className: 'attr',
+              begin: KEY,
+              endsParent: true
+            }
+          ],
+          starts: DELIM_AND_VALUE
+        },
+        // case of an empty key
+        {
+          className: 'attr',
+          begin: KEY + WS0 + '$'
+        }
+      ]
+    };
+  }
+
+  /*
+  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
+            }
+          ]
+        }
+      ]
+    };
+  }
+
+  /*
+  Language: JSON
+  Description: JSON (JavaScript Object Notation) is a lightweight data-interchange format.
+  Author: Ivan Sagalaev <maniac@softwaremaniacs.org>
+  Website: http://www.json.org
+  Category: common, protocols, web
+  */
+
+  function json(hljs) {
+    const ATTRIBUTE = {
+      className: 'attr',
+      begin: /"(\\.|[^\\"\r\n])*"(?=\s*:)/,
+      relevance: 1.01
+    };
+    const PUNCTUATION = {
+      match: /[{}[\],:]/,
+      className: "punctuation",
+      relevance: 0
+    };
+    const LITERALS = [
+      "true",
+      "false",
+      "null"
+    ];
+    // NOTE: normally we would rely on `keywords` for this but using a mode here allows us
+    // - to use the very tight `illegal: \S` rule later to flag any other character
+    // - as illegal indicating that despite looking like JSON we do not truly have
+    // - JSON and thus improve false-positively greatly since JSON will try and claim
+    // - all sorts of JSON looking stuff
+    const LITERALS_MODE = {
+      scope: "literal",
+      beginKeywords: LITERALS.join(" "),
+    };
+
+    return {
+      name: 'JSON',
+      aliases: ['jsonc'],
+      keywords:{
+        literal: LITERALS,
+      },
+      contains: [
+        ATTRIBUTE,
+        PUNCTUATION,
+        hljs.QUOTE_STRING_MODE,
+        LITERALS_MODE,
+        hljs.C_NUMBER_MODE,
+        hljs.C_LINE_COMMENT_MODE,
+        hljs.C_BLOCK_COMMENT_MODE
+      ],
+      illegal: '\\S'
+    };
+  }
+
+  var builtIns = /*#__PURE__*/Object.freeze({
+    __proto__: null,
+    grmr_java: java,
+    grmr_json: json,
+    grmr_properties: properties,
+    grmr_xml: xml
+  });
+
+  const hljs = highlight;
+
+  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/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/script.js.template b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/script.js.template
index 662cca89dc6d2..1b4a5021a08c1 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/script.js.template
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/script.js.template
@@ -21,6 +21,10 @@ var activeTableTab = "active-table-tab";
 const linkIcon = "##REPLACE:doclet.Link_icon##";
 const linkToSection = "##REPLACE:doclet.Link_to_section##";
 
+if (hljs) {
+    hljs.highlightAll();
+}
+
 function loadScripts(doc, tag) {
     createElem(doc, tag, 'script-files/search.js');
 
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties
index e4365b04cab63..d91b2568f0d53 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/standard.properties
@@ -733,6 +733,10 @@ doclet.usage.allow-script-in-comments.description=\
     Allow JavaScript in documentation comments, and options\n\
     whose value is html-code
 
+doclet.usage.syntax-highlight.description=\
+    Enable syntax highlighting for code fragments in {@snippet} tags\n\
+    and <pre><code> elements.
+
 doclet.usage.xdocrootparent.parameters=\
     <url>
 doclet.usage.xdocrootparent.description=\
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
index ac393e8a7620b..dbcc8590b8342 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
@@ -23,11 +23,11 @@
     /* Base font sizes for body and code elements */
     --body-font-size: 14.2px;
     --block-font-size: 14.4px;
-    --code-font-size: 14px;
+    --code-font-size: 13.9px;
     --nav-font-size: 13.4px;
     /* Line height for continuous text blocks */
     --block-line-height: 1.5;
-    --code-line-height: 1.55;
+    --code-line-height: 1.7;
     /* Text colors for body and block elements */
     --body-text-color: #282828;
     --block-text-color: #282828;
@@ -63,7 +63,7 @@
     --toc-highlight-color: var(--subnav-background-color);
     --toc-hover-color: #e9ecf0;
     /* Snippet and pre colors */
-    --snippet-background-color: #f0f0f2;
+    --snippet-background-color: #f2f2f4;
     --snippet-text-color: var(--block-text-color);
     --snippet-highlight-color: #f7c590;
     --pre-background-color: #f3f3f5;
@@ -565,9 +565,6 @@ ul.horizontal li {
     display:inline;
     font-size:0.9em;
 }
-div.inheritance {
-    font-size: 0.99em;
-}
 div.inheritance div.inheritance {
     margin-left:2em;
 }
@@ -1389,8 +1386,8 @@ button#page-search-copy span {
 /* snippet copy button */
 button.snippet-copy {
     position: absolute;
-    top: 2px;
-    right: 2px;
+    top: 4px;
+    right: 1px;
     height: 32px;
 }
 button.snippet-copy img {
@@ -1651,9 +1648,9 @@ table.striped > tbody > tr > th {
     }
 }
 pre.snippet {
-    background-color: var(--snippet-background-color);
+    background-color: var(--code-background-color);
     color: var(--snippet-text-color);
-    padding: 10px;
+    padding: 16px;
 }
 div.snippet-container {
     position: relative;
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/DocPaths.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/DocPaths.java
index cb2b7df761a51..21f1267361954 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/DocPaths.java
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/toolkit/util/DocPaths.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1998, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -115,6 +115,12 @@ public static DocPath indexN(int n) {
     /** The name of the right pointing angle icon. */
     public static final DocPath RIGHT_SVG = DocPath.create("right.svg");
 
+    /** The name of the syntax highlighting style sheet. */
+    public static final DocPath HIGHLIGHT_CSS = DocPath.create("highlight.css");
+
+    /** The name of the syntax highlighting script file. */
+    public static final DocPath HIGHLIGHT_JS = DocPath.create("highlight.js");
+
     /** The name of the default jQuery directory. */
     public static final DocPath JQUERY_DIR = DocPath.create("jquery");
 
diff --git a/src/jdk.javadoc/share/man/javadoc.md b/src/jdk.javadoc/share/man/javadoc.md
index 6a1a92e7d1610..8a0738acc3533 100644
--- a/src/jdk.javadoc/share/man/javadoc.md
+++ b/src/jdk.javadoc/share/man/javadoc.md
@@ -1,5 +1,5 @@
 ---
-# Copyright (c) 1994, 2024, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 1994, 2025, Oracle and/or its affiliates. All rights reserved.
 # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 #
 # This code is free software; you can redistribute it and/or modify it
@@ -867,6 +867,23 @@ The following options are provided by the standard doclet.
     letter, plus a file for any index entries that start with non-alphabetical
     symbols.
 
+<span id="option-syntax-highlight">`--syntax-highlight`</span>
+:   Enables syntax highlighting for code fragments in `{@snippet}` tags and
+    `<pre><code>` elements. For snippets, the `lang` attribute is used to 
+    determine the language of code fragments, which defaults to "java" for 
+    inline snippets and is derived from the file extension for external 
+    snippets. In HTML `<pre><code>` tags, the `class` attribute can be used
+    to specify the language of the contained code fragment as shown below:
+
+    ```
+    <pre><code class="language-java">...</code></pre>
+    ```
+
+    If neither of these attributes is available automatic language detection is
+    applied. To disable syntax highlighting for a code fragment set the language
+    to "text" using one of the mechanisms described above. The languages and 
+    formats supported by this option are Java, Properties, JSON, HTML and XML.
+
 <span id="option-tag">`-tag` *name*:*locations*:*header*</span>
 :   Specifies a custom tag with a single argument. For the `javadoc` tool to
     spell-check tag names, it is important to include a `-tag` option for every
diff --git a/test/langtools/jdk/javadoc/doclet/testSyntaxHighlightOption/TestSyntaxHighlightOption.java b/test/langtools/jdk/javadoc/doclet/testSyntaxHighlightOption/TestSyntaxHighlightOption.java
new file mode 100644
index 0000000000000..f43b50504bf81
--- /dev/null
+++ b/test/langtools/jdk/javadoc/doclet/testSyntaxHighlightOption/TestSyntaxHighlightOption.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 8348282
+ * @summary Add option for syntax highlighting in javadoc snippets
+ * @library /tools/lib ../../lib
+ * @modules jdk.javadoc/jdk.javadoc.internal.tool
+ * @build toolbox.ToolBox javadoc.tester.*
+ * @run main TestSyntaxHighlightOption
+ */
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import javadoc.tester.JavadocTester;
+import toolbox.ToolBox;
+
+public class TestSyntaxHighlightOption extends JavadocTester {
+
+    public static void main(String... args) throws Exception {
+        var tester = new TestSyntaxHighlightOption();
+        tester.runTests();
+    }
+
+    ToolBox tb = new ToolBox();
+    Path src = Path.of("src");
+
+
+    TestSyntaxHighlightOption() throws IOException {
+        tb.writeJavaFiles(src, """
+                    package p;
+                    /** Class C. */
+                    public class C {
+                        /**
+                         * Method m.
+                         */
+                        public void m() {
+                        }
+                    }
+                    """);
+
+    }
+
+    @Test
+    public void testSyntaxHighlightOption(Path base) {
+        javadoc("-d", base.resolve("out").toString(),
+                "-sourcepath", src.toString(),
+                "--syntax-highlight",
+                "p");
+        checkExit(Exit.OK);
+        checkOutput("resource-files/highlight.css", true, "Syntax highlight style sheet");
+        checkOutput("script-files/highlight.js", true, "Highlight.js v11.11.1 (git: 08cb242e7d)");
+        checkOutput("index-all.html", true, """
+                <link rel="stylesheet" type="text/css" href="resource-files/highlight.css">
+                <script type="text/javascript" src="script-files/highlight.js"></script>""");
+        checkOutput("p/package-summary.html", true, """
+                <link rel="stylesheet" type="text/css" href="../resource-files/highlight.css">
+                <script type="text/javascript" src="../script-files/highlight.js"></script>""");
+        checkOutput("p/C.html", true, """
+                <link rel="stylesheet" type="text/css" href="../resource-files/highlight.css">
+                <script type="text/javascript" src="../script-files/highlight.js"></script>""");
+    }
+
+    @Test
+    public void testNoSyntaxHighlightOption(Path base) {
+        javadoc("-d", base.resolve("out").toString(),
+                "-sourcepath", src.toString(),
+                "p");
+        checkExit(Exit.OK);
+        checkFiles(false, "resource-files/highlight.css", "script-files/highlight.js");
+        checkOutput("index-all.html", false, "highlight.css", "highlight.js");
+        checkOutput("p/package-summary.html", false, "highlight.css", "highlight.js");
+        checkOutput("p/C.html", false, "highlight.css", "highlight.js");
+    }
+}

From 7b3b55e55cf00426eb83a8e606254bb0acbfc888 Mon Sep 17 00:00:00 2001
From: Hannes Wallnoefer <hannes.wallnoefer@oracle.com>
Date: Thu, 3 Apr 2025 15:15:02 +0200
Subject: [PATCH 2/6] Fix regular expression for type arguments

---
 .../internal/doclets/formats/html/resources/highlight.js      | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.js b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.js
index 5f5e03511725b..efa14c8c306d5 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.js
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.js
@@ -2666,8 +2666,8 @@ var hljs = (function () {
   function java(hljs) {
     const regex = hljs.regex;
     const JAVA_IDENT_RE = '[\u00C0-\u02B8a-zA-Z_$][\u00C0-\u02B8a-zA-Z_$0-9]*';
-    const GENERIC_RE= recurRegex('(?:<(?:\\?(?:\\s+(?:extends|super)\\s+' + JAVA_IDENT_RE + ')?)|(?:'
-      + JAVA_IDENT_RE + ')~~~(?:\\s*,\\s*' + JAVA_IDENT_RE + '~~~)*>)?', /~~~/g, 2);
+    const TYPE_ARG_RE = '(?:(?:' + JAVA_IDENT_RE + '~~~)|(?:\\?\\s+(?:extends|super)\\s+' + JAVA_IDENT_RE + '~~~)|(?:\\?))';
+    const GENERIC_RE = recurRegex('(?:<' + TYPE_ARG_RE + '(?:\\s*,\\s*' + TYPE_ARG_RE + ')*>)?', /~~~/g, 2);
     const ARRAY_RE = '(?:(?:\\[])+)?';
     const MAIN_KEYWORDS = [
       'synchronized',

From d934aad5a8f85bde7f7fc25ac424564f20f1867d Mon Sep 17 00:00:00 2001
From: Hannes Wallnoefer <hannes.wallnoefer@oracle.com>
Date: Thu, 3 Apr 2025 16:36:11 +0200
Subject: [PATCH 3/6] Brigthen variable color

---
 .../internal/doclets/formats/html/resources/highlight.css       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css
index 138138dabc784..e5ca45dc98e84 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css
@@ -31,7 +31,7 @@
 .hljs-property,
 .hljs-attr,
 .hljs-section {
-    color: #7422a1;
+    color: #841191;
 }
 .hljs-attribute {
     color: #164ad9;

From dd76691497a2d3179ba20364237da5c1781cd587 Mon Sep 17 00:00:00 2001
From: Hannes Wallnoefer <hannes.wallnoefer@oracle.com>
Date: Thu, 3 Apr 2025 16:58:23 +0200
Subject: [PATCH 4/6] Remove trailing spaces

---
 src/jdk.javadoc/share/man/javadoc.md | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/jdk.javadoc/share/man/javadoc.md b/src/jdk.javadoc/share/man/javadoc.md
index 8a0738acc3533..2ba64dfb2e508 100644
--- a/src/jdk.javadoc/share/man/javadoc.md
+++ b/src/jdk.javadoc/share/man/javadoc.md
@@ -869,9 +869,9 @@ The following options are provided by the standard doclet.
 
 <span id="option-syntax-highlight">`--syntax-highlight`</span>
 :   Enables syntax highlighting for code fragments in `{@snippet}` tags and
-    `<pre><code>` elements. For snippets, the `lang` attribute is used to 
-    determine the language of code fragments, which defaults to "java" for 
-    inline snippets and is derived from the file extension for external 
+    `<pre><code>` elements. For snippets, the `lang` attribute is used to
+    determine the language of code fragments, which defaults to "java" for
+    inline snippets and is derived from the file extension for external
     snippets. In HTML `<pre><code>` tags, the `class` attribute can be used
     to specify the language of the contained code fragment as shown below:
 
@@ -881,7 +881,7 @@ The following options are provided by the standard doclet.
 
     If neither of these attributes is available automatic language detection is
     applied. To disable syntax highlighting for a code fragment set the language
-    to "text" using one of the mechanisms described above. The languages and 
+    to "text" using one of the mechanisms described above. The languages and
     formats supported by this option are Java, Properties, JSON, HTML and XML.
 
 <span id="option-tag">`-tag` *name*:*locations*:*header*</span>

From 2700131dbdd5478980ada3a7e18d71e6a9545d52 Mon Sep 17 00:00:00 2001
From: Hannes Wallnoefer <hannes.wallnoefer@oracle.com>
Date: Wed, 9 Apr 2025 02:44:22 +0200
Subject: [PATCH 5/6] Adjust CSS spacings, remove whitespace

---
 .../javadoc/internal/doclets/formats/html/HtmlOptions.java    | 2 +-
 .../internal/doclets/formats/html/resources/stylesheet.css    | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlOptions.java b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlOptions.java
index 0d055eaa25443..faf1f8c897d51 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlOptions.java
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/HtmlOptions.java
@@ -430,7 +430,7 @@ public boolean process(String opt, List<String> args) {
 
                 new Option(resources, "--syntax-highlight") {
                     @Override
-                    public boolean process(String opt,  List<String> args) {
+                    public boolean process(String opt, List<String> args) {
                         syntaxHighlight = true;
                         return true;
                     }
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
index dbcc8590b8342..b53069643735a 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
@@ -27,7 +27,7 @@
     --nav-font-size: 13.4px;
     /* Line height for continuous text blocks */
     --block-line-height: 1.5;
-    --code-line-height: 1.7;
+    --code-line-height: 1.6;
     /* Text colors for body and block elements */
     --body-text-color: #282828;
     --block-text-color: #282828;
@@ -1650,7 +1650,7 @@ table.striped > tbody > tr > th {
 pre.snippet {
     background-color: var(--code-background-color);
     color: var(--snippet-text-color);
-    padding: 16px;
+    padding: 12px;
 }
 div.snippet-container {
     position: relative;

From 05fc1a4a6a70df975dcbcf5d2ae6194ea2cf9445 Mon Sep 17 00:00:00 2001
From: Hannes Wallnoefer <hannes.wallnoefer@oracle.com>
Date: Wed, 9 Apr 2025 06:10:26 +0200
Subject: [PATCH 6/6] Adjust color palette

---
 .../internal/doclets/formats/html/resources/highlight.css     | 2 +-
 .../internal/doclets/formats/html/resources/stylesheet.css    | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css
index e5ca45dc98e84..145c1c7f5d281 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/highlight.css
@@ -12,7 +12,7 @@
 .hljs-code,
 .hljs-comment,
 .hljs-quote {
-    color: #707071;
+    color: #6e6e71;
     font-style: italic;
 }
 .hljs-meta {
diff --git a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
index a8d1449b15cca..c2351d113cc5e 100644
--- a/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
+++ b/src/jdk.javadoc/share/classes/jdk/javadoc/internal/doclets/formats/html/resources/stylesheet.css
@@ -66,7 +66,7 @@
     --snippet-background-color: #f2f2f4;
     --snippet-text-color: var(--block-text-color);
     --snippet-highlight-color: #f7c590;
-    --pre-background-color: #f3f3f5;
+    --pre-background-color: var(--snippet-background-color);
     --pre-text-color: var(--snippet-text-color);
     /* Border colors for structural elements and user defined tables */
     --border-color: #e6e6e6;
@@ -1648,7 +1648,7 @@ table.striped > tbody > tr > th {
     }
 }
 pre.snippet {
-    background-color: var(--code-background-color);
+    background-color: var(--snippet-background-color);
     color: var(--snippet-text-color);
     padding: 12px;
 }