Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions src/main/frontend/app/routes/editor/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Editor, { type Monaco, type OnMount } from '@monaco-editor/react'
import Editor, { type Monaco, type OnMount, type BeforeMount } from '@monaco-editor/react'
import XsdManager from 'monaco-xsd-code-completion/esm/XsdManager'
import XsdFeatures from 'monaco-xsd-code-completion/esm/XsdFeatures'
import 'monaco-xsd-code-completion/src/style.css'
Expand Down Expand Up @@ -47,6 +47,54 @@ const SAVED_DISPLAY_DURATION = 2000
const ELEMENT_ERROR_RE = /[Ee]lement [\u2018\u2019'"'{]?([\w:.-]+)[\u2018\u2019'"'}]?/
const ATTRIBUTE_ERROR_RE = /[Aa]ttribute [\u2018\u2019'"'{]?([\w:.-]+)[\u2018\u2019'"'}]?/

const XML_MONARCH_GRAMMAR = {
defaultToken: '',
tokenPostfix: '.xml',
tokenizer: {
root: [
[/[^<&]+/, ''],
[/&\w+;/, 'string.escape'],
[/<!\[CDATA\[/, { token: 'delimiter.cdata', next: '@cdata' }],
[/<!--/, { token: 'comment', next: '@comment' }],
[/(<\?)(\w[\w\d.]*)/, [{ token: 'delimiter' }, { token: 'metatag', next: '@xmlDecl' }]],
[/(<\/)(\w[\w\d.:_-]*)/, [{ token: 'delimiter' }, { token: 'tag', next: '@tag' }]],
[/(<)(\w[\w\d.:_-]*)/, [{ token: 'delimiter' }, { token: 'tag', next: '@tag' }]],
[/</, { token: 'delimiter' }],
],
cdata: [
[/[^\]]+/, ''],
[/\]\]>/, { token: 'delimiter.cdata', next: '@pop' }],
[/\]/, ''],
],
xmlDecl: [
[/\s+/, ''],
[/[\w:-]+/, 'attribute.name'],
[/=/, 'delimiter'],
[/"[^"]*"/, 'attribute.value'],
[/'[^']*'/, 'attribute.value'],
[/\?>/, { token: 'delimiter', next: '@pop' }],
],
tag: [
[/\s+/, ''],
// flow: namespace attributes — literal regexes, matched before generic rules
[/((?:xmlns:flow|flow:[\w-]+))(\s*=\s*(?:"[^"]*"|'[^']*'))/, ['flow-attribute', 'flow-attribute-value']],
[/(?:xmlns:flow|flow:[\w-]+)/, 'flow-attribute'],
// Regular attributes
[/([\w.:_-]+)(\s*=\s*(?:"[^"]*"|'[^']*'))/, ['attribute.name', 'attribute.value']],
[/[\w.:_-]+/, 'attribute.name'],
[/=/, 'delimiter'],
[/\/>/, { token: 'delimiter', next: '@pop' }],
[/>/, { token: 'delimiter', next: '@pop' }],
],
comment: [
[/-->/, { token: 'comment', next: '@pop' }],
[/[^-]+/, 'comment'],
[/--/, 'comment'],
[/./, 'comment'],
],
},
Comment on lines +53 to +95
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?
Isn't the default XML highlighter enough?
This seems like a lot of manual logic to do what should either already be done through monaco's highlighting itself or adds something custom that normal editors usually don't have either

}

function extractLocalName(name: string): string {
return name.includes(':') ? name.split(':').pop()! : name
}
Expand Down Expand Up @@ -314,11 +362,40 @@ export default function CodeEditor() {
.catch(console.error)
}, [editorMounted])

const handleEditorBeforeMount: BeforeMount = (monacoInstance) => {
monacoInstance.languages.setMonarchTokensProvider(
'xml',
XML_MONARCH_GRAMMAR as Parameters<typeof monacoInstance.languages.setMonarchTokensProvider>[1],
)

// Use non-conflicting names — 'vs-dark' is a Monaco built-in and can't safely be overridden
monacoInstance.editor.defineTheme('flow-vs-light', {
base: 'vs',
inherit: true,
rules: [
{ token: 'flow-attribute.xml', foreground: 'a8a8a8', fontStyle: 'italic' },
{ token: 'flow-attribute-value.xml', foreground: 'a8a8a8', fontStyle: 'italic' },
],
colors: {},
})
monacoInstance.editor.defineTheme('flow-vs-dark', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'flow-attribute.xml', foreground: '808080', fontStyle: 'italic' },
{ token: 'flow-attribute-value.xml', foreground: '808080', fontStyle: 'italic' },
],
colors: {},
})
}

const handleEditorMount: OnMount = (editor, monacoInstance) => {
editorReference.current = editor
monacoReference.current = monacoInstance
setEditorMounted(true)

setTimeout(() => monacoInstance.editor.setTheme(`flow-vs-${theme}`), 0)

editor.addAction({
id: 'save-file',
label: 'Save File',
Expand Down Expand Up @@ -545,8 +622,9 @@ export default function CodeEditor() {
<div className="h-full">
<Editor
language="xml"
theme={`vs-${theme}`}
theme={`flow-${theme}`}
value={xmlContent}
beforeMount={handleEditorBeforeMount}
onMount={handleEditorMount}
onChange={(value) => {
scheduleSave()
Expand Down
Loading