From b19c625e90ffc7c356cf66e0b928ec021613a73d Mon Sep 17 00:00:00 2001 From: Christian Dinkel Date: Tue, 17 Jun 2025 13:42:49 +0200 Subject: [PATCH 1/9] fix: increase type guard vigilance --- convertEdit.spec.ts | 11 ++++--- convertEdit.ts | 20 +++++-------- editv1.spec.ts | 5 +--- editv1.ts | 71 ++++++++++++++++++++++----------------------- editv2.spec.ts | 16 +++++----- editv2.ts | 66 +++++++++++++++++++++++++++++------------ package.json | 1 + 7 files changed, 106 insertions(+), 84 deletions(-) diff --git a/convertEdit.spec.ts b/convertEdit.spec.ts index 9a9964b..593eb67 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( '', @@ -26,7 +26,6 @@ const update: Update = { attributes: { name: 'A2', desc: null, - ['__proto__']: 'a string', 'myns:attr': { value: 'value1', namespaceURI: 'http://example.org/myns', @@ -43,9 +42,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: '', }, }, }; diff --git a/convertEdit.ts b/convertEdit.ts index 6abbf13..e46ded6 100644 --- a/convertEdit.ts +++ b/convertEdit.ts @@ -1,20 +1,16 @@ +import { Edit, isComplex, isNamespaced, isUpdate, Update } from './editv1.js'; + import { - Edit, - isComplex, + Attributes, + AttributesNS, + EditV2, isInsert, - isNamespaced, - isUpdate, isRemove, - Update, -} from './editv1.js'; - -import { EditV2 } from './editv2.js'; +} from './editv2.js'; function convertUpdate(edit: Update): EditV2 { - const attributes: Partial> = {}; - const attributesNS: Partial< - Record>> - > = {}; + const attributes: Attributes = {}; + const attributesNS: AttributesNS = {}; Object.entries(edit.attributes).forEach(([key, value]) => { if (isNamespaced(value!)) { 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..d3a8524 100644 --- a/editv1.ts +++ b/editv1.ts @@ -1,68 +1,67 @@ -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 AttributesV1 = 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 isAttributesV1( + attributes: unknown, +): attributes is AttributesV1 { + 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 isComplex(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 && + isAttributesV1((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 { +export function isEdit(edit: unknown): edit is Edit { if (isComplex(edit)) { - return !edit.some(e => !isEdit(e)); + 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..3dff6f1 100644 --- a/editv2.ts +++ b/editv2.ts @@ -16,6 +16,10 @@ export type SetTextContent = { textContent: string; }; +export type Attributes = Partial>; + +export type AttributesNS = Partial>; + /** Intent to set or remove (if `null`) `attributes`(-`NS`) on `element` */ export type SetAttributes = { element: Element; @@ -31,43 +35,69 @@ export type EditV2 = | Remove | EditV2[]; -export function isComplex(edit: EditV2): edit is EditV2[] { - return edit instanceof Array; +export function isAttributes(attributes: unknown): attributes is Attributes { + 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' && + isAttributes(attributes as Record), + ); +} + +export function isComplex(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 && + isAttributes((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 { +export function isEditV2(edit: unknown): edit is EditV2 { if (isComplex(edit)) { - return !edit.some(e => !isEditV2(e)); + return true; } return ( diff --git a/package.json b/package.json index cd038c1..b06f4b7 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ ], "rules": { "no-unused-vars": "off", + "no-use-before-define": "off", "class-methods-use-this": [ "error", { From 170740c4061e068b312ea4672d6203672f9bba39 Mon Sep 17 00:00:00 2001 From: Christian Dinkel Date: Tue, 17 Jun 2025 13:53:37 +0200 Subject: [PATCH 2/9] fix: expose missing module members Resolves #33 . Resolves #31 . --- convertEdit.ts | 10 ++++++++-- editv1.ts | 6 ++---- editv2.ts | 4 ++-- oscd-api.ts | 34 ++++++++++++++++++++++++++++------ 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/convertEdit.ts b/convertEdit.ts index e46ded6..586fb89 100644 --- a/convertEdit.ts +++ b/convertEdit.ts @@ -1,4 +1,10 @@ -import { Edit, isComplex, isNamespaced, isUpdate, Update } from './editv1.js'; +import { + Edit, + isComplexEdit, + isNamespaced, + isUpdate, + Update, +} from './editv1.js'; import { Attributes, @@ -41,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/editv1.ts b/editv1.ts index d3a8524..528371c 100644 --- a/editv1.ts +++ b/editv1.ts @@ -45,7 +45,7 @@ export function isAttributesV1( ); } -export function isComplex(edit: unknown): edit is Edit[] { +export function isComplexEdit(edit: unknown): edit is Edit[] { return edit instanceof Array && edit.every(isEdit); } @@ -56,10 +56,8 @@ export function isUpdate(edit: unknown): edit is Update { ); } -export type EditEvent = CustomEvent; - export function isEdit(edit: unknown): edit is Edit { - if (isComplex(edit)) { + if (isComplexEdit(edit)) { return true; } diff --git a/editv2.ts b/editv2.ts index 3dff6f1..24c77df 100644 --- a/editv2.ts +++ b/editv2.ts @@ -58,7 +58,7 @@ export function isAttributesNS( ); } -export function isComplex(edit: unknown): edit is EditV2[] { +export function isComplexEditV2(edit: unknown): edit is EditV2[] { return edit instanceof Array && edit.every(e => isEditV2(e)); } @@ -96,7 +96,7 @@ export function isInsert(edit: unknown): edit is Insert { } export function isEditV2(edit: unknown): edit is EditV2 { - if (isComplex(edit)) { + if (isComplexEditV2(edit)) { return true; } diff --git a/oscd-api.ts b/oscd-api.ts index 9d64091..27988c2 100644 --- a/oscd-api.ts +++ b/oscd-api.ts @@ -1,18 +1,35 @@ export { + Attributes, + AttributesNS, EditV2, Insert, - Remove, - SetAttributes, - SetTextContent, - isComplex, + isAttributes, + isAttributesNS, + isComplexEditV2, isEditV2, isInsert, isRemove, isSetAttributes, isSetTextContent, + Remove, + SetAttributes, + SetTextContent, } from './editv2.js'; -export { Edit, Update, isEdit } from './editv1.js'; +export { + AttributesV1, + AttributeValue, + Edit, + isAttributesV1, + isComplexEdit, + isEdit, + isNamespaced, + isUpdate, + NamespacedAttributeValue, + Update, +} from './editv1.js'; + +export { convertEdit } from './convertEdit.js'; export type { Commit, @@ -21,4 +38,9 @@ export type { TransactedCallback, } from './Transactor.js'; -export { newEditEventV2 } from './edit-event.js'; +export { + EditDetailV2, + EditEventOptions, + EditEventV2, + newEditEventV2, +} from './edit-event.js'; From 2bc194fd98b479d56980c077019094f7d9dcbdbe Mon Sep 17 00:00:00 2001 From: Christian Dinkel Date: Tue, 17 Jun 2025 14:10:32 +0200 Subject: [PATCH 3/9] refactor(package)!: move utils to separate module Resolves #32 . --- oscd-api.ts | 22 +++------------------- package.json | 12 ++++++++++-- utils.ts | 22 ++++++++++++++++++++++ 3 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 utils.ts diff --git a/oscd-api.ts b/oscd-api.ts index 27988c2..ca7f5b3 100644 --- a/oscd-api.ts +++ b/oscd-api.ts @@ -1,36 +1,21 @@ -export { +export type { Attributes, AttributesNS, EditV2, Insert, - isAttributes, - isAttributesNS, - isComplexEditV2, - isEditV2, - isInsert, - isRemove, - isSetAttributes, - isSetTextContent, Remove, SetAttributes, SetTextContent, } from './editv2.js'; -export { +export type { AttributesV1, AttributeValue, Edit, - isAttributesV1, - isComplexEdit, - isEdit, - isNamespaced, - isUpdate, NamespacedAttributeValue, Update, } from './editv1.js'; -export { convertEdit } from './convertEdit.js'; - export type { Commit, CommitOptions, @@ -38,9 +23,8 @@ export type { TransactedCallback, } from './Transactor.js'; -export { +export type { EditDetailV2, EditEventOptions, EditEventV2, - newEditEventV2, } from './edit-event.js'; diff --git a/package.json b/package.json index b06f4b7..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", diff --git a/utils.ts b/utils.ts new file mode 100644 index 0000000..726ebbd --- /dev/null +++ b/utils.ts @@ -0,0 +1,22 @@ +export { + isAttributes, + isAttributesNS, + isComplexEditV2, + isEditV2, + isInsert, + isRemove, + isSetAttributes, + isSetTextContent, +} from './editv2.js'; + +export { + isAttributesV1, + isComplexEdit, + isEdit, + isNamespaced, + isUpdate, +} from './editv1.js'; + +export { convertEdit } from './convertEdit.js'; + +export { newEditEventV2 } from './edit-event.js'; From f26a146dedbead972a0bc432c2ce4a5dcb35c822 Mon Sep 17 00:00:00 2001 From: Christian Dinkel Date: Tue, 17 Jun 2025 14:13:33 +0200 Subject: [PATCH 4/9] docs(plugin-api): remove wizard section --- docs/plugin-api.md | 90 +++------------------------------------------- 1 file changed, 5 insertions(+), 85 deletions(-) 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 +``` From d6cac6786f9c5df9054452d52cbb70a1d035f367 Mon Sep 17 00:00:00 2001 From: Christian Dinkel Date: Tue, 17 Jun 2025 14:37:49 +0200 Subject: [PATCH 5/9] docs(EditV2): document attribute types --- editv2.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/editv2.ts b/editv2.ts index 24c77df..782d4d2 100644 --- a/editv2.ts +++ b/editv2.ts @@ -16,15 +16,17 @@ export type SetTextContent = { textContent: string; }; +/** Record from attribute names to attribute values */ export type Attributes = 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: Attributes; + attributesNS: AttributesNS; }; /** Intent to change some XMLDocuments */ From 8834fb14d460932e24014d020791bf9039af97f9 Mon Sep 17 00:00:00 2001 From: Christian Dinkel Date: Tue, 17 Jun 2025 14:39:30 +0200 Subject: [PATCH 6/9] feat: add OpenEvent Resolves #30 . --- open-event.ts | 21 +++++++++++++++++++++ oscd-api.ts | 2 ++ utils.ts | 2 ++ 3 files changed, 25 insertions(+) create mode 100644 open-event.ts 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 ca7f5b3..494839a 100644 --- a/oscd-api.ts +++ b/oscd-api.ts @@ -28,3 +28,5 @@ export type { EditEventOptions, EditEventV2, } from './edit-event.js'; + +export type { OpenDetail, OpenEvent } from './open-event.js'; diff --git a/utils.ts b/utils.ts index 726ebbd..f485ee7 100644 --- a/utils.ts +++ b/utils.ts @@ -20,3 +20,5 @@ export { export { convertEdit } from './convertEdit.js'; export { newEditEventV2 } from './edit-event.js'; + +export { newOpenEvent } from './open-event.js'; From c8c112c74ca8e5d91b087399776f20184988b16d Mon Sep 17 00:00:00 2001 From: Christian Dinkel Date: Wed, 18 Jun 2025 11:06:28 +0200 Subject: [PATCH 7/9] refactor: consistently mark only V2 in names --- convertEdit.ts | 4 ++-- editv1.ts | 8 +++----- editv2.ts | 16 +++++++++------- oscd-api.ts | 4 ++-- utils.ts | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/convertEdit.ts b/convertEdit.ts index 586fb89..931d3d5 100644 --- a/convertEdit.ts +++ b/convertEdit.ts @@ -7,7 +7,7 @@ import { } from './editv1.js'; import { - Attributes, + AttributesV2, AttributesNS, EditV2, isInsert, @@ -15,7 +15,7 @@ import { } from './editv2.js'; function convertUpdate(edit: Update): EditV2 { - const attributes: Attributes = {}; + const attributes: AttributesV2 = {}; const attributesNS: AttributesNS = {}; Object.entries(edit.attributes).forEach(([key, value]) => { diff --git a/editv1.ts b/editv1.ts index 528371c..3674e03 100644 --- a/editv1.ts +++ b/editv1.ts @@ -7,7 +7,7 @@ export type NamespacedAttributeValue = { export type AttributeValue = string | null | NamespacedAttributeValue; -export type AttributesV1 = Partial>; +export type Attributes = Partial>; /** Intent to set or remove (if null) attributes on element */ export type Update = { @@ -31,9 +31,7 @@ export function isNamespaced( ); } -export function isAttributesV1( - attributes: unknown, -): attributes is AttributesV1 { +export function isAttributes(attributes: unknown): attributes is Attributes { if (attributes === null || typeof attributes !== 'object') { return false; } @@ -52,7 +50,7 @@ export function isComplexEdit(edit: unknown): edit is Edit[] { export function isUpdate(edit: unknown): edit is Update { return ( (edit as Update).element instanceof Element && - isAttributesV1((edit as Update).attributes) + isAttributes((edit as Update).attributes) ); } diff --git a/editv2.ts b/editv2.ts index 782d4d2..a2b10d0 100644 --- a/editv2.ts +++ b/editv2.ts @@ -17,16 +17,16 @@ export type SetTextContent = { }; /** Record from attribute names to attribute values */ -export type Attributes = Partial>; +export type AttributesV2 = Partial>; /** Record from namespace URIs to `Attributes` records */ -export type AttributesNS = Partial>; +export type AttributesNS = Partial>; /** Intent to set or remove (if `null`) `attributes`(-`NS`) on `element` */ export type SetAttributes = { element: Element; - attributes: Attributes; - attributesNS: AttributesNS; + attributes?: AttributesV2; + attributesNS?: AttributesNS; }; /** Intent to change some XMLDocuments */ @@ -37,7 +37,9 @@ export type EditV2 = | Remove | EditV2[]; -export function isAttributes(attributes: unknown): attributes is Attributes { +export function isAttributesV2( + attributes: unknown, +): attributes is AttributesV2 { if (typeof attributes !== 'object' || attributes === null) { return false; } @@ -56,7 +58,7 @@ export function isAttributesNS( return Object.entries(attributesNS).every( ([namespace, attributes]) => typeof namespace === 'string' && - isAttributes(attributes as Record), + isAttributesV2(attributes as Record), ); } @@ -81,7 +83,7 @@ export function isRemove(edit: unknown): edit is Remove { export function isSetAttributes(edit: unknown): edit is SetAttributes { return ( (edit as SetAttributes).element instanceof Element && - isAttributes((edit as SetAttributes).attributes) && + isAttributesV2((edit as SetAttributes).attributes) && isAttributesNS((edit as SetAttributes).attributesNS) ); } diff --git a/oscd-api.ts b/oscd-api.ts index 494839a..41e442c 100644 --- a/oscd-api.ts +++ b/oscd-api.ts @@ -1,5 +1,5 @@ export type { - Attributes, + AttributesV2, AttributesNS, EditV2, Insert, @@ -9,7 +9,7 @@ export type { } from './editv2.js'; export type { - AttributesV1, + Attributes, AttributeValue, Edit, NamespacedAttributeValue, diff --git a/utils.ts b/utils.ts index f485ee7..ac3ebd5 100644 --- a/utils.ts +++ b/utils.ts @@ -1,5 +1,5 @@ export { - isAttributes, + isAttributesV2, isAttributesNS, isComplexEditV2, isEditV2, @@ -10,7 +10,7 @@ export { } from './editv2.js'; export { - isAttributesV1, + isAttributes, isComplexEdit, isEdit, isNamespaced, From 5e7247800ecb25fecfbe173ffdbe84a68f816956 Mon Sep 17 00:00:00 2001 From: Christian Dinkel Date: Wed, 18 Jun 2025 11:12:33 +0200 Subject: [PATCH 8/9] fix(convertEdit): allow '__proto__' attribute name --- convertEdit.spec.ts | 2 ++ convertEdit.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/convertEdit.spec.ts b/convertEdit.spec.ts index 593eb67..c0090e4 100644 --- a/convertEdit.spec.ts +++ b/convertEdit.spec.ts @@ -26,6 +26,7 @@ const update: Update = { attributes: { name: 'A2', desc: null, + ['__proto__']: 'a string', 'myns:attr': { value: 'value1', namespaceURI: 'http://example.org/myns', @@ -54,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 931d3d5..6e4a0d1 100644 --- a/convertEdit.ts +++ b/convertEdit.ts @@ -15,7 +15,7 @@ import { } from './editv2.js'; function convertUpdate(edit: Update): EditV2 { - const attributes: AttributesV2 = {}; + let attributes: AttributesV2 = {}; const attributesNS: AttributesNS = {}; Object.entries(edit.attributes).forEach(([key, value]) => { @@ -28,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 }; } }); From b222fffb2a9e42f1b677312ea88c0e99a2595349 Mon Sep 17 00:00:00 2001 From: Christian Dinkel Date: Wed, 18 Jun 2025 11:52:42 +0200 Subject: [PATCH 9/9] feat: add edit-event from V1 API --- edit-event-v2.ts | 33 +++++++++++++++++++++++++++++++++ edit-event.ts | 28 ++++++---------------------- oscd-api.ts | 4 +++- utils.ts | 4 +++- 4 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 edit-event-v2.ts 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/oscd-api.ts b/oscd-api.ts index 41e442c..93c6cb3 100644 --- a/oscd-api.ts +++ b/oscd-api.ts @@ -23,10 +23,12 @@ export type { TransactedCallback, } from './Transactor.js'; +export type { EditEvent } from './edit-event.js'; + export type { EditDetailV2, EditEventOptions, EditEventV2, -} from './edit-event.js'; +} from './edit-event-v2.js'; export type { OpenDetail, OpenEvent } from './open-event.js'; diff --git a/utils.ts b/utils.ts index ac3ebd5..72a74b2 100644 --- a/utils.ts +++ b/utils.ts @@ -19,6 +19,8 @@ export { export { convertEdit } from './convertEdit.js'; -export { newEditEventV2 } from './edit-event.js'; +export { newEditEvent } from './edit-event.js'; + +export { newEditEventV2 } from './edit-event-v2.js'; export { newOpenEvent } from './open-event.js';