blob: 2e93245d60ea1d1e9006076bdda7e5780dc25758 [file]
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {css, html, LitElement, PropertyValues} from 'lit';
import {
customElement,
property,
queryAssignedElements,
} from 'lit/decorators.js';
import {Marked, Renderer, Tokenizer} from 'marked';
import {
sanitizeHtml,
setElementInnerHtml,
} from '../../../utils/inner-html-util';
declare global {
interface HTMLElementTagNameMap {
'gr-marked-element': GrMarkedElement;
}
}
/**
* This is based on [marked-element](https://github.com/PolymerElements/marked-element) by Polymer
* but converted to use Lit. It uses the [marked](https://github.com/markedjs/marked) library.
*/
@customElement('gr-marked-element')
export class GrMarkedElement extends LitElement {
@property({type: String}) markdown: string | null = null;
@property({type: Boolean}) breaks = false;
@property({type: Boolean}) pedantic = false;
@property({type: Function}) renderer: Function | null = null;
@property({type: Function}) tokenizer: Function | null = null;
@queryAssignedElements({
flatten: true,
slot: 'markdown-html',
})
private outputElement!: Array<HTMLElement>;
static override styles = css`
:host {
display: block;
}
`;
override render() {
return html`
<slot name="markdown-html">
<div id="content" slot="markdown-html"></div>
</slot>
`;
}
override connectedCallback() {
super.connectedCallback();
this.renderMarkdown();
}
protected override updated(changedProps: PropertyValues) {
const propsToWatch = [
'markdown',
'breaks',
'pedantic',
'renderer',
'tokenizer',
];
if (propsToWatch.some(prop => changedProps.has(prop))) {
this.renderMarkdown();
}
}
override firstUpdated() {
this.renderMarkdown();
}
private renderMarkdown() {
if (!this.isConnected || !this.outputElement.length) {
return;
}
if (!this.markdown) {
this.outputElement[0].textContent = '';
return;
}
const renderer = new Renderer();
if (this.renderer) {
this.renderer(renderer);
}
const tokenizer = new Tokenizer();
if (this.tokenizer) {
this.tokenizer(tokenizer);
}
const marked = new Marked();
const unsafeHtml =
marked.parse(this.markdown, {
async: false,
breaks: this.breaks,
pedantic: this.pedantic,
renderer,
tokenizer,
}) || '';
const safeHtml = sanitizeHtml(unsafeHtml);
setElementInnerHtml(this.outputElement[0], safeHtml);
this.dispatchEvent(
new CustomEvent('marked-render-complete', {bubbles: true, composed: true})
);
}
}