Skip to content

Commit

Permalink
feat: convertEdit
Browse files Browse the repository at this point in the history
  • Loading branch information
JakobVogelsang committed Oct 24, 2024
1 parent 13874f9 commit ea80749
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 0 deletions.
100 changes: 100 additions & 0 deletions convertEdit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/* eslint-disable @typescript-eslint/no-unused-expressions */
import { expect } from "@open-wc/testing";

import { Insert, Remove, Update } from "./editv1.js";

import { convertEdit } from "./convertEdit.js";
import { SetAttributes } from "./editv2.js";

const doc = new DOMParser().parseFromString(
'<SCL><Substation name="AA1"></Substation></SCL>',
"application/xml",
);

const subSt = doc.querySelector("Substation")!;

const removeV1: Remove = { node: subSt };

const insertV1: Insert = {
parent: subSt,
node: doc.createAttribute("VoltageLevel"),
reference: null,
};

const update: Update = {
element: subSt,
attributes: {
name: "A2",
desc: null,
["__proto__"]: "a string",
"myns:attr": {
value: "value1",
namespaceURI: "http://example.org/myns",
},
"myns:attr2": {
value: "value1",
namespaceURI: "http://example.org/myns",
},
attr: {
value: "value2",
namespaceURI: "http://example.org/myns2",
},
attr2: {
value: "value2",
namespaceURI: "http://example.org/myns2",
},
attr3: {
value: "value3",
namespaceURI: null,
},
},
};

const setAttributes: SetAttributes = {
element: subSt,
attributes: {
name: "A2",
desc: null,
},
attributesNS: {
"http://example.org/myns": {
"myns:attr": "value1",
"myns:attr2": "value1",
},
"http://example.org/myns2": {
attr: "value2",
attr2: "value2",
},
},
};

const invalidEdit = { someWrongKey: "someValue" } as unknown as Update;

describe("convertEditToEditV2", () => {
it("passes through a Remove edit", () =>
expect(convertEdit(removeV1)).to.deep.equal(removeV1));

it("passes through a Insert edit", () =>
expect(convertEdit(insertV1)).to.deep.equal(insertV1));

it("converts Update to SetAttributes", () =>
expect(convertEdit(update)).to.deep.equal(setAttributes));

it("converts complex edits", () => {
const editsV1 = [removeV1, insertV1, update];

const editsV2 = convertEdit(editsV1);

const [removeV2, insertV2, updateV2] = Array.from(
editsV2 as Array<Remove | Insert | SetAttributes>,
);

expect(removeV1).to.deep.equal(removeV2);
expect(insertV1).to.deep.equal(insertV2);
expect(updateV2).to.deep.equal(setAttributes);
});

it("return empty array for invalid edit", () => {
expect(convertEdit(invalidEdit)).to.be.an("array").that.is.empty;
});
});
41 changes: 41 additions & 0 deletions convertEdit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
Edit,
isComplex,
isInsert,
isNamespaced,
isUpdate,
isRemove,
Update,
} from "./editv1.js";

import { EditV2 } from "./editv2.js";

function convertUpdate(edit: Update): EditV2 {
const attributes: Partial<Record<string, string | null>> = {};
const attributesNS: Partial<
Record<string, Partial<Record<string, string | null>>>
> = {};

Object.entries(edit.attributes).forEach(([key, value]) => {
if (isNamespaced(value!)) {
const ns = value.namespaceURI;
if (!ns) return;

if (!attributesNS[ns]) {
attributesNS[ns] = {};
}
attributesNS[ns][key] = value.value;
} else attributes[key] = value;
});

return { element: edit.element, attributes, attributesNS };
}

export function convertEdit(edit: Edit): EditV2 {
if (isRemove(edit)) return edit as EditV2;
if (isInsert(edit)) return edit as EditV2;
if (isUpdate(edit)) return convertUpdate(edit);
if (isComplex(edit)) return edit.map(convertEdit);

return [];
}
51 changes: 51 additions & 0 deletions editv1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/** Intent to `parent.insertBefore(node, reference)` */
export type Insert = {
parent: Node;
node: Node;
reference: Node | null;
};

export type NamespacedAttributeValue = {
value: string | null;
namespaceURI: string | null;
};
export type AttributeValue = string | null | NamespacedAttributeValue;
/** Intent to set or remove (if null) attributes on element */
export type Update = {
element: Element;
attributes: Partial<Record<string, AttributeValue>>;
};

/** 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 is NamespacedAttributeValue {
return value !== null && typeof value !== "string";
}

export function isUpdate(edit: Edit): edit is Update {
return (edit as Update).element !== undefined;
}

export function isRemove(edit: Edit): edit is Remove {
return (
(edit as Insert).parent === undefined && (edit as Remove).node !== undefined
);
}

export type EditEvent<E extends Edit = Edit> = CustomEvent<E>;
4 changes: 4 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ export {
isSetTextContent,
} from "./editv2.js";

export { Edit, Update } from "./editv1.js";

export { handleEdit } from "./handleEdit.js";

export { convertEdit } from "./convertEdit.js";

export {
complexEdit,
edit,
Expand Down

0 comments on commit ea80749

Please sign in to comment.