From 39e26bc0c2aa3c953f84fe65dbeb539ee509ae30 Mon Sep 17 00:00:00 2001 From: barsdeveloper Date: Mon, 8 Nov 2021 22:28:26 +0100 Subject: [PATCH] Serialization fixed --- dist/ueblueprint.js | 119 ++++++++++++++++++++------ js/Blueprint.js | 16 +++- js/entity/LocalizedTextEntity.js | 4 - js/entity/PinReferenceEntity.js | 3 +- js/entity/primitive/PathSymbol.js | 17 ++++ js/export.js | 25 +++++- js/input/Copy.js | 26 ++++++ js/input/Paste.js | 9 +- js/serialization/GeneralSerializer.js | 6 +- js/serialization/Grammar.js | 5 +- js/serialization/ObjectSerializer.js | 2 +- js/serialization/Serializer.js | 11 ++- 12 files changed, 195 insertions(+), 48 deletions(-) create mode 100644 js/entity/primitive/PathSymbol.js create mode 100644 js/input/Copy.js diff --git a/dist/ueblueprint.js b/dist/ueblueprint.js index 1eb1117..fee532e 100644 --- a/dist/ueblueprint.js +++ b/dist/ueblueprint.js @@ -1,4 +1,5 @@ class Primitive { + toString() { return "Unimplemented for " + this.constructor.name } @@ -139,6 +140,7 @@ class Utility { } class Entity { + constructor(options = {}) { /** * @@ -266,16 +268,28 @@ class LocalizedTextEntity extends Entity { getAttributes() { return LocalizedTextEntity.attributes } - - toString() { - "NSLOCTEXT(" + `"${this.namespace}"` + ", " + `"${this.key}"` + ", " + `"${this.value}"` + ")"; - } +} + +class PathSymbol extends Primitive { + + constructor(value) { + super(); + this.value = new String(value).valueOf(); + } + + valueOf() { + this.value; + } + + toString() { + return this.value + } } class PinReferenceEntity extends Entity { static attributes = { - objectName: String, + objectName: PathSymbol, pinGuid: Guid } @@ -405,8 +419,8 @@ class Grammar { String = _ => P.regex(/(?:[^"\\]|\\")*/).wrap(P.string('"'), P.string('"')).desc('string (with possibility to escape the quote using \")') Word = _ => P.regex(/[a-zA-Z]+/).desc("a word") Guid = _ => P.regex(/[0-9a-zA-Z]{32}/).map(v => new Guid(v)).desc("32 digit hexadecimal (accepts all the letters for safety) value") - PathSymbol = _ => P.regex(/[0-9a-zA-Z_]+/) - ReferencePath = r => P.seq(P.string("/"), r.PathSymbol.sepBy1(P.string(".")).tieWith(".")) + PathSymbol = _ => P.regex(/[0-9a-zA-Z_]+/).map(v => new PathSymbol(v)) + ReferencePath = r => P.seq(P.string("/"), r.PathSymbol.map(v => v.toString()).sepBy1(P.string(".")).tieWith(".")) .tie() .atLeast(2) .tie() @@ -591,6 +605,7 @@ class Serializer { if (value === null) { return "()" } + const serialize = v => SerializerFactory.getSerializer(Utility.getType(v)).write(v); // This is an exact match (and not instanceof) to hit also primitive types (by accessing value.constructor they are converted to objects automatically) switch (value?.constructor) { case Function: @@ -602,8 +617,11 @@ class Serializer { case String: return `"${value}"` } + if (value instanceof Array) { + return `(${value.map(v => serialize(v) + ",")})` + } if (value instanceof Entity) { - return SerializerFactory.getSerializer(Utility.getType(value)).write(value) + return serialize(value) } if (value instanceof Primitive) { return value.toString() @@ -619,7 +637,8 @@ class Serializer { const value = object[property]; if (object[property]?.constructor === Object) { // Recursive call when finding an object - result += this.subWrite(fullKey, value, this.prefix, this.separator); + result += (result.length ? this.separator : "") + + this.subWrite(fullKey, value); } else if (this.showProperty(fullKey, value)) { result += (result.length ? this.separator : "") + this.prefix @@ -628,7 +647,7 @@ class Serializer { + this.writeValue(value); } } - if (this.trailingSeparator && result.length) { + if (this.trailingSeparator && result.length && fullKey.length === 0) { // append separator at the end if asked and there was printed content result += this.separator; } @@ -647,9 +666,9 @@ class Serializer { class GeneralSerializer extends Serializer { - constructor(keyword, entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) { + constructor(wrap, entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) { super(entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter); - this.keyword = keyword ?? ""; + this.wrap = wrap ?? (v => `(${v})`); } read(value) { @@ -663,7 +682,7 @@ class GeneralSerializer extends Serializer { } write(object) { - let result = `${this.keyword}(${this.subWrite([], object)})`; + let result = this.wrap(this.subWrite([], object)); return result } } @@ -714,7 +733,7 @@ class ObjectSerializer extends Serializer { * @returns */ write(object) { - let result = `Begin Object Class=${object.Class} Name=${object.Name} + let result = `Begin Object Class=${object.Class} Name="${object.Name}" ${this.subWrite([], object) + object .CustomProperties.map(pin => this.separator + this.prefix + "CustomProperties " + SerializerFactory.getSerializer(PinEntity).write(pin)) @@ -1604,9 +1623,7 @@ class Paste extends Context { constructor(target, blueprint, options = {}) { options.wantsFocusCallback = true; super(target, blueprint, options); - this.serializer = new ObjectSerializer(); - let self = this; this.pasteHandle = e => self.pasted(e.clipboardData.getData("Text")); } @@ -1630,8 +1647,11 @@ class Paste extends Context { }); let mousePosition = this.blueprint.entity.mousePosition; nodes.forEach(node => { - node.location[0] += mousePosition[0] - left; - node.location[1] += mousePosition[1] - top; + const locationOffset = [ + mousePosition[0] - left, + mousePosition[1] - top + ]; + node.addLocation(locationOffset); }); this.blueprint.addNode(...nodes); } @@ -1747,6 +1767,30 @@ class Zoom extends MouseWheel { } } +class Copy extends Context { + + constructor(target, blueprint, options = {}) { + options.wantsFocusCallback = true; + super(target, blueprint, options); + this.serializer = new ObjectSerializer(); + let self = this; + this.copyHandle = _ => self.copied(); + } + + blueprintFocused() { + document.body.addEventListener("copy", this.copyHandle); + } + + blueprintUnfocused() { + document.body.removeEventListener("copy", this.copyHandle); + } + + copied() { + const value = this.blueprint.getNodes(true).map(node => this.serializer.write(node.entity)).join("\n"); + navigator.clipboard.writeText(value); + } +} + /** @typedef {import("./graph/GraphNode").default} GraphNode */ class Blueprint extends GraphElement { @@ -1807,6 +1851,7 @@ class Blueprint extends GraphElement { this.querySelector('[data-nodes]').append(...this.entity.nodes); + this.copyObject = new Copy(this.getGridDOMElement(), this); this.pasteObject = new Paste(this.getGridDOMElement(), this); this.dragObject = new DragScroll(this.getGridDOMElement(), this, { @@ -2027,8 +2072,18 @@ class Blueprint extends GraphElement { * * @returns {GraphNode[]} Nodes */ - getNodes() { - return this.entity.nodes + getNodes(selected = false) { + if (selected) { + return this.entity.nodes.filter( + /** + * + * @param {GraphNode} node + */ + node => node.selected + ) + } else { + return this.entity.nodes + } } /** @@ -2098,9 +2153,25 @@ class GraphLink extends GraphElement { customElements.define('u-link', GraphLink); -SerializerFactory.registerSerializer(ObjectEntity, new ObjectSerializer()); -SerializerFactory.registerSerializer(PinEntity, new GeneralSerializer("Pin ", PinEntity, "", ",", true)); -SerializerFactory.registerSerializer(FunctionReferenceEntity, new GeneralSerializer("", FunctionReferenceEntity, "", ",", false)); -SerializerFactory.registerSerializer(LocalizedTextEntity, new GeneralSerializer("NSLOCTEXT", LocalizedTextEntity, "", ",", false, "", _ => "")); +SerializerFactory.registerSerializer( + ObjectEntity, + new ObjectSerializer() +); +SerializerFactory.registerSerializer( + PinEntity, + new GeneralSerializer(v => `Pin (${v})`, PinEntity, "", ",", true) +); +SerializerFactory.registerSerializer( + FunctionReferenceEntity, + new GeneralSerializer(v => `(${v})`, FunctionReferenceEntity, "", ",", false) +); +SerializerFactory.registerSerializer( + LocalizedTextEntity, + new GeneralSerializer(v => `NSLOCTEXT(${v})`, LocalizedTextEntity, "", ",", false, "", _ => "") +); +SerializerFactory.registerSerializer( + PinReferenceEntity, + new GeneralSerializer(v => v, PinReferenceEntity, "", " ", false, "", _ => "") +); export { Blueprint, GraphLink, GraphNode }; diff --git a/js/Blueprint.js b/js/Blueprint.js index 6bcd6ff..c4356fe 100755 --- a/js/Blueprint.js +++ b/js/Blueprint.js @@ -9,6 +9,7 @@ import Select from "./input/Select" import Unfocus from "./input/Unfocus" import Utility from "./Utility" import Zoom from "./input/Zoom" +import Copy from "./input/Copy" /** @typedef {import("./graph/GraphNode").default} GraphNode */ export default class Blueprint extends GraphElement { @@ -70,6 +71,7 @@ export default class Blueprint extends GraphElement { this.querySelector('[data-nodes]').append(...this.entity.nodes) + this.copyObject = new Copy(this.getGridDOMElement(), this) this.pasteObject = new Paste(this.getGridDOMElement(), this) this.dragObject = new DragScroll(this.getGridDOMElement(), this, { @@ -290,8 +292,18 @@ export default class Blueprint extends GraphElement { * * @returns {GraphNode[]} Nodes */ - getNodes() { - return this.entity.nodes + getNodes(selected = false) { + if (selected) { + return this.entity.nodes.filter( + /** + * + * @param {GraphNode} node + */ + node => node.selected + ) + } else { + return this.entity.nodes + } } /** diff --git a/js/entity/LocalizedTextEntity.js b/js/entity/LocalizedTextEntity.js index 8dbf985..40b58b2 100755 --- a/js/entity/LocalizedTextEntity.js +++ b/js/entity/LocalizedTextEntity.js @@ -11,8 +11,4 @@ export default class LocalizedTextEntity extends Entity { getAttributes() { return LocalizedTextEntity.attributes } - - toString() { - "NSLOCTEXT(" + `"${this.namespace}"` + ", " + `"${this.key}"` + ", " + `"${this.value}"` + ")" - } } diff --git a/js/entity/PinReferenceEntity.js b/js/entity/PinReferenceEntity.js index 7435541..2ab33e6 100755 --- a/js/entity/PinReferenceEntity.js +++ b/js/entity/PinReferenceEntity.js @@ -1,10 +1,11 @@ import Entity from "./Entity" import Guid from "./primitive/Guid" +import PathSymbol from "./primitive/PathSymbol" export default class PinReferenceEntity extends Entity { static attributes = { - objectName: String, + objectName: PathSymbol, pinGuid: Guid } diff --git a/js/entity/primitive/PathSymbol.js b/js/entity/primitive/PathSymbol.js new file mode 100644 index 0000000..d8131a3 --- /dev/null +++ b/js/entity/primitive/PathSymbol.js @@ -0,0 +1,17 @@ +import Primitive from "./Primitive"; + +export default class PathSymbol extends Primitive { + + constructor(value) { + super() + this.value = new String(value).valueOf() + } + + valueOf() { + this.value + } + + toString() { + return this.value + } +} \ No newline at end of file diff --git a/js/export.js b/js/export.js index d91a84b..7562d86 100755 --- a/js/export.js +++ b/js/export.js @@ -8,10 +8,27 @@ import SerializerFactory from "./serialization/SerializerFactory" import Blueprint from "./Blueprint" import GraphNode from "./graph/GraphNode" import GraphLink from "./graph/GraphLink" +import PinReferenceEntity from "./entity/PinReferenceEntity" -SerializerFactory.registerSerializer(ObjectEntity, new ObjectSerializer()) -SerializerFactory.registerSerializer(PinEntity, new GeneralSerializer("Pin ", PinEntity, "", ",", true)) -SerializerFactory.registerSerializer(FunctionReferenceEntity, new GeneralSerializer("", FunctionReferenceEntity, "", ",", false)) -SerializerFactory.registerSerializer(LocalizedTextEntity, new GeneralSerializer("NSLOCTEXT", LocalizedTextEntity, "", ",", false, "", _ => "")) +SerializerFactory.registerSerializer( + ObjectEntity, + new ObjectSerializer() +) +SerializerFactory.registerSerializer( + PinEntity, + new GeneralSerializer(v => `Pin (${v})`, PinEntity, "", ",", true) +) +SerializerFactory.registerSerializer( + FunctionReferenceEntity, + new GeneralSerializer(v => `(${v})`, FunctionReferenceEntity, "", ",", false) +) +SerializerFactory.registerSerializer( + LocalizedTextEntity, + new GeneralSerializer(v => `NSLOCTEXT(${v})`, LocalizedTextEntity, "", ",", false, "", _ => "") +) +SerializerFactory.registerSerializer( + PinReferenceEntity, + new GeneralSerializer(v => v, PinReferenceEntity, "", " ", false, "", _ => "") +) export { Blueprint as Blueprint, GraphNode as GraphNode, GraphLink as GraphLink } \ No newline at end of file diff --git a/js/input/Copy.js b/js/input/Copy.js new file mode 100644 index 0000000..801261b --- /dev/null +++ b/js/input/Copy.js @@ -0,0 +1,26 @@ +import Context from "./Context" +import ObjectSerializer from "../serialization/ObjectSerializer" + +export default class Copy extends Context { + + constructor(target, blueprint, options = {}) { + options.wantsFocusCallback = true + super(target, blueprint, options) + this.serializer = new ObjectSerializer() + let self = this + this.copyHandle = _ => self.copied() + } + + blueprintFocused() { + document.body.addEventListener("copy", this.copyHandle) + } + + blueprintUnfocused() { + document.body.removeEventListener("copy", this.copyHandle) + } + + copied() { + const value = this.blueprint.getNodes(true).map(node => this.serializer.write(node.entity)).join("\n") + navigator.clipboard.writeText(value) + } +} \ No newline at end of file diff --git a/js/input/Paste.js b/js/input/Paste.js index 80f0d5b..6c3faf4 100644 --- a/js/input/Paste.js +++ b/js/input/Paste.js @@ -7,9 +7,7 @@ export default class Paste extends Context { constructor(target, blueprint, options = {}) { options.wantsFocusCallback = true super(target, blueprint, options) - this.serializer = new ObjectSerializer() - let self = this this.pasteHandle = e => self.pasted(e.clipboardData.getData("Text")) } @@ -33,8 +31,11 @@ export default class Paste extends Context { }) let mousePosition = this.blueprint.entity.mousePosition nodes.forEach(node => { - node.location[0] += mousePosition[0] - left - node.location[1] += mousePosition[1] - top + const locationOffset = [ + mousePosition[0] - left, + mousePosition[1] - top + ] + node.addLocation(locationOffset) }) this.blueprint.addNode(...nodes) } diff --git a/js/serialization/GeneralSerializer.js b/js/serialization/GeneralSerializer.js index 965f513..51f2d3d 100755 --- a/js/serialization/GeneralSerializer.js +++ b/js/serialization/GeneralSerializer.js @@ -3,9 +3,9 @@ import Serializer from "./Serializer" export default class GeneralSerializer extends Serializer { - constructor(keyword, entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) { + constructor(wrap, entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) { super(entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) - this.keyword = keyword ?? "" + this.wrap = wrap ?? (v => `(${v})`) } read(value) { @@ -19,7 +19,7 @@ export default class GeneralSerializer extends Serializer { } write(object) { - let result = `${this.keyword}(${this.subWrite([], object)})` + let result = this.wrap(this.subWrite([], object)) return result } } diff --git a/js/serialization/Grammar.js b/js/serialization/Grammar.js index 26b8b53..6ef513c 100755 --- a/js/serialization/Grammar.js +++ b/js/serialization/Grammar.js @@ -8,6 +8,7 @@ import Parsimmon from "parsimmon" import PinEntity from "../entity/PinEntity" import PinReferenceEntity from "../entity/PinReferenceEntity" import Utility from "../Utility" +import PathSymbol from "../entity/primitive/PathSymbol" let P = Parsimmon @@ -24,8 +25,8 @@ export default class Grammar { String = _ => P.regex(/(?:[^"\\]|\\")*/).wrap(P.string('"'), P.string('"')).desc('string (with possibility to escape the quote using \")') Word = _ => P.regex(/[a-zA-Z]+/).desc("a word") Guid = _ => P.regex(/[0-9a-zA-Z]{32}/).map(v => new Guid(v)).desc("32 digit hexadecimal (accepts all the letters for safety) value") - PathSymbol = _ => P.regex(/[0-9a-zA-Z_]+/) - ReferencePath = r => P.seq(P.string("/"), r.PathSymbol.sepBy1(P.string(".")).tieWith(".")) + PathSymbol = _ => P.regex(/[0-9a-zA-Z_]+/).map(v => new PathSymbol(v)) + ReferencePath = r => P.seq(P.string("/"), r.PathSymbol.map(v => v.toString()).sepBy1(P.string(".")).tieWith(".")) .tie() .atLeast(2) .tie() diff --git a/js/serialization/ObjectSerializer.js b/js/serialization/ObjectSerializer.js index 231f84e..3ace935 100755 --- a/js/serialization/ObjectSerializer.js +++ b/js/serialization/ObjectSerializer.js @@ -49,7 +49,7 @@ export default class ObjectSerializer extends Serializer { * @returns */ write(object) { - let result = `Begin Object Class=${object.Class} Name=${object.Name} + let result = `Begin Object Class=${object.Class} Name="${object.Name}" ${this.subWrite([], object) + object .CustomProperties.map(pin => this.separator + this.prefix + "CustomProperties " + SerializerFactory.getSerializer(PinEntity).write(pin)) diff --git a/js/serialization/Serializer.js b/js/serialization/Serializer.js index d65fa57..2cb0be9 100755 --- a/js/serialization/Serializer.js +++ b/js/serialization/Serializer.js @@ -23,6 +23,7 @@ export default class Serializer { if (value === null) { return "()" } + const serialize = v => SerializerFactory.getSerializer(Utility.getType(v)).write(v) // This is an exact match (and not instanceof) to hit also primitive types (by accessing value.constructor they are converted to objects automatically) switch (value?.constructor) { case Function: @@ -34,8 +35,11 @@ export default class Serializer { case String: return `"${value}"` } + if (value instanceof Array) { + return `(${value.map(v => serialize(v) + ",")})` + } if (value instanceof Entity) { - return SerializerFactory.getSerializer(Utility.getType(value)).write(value) + return serialize(value) } if (value instanceof Primitive) { return value.toString() @@ -51,7 +55,8 @@ export default class Serializer { const value = object[property] if (object[property]?.constructor === Object) { // Recursive call when finding an object - result += this.subWrite(fullKey, value, this.prefix, this.separator) + result += (result.length ? this.separator : "") + + this.subWrite(fullKey, value) } else if (this.showProperty(fullKey, value)) { result += (result.length ? this.separator : "") + this.prefix @@ -60,7 +65,7 @@ export default class Serializer { + this.writeValue(value) } } - if (this.trailingSeparator && result.length) { + if (this.trailingSeparator && result.length && fullKey.length === 0) { // append separator at the end if asked and there was printed content result += this.separator }