Files
authentik/web/src/elements/Markdown.ts
Ken Sternberg 64ce170882 web: markdown: display markdown even when frontmatter is missing (#9404)
* web: fix esbuild issue with style sheets

Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.

* web: markdown: display markdown even when frontmatter is missing

Make the check for the document title comprehensive across the
entire demeter.  If there is no front matter, `data` will be missing,
not just `data.title`.
2024-04-24 22:53:18 +02:00

107 lines
3.3 KiB
TypeScript

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 { 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/;
@customElement("ak-markdown")
export class Markdown extends AKElement {
@property()
md: string = "";
@property()
meta: string = "";
@property({ attribute: false })
replacers: Replacer[] = [];
docHtml = "";
docTitle = "";
defaultReplacers: Replacer[] = [
this.replaceAdmonitions,
this.replaceList,
this.replaceRelativeLinks,
];
static get styles(): CSSResult[] {
return [
PFList,
PFContent,
css`
h2:first-of-type {
margin-top: 0;
}
`,
];
}
converter = new showdown.Converter({ metadata: true, tables: true });
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)
);
}
replaceList(input: string): string {
return input.replace("<ul>", "<ul class='pf-c-list'>");
}
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"`;
});
}
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,
);
}
}
render() {
if (!this.md) {
return nothing;
}
return html`${this.docTitle ? html`<h2>${this.docTitle}</h2>` : nothing}
${unsafeHTML(this.docHtml)}`;
}
}