Skip to content

Commit ca3909a

Browse files
committed
wip: add plugin and initial Lit UI
1 parent a144ac6 commit ca3909a

File tree

7 files changed

+3652
-6097
lines changed

7 files changed

+3652
-6097
lines changed

demo/index.html

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
11
<!DOCTYPE html>
2-
<title>OpenSCD Core Demo</title>
2+
<title>OpenSCD Diff Demo</title>
33
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300&family=Roboto:wght@300;400;500&display=swap">
4-
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons&display=block">
4+
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined">
5+
6+
<oscd-diff></oscd-diff>
7+
8+
<hr>
9+
510
<label for="scl1">SCL file 1</label>
6-
<input type="file" id="scl1" name="scl1" accept="application/xml" />
11+
<input type="file" id="scl1" name="scl1" accept="application/xml" multiple />
712

813
<hr>
914

1015
<div id="result">
1116
</div>
1217

18+
<script type="module">
19+
import OpenSCDDiffElement from "../dist/oscd-diff.js"
20+
customElements.define('oscd-diff', OpenSCDDiffElement);
21+
</script>
22+
1323
<script type="module">
1424
import { newHasher } from '../dist/hash.js';
1525
import { identity } from '@openenergytools/scl-lib';
1626

27+
const oscdDiff = document.querySelector('oscd-diff');
28+
29+
window.docs = {};
30+
31+
oscdDiff.docs = window.docs;
32+
1733
const scl1 = document.getElementById('scl1');
1834
const result = document.getElementById('result');
1935

