static init (container) {
		if (!container.demo) {
			container.demo = new LiveDemo(container);
		}

		return container.demo;
	}
/";
				}

				if (this.style.sheet) {
					let scope = this.editors.css.textarea.getAttribute("data-scope") || `#${this.container.id} .demo-target `;

					for (let rule of this.style.sheet.cssRules) {
						LiveDemo.scopeRule(rule, this.container, scope);
					}
				}
				else {
					console.log("FAIL on", this.container.id, this.style.outerHTML, this.style.media);
				}
			}
		}
	}

	updateIframe() {
		this.iframe.srcdoc = this.getHTMLPage();

		return new Promise(resolve => {
			this.iframe.onload = resolve;
		}).then(evt => {
			this.style = $("style#live", this.iframe.contentDocument);
			return evt;
		});
	}

	get html() {
		if (!this.editors.markup) {
			// No HTML editor
			return "";
		}

		var editor = this.editors.markup.source;

		if (editor) {
			let prepend = editor.dataset.prepend? editor.dataset.prepend + "\n" : "";
			let append = editor.dataset.append? "\n" + editor.dataset.append : "";
			return `${prepend}${editor.value}${append}`;
		}
		else {
			return this.element.innerHTML;
		}
	}

	get css() {
		return this.editors.css?.value;
	}

	get js() {
		return (this.editors.js || this.editors.javascript)?.value;
	}

	get title() {
		return (this.container.title || this.container.dataset.title || "") + " Demo";
	}

	getHTMLPage({title, inline} = {}) {
		return LiveDemo.getHTMLPage({
			html: this.html,
			css: this.css,
			extraCSS: this.extraCSS,
			baseCSS: this.baseCSS,
			js: this.js,
			title: title || this.title,
			inline,
			noBase: this.noBase,
			module: /^\s*import/m.test(this.js),
		});
	}

	openEditor(id) {
		for (let i in this.editors) {
			this.editors[i].wrapper.classList.toggle("collapsed", i !== id);
		}
	}

	async play (script) {
		if (!this.replayer) {
			let Replayer = await import("https://rety.verou.me/src/replayer.js").then(m => m.default);
			// let editors = Object.fromEntries(Object.entries(this.editors).map(([id, editor]) => [id, editor.textarea]));
			let editors = Object.values(this.editors).map(editor => editor.textarea);

			this.replayer = new Replayer(editors, this.replayerOptions);
		}

		return this.replayer.runAll(script);
	}

	static createEditor(container, label, o = {}) {
		var lang = o.lang || label;
		let textarea = create({
			html: `<textarea id="${container.id}-${label}-editor" class="language-${lang} editor" data-lang="${lang}">${o.fromSource()}</textarea>`,
			in: o.container || container,
			value: o.fromSource(),
			events: {
				input: o.toSource
			}
		});

		return new Prism.Live(textarea);
	}

	static createURL(html, useDataURI, type = "text/html") {
		html = html.replace(/&#x200b;/g, "");

		if (useDataURI) {
			return `data:${type},${encodeURIComponent(html)}`;
		}
		else {
			return URL.createObjectURL(new Blob([html], {type}));
		}
	}

	static scopeRule(rule, container, scope) {
		let selector = rule.selectorText;

		if (rule.cssRules) {
			// If this rule contains rules, scope those too
			// Mainly useful for @supports and @media
			for (let innerRule of rule.cssRules) {
				LiveDemo.scopeRule(innerRule, container, scope);
			}
		}

		if (selector && rule instanceof CSSStyleRule) {
			let shouldScope = !(
				selector.includes("#")  // don't do anything if the selector already contains an id
				|| [":root", "body"].includes(selector) // don't scope these
			);

			let env = {context: this, rule, container, scope, shouldScope};
			LiveDemo.hooks.run("scoperule", env);

			if ("returnValue" in env) {
				return env.returnValue;
			}

			if (env.shouldScope && selector.indexOf(scope) !== 0) {
				rule.selectorText = selector.split(",").map(s => `${scope} ${s}`).join(", ");
			}
		}
	}

	static getHTMLPage ({html="", css="", baseCSS = LiveDemo.baseCSS, extraCSS="", js="", title="Demo", inline = true, noBase = false, module = false} = {}) {

		baseCSS = baseCSS + (baseCSS && extraCSS? "\n" : "") + extraCSS;

		// Hoist imports to top
		let imports = [];
		baseCSS.replace(/@import .+;/g, $0 => {
			imports.push($0);
			return "";
		});

		if (imports.length > 0) {
			baseCSS = [...imports, baseCSS].join("\n");
		}

		if (css !== "undefined") {
			css = `<style id=live>
${css}
</style>`;
		}

		return `<!DOCTYPE html>
<html lang="en">
<head>
${noBase? "" : `<base href="${location.href}" />`}
<meta charset="UTF-8">
<title>${title}</title>
<style>
/* Base styles, not related to demo