diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 184e90b895..0e09ee0143 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -12,5 +12,6 @@ jobs: uses: zakodium/workflows/.github/workflows/nodejs.yml@nodejs-v1 with: lint-check-types: false - disable-tests: true disable-test-package: true + upload-coverage: false + node-version-matrix: '[24]' diff --git a/package-lock.json b/package-lock.json index 32c32b39a0..a3de81cb76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", + "@types/node": "^24.10.13", "add-stream": "^1.0.0", "bower": "^1.8.14", "conventional-changelog": "^6.0.0", @@ -70,6 +71,7 @@ "lodash": "^4.17.23", "mkpath": "^1.0.0", "prettier": "^3.8.1", + "requirejs": "^2.3.8", "rollup": "^4.59.0", "rollup-plugin-polyfill-node": "^0.13.0", "tempfile": "^3.0.0", @@ -2449,6 +2451,16 @@ "@types/lodash": "*" } }, + "node_modules/@types/node": { + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -11214,6 +11226,13 @@ "node": "*" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", diff --git a/package.json b/package.json index 1dd5bfa6d0..967a17adc8 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "prerelease": "grunt bump:prerelease", "prettier": "prettier --check src", "prettier-write": "prettier --write src", - "test": "npm run eslint && npm run prettier", + "test": "npm run eslint && npm run prettier && npm run test-only", + "test-only": "node --test tests/**", "release:minor": "npm run test && grunt bump:minor --release", "release:patch": "npm run test && grunt bump:patch --release" }, @@ -44,6 +45,7 @@ "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.3", + "@types/node": "^24.10.13", "add-stream": "^1.0.0", "bower": "^1.8.14", "conventional-changelog": "^6.0.0", @@ -61,6 +63,7 @@ "lodash": "^4.17.23", "mkpath": "^1.0.0", "prettier": "^3.8.1", + "requirejs": "^2.3.8", "rollup": "^4.59.0", "rollup-plugin-polyfill-node": "^0.13.0", "tempfile": "^3.0.0", diff --git a/src/modules/module.js b/src/modules/module.js index 724fc1ff15..9721f66df2 100644 --- a/src/modules/module.js +++ b/src/modules/module.js @@ -221,10 +221,10 @@ define([ toolbar[i].title || '' }">`; if (toolbar[i].icon) { - html += `
`; + html += `
`; } if (toolbar[i].cssClass) { - html += ``; + html += ``; } html += ''; } diff --git a/src/src/main/entrypoint.js b/src/src/main/entrypoint.js index 5e8612178e..90891beea0 100644 --- a/src/src/main/entrypoint.js +++ b/src/src/main/entrypoint.js @@ -3,6 +3,7 @@ define([ 'jquery', 'lodash', + 'src/util/jquery_prefilter', 'src/header/header', 'src/util/repository', 'src/main/grid', @@ -25,6 +26,7 @@ define([ ], function ( $, _, + jqueryPrefilter, Header, Repository, Grid, @@ -42,6 +44,20 @@ define([ Config, Sandbox, ) { + jQuery.htmlPrefilter = (preHtml) => { + const { warn, html } = jqueryPrefilter(preHtml); + if (warn) { + // eslint-disable-next-line no-console + console.warn( + 'jQuery.htmlPrefilter modified invalid html, make sure to update your html markup', + { + before: preHtml, + after: html, + }, + ); + } + return html; + }; var _viewLoaded, _dataLoaded, _modulesSet; var RepositoryData = new Repository(), diff --git a/src/src/util/jquery_prefilter.js b/src/src/util/jquery_prefilter.js new file mode 100644 index 0000000000..2ac5f1fd0d --- /dev/null +++ b/src/src/util/jquery_prefilter.js @@ -0,0 +1,28 @@ +'use strict'; + +define(() => { + // https://jquery.com/upgrade-guide/3.5/ + const rxhtmlTag = + /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^/\0>\u0020\t\r\n\f]*)[^>]*)\/>/gi; + + return function jqueryPrefilter(html) { + // Self-closing tags are invalid except for void elements like input + const filteredHtml = html.replaceAll(rxhtmlTag, '<$1>'); + if (filteredHtml !== html) { + // Ignore svg content because it is actually XML + const cleanedHtml = html.replaceAll(/]*>(.*?)<\/svg>/g, '').trim(); + + const result = rxhtmlTag.exec(cleanedHtml); + if (cleanedHtml && result && result[0] !== cleanedHtml) { + return { + html: filteredHtml, + warn: true, + }; + } + } + return { + html, + warn: false, + }; + }; +}); diff --git a/testcase/data/twig-form/view.json b/testcase/data/twig-form/view.json index 8ced6829ff..bac92cd340 100644 --- a/testcase/data/twig-form/view.json +++ b/testcase/data/twig-form/view.json @@ -1,5 +1,5 @@ { - "version": "2.111.2-0", + "version": "3.1.6", "grid": { "layers": { "Default layer": { @@ -17,14 +17,36 @@ "groups": { "group": [ { - "mode": ["html"], - "outputType": [null], - "btnvalue": ["Send script"], - "iseditable": [["editable"]], - "hasButton": [["button"]], - "variable": [[]], - "storeOnChange": [["store"]], - "debouncing": [250], + "mode": [ + "html" + ], + "outputType": [ + null + ], + "btnvalue": [ + "Send script" + ], + "iseditable": [ + [ + "editable" + ] + ], + "hasButton": [ + [ + "button" + ] + ], + "variable": [ + [] + ], + "storeOnChange": [ + [ + "store" + ] + ], + "debouncing": [ + 250 + ], "script": [ "\n\n
\n

My book

\n \n Title: \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
KindFirstnameLastnameNationalities
\n \n \n \n \n \n \n \n \n \n \n
\n \n
\n
\n \n

Keywords

\n \n \n \n \n
\n \n
\n\n

Parameters

\n \n \n \n \n \n
\n \n \n \n
\n
\n\n\n\n" ] @@ -32,8 +54,14 @@ ], "ace": [ { - "useSoftTabs": [["yes"]], - "tabSize": [4] + "useSoftTabs": [ + [ + "yes" + ] + ], + "tabSize": [ + 4 + ] } ] } @@ -51,15 +79,24 @@ "zIndex": 0, "display": true, "title": "", - "bgColor": [255, 255, 255, 0], + "bgColor": [ + 255, + 255, + 255, + 0 + ], "wrapper": true, "created": true, "name": "Default layer" } }, "id": 1, - "vars_in": [{}], - "actions_in": [{}], + "vars_in": [ + {} + ], + "actions_in": [ + {} + ], "vars_out": [ { "event": "onButtonClick", @@ -81,20 +118,33 @@ "icon": "", "action": "", "position": "begin", - "color": [100, 100, 100, 1] + "color": [ + 100, + 100, + 100, + 1 + ] } ] ], "common": [ { - "toolbar": [["Open Preferences"]] + "toolbar": [ + [ + "Open Preferences" + ] + ] } ] }, "css": [ { - "fontSize": [""], - "fontFamily": [""] + "fontSize": [ + "" + ], + "fontFamily": [ + "" + ] } ], "title": "" @@ -106,14 +156,32 @@ "groups": { "group": [ { - "editable": ["text"], - "expanded": [[]], - "storeObject": [[]], - "displayValue": [[]], - "searchBox": [["search"]], - "sendButton": [[]], - "output": ["new"], - "storedObject": ["{}"] + "editable": [ + "text" + ], + "expanded": [ + [] + ], + "storeObject": [ + [] + ], + "displayValue": [ + [] + ], + "searchBox": [ + [ + "search" + ] + ], + "sendButton": [ + [] + ], + "output": [ + "new" + ], + "storedObject": [ + "{}" + ] } ] } @@ -131,7 +199,12 @@ "zIndex": 0, "display": true, "title": "", - "bgColor": [255, 255, 255, 0], + "bgColor": [ + 255, + 255, + 255, + 0 + ], "wrapper": true, "created": true, "name": "Default layer" @@ -144,7 +217,9 @@ "name": "data" } ], - "actions_in": [{}], + "actions_in": [ + {} + ], "vars_out": [ { "jpath": [] @@ -168,14 +243,22 @@ ], "common": [ { - "toolbar": [["Open Preferences"]] + "toolbar": [ + [ + "Open Preferences" + ] + ] } ] }, "css": [ { - "fontSize": [""], - "fontFamily": [""] + "fontSize": [ + "" + ], + "fontFamily": [ + "" + ] } ], "title": "" @@ -187,11 +270,23 @@ "groups": { "group": [ { - "selectable": [[]], - "template": [""], - "modifyInForm": [["yes"]], - "debouncing": [100], - "formOptions": [[]] + "selectable": [ + [] + ], + "template": [ + "" + ], + "modifyInForm": [ + [ + "yes" + ] + ], + "debouncing": [ + 100 + ], + "formOptions": [ + [] + ] } ] } @@ -209,7 +304,12 @@ "zIndex": 0, "display": true, "title": "", - "bgColor": [255, 255, 255, 0], + "bgColor": [ + 255, + 255, + 255, + 0 + ], "wrapper": true, "created": true, "name": "Default layer" @@ -230,7 +330,9 @@ "name": "kinds" } ], - "actions_in": [{}], + "actions_in": [ + {} + ], "vars_out": [ { "jpath": [] @@ -249,20 +351,33 @@ "icon": "", "action": "", "position": "begin", - "color": [100, 100, 100, 1] + "color": [ + 100, + 100, + 100, + 1 + ] } ] ], "common": [ { - "toolbar": [["Open Preferences"]] + "toolbar": [ + [ + "Open Preferences" + ] + ] } ] }, "css": [ { - "fontSize": [""], - "fontFamily": [""] + "fontSize": [ + "" + ], + "fontFamily": [ + "" + ] } ], "title": "" @@ -274,15 +389,32 @@ "groups": { "group": [ { - "display": [["editor", "buttons"]], - "execOnLoad": [["yes"]], - "asyncAwait": [["top"]], + "display": [ + [ + "editor", + "buttons" + ] + ], + "execOnLoad": [ + [ + "yes" + ] + ], + "asyncAwait": [ + [ + "top" + ] + ], "script": [ "API.createData('data', {\n title:'abc',\n authors:[\n {firstname:'ab', lastname:'cd', nationalities: ['belgian']},\n {firstname:'gh', lastname:'ij', nationalities: []},\n {firstname:'kl', lastname:'mn'},\n {firstname:'op', lastname:'qr', nationalities: ['belgian','swiss']},\n ],\n info: {\n keywords:['kwd1','kw2'],\n parameters: [\n {\n \"description\": \"asdf\",\n \"value\": \"asdf\"\n },\n {\n \"description\": \"fds\",\n \"value\": \"asdff\"\n }\n ]\n }\n});\nwindow.setTimeout( () => API.getData('template').triggerChange() );\n" ] } ], - "libs": [[{}]], + "libs": [ + [ + {} + ] + ], "buttons": [ [ { @@ -308,15 +440,24 @@ "zIndex": 0, "display": true, "title": "", - "bgColor": [255, 255, 255, 0], + "bgColor": [ + 255, + 255, + 255, + 0 + ], "wrapper": true, "created": true, "name": "Default layer" } }, "id": 4, - "vars_in": [{}], - "actions_in": [{}], + "vars_in": [ + {} + ], + "actions_in": [ + {} + ], "vars_out": [ { "jpath": [] @@ -335,20 +476,33 @@ "icon": "", "action": "", "position": "begin", - "color": [100, 100, 100, 1] + "color": [ + 100, + 100, + 100, + 1 + ] } ] ], "common": [ { - "toolbar": [["Open Preferences"]] + "toolbar": [ + [ + "Open Preferences" + ] + ] } ] }, "css": [ { - "fontSize": [""], - "fontFamily": [""] + "fontSize": [ + "" + ], + "fontFamily": [ + "" + ] } ], "title": "" @@ -360,15 +514,30 @@ "groups": { "group": [ { - "display": [["editor", "buttons"]], - "execOnLoad": [[]], - "asyncAwait": [["top"]], + "display": [ + [ + "editor", + "buttons" + ] + ], + "execOnLoad": [ + [] + ], + "asyncAwait": [ + [ + "top" + ] + ], "script": [ "let data=API.getData('data');\nfor (let key of Object.keys(data)) {\n data[key]=undefined;\n}\ndata.triggerChange();\nAPI.getData('kinds').triggerChange();\nwindow.setTimeout( () => API.getData('template').triggerChange() );\n" ] } ], - "libs": [[{}]], + "libs": [ + [ + {} + ] + ], "buttons": [ [ { @@ -393,15 +562,24 @@ "zIndex": 0, "display": true, "title": "", - "bgColor": [255, 255, 255, 0], + "bgColor": [ + 255, + 255, + 255, + 0 + ], "wrapper": true, "created": true, "name": "Default layer" } }, "id": 5, - "vars_in": [{}], - "actions_in": [{}], + "vars_in": [ + {} + ], + "actions_in": [ + {} + ], "vars_out": [ { "jpath": [] @@ -425,14 +603,22 @@ ], "common": [ { - "toolbar": [["Open Preferences"]] + "toolbar": [ + [ + "Open Preferences" + ] + ] } ] }, "css": [ { - "fontSize": [""], - "fontFamily": [""] + "fontSize": [ + "" + ], + "fontFamily": [ + "" + ] } ], "title": "" @@ -444,15 +630,32 @@ "groups": { "group": [ { - "display": [["editor", "buttons"]], - "execOnLoad": [["yes"]], - "asyncAwait": [["top"]], + "display": [ + [ + "editor", + "buttons" + ] + ], + "execOnLoad": [ + [ + "yes" + ] + ], + "asyncAwait": [ + [ + "top" + ] + ], "script": [ "let kinds=[\"Author\",\"Editor\",\"Test\"];\nAPI.createData('kinds', kinds);\n" ] } ], - "libs": [[{}]], + "libs": [ + [ + {} + ] + ], "buttons": [ [ { @@ -478,15 +681,24 @@ "zIndex": 0, "display": true, "title": "", - "bgColor": [255, 255, 255, 0], + "bgColor": [ + 255, + 255, + 255, + 0 + ], "wrapper": true, "created": true, "name": "Default layer" } }, "id": 6, - "vars_in": [{}], - "actions_in": [{}], + "vars_in": [ + {} + ], + "actions_in": [ + {} + ], "vars_out": [ { "jpath": [] @@ -505,20 +717,33 @@ "icon": "", "action": "", "position": "begin", - "color": [100, 100, 100, 1] + "color": [ + 100, + 100, + 100, + 1 + ] } ] ], "common": [ { - "toolbar": [["Open Preferences"]] + "toolbar": [ + [ + "Open Preferences" + ] + ] } ] }, "css": [ { - "fontSize": [""], - "fontFamily": [""] + "fontSize": [ + "" + ], + "fontFamily": [ + "" + ] } ], "title": "" @@ -526,12 +751,14 @@ ], "variables": [ { - "jpath": [""] + "jpath": [ + "" + ] } ], "aliases": [ { - "path": "https://www.lactame.com/github/cheminfo-js/visualizer-helper/fede69d611616d26d5f70a4fc053eceac6612b68", + "path": "https://www.lactame.com/github/cheminfo-js/visualizer-helper/37fdbb1f1de6c5153451f030cf9721e885752ce3", "alias": "vh" }, { @@ -548,8 +775,12 @@ "groups": { "action": [ { - "name": [null], - "script": [null] + "name": [ + null + ], + "script": [ + null + ] } ] } @@ -561,7 +792,9 @@ "groups": { "general": [ { - "script": ["API.createData('data', {});\n"] + "script": [ + "API.createData('data', {});\n" + ] } ] } @@ -574,7 +807,11 @@ { "sections": {}, "groups": { - "modules": [[{}]] + "modules": [ + [ + {} + ] + ] } } ], @@ -582,7 +819,11 @@ { "sections": {}, "groups": { - "filters": [[{}]] + "filters": [ + [ + {} + ] + ] } } ], @@ -592,11 +833,19 @@ "groups": { "filter": [ { - "name": [null], - "script": [null] + "name": [ + null + ], + "script": [ + null + ] } ], - "libs": [[{}]] + "libs": [ + [ + {} + ] + ] } } ] @@ -608,7 +857,11 @@ { "sections": {}, "groups": { - "action": [[{}]] + "action": [ + [ + {} + ] + ] } } ] diff --git a/tests/jquery_prefilter.test.js b/tests/jquery_prefilter.test.js new file mode 100644 index 0000000000..d460a693b6 --- /dev/null +++ b/tests/jquery_prefilter.test.js @@ -0,0 +1,83 @@ +import assert from 'node:assert'; +import { it } from 'node:test'; + +import { requireAmd } from './require_amd.js'; + +const jqueryPrefilter = await requireAmd('src/util/jquery_prefilter'); + +function assertFilter(before, after, shouldWarn) { + const { warn, html } = jqueryPrefilter(before); + assert.equal(html, after); + assert.ok( + warn === shouldWarn, + `${before} is expected ${shouldWarn ? 'to' : 'not to'} produce a warning`, + ); +} +it('closes non-void elements but does not touch void elements', () => { + assertFilter( + `
`, + true, + ); +}); + +it('ignores single elements without children', () => { + assertFilter(`
`, `
`, false); + assertFilter(`
`, `
`, false); + assertFilter(``, ``, false); + assertFilter(` `, ` `, false); +}); + +it('ignores svg content when isolated', () => { + assertFilter(``, ``, false); + assertFilter( + ``, + ``, + false, + ); + assertFilter(`
`, `
`, false); +}); + +it('ignores svg when there are no issues outside of it', () => { + assertFilter( + `
`, + `
`, + false, + ); +}); + +it('transforms svg content when other tags need to be transformed', () => { + assertFilter( + `
`, + `
`, + true, + ); +}); + +it('handles multi-line html', () => { + assertFilter( + ` +
+ + `, + ` +
+ + `, + true, + ); + + assertFilter( + ` +
+ + + `, + ` +
+ + + `, + true, + ); +}); diff --git a/tests/require_amd.js b/tests/require_amd.js new file mode 100644 index 0000000000..fdd0ca2c1d --- /dev/null +++ b/tests/require_amd.js @@ -0,0 +1,18 @@ +import requirejs from 'requirejs'; +requirejs.config({ + baseUrl: new URL('../src/', import.meta.url).pathname, +}); + +export function requireAmd(deps) { + if (typeof deps === 'string') { + return new Promise((resolve, reject) => { + requirejs([deps], resolve, reject); + }); + } else if (Array.isArray(deps)) { + return new Promise((resolve, reject) => { + requirejs(deps, (...args) => resolve(args), reject); + }); + } else { + throw new Error(`Unexpected amd dependency: ${deps}`); + } +}