mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 04:51:17 +00:00
223 lines
5.6 KiB
TypeScript
223 lines
5.6 KiB
TypeScript
import { indentWithTab } from "@codemirror/commands";
|
|
import { html as htmlLang } from "@codemirror/lang-html";
|
|
import { xml } from "@codemirror/lang-xml";
|
|
import {
|
|
defaultHighlightStyle,
|
|
syntaxHighlighting,
|
|
} from "@codemirror/language";
|
|
import { Compartment, EditorState, Extension } from "@codemirror/state";
|
|
import { keymap, ViewUpdate } from "@codemirror/view";
|
|
import { basicSetup, EditorView } from "codemirror";
|
|
import { prettify as prettifyHTML, minify as minifyHTML } from "htmlfy";
|
|
import { css, html, LitElement, TemplateResult } from "lit";
|
|
import {
|
|
customElement,
|
|
property,
|
|
queryAssignedNodes,
|
|
state,
|
|
} from "lit/decorators.js";
|
|
import xmlFormat from "xml-formatter";
|
|
|
|
const language = new Compartment();
|
|
|
|
@customElement("code-editor")
|
|
export class XMLEditor extends LitElement {
|
|
@queryAssignedNodes({ slot: "textarea" })
|
|
_textarea!: NodeListOf<HTMLTextAreaElement>;
|
|
|
|
@property()
|
|
lang = "html";
|
|
|
|
@state()
|
|
editorState!: EditorState;
|
|
|
|
@state()
|
|
editorView!: EditorView;
|
|
|
|
_textareaEvents = [
|
|
{
|
|
events: ["focus", "invalid"],
|
|
onEvent: (_: Event, editor: EditorView) => {
|
|
// focus editor when textarea is focused or invalid
|
|
editor.focus();
|
|
},
|
|
},
|
|
];
|
|
|
|
firstUpdated(): void {
|
|
const minHeightEditor = EditorView.baseTheme({
|
|
".cm-content, .cm-gutter": {
|
|
minHeight: this._textarea[0].clientHeight + "px",
|
|
},
|
|
});
|
|
|
|
const extensions: Extension[] = [
|
|
basicSetup,
|
|
keymap.of([indentWithTab]),
|
|
minHeightEditor,
|
|
syntaxHighlighting(defaultHighlightStyle),
|
|
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
|
if (viewUpdate.docChanged) {
|
|
// Document changed, minify and update textarea value
|
|
switch (this.lang) {
|
|
case "xml":
|
|
this._textarea[0].value = minifyXML(
|
|
viewUpdate.state.doc.toString()
|
|
);
|
|
break;
|
|
case "html":
|
|
this._textarea[0].value = minifyHTML(
|
|
viewUpdate.state.doc.toString()
|
|
);
|
|
break;
|
|
default:
|
|
this._textarea[0].value = viewUpdate.state.doc.toString();
|
|
break;
|
|
}
|
|
}
|
|
}),
|
|
];
|
|
|
|
let editorContents = "";
|
|
switch (this.lang) {
|
|
case "xml":
|
|
editorContents = formatXML(this._textarea[0].value);
|
|
extensions.push(language.of(xml()));
|
|
break;
|
|
case "html":
|
|
editorContents = prettifyHTML(this._textarea[0].value);
|
|
extensions.push(language.of(htmlLang()));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
this.editorState = EditorState.create({
|
|
doc: editorContents,
|
|
extensions: extensions,
|
|
});
|
|
|
|
this.editorView = new EditorView({
|
|
state: this.editorState,
|
|
root: this.shadowRoot as ShadowRoot,
|
|
parent: this.shadowRoot as ShadowRoot,
|
|
});
|
|
|
|
// hide textarea
|
|
this._textarea[0].style.position = "absolute";
|
|
this._textarea[0].style.opacity = "0";
|
|
this._textarea[0].style.zIndex = "-9999";
|
|
this._textarea[0].style.pointerEvents = "none";
|
|
|
|
for (const event of this._textareaEvents) {
|
|
event.events.forEach((name) => {
|
|
this._textarea[0].addEventListener(name, (e) =>
|
|
event.onEvent(e, this.editorView)
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
disconnectedCallback(): void {
|
|
super.disconnectedCallback();
|
|
|
|
for (const event of this._textareaEvents) {
|
|
event.events.forEach((name) => {
|
|
this._textarea[0].removeEventListener(name, (e) =>
|
|
event.onEvent(e, this.editorView)
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
static styles = css`
|
|
.cm-editor {
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
border: 3px solid hsl(var(--color-border-contrast));
|
|
background-color: hsl(var(--color-background-elevated));
|
|
}
|
|
.cm-editor.cm-focused {
|
|
outline: 2px solid transparent;
|
|
box-shadow:
|
|
0 0 0 2px hsl(var(--color-background-elevated)),
|
|
0 0 0 calc(4px) hsl(var(--color-accent-base));
|
|
}
|
|
.cm-gutters {
|
|
background-color: hsl(var(--color-background-elevated)) !important;
|
|
}
|
|
|
|
.cm-activeLine {
|
|
background-color: hsla(
|
|
var(--color-background-highlight) / 0.25
|
|
) !important;
|
|
}
|
|
|
|
.cm-activeLineGutter {
|
|
background-color: hsl(var(--color-background-highlight)) !important;
|
|
}
|
|
|
|
.ͼ4 .cm-line {
|
|
caret-color: hsl(var(--color-text-base)) !important;
|
|
}
|
|
|
|
.ͼ1 .cm-cursor {
|
|
border: none;
|
|
}
|
|
`;
|
|
|
|
render(): TemplateResult<1> {
|
|
return html`<slot name="textarea"></slot>`;
|
|
}
|
|
}
|
|
|
|
function formatXML(contents: string) {
|
|
if (contents === "") {
|
|
return contents;
|
|
}
|
|
|
|
let editorContents = "";
|
|
try {
|
|
editorContents = xmlFormat(contents, {
|
|
indentation: " ",
|
|
});
|
|
} catch {
|
|
// xml doesn't have a root node
|
|
editorContents = xmlFormat("<root>" + contents + "</root>", {
|
|
indentation: " ",
|
|
});
|
|
// remove root, unnecessary lines and indents
|
|
editorContents = editorContents
|
|
.replace(/^<root>/, "")
|
|
.replace(/<\/root>$/, "")
|
|
.replace(/^\s*[\r\n]/gm, "")
|
|
.replace(/[\r\n] {2}/gm, "\r\n")
|
|
.trim();
|
|
}
|
|
|
|
return editorContents;
|
|
}
|
|
|
|
function minifyXML(contents: string) {
|
|
if (contents === "") {
|
|
return contents;
|
|
}
|
|
|
|
let minifiedContent = "";
|
|
try {
|
|
minifiedContent = xmlFormat.minify(contents, {
|
|
collapseContent: true,
|
|
});
|
|
} catch {
|
|
minifiedContent = xmlFormat.minify(`<root>${contents}</root>`, {
|
|
collapseContent: true,
|
|
});
|
|
// remove root
|
|
minifiedContent = minifiedContent
|
|
.replace(/^<root>/, "")
|
|
.replace(/<\/root>$/, "");
|
|
}
|
|
|
|
return minifiedContent;
|
|
}
|