admin: syntax highlight for html, css, js, yaml
Massimo Melina committed
Jan 4, 2025 at 21:27 UTC
ffc974a2453063e70af079a70adb1ee2706890ac
6 files changed
+29
-8
admin/package.json
+2
@@ -20,6 +20,7 @@
20
"@mui/x-tree-view": "^6.17.0",
21
"@gregoranders/csv": "^0.0.13",
22
"dayjs": "^1.11.10",
23
+ "prismjs": "^1.29.0",
24
"react": "^18.3.1",
25
"react-dom": "^18.2.0",
26
"react-router-dom": "^6.23.1",
@@ -33,6 +34,7 @@
34
"web-vitals": "^2.1.4"
35
},
36
"devDependencies": {
37
+ "@types/prismjs": "^1.26.5",
38
"@types/react": "^18.3.14",
39
"@types/react-dom": "^18.3.2",
40
"@types/react-window": "^1.8.8",
admin/src/CustomHtmlPage.ts
+1
@@ -57,6 +57,7 @@ export default function CustomHtmlPage({ setTitleSide }: PageProps) {
57
}),
58
),
59
h(TextEditor, {
60
+ lang: section === 'style' ? 'css' : section === 'script' ? 'js' : 'html',
61
value: all?.[section] || '',
62
style: { background: '#8881' },
63
onValueChange(v: string) {
admin/src/FileForm.ts
+2
-1
@@ -25,6 +25,7 @@ import MenuButton from './MenuButton'
25
import addFiles, { addLink, addVirtual } from './addFiles'
26
import { SYS_ICONS } from '@hfs/frontend/src/sysIcons'
27
import { hIcon } from '@hfs/frontend/src/misc'
28
+import { TextEditorField } from './TextEditor'
29
30
const ACCEPT_LINK = "https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept"
31
@@ -203,7 +204,7 @@ export default function FileForm({ file, addToBar, statusApi, accounts, saved }:
204
{ k: 'comment', multiline: true, xl: true },
205
isDir && { k: 'masks', multiline: true, xl: 6,
206
toField: yaml.stringify, fromField: v => v ? yaml.parse(v) : undefined,
206
- sx: { '& textarea': { fontFamily: 'monospace' } },
207
+ comp: TextEditorField, lang: 'yaml',
208
helperText: ["Special field, leave empty unless you know what you are doing. YAML syntax. ", wikiLink('Masks-field', "(examples)")]
209
},
210
]
admin/src/OptionsPage.ts
+1
-1
@@ -243,7 +243,7 @@ export default function OptionsPage() {
243
toField: x => Object.entries(x || {}).map(([k,v]) => ({ k, v })),
244
fromField: x => Object.fromEntries(x.map((row: any) => [row.k, row.v || 'auto'])),
245
},
246
- { k: 'server_code', comp: TextEditorField, sm: 12, getError: v => try_(() => new Function(v) && null, e => e.message),
246
+ { k: 'server_code', comp: TextEditorField, lang: 'js', sm: 12,
247
helperText: md(`This code works similarly to [a plugin](${REPO_URL}blob/main/dev-plugins.md) (with some limitations)`)
248
},
249
admin/src/TextEditor.ts
+19
-5
@@ -2,15 +2,22 @@ import { ComponentProps, createElement as h, forwardRef, useState } from 'react'
2
import Editor from 'react-simple-code-editor'
3
import { FieldProps } from '@hfs/mui-grid-form'
4
import { Box, TextField, TextFieldProps } from '@mui/material'
5
-import { escapeHTML, focusableSelector, isCtrlKey } from './misc'
5
+import { focusableSelector, isCtrlKey, try_ } from './misc'
6
import _ from 'lodash'
7
+import { highlight, languages } from 'prismjs'
8
+import 'prismjs/components/prism-markup'
9
+import 'prismjs/components/prism-javascript'
10
+import 'prismjs/components/prism-yaml'
11
+import 'prismjs/components/prism-css'
12
+import 'prismjs/components/prism-json'
13
+import 'prismjs/themes/prism-solarizedlight.css' // looks good both with light and dark theme
14
15
type OP = ComponentProps<typeof Editor>
16
type Already = 'highlight' | 'padding' | 'tabSize' | 'insertSpaces' | 'ignoreTabKey'
10
-type TextEditorProps = Omit<OP, Already> & Partial<Pick<OP, Already>>
11
-export const TextEditor = forwardRef(({ style, ...props }: TextEditorProps, ref) => h(Editor, {
17
+type TextEditorProps = Omit<OP, Already> & Partial<Pick<OP, Already>> & { lang?: 'plain' | 'js' | 'html' | 'css' | 'json' | 'yaml' }
18
+export const TextEditor = forwardRef(({ style, lang='plain', ...props }: TextEditorProps, ref) => h(Editor, {
19
// Editor component doesn't seem to support ref, but I didn't cause any problem yet
13
- highlight: escapeHTML,
20
+ highlight: s => highlight(s, languages[lang], lang),
21
padding: 10,
22
tabSize: 4,
23
insertSpaces: true,
@@ -26,13 +33,20 @@ export const TextEditor = forwardRef(({ style, ...props }: TextEditorProps, ref)
33
...props,
34
}))
35
29
-export function TextEditorField({ onChange, value, onBlur, setApi, ...props }: FieldProps<string> & Omit<TextFieldProps, 'onChange'>) {
36
+export function TextEditorField({ onChange, value, onBlur, setApi, lang, ...props }: FieldProps<string> & Omit<TextFieldProps, 'onChange'>) {
37
+ setApi?.({
38
+ getError() {
39
+ return lang === 'js' && value ? try_(() => new Function(value) && null, e => e.message)
40
+ : false
41
+ }
42
+ })
43
const [state, setState] = useState(value || '')
44
return h(TextField, {
45
multiline: true,
46
fullWidth: true,
47
value: state,
48
InputProps: { inputComponent: TextEditorAsInput },
49
+ inputProps: { lang },
50
onChange(event) { setState(event.target.value) },
51
onBlur(event) {
52
onBlur?.(event)
admin/src/index.scss
+4
-1
@@ -75,4 +75,7 @@ h2.MuiDialogTitle-root { /* less padding */
75
// (try) avoid hidden scrollbars
76
::-webkit-scrollbar { width: 4px }
77
::-webkit-scrollbar-thumb { background: #8888 }
78
-::-webkit-scrollbar-track { background: #8882 }
\ No newline at end of file
78
+::-webkit-scrollbar-track { background: #8882 }
79
+
80
+// adjust prism black theme
81
+.token.operator { background: unset !important; }
\ No newline at end of file