import { LitElement, html } from "lit"
import { Task } from "@lit/task"
import { unsafeHTML } from "lit/directives/unsafe-html.js"
import { parse } from "./parse.js"
/**
* A custom element that fetches and embeds markdown wc content.
*
* @example
* ```html
*
*
*
* ```
*/
export default class Element extends LitElement {
static override properties = {
src: { type: String },
base: { type: String }, // Base path to propagate
}
src?: string
base?: string
private fetchMarkdown = new Task(
this,
async ([src, base]) => {
if (src === undefined) {
throw new Error("src is undefined")
}
// Resolve the full URL using the base path, if provided
const resolvedSrc = base ? resolveUrl(src, base) : src
const text = await (await fetch(resolvedSrc)).text()
const parsed = await parse(text)
for (const importSrc of parsed.frontmatter.imports ?? []) {
const resolvedImportSrc = resolveUrl(importSrc, resolvedSrc)
// not awaited to speed up rendering. components will load in parallel
import(resolvedImportSrc)
}
return this.base
? // prefix the html with the base path for child elements to inherit
`` + parsed.html
: parsed.html
},
() => [this.src, this.base] // React to changes in src or base
)
protected override createRenderRoot() {
// Render in light DOM so nested embeds can see propagated base comments.
return this
}
override connectedCallback() {
super.connectedCallback()
if (!this.base) {
this.inheritBaseFromParent()
}
}
/**
* Inherit the `base` value from the nearest parent's HTML comment.
*/
private inheritBaseFromParent() {
let current: Node | null = this.parentNode
while (current) {
const base = this.getBaseFromComments(current)
if (base !== undefined) {
this.base = base
return
}
current = current.parentNode
}
}
private getBaseFromComments(node: Node): string | undefined {
for (const child of Array.from(node.childNodes)) {
if (child.nodeType !== Node.COMMENT_NODE) {
continue
}
const match = child.nodeValue?.match(/mwc-base=([^\s]*)/)
if (match) {
return match[1]
}
}
return undefined
}
override render() {
return html`
${this.fetchMarkdown.render({
pending: () => html`
Loading...
`,
complete: (markdown) => html` ${unsafeHTML(markdown)} `,
error: (error) => html`
Error loading markdown.
${error}
`,
})}
`
}
}
/**
* Resolves a relative URL against a base URL, handling relative paths.
* @param {string} relativePath - The relative path to resolve.
* @param {string} basePath - The base path to resolve against.
* @returns {string} - The resolved URL.
*/
function resolveUrl(relativePath: string, basePath: string): string {
// If basePath is not absolute, use document.baseURI as the absolute base
const absoluteBase =
basePath.startsWith("http://") || basePath.startsWith("https://")
? basePath
: new URL(basePath, document.baseURI).href
// Resolve the relative path
return new URL(relativePath, absoluteBase).href
}
if (typeof customElements !== "undefined" && !customElements.get("markdown-wc-embed")) {
customElements.define("markdown-wc-embed", Element)
}