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

// 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()