export class ZeroMd extends HTMLElement { get src () { return this.getAttribute('src') } set src (val) { this.reflect('src', val) } get manualRender () { return this.hasAttribute('manual-render') } set manualRender (val) { this.reflect('manual-render', val) } reflect (name, val) { if (val === false) { this.removeAttribute(name) } else { this.setAttribute(name, val === true ? '' : val) } } static get observedAttributes () { return ['src'] } attributeChangedCallback (name, old, val) { if (name === 'src' && this.connected && !this.manualRender && val !== old) { this.render() } } constructor (defaults) { super() this.version = '$VERSION' this.config = { markedUrl: 'https://cdn.jsdelivr.net/gh/markedjs/marked@2/marked.min.js', prismUrl: [ ['https://cdn.jsdelivr.net/gh/PrismJS/prism@1/prism.min.js', 'data-manual'], 'https://cdn.jsdelivr.net/gh/PrismJS/prism@1/plugins/autoloader/prism-autoloader.min.js' ], cssUrls: [ 'https://cdn.jsdelivr.net/gh/sindresorhus/github-markdown-css@4/github-markdown.min.css', 'https://cdn.jsdelivr.net/gh/PrismJS/prism@1/themes/prism.min.css' ], hostCss: ':host{display:block;position:relative;contain:content;}:host([hidden]){display:none;}', ...defaults, ...window.ZeroMdConfig } this.cache = {} this.root = this.hasAttribute('no-shadow') ? this : this.attachShadow({ mode: 'open' }) if (!this.constructor.ready) { this.constructor.ready = Promise.all([ !!window.marked || this.loadScript(this.config.markedUrl), !!window.Prism || this.loadScript(this.config.prismUrl) ]) } this.clicked = this.clicked.bind(this) if (!this.manualRender) { // Scroll to hash id after first render. However, `history.scrollRestoration` inteferes with this on refresh. // It's much better to use a `setTimeout` rather than to alter the browser's behaviour. this.render().then(() => setTimeout(() => this.goto(location.hash), 250)) } this.observer = new MutationObserver(async () => { this.observeChanges() if (!this.manualRender) { await this.render() } }) this.observeChanges() } connectedCallback () { this.connected = true this.fire('zero-md-connected', {}, { bubbles: false, composed: false }) this.waitForReady().then(() => { this.fire('zero-md-ready') }) if (this.shadowRoot) { this.shadowRoot.addEventListener('click', this.clicked) } } disconnectedCallback () { this.connected = false if (this.shadowRoot) { this.shadowRoot.removeEventListener('click', this.clicked) } } waitForReady () { const ready = this.connected || new Promise(resolve => { this.addEventListener('zero-md-connected', function handler () { this.removeEventListener('zero-md-connected', handler) resolve() }) }) return Promise.all([this.constructor.ready, ready]) } fire (name, detail = {}, opts = { bubbles: true, composed: true }) { if (detail.msg) { console.warn(detail.msg) } this.dispatchEvent(new CustomEvent(name, { detail: { node: this, ...detail }, ...opts })) } tick () { return new Promise(resolve => requestAnimationFrame(resolve)) } // Coerce anything into an array arrify (any) { return any ? (Array.isArray(any) ? any : [any]) : [] } // Promisify an element's onload callback onload (node) { return new Promise((resolve, reject) => { node.onload = resolve node.onerror = err => reject(err.path ? err.path[0] : err.composedPath()[0]) }) } // Load a url or load (in order) an array of urls via