From 0b210f50cd4ad42886c96799e789afc693e879ac Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 10 Aug 2026 03:17:21 +0100 Subject: [PATCH] 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. --- bun.lock | 3 + package.json | 5 +- pnpm-lock.yaml | 137 ++++++++---------- src/codemirror/fcTemplateLang/ejs.grammar | 45 ++++++ src/codemirror/fcTemplateLang/ejs.parser.js | 21 +++ .../fcTemplateLang/ejs.parser.terms.js | 13 ++ src/codemirror/fcTemplateLang/index.js | 62 ++++++++ src/codemirror/fcTemplateLang/tokens.js | 37 +++++ .../Dashboard/common/CodeBlockEditor.jsx | 4 +- .../Dashboard/common/TemplateEditor.jsx | 2 +- src/database/models/DocumentTemplate.js | 2 +- vite.config.js | 70 +++++++-- 12 files changed, 305 insertions(+), 96 deletions(-) create mode 100644 src/codemirror/fcTemplateLang/ejs.grammar create mode 100644 src/codemirror/fcTemplateLang/ejs.parser.js create mode 100644 src/codemirror/fcTemplateLang/ejs.parser.terms.js create mode 100644 src/codemirror/fcTemplateLang/index.js create mode 100644 src/codemirror/fcTemplateLang/tokens.js diff --git a/bun.lock b/bun.lock index 2797dce..723346d 100644 --- a/bun.lock +++ b/bun.lock @@ -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", diff --git a/package.json b/package.json index 24442c1..43a9694 100644 --- a/package.json +++ b/package.json @@ -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.", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c9e943..ee7b2cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/codemirror/fcTemplateLang/ejs.grammar b/src/codemirror/fcTemplateLang/ejs.grammar new file mode 100644 index 0000000..b366b98 --- /dev/null +++ b/src/codemirror/fcTemplateLang/ejs.grammar @@ -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 diff --git a/src/codemirror/fcTemplateLang/ejs.parser.js b/src/codemirror/fcTemplateLang/ejs.parser.js new file mode 100644 index 0000000..1d46438 --- /dev/null +++ b/src/codemirror/fcTemplateLang/ejs.parser.js @@ -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 +}) diff --git a/src/codemirror/fcTemplateLang/ejs.parser.terms.js b/src/codemirror/fcTemplateLang/ejs.parser.terms.js new file mode 100644 index 0000000..036ce73 --- /dev/null +++ b/src/codemirror/fcTemplateLang/ejs.parser.terms.js @@ -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 diff --git a/src/codemirror/fcTemplateLang/index.js b/src/codemirror/fcTemplateLang/index.js new file mode 100644 index 0000000..1c0e1ac --- /dev/null +++ b/src/codemirror/fcTemplateLang/index.js @@ -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) +} diff --git a/src/codemirror/fcTemplateLang/tokens.js b/src/codemirror/fcTemplateLang/tokens.js new file mode 100644 index 0000000..afbb582 --- /dev/null +++ b/src/codemirror/fcTemplateLang/tokens.js @@ -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 } +) diff --git a/src/components/Dashboard/common/CodeBlockEditor.jsx b/src/components/Dashboard/common/CodeBlockEditor.jsx index e4a7163..25b8bef 100644 --- a/src/components/Dashboard/common/CodeBlockEditor.jsx +++ b/src/components/Dashboard/common/CodeBlockEditor.jsx @@ -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': diff --git a/src/components/Dashboard/common/TemplateEditor.jsx b/src/components/Dashboard/common/TemplateEditor.jsx index 0934c46..80bdf84 100644 --- a/src/components/Dashboard/common/TemplateEditor.jsx +++ b/src/components/Dashboard/common/TemplateEditor.jsx @@ -89,7 +89,7 @@ const TemplateEditor = ({ name={'content'} height='100%' type='codeBlock' - language={'xml'} + language={'fcTemplateLang'} objectData={objectData} isEditing={isEditing} /> diff --git a/src/database/models/DocumentTemplate.js b/src/database/models/DocumentTemplate.js index 79a8590..9ce2ee5 100644 --- a/src/database/models/DocumentTemplate.js +++ b/src/database/models/DocumentTemplate.js @@ -212,7 +212,7 @@ export const DocumentTemplate = { label: 'Content', required: false, type: 'codeBlock', - language: 'ejs' + language: 'fcTemplateLang' }, { name: 'testObject', diff --git a/vite.config.js b/vite.config.js index ff138e7..33c148b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -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: [