jupyterhub/theme/static/components/md-block/md-block.js

302 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* <md-block> custom element
* @author Lea Verou
*/
let marked = window.marked;
let DOMPurify = window.DOMPurify;
let Prism = window.Prism;
export const URLs = {
marked: "https://cdn.jsdelivr.net/npm/marked/src/marked.min.js",
DOMPurify: "https://cdn.jsdelivr.net/npm/dompurify@2.3.3/dist/purify.es.min.js"
}
// Fix indentation
function deIndent(text) {
let indent = text.match(/^[\r\n]*([\t ]+)/);
if (indent) {
indent = indent[1];
text = text.replace(RegExp("^" + indent, "gm"), "");
}
return text;
}
export class MarkdownElement extends HTMLElement {
constructor() {
super();
this.renderer = Object.assign({}, this.constructor.renderer);
for (let property in this.renderer) {
this.renderer[property] = this.renderer[property].bind(this);
}
}
get rendered() {
return this.getAttribute("rendered");
}
get mdContent () {
return this._mdContent;
}
set mdContent (html) {
this._mdContent = html;
this._contentFromHTML = false;
this.render();
}
connectedCallback() {
Object.defineProperty(this, "untrusted", {
value: this.hasAttribute("untrusted"),
enumerable: true,
configurable: false,
writable: false
});
if (this._mdContent === undefined) {
this._contentFromHTML = true;
this._mdContent = deIndent(this.innerHTML);
}
this.render();
}
async render () {
if (!this.isConnected || this._mdContent === undefined) {
return;
}
if (!marked) {
marked = import(URLs.marked).then(m => m.marked);
}
marked = await marked;
marked.setOptions({
gfm: true,
smartypants: true,
langPrefix: "language-",
});
marked.use({renderer: this.renderer});
let html = this._parse();
if (this.untrusted) {
let mdContent = this._mdContent;
html = await MarkdownElement.sanitize(html);
if (this._mdContent !== mdContent) {
// While we were running this async call, the content changed
// We dont want to overwrite with old data. Abort mission!
return;
}
}
this.innerHTML = html;
if (!Prism && URLs.Prism && this.querySelector("code")) {
Prism = import(URLs.Prism);
if (URLs.PrismCSS) {
let link = document.createElement("link");
link.rel = "stylesheet";
link.href = URLs.PrismCSS;
document.head.appendChild(link);
}
}
if (Prism) {
await Prism; // in case it's still loading
Prism.highlightAllUnder(this);
}
if (this.src) {
this.setAttribute("rendered", this._contentFromHTML? "fallback" : "remote");
}
else {
this.setAttribute("rendered", this._contentFromHTML? "content" : "property");
}
// Fire event
let event = new CustomEvent("md-render", {bubbles: true, composed: true});
this.dispatchEvent(event);
}
static async sanitize(html) {
if (!DOMPurify) {
DOMPurify = import(URLs.DOMPurify).then(m => m.default);
}
DOMPurify = await DOMPurify; // in case it's still loading
return DOMPurify.sanitize(html);
}
};
export class MarkdownSpan extends MarkdownElement {
constructor() {
super();
}
_parse () {
return marked.parseInline(this._mdContent);
}
static renderer = {
codespan (code) {
if (this._contentFromHTML) {
// Inline HTML code needs to be escaped to not be parsed as HTML by the browser
// This results in marked double-escaping it, so we need to unescape it
code = code.replace(/&amp;(?=[lg]t;)/g, "&");
}
else {
// Remote code may include characters that need to be escaped to be visible in HTML
code = code.replace(/</g, "&lt;");
}
return `<code>${code}</code>`;
}
}
}
export class MarkdownBlock extends MarkdownElement {
constructor() {
super();
}
get src() {
return this._src;
}
set src(value) {
this.setAttribute("src", value);
}
get hmin() {
return this._hmin || 1;
}
set hmin(value) {
this.setAttribute("hmin", value);
}
get hlinks() {
return this._hlinks ?? null;
}
set hlinks(value) {
this.setAttribute("hlinks", value);
}
_parse () {
return marked.parse(this._mdContent);
}
static renderer = Object.assign({
heading (text, level, _raw, slugger) {
level = Math.min(6, level + (this.hmin - 1));
const id = slugger.slug(text);
const hlinks = this.hlinks;
let content;
if (hlinks === null) {
// No heading links
content = text;
}
else {
content = `<a href="#${id}" class="anchor">`;
if (hlinks === "") {
// Heading content is the link
content += text + "</a>";
}
else {
// Headings are prepended with a linked symbol
content += hlinks + "</a>" + text;
}
}
return `
<h${level} id="${id}">
${content}
</h${level}>`;
},
code (code, language, escaped) {
if (this._contentFromHTML) {
// Inline HTML code needs to be escaped to not be parsed as HTML by the browser
// This results in marked double-escaping it, so we need to unescape it
code = code.replace(/&amp;(?=[lg]t;)/g, "&");
}
else {
// Remote code may include characters that need to be escaped to be visible in HTML
code = code.replace(/</g, "&lt;");
}
return `<pre class="language-${language}"><code>${code}</code></pre>`;
}
}, MarkdownSpan.renderer);
static get observedAttributes() {
return ["src", "hmin", "hlinks"];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
switch (name) {
case "src":
let url;
try {
url = new URL(newValue, location);
}
catch (e) {
return;
}
let prevSrc = this.src;
this._src = url;
if (this.src !== prevSrc) {
fetch(this.src)
.then(response => {
if (!response.ok) {
throw new Error(`Failed to fetch ${this.src}: ${response.status} ${response.statusText}`);
}
return response.text();
})
.then(text => {
this.mdContent = text;
})
.catch(e => {});
}
break;
case "hmin":
if (newValue > 0) {
this._hmin = +newValue;
this.render();
}
break;
case "hlinks":
this._hlinks = newValue;
this.render();
}
}
}
customElements.define("md-block", MarkdownBlock);
customElements.define("md-span", MarkdownSpan);