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