// based on basicSetup https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts const editorSrc = ` import { keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor, rectangularSelection, crosshairCursor, lineNumbers, highlightActiveLineGutter, EditorView } from '@codemirror/view' import { EditorState, Compartment, StateEffect } from '@codemirror/state' import { defaultHighlightStyle, syntaxHighlighting, indentOnInput, bracketMatching, foldGutter, foldKeymap } from '@codemirror/language' import { defaultKeymap, history, historyKeymap } from '@codemirror/commands' import { searchKeymap, highlightSelectionMatches } from '@codemirror/search' import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete' import {lintKeymap} from '@codemirror/lint' import { javascriptLanguage } from '@codemirror/lang-javascript' import { cssLanguage } from '@codemirror/lang-css' import { jsonLanguage } from '@codemirror/lang-json' import { htmlLanguage } from '@codemirror/lang-html' window.CodeMirrorBasic = { // @codemirror/view keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor, rectangularSelection, crosshairCursor, lineNumbers, highlightActiveLineGutter, EditorView, // @codemirror/state EditorState, Compartment, StateEffect, // @codemirror/language defaultHighlightStyle, syntaxHighlighting, indentOnInput, bracketMatching, foldGutter, foldKeymap, // @codemirror/commands defaultKeymap, history, historyKeymap, // @codemirror/search searchKeymap, highlightSelectionMatches, // @codemirror/autocomplete autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap, // @codemirror/lint lintKeymap, // @codemirror/lang-javascript javascriptLanguage, // @codemirror/lang-css cssLanguage, // @codemirror/lang-json jsonLanguage, // @codemirror/lang-html htmlLanguage, }` + "\n" class Builder { // any URL that supports pkg/path o pkg@version/path baseUrl = 'https://unpkg.com' scripts = { '@rollup/browser': { version: '3.20.4', path: 'dist/rollup.browser.js', sha: 'sha256-GgOznxZmgghx1a7CH09B+VmDKtziPO5tAnC5gC+/5Kw=', }, } topLevelDeps = [ "@codemirror/autocomplete", "@codemirror/commands", "@codemirror/language", "@codemirror/lint", "@codemirror/search", "@codemirror/state", "@codemirror/view", "@codemirror/lang-html", "@codemirror/lang-css", "@codemirror/lang-json", "@codemirror/lang-javascript", ] constructor() { this.downloads = [] } checkOk(resp) { if (!resp.ok) { throw new Error(`HTTP request failed: ${resp.status}`) } } async loadDep(dep) { this.scripts[dep] = {} const pkgResp = await fetch( `${this.baseUrl}/${dep}/package.json` ) this.checkOk(pkgResp) this.log(dep) const pkg = await pkgResp.json() this.scripts[dep].version = pkg.version this.scripts[dep].path = ( pkg.module ?? pkg.main ) this.downloads.push(this.getScript(dep)) const deps = Object.keys( pkg.dependencies || {} ).filter(dep => !(dep in this.scripts)) await Promise.allSettled(deps.map(dep => ( this.loadDep(dep) ))) } async sha(ab) { const hash = await crypto.subtle.digest( "SHA-256", ab ) return 'sha256-' + btoa( String.fromCharCode( ...new Uint8Array(hash) ) ) } async checkIntegrity(resp, name, script) { const blob = await resp.blob() const ab = await blob.arrayBuffer() const sha = await this.sha(ab) if (sha !== script.sha) { throw new Error( 'failed integrity check: ' + `${checkValue} !== ${script.sha}` ) } return ab } async getScript(name) { this.log('[downloading] ' + name, 'green') const script = this.scripts[name] if (script.text) { return script.text } const url = ( `${this.baseUrl}/${name}@${script.version}/` + script.path ) const resp = await fetch(url) this.checkOk(resp) if (script.sha) { const ab = await this.checkIntegrity( resp, name, script ) script.text = new TextDecoder().decode(ab) } else { script.text = await resp.text() script.sha = resp.integrity } this.log( '[downloaded] ' + url + ` [${script.text.length}]`, 'green' ) return script.text } async loadScript(name) { const text = await this.getScript(name) const s = document.createElement('script') s.text = text document.head.append(s) } get loaderPlugin() { return { name: 'loader', resolveId: async source => { if (source === 'editor.js' || source in this.scripts) { return source } }, load: async id => { if (id === 'editor.js') { this.log(`[found] editor.js`) return editorSrc } else if (id in this.scripts) { this.log(`[found] ${id}`) return this.scripts[id].text } }, } } async build() { const result = await Promise.all( this.topLevelDeps.map(dep => ( this.loadDep(dep) )) ) await Promise.all(this.downloads) await this.loadScript('@rollup/browser') const { rollup } = window.rollup const input = 'editor.js' const plugins = [this.loaderPlugin] const bundle = await rollup({input, plugins}) const {output} = await bundle.generate({format: 'es'}) this.code = output[0].code this.sha = await this.sha( new TextEncoder().encode(this.code) ) this.log(`built ${this.sha}`) const raw = new TextEncoder().encode(this.code) this.log(`${raw.byteLength} bytes`) } } class BuildView extends HTMLElement { constructor() { super() this.attachShadow({mode: 'open'}) this.shadowRoot.adoptedStyleSheets = [ this.constructor.styleSheet ] } log(message, cls = 'cyan') { const el = document.createElement('pre') el.classList.add(cls) el.innerText = message this.shadowRoot.append(el) ;(el.scrollIntoViewIfNeeded ?? el.scrollIntoView).call(el) } static get styleSheet() { if (this._styleSheet === undefined) { this._styleSheet = new CSSStyleSheet() this._styleSheet.replaceSync(this.css) } return this._styleSheet } static css = ` :host { display: flex; flex-direction: column; align-items: stretch; margin: 10px; gap: 10px; } pre { padding: 8px; border-radius: 5px; margin: 0; overflow-x: auto; } pre.cyan { background: cyan; } pre.green { background: lightgreen; } ` } class Editor extends HTMLElement { constructor() { super() this.attachShadow({mode: 'open'}) this.shadowRoot.adoptedStyleSheets = [ this.constructor.styleSheet ] } connectedCallback() { const cm = window.CodeMirrorBasic const basicSetup = [ cm.lineNumbers(), cm.highlightActiveLineGutter(), cm.highlightSpecialChars(), cm.history(), cm.foldGutter(), cm.drawSelection(), cm.dropCursor(), cm.EditorState.allowMultipleSelections.of(true), cm.indentOnInput(), cm.syntaxHighlighting( cm.defaultHighlightStyle, {fallback: true} ), cm.bracketMatching(), cm.closeBrackets(), cm.autocompletion(), cm.rectangularSelection(), cm.crosshairCursor(), cm.highlightActiveLine(), cm.highlightSelectionMatches(), cm.keymap.of([ ...cm.closeBracketsKeymap, ...cm.defaultKeymap, ...cm.searchKeymap, ...cm.historyKeymap, ...cm.foldKeymap, ...cm.completionKeymap, ...cm.lintKeymap ]), ] this.view = new cm.EditorView({ doc: '', extensions: [ basicSetup, ], root: this.shadowRoot, }) this.shadowRoot.append(this.view.dom) } static get styleSheet() { if (this._styleSheet === undefined) { this._styleSheet = new CSSStyleSheet() this._styleSheet.replaceSync(this.css) } return this._styleSheet } static css = ` :host { display: flex; flex-direction: column; align-items: stretch; height: 33vh; background-color: #fff; border: 5px solid red } :host > * { flex-grow: 1; } ` } function req(method, path, value = undefined) { return new Promise((resolve, reject) => { const ch = new MessageChannel() const port = ch.port1 port.onmessage = e => { resolve(e.data) port.close() } window.parent.postMessage( ( method === 'get' ? [method, path] : [method, path, value] ), '*', [ch.port2] ) }) } customElements.define('m-build-view', BuildView) customElements.define('m-editor', Editor) class App { constructor() { this.builder = new Builder() this.buildView = document.createElement('m-build-view') document.body.append(this.buildView) this.builder.log = this.buildView.log.bind(this.buildView) this.run() } async run() { await this.build() await this.save() await this.display() } async build() { try { await this.builder.build() const s = document.createElement('script') s.type = 'module' s.textContent = this.builder.code document.head.append(s) } catch (e) { this.buildView.log(`${e}`) } } async save() { try { const resp = await req( 'put', '/editor-lib-codemirror/codemirror-bundle.js', this.builder.code ) if (resp.status !== 200) { throw new Error(`save attempt returned ${resp.status}`) } } catch (e) { this.buildView.log(`${e}`) } } async display() { await new Promise(r => setTimeout(() => r(), 500)) this.editor = document.createElement('m-editor') document.body.append(this.editor) } } new App()