diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 335e9c0200..43efde8599 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { - "packages/openscd": "0.35.0", - "packages/core": "0.1.2", - ".": "0.35.0" + "packages/openscd": "0.36.0", + "packages/core": "0.1.3", + ".": "0.36.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad66060b8..80601e883e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [0.36.0](https://github.com/openscd/open-scd/compare/v0.35.0...v0.36.0) (2024-11-14) + + +### ⚠ BREAKING CHANGES + +* Edit API v1 validation is no longer supported (e.g. edit api v1 checked if an elements id was unique in the document) +* Edit event v1 properties `derived` and `checkValidity` will be ignored + +### Features + +* Allow .fsd file creation ([d9a4a0c](https://github.com/openscd/open-scd/commit/d9a4a0c6f6a0c9c86927d80bf5c81b4e9f6fc6d5)) +* Edit events v1 will be converted event v2 ([14e933e](https://github.com/openscd/open-scd/commit/14e933ed776ec5592c3c38e84b9884fa41a05e81)) +* Editor plugins can be rendered without an active document ([8b06a37](https://github.com/openscd/open-scd/commit/8b06a375ecfbc6275c5238d4a95383f4e80449b8)) +* Handle Config Plugin Events ([a510664](https://github.com/openscd/open-scd/commit/a5106648367dad831a248b734cd5c34aa1043d89)) +* render plugin download UI on event ([44a51f0](https://github.com/openscd/open-scd/commit/44a51f05797e8dd6345215c177a2e7b68e189d69)) +* Support edit api v2 ([#1581](https://github.com/openscd/open-scd/issues/1581)) ([14e933e](https://github.com/openscd/open-scd/commit/14e933ed776ec5592c3c38e84b9884fa41a05e81)) + + +### Bug Fixes + +* 1553 LN LN0 wizards read only attributes ([#1568](https://github.com/openscd/open-scd/issues/1568)) ([87aa759](https://github.com/openscd/open-scd/commit/87aa75961c7ef0bfe11810d2fa5d4e08704da033)), closes [#1553](https://github.com/openscd/open-scd/issues/1553) +* correct plug-ins' paths ([a7a14ce](https://github.com/openscd/open-scd/commit/a7a14ced59294d8a24daabf5ecdc76a5dbb75237)) + ## [0.35.0](https://github.com/openscd/open-scd/compare/v0.34.0...v0.35.0) (2024-07-17) ### Features diff --git a/README.md b/README.md index a1b5467ba8..4576c80b7a 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,4 @@ How the documentation is organized. A high-level overview of how it’s organized will help you know where to look for certain things: - [⚖️ Decisions](docs/decisions/README.md) documents the decisions we made and why we made them. +- [✏️ Edit event API](docs/core-api/edit-api.md) documents the edit event API. diff --git a/docs/core-api/edit-api.md b/docs/core-api/edit-api.md new file mode 100644 index 0000000000..e5e3a459f9 --- /dev/null +++ b/docs/core-api/edit-api.md @@ -0,0 +1,248 @@ +# Edit Event API + +Open SCD offers an API for editing the scd document which can be used with [Html Custom Events](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent). The main Open SCD components listens to events of the type `oscd-edit`, applies the changes to the `doc` and updates the `editCount` property. + +The edits to the `doc` will be done in place, e.g. the `doc` changes but will keep the same reference. If your plugin needs to react to changes in the doc, you should listen to changes in the `editCount` property. + +## Event factory + +Open SCD core exports a factory function for edit events, so you do not have to build them manually. + +```ts +function newEditEvent( + edit: E, + initiator: Initiator = 'user' +): EditEvent + +type Edit = Insert | Update | Remove | Edit[]; + +type Initiator = 'user' | 'system' | 'undo' | 'redo' | string; + +``` + +Example for remove. + +```ts +import { newEditEvent, Remove } from '@openscd/core'; + +const remove: Remove = { node: someNode }; +const removeEvent = newEditEvent(remove); + +someComponent.dispatchEvent(removeEvent); + +``` + + +### Insert + +Insert events can be used to add new nodes or move existing nodes in the document. Since a node can only have one parent, using an insert on an existing node will replace it's previous parent with the new parent, essentially moving the node to a different position in the xml tree. + +If the reference is not `null`, the node will be inserted before the reference node. The reference has to be a child node of the parent. And if the reference is `null` the node will be added as the last child of the parent. + +```ts +interface Insert { + parent: Node; + node: Node; + reference: Node | null; +} +``` + + +### Remove + +This event will remove the node from the document. + +```ts +interface Remove { + node: Node; +} +``` + + +### Update + +Update can add, remove or change attributes on an existing node. Existing attributes will only be removed, if `null` is passed as value in the event's `attributes` property. + + +```ts +interface Update { + element: Element; + attributes: Partial>; +} + +// Attirubte value + +type AttributeValue = string | null | NamespacedAttributeValue; + +type NamespacedAttributeValue = { + value: string | null; + namespaceURI: string | null; +}; +``` + +Example for adding and changing values. + +```ts + +const update: Update = { + element: elementToUpdate, + attributes: { + name: 'new name', + value: 'new value' + } +}; + +``` + +To remove an existing value pass `null` as value. + +```ts + +const update: Update = { + element: elementToUpdate, + attributes: { + attributeToRemove: null + } +}; + +``` + +Update also supports [Xml namespaces](https://developer.mozilla.org/en-US/docs/Related/IMSC/Namespaces#namespaced_attributes) for attributes. To change namespaced attributes you need to pass an `NamespacedAttributeValue` instead of a plain `string`. + +```ts + +const update: Update = { + element: elementToUpdate, + attributes: { + name: { + value: 'namespaced name', + namespaceURI: 'http://www.iec.ch/61850/2003/SCLcoordinates' + }, + type: { + value: 'namespaced type', + namespaceURI: 'http://www.iec.ch/61850/2003/SCLcoordinates' + }, + } +}; + +``` + +Adding, updating and removing attributes with and without namespaces can be combined in a single `Update`. + +### Complex edits + +Complex edits can be used to apply multiple edits as a single event. This will create a single entry in the history. You can create complex edit events by passing an array of edit events to the `newEditEvent` factory function. + +```ts +import { newEditEvent } from '@openscd/core'; + +const complexEditEvent = newEditEvent([ insert, update, remove ]); + +someComponent.dispatchEvent(complexEditEvent); + +``` + + + +## History + +All edit events with initiator `user` will create a history log entry and can be undone and redone through the history addon. + +## Breaking changes due to migration +Before the edit event API the editor action API was used to edit the `doc`. It is also custom event based and listens to the events of the type `editor-action`. +For backwards compatibility the API is still supported, but it is recommended to use the edit event API instead. Internally editor actions are converted to edit events. +With open SCD version **v0.36.0** and higher some editor action features are no longer supported see [Deprecated Editor Action API](#archives---editor-action-api-deprecated). +* The editor action properties `derived` and `checkValidity` do not have any effect. +* All validation checks have been removed (i.e. check for unique `id` attribute on element before create). +* The `title` for `ComplexAction` does not have any effect. + +--- + +# Archives - Editor Action API (deprecated) + +### Event factory + +```ts + +function newActionEvent( + action: T, + initiator: Initiator = 'user', + eventInitDict?: CustomEventInit>> +): EditorActionEvent + +type SimpleAction = Update | Create | Replace | Delete | Move; +type ComplexAction = { + actions: SimpleAction[]; + title: string; + derived?: boolean; +}; +type EditorAction = SimpleAction | ComplexAction; + +``` + + +### Create + +`Create` actions are converted to `Insert` events. + +```ts +interface Create { + new: { parent: Node; element: Node; reference?: Node | null }; + derived?: boolean; + checkValidity?: () => boolean; +} +``` + +### Move + +`Move` actions are converted to `Insert` events. + +```ts +interface Move { + old: { parent: Element; element: Element; reference?: Node | null }; + new: { parent: Element; reference?: Node | null }; + derived?: boolean; + checkValidity?: () => boolean; +} +``` + + +### Delete + +`Delete` actions are converted to `Remove` events. + +```ts +interface Delete { + old: { parent: Node; element: Node; reference?: Node | null }; + derived?: boolean; + checkValidity?: () => boolean; +} +``` + + +### Update + +`Update` actions are converted to `Update` events. + +```ts +interface Update { + element: Element; + oldAttributes: Record; + newAttributes: Record; + derived?: boolean; + checkValidity?: () => boolean; +} +``` + +### Replace + +`Replace` actions are converted to a complex event with `Remove` and `Insert` events. + +```ts +interface Replace { + old: { element: Element }; + new: { element: Element }; + derived?: boolean; + checkValidity?: () => boolean; +} +``` diff --git a/package-lock.json b/package-lock.json index 2869bc6f78..dfafdf242d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9294,6 +9294,28 @@ "node": ">=4" } }, + "node_modules/default-browser-id/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/defaults": { "version": "1.0.4", "dev": true, @@ -12559,8 +12581,29 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "2.0.1", + "node_modules/internal-ip": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", + "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-gateway": "^6.0.0", + "ipaddr.js": "^1.9.1", + "is-ip": "^3.1.0", + "p-event": "^4.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/internal-ip?sponsor=1" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "license": "MIT", "dependencies": { @@ -12636,6 +12679,26 @@ "dev": true, "license": "MIT" }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "dev": true, @@ -12871,6 +12934,19 @@ "node": ">=8" } }, + "node_modules/is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-regex": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "dev": true, @@ -16351,6 +16427,22 @@ "node": ">=8" } }, + "node_modules/p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-finally": { "version": "1.0.0", "dev": true, @@ -21599,7 +21691,9 @@ } }, "node_modules/ws": { - "version": "7.5.9", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "license": "MIT", "engines": { @@ -23481,6 +23575,148 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/addons/node_modules/typedoc": { + "version": "0.21.10", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "glob": "^7.1.7", + "handlebars": "^4.7.7", + "lunr": "^2.3.9", + "marked": "^4.0.10", + "minimatch": "^3.0.0", + "progress": "^2.0.3", + "shiki": "^0.9.8", + "typedoc-default-themes": "^0.12.10" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 12.10.0" + }, + "peerDependencies": { + "typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x" + } + }, + "packages/addons/node_modules/typedoc-plugin-markdown": { + "version": "3.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "handlebars": "^4.7.7" + }, + "peerDependencies": { + "typedoc": ">=0.21.2" + } + }, + "packages/addons/node_modules/typescript": { + "version": "4.3.5", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "packages/addons/node_modules/vscode-textmate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.2.0.tgz", + "integrity": "sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==", + "dev": true + }, + "packages/addons/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "packages/addons/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "packages/core": { + "name": "@openscd/core", + "version": "0.1.2", + "license": "Apache-2.0", + "dependencies": { + "@lit/localize": "^0.11.4", + "@open-wc/lit-helpers": "^0.5.1", + "lit": "^2.2.7" + }, + "devDependencies": { + "@custom-elements-manifest/analyzer": "^0.6.3", + "@lit/localize-tools": "^0.6.5", + "@open-wc/building-rollup": "^2.2.1", + "@open-wc/eslint-config": "^7.0.0", + "@open-wc/testing": "next", + "@rollup/plugin-typescript": "^9.0.2", + "@types/node": "^18.11.9", + "@typescript-eslint/eslint-plugin": "^5.30.7", + "@typescript-eslint/parser": "^5.30.7", + "@web/dev-server": "^0.1.32", + "@web/test-runner": "next", + "@web/test-runner-playwright": "^0.8.10", + "@web/test-runner-visual-regression": "^0.6.6", + "concurrently": "^7.3.0", + "es-dev-server": "^2.1.0", + "eslint": "^8.20.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-tsdoc": "^0.2.16", + "fast-check": "^3.1.1", + "gh-pages": "^4.0.0", + "husky": "^4.3.8", + "lint-staged": "^13.0.3", + "prettier": "^2.7.1", + "tsdoc": "^0.0.4", + "tslib": "^2.4.0", + "typedoc": "^0.23.8", + "typescript": "^4.7.4" + } + }, + "packages/core/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "packages/core/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/core/node_modules/typedoc": { "version": "0.23.28", "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.28.tgz", @@ -23834,6 +24070,7 @@ "@typescript-eslint/parser": "^4.29.2", "@web/dev-server-esbuild": "^0.2.16", "@web/test-runner": "^0.13.22", + "@web/test-runner-playwright": "^0.11.0", "concurrently": "^6.2.1", "deepmerge": "^4.2.2", "es-dev-server": "^2.1.0", @@ -23892,261 +24129,1722 @@ "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", "dev": true, "dependencies": { - "type-detect": "4.0.8" + "eslint": "^7.6.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-plugin-html": "^6.0.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-lit": "^1.2.0", + "eslint-plugin-lit-a11y": "^1.0.1", + "eslint-plugin-no-only-tests": "^2.4.0", + "eslint-plugin-wc": "^1.2.0" + }, + "peerDependencies": { + "@babel/eslint-plugin": "^7.6.0", + "eslint-plugin-html": "^6.0.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-lit": "^1.3.0", + "eslint-plugin-lit-a11y": "^1.0.1", + "eslint-plugin-no-only-tests": "^2.4.0", + "eslint-plugin-wc": "^1.2.0" } }, - "packages/openscd/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "packages/openscd/node_modules/@open-wc/scoped-elements": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@open-wc/scoped-elements/-/scoped-elements-1.3.7.tgz", + "integrity": "sha512-q/wKf4sXl7cr1kNfl8z6TLO2TrpXsFMCrfCD51sCEljltwYIXOmI6SnRXmWlnzG37A8AwHRpDXYmjPj2F4gPxA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "@open-wc/dedupe-mixin": "^1.3.0", + "lit-html": "^1.0.0" } }, - "packages/openscd/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "packages/openscd/node_modules/@open-wc/testing": { + "version": "2.5.33", + "resolved": "https://registry.npmjs.org/@open-wc/testing/-/testing-2.5.33.tgz", + "integrity": "sha512-+EJNs0i+VV4nE+BrG70l2DNGXOZTSrluruaaU06HUSk57ZlKa+kIxWmkLxCOLlbgnQgrPrQWxbs3lgB1tIx/YA==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "@open-wc/chai-dom-equals": "^0.12.36", + "@open-wc/semantic-dom-diff": "^0.19.3", + "@open-wc/testing-helpers": "^1.8.12", + "@types/chai": "^4.2.11", + "@types/chai-dom": "^0.0.9", + "@types/mocha": "^5.2.7", + "@types/sinon-chai": "^3.2.3", + "chai": "^4.2.0", + "chai-a11y-axe": "^1.3.1", + "chai-dom": "^1.8.1", + "mocha": "^6.2.2", + "sinon-chai": "^3.5.0" } }, - "packages/openscd/node_modules/minimatch": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", - "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "packages/openscd/node_modules/@open-wc/testing-helpers": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/@open-wc/testing-helpers/-/testing-helpers-1.8.12.tgz", + "integrity": "sha512-+4exEHYvnFqI1RGDDIKFHPZ7Ws5NK1epvEku3zLaOYN3zc+huX19SndNc5+X++v8A+quN/iXbHlh80ROyNaYDA==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "@open-wc/scoped-elements": "^1.2.4", + "lit-element": "^2.2.1", + "lit-html": "^1.0.0" } }, - "packages/openscd/node_modules/shiki": { - "version": "0.14.7", + "packages/openscd/node_modules/@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, + "packages/openscd/node_modules/@types/node": { + "version": "16.18.98", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.98.tgz", + "integrity": "sha512-fpiC20NvLpTLAzo3oVBKIqBGR6Fx/8oAK/SSf7G+fydnXMY1x4x9RZ6sBXhqKlCU21g2QapUsbLlhv3+a7wS+Q==", + "dev": true + }, + "packages/openscd/node_modules/@typescript-eslint/eslint-plugin": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", + "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "4.33.0", + "@typescript-eslint/scope-manager": "4.33.0", + "debug": "^4.3.1", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^4.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/openscd/node_modules/@typescript-eslint/parser": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", + "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "debug": "^4.3.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/openscd/node_modules/@typescript-eslint/scope-manager": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", + "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/openscd/node_modules/@typescript-eslint/types": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", + "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", + "dev": true, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/openscd/node_modules/@typescript-eslint/typescript-estree": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", + "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/openscd/node_modules/@typescript-eslint/visitor-keys": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", + "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/openscd/node_modules/@web/browser-logs": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.0.tgz", + "integrity": "sha512-/EBiDAUCJ2DzZhaFxTPRIznEPeafdLbXShIL6aTu7x73x7ZoxSDv7DGuTsh2rWNMUa4+AKli4UORrpyv6QBOiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "errorstacks": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/openscd/node_modules/@web/dev-server-core": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.3.tgz", + "integrity": "sha512-GS+Ok6HiqNZOsw2oEv5V2OISZ2s/6icJodyGjUuD3RChr0G5HiESbKf2K8mZV4shTz9sRC9KSQf8qvno2gPKrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/koa": "^2.11.6", + "@types/ws": "^7.4.0", + "@web/parse5-utils": "^2.1.0", + "chokidar": "^4.0.1", + "clone": "^2.1.2", + "es-module-lexer": "^1.0.0", + "get-stream": "^6.0.0", + "is-stream": "^2.0.0", + "isbinaryfile": "^5.0.0", + "koa": "^2.13.0", + "koa-etag": "^4.0.0", + "koa-send": "^5.0.1", + "koa-static": "^5.0.0", + "lru-cache": "^8.0.4", + "mime-types": "^2.1.27", + "parse5": "^6.0.1", + "picomatch": "^2.2.2", + "ws": "^7.5.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/openscd/node_modules/@web/dev-server-core/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "packages/openscd/node_modules/@web/parse5-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.0.tgz", + "integrity": "sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse5": "^6.0.1", + "parse5": "^6.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/openscd/node_modules/@web/parse5-utils/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "packages/openscd/node_modules/@web/test-runner-core": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.4.tgz", + "integrity": "sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.11", + "@types/babel__code-frame": "^7.0.2", + "@types/co-body": "^6.1.0", + "@types/convert-source-map": "^2.0.0", + "@types/debounce": "^1.2.0", + "@types/istanbul-lib-coverage": "^2.0.3", + "@types/istanbul-reports": "^3.0.0", + "@web/browser-logs": "^0.4.0", + "@web/dev-server-core": "^0.7.3", + "chokidar": "^4.0.1", + "cli-cursor": "^3.1.0", + "co-body": "^6.1.0", + "convert-source-map": "^2.0.0", + "debounce": "^1.2.0", + "dependency-graph": "^0.11.0", + "globby": "^11.0.1", + "internal-ip": "^6.2.0", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.0.2", + "log-update": "^4.0.0", + "nanocolors": "^0.2.1", + "nanoid": "^3.1.25", + "open": "^8.0.2", + "picomatch": "^2.2.2", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/openscd/node_modules/@web/test-runner-coverage-v8": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.8.0.tgz", + "integrity": "sha512-PskiucYpjUtgNfR2zF2AWqWwjXL7H3WW/SnCAYmzUrtob7X9o/+BjdyZ4wKbOxWWSbJO4lEdGIDLu+8X2Xw+lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "istanbul-lib-coverage": "^3.0.0", + "lru-cache": "^8.0.4", + "picomatch": "^2.2.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/openscd/node_modules/@web/test-runner-playwright": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-playwright/-/test-runner-playwright-0.11.0.tgz", + "integrity": "sha512-s+f43DSAcssKYVOD9SuzueUcctJdHzq1by45gAnSCKa9FQcaTbuYe8CzmxA21g+NcL5+ayo4z+MA9PO4H+PssQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "@web/test-runner-coverage-v8": "^0.8.0", + "playwright": "^1.22.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/openscd/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "packages/openscd/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "packages/openscd/node_modules/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "engines": { + "node": ">=6.0" + } + }, + "packages/openscd/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "packages/openscd/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "packages/openscd/node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "packages/openscd/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "date-fns": "^2.16.1", + "lodash": "^4.17.21", + "rxjs": "^6.6.3", + "spawn-command": "^0.0.2-1", + "supports-color": "^8.1.0", + "tree-kill": "^1.2.2", + "yargs": "^16.2.0" + }, + "bin": { + "concurrently": "bin/concurrently.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "packages/openscd/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "packages/openscd/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "packages/openscd/node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "license": "MIT" + }, + "packages/openscd/node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/openscd/node_modules/eslint-config-airbnb-base": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", + "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.2" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", + "eslint-plugin-import": "^2.22.1" + } + }, + "packages/openscd/node_modules/eslint-plugin-lit-a11y": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-lit-a11y/-/eslint-plugin-lit-a11y-1.1.0.tgz", + "integrity": "sha512-reJqT0UG/Y8OC2z7pfgm0ODK1D6o5TgQpGdlgN1ja0HjdREXLqFVoYiEv013oNx3kBhTUaLlic64rRNw+386xw==", + "dev": true, + "dependencies": { + "aria-query": "^4.2.2", + "axe-core": "^4.3.3", + "axobject-query": "^2.2.0", + "dom5": "^3.0.1", + "emoji-regex": "^9.2.0", + "eslint": "^7.6.0", + "eslint-rule-extender": "0.0.1", + "intl-list-format": "^1.0.3", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "requireindex": "~1.2.0" + }, + "peerDependencies": { + "eslint": ">= 5" + } + }, + "packages/openscd/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "packages/openscd/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "packages/openscd/node_modules/eslint/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "packages/openscd/node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "packages/openscd/node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "packages/openscd/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/openscd/node_modules/shiki": { + "version": "0.14.7", "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", "dev": true, "dependencies": { - "ansi-sequence-parser": "^1.1.0", - "jsonc-parser": "^3.2.0", - "vscode-oniguruma": "^1.7.0", - "vscode-textmate": "^8.0.0" + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "packages/openscd/node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + }, + "packages/openscd/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/openscd/node_modules/husky": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz", + "integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "packages/openscd/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "packages/openscd/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "packages/openscd/node_modules/lint-staged": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.2.6.tgz", + "integrity": "sha512-Vti55pUnpvPE0J9936lKl0ngVeTdSZpEdTNhASbkaWX7J5R9OEifo1INBGQuGW4zmy6OG+TcWPJ3m5yuy5Q8Tg==", + "dev": true, + "dependencies": { + "cli-truncate": "2.1.0", + "colorette": "^1.4.0", + "commander": "^8.2.0", + "cosmiconfig": "^7.0.1", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "execa": "^5.1.1", + "listr2": "^3.12.2", + "micromatch": "^4.0.4", + "normalize-path": "^3.0.0", + "please-upgrade-node": "^3.2.0", + "string-argv": "0.3.1", + "stringify-object": "3.3.0", + "supports-color": "8.1.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "packages/openscd/node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "packages/openscd/node_modules/listr2/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "packages/openscd/node_modules/listr2/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "packages/openscd/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16.14" + } + }, + "packages/openscd/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "packages/openscd/node_modules/pure-rand": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-5.0.5.tgz", + "integrity": "sha512-BwQpbqxSCBJVpamI6ydzcKqyFmnd5msMWUGvzXLm1aXvusbbgkbOto/EUPM00hjveJEaJtdbhUjKSzWRhQVkaw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "packages/openscd/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "packages/openscd/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "packages/openscd/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "packages/openscd/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "packages/openscd/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "packages/openscd/node_modules/string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "packages/openscd/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "packages/openscd/node_modules/typedoc": { + "version": "0.23.28", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.28.tgz", + "integrity": "sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.2.12", + "minimatch": "^7.1.3", + "shiki": "^0.14.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 14.14" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x" + } + }, + "packages/openscd/node_modules/typedoc-plugin-markdown": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.12.0.tgz", + "integrity": "sha512-yKl7/KWD8nP6Ot6OzMLLc8wBzN3CmkBoI/YQzxT62a9xmDgxyeTxGbHbkUoSzhKFqMI3SR0AqV6prAhVKbYnxw==", + "dev": true, + "dependencies": { + "handlebars": "^4.7.7" + }, + "peerDependencies": { + "typedoc": ">=0.22.0" + } + }, + "packages/openscd/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "packages/openscd/node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "packages/openscd/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "packages/openscd/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "packages/plugins": { + "name": "@openscd/plugins", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@material/mwc-dialog": "0.22.1", + "@material/mwc-fab": "0.22.1", + "@material/mwc-formfield": "0.22.1", + "@material/mwc-icon": "0.22.1", + "@material/mwc-icon-button": "0.22.1", + "@material/mwc-icon-button-toggle": "0.22.1", + "@material/mwc-list": "0.22.1", + "@material/mwc-menu": "0.22.1", + "@material/mwc-select": "0.22.1", + "@material/mwc-switch": "0.22.1", + "@material/mwc-textarea": "0.22.1", + "@material/mwc-textfield": "0.22.1", + "@openscd/core": "*", + "@openscd/open-scd": "*", + "@openscd/wizards": "*", + "@openscd/xml": "*", + "lit": "^2.2.7", + "lit-translate": "^1.2.1", + "marked": "^4.0.10", + "panzoom": "^9.4.2" + }, + "devDependencies": { + "@commitlint/cli": "^13.1.0", + "@commitlint/config-conventional": "^13.1.0", + "@open-wc/eslint-config": "^4.3.0", + "@open-wc/semantic-dom-diff": "^0.19.5", + "@open-wc/testing": "^2.5.33", + "@snowpack/plugin-typescript": "^1.2.1", + "@types/marked": "^2.0.4", + "@types/node": "^16.6.1", + "@typescript-eslint/eslint-plugin": "^4.29.2", + "@typescript-eslint/parser": "^4.29.2", + "@web/dev-server-esbuild": "^0.2.16", + "@web/test-runner": "^0.13.22", + "@web/test-runner-playwright": "^0.11.0", + "concurrently": "^6.2.1", + "deepmerge": "^4.2.2", + "es-dev-server": "^2.1.0", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-babel": "^5.3.1", + "eslint-plugin-tsdoc": "^0.2.14", + "fast-check": "^2.19.0", + "husky": "^7.0.1", + "lint-staged": "^11.1.2", + "prettier": "^2.3.2", + "sinon": "^17.0.1", + "snowpack": "3.8.6", + "source-map": "^0.7.4", + "standard-version": "^9.3.1", + "tslib": "^2.3.1", + "typedoc": "^0.23.8", + "typedoc-plugin-markdown": "3.12.0", + "typescript": "^4.7.4", + "web-component-analyzer": "^1.1.6", + "workbox-cli": "^6.2.4" + } + }, + "packages/plugins/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "packages/plugins/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "packages/plugins/node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "packages/plugins/node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "eslint": "^7.6.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-plugin-html": "^6.0.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-lit": "^1.2.0", + "eslint-plugin-lit-a11y": "^1.0.1", + "eslint-plugin-no-only-tests": "^2.4.0", + "eslint-plugin-wc": "^1.2.0" + }, + "peerDependencies": { + "@babel/eslint-plugin": "^7.6.0", + "eslint-plugin-html": "^6.0.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-lit": "^1.3.0", + "eslint-plugin-lit-a11y": "^1.0.1", + "eslint-plugin-no-only-tests": "^2.4.0", + "eslint-plugin-wc": "^1.2.0" + } + }, + "packages/plugins/node_modules/@open-wc/scoped-elements": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@open-wc/scoped-elements/-/scoped-elements-1.3.7.tgz", + "integrity": "sha512-q/wKf4sXl7cr1kNfl8z6TLO2TrpXsFMCrfCD51sCEljltwYIXOmI6SnRXmWlnzG37A8AwHRpDXYmjPj2F4gPxA==", + "dev": true, + "dependencies": { + "@open-wc/dedupe-mixin": "^1.3.0", + "lit-html": "^1.0.0" + } + }, + "packages/plugins/node_modules/@open-wc/testing": { + "version": "2.5.33", + "resolved": "https://registry.npmjs.org/@open-wc/testing/-/testing-2.5.33.tgz", + "integrity": "sha512-+EJNs0i+VV4nE+BrG70l2DNGXOZTSrluruaaU06HUSk57ZlKa+kIxWmkLxCOLlbgnQgrPrQWxbs3lgB1tIx/YA==", + "dev": true, + "dependencies": { + "@open-wc/chai-dom-equals": "^0.12.36", + "@open-wc/semantic-dom-diff": "^0.19.3", + "@open-wc/testing-helpers": "^1.8.12", + "@types/chai": "^4.2.11", + "@types/chai-dom": "^0.0.9", + "@types/mocha": "^5.2.7", + "@types/sinon-chai": "^3.2.3", + "chai": "^4.2.0", + "chai-a11y-axe": "^1.3.1", + "chai-dom": "^1.8.1", + "mocha": "^6.2.2", + "sinon-chai": "^3.5.0" + } + }, + "packages/plugins/node_modules/@open-wc/testing-helpers": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/@open-wc/testing-helpers/-/testing-helpers-1.8.12.tgz", + "integrity": "sha512-+4exEHYvnFqI1RGDDIKFHPZ7Ws5NK1epvEku3zLaOYN3zc+huX19SndNc5+X++v8A+quN/iXbHlh80ROyNaYDA==", + "dev": true, + "dependencies": { + "@open-wc/scoped-elements": "^1.2.4", + "lit-element": "^2.2.1", + "lit-html": "^1.0.0" + } + }, + "packages/plugins/node_modules/@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, + "packages/plugins/node_modules/@types/node": { + "version": "16.18.98", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.98.tgz", + "integrity": "sha512-fpiC20NvLpTLAzo3oVBKIqBGR6Fx/8oAK/SSf7G+fydnXMY1x4x9RZ6sBXhqKlCU21g2QapUsbLlhv3+a7wS+Q==", + "dev": true + }, + "packages/plugins/node_modules/@typescript-eslint/eslint-plugin": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", + "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "4.33.0", + "@typescript-eslint/scope-manager": "4.33.0", + "debug": "^4.3.1", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^4.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/plugins/node_modules/@typescript-eslint/parser": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", + "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "debug": "^4.3.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/plugins/node_modules/@typescript-eslint/scope-manager": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", + "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/plugins/node_modules/@typescript-eslint/types": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", + "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", + "dev": true, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/plugins/node_modules/@typescript-eslint/typescript-estree": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", + "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/plugins/node_modules/@typescript-eslint/visitor-keys": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", + "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/plugins/node_modules/@web/browser-logs": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.0.tgz", + "integrity": "sha512-/EBiDAUCJ2DzZhaFxTPRIznEPeafdLbXShIL6aTu7x73x7ZoxSDv7DGuTsh2rWNMUa4+AKli4UORrpyv6QBOiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "errorstacks": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/plugins/node_modules/@web/dev-server-core": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.3.tgz", + "integrity": "sha512-GS+Ok6HiqNZOsw2oEv5V2OISZ2s/6icJodyGjUuD3RChr0G5HiESbKf2K8mZV4shTz9sRC9KSQf8qvno2gPKrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/koa": "^2.11.6", + "@types/ws": "^7.4.0", + "@web/parse5-utils": "^2.1.0", + "chokidar": "^4.0.1", + "clone": "^2.1.2", + "es-module-lexer": "^1.0.0", + "get-stream": "^6.0.0", + "is-stream": "^2.0.0", + "isbinaryfile": "^5.0.0", + "koa": "^2.13.0", + "koa-etag": "^4.0.0", + "koa-send": "^5.0.1", + "koa-static": "^5.0.0", + "lru-cache": "^8.0.4", + "mime-types": "^2.1.27", + "parse5": "^6.0.1", + "picomatch": "^2.2.2", + "ws": "^7.5.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/plugins/node_modules/@web/dev-server-core/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "packages/plugins/node_modules/@web/parse5-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.0.tgz", + "integrity": "sha512-GzfK5disEJ6wEjoPwx8AVNwUe9gYIiwc+x//QYxYDAFKUp4Xb1OJAGLc2l2gVrSQmtPGLKrTRcW90Hv4pEq1qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse5": "^6.0.1", + "parse5": "^6.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/plugins/node_modules/@web/parse5-utils/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "packages/plugins/node_modules/@web/test-runner-core": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.4.tgz", + "integrity": "sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.11", + "@types/babel__code-frame": "^7.0.2", + "@types/co-body": "^6.1.0", + "@types/convert-source-map": "^2.0.0", + "@types/debounce": "^1.2.0", + "@types/istanbul-lib-coverage": "^2.0.3", + "@types/istanbul-reports": "^3.0.0", + "@web/browser-logs": "^0.4.0", + "@web/dev-server-core": "^0.7.3", + "chokidar": "^4.0.1", + "cli-cursor": "^3.1.0", + "co-body": "^6.1.0", + "convert-source-map": "^2.0.0", + "debounce": "^1.2.0", + "dependency-graph": "^0.11.0", + "globby": "^11.0.1", + "internal-ip": "^6.2.0", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.0.2", + "log-update": "^4.0.0", + "nanocolors": "^0.2.1", + "nanoid": "^3.1.25", + "open": "^8.0.2", + "picomatch": "^2.2.2", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/plugins/node_modules/@web/test-runner-coverage-v8": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.8.0.tgz", + "integrity": "sha512-PskiucYpjUtgNfR2zF2AWqWwjXL7H3WW/SnCAYmzUrtob7X9o/+BjdyZ4wKbOxWWSbJO4lEdGIDLu+8X2Xw+lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "istanbul-lib-coverage": "^3.0.0", + "lru-cache": "^8.0.4", + "picomatch": "^2.2.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/plugins/node_modules/@web/test-runner-playwright": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-playwright/-/test-runner-playwright-0.11.0.tgz", + "integrity": "sha512-s+f43DSAcssKYVOD9SuzueUcctJdHzq1by45gAnSCKa9FQcaTbuYe8CzmxA21g+NcL5+ayo4z+MA9PO4H+PssQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "@web/test-runner-coverage-v8": "^0.8.0", + "playwright": "^1.22.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "packages/plugins/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "packages/plugins/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "packages/plugins/node_modules/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "engines": { + "node": ">=6.0" + } + }, + "packages/plugins/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" } }, - "packages/openscd/node_modules/sinon": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", - "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "packages/plugins/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "dev": true, + "license": "MIT", "dependencies": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^11.2.2", - "@sinonjs/samsam": "^8.0.0", - "diff": "^5.1.0", - "nise": "^5.1.5", - "supports-color": "^7.2.0" + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" + "url": "https://paulmillr.com/funding/" } }, - "packages/openscd/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "packages/plugins/node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { "node": ">=8" } }, - "packages/openscd/node_modules/typedoc": { - "version": "0.23.28", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.28.tgz", - "integrity": "sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==", + "packages/plugins/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", "dev": true, "dependencies": { - "lunr": "^2.3.9", - "marked": "^4.2.12", - "minimatch": "^7.1.3", - "shiki": "^0.14.1" + "chalk": "^4.1.0", + "date-fns": "^2.16.1", + "lodash": "^4.17.21", + "rxjs": "^6.6.3", + "spawn-command": "^0.0.2-1", + "supports-color": "^8.1.0", + "tree-kill": "^1.2.2", + "yargs": "^16.2.0" }, "bin": { - "typedoc": "bin/typedoc" + "concurrently": "bin/concurrently.js" }, "engines": { - "node": ">= 14.14" - }, - "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x" + "node": ">=10.0.0" } }, - "packages/openscd/node_modules/typedoc-plugin-markdown": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.12.0.tgz", - "integrity": "sha512-yKl7/KWD8nP6Ot6OzMLLc8wBzN3CmkBoI/YQzxT62a9xmDgxyeTxGbHbkUoSzhKFqMI3SR0AqV6prAhVKbYnxw==", + "packages/plugins/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dev": true, "dependencies": { - "handlebars": "^4.7.7" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" }, - "peerDependencies": { - "typedoc": ">=0.22.0" + "engines": { + "node": ">=10" } }, - "packages/openscd/node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "packages/plugins/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "packages/plugins/node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "license": "MIT" + }, + "packages/plugins/node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=4.2.0" + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "packages/openscd/node_modules/vscode-textmate": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", - "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", - "dev": true - }, - "packages/plugins": { - "name": "@openscd/plugins", - "version": "0.0.1", - "license": "Apache-2.0", + "packages/plugins/node_modules/eslint-config-airbnb-base": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", + "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", + "dev": true, "dependencies": { - "@material/mwc-dialog": "0.22.1", - "@material/mwc-fab": "0.22.1", - "@material/mwc-formfield": "0.22.1", - "@material/mwc-icon": "0.22.1", - "@material/mwc-icon-button": "0.22.1", - "@material/mwc-icon-button-toggle": "0.22.1", - "@material/mwc-list": "0.22.1", - "@material/mwc-menu": "0.22.1", - "@material/mwc-select": "0.22.1", - "@material/mwc-switch": "0.22.1", - "@material/mwc-textarea": "0.22.1", - "@material/mwc-textfield": "0.22.1", - "@openscd/core": "*", - "@openscd/open-scd": "*", - "@openscd/wizards": "*", - "@openscd/xml": "*", - "lit": "^2.2.7", - "lit-translate": "^1.2.1", - "marked": "^4.0.10", - "panzoom": "^9.4.2" + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.2" }, - "devDependencies": { - "@commitlint/cli": "^13.1.0", - "@commitlint/config-conventional": "^13.1.0", - "@open-wc/eslint-config": "^4.3.0", - "@open-wc/semantic-dom-diff": "^0.19.5", - "@open-wc/testing": "^2.5.33", - "@snowpack/plugin-typescript": "^1.2.1", - "@types/marked": "^2.0.4", - "@types/node": "^16.6.1", - "@typescript-eslint/eslint-plugin": "^4.29.2", - "@typescript-eslint/parser": "^4.29.2", - "@web/dev-server-esbuild": "^0.2.16", - "@web/test-runner": "^0.13.22", - "concurrently": "^6.2.1", - "deepmerge": "^4.2.2", - "es-dev-server": "^2.1.0", - "eslint": "^7.32.0", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-babel": "^5.3.1", - "eslint-plugin-tsdoc": "^0.2.14", - "fast-check": "^2.19.0", - "husky": "^7.0.1", - "lint-staged": "^11.1.2", - "prettier": "^2.3.2", - "sinon": "^17.0.1", - "snowpack": "3.8.6", - "source-map": "^0.7.4", - "standard-version": "^9.3.1", - "tslib": "^2.3.1", - "typedoc": "^0.23.8", - "typedoc-plugin-markdown": "3.12.0", - "typescript": "^4.7.4", - "web-component-analyzer": "^1.1.6", - "workbox-cli": "^6.2.4" + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", + "eslint-plugin-import": "^2.22.1" } }, - "packages/plugins/node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "packages/plugins/node_modules/eslint-plugin-lit-a11y": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-lit-a11y/-/eslint-plugin-lit-a11y-1.1.0.tgz", + "integrity": "sha512-reJqT0UG/Y8OC2z7pfgm0ODK1D6o5TgQpGdlgN1ja0HjdREXLqFVoYiEv013oNx3kBhTUaLlic64rRNw+386xw==", "dev": true, "dependencies": { - "type-detect": "4.0.8" + "aria-query": "^4.2.2", + "axe-core": "^4.3.3", + "axobject-query": "^2.2.0", + "dom5": "^3.0.1", + "emoji-regex": "^9.2.0", + "eslint": "^7.6.0", + "eslint-rule-extender": "0.0.1", + "intl-list-format": "^1.0.3", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "requireindex": "~1.2.0" + }, + "peerDependencies": { + "eslint": ">= 5" } }, - "packages/plugins/node_modules/@sinonjs/fake-timers": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "packages/plugins/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "packages/plugins/node_modules/@sinonjs/samsam": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "packages/plugins/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, - "dependencies": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" + "engines": { + "node": ">=4" } }, - "packages/plugins/node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "packages/plugins/node_modules/eslint/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true, - "dependencies": { - "type-detect": "4.0.8" + "engines": { + "node": ">= 4" } }, - "packages/plugins/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "packages/plugins/node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "packages/plugins/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "packages/plugins/node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true, "engines": { - "node": ">=8" + "node": ">=4" } }, - "packages/plugins/node_modules/minimatch": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", - "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "packages/plugins/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { "node": ">=10" @@ -24182,7 +25880,222 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/sinon" + "url": "https://opencollective.com/fast-check" + } + }, + "packages/plugins/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/plugins/node_modules/husky": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz", + "integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "packages/plugins/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "packages/plugins/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "packages/plugins/node_modules/lint-staged": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.2.6.tgz", + "integrity": "sha512-Vti55pUnpvPE0J9936lKl0ngVeTdSZpEdTNhASbkaWX7J5R9OEifo1INBGQuGW4zmy6OG+TcWPJ3m5yuy5Q8Tg==", + "dev": true, + "dependencies": { + "cli-truncate": "2.1.0", + "colorette": "^1.4.0", + "commander": "^8.2.0", + "cosmiconfig": "^7.0.1", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "execa": "^5.1.1", + "listr2": "^3.12.2", + "micromatch": "^4.0.4", + "normalize-path": "^3.0.0", + "please-upgrade-node": "^3.2.0", + "string-argv": "0.3.1", + "stringify-object": "3.3.0", + "supports-color": "8.1.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "packages/plugins/node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "packages/plugins/node_modules/listr2/node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "packages/plugins/node_modules/listr2/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "packages/plugins/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16.14" + } + }, + "packages/plugins/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "packages/plugins/node_modules/pure-rand": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-5.0.5.tgz", + "integrity": "sha512-BwQpbqxSCBJVpamI6ydzcKqyFmnd5msMWUGvzXLm1aXvusbbgkbOto/EUPM00hjveJEaJtdbhUjKSzWRhQVkaw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "packages/plugins/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "packages/plugins/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "packages/plugins/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "packages/plugins/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "packages/plugins/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "packages/plugins/node_modules/string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true, + "engines": { + "node": ">=0.6.19" } }, "packages/plugins/node_modules/supports-color": { @@ -24243,11 +26156,46 @@ "node": ">=4.2.0" } }, - "packages/plugins/node_modules/vscode-textmate": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", - "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", - "dev": true + "packages/plugins/node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "packages/plugins/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "packages/plugins/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } }, "packages/wizards": { "name": "@openscd/wizards", diff --git a/package.json b/package.json index 7c44b96930..e4fca6fe41 100644 --- a/package.json +++ b/package.json @@ -21,4 +21,4 @@ "optionalDependencies": { "@nx/nx-linux-x64-gnu": "18.3.4" } -} +} \ No newline at end of file diff --git a/packages/compas-open-scd/Dockerfile b/packages/compas-open-scd/Dockerfile index 0b95836260..9a204baf8d 100644 --- a/packages/compas-open-scd/Dockerfile +++ b/packages/compas-open-scd/Dockerfile @@ -1,4 +1,4 @@ -FROM bitnami/nginx:1.25.5 +FROM bitnami/nginx:1.27.3 COPY build/. /app/ VOLUME /opt/bitnami/nginx/conf/server_blocks/ diff --git a/packages/compas-open-scd/package.json b/packages/compas-open-scd/package.json index 9a75cda0bf..f33385e91a 100644 --- a/packages/compas-open-scd/package.json +++ b/packages/compas-open-scd/package.json @@ -1,6 +1,6 @@ { "name": "compas-open-scd", - "version": "0.35.0-1", + "version": "0.36.0-1", "repository": "https://github.com/openscd/open-scd.git", "description": "OpenSCD CoMPAS Edition", "directory": "packages/compas-open-scd", diff --git a/packages/compas-open-scd/src/addons/CompasHistory.ts b/packages/compas-open-scd/src/addons/CompasHistory.ts index 5a992e1ce5..feae0381d6 100644 --- a/packages/compas-open-scd/src/addons/CompasHistory.ts +++ b/packages/compas-open-scd/src/addons/CompasHistory.ts @@ -1,15 +1,9 @@ import { html, - state, - property, - query, TemplateResult, customElement, - LitElement, } from 'lit-element'; -import { get } from 'lit-translate'; - import '@material/mwc-button'; import '@material/mwc-dialog'; import '@material/mwc-icon'; @@ -18,66 +12,18 @@ import '@material/mwc-icon-button-toggle'; import '@material/mwc-list'; import '@material/mwc-list/mwc-list-item'; import '@material/mwc-snackbar'; -import { Dialog } from '@material/mwc-dialog'; -import { Snackbar } from '@material/mwc-snackbar'; import '@openscd/open-scd/src/filtered-list.js'; import { - CommitDetail, - CommitEntry, - InfoDetail, - InfoEntry, IssueDetail, - IssueEvent, - LogEntry, - LogEntryType, - LogEvent, } from '@openscd/core/foundation/deprecated/history.js'; -import { - newActionEvent, - invert, -} from '@openscd/core/foundation/deprecated/editor.js'; - -import { getFilterIcon, iconColors } from '@openscd/open-scd/src/icons/icons.js'; - -import { Plugin } from '@openscd/core/'; -import { HistoryUIDetail } from '@openscd/open-scd/src/addons/History.js'; +import { HistoryUIDetail, OscdHistory } from '@openscd/open-scd/src/addons/History.js'; import { wizards } from '@openscd/plugins/src/wizards/wizard-library'; import { newWizardEvent, SCLTag } from '@openscd/open-scd/src/foundation'; import { nothing } from 'lit-html'; -const icons = { - info: 'info', - warning: 'warning', - error: 'report', -}; - -function getPluginName(src: string): string { - - let storedPluginsString = localStorage.getItem('plugins'); - if(!storedPluginsString) { - storedPluginsString = '[]'; - } - - const storedPlugins = JSON.parse(storedPluginsString) as Plugin[]; - const wantedPlugin = storedPlugins.find((p: Plugin) => p.src === src); - - if(!wantedPlugin) { - return `pluginnotfound: ${src} in ${storedPluginsString}`; - } - - const name = wantedPlugin.name; - - if(!name){ - return `pluginhasnoname:${src}`; - } - - return name; - -} - export enum HistoryUIKind { log = 'log', history = 'history', @@ -130,177 +76,7 @@ export function newRedoEvent(): CustomEvent { } @customElement('compas-history') -export class CompasHistory extends LitElement { - /** All [[`LogEntry`]]s received so far through [[`LogEvent`]]s. */ - @property({ type: Array }) - log: InfoEntry[] = []; - - /** All [[`CommitEntry`]]s received so far through [[`LogEvent`]]s */ - @property({ type: Array }) - history: CommitEntry[] = []; - - /** Index of the last [[`EditorAction`]] applied. */ - @property({ type: Number }) - editCount = -1; - - @property() - diagnoses = new Map(); - - @property({ - type: Object, - }) - host!: HTMLElement; - - @state() - latestIssue!: IssueDetail; - - @query('#log') logUI!: Dialog; - @query('#history') historyUI!: Dialog; - @query('#diagnostic') diagnosticUI!: Dialog; - @query('#error') errorUI!: Snackbar; - @query('#warning') warningUI!: Snackbar; - @query('#info') infoUI!: Snackbar; - @query('#issue') issueUI!: Snackbar; - - get canUndo(): boolean { - return this.editCount >= 0; - } - get canRedo(): boolean { - return this.nextAction >= 0; - } - - get previousAction(): number { - if (!this.canUndo) return -1; - return this.history - .slice(0, this.editCount) - .map(entry => (entry.kind == 'action' ? true : false)) - .lastIndexOf(true); - } - get nextAction(): number { - let index = this.history - .slice(this.editCount + 1) - .findIndex(entry => entry.kind == 'action'); - if (index >= 0) index += this.editCount + 1; - return index; - } - - private onIssue(de: IssueEvent): void { - const issues = this.diagnoses.get(de.detail.validatorId); - - if (!issues) this.diagnoses.set(de.detail.validatorId, [de.detail]); - else issues?.push(de.detail); - - this.latestIssue = de.detail; - this.issueUI.close(); - this.issueUI.show(); - } - - undo(): boolean { - if (!this.canUndo) return false; - const invertedAction = invert( - (this.history[this.editCount]).action - ); - this.dispatchEvent(newActionEvent(invertedAction, 'undo')); - this.editCount = this.previousAction; - return true; - } - redo(): boolean { - if (!this.canRedo) return false; - const nextAction = (this.history[this.nextAction]).action; - this.dispatchEvent(newActionEvent(nextAction, 'redo')); - this.editCount = this.nextAction; - return true; - } - - private onHistory(detail: CommitDetail) { - const entry: CommitEntry = { - time: new Date(), - ...detail, - }; - - if (entry.kind === 'action') { - if (entry.action.derived) return; - entry.action.derived = true; - if (this.nextAction !== -1) this.history.splice(this.nextAction); - this.editCount = this.history.length; - } - - this.history.push(entry); - this.requestUpdate('history', []); - } - - private onReset() { - this.log = []; - this.history = []; - this.editCount = -1; - } - - private onInfo(detail: InfoDetail) { - const entry: InfoEntry = { - time: new Date(), - ...detail, - }; - - this.log.push(entry); - if (!this.logUI.open) { - const ui = { - error: this.errorUI, - warning: this.warningUI, - info: this.infoUI, - }[detail.kind]; - - ui.close(); - ui.show(); - } - if (detail.kind == 'error') { - this.errorUI.close(); // hack to reset timeout - this.errorUI.show(); - } - this.requestUpdate('log', []); - } - - private onLog(le: LogEvent): void { - switch (le.detail.kind) { - case 'reset': - this.onReset(); - break; - case 'action': - this.onHistory(le.detail); - break; - default: - this.onInfo(le.detail); - break; - } - } - - private historyUIHandler(e: HistoryUIEvent): void { - const ui = { - log: this.logUI, - history: this.historyUI, - diagnostic: this.diagnosticUI, - }[e.detail.kind]; - - if (e.detail.show) ui.show(); - else ui.close(); - } - - private emptyIssuesHandler(e: EmptyIssuesEvent): void { - if (this.diagnoses.get(e.detail.pluginSrc)) - this.diagnoses.get(e.detail.pluginSrc)!.length = 0; - } - - private handleKeyPress(e: KeyboardEvent): void { - const ctrlAnd = (key: string) => e.key === key && e.ctrlKey; - - if (ctrlAnd('y')) this.redo(); - if (ctrlAnd('z')) this.undo(); - if (ctrlAnd('l')) this.logUI.open ? this.logUI.close() : this.logUI.show(); - if (ctrlAnd('d')) - this.diagnosticUI.open - ? this.diagnosticUI.close() - : this.diagnosticUI.show(); - } - +export class CompasHistory extends OscdHistory { private openEditWizard(element: Element | undefined): void { if (element) { const wizard = wizards[element.tagName]?.edit(element); @@ -314,110 +90,8 @@ export class CompasHistory extends LitElement { } return false; } - - constructor() { - super(); - this.undo = this.undo.bind(this); - this.redo = this.redo.bind(this); - this.onLog = this.onLog.bind(this); - this.onIssue = this.onIssue.bind(this); - this.historyUIHandler = this.historyUIHandler.bind(this); - this.emptyIssuesHandler = this.emptyIssuesHandler.bind(this); - this.handleKeyPress = this.handleKeyPress.bind(this); - document.onkeydown = this.handleKeyPress; - } - - connectedCallback(): void { - super.connectedCallback(); - - this.host.addEventListener('log', this.onLog); - this.host.addEventListener('issue', this.onIssue); - this.host.addEventListener('history-dialog-ui', this.historyUIHandler); - this.host.addEventListener('empty-issues', this.emptyIssuesHandler); - this.host.addEventListener('undo', this.undo); - this.host.addEventListener('redo', this.redo); - this.diagnoses.clear(); - } - - renderLogEntry( - entry: InfoEntry, - index: number, - log: LogEntry[] - ): TemplateResult { - return html` - - - - ${entry.time?.toLocaleString()} - ${entry.title} - ${entry.message} - ${icons[entry.kind]} - `; - } - - renderHistoryEntry( - entry: CommitEntry, - index: number, - history: LogEntry[] - ): TemplateResult { - return html` - - - - ${entry.time?.toLocaleString()} - ${entry.title} - ${entry.message} - history - `; - } - - private renderLog(): TemplateResult[] | TemplateResult { - if (this.log.length > 0) - return this.log.slice().reverse().map(this.renderLogEntry, this); - else - return html` - ${get('log.placeholder')} - info - `; - } - - private renderHistory(): TemplateResult[] | TemplateResult { - if (this.history.length > 0) - return this.history.slice().reverse().map(this.renderHistoryEntry, this); - else - return html` - ${get('history.placeholder')} - info - `; - } - - private renderIssueEntry(issue: IssueDetail): TemplateResult { + + protected renderIssueEntry(issue: IssueDetail): TemplateResult { return html` `; } - - renderValidatorsIssues(issues: IssueDetail[]): TemplateResult[] { - if (issues.length === 0) return [html``]; - return [ - html` - - ${getPluginName(issues[0].validatorId)} - - `, - html`
  • `, - ...issues.map(issue => this.renderIssueEntry(issue)), - ]; - } - - private renderIssues(): TemplateResult[] | TemplateResult { - const issueItems: TemplateResult[] = []; - - this.diagnoses.forEach(issues => { - this.renderValidatorsIssues(issues).forEach(issueItem => - issueItems.push(issueItem) - ); - }); - - return issueItems.length - ? issueItems - : html` - ${get('diag.placeholder')} - info - `; - } - - private renderFilterButtons() { - return (Object.keys(icons)).map( - kind => html`${getFilterIcon(kind, false)} - ${getFilterIcon(kind, true)}` - ); - } - - private renderLogDialog(): TemplateResult { - return html` - ${this.renderFilterButtons()} - ${this.renderLog()} - ${get('close')} - `; - } - - private renderHistoryUI(): TemplateResult { - return html` - ${this.renderHistory()} - - - ${get('close')} - `; - } - - render(): TemplateResult { - return html` - - ${this.renderLogDialog()} ${this.renderHistoryUI()} - - - ${this.renderIssues()} - - - ${get('close')} - - - - - - - - this.logUI.show()} - >${get('log.snackbar.show')} - - - - this.logUI.show()} - >${get('log.snackbar.show')} - - - - this.diagnosticUI.show()} - >${get('log.snackbar.show')} - - `; - } } declare global { diff --git a/packages/compas-open-scd/src/addons/CompasLayout.ts b/packages/compas-open-scd/src/addons/CompasLayout.ts index 5b586663b1..09f688e039 100644 --- a/packages/compas-open-scd/src/addons/CompasLayout.ts +++ b/packages/compas-open-scd/src/addons/CompasLayout.ts @@ -13,17 +13,19 @@ import { newPendingStateEvent } from '@openscd/core/foundation/deprecated/waiter import { newSettingsUIEvent } from '@openscd/core/foundation/deprecated/settings.js'; import { MenuItem, - Plugin, Validator, - PluginKind, - MenuPosition, MenuPlugin, - menuPosition, pluginIcons, newResetPluginsEvent, newAddExternalPluginEvent, newSetPluginsEvent, } from '@openscd/open-scd/src/open-scd.js'; +import { + Plugin, + PluginKind, + MenuPosition, + menuPosition +} from '@openscd/open-scd/src/plugin.js' import { HistoryUIKind, newEmptyIssuesEvent, diff --git a/packages/compas-open-scd/src/open-scd.ts b/packages/compas-open-scd/src/open-scd.ts index 66478332c2..e28a1b8e62 100644 --- a/packages/compas-open-scd/src/open-scd.ts +++ b/packages/compas-open-scd/src/open-scd.ts @@ -20,7 +20,13 @@ import './addons/CompasSettings.js'; import '@openscd/open-scd/src/addons/Waiter.js'; import { initializeNsdoc, Nsdoc } from '@openscd/open-scd/src/foundation/nsdoc.js'; -import { AddExternalPluginEvent, InstalledOfficialPlugin, SetPluginsEvent, withoutContent, Plugin, MenuPosition, PluginKind, menuOrder, pluginTag, staticTagHtml } from '@openscd/open-scd/src/open-scd.js'; +import { AddExternalPluginEvent, SetPluginsEvent, withoutContent, menuOrder, pluginTag, staticTagHtml } from '@openscd/open-scd/src/open-scd.js'; +import { + InstalledOfficialPlugin, + Plugin, + MenuPosition, + PluginKind, +} from '@openscd/open-scd/src/plugin.js'; import { officialPlugins } from '../public/js/plugins.js'; import type { PluginSet, diff --git a/packages/compas-open-scd/test/integration/compas-editors/CompasVersions.test.ts b/packages/compas-open-scd/test/integration/compas-editors/CompasVersions.test.ts index 2453bfd6ae..e2894c36ee 100644 --- a/packages/compas-open-scd/test/integration/compas-editors/CompasVersions.test.ts +++ b/packages/compas-open-scd/test/integration/compas-editors/CompasVersions.test.ts @@ -1,8 +1,9 @@ import { expect, fixtureSync, html, waitUntil } from '@open-wc/testing'; import sinon, { SinonSpy, spy, SinonStub } from 'sinon'; -import { Editing } from '@openscd/open-scd/src/Editing.js'; import { Wizarding } from '@openscd/open-scd/src/Wizarding.js'; +import '@openscd/open-scd/test/mock-editor-logger.js'; +import { MockEditorLogger } from '@openscd/open-scd/test/mock-editor-logger.js'; import { BASIC_VERSIONS_LIST_RESPONSE, @@ -18,7 +19,7 @@ describe('compas-versions-plugin', () => { customElements.define( 'compas-versions-plugin', - Wizarding(Editing(CompasVersionsPlugin)) + Wizarding(CompasVersionsPlugin) ); let doc: Document; let element: CompasVersionsPlugin; diff --git a/packages/compas-open-scd/test/integration/compas-editors/autogen-substation.test.ts b/packages/compas-open-scd/test/integration/compas-editors/autogen-substation.test.ts index c7386d3ae2..8116cc970a 100644 --- a/packages/compas-open-scd/test/integration/compas-editors/autogen-substation.test.ts +++ b/packages/compas-open-scd/test/integration/compas-editors/autogen-substation.test.ts @@ -1,7 +1,7 @@ import { expect, fixture, html } from '@open-wc/testing'; -import '@openscd/open-scd/test/unit/mock-editor.js'; -import { MockEditor } from '@openscd/open-scd/test/unit/mock-editor.js'; +import '@openscd/open-scd/test/mock-editor-logger.js'; +import { MockEditorLogger } from '@openscd/open-scd/test/mock-editor-logger.js'; import '../../../src/compas-editors/autogen-substation.js'; import CompasAutogenerateSubstation from '../../../src/compas-editors/autogen-substation.js'; @@ -9,13 +9,13 @@ describe('autogen-substation-integration', () => { if (customElements.get('') === undefined) customElements.define('autogen-substation', CompasAutogenerateSubstation); - let parent: MockEditor; + let parent: MockEditorLogger; let element: CompasAutogenerateSubstation; let validSCL: XMLDocument; before(async () => { parent = await fixture(html` - + `); validSCL = await fetch( diff --git a/packages/compas-open-scd/test/integration/compas-editors/sitipe-bay.test.ts b/packages/compas-open-scd/test/integration/compas-editors/sitipe-bay.test.ts index 62a364f196..2449a15970 100644 --- a/packages/compas-open-scd/test/integration/compas-editors/sitipe-bay.test.ts +++ b/packages/compas-open-scd/test/integration/compas-editors/sitipe-bay.test.ts @@ -1,9 +1,9 @@ import { expect, fixture, html } from '@open-wc/testing'; import { stub } from 'sinon'; -import '@openscd/open-scd/test/unit/mock-editor.js'; import { SitipeBay } from '../../../src/compas-editors/sitipe/sitipe-bay.js'; -import { MockEditor } from '@openscd/open-scd/test/unit/mock-editor.js'; +import '@openscd/open-scd/test/mock-editor-logger.js'; +import { MockEditorLogger } from '@openscd/open-scd/test/mock-editor-logger.js'; describe('sitipe-bay-integration', () => { if (customElements.get('sitipe-bay') === undefined) { @@ -11,7 +11,7 @@ describe('sitipe-bay-integration', () => { } let element: SitipeBay; - let parent: MockEditor; + let parent: MockEditorLogger; let validSCL: XMLDocument; let bayTypicals: unknown[]; @@ -56,13 +56,13 @@ describe('sitipe-bay-integration', () => { ]; parent = await fixture( - html`` + >` ); element = parent.querySelector('sitipe-bay')!; diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index 466136ed83..c7cd164a43 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -26,7 +26,6 @@ export type { export { cyrb64 } from './foundation/cyrb64.js'; -export { Editing } from './mixins/Editing.js'; export type { Plugin, PluginSet } from './foundation/plugin.js'; export { newEditCompletedEvent } from './foundation/edit-completed-event.js'; diff --git a/packages/core/foundation/deprecated/history.ts b/packages/core/foundation/deprecated/history.ts index 2c128be2ef..d6c6d6c648 100644 --- a/packages/core/foundation/deprecated/history.ts +++ b/packages/core/foundation/deprecated/history.ts @@ -1,4 +1,4 @@ -import { EditorAction } from './editor'; +import { Edit } from '../edit-event.js'; type InfoEntryKind = 'info' | 'warning' | 'error'; @@ -12,7 +12,8 @@ export interface LogDetailBase { /** The [[`LogEntry`]] for a committed [[`EditorAction`]]. */ export interface CommitDetail extends LogDetailBase { kind: 'action'; - action: EditorAction; + redo: Edit; + undo: Edit; } /** A [[`LogEntry`]] for notifying the user. */ export interface InfoDetail extends LogDetailBase { diff --git a/packages/core/mixins/Editing.ts b/packages/core/mixins/Editing.ts deleted file mode 100644 index c42845491a..0000000000 --- a/packages/core/mixins/Editing.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { LitElement } from 'lit'; - -import { property, state } from 'lit/decorators.js'; - -import { - AttributeValue, - Edit, - EditEvent, - Insert, - isComplex, - isInsert, - isNamespaced, - isRemove, - isUpdate, - LitElementConstructor, - OpenEvent, - Remove, - Update, -} from '../foundation.js'; - -function localAttributeName(attribute: string): string { - return attribute.includes(':') ? attribute.split(':', 2)[1] : attribute; -} - -function handleInsert({ - parent, - node, - reference, -}: Insert): Insert | Remove | [] { - try { - const { parentNode, nextSibling } = node; - parent.insertBefore(node, reference); - if (parentNode) - return { - node, - parent: parentNode, - reference: nextSibling, - }; - return { node }; - } catch (e) { - // do nothing if insert doesn't work on these nodes - return []; - } -} - -function handleUpdate({ element, attributes }: Update): Update { - const oldAttributes = { ...attributes }; - Object.entries(attributes) - .reverse() - .forEach(([name, value]) => { - let oldAttribute: AttributeValue; - if (isNamespaced(value!)) - oldAttribute = { - value: element.getAttributeNS( - value.namespaceURI, - localAttributeName(name) - ), - namespaceURI: value.namespaceURI, - }; - else - oldAttribute = element.getAttributeNode(name)?.namespaceURI - ? { - value: element.getAttribute(name), - namespaceURI: element.getAttributeNode(name)!.namespaceURI!, - } - : element.getAttribute(name); - oldAttributes[name] = oldAttribute; - }); - for (const entry of Object.entries(attributes)) { - try { - const [attribute, value] = entry as [string, AttributeValue]; - if (isNamespaced(value)) { - if (value.value === null) - element.removeAttributeNS( - value.namespaceURI, - localAttributeName(attribute) - ); - else element.setAttributeNS(value.namespaceURI, attribute, value.value); - } else if (value === null) element.removeAttribute(attribute); - else element.setAttribute(attribute, value); - } catch (e) { - // do nothing if update doesn't work on this attribute - delete oldAttributes[entry[0]]; - } - } - return { - element, - attributes: oldAttributes, - }; -} - -function handleRemove({ node }: Remove): Insert | [] { - const { parentNode: parent, nextSibling: reference } = node; - node.parentNode?.removeChild(node); - if (parent) - return { - node, - parent, - reference, - }; - return []; -} - -function handleEdit(edit: Edit): Edit { - if (isInsert(edit)) return handleInsert(edit); - if (isUpdate(edit)) return handleUpdate(edit); - if (isRemove(edit)) return handleRemove(edit); - if (isComplex(edit)) return edit.map(handleEdit).reverse(); - return []; -} - -export type LogEntry = { undo: Edit; redo: Edit }; - -export interface EditingMixin { - doc: XMLDocument; - history: LogEntry[]; - editCount: number; - last: number; - canUndo: boolean; - canRedo: boolean; - docs: Record; - docName: string; - handleOpenDoc(evt: OpenEvent): void; - handleEditEvent(evt: EditEvent): void; - undo(n?: number): void; - redo(n?: number): void; -} - -type ReturnConstructor = new (...args: any[]) => LitElement & EditingMixin; - -/** A mixin for editing a set of [[docs]] using [[EditEvent]]s */ -export function Editing( - Base: TBase -): TBase & ReturnConstructor { - class EditingElement extends Base { - @state() - /** The `XMLDocument` currently being edited */ - get doc(): XMLDocument { - return this.docs[this.docName]; - } - - @state() - history: LogEntry[] = []; - - @state() - editCount: number = 0; - - @state() - get last(): number { - return this.editCount - 1; - } - - @state() - get canUndo(): boolean { - return this.last >= 0; - } - - @state() - get canRedo(): boolean { - return this.editCount < this.history.length; - } - - /** - * The set of `XMLDocument`s currently loaded - * - * @prop {Record} docs - Record of loaded XML documents - */ - @state() - docs: Record = {}; - - /** - * The name of the [[`doc`]] currently being edited - * - * @prop {String} docName - name of the document that is currently being edited - */ - @property({ type: String, reflect: true }) docName = ''; - - handleOpenDoc({ detail: { docName, doc } }: OpenEvent) { - this.docName = docName; - this.docs[this.docName] = doc; - } - - handleEditEvent(event: EditEvent) { - const edit = event.detail.edit; - this.history.splice(this.editCount); - this.history.push({ undo: handleEdit(edit), redo: edit }); - this.editCount += 1; - } - - /** Undo the last `n` [[Edit]]s committed */ - undo(n = 1) { - if (!this.canUndo || n < 1) return; - handleEdit(this.history[this.last!].undo); - this.editCount -= 1; - if (n > 1) this.undo(n - 1); - } - - /** Redo the last `n` [[Edit]]s that have been undone */ - redo(n = 1) { - if (!this.canRedo || n < 1) return; - handleEdit(this.history[this.editCount].redo); - this.editCount += 1; - if (n > 1) this.redo(n - 1); - } - - constructor(...args: any[]) { - super(...args); - - this.addEventListener('oscd-open', this.handleOpenDoc); - this.addEventListener('oscd-edit', event => this.handleEditEvent(event)); - } - } - return EditingElement; -} diff --git a/packages/core/package.json b/packages/core/package.json index e8e82355ba..8c5eba0ffb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@openscd/core", - "version": "0.1.2", + "version": "0.1.3", "description": "The core editor component of open-scd, without any extensions pre-installed.", "author": "Open-SCD", "license": "Apache-2.0", diff --git a/packages/core/web-test-runner.config.js b/packages/core/web-test-runner.config.js index a88fa3d2f6..334e8ef62d 100644 --- a/packages/core/web-test-runner.config.js +++ b/packages/core/web-test-runner.config.js @@ -20,8 +20,8 @@ const filteredLogs = [ const browsers = [ playwrightLauncher({ product: 'chromium' }), - playwrightLauncher({ product: 'firefox' }), - playwrightLauncher({ product: 'webkit' }), + // playwrightLauncher({ product: 'firefox' }), + // playwrightLauncher({ product: 'webkit' }), ]; function defaultGetImageDiff({ baselineImage, image, options }) { diff --git a/packages/distribution/index.html b/packages/distribution/index.html index cbdee7c730..9d7b82c048 100644 --- a/packages/distribution/index.html +++ b/packages/distribution/index.html @@ -17,6 +17,8 @@ + ; - -/** @typeParam TBase - a type extending `LitElement` - * @returns `Base` with an `XMLDocument` property "`doc`" and an event listener - * applying [[`EditorActionEvent`]]s and dispatching [[`LogEvent`]]s. */ -export function Editing(Base: TBase) { - class EditingElement extends Base { - /** The `XMLDocument` to be edited */ - @property({ attribute: false }) - doc: XMLDocument | null = null; - /** The name of the current [[`doc`]] */ - @property({ type: String }) docName = ''; - /** The UUID of the current [[`doc`]] */ - @property({ type: String }) docId = ''; - - private checkCreateValidity(create: Create): boolean { - if (create.checkValidity !== undefined) return create.checkValidity(); - - if ( - !(create.new.element instanceof Element) || - !(create.new.parent instanceof Element) - ) - return true; - - const invalidNaming = - create.new.element.hasAttribute('name') && - Array.from(create.new.parent.children).some( - elm => - elm.tagName === (create.new.element).tagName && - elm.getAttribute('name') === - (create.new.element).getAttribute('name') - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.create', { - name: create.new.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: - create.new.parent instanceof HTMLElement - ? create.new.parent.tagName - : 'Document', - child: create.new.element.tagName, - name: create.new.element.getAttribute('name')!, - }), - }) - ); - - return false; - } - - const invalidId = - create.new.element.hasAttribute('id') && - Array.from( - create.new.parent.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some( - elm => - elm.getAttribute('id') === - (create.new.element).getAttribute('id') - ); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.create', { - name: create.new.element.tagName, - }), - message: get('editing.error.idClash', { - id: create.new.element.getAttribute('id')!, - }), - }) - ); - - return false; - } - - return true; - } - - private onCreate(action: Create) { - if (!this.checkCreateValidity(action)) return false; - - if ( - action.new.reference === undefined && - action.new.element instanceof Element && - action.new.parent instanceof Element - ) - action.new.reference = getReference( - action.new.parent, - action.new.element.tagName - ); - else action.new.reference = action.new.reference ?? null; - - action.new.parent.insertBefore(action.new.element, action.new.reference); - return true; - } - - private logCreate(action: Create) { - const name = - action.new.element instanceof Element - ? action.new.element.tagName - : get('editing.node'); - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.created', { name }), - action, - }) - ); - } - - private onDelete(action: Delete) { - if (!action.old.reference) - action.old.reference = action.old.element.nextSibling; - - if (action.old.element.parentNode !== action.old.parent) return false; - - action.old.parent.removeChild(action.old.element); - return true; - } - - private logDelete(action: Delete) { - const name = - action.old.element instanceof Element - ? action.old.element.tagName - : get('editing.node'); - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.deleted', { name }), - action, - }) - ); - } - - private checkMoveValidity(move: Move): boolean { - if (move.checkValidity !== undefined) return move.checkValidity(); - - const invalid = - move.old.element.hasAttribute('name') && - move.new.parent !== move.old.parent && - Array.from(move.new.parent.children).some( - elm => - elm.tagName === move.old.element.tagName && - elm.getAttribute('name') === move.old.element.getAttribute('name') - ); - - if (invalid) - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.move', { - name: move.old.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: move.new.parent.tagName, - child: move.old.element.tagName, - name: move.old.element.getAttribute('name')!, - }), - }) - ); - - return !invalid; - } - - private onMove(action: Move) { - if (!this.checkMoveValidity(action)) return false; - - if (!action.old.reference) - action.old.reference = action.old.element.nextSibling; - - if (action.new.reference === undefined) - action.new.reference = getReference( - action.new.parent, - action.old.element.tagName - ); - - action.new.parent.insertBefore(action.old.element, action.new.reference); - return true; - } - - private logMove(action: Move) { - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.moved', { - name: action.old.element.tagName, - }), - action: action, - }) - ); - } - - private checkReplaceValidity(replace: Replace): boolean { - if (replace.checkValidity !== undefined) return replace.checkValidity(); - - const invalidNaming = - replace.new.element.hasAttribute('name') && - replace.new.element.getAttribute('name') !== - replace.old.element.getAttribute('name') && - Array.from(replace.old.element.parentElement?.children ?? []).some( - elm => - elm.tagName === replace.new.element.tagName && - elm.getAttribute('name') === - replace.new.element.getAttribute('name') - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: replace.new.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: replace.old.element.parentElement!.tagName, - child: replace.new.element.tagName, - name: replace.new.element.getAttribute('name')!, - }), - }) - ); - - return false; - } - - const invalidId = - replace.new.element.hasAttribute('id') && - replace.new.element.getAttribute('id') !== - replace.old.element.getAttribute('id') && - Array.from( - replace.new.element.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some( - elm => - elm.getAttribute('id') === - (replace.new.element).getAttribute('id') - ); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: replace.new.element.tagName, - }), - message: get('editing.error.idClash', { - id: replace.new.element.getAttribute('id')!, - }), - }) - ); - - return false; - } - - return true; - } - - private onReplace(action: Replace) { - if (!this.checkReplaceValidity(action)) return false; - - action.new.element.append(...Array.from(action.old.element.children)); - action.old.element.replaceWith(action.new.element); - return true; - } - - private logUpdate(action: Replace | Update) { - const name = isReplace(action) - ? action.new.element.tagName - : (action as Update).element.tagName; - - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.updated', { - name, - }), - action: action, - }) - ); - } - - private checkUpdateValidity(update: Update): boolean { - if (update.checkValidity !== undefined) return update.checkValidity(); - - if (update.oldAttributes['name'] !== update.newAttributes['name']) { - const invalidNaming = Array.from( - update.element.parentElement?.children ?? [] - ).some( - elm => - elm.tagName === update.element.tagName && - elm.getAttribute('name') === update.newAttributes['name'] - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: update.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: update.element.parentElement!.tagName, - child: update.element.tagName, - name: update.newAttributes['name']!, - }), - }) - ); - - return false; - } - } - - const invalidId = - update.newAttributes['id'] && - Array.from( - update.element.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some(elm => elm.getAttribute('id') === update.newAttributes['id']); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: update.element.tagName, - }), - message: get('editing.error.idClash', { - id: update.newAttributes['id']!, - }), - }) - ); - - return false; - } - - return true; - } - - private onUpdate(action: Update) { - if (!this.checkUpdateValidity(action)) return false; - - Array.from(action.element.attributes).forEach(attr => - action.element.removeAttributeNode(attr) - ); - - Object.entries(action.newAttributes).forEach(([key, value]) => { - if (value !== null && value !== undefined) - action.element.setAttribute(key, value); - }); - - return true; - } - - private onSimpleAction(action: SimpleAction) { - if (isMove(action)) return this.onMove(action as Move); - else if (isCreate(action)) return this.onCreate(action as Create); - else if (isDelete(action)) return this.onDelete(action as Delete); - else if (isReplace(action)) return this.onReplace(action as Replace); - else if (isUpdate(action)) return this.onUpdate(action as Update); - } - - private logSimpleAction(action: SimpleAction) { - if (isMove(action)) this.logMove(action as Move); - else if (isCreate(action)) this.logCreate(action as Create); - else if (isDelete(action)) this.logDelete(action as Delete); - else if (isReplace(action)) this.logUpdate(action as Replace); - else if (isUpdate(action)) this.logUpdate(action as Update); - } - - private async onAction(event: EditorActionEvent) { - if (isSimple(event.detail.action)) { - if (this.onSimpleAction(event.detail.action)) - this.logSimpleAction(event.detail.action); - } else if (event.detail.action.actions.length > 0) { - event.detail.action.actions.forEach(element => - this.onSimpleAction(element) - ); - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: event.detail.action.title, - action: event.detail.action, - }) - ); - } else return; - - if (!this.doc) return; - - await this.updateComplete; - this.dispatchEvent(newValidateEvent()); - } - - /** - * - * @deprecated [Move to handleOpenDoc instead] - */ - private async onOpenDoc(event: OpenDocEvent) { - this.doc = event.detail.doc; - this.docName = event.detail.docName; - this.docId = event.detail.docId ?? ''; - - await this.updateComplete; - - this.dispatchEvent(newValidateEvent()); - - this.dispatchEvent( - newLogEvent({ - kind: 'info', - title: get('openSCD.loaded', { name: this.docName }), - }) - ); - } - - handleOpenDoc({ detail: { docName, doc } }: OpenEvent) { - this.doc = doc; - this.docName = docName; - } - - constructor(...args: any[]) { - super(...args); - - this.addEventListener('editor-action', this.onAction); - this.addEventListener('open-doc', this.onOpenDoc); - this.addEventListener('oscd-open', this.handleOpenDoc); - } - } - - return EditingElement; -} diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index c1ab8901a9..a2ea531dae 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -1,4 +1,4 @@ -import { OpenEvent, newEditCompletedEvent } from '@openscd/core'; +import { OpenEvent, newEditCompletedEvent, newEditEvent } from '@openscd/core'; import { property, LitElement, @@ -9,28 +9,29 @@ import { import { get } from 'lit-translate'; import { - Move, - Create, - Delete, EditorAction, EditorActionEvent, - SimpleAction, - Replace, - Update, } from '@openscd/core/foundation/deprecated/editor.js'; import { newLogEvent } from '@openscd/core/foundation/deprecated/history.js'; import { newValidateEvent } from '@openscd/core/foundation/deprecated/validation.js'; import { OpenDocEvent } from '@openscd/core/foundation/deprecated/open-event.js'; -import { getReference, SCLTag } from '../foundation.js'; + import { - isCreate, - isDelete, - isMove, - isSimple, - isReplace, + AttributeValue, + Edit, + EditEvent, + Insert, + isComplex, + isInsert, + isNamespaced, + isRemove, isUpdate, -} from '@openscd/core/foundation/deprecated/editor.js'; + Remove, + Update, +} from '@openscd/core'; + +import { convertEditV1toV2 } from './editor/edit-v1-to-v2-converter.js'; @customElement('oscd-editor') export class OscdEditor extends LitElement { @@ -47,435 +48,219 @@ export class OscdEditor extends LitElement { }) host!: HTMLElement; - @property({ - type: Number, - }) - editCount = -1; - - private checkCreateValidity(create: Create): boolean { - if (create.checkValidity !== undefined) return create.checkValidity(); - - if ( - !(create.new.element instanceof Element) || - !(create.new.parent instanceof Element) - ) - return true; - - const invalidNaming = - create.new.element.hasAttribute('name') && - Array.from(create.new.parent.children).some( - elm => - elm.tagName === (create.new.element).tagName && - elm.getAttribute('name') === - (create.new.element).getAttribute('name') - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.create', { - name: create.new.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: - create.new.parent instanceof HTMLElement - ? create.new.parent.tagName - : 'Document', - child: create.new.element.tagName, - name: create.new.element.getAttribute('name')!, - }), - }) - ); - - return false; + private getLogText(edit: Edit): { title: string, message?: string } { + if (isInsert(edit)) { + const name = edit.node instanceof Element ? + edit.node.tagName : + get('editing.node'); + return { title: get('editing.created', { name }) }; + } else if (isUpdate(edit)) { + const name = edit.element.tagName; + return { title: get('editing.updated', { name }) }; + } else if (isRemove(edit)) { + const name = edit.node instanceof Element ? + edit.node.tagName : + get('editing.node'); + return { title: get('editing.deleted', { name }) }; + } else if (isComplex(edit)) { + const message = edit.map(e => this.getLogText(e)).map(({ title }) => title).join(', '); + return { title: get('editing.complex'), message }; } - const invalidId = - create.new.element.hasAttribute('id') && - Array.from( - create.new.parent.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some( - elm => - elm.getAttribute('id') === - (create.new.element).getAttribute('id') - ); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.create', { - name: create.new.element.tagName, - }), - message: get('editing.error.idClash', { - id: create.new.element.getAttribute('id')!, - }), - }) - ); - - return false; - } - - return true; - } - - private onCreate(action: Create) { - if (!this.checkCreateValidity(action)) return false; - - if ( - action.new.reference === undefined && - action.new.element instanceof Element && - action.new.parent instanceof Element - ) - action.new.reference = getReference( - action.new.parent, - action.new.element.tagName - ); - else action.new.reference = action.new.reference ?? null; - - action.new.parent.insertBefore(action.new.element, action.new.reference); - return true; + return { title: '' }; } - private logCreate(action: Create) { - const name = - action.new.element instanceof Element - ? action.new.element.tagName - : get('editing.node'); + private onAction(event: EditorActionEvent) { + const edit = convertEditV1toV2(event.detail.action); + const initiator = event.detail.initiator; - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.created', { name }), - action, - }) - ); + this.host.dispatchEvent(newEditEvent(edit, initiator)); } - private onDelete(action: Delete) { - if (!action.old.reference) - action.old.reference = action.old.element.nextSibling; - - if (action.old.element.parentNode !== action.old.parent) return false; + /** + * + * @deprecated [Move to handleOpenDoc instead] + */ + private async onOpenDoc(event: OpenDocEvent) { + this.doc = event.detail.doc; + this.docName = event.detail.docName; + this.docId = event.detail.docId ?? ''; - action.old.parent.removeChild(action.old.element); - return true; - } + await this.updateComplete; - private logDelete(action: Delete) { - const name = - action.old.element instanceof Element - ? action.old.element.tagName - : get('editing.node'); + this.dispatchEvent(newValidateEvent()); this.dispatchEvent( newLogEvent({ - kind: 'action', - title: get('editing.deleted', { name }), - action, + kind: 'info', + title: get('openSCD.loaded', { name: this.docName }), }) ); } - private checkMoveValidity(move: Move): boolean { - if (move.checkValidity !== undefined) return move.checkValidity(); - - const invalid = - move.old.element.hasAttribute('name') && - move.new.parent !== move.old.parent && - Array.from(move.new.parent.children).some( - elm => - elm.tagName === move.old.element.tagName && - elm.getAttribute('name') === move.old.element.getAttribute('name') - ); - - if (invalid) - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.move', { - name: move.old.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: move.new.parent.tagName, - child: move.old.element.tagName, - name: move.old.element.getAttribute('name')!, - }), - }) - ); - - return !invalid; + handleOpenDoc({ detail: { docName, doc } }: OpenEvent) { + this.doc = doc; + this.docName = docName; } - private onMove(action: Move) { - if (!this.checkMoveValidity(action)) return false; - - if (!action.old.reference) - action.old.reference = action.old.element.nextSibling; + connectedCallback(): void { + super.connectedCallback(); - if (action.new.reference === undefined) - action.new.reference = getReference( - action.new.parent, - action.old.element.tagName - ); + // Deprecated editor action API, use 'oscd-edit' instead. + this.host.addEventListener('editor-action', this.onAction.bind(this)); - action.new.parent.insertBefore(action.old.element, action.new.reference); - return true; + this.host.addEventListener('oscd-edit', event => this.handleEditEvent(event)); + this.host.addEventListener('open-doc', this.onOpenDoc); + this.host.addEventListener('oscd-open', this.handleOpenDoc); } - private logMove(action: Move) { - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.moved', { - name: action.old.element.tagName, - }), - action: action, - }) - ); + render(): TemplateResult { + return html``; } - private checkReplaceValidity(replace: Replace): boolean { - if (replace.checkValidity !== undefined) return replace.checkValidity(); - - const invalidNaming = - replace.new.element.hasAttribute('name') && - replace.new.element.getAttribute('name') !== - replace.old.element.getAttribute('name') && - Array.from(replace.old.element.parentElement?.children ?? []).some( - elm => - elm.tagName === replace.new.element.tagName && - elm.getAttribute('name') === replace.new.element.getAttribute('name') - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: replace.new.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: replace.old.element.parentElement!.tagName, - child: replace.new.element.tagName, - name: replace.new.element.getAttribute('name')!, - }), - }) - ); - - return false; + async handleEditEvent(event: EditEvent) { + /** + * This is a compatibility fix for plugins based on open energy tools edit events + * because their edit event look slightly different + * see https://github.com/OpenEnergyTools/open-scd-core/blob/main/foundation/edit-event-v1.ts for details + */ + if (isOpenEnergyEditEvent(event)) { + event = convertOpenEnergyEditEventToEditEvent(event); } - const invalidId = - replace.new.element.hasAttribute('id') && - replace.new.element.getAttribute('id') !== - replace.old.element.getAttribute('id') && - Array.from( - replace.new.element.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some( - elm => - elm.getAttribute('id') === - (replace.new.element).getAttribute('id') - ); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: replace.new.element.tagName, - }), - message: get('editing.error.idClash', { - id: replace.new.element.getAttribute('id')!, - }), - }) - ); - - return false; - } - - return true; - } - - private onReplace(action: Replace) { - if (!this.checkReplaceValidity(action)) return false; - - action.new.element.append(...Array.from(action.old.element.children)); - action.old.element.replaceWith(action.new.element); - return true; - } - - private logUpdate(action: Replace | Update) { - const name = isReplace(action) - ? action.new.element.tagName - : (action as Update).element.tagName; + const edit = event.detail.edit; + const undoEdit = handleEdit(edit); this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: get('editing.updated', { - name, - }), - action: action, - }) + newEditCompletedEvent(event.detail.edit, event.detail.initiator) ); - } - private checkUpdateValidity(update: Update): boolean { - if (update.checkValidity !== undefined) return update.checkValidity(); - - if (update.oldAttributes['name'] !== update.newAttributes['name']) { - const invalidNaming = Array.from( - update.element.parentElement?.children ?? [] - ).some( - elm => - elm.tagName === update.element.tagName && - elm.getAttribute('name') === update.newAttributes['name'] - ); - - if (invalidNaming) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: update.element.tagName, - }), - message: get('editing.error.nameClash', { - parent: update.element.parentElement!.tagName, - child: update.element.tagName, - name: update.newAttributes['name']!, - }), - }) - ); - - return false; - } - } - - const invalidId = - update.newAttributes['id'] && - Array.from( - update.element.ownerDocument.querySelectorAll( - 'LNodeType, DOType, DAType, EnumType' - ) - ).some(elm => elm.getAttribute('id') === update.newAttributes['id']); - - if (invalidId) { - this.dispatchEvent( - newLogEvent({ - kind: 'error', - title: get('editing.error.update', { - name: update.element.tagName, - }), - message: get('editing.error.idClash', { - id: update.newAttributes['id']!, - }), - }) - ); - - return false; - } - - return true; - } - - private onUpdate(action: Update) { - if (!this.checkUpdateValidity(action)) return false; - - Array.from(action.element.attributes).forEach(attr => - action.element.removeAttributeNode(attr) - ); + const shouldCreateHistoryEntry = event.detail.initiator !== 'redo' && event.detail.initiator !== 'undo'; - Object.entries(action.newAttributes).forEach(([key, value]) => { - if (value !== null && value !== undefined) - action.element.setAttribute(key, value); - }); - - return true; - } - - private onSimpleAction(action: SimpleAction) { - if (isMove(action)) return this.onMove(action as Move); - else if (isCreate(action)) return this.onCreate(action as Create); - else if (isDelete(action)) return this.onDelete(action as Delete); - else if (isReplace(action)) return this.onReplace(action as Replace); - else if (isUpdate(action)) return this.onUpdate(action as Update); - } - - private logSimpleAction(action: SimpleAction) { - if (isMove(action)) this.logMove(action as Move); - else if (isCreate(action)) this.logCreate(action as Create); - else if (isDelete(action)) this.logDelete(action as Delete); - else if (isReplace(action)) this.logUpdate(action as Replace); - else if (isUpdate(action)) this.logUpdate(action as Update); - } + if (shouldCreateHistoryEntry) { + const { title, message } = this.getLogText(edit); - private async onAction(event: EditorActionEvent) { - if (isSimple(event.detail.action)) { - if (this.onSimpleAction(event.detail.action)) - this.logSimpleAction(event.detail.action); - } else if (event.detail.action.actions.length > 0) { - event.detail.action.actions.forEach(element => - this.onSimpleAction(element) - ); - this.dispatchEvent( - newLogEvent({ - kind: 'action', - title: event.detail.action.title, - action: event.detail.action, - }) - ); - } else return; - - if (!this.doc) return; + this.dispatchEvent(newLogEvent({ + kind: 'action', + title, + message, + redo: edit, + undo: undoEdit, + })); + } await this.updateComplete; this.dispatchEvent(newValidateEvent()); - this.dispatchEvent( - newEditCompletedEvent(event.detail.action, event.detail.initiator) - ); } +} - /** - * - * @deprecated [Move to handleOpenDoc instead] - */ - private async onOpenDoc(event: OpenDocEvent) { - this.doc = event.detail.doc; - this.docName = event.detail.docName; - this.docId = event.detail.docId ?? ''; +function handleEdit(edit: Edit): Edit { + if (isInsert(edit)) return handleInsert(edit); + if (isUpdate(edit)) return handleUpdate(edit); + if (isRemove(edit)) return handleRemove(edit); + if (isComplex(edit)) return edit.map(handleEdit).reverse(); + return []; +} - await this.updateComplete; +function localAttributeName(attribute: string): string { + return attribute.includes(':') ? attribute.split(':', 2)[1] : attribute; +} - this.dispatchEvent(newValidateEvent()); +function handleInsert({ + parent, + node, + reference, +}: Insert): Insert | Remove | [] { + try { + const { parentNode, nextSibling } = node; + + /** + * This is a workaround for converted edit api v1 events, + * because if multiple edits are converted, they are converted before the changes from the previous edits are applied to the document + * so if you first remove an element and then add a clone with changed attributes, the reference will be the element to remove since it hasnt been removed yet + */ + if (!parent.contains(reference)) { + reference = null; + } - this.dispatchEvent( - newLogEvent({ - kind: 'info', - title: get('openSCD.loaded', { name: this.docName }), - }) - ); + parent.insertBefore(node, reference); + if (parentNode) + return { + node, + parent: parentNode, + reference: nextSibling, + }; + return { node }; + } catch (e) { + // do nothing if insert doesn't work on these nodes + return []; } +} - handleOpenDoc({ detail: { docName, doc } }: OpenEvent) { - this.doc = doc; - this.docName = docName; +function handleUpdate({ element, attributes }: Update): Update { + const oldAttributes = { ...attributes }; + Object.entries(attributes) + .reverse() + .forEach(([name, value]) => { + let oldAttribute: AttributeValue; + if (isNamespaced(value!)) + oldAttribute = { + value: element.getAttributeNS( + value.namespaceURI, + localAttributeName(name) + ), + namespaceURI: value.namespaceURI, + }; + else + oldAttribute = element.getAttributeNode(name)?.namespaceURI + ? { + value: element.getAttribute(name), + namespaceURI: element.getAttributeNode(name)!.namespaceURI!, + } + : element.getAttribute(name); + oldAttributes[name] = oldAttribute; + }); + for (const entry of Object.entries(attributes)) { + try { + const [attribute, value] = entry as [string, AttributeValue]; + if (isNamespaced(value)) { + if (value.value === null) + element.removeAttributeNS( + value.namespaceURI, + localAttributeName(attribute) + ); + else element.setAttributeNS(value.namespaceURI, attribute, value.value); + } else if (value === null) element.removeAttribute(attribute); + else element.setAttribute(attribute, value); + } catch (e) { + // do nothing if update doesn't work on this attribute + delete oldAttributes[entry[0]]; + } } + return { + element, + attributes: oldAttributes, + }; +} - connectedCallback(): void { - super.connectedCallback(); +function handleRemove({ node }: Remove): Insert | [] { + const { parentNode: parent, nextSibling: reference } = node; + node.parentNode?.removeChild(node); + if (parent) + return { + node, + parent, + reference, + }; + return []; +} - this.host.addEventListener('editor-action', this.onAction.bind(this)); - this.host.addEventListener('open-doc', this.onOpenDoc); - this.host.addEventListener('oscd-open', this.handleOpenDoc); - } +function isOpenEnergyEditEvent(event: CustomEvent): boolean { + const eventDetail = event.detail as Edit; + return isComplex(eventDetail) || isInsert(eventDetail) || isUpdate(eventDetail) || isRemove(eventDetail); +} - render(): TemplateResult { - return html``; - } +function convertOpenEnergyEditEventToEditEvent(event: CustomEvent): EditEvent { + const eventDetail = event.detail as Edit; + return newEditEvent(eventDetail); } diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index eac9e5b1ab..eae7c5d7c8 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -35,14 +35,28 @@ import { LogEvent, } from '@openscd/core/foundation/deprecated/history.js'; -import { - newActionEvent, - invert, -} from '@openscd/core/foundation/deprecated/editor.js'; - import { getFilterIcon, iconColors } from '../icons/icons.js'; -import { Plugin } from '../open-scd.js'; +import { Plugin } from '../plugin.js'; +import { newEditEvent } from '@openscd/core'; + +export const historyStateEvent = 'history-state'; +export interface HistoryState { + editCount: number; + canUndo: boolean; + canRedo: boolean; +} +export type HistoryStateEvent = CustomEvent; + +function newHistoryStateEvent(state: HistoryState): HistoryStateEvent { + return new CustomEvent(historyStateEvent, { detail: state }); +} + +declare global { + interface ElementEventMap { + [historyStateEvent]: HistoryStateEvent; + } +} const icons = { info: 'info', @@ -197,18 +211,20 @@ export class OscdHistory extends LitElement { undo(): boolean { if (!this.canUndo) return false; - const invertedAction = invert( - (this.history[this.editCount]).action - ); - this.dispatchEvent(newActionEvent(invertedAction, 'undo')); - this.editCount = this.previousAction; + + const undoEdit = (this.history[this.editCount]).undo; + this.host.dispatchEvent(newEditEvent(undoEdit, 'undo')); + this.setEditCount(this.previousAction); + return true; } redo(): boolean { if (!this.canRedo) return false; - const nextAction = (this.history[this.nextAction]).action; - this.dispatchEvent(newActionEvent(nextAction, 'redo')); - this.editCount = this.nextAction; + + const redoEdit = (this.history[this.nextAction]).redo; + this.host.dispatchEvent(newEditEvent(redoEdit, 'redo')); + this.setEditCount(this.nextAction); + return true; } @@ -218,21 +234,34 @@ export class OscdHistory extends LitElement { ...detail, }; - if (entry.kind === 'action') { - if (entry.action.derived) return; - entry.action.derived = true; - if (this.nextAction !== -1) this.history.splice(this.nextAction); - this.editCount = this.history.length; + if (this.nextAction !== -1) { + this.history.splice(this.nextAction); } this.history.push(entry); + this.setEditCount(this.history.length - 1); this.requestUpdate('history', []); } private onReset() { this.log = []; this.history = []; - this.editCount = -1; + this.setEditCount(-1); + } + + private setEditCount(count: number): void { + this.editCount = count; + this.dispatchHistoryStateEvent(); + } + + private dispatchHistoryStateEvent(): void { + this.host.dispatchEvent( + newHistoryStateEvent({ + editCount: this.editCount, + canUndo: this.canUndo, + canRedo: this.canRedo + }) + ); } private onInfo(detail: InfoDetail) { @@ -310,6 +339,7 @@ export class OscdHistory extends LitElement { this.historyUIHandler = this.historyUIHandler.bind(this); this.emptyIssuesHandler = this.emptyIssuesHandler.bind(this); this.handleKeyPress = this.handleKeyPress.bind(this); + this.dispatchHistoryStateEvent = this.dispatchHistoryStateEvent.bind(this); document.onkeydown = this.handleKeyPress; } @@ -403,7 +433,7 @@ export class OscdHistory extends LitElement {
    `; } - private renderIssueEntry(issue: IssueDetail): TemplateResult { + protected renderIssueEntry(issue: IssueDetail): TemplateResult { return html` ${issue.title} diff --git a/packages/openscd/src/addons/Layout.ts b/packages/openscd/src/addons/Layout.ts index 4080a4fb00..41d56a8cd7 100644 --- a/packages/openscd/src/addons/Layout.ts +++ b/packages/openscd/src/addons/Layout.ts @@ -13,18 +13,23 @@ import { newPendingStateEvent } from '@openscd/core/foundation/deprecated/waiter import { newSettingsUIEvent } from '@openscd/core/foundation/deprecated/settings.js'; import { MenuItem, - Plugin, Validator, - PluginKind, - MenuPosition, MenuPlugin, - menuPosition, pluginIcons, newResetPluginsEvent, newAddExternalPluginEvent, newSetPluginsEvent, } from '../open-scd.js'; + +import { + MenuPosition, + Plugin, + menuPosition, + PluginKind, +} from "../plugin.js" + import { + HistoryState, HistoryUIKind, newEmptyIssuesEvent, newHistoryUIEvent, @@ -51,6 +56,20 @@ import { EditCompletedEvent } from '@openscd/core'; @customElement('oscd-layout') export class OscdLayout extends LitElement { + + render(): TemplateResult { + return html` +
    this.pluginDownloadUI.show()} + > + + ${this.renderHeader()} ${this.renderAside()} ${this.renderContent()} + ${this.renderLanding()} ${this.renderPlugging()} +
    + `; + } + + /** The `XMLDocument` to be edited */ @property({ attribute: false }) doc: XMLDocument | null = null; @@ -72,23 +91,15 @@ export class OscdLayout extends LitElement { @property({ type: Object }) host!: HTMLElement; + @property({ type: Object }) + historyState!: HistoryState; + @state() validated: Promise = Promise.resolve(); @state() shouldValidate = false; - @state() - redoCount = 0; - - get canUndo(): boolean { - return this.editCount >= 0; - } - - get canRedo(): boolean { - return this.redoCount > 0; - } - @query('#menu') menuUI!: Drawer; @query('#pluginManager') @@ -120,97 +131,13 @@ export class OscdLayout extends LitElement { return this.menuEntries.filter(plugin => plugin.position === 'bottom'); } - get menu(): (MenuItem | 'divider')[] { - const topMenu: (MenuItem | 'divider')[] = []; - const middleMenu: (MenuItem | 'divider')[] = []; - const bottomMenu: (MenuItem | 'divider')[] = []; - const validators: (MenuItem | 'divider')[] = []; - - this.topMenu.forEach(plugin => - topMenu.push({ - icon: plugin.icon || pluginIcons['menu'], - name: plugin.name, - action: ae => { - this.dispatchEvent( - newPendingStateEvent( - (( - (( - (ae.target).items[ae.detail.index].nextElementSibling - )) - )).run() - ) - ); - }, - disabled: (): boolean => plugin.requireDoc! && this.doc === null, - content: plugin.content, - kind: 'top', - }) - ); - - this.middleMenu.forEach(plugin => - middleMenu.push({ - icon: plugin.icon || pluginIcons['menu'], - name: plugin.name, - action: ae => { - this.dispatchEvent( - newPendingStateEvent( - (( - (( - (ae.target).items[ae.detail.index].nextElementSibling - )) - )).run() - ) - ); - }, - disabled: (): boolean => plugin.requireDoc! && this.doc === null, - content: plugin.content, - kind: 'middle', - }) - ); - this.bottomMenu.forEach(plugin => - bottomMenu.push({ - icon: plugin.icon || pluginIcons['menu'], - name: plugin.name, - action: ae => { - this.dispatchEvent( - newPendingStateEvent( - (( - (( - (ae.target).items[ae.detail.index].nextElementSibling - )) - )).run() - ) - ); - }, - disabled: (): boolean => plugin.requireDoc! && this.doc === null, - content: plugin.content, - kind: 'middle', - }) - ); + get menu(): (MenuItem | 'divider')[] { - this.validators.forEach(plugin => - validators.push({ - icon: plugin.icon || pluginIcons['validator'], - name: plugin.name, - action: ae => { - this.dispatchEvent(newEmptyIssuesEvent(plugin.src)); - - this.dispatchEvent( - newPendingStateEvent( - (( - (( - (ae.target).items[ae.detail.index].nextElementSibling - )) - )).validate() - ) - ); - }, - disabled: (): boolean => this.doc === null, - content: plugin.content, - kind: 'validator', - }) - ); + const topMenu = this.generateMenu(this.topMenu, 'top'); + const middleMenu = this.generateMenu(this.middleMenu, 'middle'); + const bottomMenu = this.generateMenu(this.bottomMenu, 'bottom'); + const validators = this.generateValidatorMenus(this.validators); if (middleMenu.length > 0) middleMenu.push('divider'); if (bottomMenu.length > 0) bottomMenu.push('divider'); @@ -226,7 +153,7 @@ export class OscdLayout extends LitElement { action: (): void => { this.dispatchEvent(newUndoEvent()); }, - disabled: (): boolean => !this.canUndo, + disabled: (): boolean => !this.historyState.canUndo, kind: 'static', }, { @@ -236,7 +163,7 @@ export class OscdLayout extends LitElement { action: (): void => { this.dispatchEvent(newRedoEvent()); }, - disabled: (): boolean => !this.canRedo, + disabled: (): boolean => !this.historyState.canRedo, kind: 'static', }, ...validators, @@ -295,26 +222,22 @@ export class OscdLayout extends LitElement { // Keyboard Shortcuts private handleKeyPress(e: KeyboardEvent): void { - let handled = false; - const ctrlAnd = (key: string) => - e.key === key && e.ctrlKey && (handled = true); - - if (ctrlAnd('m')) this.menuUI.open = !this.menuUI.open; - if (ctrlAnd('o')) - this.menuUI - .querySelector('mwc-list-item[iconid="folder_open"]') - ?.click(); - if (ctrlAnd('O')) - this.menuUI - .querySelector('mwc-list-item[iconid="create_new_folder"]') - ?.click(); - if (ctrlAnd('s')) - this.menuUI - .querySelector('mwc-list-item[iconid="save"]') - ?.click(); - if (ctrlAnd('P')) this.pluginUI.show(); - - if (handled) e.preventDefault(); + // currently we only handley key shortcuts when users press ctrl + if(!e.ctrlKey){ return } + + const keyFunctionMap: {[key:string]: () => void} = { + 'm': () => this.menuUI.open = !this.menuUI.open, + 'o': () => this.menuUI.querySelector('mwc-list-item[iconid="folder_open"]')?.click(), + 'O': () => this.menuUI.querySelector('mwc-list-item[iconid="create_new_folder"]')?.click(), + 's': () => this.menuUI.querySelector('mwc-list-item[iconid="save"]')?.click(), + 'P': () => this.pluginUI.show(), + } + + const fn = keyFunctionMap[e.key]; + if(!fn){ return; } + + e.preventDefault(); + fn(); } private handleAddPlugin() { @@ -370,7 +293,7 @@ export class OscdLayout extends LitElement { this.shouldValidate = true; await this.validated; - if (!this.shouldValidate) return; + if (!this.shouldValidate){ return; } this.shouldValidate = false; @@ -387,26 +310,64 @@ export class OscdLayout extends LitElement { this.handleKeyPress = this.handleKeyPress.bind(this); document.onkeydown = this.handleKeyPress; - this.host.addEventListener( - 'oscd-edit-completed', - (evt: EditCompletedEvent) => { - const initiator = evt.detail.initiator; + document.addEventListener("open-plugin-download", () => { + this.pluginDownloadUI.show(); + }); + } - if (initiator === 'undo') { - this.redoCount += 1; - } else if (initiator === 'redo') { - this.redoCount -= 1; - } - this.requestUpdate(); + private generateMenu(plugins:Plugin[], kind: 'top' | 'middle' | 'bottom'): (MenuItem | 'divider')[]{ + return plugins.map(plugin => { + return { + icon: plugin.icon || pluginIcons['menu'], + name: plugin.name, + action: ae => { + this.dispatchEvent( + newPendingStateEvent( + (( + (( + (ae.target).items[ae.detail.index].nextElementSibling + )) + )).run() + ) + ); + }, + disabled: (): boolean => plugin.requireDoc! && this.doc === null, + content: plugin.content, + kind: kind, } - ); + }) + } + + private generateValidatorMenus(plugins: Plugin[]): (MenuItem | 'divider')[] { + return plugins.map(plugin =>{ + return { + icon: plugin.icon || pluginIcons['validator'], + name: plugin.name, + action: ae => { + this.dispatchEvent(newEmptyIssuesEvent(plugin.src)); + + this.dispatchEvent( + newPendingStateEvent( + (( + (( + (ae.target).items[ae.detail.index].nextElementSibling + )) + )).validate() + ) + ); + }, + disabled: (): boolean => this.doc === null, + content: plugin.content, + kind: 'validator', + } + }); } private renderMenuItem(me: MenuItem | 'divider'): TemplateResult { - if (me === 'divider') - return html`
  • `; - if (me.actionItem) return html``; + if (me === 'divider') { return html`
  • `; } + if (me.actionItem){ return html``; } + return html` `; - else return html``; + if(me === 'divider' || !me.actionItem){ return html`` } + + return html` + `; } private renderEditorTab({ name, icon }: Plugin): TemplateResult { - return html` `; + return html` `; } /** Renders top bar which features icon buttons for undo, redo, log, scl history and diagnostics*/ @@ -453,76 +415,126 @@ export class OscdLayout extends LitElement { `; } - /** Renders a drawer toolbar featuring the scl filename, enabled menu plugins, settings, help, scl history and plug-ins management */ + /** + * Renders a drawer toolbar featuring the scl filename, enabled menu plugins, + * settings, help, scl history and plug-ins management + */ protected renderAside(): TemplateResult { + return html` ${get('menu.title')} - ${this.docName - ? html`${this.docName}` - : ''} + ${renderTitle(this.docName)} ) => { - //FIXME: dirty hack to be fixed in open-scd-core - // if clause not necessary when oscd... components in open-scd not list - if (ae.target instanceof List) - (( - this.menu.filter( - item => item !== 'divider' && !item.actionItem - )[ae.detail.index] - ))?.action?.(ae); - }} + @action=${makeListAction(this.menu)} > ${this.menu.map(this.renderMenuItem)} `; + + function renderTitle(docName?: string){ + if(!docName) return html``; + + return html`${docName}`; + } + + function makeListAction(menuItems : (MenuItem|'divider')[]){ + return function listAction(ae: CustomEvent){ + //FIXME: dirty hack to be fixed in open-scd-core + // if clause not necessary when oscd... components in open-scd not list + if (ae.target instanceof List) + (( + menuItems.filter( + item => item !== 'divider' && !item.actionItem + )[ae.detail.index] + ))?.action?.(ae); + } + } + + } /** Renders the enabled editor plugins and a tab bar to switch between them*/ protected renderContent(): TemplateResult { + const hasActiveDoc = Boolean(this.doc); + + const activeEditors = this.editors + .filter(editor => { + // this is necessary because `requireDoc` can be undefined + // and that is not the same as false + const doesNotRequireDoc = editor.requireDoc === false + return doesNotRequireDoc || hasActiveDoc + }) + .map(this.renderEditorTab) + + const hasActiveEditors = activeEditors.length > 0; + if(!hasActiveEditors){ return html``; } + return html` - ${this.doc - ? html` - (this.activeTab = e.detail.index)} - > - ${this.editors.map(this.renderEditorTab)} - - ${this.editors[this.activeTab]?.content - ? this.editors[this.activeTab].content - : ``}` - : ``} + (this.activeTab = e.detail.index)}> + ${activeEditors} + + ${renderEditorContent(this.editors, this.activeTab, this.doc)} `; + + function renderEditorContent(editors: Plugin[], activeTab: number, doc: XMLDocument | null){ + const editor = editors[activeTab]; + const requireDoc = editor?.requireDoc + if(requireDoc && !doc) { return html`` } + + const content = editor?.content; + if(!content) { return html`` } + + return html`${content}`; + } } - /** Renders the landing buttons (open project and new project)*/ + /** + * Renders the landing buttons (open project and new project) + * it no document loaded we display the menu item that are in the position + * 'top' and are not disabled + * + * To enable replacement of this part we have to convert it to either an addon + * or a plugin + */ protected renderLanding(): TemplateResult { - return html` ${!this.doc - ? html`
    - ${(this.menu.filter(mi => mi !== 'divider')).map( - (mi: MenuItem, index) => - mi.kind === 'top' && !mi.disabled?.() - ? html` - -
    ${mi.name}
    -
    - ` - : html`` - )} -
    ` - : ``}`; - } + if(this.doc){ return html``; } + + return html` +
    + ${renderMenuItems(this.menu, this.menuUI)} +
    ` + + function renderMenuItems(menuItemsAndDividers: (MenuItem | 'divider')[], menuUI: Drawer){ + + const menuItems = menuItemsAndDividers.filter(mi => mi !== 'divider') as MenuItem[]; + + return menuItems.map((mi: MenuItem, index) => { + if(mi.kind !== 'top' || mi.disabled?.()) { return html``; } + + return html` + +
    ${mi.name}
    +
    + ` + }) + + function clickListItem(index:number) { + const listItem = menuUI.querySelector('mwc-list')!.items[index]; + listItem.click(); + } + + } + } /** Renders the "Add Custom Plug-in" UI*/ + // TODO: this should be its own isolated element protected renderDownloadUI(): TemplateResult { return html` @@ -615,30 +627,42 @@ export class OscdLayout extends LitElement { `; } + // Note: why is the type here if note used? private renderPluginKind( type: PluginKind | MenuPosition, plugins: Plugin[] ): TemplateResult { return html` - ${plugins.map( - plugin => - html` html` + ) => { + if(e.detail.source !== 'interaction'){ + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + return false; + } + }} hasMeta left > - ${plugin.icon || pluginIcons[plugin.kind]} + + ${plugin.icon || pluginIcons[plugin.kind]} + ${plugin.name} - ` + + ` )} `; } - /** Renders the plug-in management UI (turning plug-ins on/off)*/ + /** + * Renders the plug-in management UI (turning plug-ins on/off) + * TODO: this is big enough to be its own isolated element + */ protected renderPluginUI(): TemplateResult { return html` - ${this.renderHeader()} ${this.renderAside()} ${this.renderContent()} - ${this.renderLanding()} ${this.renderPlugging()} - `; - } + static styles = css` mwc-drawer { diff --git a/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts new file mode 100644 index 0000000000..f33d76d27f --- /dev/null +++ b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts @@ -0,0 +1,130 @@ +import { + Create, + Delete, + EditorAction, + isCreate, + isDelete, + isMove, + isReplace, + isSimple, + isUpdate, + Move, + Replace, + SimpleAction, + Update +} from '@openscd/core/foundation/deprecated/editor.js'; +import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; +import { getReference, SCLTag } from '../../foundation.js'; + + +export function convertEditV1toV2(action: EditorAction): Edit { + if (isSimple(action)) { + return convertSimpleAction(action); + } else { + return action.actions.map(convertSimpleAction); + } +} + +function convertSimpleAction(action: SimpleAction): Edit { + if (isCreate(action)) { + return convertCreate(action); + } else if (isDelete(action)) { + return convertDelete(action); + } else if (isUpdate(action)) { + return convertUpdate(action); + } else if (isMove(action)) { + return convertMove(action); + } else if (isReplace(action)) { + return convertReplace(action); + } + + throw new Error('Unknown action type'); +} + +function convertCreate(action: Create): Insert { + let reference: Node | null = null; + if ( + action.new.reference === undefined && + action.new.element instanceof Element && + action.new.parent instanceof Element + ) { + reference = getReference( + action.new.parent, + action.new.element.tagName + ); + } else { + reference = action.new.reference ?? null; + } + + return { + parent: action.new.parent, + node: action.new.element, + reference + }; +} + +function convertDelete(action: Delete): Remove { + return { + node: action.old.element + }; +} + +function convertUpdate(action: Update): UpdateV2 { + const oldAttributesToRemove: Record = {}; + Array.from(action.element.attributes).forEach(attr => { + oldAttributesToRemove[attr.name] = null; + }); + + const attributes = { + ...oldAttributesToRemove, + ...action.newAttributes + }; + + return { + element: action.element, + attributes + }; +} + +function convertMove(action: Move): Insert { + if (action.new.reference === undefined) { + action.new.reference = getReference( + action.new.parent, + action.old.element.tagName + ); + } + + return { + parent: action.new.parent, + node: action.old.element, + reference: action.new.reference ?? null + } +} + +function convertReplace(action: Replace): Edit { + const oldChildren = action.old.element.children; + // We have to clone the children, because otherwise undoing the action would remove the children from the old element, because append removes the old parent + const copiedChildren = Array.from(oldChildren).map(e => e.cloneNode(true)); + + const newNode = action.new.element.cloneNode(true) as Element; + newNode.append(...Array.from(copiedChildren)); + const parent = action.old.element.parentElement; + + if (!parent) { + throw new Error('Replace action called without parent in old element'); + } + + const reference = action.old.element.nextSibling; + + const remove: Remove = { node: action.old.element }; + const insert: Insert = { + parent, + node: newNode, + reference + }; + + return [ + remove, + insert + ]; +} diff --git a/packages/openscd/src/foundation/nsdoc.ts b/packages/openscd/src/foundation/nsdoc.ts index 53775fea79..810969c6ab 100644 --- a/packages/openscd/src/foundation/nsdoc.ts +++ b/packages/openscd/src/foundation/nsdoc.ts @@ -10,12 +10,17 @@ export interface Nsdoc { const [nsd72, nsd73, nsd74, nsd81] = await Promise.all([iec6185072, iec6185073, iec6185074, iec6185081]); +let nsdoc72: Document | undefined = undefined; +let nsdoc73: Document | undefined = undefined; +let nsdoc74: Document | undefined = undefined; +let nsdoc81: Document | undefined = undefined; + /** * Initialize the full Nsdoc object. * @returns A fully initialized Nsdoc object for wizards/editors to use. */ export function initializeNsdoc(): Nsdoc { - const [nsdoc72, nsdoc73, nsdoc74, nsdoc81] = [ + [nsdoc72, nsdoc73, nsdoc74, nsdoc81] = [ localStorage.getItem('IEC 61850-7-2') ? new DOMParser().parseFromString(localStorage.getItem('IEC 61850-7-2')!, 'application/xml') : undefined, localStorage.getItem('IEC 61850-7-3') ? new DOMParser().parseFromString(localStorage.getItem('IEC 61850-7-3')!, 'application/xml') : undefined, localStorage.getItem('IEC 61850-7-4') ? new DOMParser().parseFromString(localStorage.getItem('IEC 61850-7-4')!, 'application/xml') : undefined, diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index ce7229ec12..2a7976c3aa 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -38,7 +38,7 @@ import './addons/Layout.js'; import { ActionDetail } from '@material/mwc-list'; -import { officialPlugins } from './plugins.js'; +import { officialPlugins as builtinPlugins } from './plugins.js'; import { initializeNsdoc, Nsdoc } from './foundation/nsdoc.js'; import type { PluginSet, @@ -46,6 +46,12 @@ import type { EditCompletedEvent, } from '@openscd/core'; +import { HistoryState, historyStateEvent } from './addons/History.js'; + +import { InstalledOfficialPlugin, MenuPosition, PluginKind, Plugin } from "./plugin.js" +import { ConfigurePluginEvent, ConfigurePluginDetail, newConfigurePluginEvent } from './plugin.events.js'; +import { newLogEvent } from '@openscd/core/foundation/deprecated/history'; + // HOSTING INTERFACES export interface MenuItem { @@ -173,29 +179,6 @@ export function staticTagHtml( return html(strings, ...args); } -export type PluginKind = 'editor' | 'menu' | 'validator'; -export const menuPosition = ['top', 'middle', 'bottom'] as const; -export type MenuPosition = (typeof menuPosition)[number]; - -export type Plugin = { - name: string; - src: string; - icon?: string; - default?: boolean; - kind: PluginKind; - requireDoc?: boolean; - position?: MenuPosition; - installed: boolean; - official?: boolean; - content?: TemplateResult; -}; - -export type InstalledOfficialPlugin = { - src: string; - official: true; - installed: boolean; -}; - export function withoutContent

    ( plugin: P ): P { @@ -245,9 +228,12 @@ export class OpenSCD extends LitElement { /** The UUID of the current [[`doc`]] */ @property({ type: String }) docId = ''; - /** Index of the last [[`EditorAction`]] applied. */ @state() - editCount = -1; + historyState: HistoryState = { + editCount: -1, + canRedo: false, + canUndo: false, + } /** Object containing all *.nsdoc files and a function extracting element's label form them*/ @property({ attribute: false }) @@ -278,15 +264,53 @@ export class OpenSCD extends LitElement { if (src.startsWith('blob:')) URL.revokeObjectURL(src); } + /** + * + * @deprecated Use `handleConfigurationPluginEvent` instead + */ + public handleAddExternalPlugin(e: AddExternalPluginEvent){ + this.addExternalPlugin(e.detail.plugin); + const {name, kind} = e.detail.plugin + + const event = newConfigurePluginEvent(name,kind, e.detail.plugin) + + this.handleConfigurationPluginEvent(event) + } + + + public handleConfigurationPluginEvent(e: ConfigurePluginEvent){ + const { name, kind, config } = e.detail; + + const hasPlugin = this.hasPlugin(name, kind); + const hasConfig = config !== null; + const isChangeEvent = hasPlugin && hasConfig; + const isRemoveEvent = hasPlugin && !hasConfig; + const isAddEvent = !hasPlugin && hasConfig; + + // the `&& config`is only because typescript + // cannot infer that `isChangeEvent` and `isAddEvent` implies `config !== null` + if(isChangeEvent && config){ + this.changePlugin(config); + + }else if(isRemoveEvent){ + this.removePlugin(name, kind); + + }else if(isAddEvent && config){ + this.addPlugin(config); + + }else{ + const event = newLogEvent({ + kind: "error", + title: "Invalid plugin configuration event", + message: JSON.stringify({name, kind, config}), + }); + this.dispatchEvent(event); + } + } + connectedCallback(): void { super.connectedCallback(); this.addEventListener('reset-plugins', this.resetPlugins); - this.addEventListener( - 'add-external-plugin', - (e: AddExternalPluginEvent) => { - this.addExternalPlugin(e.detail.plugin); - } - ); this.addEventListener('set-plugins', (e: SetPluginsEvent) => { this.setPlugins(e.detail.indices); }); @@ -294,15 +318,8 @@ export class OpenSCD extends LitElement { this.updatePlugins(); this.requestUpdate(); - this.addEventListener('oscd-edit-completed', (evt: EditCompletedEvent) => { - const initiator = evt.detail.initiator; - - if (initiator === 'undo') { - this.editCount -= 1; - } else { - this.editCount += 1; - } - + this.addEventListener(historyStateEvent, (e: CustomEvent) => { + this.historyState = e.detail; this.requestUpdate(); }); } @@ -311,19 +328,22 @@ export class OpenSCD extends LitElement { return html` - + @@ -341,9 +361,64 @@ export class OpenSCD extends LitElement { ); this.requestUpdate(); } + + /** + * + * @param name + * @param kind + * @returns the index of the plugin in the stored plugin list + */ + private findPluginIndex(name: string, kind: PluginKind): number { + return this.storedPlugins.findIndex(p => p.name === name && p.kind === kind); + } + + private hasPlugin(name: string, kind: PluginKind): boolean { + return this.findPluginIndex(name, kind) > -1; + } + + private removePlugin(name: string, kind: PluginKind) { + const newPlugins = this.storedPlugins.filter( + p => p.name !== name || p.kind !== kind + ); + this.storePlugins(newPlugins); + } + + private addPlugin(plugin: Plugin) { + const newPlugins = [...this.storedPlugins, plugin]; + this.storePlugins(newPlugins); + } + + /** + * + * @param plugin + * @throws if the plugin is not found + */ + private changePlugin(plugin: Plugin) { + const storedPlugins = this.storedPlugins; + const {name, kind} = plugin; + const pluginIndex = this.findPluginIndex(name, kind); + + if(pluginIndex < 0) { + const event = newLogEvent({ + kind: "error", + title: "Plugin not found, stopping change process", + message: JSON.stringify({name, kind}), + }) + this.dispatchEvent(event); + return; + } + + const pluginToChange = storedPlugins[pluginIndex] + const changedPlugin = {...pluginToChange, ...plugin} + const newPlugins = [...storedPlugins] + newPlugins.splice(pluginIndex, 1, changedPlugin) + + this.storePlugins(newPlugins); + } + private resetPlugins(): void { this.storePlugins( - (officialPlugins as Plugin[]).concat(this.parsedPlugins).map(plugin => { + (builtinPlugins as Plugin[]).concat(this.parsedPlugins).map(plugin => { return { src: plugin.src, installed: plugin.default ?? false, @@ -360,48 +435,66 @@ export class OpenSCD extends LitElement { plugins: PluginSet = { menu: [], editor: [] }; get parsedPlugins(): Plugin[] { - return this.plugins.menu - .map((p: CorePlugin) => ({ - ...p, - position: - typeof p.position !== 'number' - ? (p.position as MenuPosition) - : undefined, - kind: 'menu' as PluginKind, - installed: p.active ?? false, - })) - .concat( - this.plugins.editor.map((p: CorePlugin) => ({ - ...p, - position: undefined, - kind: 'editor' as PluginKind, - installed: p.active ?? false, - })) - ); + + const menuPlugins = this.plugins.menu.map((plugin: CorePlugin) => { + let newPosition: MenuPosition | undefined = plugin.position as MenuPosition; + if(typeof plugin.position === 'number') { + newPosition = undefined + } + + return { + ...plugin, + position: newPosition, + kind: 'menu' as PluginKind, + installed: plugin.active ?? false, + } + }) + + const editorPlugins = this.plugins.editor.map((plugin: CorePlugin) => ({ + ...plugin, + position: undefined, + kind: 'editor' as PluginKind, + installed: plugin.active ?? false, + })) + + const allPlugnis = [...menuPlugins, ...editorPlugins] + return allPlugnis } private get sortedStoredPlugins(): Plugin[] { - return this.storedPlugins - .map(plugin => { - if (!plugin.official) return plugin; - const officialPlugin = (officialPlugins as Plugin[]) - .concat(this.parsedPlugins) - .find(needle => needle.src === plugin.src); + + const mergedPlugins = this.storedPlugins.map(plugin => { + if (!plugin.official){ return plugin }; + + const officialPlugin = (builtinPlugins as Plugin[]) + .concat(this.parsedPlugins) + .find(needle => needle.src === plugin.src); + return { ...officialPlugin, ...plugin, }; - }) + }) + + + return mergedPlugins .sort(compareNeedsDoc) .sort(menuCompare); } private get storedPlugins(): Plugin[] { - return ( - JSON.parse(localStorage.getItem('plugins') ?? '[]', (key, value) => - value.src && value.installed ? this.addContent(value) : value - ) - ); + const pluginsConfigStr = localStorage.getItem('plugins') ?? '[]' + const storedPlugins = JSON.parse(pluginsConfigStr) as Plugin[] + + const plugins = storedPlugins.map(plugin => { + const isInstalled = plugin.src && plugin.installed + if(!isInstalled) { return plugin } + + return this.addContent(plugin) + }) + + return plugins + } protected get locale(): string { @@ -420,16 +513,20 @@ export class OpenSCD extends LitElement { private setPlugins(indices: Set) { const newPlugins = this.sortedStoredPlugins.map((plugin, index) => { - return { ...plugin, installed: indices.has(index) }; + return { + ...plugin, + installed: indices.has(index) + }; }); this.storePlugins(newPlugins); } private updatePlugins() { + const stored: Plugin[] = this.storedPlugins; const officialStored = stored.filter(p => p.official); const newOfficial: Array = ( - officialPlugins as Plugin[] + builtinPlugins as Plugin[] ) .concat(this.parsedPlugins) .filter(p => !officialStored.find(o => o.src === p.src)) @@ -440,9 +537,10 @@ export class OpenSCD extends LitElement { official: true as const, }; }); + const oldOfficial = officialStored.filter( p => - !(officialPlugins as Plugin[]) + !(builtinPlugins as Plugin[]) .concat(this.parsedPlugins) .find(o => p.src === o.src) ); @@ -465,16 +563,18 @@ export class OpenSCD extends LitElement { private addContent(plugin: Omit): Plugin { const tag = pluginTag(plugin.src); + if (!loadedPlugins.has(tag)) { loadedPlugins.add(tag); import(plugin.src).then(mod => customElements.define(tag, mod.default)); } + return { ...plugin, content: staticTagHtml`<${tag} .doc=${this.doc} .docName=${this.docName} - .editCount=${this.editCount} + .editCount=${this.historyState.editCount} .docId=${this.docId} .pluginId=${plugin.src} .nsdoc=${this.nsdoc} diff --git a/packages/openscd/src/plugin.events.ts b/packages/openscd/src/plugin.events.ts new file mode 100644 index 0000000000..1f1c6111f6 --- /dev/null +++ b/packages/openscd/src/plugin.events.ts @@ -0,0 +1,26 @@ +import { Plugin, PluginKind } from './plugin.js'; + +/** + * The configure plugin event allows the plugin to request that OpenSCD core add, remove, or reconfigure a plugin. + */ +export type ConfigurePluginDetail = { + name: string; + // The API describes only 'menu' and 'editor' kinds b + // but we still use the 'validator' too, so I just use the type PluginKind + kind: PluginKind; + config: Plugin | null; +}; + +export type ConfigurePluginEvent = CustomEvent; + +/** + * The combination of name and kind uniquely identifies the plugin to be configured. + * If config is null, the plugin is removed. Otherwise, the plugin is added or reconfigured. + */ +export function newConfigurePluginEvent(name: string, kind: PluginKind, config: Plugin | null): ConfigurePluginEvent { + return new CustomEvent('oscd-configure-plugin', { + bubbles: true, + composed: true, + detail: { name, kind, config }, + }); +} diff --git a/packages/openscd/src/plugin.ts b/packages/openscd/src/plugin.ts new file mode 100644 index 0000000000..4b9dfba41e --- /dev/null +++ b/packages/openscd/src/plugin.ts @@ -0,0 +1,25 @@ +import { TemplateResult } from 'lit-element'; + +export type Plugin = { + name: string; + src: string; + icon?: string; + default?: boolean; + kind: PluginKind; + requireDoc?: boolean; + position?: MenuPosition; + installed: boolean; + official?: boolean; + content?: TemplateResult; +}; + +export type InstalledOfficialPlugin = { + src: string; + official: true; + installed: boolean; +}; + + +export type PluginKind = 'editor' | 'menu' | 'validator'; +export const menuPosition = ['top', 'middle', 'bottom'] as const; +export type MenuPosition = (typeof menuPosition)[number]; diff --git a/packages/openscd/src/plugins.ts b/packages/openscd/src/plugins.ts index 62f5ed915d..0c33dfa6c1 100644 --- a/packages/openscd/src/plugins.ts +++ b/packages/openscd/src/plugins.ts @@ -9,6 +9,7 @@ export const officialPlugins = [ icon: 'developer_board', default: true, kind: 'editor', + requireDoc: true, }, { name: 'Substation', @@ -16,6 +17,7 @@ export const officialPlugins = [ icon: 'margin', default: true, kind: 'editor', + requireDoc: true, }, { name: 'Single Line Diagram', @@ -23,6 +25,7 @@ export const officialPlugins = [ icon: 'edit', default: false, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Message Binding (GOOSE)', @@ -30,6 +33,7 @@ export const officialPlugins = [ icon: 'link', default: false, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Data Binding (GOOSE)', @@ -37,6 +41,7 @@ export const officialPlugins = [ icon: 'link', default: false, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Later Binding (GOOSE)', @@ -44,6 +49,7 @@ export const officialPlugins = [ icon: 'link', default: true, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Message Binding (SMV)', @@ -51,6 +57,7 @@ export const officialPlugins = [ icon: 'link', default: false, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Data Binding (SMV)', @@ -58,6 +65,7 @@ export const officialPlugins = [ icon: 'link', default: false, kind: 'editor', + requireDoc: true, }, { name: 'Subscriber Later Binding (SMV)', @@ -65,6 +73,7 @@ export const officialPlugins = [ icon: 'link', default: true, kind: 'editor', + requireDoc: true, }, { name: 'Communication', @@ -72,6 +81,7 @@ export const officialPlugins = [ icon: 'settings_ethernet', default: true, kind: 'editor', + requireDoc: true, }, { name: '104', @@ -79,6 +89,7 @@ export const officialPlugins = [ icon: 'settings_ethernet', default: false, kind: 'editor', + requireDoc: true, }, { name: 'Templates', @@ -86,6 +97,7 @@ export const officialPlugins = [ icon: 'copy_all', default: true, kind: 'editor', + requireDoc: true, }, { name: 'Publisher', @@ -93,6 +105,7 @@ export const officialPlugins = [ icon: 'publish', default: false, kind: 'editor', + requireDoc: true, }, { name: 'Cleanup', @@ -100,6 +113,7 @@ export const officialPlugins = [ icon: 'cleaning_services', default: false, kind: 'editor', + requireDoc: true, }, { name: 'Open project', diff --git a/packages/openscd/src/themes.ts b/packages/openscd/src/themes.ts index 51628775ff..ddc542df86 100644 --- a/packages/openscd/src/themes.ts +++ b/packages/openscd/src/themes.ts @@ -38,6 +38,22 @@ export function getTheme(theme: Settings['theme']): TemplateResult { --mdc-dialog-heading-ink-color: var(--base00); --mdc-icon-font: 'Material Icons Outlined'; + + --oscd-primary: var(--oscd-theme-primary, var(--cyan)); + --oscd-secondary: var(--oscd-theme-secondary, var(--violet)); + --oscd-error: var(--oscd-theme-error, var(--red)); + + --oscd-base03: var(--oscd-theme-base03, var(--base03)); + --oscd-base02: var(--oscd-theme-base02, var(--base02)); + --oscd-base01: var(--oscd-theme-base01, var(--base01)); + --oscd-base00: var(--oscd-theme-base00, var(--base00)); + --oscd-base0: var(--oscd-theme-base0, var(--base0)); + --oscd-base1: var(--oscd-theme-base1, var(--base1)); + --oscd-base2: var(--oscd-theme-base2, var(--base2)); + --oscd-base3: var(--oscd-theme-base3, var(--base3)); + + --oscd-text-font: var(--oscd-theme-text-font, 'Roboto'); + --oscd-icon-font: var(--oscd-theme-icon-font, 'Material Icons'); } .mdc-drawer span.mdc-drawer__title { diff --git a/packages/openscd/src/translations/de.ts b/packages/openscd/src/translations/de.ts index 81d1e88891..67e314ede6 100644 --- a/packages/openscd/src/translations/de.ts +++ b/packages/openscd/src/translations/de.ts @@ -117,6 +117,7 @@ export const de: Translations = { moved: '{{ name }} verschoben', updated: '{{ name }} bearbeitet', import: '{{name}} importiert', + complex: 'Mehrere Elemente bearbeitet', error: { create: 'Konnte {{ name }} nicht hinzufügen', update: 'Konnte {{ name }} nicht bearbeiten', diff --git a/packages/openscd/src/translations/en.ts b/packages/openscd/src/translations/en.ts index 156be665bb..ca983fd40d 100644 --- a/packages/openscd/src/translations/en.ts +++ b/packages/openscd/src/translations/en.ts @@ -115,6 +115,7 @@ export const en = { moved: 'Moved {{ name }}', updated: 'Edited {{ name }}', import: 'Imported {{name}}', + complex: 'Multiple elements edited', error: { create: 'Could not add {{ name }}', update: 'Could not edit {{ name }}', diff --git a/packages/openscd/test/integration/Editing.test.ts b/packages/openscd/test/integration/Editing.test.ts index 809bcdb746..9ecee22da6 100644 --- a/packages/openscd/test/integration/Editing.test.ts +++ b/packages/openscd/test/integration/Editing.test.ts @@ -209,8 +209,9 @@ describe('Editing-Logging integration', () => { expect(element.parentElement).to.equal(parent); }); - it('can be redone', () => { + it('can be redone', async () => { elm.dispatchEvent( + // Replace: Q01 -> Q03 (new element) newActionEvent({ old: { element }, new: { element: newElement } }) ); @@ -218,30 +219,33 @@ describe('Editing-Logging integration', () => { elm.history.redo(); - expect(newElement.parentElement).to.equal(parent); + const newEle = parent.querySelector('Bay[name="Q03"]')!; + + expect(newEle.parentElement).to.equal(parent); expect(element.parentElement).to.be.null; }); - it('correctly copying child elements between element and newElement for multiple undo/redo', () => { + it('correctly copying child elements between element and newElement for multiple undo/redo', async () => { const originOldChildCount = element.children.length; - const originNewChildCount = newElement.children.length; elm.dispatchEvent( newActionEvent({ old: { element }, new: { element: newElement } }) ); - expect(element.children).to.have.lengthOf(originNewChildCount); - expect(newElement.children).to.have.lengthOf(originOldChildCount); + + let newEle = parent.querySelector('Bay[name="Q03"]')!; + expect(newEle.children).to.have.lengthOf(originOldChildCount); elm.history.undo(); elm.history.redo(); elm.history.undo(); - expect(element.children).to.have.lengthOf(originOldChildCount); - expect(newElement.children).to.have.lengthOf(originNewChildCount); + + const ele = parent.querySelector('Bay[name="Q01"]')!; + expect(ele.children).to.have.lengthOf(originOldChildCount); elm.history.redo(); - expect(element.children).to.have.lengthOf(originNewChildCount); - expect(newElement.children).to.have.lengthOf(originOldChildCount); + newEle = parent.querySelector('Bay[name="Q03"]')!; + expect(newEle.children).to.have.lengthOf(originOldChildCount); }); }); diff --git a/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js b/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js index 5da7ce13fc..a43b37e707 100644 --- a/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js +++ b/packages/openscd/test/integration/__snapshots__/open-scd.test.snap.js @@ -1,7 +1,7 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["open-scd looks like its snapshot"] = +snapshots["open-scd looks like its snapshot"] = ` @@ -18,3215 +18,3221 @@ snapshots["open-scd looks like its snapshot"] = /* end snapshot open-scd looks like its snapshot */ snapshots["open-scd renders menu plugins passed down as props and it looks like its snapshot"] = -` - - - - -

    -
    - - - - - - - - - - - - - - Menu - - -
  • -
  • - - - folder_open - - - Open project - - - - - - - create_new_folder - - - New project - - - - - - - save - - - Save project - - - - -
  • -
  • - - - rule_folder - - - Validate Schema - - - - - - - rule_folder - - - Validate Templates - - - - -
  • -
  • - - - snippet_folder - - - Import IEDs - - - - - - - play_circle - - - Subscriber Update - - - - - - - merge_type - - - Merge Project - - - - - + + + + + +
    - - merge_type - - - Update Substation - - - - - + - - compare_arrows - - - Compare IED - - - - -
  • -
  • - - - settings - - - Settings - - - - - help - - - Help - - - - - + - - history_toggle_off - - - Show SCL History - - - - -
  • -
  • - - - extension - - - Plug-ins - - - - -
    - -
    - Open project -
    -
    - -
    - New project -
    -
    -
    - - + + + + + + + + + - - - Editor tab - - - tab - - -
  • -
  • - - - developer_board - - IED - - - - margin - - Substation - - - - edit - - Single Line Diagram - - - - link - - Subscriber Message Binding (GOOSE) - - - - link - - Subscriber Data Binding (GOOSE) - - - - link - - Subscriber Later Binding (GOOSE) - - - - link - - Subscriber Message Binding (SMV) - - - - link - - Subscriber Data Binding (SMV) - - - - link - - Subscriber Later Binding (SMV) - - - - settings_ethernet - - Communication - - - - settings_ethernet - - 104 - - - - copy_all - - Templates - - - - publish - - Publisher - - - - cleaning_services - - Cleanup - - - - Menu entry - - + Menu + + +
  • - +
  • + + + folder_open + + + Open project + + + + + + + create_new_folder + + + New project + + + + + + + save + + + Save project + + + + +
  • +
  • + + + rule_folder + + + Validate Schema + + + + + + + rule_folder + + + Validate Templates + + + + +
  • +
  • + + + snippet_folder + + + Import IEDs + + + + + + play_circle -
    -
    -
    -
  • -
  • - - - folder_open - - Open project - - - - create_new_folder - - New project - - - - link - - Top Mock Plugin - - - - save - - Save project - -
  • -
  • - - - rule_folder - - Validate Schema - - - - rule_folder - - Validate Templates - -
  • -
  • - - - link - - Middle Mock Plugin - - - - snippet_folder - - Import IEDs - - - - developer_board - - Create Virtual IED - - - - play_circle - - Subscriber Update - - - - play_circle - - Update desc (ABB) - - - - play_circle - - Update desc (SEL) - - - - merge_type - - Merge Project - - - - merge_type - - Update Substation - - - - compare_arrows - - Compare IED - - - - sim_card_download - - Export Communication Section - -
  • -
  • - - - help - - Help - - + + Subscriber Update + + + + + + + merge_type + + + Merge Project + + + + + + + merge_type + + + Update Substation + + + + + + + compare_arrows + + + Compare IED + + + + +
  • +
  • + + + settings + + + Settings + + + + + help + + + Help + + + + + + + history_toggle_off + + + Show SCL History + + + + +
  • +
  • + + + extension + + + Plug-ins + + +
    + +
    + - - link - - Bottom Mock Plugin - - + Open project +
    + + - - history_toggle_off - - Show SCL History - - - - - - - + New project +
    + + + - - - -
    -

    - Here you may add remote plug-ins directly from a custom URL. - You do this at your own risk. -

    - - - - + + Editor tab + + + tab + + +
  • +
  • + - Editor tab - tab + developer_board -
    - + - Menu entry - play_circle + margin - - - + - Validator - rule_folder + edit - -
    - - -
    - - - - -
    -`; -/* end snapshot open-scd renders menu plugins passed down as props and it looks like its snapshot */ - -snapshots["open-scd renders editor plugins passed down as props and it looks like its snapshot"] = -` - - - - -
    -
    - - - - - - - - - - -
    - - - Menu - - -
  • -
  • - - - folder_open - - - Open project - - - - - - - create_new_folder - - - New project - - - - - - - save - - - Save project - - - - -
  • -
  • - - - rule_folder - - - Validate Schema - - - - - - - rule_folder - - - Validate Templates - - - - -
  • -
  • - - - snippet_folder - - - Import IEDs - - - - - - - play_circle - - - Subscriber Update - - - - - - - merge_type - - - Merge Project - - - - - - - merge_type - - - Update Substation - - - - - - - compare_arrows - - - Compare IED - - - - -
  • -
  • - - - settings - - - Settings - - - - - help - - - Help - - - - - - - history_toggle_off - - - Show SCL History - - - - -
  • -
  • - - - extension - - - Plug-ins - - -
    -
    -
    - -
    - Open project -
    -
    - -
    - New project -
    -
    -
    - - - - - Editor tab - - - tab - - -
  • -
  • - - - developer_board - - IED - - - - margin - - Substation - - - - edit - - Single Line Diagram - - - - link - - Subscriber Message Binding (GOOSE) - - - - link - - Subscriber Data Binding (GOOSE) - - - - link - - Subscriber Later Binding (GOOSE) - - - - link - - Subscriber Message Binding (SMV) - - - - link - - Subscriber Data Binding (SMV) - - - - link - - Subscriber Later Binding (SMV) - - + + + link + + Subscriber Message Binding (GOOSE) + + + + link + + Subscriber Data Binding (GOOSE) + + + + link + + Subscriber Later Binding (GOOSE) + + + + link + + Subscriber Message Binding (SMV) + + + + link + + Subscriber Data Binding (SMV) + + + + link + + Subscriber Later Binding (SMV) + + + + settings_ethernet + + Communication + + + + settings_ethernet + + 104 + + + + copy_all + + Templates + + + + publish + + Publisher + + + + cleaning_services + + Cleanup + + + + Menu entry + + + + play_circle + + + +
  • +
  • + + + folder_open + + Open project + + + + create_new_folder + + New project + + + + link + + Top Mock Plugin + + + + save + + Save project + +
  • +
  • + + + rule_folder + + Validate Schema + + + + rule_folder + + Validate Templates + +
  • +
  • + + + link + + Middle Mock Plugin + + + + snippet_folder + + Import IEDs + + + + developer_board + + Create Virtual IED + + + + play_circle + + Subscriber Update + + + + play_circle + + Update desc (ABB) + + + + play_circle + + Update desc (SEL) + + + + merge_type + + Merge Project + + + + merge_type + + Update Substation + + + + compare_arrows + + Compare IED + + + + sim_card_download + + Export Communication Section + +
  • +
  • + + + link + + Bottom Mock Plugin + + + + help + + Help + + + + history_toggle_off + + Show SCL History + +
    + + + + + + +
    + +
    +

    + Here you may add remote plug-ins directly from a custom URL. + You do this at your own risk. +

    + + + + + Editor tab + + tab + + + + Menu entry + + play_circle + + + + + Validator + + rule_folder + + + + + +
    + + + + +
    + +`; +/* end snapshot open-scd renders menu plugins passed down as props and it looks like its snapshot */ + +snapshots["open-scd renders editor plugins passed down as props and it looks like its snapshot"] = +`
    + + + + + +
    - - settings_ethernet - - Communication - - + - - settings_ethernet - - 104 - - + + + + + + + + + + + + Menu + + +
  • +
  • + + + folder_open + + + Open project + + + + + + + create_new_folder + + + New project + + + + + + + save + + + Save project + + + + +
  • +
  • + + + rule_folder + + + Validate Schema + + + + + + + rule_folder + + + Validate Templates + + + + +
  • +
  • + + + snippet_folder + + + Import IEDs + + + + + + + play_circle + + + Subscriber Update + + + + + + + merge_type + + + Merge Project + + + + + + + merge_type + + + Update Substation + + + + + + + compare_arrows + + + Compare IED + + + + +
  • +
  • + + + settings + + + Settings + + + + + help + + + Help + + + + + + + history_toggle_off + + + Show SCL History + + + + +
  • +
  • + + + extension + + + Plug-ins + + +
    +
    +
    + - - copy_all - - Templates - - + Open project +
    + + - - publish - - Publisher -
    - + New project +
    + +
    + + - - cleaning_services - - Cleanup - - + + Editor tab + + + tab + +
    +
  • +
  • + + + developer_board + + IED + + + + margin + + Substation + + + + edit + + Single Line Diagram + + + + link + + Subscriber Message Binding (GOOSE) + + + + link + + Subscriber Data Binding (GOOSE) + + + + link + + Subscriber Later Binding (GOOSE) + + + + link + + Subscriber Message Binding (SMV) + + + + link + + Subscriber Data Binding (SMV) + + + + link + + Subscriber Later Binding (SMV) + + + + settings_ethernet + + Communication + + + + settings_ethernet + + 104 + + + + copy_all + + Templates + + + + publish + + Publisher + + + + cleaning_services + + Cleanup + + + + link + + Mock Editor Plugin + + + + Menu entry + + + + play_circle + + + +
  • +
  • + + + folder_open + + Open project + + + + create_new_folder + + New project + + + + save + + Save project + +
  • +
  • + + + rule_folder + + Validate Schema + + + + rule_folder + + Validate Templates + +
  • +
  • + + + snippet_folder + + Import IEDs + + + + developer_board + + Create Virtual IED + + + + play_circle + + Subscriber Update + + + + play_circle + + Update desc (ABB) + + + + play_circle + + Update desc (SEL) + + + + merge_type + + Merge Project + + + + merge_type + + Update Substation + + + + compare_arrows + + Compare IED + + + + sim_card_download + + Export Communication Section + +
  • +
  • + + + help + + Help + + + + history_toggle_off + + Show SCL History + +
    + + + + + + +
    + +
    +

    + Here you may add remote plug-ins directly from a custom URL. + You do this at your own risk. +

    + + + + + Editor tab + + tab + + + + Menu entry + + play_circle + + + + + Validator + + rule_folder + + + + + +
    + + + + +
    + +`; +/* end snapshot open-scd renders editor plugins passed down as props and it looks like its snapshot */ + +snapshots["open-scd layout looks like its snapshot"] = +`
    + + + + + +
    - - link - - Mock Editor Plugin - - + - - Menu entry - - + + + + + + + + + + + + Menu + + +
  • - +
  • + + + folder_open + + + Open project + + + + + + + create_new_folder + + + New project + + + + + + + save + + + Save project + + + + +
  • +
  • + + + rule_folder + + + Validate Schema + + + + + + + rule_folder + + + Validate Templates + + + + +
  • +
  • + + + snippet_folder + + + Import IEDs + + + + + + play_circle -
    -
    -
    -
  • -
  • - - - folder_open - - Open project - - - - create_new_folder - - New project - - - - save - - Save project - -
  • -
  • - - - rule_folder - - Validate Schema - - - - rule_folder - - Validate Templates - -
  • -
  • - - - snippet_folder - - Import IEDs - - - - developer_board - - Create Virtual IED - - - - play_circle - - Subscriber Update - - - - play_circle - - Update desc (ABB) - - - - play_circle - - Update desc (SEL) - - - - merge_type - - Merge Project - - - - merge_type - - Update Substation - - - - compare_arrows - - Compare IED - - - - sim_card_download - - Export Communication Section - -
  • -
  • - + + Subscriber Update + + + + + + + merge_type + + + Merge Project + + + + + + + merge_type + + + Update Substation + + + + + + + compare_arrows + + + Compare IED + + + + +
  • +
  • + + + settings + + + Settings + + + + + help + + + Help + + + + + + + history_toggle_off + + + Show SCL History + + + + +
  • +
  • + + + extension + + + Plug-ins + + + + +
    + - - help - - Help - - + Open project +
    + + - - history_toggle_off - - Show SCL History -
    - - - - - - + New project +
    + +
    + - - - -
    -

    - Here you may add remote plug-ins directly from a custom URL. - You do this at your own risk. -

    - - - - + + Editor tab + + + tab + + +
  • +
  • + + + developer_board + + IED + + + + margin + + Substation + + + + edit + + Single Line Diagram + + + + link + + Subscriber Message Binding (GOOSE) + + + + link + + Subscriber Data Binding (GOOSE) + + + + link + + Subscriber Later Binding (GOOSE) + + + + link + + Subscriber Message Binding (SMV) + + + + link + + Subscriber Data Binding (SMV) + + + + link + + Subscriber Later Binding (SMV) + + + + settings_ethernet + + Communication + + + + settings_ethernet + + 104 + + + + copy_all + + Templates + + + + publish + + Publisher + + + + cleaning_services + + Cleanup + + + + Menu entry + + + + play_circle + + + +
  • +
  • + - Editor tab - tab + folder_open -
    - + - Menu entry - play_circle + create_new_folder - - - + - Validator - rule_folder + save - -
    - - -
    - - - - -
    -`; -/* end snapshot open-scd renders editor plugins passed down as props and it looks like its snapshot */ - -snapshots["open-scd layout looks like its snapshot"] = -` - - - - -
    -
    - - - - - - - - - - -
    - - - Menu - - -
  • -
  • - - - folder_open - - - Open project - - - - - - - create_new_folder - - - New project - - - - - - - save - - Save project - - - - -
  • -
  • - - - rule_folder - - + +
  • +
  • + + + rule_folder + Validate Schema -
    -
    - - - - - rule_folder - - + + + + rule_folder + Validate Templates - - - - -
  • -
  • - - - snippet_folder - - + +
  • +
  • + + + snippet_folder + Import IEDs -
    -
    - - - - - play_circle - - + + + + developer_board + + Create Virtual IED + + + + play_circle + Subscriber Update - - - - - - - merge_type - - - Merge Project - - - - - - - merge_type - - - Update Substation - - - - - - - compare_arrows - - - Compare IED - - - - -
  • -
  • - - - settings - - - Settings - - - - - help - - - Help - - - - - - - history_toggle_off - - - Show SCL History - - - - -
  • -
  • - - - extension - - - Plug-ins - - -
    -
    -
    - -
    - Open project -
    -
    - -
    - New project -
    -
    -
    - - - - - Editor tab - - - tab - - -
  • -
  • - - - developer_board - - IED - - - - margin - - Substation - - - - edit - - Single Line Diagram - - - - link - - Subscriber Message Binding (GOOSE) - - - - link - - Subscriber Data Binding (GOOSE) - - - - link - - Subscriber Later Binding (GOOSE) - - - - link - - Subscriber Message Binding (SMV) - - - - link - - Subscriber Data Binding (SMV) - - - - link - - Subscriber Later Binding (SMV) - - - - settings_ethernet - - Communication - - - - settings_ethernet - - 104 - - - - copy_all - - Templates - - - - publish - - Publisher - - - - cleaning_services - - Cleanup - - - - Menu entry - - + - + play_circle - - - -
  • -
  • - - - folder_open - - Open project - - - - create_new_folder - - New project - - - - save - - Save project - -
  • -
  • - - - rule_folder - - Validate Schema - - - - rule_folder - - Validate Templates - -
  • -
  • - - - snippet_folder - - Import IEDs - - - - developer_board - - Create Virtual IED - - - - play_circle - - Subscriber Update - - - - play_circle - - Update desc (ABB) - - - - play_circle - - Update desc (SEL) - - - - merge_type - - Merge Project - - - - merge_type - - Update Substation - - - - compare_arrows - - Compare IED - - - - sim_card_download - - Export Communication Section - -
  • -
  • - - - help - - Help - - - - history_toggle_off - - Show SCL History - -
    - - - - - - -
    - -
    -

    - Here you may add remote plug-ins directly from a custom URL. - You do this at your own risk. -

    - - - - + Update desc (ABB) + + + + play_circle + + Update desc (SEL) + + - Editor tab - tab + merge_type - - + - Menu entry - play_circle + merge_type - - - + + + compare_arrows + + Compare IED + + - Validator - rule_folder + sim_card_download + + Export Communication Section + +
  • +
  • + + + help + + Help + + + + history_toggle_off -
    + Show SCL History +
    - - -
    - + + + + + +
    + - - - - +
    +

    + Here you may add remote plug-ins directly from a custom URL. + You do this at your own risk. +

    + + + + + Editor tab + + tab + + + + Menu entry + + play_circle + + + + + Validator + + rule_folder + + + + + +
    + + + + +
    + `; /* end snapshot open-scd layout looks like its snapshot */ diff --git a/packages/openscd/test/mock-edits.ts b/packages/openscd/test/mock-edits.ts new file mode 100644 index 0000000000..be49a4f387 --- /dev/null +++ b/packages/openscd/test/mock-edits.ts @@ -0,0 +1,16 @@ +import { Edit, Insert, Remove, Update } from '@openscd/core'; + + +const element = document.createElement('test-element'); +const parent = document.createElement('test-parent'); +const reference = document.createElement('test-sibling'); + +parent.appendChild(element); +parent.appendChild(reference); + +export const mockEdits = { + insert: (): Insert => ({ parent, node: element, reference }), + remove: (): Remove => ({ node: element }), + update: (): Update => ({ element, attributes: { test: 'value' } }), + complex: (): Edit[] => [ mockEdits.insert(), mockEdits.remove(), mockEdits.update() ], +} diff --git a/packages/openscd/test/mock-wizard-editor.ts b/packages/openscd/test/mock-wizard-editor.ts index b30adfa324..a1962b0846 100644 --- a/packages/openscd/test/mock-wizard-editor.ts +++ b/packages/openscd/test/mock-wizard-editor.ts @@ -1,22 +1,39 @@ -import { Editing } from '../src/Editing.js'; import { LitElement, customElement, TemplateResult, html, query, + property } from 'lit-element'; import '../src/addons/Wizards.js'; + +import '../src/addons/Editor.js'; + import { OscdWizards } from '../src/addons/Wizards.js'; @customElement('mock-wizard-editor') -export class MockWizardEditor extends Editing(LitElement) { +export class MockWizardEditor extends LitElement { + @property({ type: Object }) doc!: XMLDocument; + @query('oscd-wizards') wizards!: OscdWizards; render(): TemplateResult { - return html``; + return html` + + + + + + `; } get wizardUI() { diff --git a/packages/openscd/test/unit/Editing.test.ts b/packages/openscd/test/unit/Editing.test.ts deleted file mode 100644 index 4b1dfb8351..0000000000 --- a/packages/openscd/test/unit/Editing.test.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { html, fixture, expect } from '@open-wc/testing'; -import { SinonSpy, spy } from 'sinon'; - -import './mock-editor.js'; -import { MockEditor } from './mock-editor.js'; - -import { createUpdateAction, newActionEvent } from '@openscd/core/foundation/deprecated/editor.js'; - -describe('EditingElement', () => { - let elm: MockEditor; - let doc: XMLDocument; - let parent: Element; - let element: Element; - let reference: Node | null; - - let validateEvent: SinonSpy; - - beforeEach(async () => { - doc = await fetch('/test/testfiles/Editing.scd') - .then(response => response.text()) - .then(str => new DOMParser().parseFromString(str, 'application/xml')); - elm = ( - await fixture(html``) - ); - - parent = elm.doc!.querySelector('VoltageLevel[name="E1"]')!; - element = parent.querySelector('Bay[name="Q01"]')!; - reference = element.nextSibling; - - validateEvent = spy(); - window.addEventListener('validate', validateEvent); - }); - - it('creates an element on receiving a Create Action', () => { - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: elm.doc!.createElement('newBay'), - reference: null, - }, - }) - ); - expect(elm.doc!.querySelector('newBay')).to.not.be.null; - }); - - it('creates an Node on receiving a Create Action', () => { - const testNode = document.createTextNode('myTestNode'); - - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: testNode, - }, - }) - ); - expect(parent.lastChild).to.equal(testNode); - }); - - it('creates the Node based on the reference definition', () => { - const testNode = document.createTextNode('myTestNode'); - - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: testNode, - reference: parent.firstChild, - }, - }) - ); - expect(parent.firstChild).to.equal(testNode); - }); - - it('triggers getReference with missing reference on Create Action', () => { - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: elm.doc!.createElement('Bay'), - }, - }) - ); - expect(parent.querySelector('Bay')?.nextElementSibling).to.equal( - parent.querySelector('Bay[name="Q01"]') - ); - }); - - it('ignores getReference with existing reference on Create Action', () => { - const newElement = elm.doc!.createElement('Bay'); - newElement?.setAttribute('name', 'Q03'); - - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: newElement, - reference: parent.querySelector('Bay[name="Q02"]'), - }, - }) - ); - expect( - parent.querySelector('Bay[name="Q03"]')?.nextElementSibling - ).to.equal(parent.querySelector('Bay[name="Q02"]')); - }); - - it('does not creates an element on name attribute conflict', () => { - const newElement = elm.doc!.createElement('Bay'); - newElement?.setAttribute('name', 'Q01'); - - elm.dispatchEvent( - newActionEvent({ - new: { - parent, - element: newElement, - reference: null, - }, - }) - ); - expect(parent.querySelectorAll('Bay[name="Q01"]').length).to.be.equal(1); - }); - - it('does not creates an element on id attribute conflict', () => { - const newElement = elm.doc!.createElement('DOType'); - newElement?.setAttribute('id', 'testId'); - - elm.dispatchEvent( - newActionEvent({ - new: { - parent: doc.querySelector('DataTypeTemplates')!, - element: newElement, - reference: null, - }, - }) - ); - expect(doc.querySelector('DOType')).to.be.null; - }); - - it('deletes an element on receiving a Delete action', () => { - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element, - reference, - }, - }) - ); - expect(elm.doc!.querySelector('VoltageLevel[name="E1"] > Bay[name="Q01"]')) - .to.be.null; - }); - - it('deletes a Node on receiving a Delete action', () => { - const testNode = document.createTextNode('myTestNode'); - parent.appendChild(testNode); - expect(testNode.parentNode).to.be.equal(parent); - - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element: testNode, - }, - }) - ); - - expect(parent.lastChild).to.not.equal(testNode); - expect(testNode.parentNode).to.be.null; - }); - - it('correctly handles incorrect delete action definition', () => { - const testNode = document.createTextNode('myTestNode'); - expect(testNode.parentNode).to.null; - - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element: testNode, - }, - }) - ); - - expect(parent.lastChild).to.not.equal(testNode); - expect(testNode.parentNode).to.null; - }); - - it('replaces an element on receiving an Replace action', () => { - elm.dispatchEvent( - newActionEvent({ - old: { - element, - }, - new: { - element: elm.doc!.createElement('newBay'), - }, - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.be.null; - expect(parent.querySelector('newBay')).to.not.be.null; - expect(parent.querySelector('newBay')?.nextElementSibling).to.equal( - parent.querySelector('Bay[name="Q02"]') - ); - }); - - it('does not replace an element in case of name conflict', () => { - const newElement = elm.doc!.createElement('Bay'); - newElement?.setAttribute('name', 'Q02'); - - elm.dispatchEvent( - newActionEvent({ - old: { - element, - }, - new: { - element: newElement, - }, - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.not.be.null; - expect( - parent.querySelector('Bay[name="Q01"]')?.nextElementSibling - ).to.equal(parent.querySelector('Bay[name="Q02"]')); - }); - - it('replaces id defined element on receiving Replace action', () => { - expect(doc.querySelector('LNodeType[id="testId"]')).to.not.be.null; - - const newElement = doc.createElement('LNodeType'); - newElement?.setAttribute('id', 'testId3'); - - elm.dispatchEvent( - newActionEvent({ - old: { - element: doc.querySelector('LNodeType[id="testId"]')!, - }, - new: { - element: newElement, - }, - }) - ); - expect(elm.doc!.querySelector('LNodeType[id="testId"]')).to.be.null; - expect(elm.doc!.querySelector('LNodeType[id="testId3"]')).to.not.be.null; - }); - - it('does not replace an element in case of id conflict', () => { - expect(doc.querySelector('LNodeType[id="testId"]')).to.not.be.null; - - const newElement = elm.doc!.createElement('LNodeType'); - newElement?.setAttribute('id', 'testId1'); - - elm.dispatchEvent( - newActionEvent({ - old: { - element: doc.querySelector('LNodeType[id="testId"]')!, - }, - new: { - element: newElement, - }, - }) - ); - expect(elm.doc!.querySelector('LNodeType[id="testId"]')).to.not.be.null; - expect(elm.doc!.querySelector('LNodeType[id="testId1"]')).to.be.null; - }); - - it('moves an element on receiving a Move action', () => { - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element, - reference, - }, - new: { - parent: elm.doc!.querySelector('VoltageLevel[name="J1"]')!, - reference: null, - }, - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.be.null; - expect(elm.doc!.querySelector('VoltageLevel[name="J1"] > Bay[name="Q01"]')) - .to.not.be.null; - }); - - it('triggers getReference with missing reference on Move action', () => { - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element, - reference, - }, - new: { - parent: elm.doc!.querySelector('VoltageLevel[name="J1"]')!, - }, - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.be.null; - expect(elm.doc!.querySelector('VoltageLevel[name="J1"] > Bay[name="Q01"]')) - .to.not.be.null; - expect( - elm.doc!.querySelector('VoltageLevel[name="J1"] > Bay[name="Q01"]') - ?.nextElementSibling - ).to.equal(elm.doc!.querySelector('VoltageLevel[name="J1"] > Function')); - }); - - it('does not move an element in case of name conflict', () => { - elm.dispatchEvent( - newActionEvent({ - old: { - parent, - element, - reference, - }, - new: { - parent: elm.doc!.querySelector('VoltageLevel[name="J1"]')!, - reference: null, - }, - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.be.null; - expect(elm.doc!.querySelector('VoltageLevel[name="J1"] > Bay[name="Q01"]')) - .to.not.be.null; - expect( - elm.doc!.querySelector('VoltageLevel[name="J1"] > Bay[name="Q01"]') - ?.nextElementSibling - ).to.be.null; - }); - - it('updates an element on receiving an Update action', () => { - const newAttributes: Record = {}; - newAttributes['name'] = 'Q03'; - - elm.dispatchEvent( - newActionEvent(createUpdateAction(element, newAttributes)) - ); - - expect(element.parentElement).to.equal(parent); - expect(element).to.have.attribute('name', 'Q03'); - expect(element).to.not.have.attribute('desc'); - }); - - it('allows empty string as attribute value', () => { - const newAttributes: Record = {}; - newAttributes['name'] = ''; - - elm.dispatchEvent( - newActionEvent(createUpdateAction(element, newAttributes)) - ); - - expect(element.parentElement).to.equal(parent); - expect(element).to.have.attribute('name', ''); - expect(element).to.not.have.attribute('desc'); - }); - - it('does not update an element in case of name conflict', () => { - const newAttributes: Record = {}; - newAttributes['name'] = 'Q02'; - - elm.dispatchEvent( - newActionEvent(createUpdateAction(element, newAttributes)) - ); - - expect(element.parentElement).to.equal(parent); - expect(element).to.have.attribute('name', 'Q01'); - expect(element).to.have.attribute('desc', 'Bay'); - }); - - it('does not update an element in case of id conflict', () => { - const newAttributes: Record = {}; - newAttributes['id'] = 'testId1'; - - elm.dispatchEvent( - newActionEvent( - createUpdateAction(doc.querySelector('LNodeType')!, newAttributes) - ) - ); - - expect(elm.doc!.querySelector('LNodeType[id="testId"]')).to.exist; - expect(elm.doc!.querySelector('LNodeType[id="testId1"]')).to.not.exist; - }); - - it('carries out subactions sequentially on receiving a ComplexAction', () => { - const child3 = elm.doc!.createElement('newBay'); - elm.dispatchEvent( - newActionEvent({ - title: 'Test complex action', - actions: [ - { - old: { element }, - new: { element: child3 }, - }, - { - old: { - parent, - element: child3, - reference, - }, - new: { - parent: elm.doc!.querySelector('VoltageLevel[name="J1"]')!, - reference: null, - }, - }, - ], - }) - ); - expect(parent.querySelector('Bay[name="Q01"]')).to.be.null; - expect(elm.doc!.querySelector('VoltageLevel[name="J1"] > newBay')).to.not.be - .null; - }); - - it('triggers a validation event on receiving a ComplexAction', async () => { - const child3 = elm.doc!.createElement('newBay'); - elm.dispatchEvent( - newActionEvent({ - title: 'Test complex action', - actions: [ - { - old: { element }, - new: { element: child3 }, - }, - { - old: { - parent, - element: child3, - reference, - }, - new: { - parent: elm.doc!.querySelector('VoltageLevel[name="J1"]')!, - reference: null, - }, - }, - ], - }) - ); - await elm.updateComplete; - - expect(validateEvent).to.be.calledOnce; - }); - - it('does not exchange doc with empty complex action', async () => { - elm.dispatchEvent( - newActionEvent({ - title: 'Test complex action', - actions: [], - }) - ); - await elm.updateComplete; - - expect(doc).to.equal(elm.doc); - }); - - it('does not trigger validation with empty complex action', async () => { - elm.dispatchEvent( - newActionEvent({ - title: 'Test complex action', - actions: [], - }) - ); - await elm.updateComplete; - - expect(validateEvent).to.not.been.called; - }); -}); diff --git a/packages/openscd/test/unit/Editor.test.ts b/packages/openscd/test/unit/Editor.test.ts new file mode 100644 index 0000000000..f26079472a --- /dev/null +++ b/packages/openscd/test/unit/Editor.test.ts @@ -0,0 +1,486 @@ +import { html, fixture, expect } from '@open-wc/testing'; + +import '../../src/addons/Editor.js'; +import { OscdEditor } from '../../src/addons/Editor.js'; +import { Insert, newEditEvent, Remove, Update } from '@openscd/core'; +import { CommitDetail, LogDetail } from '@openscd/core/foundation/deprecated/history.js'; + + +describe('OSCD-Editor', () => { + let element: OscdEditor; + let host: HTMLElement; + let scd: XMLDocument; + + let voltageLevel1: Element; + let voltageLevel2: Element; + let bay1: Element; + let bay2: Element; + let bay4: Element; + let bay5: Element; + let lnode1: Element; + let lnode2: Element; + + const nsXsi = 'urn:example.com'; + const nsTd = 'urn:typedesigner.com'; + + beforeEach(async () => { + scd = new DOMParser().parseFromString( + ` + + + + + + + + + + + + + + + `, + 'application/xml', + ); + + host = document.createElement('div'); + + element = await fixture(html``, { parentNode: host }); + + voltageLevel1 = scd.querySelector('VoltageLevel[name="v1"]')!; + voltageLevel2 = scd.querySelector('VoltageLevel[name="v2"]')!; + bay1 = scd.querySelector('Bay[name="b1"]')!; + bay2 = scd.querySelector('Bay[name="b2"]')!; + bay4 = scd.querySelector('Bay[name="b4"]')!; + bay5 = scd.querySelector('Bay[name="b5"]')!; + lnode1 = scd.querySelector('LNode[name="l1"]')!; + lnode2 = scd.querySelector('LNode[name="l2"]')!; + }); + + describe('Editing', () => { + it('should insert new node', () => { + const newNode = scd.createElement('Bay'); + newNode.setAttribute('name', 'b3'); + + const insert: Insert = { + parent: voltageLevel1, + node: newNode, + reference: null + }; + + host.dispatchEvent(newEditEvent(insert)); + + const newNodeFromScd = scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]'); + + expect(newNodeFromScd).to.deep.equal(newNode); + }); + + it('should insert new node before reference', () => { + const newNode = scd.createElement('Bay'); + newNode.setAttribute('name', 'b3'); + + const insert: Insert = { + parent: voltageLevel1, + node: newNode, + reference: bay1 + }; + + host.dispatchEvent(newEditEvent(insert)); + + const newNodeFromScd = scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]'); + + expect(newNodeFromScd?.nextSibling).to.deep.equal(bay1); + }); + + it('should move node when inserting existing node', () => { + const insertMove: Insert = { + parent: voltageLevel1, + node: bay2, + reference: null + }; + + host.dispatchEvent(newEditEvent(insertMove)); + + expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.be.null; + expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b2"]')).to.deep.equal(bay2); + }); + + it('should remove node', () => { + const remove: Remove = { + node: bay1 + }; + + host.dispatchEvent(newEditEvent(remove)); + + expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b1"]')).to.be.null; + }); + + describe('Update', () => { + it('should add new attributes and leave old attributes', () => { + const bay1NewAttributes = { + desc: 'new description', + type: 'Superbay' + }; + + const oldAttributes = elementAttributesToMap(bay1); + + const update: Update = { + element: bay1, + attributes: bay1NewAttributes + }; + + host.dispatchEvent(newEditEvent(update)); + + const updatedElement = scd.querySelector('Bay[name="b1"]')!; + + const expectedAttributes = { + ...oldAttributes, + ...bay1NewAttributes + }; + + expect(elementAttributesToMap(updatedElement)).to.deep.equal(expectedAttributes); + }); + + it('should remove attribute with null value', () => { + const bay1NewAttributes = { + kind: null + }; + + const update: Update = { + element: bay1, + attributes: bay1NewAttributes + }; + + host.dispatchEvent(newEditEvent(update)); + + const updatedElement = scd.querySelector('Bay[name="b1"]')!; + + expect(updatedElement.getAttribute('kind')).to.be.null; + }); + + it('should change, add and remove attributes in one update', () => { + const bay1NewAttributes = { + name: 'b5', + kind: null, + desc: 'new description' + }; + + const oldAttributes = elementAttributesToMap(bay1); + + const update: Update = { + element: bay1, + attributes: bay1NewAttributes + }; + + host.dispatchEvent(newEditEvent(update)); + + const updatedElement = scd.querySelector(`Bay[name="${bay1NewAttributes.name}"]`)!; + + const { kind, ...expectedAttributes } = { + ...oldAttributes, + ...bay1NewAttributes + }; + + expect(elementAttributesToMap(updatedElement)).to.deep.equal(expectedAttributes); + }); + + describe('namespaced attributes', () => { + it('should update attribute with namespace', () => { + const update: Update = { + element: lnode1, + attributes: { + type: { value: 'newType', namespaceURI: 'xsi' } + } + }; + + host.dispatchEvent(newEditEvent(update)); + + expect(lnode1.getAttributeNS('xsi', 'type')).to.equal('newType'); + }); + + it('should handle multiple namespaces', () => { + const update: Update = { + element: lnode1, + attributes: { + type: { value: 'newTypeXSI', namespaceURI: nsXsi } + } + }; + + host.dispatchEvent(newEditEvent(update)); + + const update2: Update = { + element: lnode1, + attributes: { + type: { value: 'newTypeTD', namespaceURI: nsTd } + } + }; + + host.dispatchEvent(newEditEvent(update2)); + + expect(lnode1.getAttributeNS(nsXsi, 'type')).to.equal('newTypeXSI'); + expect(lnode1.getAttributeNS(nsTd, 'type')).to.equal('newTypeTD'); + }); + + it('should remove namespaced attribute', () => { + const update: Update = { + element: lnode2, + attributes: { + type: { value: null, namespaceURI: nsXsi } + } + }; + + host.dispatchEvent(newEditEvent(update)); + + expect(lnode2.getAttributeNS(nsXsi, 'type')).to.be.null; + expect(lnode2.getAttributeNS(nsTd, 'type')).to.equal('typeTD'); + }); + + it('should add and remove multiple normal and namespaced attributes', () => { + const update: Update = { + element: lnode2, + attributes: { + type: { value: null, namespaceURI: nsXsi }, + kind: { value: 'td-kind', namespaceURI: nsTd }, + normalAttribute: 'normalValue', + lnClass: null + } + }; + + host.dispatchEvent(newEditEvent(update)); + + expect(lnode2.getAttributeNS(nsXsi, 'type')).to.be.null; + expect(lnode2.getAttributeNS(nsTd, 'kind')).to.equal('td-kind'); + expect(lnode2.getAttribute('normalAttribute')).to.equal('normalValue'); + expect(lnode2.getAttribute('lnClass')).to.be.null; + }); + }); + + describe('Complex action', () => { + it('should apply each edit from a complex edit', () => { + const newNode = scd.createElement('Bay'); + newNode.setAttribute('name', 'b3'); + + const insert: Insert = { + parent: voltageLevel1, + node: newNode, + reference: bay1 + }; + + const remove: Remove = { + node: bay2 + }; + + const update: Update = { + element: bay1, + attributes: { + desc: 'new description' + } + }; + + host.dispatchEvent(newEditEvent([insert, remove, update])); + + expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.deep.equal(newNode); + expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.be.null; + expect(scd.querySelector('Bay[name="b1"]')?.getAttribute('desc')).to.equal('new description'); + }); + }); + + describe('log edits', () => { + let log: LogDetail[] = []; + beforeEach(() => { + log = []; + + element.addEventListener('log', (e: CustomEvent) => { + log.push(e.detail); + }); + }); + + it('should log edit for user event', () => { + const remove: Remove = { + node: bay2, + }; + + host.dispatchEvent(newEditEvent(remove, 'user')); + + expect(log).to.have.lengthOf(1); + const logEntry = log[0] as CommitDetail; + expect(logEntry.kind).to.equal('action'); + expect(logEntry.title).to.equal('[editing.deleted]'); + expect(logEntry.redo).to.deep.equal(remove); + }); + + it('should not log edit for undo or redo event', () => { + const remove: Remove = { + node: bay2, + }; + + host.dispatchEvent(newEditEvent(remove, 'redo')); + host.dispatchEvent(newEditEvent(remove, 'undo')); + + expect(log).to.have.lengthOf(0); + }); + + describe('validate after edit', () => { + let hasTriggeredValidate = false; + beforeEach(() => { + hasTriggeredValidate = false; + + element.addEventListener('validate', () => { + hasTriggeredValidate = true; + }); + }); + + it('should dispatch validate event after edit', async () => { + const remove: Remove = { + node: bay2, + }; + + host.dispatchEvent(newEditEvent(remove)); + + await element.updateComplete; + + expect(hasTriggeredValidate).to.be.true; + }); + }); + }); + }); + }); + + describe('Undo/Redo', () => { + let log: CommitDetail[] = []; + beforeEach(() => { + log = []; + + element.addEventListener('log', (e: CustomEvent) => { + log.push(e.detail as CommitDetail); + }); + }); + + it('should undo insert', () => { + const newNode = scd.createElement('Bay'); + newNode.setAttribute('name', 'b3'); + + const insert: Insert = { + parent: voltageLevel1, + node: newNode, + reference: null + }; + + host.dispatchEvent(newEditEvent(insert)); + + const undoInsert = log[0].undo as Remove; + + host.dispatchEvent(newEditEvent(undoInsert)); + + expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; + }); + + it('should undo remove', () => { + const remove: Remove = { + node: bay4 + }; + + host.dispatchEvent(newEditEvent(remove)); + + const undoRemove = log[0].undo as Insert; + + host.dispatchEvent(newEditEvent(undoRemove)); + + const bay4FromScd = scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b4"]'); + expect(bay4FromScd).to.deep.equal(bay4); + }); + + it('should undo update', () => { + const update: Update = { + element: bay1, + attributes: { + desc: 'new description', + kind: 'superbay' + } + }; + + host.dispatchEvent(newEditEvent(update)); + + const undoUpdate = log[0].undo as Update; + + host.dispatchEvent(newEditEvent(undoUpdate)); + + expect(bay1.getAttribute('desc')).to.be.null; + expect(bay1.getAttribute('kind')).to.equal('bay'); + }); + + it('should redo previously undone action', () => { + const newNode = scd.createElement('Bay'); + newNode.setAttribute('name', 'b3'); + + const insert: Insert = { + parent: voltageLevel1, + node: newNode, + reference: null + }; + + host.dispatchEvent(newEditEvent(insert)); + + const undoIsert = log[0].undo; + const redoInsert = log[0].redo; + + host.dispatchEvent(newEditEvent(undoIsert)); + + expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; + + host.dispatchEvent(newEditEvent(redoInsert)); + + expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.deep.equal(newNode); + }); + + it('should undo and redo complex edit', () => { + const newNode = scd.createElement('Bay'); + newNode.setAttribute('name', 'b3'); + + const insert: Insert = { + parent: voltageLevel1, + node: newNode, + reference: bay1 + }; + + const remove: Remove = { + node: bay2 + }; + + const update: Update = { + element: bay1, + attributes: { + desc: 'new description' + } + }; + + host.dispatchEvent(newEditEvent([insert, remove, update])); + + const undoComplex = log[0].undo; + const redoComplex = log[0].redo; + + host.dispatchEvent(newEditEvent(undoComplex)); + + expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; + expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.deep.equal(bay2); + expect(bay1.getAttribute('desc')).to.be.null; + + host.dispatchEvent(newEditEvent(redoComplex)); + + expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.deep.equal(newNode); + expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.be.null; + expect(bay1.getAttribute('desc')).to.equal('new description'); + }); + }); +}); + +function elementAttributesToMap(element: Element): Record { + const attributes: Record = {}; + Array.from(element.attributes).forEach(attr => { + attributes[attr.name] = attr.value; + }); + + return attributes; +} + diff --git a/packages/openscd/test/unit/Historing.test.ts b/packages/openscd/test/unit/Historing.test.ts index f85ac3346a..c93869afba 100644 --- a/packages/openscd/test/unit/Historing.test.ts +++ b/packages/openscd/test/unit/Historing.test.ts @@ -1,7 +1,7 @@ import { expect, fixture, html } from '@open-wc/testing'; import '../mock-open-scd.js'; -import { MockAction } from './mock-actions.js'; +import { mockEdits } from '../mock-edits.js'; import { MockOpenSCD } from '../mock-open-scd.js'; import { @@ -107,7 +107,8 @@ describe('HistoringElement', () => { newLogEvent({ kind: 'action', title: 'test MockAction', - action: MockAction.cre, + redo: mockEdits.insert(), + undo: mockEdits.insert() }) ); element.requestUpdate(); @@ -126,18 +127,6 @@ describe('HistoringElement', () => { it('has no next action', () => expect(element).to.have.property('nextAction', -1)); - it('does not log derived actions', () => { - expect(element).property('history').to.have.lengthOf(1); - element.dispatchEvent( - newLogEvent({ - kind: 'action', - title: 'test MockAction', - action: (element.history[0]).action, - }) - ); - expect(element).property('history').to.have.lengthOf(1); - }); - it('can reset its log', () => { element.dispatchEvent(newLogEvent({ kind: 'reset' })); expect(element).property('log').to.be.empty; @@ -160,7 +149,8 @@ describe('HistoringElement', () => { newLogEvent({ kind: 'action', title: 'test MockAction', - action: MockAction.del, + redo: mockEdits.remove(), + undo: mockEdits.remove() }) ); }); @@ -189,7 +179,8 @@ describe('HistoringElement', () => { newLogEvent({ kind: 'action', title: 'test MockAction', - action: MockAction.mov, + redo: mockEdits.insert(), + undo: mockEdits.insert() }) ); await element.updateComplete; diff --git a/packages/openscd/test/unit/Plugging.test.ts b/packages/openscd/test/unit/Plugging.test.ts index 60a5605585..51129b7833 100644 --- a/packages/openscd/test/unit/Plugging.test.ts +++ b/packages/openscd/test/unit/Plugging.test.ts @@ -4,6 +4,8 @@ import '../mock-open-scd.js'; import { MockOpenSCD } from '../mock-open-scd.js'; import { TextField } from '@material/mwc-textfield'; +import { Plugin } from '../../src/plugin'; +import { ConfigurePluginDetail, ConfigurePluginEvent, newConfigurePluginEvent } from '../../src/plugin.events'; describe('OpenSCD-Plugin', () => { let element: MockOpenSCD; @@ -26,8 +28,9 @@ describe('OpenSCD-Plugin', () => { await element.updateComplete; }); - it('stores default plugins on load', () => - expect(element.layout).property('editors').to.have.lengthOf(6)); + it('stores default plugins on load', () =>{ + expect(element.layout).property('editors').to.have.lengthOf(6) + }); it('has Locale property', async () => { expect(element).to.have.property('locale'); @@ -125,26 +128,42 @@ describe('OpenSCD-Plugin', () => { ); }); - it('requires a name and a valid URL to add a plugin', async () => { - primaryAction.click(); - expect(element.layout.pluginDownloadUI).to.have.property('open', true); + describe('requires a name and a valid URL to add a plugin', async () => { - src.value = 'http://example.com/plugin.js'; - await src.updateComplete; - primaryAction.click(); - expect(element.layout.pluginDownloadUI).to.have.property('open', true); + it('does not add without user interaction', async () => { + primaryAction.click(); + expect(element.layout.pluginDownloadUI).to.have.property('open', true); + }) - src.value = 'notaURL'; - name.value = 'testName'; - await src.updateComplete; - await name.updateComplete; - primaryAction.click(); - expect(element.layout.pluginDownloadUI).to.have.property('open', true); + it('does not add without a name', async () => { + src.value = 'http://example.com/plugin.js'; + await src.updateComplete; + primaryAction.click(); + expect(element.layout.pluginDownloadUI).to.have.property('open', true); + }) + + it('does not add plugin with incorrect url', async () => { + src.value = 'notaURL'; + name.value = 'testName'; + await src.updateComplete; + await name.updateComplete; + primaryAction.click(); + expect(element.layout.pluginDownloadUI).to.have.property('open', true); + }); + + + it('adds a plugin with a name and a valid URL', async () => { + name.value = 'testName'; + await name.updateComplete; + + src.value = 'http://localhost:8080/plugin/plugin.js'; + await src.updateComplete; + + primaryAction.click(); + + expect(element.layout.pluginDownloadUI).to.have.property('open', false); + }) - src.value = 'http://example.com/plugin.js'; - await src.updateComplete; - primaryAction.click(); - expect(element.layout.pluginDownloadUI).to.have.property('open', false); }); it('adds a new editor kind plugin on add button click', async () => { @@ -156,6 +175,7 @@ describe('OpenSCD-Plugin', () => { await element.updateComplete; expect(element.layout.editors).to.have.lengthOf(7); }); + it('adds a new menu kind plugin on add button click', async () => { const lengthMenuKindPlugins = element.layout.menuEntries.length; src.value = 'http://example.com/plugin.js'; @@ -167,6 +187,7 @@ describe('OpenSCD-Plugin', () => { await element.updateComplete; expect(element.layout.menuEntries).to.have.lengthOf(lengthMenuKindPlugins + 1); }); + it('sets requireDoc and position for new menu kind plugin', async () => { src.value = 'http://example.com/plugin.js'; name.value = 'testName'; @@ -195,4 +216,218 @@ describe('OpenSCD-Plugin', () => { expect(element.layout.validators).to.have.lengthOf(3); }); }); + + describe('ConfigurePluginEvent', () => { + + type TestCase = { + desc: string + currentPlugins: Plugin[] + eventDetails: ConfigurePluginDetail + expectedPlugins: Plugin[] + } + + const featureTests: TestCase[] = [ + { + desc: ` + adds plugin, + if a plugin with same name and kind does not exsits + and there is a config + `, + currentPlugins: [], + eventDetails: { + name: "new plugin", + kind: "editor", + config: { + name: "new plugin", + kind: "editor", + src: "https://example.com/new-plugin.js", + installed: false, + }, + }, + expectedPlugins: [ + { + name: "new plugin", + kind: "editor", + src: "https://example.com/new-plugin.js", + installed: false, + } + ] + }, + { + desc: ` + adds plugin, + if a plugin with same exists but with different kind + and there is a config + `, + currentPlugins: [ + { + name: "an existing plugin", + kind: "menu", + src: "https://example.com/new-plugin.js", + installed: false, + } + ], + eventDetails: { + name: "an existing plugin", + kind: "editor", + config: { + name: "an existing plugin", + kind: "editor", + src: "https://example.com/new-plugin.js", + installed: false, + }, + }, + expectedPlugins: [ + { + name: "an existing plugin", + kind: "menu", + src: "https://example.com/new-plugin.js", + installed: false, + }, + { + name: "an existing plugin", + kind: "editor", + src: "https://example.com/new-plugin.js", + installed: false, + } + ] + }, + { + desc: ` + changes plugin, + if a plugin exists with same name and kind, and there is a config + `, + currentPlugins: [ + { + name: "I want to change this plugin", + kind: "editor", + src: "https://example.com/new-plugin.js", + installed: false, + } + ], + eventDetails: { + name: "I want to change this plugin", + kind: "editor", + config: { + name: "I want to change this plugin", + kind: "editor", + src: "https://example.com/changed-url.js", + installed: true, + }, + }, + expectedPlugins: [ + { + name: "I want to change this plugin", + kind: "editor", + src: "https://example.com/changed-url.js", + installed: true, + }, + ] + }, + { + desc: ` + removes plugin, + if it finds it by name and kind and the econfig is 'null' + `, + currentPlugins: [{ + name: "plugin to remove", + kind: "editor", + src: "https://example.com/plugin-to-remove.js", + installed: false, + }], + eventDetails: { + name: "plugin to remove", + kind: "editor", + config: null + }, + expectedPlugins: [] + }, + { + desc: ` + does not remove plugin, + if it does not find it by name + `, + currentPlugins: [{ + name: "plugin to remove", + kind: "editor", + src: "https://example.com/plugin-to-remove.js", + installed: false, + }], + eventDetails: { + name: "wrong name", + kind: "editor", + config: null + }, + expectedPlugins: [{ + name: "plugin to remove", + kind: "editor", + src: "https://example.com/plugin-to-remove.js", + installed: false, + }] + }, + { + desc: ` + does not remove plugin, + if it does not find it by kind + `, + currentPlugins: [{ + name: "plugin to remove, but wrong kind", + kind: "editor", + src: "https://example.com/plugin-to-remove.js", + installed: false, + }], + eventDetails: { + name: "plugin to remove, but wrong kind", + kind: "menu", + config: null + }, + expectedPlugins: [{ + name: "plugin to remove, but wrong kind", + kind: "editor", + src: "https://example.com/plugin-to-remove.js", + installed: false, + }] + }, + ] + + featureTests.forEach(testFeature) + + function testFeature(tc: TestCase) { + it(tc.desc, async () => { + // ARRANGE + + // @ts-ignore: we use the private to arrange the scenario + element.storePlugins(tc.currentPlugins) + await element.updateComplete + + // ACT + const event = newConfigurePluginEvent(tc.eventDetails.name, tc.eventDetails.kind, tc.eventDetails.config) + element.layout.dispatchEvent(event) + await element.updateComplete + + // ASSERT + + // I remove all the keys that we don't have because + // the stored plugins get new keys and + // I could not figure how to compare the two lists + // I've tried to use chai's deep.members and deep.include.members + // and others but non of them worked. + const keys = ["name", "kind", "src", "installed"] + const storedPlugins = element.layout.plugins.map((plugin) => { + Object.keys(plugin).forEach((key) => { + if(!keys.includes(key)) { + delete plugin[key] + } + }) + + return plugin + }) + + const msg = `expected: ${JSON.stringify(tc.expectedPlugins)} but got: ${JSON.stringify(element.layout.plugins)}` + expect(tc.expectedPlugins).to.have.deep.members(storedPlugins, msg) + + }) + } + + }) }); diff --git a/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts b/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts new file mode 100644 index 0000000000..6676a1ffed --- /dev/null +++ b/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts @@ -0,0 +1,148 @@ +import { html, fixture, expect } from '@open-wc/testing'; + +import { + Create, + Delete, + EditorAction, + isCreate, + isDelete, + isMove, + isReplace, + isSimple, + isUpdate, + Move, + Replace, + SimpleAction, + Update, + createUpdateAction +} from '@openscd/core/foundation/deprecated/editor.js'; +import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; + +import { convertEditV1toV2 } from '../../src/addons/editor/edit-v1-to-v2-converter.js'; + + +describe('edit-v1-to-v2-converter', () => { + const doc = new DOMParser().parseFromString( + ` + + + + + + `, + 'application/xml' + ); + const substation = doc.querySelector('Substation')!; + const substation2 = doc.querySelector('Substation[name="sub2"]')!; + const bay = doc.querySelector('Bay')!; + + it('should convert delete to remove', () => { + const deleteAction: Delete = { + old: { + parent: substation, + element: bay + } + }; + + const remove = convertEditV1toV2(deleteAction); + + const expectedRemove: Remove = { + node: bay + }; + + expect(remove).to.deep.equal(expectedRemove); + }); + + it('should convert create to insert', () => { + const newBay = doc.createElement('Bay'); + newBay.setAttribute('name', 'bay2'); + + const createAction: Create = { + new: { + parent: substation, + element: newBay + } + }; + + const insert = convertEditV1toV2(createAction); + + const expectedInsert: Insert = { + parent: substation, + node: newBay, + reference: null + }; + + expect(insert).to.deep.equal(expectedInsert); + }); + + it('should convert update to updateV2', () => { + const newAttributes = { + name: 'newBayName', + }; + const updateAction = createUpdateAction(bay, newAttributes); + + const updateV2 = convertEditV1toV2(updateAction); + + const expectedUpdateV2: UpdateV2 = { + element: bay, + attributes: { + ...newAttributes, + desc: null + } + }; + + expect(updateV2).to.deep.equal(expectedUpdateV2); + }); + + it('should convert move to insert', () => { + const moveAction: Move = { + old: { + parent: substation, + element: bay, + reference: null + }, + new: { + parent: substation2, + reference: null + } + }; + + const insert = convertEditV1toV2(moveAction); + + const expectedInsert: Insert = { + parent: substation2, + node: bay, + reference: null + }; + + expect(insert).to.deep.equal(expectedInsert); + }); + + it('should convert replace to complex action with remove and insert', () => { + const ied = doc.createElement('IED'); + ied.setAttribute('name', 'ied'); + + const replace: Replace = { + old: { + element: bay + }, + new: { + element: ied + } + }; + + const [ remove, insert ] = convertEditV1toV2(replace) as Edit[]; + + const expectedRemove: Remove = { + node: bay + }; + const expectedInsert: Insert = { + parent: substation, + node: ied, + reference: bay.nextSibling + }; + + expect(remove).to.deep.equal(expectedRemove); + expect(insert).to.deep.equal(expectedInsert); + }); +}); diff --git a/packages/openscd/test/unit/foundation.test.ts b/packages/openscd/test/unit/foundation.test.ts index 8ea76ba66a..081fc52362 100644 --- a/packages/openscd/test/unit/foundation.test.ts +++ b/packages/openscd/test/unit/foundation.test.ts @@ -172,20 +172,22 @@ describe('foundation', () => { }); }); - describe('ifImplemented', () => { - let nonEmpty: HTMLElement; - let empty: HTMLElement; +// skipped becase of flakiness + describe.skip('ifImplemented', () => { - beforeEach(async () => { - nonEmpty = await fixture(html`

    ${ifImplemented('test')}

    `); - empty = await fixture(html`

    ${ifImplemented({})}

    `); + + it('renders non-empty objects into its template', async () => { + const nonEmpty = await fixture(html`

    ${ifImplemented('test')}

    `); + console.log("nonEmpty", nonEmpty.outerHTML); + expect(nonEmpty).dom.to.have.text('test') }); - it('renders non-empty objects into its template', () => - expect(nonEmpty).dom.to.have.text('test')); + it('does not render empty objects into its template', async () => { + const empty = await fixture(html`

    ${ifImplemented({})}

    `); + console.log("empty", empty.outerHTML); + expect(empty).dom.to.be.empty + }); - it('does not render empty objects into its template', () => - expect(empty).dom.to.be.empty); }); describe('isSame', () => { diff --git a/packages/openscd/test/unit/wizard-dialog.test.ts b/packages/openscd/test/unit/wizard-dialog.test.ts index 3419c35a0b..458a19705b 100644 --- a/packages/openscd/test/unit/wizard-dialog.test.ts +++ b/packages/openscd/test/unit/wizard-dialog.test.ts @@ -1,17 +1,15 @@ import { html, fixture, expect } from '@open-wc/testing'; -import './mock-editor.js'; - import { Button } from '@material/mwc-button'; import '../../src/wizard-textfield.js'; import '../../src/wizard-dialog.js'; import { WizardDialog } from '../../src/wizard-dialog.js'; -import { WizardInputElement } from '../../src/foundation.js'; +import { checkValidity, WizardInputElement } from '../../src/foundation.js'; import { WizardCheckbox } from '../../src/wizard-checkbox.js'; import { WizardSelect } from '../../src/wizard-select.js'; import { WizardTextField } from '../../src/wizard-textfield.js'; -import { EditorAction } from '@openscd/core/foundation/deprecated/editor.js'; +import { ComplexAction, Create, Delete, EditorAction } from '@openscd/core/foundation/deprecated/editor.js'; describe('wizard-dialog', () => { let element: WizardDialog; @@ -230,9 +228,7 @@ describe('wizard-dialog', () => { let host: Element; beforeEach(async () => { - element = await fixture( - html`` - ).then(elm => elm.querySelector('wizard-dialog')!); + element = await fixture(html``); localStorage.setItem('mode', 'pro'); element.requestUpdate(); await element.updateComplete; @@ -274,6 +270,9 @@ describe('wizard-dialog', () => { }); it('commits the code action on primary button click', async () => { + let editorAction: ComplexAction; + element.addEventListener('editor-action', (action) => editorAction = action.detail.action as ComplexAction); + element.dialog ?.querySelector('ace-editor') ?.setAttribute('value', ''); @@ -282,7 +281,22 @@ describe('wizard-dialog', () => { ?.querySelector