mirror of
https://github.com/barsdeveloper/ueblueprint.git
synced 2026-02-03 23:55:04 +08:00
* Fix node reference when changing elements * Fix ScriptVariables parsing * Fix invariant text and niagara types * Niagara convert nodes * Move node tests to own files * More Niagara tests * Niagara float and smaller fixes * More Decoding * More decoding * WIP * Float is real * WIP * More types and colors * Test case and small polish * WIP * WIP * Fix niagara script variables merging * Fix Niagara variables * Fixing mirrored ExportPath * Fix Export paths name adjustments * Simplify arc calculation * Simplify a bit arc calculation * source / destionation => origin / target * Minor refactoring * Fix switched link position * Rename some properties for uniformity * Fix input escape * Simplify test * About window * Dialog backdrop style * About dialog touches * Remove dependency and minot improvement * Light mode * Fix link location and css small improvement * Link direction and minor fixes * Some minor fixes and refactoring * Refactoring WIP * Shorting repetitive bits * More tests * Simplify linking tests
439 lines
14 KiB
JavaScript
Executable File
439 lines
14 KiB
JavaScript
Executable File
import P from "parsernostrum"
|
|
import Utility from "../Utility.js"
|
|
|
|
/** @abstract */
|
|
export default class IEntity {
|
|
|
|
/** @type {(v: String) => String} */
|
|
static same = v => v
|
|
/** @type {(entity: IEntity, serialized: String) => String} */
|
|
static notWrapped = (entity, serialized) => serialized
|
|
/** @type {(entity: IEntity, serialized: String) => String} */
|
|
static defaultWrapped = (entity, serialized) => `${entity.#lookbehind}(${serialized})`
|
|
static wrap = this.defaultWrapped
|
|
static attributeSeparator = ","
|
|
static keySeparator = "="
|
|
/** @type {(k: String) => String} */
|
|
static printKey = k => k
|
|
static grammar = P.lazy(() => this.createGrammar())
|
|
/** @type {P<IEntity>} */
|
|
static unknownEntityGrammar
|
|
static unknownEntity
|
|
/** @type {{ [key: String]: typeof IEntity }} */
|
|
static attributes = {}
|
|
/** @type {String | String[]} */
|
|
static lookbehind = ""
|
|
/** @type {(type: typeof IEntity) => InstanceType<typeof IEntity>} */
|
|
static default
|
|
static nullable = false
|
|
static ignored = false // Never serialize or deserialize
|
|
static serialized = false // Value is written and read as string
|
|
static expected = false // Must be there
|
|
static inlined = false // The key is a subobject or array and printed as inlined (A.B=123, A(0)=123)
|
|
/** @type {Boolean} */
|
|
static quoted // Key is serialized with quotes
|
|
static silent = false // Do not serialize if default
|
|
static trailing = false // Add attribute separator after the last attribute when serializing
|
|
|
|
/** @type {String[]} */
|
|
#keys
|
|
get keys() {
|
|
return this.#keys ?? Object.keys(this)
|
|
}
|
|
set keys(value) {
|
|
this.#keys = [... new Set(value)]
|
|
}
|
|
|
|
// @ts-expect-error
|
|
#lookbehind = /** @type {String} */(this.constructor.lookbehind)
|
|
get lookbehind() {
|
|
return this.#lookbehind.trim()
|
|
}
|
|
set lookbehind(value) {
|
|
this.#lookbehind = value
|
|
}
|
|
|
|
#ignored = /** @type {typeof IEntity} */(this.constructor).ignored
|
|
get ignored() {
|
|
return this.#ignored
|
|
}
|
|
set ignored(value) {
|
|
this.#ignored = value
|
|
}
|
|
|
|
#inlined = /** @type {typeof IEntity} */(this.constructor).inlined
|
|
get inlined() {
|
|
return this.#inlined
|
|
}
|
|
set inlined(value) {
|
|
this.#inlined = value
|
|
}
|
|
|
|
#quoted
|
|
get quoted() {
|
|
return this.#quoted ?? /** @type {typeof IEntity} */(this.constructor).quoted ?? false
|
|
}
|
|
set quoted(value) {
|
|
this.#quoted = value
|
|
}
|
|
|
|
/** @type {Boolean} */
|
|
#trailing
|
|
get trailing() {
|
|
return this.#trailing ?? /** @type {typeof IEntity} */(this.constructor).trailing ?? false
|
|
}
|
|
set trailing(value) {
|
|
this.#trailing = value
|
|
}
|
|
|
|
constructor(values = {}) {
|
|
const attributes = /** @type {typeof IEntity} */(this.constructor).attributes
|
|
const keys = Utility.mergeArrays(
|
|
Object.keys(values),
|
|
Object.entries(attributes).filter(([k, v]) => v.default !== undefined).map(([k, v]) => k)
|
|
)
|
|
for (const key of keys) {
|
|
if (values[key] !== undefined) {
|
|
if (values[key].constructor === Object) {
|
|
// It is part of a nested key (words separated by ".")
|
|
values[key] = new (
|
|
attributes[key] !== undefined ? attributes[key] : IEntity.unknownEntity
|
|
)(values[key])
|
|
}
|
|
const computedEntity = /** @type {ComputedTypeEntityConstructor} */(attributes[key])
|
|
this[key] = values[key]
|
|
if (computedEntity?.compute) {
|
|
/** @type {typeof IEntity} */
|
|
const actualEntity = computedEntity.compute(this)
|
|
const parsed = actualEntity.grammar.run(values[key].toString())
|
|
if (parsed.status) {
|
|
this[key] = parsed.value
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
const attribute = attributes[key]
|
|
if (attribute.default !== undefined) {
|
|
this[key] = attribute.default(attribute)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @protected
|
|
* @returns {P<IEntity>}
|
|
*/
|
|
static createGrammar() {
|
|
return this.unknownEntityGrammar
|
|
}
|
|
|
|
static actualClass() {
|
|
let self = this
|
|
while (!self.name) {
|
|
self = Object.getPrototypeOf(self)
|
|
}
|
|
return self
|
|
}
|
|
|
|
static className() {
|
|
return this.actualClass().name
|
|
}
|
|
|
|
/**
|
|
* @protected
|
|
* @template {typeof IEntity} T
|
|
* @this {T}
|
|
* @returns {T}
|
|
*/
|
|
static asUniqueClass(alwaysCreate = false) {
|
|
let result = this
|
|
if (this.name.length || alwaysCreate) {
|
|
// @ts-expect-error
|
|
result = (() => class extends this { })() // Comes from a lambda otherwise the class will have name "result"
|
|
result.grammar = result.createGrammar() // Reassign grammar to capture the correct this from subclass
|
|
|
|
}
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @template {typeof IEntity} T
|
|
* @this {T}
|
|
* @param {String} value
|
|
*/
|
|
static withLookbehind(value) {
|
|
const result = this.asUniqueClass()
|
|
result.lookbehind = value
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @template {typeof IEntity} T
|
|
* @this {T}
|
|
* @param {(type: T) => (InstanceType<T> | NullEntity)} value
|
|
* @returns {T}
|
|
*/
|
|
static withDefault(value = type => new type()) {
|
|
const result = this.asUniqueClass()
|
|
result.default = value
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @template {typeof IEntity} T
|
|
* @this {T}
|
|
*/
|
|
static flagNullable(value = true) {
|
|
const result = this.asUniqueClass()
|
|
result.nullable = value
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @template {typeof IEntity} T
|
|
* @this {T}
|
|
*/
|
|
static flagIgnored(value = true) {
|
|
const result = this.asUniqueClass()
|
|
result.ignored = value
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @template {typeof IEntity} T
|
|
* @this {T}
|
|
*/
|
|
static flagSerialized(value = true) {
|
|
const result = this.asUniqueClass()
|
|
result.serialized = value
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @template {typeof IEntity} T
|
|
* @this {T}
|
|
*/
|
|
static flagInlined(value = true) {
|
|
const result = this.asUniqueClass()
|
|
result.inlined = value
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @template {typeof IEntity} T
|
|
* @this {T}
|
|
*/
|
|
static flagQuoted(value = true) {
|
|
const result = this.asUniqueClass()
|
|
result.quoted = value
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @template {typeof IEntity} T
|
|
* @this {T}
|
|
*/
|
|
static flagSilent(value = true) {
|
|
const result = this.asUniqueClass()
|
|
result.silent = value
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @template {typeof IEntity} T
|
|
* @this {T}
|
|
*/
|
|
static flagTrailing(value = true) {
|
|
const result = this.asUniqueClass()
|
|
result.trailing = value
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @protected
|
|
* @param {String} string
|
|
*/
|
|
static asSerializedString(string) {
|
|
return `"${string.replaceAll(/(?<=(?:[^\\]|^)(?:\\\\)*?)"/g, '\\"')}"`
|
|
}
|
|
|
|
/** @param {String} key */
|
|
showProperty(key) {
|
|
/** @type {IEntity} */
|
|
let value = this[key]
|
|
const valueType = /** @type {typeof IEntity} */(value.constructor)
|
|
if (valueType.silent && valueType.default !== undefined) {
|
|
if (valueType["#default"] === undefined) {
|
|
valueType["#default"] = valueType.default(valueType)
|
|
}
|
|
const defaultValue = valueType["#default"]
|
|
return !value.equals(defaultValue)
|
|
}
|
|
return true
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {String} attributeName
|
|
* @param {(v: any) => void} callback
|
|
*/
|
|
listenAttribute(attributeName, callback) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(this, attributeName)
|
|
const setter = descriptor.set
|
|
if (setter) {
|
|
descriptor.set = v => {
|
|
setter(v)
|
|
callback(v)
|
|
}
|
|
Object.defineProperties(this, { [attributeName]: descriptor })
|
|
} else if (descriptor.value) {
|
|
|
|
Object.defineProperties(this, {
|
|
["#" + attributeName]: {
|
|
value: descriptor.value,
|
|
writable: true,
|
|
enumerable: false,
|
|
},
|
|
[attributeName]: {
|
|
enumerable: true,
|
|
get() {
|
|
return this["#" + attributeName]
|
|
},
|
|
set(v) {
|
|
callback(v)
|
|
this["#" + attributeName] = v
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
/** @this {IEntity | Array} */
|
|
doSerialize(
|
|
insideString = false,
|
|
indentation = "",
|
|
Self = /** @type {typeof IEntity} */(this.constructor),
|
|
printKey = Self.printKey,
|
|
keySeparator = Self.keySeparator,
|
|
attributeSeparator = Self.attributeSeparator,
|
|
wrap = Self.wrap,
|
|
) {
|
|
const isSelfOverriden = Self !== this.constructor
|
|
let result = ""
|
|
let first = true
|
|
const keys = this instanceof IEntity ? this.keys : Object.keys(this)
|
|
for (const key of keys) {
|
|
/** @type {IEntity} */
|
|
const value = this[key]
|
|
const valueType = /** @type {typeof IEntity} */(value?.constructor)
|
|
if (value === undefined || this instanceof IEntity && !this.showProperty(key)) {
|
|
continue
|
|
}
|
|
if (first) {
|
|
first = false
|
|
} else {
|
|
result += attributeSeparator
|
|
}
|
|
let keyValue = this instanceof Array ? `(${key})` : key
|
|
if (keyValue.length && (Self.attributes[key]?.quoted || value.quoted)) {
|
|
keyValue = `"${keyValue}"`
|
|
}
|
|
if (value.inlined) {
|
|
const inlinedPrintKey = valueType.className() === "ArrayEntity"
|
|
? k => printKey(`${keyValue}${k}`)
|
|
: k => printKey(`${keyValue}.${k}`)
|
|
result += value.serialize(
|
|
insideString,
|
|
indentation,
|
|
undefined,
|
|
inlinedPrintKey,
|
|
keySeparator,
|
|
attributeSeparator,
|
|
Self.notWrapped
|
|
)
|
|
continue
|
|
}
|
|
keyValue = printKey(keyValue)
|
|
if (keyValue.length) {
|
|
result += (attributeSeparator.includes("\n") ? indentation : "") + keyValue + keySeparator
|
|
}
|
|
let serialization = value?.serialize(insideString, indentation)
|
|
result += serialization
|
|
}
|
|
if (this instanceof IEntity && (isSelfOverriden && Self.trailing || this.trailing) && result.length) {
|
|
result += attributeSeparator
|
|
}
|
|
return wrap(/** @type {IEntity} */(this), result)
|
|
}
|
|
|
|
/** @this {IEntity | Array} */
|
|
serialize(
|
|
insideString = false,
|
|
indentation = "",
|
|
Self = /** @type {typeof IEntity} */(this.constructor),
|
|
printKey = Self.printKey,
|
|
keySeparator = Self.keySeparator,
|
|
attributeSeparator = Self.attributeSeparator,
|
|
wrap = Self.wrap,
|
|
) {
|
|
const isSelfOverriden = Self !== this.constructor
|
|
let result = this instanceof Array
|
|
? IEntity.prototype.doSerialize.bind(this)(insideString, indentation, Self, printKey, keySeparator, attributeSeparator, wrap)
|
|
: this.doSerialize(insideString, indentation, Self, printKey, keySeparator, attributeSeparator, wrap)
|
|
if (Self.serialized) {
|
|
result = IEntity.asSerializedString(result)
|
|
}
|
|
return result
|
|
}
|
|
|
|
equals(other) {
|
|
if (!(other instanceof IEntity)) {
|
|
return false
|
|
}
|
|
const thisKeys = Object.keys(this)
|
|
const otherKeys = Object.keys(other)
|
|
const thisType = /** @type {typeof IEntity} */(this.constructor).actualClass()
|
|
const otherType = /** @type {typeof IEntity} */(other.constructor).actualClass()
|
|
if (
|
|
thisKeys.length !== otherKeys.length
|
|
|| this.lookbehind != other.lookbehind
|
|
|| !(other instanceof thisType) && !(this instanceof otherType)
|
|
) {
|
|
return false
|
|
}
|
|
for (let i = 0; i < thisKeys.length; ++i) {
|
|
const k = thisKeys[i]
|
|
if (!otherKeys.includes(k)) {
|
|
return false
|
|
}
|
|
const a = this[k]
|
|
const b = other[k]
|
|
if (a instanceof IEntity) {
|
|
if (!a.equals(b)) {
|
|
return false
|
|
}
|
|
} else if (a instanceof Array && b instanceof Array) {
|
|
if (a.length !== b.length) {
|
|
return false
|
|
}
|
|
for (let j = 0; j < a.length; ++j) {
|
|
if (!(a[j] instanceof IEntity && a[j].equals(b[j])) && a[j] !== b[j]) {
|
|
return false
|
|
}
|
|
}
|
|
} else {
|
|
if (a !== b) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
/** @returns {IEntity | Boolean | Number | String | BigInt | (IEntity | Boolean | Number | String | BigInt)[]} */
|
|
valueOf() {
|
|
return this
|
|
}
|
|
}
|