mirror of
https://github.com/goauthentik/authentik
synced 2026-05-07 07:32:23 +02:00
Compare commits
4 Commits
blueprint_
...
mdx-sans-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f7802a868 | ||
|
|
35f84aa61e | ||
|
|
d969619787 | ||
|
|
bafdd10f94 |
134
web/bundler/mdx-plugin/compile.js
Normal file
134
web/bundler/mdx-plugin/compile.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @file Build-time markdown → HTML pipeline.
|
||||
*
|
||||
* The output is wrapped in a `<div class="pf-c-content" part="content">`
|
||||
* envelope so consuming `<ak-mdx>` elements can rely on PatternFly content
|
||||
* styles and expose CSS parts (`title`, `content`) to host pages.
|
||||
*/
|
||||
|
||||
import { rehypeAnchors, rehypeMermaid } from "./rehype.js";
|
||||
import {
|
||||
normalizeAdmonitionLabels,
|
||||
remarkAdmonition,
|
||||
remarkHeadings,
|
||||
remarkLists,
|
||||
} from "./remark.js";
|
||||
|
||||
import { toHtml } from "hast-util-to-html";
|
||||
import apacheGrammar from "highlight.js/lib/languages/apache";
|
||||
import diffGrammar from "highlight.js/lib/languages/diff";
|
||||
import confGrammar from "highlight.js/lib/languages/ini";
|
||||
import nginxGrammar from "highlight.js/lib/languages/nginx";
|
||||
import { common } from "lowlight";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import remarkDirective from "remark-directive";
|
||||
import remarkFrontmatter from "remark-frontmatter";
|
||||
import remarkGFM from "remark-gfm";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import { unified } from "unified";
|
||||
import { parse as parseYAML } from "yaml";
|
||||
|
||||
/**
|
||||
* Pull a YAML frontmatter block off the top of `source` and return both
|
||||
* pieces. Returns an empty object if there is no frontmatter.
|
||||
*
|
||||
* @param {string} source
|
||||
* @returns {{ body: string, frontmatter: Record<string, unknown> }}
|
||||
*/
|
||||
function splitFrontmatter(source) {
|
||||
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
||||
if (!match) return { body: source, frontmatter: {} };
|
||||
const frontmatter = parseYAML(match[1]) || {};
|
||||
return { body: source.slice(match[0].length), frontmatter };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the wrapping `<div class="pf-c-content" part="content">` envelope
|
||||
* with an optional `<h1 part="title">` prefix, then serialize the whole
|
||||
* tree through `hast-util-to-html`. One serializer means one set of
|
||||
* escaping rules — no hand-rolled `&`/`<`/`>`/`"` replacement that has
|
||||
* to be remembered and audited separately.
|
||||
*
|
||||
* @param {import('hast').Element[]} bodyChildren Hast nodes from the markdown pipeline.
|
||||
* @param {string | null} title Frontmatter title, or `null` to omit the `<h1>`.
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderEnvelope(bodyChildren, title) {
|
||||
/** @type {import('hast').Element[]} */
|
||||
const children = [];
|
||||
|
||||
if (title) {
|
||||
children.push({
|
||||
type: "element",
|
||||
tagName: "h1",
|
||||
properties: { part: "title" },
|
||||
children: [{ type: "text", value: title }],
|
||||
});
|
||||
}
|
||||
|
||||
children.push(...bodyChildren);
|
||||
|
||||
/** @type {import('hast').Root} */
|
||||
const root = {
|
||||
type: "root",
|
||||
children: [
|
||||
{
|
||||
type: "element",
|
||||
tagName: "div",
|
||||
properties: { className: ["pf-c-content"], part: "content" },
|
||||
children,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return toHtml(root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a markdown source string to a wrapped HTML string and parsed
|
||||
* frontmatter. Used by the build-time plugin; the runtime side mirrors
|
||||
* this pipeline in the browser for admin-supplied prose.
|
||||
*
|
||||
* @param {string} source
|
||||
* @param {string} publicDirectory Path of the file's directory inside the
|
||||
* docs site, used to resolve relative `<a>` hrefs at build time.
|
||||
* @returns {Promise<{ html: string, frontmatter: Record<string, unknown> }>}
|
||||
*/
|
||||
export async function compileMarkdown(source, publicDirectory) {
|
||||
const { body: rawBody, frontmatter } = splitFrontmatter(source);
|
||||
const body = normalizeAdmonitionLabels(rawBody);
|
||||
|
||||
// Run the pipeline up to (but not including) HTML stringification —
|
||||
// we want the hast tree so we can splice it into the envelope and
|
||||
// serialize the whole thing in one pass below.
|
||||
const processor = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGFM)
|
||||
.use(remarkFrontmatter, ["yaml"])
|
||||
.use(remarkDirective)
|
||||
.use(remarkAdmonition)
|
||||
.use(remarkHeadings)
|
||||
.use(remarkLists)
|
||||
.use(remarkRehype, { allowDangerousHtml: false })
|
||||
.use(rehypeAnchors, { publicDirectory })
|
||||
.use(rehypeHighlight, {
|
||||
languages: {
|
||||
...common,
|
||||
nginx: nginxGrammar,
|
||||
apache: apacheGrammar,
|
||||
conf: confGrammar,
|
||||
diff: diffGrammar,
|
||||
},
|
||||
})
|
||||
.use(rehypeMermaid);
|
||||
|
||||
const tree = /** @type {import('hast').Root} */ (
|
||||
await processor.run(processor.parse(body), body)
|
||||
);
|
||||
|
||||
const title = typeof frontmatter.title === "string" ? frontmatter.title : null;
|
||||
const html = renderEnvelope(/** @type {import('hast').Element[]} */ (tree.children), title);
|
||||
|
||||
return { html, frontmatter };
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
/**
|
||||
* @file MDX plugin for ESBuild.
|
||||
* @file Markdown plugin for ESBuild.
|
||||
*
|
||||
* Resolves `~docs/...` imports to the website docs tree, then compiles each
|
||||
* `.md` / `.mdx` file to HTML at build time. The compiled HTML uses
|
||||
* `<ak-md-a>` and `<ak-alert>` custom elements so the runtime side can
|
||||
* stamp the HTML directly into shadow DOM without any client-side
|
||||
* JavaScript evaluation — this is what lets the page CSP drop
|
||||
* `'unsafe-eval'`.
|
||||
*
|
||||
* The on-load result is shipped via the `file` loader so the JSON travels
|
||||
* over the existing fetch-then-set-innerHTML path used by `<ak-mdx>`. The
|
||||
* shape is `{ content, frontmatter, publicPath, publicDirectory }` where
|
||||
* `content` is now pre-rendered HTML rather than raw markdown source.
|
||||
*
|
||||
* @import {
|
||||
* OnLoadArgs,
|
||||
@@ -14,35 +26,25 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { MonoRepoRoot } from "@goauthentik/core/paths/node";
|
||||
import { compileMarkdown } from "./compile.js";
|
||||
|
||||
/**
|
||||
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData Data passed to `onload`.
|
||||
*
|
||||
* @typedef LoadDataFields Extra fields given in `data` to `onload`.
|
||||
* @property {PluginData | null | undefined} [pluginData] Plugin data.
|
||||
*
|
||||
* @typedef PluginData Extra data passed.
|
||||
* @property {Buffer | string | null | undefined} [contents] File contents.
|
||||
*/
|
||||
import { MonoRepoRoot } from "@goauthentik/core/paths/node";
|
||||
|
||||
const pluginName = "mdx-plugin";
|
||||
|
||||
/**
|
||||
* @typedef MDXPluginOptions
|
||||
*
|
||||
* @property {string} root Root directory.
|
||||
* @property {string} root Repository root.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bundle MDX into JSON modules.
|
||||
* Bundle markdown and MDX source into JSON modules.
|
||||
*
|
||||
* @param {MDXPluginOptions} options
|
||||
* @returns {Plugin}
|
||||
*/
|
||||
export function mdxPlugin({ root }) {
|
||||
const prefix = "~docs";
|
||||
|
||||
// TODO: Replace with `resolvePackage` after NPM Workspaces support is added.
|
||||
const docsPackageRoot = path.resolve(MonoRepoRoot, "website");
|
||||
|
||||
@@ -59,32 +61,32 @@ export function mdxPlugin({ root }) {
|
||||
|
||||
return {
|
||||
path: path.join(docsPackageRoot, "docs", args.path.slice(prefix.length)),
|
||||
|
||||
pluginName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LoadData} data
|
||||
* @param {OnLoadArgs} args
|
||||
* @returns {Promise<OnLoadResult>}
|
||||
*/
|
||||
async function loadListener(data) {
|
||||
const content = String(
|
||||
data.pluginData &&
|
||||
data.pluginData.contents !== null &&
|
||||
data.pluginData.contents !== undefined
|
||||
? data.pluginData.contents
|
||||
: await fs.readFile(data.path),
|
||||
);
|
||||
async function loadListener(args) {
|
||||
const source = String(await fs.readFile(args.path));
|
||||
|
||||
const publicPath = path.resolve(
|
||||
"/",
|
||||
path.relative(path.join(root, "website", "docs"), data.path),
|
||||
path.relative(path.join(root, "website", "docs"), args.path),
|
||||
);
|
||||
const publicDirectory = path.dirname(publicPath);
|
||||
|
||||
const { html, frontmatter } = await compileMarkdown(source, publicDirectory);
|
||||
|
||||
return {
|
||||
contents: JSON.stringify({ content, publicPath, publicDirectory }),
|
||||
contents: JSON.stringify({
|
||||
content: html,
|
||||
frontmatter,
|
||||
publicPath,
|
||||
publicDirectory,
|
||||
}),
|
||||
loader: "file",
|
||||
pluginName,
|
||||
};
|
||||
|
||||
117
web/bundler/mdx-plugin/rehype.js
Normal file
117
web/bundler/mdx-plugin/rehype.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @file Rehype plugins for the build-time markdown pipeline.
|
||||
*/
|
||||
|
||||
import { CurrentReleaseDocsURL } from "@goauthentik/core/version/node";
|
||||
|
||||
import { SKIP, visit } from "unist-util-visit";
|
||||
|
||||
/**
|
||||
* Resolve a relative `href` against the docs base URL. Same logic the old
|
||||
* runtime `MDXAnchor` used: take a `./...` href relative to the file's
|
||||
* `publicDirectory`, drop trailing `index`/`.md`/`.mdx`, and absolutize
|
||||
* against {@linkcode CurrentReleaseDocsURL}.
|
||||
*
|
||||
* @param {string} href
|
||||
* @param {string} publicDirectory
|
||||
* @returns {string}
|
||||
*/
|
||||
function resolveDocsHref(href, publicDirectory) {
|
||||
// `new URL(...)` against `file:///` lets us reuse the browser-style
|
||||
// path resolver while preserving the hash and any query string.
|
||||
const joined = `${publicDirectory}/${href}`.replace(/\/{2,}/g, "/");
|
||||
const placeholder = new URL(joined, "file:///");
|
||||
const next = new URL(placeholder.pathname, CurrentReleaseDocsURL);
|
||||
next.pathname = next.pathname.replace(/(index)?\.mdx?$/, "");
|
||||
next.search = placeholder.search;
|
||||
next.hash = placeholder.hash;
|
||||
return next.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehype plugin: resolve relative anchors at build time and wrap every
|
||||
* `<a>` in an `<ak-md-a>` light-DOM custom element. The wrapper attaches
|
||||
* the fragment-link click interceptor at runtime so clicks on
|
||||
* `<a href="#section">` scroll within the host shadow tree rather than
|
||||
* overwriting `location.hash` (which would yank the hash-routed SPA off
|
||||
* its current page).
|
||||
*
|
||||
* Wrapping (rather than replacing) keeps the real `<a>` element inside
|
||||
* `<ak-mdx>`'s shadow tree where the existing PatternFly link CSS in
|
||||
* `styles.css` applies. The wrapper itself uses `display: contents` so
|
||||
* it does not perturb inline-flow layout.
|
||||
*
|
||||
* @param {{ publicDirectory: string }} options
|
||||
*/
|
||||
export function rehypeAnchors({ publicDirectory }) {
|
||||
return (/** @type {import('hast').Root} */ tree) => {
|
||||
visit(tree, "element", (node) => {
|
||||
if (node.tagName !== "a") return;
|
||||
|
||||
const props = node.properties || (node.properties = {});
|
||||
const href = typeof props.href === "string" ? props.href : "";
|
||||
|
||||
if (!href) return;
|
||||
|
||||
if (href.startsWith(".")) {
|
||||
props.href = resolveDocsHref(href, publicDirectory);
|
||||
props.target = "_blank";
|
||||
props.rel = "noopener noreferrer";
|
||||
} else if (!href.startsWith("#")) {
|
||||
// Already-absolute external link: open in a new tab.
|
||||
props.target = "_blank";
|
||||
props.rel = "noopener noreferrer";
|
||||
}
|
||||
|
||||
// Wrap the anchor in `<ak-md-a>` by mutating the node in
|
||||
// place: the `<a>`'s contents become a single child, the
|
||||
// outer node becomes the wrapper. Returning `SKIP` keeps
|
||||
// the visitor from descending into the freshly-stamped
|
||||
// child anchor (which would re-match this filter and
|
||||
// recurse forever).
|
||||
/** @type {import('hast').Element} */
|
||||
const original = {
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
properties: { ...props },
|
||||
children: node.children,
|
||||
};
|
||||
|
||||
node.tagName = "ak-md-a";
|
||||
node.properties = {};
|
||||
node.children = [original];
|
||||
|
||||
return SKIP;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rehype plugin: replace `language-mermaid` code blocks with
|
||||
* `<ak-diagram>` elements carrying the mermaid source as text content.
|
||||
* `<ak-diagram>` reads its own `textContent` and renders the SVG, so no
|
||||
* wrapper element is needed.
|
||||
*/
|
||||
export function rehypeMermaid() {
|
||||
return (/** @type {import('hast').Root} */ tree) => {
|
||||
visit(tree, "element", (node) => {
|
||||
if (node.tagName !== "pre") return;
|
||||
const child = node.children?.[0];
|
||||
if (!child || child.type !== "element" || child.tagName !== "code") return;
|
||||
|
||||
const className = child.properties?.className ?? [];
|
||||
const classes = Array.isArray(className) ? className : [className];
|
||||
if (!classes.includes("language-mermaid")) return;
|
||||
|
||||
const source = (child.children ?? [])
|
||||
.map((c) => (c.type === "text" ? c.value : ""))
|
||||
.join("");
|
||||
|
||||
node.tagName = "ak-diagram";
|
||||
node.properties = {};
|
||||
node.children = [{ type: "text", value: source }];
|
||||
|
||||
return SKIP;
|
||||
});
|
||||
};
|
||||
}
|
||||
134
web/bundler/mdx-plugin/remark.js
Normal file
134
web/bundler/mdx-plugin/remark.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @file Remark plugins for the build-time markdown pipeline.
|
||||
*
|
||||
* The runtime side (`src/elements/ak-mdx/remark/*`) mirrors a subset of
|
||||
* these. Keeping the shapes parallel makes it easier to spot drift when
|
||||
* either pipeline grows a new transform.
|
||||
*/
|
||||
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
const ADMONITIONS = new Set(["info", "warning", "danger", "note", "caution", "tip"]);
|
||||
|
||||
/**
|
||||
* `caution` and `tip` aren't first-class PatternFly alert levels — map
|
||||
* them onto the closest equivalent so PFAlert styles render correctly.
|
||||
*/
|
||||
const ADMONITION_LEVEL = {
|
||||
info: "pf-m-info",
|
||||
warning: "pf-m-warning",
|
||||
danger: "pf-m-danger",
|
||||
note: "pf-m-info",
|
||||
caution: "pf-m-warning",
|
||||
tip: "pf-m-success",
|
||||
};
|
||||
|
||||
/**
|
||||
* Match a Docusaurus-style admonition opening line:
|
||||
*
|
||||
* :::caution Reserved application slugs
|
||||
*
|
||||
* `remark-directive` only understands the spec form `:::name[label]{attrs}`
|
||||
* — a bare-space label silently falls through as plain text. We rewrite
|
||||
* the source so the directive parser sees the bracketed form and the
|
||||
* label is preserved as the directive's first paragraph.
|
||||
*/
|
||||
const ADMONITION_BARE_LABEL_RE = new RegExp(
|
||||
`^(:::(?:${[...ADMONITIONS].join("|")}))[ \\t]+(.+?)[ \\t]*$`,
|
||||
"gm",
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {string} source
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeAdmonitionLabels(source) {
|
||||
return source.replace(ADMONITION_BARE_LABEL_RE, "$1[$2]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugin: convert `:::info` / `:::warning` / `:::danger` / `:::note`
|
||||
* directives into `<ak-alert>` elements with a level attribute. The first
|
||||
* child paragraph carrying the `directiveLabel` flag (i.e. `:::info[Title]`
|
||||
* syntax) is promoted to a `<strong>` so the title renders as a heading-ish
|
||||
* element inside the slot.
|
||||
*/
|
||||
export function remarkAdmonition() {
|
||||
return (/** @type {import('mdast').Root} */ tree) => {
|
||||
visit(tree, (node) => {
|
||||
if (
|
||||
node.type !== "containerDirective" &&
|
||||
node.type !== "leafDirective" &&
|
||||
node.type !== "textDirective"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!ADMONITIONS.has(node.name)) return;
|
||||
|
||||
const tagName = node.type === "textDirective" ? "span" : "ak-alert";
|
||||
const data = node.data || (node.data = {});
|
||||
data.hName = tagName;
|
||||
data.hProperties = {
|
||||
...(data.hProperties || {}),
|
||||
...(node.attributes || {}),
|
||||
level:
|
||||
/** @type {Record<string, string>} */ (ADMONITION_LEVEL)[node.name] ??
|
||||
`pf-m-${node.name}`,
|
||||
};
|
||||
|
||||
const children = /** @type {any[]} */ (node.children || []);
|
||||
const labelIndex = children.findIndex(
|
||||
(c) => c.type === "paragraph" && c.data?.directiveLabel,
|
||||
);
|
||||
if (labelIndex !== -1) {
|
||||
const label = children[labelIndex];
|
||||
children[labelIndex] = {
|
||||
type: "paragraph",
|
||||
children: [{ type: "strong", children: label.children }],
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugin: kebab-case heading slugs into `id` attributes.
|
||||
*/
|
||||
export function remarkHeadings() {
|
||||
/**
|
||||
* @param {{ value?: string, children?: any[] }} n
|
||||
* @returns {string}
|
||||
*/
|
||||
const flatten = (n) => {
|
||||
if (n.value) return n.value;
|
||||
if (n.children) return n.children.map(flatten).join("");
|
||||
return "";
|
||||
};
|
||||
|
||||
return (/** @type {import('mdast').Root} */ tree) => {
|
||||
visit(tree, "heading", (node) => {
|
||||
const id = flatten(node)
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
const data = node.data || (node.data = {});
|
||||
data.hProperties = { ...(data.hProperties || {}), id };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugin: tag lists with PatternFly's content class.
|
||||
*/
|
||||
export function remarkLists() {
|
||||
return (/** @type {import('mdast').Root} */ tree) => {
|
||||
visit(tree, "list", (node) => {
|
||||
const data = node.data || (node.data = {});
|
||||
data.hProperties = {
|
||||
...(data.hProperties || {}),
|
||||
className: "pf-c-list",
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
786
web/package-lock.json
generated
786
web/package-lock.json
generated
@@ -35,7 +35,6 @@
|
||||
"@lit/localize-tools": "^0.8.1",
|
||||
"@lit/reactive-element": "^2.1.2",
|
||||
"@lit/task": "^1.0.3",
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@mrmarble/djangoql-completion": "^0.8.3",
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@openlayers-elements/core": "^0.4.0",
|
||||
@@ -77,6 +76,7 @@
|
||||
"fuse.js": "^7.3.0",
|
||||
"globals": "^17.5.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hast-util-to-html": "^9.0.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"knip": "^6.6.0",
|
||||
"lex": "^2025.11.0",
|
||||
@@ -85,6 +85,7 @@
|
||||
"lit-element": "^4.2.2",
|
||||
"lit-html": "^3.3.2",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mermaid": "^11.14.0",
|
||||
"node-domexception": "^2025.11.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -95,16 +96,14 @@
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"pseudolocale": "^2.2.0",
|
||||
"rapidoc": "^9.3.8",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-mermaid": "^3.0.0",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-directive": "^4.0.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-mdx-frontmatter": "^5.2.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"storybook": "^10.2.1",
|
||||
"style-mod": "^4.1.3",
|
||||
"trusted-types": "^2.0.0",
|
||||
@@ -113,6 +112,7 @@
|
||||
"type-fest": "^5.6.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"vite": "^8.0.8",
|
||||
"vitest": "^4.1.1",
|
||||
@@ -1762,43 +1762,6 @@
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mdx-js/mdx": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz",
|
||||
"integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdx": "^2.0.0",
|
||||
"acorn": "^8.0.0",
|
||||
"collapse-white-space": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"estree-util-scope": "^1.0.0",
|
||||
"estree-walker": "^3.0.0",
|
||||
"hast-util-to-jsx-runtime": "^2.0.0",
|
||||
"markdown-extensions": "^2.0.0",
|
||||
"recma-build-jsx": "^1.0.0",
|
||||
"recma-jsx": "^1.0.0",
|
||||
"recma-stringify": "^1.0.0",
|
||||
"rehype-recma": "^1.0.0",
|
||||
"remark-mdx": "^3.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.0.0",
|
||||
"source-map": "^0.7.0",
|
||||
"unified": "^11.0.0",
|
||||
"unist-util-position-from-estree": "^2.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/@mdx-js/react": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz",
|
||||
@@ -5200,15 +5163,6 @@
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree-jsx": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
|
||||
"integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
@@ -6478,15 +6432,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/astring": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",
|
||||
"integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"astring": "bin/astring"
|
||||
}
|
||||
},
|
||||
"node_modules/async-function": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
||||
@@ -7301,16 +7246,6 @@
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/collapse-white-space": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
|
||||
"integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -8635,38 +8570,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/esast-util-from-estree": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz",
|
||||
"integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-visit": "^2.0.0",
|
||||
"unist-util-position-from-estree": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/esast-util-from-js": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz",
|
||||
"integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"acorn": "^8.0.0",
|
||||
"esast-util-from-estree": "^2.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
@@ -9108,100 +9011,6 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-attach-comments": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz",
|
||||
"integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-build-jsx": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz",
|
||||
"integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"estree-walker": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-is-identifier-name": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
|
||||
"integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-scope": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz",
|
||||
"integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"devlop": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-to-js": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz",
|
||||
"integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"astring": "^1.8.0",
|
||||
"source-map": "^0.7.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-value-to-estree": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz",
|
||||
"integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/remcohaszing"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-visit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz",
|
||||
"integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
@@ -10274,21 +10083,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-dom": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz",
|
||||
"integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"hastscript": "^9.0.0",
|
||||
"web-namespaces": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-html": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
|
||||
@@ -10307,22 +10101,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-html-isomorphic": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz",
|
||||
"integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"hast-util-from-dom": "^5.0.0",
|
||||
"hast-util-from-html": "^2.0.0",
|
||||
"unist-util-remove-position": "^5.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-from-parse5": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
|
||||
@@ -10369,34 +10147,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-estree": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz",
|
||||
"integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-attach-comments": "^3.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"mdast-util-mdx-expression": "^2.0.0",
|
||||
"mdast-util-mdx-jsx": "^3.0.0",
|
||||
"mdast-util-mdxjs-esm": "^2.0.0",
|
||||
"property-information": "^7.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"style-to-js": "^1.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"zwitch": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-html": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
|
||||
@@ -10420,33 +10170,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-jsx-runtime": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
||||
"integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"mdast-util-mdx-expression": "^2.0.0",
|
||||
"mdast-util-mdx-jsx": "^3.0.0",
|
||||
"mdast-util-mdxjs-esm": "^2.0.0",
|
||||
"property-information": "^7.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"style-to-js": "^1.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-to-text": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
|
||||
@@ -10688,12 +10411,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/inline-style-parser": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
|
||||
"integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inspect-with-kind": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz",
|
||||
@@ -12256,18 +11973,6 @@
|
||||
"integrity": "sha512-VJ6nB8emkO9VODI0Fk+TQ/0zKBTqmf/Pkt8Xv0kHstoc0iXRajA00DAid4Kc3K5xeFIOoiZrVxijEzj0GLVO2w==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/markdown-extensions": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz",
|
||||
"integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-table": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
||||
@@ -12512,83 +12217,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdx": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz",
|
||||
"integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-mdx-expression": "^2.0.0",
|
||||
"mdast-util-mdx-jsx": "^3.0.0",
|
||||
"mdast-util-mdxjs-esm": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdx-expression": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
|
||||
"integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdx-jsx": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
|
||||
"integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"devlop": "^1.1.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0",
|
||||
"parse-entities": "^4.0.0",
|
||||
"stringify-entities": "^4.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdxjs-esm": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
|
||||
"integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-phrasing": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
|
||||
@@ -12710,37 +12338,6 @@
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid-isomorphic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid-isomorphic/-/mermaid-isomorphic-3.1.0.tgz",
|
||||
"integrity": "sha512-mzrvfEVjnJIkJlEqxp3eMuR1wS0TeLCH1VK5E/T5yzWaBwI3JqjJuw70yUIThSCDJ5bRs6O3rgfp00oBAbvSeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||
"katex": "^0.16.0",
|
||||
"mermaid": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/remcohaszing"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid-isomorphic/node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
|
||||
"integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
|
||||
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
|
||||
@@ -12966,108 +12563,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-mdx-expression": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz",
|
||||
"integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-mdx-expression": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-events-to-acorn": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-mdx-jsx": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz",
|
||||
"integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"micromark-factory-mdx-expression": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-events-to-acorn": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-mdx-md": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz",
|
||||
"integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-mdxjs": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz",
|
||||
"integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.0.0",
|
||||
"acorn-jsx": "^5.0.0",
|
||||
"micromark-extension-mdx-expression": "^3.0.0",
|
||||
"micromark-extension-mdx-jsx": "^3.0.0",
|
||||
"micromark-extension-mdx-md": "^2.0.0",
|
||||
"micromark-extension-mdxjs-esm": "^3.0.0",
|
||||
"micromark-util-combine-extensions": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-mdxjs-esm": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz",
|
||||
"integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-core-commonmark": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-events-to-acorn": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0",
|
||||
"unist-util-position-from-estree": "^2.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-destination": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
||||
@@ -13111,33 +12606,6 @@
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-mdx-expression": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz",
|
||||
"integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-events-to-acorn": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0",
|
||||
"unist-util-position-from-estree": "^2.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-space": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
|
||||
@@ -13339,31 +12807,6 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromark-util-events-to-acorn": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz",
|
||||
"integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "GitHub Sponsors",
|
||||
"url": "https://github.com/sponsors/unifiedjs"
|
||||
},
|
||||
{
|
||||
"type": "OpenCollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-visit": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-util-html-tag-name": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
|
||||
@@ -13572,15 +13015,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-svg-data-uri": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||
"integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mini-svg-data-uri": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/minim": {
|
||||
"version": "0.23.8",
|
||||
"resolved": "https://registry.npmjs.org/minim/-/minim-0.23.8.tgz",
|
||||
@@ -15343,73 +14777,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recma-build-jsx": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz",
|
||||
"integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-util-build-jsx": "^3.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/recma-jsx": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz",
|
||||
"integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn-jsx": "^5.0.0",
|
||||
"estree-util-to-js": "^2.0.0",
|
||||
"recma-parse": "^1.0.0",
|
||||
"recma-stringify": "^1.0.0",
|
||||
"unified": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recma-parse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz",
|
||||
"integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"esast-util-from-js": "^2.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/recma-stringify": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz",
|
||||
"integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-util-to-js": "^2.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/redent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
@@ -15482,34 +14849,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-mermaid": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rehype-mermaid/-/rehype-mermaid-3.0.0.tgz",
|
||||
"integrity": "sha512-fxrD5E4Fa1WXUjmjNDvLOMT4XB1WaxcfycFIWiYU0yEMQhcTDElc9aDFnbDFRLxG1Cfo1I3mfD5kg4sjlWaB+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"hast-util-from-html-isomorphic": "^2.0.0",
|
||||
"hast-util-to-text": "^4.0.0",
|
||||
"mermaid-isomorphic": "^3.0.0",
|
||||
"mini-svg-data-uri": "^1.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"unist-util-visit-parents": "^6.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/remcohaszing"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-parse": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz",
|
||||
@@ -15525,21 +14864,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-recma": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz",
|
||||
"integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"hast-util-to-estree": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/rehype-stringify": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz",
|
||||
@@ -15605,37 +14929,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-mdx": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz",
|
||||
"integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-mdx": "^3.0.0",
|
||||
"micromark-extension-mdxjs": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-mdx-frontmatter": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-mdx-frontmatter/-/remark-mdx-frontmatter-5.2.0.tgz",
|
||||
"integrity": "sha512-U/hjUYTkQqNjjMRYyilJgLXSPF65qbLPdoESOkXyrwz2tVyhAnm4GUKhfXqOOS9W34M3545xEMq+aMpHgVjEeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"estree-util-value-to-estree": "^3.0.0",
|
||||
"toml": "^3.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"unist-util-mdx-define": "^1.0.0",
|
||||
"yaml": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/remcohaszing"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-parse": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
||||
@@ -17408,24 +16701,6 @@
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/style-to-js": {
|
||||
"version": "1.1.21",
|
||||
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
|
||||
"integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"style-to-object": "1.0.14"
|
||||
}
|
||||
},
|
||||
"node_modules/style-to-object": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
|
||||
"integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inline-style-parser": "0.2.7"
|
||||
}
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
||||
@@ -17695,12 +16970,6 @@
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/toml": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
|
||||
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
@@ -18138,24 +17407,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-mdx-define": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-mdx-define/-/unist-util-mdx-define-1.1.2.tgz",
|
||||
"integrity": "sha512-9ncH7i7TN5Xn7/tzX5bE3rXgz1X/u877gYVAUB3mLeTKYJmQHmqKTDBi6BTGXV7AeolBCI9ErcVsOt2qryoD0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"estree-util-scope": "^1.0.0",
|
||||
"estree-walker": "^3.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/remcohaszing"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-position": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
|
||||
@@ -18169,33 +17420,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-position-from-estree": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz",
|
||||
"integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-remove-position": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
|
||||
"integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-stringify-position": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
||||
|
||||
@@ -111,7 +111,6 @@
|
||||
"@lit/localize-tools": "^0.8.1",
|
||||
"@lit/reactive-element": "^2.1.2",
|
||||
"@lit/task": "^1.0.3",
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@mrmarble/djangoql-completion": "^0.8.3",
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@openlayers-elements/core": "^0.4.0",
|
||||
@@ -153,6 +152,7 @@
|
||||
"fuse.js": "^7.3.0",
|
||||
"globals": "^17.5.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hast-util-to-html": "^9.0.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"knip": "^6.6.0",
|
||||
"lex": "^2025.11.0",
|
||||
@@ -161,6 +161,7 @@
|
||||
"lit-element": "^4.2.2",
|
||||
"lit-html": "^3.3.2",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mermaid": "^11.14.0",
|
||||
"node-domexception": "^2025.11.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -171,16 +172,14 @@
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"pseudolocale": "^2.2.0",
|
||||
"rapidoc": "^9.3.8",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-mermaid": "^3.0.0",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-directive": "^4.0.0",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-mdx-frontmatter": "^5.2.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"storybook": "^10.2.1",
|
||||
"style-mod": "^4.1.3",
|
||||
"trusted-types": "^2.0.0",
|
||||
@@ -189,6 +188,7 @@
|
||||
"type-fest": "^5.6.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.57.2",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
"vite": "^8.0.8",
|
||||
"vitest": "^4.1.1",
|
||||
|
||||
@@ -6,22 +6,29 @@ import { Diagram } from "#elements/Diagram";
|
||||
|
||||
import { FlowsApi } from "@goauthentik/api";
|
||||
|
||||
import { PropertyValues } from "lit-element/lit-element.js";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-flow-diagram")
|
||||
export class FlowDiagram extends Diagram {
|
||||
@property()
|
||||
flowSlug?: string;
|
||||
@property({ type: String, useDefault: true })
|
||||
flowSlug: string | null = null;
|
||||
|
||||
refreshHandler = (): void => {
|
||||
this.diagram = undefined;
|
||||
protected override updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("flowSlug")) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
protected refresh = (): void => {
|
||||
new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsInstancesDiagramRetrieve({
|
||||
slug: this.flowSlug || "",
|
||||
})
|
||||
.then((data) => {
|
||||
this.diagram = data.diagram;
|
||||
this.requestUpdate();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,24 +9,28 @@ import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-source-oauth-diagram")
|
||||
export class OAuthSourceDiagram extends Diagram {
|
||||
@property({ attribute: false })
|
||||
source?: OAuthSource;
|
||||
@property({ attribute: false, useDefault: true })
|
||||
public source: OAuthSource | null = null;
|
||||
|
||||
refreshHandler = (): void => {
|
||||
protected override syncDiagramContent = (): void => {
|
||||
if (!this.source) return;
|
||||
const graph = ["graph LR"];
|
||||
graph.push(`source[${msg(str`OAuth Source ${this.source.name}`)}]`);
|
||||
graph.push(
|
||||
|
||||
const graph = [
|
||||
"graph LR",
|
||||
`source[${msg(str`OAuth Source ${this.source.name}`)}]`,
|
||||
`source --> flow_manager["${UserMatchingModeToLabel(
|
||||
this.source.userMatchingMode || UserMatchingModeEnum.Identifier,
|
||||
)}"]`,
|
||||
);
|
||||
];
|
||||
|
||||
if (this.source.enrollmentFlow) {
|
||||
graph.push("flow_manager --> flow_enroll[Enrollment flow]");
|
||||
}
|
||||
|
||||
if (this.source.authenticationFlow) {
|
||||
graph.push("flow_manager --> flow_auth[Authentication flow]");
|
||||
}
|
||||
|
||||
this.diagram = graph.join("\n");
|
||||
};
|
||||
}
|
||||
|
||||
49
web/src/common/mermaid.ts
Normal file
49
web/src/common/mermaid.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { DOM_PURIFY_STRICT } from "#common/purify";
|
||||
import { ResolvedUITheme } from "#common/theme";
|
||||
|
||||
import type { Mermaid, MermaidConfig } from "mermaid";
|
||||
|
||||
export const DefaultMermaidConfig: Readonly<MermaidConfig> = {
|
||||
logLevel: "fatal",
|
||||
startOnLoad: false,
|
||||
flowchart: {
|
||||
curve: "linear",
|
||||
},
|
||||
htmlLabels: false,
|
||||
securityLevel: "strict",
|
||||
dompurifyConfig: DOM_PURIFY_STRICT,
|
||||
};
|
||||
|
||||
let lastActiveTheme: ResolvedUITheme | null = null;
|
||||
let mermaid: Mermaid | null = null;
|
||||
|
||||
/**
|
||||
* Load the Mermaid library and initialize it with the appropriate theme based on the provided UI theme.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Mermaid is only loaded once and cached for subsequent calls. Note that
|
||||
* Mermaid is a singleton and does not support multiple instances with different configurations.
|
||||
*/
|
||||
export async function loadMermaid(uiTheme: ResolvedUITheme): Promise<Mermaid> {
|
||||
if (!mermaid) {
|
||||
const mermaidModule = await import("mermaid");
|
||||
mermaid = mermaidModule.default;
|
||||
}
|
||||
|
||||
if (uiTheme && uiTheme === lastActiveTheme) {
|
||||
return mermaid;
|
||||
}
|
||||
|
||||
const theme = uiTheme === "dark" ? "dark" : "default";
|
||||
|
||||
mermaid.initialize({
|
||||
...DefaultMermaidConfig,
|
||||
theme,
|
||||
darkMode: uiTheme === "dark",
|
||||
});
|
||||
|
||||
lastActiveTheme = uiTheme;
|
||||
|
||||
return mermaid;
|
||||
}
|
||||
@@ -50,6 +50,17 @@ export const SanitizedTrustPolicy = trustedTypes.createPolicy("authentik-sanitiz
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Trusted types policy for HTML produced by our own build-time markdown
|
||||
* pipeline. The HTML is generated from source we own (the `mdx-plugin`),
|
||||
* including custom elements like `<ak-md-a>` and `<ak-alert>` that
|
||||
* DOMPurify's default tag list would strip. Treat the input as already
|
||||
* trusted and pass it through unmodified.
|
||||
*/
|
||||
export const CompiledMarkdownTrustPolicy = trustedTypes.createPolicy("authentik-markdown", {
|
||||
createHTML: (trustedHTML: string) => trustedHTML,
|
||||
});
|
||||
|
||||
/**
|
||||
* Trusted types policy, allowing a minimal set of _safe_ HTML tags supplied by
|
||||
* a trusted source, such as the brand API.
|
||||
|
||||
@@ -261,26 +261,29 @@ declare global {
|
||||
* @param hint The color scheme hint to use.
|
||||
* @param doc The document to apply the theme to.
|
||||
*/
|
||||
export const applyDocumentTheme = ((currentUITheme = resolveUITheme(), doc = document): void => {
|
||||
export const applyDocumentTheme = ((
|
||||
currentUITheme = resolveUITheme(),
|
||||
ownerDocument = document,
|
||||
): void => {
|
||||
console.debug(`authentik/theme (document): want to switch to ${currentUITheme} theme`);
|
||||
|
||||
const { themeChoice } = doc.documentElement.dataset;
|
||||
const { themeChoice } = ownerDocument.documentElement.dataset;
|
||||
|
||||
if (themeChoice && themeChoice !== "auto") {
|
||||
console.debug(
|
||||
`authentik/theme (document): skipping theme application due to explicit choice (${themeChoice})`,
|
||||
);
|
||||
|
||||
doc.dispatchEvent(new ThemeChangeEvent(themeChoice));
|
||||
ownerDocument.dispatchEvent(new ThemeChangeEvent(themeChoice));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
doc.documentElement.dataset.theme = currentUITheme;
|
||||
ownerDocument.documentElement.dataset.theme = currentUITheme;
|
||||
|
||||
console.debug(`authentik/theme (document): switching to ${currentUITheme} theme`);
|
||||
|
||||
doc.dispatchEvent(new ThemeChangeEvent(currentUITheme));
|
||||
ownerDocument.dispatchEvent(new ThemeChangeEvent(currentUITheme));
|
||||
}) satisfies UIThemeListener;
|
||||
|
||||
/**
|
||||
|
||||
41
web/src/elements/Diagram.css
Normal file
41
web/src/elements/Diagram.css
Normal file
@@ -0,0 +1,41 @@
|
||||
:host {
|
||||
--ak-mermaid-message-text: var(--pf-c-content--Color);
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
svg#diagram {
|
||||
.node {
|
||||
rect,
|
||||
circle,
|
||||
ellipse,
|
||||
polygon,
|
||||
path {
|
||||
fill: var(--pf-global--BackgroundColor--300);
|
||||
}
|
||||
}
|
||||
|
||||
.rect {
|
||||
fill: var(
|
||||
--ak-mermaid-box-background-color,
|
||||
var(--pf-global--BackgroundColor--light-300)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.messageText {
|
||||
stroke-width: 4;
|
||||
fill: var(--ak-mermaid-message-text) !important;
|
||||
paint-order: stroke;
|
||||
}
|
||||
}
|
||||
|
||||
/* #region Dark Theme */
|
||||
|
||||
:host([theme="dark"]) {
|
||||
--ak-mermaid-message-text: var(--ak-dark-foreground);
|
||||
--ak-mermaid-box-background-color: var(--ak-dark-background-lighter);
|
||||
--ak-table-stripe-background: var(--pf-global--BackgroundColor--dark-200);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@@ -1,98 +1,82 @@
|
||||
import "#elements/EmptyState";
|
||||
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
import { DOM_PURIFY_STRICT } from "#common/purify";
|
||||
import { ThemeChangeEvent } from "#common/theme";
|
||||
import { AKRefreshEvent } from "#common/events";
|
||||
import { loadMermaid } from "#common/mermaid";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { listen } from "#elements/decorators/listen";
|
||||
import Styles from "#elements/Diagram.css";
|
||||
import { EmptyState } from "#elements/EmptyState";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
import mermaid, { MermaidConfig } from "mermaid";
|
||||
|
||||
import { css, CSSResult, html, TemplateResult } from "lit";
|
||||
import { CSSResult, PropertyValues } from "lit";
|
||||
import { guard } from "lit-html/directives/guard.js";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
@customElement("ak-diagram")
|
||||
export class Diagram extends AKElement {
|
||||
@property({ attribute: false })
|
||||
diagram?: string;
|
||||
static styles: CSSResult[] = [Styles];
|
||||
|
||||
refreshHandler = (): void => {
|
||||
#diagram = "";
|
||||
@property({ attribute: false, useDefault: true })
|
||||
public get diagram(): string {
|
||||
return this.#diagram || this.textContent.trim() || "";
|
||||
}
|
||||
|
||||
public set diagram(value: string) {
|
||||
const previous = this.#diagram;
|
||||
this.#diagram = value.trim();
|
||||
|
||||
this.requestUpdate("diagram", previous);
|
||||
}
|
||||
|
||||
@listen(AKRefreshEvent, {
|
||||
target: window,
|
||||
})
|
||||
protected syncDiagramContent = (): void => {
|
||||
if (!this.textContent) return;
|
||||
this.diagram = this.textContent;
|
||||
};
|
||||
|
||||
handlerBound = false;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
config: MermaidConfig;
|
||||
loadingPlaceholder: EmptyState;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.config = {
|
||||
// The type definition for this says number
|
||||
// but the example use strings
|
||||
// and numbers don't work
|
||||
logLevel: "fatal",
|
||||
startOnLoad: false,
|
||||
flowchart: {
|
||||
curve: "linear",
|
||||
},
|
||||
htmlLabels: false,
|
||||
securityLevel: "strict",
|
||||
dompurifyConfig: DOM_PURIFY_STRICT,
|
||||
};
|
||||
mermaid.initialize(this.config);
|
||||
this.loadingPlaceholder = new EmptyState();
|
||||
this.loadingPlaceholder.loading = true;
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
if (this.handlerBound) return;
|
||||
window.addEventListener(EVENT_REFRESH, this.refreshHandler);
|
||||
this.addEventListener(ThemeChangeEvent.eventName, ((ev: CustomEvent<UiThemeEnum>) => {
|
||||
if (ev.detail === UiThemeEnum.Dark) {
|
||||
this.config.theme = "dark";
|
||||
} else {
|
||||
this.config.theme = "default";
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.syncDiagramContent();
|
||||
}
|
||||
|
||||
protected renderMermaid(): Promise<SlottedTemplateResult> {
|
||||
return loadMermaid(this.activeTheme).then((mermaid) => {
|
||||
if (!this.diagram) {
|
||||
return null;
|
||||
}
|
||||
mermaid.initialize(this.config);
|
||||
}) as EventListener);
|
||||
this.handlerBound = true;
|
||||
this.refreshHandler();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(EVENT_REFRESH, this.refreshHandler);
|
||||
}
|
||||
return mermaid.render("diagram", this.diagram).then((result) => {
|
||||
result.bindFunctions?.(this.renderRoot as HTMLElement);
|
||||
|
||||
render(): TemplateResult {
|
||||
this.querySelectorAll("*").forEach((el) => {
|
||||
try {
|
||||
el.remove();
|
||||
} catch {
|
||||
console.debug(`authentik/diagram: failed to remove element ${el}`);
|
||||
}
|
||||
return unsafeHTML(result.svg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
const { diagram, loadingPlaceholder, activeTheme } = this;
|
||||
|
||||
return guard([diagram, activeTheme], () => {
|
||||
if (!diagram) {
|
||||
return loadingPlaceholder;
|
||||
}
|
||||
|
||||
return until(this.renderMermaid(), loadingPlaceholder);
|
||||
});
|
||||
if (!this.diagram) {
|
||||
return html`<ak-empty-state loading></ak-empty-state>`;
|
||||
}
|
||||
return html`${until(
|
||||
mermaid.render("graph", this.diagram).then((r) => {
|
||||
r.bindFunctions?.(this.shadowRoot as unknown as Element);
|
||||
return unsafeHTML(r.svg);
|
||||
}),
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export abstract class Interface extends AKElement {
|
||||
|
||||
public override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.commandPalette.modal.setCommands(
|
||||
createCommonCommands().map((command) => ({
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import type { MDXModule } from "~docs/types";
|
||||
|
||||
/**
|
||||
* Fetches an MDX module from a URL or ESBuild static asset.
|
||||
*/
|
||||
export function fetchMDXModule(url: string | URL): Promise<MDXModule> {
|
||||
return fetch(url)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch content: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching content", error);
|
||||
return { content: "", publicPath: "", publicDirectory: "" };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A context for the current MDX module.
|
||||
*/
|
||||
export const MDXModuleContext = createContext<MDXModule>({
|
||||
content: "",
|
||||
});
|
||||
|
||||
MDXModuleContext.displayName = "MDXModuleContext";
|
||||
|
||||
/**
|
||||
* A hook to access the current MDX module.
|
||||
*/
|
||||
export function useMDXModule(): MDXModule {
|
||||
return useContext(MDXModuleContext);
|
||||
}
|
||||
146
web/src/elements/ak-mdx/ak-mdx.ts
Normal file
146
web/src/elements/ak-mdx/ak-mdx.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import "#elements/Alert";
|
||||
import "#elements/Diagram";
|
||||
import "#elements/ak-mdx/components/ak-md-a";
|
||||
|
||||
import { globalAK } from "#common/global";
|
||||
import { BrandedHTMLPolicy, CompiledMarkdownTrustPolicy, sanitizeHTML } from "#common/purify";
|
||||
|
||||
import { compileRuntimeMarkdown } from "#elements/ak-mdx/markdown";
|
||||
import Styles from "#elements/ak-mdx/styles.css";
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { DistDirectoryName, StaticDirectoryName } from "#paths";
|
||||
import OneDark from "#styles/atom/one-dark.css";
|
||||
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
||||
|
||||
/**
|
||||
* The JSON envelope our build-time `mdx-plugin` emits for every imported
|
||||
* `.md` / `.mdx` file: the `content` field is **pre-rendered HTML**, not
|
||||
* raw markdown source.
|
||||
*/
|
||||
interface MarkdownModule {
|
||||
content: string;
|
||||
frontmatter?: Record<string, unknown>;
|
||||
publicPath?: string;
|
||||
publicDirectory?: string;
|
||||
}
|
||||
|
||||
async function fetchMarkdownModule(url: string | URL): Promise<MarkdownModule> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`Failed to fetch markdown: ${response.statusText}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* A replacer applied to the compiled HTML before it is stamped into the
|
||||
* shadow DOM. Used by callers who need to substitute `{placeholder}`-style
|
||||
* tokens (e.g. proxy-provider sample configs).
|
||||
*/
|
||||
export type Replacer = (input: string) => string;
|
||||
|
||||
/**
|
||||
* Renders markdown into shadow DOM with no client-side JavaScript
|
||||
* evaluation. Two modes:
|
||||
*
|
||||
* - `url`: resolves to a JSON envelope produced by the build-time
|
||||
* `mdx-plugin`. The envelope's `content` is already HTML.
|
||||
* - `content`: an admin-supplied markdown string. Compiled in-browser
|
||||
* through a pure `unified` / remark / rehype pipeline (no `eval`,
|
||||
* no `Function`), then sanitized via `BrandedHTMLPolicy`.
|
||||
*/
|
||||
@customElement("ak-mdx")
|
||||
export class AKMDX extends AKElement {
|
||||
@property({ type: String, reflect: true, useDefault: true })
|
||||
public url: string | null = null;
|
||||
|
||||
@property()
|
||||
public content?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public replacers: Replacer[] = [];
|
||||
|
||||
@state()
|
||||
protected compiledTemplate: SlottedTemplateResult = null;
|
||||
|
||||
static styles = [
|
||||
// ---
|
||||
PFList,
|
||||
PFTable,
|
||||
PFContent,
|
||||
OneDark,
|
||||
Styles,
|
||||
];
|
||||
|
||||
public override async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.hydrate();
|
||||
}
|
||||
|
||||
#applyReplacers(html: string): string {
|
||||
return this.replacers.reduce((acc, replacer) => replacer(acc), html);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL mode: HTML comes from our build-time pipeline. It may contain
|
||||
* custom-element tags (`<ak-alert>`, `<ak-md-a>`) that DOMPurify's
|
||||
* default tag list would strip, so we route it through a Trusted
|
||||
* Types policy that passes the input through unmodified.
|
||||
*/
|
||||
async #hydrateFromURL(url: string): Promise<SlottedTemplateResult> {
|
||||
const { relBase } = globalAK().api;
|
||||
const pathname =
|
||||
relBase +
|
||||
StaticDirectoryName +
|
||||
"/" +
|
||||
DistDirectoryName +
|
||||
url.slice(url.indexOf("/assets"));
|
||||
const module = await fetchMarkdownModule(pathname);
|
||||
|
||||
if (module.publicDirectory) {
|
||||
this.dataset.publicDirectory = module.publicDirectory;
|
||||
}
|
||||
|
||||
const trustedHTML = CompiledMarkdownTrustPolicy.createHTML(
|
||||
this.#applyReplacers(module.content),
|
||||
);
|
||||
|
||||
return unsafeHTML(trustedHTML.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Content mode: admin-supplied markdown compiled in-browser through
|
||||
* a pure `unified` / remark / rehype pipeline (no `eval`, no
|
||||
* `Function`), then sanitized via `BrandedHTMLPolicy`.
|
||||
*/
|
||||
async #hydrateFromContent(source: string): Promise<SlottedTemplateResult> {
|
||||
const html = this.#applyReplacers(await compileRuntimeMarkdown(source));
|
||||
return sanitizeHTML(BrandedHTMLPolicy, html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `url` or `content` into a template result and stash it on
|
||||
* reactive state. After this completes, Lit's render takes over.
|
||||
*/
|
||||
protected async hydrate(): Promise<void> {
|
||||
this.compiledTemplate = this.url
|
||||
? await this.#hydrateFromURL(this.url)
|
||||
: await this.#hydrateFromContent(this.content ?? "");
|
||||
}
|
||||
|
||||
public override render(): SlottedTemplateResult {
|
||||
return this.compiledTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-mdx": AKMDX;
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import "#elements/Alert";
|
||||
|
||||
import { globalAK } from "#common/global";
|
||||
import { BrandedHTMLPolicy } from "#common/purify";
|
||||
|
||||
import { MDXAnchor } from "#elements/ak-mdx/components/MDXAnchor";
|
||||
import { MDXWrapper } from "#elements/ak-mdx/components/MDXWrapper";
|
||||
import { fetchMDXModule, MDXModuleContext } from "#elements/ak-mdx/MDXModuleContext";
|
||||
import { remarkAdmonition } from "#elements/ak-mdx/remark/remark-admonition";
|
||||
import { remarkHeadings } from "#elements/ak-mdx/remark/remark-headings";
|
||||
import { remarkLists } from "#elements/ak-mdx/remark/remark-lists";
|
||||
import Styles from "#elements/ak-mdx/styles.css";
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { DistDirectoryName, StaticDirectoryName } from "#paths";
|
||||
import OneDark from "#styles/atom/one-dark.css";
|
||||
|
||||
import { UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
import { compile as compileMDX, run as runMDX } from "@mdx-js/mdx";
|
||||
import apacheGrammar from "highlight.js/lib/languages/apache";
|
||||
import diffGrammar from "highlight.js/lib/languages/diff";
|
||||
import confGrammar from "highlight.js/lib/languages/ini";
|
||||
import nginxGrammar from "highlight.js/lib/languages/nginx";
|
||||
import { common } from "lowlight";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import * as runtime from "react/jsx-runtime";
|
||||
import rehypeHighlight, { Options as HighlightOptions } from "rehype-highlight";
|
||||
import rehypeMermaid, { RehypeMermaidOptions } from "rehype-mermaid";
|
||||
import remarkDirective from "remark-directive";
|
||||
import remarkFrontmatter from "remark-frontmatter";
|
||||
import remarkGFM from "remark-gfm";
|
||||
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
|
||||
import remarkParse from "remark-parse";
|
||||
import type { MDXModule } from "~docs/types";
|
||||
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFTable from "@patternfly/patternfly/components/Table/table.css";
|
||||
|
||||
const highlightThemeOptions: HighlightOptions = {
|
||||
languages: {
|
||||
...common,
|
||||
nginx: nginxGrammar,
|
||||
apache: apacheGrammar,
|
||||
conf: confGrammar,
|
||||
diff: diffGrammar,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A replacer function that can be used to modify the output of the MDX component.
|
||||
*/
|
||||
export type Replacer = (input: string) => string;
|
||||
|
||||
@customElement("ak-mdx")
|
||||
export class AKMDX extends AKElement {
|
||||
// HACK: Fixes Lit Analyzer's parsing of TSX files with decorators.
|
||||
|
||||
@((property as typeof property)({ type: String, reflect: true }))
|
||||
public url?: string;
|
||||
|
||||
@((property as typeof property)())
|
||||
public content?: string;
|
||||
|
||||
@((property as typeof property)({ attribute: false }))
|
||||
public replacers: Replacer[] = [];
|
||||
|
||||
#reactRoot: Root | null = null;
|
||||
|
||||
static styles = [
|
||||
// ---
|
||||
|
||||
PFList,
|
||||
PFTable,
|
||||
PFContent,
|
||||
OneDark,
|
||||
Styles,
|
||||
];
|
||||
|
||||
public async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.#reactRoot = createRoot(this.shadowRoot!);
|
||||
|
||||
let nextMDXModule: MDXModule | undefined;
|
||||
const { relBase } = globalAK().api;
|
||||
|
||||
if (this.url) {
|
||||
const pathname =
|
||||
relBase +
|
||||
StaticDirectoryName +
|
||||
"/" +
|
||||
DistDirectoryName +
|
||||
this.url.slice(this.url.indexOf("/assets"));
|
||||
|
||||
nextMDXModule = await fetchMDXModule(pathname);
|
||||
} else {
|
||||
nextMDXModule = {
|
||||
content: `${BrandedHTMLPolicy.createHTML(this.content || "")}`,
|
||||
};
|
||||
}
|
||||
|
||||
return this.delegateRender(nextMDXModule);
|
||||
}
|
||||
|
||||
protected async delegateRender(mdxModule: MDXModule): Promise<void> {
|
||||
if (!this.#reactRoot) return;
|
||||
|
||||
const normalized = this.replacers.reduce(
|
||||
(content, replacer) => replacer(content),
|
||||
mdxModule.content,
|
||||
);
|
||||
|
||||
const mdx = await compileMDX(normalized, {
|
||||
outputFormat: "function-body",
|
||||
remarkPlugins: [
|
||||
remarkParse,
|
||||
remarkDirective,
|
||||
remarkAdmonition,
|
||||
remarkGFM,
|
||||
remarkFrontmatter,
|
||||
remarkMdxFrontmatter,
|
||||
remarkHeadings,
|
||||
remarkLists,
|
||||
],
|
||||
rehypePlugins: [
|
||||
// ---
|
||||
[rehypeHighlight, highlightThemeOptions],
|
||||
[
|
||||
rehypeMermaid,
|
||||
{
|
||||
prefix: "mermaid-svg-",
|
||||
colorScheme: this.activeTheme === UiThemeEnum.Dark ? "dark" : "light",
|
||||
} satisfies RehypeMermaidOptions,
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
const { default: Content, ...mdxExports } = await runMDX(mdx, {
|
||||
...runtime,
|
||||
baseUrl: import.meta.url,
|
||||
});
|
||||
|
||||
const { frontmatter = {} } = mdxExports;
|
||||
this.#reactRoot.render(
|
||||
<MDXModuleContext.Provider value={mdxModule}>
|
||||
<Content
|
||||
frontmatter={frontmatter}
|
||||
components={{
|
||||
wrapper: MDXWrapper,
|
||||
a: MDXAnchor,
|
||||
}}
|
||||
/>
|
||||
</MDXModuleContext.Provider>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-mdx": AKMDX;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useMDXModule } from "#elements/ak-mdx/MDXModuleContext";
|
||||
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* A simplified version of Node's `path.resolve`:
|
||||
*/
|
||||
function resolvePath(...args: string[]): string {
|
||||
const pathname = args
|
||||
// Combine all arguments into a single path...
|
||||
.join("/")
|
||||
// Normalizing any delimiting slashes...
|
||||
.replace(/\/{2,}/g, "/");
|
||||
|
||||
return new URL(pathname, "file:///").pathname;
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom anchor element that applies special behavior for MDX content.
|
||||
*
|
||||
* - Resolves relative links to the public directory in the public docs domain.
|
||||
* - Intercepts local links and scrolls to the target element.
|
||||
*/
|
||||
export const MDXAnchor = ({
|
||||
href,
|
||||
children,
|
||||
...props
|
||||
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const { publicDirectory } = useMDXModule();
|
||||
|
||||
if (href?.startsWith(".") && publicDirectory) {
|
||||
const nextPathname = resolvePath(publicDirectory, href);
|
||||
|
||||
const nextURL = new URL(nextPathname, import.meta.env.AK_DOCS_URL);
|
||||
// Remove trailing .md and .mdx, and trailing "index".
|
||||
nextURL.pathname = nextURL.pathname.replace(/(index)?\.mdx?$/, "");
|
||||
href = nextURL.toString();
|
||||
}
|
||||
|
||||
const interceptHeadingLinks = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (!href || !href.startsWith("#")) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rootNode = event.currentTarget.getRootNode() as ShadowRoot;
|
||||
|
||||
const elementID = href.slice(1);
|
||||
const target = rootNode.getElementById(elementID);
|
||||
|
||||
if (!target) {
|
||||
console.warn(`Element with ID ${elementID} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={interceptHeadingLinks}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export interface MDXWrapperProps {
|
||||
children: React.ReactNode;
|
||||
frontmatter: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper component for MDX content that adds a title if one is provided in the frontmatter.
|
||||
*/
|
||||
export const MDXWrapper = ({ children, frontmatter }: MDXWrapperProps) => {
|
||||
const { title } = frontmatter;
|
||||
const nextChildren = React.Children.toArray(children);
|
||||
|
||||
if (title) {
|
||||
nextChildren.unshift(
|
||||
<h1 key="header-title" part="title">
|
||||
{title}
|
||||
</h1>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pf-c-content" part="content">
|
||||
{nextChildren}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
63
web/src/elements/ak-mdx/components/ak-md-a.ts
Normal file
63
web/src/elements/ak-mdx/components/ak-md-a.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { AKElement } from "#elements/Base";
|
||||
|
||||
import { css, PropertyValues } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-md-a")
|
||||
export class AKMarkdownAnchor extends AKElement {
|
||||
public static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
protected defaultSlot: HTMLSlotElement = this.ownerDocument.createElement("slot");
|
||||
|
||||
protected override render() {
|
||||
return this.defaultSlot;
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues<this>): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
const anchors = this.defaultSlot
|
||||
.assignedElements({ flatten: true })
|
||||
.filter((element) => element.matches("a"));
|
||||
|
||||
for (const anchor of anchors) {
|
||||
anchor.addEventListener("click", this.clickListener);
|
||||
}
|
||||
}
|
||||
|
||||
protected clickListener(event: MouseEvent): void {
|
||||
const anchor = event.currentTarget as HTMLAnchorElement;
|
||||
const href = anchor.getAttribute("href");
|
||||
|
||||
if (!href || !href.startsWith("#")) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const rootNode = anchor.getRootNode() as ShadowRoot;
|
||||
|
||||
const elementID = href.slice(1);
|
||||
const target = rootNode.getElementById(elementID);
|
||||
|
||||
if (!target) {
|
||||
console.warn(`Element with ID ${elementID} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-md-a": AKMarkdownAnchor;
|
||||
}
|
||||
}
|
||||
46
web/src/elements/ak-mdx/markdown.ts
Normal file
46
web/src/elements/ak-mdx/markdown.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
normalizeAdmonitionLabels,
|
||||
remarkAdmonition,
|
||||
} from "#elements/ak-mdx/remark/remark-admonition";
|
||||
import { remarkHeadings } from "#elements/ak-mdx/remark/remark-headings";
|
||||
import { remarkLists } from "#elements/ak-mdx/remark/remark-lists";
|
||||
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
import remarkDirective from "remark-directive";
|
||||
import remarkGFM from "remark-gfm";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import { unified } from "unified";
|
||||
|
||||
/**
|
||||
* Compile an admin-supplied markdown string to an HTML string in the
|
||||
* browser. The pipeline is a strict subset of the build-time one: no
|
||||
* syntax highlighting, no anchor rewriting — the output is plain HTML
|
||||
* that the existing `BrandedHTMLPolicy` (DOMPurify) sanitizes cleanly.
|
||||
*
|
||||
* Unlike `@mdx-js/mdx`'s `evaluate` / `run`, none of the `unified`,
|
||||
* `remark-*`, or `rehype-*` packages execute the input as JavaScript:
|
||||
* they are pure tree transformers. This is what lets us drop
|
||||
* `'unsafe-eval'` from the page CSP.
|
||||
*/
|
||||
export async function compileRuntimeMarkdown(source: string): Promise<string> {
|
||||
if (!source.trim()) return "";
|
||||
|
||||
// Translate Docusaurus's `:::name Title` syntax to `:::name[Title]`
|
||||
// before remark-directive parses it; otherwise it falls through as
|
||||
// plain text.
|
||||
const normalized = normalizeAdmonitionLabels(source);
|
||||
|
||||
const file = await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGFM)
|
||||
.use(remarkDirective)
|
||||
.use(remarkAdmonition)
|
||||
.use(remarkHeadings)
|
||||
.use(remarkLists)
|
||||
.use(remarkRehype, { allowDangerousHtml: false })
|
||||
.use(rehypeStringify)
|
||||
.process(normalized);
|
||||
|
||||
return String(file);
|
||||
}
|
||||
@@ -1,35 +1,77 @@
|
||||
import { UnwrapSet } from "#common/sets";
|
||||
|
||||
import { h } from "hastscript";
|
||||
import type { Root } from "mdast";
|
||||
import type { Paragraph, Root } from "mdast";
|
||||
import type { Directives } from "mdast-util-directive";
|
||||
import type { Plugin } from "unified";
|
||||
import { visit } from "unist-util-visit";
|
||||
import type { VFile } from "vfile";
|
||||
|
||||
const ADMONITION_TYPES = new Set(["info", "warning", "danger", "note"]);
|
||||
export const ADMONITION_TYPES = new Set([
|
||||
"info",
|
||||
"warning",
|
||||
"danger",
|
||||
"note",
|
||||
"caution",
|
||||
"tip",
|
||||
] as const);
|
||||
|
||||
export type AdmonitionType = UnwrapSet<typeof ADMONITION_TYPES>;
|
||||
|
||||
export function isAdmonitionType(value: string): value is AdmonitionType {
|
||||
return ADMONITION_TYPES.has(value as AdmonitionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remark plugin to add admonition classes to directives.
|
||||
* `caution` and `tip` are not first-class PatternFly alert levels — map
|
||||
* them to the closest equivalent so PFAlert styles render correctly.
|
||||
*/
|
||||
export const remarkAdmonition: Plugin<[unknown], Root, VFile> = () => {
|
||||
const ADMONITION_LEVEL = {
|
||||
info: "pf-m-info",
|
||||
warning: "pf-m-warning",
|
||||
danger: "pf-m-danger",
|
||||
note: "pf-m-info",
|
||||
caution: "pf-m-warning",
|
||||
tip: "pf-m-success",
|
||||
} as const satisfies Record<AdmonitionType, string>;
|
||||
|
||||
/**
|
||||
* Remark plugin to convert `:::info` / `:::warning` / etc. directives
|
||||
* to `<ak-alert>` elements. The first child paragraph carrying the
|
||||
* `directiveLabel` flag (i.e. `:::info[Title]` syntax) is promoted to
|
||||
* a `<strong>` so the title renders inside the admonition slot.
|
||||
*/
|
||||
export const remarkAdmonition: Plugin<[], Root, VFile> = () => {
|
||||
return function transformer(tree) {
|
||||
const visitor = (node: Directives) => {
|
||||
if (
|
||||
node.type === "containerDirective" ||
|
||||
node.type === "leafDirective" ||
|
||||
node.type === "textDirective"
|
||||
node.type !== "containerDirective" &&
|
||||
node.type !== "leafDirective" &&
|
||||
node.type !== "textDirective"
|
||||
) {
|
||||
if (!ADMONITION_TYPES.has(node.name)) return;
|
||||
return;
|
||||
}
|
||||
|
||||
const data = node.data || (node.data = {});
|
||||
if (!isAdmonitionType(node.name)) return;
|
||||
|
||||
const tagName = node.type === "textDirective" ? "span" : "ak-alert";
|
||||
const data = node.data || (node.data = {});
|
||||
const tagName = node.type === "textDirective" ? "span" : "ak-alert";
|
||||
data.hName = tagName;
|
||||
|
||||
data.hName = tagName;
|
||||
const element = h(tagName, node.attributes || {});
|
||||
data.hProperties = element.properties || {};
|
||||
data.hProperties.level = ADMONITION_LEVEL[node.name] ?? `pf-m-${node.name}`;
|
||||
|
||||
const element = h(tagName, node.attributes || {});
|
||||
|
||||
data.hProperties = element.properties || {};
|
||||
data.hProperties.level = `pf-m-${node.name}`;
|
||||
const children = node.children as Paragraph[];
|
||||
const labelIndex = children.findIndex(
|
||||
(c) => c.type === "paragraph" && c.data?.directiveLabel,
|
||||
);
|
||||
if (labelIndex !== -1) {
|
||||
const label = children[labelIndex];
|
||||
children[labelIndex] = {
|
||||
type: "paragraph",
|
||||
children: [{ type: "strong", children: label.children }],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,3 +79,22 @@ export const remarkAdmonition: Plugin<[unknown], Root, VFile> = () => {
|
||||
visit(tree, visitor);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Match a Docusaurus-style admonition opening line:
|
||||
*
|
||||
* ```
|
||||
* :::info Title
|
||||
*```
|
||||
* `remark-directive` only understands the spec form `:::name[label]{attrs}`,
|
||||
* so a bare-space label silently falls through as plain text. Rewrite
|
||||
* the source so the directive parser sees the bracketed form.
|
||||
*/
|
||||
const ADMONITION_BARE_LABEL_RE = new RegExp(
|
||||
`^(:::(?:${[...ADMONITION_TYPES].join("|")}))[ \\t]+(.+?)[ \\t]*$`,
|
||||
"gm",
|
||||
);
|
||||
|
||||
export function normalizeAdmonitionLabels(source: string): string {
|
||||
return source.replace(ADMONITION_BARE_LABEL_RE, "$1[$2]");
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { VFile } from "vfile";
|
||||
/**
|
||||
* Remark plugin to add IDs to headings.
|
||||
*/
|
||||
export const remarkHeadings: Plugin<[unknown], Root, VFile> = () => {
|
||||
export const remarkHeadings: Plugin<[], Root, VFile> = () => {
|
||||
return function transformer(tree) {
|
||||
const visitor = (node: Heading) => {
|
||||
const textContent = toString(node);
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { VFile } from "vfile";
|
||||
/**
|
||||
* Remark plugin to process lists.
|
||||
*/
|
||||
export const remarkLists: Plugin<[unknown], Root, VFile> = () => {
|
||||
export const remarkLists: Plugin<[], Root, VFile> = () => {
|
||||
return function transformer(tree) {
|
||||
const visitor = (node: List) => {
|
||||
node.data = node.data || {};
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
:host {
|
||||
--ak-mermaid-message-text: var(--pf-c-content--Color);
|
||||
--ak-table-stripe-background: var(--pf-global--BackgroundColor--light-200);
|
||||
}
|
||||
|
||||
/*
|
||||
* `<ak-alert>` deliberately does not set its own `:host { display }` —
|
||||
* every consumer is expected to set it (see e.g. captcha/styles.css).
|
||||
* Without this, the shadow-tree flex layout collapses into an inline
|
||||
* box and the admonition is unreadable.
|
||||
*/
|
||||
ak-alert {
|
||||
display: block;
|
||||
margin-block-start: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
ak-alert + p {
|
||||
margin-block-start: var(--pf-global--spacer--md);
|
||||
}
|
||||
@@ -59,37 +69,10 @@ pre:has(.hljs) {
|
||||
padding: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
svg[id^="mermaid-svg-"] {
|
||||
.rect {
|
||||
fill: var(
|
||||
--ak-mermaid-box-background-color,
|
||||
var(--pf-global--BackgroundColor--light-300)
|
||||
) !important;
|
||||
}
|
||||
|
||||
.messageText {
|
||||
stroke-width: 4;
|
||||
fill: var(--ak-mermaid-message-text) !important;
|
||||
paint-order: stroke;
|
||||
}
|
||||
}
|
||||
|
||||
/* #region Dark Theme */
|
||||
|
||||
:host([theme="dark"]) {
|
||||
--ak-mermaid-message-text: var(--ak-dark-foreground);
|
||||
--ak-mermaid-box-background-color: var(--ak-dark-background-lighter);
|
||||
--ak-table-stripe-background: var(--pf-global--BackgroundColor--dark-200);
|
||||
|
||||
svg[id^="mermaid-svg-"] {
|
||||
line[class^="messageLine"] {
|
||||
/*
|
||||
Mermaid's support for dynamic palette changes leaves a lot to be desired.
|
||||
This is a workaround to keep content readable while not breaking the rest of the theme.
|
||||
*/
|
||||
filter: invert(1) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ifPresent } from "#elements/utils/attributes";
|
||||
import type { ThemedUrls } from "@goauthentik/api";
|
||||
|
||||
import { spread } from "@open-wc/lit-helpers";
|
||||
import { ImgHTMLAttributes } from "react";
|
||||
import type { ImgHTMLAttributes } from "react";
|
||||
|
||||
import { html, nothing } from "lit";
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
* @see https://github.com/atom/one-dark-syntax
|
||||
*/
|
||||
|
||||
:root {
|
||||
:root,
|
||||
:host {
|
||||
--one-dark-base: #282c34;
|
||||
--one-dark-mono-1: #abb2bf;
|
||||
--one-dark-mono-2: #818896;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Application } from "@goauthentik/api";
|
||||
|
||||
import { spread } from "@open-wc/lit-helpers";
|
||||
import { kebabCase } from "change-case";
|
||||
import { HTMLAttributes } from "react";
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
|
||||
198
web/test/browser/ak-mdx.test.ts
Normal file
198
web/test/browser/ak-mdx.test.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { expect, test } from "#e2e";
|
||||
import { randomName } from "#e2e/utils/generators";
|
||||
|
||||
import { IDGenerator } from "@goauthentik/core/id";
|
||||
import { series } from "@goauthentik/core/promises";
|
||||
|
||||
/**
|
||||
* `<ak-mdx>` renders the OAuth 2.0 provider docs (`oauth2/index.mdx`) on
|
||||
* the OAuth2 provider view page. That document is well-suited to exercise
|
||||
* the full pipeline because it contains:
|
||||
*
|
||||
* - frontmatter (`title: OAuth 2.0 provider`)
|
||||
* - multiple H2 headings (id slugs)
|
||||
* - `:::caution` and `:::info` admonitions (two flavours: with title, without)
|
||||
* - relative-doc links (`./create-oauth2-provider.md`)
|
||||
* - external links (`https://oauth.net/2/`)
|
||||
* - a `mermaid` sequence diagram
|
||||
*
|
||||
* These tests boot the admin UI, create a fresh OAuth2 provider, navigate
|
||||
* to its view page, and then assert against the rendered DOM inside
|
||||
* `<ak-mdx>`'s shadow root.
|
||||
*/
|
||||
test.describe("ak-mdx renders compiled markdown", () => {
|
||||
let providerName: string;
|
||||
|
||||
test.beforeEach("Provision an OAuth2 provider", async ({ session, form, pointer, page }) => {
|
||||
const seed = IDGenerator.randomID(6);
|
||||
providerName = `${randomName(seed)} (${seed})`;
|
||||
|
||||
const { fill, selectSearchValue } = form;
|
||||
const { click } = pointer;
|
||||
|
||||
await test.step("Authenticate", () => session.login({ to: "/if/admin/#/core/providers" }));
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "New Provider Wizard" });
|
||||
|
||||
await test.step("Create provider via wizard", async () => {
|
||||
await expect(dialog).toBeHidden();
|
||||
await page.getByRole("button", { name: "New Provider" }).click();
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await series(
|
||||
[click, "OAuth2/OpenID", "option"],
|
||||
[fill, "Provider Name", providerName],
|
||||
[
|
||||
selectSearchValue,
|
||||
"Authorization Flow",
|
||||
/default-provider-authorization-explicit-consent/,
|
||||
],
|
||||
[click, "Create", "button", dialog],
|
||||
);
|
||||
|
||||
await expect(dialog).toBeHidden();
|
||||
});
|
||||
|
||||
await test.step("Navigate to the provider's view page", async () => {
|
||||
const $row = await form.search(providerName);
|
||||
// The provider name cell is a link that opens the view page.
|
||||
await $row.getByRole("link", { name: providerName }).first().click();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @returns a Locator scoped to the rendered `<ak-mdx>` element on the
|
||||
* provider view page (there is exactly one inside the docs card).
|
||||
*/
|
||||
const $mdx = (page: import("@playwright/test").Page) =>
|
||||
page.locator("ak-mdx").filter({ has: page.locator('h1[part="title"]') });
|
||||
|
||||
test("frontmatter title and heading slugs are rendered", async ({ page }) => {
|
||||
const mdx = $mdx(page);
|
||||
|
||||
await expect(
|
||||
mdx.locator('h1[part="title"]'),
|
||||
"Frontmatter `title` rendered as an `<h1 part=title>`",
|
||||
).toHaveText("OAuth 2.0 provider");
|
||||
|
||||
await expect(
|
||||
mdx.locator("h2#authentik-and-oauth-2-0"),
|
||||
"H2 carries a kebab-cased id slug derived from its text",
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
mdx.locator("h2#about-oauth-2-0-and-oidc"),
|
||||
"Multiple H2s each receive their own slug",
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("admonitions render as <ak-alert> with the right level", async ({ page }) => {
|
||||
const mdx = $mdx(page);
|
||||
|
||||
const $caution = mdx
|
||||
.locator('ak-alert[level="pf-m-warning"]')
|
||||
.filter({ hasText: "Reserved application slugs" });
|
||||
await expect(
|
||||
$caution,
|
||||
"`:::caution Title` renders an `<ak-alert level=pf-m-warning>` with the title in `<strong>`",
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
$caution.locator("strong"),
|
||||
"Bare-space directive label is promoted to `<strong>`",
|
||||
).toHaveText("Reserved application slugs");
|
||||
|
||||
await expect(
|
||||
mdx.locator('ak-alert[level="pf-m-info"]').first(),
|
||||
"`:::info` blocks render as `<ak-alert level=pf-m-info>`",
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("links are wrapped in <ak-md-a> with build-time URL resolution", async ({ page }) => {
|
||||
const mdx = $mdx(page);
|
||||
|
||||
const $external = mdx.locator('ak-md-a > a[href="https://oauth.net/2/"]');
|
||||
await expect($external, "External link preserved verbatim").toBeVisible();
|
||||
await expect($external).toHaveAttribute("target", "_blank");
|
||||
await expect($external).toHaveAttribute("rel", "noopener noreferrer");
|
||||
|
||||
const $relative = mdx
|
||||
.locator('ak-md-a > a[href*="next.goauthentik.io"][href*="create-oauth2-provider"]')
|
||||
.first();
|
||||
await expect(
|
||||
$relative,
|
||||
"Relative `./create-oauth2-provider.md` resolved to docs site URL at build time",
|
||||
).toBeVisible();
|
||||
await expect($relative).toHaveAttribute("target", "_blank");
|
||||
|
||||
// Fragment href is preserved verbatim from the source markdown,
|
||||
// even when (as here) the docs author's intended target slug
|
||||
// doesn't match this pipeline's slug algorithm. The wrapper
|
||||
// intercepts the click regardless — the lookup only fails the
|
||||
// scroll, not the link itself.
|
||||
const $fragment = mdx.locator('ak-md-a > a[href="#about-oauth-20-and-oidc"]').first();
|
||||
await expect(
|
||||
$fragment,
|
||||
"Fragment links are kept as `#…` so the wrapper can intercept them",
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
$fragment,
|
||||
"Fragment links do NOT receive `target=_blank`",
|
||||
).not.toHaveAttribute("target", "_blank");
|
||||
});
|
||||
|
||||
test("mermaid diagrams render via <ak-diagram>", async ({ page }) => {
|
||||
const mdx = $mdx(page);
|
||||
|
||||
const $diagram = mdx.locator("ak-diagram").first();
|
||||
await expect($diagram).toBeVisible();
|
||||
|
||||
const $svg = $diagram.locator("svg");
|
||||
await expect(
|
||||
$svg,
|
||||
"<ak-diagram> resolves the mermaid SVG into its shadow root",
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test("mermaid responds to theme changes", async ({ page }) => {
|
||||
const mdx = $mdx(page);
|
||||
const $svg = mdx.locator("ak-diagram svg").first();
|
||||
await expect($svg).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// `<ak-diagram>` re-renders the whole SVG via `mermaid.render(...)` on
|
||||
// every `AKMermaidRefreshEvent`. Mermaid bakes the active theme into
|
||||
// an inline `<style>` block inside the SVG, so the easiest stable
|
||||
// signal that the right theme was applied is to assert the
|
||||
// serialized SVG content changes between toggles.
|
||||
const captureSVG = () => $svg.evaluate((el) => el.outerHTML);
|
||||
const darkSVG = await captureSVG();
|
||||
expect(darkSVG.length, "Initial mermaid SVG is non-empty").toBeGreaterThan(0);
|
||||
|
||||
await test.step("Toggle to light theme", async () => {
|
||||
await page.evaluate(() => {
|
||||
document.documentElement.dataset.themeChoice = "light";
|
||||
});
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(captureSVG, {
|
||||
message: "SVG content should change when re-rendered for light theme",
|
||||
timeout: 10_000,
|
||||
})
|
||||
.not.toBe(darkSVG);
|
||||
|
||||
const lightSVG = await captureSVG();
|
||||
|
||||
await test.step("Toggle back to dark theme", async () => {
|
||||
await page.evaluate(() => {
|
||||
document.documentElement.dataset.themeChoice = "dark";
|
||||
});
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(captureSVG, {
|
||||
message: "SVG content should change again when re-rendered for dark theme",
|
||||
timeout: 10_000,
|
||||
})
|
||||
.not.toBe(lightSVG);
|
||||
});
|
||||
});
|
||||
37
web/types/mdx.d.ts
vendored
37
web/types/mdx.d.ts
vendored
@@ -1,39 +1,22 @@
|
||||
/**
|
||||
* @file Provides types for ESBuild "virtual modules" generated from MDX files.
|
||||
* @file Provides types for ESBuild "virtual modules" generated from
|
||||
* Markdown / MDX files. The bundler's `mdx-plugin` compiles these to
|
||||
* HTML at build time and emits a JSON envelope; importing the file
|
||||
* yields the URL of that JSON envelope.
|
||||
*/
|
||||
|
||||
declare module "~docs/types" {
|
||||
/**
|
||||
* A parsed JSON module containing MDX content and metadata from ESBuild.
|
||||
*/
|
||||
export interface MDXModule {
|
||||
/**
|
||||
* The Markdown content of the module.
|
||||
*/
|
||||
content: string;
|
||||
/**
|
||||
* The public path of the module, typically identical to the docs page path.
|
||||
*/
|
||||
publicPath?: string;
|
||||
/**
|
||||
* The public directory of the module, used to resolve relative links.
|
||||
*/
|
||||
publicDirectory?: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "~docs/*.md" {
|
||||
/**
|
||||
* The serialized JSON content of an MD file.
|
||||
* URL of the JSON envelope emitted for the imported file.
|
||||
*/
|
||||
const serializedJSON: string;
|
||||
export default serializedJSON;
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
|
||||
declare module "~docs/*.mdx" {
|
||||
/**
|
||||
* The serialized JSON content of an MDX file.
|
||||
* URL of the JSON envelope emitted for the imported file.
|
||||
*/
|
||||
const serializedJSON: string;
|
||||
export default serializedJSON;
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user