web: Fix inline documentation rendering (#13379)

web: Fix issues surrounding markdown rendering.

- Fix issue where Mermaid diagrams do not render.
- Fix link colors in dark mode.
- Fix anchored links triggering router.
- Fix issue where links occasionally link to missing page.
This commit is contained in:
Teffen Ellis
2025-03-19 17:09:47 +01:00
committed by GitHub
parent 74292e6c23
commit b6442c233d
26 changed files with 5117 additions and 299 deletions

View File

@ -8,6 +8,7 @@ import { localized } from "@lit/localize";
import { LitElement, ReactiveElement } from "lit";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import OneDark from "@goauthentik/common/styles/one-dark.css";
import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api";
@ -70,6 +71,7 @@ export class AKElement extends LitElement {
styleRoot.adoptedStyleSheets = adaptCSS([
...styleRoot.adoptedStyleSheets,
ensureCSSStyleSheet(AKGlobal),
ensureCSSStyleSheet(OneDark),
]);
this._initTheme(styleRoot);
this._initCustomCSS(styleRoot);

View File

@ -1,30 +1,19 @@
import { docLink } from "@goauthentik/common/global";
import "@goauthentik/elements/Alert";
import { Level } from "@goauthentik/elements/Alert";
import { AKElement } from "@goauthentik/elements/Base";
import { matter } from "md-front-matter";
import * as showdown from "showdown";
import { CSSResult, PropertyValues, css, html, nothing } from "lit";
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 interface MarkdownDocument {
path: string;
}
export type Replacer = (input: string, md: MarkdownDocument) => string;
const isRelativeLink = /href="(\.[^"]*)"/gm;
const isFile = /[^/]+\.md/;
export type Replacer = (input: string) => string;
@customElement("ak-markdown")
export class Markdown extends AKElement {
@property()
md: string = "";
content: string = "";
@property()
meta: string = "";
@ -32,14 +21,7 @@ export class Markdown extends AKElement {
@property({ attribute: false })
replacers: Replacer[] = [];
docHtml = "";
docTitle = "";
defaultReplacers: Replacer[] = [
this.replaceAdmonitions,
this.replaceList,
this.replaceRelativeLinks,
];
resolvedHTML = "";
static get styles(): CSSResult[] {
return [
@ -53,55 +35,47 @@ export class Markdown extends AKElement {
];
}
converter = new showdown.Converter({ metadata: true, tables: true });
protected firstUpdated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
replaceAdmonitions(input: string): string {
const admonitionStart = /:::(\w+)(<br\s*\/>|\s*$)/gm;
const admonitionEnd = /:::/gm;
return (
input
.replaceAll(admonitionStart, "<ak-alert level='pf-m-$1'>")
.replaceAll(admonitionEnd, "</ak-alert>")
// Workaround for admonitions using caution instead of warning
.replaceAll("pf-m-caution", Level.Warning)
);
}
const headingLinks =
this.shadowRoot?.querySelectorAll<HTMLAnchorElement>("a.markdown-heading") ?? [];
replaceList(input: string): string {
return input.replace("<ul>", "<ul class='pf-c-list'>");
}
for (const headingLink of headingLinks) {
headingLink.addEventListener("click", (ev) => {
ev.preventDefault();
replaceRelativeLinks(input: string, md: MarkdownDocument): string {
const baseName = md.path.replace(isFile, "");
const baseUrl = docLink("");
return input.replace(isRelativeLink, (_match, path) => {
const pathName = path.replace(".md", "");
const link = `docs/${baseName}${pathName}`;
const url = new URL(link, baseUrl).toString();
return `href="${url}" _target="blank" rel="noopener noreferrer"`;
});
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("md") || properties.has("meta")) {
const parsedContent = matter(this.md);
const parsedHTML = this.converter.makeHtml(parsedContent.content);
const replacers = [...this.defaultReplacers, ...this.replacers];
this.docTitle = parsedContent?.data?.title ?? "";
this.docHtml = replacers.reduce(
(html, replacer) => replacer(html, { path: this.meta }),
parsedHTML,
if (properties.has("content")) {
this.resolvedHTML = this.replacers.reduce(
(html, replacer) => replacer(html),
this.content,
);
}
}
render() {
if (!this.md) {
return nothing;
}
if (!this.content) return nothing;
return html`${this.docTitle ? html`<h2>${this.docTitle}</h2>` : nothing}
${unsafeHTML(this.docHtml)}`;
return unsafeHTML(this.resolvedHTML);
}
}