@@ -75,11 +91,15 @@
7591
scl1.addEventListener('change', e => {
7692
for (const file of scl1.files) {
7793
file.text().then(text => {
78-
const t0 = (new Date().getTime());
94+
const parser = new DOMParser();
95+
const doc = parser.parseFromString(text, 'application/xml');
96+
const sclElement = doc.documentElement;
97+
docs[file.name] = doc;
98+
oscdDiff.requestUpdate();
99+
return;
100+
console.time('hash ' + file.name);
79101
console.log(hash(new DOMParser().parseFromString(text, 'application/xml').documentElement))
80-
const t1 = (new Date().getTime());
81-
console.warn((t1 - t0) / 60000, 'min')
82-
console.warn((t1 - t0) / 1000, 'sec')
102+
console.timeEnd('hash ' + file.name);
83103
Object.entries(db.SCL).forEach(([sclHash, description]) => {
84104
const details = document.createElement('details');
85105
const summary = document.createElement('summary');

diff-tree.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { LitElement, html, nothing } from "lit";
2+
import { customElement, property, query } from "lit/decorators.js";
3+
import { identity, find } from "@openenergytools/scl-lib";
4+
5+
import "@material/web/all.js";
6+
7+
import type { newHasher } from "./hash.js";
8+
9+
type Description = Record<string, string | string[]> & {
10+
eNS?: Record<string, Record<string, string>>;
11+
};
12+
13+
function getDiff(ours: Description, theirs: Description) {
14+
const diff: Record<string, { ours?: any; theirs?: any }> = {};
15+
16+
const keys = new Set([...Object.keys(ours), ...Object.keys(theirs)]);
17+
18+
keys.forEach((key) => {
19+
if (ours[key] === theirs[key]) return;
20+
const val = ours[key] ?? theirs[key];
21+
if (typeof val !== "object")
22+
diff[key] = { ours: ours[key], theirs: theirs[key] };
23+
else if (Array.isArray(val)) {
24+
const arrayDiff = { ours: [] as string[], theirs: [] as string[] };
25+
const vals = new Set([...ours[key], ...theirs[key]]);
26+
vals.forEach((val) => {
27+
const inOurs = ours[key]?.includes(val);
28+
const inTheirs = theirs[key]?.includes(val);
29+
if (inOurs && inTheirs) return;
30+
arrayDiff[inOurs ? "ours" : "theirs"].push(val);
31+
});
32+
if (arrayDiff.ours.length || arrayDiff.theirs.length)
33+
diff[key] = arrayDiff;
34+
} else if (key === "eNS") {
35+
const eNSDiff: Record<
36+
string,
37+
Record<string, { ours?: string; theirs?: string }>
38+
> = {};
39+
const eNS = new Set([
40+
...Object.keys(ours.eNS ?? {}),
41+
...Object.keys(theirs.eNS ?? {}),
42+
]);
43+
eNS.forEach((ns) => {
44+
const ks = new Set([
45+
...Object.keys(ours.eNS?.[ns] ?? {}),
46+
...Object.keys(theirs.eNS?.[ns] ?? {}),
47+
]);
48+
ks.forEach((k) => {
49+
if (ours.eNS?.[ns]?.[k] === theirs.eNS?.[ns]?.[k]) return;
50+
eNSDiff[ns] ??= {};
51+
eNSDiff[ns][k] = {
52+
ours: ours.eNS?.[ns]?.[k],
53+
theirs: theirs.eNS?.[ns]?.[k],
54+
};
55+
});
56+
});
57+
if (Object.keys(eNSDiff).length) diff[key] = eNSDiff;
58+
} else
59+
diff[key] = {
60+
ours: "undiffable data type",
61+
theirs: "undiffable data type",
62+
};
63+
});
64+
65+
return diff;
66+
}
67+
68+
@customElement("diff-tree")
69+
export class DiffTree extends LitElement {
70+
@property() ours?: Element;
71+
@property() theirs?: Element;
72+
@property() hashers = new WeakMap<
73+
XMLDocument,
74+
ReturnType<typeof newHasher>
75+
>();
76+
@query("md-icon-button") expandButton!: HTMLElement;
77+
78+
get expanded(): boolean {
79+
return this.expandButton?.hasAttribute("selected");
80+
}
81+
82+
get ourHasher(): ReturnType<typeof newHasher> | undefined {
83+
return this.ours ? this.hashers.get(this.ours.ownerDocument) : undefined;
84+
}
85+
get theirHasher(): ReturnType<typeof newHasher> | undefined {
86+
return this.theirs
87+
? this.hashers.get(this.theirs.ownerDocument)
88+
: undefined;
89+
}
90+
91+
get ourHash(): string | undefined {
92+
return this.ourHasher?.hash(this.ours!);
93+
}
94+
get theirHash(): string | undefined {
95+
return this.theirHasher?.hash(this.theirs!);
96+
}
97+
98+
get ourDescription(): Description | undefined {
99+
return this.ourHasher?.db[this.ours!.tagName][this.ourHash ?? ""] as
100+
| Description
101+
| undefined;
102+
}
103+
get theirDescription(): Description | undefined {
104+
return this.theirHasher?.db[this.theirs!.tagName][this.theirHash ?? ""] as
105+
| Description
106+
| undefined;
107+
}
108+
109+
get diff(): Record<string, { ours?: any; theirs?: any }> {
110+
return getDiff(this.ourDescription!, this.theirDescription!);
111+
}
112+
113+
renderChildDiffs() {
114+
if (!this.expanded) return nothing;
115+
return html`<div>
116+
${Object.entries(this.diff).map(([key, { ours, theirs }]) => {
117+
if (!key.startsWith("@")) return nothing;
118+
const tag = key.slice(1);
119+
const elementDiff: Record<
120+
string,
121+
{ ours?: Element; theirs?: Element }
122+
> = {};
123+
ours.forEach((digest: string) => {
124+
const element = Array.from(
125+
this.ourHasher?.eDb.h2e.get(digest)?.values() ?? [],
126+
).find((e) => e.tagName === tag);
127+
if (!element) return;
128+
const id = identity(element as Element);
129+
elementDiff[id] ??= {};
130+
elementDiff[id].ours = element;
131+
});
132+
theirs.forEach((digest: string) => {
133+
const element = Array.from(
134+
this.theirHasher?.eDb.h2e.get(digest)?.values() ?? [],
135+
).find((e) => e.tagName === tag);
136+
if (!element) return;
137+
const id = identity(element as Element);
138+
elementDiff[id] ??= {};
139+
elementDiff[id].theirs = element;
140+
});
141+
console.warn(elementDiff);
142+
return Object.entries(elementDiff).map(([id, { ours, theirs }]) => {
143+
return html`<diff-tree
144+
.ours=${ours}
145+
.theirs=${theirs}
146+
.hashers=${this.hashers}
147+
></diff-tree>`;
148+
});
149+
})}
150+
</div>`;
151+
}
152+
153+
renderDiff() {
154+
return html`<pre>
155+
${JSON.stringify(this.diff, null, 2)}
156+
</pre>
157+
${this.renderChildDiffs()}`;
158+
}
159+
160+
render() {
161+
if (!this.ours || !this.theirs)
162+
return html`<h2>missing ${this.ours ? "their" : "our"} element</h2>`;
163+
if (!this.ourHasher || !this.theirHasher)
164+
return html`<h2>missing ${this.ourHasher ? "their" : "our"} hasher</h2>`;
165+
if (!this.ourDescription || !this.theirDescription)
166+
return html`<h2>
167+
missing ${this.ourDescription ? "their" : "our"} description
168+
</h2>`;
169+
if (this.ourHash === this.theirHash) return nothing;
170+
171+
Object.keys(this.ourDescription ?? {}).forEach((key) => {});
172+
173+
return html`<h2>
174+
${identity(this.ours) || this.ours.tagName}
175+
<md-icon>arrow_forward</md-icon> ${identity(this.theirs) ||
176+
this.theirs.tagName}
177+
</h2>
178+
<md-icon-button toggle @click=${() => this.requestUpdate()}>
179+
<md-icon>unfold_more</md-icon>
180+
<md-icon slot="selected">unfold_less</md-icon>
181+
</md-icon-button>
182+
${this.expanded ? this.renderDiff() : ""} `;
183+
}
184+
}

hash.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function siblingCount(element: Element, name: string): number {
2929
return parseInt(count, 10);
3030
}
3131

32-
interface ElementDB {
32+
export interface ElementDB {
3333
e2h: WeakMap<Element, string>;
3434
h2e: Map<string, Set<Element>>;
3535
}
@@ -881,7 +881,7 @@ export function hasher(
881881
if (e.tagName in descriptions) return descriptions[e.tagName](e);
882882
else if (e.tagName === "Private") return { xml: e.outerHTML };
883883
else if (e.namespaceURI === "http://www.iec.ch/61850/2003/SCL")
884-
describeNaming(e);
884+
return describeNaming(e);
885885
return { xml: e.outerHTML };
886886
}
887887

0 commit comments

Comments
 (0)