Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ node_modules
coverage
dist
doc

.vscode
.idea
*.log
*.tsbuildinfo
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ There are various types of functions you can find in this library grouped into:

- <u>edit checks</u>: Those function are triggered by the wish to edit a specific SCL element, e.g. `GSEControl`.

- The input is a delta to the actual SCL formulated as `Update`, `Insert` or `Remove`.
- The output is a corrected delta formulated as an array of `Update`, `Insert` or `Remove`. The difference between the input and output contains expertise related to IEC 61850-6.
- The input is a delta to the actual SCL formulated as `SetAttributes`, `Insert` or `Remove`.
- The output is a corrected delta formulated as an array of `SetAttributes`, `Insert` or `Remove`. The difference between the input and output contains expertise related to IEC 61850-6.

- <u>generators</u>: Generator functions that allow to dynamically create unique value such as MAC-addresses, APPID and others
156 changes: 156 additions & 0 deletions foundation/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
import {
EditV2,
Insert,
Remove,
SetAttributes,
SetTextContent,
} from "@openscd/oscd-api";

import {
isComplexEditV2,
isInsert,
isRemove,
isSetAttributes,
isSetTextContent,
} from "@openscd/oscd-api/utils.js";

export function findElement(
str: string,
selector?: string,
Expand All @@ -8,3 +24,143 @@ export function findElement(
.parseFromString(str, "application/xml")
.querySelector(selector);
}

function handleSetTextContent({
element,
textContent,
}: SetTextContent): (SetTextContent | Insert)[] {
const { childNodes } = element;

const restoreChildNodes: Insert[] = Array.from(childNodes).map((node) => ({
parent: element,
node,
reference: null,
}));

element.textContent = textContent;

const undoTextContent: SetTextContent = { element, textContent: "" };

return [undoTextContent, ...restoreChildNodes];
}

function handleSetAttributes({
element,
attributes = {},
attributesNS = {},
}: SetAttributes): SetAttributes {
const oldAttributes = { ...attributes };
const oldAttributesNS = { ...attributesNS };

// save element's non-prefixed attributes for undo
if (attributes)
Object.keys(attributes)
.reverse()
.forEach((name) => {
oldAttributes[name] = element.getAttribute(name);
});

// change element's non-prefixed attributes
if (attributes)
for (const entry of Object.entries(attributes)) {
try {
const [name, value] = entry as [string, string | null];
if (value === null) element.removeAttribute(name);
else element.setAttribute(name, value);
} catch (_e) {
// undo nothing if update didn't work on this attribute
delete oldAttributes[entry[0]];
}
}

// save element's namespaced attributes for undo
if (attributesNS)
Object.entries(attributesNS).forEach(([ns, attrs]) => {
Object.keys(attrs!)
.reverse()
.forEach((name) => {
oldAttributesNS[ns] = {
...oldAttributesNS[ns],
[name]: element.getAttributeNS(ns, name.split(":").pop()!),
};
});
});

// change element's namespaced attributes
if (attributesNS)
for (const nsEntry of Object.entries(attributesNS)) {
const [ns, attrs] = nsEntry as [
string,
Partial<Record<string, string | null>>,
];
for (const entry of Object.entries(attrs)) {
try {
const [name, value] = entry as [string, string | null];
if (value === null) {
element.removeAttributeNS(ns, name.split(":").pop()!);
} else {
element.setAttributeNS(ns, name, value);
}
} catch (_e) {
delete oldAttributesNS[ns]![entry[0]];
}
}
}

return {
element,
attributes: oldAttributes,
attributesNS: oldAttributesNS,
};
}

function handleRemove({ node }: Remove): Insert | [] {
const { parentNode: parent, nextSibling: reference } = node;

if (!parent) return [];

parent.removeChild(node);
return {
node,
parent,
reference,
};
}

function handleInsert({
parent,
node,
reference,
}: Insert): Insert | Remove | [] {
try {
const { parentNode, nextSibling } = node;
parent.insertBefore(node, reference);
if (parentNode)
// undo: move child node back to original place
return {
node,
parent: parentNode,
reference: nextSibling,
};
// undo: remove orphaned node
return { node };
} catch (_e) {
// undo nothing if insert doesn't work on these nodes
return [];
}
}

/** Applies an EditV2, returning the corresponding "undo" EditV2. */
export function handleEdit(edit: EditV2): EditV2 {
if (isInsert(edit)) return handleInsert(edit);
if (isRemove(edit)) return handleRemove(edit);
if (isSetAttributes(edit)) return handleSetAttributes(edit);
if (isSetTextContent(edit)) return handleSetTextContent(edit);
if (isComplexEditV2(edit))
return edit
.map((edit) => handleEdit(edit))
.reverse()
.flat(Infinity as 1);

return [];
}
33 changes: 0 additions & 33 deletions foundation/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,6 @@ export type TreeSelection = {
[name: string]: TreeSelection;
};

/** Intent to `parent.insertBefore(node, reference)` */
export type Insert = {
parent: Node;
node: Node;
reference: Node | null;
};

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

/** 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 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 function isInsert(edit: Edit): edit is Insert {
return (edit as Insert).parent !== undefined;
}

/** Utility function to create element with `tagName` and its`attributes` */
export function createElement(
doc: XMLDocument,
Expand Down
5 changes: 1 addition & 4 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
export {
Edit,
Update,
Insert,
Remove,
TreeSelection,
} from "./foundation/utils.js";

Expand All @@ -15,6 +11,7 @@ export { updateLnType } from "./tSubstation/updateLnType.js";
export { InsertIedOptions, insertIed } from "./tIED/insertIED.js";
export { updateIED } from "./tIED/updateIED.js";
export { removeIED } from "./tIED/removeIED.js";
export type { RemoveIedOptions } from "./tIED/removeIED.js";

export { findControlBlockSubscription } from "./tControl/findControlSubscription.js";
export { controlBlockObjRef } from "./tControl/controlBlockObjRef.js";
Expand Down
Loading