/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
const t$2=window,e$3=t$2.ShadowRoot&&(void 0===t$2.ShadyCSS||t$2.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s$3=Symbol(),n$4=new WeakMap;class o$4{constructor(t,e,n){if(this._$cssResult$=!0,n!==s$3)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e;}get styleSheet(){let t=this.o;const s=this.t;if(e$3&&void 0===t){const e=void 0!==s&&1===s.length;e&&(t=n$4.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&n$4.set(s,t));}return t}toString(){return this.cssText}}const r$2=t=>new o$4("string"==typeof t?t:t+"",void 0,s$3),i$3=(t,...e)=>{const n=1===t.length?t[0]:e.reduce(((e,s,n)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+t[n+1]),t[0]);return new o$4(n,t,s$3)},S$1=(s,n)=>{e$3?s.adoptedStyleSheets=n.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):n.forEach((e=>{const n=document.createElement("style"),o=t$2.litNonce;void 0!==o&&n.setAttribute("nonce",o),n.textContent=e.cssText,s.appendChild(n);}));},c$1=e$3?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const s of t.cssRules)e+=s.cssText;return r$2(e)})(t):t;
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/var s$2;const e$2=window,r$1=e$2.trustedTypes,h$1=r$1?r$1.emptyScript:"",o$3=e$2.reactiveElementPolyfillSupport,n$3={toAttribute(t,i){switch(i){case Boolean:t=t?h$1:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t);}return t},fromAttribute(t,i){let s=t;switch(i){case Boolean:s=null!==t;break;case Number:s=null===t?null:Number(t);break;case Object:case Array:try{s=JSON.parse(t);}catch(t){s=null;}}return s}},a$1=(t,i)=>i!==t&&(i==i||t==t),l$2={attribute:!0,type:String,converter:n$3,reflect:!1,hasChanged:a$1},d$1="finalized";class u$1 extends HTMLElement{constructor(){super(),this._$Ei=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$El=null,this._$Eu();}static addInitializer(t){var i;this.finalize(),(null!==(i=this.h)&&void 0!==i?i:this.h=[]).push(t);}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((i,s)=>{const e=this._$Ep(s,i);void 0!==e&&(this._$Ev.set(e,s),t.push(e));})),t}static createProperty(t,i=l$2){if(i.state&&(i.attribute=!1),this.finalize(),this.elementProperties.set(t,i),!i.noAccessor&&!this.prototype.hasOwnProperty(t)){const s="symbol"==typeof t?Symbol():"__"+t,e=this.getPropertyDescriptor(t,s,i);void 0!==e&&Object.defineProperty(this.prototype,t,e);}}static getPropertyDescriptor(t,i,s){return {get(){return this[i]},set(e){const r=this[t];this[i]=e,this.requestUpdate(t,r,s);},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||l$2}static finalize(){if(this.hasOwnProperty(d$1))return !1;this[d$1]=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),void 0!==t.h&&(this.h=[...t.h]),this.elementProperties=new Map(t.elementProperties),this._$Ev=new Map,this.hasOwnProperty("properties")){const t=this.properties,i=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const s of i)this.createProperty(s,t[s]);}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(i){const s=[];if(Array.isArray(i)){const e=new Set(i.flat(1/0).reverse());for(const i of e)s.unshift(c$1(i));}else void 0!==i&&s.push(c$1(i));return s}static _$Ep(t,i){const s=i.attribute;return !1===s?void 0:"string"==typeof s?s:"string"==typeof t?t.toLowerCase():void 0}_$Eu(){var t;this._$E_=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$Eg(),this.requestUpdate(),null===(t=this.constructor.h)||void 0===t||t.forEach((t=>t(this)));}addController(t){var i,s;(null!==(i=this._$ES)&&void 0!==i?i:this._$ES=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(s=t.hostConnected)||void 0===s||s.call(t));}removeController(t){var i;null===(i=this._$ES)||void 0===i||i.splice(this._$ES.indexOf(t)>>>0,1);}_$Eg(){this.constructor.elementProperties.forEach(((t,i)=>{this.hasOwnProperty(i)&&(this._$Ei.set(i,this[i]),delete this[i]);}));}createRenderRoot(){var t;const s=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return S$1(s,this.constructor.elementStyles),s}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$ES)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostConnected)||void 0===i?void 0:i.call(t)}));}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$ES)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostDisconnected)||void 0===i?void 0:i.call(t)}));}attributeChangedCallback(t,i,s){this._$AK(t,s);}_$EO(t,i,s=l$2){var e;const r=this.constructor._$Ep(t,s);if(void 0!==r&&!0===s.reflect){const h=(void 0!==(null===(e=s.converter)||void 0===e?void 0:e.toAttribute)?s.converter:n$3).toAttribute(i,s.type);this._$El=t,null==h?this.removeAttribute(r):this.setAttribute(r,h),this._$El=null;}}_$AK(t,i){var s;const e=this.constructor,r=e._$Ev.get(t);if(void 0!==r&&this._$El!==r){const t=e.getPropertyOptions(r),h="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==(null===(s=t.converter)||void 0===s?void 0:s.fromAttribute)?t.converter:n$3;this._$El=r,this[r]=h.fromAttribute(i,t.type),this._$El=null;}}requestUpdate(t,i,s){let e=!0;void 0!==t&&(((s=s||this.constructor.getPropertyOptions(t)).hasChanged||a$1)(this[t],i)?(this._$AL.has(t)||this._$AL.set(t,i),!0===s.reflect&&this._$El!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,s))):e=!1),!this.isUpdatePending&&e&&(this._$E_=this._$Ej());}async _$Ej(){this.isUpdatePending=!0;try{await this._$E_;}catch(t){Promise.reject(t);}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Ei&&(this._$Ei.forEach(((t,i)=>this[i]=t)),this._$Ei=void 0);let i=!1;const s=this._$AL;try{i=this.shouldUpdate(s),i?(this.willUpdate(s),null===(t=this._$ES)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostUpdate)||void 0===i?void 0:i.call(t)})),this.update(s)):this._$Ek();}catch(t){throw i=!1,this._$Ek(),t}i&&this._$AE(s);}willUpdate(t){}_$AE(t){var i;null===(i=this._$ES)||void 0===i||i.forEach((t=>{var i;return null===(i=t.hostUpdated)||void 0===i?void 0:i.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t);}_$Ek(){this._$AL=new Map,this.isUpdatePending=!1;}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$E_}shouldUpdate(t){return !0}update(t){void 0!==this._$EC&&(this._$EC.forEach(((t,i)=>this._$EO(i,this[i],t))),this._$EC=void 0),this._$Ek();}updated(t){}firstUpdated(t){}}u$1[d$1]=!0,u$1.elementProperties=new Map,u$1.elementStyles=[],u$1.shadowRootOptions={mode:"open"},null==o$3||o$3({ReactiveElement:u$1}),(null!==(s$2=e$2.reactiveElementVersions)&&void 0!==s$2?s$2:e$2.reactiveElementVersions=[]).push("1.6.3");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
var t$1;const i$2=window,s$1=i$2.trustedTypes,e$1=s$1?s$1.createPolicy("lit-html",{createHTML:t=>t}):void 0,o$2="$lit$",n$2=`lit$${(Math.random()+"").slice(9)}$`,l$1="?"+n$2,h=`<${l$1}>`,r=document,u=()=>r.createComment(""),d=t=>null===t||"object"!=typeof t&&"function"!=typeof t,c=Array.isArray,v=t=>c(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator]),a="[ \t\n\f\r]",f=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,_=/-->/g,m=/>/g,p=RegExp(`>|${a}(?:([^\\s"'>=/]+)(${a}*=${a}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),g=/'/g,$=/"/g,y=/^(?:script|style|textarea|title)$/i,w=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),x=w(1),T=Symbol.for("lit-noChange"),A=Symbol.for("lit-nothing"),E=new WeakMap,C=r.createTreeWalker(r,129,null,!1);function P(t,i){if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==e$1?e$1.createHTML(i):i}const V=(t,i)=>{const s=t.length-1,e=[];let l,r=2===i?"":"")),e]};class N{constructor({strings:t,_$litType$:i},e){let h;this.parts=[];let r=0,d=0;const c=t.length-1,v=this.parts,[a,f]=V(t,i);if(this.el=N.createElement(a,e),C.currentNode=this.el.content,2===i){const t=this.el.content,i=t.firstChild;i.remove(),t.append(...i.childNodes);}for(;null!==(h=C.nextNode())&&v.length0){h.textContent=s$1?s$1.emptyScript:"";for(let s=0;s2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=A;}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,i=this,s,e){const o=this.strings;let n=!1;if(void 0===o)t=S(this,t,i,0),n=!d(t)||t!==this._$AH&&t!==T,n&&(this._$AH=t);else {const e=t;let l,h;for(t=o[0],l=0;l{var e,o;const n=null!==(e=null==s?void 0:s.renderBefore)&&void 0!==e?e:i;let l=n._$litPart$;if(void 0===l){const t=null!==(o=null==s?void 0:s.renderBefore)&&void 0!==o?o:null;n._$litPart$=l=new R(i.insertBefore(u(),t),t,void 0,null!=s?s:{});}return l._$AI(t),l};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/var l,o$1;class s extends u$1{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0;}createRenderRoot(){var t,e;const i=super.createRenderRoot();return null!==(t=(e=this.renderOptions).renderBefore)&&void 0!==t||(e.renderBefore=i.firstChild),i}update(t){const i=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=D(i,this.renderRoot,this.renderOptions);}connectedCallback(){var t;super.connectedCallback(),null===(t=this._$Do)||void 0===t||t.setConnected(!0);}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this._$Do)||void 0===t||t.setConnected(!1);}render(){return T}}s.finalized=!0,s._$litElement$=!0,null===(l=globalThis.litElementHydrateSupport)||void 0===l||l.call(globalThis,{LitElement:s});const n$1=globalThis.litElementPolyfillSupport;null==n$1||n$1({LitElement:s});(null!==(o$1=globalThis.litElementVersions)&&void 0!==o$1?o$1:globalThis.litElementVersions=[]).push("3.3.3");
class Configuration {
static nodeColors = {
black: i$3`20, 20, 20`,
blue: i$3`84, 122, 156`,
darkBlue: i$3`32, 80, 128`,
darkerBlue: i$3`18, 18, 130`,
darkTurquoise: i$3`19, 100, 137`,
gray: i$3`150,150,150`,
green: i$3`95, 129, 90`,
intenseGreen: i$3`42, 140, 42`,
lime: i$3`150, 160, 30`,
red: i$3`151, 33, 32`,
turquoise: i$3`46, 104, 106`,
violet: i$3`126, 28, 150`,
yellow: i$3`148, 116, 24`,
}
static alphaPattern = "repeating-conic-gradient(#7c8184 0% 25%, #c2c3c4 0% 50%) 50% / 10px 10px"
static colorDragEventName = "ueb-color-drag"
static colorPickEventName = "ueb-color-pick"
static colorWindowEventName = "ueb-color-window"
static colorWindowName = "Color Picker"
static defaultCommentHeight = 96
static defaultCommentWidth = 400
static distanceThreshold = 5 // px
static dragEventName = "ueb-drag"
static dragGeneralEventName = "ueb-drag-general"
static edgeScrollThreshold = 50
static editTextEventName = {
begin: "ueb-edit-text-begin",
end: "ueb-edit-text-end",
}
static expandGridSize = 400
static focusEventName = {
begin: "blueprint-focus",
end: "blueprint-unfocus",
}
static fontSize = i$3`13px`
static gridAxisLineColor = i$3`black`
static gridExpandThreshold = 0.25 // remaining size factor threshold to cause an expansion event
static gridLineColor = i$3`#353535`
static gridLineWidth = 1 // px
static gridSet = 8
static gridSetLineColor = i$3`#161616`
static gridShrinkThreshold = 4 // exceding size factor threshold to cause a shrink event
static gridSize = 16 // px
static hexColorRegex = /^\s*#(?[0-9a-fA-F]{2})(?[0-9a-fA-F]{2})(?[0-9a-fA-F]{2})([0-9a-fA-F]{2})?|#(?[0-9a-fA-F])(?[0-9a-fA-F])(?[0-9a-fA-F])\s*$/
static indentation = " "
static keysSeparator = /[\.\(\)]/
static knotOffset = [-Configuration.gridSize, -0.5 * Configuration.gridSize]
static lineTracePattern = /LineTrace(Single|Multi)(\w*)/
static linkCurveHeight = 15 // px
static linkCurveWidth = 80 // px
static linkMinWidth = 100 // px
static nameRegexSpaceReplacement = new RegExp(
// Leading K2_ or K2Node_ is removed
"^K2(?:[Nn]ode)?_"
// End of a word (lower case followed by either upper case or number): "AlphaBravo" => "Alpha Bravo"
+ "|(?<=[a-z])(?=[A-Z0-9])"
// End of upper case word (upper case followed by either word or number)
+ "|(?<=[A-Z])"
+ /* Except "UVs" */ "(? "Alpha Bravo"
+ "|\\s*_+\\s*"
+ "|\\s{2,}",
"g"
)
/**
* @param {Number} start
* @param {Number} c1
* @param {Number} c2
*/
static linkRightSVGPath = (start, c1, c2) => {
let end = 100 - start;
return `M ${start} 0 C ${c1.toFixed(3)} 0, ${c2.toFixed(3)} 0, 50 50 S ${(end - c1 + start).toFixed(3)} 100, `
+ `${end.toFixed(3)} 100`
}
static maxZoom = 7
static minZoom = -12
static mouseClickButton = 0
static mouseRightClickButton = 2
static mouseWheelZoomThreshold = 80
static nodeDragEventName = "ueb-node-drag"
static nodeDragGeneralEventName = "ueb-node-drag-general"
static nodeTitle = (name, counter) => `${name}_${counter}`
static nodeRadius = 8 // px
static nodeReflowEventName = "ueb-node-reflow"
static paths = {
actorBoundEvent: "/Script/BlueprintGraph.K2Node_ActorBoundEvent",
addDelegate: "/Script/BlueprintGraph.K2Node_AddDelegate",
ambientSound: "/Script/Engine.AmbientSound",
asyncAction: "/Script/BlueprintGraph.K2Node_AsyncAction",
blueprint: "/Script/Engine.Blueprint",
blueprintGameplayTagLibrary: "/Script/GameplayTags.BlueprintGameplayTagLibrary",
blueprintMapLibrary: "/Script/Engine.BlueprintMapLibrary",
blueprintSetLibrary: "/Script/Engine.BlueprintSetLibrary",
callArrayFunction: "/Script/BlueprintGraph.K2Node_CallArrayFunction",
callDelegate: "/Script/BlueprintGraph.K2Node_CallDelegate",
callFunction: "/Script/BlueprintGraph.K2Node_CallFunction",
comment: "/Script/UnrealEd.EdGraphNode_Comment",
commutativeAssociativeBinaryOperator: "/Script/BlueprintGraph.K2Node_CommutativeAssociativeBinaryOperator",
componentBoundEvent: "/Script/BlueprintGraph.K2Node_ComponentBoundEvent",
createDelegate: "/Script/BlueprintGraph.K2Node_CreateDelegate",
customEvent: "/Script/BlueprintGraph.K2Node_CustomEvent",
doN: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:Do N",
doOnce: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:DoOnce",
dynamicCast: "/Script/BlueprintGraph.K2Node_DynamicCast",
eAttachmentRule: "/Script/Engine.EAttachmentRule",
edGraph: "/Script/Engine.EdGraph",
eDrawDebugTrace: "/Script/Engine.EDrawDebugTrace",
eMaterialSamplerType: "/Script/Engine.EMaterialSamplerType",
eNiagara_Float4Channel: "/Niagara/Enums/ENiagara_Float4Channel.ENiagara_Float4Channel",
enum: "/Script/CoreUObject.Enum",
enumLiteral: "/Script/BlueprintGraph.K2Node_EnumLiteral",
eSamplerSourceMode: "/Script/Engine.ESamplerSourceMode",
eSearchCase: "/Script/CoreUObject.ESearchCase",
eSearchDir: "/Script/CoreUObject.ESearchDir",
eSpawnActorCollisionHandlingMethod: "/Script/Engine.ESpawnActorCollisionHandlingMethod",
eTextureMipValueMode: "/Script/Engine.ETextureMipValueMode",
eTraceTypeQuery: "/Script/Engine.ETraceTypeQuery",
event: "/Script/BlueprintGraph.K2Node_Event",
eWorldPositionIncludedOffsets: "/Script/Engine.EWorldPositionIncludedOffsets",
executionSequence: "/Script/BlueprintGraph.K2Node_ExecutionSequence",
flipflop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:FlipFlop",
forEachElementInEnum: "/Script/BlueprintGraph.K2Node_ForEachElementInEnum",
forEachLoop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:ForEachLoop",
forEachLoopWithBreak: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:ForEachLoopWithBreak",
forLoop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:ForLoop",
forLoopWithBreak: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:ForLoopWithBreak",
functionEntry: "/Script/BlueprintGraph.K2Node_FunctionEntry",
functionResult: "/Script/BlueprintGraph.K2Node_FunctionResult",
gameplayTag: "/Script/GameplayTags.GameplayTag",
getInputAxisKeyValue: "/Script/BlueprintGraph.K2Node_GetInputAxisKeyValue",
ifThenElse: "/Script/BlueprintGraph.K2Node_IfThenElse",
inputAxisKeyEvent: "/Script/BlueprintGraph.K2Node_InputAxisKeyEvent",
inputDebugKey: "/Script/InputBlueprintNodes.K2Node_InputDebugKey",
inputKey: "/Script/BlueprintGraph.K2Node_InputKey",
inputVectorAxisEvent: "/Script/BlueprintGraph.K2Node_InputVectorAxisEvent",
isValid: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:IsValid",
kismetArrayLibrary: "/Script/Engine.KismetArrayLibrary",
kismetMathLibrary: "/Script/Engine.KismetMathLibrary",
knot: "/Script/BlueprintGraph.K2Node_Knot",
linearColor: "/Script/CoreUObject.LinearColor",
literal: "/Script/BlueprintGraph.K2Node_Literal",
macro: "/Script/BlueprintGraph.K2Node_MacroInstance",
makeArray: "/Script/BlueprintGraph.K2Node_MakeArray",
makeMap: "/Script/BlueprintGraph.K2Node_MakeMap",
makeSet: "/Script/BlueprintGraph.K2Node_MakeSet",
makeStruct: "/Script/BlueprintGraph.K2Node_MakeStruct",
materialExpressionComponentMask: "/Script/Engine.MaterialExpressionComponentMask",
materialExpressionConstant: "/Script/Engine.MaterialExpressionConstant",
materialExpressionConstant2Vector: "/Script/Engine.MaterialExpressionConstant2Vector",
materialExpressionConstant3Vector: "/Script/Engine.MaterialExpressionConstant3Vector",
materialExpressionConstant4Vector: "/Script/Engine.MaterialExpressionConstant4Vector",
materialExpressionFunctionInput: "/Script/Engine.MaterialExpressionFunctionInput",
materialExpressionLogarithm: "/Script/InterchangeImport.MaterialExpressionLogarithm",
materialExpressionLogarithm10: "/Script/Engine.MaterialExpressionLogarithm10",
materialExpressionLogarithm2: "/Script/Engine.MaterialExpressionLogarithm2",
materialExpressionMaterialFunctionCall: "/Script/Engine.MaterialExpressionMaterialFunctionCall",
materialExpressionSquareRoot: "/Script/Engine.MaterialExpressionSquareRoot",
materialExpressionSubtract: "/Script/Engine.MaterialExpressionSubtract",
materialExpressionTextureCoordinate: "/Script/Engine.MaterialExpressionTextureCoordinate",
materialExpressionTextureSample: "/Script/Engine.MaterialExpressionTextureSample",
materialExpressionWorldPosition: "/Script/Engine.MaterialExpressionWorldPosition",
materialGraphNode: "/Script/UnrealEd.MaterialGraphNode",
materialGraphNodeComment: "/Script/UnrealEd.MaterialGraphNode_Comment",
metasoundEditorGraphExternalNode: "/Script/MetasoundEditor.MetasoundEditorGraphExternalNode",
multiGate: "/Script/BlueprintGraph.K2Node_MultiGate",
niagaraBool: "/Script/Niagara.NiagaraBool",
niagaraClipboardContent: "/Script/NiagaraEditor.NiagaraClipboardContent",
niagaraDataInterfaceVolumeTexture: "/Script/Niagara.NiagaraDataInterfaceVolumeTexture",
niagaraFloat: "/Script/Niagara.NiagaraFloat",
niagaraMatrix: "/Script/Niagara.NiagaraMatrix",
niagaraNodeFunctionCall: "/Script/NiagaraEditor.NiagaraNodeFunctionCall",
niagaraNodeOp: "/Script/NiagaraEditor.NiagaraNodeOp",
niagaraNumeric: "/Script/Niagara.NiagaraNumeric",
niagaraPosition: "/Script/Niagara.NiagaraPosition",
pawn: "/Script/Engine.Pawn",
pcgEditorGraphNode: "/Script/PCGEditor.PCGEditorGraphNode",
pcgEditorGraphNodeInput: "/Script/PCGEditor.PCGEditorGraphNodeInput",
pcgEditorGraphNodeOutput: "/Script/PCGEditor.PCGEditorGraphNodeOutput",
pcgHiGenGridSizeSettings: "/Script/PCG.PCGHiGenGridSizeSettings",
pcgSubgraphSettings: "/Script/PCG.PCGSubgraphSettings",
promotableOperator: "/Script/BlueprintGraph.K2Node_PromotableOperator",
quat4f: "/Script/CoreUObject.Quat4f",
reverseForEachLoop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:ReverseForEachLoop",
rotator: "/Script/CoreUObject.Rotator",
select: "/Script/BlueprintGraph.K2Node_Select",
self: "/Script/BlueprintGraph.K2Node_Self",
slateBlueprintLibrary: "/Script/UMG.SlateBlueprintLibrary",
spawnActorFromClass: "/Script/BlueprintGraph.K2Node_SpawnActorFromClass",
switchEnum: "/Script/BlueprintGraph.K2Node_SwitchEnum",
switchGameplayTag: "/Script/GameplayTagsEditor.GameplayTagsK2Node_SwitchGameplayTag",
switchInteger: "/Script/BlueprintGraph.K2Node_SwitchInteger",
switchName: "/Script/BlueprintGraph.K2Node_SwitchName",
switchString: "/Script/BlueprintGraph.K2Node_SwitchString",
timeline: "/Script/BlueprintGraph.K2Node_Timeline",
timeManagementBlueprintLibrary: "/Script/TimeManagement.TimeManagementBlueprintLibrary",
transform: "/Script/CoreUObject.Transform",
userDefinedEnum: "/Script/Engine.UserDefinedEnum",
variableGet: "/Script/BlueprintGraph.K2Node_VariableGet",
variableSet: "/Script/BlueprintGraph.K2Node_VariableSet",
vector: "/Script/CoreUObject.Vector",
vector2D: "/Script/CoreUObject.Vector2D",
vector3f: "/Script/CoreUObject.Vector3f",
vector4f: "/Script/CoreUObject.Vector4f",
whileLoop: "/Engine/EditorBlueprintResources/StandardMacros.StandardMacros:WhileLoop",
}
static pinInputWrapWidth = 145 // px
static removeEventName = "ueb-element-delete"
static scale = {
[-12]: 0.133333,
[-11]: 0.166666,
[-10]: 0.2,
[-9]: 0.233333,
[-8]: 0.266666,
[-7]: 0.3,
[-6]: 0.333333,
[-5]: 0.375,
[-4]: 0.5,
[-3]: 0.675,
[-2]: 0.75,
[-1]: 0.875,
0: 1,
1: 1.25,
2: 1.375,
3: 1.5,
4: 1.675,
5: 1.75,
6: 1.875,
7: 2,
}
static smoothScrollTime = 1000 // ms
static stringEscapedCharacters = /["\\]/g // Try to remove
static subObjectAttributeNamePrefix = "#SubObject"
/** @param {ObjectEntity} objectEntity */
static subObjectAttributeNameFromEntity = (objectEntity, nameOnly = false) =>
this.subObjectAttributeNamePrefix + (!nameOnly && objectEntity.Class ? `_${objectEntity.Class.type}` : "")
+ "_" + objectEntity.Name
/** @param {ObjectReferenceEntity} objectReferenceEntity */
static subObjectAttributeNameFromReference = (objectReferenceEntity, nameOnly = false) =>
this.subObjectAttributeNamePrefix + (!nameOnly ? "_" + objectReferenceEntity.type : "")
+ "_" + objectReferenceEntity.path
static subObjectAttributeNameFromName = name => this.subObjectAttributeNamePrefix + "_" + name
static switchTargetPattern = /\/Script\/[\w\.\/\:]+K2Node_Switch([A-Z]\w+)+/
static trackingMouseEventName = {
begin: "ueb-tracking-mouse-begin",
end: "ueb-tracking-mouse-end",
}
static unescapedBackslash = /(?<=(?:[^\\]|^)(?:\\\\)*)\\(?!\\)/ // Try to remove
static windowApplyEventName = "ueb-window-apply"
static windowApplyButtonText = "OK"
static windowCancelEventName = "ueb-window-cancel"
static windowCancelButtonText = "Cancel"
static windowCloseEventName = "ueb-window-close"
static CommonEnums = {
[this.paths.eAttachmentRule]: [
"KeepRelative",
"KeepWorld",
"SnapToTarget",
],
[this.paths.eDrawDebugTrace]: ["None", "ForOneFrame", "ForDuration", "Persistent"],
[this.paths.eMaterialSamplerType]: [
"Color",
"Grayscale",
"Alpha",
"Normal",
"Masks",
"Distance Field Font",
"Linear Color",
"Linear Grayscale",
"Data",
"External",
"Virtual Color",
"Virtual Grayscale",
"Virtual Alpha",
"Virtual Normal",
"Virtual Mask",
"Virtual Linear Color",
"Virtual Linear Grayscal",
],
[this.paths.eNiagara_Float4Channel]: [
["NewEnumerator0", "R"],
["NewEnumerator1", "G"],
["NewEnumerator2", "B"],
["NewEnumerator3", "A"],
],
[this.paths.eSamplerSourceMode]: ["From texture asset", "Shared: Wrap", "Shared: Clamp", "Hidden"],
[this.paths.eSearchCase]: ["CaseSensitive", "IgnoreCase"],
[this.paths.eWorldPositionIncludedOffsets]: [
"Absolute World Position (Including Material Shader Offsets)",
"Absolute World Position (Excluding Material Shader Offsets)",
"Camera Relative World Position (Including Material Shader Offsets)",
"Camera Relative World Position (Excluding Material Shader Offsets)",
],
[this.paths.eSearchDir]: ["FromStart", "FromEnd"],
[this.paths.eSpawnActorCollisionHandlingMethod]: [
["Undefined", "Default"],
["AlwaysSpawn", "Always Spawn, Ignore Collisions"],
["AdjustIfPossibleButAlwaysSpawn", "Try To Adjust Location, But Always Spawn"],
["AdjustIfPossibleButDontSpawnIfColliding", "Try To Adjust Location, Don't Spawn If Still Colliding"],
["DontSpawnIfColliding", "Do Not Spawn"],
],
[this.paths.eTextureMipValueMode]: [
"None (use computed mip level)",
"MipLevel (absolute, 0 is full resolution)",
"MipBias (relative to the computed mip level)",
"Derivative (explicit derivative to compute mip level)",
],
[this.paths.eTraceTypeQuery]: [["TraceTypeQuery1", "Visibility"], ["TraceTypeQuery2", "Camera"]]
}
static ModifierKeys = [
"Ctrl",
"Shift",
"Alt",
"Meta",
]
static rgba = ["R", "G", "B", "A"]
static Keys = {
/* UE name: JS name */
"Backspace": "Backspace",
"Tab": "Tab",
"LeftControl": "ControlLeft",
"RightControl": "ControlRight",
"LeftShift": "ShiftLeft",
"RightShift": "ShiftRight",
"LeftAlt": "AltLeft",
"RightAlt": "AltRight",
"Enter": "Enter",
"Pause": "Pause",
"CapsLock": "CapsLock",
"Escape": "Escape",
"Space": "Space",
"PageUp": "PageUp",
"PageDown": "PageDown",
"End": "End",
"Home": "Home",
"ArrowLeft": "ArrowLeft",
"ArrowUp": "ArrowUp",
"ArrowRight": "ArrowRight",
"ArrowDown": "ArrowDown",
"PrintScreen": "PrintScreen",
"Insert": "Insert",
"Delete": "Delete",
"Zero": "Digit0",
"One": "Digit1",
"Two": "Digit2",
"Three": "Digit3",
"Four": "Digit4",
"Five": "Digit5",
"Six": "Digit6",
"Seven": "Digit7",
"Eight": "Digit8",
"Nine": "Digit9",
"A": "KeyA",
"B": "KeyB",
"C": "KeyC",
"D": "KeyD",
"E": "KeyE",
"F": "KeyF",
"G": "KeyG",
"H": "KeyH",
"I": "KeyI",
"K": "KeyK",
"L": "KeyL",
"M": "KeyM",
"N": "KeyN",
"O": "KeyO",
"P": "KeyP",
"Q": "KeyQ",
"R": "KeyR",
"S": "KeyS",
"T": "KeyT",
"U": "KeyU",
"V": "KeyV",
"W": "KeyW",
"X": "KeyX",
"Y": "KeyY",
"Z": "KeyZ",
"NumPadZero": "Numpad0",
"NumPadOne": "Numpad1",
"NumPadTwo": "Numpad2",
"NumPadThree": "Numpad3",
"NumPadFour": "Numpad4",
"NumPadFive": "Numpad5",
"NumPadSix": "Numpad6",
"NumPadSeven": "Numpad7",
"NumPadEight": "Numpad8",
"NumPadNine": "Numpad9",
"Multiply": "NumpadMultiply",
"Add": "NumpadAdd",
"Subtract": "NumpadSubtract",
"Decimal": "NumpadDecimal",
"Divide": "NumpadDivide",
"F1": "F1",
"F2": "F2",
"F3": "F3",
"F4": "F4",
"F5": "F5",
"F6": "F6",
"F7": "F7",
"F8": "F8",
"F9": "F9",
"F10": "F10",
"F11": "F11",
"F12": "F12",
"NumLock": "NumLock",
"ScrollLock": "ScrollLock",
}
}
class Utility {
/** @param {Number} value */
static clamp(value, min = -Infinity, max = Infinity) {
return Math.min(Math.max(value, min), max)
}
/** @param {HTMLElement} element */
static getScale(element) {
// @ts-expect-error
const scale = element.blueprint?.getScale() ?? getComputedStyle(element).getPropertyValue("--ueb-scale");
return scale != "" ? parseFloat(scale) : 1
}
/**
* @param {Number} num
* @param {Number} decimals
*/
static minDecimals(num, decimals = 1, epsilon = 1e-8) {
const powered = num * 10 ** decimals;
if (Math.abs(powered % 1) > epsilon) {
// More decimal digits than required
return num.toString()
}
return num.toFixed(decimals)
}
/**
* @param {Number} num
* @param {Number} decimals
*/
static roundDecimals(num, decimals = 1) {
const power = 10 ** decimals;
return Math.round(num * power) / power
}
/** @param {Number} num */
static printExponential(num) {
if (num == Number.POSITIVE_INFINITY) {
return "inf"
} else if (num == Number.NEGATIVE_INFINITY) {
return "-inf"
}
const int = Math.round(num);
if (int >= 1000) {
const exp = Math.floor(Math.log10(int));
const dec = Math.round(num / 10 ** (exp - 2)) / 100;
// Not using num.toExponential() because of the omitted leading 0 on the exponential
return `${dec}e+${exp < 10 ? "0" : ""}${exp}`
}
const intPart = Math.floor(num);
if (intPart == 0) {
return num.toString()
}
return this.roundDecimals(num, Math.max(0, 3 - Math.floor(num).toString().length)).toString()
}
/**
* @param {Number} a
* @param {Number} b
*/
static approximatelyEqual(a, b, epsilon = 1e-8) {
return !(Math.abs(a - b) > epsilon)
}
/**
* @param {Coordinates} viewportLocation
* @param {HTMLElement} movementElement
*/
static convertLocation(viewportLocation, movementElement, ignoreScale = false) {
const scaleCorrection = ignoreScale ? 1 : 1 / Utility.getScale(movementElement);
const bounding = movementElement.getBoundingClientRect();
const location = /** @type {Coordinates} */([
Math.round((viewportLocation[0] - bounding.x) * scaleCorrection),
Math.round((viewportLocation[1] - bounding.y) * scaleCorrection)
]);
return location
}
/**
* @param {Attribute} entity
* @param {String} key
* @returns {Boolean}
*/
static isSerialized(entity, key) {
return entity["attributes"]?.[key]?.serialized
?? entity.constructor["attributes"]?.[key]?.serialized
?? false
}
/** @param {String[]} keys */
static objectGet(target, keys, defaultValue = undefined) {
if (target === undefined) {
return undefined
}
if (!(keys instanceof Array)) {
throw new TypeError("UEBlueprint: Expected keys to be an array")
}
if (keys.length == 0 || !(keys[0] in target) || target[keys[0]] === undefined) {
return defaultValue
}
if (keys.length == 1) {
return target[keys[0]]
}
return Utility.objectGet(target[keys[0]], keys.slice(1), defaultValue)
}
/**
* @param {String[]} keys
* @returns {Boolean}
*/
static objectSet(target, keys, value, defaultDictType = Object) {
if (!(keys instanceof Array)) {
throw new TypeError("Expected keys to be an array.")
}
if (keys.length == 1) {
if (keys[0] in target || target[keys[0]] === undefined) {
target[keys[0]] = value;
return true
}
} else if (keys.length > 0) {
if (!(target[keys[0]] instanceof Object)) {
target[keys[0]] = new defaultDictType();
}
return Utility.objectSet(target[keys[0]], keys.slice(1), value, defaultDictType)
}
return false
}
/**
* @param {Number} x
* @param {Number} y
* @param {Number} gridSize
* @returns {Coordinates}
*/
static snapToGrid(x, y, gridSize) {
if (gridSize === 1) {
return [x, y]
}
return [
gridSize * Math.floor(x / gridSize),
gridSize * Math.floor(y / gridSize)
]
}
/**
* @template T
* @param {Array} a
* @param {Array} b
* @param {(l: T, r: T) => Boolean} predicate
*/
static mergeArrays(a = [], b = [], predicate = (l, r) => l == r) {
let result = [];
a = [...a];
b = [...b];
restart:
while (true) {
for (let j = 0; j < b.length; ++j) {
for (let i = 0; i < a.length; ++i) {
if (predicate(a[i], b[j])) {
// Found an element in common in the two arrays
result.push(
// Take and append all the elements skipped from a
...a.splice(0, i),
// Take and append all the elements skippend from b
...b.splice(0, j),
// Take and append the element in common
...a.splice(0, 1)
);
b.shift(); // Remove the same element from b
continue restart
}
}
}
break restart
}
// Append remaining the elements in the arrays and make it unique
return [...(new Set(result.concat(...a, ...b)))]
}
/** @param {String} value */
static escapeNewlines(value) {
return value
.replaceAll("\n", "\\n") // Replace newline with \n
.replaceAll("\t", "\\t") // Replace tab with \t
}
/** @param {String} value */
static escapeString(value, inline = true) {
let result = value.replaceAll(new RegExp(`(${Configuration.stringEscapedCharacters.source})`, "g"), '\\$1');
if (inline) {
result = result
.replaceAll("\n", "\\n") // Replace newline with \n
.replaceAll("\t", "\\t"); // Replace tab with \t
}
return result
}
/** @param {String} value */
static unescapeString(value) {
return value
.replaceAll(new RegExp(Configuration.unescapedBackslash.source + "t", "g"), "\t") // Replace tab with \t
.replaceAll(new RegExp(Configuration.unescapedBackslash.source + "n", "g"), "\n") // Replace newline with \n
.replaceAll(new RegExp(`\\\\(${Configuration.stringEscapedCharacters.source})`, "g"), "$1")
}
/** @param {String} value */
static clearHTMLWhitespace(value) {
return value
.replaceAll(" ", "\u00A0") // whitespace
.replaceAll(/
|
/g, "\n") // newlines
.replaceAll(/(\)/g, "") // html comments
}
/** @param {String} value */
static encodeHTMLWhitespace(value) {
return value.replaceAll(" ", "\u00A0")
}
/** @param {String} value */
static capitalFirstLetter(value) {
if (value.length === 0) {
return value
}
return value.charAt(0).toUpperCase() + value.slice(1)
}
/** @param {String} value */
static formatStringName(value = "") {
return value
// Remove leading b (for boolean values) or newlines
.replace(/^\s*b(?=[A-Z])/, "")
// Insert a space where needed, possibly removing unnecessary elading characters
.replaceAll(Configuration.nameRegexSpaceReplacement, " ")
.trim()
.split(" ")
.map(v => Utility.capitalFirstLetter(v))
.join(" ")
}
/** @param {String} value */
static getIdFromReference(value) {
return value
.replace(/(?:.+\.)?([^\.]+)$/, "$1")
.replaceAll(/(?<=[a-z\d])(?=[A-Z])|(?<=[a-zA-Z])(?=\d)|(?<=[A-Z]{2})(?=[A-Z][a-z])/g, "-")
.toLowerCase()
}
/** @param {String} pathValue */
static getNameFromPath(pathValue, dropCounter = false) {
// From end to the first "/" or "."
const regex = dropCounter ? /([^\.\/]+?)(?:_\d+)$/ : /([^\.\/]+)$/;
return pathValue.match(regex)?.[1] ?? ""
}
/**
* @param {Number} x
* @param {Number} y
* @returns {Coordinates}
*/
static getPolarCoordinates(x, y, positiveTheta = false) {
let theta = Math.atan2(y, x);
if (positiveTheta && theta < 0) {
theta = 2 * Math.PI + theta;
}
return [
Math.sqrt(x * x + y * y),
theta,
]
}
/**
* @param {Number} r
* @param {Number} theta
* @returns {Coordinates}
*/
static getCartesianCoordinates(r, theta) {
return [
r * Math.cos(theta),
r * Math.sin(theta)
]
}
/**
* @param {Number} begin
* @param {Number} end
*/
static range(begin = 0, end = 0, step = end >= begin ? 1 : -1) {
return Array.from({ length: Math.ceil((end - begin) / step) }, (_, i) => begin + (i * step))
}
/** @param {String[]} words */
static getFirstWordOrder(words) {
return new RegExp(/\s*/.source + words.join(/[^\n]+\n\s*/.source) + /\s*/.source)
}
/**
* @param {HTMLElement} element
* @param {String} value
*/
static paste(element, value) {
const event = new ClipboardEvent("paste", {
bubbles: true,
cancelable: true,
clipboardData: new DataTransfer(),
});
event.clipboardData.setData("text", value);
element.dispatchEvent(event);
}
/** @param {Blueprint} blueprint */
static async copy(blueprint) {
const event = new ClipboardEvent("copy", {
bubbles: true,
cancelable: true,
clipboardData: new DataTransfer(),
});
blueprint.dispatchEvent(event);
}
static animate(
from,
to,
intervalSeconds,
callback,
acknowledgeRequestAnimationId = id => { },
timingFunction = x => {
const v = x ** 3.5;
return v / (v + ((1 - x) ** 3.5))
}
) {
let startTimestamp;
const doAnimation = currentTimestamp => {
if (startTimestamp === undefined) {
startTimestamp = currentTimestamp;
}
let delta = (currentTimestamp - startTimestamp) / intervalSeconds;
if (Utility.approximatelyEqual(delta, 1) || delta > 1) {
delta = 1;
} else {
acknowledgeRequestAnimationId(requestAnimationFrame(doAnimation));
}
const currentValue = from + (to - from) * timingFunction(delta);
callback(currentValue);
};
acknowledgeRequestAnimationId(requestAnimationFrame(doAnimation));
}
}
/**
* @template {IEntity} EntityT
* @template {ITemplate} TemplateT
*/
class IElement extends s {
/** @type {Blueprint} */
#blueprint
get blueprint() {
return this.#blueprint
}
set blueprint(v) {
this.#blueprint = v;
}
/** @type {EntityT} */
#entity
get entity() {
return this.#entity
}
set entity(entity) {
this.#entity = entity;
}
/** @type {TemplateT} */
#template
get template() {
return this.#template
}
isInitialized = false
isSetup = false
/** @type {IInput[]} */
inputObjects = []
/**
* @param {EntityT} entity
* @param {TemplateT} template
*/
initialize(entity, template) {
this.requestUpdate();
this.#entity = entity;
this.#template = template;
this.#template.initialize(this);
if (this.isConnected) {
this.updateComplete.then(() => this.setup());
}
this.isInitialized = true;
}
connectedCallback() {
super.connectedCallback();
this.blueprint = /** @type {Blueprint} */(this.closest("ueb-blueprint"));
if (this.isInitialized) {
this.requestUpdate();
this.updateComplete.then(() => this.setup());
}
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.isSetup) {
this.updateComplete.then(() => this.cleanup());
}
this.acknowledgeDelete();
}
createRenderRoot() {
return this
}
setup() {
this.template.setup();
this.isSetup = true;
}
cleanup() {
this.template.cleanup();
this.isSetup = false;
}
/** @param {PropertyValues} changedProperties */
willUpdate(changedProperties) {
super.willUpdate(changedProperties);
this.template.willUpdate(changedProperties);
}
/** @param {PropertyValues} changedProperties */
update(changedProperties) {
super.update(changedProperties);
this.template.update(changedProperties);
}
render() {
return this.template.render()
}
/** @param {PropertyValues} changedProperties */
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.template.firstUpdated(changedProperties);
this.template.inputSetup();
}
/** @param {PropertyValues} changedProperties */
updated(changedProperties) {
super.updated(changedProperties);
this.template.updated(changedProperties);
}
acknowledgeDelete() {
let deleteEvent = new CustomEvent(Configuration.removeEventName);
this.dispatchEvent(deleteEvent);
}
/** @param {IElement} element */
isSameGraph(element) {
return this.blueprint && this.blueprint == element?.blueprint
}
}
class SVGIcon {
static arrayPin = x`
`
static branchNode = x`
`
static breakStruct = x`
`
static cast = x`
`
static close = x`
`
static convert = x`
`
static correct = x`
`
static delegate = x`
`
static doN = x`
`
static doOnce = x`
`
static enum = x`
`
static event = x`
`
static execPin = x`
`
static expandIcon = x`
`
static flipflop = x`
`
static forEachLoop = x`
`
static functionSymbol = x`
`
static gamepad = x`
`
static genericPin = x`
`
static keyboard = x`
`
static loop = x`
`
static macro = x`
`
static mapPin = x`
`
static makeArray = x`
`
static makeMap = x`
`
static makeSet = x`
`
static makeStruct = x`
`
static metasoundFunction = x`
`
static mouse = x`
`
static node = x`
`
static operationPin = x`
`
static pcgStackPin = x`
`
static pcgPin = x`
`
static pcgParamPin = x`
`
static pcgSpatialPin = x`
`
static plusCircle = x`
`
static questionMark = x`
`
static referencePin = x`
`
static reject = x`
`
static setPin = x`
`
static select = x`
`
static sequence = x`
`
static sound = x`
`
static spawnActor = x`
`
static switch = x`
`
static timer = x`
`
static touchpad = x`
`
}
class Reply {
/**
* @template T
* @param {Number} position
* @param {T} value
* @param {PathNode} bestPath
* @returns {Result}
*/
static makeSuccess(position, value, bestPath = null, bestPosition = 0) {
return {
status: true,
value: value,
position: position,
bestParser: bestPath,
bestPosition: bestPosition,
}
}
/**
* @param {PathNode} bestPath
* @returns {Result}
*/
static makeFailure(position = 0, bestPath = null, bestPosition = 0) {
return {
status: false,
value: null,
position,
bestParser: bestPath,
bestPosition: bestPosition,
}
}
/** @param {Parsernostrum} parsernostrum */
static makeContext(parsernostrum = null, input = "") {
return /** @type {Context} */({
parsernostrum,
input,
highlighted: null,
})
}
static makePathNode(parser, index = 0, previous = null) {
return /** @type {PathNode} */({
parent: previous,
current: parser,
index,
})
}
}
/** @template T */
class Parser {
static indentation = " "
static highlight = "Last valid parser"
/** @type {(new (...args: any) => Parser) & typeof Parser} */
Self
/** @param {String} value */
static frame(value, label = "", indentation = "") {
label = value ? "[ " + label + " ]" : "";
let rows = value.split("\n");
const width = Math.max(...rows.map(r => r.length));
const rightPadding = width < label.length ? " ".repeat(label.length - width) : "";
for (let i = 0; i < rows.length; ++i) {
rows[i] =
indentation
+ "| "
+ rows[i]
+ " ".repeat(width - rows[i].length)
+ rightPadding
+ " |";
}
if (label.length < width) {
label = label + "─".repeat(width - label.length);
}
const rowA = "┌─" + label + "─┐";
const rowB = indentation + "└─" + "─".repeat(label.length) + "─┘";
rows = [rowA, ...rows, rowB];
return rows.join("\n")
}
/**
* @param {PathNode} path
* @param {Number} index
* @returns {PathNode}
*/
makePath(path, index) {
return { current: this, parent: path, index }
}
/**
* @param {Context} context
* @param {PathNode} path
*/
isHighlighted(context, path) {
if (context.highlighted instanceof Parser) {
return context.highlighted === this
}
if (!context.highlighted || !path?.current) {
return false
}
let a, b;
for (
a = path,
b = /** @type {PathNode} */(context.highlighted);
a.current && b.current;
a = a.parent,
b = b.parent
) {
if (a.current !== b.current || a.index !== b.index) {
return false
}
}
return !a.current && !b.current
}
/** @param {PathNode?} path */
isVisited(path) {
if (!path) {
return false
}
for (path = path.parent; path != null; path = path.parent) {
if (path.current === this) {
return true
}
}
return false
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
* @returns {Result}
*/
parse(context, position, path, index) {
return null
}
/** @param {PathNode} path */
toString(context = Reply.makeContext(null, ""), indentation = "", path = null, index = 0) {
path = this.makePath(path, index);
if (this.isVisited(path)) {
return "<...>"
}
const isVisited = this.isVisited(path);
const isHighlighted = this.isHighlighted(context, path);
let result = isVisited ? "<...>" : this.doToString(context, isHighlighted ? "" : indentation, path, index);
if (isHighlighted) {
/** @type {String[]} */
result = Parser.frame(result, Parser.highlight, indentation);
}
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
return `${this.constructor.name} does not implement toString()`
}
}
/** @template {String} T */
class StringParser extends Parser {
#value
get value() {
return this.#value
}
/** @param {T} value */
constructor(value) {
super();
this.#value = value;
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
* @returns {Result}
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
const end = position + this.#value.length;
const value = context.input.substring(position, end);
const result = this.#value === value
? Reply.makeSuccess(end, this.#value, path, end)
: Reply.makeFailure();
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
return `"${this.value.replaceAll("\n", "\\n").replaceAll('"', '\\"')}"`
}
}
/** @extends Parser<""> */
class SuccessParser extends Parser {
static instance = new SuccessParser()
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
* @returns {Result<"">}
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
return Reply.makeSuccess(position, "", path, 0)
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
return ""
}
}
/**
* @template {any[]} T
* @typedef {T extends [infer A] ? A
* : T extends [infer A, ...infer B] ? A | Union
* : never
* } Union
*/
/**
* @template {any[]} T
* @extends Parser>
*/
class AlternativeParser extends Parser {
#parsers
get parsers() {
return this.#parsers
}
/** @param {Parser[]} parsers */
constructor(...parsers) {
super();
this.#parsers = parsers;
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
const result = /** @type {Result>} */(Reply.makeSuccess(0, ""));
for (let i = 0; i < this.#parsers.length; ++i) {
const outcome = this.#parsers[i].parse(context, position, path, i);
if (outcome.bestPosition > result.bestPosition) {
result.bestParser = outcome.bestParser;
result.bestPosition = outcome.bestPosition;
}
if (outcome.status) {
result.value = outcome.value;
result.position = outcome.position;
return result
}
}
result.status = false;
result.value = null;
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
// Short syntax for optional parser
if (this.#parsers.length === 2 && this.#parsers[1] instanceof SuccessParser) {
let result = this.#parsers[0].toString(context, indentation, path, 0);
if (!(this.#parsers[0] instanceof StringParser)) {
result = "<" + result + ">";
}
result += "?";
return result
}
const deeperIndentation = indentation + Parser.indentation;
let result = "ALT<\n"
+ deeperIndentation
+ this.#parsers
.map((parser, i) => parser.toString(
context,
deeperIndentation + " ".repeat(i === 0 ? 0 : Parser.indentation.length - 2),
path,
i,
))
.join("\n" + deeperIndentation + "| ")
+ "\n" + indentation + ">";
return result
}
}
/**
* @template S
* @template T
* @extends Parser
*/
class ChainedParser extends Parser {
#parser
get parser() {
return this.#parser
}
#fn
/**
* @param {Parser} parser
* @param {(v: S, input: String, position: Number) => Parsernostrum} chained
*/
constructor(parser, chained) {
super();
this.#parser = parser;
this.#fn = chained;
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
* @returns {Result}
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
const outcome = this.#parser.parse(context, position, path, 0);
if (!outcome.status) {
// @ts-expect-error
return outcome
}
const result = this.#fn(outcome.value, context.input, outcome.position)
.getParser()
.parse(context, outcome.position, path, 0);
if (outcome.bestPosition > result.bestPosition) {
result.bestParser = outcome.bestParser;
result.bestPosition = outcome.bestPosition;
}
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
const result = this.#parser.toString(context, indentation, path, 0) + " => chained";
return result
}
}
/** @extends Parser */
class FailureParser extends Parser {
static instance = new FailureParser()
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
*/
parse(context, position, path, index) {
return Reply.makeFailure()
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
return ""
}
}
/**
* @template T
* @extends Parser
*/
class Label extends Parser {
#parser
get parser() {
return this.#parser
}
#label = ""
/**
* @param {Parser} parser
* @param {String} label
*/
constructor(parser, label) {
super();
this.#parser = parser;
this.#label = label;
}
/**
* @param {PathNode} path
* @param {Number} index
*/
makePath(path, index) {
return path // Label does not alter the path
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
*/
parse(context, position, path, index) {
this.parse = this.#parser.parse.bind(this.#parser);
return this.parse(context, position, path, index)
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
let result = this.#parser.toString(context, "", path, index);
result = Parser.frame(result, this.#label, indentation);
return result
}
}
/**
* @template T
* @extends Parser
*/
class LazyParser extends Parser {
#parser
/** @type {Parser} */
#resolvedPraser
/** @param {() => Parsernostrum} parser */
constructor(parser) {
super();
this.#parser = parser;
}
/**
* @param {PathNode} path
* @param {Number} index
*/
makePath(path, index) {
return path
}
/**
* @param {Context} context
* @param {PathNode} path
*/
isHighlighted(context, path) {
if (super.isHighlighted(context, path)) {
// If LazyParser is highlighted, then highlight its child
const childrenPath = { parent: path, parser: this.#resolvedPraser, index: 0 };
context.highlighted = context.highlighted instanceof Parser ? this.#resolvedPraser : childrenPath;
}
return false
}
resolve() {
if (!this.#resolvedPraser) {
this.#resolvedPraser = this.#parser().getParser();
}
return this.#resolvedPraser
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
* @returns {Result}
*/
parse(context, position, path, index) {
this.resolve();
this.parse = this.#resolvedPraser.parse.bind(this.#resolvedPraser);
return this.parse(context, position, path, index)
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
this.resolve();
this.doToString = this.#resolvedPraser.toString.bind(this.#resolvedPraser);
return this.doToString(context, indentation, path, index)
}
}
/** @extends Parser<""> */
class Lookahead extends Parser {
#parser
get parser() {
return this.#parser
}
#type
get type() {
return this.#type
}
/**
* @readonly
* @enum {String}
*/
static Type = {
NEGATIVE_AHEAD: "?!",
NEGATIVE_BEHIND: "?}
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
let result = this.#parser.parse(context, position, path, 0);
result = result.status == (this.#type === Lookahead.Type.POSITIVE_AHEAD)
? Reply.makeSuccess(position, "", path, position)
: Reply.makeFailure();
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
return "(" + this.#type + this.#parser.toString(context, indentation, path, 0) + ")"
}
}
/**
* @template T
* @extends Parser
*/
class RegExpParser extends Parser {
/** @type {RegExp} */
#regexp
get regexp() {
return this.#regexp
}
/** @type {RegExp} */
#anchoredRegexp
#matchMapper
static #createEscapeable = character => String.raw`[^${character}\\]*(?:\\.[^${character}\\]*)*`
static #numberRegex = /[-\+]?(?:\d*\.)?\d+/
static common = {
number: new RegExp(this.#numberRegex.source + String.raw`(?!\.)`),
numberInteger: /[\-\+]?\d+(?!\.\d)/,
numberNatural: /\d+/,
numberExponential: new RegExp(this.#numberRegex.source + String.raw`(?:[eE][\+\-]?\d+)?(?!\.)`),
numberUnit: /\+?(?:0(?:\.\d+)?|1(?:\.0+)?)(?![\.\d])/,
numberByte: /0*(?:25[0-5]|2[0-4]\d|1?\d?\d)(?!\d|\.)/,
whitespace: /\s+/,
whitespaceOpt: /\s*/,
whitespaceInline: /[^\S\n]+/,
whitespaceInlineOpt: /[^\S\n]*/,
whitespaceMultiline: /\s*?\n\s*/,
doubleQuotedString: new RegExp(`"(${this.#createEscapeable('"')})"`),
singleQuotedString: new RegExp(`'(${this.#createEscapeable("'")})'`),
backtickQuotedString: new RegExp("`(" + this.#createEscapeable("`") + ")`"),
}
/**
* @param {RegExp} regexp
* @param {(match: RegExpExecArray) => T} matchMapper
*/
constructor(regexp, matchMapper) {
super();
this.#regexp = regexp;
this.#anchoredRegexp = new RegExp(`^(?:${regexp.source})`, regexp.flags);
this.#matchMapper = matchMapper;
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
* @returns {Result}
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
const match = this.#anchoredRegexp.exec(context.input.substring(position));
if (match) {
position += match[0].length;
}
const result = match
? Reply.makeSuccess(position, this.#matchMapper(match), path, position)
: Reply.makeFailure();
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
let result = "/" + this.#regexp.source + "/";
const shortname = Object.entries(RegExpParser.common).find(([k, v]) => v.source === this.#regexp.source)?.[0];
if (shortname) {
result = "P." + shortname;
}
return result
}
}
/**
* @template S
* @template T
* @extends Parser
*/
class MapParser extends Parser {
#parser
get parser() {
return this.#parser
}
#mapper
get mapper() {
return this.#mapper
}
/**
* @param {Parser} parser
* @param {(v: S) => T} mapper
*/
constructor(parser, mapper) {
super();
this.#parser = parser;
this.#mapper = mapper;
}
/**
* @param {Context} context
* @param {PathNode} path
*/
isHighlighted(context, path) {
if (super.isHighlighted(context, path)) {
// If MapParser is highlighted, then highlight its child
const childrenPath = { parent: path, parser: this.#parser, index: 0 };
context.highlighted = context.highlighted instanceof Parser ? this.#parser : childrenPath;
}
return false
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
* @returns {Result}
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
// @ts-expect-error
const result = /** @type {Result} */(this.#parser.parse(context, position, path, 0));
if (result.status) {
// @ts-expect-error
result.value = this.#mapper(result.value);
}
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
let result = this.#parser.toString(context, indentation, path, 0);
if (this.#parser instanceof RegExpParser) {
if (Object.values(RegExpParser.common).includes(this.#parser.regexp)) {
if (
this.#parser.regexp === RegExpParser.common.numberInteger
&& this.#mapper === /** @type {(v: any) => BigInt} */(BigInt)
) {
return "P.numberBigInteger"
}
return result
}
}
let serializedMapper = this.#mapper.toString();
if (serializedMapper.length > 60 || serializedMapper.includes("\n")) {
serializedMapper = "(...) => { ... }";
}
result += ` -> map<${serializedMapper}>`;
return result
}
}
/** @extends {RegExpParser} */
class RegExpArrayParser extends RegExpParser {
/** @param {RegExpExecArray} match */
static #mapper = match => match
/** @param {RegExp} regexp */
constructor(regexp) {
super(regexp, RegExpArrayParser.#mapper);
}
}
/** @extends {RegExpParser} */
class RegExpValueParser extends RegExpParser {
/** @param {RegExp} regexp */
constructor(regexp, group = 0) {
super(regexp, match => match[group]);
}
}
/**
* @template {any[]} T
* @extends Parser
*/
class SequenceParser extends Parser {
#parsers
get parsers() {
return this.#parsers
}
/** @param {Parser[]} parsers */
constructor(...parsers) {
super();
this.#parsers = parsers;
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
* @returns {Result}
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
const value = /** @type {ParserValue} */(new Array(this.#parsers.length));
const result = Reply.makeSuccess(position, value);
for (let i = 0; i < this.#parsers.length; ++i) {
const outcome = this.#parsers[i].parse(context, result.position, path, i);
if (outcome.bestPosition > result.bestPosition) {
result.bestParser = outcome.bestParser;
result.bestPosition = outcome.bestPosition;
}
if (!outcome.status) {
result.status = false;
result.value = null;
break
}
result.value[i] = outcome.value;
result.position = outcome.position;
}
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
const deeperIndentation = indentation + Parser.indentation;
const result = "SEQ<\n"
+ deeperIndentation
+ this.#parsers
.map((parser, index) => parser.toString(context, deeperIndentation, path, index))
.join("\n" + deeperIndentation)
+ "\n" + indentation + ">";
return result
}
}
/**
* @template T
* @extends Parser
*/
class TimesParser extends Parser {
#parser
get parser() {
return this.#parser
}
#min
get min() {
return this.#min
}
#max
get max() {
return this.#max
}
/** @param {Parser} parser */
constructor(parser, min = 0, max = Number.POSITIVE_INFINITY) {
super();
if (min > max) {
throw new Error("Min is greater than max")
}
this.#parser = parser;
this.#min = min;
this.#max = max;
}
/**
* @param {Context} context
* @param {Number} position
* @param {PathNode} path
* @param {Number} index
* @returns {Result}
*/
parse(context, position, path, index) {
path = this.makePath(path, index);
const value = /** @type {ParserValue[]} */([]);
const result = Reply.makeSuccess(position, value, path);
for (let i = 0; i < this.#max; ++i) {
const outcome = this.#parser.parse(context, result.position, path, 0);
if (outcome.bestPosition > result.bestPosition) {
result.bestParser = outcome.bestParser;
result.bestPosition = outcome.bestPosition;
}
if (!outcome.status) {
if (i < this.#min) {
result.status = false;
result.value = null;
}
break
}
// @ts-expect-error
result.value.push(outcome.value);
result.position = outcome.position;
}
// @ts-expect-error
return result
}
/**
* @protected
* @param {Context} context
* @param {String} indentation
* @param {PathNode} path
* @param {Number} index
*/
doToString(context, indentation, path, index) {
let result = this.parser.toString(context, indentation, path, 0);
const serialized =
this.#min === 0 && this.#max === 1 ? "?"
: this.#min === 0 && this.#max === Number.POSITIVE_INFINITY ? "*"
: this.#min === 1 && this.#max === Number.POSITIVE_INFINITY ? "+"
: "{"
+ this.#min
+ (this.#min !== this.#max ? "," + this.#max : "")
+ "}";
result += serialized;
return result
}
}
/** @template T */
class Parsernostrum {
#parser
/** @type {(new (parser: Parser) => Parsernostrum) & typeof Parsernostrum} */
Self
static lineColumnFromOffset(string, offset) {
const lines = string.substring(0, offset).split('\n');
const line = lines.length;
const column = lines[lines.length - 1].length + 1;
return { line, column }
}
/** @param {[any, ...any] | RegExpExecArray} param0 */
static #firstElementGetter = ([v, _]) => v
/** @param {[any, any, ...any] | RegExpExecArray} param0 */
static #secondElementGetter = ([_, v]) => v
static #arrayFlatter = ([first, rest]) => [first, ...rest]
/**
* @template T
* @param {T} v
* @returns {T extends Array ? String : T}
*/
// @ts-expect-error
static #joiner = v => v instanceof Array ? v.join("") : v
// Prefedined parsers
/** Parser accepting any valid decimal, possibly signed number */
static number = this.reg(RegExpParser.common.number).map(Number)
/** Parser accepting any digits only number */
static numberInteger = this.reg(RegExpParser.common.numberInteger).map(Number)
/** Parser accepting any digits only number and returns a BigInt */
// @ts-expect-error
static numberBigInteger = this.reg(this.numberInteger.getParser().parser.regexp).map(BigInt)
/** Parser accepting any digits only number */
static numberNatural = this.reg(RegExpParser.common.numberNatural).map(Number)
/** Parser accepting any valid decimal, possibly signed, possibly in the exponential form number */
static numberExponential = this.reg(RegExpParser.common.numberExponential).map(Number)
/** Parser accepting any valid decimal number between 0 and 1 */
static numberUnit = this.reg(RegExpParser.common.numberUnit).map(Number)
/** Parser accepting any integer between 0 and 255 */
static numberByte = this.reg(RegExpParser.common.numberByte).map(Number)
/** Parser accepting whitespace */
static whitespace = this.reg(RegExpParser.common.whitespace)
/** Parser accepting whitespace */
static whitespaceOpt = this.reg(RegExpParser.common.whitespaceOpt)
/** Parser accepting whitespace that spans on a single line */
static whitespaceInline = this.reg(RegExpParser.common.whitespaceInline)
/** Parser accepting whitespace that spans on a single line */
static whitespaceInlineOpt = this.reg(RegExpParser.common.whitespaceInlineOpt)
/** Parser accepting whitespace that contains a list a newline */
static whitespaceMultiline = this.reg(RegExpParser.common.whitespaceMultiline)
/** Parser accepting a double quoted string and returns the content */
static doubleQuotedString = this.reg(RegExpParser.common.doubleQuotedString, 1)
/** Parser accepting a single quoted string and returns the content */
static singleQuotedString = this.reg(RegExpParser.common.singleQuotedString, 1)
/** Parser accepting a backtick quoted string and returns the content */
static backtickQuotedString = this.reg(RegExpParser.common.backtickQuotedString, 1)
/** @param {Parser} parser */
constructor(parser, optimized = false) {
this.#parser = parser;
}
/** @param {PathNode} path */
static #simplifyPath(path) {
/** @type {PathNode[]} */
const array = [];
while (path) {
array.push(path);
path = path.parent;
}
array.reverse();
/** @type {Map} */
let visited = new Map();
for (let i = 1; i < array.length; ++i) {
const existing = visited.get(array[i].current);
if (existing !== undefined) {
if (array[i + 1]) {
array[i + 1].parent = array[existing];
}
visited = new Map([...visited.entries()].filter(([parser, index]) => index <= existing || index > i));
visited.set(array[i].current, existing);
array.splice(existing + 1, i - existing);
i = existing;
} else {
visited.set(array[i].current, i);
}
}
return array[array.length - 1]
}
getParser() {
return this.#parser
}
/** @param {String} input */
run(input) {
const result = this.#parser.parse(Reply.makeContext(this, input), 0, Reply.makePathNode(), 0);
if (result.position !== input.length) {
result.status = false;
}
return /** @type {Result} */(result)
}
/**
* @param {String} input
* @throws {Error} when the parser fails to match
*/
parse(input, printParser = true) {
const result = this.run(input);
if (result.status) {
return result.value
}
const chunkLength = 60;
const chunkRange = /** @type {[Number, Number]} */(
[Math.ceil(chunkLength / 2), Math.floor(chunkLength / 2)]
);
const position = Parsernostrum.lineColumnFromOffset(input, result.bestPosition);
let bestPosition = result.bestPosition;
const inlineInput = input.replaceAll(
/^(\s)+|\s{6,}|\s*?\n\s*/g,
(m, startingSpace, offset) => {
let replaced = startingSpace ? "..." : " ... ";
if (offset <= result.bestPosition) {
if (result.bestPosition < offset + m.length) {
bestPosition -= result.bestPosition - offset;
} else {
bestPosition -= m.length - replaced.length;
}
}
return replaced
}
);
const string = inlineInput.substring(0, chunkLength).trimEnd();
const leadingWhitespaceLength = Math.min(
input.substring(result.bestPosition - chunkRange[0]).match(/^\s*/)[0].length,
chunkRange[0] - 1,
);
let offset = Math.min(bestPosition, chunkRange[0] - leadingWhitespaceLength);
chunkRange[0] = Math.max(0, bestPosition - chunkRange[0]) + leadingWhitespaceLength;
chunkRange[1] = Math.min(input.length, chunkRange[0] + chunkLength);
let segment = inlineInput.substring(...chunkRange);
if (chunkRange[0] > 0) {
segment = "..." + segment;
offset += 3;
}
if (chunkRange[1] < inlineInput.length - 1) {
segment = segment + "...";
}
const bestParser = this.toString(Parser.indentation, true, Parsernostrum.#simplifyPath(result.bestParser));
throw new Error(
`Could not parse: ${string}\n\n`
+ `Input: ${segment}\n`
+ " " + " ".repeat(offset)
+ `^ From here (line: ${position.line}, `
+ `column: ${position.column}, `
+ `offset: ${result.bestPosition})${result.bestPosition === input.length ? ", end of string" : ""}\n`
+ (printParser
? "\n"
+ (result.bestParser ? "Last valid parser matched:" : "No parser matched:")
+ bestParser
+ "\n"
: ""
)
)
}
// Parsers
/**
* @template {String} S
* @param {S} value
*/
static str(value) {
return new this(new StringParser(value))
}
/** @param {RegExp} value */
static reg(value, group = 0) {
return new this(new RegExpValueParser(value, group))
}
/** @param {RegExp} value */
static regArray(value) {
return new this(new RegExpArrayParser(value))
}
static success() {
return new this(SuccessParser.instance)
}
static failure() {
return new this(FailureParser.instance)
}
// Combinators
/**
* @template {Parsernostrum[]} P
* @param {P} parsers
* @returns {Parsernostrum>}
*/
static seq(...parsers) {
return new this(new SequenceParser(...parsers.map(p => p.getParser())))
}
/**
* @template {Parsernostrum[]} P
* @param {P} parsers
* @returns {Parsernostrum>>}
*/
static alt(...parsers) {
return new this(new AlternativeParser(...parsers.map(p => p.getParser())))
}
/**
* @template {Parsernostrum} P
* @param {P} parser
*/
static lookahead(parser) {
return new this(new Lookahead(parser.getParser(), Lookahead.Type.POSITIVE_AHEAD))
}
/**
* @template {Parsernostrum} P
* @param {() => P} parser
* @returns {Parsernostrum>}
*/
static lazy(parser) {
return new this(new LazyParser(parser))
}
/** @param {Number} min */
times(min, max = min) {
return new Parsernostrum(new TimesParser(this.#parser, min, max))
}
many() {
return this.times(0, Number.POSITIVE_INFINITY)
}
/** @param {Number} n */
atLeast(n) {
return this.times(n, Number.POSITIVE_INFINITY)
}
/** @param {Number} n */
atMost(n) {
return this.times(0, n)
}
/**
* @param {any} emptyResult
* @returns {Parsernostrum}
*/
opt(emptyResult = "") {
let success = Parsernostrum.success();
if (emptyResult !== "") {
success = success.map(() => emptyResult);
}
// @ts-expect-error
return Parsernostrum.alt(this, success)
}
/**
* @template {Parsernostrum} P
* @param {P} separator
*/
sepBy(separator, atLeast = 1, allowTrailing = false) {
let result = Parsernostrum.seq(
this,
Parsernostrum.seq(separator, this).map(Parsernostrum.#secondElementGetter).atLeast(atLeast - 1),
...(allowTrailing ? [separator.opt([])] : [])
).map(Parsernostrum.#arrayFlatter);
if (atLeast === 0) {
result = result.opt([]);
}
return result
}
skipSpace() {
return Parsernostrum.seq(this, Parsernostrum.whitespaceOpt).map(Parsernostrum.#firstElementGetter)
}
/**
* @template R
* @param {(v: T) => R} fn
* @returns {Parsernostrum}
*/
map(fn) {
return new Parsernostrum(new MapParser(this.#parser, fn))
}
/**
* @template {Parsernostrum} P
* @param {(v: T, input: String, position: Number) => P} fn
* @returns {P}
*/
chain(fn) {
// @ts-expect-error
return new Parsernostrum(new ChainedParser(this.#parser, fn))
}
/**
* @param {(v: T, input: String, position: Number) => boolean} fn
* @return {Parsernostrum}
*/
assert(fn) {
return this.chain((v, input, position) => fn(v, input, position)
? Parsernostrum.success().map(() => v)
: Parsernostrum.failure()
)
}
join(value = "") {
return this.map(Parsernostrum.#joiner)
}
/** @return {Parsernostrum} */
label(value = "") {
return new Parsernostrum(new Label(this.#parser, value))
}
/** @param {Parsernostrum | Parser | PathNode} highlight */
toString(indentation = "", newline = false, highlight = null) {
if (highlight instanceof Parsernostrum) {
highlight = highlight.getParser();
}
const context = Reply.makeContext(this, "");
context.highlighted = highlight;
const path = Reply.makePathNode();
return (newline ? "\n" + indentation : "") + this.#parser.toString(context, indentation, path)
}
}
/** @abstract */
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 = Parsernostrum.lazy(() => this.createGrammar())
/** @type {P} */
static unknownEntityGrammar
static unknownEntity
/** @type {{ [key: String]: typeof IEntity }} */
static attributes = {}
/** @type {String | String[]} */
static lookbehind = ""
/** @type {(type: typeof IEntity) => InstanceType} */
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)];
}
#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}
*/
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() {
let result = this;
if (this.name.length) {
// @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 | 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,
) {
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
}
}
class BooleanEntity extends IEntity {
static grammar = this.createGrammar()
static booleanConverter = {
fromAttribute: (value, type) => {
},
toAttribute: (value, type) => {
if (value === true) {
return "true"
}
if (value === false) {
return "false"
}
return ""
}
}
#uppercase = true
get uppercase() {
return this.#uppercase
}
set uppercase(value) {
this.#uppercase = value;
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.regArray(/(true)|(True)|(false)|(False)/)
.map(v => {
const result = (v[1] ?? v[2]) ? new this(true) : new this(false);
result.uppercase = (v[2] ?? v[4]) !== undefined;
return result
})
.label("BooleanEntity")
}
constructor(value = false) {
super();
this.value = value;
}
serialize(
insideString = false,
indentation = "",
Self = /** @type {typeof IEntity} */(this.constructor),
) {
let result = this.value
? this.#uppercase ? "True" : "true"
: this.#uppercase ? "False" : "false";
if (Self.serialized) {
result = `"${result}"`;
}
return result
}
valueOf() {
return this.value
}
}
class ElementFactory {
/** @type {Map} */
static #elementConstructors = new Map()
/**
* @param {String} tagName
* @param {IElementConstructor} entityConstructor
*/
static registerElement(tagName, entityConstructor) {
ElementFactory.#elementConstructors.set(tagName, entityConstructor);
}
/** @param {String} tagName */
static getConstructor(tagName) {
return ElementFactory.#elementConstructors.get(tagName)
}
}
/** @template {(typeof IEntity)[]} T */
class AlternativesEntity extends IEntity {
/** @type {(typeof IEntity)[]} */
static alternatives = []
static className() {
let result = super.className();
if (this.alternatives.length) {
result += ".accepting(" + this.alternatives.map(v => v.className()).join(", ") + ")";
}
return result
}
static createGrammar() {
const grammars = this.alternatives.map(entity => entity.grammar);
if (this.alternatives.length == 0 || grammars.includes(this.unknownEntityGrammar)) {
return this.unknownEntityGrammar
}
return Parsernostrum.alt(...grammars)
}
/**
* @template {(typeof IEntity)[]} Types
* @param {Types} types
*/
static accepting(...types) {
const result = /** @type {typeof AlternativesEntity & { alternatives: Types }} */(
this.asUniqueClass()
);
result.alternatives = types;
result.grammar = result.createGrammar();
return result
}
}
class Grammar {
/** @type {String} */
// @ts-expect-error
static numberRegexSource = Parsernostrum.number.getParser().parser.regexp.source
static separatedBy = (source, separator, min = 1) =>
new RegExp(
source + "(?:" + separator + source + ")"
+ (min === 1 ? "*" : min === 2 ? "+" : `{${min},}`)
)
static Regex = class {
static HexDigit = /[0-9a-fA-F]/
static InsideString = /(?:[^"\\]|\\.)*/
static InsideSingleQuotedString = /(?:[^'\\]|\\.)*/
static Integer = /[\-\+]?\d+(?!\d|\.)/
static Number = /[-\+]?(?:\d*\.)?\d+(?!\d|\.)/
static RealUnit = /\+?(?:0(?:\.\d+)?|1(?:\.0+)?)(?![\.\d])/ // A number between 0 and 1 included
static Word = Grammar.separatedBy("[a-zA-Z]", "_")
static Symbol = /[a-zA-Z_]\w*/
static DotSeparatedSymbols = Grammar.separatedBy(this.Symbol.source, "\\.")
static MultipleWordsSymbols = Grammar.separatedBy(this.Symbol.source, "(?:\\.|\\ +)")
static PathFragment = Grammar.separatedBy(this.Symbol.source, "[\\.:]")
static PathSpaceFragment = Grammar.separatedBy(this.Symbol.source, "[\\.:\\ ]")
static Path = new RegExp(`(?:\\/${this.PathFragment.source}){2,}`) // Multiple (2+) /PathFragment
}
/* --- Primitive --- */
static null = Parsernostrum.reg(/\(\s*\)/).map(() => null)
static true = Parsernostrum.reg(/true/i).map(() => true)
static false = Parsernostrum.reg(/false/i).map(() => false)
static number = Parsernostrum.regArray(
// @ts-expect-error
new RegExp(`(${Parsernostrum.number.getParser().parser.regexp.source})|(\\+?inf)|(-inf)`)
).map(([_0, n, plusInf, minusInf]) => n ? Number(n) : plusInf ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY)
// @ts-expect-error
static bigInt = Parsernostrum.reg(new RegExp(Parsernostrum.number.getParser().parser.regexp.source)).map(BigInt)
.map(result =>
result[2] !== undefined
? Number.POSITIVE_INFINITY
: result[3] !== undefined
? Number.NEGATIVE_INFINITY
: Number(result[1])
)
static naturalNumber = Parsernostrum.lazy(() => Parsernostrum.reg(/\d+/).map(Number))
static string = Parsernostrum.doubleQuotedString.map(insideString => Utility.unescapeString(insideString))
/* --- Fragment --- */
static colorValue = Parsernostrum.numberByte
static word = Parsernostrum.reg(Grammar.Regex.Word)
static symbol = Parsernostrum.reg(Grammar.Regex.Symbol)
static symbolQuoted = Parsernostrum.reg(new RegExp('"(' + Grammar.Regex.Symbol.source + ')"'), 1)
static attributeName = Parsernostrum.reg(Grammar.Regex.DotSeparatedSymbols)
static attributeNameQuoted = Parsernostrum.reg(new RegExp('"(' + Grammar.Regex.InsideString.source + ')"'), 1)
static guid = Parsernostrum.reg(new RegExp(`${Grammar.Regex.HexDigit.source}{32}`))
static commaSeparation = Parsernostrum.reg(/\s*,\s*(?!\))/)
static commaOrSpaceSeparation = Parsernostrum.reg(/\s*,\s*(?!\))|\s+/)
static equalSeparation = Parsernostrum.reg(/\s*=\s*/)
static hexColorChannel = Parsernostrum.reg(new RegExp(Grammar.Regex.HexDigit.source + "{2}"))
/* --- Factory --- */
/**
* @param {typeof IEntity} entityType
* @param {String[]} key
* @returns {typeof IEntity}
*/
static getAttribute(entityType, [key, ...keys]) {
const attribute = entityType?.attributes?.[key];
if (!attribute) {
return
}
if (attribute.prototype instanceof AlternativesEntity) {
for (const alternative of /** @type {typeof AlternativesEntity} */(attribute).alternatives) {
const candidate = this.getAttribute(alternative, keys);
if (candidate) {
return candidate
}
}
}
if (keys.length > 0) {
return this.getAttribute(attribute, keys)
}
return attribute
}
/** @param {typeof IEntity} entityType */
static createAttributeGrammar(
entityType,
attributeNameGrammar = this.attributeName,
valueSeparator = this.equalSeparation,
handleObjectSet = (values, attributeKey, attributeValue) => { },
) {
return Parsernostrum.seq(
attributeNameGrammar,
valueSeparator,
).chain(([attributeName, _1]) => {
const attributeKey = attributeName.split(Configuration.keysSeparator);
const attributeValue = this.getAttribute(entityType, attributeKey);
const grammar = attributeValue ? attributeValue.grammar : IEntity.unknownEntityGrammar;
const inlined = attributeKey.length > 1;
return grammar.map(attributeValue =>
values => {
Utility.objectSet(values, attributeKey, attributeValue);
attributeKey.reduce(
(acc, cur, i) => {
acc[cur]["inlined"] = inlined && i < attributeKey.length - 1;
return acc[cur]
},
values
);
handleObjectSet(values, attributeKey, attributeValue);
}
)
})
}
/**
* @template {typeof IEntity & (new (...values: any) => InstanceType)} T
* @param {T} entityType
* @param {Number} completeness
* @return {Parsernostrum>}
*/
static createEntityGrammar(entityType, entriesSeparator = this.commaSeparation, completeness = null, minKeys = 1) {
const lookbehind = entityType.lookbehind instanceof Array
? entityType.lookbehind.join("|")
: entityType.lookbehind;
return Parsernostrum.seq(
Parsernostrum.reg(new RegExp(String.raw`(${lookbehind}\s*)\(\s*`), 1),
this.createAttributeGrammar(entityType).sepBy(entriesSeparator, minKeys),
Parsernostrum.reg(/\s*(,\s*)?\)/, 1), // optional trailing comma
)
.map(([lookbehind, attributes, trailing]) => {
let values = {};
if (lookbehind.length) {
values["lookbehind"] = lookbehind;
}
attributes.forEach(attributeSetter => attributeSetter(values));
values["trailing"] = trailing !== undefined;
return values
})
// Decide if we accept the entity or not. It is accepted if it doesn't have too many unexpected keys
.chain(values => {
if (entityType.lookbehind instanceof Array || entityType.lookbehind !== lookbehind) {
entityType = entityType.withLookbehind(lookbehind);
}
const keys = Object.keys(values);
const expectedKeys = Object.keys(entityType.attributes);
return completeness != null
? Parsernostrum.success()
.assert(
v => keys.filter(k => expectedKeys.includes(k)).length / expectedKeys.length >= completeness
)
.map(() => new entityType(values))
: Parsernostrum.success().map(() => new entityType(values))
})
}
}
class ColorChannelEntity extends IEntity {
static grammar = this.createGrammar()
constructor(value = 0) {
super();
this.value = value;
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.number.map(v => new this(v))
}
serialize(
insideString = false,
indentation = "",
Self = /** @type {typeof IEntity} */(this.constructor),
) {
let result = this.value.toFixed(6);
if (Self.serialized) {
result = `"${result}"`;
}
return result
}
valueOf() {
return this.value
}
}
class LinearColorEntity extends IEntity {
static attributes = {
...super.attributes,
R: ColorChannelEntity.withDefault(),
G: ColorChannelEntity.withDefault(),
B: ColorChannelEntity.withDefault(),
A: ColorChannelEntity.withDefault(type => new type(1)),
}
static grammar = this.createGrammar()
#H = new ColorChannelEntity()
get H() {
return this.#H
}
set H(value) {
this.#H = value;
}
#S = new ColorChannelEntity()
get S() {
return this.#S
}
set S(value) {
this.#S = value;
}
#V = new ColorChannelEntity()
get V() {
return this.#V
}
set V(value) {
this.#V = value;
}
constructor(values) {
super(values);
if (values instanceof Array) {
values = {
R: values[0] ?? 0,
G: values[1] ?? 0,
B: values[2] ?? 0,
A: values[3] ?? 1,
};
}
/** @type {InstanceType} */ this.R;
/** @type {InstanceType} */ this.G;
/** @type {InstanceType} */ this.B;
/** @type {InstanceType} */ this.A;
this.#updateHSV();
}
/** @returns {P} */
static createGrammar() {
return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 0.5).label("LinearColorEntity")
}
/** @param {LinearColorEntity} value */
static printLinearColor(value) {
return `${Math.round(value.R.valueOf() * 255)}, ${Math.round(value.G.valueOf() * 255)}, ${Math.round(value.B.valueOf() * 255)}`
}
/** @param {Number} x */
static linearToSRGB(x) {
if (x <= 0) {
return 0
} else if (x >= 1) {
return 1
} else if (x < 0.0031308) {
return x * 12.92
} else {
return Math.pow(x, 1 / 2.4) * 1.055 - 0.055
}
}
/** @param {Number} x */
static sRGBtoLinear(x) {
if (x <= 0) {
return 0
} else if (x >= 1) {
return 1
} else if (x < 0.04045) {
return x / 12.92
} else {
return Math.pow((x + 0.055) / 1.055, 2.4)
}
}
static getWhite() {
return new LinearColorEntity({
R: new ColorChannelEntity(1),
G: new ColorChannelEntity(1),
B: new ColorChannelEntity(1),
})
}
static getLinearColorFromHexGrammar() {
const hexDigit = /[0-9a-fA-F]/;
return Parsernostrum.regArray(new RegExp(
"#(" + hexDigit.source + "{2})"
+ "(" + hexDigit.source + "{2})"
+ "(" + hexDigit.source + "{2})"
+ "(" + hexDigit.source + "{2})?"
)).map(([m, R, G, B, A]) => new this({
R: parseInt(R, 16) / 255,
G: parseInt(G, 16) / 255,
B: parseInt(B, 16) / 255,
A: parseInt(A ?? "FF", 16) / 255,
}))
}
static getLinearColorRGBListGrammar() {
return Parsernostrum.seq(
Parsernostrum.numberByte,
Grammar.commaSeparation,
Parsernostrum.numberByte,
Grammar.commaSeparation,
Parsernostrum.numberByte,
).map(([R, _1, G, _3, B]) => new this({
R: R / 255,
G: G / 255,
B: B / 255,
A: 1,
}))
}
static getLinearColorRGBGrammar() {
return Parsernostrum.seq(
Parsernostrum.reg(/rgb\s*\(\s*/),
this.getLinearColorRGBListGrammar(),
Parsernostrum.reg(/\s*\)/)
).map(([_0, linearColor, _2]) => linearColor)
}
static getLinearColorRGBAGrammar() {
return Parsernostrum.seq(
Parsernostrum.reg(/rgba\s*\(\s*/),
this.getLinearColorRGBListGrammar(),
Parsernostrum.reg(/\s*\)/)
).map(([_0, linearColor, _2]) => linearColor)
}
static getLinearColorFromAnyFormat() {
return Parsernostrum.alt(
this.getLinearColorFromHexGrammar(),
this.getLinearColorRGBAGrammar(),
this.getLinearColorRGBGrammar(),
this.getLinearColorRGBListGrammar(),
)
}
#updateHSV() {
const r = this.R.value;
const g = this.G.value;
const b = this.B.value;
if (Utility.approximatelyEqual(r, g) && Utility.approximatelyEqual(r, b) && Utility.approximatelyEqual(g, b)) {
this.S.value = 0;
this.V.value = r;
return
}
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
let h;
switch (max) {
case min:
h = 0;
break
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break
case g:
h = (b - r) / d + 2;
break
case b:
h = (r - g) / d + 4;
break
}
h /= 6;
this.H.value = h;
this.S.value = max == 0 ? 0 : d / max;
this.V.value = max;
}
/**
* @param {Number} r
* @param {Number} g
* @param {Number} b
* @param {Number} a
*/
setFromRGBA(r, g, b, a = 1) {
this.R.value = r;
this.G.value = g;
this.B.value = b;
this.A.value = a;
this.#updateHSV();
}
/**
* @param {Number} h
* @param {Number} s
* @param {Number} v
* @param {Number} a
*/
setFromHSVA(h, s, v, a = 1) {
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
const values = [v, q, p, p, t, v];
const [r, g, b] = [values[i % 6], values[(i + 4) % 6], values[(i + 2) % 6]];
this.R.value = r;
this.G.value = g;
this.B.value = b;
this.A.value = a;
this.H.value = h;
this.S.value = s;
this.V.value = v;
}
/**
* @param {Number} x
* @param {Number} y
* @param {Number} v
* @param {Number} a
*/
setFromWheelLocation(x, y, v, a) {
const [r, theta] = Utility.getPolarCoordinates(x, y, true);
this.setFromHSVA(1 - theta / (2 * Math.PI), r, v, a);
}
toDimmedColor(minV = 0) {
const result = new LinearColorEntity();
result.setFromRGBANumber(this.toNumber());
result.setFromHSVA(
result.H.value,
result.S.value * 0.6,
Math.pow(result.V.value + minV, 0.55) * 0.7
);
return result
}
toCSSRGBValues() {
const r = Math.round(this.R.value * 255);
const g = Math.round(this.G.value * 255);
const b = Math.round(this.B.value * 255);
return i$3`${r}, ${g}, ${b}`
}
toRGBA() {
return [
Math.round(this.R.value * 255),
Math.round(this.G.value * 255),
Math.round(this.B.value * 255),
Math.round(this.A.value * 255),
]
}
toSRGBA() {
return [
Math.round(LinearColorEntity.linearToSRGB(this.R.value) * 255),
Math.round(LinearColorEntity.linearToSRGB(this.G.value) * 255),
Math.round(LinearColorEntity.linearToSRGB(this.B.value) * 255),
Math.round(this.A.value * 255),
]
}
toRGBAString() {
return this
.toRGBA()
.map(v => v.toString(16).toUpperCase().padStart(2, "0"))
.join("")
}
toSRGBAString() {
return this
.toSRGBA()
.map(v => v.toString(16).toUpperCase().padStart(2, "0"))
.join("")
}
toHSVA() {
return [this.H.value, this.S.value, this.V.value, this.A.value]
}
toNumber() {
return (
Math.round(this.R.value * 0xff) << 24)
+ (Math.round(this.G.value * 0xff) << 16)
+ (Math.round(this.B.value * 0xff) << 8)
+ Math.round(this.A.value * 0xff)
}
/** @returns {[Number, Number, Number, Number]} */
toArray() {
return [this.R.value, this.G.value, this.B.value, this.A.value]
}
/** @param {Number} number */
setFromRGBANumber(number) {
this.A.value = (number & 0xff) / 0xff;
this.B.value = ((number >> 8) & 0xff) / 0xff;
this.G.value = ((number >> 16) & 0xff) / 0xff;
this.R.value = ((number >> 24) & 0xff) / 0xff;
this.#updateHSV();
}
/** @param {Number} number */
setFromSRGBANumber(number) {
this.A.value = (number & 0xff) / 0xff;
this.B.value = LinearColorEntity.sRGBtoLinear(((number >> 8) & 0xff) / 0xff);
this.G.value = LinearColorEntity.sRGBtoLinear(((number >> 16) & 0xff) / 0xff);
this.R.value = LinearColorEntity.sRGBtoLinear(((number >> 24) & 0xff) / 0xff);
this.#updateHSV();
}
toString() {
return LinearColorEntity.printLinearColor(this)
}
}
/** @param {ObjectEntity} entity */
function nodeColor(entity) {
switch (entity.getType()) {
case Configuration.paths.materialExpressionConstant2Vector:
case Configuration.paths.materialExpressionConstant3Vector:
case Configuration.paths.materialExpressionConstant4Vector:
return Configuration.nodeColors.yellow
case Configuration.paths.materialExpressionFunctionInput:
case Configuration.paths.materialExpressionTextureCoordinate:
case Configuration.paths.materialExpressionWorldPosition:
case Configuration.paths.pcgEditorGraphNodeInput:
case Configuration.paths.pcgEditorGraphNodeOutput:
return Configuration.nodeColors.red
case Configuration.paths.makeStruct:
return Configuration.nodeColors.darkBlue
case Configuration.paths.materialExpressionMaterialFunctionCall:
return Configuration.nodeColors.blue
case Configuration.paths.materialExpressionTextureSample:
return Configuration.nodeColors.darkTurquoise
}
switch (entity.getClass()) {
case Configuration.paths.callFunction:
return entity.bIsPureFunc?.valueOf()
? Configuration.nodeColors.green
: Configuration.nodeColors.blue
case Configuration.paths.niagaraNodeFunctionCall:
return Configuration.nodeColors.darkerBlue
case Configuration.paths.dynamicCast:
return Configuration.nodeColors.turquoise
case Configuration.paths.inputDebugKey:
case Configuration.paths.inputKey:
return Configuration.nodeColors.red
case Configuration.paths.createDelegate:
case Configuration.paths.enumLiteral:
case Configuration.paths.makeArray:
case Configuration.paths.makeMap:
case Configuration.paths.materialGraphNode:
case Configuration.paths.select:
return Configuration.nodeColors.green
case Configuration.paths.executionSequence:
case Configuration.paths.ifThenElse:
case Configuration.paths.macro:
case Configuration.paths.multiGate:
return Configuration.nodeColors.gray
case Configuration.paths.functionEntry:
case Configuration.paths.functionResult:
return Configuration.nodeColors.violet
case Configuration.paths.timeline:
return Configuration.nodeColors.yellow
}
if (entity.switchTarget()) {
return Configuration.nodeColors.lime
}
if (entity.isEvent()) {
return Configuration.nodeColors.red
}
if (entity.isComment()) {
return (entity.CommentColor ? entity.CommentColor : LinearColorEntity.getWhite())
.toDimmedColor()
.toCSSRGBValues()
}
const pcgSubobject = entity.getPcgSubobject();
if (pcgSubobject) {
if (pcgSubobject.NodeTitleColor) {
return pcgSubobject.NodeTitleColor.toDimmedColor(0.1).toCSSRGBValues()
}
switch (entity.PCGNode?.getName(true)) {
case "Branch":
case "Select":
return Configuration.nodeColors.intenseGreen
}
}
if (entity.bIsPureFunc?.valueOf()) {
return Configuration.nodeColors.green
}
return Configuration.nodeColors.blue
}
/** @template {typeof IEntity} T */
class MirroredEntity extends IEntity {
/** @type {typeof IEntity} */
static type
/** @param {() => InstanceType} getter */
constructor(getter = null) {
super();
const self = /** @type {typeof MirroredEntity} */(this.constructor);
getter ??= self.default !== undefined ? /** @type {MirroredEntity} */(self.default(self)).getter : getter;
this.getter = getter;
}
static createGrammar(elementGrammar = this.type?.grammar ?? Parsernostrum.lazy(() => this.unknownEntityGrammar)) {
return this.type?.grammar.map(v => new this(() => v))
}
/**
* @template {typeof IEntity} T
* @this {T}
* @param {(type: T) => (InstanceType | NullEntity)} value
* @returns {T}
*/
// @ts-expect-error
static withDefault(value = type => new type(() => new (type.type)())) {
// @ts-expect-error
return super.withDefault(value)
}
/**
* @template {typeof IEntity} T
* @param {T} type
*/
static of(type) {
const result = /** @type {{type: T, grammar: P> } & typeof MirroredEntity} */(
this.asUniqueClass()
);
result.type = type;
result.grammar = result.createGrammar();
return result
}
doSerialize(
insideString = false,
indentation = "",
Self = /** @type {typeof MirroredEntity} */(this.constructor),
printKey = Self.printKey,
keySeparator = Self.keySeparator,
attributeSeparator = Self.attributeSeparator,
wrap = Self.wrap,
) {
const value = this.getter();
return value.serialize(insideString, indentation, Self.type, printKey, keySeparator, attributeSeparator, wrap)
}
/** @param {IEntity} other */
equals(other) {
if (other instanceof MirroredEntity) {
other = other.getter?.();
}
return this.getter?.().equals(other)
}
valueOf() {
this.valueOf = this.getter().valueOf.bind(this.getter());
return this.valueOf()
}
toString() {
this.toString = this.getter().toString.bind(this.getter());
return this.toString()
}
}
class NumberEntity extends IEntity {
static numberRegexSource = String.raw`${Grammar.numberRegexSource}(?<=(?:\.(\d*0+))?)`
static grammar = this.createGrammar()
/** @type {Number} */
static precision // Can override this.precision
#precision
get precision() {
return /** @type {typeof NumberEntity} */(this.constructor).precision ?? this.#precision
}
set precision(value) {
this.#precision = value;
}
/**
* @protected
* @type {Number}
*/
_value
get value() {
return this._value
}
set value(value) {
if (value === -0) {
value = 0;
}
this._value = value;
}
constructor(value = 0, precision = null) {
super();
this.value = Number(value);
if (precision !== null) {
this.#precision = Number(precision);
}
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.regArray(
new RegExp(`(?${this.numberRegexSource})|(?\\+?inf)|(?-inf)`)
).map(({ 2: precision, groups: { n, posInf, negInf } }) => new this(
n ? Number(n) : posInf ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY,
precision?.length
)
).label("NumberEntity")
}
/**
* @template {typeof NumberEntity} T
* @this {T}
* @returns {T}
*/
static withPrecision(value = 0) {
const result = this.asUniqueClass();
result.precision = value;
return result
}
/** @param {Number} num */
static printNumber(num) {
if (num == Number.POSITIVE_INFINITY) {
return "inf"
} else if (num == Number.NEGATIVE_INFINITY) {
return "-inf"
}
return Utility.minDecimals(num)
}
serialize(
insideString = false,
indentation = "",
Self = /** @type {typeof NumberEntity} */(this.constructor),
) {
if (this.value === Number.POSITIVE_INFINITY) {
return "+inf"
}
if (this.value === Number.NEGATIVE_INFINITY) {
return "-inf"
}
const precision = Self.precision ?? this.precision;
let result = precision !== undefined ? this.value.toFixed(precision) : this.value.toString();
if (Self.serialized) {
result = `"${result}"`;
}
return result
}
valueOf() {
return this.value
}
toString() {
return this.value.toString()
}
}
class VectorEntity extends IEntity {
static attributes = {
...super.attributes,
X: NumberEntity.withDefault(),
Y: NumberEntity.withDefault(),
Z: NumberEntity.withDefault(),
}
static grammar = this.createGrammar()
constructor(values) {
super(values);
/** @type {InstanceType} */ this.X;
/** @type {InstanceType} */ this.Y;
/** @type {InstanceType} */ this.Z;
}
/** @returns {P} */
static createGrammar() {
return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 1).label("VectorEntity")
}
/** @returns {[Number, Number, Number]} */
toArray() {
return [this.X.valueOf(), this.Y.valueOf(), this.Z.valueOf()]
}
}
const sequencerScriptingNameRegex = /\/Script\/SequencerScripting\.MovieSceneScripting(.+)Channel/;
const keyNameValue = {
"A_AccentGrave": "à",
"Add": "Num +",
"C_Cedille": "ç",
"Decimal": "Num .",
"Divide": "Num /",
"E_AccentAigu": "é",
"E_AccentGrave": "è",
"F1": "F1", // Otherwise F and number will be separated
"F10": "F10",
"F11": "F11",
"F12": "F12",
"F2": "F2",
"F3": "F3",
"F4": "F4",
"F5": "F5",
"F6": "F6",
"F7": "F7",
"F8": "F8",
"F9": "F9",
"Gamepad_Special_Left_X": "Touchpad Button X Axis",
"Gamepad_Special_Left_Y": "Touchpad Button Y Axis",
"Mouse2D": "Mouse XY 2D-Axis",
"Multiply": "Num *",
"Section": "§",
"Subtract": "Num -",
"Tilde": "`",
};
/** @param {String} value */
function numberFromText(value = "") {
value = value.toLowerCase();
switch (value) {
case "zero": return 0
case "one": return 1
case "two": return 2
case "three": return 3
case "four": return 4
case "five": return 5
case "six": return 6
case "seven": return 7
case "eight": return 8
case "nine": return 9
}
}
function keyName(value) {
/** @type {String} */
let result = keyNameValue[value];
if (result) {
return result
}
result = numberFromText(value)?.toString();
if (result) {
return result
}
const match = value.match(/NumPad([a-zA-Z]+)/);
if (match) {
result = numberFromText(match[1]).toString();
if (result) {
return "Num " + result
}
}
}
/**
* @param {ObjectEntity} entity
* @returns {String}
*/
function nodeTitle(entity) {
let input;
switch (entity.getType()) {
case Configuration.paths.asyncAction:
if (entity.ProxyFactoryFunctionName) {
return Utility.formatStringName(entity.ProxyFactoryFunctionName?.toString())
}
case Configuration.paths.actorBoundEvent:
case Configuration.paths.componentBoundEvent:
return `${Utility.formatStringName(entity.DelegatePropertyName?.toString())} (${entity.ComponentPropertyName?.toString() ?? "Unknown"})`
case Configuration.paths.callDelegate:
return `Call ${entity.DelegateReference?.MemberName?.toString() ?? "None"}`
case Configuration.paths.createDelegate:
return "Create Event"
case Configuration.paths.customEvent:
if (entity.CustomFunctionName) {
return entity.CustomFunctionName?.toString()
}
case Configuration.paths.dynamicCast:
if (!entity.TargetType) {
return "Bad cast node" // Target type not found
}
return `Cast To ${entity.TargetType?.getName()}`
case Configuration.paths.enumLiteral:
return `Literal enum ${entity.Enum?.getName()}`
case Configuration.paths.event:
return `Event ${(entity.EventReference?.MemberName?.toString() ?? "").replace(/^Receive/, "")}`
case Configuration.paths.executionSequence:
return "Sequence"
case Configuration.paths.forEachElementInEnum:
return `For Each ${entity.Enum?.getName()}`
case Configuration.paths.forEachLoopWithBreak:
return "For Each Loop with Break"
case Configuration.paths.functionEntry:
return entity.FunctionReference?.MemberName?.toString() === "UserConstructionScript"
? "Construction Script"
: entity.FunctionReference?.MemberName?.toString()
case Configuration.paths.functionResult:
return "Return Node"
case Configuration.paths.ifThenElse:
return "Branch"
case Configuration.paths.makeStruct:
if (entity.StructType) {
return `Make ${entity.StructType.getName()}`
}
case Configuration.paths.materialExpressionComponentMask: {
const materialObject = entity.getMaterialSubobject();
if (materialObject) {
return `Mask ( ${Configuration.rgba
.filter(k => /** @type {MirroredEntity} */(materialObject[k]).getter().value === true)
.map(v => v + " ")
.join("")})`
}
}
case Configuration.paths.materialExpressionConstant:
input ??= [entity.getCustomproperties().find(pinEntity => pinEntity.PinName.toString() == "Value")?.DefaultValue];
case Configuration.paths.materialExpressionConstant2Vector:
input ??= [
entity.getCustomproperties().find(pinEntity => pinEntity.PinName?.toString() == "X")?.DefaultValue,
entity.getCustomproperties().find(pinEntity => pinEntity.PinName?.toString() == "Y")?.DefaultValue,
];
case Configuration.paths.materialExpressionConstant3Vector:
case Configuration.paths.materialExpressionConstant4Vector:
if (!input) {
const vector = entity.getCustomproperties()
.find(pinEntity => pinEntity.PinName?.toString() == "Constant")
?.DefaultValue;
input = vector instanceof VectorEntity ? [vector.X, vector.Y, vector.Z].map(v => v.valueOf())
: vector instanceof LinearColorEntity ? [vector.R, vector.G, vector.B, vector.A].map(v => v.valueOf())
: /** @type {Number[]} */([]);
}
if (input.length > 0) {
return input.map(v => Utility.printExponential(v)).join(",")
}
break
case Configuration.paths.materialExpressionFunctionInput: {
const materialObject = entity.getMaterialSubobject();
const inputName = materialObject?.InputName ?? "In";
const inputType = materialObject?.InputType?.value.match(/^.+?_(\w+)$/)?.[1] ?? "Vector3";
return `Input ${inputName} (${inputType})`
}
case Configuration.paths.materialExpressionLogarithm:
return "Ln"
case Configuration.paths.materialExpressionLogarithm10:
return "Log10"
case Configuration.paths.materialExpressionLogarithm2:
return "Log2"
case Configuration.paths.materialExpressionMaterialFunctionCall:
const materialFunction = entity.getMaterialSubobject()?.MaterialFunction;
if (materialFunction) {
return materialFunction.getName()
}
break
case Configuration.paths.materialExpressionSquareRoot:
return "Sqrt"
case Configuration.paths.materialExpressionSubtract:
const materialObject = entity.getMaterialSubobject();
if (materialObject) {
return `Subtract(${materialObject.ConstA ?? "1"},${materialObject.ConstB ?? "1"})`
}
case Configuration.paths.metasoundEditorGraphExternalNode: {
const name = entity["ClassName"]?.["Name"];
if (name) {
switch (name) {
case "Add": return "+"
default: return name
}
}
}
case Configuration.paths.pcgEditorGraphNodeInput:
return "Input"
case Configuration.paths.pcgEditorGraphNodeOutput:
return "Output"
case Configuration.paths.spawnActorFromClass:
let className = entity.getCustomproperties()
.find(pinEntity => pinEntity.PinName.toString() == "ReturnValue")
?.PinType
?.PinSubCategoryObject
?.getName();
if (className === "Actor") {
className = null;
}
return `SpawnActor ${Utility.formatStringName(className ?? "NONE")}`
case Configuration.paths.switchEnum:
return `Switch on ${entity.Enum?.getName() ?? "Enum"}`
case Configuration.paths.switchInteger:
return `Switch on Int`
case Configuration.paths.variableGet:
return ""
case Configuration.paths.variableSet:
return "SET"
}
let switchTarget = entity.switchTarget();
if (switchTarget) {
if (switchTarget[0] !== "E") {
switchTarget = Utility.formatStringName(switchTarget);
}
return `Switch on ${switchTarget}`
}
if (entity.isComment()) {
return entity.NodeComment.toString()
}
const keyNameSymbol = entity.getHIDAttribute();
if (keyNameSymbol) {
const name = keyNameSymbol.toString();
let title = keyName(name) ?? Utility.formatStringName(name);
if (entity.getClass() === Configuration.paths.inputDebugKey) {
title = "Debug Key " + title;
} else if (entity.getClass() === Configuration.paths.getInputAxisKeyValue) {
title = "Get " + title;
}
return title
}
if (entity.getClass() === Configuration.paths.macro) {
return Utility.formatStringName(entity.MacroGraphReference?.getMacroName())
}
if (entity.isMaterial() && entity.getMaterialSubobject()) {
let result = nodeTitle(entity.getMaterialSubobject());
result = result.match(/Material Expression (.+)/)?.[1] ?? result;
return result
}
if (entity.isPcg() && entity.getPcgSubobject()) {
let pcgSubobject = entity.getPcgSubobject();
let result = pcgSubobject.NodeTitle ? pcgSubobject.NodeTitle.toString() : nodeTitle(pcgSubobject);
return result
}
const subgraphObject = entity.getSubgraphObject();
if (subgraphObject) {
return subgraphObject.Graph.getName()
}
const settingsObject = entity.getSettingsObject();
if (settingsObject) {
if (settingsObject.ExportPath.type === Configuration.paths.pcgHiGenGridSizeSettings) {
return `Grid Size: ${(
settingsObject.HiGenGridSize?.toString().match(/\d+/)?.[0]?.concat("00")
?? settingsObject.HiGenGridSize?.toString().match(/^\w+$/)?.[0]
) ?? "256"}`
}
if (settingsObject.BlueprintElementInstance) {
return Utility.formatStringName(settingsObject.BlueprintElementType.getName())
}
if (settingsObject.Operation) {
const match = settingsObject.Name?.toString().match(/PCGMetadata(\w+)Settings_\d+/);
if (match) {
return Utility.formatStringName(match[1] + ": " + settingsObject.Operation)
}
}
const settingsSubgraphObject = settingsObject.getSubgraphObject();
if (settingsSubgraphObject && settingsSubgraphObject.Graph) {
return settingsSubgraphObject.Graph.getName()
}
}
let memberName = entity.FunctionReference?.MemberName?.toString();
if (memberName) {
const memberParent = entity.FunctionReference.MemberParent?.path ?? "";
switch (memberName) {
case "AddKey":
let result = memberParent.match(sequencerScriptingNameRegex);
if (result) {
return `Add Key (${Utility.formatStringName(result[1])})`
}
case "Concat_StrStr":
return "Append"
}
const memberNameTraceLineMatch = memberName.match(Configuration.lineTracePattern);
if (memberNameTraceLineMatch) {
return "Line Trace"
+ (memberNameTraceLineMatch[1] === "Multi" ? " Multi " : " ")
+ (memberNameTraceLineMatch[2] === ""
? "By Channel"
: Utility.formatStringName(memberNameTraceLineMatch[2])
)
}
switch (memberParent) {
case Configuration.paths.blueprintGameplayTagLibrary:
case Configuration.paths.kismetMathLibrary:
case Configuration.paths.slateBlueprintLibrary:
case Configuration.paths.timeManagementBlueprintLibrary:
const leadingLetter = memberName.match(/[BF]([A-Z]\w+)/);
if (leadingLetter) {
// Some functions start with B or F (Like FCeil, FMax, BMin)
memberName = leadingLetter[1];
}
switch (memberName) {
case "Abs": return "ABS"
case "BooleanAND": return "AND"
case "BooleanNAND": return "NAND"
case "BooleanOR": return "OR"
case "Exp": return "e"
case "LineTraceSingle": return "Line Trace By Channel"
case "Max": return "MAX"
case "MaxInt64": return "MAX"
case "Min": return "MIN"
case "MinInt64": return "MIN"
case "Not_PreBool": return "NOT"
case "Sin": return "SIN"
case "Sqrt": return "SQRT"
case "Square": return "^2"
// Dot products not respecting MemberName pattern
case "CrossProduct2D": return "cross"
case "Vector4_CrossProduct3": return "cross3"
case "DotProduct2D":
case "Vector4_DotProduct":
return "dot"
case "Vector4_DotProduct3": return "dot3"
}
if (memberName.startsWith("Add_")) {
return "+"
}
if (memberName.startsWith("And_")) {
return "&"
}
if (memberName.startsWith("Conv_")) {
return "" // Conversion nodes do not have visible names
}
if (memberName.startsWith("Cross_")) {
return "cross"
}
if (memberName.startsWith("Divide_")) {
return String.fromCharCode(0x00f7)
}
if (memberName.startsWith("Dot_")) {
return "dot"
}
if (memberName.startsWith("EqualEqual_")) {
return "=="
}
if (memberName.startsWith("Greater_")) {
return ">"
}
if (memberName.startsWith("GreaterEqual_")) {
return ">="
}
if (memberName.startsWith("Less_")) {
return "<"
}
if (memberName.startsWith("LessEqual_")) {
return "<="
}
if (memberName.startsWith("Multiply_")) {
return String.fromCharCode(0x2a2f)
}
if (memberName.startsWith("Not_")) {
return "~"
}
if (memberName.startsWith("NotEqual_")) {
return "!="
}
if (memberName.startsWith("Or_")) {
return "|"
}
if (memberName.startsWith("Percent_")) {
return "%"
}
if (memberName.startsWith("Subtract_")) {
return "-"
}
if (memberName.startsWith("Xor_")) {
return "^"
}
break
case Configuration.paths.blueprintSetLibrary:
{
const setOperationMatch = memberName.match(/Set_(\w+)/);
if (setOperationMatch) {
return Utility.formatStringName(setOperationMatch[1]).toUpperCase()
}
}
break
case Configuration.paths.blueprintMapLibrary:
{
const setOperationMatch = memberName.match(/Map_(\w+)/);
if (setOperationMatch) {
return Utility.formatStringName(setOperationMatch[1]).toUpperCase()
}
}
break
case Configuration.paths.kismetArrayLibrary:
{
const arrayOperationMath = memberName.match(/Array_(\w+)/);
if (arrayOperationMath) {
return arrayOperationMath[1].toUpperCase()
}
}
break
}
return Utility.formatStringName(memberName)
}
if (entity.OpName) {
switch (entity.OpName.toString()) {
case "Boolean::LogicAnd": return "Logic AND"
case "Boolean::LogicEq": return "=="
case "Boolean::LogicNEq": return "!="
case "Boolean::LogicNot": return "Logic NOT"
case "Boolean::LogicOr": return "Logic OR"
case "Matrix::MatrixMultiply": return "Multiply (Matrix * Matrix)"
case "Matrix::MatrixVectorMultiply": return "Multiply (Matrix * Vector4)"
case "Numeric::Abs": return "Abs"
case "Numeric::Add": return "+"
case "Numeric::DistancePos": return "Distance"
case "Numeric::Mul": return String.fromCharCode(0x2a2f)
}
return Utility.formatStringName(entity.OpName.toString()).replaceAll("::", " ")
}
if (entity.FunctionDisplayName) {
return Utility.formatStringName(entity.FunctionDisplayName.toString())
}
if (entity.ObjectRef) {
return entity.ObjectRef.getName()
}
return Utility.formatStringName(entity.getNameAndCounter()[0])
}
/** @param {ObjectEntity} entity */
function nodeIcon(entity) {
if (entity.isMaterial() || entity.isPcg() || entity.isNiagara()) {
return null
}
switch (entity.getType()) {
case Configuration.paths.addDelegate:
case Configuration.paths.asyncAction:
case Configuration.paths.callDelegate:
case Configuration.paths.createDelegate:
case Configuration.paths.functionEntry:
case Configuration.paths.functionResult:
return SVGIcon.node
case Configuration.paths.customEvent: return SVGIcon.event
case Configuration.paths.doN: return SVGIcon.doN
case Configuration.paths.doOnce: return SVGIcon.doOnce
case Configuration.paths.dynamicCast: return SVGIcon.cast
case Configuration.paths.enumLiteral: return SVGIcon.enum
case Configuration.paths.event: return SVGIcon.event
case Configuration.paths.executionSequence:
case Configuration.paths.multiGate:
return SVGIcon.sequence
case Configuration.paths.flipflop:
return SVGIcon.flipflop
case Configuration.paths.forEachElementInEnum:
case Configuration.paths.forLoop:
case Configuration.paths.forLoopWithBreak:
case Configuration.paths.whileLoop:
return SVGIcon.loop
case Configuration.paths.forEachLoop:
case Configuration.paths.forEachLoopWithBreak:
return SVGIcon.forEachLoop
case Configuration.paths.ifThenElse: return SVGIcon.branchNode
case Configuration.paths.isValid: return SVGIcon.questionMark
case Configuration.paths.makeArray: return SVGIcon.makeArray
case Configuration.paths.makeMap: return SVGIcon.makeMap
case Configuration.paths.makeSet: return SVGIcon.makeSet
case Configuration.paths.makeStruct: return SVGIcon.makeStruct
case Configuration.paths.metasoundEditorGraphExternalNode: return SVGIcon.metasoundFunction
case Configuration.paths.select: return SVGIcon.select
case Configuration.paths.spawnActorFromClass: return SVGIcon.spawnActor
case Configuration.paths.timeline: return SVGIcon.timer
}
if (entity.switchTarget()) {
return SVGIcon.switch
}
if (nodeTitle(entity).startsWith("Break")) {
return SVGIcon.breakStruct
}
if (entity.getClass() === Configuration.paths.macro) {
return SVGIcon.macro
}
const hidValue = entity.getHIDAttribute()?.toString();
if (hidValue) {
if (hidValue.includes("Mouse")) {
return SVGIcon.mouse
} else if (hidValue.includes("Gamepad_Special")) {
return SVGIcon.keyboard // It is called Touchpad in UE
} else if (hidValue.includes("Gamepad") || hidValue.includes("Steam")) {
return SVGIcon.gamepad
} else if (hidValue.includes("Touch")) {
return SVGIcon.touchpad
} else {
return SVGIcon.keyboard
}
}
if (entity.getDelegatePin()) {
return SVGIcon.event
}
if (entity.ObjectRef?.type === Configuration.paths.ambientSound) {
return SVGIcon.sound
}
return SVGIcon.functionSymbol
}
/** @template {typeof IEntity} T */
class ArrayEntity extends IEntity {
/** @type {typeof IEntity} */
static type
static grammar = this.createGrammar()
get length() {
return this.values.length
}
/** @param {(ExtractType)[]} values */
constructor(values = []) {
super();
this.values = values;
}
/** @returns {P>} */
static createGrammar(elementGrammar = this.type?.grammar ?? Parsernostrum.lazy(() => this.unknownEntityGrammar)) {
return this.inlined
? elementGrammar
: Parsernostrum.seq(
Parsernostrum.reg(/\(\s*/),
elementGrammar.sepBy(Grammar.commaSeparation).opt(),
Parsernostrum.reg(/\s*(,\s*)?\)/, 1),
).map(([_0, values, trailing]) => {
values = values instanceof Array ? values : [];
let Self = this;
if ((trailing !== undefined) !== Self.trailing) {
Self = Self.flagTrailing(trailing !== undefined);
}
const result = new Self(values);
return result
}).label(`ArrayEntity of ${this.type?.className() ?? "unknown values"}`)
}
/**
* @template {typeof IEntity} T
* @this {T}
*/
static flagInlined(value = true) {
const result = this.asUniqueClass();
result.inlined = value;
result.grammar = /** @type {P} */(result.createGrammar());
return result
}
/**
* @template {typeof IEntity} T
* @param {T} type
*/
static of(type) {
const result = /** @type {{type: T, grammar: P> } & typeof ArrayEntity} */(
this.asUniqueClass()
);
result.type = type;
result.grammar = /** @type {P} */(result.createGrammar());
return result
}
doSerialize(
insideString = false,
indentation = "",
Self = /** @type {typeof ArrayEntity} */(this.constructor),
printKey = Self.printKey,
keySeparator = Self.keySeparator,
attributeSeparator = Self.attributeSeparator,
wrap = Self.wrap,
) {
if (Self.inlined) {
return super.serialize.bind(
this.values,
insideString,
indentation,
Self,
printKey,
keySeparator,
attributeSeparator,
wrap
)()
}
let result = this.values.map(v => v?.serialize(insideString)).join(Self.attributeSeparator);
if (this.trailing) {
result += Self.attributeSeparator;
}
return `(${result})`
}
valueOf() {
return this.values
}
/** @param {IEntity} other */
equals(other) {
if (!(other instanceof ArrayEntity) || this.values.length !== other.values.length) {
return false
}
for (let i = 0; i < this.values.length; ++i) {
if (!this.values[i].equals(other.values[i])) {
return false
}
}
return true
}
}
var crypto;
if (typeof window === "undefined") {
// When used in nodejs, mainly for test purpose
import('crypto').then(mod => crypto = mod.default).catch();
} else {
crypto = window.crypto;
}
class GuidEntity extends IEntity {
static grammar = this.createGrammar()
static generateGuid() {
let values = new Uint32Array(4);
crypto.getRandomValues(values);
let guid = "";
values.forEach(n => {
guid += ("0".repeat(8) + n.toString(16).toUpperCase()).slice(-8);
});
return guid
}
constructor(value = GuidEntity.generateGuid()) {
super();
this.value = value;
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.reg(/[0-9A-F]{32}/i).map(v => new this(v)).label("GuidEntity")
}
serialize(
insideString = false,
indentation = "",
Self = /** @type {typeof IEntity} */(this.constructor),
) {
let result = this.value;
if (Self.serialized) {
result = `"${result}"`;
}
return result
}
toString() {
return this.value
}
}
class IntegerEntity extends NumberEntity {
static grammar = this.createGrammar()
get value() {
return super.value
}
set value(value) {
value = Math.trunc(value);
if (value >= 1 << 31 && value < -(1 << 31)) {
value = Math.floor(value);
super.value = value;
}
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.numberInteger.map(v => new this(v))
}
}
class NaturalNumberEntity extends IntegerEntity {
static grammar = this.createGrammar()
get value() {
return super.value
}
set value(value) {
value = Math.round(Utility.clamp(value, 0));
super.value = value;
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.numberNatural.map(v => new this(v))
}
}
const colors = {
[Configuration.paths.niagaraBool]: i$3`146, 0, 0`,
[Configuration.paths.niagaraDataInterfaceVolumeTexture]: i$3`0, 168, 242`,
[Configuration.paths.niagaraFloat]: i$3`160, 250, 68`,
[Configuration.paths.niagaraMatrix]: i$3`0, 88, 200`,
[Configuration.paths.niagaraNumeric]: i$3`0, 88, 200`,
[Configuration.paths.niagaraPosition]: i$3`251, 146, 251`,
[Configuration.paths.quat4f]: i$3`0, 88, 200`,
[Configuration.paths.rotator]: i$3`157, 177, 251`,
[Configuration.paths.transform]: i$3`227, 103, 0`,
[Configuration.paths.vector]: i$3`251, 198, 34`,
[Configuration.paths.vector3f]: i$3`250, 200, 36`,
[Configuration.paths.vector4f]: i$3`0, 88, 200`,
"Any": i$3`132, 132, 132`,
"Any[]": i$3`132, 132, 132`,
"audio": i$3`252, 148, 252`,
"blue": i$3`0, 0, 255`,
"bool": i$3`146, 0, 0`,
"byte": i$3`0, 109, 99`,
"class": i$3`88, 0, 186`,
"default": i$3`255, 255, 255`,
"delegate": i$3`255, 56, 56`,
"enum": i$3`0, 109, 99`,
"exec": i$3`240, 240, 240`,
"float": i$3`160, 252, 70`,
"green": i$3`0, 255, 0`,
"int": i$3`31, 224, 172`,
"int32": i$3`30, 224, 172`,
"int64": i$3`169, 223, 172`,
"interface": i$3`238, 252, 168`,
"name": i$3`201, 128, 251`,
"object": i$3`0, 168, 242`,
"Param": i$3`255, 166, 39`,
"Param[]": i$3`255, 166, 39`,
"Point": i$3`63, 137, 255`,
"Point[]": i$3`63, 137, 255`,
"real": i$3`54, 208, 0`,
"red": i$3`255, 0, 0`,
"string": i$3`251, 0, 208`,
"struct": i$3`0, 88, 200`,
"Surface": i$3`69, 196, 126`,
"Surface[]": i$3`69, 196, 126`,
"text": i$3`226, 121, 167`,
"time": i$3`148, 252, 252`,
"Volume": i$3`230, 69, 188`,
"Volume[]": i$3`230, 69, 188`,
"wildcard": i$3`128, 120, 120`,
};
const pinColorMaterial = i$3`120, 120, 120`;
/** @param {PinEntity} entity */
function pinColor(entity) {
if (entity.PinType.PinCategory?.toString() === "mask") {
const result = colors[entity.PinType.PinSubCategory?.toString()];
if (result) {
return result
}
} else if (entity.PinType.PinCategory?.toString() === "optional") {
return pinColorMaterial
}
return colors[entity.getType()]
?? colors[entity.PinType.PinCategory?.toString().toLowerCase()]
?? colors["default"]
}
/** @param {PinEntity} entity */
function pinTitle(entity) {
let result = entity.PinFriendlyName
? entity.PinFriendlyName.toString()
: Utility.formatStringName(entity.PinName?.toString() ?? "");
let match;
if (match = entity.PinToolTip?.toString().match(/\s*(.+?(?=\n)|.+\S)\s*/)) {
if (match[1].toLowerCase() === result.toLowerCase()) {
return match[1] // In case they match, then keep the case of the PinToolTip
}
}
return result
}
class ByteEntity extends IntegerEntity {
static grammar = this.createGrammar()
get value() {
return super.value
}
set value(value) {
value = Math.trunc(value);
if (value >= 0 && value < 1 << 8) {
super.value = value;
}
}
/** @returns {P} */
createGrammar() {
// @ts-expect-error
return Parsernostrum.numberByte.map(v => new this(v))
}
}
class StringEntity extends IEntity {
static grammar = this.createGrammar()
static escapedCharacters = /['"\\]/g
static unescapedBackslash = /(?<=(?:[^\\]|^)(?:\\\\)*)\\(?!\\)/
constructor(value = "") {
super();
this.value = value;
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.doubleQuotedString
.map(insideString => new this(StringEntity.unescape(insideString)))
.label("StringEntity")
}
/** @param {String} value */
static escape(value, inline = true) {
let result = value.replaceAll(new RegExp(`(${StringEntity.escapedCharacters.source})`, "g"), '\\$1');
if (inline) {
result = result
.replaceAll("\n", "\\n") // Replace newline with \n
.replaceAll("\t", "\\t"); // Replace tab with \t
}
return result
}
/** @param {String} value */
static unescape(value) {
return value
.replaceAll(new RegExp(StringEntity.unescapedBackslash.source + "t", "g"), "\t") // Replace tab with \t
.replaceAll(new RegExp(StringEntity.unescapedBackslash.source + "n", "g"), "\n") // Replace newline with \n
.replaceAll(new RegExp(`\\\\(${StringEntity.escapedCharacters.source})`, "g"), "$1")
}
doSerialize(insideString = false) {
let result = `"${StringEntity.escape(this.value)}"`;
if (insideString) {
result = StringEntity.escape(result, false);
}
return result
}
valueOf() {
return this.value
}
toString() {
return this.value
}
}
class ComputedTypeEntity extends IEntity {
static grammar = this.createGrammar()
/** @type {(entity: IEntity) => typeof IEntity} */
static f
static createGrammar() {
return StringEntity.grammar
}
/**
* @template {typeof ComputedTypeEntity.f} T
* @param {T} producer
*/
static from(producer) {
const result = /** @type {(typeof ComputedTypeEntity) & { f: T }} */(this.asUniqueClass());
result.f = producer;
return result
}
/** @param {IEntity} entity */
static compute(entity) {
return this.f(entity)
}
}
class SymbolEntity extends IEntity {
static attributeConverter = {
fromAttribute: (value, type) => new this(value),
toAttribute: (value, type) => value.toString()
}
static grammar = this.createGrammar()
/** @returns {P} */
static createGrammar() {
return Grammar.symbol.map(v => new this(v)).label("SymbolEntity")
}
constructor(value = "") {
super();
this.value = value;
}
serialize(
insideString = false,
indentation = "",
Self = /** @type {typeof IEntity} */(this.constructor),
) {
let result = this.value;
if (Self.serialized) {
result = `"${result}"`;
}
return result
}
toString() {
return this.value
}
}
class EnumEntity extends SymbolEntity {
static grammar = this.createGrammar()
/** @returns {P} */
static createGrammar() {
return Grammar.symbol.map(v => new this(v))
}
}
class EnumDisplayValueEntity extends EnumEntity {
static grammar = this.createGrammar()
/** @returns {P} */
static createGrammar() {
return Parsernostrum.reg(Grammar.Regex.InsideString).map(v => new this(v))
}
}
class InvariantTextEntity extends IEntity {
static lookbehind = "INVTEXT"
static grammar = this.createGrammar()
constructor(value = "") {
super();
this.value = value;
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.alt(
Parsernostrum.seq(
Parsernostrum.reg(new RegExp(`${this.lookbehind}\\s*\\(`)),
Parsernostrum.doubleQuotedString,
Parsernostrum.reg(/\s*\)/)
).map(([_0, value, _2]) => Number(value)),
Parsernostrum.reg(new RegExp(this.lookbehind)).map(() => 0) // InvariantTextEntity can not have arguments
)
.map(value => new this(value))
.label("InvariantTextEntity")
}
doSerialize() {
return this.lookbehind + "(" + this.value + ")"
}
valueOf() {
return this.value
}
}
class LocalizedTextEntity extends IEntity {
static attributeSeparator = ", "
static printKey = k => ""
static lookbehind = "NSLOCTEXT"
static attributes = {
...super.attributes,
namespace: StringEntity.withDefault(),
key: StringEntity.withDefault(),
value: StringEntity.withDefault(),
}
static grammar = this.createGrammar()
constructor(values = {}) {
super(values);
/** @type {InstanceType} */ this.namespace;
/** @type {InstanceType} */ this.key;
/** @type {InstanceType} */ this.value;
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.regArray(new RegExp(
String.raw`${LocalizedTextEntity.lookbehind}\s*\(`
+ String.raw`\s*"(?${Grammar.Regex.InsideString.source})"\s*,`
+ String.raw`\s*"(?${Grammar.Regex.InsideString.source})"\s*,`
+ String.raw`\s*"(?${Grammar.Regex.InsideString.source})"\s*`
+ String.raw`(?,\s+)?`
+ String.raw`\)`,
"m"
)).map(({ groups: { namespace, key, value, trailing } }) => {
return new this({
namespace: new (this.attributes.namespace)(Utility.unescapeString(namespace)),
key: new (this.attributes.namespace)(Utility.unescapeString(key)),
value: new (this.attributes.namespace)(Utility.unescapeString(value)),
trailing: trailing !== undefined,
})
}).label("LocalizedTextEntity")
}
toString() {
return Utility.capitalFirstLetter(this.value.valueOf())
}
}
class FormatTextEntity extends IEntity {
static attributeSeparator = ", "
static lookbehind = ["LOCGEN_FORMAT_NAMED", "LOCGEN_FORMAT_ORDERED"]
static grammar = this.createGrammar()
/** @param {(StringEntity | LocalizedTextEntity | InvariantTextEntity | FormatTextEntity)[]} values */
constructor(values) {
super();
this.values = values;
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.lazy(() => Parsernostrum.seq(
// Resulting regex: /(LOCGEN_FORMAT_NAMED|LOCGEN_FORMAT_ORDERED)\s*/
Parsernostrum.reg(new RegExp(String.raw`(${this.lookbehind.join("|")})\s*\(\s*`), 1),
Parsernostrum.alt(
...[StringEntity, LocalizedTextEntity, InvariantTextEntity, FormatTextEntity].map(type => type.grammar)
).sepBy(Parsernostrum.reg(/\s*\,\s*/)),
Parsernostrum.reg(/\s*\)/)
)
.map(([lookbehind, values]) => {
const result = new this(values);
result.lookbehind = lookbehind;
return result
}))
.label("FormatTextEntity")
}
doSerialize(
insideString = false,
indentation = "",
Self = /** @type {typeof FormatTextEntity} */(this.constructor),
printKey = Self.printKey,
keySeparator = Self.keySeparator,
attributeSeparator = Self.attributeSeparator,
wrap = Self.wrap,
) {
const separator = Self.attributeSeparator;
return this.lookbehind + "("
+ this.values.map(v => v.serialize(insideString)).join(separator)
+ (Self.trailing ? separator : "")
+ ")"
}
toString() {
const pattern = this.values?.[0]?.toString(); // The pattern is always the first element of the array
if (!pattern) {
return ""
}
const values = this.values.slice(1).map(v => v?.valueOf());
let result = this.lookbehind == "LOCGEN_FORMAT_NAMED"
? pattern.replaceAll(/\{([a-zA-Z]\w*)\}/g, (substring, arg) => {
const argLocation = values.indexOf(arg) + 1;
return argLocation > 0 && argLocation < values.length
? values[argLocation]
: substring
})
: this.lookbehind == "LOCGEN_FORMAT_ORDERED"
? pattern.replaceAll(/\{(\d+)\}/g, (substring, arg) => {
const argValue = Number(arg);
return argValue < values.length
? values[argValue]
: substring
})
: "";
return result
}
}
class Integer64Entity extends IEntity {
static grammar = this.createGrammar()
/**
* @protected
* @type {bigint}
*/
_value
get value() {
return this._value
}
set value(value) {
if (value >= -(1n << 63n) && value < 1n << 63n) {
this._value = value;
}
}
/** @param {bigint | Number} value */
constructor(value = 0n) {
super();
this.value = BigInt(value);
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.numberBigInteger.map(v => new this(v))
}
serialize(
insideString = false,
indentation = "",
Self = /** @type {typeof IEntity} */(this.constructor),
) {
let result = this.value.toString();
if (Self.serialized) {
result = `"${result}"`;
}
return result
}
valueOf() {
return this.value
}
toString() {
return this.value.toString()
}
}
class ObjectReferenceEntity extends IEntity {
/** @protected */
static _quotedParser = Parsernostrum.regArray(new RegExp(
`'"(${Grammar.Regex.InsideString.source})"'`
+ "|"
+ `'(${Grammar.Regex.InsideSingleQuotedString.source})'`
)).map(([_0, a, b]) => a ?? b)
static typeReference = Parsernostrum.reg(
// @ts-expect-error
new RegExp(Grammar.Regex.Path.source + "|" + Grammar.symbol.getParser().regexp.source)
)
static fullReferenceGrammar = this.createFullReferenceGrammar()
static grammar = this.createGrammar()
#type
get type() {
return this.#type
}
set type(value) {
this.#type = value;
}
#path
get path() {
return this.#path
}
set path(value) {
this.#path = value;
}
#fullEscaped
/** @type {String} */
#full
get full() {
return this.#full
}
set full(value) {
this.#full = value;
}
constructor(type = "None", path = "", full = null) {
super();
this.#type = type;
this.#path = path;
this.#full = full ?? (
this.type.includes("/") || this.path
? `"${this.type + (this.path ? (`'${this.path}'`) : "")}"`
: this.type
);
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.alt(
this.createFullReferenceSerializedGrammar(),
this.createFullReferenceGrammar(),
this.createTypeReferenceGrammar(),
).label("ObjectReferenceEntity")
}
/** @returns {P} */
static createFullReferenceGrammar() {
return Parsernostrum.regArray(
new RegExp(
// @ts-expect-error
"(" + this.typeReference.getParser().regexp.source + ")"
// @ts-expect-error
+ "(?:" + this._quotedParser.getParser().parser.regexp.source + ")"
)
).map(([full, type, ...path]) => new this(type, path.find(v => v), full))
}
/** @returns {P} */
static createFullReferenceSerializedGrammar() {
return Parsernostrum.regArray(
new RegExp(
'"(' + Grammar.Regex.InsideString.source + "?)"
+ "(?:'(" + Grammar.Regex.InsideSingleQuotedString.source + `?)')?"`
)
).map(([full, type, path]) => new this(type, path, full))
}
/** @returns {P} */
static createTypeReferenceGrammar() {
return this.typeReference.map(v => new this(v, "", v))
}
static createNoneInstance() {
return new ObjectReferenceEntity("None", "", "None")
}
getName(dropCounter = false) {
return Utility.getNameFromPath(this.path.replace(/_C$/, ""), dropCounter)
}
doSerialize(insideString = false) {
if (insideString) {
if (this.#fullEscaped === undefined) {
this.#fullEscaped = Utility.escapeString(this.#full, false);
}
return this.#fullEscaped
}
return this.full
}
/** @param {IEntity} other */
equals(other) {
if (!(other instanceof ObjectReferenceEntity)) {
return false
}
return this.type == other.type && this.path == other.path
}
}
class PinReferenceEntity extends IEntity {
static grammar = this.createGrammar()
/**
* @param {SymbolEntity} objectName
* @param {GuidEntity} pinGuid
*/
constructor(objectName = null, pinGuid = null) {
super();
this.objectName = objectName;
this.pinGuid = pinGuid;
}
/** @returns {P} */
static createGrammar() {
return Parsernostrum.seq(
SymbolEntity.grammar,
Parsernostrum.whitespace,
GuidEntity.grammar
)
.map(([objectName, _1, pinGuid]) => new this(objectName, pinGuid))
.label("PinReferenceEntity")
}
doSerialize() {
return this.objectName.serialize() + " " + this.pinGuid.serialize()
}
}
class FunctionReferenceEntity extends IEntity {
static attributes = {
...super.attributes,
MemberParent: ObjectReferenceEntity,
MemberName: StringEntity,
MemberGuid: GuidEntity,
}
static grammar = this.createGrammar()
constructor(values) {
super(values);
/** @type {InstanceType} */ this.MemberParent;
/** @type {InstanceType} */ this.MemberName;
/** @type {InstanceType} */ this.MemberGuid;
}
/** @returns {P} */
static createGrammar() {
return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 0, 0)
}
}
class PinTypeEntity extends IEntity {
static attributes = {
...super.attributes,
PinCategory: StringEntity.withDefault(),
PinSubCategory: StringEntity,
PinSubCategoryObject: ObjectReferenceEntity,
PinSubCategoryMemberReference: FunctionReferenceEntity,
ContainerType: SymbolEntity,
bIsReference: BooleanEntity,
bIsConst: BooleanEntity,
bIsWeakPointer: BooleanEntity,
bIsUObjectWrapper: BooleanEntity,
bSerializeAsSinglePrecisionFloat: BooleanEntity,
}
static grammar = this.createGrammar()
constructor(values = {}) {
super(values);
/** @type {InstanceType} */ this.PinCategory;
/** @type {InstanceType} */ this.PinSubCategory;
/** @type {InstanceType} */ this.PinSubCategoryObject;
/** @type {InstanceType} */ this.PinSubCategoryMemberReference;
/** @type {InstanceType} */ this.ContainerType;
/** @type {InstanceType} */ this.bIsReference;
/** @type {InstanceType} */ this.bIsConst;
/** @type {InstanceType} */ this.bIsWeakPointer;
/** @type {InstanceType} */ this.bIsUObjectWrapper;
/** @type {InstanceType} */ this.bIsUObjectWrapper;
/** @type {InstanceType} */ this.bSerializeAsSinglePrecisionFloat;
}
/** @returns {P} */
static createGrammar() {
return Grammar.createEntityGrammar(this).label("PinTypeEntity")
}
/** @param {PinTypeEntity} other */
copyTypeFrom(other) {
for (const key of this.keys) {
if (other[key] !== undefined) {
this[key] = other[key];
}
}
}
}
class Vector2DEntity extends IEntity {
static attributes = {
...super.attributes,
X: NumberEntity.withDefault(),
Y: NumberEntity.withDefault(),
}
static grammar = this.createGrammar()
constructor(values) {
super(values);
/** @type {InstanceType} */ this.X;
/** @type {InstanceType} */ this.Y;
}
/** @returns {P} */
static createGrammar() {
return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 1).label("Vector2DEntity")
}
/** @returns {[Number, Number]} */
toArray() {
return [this.X.valueOf(), this.Y.valueOf()]
}
}
class RBSerializationVector2DEntity extends Vector2DEntity {
static grammar = this.createGrammar()
/** @returns {P} */
static createGrammar() {
return Parsernostrum.alt(
Parsernostrum.regArray(new RegExp(
/X\s*=\s*/.source + "(?" + Grammar.numberRegexSource + ")"
+ "\\s+"
+ /Y\s*=\s*/.source + "(?" + Grammar.numberRegexSource + ")"
)).map(({ groups: { x, y } }) => new this({
X: new (Vector2DEntity.attributes.X)(x),
Y: new (Vector2DEntity.attributes.Y)(y),
})),
Vector2DEntity.grammar.map(v => new this({
X: v.X,
Y: v.Y,
}))
).label("RBSerializationVector2DEntity")
}
}
class RotatorEntity extends IEntity {
static attributes = {
...super.attributes,
R: NumberEntity.withDefault(),
P: NumberEntity.withDefault(),
Y: NumberEntity.withDefault(),
}
static grammar = this.createGrammar()
constructor(values) {
super(values);
/** @type {InstanceType} */ this.R;
/** @type {InstanceType} */ this.P;
/** @type {InstanceType} */ this.Y;
}
/** @returns {P} */
static createGrammar() {
return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 1).label("RotatorEntity")
}
getRoll() {
return this.R
}
getPitch() {
return this.P
}
getYaw() {
return this.Y
}
}
class SimpleSerializationRotatorEntity extends RotatorEntity {
static attributeSeparator = ", "
static grammar = this.createGrammar()
/** @returns {P} */
static createGrammar() {
return Parsernostrum.alt(
Parsernostrum.regArray(new RegExp(
`(${NumberEntity.numberRegexSource})`
+ String.raw`\s*,\s*`
+ `(${NumberEntity.numberRegexSource})`
+ String.raw`\s*,\s*`
+ `(${NumberEntity.numberRegexSource})`
)).map(([_, p, pPrecision, y, yPrecision, r, rPrecision]) => new this({
R: new (RotatorEntity.attributes.R)(r, rPrecision?.length),
P: new (RotatorEntity.attributes.P)(p, pPrecision?.length),
Y: new (RotatorEntity.attributes.Y)(y, yPrecision?.length),
})),
RotatorEntity.grammar.map(v => new this({
R: v.R,
P: v.P,
Y: v.Y,
}))
).label("SimpleSerializationRotatorEntity")
}
doSerialize() {
const attributeSeparator = /** @type {typeof SimpleSerializationRotatorEntity} */(
this.constructor
).attributeSeparator;
return this.P.serialize() + attributeSeparator
+ this.Y.serialize() + attributeSeparator
+ this.R.serialize() + (this.trailing ? attributeSeparator : "")
}
}
class SimpleSerializationVector2DEntity extends Vector2DEntity {
static attributeSeparator = ", "
static grammar = this.createGrammar()
/** @returns {P} */
static createGrammar() {
return Parsernostrum.alt(
Parsernostrum.regArray(new RegExp(
`(${NumberEntity.numberRegexSource})`
+ String.raw`\s*,\s*`
+ `(${NumberEntity.numberRegexSource})`
)).map(([_, x, xPrecision, y, yPrecision]) => new this({
X: new (Vector2DEntity.attributes.X)(x, xPrecision?.length),
Y: new (Vector2DEntity.attributes.Y)(y, yPrecision?.length),
})),
Vector2DEntity.grammar.map(v => new this({
X: v.X,
Y: v.Y,
}))
).label("SimpleSerializationVector2DEntity")
}
doSerialize() {
const attributeSeparator = /** @type {typeof SimpleSerializationVector2DEntity} */(
this.constructor
).attributeSeparator;
return this.X.serialize() + attributeSeparator
+ this.Y.serialize() + (this.trailing ? attributeSeparator : "")
}
}
class Vector4DEntity extends IEntity {
static attributes = {
...super.attributes,
X: NumberEntity.withDefault(),
Y: NumberEntity.withDefault(),
Z: NumberEntity.withDefault(),
W: NumberEntity.withDefault(),
}
static grammar = this.createGrammar()
constructor(values) {
super(values);
/** @type {InstanceType} */ this.X;
/** @type {InstanceType} */ this.Y;
/** @type {InstanceType} */ this.Z;
/** @type {InstanceType} */ this.W;
}
/** @returns {P} */
static createGrammar() {
return Grammar.createEntityGrammar(this, Grammar.commaSeparation, 1).label("Vector4DEntity")
}
/** @returns {[Number, Number, Number, Number]} */
toArray() {
return [this.X.valueOf(), this.Y.valueOf(), this.Z.valueOf(), this.W.valueOf()]
}
}
class SimpleSerializationVector4DEntity extends Vector4DEntity {
static grammar = this.createGrammar()
/** @returns {P } */
static createGrammar() {
return Parsernostrum.alt(
Parsernostrum.regArray(new RegExp(
`(${Grammar.numberRegexSource})`
+ String.raw`\s*,\s*`
+ `(${Grammar.numberRegexSource})`
+ String.raw`\s*,\s*`
+ `(${Grammar.numberRegexSource})`
+ String.raw`\s*,\s*`
+ `(${Grammar.numberRegexSource})`
))
.map(([_0, x, y, z, w]) => new this({
X: new (Vector4DEntity.attributes.X)(x),
Y: new (Vector4DEntity.attributes.Y)(y),
Z: new (Vector4DEntity.attributes.Z)(z),
W: new (Vector4DEntity.attributes.W)(w),
})),
Vector4DEntity.grammar
)
}
}
class SimpleSerializationVectorEntity extends VectorEntity {
static allowShortSerialization = false
static attributeSeparator = ", "
static grammar = this.createGrammar()
/** @returns {P} */
static createGrammar() {
return Parsernostrum.alt(
Parsernostrum.regArray(new RegExp(
`(${NumberEntity.numberRegexSource})`
// If allow simple serialization then it can parse only a single number ...
+ (this.allowShortSerialization ? `(?:` : "")
+ String.raw`\s*,\s*`
+ `(${NumberEntity.numberRegexSource})`
+ String.raw`\s*,\s*`
+ `(${NumberEntity.numberRegexSource})`
// ... that will be assigned to X and the rest is optional and set to 0
+ (this.allowShortSerialization ? `)?` : "")
))
.map(([_, x, xPrecision, y, yPrecision, z, zPrecision]) => new this({
X: new (VectorEntity.attributes.X)(x, xPrecision?.length),
Y: new (VectorEntity.attributes.Y)(y, yPrecision?.length),
Z: new (VectorEntity.attributes.Z)(z, zPrecision?.length),
})),
VectorEntity.grammar.map(v => new this({
X: v.X,
Y: v.Y,
Z: v.Z,
}))
)
}
/**
* @template {typeof SimpleSerializationVectorEntity} T
* @this {T}
*/
static flagAllowShortSerialization(value = true) {
const result = this.asUniqueClass();
if (value !== result.allowShortSerialization) {
result.allowShortSerialization = value;
result.grammar = result.createGrammar();
}
return result
}
doSerialize() {
const attributeSeparator = /** @type {typeof SimpleSerializationVectorEntity} */(
this.constructor
).attributeSeparator;
return this.X.serialize() + attributeSeparator
+ this.Y.serialize() + attributeSeparator
+ this.Z.serialize() + (this.trailing ? attributeSeparator : "")
}
}
/** @template {IEntity} T */
class PinEntity extends IEntity {
static lookbehind = "Pin"
static #typeEntityMap = {
"bool": BooleanEntity,
"byte": ByteEntity,
"enum": EnumEntity,
"exec": StringEntity,
"int": IntegerEntity,
"int64": Integer64Entity,
"name": StringEntity,
"real": NumberEntity,
"string": StringEntity,
[Configuration.paths.linearColor]: LinearColorEntity,
[Configuration.paths.niagaraPosition]: VectorEntity,
[Configuration.paths.rotator]: RotatorEntity,
[Configuration.paths.vector]: VectorEntity,
[Configuration.paths.vector2D]: Vector2DEntity,
[Configuration.paths.vector4f]: Vector4DEntity,
}
static #alternativeTypeEntityMap = {
"enum": EnumDisplayValueEntity,
"rg": RBSerializationVector2DEntity,
[Configuration.paths.niagaraPosition]: SimpleSerializationVectorEntity.flagAllowShortSerialization(),
[Configuration.paths.rotator]: SimpleSerializationRotatorEntity,
[Configuration.paths.vector]: SimpleSerializationVectorEntity,
[Configuration.paths.vector2D]: SimpleSerializationVector2DEntity,
[Configuration.paths.vector3f]: SimpleSerializationVectorEntity,
[Configuration.paths.vector4f]: SimpleSerializationVector4DEntity,
}
static attributes = {
PinId: GuidEntity.withDefault(),
PinName: StringEntity.withDefault(),
PinFriendlyName: AlternativesEntity.accepting(
LocalizedTextEntity,
FormatTextEntity,
InvariantTextEntity,
StringEntity
),
PinToolTip: StringEntity,
Direction: StringEntity,
PinType: PinTypeEntity.withDefault().flagInlined(),
LinkedTo: ArrayEntity.of(PinReferenceEntity).withDefault().flagSilent(),
SubPins: ArrayEntity.of(PinReferenceEntity),
ParentPin: PinReferenceEntity,
DefaultValue:
ComputedTypeEntity.from(
/** @param {PinEntity} pinEntity */
pinEntity => pinEntity.getEntityType(true)?.flagSerialized() ?? StringEntity
),
AutogeneratedDefaultValue: StringEntity,
DefaultObject: ObjectReferenceEntity,
PersistentGuid: GuidEntity,
bHidden: BooleanEntity,
bNotConnectable: BooleanEntity,
bDefaultValueIsReadOnly: BooleanEntity,
bDefaultValueIsIgnored: BooleanEntity,
bAdvancedView: BooleanEntity,
bOrphanedPin: BooleanEntity,
}
static grammar = this.createGrammar()
#recomputesNodeTitleOnChange = false
set recomputesNodeTitleOnChange(value) {
this.#recomputesNodeTitleOnChange = value;
}
get recomputesNodeTitleOnChange() {
return this.#recomputesNodeTitleOnChange
}
/** @type {ObjectEntity} */
#objectEntity = null
get objectEntity() {
try {
/*
* Why inside a try block ?
* It is because of this issue: https://stackoverflow.com/questions/61237153/access-private-method-in-an-overriden-method-called-from-the-base-class-construc
* super(values) will call IEntity constructor while this instance is not yet fully constructed
* IEntity will call computedEntity.compute(this) to initialize DefaultValue from this class
* Which in turn calls pinEntity.getEntityType(true)
* Which calls this.getType()
* Which calls this.objectEntity?.isPcg()
* Which would access #objectEntity through get objectEntity()
* And this would violate the private access rule (because this class is not yet constructed)
* If this issue in the future will be fixed in all the major browsers, please remove this try catch
*/
return this.#objectEntity
} catch (e) {
return null
}
}
set objectEntity(value) {
this.#objectEntity = value;
}
#pinIndex
get pinIndex() {
return this.#pinIndex
}
set pinIndex(value) {
this.#pinIndex = value;
}
constructor(values = {}) {
super(values);
/** @type {InstanceType} */ this.PinId;
/** @type {InstanceType} */ this.PinName;
/** @type {InstanceType} */ this.PinFriendlyName;
/** @type {InstanceType} */ this.PinToolTip;
/** @type {InstanceType} */ this.Direction;
/** @type {InstanceType} */ this.PinType;
/** @type {InstanceType} */ this.LinkedTo;
/** @type {T} */ this.DefaultValue;
/** @type {InstanceType} */ this.AutogeneratedDefaultValue;
/** @type {InstanceType} */ this.DefaultObject;
/** @type {InstanceType} */ this.PersistentGuid;
/** @type {InstanceType} */ this.bHidden;
/** @type {InstanceType} */ this.bNotConnectable;
/** @type {InstanceType} */ this.bDefaultValueIsReadOnly;
/** @type {InstanceType} */ this.bDefaultValueIsIgnored;
/** @type {InstanceType} */ this.bAdvancedView;
/** @type {InstanceType} */ this.bOrphanedPin;
/** @type {ObjectEntity} */ this.objectEntity;
}
/** @returns {P} */
static createGrammar() {
return Grammar.createEntityGrammar(this)
}
/** @param {ObjectEntity} objectEntity */
static fromLegacyObject(objectEntity) {
return new PinEntity(objectEntity)
}
getType() {
const category = this.PinType.PinCategory?.toString().toLocaleLowerCase();
if (category === "struct" || category === "class" || category === "object" || category === "type") {
return this.PinType.PinSubCategoryObject?.path
}
if (this.isEnum()) {
return "enum"
}
if (this.objectEntity?.isPcg()) {
const pcgSuboject = this.objectEntity.getPcgSubobject();
const pinObjectReference = this.isInput()
? pcgSuboject.InputPins?.valueOf()[this.pinIndex]
: pcgSuboject.OutputPins?.valueOf()[this.pinIndex];
if (pinObjectReference) {
/** @type {ObjectEntity} */
const pinObject = pcgSuboject[Configuration.subObjectAttributeNameFromReference(pinObjectReference, true)];
let allowedTypes = pinObject.Properties?.AllowedTypes?.toString() ?? "";
if (allowedTypes == "") {
allowedTypes = this.PinType.PinCategory ?? "";
if (allowedTypes == "") {
allowedTypes = "Any";
}
}
if (allowedTypes) {
if (
pinObject.Properties.bAllowMultipleData?.valueOf() !== false
&& pinObject.Properties.bAllowMultipleConnections?.valueOf() !== false
) {
allowedTypes += "[]";
}
return allowedTypes
}
}
}
if (category === "optional") {
const subCategory = this.PinType.PinSubCategory?.toString();
switch (subCategory) {
case "red":
return "real"
case "rg":
return "rg"
case "rgb":
return Configuration.paths.vector
case "rgba":
return Configuration.paths.linearColor
default:
return subCategory
}
}
return category
}
/** @returns {typeof IEntity} */
getEntityType(alternative = false) {
const type = this.getType();
const entity = PinEntity.#typeEntityMap[type];
const alternativeEntity = PinEntity.#alternativeTypeEntityMap[type];
return alternative && alternativeEntity !== undefined
? alternativeEntity
: entity
}
pinTitle() {
return pinTitle(this)
}
/** @param {PinEntity} other */
copyTypeFrom(other) {
this.PinType = other.PinType;
}
getDefaultValue(maybeCreate = false) {
if (this.DefaultValue === undefined && maybeCreate) {
this.DefaultValue = /** @type {T} */(new (this.getEntityType(true))());
}
return this.DefaultValue
}
isEnum() {
const type = this.PinType.PinSubCategoryObject?.type;
return type === Configuration.paths.enum
|| type === Configuration.paths.userDefinedEnum
|| type?.toLowerCase() === "enum"
}
isExecution() {
return this.PinType.PinCategory.toString() === "exec"
}
isHidden() {
return this.bHidden?.valueOf()
}
isInput() {
return !this.isHidden() && this.Direction?.toString() != "EGPD_Output"
}
isOutput() {
return !this.isHidden() && this.Direction?.toString() == "EGPD_Output"
}
isLinked() {
return this.LinkedTo?.length > 0 ?? false
}
/**
* @param {String} targetObjectName
* @param {PinEntity} targetPinEntity
* @returns true if it was not already linked to the tarket
*/
linkTo(targetObjectName, targetPinEntity) {
const linkFound = this.LinkedTo.values?.some(pinReferenceEntity =>
pinReferenceEntity.objectName.toString() == targetObjectName
&& pinReferenceEntity.pinGuid.toString() == targetPinEntity.PinId.toString()
);
if (!linkFound) {
this.LinkedTo.values.push(new PinReferenceEntity(new SymbolEntity(targetObjectName), targetPinEntity.PinId));
return true
}
return false // Already linked
}
/**
* @param {String} targetObjectName
* @param {PinEntity} targetPinEntity
* @returns true if it was linked to the target
*/
unlinkFrom(targetObjectName, targetPinEntity) {
const indexElement = this.LinkedTo.values?.findIndex(pinReferenceEntity => {
return pinReferenceEntity.objectName.toString() == targetObjectName
&& pinReferenceEntity.pinGuid.toString() == targetPinEntity.PinId.toString()
});
if (indexElement >= 0) {
this.LinkedTo.values.splice(indexElement, 1);
if (this.LinkedTo.length === 0 && PinEntity.attributes.LinkedTo.default === undefined) {
this.LinkedTo.values = [];
}
return true
}
return false
}
getSubCategory() {
return this.PinType.PinSubCategoryObject?.path
}
pinColor() {
return pinColor(this)
}
}
/** @param {PinEntity} pinEntity */
const indexFromUpperCaseLetterName = pinEntity =>
pinEntity.PinName?.toString().match(/^\s*([A-Z])\s*$/)?.[1]?.charCodeAt(0) - "A".charCodeAt(0);
/** @param {ObjectEntity} entity */
function nodeVariadic(entity) {
/** @type {() => PinEntity[]} */
let pinEntities;
/** @type {(pinEntity: PinEntity) => Number} */
let pinIndexFromEntity;
/** @type {(newPinIndex: Number, minIndex: Number, maxIndex: Number, newPin: PinEntity) => String} */
let pinNameFromIndex;
const type = entity.getType();
let prefix;
let name;
switch (type) {
case Configuration.paths.commutativeAssociativeBinaryOperator:
case Configuration.paths.promotableOperator:
name = entity.FunctionReference?.MemberName?.toString();
switch (name) {
default:
if (
!name?.startsWith("Add_")
&& !name?.startsWith("Subtract_")
&& !name?.startsWith("Multiply_")
&& !name?.startsWith("Divide_")
) {
break
}
case "And_Int64Int64":
case "And_IntInt":
case "BMax":
case "BMin":
case "BooleanAND":
case "BooleanNAND":
case "BooleanOR":
case "Concat_StrStr":
case "FMax":
case "FMin":
case "Max":
case "MaxInt64":
case "Min":
case "MinInt64":
case "Or_Int64Int64":
case "Or_IntInt":
pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isInput());
pinIndexFromEntity ??= indexFromUpperCaseLetterName;
pinNameFromIndex ??= (index, min = -1, max = -1) => {
const result = String.fromCharCode(index >= 0 ? index : max + "A".charCodeAt(0) + 1);
entity.NumAdditionalInputs = new NaturalNumberEntity(pinEntities().length - 1);
return result
};
break
}
break
case Configuration.paths.executionSequence:
prefix ??= "Then";
case Configuration.paths.multiGate:
prefix ??= "Out";
pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isOutput());
pinIndexFromEntity ??= pinEntity => Number(
pinEntity.PinName?.toString().match(new RegExp(String.raw`^\s*${prefix}[_\s]+(\d+)\s*$`, "i"))?.[1]
);
pinNameFromIndex ??= (index, min = -1, max = -1, newPin) =>
`${prefix} ${index >= 0 ? index : min > 0 ? `${prefix} 0` : max + 1}`;
break
// case Configuration.paths.niagaraNodeOp:
// pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isInput())
// pinIndexFromEntity ??= indexFromUpperCaseLetterName
// pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => {
// const result = String.fromCharCode(index >= 0 ? index : max + "A".charCodeAt(0) + 1)
// entity.AddedPins ??= []
// entity.AddedPins.push(newPin)
// return result
// }
// break
case Configuration.paths.switchInteger:
pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isOutput());
pinIndexFromEntity ??= pinEntity => Number(pinEntity.PinName?.toString().match(/^\s*(\d+)\s*$/)?.[1]);
pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => (index < 0 ? max + 1 : index).toString();
break
case Configuration.paths.switchGameplayTag:
pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => {
const result = `Case_${index >= 0 ? index : min > 0 ? "0" : max + 1}`;
entity.PinNames ??= new ArrayEntity();
entity.PinNames.valueOf().push(new StringEntity(result));
delete entity.PinTags.valueOf()[entity.PinTags.length - 1];
entity.PinTags.valueOf()[entity.PinTags.length] = null;
return result
};
case Configuration.paths.switchName:
case Configuration.paths.switchString:
pinEntities ??= () => entity.getPinEntities().filter(pinEntity => pinEntity.isOutput());
pinIndexFromEntity ??= pinEntity => Number(pinEntity.PinName.toString().match(/^\s*Case[_\s]+(\d+)\s*$/i)?.[1]);
pinNameFromIndex ??= (index, min = -1, max = -1, newPin) => {
const result = `Case_${index >= 0 ? index : min > 0 ? "0" : max + 1}`;
entity.PinNames ??= new ArrayEntity();
entity.PinNames.valueOf().push(new StringEntity(result));
return result
};
break
}
if (pinEntities) {
return () => {
let min = Number.MAX_SAFE_INTEGER;
let max = Number.MIN_SAFE_INTEGER;
let values = [];
const modelPin = pinEntities().reduce(
(acc, cur) => {
const value = pinIndexFromEntity(cur);
if (!isNaN(value)) {
values.push(value);
min = Math.min(value, min);
if (value > max) {
max = value;
return cur
}
} else if (acc === undefined) {
return cur
}
return acc
},
undefined
);
if (min === Number.MAX_SAFE_INTEGER || max === Number.MIN_SAFE_INTEGER) {
min = undefined;
max = undefined;
}
if (!modelPin) {
return null
}
values.sort((a, b) => a < b ? -1 : a === b ? 0 : 1);
let prev = values[0];
let index = values.findIndex(
// Search for a gap
value => {
const result = value - prev > 1;
prev = value;
return result
}
);
const newPin = new PinEntity(modelPin);
newPin.PinId = new GuidEntity();
newPin.PinName = new StringEntity(pinNameFromIndex(index, min, max, newPin));
newPin.PinToolTip = undefined;
if (newPin.DefaultValue) {
// @ts-expect-error
newPin.DefaultValue = new (newPin.DefaultValue.constructor)();
}
entity.getCustomproperties(true).push(newPin);
return newPin
}
}
}
class MacroGraphReferenceEntity extends IEntity {
static attributes = {
...super.attributes,
MacroGraph: ObjectReferenceEntity,
GraphBlueprint: ObjectReferenceEntity,
GraphGuid: GuidEntity,
}
static grammar = this.createGrammar()
constructor(values) {
super(values);
/** @type {InstanceType} */ this.MacroGraph;
/** @type {InstanceType} */ this.GraphBlueprint;
/** @type {InstanceType} */ this.GraphGuid;
}
/** @returns {P} */
static createGrammar() {
return Grammar.createEntityGrammar(this)
}
getMacroName() {
const colonIndex = this.MacroGraph.path.search(":");
return this.MacroGraph.path.substring(colonIndex + 1)
}
}
class NullEntity extends IEntity {
static grammar = this.createGrammar()
/** @returns {P} */
static createGrammar() {
// @ts-expect-error
return Parsernostrum.reg(new RegExp(String.raw`\(${Parsernostrum.whitespaceInlineOpt.getParser().regexp.source}\)`))
.map(v => new this())
.label("NullEntity")
}
serialize(
insideString = false,
indentation = "",
Self = /** @type {typeof IEntity} */(this.constructor)
) {
let result = "()";
if (Self.serialized) {
result = `"${result}"`;
}
return result
}
}
class ScriptVariableEntity extends IEntity {
static attributes = {
...super.attributes,
ScriptVariable: ObjectReferenceEntity,
OriginalChangeId: GuidEntity,
}
static grammar = this.createGrammar()
constructor(values = {}) {
super(values);
/** @type {InstanceType} */ this.ScriptVariable;
/** @type {InstanceType} */ this.OriginalChangeId;
}
/** @returns {P} */
static createGrammar() {
return Grammar.createEntityGrammar(this).label("ScriptVariableEntity")
}
}
class UnknownPinEntity extends PinEntity {
static attributes = {
...super.attributes,
PinId: GuidEntity
}
static grammar = this.createGrammar()
/** @returns {P} */
static createGrammar() {
return Parsernostrum.seq(
// Lookbehind
Parsernostrum.reg(new RegExp(`(${Grammar.Regex.Symbol.source}\\s*)\\(\\s*`), 1),
Grammar.createAttributeGrammar(this).sepBy(Grammar.commaSeparation),
Parsernostrum.reg(/\s*(?:,\s*)?\)/)
).map(([lookbehind, attributes, _2]) => {
lookbehind ??= "";
let values = {};
if (lookbehind.length) {
values.lookbehind = lookbehind;
}
attributes.forEach(attributeSetter => attributeSetter(values));
return new this(values)
}).label("UnknownPinEntity")
}
}
class VariableReferenceEntity extends IEntity {
static attributes = {
...super.attributes,
MemberScope: StringEntity,
MemberName: StringEntity.withDefault(),
MemberGuid: GuidEntity,
bSelfContext: BooleanEntity,
}
static grammar = this.createGrammar()
constructor(values) {
super(values);
/** @type {InstanceType} */ this.MemberScope;
/** @type {InstanceType} */ this.MemberName;
/** @type {InstanceType} */ this.MemberGuid;
/** @type {InstanceType} */ this.bSelfContext;
}
/** @returns {P} */
static createGrammar() {
return Grammar.createEntityGrammar(this).label("VariableReferenceEntity")
}
}
class ObjectEntity extends IEntity {
#exported = false
get exported() {
return this.#exported
}
set exported(value) {
this.#exported = value;
}
static #nameRegex = /^(\w+?)(?:_(\d+))?$/
/** @type {(k: String) => String} */
static printKey = k => !k.startsWith(Configuration.subObjectAttributeNamePrefix) ? k : ""
static attributeSeparator = "\n"
static wrap = this.notWrapped
static trailing = true
static attributes = {
...super.attributes,
Class: ObjectReferenceEntity,
Name: StringEntity,
Archetype: ObjectReferenceEntity,
ExportPath: ObjectReferenceEntity,
ObjectRef: ObjectReferenceEntity,
BlueprintElementType: ObjectReferenceEntity,
BlueprintElementInstance: ObjectReferenceEntity,
ConstA: MirroredEntity.of(NumberEntity),
ConstB: MirroredEntity.of(NumberEntity),
PinTags: ArrayEntity.of(NullEntity).flagInlined(),
PinNames: ArrayEntity.of(StringEntity).flagInlined(),
AxisKey: SymbolEntity,
InputAxisKey: SymbolEntity,
InputName: StringEntity,
InputType: SymbolEntity,
NumAdditionalInputs: NaturalNumberEntity,
bIsPureFunc: BooleanEntity,
bIsConstFunc: BooleanEntity,
bIsCaseSensitive: BooleanEntity,
VariableReference: VariableReferenceEntity,
SelfContextInfo: SymbolEntity,
DelegatePropertyName: StringEntity,
DelegateOwnerClass: ObjectReferenceEntity,
ComponentPropertyName: StringEntity,
EventReference: FunctionReferenceEntity,
FunctionReference: FunctionReferenceEntity,
FunctionScript: ObjectReferenceEntity,
CustomFunctionName: StringEntity,
TargetType: ObjectReferenceEntity,
MacroGraphReference: MacroGraphReferenceEntity,
Enum: ObjectReferenceEntity,
EnumEntries: ArrayEntity.of(StringEntity).flagInlined(),
InputKey: SymbolEntity,
OpName: StringEntity,
CachedChangeId: GuidEntity,
FunctionDisplayName: StringEntity,
AddedPins: ArrayEntity.of(UnknownPinEntity).withDefault().flagInlined().flagSilent(),
ChangeId: GuidEntity,
MaterialFunction: ObjectReferenceEntity,
bOverrideFunction: BooleanEntity,
bInternalEvent: BooleanEntity,
bConsumeInput: BooleanEntity,
bExecuteWhenPaused: BooleanEntity,
bOverrideParentBinding: BooleanEntity,
bControl: BooleanEntity,
bAlt: BooleanEntity,
bShift: BooleanEntity,
bCommand: BooleanEntity,
CommentColor: LinearColorEntity,
bCommentBubbleVisible_InDetailsPanel: BooleanEntity,
bColorCommentBubble: BooleanEntity,
ProxyFactoryFunctionName: StringEntity,
ProxyFactoryClass: ObjectReferenceEntity,
ProxyClass: ObjectReferenceEntity,
StructType: ObjectReferenceEntity,
MaterialExpression: ObjectReferenceEntity,
MaterialExpressionComment: ObjectReferenceEntity,
MoveMode: SymbolEntity,
TimelineName: StringEntity,
TimelineGuid: GuidEntity,
SizeX: MirroredEntity.of(IntegerEntity),
SizeY: MirroredEntity.of(IntegerEntity),
Text: MirroredEntity.of(StringEntity),
MaterialExpressionEditorX: MirroredEntity.of(IntegerEntity),
MaterialExpressionEditorY: MirroredEntity.of(IntegerEntity),
NodeTitle: StringEntity,
NodeTitleColor: LinearColorEntity,
PositionX: MirroredEntity.of(IntegerEntity),
PositionY: MirroredEntity.of(IntegerEntity),
SettingsInterface: ObjectReferenceEntity,
PCGNode: ObjectReferenceEntity,
HiGenGridSize: SymbolEntity,
Operation: SymbolEntity,
NodePosX: IntegerEntity,
NodePosY: IntegerEntity,
NodeHeight: IntegerEntity,
NodeWidth: IntegerEntity,
Graph: ObjectReferenceEntity,
SubgraphInstance: StringEntity,
InputPins: ArrayEntity.of(ObjectReferenceEntity).flagInlined(),
OutputPins: ArrayEntity.of(ObjectReferenceEntity).flagInlined(),
bExposeToLibrary: BooleanEntity,
bCanRenameNode: BooleanEntity,
bCommentBubblePinned: BooleanEntity,
bCommentBubbleVisible: BooleanEntity,
NodeComment: StringEntity,
AdvancedPinDisplay: SymbolEntity,
DelegateReference: VariableReferenceEntity,
EnabledState: SymbolEntity,
NodeGuid: GuidEntity,
ErrorType: IntegerEntity,
ErrorMsg: StringEntity,
ScriptVariables: ArrayEntity.of(ScriptVariableEntity),
Node: MirroredEntity.of(ObjectReferenceEntity),
ExportedNodes: StringEntity,
CustomProperties: ArrayEntity.of(AlternativesEntity.accepting(PinEntity, UnknownPinEntity)).withDefault().flagSilent(),
}
static customPropertyGrammar = Parsernostrum.seq(
Parsernostrum.reg(/CustomProperties\s+/),
this.attributes.CustomProperties.type.grammar,
).map(([_0, pin]) => values => {
/** @type {InstanceType} */(
values.CustomProperties ??= new (this.attributes.CustomProperties)()
).values.push(pin);
})
static inlinedArrayEntryGrammar = Parsernostrum.seq(
Parsernostrum.alt(
Grammar.symbolQuoted.map(v => [v, true]),
Grammar.symbol.map(v => [v, false]),
),
Parsernostrum.reg(new RegExp(String.raw`\s*\(\s*(\d+)\s*\)\s*\=\s*`), 1).map(Number)
)
.chain(
/** @param {[[keyof ObjectEntity.attributes, Boolean], Number]} param */
([[symbol, quoted], index]) =>
(this.attributes[symbol]?.grammar ?? IEntity.unknownEntityGrammar).map(currentValue =>
values => {
if (values[symbol] === undefined) {
let arrayEntity = ArrayEntity;
if (quoted != arrayEntity.quoted) {
arrayEntity = arrayEntity.flagQuoted(quoted);
}
if (!arrayEntity.inlined) {
arrayEntity = arrayEntity.flagInlined();
}
values[symbol] = new arrayEntity();
}
/** @type {ArrayEntity} */
const target = values[symbol];
target.values[index] = currentValue;
}
)
)
static grammar = this.createGrammar()
static grammarMultipleObjects = Parsernostrum.seq(
Parsernostrum.whitespaceOpt,
this.grammar,
Parsernostrum.seq(
Parsernostrum.whitespace,
this.grammar,
)
.map(([_0, object]) => object)
.many(),
Parsernostrum.whitespaceOpt
).map(([_0, first, remaining, _4]) => [first, ...remaining])
constructor(values = {}) {
if (("NodePosX" in values) !== ("NodePosY" in values)) {
const entries = Object.entries(values);
const [key, position] = "NodePosX" in values
? ["NodePosY", Object.keys(values).indexOf("NodePosX") + 1]
: ["NodePosX", Object.keys(values).indexOf("NodePosY")];
entries.splice(position, 0, [key, new IntegerEntity(0)]);
values = Object.fromEntries(entries);
}
super(values);
// Attributes
/** @type {InstanceType} */ this.AddedPins;
/** @type {InstanceType} */ this.AdvancedPinDisplay;
/** @type {InstanceType} */ this.Archetype;
/** @type {InstanceType} */ this.AxisKey;
/** @type {InstanceType} */ this.bIsPureFunc;
/** @type {InstanceType} */ this.BlueprintElementInstance;
/** @type {InstanceType} */ this.ConstA;
/** @type {InstanceType} */ this.ConstB;
/** @type {InstanceType} */ this.BlueprintElementType;
/** @type {InstanceType} */ this.Class;
/** @type {InstanceType} */ this.CommentColor;
/** @type {InstanceType} */ this.ComponentPropertyName;
/** @type {InstanceType} */ this.CustomFunctionName;
/** @type {ArrayEntity} */ this.CustomProperties;
/** @type {InstanceType} */ this.DelegatePropertyName;
/** @type {InstanceType} */ this.DelegateReference;
/** @type {InstanceType} */ this.EnabledState;
/** @type {InstanceType} */ this.Enum;
/** @type {InstanceType} */ this.EnumEntries;
/** @type {InstanceType} */ this.EventReference;
/** @type {InstanceType} */ this.ExportedNodes;
/** @type {InstanceType} */ this.ExportPath;
/** @type {InstanceType} */ this.FunctionDisplayName;
/** @type {InstanceType} */ this.FunctionReference;
/** @type {InstanceType} */ this.FunctionScript;
/** @type {InstanceType} */ this.Graph;
/** @type {InstanceType} */ this.HiGenGridSize;
/** @type {InstanceType} */ this.InputAxisKey;
/** @type {InstanceType} */ this.InputKey;
/** @type {InstanceType} */ this.InputName;
/** @type {InstanceType} */ this.InputPins;
/** @type {InstanceType} */ this.InputType;
/** @type {InstanceType} */ this.MacroGraphReference;
/** @type {InstanceType} */ this.MaterialExpression;
/** @type {InstanceType} */ this.MaterialExpressionComment;
/** @type {InstanceType} */ this.MaterialExpressionEditorX;
/** @type {InstanceType} */ this.MaterialExpressionEditorY;
/** @type {InstanceType} */ this.MaterialFunction;
/** @type {InstanceType} */ this.Name;
/** @type {InstanceType} */ this.Node;
/** @type {InstanceType} */ this.NodeComment;
/** @type {InstanceType} */ this.NodeHeight;
/** @type {InstanceType} */ this.NodePosX;
/** @type {InstanceType} */ this.NodePosY;
/** @type {InstanceType} */ this.NodeTitle;
/** @type {InstanceType} */ this.NodeTitleColor;
/** @type {InstanceType} */ this.NodeWidth;
/** @type {InstanceType} */ this.NumAdditionalInputs;
/** @type {InstanceType} */ this.ObjectRef;
/** @type {InstanceType} */ this.Operation;
/** @type {InstanceType} */ this.OpName;
/** @type {InstanceType} */ this.OutputPins;
/** @type {InstanceType} */ this.PCGNode;
/** @type {InstanceType} */ this.PinTags;
/** @type {InstanceType} */ this.PinNames;
/** @type {InstanceType} */ this.PositionX;
/** @type {InstanceType} */ this.PositionY;
/** @type {InstanceType} */ this.ProxyFactoryFunctionName;
/** @type {InstanceType} */ this.ScriptVariables;
/** @type {InstanceType} */ this.SettingsInterface;
/** @type {InstanceType} */ this.SizeX;
/** @type {InstanceType} */ this.SizeY;
/** @type {InstanceType} */ this.StructType;
/** @type {InstanceType} */ this.SubgraphInstance;
/** @type {InstanceType} */ this.TargetType;
/** @type {InstanceType