diff --git a/AGENTS.md b/AGENTS.md index 352bf4088..e722bbe53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,6 +69,9 @@ The repository contains: - Preserve the existing visual and API style of sibling packages instead of inventing a new pattern. - When touching demo examples, make sure the example still works in `http://localhost:8082`. - Do not hand-edit `dist/`, `docs/compiled/`, or `docs/uncompiled/` unless a build step generated them. +- Desktop editor sources live in `packages/artplayer-vitepress/browser/`; `docs/assets/js/common.js`, + `bootstrap.js`, `loader.js`, `mobile.js` and `packages/artplayer-vitepress/docs/public/main.js` + are generated with `yarn build:site-assets`. `editor-libraries.ts` is generated by `yarn build:ts`. ## Primary Commands diff --git a/docs/assets/js/bootstrap.js b/docs/assets/js/bootstrap.js new file mode 100644 index 000000000..b42d90fe3 --- /dev/null +++ b/docs/assets/js/bootstrap.js @@ -0,0 +1,42 @@ +// Generated by yarn build:site-assets. Do not edit. +"use strict"; +(() => { + // packages/artplayer-vitepress/browser/editor-preferences.ts + function readPreference(key, storage = () => localStorage) { + try { + return storage().getItem(key) === "true"; + } catch { + return false; + } + } + + // packages/artplayer-vitepress/browser/bootstrap.ts + function loadScript(url) { + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + const clear = () => { + script.onload = null; + script.onerror = null; + }; + script.onload = () => { + clear(); + resolve(); + }; + script.onerror = () => { + clear(); + script.remove(); + reject(new Error(`Editor bootstrap failed: ${url}`)); + }; + script.src = url; + document.body.appendChild(script); + }); + } + async function bootstrap() { + await loadScript( + readPreference("prod") ? "./compiled/artplayer.js" : "./uncompiled/artplayer/index.js" + ); + await loadScript("./assets/js/vs/loader.js"); + await loadScript("./assets/js/common.js"); + } + void bootstrap().catch((error) => console.error(error)); +})(); diff --git a/docs/assets/js/common.js b/docs/assets/js/common.js index d092642b4..94c52ff28 100644 --- a/docs/assets/js/common.js +++ b/docs/assets/js/common.js @@ -1,245 +1,416 @@ -/* global Artplayer, monaco */ -(function () { - Artplayer.DEBUG = true - - if (Artplayer.utils.isMobile) { - window.location.href = `./mobile.html${location.search}` +// Generated by yarn build:site-assets. Do not edit. +"use strict"; +(() => { + // packages/artplayer-vitepress/browser/editor-files.ts + function readEditorFile(file, signal, createReader = () => new FileReader()) { + return new Promise((resolve, reject) => { + const reader = createReader(); + function clear() { + reader.onload = null; + reader.onerror = null; + reader.onabort = null; + signal.removeEventListener("abort", cancel); + } + function fail(error) { + clear(); + reject(error); + } + function cancel() { + reader.abort(); + fail(new Error(`File import cancelled: ${file.name}`)); + } + reader.onload = () => { + if (typeof reader.result !== "string") { + fail(new Error(`File is not text: ${file.name}`)); + return; + } + const value = reader.result; + clear(); + resolve(value); + }; + reader.onerror = () => fail(reader.error || new Error(`Reading file failed: ${file.name}`)); + reader.onabort = () => fail(new Error(`File import cancelled: ${file.name}`)); + signal.addEventListener("abort", cancel, { once: true }); + try { + if (signal.aborted) + cancel(); + else reader.readAsText(file); + } catch (error) { + fail(error); + } + }); } - - const $codeMirror = document.querySelector('.codeMirrorWrap') - const $lib = document.querySelector('.libsInput') - const $run = document.querySelector('.run') - const $popups = document.querySelector('.popups') - const $console = document.querySelector('.console') - const $prod = document.querySelector('#prod') - const $ts = document.querySelector('#ts') - const $code = document.querySelector('#code') - const $log = document.querySelector('#log') - const $file = document.querySelector('#file') - const $editor = document.querySelector('#editor') - - window.consoleLog($console) - - // Helper functions for localStorage - const getStorageBoolean = key => localStorage.getItem(key) === 'true' - const setStorageBoolean = (key, value) => localStorage.setItem(key, value ? 'true' : 'false') - - $prod.checked = getStorageBoolean('prod') - $ts.checked = getStorageBoolean('ts') - $code.checked = getStorageBoolean('code') - $log.checked = getStorageBoolean('log') - - if ($code.checked) { - $editor.style.display = 'none' - } - - if ($log.checked) { - $console.style.display = 'none' - } - - let editor = null - let initialization = 0 - require.config({ paths: { vs: './assets/js/vs' } }) - require(['vs/editor/editor.main'], async () => { - monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ - noSemanticValidation: true, - noSyntaxValidation: false, - }) - - monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ - target: monaco.languages.typescript.ScriptTarget.ES6, - allowNonTsExtensions: true, - }) - - const libUris = [ - './assets/ts/artplayer-plugin-ads.d.ts', - './assets/ts/artplayer-plugin-ambilight.d.ts', - './assets/ts/artplayer-plugin-asr.d.ts', - './assets/ts/artplayer-plugin-audio-track.d.ts', - './assets/ts/artplayer-plugin-auto-thumbnail.d.ts', - './assets/ts/artplayer-plugin-chapter.d.ts', - './assets/ts/artplayer-plugin-chromecast.d.ts', - './assets/ts/artplayer-plugin-danmuku-mask.d.ts', - './assets/ts/artplayer-plugin-danmuku.d.ts', - './assets/ts/artplayer-plugin-dash-control.d.ts', - './assets/ts/artplayer-plugin-document-pip.d.ts', - './assets/ts/artplayer-plugin-hls-control.d.ts', - './assets/ts/artplayer-plugin-jassub.d.ts', - './assets/ts/artplayer-plugin-multiple-subtitles.d.ts', - './assets/ts/artplayer-plugin-vast.d.ts', - './assets/ts/artplayer-plugin-vtt-thumbnail.d.ts', - './assets/ts/artplayer-proxy-canvas.d.ts', - './assets/ts/artplayer-proxy-mediabunny.d.ts', - './assets/ts/artplayer-tool-iframe.d.ts', - './assets/ts/artplayer-tool-thumbnail.d.ts', - './assets/ts/artplayer.d.ts', - './assets/ts/artplayer-i18n.d.ts', - ] - - for (let index = 0; index < libUris.length; index++) { - const libUri = libUris[index] - const libSource = await (await fetch(libUri)).text() - monaco.languages.typescript.javascriptDefaults.addExtraLib(libSource, libUri) - monaco.editor.createModel(libSource, 'typescript', monaco.Uri.parse(libUri)) - } - - const disposable = monaco.editor.onDidCreateEditor(() => { - disposable.dispose() - setTimeout(initApp, 1000) - }) - - editor = monaco.editor.create($codeMirror, { - theme: 'vs-dark', - folding: true, - automaticLayout: true, - quickSuggestions: { - other: true, - comments: true, - strings: true, + function createFileImports(document, label) { + const controller = new AbortController(); + const elements = []; + let queue = Promise.resolve(); + return { + add(files) { + const work = queue.then(async () => { + for (const file of files) { + const name = file.name.toLowerCase().trim(); + if (!name.endsWith(".js") && !name.endsWith(".css")) + continue; + const text = await readEditorFile(file, controller.signal); + if (controller.signal.aborted) + return; + const element = document.createElement( + name.endsWith(".css") ? "style" : "script" + ); + element.textContent = text; + document.body.appendChild(element); + elements.push(element); + label(name); + } + }); + queue = work.catch(() => { + }); + return work; }, - model: monaco.editor.createModel( + dispose() { + controller.abort(); + for (const element of elements) element.remove(); + elements.length = 0; + } + }; + } + + // packages/artplayer-vitepress/browser/editor-libraries.ts + var libUris = [ + "./assets/ts/artplayer-plugin-ads.d.ts", + "./assets/ts/artplayer-plugin-ambilight.d.ts", + "./assets/ts/artplayer-plugin-asr.d.ts", + "./assets/ts/artplayer-plugin-audio-track.d.ts", + "./assets/ts/artplayer-plugin-auto-thumbnail.d.ts", + "./assets/ts/artplayer-plugin-chapter.d.ts", + "./assets/ts/artplayer-plugin-chromecast.d.ts", + "./assets/ts/artplayer-plugin-danmuku-mask.d.ts", + "./assets/ts/artplayer-plugin-danmuku.d.ts", + "./assets/ts/artplayer-plugin-dash-control.d.ts", + "./assets/ts/artplayer-plugin-document-pip.d.ts", + "./assets/ts/artplayer-plugin-hls-control.d.ts", + "./assets/ts/artplayer-plugin-jassub.d.ts", + "./assets/ts/artplayer-plugin-multiple-subtitles.d.ts", + "./assets/ts/artplayer-plugin-vast.d.ts", + "./assets/ts/artplayer-plugin-vtt-thumbnail.d.ts", + "./assets/ts/artplayer-proxy-canvas.d.ts", + "./assets/ts/artplayer-proxy-mediabunny.d.ts", + "./assets/ts/artplayer-tool-iframe.d.ts", + "./assets/ts/artplayer-tool-thumbnail.d.ts", + "./assets/ts/artplayer.d.ts", + "./assets/ts/artplayer-i18n.d.ts" + ]; + + // packages/artplayer-vitepress/browser/editor-monaco.ts + async function createMonaco(host, container, typescript, signal) { + host.require.config({ paths: { vs: "./assets/js/vs" } }); + await new Promise( + (resolve, reject) => host.require(["vs/editor/editor.main"], resolve, reject) + ); + await new Promise( + (resolve, reject) => host.require( [ - 'var art = new Artplayer({', - '\tcontainer: \'.artplayer-app\',', - '\turl: \'/assets/sample/video.mp4\',', - '});', - ].join('\n'), - $ts.checked ? 'typescript' : 'javascript', - ), - }) - }) - - async function loadLib(libs) { - $lib.value = decodeURIComponent(libs || '') - return window.ArtplayerDocsLoader.loadLibraries(libs) - } - - async function runExample(name, current) { - const text = await window.ArtplayerDocsLoader.exampleSource(name) - if (current !== initialization) - return - editor.setValue(text) - runCode() - } - - function loadCode(code, example, current) { - if (current !== initialization) - return - if (example) - return runExample(example, current) - if (code) { - editor.setValue(decodeURIComponent(code).trim()) - runCode() - } - else { - return runExample('index', current) - } - } - - function runCode() { - window.dispatchEvent(new Event('artplayer:example:cleanup')) - // Destroy all existing instances - Artplayer.instances.slice().forEach(art => art.destroy(true)) - - const value = editor.getValue() - // eslint-disable-next-line no-eval -- Preserve the editor's classic-script Run scope for user-provided examples. - eval(value) - window.art = Artplayer.instances[0] - } - - function initApp() { - const current = ++initialization - const urlParams = window.ArtplayerDocsLoader.parameters(window.location.href) - const { code, libs, example } = urlParams - - loadLib(libs) - .then(() => loadCode(code, example, current)) - .catch((err) => { - console.error('Failed to initialize app:', err) - }) - } - - function restart() { - const libs = encodeURIComponent($lib.value) - const code = encodeURIComponent(editor.getValue()) - const url = `${window.location.origin}${window.location.pathname}?libs=${libs}&code=${code}` - history.pushState(null, null, url) - initApp() - } - - function readFile(file) { - return new Promise((resolve) => { - const reader = new FileReader() - reader.onload = () => resolve(reader.result) - reader.readAsText(file) - }) - } - - $run.addEventListener('click', () => { - restart() - }) - - $popups.addEventListener('click', function (event) { - if (event.target === this) { - this.style.display = 'none' - } - }) - - $prod.addEventListener('change', () => { - setStorageBoolean('prod', $prod.checked) - window.location.reload() - }) - - $ts.addEventListener('change', () => { - setStorageBoolean('ts', $ts.checked) - window.location.reload() - }) - - $code.addEventListener('change', () => { - setStorageBoolean('code', $code.checked) - window.location.reload() - }) - - $log.addEventListener('change', () => { - setStorageBoolean('log', $log.checked) - window.location.reload() - }) - - $file.addEventListener('change', async () => { - for (let index = 0; index < $file.files.length; index++) { - const file = $file.files[index] - const name = file.name.toLowerCase().trim() - if (name.endsWith('.css')) { - const text = await readFile(file) - const $style = document.createElement('style') - $style.textContent = text - document.body.appendChild($style) - $lib.value = `\n[${name}]` - $lib.value = $lib.value.trim() + "vs/basic-languages/typescript/typescript", + "vs/basic-languages/javascript/javascript", + "vs/language/typescript/tsMode" + ], + resolve, + reject + ) + ); + if (signal.aborted) + throw new Error("Editor initialization cancelled"); + const api = host.monaco; + const types = api.languages.typescript; + const resources = []; + const temporary = /* @__PURE__ */ new Set(); + let editor; + let disposed = false; + const dispose = () => { + if (disposed) + return; + disposed = true; + signal.removeEventListener("abort", dispose); + editor?.dispose(); + editor = void 0; + for (const model of temporary) model.dispose(); + temporary.clear(); + for (const resource of resources.splice(0).reverse()) resource.dispose(); + }; + signal.addEventListener("abort", dispose, { once: true }); + try { + for (const defaults of [ + types.javascriptDefaults, + types.typescriptDefaults + ]) { + defaults.setDiagnosticsOptions({ + noSemanticValidation: defaults === types.javascriptDefaults, + noSyntaxValidation: false + }); + defaults.setCompilerOptions({ + target: types.ScriptTarget.ES2015, + module: types.ModuleKind.None, + allowNonTsExtensions: true, + noEmit: false + }); } - if (name.endsWith('.js')) { - const text = await readFile(file) - const $script = document.createElement('script') - $script.textContent = text - document.body.appendChild($script) - $lib.value = `\n[${name}]` - $lib.value = $lib.value.trim() + for (const uri of libUris) { + const response = await host.fetch(uri, { signal }); + if (!response.ok) { + throw new Error( + `Loading editor declaration failed: ${uri} (${response.status})` + ); + } + const source = await response.text(); + if (signal.aborted) + throw new Error("Editor initialization cancelled"); + resources.push( + types.javascriptDefaults.addExtraLib(source, uri), + types.typescriptDefaults.addExtraLib(source, uri) + ); + resources.push( + api.editor.createModel(source, "typescript", api.Uri.parse(uri)) + ); } + const model = api.editor.createModel( + "var art = new Artplayer({\n container: '.artplayer-app',\n url: '/assets/sample/video.mp4',\n});", + typescript ? "typescript" : "javascript" + ); + resources.push(model); + editor = api.editor.create(container, { + theme: "vs-dark", + folding: true, + automaticLayout: true, + quickSuggestions: { other: true, comments: true, strings: true }, + model + }); + return { + editor, + async compile(source) { + if (disposed) + throw new Error("Editor was disposed"); + if (!typescript) + return source; + const model2 = api.editor.createModel(source, "typescript"); + temporary.add(model2); + try { + const worker = await (await types.getTypeScriptWorker())(model2.uri); + const file = model2.uri.toString(); + const syntax = await worker.getSyntacticDiagnostics(file); + if (syntax.length) { + throw new Error( + `TypeScript syntax error: ${syntax.map((item) => typeof item.messageText === "string" ? item.messageText : item.messageText.messageText).join("; ")}` + ); + } + const output = await worker.getEmitOutput(file); + const script = output.outputFiles.find( + (file2) => file2.name.endsWith(".js") + ); + if (output.emitSkipped || !script) + throw new Error("TypeScript worker did not emit JavaScript"); + return script.text; + } finally { + temporary.delete(model2); + if (!model2.isDisposed()) + model2.dispose(); + } + }, + dispose + }; + } catch (error) { + dispose(); + throw error; } - }) + } - window.addEventListener('error', (err) => { - console.error(err) - }) - - window.addEventListener('unhandledrejection', (err) => { - console.error(err) - }) - - document.addEventListener('keydown', (event) => { - if ((event.ctrlKey || event.metaKey) && event.key === 's') { - event.preventDefault() - restart() + // packages/artplayer-vitepress/browser/editor-preferences.ts + function readPreference(key, storage = () => localStorage) { + try { + return storage().getItem(key) === "true"; + } catch { + return false; } - }) -})() + } + function writePreference(key, value, storage = () => localStorage) { + try { + storage().setItem(key, value ? "true" : "false"); + return true; + } catch { + return false; + } + } + + // packages/artplayer-vitepress/browser/editor-session.ts + function createEditorSession(options) { + let generation = 0; + let disposed = false; + return { + async run({ + libs = "", + code, + example + }) { + const current = ++generation; + const active = () => !disposed && current === generation; + if (!active()) + return; + await options.loadLibraries(libs); + if (!active()) + return; + const source = example ? await options.exampleSource(example) : code ? decodeURIComponent(code).trim() : await options.exampleSource("index"); + if (!active()) + return; + options.writeCode(source); + const executable = await options.compile(source); + if (active()) + options.execute(executable); + }, + dispose() { + disposed = true; + generation++; + } + }; + } + function executeClassic(code) { + new Function(code)(); + } + + // packages/artplayer-vitepress/browser/editor.ts + async function installEditor(host) { + const document = host.document; + const element = (selector) => { + const node = document.querySelector(selector); + if (!node) + throw new Error(`Missing editor element: ${selector}`); + return node; + }; + host.Artplayer.DEBUG = true; + if (host.Artplayer.utils.isMobile) { + host.location.href = `./mobile.html${host.location.search}`; + return; + } + const run = element(".run"); + const libs = element(".libsInput"); + const file = element("#file"); + const popup = element(".popups"); + const output = element(".console"); + host.consoleLog(output); + const controller = new AbortController(); + const cleanups = []; + const listen = (target, type, callback) => { + target.addEventListener(type, callback); + cleanups.push(() => target.removeEventListener(type, callback)); + }; + const report = (error) => { + if (!controller.signal.aborted) + host.console.error(error); + }; + listen(host, "error", (event) => host.console.error(event)); + listen(host, "unhandledrejection", (event) => host.console.error(event)); + for (const key of ["prod", "ts", "code", "log"]) { + const checkbox = element(`#${key}`); + checkbox.checked = readPreference(key, () => host.localStorage); + listen(checkbox, "change", () => { + if (writePreference(key, checkbox.checked, () => host.localStorage)) { + host.location.reload(); + } else { + checkbox.checked = readPreference(key, () => host.localStorage); + report( + new Error("Browser storage is unavailable; preference was not saved") + ); + } + }); + } + if (element("#code").checked) + element("#editor").style.display = "none"; + if (element("#log").checked) + output.style.display = "none"; + const imports = createFileImports(document, (name) => { + libs.value = `[${name}]`; + }); + cleanups.push(() => imports.dispose()); + cleanups.push(() => { + host.dispatchEvent(new Event("artplayer:example:cleanup")); + for (const art of host.Artplayer.instances.slice()) { + try { + art.destroy(true); + } catch (error) { + host.console.error(error); + } + } + host.art = void 0; + }); + listen(file, "change", () => { + void imports.add(Array.from(file.files || [])).catch(report); + }); + listen(popup, "click", (event) => { + if (event.target === popup) + popup.style.display = "none"; + }); + const onPageHide = (event) => { + if (event.persisted) + return; + controller.abort(); + for (const cleanup of cleanups.reverse()) { + try { + cleanup(); + } catch (error) { + host.console.error(error); + } + } + }; + host.addEventListener("pagehide", onPageHide); + cleanups.push(() => host.removeEventListener("pagehide", onPageHide)); + run.setAttribute("aria-disabled", "true"); + try { + let restart2 = function() { + const code = encodeURIComponent(monaco.editor.getValue()); + const dependencies = encodeURIComponent(libs.value); + host.history.pushState( + null, + "", + `${host.location.origin}${host.location.pathname}?libs=${dependencies}&code=${code}` + ); + void session.run({ libs: dependencies, code }).catch(report); + }; + var restart = restart2; + const monaco = await createMonaco( + host, + element(".codeMirrorWrap"), + element("#ts").checked, + controller.signal + ); + const session = createEditorSession({ + loadLibraries: (encoded) => { + libs.value = decodeURIComponent(encoded); + return host.ArtplayerDocsLoader.loadLibraries(encoded); + }, + exampleSource: (name) => host.ArtplayerDocsLoader.exampleSource(name), + writeCode: (code) => monaco.editor.setValue(code), + compile: monaco.compile, + execute: (code) => { + host.dispatchEvent(new Event("artplayer:example:cleanup")); + for (const art of host.Artplayer.instances.slice()) art.destroy(true); + executeClassic(code); + host.art = host.Artplayer.instances[0]; + } + }); + cleanups.push(() => session.dispose(), monaco.dispose); + listen(run, "click", restart2); + listen(document, "keydown", (event) => { + const key = event; + if ((key.ctrlKey || key.metaKey) && key.key === "s") { + key.preventDefault(); + restart2(); + } + }); + run.removeAttribute("aria-disabled"); + await session.run(host.ArtplayerDocsLoader.parameters(host.location.href)); + } catch (error) { + report(error); + } + } + void installEditor(window).catch( + (error) => console.error(error) + ); +})(); diff --git a/docs/index.html b/docs/index.html index 84f291249..534cce34c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -230,29 +230,7 @@ - +