You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
442 lines
9.9 KiB
JavaScript
442 lines
9.9 KiB
JavaScript
// 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() |