web: Client-side MDX rendering (#13610)
* web: Allow build errors to propagate. * web: Refactor MDX for client-side rendering. * Remove override Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> * revert css for links and tables Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web: Move Markdown specific styles. --------- Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -19,5 +19,5 @@
|
||||
"importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true,
|
||||
"importOrderParserPlugins": ["typescript", "classProperties", "decorators-legacy"]
|
||||
"importOrderParserPlugins": ["typescript", "jsx", "classProperties", "decorators-legacy"]
|
||||
}
|
||||
|
2319
web/package-lock.json
generated
2319
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,6 @@
|
||||
"@lit/localize": "^0.12.2",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"@lit/task": "^1.0.1",
|
||||
"@mdx-js/esbuild": "^3.1.0",
|
||||
"@mdx-js/mdx": "^3.1.0",
|
||||
"@open-wc/lit-helpers": "^0.7.0",
|
||||
"@patternfly/elements": "^4.0.2",
|
||||
@ -27,6 +26,7 @@
|
||||
"@spotlightjs/spotlight": "^2.4.2",
|
||||
"@webcomponents/webcomponentsjs": "^2.8.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"change-case": "^5.4.4",
|
||||
"chart.js": "^4.4.4",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"codemirror": "^6.0.1",
|
||||
@ -34,18 +34,28 @@
|
||||
"core-js": "^3.38.1",
|
||||
"country-flag-icons": "^1.5.13",
|
||||
"date-fns": "^4.1.0",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
"dompurify": "^3.2.4",
|
||||
"fuse.js": "^7.0.0",
|
||||
"guacamole-common-js": "^1.5.0",
|
||||
"hastscript": "^9.0.1",
|
||||
"lit": "^3.2.0",
|
||||
"md-front-matter": "^1.0.4",
|
||||
"mdx-mermaid": "^2.0.3",
|
||||
"mermaid": "^11.4.1",
|
||||
"rapidoc": "^9.3.7",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"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.0.0",
|
||||
"style-mod": "^4.1.2",
|
||||
"ts-pattern": "^5.4.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"webcomponent-qr-code": "^1.2.0",
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
@ -72,14 +82,16 @@
|
||||
"@types/mocha": "^10.0.8",
|
||||
"@types/node": "^22.7.4",
|
||||
"@types/react": "^18.3.13",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.0",
|
||||
"@typescript-eslint/parser": "^8.8.0",
|
||||
"@wdio/browser-runner": "9.4",
|
||||
"@wdio/cli": "9.4",
|
||||
"@wdio/spec-reporter": "^9.1.2",
|
||||
"change-case": "^5.4.4",
|
||||
"chromedriver": "^131.0.1",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"esbuild-plugins-node-modules-polyfill": "^1.7.0",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-lit": "^1.15.0",
|
||||
"eslint-plugin-wc": "^2.1.1",
|
||||
@ -92,13 +104,6 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"pseudolocale": "^2.1.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"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.0.0",
|
||||
"rollup-plugin-modify": "^3.0.0",
|
||||
"rollup-plugin-postcss-lit": "^2.1.0",
|
||||
"storybook": "^8.3.4",
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { execFileSync } from "child_process";
|
||||
import { deepmerge } from "deepmerge-ts";
|
||||
import esbuild from "esbuild";
|
||||
import { polyfillNode } from "esbuild-plugin-polyfill-node";
|
||||
import findFreePorts from "find-free-ports";
|
||||
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
|
||||
import { globSync } from "glob";
|
||||
@ -108,7 +110,7 @@ const entryPoints = [
|
||||
];
|
||||
|
||||
/**
|
||||
* @satisfies {import("esbuild").BuildOptions}
|
||||
* @type {import("esbuild").BuildOptions}
|
||||
*/
|
||||
const BASE_ESBUILD_OPTIONS = {
|
||||
bundle: true,
|
||||
@ -121,11 +123,19 @@ const BASE_ESBUILD_OPTIONS = {
|
||||
tsconfig: "./tsconfig.json",
|
||||
loader: {
|
||||
".css": "text",
|
||||
".md": "text",
|
||||
},
|
||||
plugins: [
|
||||
polyfillNode({
|
||||
polyfills: {
|
||||
path: true,
|
||||
},
|
||||
}),
|
||||
mdxPlugin({
|
||||
root: authentikProjectRoot,
|
||||
}),
|
||||
],
|
||||
define: definitions,
|
||||
format: "esm",
|
||||
plugins: [mdxPlugin()],
|
||||
logOverride: {
|
||||
/**
|
||||
* HACK: Silences issue originating in ESBuild.
|
||||
@ -162,22 +172,33 @@ function composeVersionID() {
|
||||
function createEntryPointOptions([source, dest], overrides = {}) {
|
||||
const outdir = path.join(__dirname, "..", "dist", dest);
|
||||
|
||||
return {
|
||||
...BASE_ESBUILD_OPTIONS,
|
||||
/**
|
||||
* @type {esbuild.BuildOptions}
|
||||
*/
|
||||
|
||||
const entryPointConfig = {
|
||||
entryPoints: [`./src/${source}`],
|
||||
entryNames: `[dir]/[name]-${composeVersionID()}`,
|
||||
publicPath: path.join("/static", "dist", dest),
|
||||
outdir,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {esbuild.BuildOptions}
|
||||
*/
|
||||
const mergedConfig = deepmerge(BASE_ESBUILD_OPTIONS, entryPointConfig, overrides);
|
||||
|
||||
return mergedConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build all entry points in parallel.
|
||||
*
|
||||
* @param {EntryPoint[]} entryPoints
|
||||
* @returns {Promise<esbuild.BuildResult[]>}
|
||||
*/
|
||||
async function buildParallel(entryPoints) {
|
||||
await Promise.allSettled(
|
||||
return Promise.all(
|
||||
entryPoints.map((entryPoint) => {
|
||||
return esbuild.build(createEntryPointOptions(entryPoint));
|
||||
}),
|
||||
@ -209,7 +230,6 @@ async function doWatch() {
|
||||
return esbuild.context(
|
||||
createEntryPointOptions(entryPoint, {
|
||||
plugins: [
|
||||
...BASE_ESBUILD_OPTIONS.plugins,
|
||||
buildObserverPlugin({
|
||||
serverURL,
|
||||
logPrefix: entryPoint[1],
|
||||
|
@ -1,44 +1,13 @@
|
||||
/**
|
||||
* @import {Options as HighlightOptions} from 'rehype-highlight'
|
||||
* @import {CompileOptions} from '@mdx-js/mdx'
|
||||
* @import {mdxmermaid} from 'mdx-mermaid'
|
||||
* @import {Message,
|
||||
* @import {
|
||||
OnLoadArgs,
|
||||
OnLoadResult,
|
||||
Plugin,
|
||||
PluginBuild
|
||||
* } from 'esbuild'
|
||||
*/
|
||||
import { run as runMDX } from "@mdx-js/mdx";
|
||||
import { createFormatAwareProcessors } from "@mdx-js/mdx/internal-create-format-aware-processors";
|
||||
import { extnamesToRegex } from "@mdx-js/mdx/internal-extnames-to-regex";
|
||||
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 mdxMermaid from "mdx-mermaid";
|
||||
import { Mermaid } from "mdx-mermaid/lib/Mermaid";
|
||||
import assert from "node:assert";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import * as runtime from "react/jsx-runtime";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
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 { SourceMapGenerator } from "source-map";
|
||||
import { VFile } from "vfile";
|
||||
import { VFileMessage } from "vfile-message";
|
||||
|
||||
import { remarkAdmonition } from "./remark/remark-admonition.mjs";
|
||||
import { remarkHeadings } from "./remark/remark-headings.mjs";
|
||||
import { remarkLinks } from "./remark/remark-links.mjs";
|
||||
import { remarkLists } from "./remark/remark-lists.mjs";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
|
||||
/**
|
||||
* @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData
|
||||
@ -49,77 +18,28 @@ import { remarkLists } from "./remark/remark-lists.mjs";
|
||||
* @property {PluginData | null | undefined} [pluginData]
|
||||
* Plugin data.
|
||||
*
|
||||
* @typedef {CompileOptions} Options
|
||||
* Configuration.
|
||||
*
|
||||
* Options are the same as `compile` from `@mdx-js/mdx`.
|
||||
*
|
||||
* @typedef PluginData
|
||||
* Extra data passed.
|
||||
* @property {Buffer | string | null | undefined} [contents]
|
||||
* File contents.
|
||||
*
|
||||
* @typedef State
|
||||
* Info passed around.
|
||||
* @property {string} doc
|
||||
* File value.
|
||||
* @property {string} name
|
||||
* Plugin name.
|
||||
* @property {string} path
|
||||
* File path.
|
||||
*/
|
||||
|
||||
const eol = /\r\n|\r|\n|\u2028|\u2029/g;
|
||||
|
||||
const name = "@mdx-js/esbuild";
|
||||
const name = "mdx-plugin";
|
||||
|
||||
/**
|
||||
* Compile MDX to HTML.
|
||||
* *
|
||||
* @param {Readonly<Options> | null | undefined} [mdxOptions]
|
||||
* Configuration (optional).
|
||||
* @return {Plugin}
|
||||
* Plugin.
|
||||
* @typedef MDXPluginOptions
|
||||
*
|
||||
* @property {string} root Root directory.
|
||||
*/
|
||||
export function mdxPlugin(mdxOptions) {
|
||||
/** @type {mdxmermaid.Config} */
|
||||
const mermaidConfig = {
|
||||
output: "svg",
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {HighlightOptions}
|
||||
*/
|
||||
const highlightThemeOptions = {
|
||||
languages: {
|
||||
...common,
|
||||
nginx: nginxGrammar,
|
||||
apache: apacheGrammar,
|
||||
conf: confGrammar,
|
||||
diff: diffGrammar,
|
||||
},
|
||||
};
|
||||
|
||||
const { extnames, process } = createFormatAwareProcessors({
|
||||
...mdxOptions,
|
||||
SourceMapGenerator,
|
||||
outputFormat: "function-body",
|
||||
|
||||
remarkPlugins: [
|
||||
remarkParse,
|
||||
remarkDirective,
|
||||
remarkAdmonition,
|
||||
remarkGFM,
|
||||
remarkFrontmatter,
|
||||
remarkMdxFrontmatter,
|
||||
remarkHeadings,
|
||||
remarkLinks,
|
||||
remarkLists,
|
||||
[mdxMermaid, mermaidConfig],
|
||||
],
|
||||
rehypePlugins: [[rehypeHighlight, highlightThemeOptions]],
|
||||
});
|
||||
|
||||
/**
|
||||
* Bundle MDX into JSON modules.
|
||||
*
|
||||
* @param {MDXPluginOptions} options Options.
|
||||
* @returns {Plugin} Plugin.
|
||||
*/
|
||||
export function mdxPlugin({ root }) {
|
||||
return { name, setup };
|
||||
|
||||
/**
|
||||
@ -129,7 +49,7 @@ export function mdxPlugin(mdxOptions) {
|
||||
* Nothing.
|
||||
*/
|
||||
function setup(build) {
|
||||
build.onLoad({ filter: extnamesToRegex(extnames) }, onload);
|
||||
build.onLoad({ filter: /\.mdx?$/ }, onload);
|
||||
|
||||
/**
|
||||
* @param {LoadData} data
|
||||
@ -138,7 +58,7 @@ export function mdxPlugin(mdxOptions) {
|
||||
* Result.
|
||||
*/
|
||||
async function onload(data) {
|
||||
const document = String(
|
||||
const content = String(
|
||||
data.pluginData &&
|
||||
data.pluginData.contents !== null &&
|
||||
data.pluginData.contents !== undefined
|
||||
@ -146,154 +66,16 @@ export function mdxPlugin(mdxOptions) {
|
||||
: await fs.readFile(data.path),
|
||||
);
|
||||
|
||||
/** @type {State} */
|
||||
const state = {
|
||||
doc: document,
|
||||
name,
|
||||
path: data.path,
|
||||
};
|
||||
|
||||
let file = new VFile({
|
||||
path: data.path,
|
||||
value: document,
|
||||
});
|
||||
|
||||
/** @type {string | undefined} */
|
||||
let value;
|
||||
|
||||
/** @type {Array<VFileMessage>} */
|
||||
let messages = [];
|
||||
|
||||
/** @type {Array<Message>} */
|
||||
const errors = [];
|
||||
|
||||
/** @type {Array<Message>} */
|
||||
const warnings = [];
|
||||
|
||||
/**
|
||||
* @type {React.ComponentType<{children: React.ReactNode, frontmatter: Record<string, string>}>}
|
||||
*/
|
||||
const wrapper = ({ children, frontmatter }) => {
|
||||
const title = frontmatter.title;
|
||||
const nextChildren = React.Children.toArray(children);
|
||||
|
||||
if (title) {
|
||||
nextChildren.unshift(React.createElement("h1", { key: "title" }, title));
|
||||
}
|
||||
|
||||
return React.createElement(React.Fragment, null, nextChildren);
|
||||
};
|
||||
|
||||
try {
|
||||
file = await process(file);
|
||||
const { default: Content, ...mdxExports } = await runMDX(file, {
|
||||
...runtime,
|
||||
useMDXComponents: () => {
|
||||
return {
|
||||
mermaid: Mermaid,
|
||||
Mermaid,
|
||||
};
|
||||
},
|
||||
baseUrl: import.meta.url,
|
||||
});
|
||||
|
||||
const { frontmatter = {} } = mdxExports;
|
||||
const result = renderToStaticMarkup(
|
||||
Content({
|
||||
frontmatter,
|
||||
components: {
|
||||
wrapper,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
value = result;
|
||||
|
||||
messages = file.messages;
|
||||
} catch (error_) {
|
||||
const cause = /** @type {VFileMessage | Error} */ (error_);
|
||||
|
||||
console.error(cause);
|
||||
|
||||
const message =
|
||||
"reason" in cause
|
||||
? cause
|
||||
: new VFileMessage("Cannot process MDX file with esbuild", {
|
||||
cause,
|
||||
ruleId: "process-error",
|
||||
source: "@mdx-js/esbuild",
|
||||
});
|
||||
|
||||
message.fatal = true;
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
const list = message.fatal ? errors : warnings;
|
||||
list.push(vfileMessageToEsbuild(state, message));
|
||||
}
|
||||
|
||||
// Safety check: the file has a path, so there has to be a `dirname`.
|
||||
assert(file.dirname, "expected `dirname` to be defined");
|
||||
const publicPath = path.resolve(
|
||||
"/",
|
||||
path.relative(path.join(root, "website"), data.path),
|
||||
);
|
||||
const publicDirectory = path.dirname(publicPath);
|
||||
|
||||
return {
|
||||
contents: value || "",
|
||||
loader: "text",
|
||||
errors,
|
||||
resolveDir: path.resolve(file.cwd, file.dirname),
|
||||
warnings,
|
||||
contents: JSON.stringify({ content, publicPath, publicDirectory }),
|
||||
loader: "file",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<State>} state
|
||||
* Info passed around.
|
||||
* @param {Readonly<VFileMessage>} message
|
||||
* VFile message or error.
|
||||
* @returns {Message}
|
||||
* ESBuild message.
|
||||
*/
|
||||
function vfileMessageToEsbuild(state, message) {
|
||||
const place = message.place;
|
||||
const start = place ? ("start" in place ? place.start : place) : undefined;
|
||||
const end = place && "end" in place ? place.end : undefined;
|
||||
let length = 0;
|
||||
let lineStart = 0;
|
||||
let line = 0;
|
||||
let column = 0;
|
||||
|
||||
if (start && start.offset !== undefined) {
|
||||
line = start.line;
|
||||
column = start.column - 1;
|
||||
lineStart = start.offset - column;
|
||||
length = 1;
|
||||
|
||||
if (end && end.offset !== undefined) {
|
||||
length = end.offset - start.offset;
|
||||
}
|
||||
}
|
||||
|
||||
eol.lastIndex = lineStart;
|
||||
|
||||
const match = eol.exec(state.doc);
|
||||
const lineEnd = match ? match.index : state.doc.length;
|
||||
|
||||
return {
|
||||
detail: message,
|
||||
id: "",
|
||||
location: {
|
||||
column,
|
||||
file: state.path,
|
||||
length: Math.min(length, lineEnd),
|
||||
line,
|
||||
lineText: state.doc.slice(lineStart, lineEnd),
|
||||
namespace: "file",
|
||||
suggestion: "",
|
||||
},
|
||||
notes: [],
|
||||
pluginName: state.name,
|
||||
text: message.reason,
|
||||
};
|
||||
}
|
||||
|
@ -1,59 +0,0 @@
|
||||
/**
|
||||
* @import {Plugin} from 'unified'
|
||||
* @import {} from 'mdast-util-directive'
|
||||
* @import {} from 'mdast-util-to-hast'
|
||||
* @import {Root, Link} from 'mdast'
|
||||
* @import {VFile} from 'vfile'
|
||||
*/
|
||||
import * as path from "node:path";
|
||||
import { visit } from "unist-util-visit";
|
||||
|
||||
const DOCS_DOMAIN = "https://goauthentik.io";
|
||||
|
||||
/**
|
||||
* Remark plugin to process links
|
||||
* @type {Plugin<[unknown], Root, VFile>}
|
||||
*/
|
||||
export const remarkLinks = () => {
|
||||
return function transformer(tree, file) {
|
||||
const docsRoot = path.resolve(file.cwd, "..", "website");
|
||||
|
||||
/**
|
||||
* @param {Link} node
|
||||
*/
|
||||
const visitor = (node) => {
|
||||
node.data = node.data || {};
|
||||
|
||||
if (node.url.startsWith("#")) {
|
||||
node.data.hProperties = {
|
||||
className: "markdown-heading",
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
node.data.hProperties = {
|
||||
...node.data.hProperties,
|
||||
rel: "noopener noreferrer",
|
||||
target: "_blank",
|
||||
};
|
||||
|
||||
if (node.url.startsWith(".") && file.dirname) {
|
||||
const nextPathname = path.resolve(
|
||||
"/",
|
||||
path.relative(docsRoot, file.dirname),
|
||||
node.url,
|
||||
);
|
||||
const nextURL = new URL(nextPathname, DOCS_DOMAIN);
|
||||
|
||||
// Remove trailing .md and .mdx, and trailing "index".
|
||||
nextURL.pathname = nextURL.pathname.replace(/(index)?\.mdx?$/, "");
|
||||
|
||||
node.data.hProperties.href = nextURL.toString();
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore - visit cannot infer the type of the visitor.
|
||||
visit(tree, "link", visitor);
|
||||
};
|
||||
};
|
@ -5,7 +5,6 @@ import "@goauthentik/components/events/ObjectChangelog";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/PageHeader";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
|
@ -3,7 +3,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import MDApplication from "@goauthentik/docs/add-secure-apps/applications/index.md";
|
||||
import "@goauthentik/elements/AppIcon.js";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/ak-mdx";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
@ -89,7 +89,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown .content=${MDApplication}></ak-markdown>
|
||||
<ak-mdx .url=${MDApplication}></ak-mdx>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
@ -7,7 +7,6 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import "@goauthentik/components/ak-status-label";
|
||||
import "@goauthentik/components/events/ObjectChangelog";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/SyncStatusCard";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
|
@ -6,7 +6,6 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import "@goauthentik/components/events/ObjectChangelog";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
|
@ -8,8 +8,8 @@ import MDProviderOAuth2 from "@goauthentik/docs/add-secure-apps/providers/oauth2
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/ak-mdx";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
|
||||
@ -357,20 +357,20 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
|
||||
>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown
|
||||
.content=${MDProviderOAuth2}
|
||||
<ak-mdx
|
||||
.url=${MDProviderOAuth2}
|
||||
.replacers=${[
|
||||
(input: string) => {
|
||||
if (!this.provider) {
|
||||
return input;
|
||||
}
|
||||
return input.replaceAll(
|
||||
"<application slug>",
|
||||
"<application slug>",
|
||||
this.provider.assignedApplicationSlug,
|
||||
);
|
||||
},
|
||||
]}
|
||||
></ak-markdown>
|
||||
></ak-mdx>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
@ -16,10 +16,9 @@ import MDTraefikStandalone from "@goauthentik/docs/add-secure-apps/providers/pro
|
||||
import MDHeaderAuthentication from "@goauthentik/docs/add-secure-apps/providers/proxy/header_authentication.mdx";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import { Replacer } from "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/ak-mdx";
|
||||
import type { Replacer } from "@goauthentik/elements/ak-mdx";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import { getURLParam } from "@goauthentik/elements/router/RouteMatch";
|
||||
@ -127,37 +126,30 @@ export class ProxyProviderViewPage extends AKElement {
|
||||
{
|
||||
label: msg("Nginx (Ingress)"),
|
||||
md: MDNginxIngress,
|
||||
meta: "providers/proxy/_nginx_ingress.md",
|
||||
},
|
||||
{
|
||||
label: msg("Nginx (Proxy Manager)"),
|
||||
md: MDNginxPM,
|
||||
meta: "providers/proxy/_nginx_proxy_manager.md",
|
||||
},
|
||||
{
|
||||
label: msg("Nginx (standalone)"),
|
||||
md: MDNginxStandalone,
|
||||
meta: "providers/proxy/_nginx_standalone.md",
|
||||
},
|
||||
{
|
||||
label: msg("Traefik (Ingress)"),
|
||||
md: MDTraefikIngress,
|
||||
meta: "providers/proxy/_traefik_ingress.md",
|
||||
},
|
||||
{
|
||||
label: msg("Traefik (Compose)"),
|
||||
md: MDTraefikCompose,
|
||||
meta: "providers/proxy/_traefik_compose.md",
|
||||
},
|
||||
{
|
||||
label: msg("Traefik (Standalone)"),
|
||||
md: MDTraefikStandalone,
|
||||
meta: "providers/proxy/_traefik_standalone.md",
|
||||
},
|
||||
{
|
||||
label: msg("Caddy (Standalone)"),
|
||||
md: MDCaddyStandalone,
|
||||
meta: "providers/proxy/_caddy_standalone.md",
|
||||
},
|
||||
];
|
||||
const replacers: Replacer[] = [
|
||||
@ -195,11 +187,7 @@ export class ProxyProviderViewPage extends AKElement {
|
||||
data-tab-title="${server.label}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile ak-markdown-section"
|
||||
>
|
||||
<ak-markdown
|
||||
.content=${server.md}
|
||||
.replacers=${replacers}
|
||||
meta=${server.meta}
|
||||
></ak-markdown>
|
||||
<ak-mdx .url=${server.md} .replacers=${replacers}></ak-mdx>
|
||||
</section>`;
|
||||
})}</ak-tabs
|
||||
>`;
|
||||
@ -265,10 +253,7 @@ export class ProxyProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown
|
||||
.content=${MDHeaderAuthentication}
|
||||
meta="proxy/header_authentication.md"
|
||||
></ak-markdown>
|
||||
<ak-mdx .url=${MDHeaderAuthentication}></ak-mdx>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
@ -9,9 +9,9 @@ import "@goauthentik/components/ak-status-label";
|
||||
import "@goauthentik/components/events/ObjectChangelog";
|
||||
import MDSCIMProvider from "@goauthentik/docs/add-secure-apps/providers/scim/index.md";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/SyncStatusCard";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/ak-mdx";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
|
||||
@ -243,10 +243,7 @@ export class SCIMProviderViewPage extends AKElement {
|
||||
</div>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-5-col">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown
|
||||
.content=${MDSCIMProvider}
|
||||
meta="providers/scim/index.md"
|
||||
></ak-markdown>
|
||||
<ak-mdx .content=${MDSCIMProvider}></ak-mdx>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
@ -7,7 +7,6 @@ import "@goauthentik/components/events/ObjectChangelog";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
|
@ -7,9 +7,9 @@ import "@goauthentik/components/events/ObjectChangelog";
|
||||
import MDSourceKerberosBrowser from "@goauthentik/docs/users-sources/sources/protocols/kerberos/browser.md";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/Markdown";
|
||||
import "@goauthentik/elements/SyncStatusCard";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/ak-mdx";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
@ -186,11 +186,7 @@ export class KerberosSourceViewPage extends AKElement {
|
||||
${this.renderSyncCards()}
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-markdown
|
||||
.content=${MDSourceKerberosBrowser}
|
||||
meta="users-sources/protocols/kerberos/browser.md"
|
||||
;
|
||||
></ak-markdown>
|
||||
<ak-mdx .url=${MDSourceKerberosBrowser}></ak-mdx>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,6 +18,7 @@ export const TAG_SENTRY_CAPABILITIES = "authentik.capabilities";
|
||||
|
||||
export async function configureSentry(canDoPpi = false): Promise<Config> {
|
||||
const cfg = await config();
|
||||
|
||||
if (cfg.errorReporting.enabled) {
|
||||
init({
|
||||
dsn: cfg.errorReporting.sentryDsn,
|
||||
|
@ -49,41 +49,6 @@ html > form > input {
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Anchors */
|
||||
|
||||
a {
|
||||
--pf-global--link--Color: var(--pf-global--link--Color--light);
|
||||
--pf-global--link--Color--hover: var(--pf-global--link--Color--light--hover);
|
||||
--pf-global--link--Color--visited: var(--pf-global--link--Color);
|
||||
}
|
||||
|
||||
/*
|
||||
Note that order of anchor pseudo-selectors must follow:
|
||||
|
||||
1. link
|
||||
2. visited
|
||||
3. hover
|
||||
4. active
|
||||
*/
|
||||
|
||||
a:link {
|
||||
color: var(--pf-global--link--Color);
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: var(--pf-global--link--Color--visited);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--pf-global--link--Color--hover);
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: var(--pf-global--link--Color);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Icons */
|
||||
|
||||
.pf-icon {
|
||||
@ -228,43 +193,6 @@ a:active {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
/* #region Mermaid */
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Tables */
|
||||
|
||||
table thead,
|
||||
table tr:nth-child(2n) {
|
||||
background-color: var(
|
||||
--ak-table-stripe-background,
|
||||
var(--pf-global--BackgroundColor--light-200)
|
||||
);
|
||||
}
|
||||
|
||||
table td,
|
||||
table th {
|
||||
border: var(--pf-table-border-width) solid var(--ifm-table-border-color);
|
||||
padding: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
/* #region Code blocks */
|
||||
|
||||
pre:has(.hljs) {
|
||||
|
@ -1,86 +0,0 @@
|
||||
import "@goauthentik/elements/Alert";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
|
||||
import { CSSResult, PropertyValues, css, nothing } from "lit";
|
||||
import { customElement, property } 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";
|
||||
|
||||
export type Replacer = (input: string) => string;
|
||||
|
||||
@customElement("ak-markdown")
|
||||
export class Markdown extends AKElement {
|
||||
@property()
|
||||
content: string = "";
|
||||
|
||||
@property()
|
||||
meta: string = "";
|
||||
|
||||
@property({ attribute: false })
|
||||
replacers: Replacer[] = [];
|
||||
|
||||
resolvedHTML = "";
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFList,
|
||||
PFContent,
|
||||
css`
|
||||
h2:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
const headingLinks =
|
||||
this.shadowRoot?.querySelectorAll<HTMLAnchorElement>("a.markdown-heading") ?? [];
|
||||
|
||||
for (const headingLink of headingLinks) {
|
||||
headingLink.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const url = new URL(headingLink.href);
|
||||
const elementID = url.hash.slice(1);
|
||||
|
||||
const target = this.shadowRoot?.getElementById(elementID);
|
||||
|
||||
if (!target) {
|
||||
console.warn(`Element with ID ${elementID} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
target.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
willUpdate(properties: PropertyValues<this>) {
|
||||
if (properties.has("content")) {
|
||||
this.resolvedHTML = this.replacers.reduce(
|
||||
(html, replacer) => replacer(html),
|
||||
this.content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.content) return nothing;
|
||||
|
||||
return unsafeHTML(this.resolvedHTML);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-markdown": Markdown;
|
||||
}
|
||||
}
|
53
web/src/elements/ak-mdx/MDXModuleContext.ts
Normal file
53
web/src/elements/ak-mdx/MDXModuleContext.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
220
web/src/elements/ak-mdx/ak-mdx.tsx
Normal file
220
web/src/elements/ak-mdx/ak-mdx.tsx
Normal file
@ -0,0 +1,220 @@
|
||||
import "@goauthentik/elements/Alert";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import {
|
||||
MDXModule,
|
||||
MDXModuleContext,
|
||||
fetchMDXModule,
|
||||
} from "@goauthentik/elements/ak-mdx/MDXModuleContext";
|
||||
import { MDXAnchor } from "@goauthentik/elements/ak-mdx/components/MDXAnchor";
|
||||
import { MDXWrapper } from "@goauthentik/elements/ak-mdx/components/MDXWrapper";
|
||||
import { remarkAdmonition } from "@goauthentik/elements/ak-mdx/remark/remark-admonition";
|
||||
import { remarkHeadings } from "@goauthentik/elements/ak-mdx/remark/remark-headings";
|
||||
import { remarkLists } from "@goauthentik/elements/ak-mdx/remark/remark-lists";
|
||||
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 { Root, createRoot } from "react-dom/client";
|
||||
import * as runtime from "react/jsx-runtime";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import { 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 { CSSResult, css } from "lit";
|
||||
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 { UiThemeEnum } from "@goauthentik/api";
|
||||
|
||||
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 {
|
||||
@property({
|
||||
reflect: true,
|
||||
})
|
||||
url: string = "";
|
||||
|
||||
@property()
|
||||
content: string = "";
|
||||
|
||||
@property({ attribute: false })
|
||||
replacers: Replacer[] = [];
|
||||
|
||||
#reactRoot: Root | null = null;
|
||||
|
||||
resolvedHTML = "";
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFList,
|
||||
PFContent,
|
||||
css`
|
||||
a {
|
||||
--pf-global--link--Color: var(--pf-global--link--Color--light);
|
||||
--pf-global--link--Color--hover: var(--pf-global--link--Color--light--hover);
|
||||
--pf-global--link--Color--visited: var(--pf-global--link--Color);
|
||||
}
|
||||
|
||||
/*
|
||||
Note that order of anchor pseudo-selectors must follow:
|
||||
1. link
|
||||
2. visited
|
||||
3. hover
|
||||
4. active
|
||||
*/
|
||||
|
||||
a:link {
|
||||
color: var(--pf-global--link--Color);
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: var(--pf-global--link--Color--visited);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--pf-global--link--Color--hover);
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: var(--pf-global--link--Color);
|
||||
}
|
||||
|
||||
h2:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
table thead,
|
||||
table tr:nth-child(2n) {
|
||||
background-color: var(
|
||||
--ak-table-stripe-background,
|
||||
var(--pf-global--BackgroundColor--light-200)
|
||||
);
|
||||
}
|
||||
|
||||
table td,
|
||||
table th {
|
||||
border: var(--pf-table-border-width) solid var(--ifm-table-border-color);
|
||||
padding: var(--pf-global--spacer--md);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
public async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.#reactRoot = createRoot(this.shadowRoot!);
|
||||
|
||||
let nextMDXModule: MDXModule | undefined;
|
||||
|
||||
if (this.url) {
|
||||
nextMDXModule = await fetchMDXModule(this.url);
|
||||
} else {
|
||||
nextMDXModule = {
|
||||
content: 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;
|
||||
}
|
||||
}
|
61
web/src/elements/ak-mdx/components/MDXAnchor.tsx
Normal file
61
web/src/elements/ak-mdx/components/MDXAnchor.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { useMDXModule } from "@goauthentik/elements/ak-mdx/MDXModuleContext";
|
||||
import { resolve } from "node:path";
|
||||
import React, { memo } from "react";
|
||||
|
||||
const DOCS_DOMAIN = "https://goauthentik.io";
|
||||
|
||||
/**
|
||||
* 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 = memo<React.AnchorHTMLAttributes<HTMLAnchorElement>>(
|
||||
({ href, children, ...props }) => {
|
||||
const { publicDirectory } = useMDXModule();
|
||||
|
||||
if (href?.startsWith(".") && publicDirectory) {
|
||||
const nextPathname = resolve(publicDirectory, href);
|
||||
|
||||
const nextURL = new URL(nextPathname, DOCS_DOMAIN);
|
||||
// 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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MDXAnchor.displayName = "MDXAnchor";
|
20
web/src/elements/ak-mdx/components/MDXWrapper.tsx
Normal file
20
web/src/elements/ak-mdx/components/MDXWrapper.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
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: React.FC<MDXWrapperProps> = ({ children, frontmatter }) => {
|
||||
const { title } = frontmatter;
|
||||
const nextChildren = React.Children.toArray(children);
|
||||
|
||||
if (title) {
|
||||
nextChildren.unshift(<h1 key="header-title">{title}</h1>);
|
||||
}
|
||||
|
||||
return <>{nextChildren}</>;
|
||||
};
|
2
web/src/elements/ak-mdx/index.ts
Normal file
2
web/src/elements/ak-mdx/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./ak-mdx";
|
||||
export { AKMDX as default } from "./ak-mdx";
|
@ -1,25 +1,18 @@
|
||||
/**
|
||||
* @import {Plugin} from 'unified'
|
||||
* @import {Directives} from 'mdast-util-directive'
|
||||
* @import {} from 'mdast-util-to-hast'
|
||||
* @import {Root} from 'mdast'
|
||||
* @import {VFile} from 'vfile'
|
||||
*/
|
||||
import { h } from "hastscript";
|
||||
import type { 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"]);
|
||||
|
||||
/**
|
||||
* Remark plugin to process links
|
||||
* @type {Plugin<[unknown], Root, VFile>}
|
||||
* Remark plugin to add admonition classes to directives.
|
||||
*/
|
||||
export function remarkAdmonition() {
|
||||
export const remarkAdmonition: Plugin<[unknown], Root, VFile> = () => {
|
||||
return function transformer(tree) {
|
||||
/**
|
||||
* @param {Directives} node
|
||||
*/
|
||||
const visitor = (node) => {
|
||||
const visitor = (node: Directives) => {
|
||||
if (
|
||||
node.type === "containerDirective" ||
|
||||
node.type === "leafDirective" ||
|
||||
@ -43,4 +36,4 @@ export function remarkAdmonition() {
|
||||
// @ts-ignore - visit cannot infer the type of the visitor.
|
||||
visit(tree, visitor);
|
||||
};
|
||||
}
|
||||
};
|
@ -1,22 +1,16 @@
|
||||
/**
|
||||
* @import {Plugin} from 'unified'
|
||||
* @import {Root, Heading} from 'mdast'
|
||||
* @import {VFile} from 'vfile'
|
||||
*/
|
||||
import { kebabCase } from "change-case";
|
||||
import { Heading, Root } from "mdast";
|
||||
import { toString } from "mdast-util-to-string";
|
||||
import { Plugin } from "unified";
|
||||
import { visit } from "unist-util-visit";
|
||||
import { VFile } from "vfile";
|
||||
|
||||
/**
|
||||
* Remark plugin to process links
|
||||
* @type {Plugin<[unknown], Root, VFile>}
|
||||
* Remark plugin to add IDs to headings.
|
||||
*/
|
||||
export const remarkHeadings = () => {
|
||||
export const remarkHeadings: Plugin<[unknown], Root, VFile> = () => {
|
||||
return function transformer(tree) {
|
||||
/**
|
||||
* @param {Heading} node
|
||||
*/
|
||||
const visitor = (node) => {
|
||||
const visitor = (node: Heading) => {
|
||||
const textContent = toString(node);
|
||||
const id = kebabCase(textContent);
|
||||
|
@ -1,20 +1,14 @@
|
||||
/**
|
||||
* @import {Plugin} from 'unified'
|
||||
* @import {Root, List} from 'mdast'
|
||||
* @import {VFile} from 'vfile'
|
||||
*/
|
||||
import type { List, Root } from "mdast";
|
||||
import type { Plugin } from "unified";
|
||||
import { visit } from "unist-util-visit";
|
||||
import type { VFile } from "vfile";
|
||||
|
||||
/**
|
||||
* Remark plugin to process links
|
||||
* @type {Plugin<[unknown], Root, VFile>}
|
||||
* Remark plugin to process lists.
|
||||
*/
|
||||
export const remarkLists = () => {
|
||||
export const remarkLists: Plugin<[unknown], Root, VFile> = () => {
|
||||
return function transformer(tree) {
|
||||
/**
|
||||
* @param {List} node
|
||||
*/
|
||||
const visitor = (node) => {
|
||||
const visitor = (node: List) => {
|
||||
node.data = node.data || {};
|
||||
|
||||
node.data.hProperties = {
|
12
web/src/global.d.ts
vendored
12
web/src/global.d.ts
vendored
@ -2,18 +2,18 @@ declare module "*.css";
|
||||
|
||||
declare module "*.md" {
|
||||
/**
|
||||
* The HTML content of the markdown file.
|
||||
* The serialized JSON content of an MD file.
|
||||
*/
|
||||
const html: string;
|
||||
export default html;
|
||||
const serializedJSON: string;
|
||||
export default serializedJSON;
|
||||
}
|
||||
|
||||
declare module "*.mdx" {
|
||||
/**
|
||||
* The HTML content of the markdown file.
|
||||
* The serialized JSON content of an MDX file.
|
||||
*/
|
||||
const html: string;
|
||||
export default html;
|
||||
const serializedJSON: string;
|
||||
export default serializedJSON;
|
||||
}
|
||||
|
||||
declare namespace Intl {
|
||||
|
@ -13,6 +13,7 @@
|
||||
"expect-webdriverio",
|
||||
"grecaptcha"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"experimentalDecorators": true,
|
||||
|
Reference in New Issue
Block a user