rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
420 lines (362 loc) • 12.1 kB
JavaScript
// CSS Custom Highlight API-based Syntax Highlighter using bundled TextMate grammars
import astro from "microlighter/grammars/astro.js";
import bash from "microlighter/grammars/bash.js";
import c from "microlighter/grammars/c.js";
import cpp from "microlighter/grammars/cpp.js";
import csharp from "microlighter/grammars/csharp.js";
import cssGrammar from "microlighter/grammars/css.js";
import go from "microlighter/grammars/go.js";
import html from "microlighter/grammars/html.js";
import java from "microlighter/grammars/java.js";
import javascript from "microlighter/grammars/javascript.js";
import json from "microlighter/grammars/json.js";
import kotlin from "microlighter/grammars/kotlin.js";
import markdown from "microlighter/grammars/markdown.js";
import objectiveC from "microlighter/grammars/objective-c.js";
import perl from "microlighter/grammars/perl.js";
import php from "microlighter/grammars/php.js";
import powershell from "microlighter/grammars/powershell.js";
import python from "microlighter/grammars/python.js";
import r from "microlighter/grammars/r.js";
import ruby from "microlighter/grammars/ruby.js";
import rust from "microlighter/grammars/rust.js";
import svelte from "microlighter/grammars/svelte.js";
import toml from "microlighter/grammars/toml.js";
import tsx from "microlighter/grammars/tsx.js";
import typescript from "microlighter/grammars/typescript.js";
import vue from "microlighter/grammars/vue.js";
import yaml from "microlighter/grammars/yaml.js";
const httpGrammar = {
scopeName: "source.http",
patterns: [
{
match:
"^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|TRACE|CONNECT)\\s+([^\\s]+)\\s+(HTTP\\/[0-9.]+)",
captures: {
1: { name: "keyword.control.http" },
2: { name: "string.unquoted.http" },
3: { name: "keyword.other.http" },
},
},
{
match: "^(HTTP\\/[0-9.]+)\\s+([0-9]{3})(?:\\s+(.*))?",
captures: {
1: { name: "keyword.other.http" },
2: { name: "constant.numeric.http" },
3: { name: "string.unquoted.http" },
},
},
{
match: "^([a-zA-Z0-9_-]+):\\s*(.*)",
captures: {
1: { name: "support.type.property-name.http" },
2: { name: "string.unquoted.http" },
},
},
],
};
const grammars = {
astro,
bash,
c,
cpp,
csharp,
css: cssGrammar,
go,
html,
http: httpGrammar,
java,
javascript,
json,
kotlin,
markdown,
"objective-c": objectiveC,
perl,
php,
powershell,
python,
r,
ruby,
rust,
svelte,
toml,
tsx,
typescript,
vue,
yaml,
};
const scopes = new Map();
Object.values(grammars).forEach((grammar) => {
if (grammar?.scopeName) {
scopes.set(grammar.scopeName, grammar);
}
});
const languageAliases = {
"c++": "cpp",
cs: "csharp",
curl: "bash",
js: "javascript",
jsx: "javascript",
markup: "html",
md: "markdown",
objc: "objective-c",
ps1: "powershell",
py: "python",
rb: "ruby",
sh: "bash",
shell: "bash",
svg: "html",
ts: "typescript",
xml: "html",
yml: "yaml",
zsh: "bash",
};
const noMatch = { indices: [[Infinity, Infinity]] };
const highlights = new Map();
const getCategory = (scope) => {
const parts = scope.split(".");
const [first, second, third] = parts;
const last = parts.at(-1);
if (first === "markup" && ["quote", "inserted", "deleted", "raw"].includes(second)) return second;
if (first === "entity" && second === "name") return third;
if (scope.startsWith("constant.character.entity")) return "character-entity";
if (parts.includes("numeric")) return "numeric";
if (scope.startsWith("support.type.property-name")) return "property";
if (parts.includes("attribute-value")) return "attribute-value";
if (scope.startsWith("string.other.link")) return "link";
if (
[
"doctype",
"at-rule",
"important",
"regexp",
"boolean",
"symbol",
"operator",
"attribute-name",
].includes(last)
)
return last;
if (
[
"comment",
"string",
"constant",
"storage",
"keyword",
"variable",
"punctuation",
"entity",
"support",
].includes(first)
)
return first;
};
const addRange = (node, start, end, scope) => {
const category = getCategory(scope);
if (!category || start === end) return;
const range = new Range();
range.setStart(node, start);
range.setEnd(node, end);
if (!highlights.has(category)) {
highlights.set(category, new Highlight());
}
highlights.get(category).add(range);
};
const addCaptures = (node, match, captures = {}) => {
Object.entries(captures).forEach(([index, capture]) => {
const offsets = match.indices[+index];
if (offsets) addRange(node, ...offsets, capture.name);
});
};
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const expandEnd = (pattern, beginMatch) =>
pattern.replace(/\\(\d+)/g, (reference, index) =>
beginMatch[index] === undefined ? reference : escapeRegex(beginMatch[index]),
);
const getRules = (rule) => {
if (!rule) return [];
if (Array.isArray(rule)) return rule;
if (rule.match || rule.begin || rule.include) return [rule];
return rule.patterns || [];
};
const resolveInclude = (include, grammar, baseGrammar) => {
if (include === "$self") return { grammar, rules: grammar.patterns };
if (include === "$base") return { grammar: baseGrammar, rules: baseGrammar.patterns };
if (include[0] === "#") {
return { grammar, rules: getRules(grammar.repository?.[include.slice(1)]) };
}
const [scopeName, repositoryName] = include.split("#");
const includedGrammar = scopes.get(scopeName);
if (!includedGrammar) return null;
return {
grammar: includedGrammar,
rules: repositoryName
? getRules(includedGrammar.repository?.[repositoryName])
: includedGrammar.patterns,
};
};
const expandRules = (rules, grammar, baseGrammar, activeIncludes = new Set()) => {
const expanded = [];
rules.forEach((rule) => {
if (rule.include) {
const includeKey = `${grammar.scopeName}:${rule.include}`;
if (activeIncludes.has(includeKey)) return;
const included = resolveInclude(rule.include, grammar, baseGrammar);
if (!included) return;
const nestedIncludes = new Set(activeIncludes);
nestedIncludes.add(includeKey);
expanded.push(...expandRules(included.rules, included.grammar, baseGrammar, nestedIncludes));
return;
}
if (rule.match || (rule.begin && rule.end)) expanded.push({ grammar, rule });
});
return expanded;
};
const regexes = new Map();
let matches = new Map();
const exec = (pattern, node, start, end) => {
let match = matches.get(pattern);
if (!match || match.indices[0][0] < start) {
if (!regexes.has(pattern)) regexes.set(pattern, new RegExp(pattern, "dgm"));
const regex = regexes.get(pattern);
regex.lastIndex = start;
match = regex.exec(node.data) || noMatch;
matches.set(pattern, match);
}
return match.indices[0][0] < end && match.indices[0][1] <= end ? match : null;
};
const nextRule = (node, contexts, start, end) => {
let winner = null;
contexts.forEach((context) => {
const pattern = context.rule.match || context.rule.begin;
const match = exec(pattern, node, start, end);
if (!match) return;
if (!winner || match.indices[0][0] < winner.match.indices[0][0]) {
winner = { ...context, match };
}
});
return winner;
};
const scanRegion = (node, rules, start, end, grammar, baseGrammar = grammar, closing = null) => {
const contexts = expandRules(rules, grammar, baseGrammar);
let cursor = start;
while (cursor < end) {
const candidate = nextRule(node, contexts, cursor, end);
const endMatch = closing ? exec(closing.pattern, node, cursor, end) : null;
const candidateStart = candidate?.match.indices[0][0] ?? Infinity;
const endStart = endMatch?.indices[0][0] ?? Infinity;
if (
endMatch &&
(endStart < candidateStart || (endStart === candidateStart && !closing?.applyEndPatternLast))
) {
return { contentEnd: endStart, end: endMatch.indices[0][1], match: endMatch };
}
if (!candidate) return { contentEnd: end, end, match: null };
const { rule, match, grammar: ruleGrammar } = candidate;
if (rule.match) {
if (rule.name) addRange(node, ...match.indices[0], rule.name);
addCaptures(node, match, rule.captures);
cursor = match.indices[0][1] > cursor ? match.indices[0][1] : cursor + 1;
continue;
}
const nested = scanRegion(
node,
rule.patterns || [],
match.indices[0][1],
end,
ruleGrammar,
baseGrammar,
{
pattern: expandEnd(rule.end, match),
applyEndPatternLast: rule.applyEndPatternLast,
},
);
if (rule.name) addRange(node, match.indices[0][0], nested.end, rule.name);
if (rule.contentName) addRange(node, match.indices[0][1], nested.contentEnd, rule.contentName);
addCaptures(node, match, rule.beginCaptures || rule.captures);
if (nested.match) addCaptures(node, nested.match, rule.endCaptures || rule.captures);
cursor = nested.end > cursor ? nested.end : cursor + 1;
}
return { contentEnd: end, end, match: null };
};
export const getLanguage = (codeBlock) => {
const pre = codeBlock.parentElement;
const getLangClass = (el) => {
if (!el || !el.classList) return null;
return [...el.classList].find((c) => c.startsWith("language-"))?.slice("language-".length);
};
const lang =
getLangClass(codeBlock) ||
codeBlock.dataset?.language ||
getLangClass(pre) ||
pre?.dataset?.language ||
pre?.getAttribute?.("lang") ||
"";
return lang.toLowerCase();
};
export const normalizeLanguage = (lang) => languageAliases[lang] || lang;
export const getAllCodeBlocks = (rootNode = document) => {
const blocks = [];
const visitedRoots = new Set();
const traverse = (node) => {
if (!node || visitedRoots.has(node)) return;
visitedRoots.add(node);
if (node.querySelectorAll) {
const codeEls = node.querySelectorAll("pre > code");
blocks.push(...codeEls);
const allChildren = node.querySelectorAll("*");
for (let i = 0; i < allChildren.length; i++) {
const child = allChildren[i];
if (child.shadowRoot) {
traverse(child.shadowRoot);
}
}
}
};
traverse(rootNode);
return blocks;
};
let highlightScheduled = false;
export const highlightAllCode = (rootNode = document) => {
if (typeof window === "undefined" || typeof CSS === "undefined" || !CSS.highlights) {
return;
}
// Find all code blocks across the root and any nested shadow roots
const codeBlocks = getAllCodeBlocks(rootNode).filter(getLanguage);
if (codeBlocks.length === 0) return;
// Clear previous ranges
highlights.forEach((ranges, category) => {
if (CSS.highlights.get(category) === ranges) CSS.highlights.delete(category);
ranges.clear();
});
codeBlocks.forEach((codeBlock) => {
const rawLang = getLanguage(codeBlock);
const lang = normalizeLanguage(rawLang);
const grammar = grammars[lang];
if (!grammar) return;
// Scan all text nodes without mutating or removing Lit marker comment nodes
const textNodes = Array.from(codeBlock.childNodes).filter(
(child) => child.nodeType === Node.TEXT_NODE && child.data?.length > 0,
);
if (textNodes.length === 0) return;
matches = new Map();
textNodes.forEach((node) => {
scanRegion(node, grammar.patterns, 0, node.data.length, grammar);
});
});
highlights.forEach((ranges, category) => {
if (ranges.size) CSS.highlights.set(category, ranges);
});
};
export const scheduleHighlight = (rootNode = document) => {
if (typeof window === "undefined" || typeof CSS === "undefined" || !CSS.highlights) {
return;
}
if (highlightScheduled) return;
highlightScheduled = true;
requestAnimationFrame(() => {
highlightScheduled = false;
highlightAllCode(rootNode);
});
};