diff --git a/dist/ueblueprint.js b/dist/ueblueprint.js index 3d78921..ae6fd89 100755 --- a/dist/ueblueprint.js +++ b/dist/ueblueprint.js @@ -214,6 +214,33 @@ class IInput { // @ts-check +/** + * @typedef {import("../entity/IEntity").default} IEntity + * @typedef {(new (object?: Object) => IEntity) | StringConstructor | NumberConstructor | BooleanConstructor} Constructor + * @typedef {Constructor|Constructor[]} AcceptedType + */ + +class SerializedType { + + /** @type {(Constructor|Array)[]} */ + #types + get types() { + return this.#types + } + set types(v) { + this.#types = v; + } + + /** + * @param {...AcceptedType} acceptedTypes + */ + constructor(...acceptedTypes) { + this.#types = acceptedTypes; + } +} + +// @ts-check + /** * @template T */ @@ -249,21 +276,21 @@ class TypeInitialization { if (targetType === undefined) { targetType = value?.constructor; } - let wrongType = false; - if (targetType && value?.constructor !== targetType && !(value instanceof targetType)) { - wrongType = true; + if ( + targetType + && targetType !== SerializedType + && !(value?.constructor === targetType || value instanceof targetType) + ) { + value = new targetType(value); } if (value instanceof Boolean || value instanceof Number || value instanceof String) { value = value.valueOf(); // Get the relative primitive value } - if (wrongType) { - return new targetType(value) - } return value } /** - * @typedef {(new () => T) | StringConstructor | NumberConstructor | BooleanConstructor} Constructor + * @typedef {(new () => T) | SerializedType | StringConstructor | NumberConstructor | BooleanConstructor} Constructor * @param {Constructor|Array} type * @param {Boolean} showDefault * @param {any} value @@ -272,6 +299,8 @@ class TypeInitialization { if (value === undefined) { if (type instanceof Array) { value = []; + } else if (type instanceof SerializedType) { + value = ""; } else { value = TypeInitialization.sanitize(new type()); } @@ -487,13 +516,30 @@ class Utility { } } +class ISerializable { + + #showAsString = false + + isShownAsString() { + return this.#showAsString + } + + /** + * @param {Boolean} v + */ + setShowAsString(v) { + this.#showAsString = v; + } +} + // @ts-check -class IEntity { +class IEntity extends ISerializable { static attributes = {} constructor(values) { + super(); /** * @param {Object} target * @param {Object} properties @@ -532,6 +578,7 @@ class IEntity { const value = Utility.objectGet(values, [property]); if (value !== undefined) { target[property] = TypeInitialization.sanitize(value, defaultType); + // We have a value, need nothing more continue } if (defaultValue instanceof TypeInitialization) { @@ -722,6 +769,26 @@ class KeyBindingEntity extends IEntity { // @ts-check +class LinearColorEntity extends IEntity { + + static attributes = { + R: Number, + G: Number, + B: Number, + A: Number, + } + + constructor(options = {}) { + super(options); + /** @type {Number} */ this.R; + /** @type {Number} */ this.G; + /** @type {Number} */ this.B; + /** @type {Number} */ this.A; + } +} + +// @ts-check + class LocalizedTextEntity extends IEntity { static lookbehind = "NSLOCTEXT" @@ -802,7 +869,7 @@ class PinEntity extends IEntity { bSerializeAsSinglePrecisionFloat: false, }, LinkedTo: new TypeInitialization([PinReferenceEntity], false), - DefaultValue: new TypeInitialization(String, false), + DefaultValue: new TypeInitialization(new SerializedType(LinearColorEntity, String), false), AutogeneratedDefaultValue: new TypeInitialization(String, false), DefaultObject: new TypeInitialization(ObjectReferenceEntity, false, null), PersistentGuid: GuidEntity, @@ -912,6 +979,14 @@ class PinEntity extends IEntity { getType() { return this.PinType.PinCategory } + + getSubCategory() { + return this.PinType.PinSubCategoryObject.path + } + + getColorValue() { + if (this.PinType.PinSubCategoryObject.path == "/Script/CoreUObject.LinearColor") ; + } } // @ts-check @@ -1023,6 +1098,10 @@ var Parsimmon = /*@__PURE__*/getDefaultExportFromCjs(parsimmon_umd_min.exports); // @ts-check +/** + * @typedef {import("../entity/IEntity").default} IEntity + */ + let P = Parsimmon; class Grammar { @@ -1032,6 +1111,27 @@ class Grammar { static getGrammarForType(r, attributeType, defaultGrammar) { if (attributeType instanceof TypeInitialization) { attributeType = attributeType.type; + return Grammar.getGrammarForType(r, attributeType, defaultGrammar) + } + if (attributeType instanceof SerializedType) { + const noStringTypes = attributeType.types.filter(t => t !== String); + let result = P.alt( + ...noStringTypes.map(t => + Grammar.getGrammarForType(r, t).wrap(P.string('"'), P.string('"')).map( + /** + * @param {IEntity} entity + */ + entity => { + entity.setShowAsString(true); // Showing as string because it is inside a SerializedType + return entity + } + ) + ) + ); + if (noStringTypes.length < attributeType.types.length) { + result = result.or(r.String); // Separated because it cannot be wrapped into " and " + } + return result } switch (Utility.getType(attributeType)) { case Boolean: @@ -1054,6 +1154,8 @@ class Grammar { return r.InvariantText case PinReferenceEntity: return r.PinReference + case LinearColorEntity: + return r.LinearColor case FunctionReferenceEntity: return r.FunctionReference case PinEntity: @@ -1170,7 +1272,7 @@ class Grammar { r.None, ...[r.ReferencePath.map(path => new ObjectReferenceEntity({ type: "", path: path }))] .flatMap(referencePath => [ - referencePath, // version having just path + referencePath, // Version having just path referencePath.trim(P.string('"')) // Version having path surround with double quotes ]), P.seqMap( @@ -1229,6 +1331,8 @@ class Grammar { }) ) + LinearColor = r => Grammar.createMultiAttributeGrammar(r, LinearColorEntity) + FunctionReference = r => Grammar.createMultiAttributeGrammar(r, FunctionReferenceEntity) KeyBinding = r => P.alt( @@ -1289,6 +1393,9 @@ class SerializerFactory { // @ts-check +/** + * @template {IEntity} T + */ class ISerializer { static grammar = Parsimmon.createLanguage(new Grammar()) @@ -1302,21 +1409,66 @@ class ISerializer { this.attributeKeyPrinter = attributeKeyPrinter ?? (k => k.join(".")); } - writeValue(value, fullKey = undefined) { + /** + * @param {String} value + * @returns {T} + */ + deserialize(value) { + return this.read(value) + } + + /** + * @param {T} object + * @param {Boolean} insideString + * @returns {String} + */ + serialize(object, insideString) { + insideString ||= object.isShownAsString(); + let result = this.write(object, insideString); + if (object.isShownAsString()) { + result = `"${result}"`; + } + return result + } + + /** + * @param {String} value + * @returns {T} + */ + read(value) { + throw new Error("Not implemented") + } + + /** + * @param {T} object + * @param {Boolean} insideString + * @returns {String} + */ + write(object, insideString) { + throw new Error("Not implemented") + } + + /** + * @param {String[]} fullKey + * @param {Boolean} insideString + */ + writeValue(value, fullKey, insideString) { if (value === null) { return "()" } - const serialize = v => SerializerFactory.getSerializer(Utility.getType(v)).write(v); + const serialize = v => SerializerFactory.getSerializer(Utility.getType(v)).serialize(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: - return this.writeValue(value(), fullKey) + return this.writeValue(value(), fullKey, insideString) case Boolean: return Utility.FirstCapital(value.toString()) case Number: return value.toString() case String: - return `"${Utility.encodeString(value)}"` + return insideString + ? `\\"${Utility.encodeString(value)}\\"` + : `"${Utility.encodeString(value)}"` } if (value instanceof Array) { return `(${value.map(v => serialize(v) + ",").join("")})` @@ -1329,25 +1481,26 @@ class ISerializer { /** * @param {String[]} key * @param {Object} object + * @param {Boolean} insideString * @returns {String} */ - subWrite(key, object) { + subWrite(key, object, insideString) { let result = ""; let fullKey = key.concat(""); const last = fullKey.length - 1; for (const property of Object.getOwnPropertyNames(object)) { fullKey[last] = property; const value = object[property]; - if (object[property]?.constructor === Object) { + if (value?.constructor === Object) { // Recursive call when finding an object result += (result.length ? this.separator : "") - + this.subWrite(fullKey, value); + + this.subWrite(fullKey, value, insideString); } else if (value !== undefined && this.showProperty(object, fullKey, value)) { result += (result.length ? this.separator : "") + this.prefix + this.attributeKeyPrinter(fullKey) + this.attributeValueConjunctionSign - + this.writeValue(value, fullKey); + + this.writeValue(value, fullKey, insideString); } } if (this.trailingSeparator && result.length && fullKey.length === 1) { @@ -1386,6 +1539,9 @@ class ObjectSerializer extends ISerializer { return super.showProperty(object, attributeKey, attributeValue) } + /** + * @param {String} value + */ read(value) { const parseResult = ISerializer.grammar.Object.parse(value); if (!parseResult.status) { @@ -1396,7 +1552,6 @@ class ObjectSerializer extends ISerializer { /** * @param {String} value - * @returns {ObjectEntity[]} */ readMultiple(value) { const parseResult = ISerializer.grammar.MultipleObject.parse(value); @@ -1408,16 +1563,17 @@ class ObjectSerializer extends ISerializer { /** * @param {ObjectEntity} object + * @param {Boolean} insideString */ - write(object) { - let result = `Begin Object Class=${object.Class.path} Name=${this.writeValue(object.Name, "Name")} -${this.subWrite([], object) + write(object, insideString) { + let result = `Begin Object Class=${object.Class.path} Name=${this.writeValue(object.Name, ["Name"], insideString)} +${this.subWrite([], object, insideString) + object .CustomProperties.map(pin => this.separator + this.prefix + "CustomProperties " - + SerializerFactory.getSerializer(PinEntity).write(pin) + + SerializerFactory.getSerializer(PinEntity).serialize(pin) ) .join("")} End Object\n`; @@ -1450,7 +1606,7 @@ class Copy extends IInput { } copied() { - const value = this.blueprint.getNodes(true).map(node => this.serializer.write(node.entity)).join("\n"); + const value = this.blueprint.getNodes(true).map(node => this.serializer.serialize(node.entity)).join("\n"); navigator.clipboard.writeText(value); } } @@ -3101,6 +3257,65 @@ class BoolPinTemplate extends IInputPinTemplate { // @ts-check +/** + * @typedef {import("../element/PinElement").default} PinElement + */ + +class ColorPinTemplate extends IInputPinTemplate { + + /** @type {HTMLInputElement} */ + #input + + /** + * @param {PinElement} pin + */ + setup(pin) { + super.setup(pin); + this.#input = pin.querySelector(".ueb-pin-input"); + let self = this; + this.onChangeHandler = _ => pin.entity.DefaultValue = self.#input.checked ? "true" : "false"; + this.#input.addEventListener("change", this.onChangeHandler); + } + + /** + * @param {PinElement} pin + */ + cleanup(pin) { + super.cleanup(pin); + this.#input.removeEventListener("change", this.onChangeHandler); + } + + /** + * @param {PinElement} pin + */ + getInputs(pin) { + return [this.#input.checked ? "true" : "false"] + } + + /** + * @param {PinElement} pin + * @param {String[]?} value + */ + setInputs(pin, value = []) { + pin.entity.DefaultValue = value.length ? value[0] : this.getInput(pin); + this.#input.checked = pin.entity.DefaultValue == "true"; + } + + /** + * @param {PinElement} pin + */ + renderInput(pin) { + if (pin.isInput()) { + return html` + + ` + } + return super.renderInput(pin) + } +} + +// @ts-check + /** * @typedef {import("../element/PinElement").default} PinElement */ @@ -3133,51 +3348,6 @@ class ExecPinTemplate extends PinTemplate { // @ts-check -/** - * @typedef {import("../element/PinElement").default} PinElement - */ - -class RealPinTemplate extends IInputPinTemplate { - - /** - * @param {PinElement} pin - * @param {String[]?} values - */ - setInputs(pin, values = []) { - let num = parseFloat(values.length ? values[0] : this.getInput(pin)); - let updateDefaultValue = true; - if (isNaN(num)) { - num = parseFloat(pin.entity.DefaultValue != "" - ? pin.entity.DefaultValue - : pin.entity.AutogeneratedDefaultValue); - } - if (isNaN(num)) { - num = 0; - updateDefaultValue = false; - } - values[0] = Utility.minDecimals(num); - super.setInputs(pin, values, updateDefaultValue); - } -} - -// @ts-check - -/** - * @typedef {import("../element/PinElement").default} PinElement - */ - -class StringPinTemplate extends IInputPinTemplate { - - /** - * @param {PinElement} pin - */ - setup(pin) { - super.setup(pin); - } -} - -// @ts-check - /** * @typedef {import("../element/PinElement").default} PinElement */ @@ -3230,6 +3400,51 @@ class NamePinTemplate extends IInputPinTemplate { // @ts-check +/** + * @typedef {import("../element/PinElement").default} PinElement + */ + +class RealPinTemplate extends IInputPinTemplate { + + /** + * @param {PinElement} pin + * @param {String[]?} values + */ + setInputs(pin, values = []) { + let num = parseFloat(values.length ? values[0] : this.getInput(pin)); + let updateDefaultValue = true; + if (isNaN(num)) { + num = parseFloat(pin.entity.DefaultValue != "" + ? pin.entity.DefaultValue + : pin.entity.AutogeneratedDefaultValue); + } + if (isNaN(num)) { + num = 0; + updateDefaultValue = false; + } + values[0] = Utility.minDecimals(num); + super.setInputs(pin, values, updateDefaultValue); + } +} + +// @ts-check + +/** + * @typedef {import("../element/PinElement").default} PinElement + */ + +class StringPinTemplate extends IInputPinTemplate { + + /** + * @param {PinElement} pin + */ + setup(pin) { + super.setup(pin); + } +} + +// @ts-check + /** * @typedef {import("../entity/GuidEntity").default} GuidEntity * @typedef {import("../entity/PinEntity").default} PinEntity @@ -3248,6 +3463,21 @@ class PinElement extends IElement { "name": NamePinTemplate, "real": RealPinTemplate, "string": StringPinTemplate, + "struct": { + "/Script/CoreUObject.LinearColor": ColorPinTemplate, + } + } + + /** + * @param {PinEntity} pinEntity + * @return {PinTemplate} + */ + static getTypeTemplate(pinEntity) { + let result = PinElement.#typeTemplateMap[pinEntity.getType()]; + if (result.constructor === Object) { + result = result[pinEntity.getSubCategory()]; + } + return result ?? PinTemplate } #color = "" @@ -3266,7 +3496,8 @@ class PinElement extends IElement { constructor(entity) { super( entity, - new (PinElement.#typeTemplateMap[entity.getType()] ?? PinTemplate)() + // @ts-expect-error + new (PinElement.getTypeTemplate(entity))() ); } @@ -3636,7 +3867,7 @@ class NodeElement extends ISelectableDraggableElement { */ static fromSerializedObject(str) { str = str.trim(); - let entity = SerializerFactory.getSerializer(ObjectEntity).read(str); + let entity = SerializerFactory.getSerializer(ObjectEntity).deserialize(str); return new NodeElement(entity) } @@ -4854,8 +5085,18 @@ customElements.define("ueb-blueprint", Blueprint); // @ts-check +/** + * @typedef {import("../entity/IEntity").default} IEntity + */ + +/** + * @template {IEntity} T + */ class GeneralSerializer extends ISerializer { + /** + * @param {new () => T} entityType + */ constructor(wrap, entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) { wrap = wrap ?? (v => `(${v})`); super(entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter); @@ -4863,7 +5104,6 @@ class GeneralSerializer extends ISerializer { } /** - * @template T * @param {String} value * @returns {T} */ @@ -4877,27 +5117,42 @@ class GeneralSerializer extends ISerializer { } /** - * @template T * @param {T} object + * @param {Boolean} insideString * @returns {String} */ - write(object) { - let result = this.wrap(this.subWrite([], object)); + write(object, insideString = false) { + let result = this.wrap(this.subWrite([], object, insideString)); return result } } // @ts-check +/** + * @typedef {import("../entity/IEntity").default} IEntity + */ + +/** + * @template {IEntity} T + */ class CustomSerializer extends GeneralSerializer { + /** + * @param {new () => T} entityType + */ constructor(objectWriter, entityType) { super(undefined, entityType); this.objectWriter = objectWriter; } - write(object) { - let result = this.objectWriter(object); + /** + * @param {T} object + * @param {Boolean} insideString + * @returns {String} + */ + write(object, insideString = false) { + let result = this.objectWriter(object, insideString); return result } } @@ -4910,25 +5165,45 @@ class PinSerializer extends GeneralSerializer { super(v => `${PinEntity.lookbehind} (${v})`, PinEntity, "", ",", true); } - writeValue(value, fullKey) { - if (value?.constructor === String && fullKey == "DefaultValue") { + /** + * @param {String[]} fullKey + * @param {Boolean} insideString + */ + writeValue(value, fullKey, insideString) { + if (value?.constructor === String && fullKey.length == 1 && fullKey[0] == "DefaultValue") { // @ts-expect-error return `"${Utility.encodeInputString(value)}"` } - return super.writeValue(value, fullKey) + return super.writeValue(value, fullKey, insideString) } } // @ts-check +/** + * @typedef {import("../entity/IEntity").default} IEntity + */ + +/** + * @template {IEntity} T + */ class ToStringSerializer extends GeneralSerializer { + /** + * @param {new () => T} entityType + */ constructor(entityType) { super(undefined, entityType); } - write(object) { - let result = object.toString(); + /** + * @param {T} object + * @param {Boolean} insideString + */ + write(object, insideString) { + let result = insideString || object.isShownAsString() + ? `"${object.toString().replaceAll('"', '\\"')}"` + : object.toString(); return result } } @@ -4937,6 +5212,13 @@ class ToStringSerializer extends GeneralSerializer { function initializeSerializerFactory() { + const bracketsWrapped = v => `(${v})`; + + SerializerFactory.registerSerializer( + LinearColorEntity, + new GeneralSerializer(bracketsWrapped, LinearColorEntity) + ); + SerializerFactory.registerSerializer( ObjectEntity, new ObjectSerializer() @@ -4949,12 +5231,12 @@ function initializeSerializerFactory() { SerializerFactory.registerSerializer( FunctionReferenceEntity, - new GeneralSerializer(v => `(${v})`, FunctionReferenceEntity, "", ",", false) + new GeneralSerializer(bracketsWrapped, FunctionReferenceEntity) ); SerializerFactory.registerSerializer( KeyBindingEntity, - new GeneralSerializer(v => `(${v})`, KeyBindingEntity, "", ",", false) + new GeneralSerializer(bracketsWrapped, KeyBindingEntity) ); SerializerFactory.registerSerializer( @@ -4980,7 +5262,9 @@ function initializeSerializerFactory() { objectReference.path ? objectReference.type ? `'"${objectReference.path}"'` : `"${objectReference.path}"` : "" - )) + ), + ObjectReferenceEntity + ) ); SerializerFactory.registerSerializer(IdentifierEntity, new ToStringSerializer(IdentifierEntity)); diff --git a/js/element/NodeElement.js b/js/element/NodeElement.js index 3b0bf6e..c075e41 100644 --- a/js/element/NodeElement.js +++ b/js/element/NodeElement.js @@ -28,7 +28,7 @@ export default class NodeElement extends ISelectableDraggableElement { */ static fromSerializedObject(str) { str = str.trim() - let entity = SerializerFactory.getSerializer(ObjectEntity).read(str) + let entity = SerializerFactory.getSerializer(ObjectEntity).deserialize(str) return new NodeElement(entity) } diff --git a/js/element/PinElement.js b/js/element/PinElement.js index b0f0530..64432a2 100644 --- a/js/element/PinElement.js +++ b/js/element/PinElement.js @@ -1,14 +1,15 @@ // @ts-check import BoolPinTemplate from "../template/BoolPinTemplate" +import ColorPinTemplate from "../template/ColorPinTemplate" import ExecPinTemplate from "../template/ExecPinTemplate" import IElement from "./IElement" import LinkElement from "./LinkElement" +import NamePinTemplate from "../template/NamePinTemplate" import PinTemplate from "../template/PinTemplate" import RealPinTemplate from "../template/RealPinTemplate" import StringPinTemplate from "../template/StringPinTemplate" import Utility from "../Utility" -import NamePinTemplate from "../template/NamePinTemplate" /** * @typedef {import("../entity/GuidEntity").default} GuidEntity @@ -28,6 +29,21 @@ export default class PinElement extends IElement { "name": NamePinTemplate, "real": RealPinTemplate, "string": StringPinTemplate, + "struct": { + "/Script/CoreUObject.LinearColor": ColorPinTemplate, + } + } + + /** + * @param {PinEntity} pinEntity + * @return {PinTemplate} + */ + static getTypeTemplate(pinEntity) { + let result = PinElement.#typeTemplateMap[pinEntity.getType()] + if (result.constructor === Object) { + result = result[pinEntity.getSubCategory()] + } + return result ?? PinTemplate } #color = "" @@ -46,7 +62,8 @@ export default class PinElement extends IElement { constructor(entity) { super( entity, - new (PinElement.#typeTemplateMap[entity.getType()] ?? PinTemplate)() + // @ts-expect-error + new (PinElement.getTypeTemplate(entity))() ) } diff --git a/js/entity/IEntity.js b/js/entity/IEntity.js index cd827d9..d7c751b 100644 --- a/js/entity/IEntity.js +++ b/js/entity/IEntity.js @@ -1,13 +1,15 @@ // @ts-check +import ISerializable from "./ISerializable" import TypeInitialization from "./TypeInitialization" import Utility from "../Utility" -export default class IEntity { +export default class IEntity extends ISerializable { static attributes = {} constructor(values) { + super() /** * @param {Object} target * @param {Object} properties @@ -46,6 +48,7 @@ export default class IEntity { const value = Utility.objectGet(values, [property]) if (value !== undefined) { target[property] = TypeInitialization.sanitize(value, defaultType) + // We have a value, need nothing more continue } if (defaultValue instanceof TypeInitialization) { diff --git a/js/entity/ISerializable.js b/js/entity/ISerializable.js new file mode 100644 index 0000000..4800806 --- /dev/null +++ b/js/entity/ISerializable.js @@ -0,0 +1,15 @@ +export default class ISerializable { + + #showAsString = false + + isShownAsString() { + return this.#showAsString + } + + /** + * @param {Boolean} v + */ + setShowAsString(v) { + this.#showAsString = v + } +} \ No newline at end of file diff --git a/js/entity/LinearColorEntity.js b/js/entity/LinearColorEntity.js new file mode 100644 index 0000000..6a07352 --- /dev/null +++ b/js/entity/LinearColorEntity.js @@ -0,0 +1,21 @@ +// @ts-check + +import IEntity from "./IEntity" + +export default class LinearColorEntity extends IEntity { + + static attributes = { + R: Number, + G: Number, + B: Number, + A: Number, + } + + constructor(options = {}) { + super(options) + /** @type {Number} */ this.R + /** @type {Number} */ this.G + /** @type {Number} */ this.B + /** @type {Number} */ this.A + } +} diff --git a/js/entity/PinEntity.js b/js/entity/PinEntity.js index cca778f..5cfdeb3 100755 --- a/js/entity/PinEntity.js +++ b/js/entity/PinEntity.js @@ -2,9 +2,11 @@ import GuidEntity from "./GuidEntity" import IEntity from "./IEntity" +import LinearColorEntity from "./LinearColorEntity" import LocalizedTextEntity from "./LocalizedTextEntity" import ObjectReferenceEntity from "./ObjectReferenceEntity" import PinReferenceEntity from "./PinReferenceEntity" +import SerializedType from "./SerializedType" import TypeInitialization from "./TypeInitialization" export default class PinEntity extends IEntity { @@ -30,7 +32,7 @@ export default class PinEntity extends IEntity { bSerializeAsSinglePrecisionFloat: false, }, LinkedTo: new TypeInitialization([PinReferenceEntity], false), - DefaultValue: new TypeInitialization(String, false), + DefaultValue: new TypeInitialization(new SerializedType(LinearColorEntity, String), false), AutogeneratedDefaultValue: new TypeInitialization(String, false), DefaultObject: new TypeInitialization(ObjectReferenceEntity, false, null), PersistentGuid: GuidEntity, @@ -140,4 +142,14 @@ export default class PinEntity extends IEntity { getType() { return this.PinType.PinCategory } + + getSubCategory() { + return this.PinType.PinSubCategoryObject.path + } + + getColorValue() { + if (this.PinType.PinSubCategoryObject.path == "/Script/CoreUObject.LinearColor") { + + } + } } diff --git a/js/entity/SerializedType.js b/js/entity/SerializedType.js new file mode 100644 index 0000000..231faf3 --- /dev/null +++ b/js/entity/SerializedType.js @@ -0,0 +1,26 @@ +// @ts-check + +/** + * @typedef {import("../entity/IEntity").default} IEntity + * @typedef {(new (object?: Object) => IEntity) | StringConstructor | NumberConstructor | BooleanConstructor} Constructor + * @typedef {Constructor|Constructor[]} AcceptedType + */ + +export default class SerializedType { + + /** @type {(Constructor|Array)[]} */ + #types + get types() { + return this.#types + } + set types(v) { + this.#types = v + } + + /** + * @param {...AcceptedType} acceptedTypes + */ + constructor(...acceptedTypes) { + this.#types = acceptedTypes + } +} diff --git a/js/entity/TypeInitialization.js b/js/entity/TypeInitialization.js index abed842..71fe91e 100755 --- a/js/entity/TypeInitialization.js +++ b/js/entity/TypeInitialization.js @@ -1,5 +1,7 @@ // @ts-check +import SerializedType from "./SerializedType" + /** * @template T */ @@ -36,20 +38,21 @@ export default class TypeInitialization { targetType = value?.constructor } let wrongType = false - if (targetType && value?.constructor !== targetType && !(value instanceof targetType)) { - wrongType = true + if ( + targetType + && targetType !== SerializedType + && !(value?.constructor === targetType || value instanceof targetType) + ) { + value = new targetType(value) } if (value instanceof Boolean || value instanceof Number || value instanceof String) { value = value.valueOf() // Get the relative primitive value } - if (wrongType) { - return new targetType(value) - } return value } /** - * @typedef {(new () => T) | StringConstructor | NumberConstructor | BooleanConstructor} Constructor + * @typedef {(new () => T) | SerializedType | StringConstructor | NumberConstructor | BooleanConstructor} Constructor * @param {Constructor|Array} type * @param {Boolean} showDefault * @param {any} value @@ -58,6 +61,8 @@ export default class TypeInitialization { if (value === undefined) { if (type instanceof Array) { value = [] + } else if (type instanceof SerializedType) { + value = "" } else { value = TypeInitialization.sanitize(new type()) } diff --git a/js/input/common/Copy.js b/js/input/common/Copy.js index 7944262..234671d 100755 --- a/js/input/common/Copy.js +++ b/js/input/common/Copy.js @@ -26,7 +26,7 @@ export default class Copy extends IInput { } copied() { - const value = this.blueprint.getNodes(true).map(node => this.serializer.write(node.entity)).join("\n") + const value = this.blueprint.getNodes(true).map(node => this.serializer.serialize(node.entity, false)).join("\n") navigator.clipboard.writeText(value) } } diff --git a/js/serialization/CustomSerializer.js b/js/serialization/CustomSerializer.js index d59d02d..405c814 100755 --- a/js/serialization/CustomSerializer.js +++ b/js/serialization/CustomSerializer.js @@ -2,15 +2,30 @@ import GeneralSerializer from "./GeneralSerializer" +/** + * @typedef {import("../entity/IEntity").default} IEntity + */ + +/** + * @template {IEntity} T + */ export default class CustomSerializer extends GeneralSerializer { + /** + * @param {new () => T} entityType + */ constructor(objectWriter, entityType) { super(undefined, entityType) this.objectWriter = objectWriter } - write(object) { - let result = this.objectWriter(object) + /** + * @param {T} object + * @param {Boolean} insideString + * @returns {String} + */ + write(object, insideString = false) { + let result = this.objectWriter(object, insideString) return result } } diff --git a/js/serialization/GeneralSerializer.js b/js/serialization/GeneralSerializer.js index 1a33027..c444048 100755 --- a/js/serialization/GeneralSerializer.js +++ b/js/serialization/GeneralSerializer.js @@ -3,8 +3,18 @@ import Grammar from "./Grammar" import ISerializer from "./ISerializer" +/** + * @typedef {import("../entity/IEntity").default} IEntity + */ + +/** + * @template {IEntity} T + */ export default class GeneralSerializer extends ISerializer { + /** + * @param {new () => T} entityType + */ constructor(wrap, entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) { wrap = wrap ?? (v => `(${v})`) super(entityType, prefix, separator, trailingSeparator, attributeValueConjunctionSign, attributeKeyPrinter) @@ -12,7 +22,6 @@ export default class GeneralSerializer extends ISerializer { } /** - * @template T * @param {String} value * @returns {T} */ @@ -26,12 +35,12 @@ export default class GeneralSerializer extends ISerializer { } /** - * @template T * @param {T} object + * @param {Boolean} insideString * @returns {String} */ - write(object) { - let result = this.wrap(this.subWrite([], object)) + write(object, insideString = false) { + let result = this.wrap(this.subWrite([], object, insideString)) return result } } diff --git a/js/serialization/Grammar.js b/js/serialization/Grammar.js index 90e8195..ae905c8 100755 --- a/js/serialization/Grammar.js +++ b/js/serialization/Grammar.js @@ -6,6 +6,7 @@ import IdentifierEntity from "../entity/IdentifierEntity" import IntegerEntity from "../entity/IntegerEntity" import InvariantTextEntity from "../entity/InvariantTextEntity" import KeyBindingEntity from "../entity/KeyBindingEntity" +import LinearColorEntity from "../entity/LinearColorEntity" import LocalizedTextEntity from "../entity/LocalizedTextEntity" import ObjectEntity from "../entity/ObjectEntity" import ObjectReferenceEntity from "../entity/ObjectReferenceEntity" @@ -13,9 +14,14 @@ import Parsimmon from "parsimmon" import PathSymbolEntity from "../entity/PathSymbolEntity" import PinEntity from "../entity/PinEntity" import PinReferenceEntity from "../entity/PinReferenceEntity" +import SerializedType from "../entity/SerializedType" import TypeInitialization from "../entity/TypeInitialization" import Utility from "../Utility" +/** + * @typedef {import("../entity/IEntity").default} IEntity + */ + let P = Parsimmon export default class Grammar { @@ -25,6 +31,27 @@ export default class Grammar { static getGrammarForType(r, attributeType, defaultGrammar) { if (attributeType instanceof TypeInitialization) { attributeType = attributeType.type + return Grammar.getGrammarForType(r, attributeType, defaultGrammar) + } + if (attributeType instanceof SerializedType) { + const noStringTypes = attributeType.types.filter(t => t !== String) + let result = P.alt( + ...noStringTypes.map(t => + Grammar.getGrammarForType(r, t).wrap(P.string('"'), P.string('"')).map( + /** + * @param {IEntity} entity + */ + entity => { + entity.setShowAsString(true) // Showing as string because it is inside a SerializedType + return entity + } + ) + ) + ) + if (noStringTypes.length < attributeType.types.length) { + result = result.or(r.String) // Separated because it cannot be wrapped into " and " + } + return result } switch (Utility.getType(attributeType)) { case Boolean: @@ -47,6 +74,8 @@ export default class Grammar { return r.InvariantText case PinReferenceEntity: return r.PinReference + case LinearColorEntity: + return r.LinearColor case FunctionReferenceEntity: return r.FunctionReference case PinEntity: @@ -163,7 +192,7 @@ export default class Grammar { r.None, ...[r.ReferencePath.map(path => new ObjectReferenceEntity({ type: "", path: path }))] .flatMap(referencePath => [ - referencePath, // version having just path + referencePath, // Version having just path referencePath.trim(P.string('"')) // Version having path surround with double quotes ]), P.seqMap( @@ -222,6 +251,8 @@ export default class Grammar { }) ) + LinearColor = r => Grammar.createMultiAttributeGrammar(r, LinearColorEntity) + FunctionReference = r => Grammar.createMultiAttributeGrammar(r, FunctionReferenceEntity) KeyBinding = r => P.alt( diff --git a/js/serialization/ISerializer.js b/js/serialization/ISerializer.js index 96777c5..0c89f1e 100644 --- a/js/serialization/ISerializer.js +++ b/js/serialization/ISerializer.js @@ -7,6 +7,9 @@ import SerializerFactory from "./SerializerFactory" import TypeInitialization from "../entity/TypeInitialization" import Utility from "../Utility" +/** + * @template {IEntity} T + */ export default class ISerializer { static grammar = Parsimmon.createLanguage(new Grammar()) @@ -20,21 +23,66 @@ export default class ISerializer { this.attributeKeyPrinter = attributeKeyPrinter ?? (k => k.join(".")) } - writeValue(value, fullKey = undefined) { + /** + * @param {String} value + * @returns {T} + */ + deserialize(value) { + return this.read(value) + } + + /** + * @param {T} object + * @param {Boolean} insideString + * @returns {String} + */ + serialize(object, insideString) { + insideString ||= object.isShownAsString() + let result = this.write(object, insideString) + if (object.isShownAsString()) { + result = `"${result}"` + } + return result + } + + /** + * @param {String} value + * @returns {T} + */ + read(value) { + throw new Error("Not implemented") + } + + /** + * @param {T} object + * @param {Boolean} insideString + * @returns {String} + */ + write(object, insideString) { + throw new Error("Not implemented") + } + + /** + * @param {String[]} fullKey + * @param {Boolean} insideString + */ + writeValue(value, fullKey, insideString) { if (value === null) { return "()" } - const serialize = v => SerializerFactory.getSerializer(Utility.getType(v)).write(v) + const serialize = v => SerializerFactory.getSerializer(Utility.getType(v)).serialize(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: - return this.writeValue(value(), fullKey) + return this.writeValue(value(), fullKey, insideString) case Boolean: return Utility.FirstCapital(value.toString()) case Number: return value.toString() case String: - return `"${Utility.encodeString(value)}"` + return insideString + ? `\\"${Utility.encodeString(value)}\\"` + : `"${Utility.encodeString(value)}"` } if (value instanceof Array) { return `(${value.map(v => serialize(v) + ",").join("")})` @@ -47,25 +95,26 @@ export default class ISerializer { /** * @param {String[]} key * @param {Object} object + * @param {Boolean} insideString * @returns {String} */ - subWrite(key, object) { + subWrite(key, object, insideString) { let result = "" let fullKey = key.concat("") const last = fullKey.length - 1 for (const property of Object.getOwnPropertyNames(object)) { fullKey[last] = property const value = object[property] - if (object[property]?.constructor === Object) { + if (value?.constructor === Object) { // Recursive call when finding an object result += (result.length ? this.separator : "") - + this.subWrite(fullKey, value) + + this.subWrite(fullKey, value, insideString) } else if (value !== undefined && this.showProperty(object, fullKey, value)) { result += (result.length ? this.separator : "") + this.prefix + this.attributeKeyPrinter(fullKey) + this.attributeValueConjunctionSign - + this.writeValue(value, fullKey) + + this.writeValue(value, fullKey, insideString) } } if (this.trailingSeparator && result.length && fullKey.length === 1) { diff --git a/js/serialization/ObjectSerializer.js b/js/serialization/ObjectSerializer.js index 884bae8..b724da9 100755 --- a/js/serialization/ObjectSerializer.js +++ b/js/serialization/ObjectSerializer.js @@ -22,6 +22,9 @@ export default class ObjectSerializer extends ISerializer { return super.showProperty(object, attributeKey, attributeValue) } + /** + * @param {String} value + */ read(value) { const parseResult = ISerializer.grammar.Object.parse(value) if (!parseResult.status) { @@ -32,7 +35,6 @@ export default class ObjectSerializer extends ISerializer { /** * @param {String} value - * @returns {ObjectEntity[]} */ readMultiple(value) { const parseResult = ISerializer.grammar.MultipleObject.parse(value) @@ -44,16 +46,17 @@ export default class ObjectSerializer extends ISerializer { /** * @param {ObjectEntity} object + * @param {Boolean} insideString */ - write(object) { - let result = `Begin Object Class=${object.Class.path} Name=${this.writeValue(object.Name, "Name")} -${this.subWrite([], object) + write(object, insideString) { + let result = `Begin Object Class=${object.Class.path} Name=${this.writeValue(object.Name, ["Name"], insideString)} +${this.subWrite([], object, insideString) + object .CustomProperties.map(pin => this.separator + this.prefix + "CustomProperties " - + SerializerFactory.getSerializer(PinEntity).write(pin) + + SerializerFactory.getSerializer(PinEntity).serialize(pin) ) .join("")} End Object\n` diff --git a/js/serialization/PinSerializer.js b/js/serialization/PinSerializer.js index 7c4f6c4..e7d7e41 100755 --- a/js/serialization/PinSerializer.js +++ b/js/serialization/PinSerializer.js @@ -10,11 +10,15 @@ export default class PinSerializer extends GeneralSerializer { super(v => `${PinEntity.lookbehind} (${v})`, PinEntity, "", ",", true) } - writeValue(value, fullKey) { - if (value?.constructor === String && fullKey == "DefaultValue") { + /** + * @param {String[]} fullKey + * @param {Boolean} insideString + */ + writeValue(value, fullKey, insideString) { + if (value?.constructor === String && fullKey.length == 1 && fullKey[0] == "DefaultValue") { // @ts-expect-error return `"${Utility.encodeInputString(value)}"` } - return super.writeValue(value, fullKey) + return super.writeValue(value, fullKey, insideString) } } diff --git a/js/serialization/ToStringSerializer.js b/js/serialization/ToStringSerializer.js index 330f336..07ce93d 100755 --- a/js/serialization/ToStringSerializer.js +++ b/js/serialization/ToStringSerializer.js @@ -2,14 +2,30 @@ import GeneralSerializer from "./GeneralSerializer" +/** + * @typedef {import("../entity/IEntity").default} IEntity + */ + +/** + * @template {IEntity} T + */ export default class ToStringSerializer extends GeneralSerializer { + /** + * @param {new () => T} entityType + */ constructor(entityType) { super(undefined, entityType) } - write(object) { - let result = object.toString() + /** + * @param {T} object + * @param {Boolean} insideString + */ + write(object, insideString) { + let result = insideString || object.isShownAsString() + ? `"${object.toString().replaceAll('"', '\\"')}"` + : object.toString() return result } } diff --git a/js/serialization/initializeSerializerFactory.js b/js/serialization/initializeSerializerFactory.js index ce099ae..49606c5 100755 --- a/js/serialization/initializeSerializerFactory.js +++ b/js/serialization/initializeSerializerFactory.js @@ -8,6 +8,7 @@ import IdentifierEntity from "../entity/IdentifierEntity" import IntegerEntity from "../entity/IntegerEntity" import InvariantTextEntity from "../entity/InvariantTextEntity" import KeyBindingEntity from "../entity/KeyBindingEntity" +import LinearColorEntity from "../entity/LinearColorEntity" import LocalizedTextEntity from "../entity/LocalizedTextEntity" import ObjectEntity from "../entity/ObjectEntity" import ObjectReferenceEntity from "../entity/ObjectReferenceEntity" @@ -21,6 +22,13 @@ import ToStringSerializer from "./ToStringSerializer" export default function initializeSerializerFactory() { + const bracketsWrapped = v => `(${v})` + + SerializerFactory.registerSerializer( + LinearColorEntity, + new GeneralSerializer(bracketsWrapped, LinearColorEntity) + ) + SerializerFactory.registerSerializer( ObjectEntity, new ObjectSerializer() @@ -33,12 +41,12 @@ export default function initializeSerializerFactory() { SerializerFactory.registerSerializer( FunctionReferenceEntity, - new GeneralSerializer(v => `(${v})`, FunctionReferenceEntity, "", ",", false) + new GeneralSerializer(bracketsWrapped, FunctionReferenceEntity) ) SerializerFactory.registerSerializer( KeyBindingEntity, - new GeneralSerializer(v => `(${v})`, KeyBindingEntity, "", ",", false) + new GeneralSerializer(bracketsWrapped, KeyBindingEntity) ) SerializerFactory.registerSerializer( @@ -64,7 +72,9 @@ export default function initializeSerializerFactory() { objectReference.path ? objectReference.type ? `'"${objectReference.path}"'` : `"${objectReference.path}"` : "" - )) + ), + ObjectReferenceEntity + ) ) SerializerFactory.registerSerializer(IdentifierEntity, new ToStringSerializer(IdentifierEntity)) diff --git a/js/template/ColorPinTemplate.js b/js/template/ColorPinTemplate.js new file mode 100644 index 0000000..4f7c2c2 --- /dev/null +++ b/js/template/ColorPinTemplate.js @@ -0,0 +1,61 @@ +// @ts-check + +import html from "./html" +import IInputPinTemplate from "./IInputPinTemplate" + +/** + * @typedef {import("../element/PinElement").default} PinElement + */ + +export default class ColorPinTemplate extends IInputPinTemplate { + + /** @type {HTMLInputElement} */ + #input + + /** + * @param {PinElement} pin + */ + setup(pin) { + super.setup(pin) + this.#input = pin.querySelector(".ueb-pin-input") + let self = this + this.onChangeHandler = _ => pin.entity.DefaultValue = self.#input.checked ? "true" : "false" + this.#input.addEventListener("change", this.onChangeHandler) + } + + /** + * @param {PinElement} pin + */ + cleanup(pin) { + super.cleanup(pin) + this.#input.removeEventListener("change", this.onChangeHandler) + } + + /** + * @param {PinElement} pin + */ + getInputs(pin) { + return [this.#input.checked ? "true" : "false"] + } + + /** + * @param {PinElement} pin + * @param {String[]?} value + */ + setInputs(pin, value = []) { + pin.entity.DefaultValue = value.length ? value[0] : this.getInput(pin) + this.#input.checked = pin.entity.DefaultValue == "true" + } + + /** + * @param {PinElement} pin + */ + renderInput(pin) { + if (pin.isInput()) { + return html` + + ` + } + return super.renderInput(pin) + } +} diff --git a/package.json b/package.json index 81c19b2..b52f145 100755 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "terser": "^5.9.0" }, "dependencies": { + "@easylogic/colorpicker": "^1.10.11", "parsimmon": "^1.18.0" } }