Add fcTemplateLang support for EJS templates in CodeMirror

- Introduced a new language support for EJS templates with XML integration, allowing for custom scriptlets and improved syntax highlighting.
- Updated CodeBlockEditor and TemplateEditor components to utilize the new fcTemplateLang for EJS language handling.
- Modified DocumentTemplate model to reflect the new language type for content.
- Added necessary grammar and parser files for fcTemplateLang, enhancing the overall template editing experience.
This commit is contained in:
Tom Butcher 2026-08-10 03:17:21 +01:00
parent 3a70df3ef7
commit 0b210f50cd
12 changed files with 305 additions and 96 deletions

View File

@ -26,6 +26,9 @@
"@codemirror/state": "6.7.1",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "6.39.12",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.8",
"@simplewebauthn/browser": "^13.1.2",
"@tanstack/react-query": "^5.90.10",
"@tiptap/extension-link": "^3.20.1",

View File

@ -74,7 +74,10 @@
"styled-components": "^6.1.19",
"three": "^0.179.1",
"tsparticles": "^3.9.1",
"web-vitals": "^5.1.0"
"web-vitals": "^5.1.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.8"
},
"main": "build/electron.js",
"description": "3D Printer ERP and Control Software.",

137
pnpm-lock.yaml generated
View File

@ -79,6 +79,15 @@ importers:
'@codemirror/view':
specifier: 6.39.12
version: 6.39.12
'@lezer/common':
specifier: ^1.5.0
version: 1.5.2
'@lezer/highlight':
specifier: ^1.2.3
version: 1.2.3
'@lezer/lr':
specifier: ^1.4.8
version: 1.4.8
'@simplewebauthn/browser':
specifier: ^13.1.2
version: 13.2.2
@ -1134,79 +1143,67 @@ packages:
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
@ -1379,9 +1376,6 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@lezer/common@1.5.0':
resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==}
'@lezer/common@1.5.2':
resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==}
@ -1598,79 +1592,66 @@ packages:
resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.57.1':
resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.57.1':
resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.57.1':
resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.57.1':
resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.57.1':
resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.57.1':
resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.57.1':
resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.57.1':
resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.57.1':
resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.57.1':
resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.57.1':
resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.57.1':
resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.57.1':
resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==}
@ -6927,7 +6908,7 @@ snapshots:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.7.1
'@codemirror/view': 6.39.12
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@codemirror/commands@6.10.4':
dependencies:
@ -6946,7 +6927,7 @@ snapshots:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/css': 1.3.0
'@codemirror/lang-go@6.0.1':
@ -6954,7 +6935,7 @@ snapshots:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/go': 1.0.1
'@codemirror/lang-html@6.4.11':
@ -6965,7 +6946,7 @@ snapshots:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.7.1
'@codemirror/view': 6.39.12
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/css': 1.3.0
'@lezer/html': 1.3.13
@ -6981,7 +6962,7 @@ snapshots:
'@codemirror/lint': 6.9.3
'@codemirror/state': 6.7.1
'@codemirror/view': 6.39.12
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/javascript': 1.5.4
'@codemirror/lang-json@6.0.2':
@ -6996,7 +6977,7 @@ snapshots:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.7.1
'@codemirror/view': 6.39.12
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/markdown': 1.6.3
'@codemirror/lang-php@6.0.2':
@ -7004,7 +6985,7 @@ snapshots:
'@codemirror/lang-html': 6.4.11
'@codemirror/language': 6.12.1
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/php': 1.0.5
'@codemirror/lang-python@6.2.1':
@ -7012,7 +6993,7 @@ snapshots:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/python': 1.1.18
'@codemirror/lang-rust@6.0.2':
@ -7025,7 +7006,7 @@ snapshots:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
@ -7035,7 +7016,7 @@ snapshots:
'@codemirror/language': 6.12.1
'@codemirror/state': 6.7.1
'@codemirror/view': 6.39.12
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/xml': 1.0.6
'@codemirror/lang-yaml@6.1.2':
@ -7043,7 +7024,7 @@ snapshots:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
'@codemirror/state': 6.7.1
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/yaml': 1.0.4
@ -7913,86 +7894,84 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@lezer/common@1.5.0': {}
'@lezer/common@1.5.2': {}
'@lezer/cpp@1.1.5':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/css@1.3.0':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/go@1.0.1':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/highlight@1.2.3':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/html@1.3.13':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/java@1.1.3':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/javascript@1.5.4':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/json@1.0.3':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/lr@1.4.8':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/markdown@1.6.3':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/php@1.0.5':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/python@1.1.18':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/rust@1.0.2':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
'@lezer/xml@1.0.6':
dependencies:
'@lezer/common': 1.5.0
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.8
@ -10339,17 +10318,17 @@ snapshots:
dependencies:
eslint: 9.39.2(jiti@2.6.1)
eslint-config-standard-jsx@11.0.0(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1):
eslint-config-standard-jsx@11.0.0(eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)))(eslint@8.57.1):
dependencies:
eslint: 8.57.1
eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
eslint-config-standard@17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1):
dependencies:
eslint: 8.57.1
eslint-plugin-import: 2.32.0(eslint@8.57.1)
eslint-plugin-n: 15.7.0(eslint@8.57.1)
eslint-plugin-promise: 6.6.0(eslint@8.57.1)
eslint-plugin-import: 2.32.0(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-n: 15.7.0(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-promise: 6.6.0(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-node@0.3.9:
dependencies:
@ -10359,22 +10338,22 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint@8.57.1):
eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
eslint: 8.57.1
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
transitivePeerDependencies:
- supports-color
eslint-plugin-es@4.1.0(eslint@8.57.1):
eslint-plugin-es@4.1.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
eslint: 8.57.1
eslint: 9.39.2(jiti@2.6.1)
eslint-utils: 2.1.0
regexpp: 3.2.0
eslint-plugin-import@2.32.0(eslint@8.57.1):
eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@ -10383,9 +10362,9 @@ snapshots:
array.prototype.flatmap: 1.3.3
debug: 3.2.7
doctrine: 2.1.0
eslint: 8.57.1
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint@8.57.1)
eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -10401,12 +10380,12 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-n@15.7.0(eslint@8.57.1):
eslint-plugin-n@15.7.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
builtins: 5.1.0
eslint: 8.57.1
eslint-plugin-es: 4.1.0(eslint@8.57.1)
eslint-utils: 3.0.0(eslint@8.57.1)
eslint: 9.39.2(jiti@2.6.1)
eslint-plugin-es: 4.1.0(eslint@9.39.2(jiti@2.6.1))
eslint-utils: 3.0.0(eslint@9.39.2(jiti@2.6.1))
ignore: 5.3.2
is-core-module: 2.16.1
minimatch: 3.1.2
@ -10423,9 +10402,9 @@ snapshots:
'@types/eslint': 9.6.1
eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-promise@6.6.0(eslint@8.57.1):
eslint-plugin-promise@6.6.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
eslint: 8.57.1
eslint: 9.39.2(jiti@2.6.1)
eslint-plugin-react-hooks@5.2.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
@ -10493,9 +10472,9 @@ snapshots:
dependencies:
eslint-visitor-keys: 1.3.0
eslint-utils@3.0.0(eslint@8.57.1):
eslint-utils@3.0.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
eslint: 8.57.1
eslint: 9.39.2(jiti@2.6.1)
eslint-visitor-keys: 2.1.0
eslint-visitor-keys@1.3.0: {}
@ -13636,10 +13615,10 @@ snapshots:
dependencies:
eslint: 8.57.1
eslint-config-standard: 17.1.0(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)
eslint-config-standard-jsx: 11.0.0(eslint-plugin-react@7.37.5(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.32.0(eslint@8.57.1)
eslint-plugin-n: 15.7.0(eslint@8.57.1)
eslint-plugin-promise: 6.6.0(eslint@8.57.1)
eslint-config-standard-jsx: 11.0.0(eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)))(eslint@8.57.1)
eslint-plugin-import: 2.32.0(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-n: 15.7.0(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-promise: 6.6.0(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@8.57.1)
standard-engine: 15.1.0
version-guard: 1.1.3

View File

@ -0,0 +1,45 @@
// FarmControl EJS outer grammar for fcTemplateLang.
// Regenerate the parser with:
// pnpm exec lezer-generator src/codemirror/fcTemplateLang/ejs.grammar --output src/codemirror/fcTemplateLang/ejs.parser.js
//
// Outer language = EJS scriptlets; Text regions are overlaid with XML and
// JavascriptExpression nodes are mounted with the JS parser (parseMixed).
@top Template { content* }
content {
Scriptlet |
Output |
Comment |
Text
}
Scriptlet { OpeningTag JavascriptExpression ClosingTag }
Output { OutputTag JavascriptExpression ClosingTag }
@skip {} {
Comment { CommentTag (CommentContent | commentNewLine)* commentEnd }
}
@local tokens {
commentEnd { ClosingTag }
commentNewLine { "\n" }
@else CommentContent
}
@tokens {
// Prefer longer tags via @precedence below
OpeningTag[closedBy=ClosingTag] { "<%" }
OutputTag[closedBy=ClosingTag] { "<%=" | "<%-" | "<%_" }
CommentTag[closedBy=ClosingTag] { "<%#" }
ClosingTag[openedBy="OpeningTag OutputTag CommentTag"] { ("-" | "_")? "%>" }
@precedence { OutputTag, CommentTag, OpeningTag }
}
@external tokens javascriptExpressionOrText from "./tokens.js" {
JavascriptExpression,
Text
}
@detectDelim

View File

@ -0,0 +1,21 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser, LocalTokenGroup} from "@lezer/lr"
import {javascriptExpressionOrText} from "./tokens.js"
export const parser = LRParser.deserialize({
version: 14,
states: "#YQQOROOO`OQO'#C`OeOQO'#CcOjOSO'#CeOOOQ'#Ck'#CkOOOQ'#Ch'#ChQQOROOOuOPO,58zOzOPO,58}OOOO'#Ci'#CiO!POSO,59POOOQ,59P,59POOOQ-E6f-E6fOOOQ1G.f1G.fOOOQ1G.i1G.iOOOO-E6g-E6gOOOQ1G.k1G.k",
stateData: "!a~OQSOTPOWQOYRO~OPVO~OPWO~OZXO`XOaZO~OU]O~OU^O~OZXO`XOa`O~OWYTY~",
goto: "u`PPPPaPPaPaPPekPqTSOUQUOR[UQYRR_YTTOU",
nodeNames: "⚠ JavascriptExpression Text Template Scriptlet OpeningTag ClosingTag Output OutputTag Comment CommentTag CommentContent",
maxTerm: 17,
nodeProps: [
["closedBy", -3,5,8,10,"ClosingTag"],
["openedBy", 6,"OpeningTag OutputTag CommentTag"]
],
skippedNodes: [0],
repeatNodeCount: 2,
tokenData: "!c~RSuv_}!Oj!^!_p#R#Sj~bP!`!ae~jOU~~mPuv_~sPuvv~{ST~st!X}!O!^!_!`!^#R#S!^~!^OY~~!cOW~",
tokenizers: [1, javascriptExpressionOrText, new LocalTokenGroup("v~RSYZ_uvd}!Oo#R#So~dO`~~gP!`!aj~oOa~~rPuvd~", 37, 11)],
topRules: {"Template":[0,3]},
tokenPrec: 58
})

View File

@ -0,0 +1,13 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
JavascriptExpression = 1,
Text = 2,
Template = 3,
Scriptlet = 4,
OpeningTag = 5,
ClosingTag = 6,
Output = 7,
OutputTag = 8,
Comment = 9,
CommentTag = 10,
CommentContent = 11

View File

@ -0,0 +1,62 @@
import {
LRLanguage,
LanguageSupport,
indentNodeProp,
delimitedIndent,
foldNodeProp,
foldInside
} from '@codemirror/language'
import { styleTags, tags as t } from '@lezer/highlight'
import { parseMixed } from '@lezer/common'
import { javascriptLanguage } from '@codemirror/lang-javascript'
import { xmlLanguage } from '@codemirror/lang-xml'
import { parser as ejsParser } from './ejs.parser.js'
/**
* FarmControl document template language: custom XML tags with EJS
* scriptlets (`<% %>`, `<%= %>`, etc.). Uses mixed-language parsing so
* XML and JavaScript each get their own highlighting/support.
*
* @see https://codemirror.net/examples/mixed-language/
*/
const fcTemplateParser = ejsParser.configure({
props: [
indentNodeProp.add({
JavascriptExpression: delimitedIndent({ closing: '%>', align: false })
}),
foldNodeProp.add({
Scriptlet: foldInside,
Output: foldInside,
Comment: foldInside
}),
styleTags({
'OpeningTag OutputTag CommentTag ClosingTag': t.controlKeyword,
CommentContent: t.blockComment
})
],
wrap: parseMixed((node) => {
if (node.name === 'JavascriptExpression') {
return { parser: javascriptLanguage.parser }
}
if (node.name === 'Text') {
return { parser: xmlLanguage.parser }
}
return null
})
})
export const fcTemplateLanguage = LRLanguage.define({
name: 'fcTemplateLang',
parser: fcTemplateParser,
languageData: {
commentTokens: { block: { open: '<%#', close: '%>' } },
closeBrackets: { brackets: ['(', '[', '{', '"', "'", '`'] }
}
})
/**
* Language support for FarmControl EJS+XML document templates.
*/
export function fcTemplateLang() {
return new LanguageSupport(fcTemplateLanguage)
}

View File

@ -0,0 +1,37 @@
import { ExternalTokenizer } from '@lezer/lr'
import { JavascriptExpression, Text } from './ejs.parser.terms.js'
const percent = 37
const closeAngle = 62
const dash = 45
const underscore = 95
const openAngle = 60
export const javascriptExpressionOrText = new ExternalTokenizer(
(input, stack) => {
let { next } = input
const start = input.pos
while (next !== -1) {
if (next === openAngle && input.peek(1) === percent) break
if (
([dash, underscore].includes(next) &&
input.peek(1) === percent &&
input.peek(2) === closeAngle) ||
(next === percent && input.peek(1) === closeAngle)
) {
break
}
next = input.advance()
}
if (stack.canShift(JavascriptExpression)) {
// Empty expressions are valid: <% %>
input.acceptToken(JavascriptExpression)
} else if (stack.canShift(Text) && input.pos > start) {
// Never emit empty Text — that loops forever at EOF
input.acceptToken(Text)
}
},
{ contextual: true }
)

View File

@ -16,6 +16,7 @@ import { php } from '@codemirror/lang-php'
import { yaml } from '@codemirror/lang-yaml'
import { xml } from '@codemirror/lang-xml'
import { oneDark } from '@codemirror/theme-one-dark'
import { fcTemplateLang } from '../../../codemirror/fcTemplateLang'
import { useThemeContext } from '../context/ThemeContext'
import { Button, Modal, Typography } from 'antd'
@ -54,8 +55,9 @@ export default function CodeBlockEditor({
return json()
case 'xml':
return xml()
case 'fctemplatelang':
case 'ejs':
return xml()
return fcTemplateLang()
case 'html':
return html()
case 'css':

View File

@ -89,7 +89,7 @@ const TemplateEditor = ({
name={'content'}
height='100%'
type='codeBlock'
language={'xml'}
language={'fcTemplateLang'}
objectData={objectData}
isEditing={isEditing}
/>

View File

@ -212,7 +212,7 @@ export const DocumentTemplate = {
label: 'Content',
required: false,
type: 'codeBlock',
language: 'ejs'
language: 'fcTemplateLang'
},
{
name: 'testObject',

View File

@ -15,28 +15,72 @@ const __dirname = path.dirname(__filename)
const isCloudflare = process.env.VITE_DEPLOY_TARGET === 'cloudflare'
/** Resolve a package's ESM entry to an absolute path (pnpm-safe single instance). */
function esmEntry(id) {
const cjs = require.resolve(id)
const root = path.dirname(path.dirname(cjs)) // .../pkg/dist → .../pkg
const pkg = require(path.join(root, 'package.json'))
const rel =
(typeof pkg.exports?.['.'] === 'object' && pkg.exports['.'].import) ||
(typeof pkg.exports === 'object' && pkg.exports.import) ||
pkg.module ||
pkg.exports?.['.'] ||
'dist/index.js'
return path.join(root, rel)
}
const codemirrorSingletons = {
'@codemirror/state': esmEntry('@codemirror/state'),
'@codemirror/view': esmEntry('@codemirror/view'),
'@codemirror/language': esmEntry('@codemirror/language'),
'@codemirror/commands': esmEntry('@codemirror/commands'),
'@codemirror/autocomplete': esmEntry('@codemirror/autocomplete'),
'@lezer/common': esmEntry('@lezer/common'),
'@lezer/lr': esmEntry('@lezer/lr'),
'@lezer/highlight': esmEntry('@lezer/highlight')
}
export default defineConfig({
base: isCloudflare ? '/' : './', // Cloudflare Pages needs absolute paths; Electron needs relative for file://
resolve: {
alias: {
three: path.resolve(__dirname, 'node_modules/three')
three: path.resolve(__dirname, 'node_modules/three'),
...codemirrorSingletons
},
dedupe: [
dedupe: Object.keys(codemirrorSingletons)
},
optimizeDeps: {
// Do not prebundle CodeMirror — pnpm + esbuild duplicates @codemirror/state
// and breaks FacetProvider instanceof checks.
include: ['@ant-design/icons'],
exclude: [
'@uiw/react-codemirror',
'codemirror',
'@codemirror/state',
'@codemirror/view',
'@codemirror/language',
'@codemirror/commands',
'codemirror'
]
},
optimizeDeps: {
include: [
'@ant-design/icons',
'@codemirror/state',
'@codemirror/view',
'@codemirror/language',
'codemirror',
'@uiw/react-codemirror'
'@codemirror/autocomplete',
'@codemirror/theme-one-dark',
'@codemirror/lang-javascript',
'@codemirror/lang-xml',
'@codemirror/lang-json',
'@codemirror/lang-html',
'@codemirror/lang-css',
'@codemirror/lang-python',
'@codemirror/lang-markdown',
'@codemirror/lang-sql',
'@codemirror/lang-java',
'@codemirror/lang-cpp',
'@codemirror/lang-rust',
'@codemirror/lang-go',
'@codemirror/lang-php',
'@codemirror/lang-yaml',
'@lezer/common',
'@lezer/lr',
'@lezer/highlight',
'@lezer/javascript',
'@lezer/xml'
]
},
plugins: [