diff --git a/convertEdit.spec.ts b/convertEdit.spec.ts
index 9a9964b..c0090e4 100644
--- a/convertEdit.spec.ts
+++ b/convertEdit.spec.ts
@@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { expect } from '@open-wc/testing';
-import { Insert, Remove, Update } from './editv1.js';
+import { Update } from './editv1.js';
import { convertEdit } from './convertEdit.js';
-import { SetAttributes } from './editv2.js';
+import { Insert, Remove, SetAttributes } from './editv2.js';
const doc = new DOMParser().parseFromString(
'',
@@ -43,9 +43,9 @@ const update: Update = {
value: 'value2',
namespaceURI: 'http://example.org/myns2',
},
- attr3: {
- value: 'value3',
- namespaceURI: null,
+ invalid: {
+ value: 'great, but with empty namespace URI',
+ namespaceURI: '',
},
},
};
@@ -55,6 +55,7 @@ const setAttributes: SetAttributes = {
attributes: {
name: 'A2',
desc: null,
+ ['__proto__']: 'a string',
},
attributesNS: {
'http://example.org/myns': {
diff --git a/convertEdit.ts b/convertEdit.ts
index 6abbf13..6e4a0d1 100644
--- a/convertEdit.ts
+++ b/convertEdit.ts
@@ -1,20 +1,22 @@
import {
Edit,
- isComplex,
- isInsert,
+ isComplexEdit,
isNamespaced,
isUpdate,
- isRemove,
Update,
} from './editv1.js';
-import { EditV2 } from './editv2.js';
+import {
+ AttributesV2,
+ AttributesNS,
+ EditV2,
+ isInsert,
+ isRemove,
+} from './editv2.js';
function convertUpdate(edit: Update): EditV2 {
- const attributes: Partial> = {};
- const attributesNS: Partial<
- Record>>
- > = {};
+ let attributes: AttributesV2 = {};
+ const attributesNS: AttributesNS = {};
Object.entries(edit.attributes).forEach(([key, value]) => {
if (isNamespaced(value!)) {
@@ -26,9 +28,9 @@ function convertUpdate(edit: Update): EditV2 {
if (!attributesNS[ns]) {
attributesNS[ns] = {};
}
- attributesNS[ns][key] = value.value;
+ attributesNS[ns] = { ...attributesNS[ns], [key]: value.value };
} else {
- attributes[key] = value;
+ attributes = { ...attributes, [key]: value };
}
});
@@ -45,7 +47,7 @@ export function convertEdit(edit: Edit): EditV2 {
if (isUpdate(edit)) {
return convertUpdate(edit);
}
- if (isComplex(edit)) {
+ if (isComplexEdit(edit)) {
return edit.map(convertEdit);
}
diff --git a/docs/plugin-api.md b/docs/plugin-api.md
index 3ac419f..be76c9e 100644
--- a/docs/plugin-api.md
+++ b/docs/plugin-api.md
@@ -52,20 +52,20 @@ Plugins communicate user intent to OpenSCD core by dispatching the following [cu
The **edit event** allows a plugin to describe the changes it wants to make to the current `doc`.
```typescript
-export type EditDetailV2 = {
+export type EditDetailV2 = {
edit: E;
title?: string;
squash?: boolean;
}
-export type EditEventV2 = CustomEvent>;
+export type EditEventV2 = CustomEvent>;
export type EditEventOptions = {
title?: string;
squash?: boolean;
}
-export function newEditEventV2(edit: E, options: EditEventOptions): EditEventV2 {
+export function newEditEventV2(edit: E, options: EditEventOptions): EditEventV2 {
return new CustomEvent('oscd-edit-v2', {
composed: true,
bubbles: true,
@@ -84,7 +84,7 @@ Its `title` property is a human-readable description of the edit.
The `squash` flag indicates whether the edit should be merged with the previous edit in the history.
-#### `Edit` type
+#### `EditV2` type
The `EditDetailV2` defined above contains an `edit` of this type:
```typescript
@@ -153,86 +153,6 @@ declare global {
}
```
-### `WizardEvent`
-
-The **wizard event** allows the plugin to request opening a modal dialog enabling the user to edit an arbitrary SCL `element`, regardless of how the dialog for editing this particular type of element looks and works.
-
-```typescript
-/* eslint-disable no-undef */
-interface WizardRequestBase {
- subWizard?: boolean; // TODO: describe what this currently means
-}
-
-export interface EditWizardRequest extends WizardRequestBase {
- element: Element;
-}
-
-export interface CreateWizardRequest extends WizardRequestBase {
- parent: Element;
- tagName: string;
-}
-
-export type WizardRequest = EditWizardRequest | CreateWizardRequest;
-
-type EditWizardEvent = CustomEvent;
-type CreateWizardEvent = CustomEvent;
-export type WizardEvent = EditWizardEvent | CreateWizardEvent;
-
-type CloseWizardEvent = CustomEvent;
-
-export function newEditWizardEvent(
- element: Element,
- subWizard?: boolean,
- eventInitDict?: CustomEventInit>
-): EditWizardEvent {
- return new CustomEvent('oscd-edit-wizard-request', {
- bubbles: true,
- composed: true,
- ...eventInitDict,
- detail: { element, subWizard, ...eventInitDict?.detail },
- });
-}
-
-export function newCreateWizardEvent(
- parent: Element,
- tagName: string,
- subWizard?: boolean,
- eventInitDict?: CustomEventInit>
-): CreateWizardEvent {
- return new CustomEvent('oscd-create-wizard-request', {
- bubbles: true,
- composed: true,
- ...eventInitDict,
- detail: {
- parent,
- tagName,
- subWizard,
- ...eventInitDict?.detail,
- },
- });
-}
-
-export function newCloseWizardEvent(
- wizard: WizardRequest,
- eventInitDict?: CustomEventInit>
-): CloseWizardEvent {
- return new CustomEvent('oscd-close-wizard', {
- bubbles: true,
- composed: true,
- ...eventInitDict,
- detail: wizard,
- });
-}
-
-declare global {
- interface ElementEventMap {
- ['oscd-edit-wizard-request']: EditWizardRequest;
- ['oscd-create-wizard-request']: CreateWizardRequest;
- ['oscd-close-wizard']: WizardEvent;
- }
-}
-```
-
## Theming
OpenSCD core sets the following CSS variables on the plugin:
@@ -256,4 +176,4 @@ OpenSCD core sets the following CSS variables on the plugin:
--oscd-text-font-mono: var(--oscd-theme-text-font-mono, 'Roboto Mono');
--oscd-icon-font: var(--oscd-theme-icon-font, 'Material Icons');
}
-```
\ No newline at end of file
+```
diff --git a/edit-event-v2.ts b/edit-event-v2.ts
new file mode 100644
index 0000000..214d8ae
--- /dev/null
+++ b/edit-event-v2.ts
@@ -0,0 +1,33 @@
+import { EditV2 } from './editv2.js';
+
+export type EditDetailV2 = {
+ edit: E;
+ title?: string;
+ squash?: boolean;
+};
+
+export type EditEventV2 = CustomEvent<
+ EditDetailV2
+>;
+
+export type EditEventOptions = {
+ title?: string;
+ squash?: boolean;
+};
+
+export function newEditEventV2(
+ edit: E,
+ options?: EditEventOptions,
+): EditEventV2 {
+ return new CustomEvent>('oscd-edit-v2', {
+ composed: true,
+ bubbles: true,
+ detail: { ...options, edit },
+ });
+}
+
+declare global {
+ interface ElementEventMap {
+ ['oscd-edit-v2']: EditEventV2;
+ }
+}
diff --git a/edit-event.ts b/edit-event.ts
index 9483986..ce3ce6d 100644
--- a/edit-event.ts
+++ b/edit-event.ts
@@ -1,33 +1,17 @@
-import { EditV2 } from './editv2.js';
+import { Edit } from './editv1.js';
-export type EditDetailV2 = {
- edit: E;
- title?: string;
- squash?: boolean;
-};
+export type EditEvent = CustomEvent;
-export type EditEventV2 = CustomEvent<
- EditDetailV2
->;
-
-export type EditEventOptions = {
- title?: string;
- squash?: boolean;
-};
-
-export function newEditEventV2(
- edit: E,
- options?: EditEventOptions,
-): EditEventV2 {
- return new CustomEvent>('oscd-edit-v2', {
+export function newEditEvent(edit: E): EditEvent {
+ return new CustomEvent('oscd-edit-v2', {
composed: true,
bubbles: true,
- detail: { ...options, edit },
+ detail: edit,
});
}
declare global {
interface ElementEventMap {
- ['oscd-edit-v2']: EditEventV2;
+ ['oscd-edit']: EditEvent;
}
}
diff --git a/editv1.spec.ts b/editv1.spec.ts
index f90119b..ac26efe 100644
--- a/editv1.spec.ts
+++ b/editv1.spec.ts
@@ -28,14 +28,11 @@ describe('type guard functions for editv1', () => {
it('returns true for Remove', () => expect(remove).to.satisfy(isEdit));
- it('returns false for SetAttributes', () =>
- expect(setAttributes).to.not.satisfy(isEdit));
-
it('returns true for SetTextContent', () =>
expect(setTextContent).to.not.satisfy(isEdit));
it('returns false on mixed edit and editV2 array', () =>
- expect([update, setAttributes]).to.not.satisfy(isEdit));
+ expect([update, setTextContent]).to.not.satisfy(isEdit));
it('returns true on edit array', () =>
expect([update, remove, insert]).to.satisfy(isEdit));
diff --git a/editv1.ts b/editv1.ts
index bba871d..3674e03 100644
--- a/editv1.ts
+++ b/editv1.ts
@@ -1,68 +1,63 @@
-import { isSetAttributes } from './editv2.js';
-
-/** Intent to `parent.insertBefore(node, reference)` */
-export type Insert = {
- parent: Node;
- node: Node;
- reference: Node | null;
-};
+import { isInsert, isRemove, Insert, Remove } from './editv2.js';
export type NamespacedAttributeValue = {
value: string | null;
namespaceURI: string | null;
};
+
export type AttributeValue = string | null | NamespacedAttributeValue;
+
+export type Attributes = Partial>;
+
/** Intent to set or remove (if null) attributes on element */
export type Update = {
element: Element;
attributes: Partial>;
};
-/** Intent to remove a node from its ownerDocument */
-export type Remove = {
- node: Node;
-};
-
/** Represents the user's intent to change an XMLDocument */
export type Edit = Insert | Update | Remove | Edit[];
-export function isComplex(edit: Edit): edit is Edit[] {
- return edit instanceof Array;
-}
-
-export function isInsert(edit: Edit): edit is Insert {
- return (edit as Insert).parent !== undefined;
-}
-
export function isNamespaced(
- value: AttributeValue,
+ value: unknown,
): value is NamespacedAttributeValue {
- return value !== null && typeof value !== 'string';
+ return (
+ value !== null &&
+ typeof value === 'object' &&
+ 'namespaceURI' in value &&
+ typeof value.namespaceURI === 'string' &&
+ 'value' in value &&
+ typeof value.value === 'string'
+ );
}
-export function isUpdate(edit: Edit): edit is Update {
- return (
- (edit as Update).element !== undefined &&
- (edit as Update).attributes !== undefined
+export function isAttributes(attributes: unknown): attributes is Attributes {
+ if (attributes === null || typeof attributes !== 'object') {
+ return false;
+ }
+
+ return Object.entries(attributes).every(
+ ([key, value]) =>
+ typeof key === 'string' &&
+ (value === null || typeof value === 'string' || isNamespaced(value)),
);
}
-export function isRemove(edit: Edit): edit is Remove {
+export function isComplexEdit(edit: unknown): edit is Edit[] {
+ return edit instanceof Array && edit.every(isEdit);
+}
+
+export function isUpdate(edit: unknown): edit is Update {
return (
- (edit as Insert).parent === undefined && (edit as Remove).node !== undefined
+ (edit as Update).element instanceof Element &&
+ isAttributes((edit as Update).attributes)
);
}
-export type EditEvent = CustomEvent;
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function isEdit(edit: any): edit is Edit {
- if (isComplex(edit)) {
- return !edit.some(e => !isEdit(e));
+export function isEdit(edit: unknown): edit is Edit {
+ if (isComplexEdit(edit)) {
+ return true;
}
- return (
- !isSetAttributes(edit) &&
- (isUpdate(edit) || isInsert(edit) || isRemove(edit))
- );
+ return isUpdate(edit) || isInsert(edit) || isRemove(edit);
}
diff --git a/editv2.spec.ts b/editv2.spec.ts
index 575c676..0cdf5af 100644
--- a/editv2.spec.ts
+++ b/editv2.spec.ts
@@ -19,16 +19,16 @@ const insert: Insert = { parent: element, node: element, reference: null };
const remove: Remove = { node: element };
const setAttributes: SetAttributes = {
element,
- attributes: {},
- attributesNS: {},
+ attributes: { name: 'value' },
+ attributesNS: { namespaceURI: { name: 'value' } },
};
const setTextContent: SetTextContent = { element, textContent: '' };
-describe('type guard functions for editv2', () => {
- it('returns false on invalid Edit type', () =>
+describe('isEditV2', () => {
+ it('returns false for invalid Edit type', () =>
expect('invalid edit').to.not.satisfy(isEditV2));
- it('returns false on Update', () => expect(update).to.not.satisfy(isEditV2));
+ it('returns false for Update', () => expect(update).to.not.satisfy(isEditV2));
it('returns true for Insert', () => expect(insert).to.satisfy(isEditV2));
@@ -40,13 +40,13 @@ describe('type guard functions for editv2', () => {
it('returns true for SetTextContent', () =>
expect(setTextContent).to.satisfy(isEditV2));
- it('returns false on mixed edit and editV2 array', () =>
+ it('returns false for a mixed edit and editV2 array', () =>
expect([update, setAttributes]).to.not.satisfy(isEditV2));
- it('returns false on edit array', () =>
+ it('returns false for edit array', () =>
expect([update, update]).to.not.satisfy(isEditV2));
- it('returns true on editV2 array', () =>
+ it('returns true for editV2 array', () =>
expect([setAttributes, remove, insert, setTextContent]).to.satisfy(
isEditV2,
));
diff --git a/editv2.ts b/editv2.ts
index 719ab12..a2b10d0 100644
--- a/editv2.ts
+++ b/editv2.ts
@@ -16,11 +16,17 @@ export type SetTextContent = {
textContent: string;
};
+/** Record from attribute names to attribute values */
+export type AttributesV2 = Partial>;
+
+/** Record from namespace URIs to `Attributes` records */
+export type AttributesNS = Partial>;
+
/** Intent to set or remove (if `null`) `attributes`(-`NS`) on `element` */
export type SetAttributes = {
element: Element;
- attributes: Partial>;
- attributesNS: Partial>>>;
+ attributes?: AttributesV2;
+ attributesNS?: AttributesNS;
};
/** Intent to change some XMLDocuments */
@@ -31,43 +37,71 @@ export type EditV2 =
| Remove
| EditV2[];
-export function isComplex(edit: EditV2): edit is EditV2[] {
- return edit instanceof Array;
+export function isAttributesV2(
+ attributes: unknown,
+): attributes is AttributesV2 {
+ if (typeof attributes !== 'object' || attributes === null) {
+ return false;
+ }
+ return Object.entries(attributes).every(
+ ([key, value]) =>
+ typeof key === 'string' && (value === null || typeof value === 'string'),
+ );
+}
+
+export function isAttributesNS(
+ attributesNS: unknown,
+): attributesNS is AttributesNS {
+ if (typeof attributesNS !== 'object' || attributesNS === null) {
+ return false;
+ }
+ return Object.entries(attributesNS).every(
+ ([namespace, attributes]) =>
+ typeof namespace === 'string' &&
+ isAttributesV2(attributes as Record),
+ );
+}
+
+export function isComplexEditV2(edit: unknown): edit is EditV2[] {
+ return edit instanceof Array && edit.every(e => isEditV2(e));
}
-export function isSetTextContent(edit: EditV2): edit is SetTextContent {
+export function isSetTextContent(edit: unknown): edit is SetTextContent {
return (
- (edit as SetTextContent).element !== undefined &&
- (edit as SetTextContent).textContent !== undefined
+ (edit as SetTextContent).element instanceof Element &&
+ typeof (edit as SetTextContent).textContent === 'string'
);
}
-export function isRemove(edit: EditV2): edit is Remove {
+export function isRemove(edit: unknown): edit is Remove {
return (
- (edit as Insert).parent === undefined && (edit as Remove).node !== undefined
+ (edit as Insert).parent === undefined &&
+ (edit as Remove).node instanceof Node
);
}
-export function isSetAttributes(edit: EditV2): edit is SetAttributes {
+export function isSetAttributes(edit: unknown): edit is SetAttributes {
return (
- (edit as SetAttributes).element !== undefined &&
- (edit as SetAttributes).attributes !== undefined &&
- (edit as SetAttributes).attributesNS !== undefined
+ (edit as SetAttributes).element instanceof Element &&
+ isAttributesV2((edit as SetAttributes).attributes) &&
+ isAttributesNS((edit as SetAttributes).attributesNS)
);
}
-export function isInsert(edit: EditV2): edit is Insert {
+export function isInsert(edit: unknown): edit is Insert {
return (
- (edit as Insert).parent !== undefined &&
- (edit as Insert).node !== undefined &&
- (edit as Insert).reference !== undefined
+ ((edit as Insert).parent instanceof Element ||
+ (edit as Insert).parent instanceof Document ||
+ (edit as Insert).parent instanceof DocumentFragment) &&
+ (edit as Insert).node instanceof Node &&
+ ((edit as Insert).reference instanceof Node ||
+ (edit as Insert).reference === null)
);
}
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function isEditV2(edit: any): edit is EditV2 {
- if (isComplex(edit)) {
- return !edit.some(e => !isEditV2(e));
+export function isEditV2(edit: unknown): edit is EditV2 {
+ if (isComplexEditV2(edit)) {
+ return true;
}
return (
diff --git a/open-event.ts b/open-event.ts
new file mode 100644
index 0000000..99b7414
--- /dev/null
+++ b/open-event.ts
@@ -0,0 +1,21 @@
+export type OpenDetail = {
+ doc: XMLDocument;
+ docName: string;
+};
+
+/** Represents the intent to open `doc` with filename `docName`. */
+export type OpenEvent = CustomEvent;
+
+export function newOpenEvent(doc: XMLDocument, docName: string): OpenEvent {
+ return new CustomEvent('oscd-open', {
+ bubbles: true,
+ composed: true,
+ detail: { doc, docName },
+ });
+}
+
+declare global {
+ interface ElementEventMap {
+ ['oscd-open']: OpenEvent;
+ }
+}
diff --git a/oscd-api.ts b/oscd-api.ts
index 9d64091..93c6cb3 100644
--- a/oscd-api.ts
+++ b/oscd-api.ts
@@ -1,18 +1,20 @@
-export {
+export type {
+ AttributesV2,
+ AttributesNS,
EditV2,
Insert,
Remove,
SetAttributes,
SetTextContent,
- isComplex,
- isEditV2,
- isInsert,
- isRemove,
- isSetAttributes,
- isSetTextContent,
} from './editv2.js';
-export { Edit, Update, isEdit } from './editv1.js';
+export type {
+ Attributes,
+ AttributeValue,
+ Edit,
+ NamespacedAttributeValue,
+ Update,
+} from './editv1.js';
export type {
Commit,
@@ -21,4 +23,12 @@ export type {
TransactedCallback,
} from './Transactor.js';
-export { newEditEventV2 } from './edit-event.js';
+export type { EditEvent } from './edit-event.js';
+
+export type {
+ EditDetailV2,
+ EditEventOptions,
+ EditEventV2,
+} from './edit-event-v2.js';
+
+export type { OpenDetail, OpenEvent } from './open-event.js';
diff --git a/package.json b/package.json
index cd038c1..bd07b2e 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,16 @@
"version": "0.0.14",
"description": "OpenSCD API for IEC 61850 SCL files",
"type": "module",
- "main": "./dist/oscd-api.js",
- "types": "./dist/oscd-api.d.ts",
+ "exports": {
+ ".": {
+ "default": "./dist/oscd-api.js",
+ "types": "./dist/oscd-api.d.ts"
+ },
+ "./utils.js": {
+ "default": "./dist/utils.js",
+ "types": "./dist/utils.d.ts"
+ }
+ },
"scripts": {
"lint": "eslint . && prettier \"**/*.ts\" --check --ignore-path .gitignore",
"format": "eslint ./*.ts --fix",
@@ -86,6 +94,7 @@
],
"rules": {
"no-unused-vars": "off",
+ "no-use-before-define": "off",
"class-methods-use-this": [
"error",
{
diff --git a/utils.ts b/utils.ts
new file mode 100644
index 0000000..72a74b2
--- /dev/null
+++ b/utils.ts
@@ -0,0 +1,26 @@
+export {
+ isAttributesV2,
+ isAttributesNS,
+ isComplexEditV2,
+ isEditV2,
+ isInsert,
+ isRemove,
+ isSetAttributes,
+ isSetTextContent,
+} from './editv2.js';
+
+export {
+ isAttributes,
+ isComplexEdit,
+ isEdit,
+ isNamespaced,
+ isUpdate,
+} from './editv1.js';
+
+export { convertEdit } from './convertEdit.js';
+
+export { newEditEvent } from './edit-event.js';
+
+export { newEditEventV2 } from './edit-event-v2.js';
+
+export { newOpenEvent } from './open-event.js';