easy-keyvalues
Version:
Parse Valve KeyValues Format and easy to use in nodejs or browser.
1,680 lines (1,675 loc) • 65.8 kB
JavaScript
'use strict';
var fs = require('fs');
var path = require('path');
class KeyValuesComments {
constructor(comments = [], endOfLineComment = '') {
this.comments = comments;
this.endOfLineComment = endOfLineComment;
}
GetComments() {
return this.comments;
}
HasComments() {
return this.comments.length > 0;
}
SetComments(list) {
for (const v of list) {
if (v.includes('\n')) {
throw Error('The comment only allowed one line');
}
}
this.comments = list;
return this;
}
AppendComment(text) {
if (text.includes('\n')) {
throw Error('The comment only allowed one line');
}
this.comments.push(text);
return this;
}
SetEndOfLineComment(text) {
if (text.includes('\n')) {
throw Error('The comment only allowed one line');
}
this.endOfLineComment = text;
return this;
}
GetEndOfLineComment() {
return this.endOfLineComment;
}
HasEndOfLineComment() {
return this.endOfLineComment.length > 0;
}
}
class KeyValues3Comments {
constructor(comments = [], endOfLineComment = '') {
this.comments = comments;
this.endOfLineComment = endOfLineComment;
}
GetComments() {
return this.comments;
}
HasComments() {
return this.comments.length > 0;
}
SetComments(list) {
this.comments = list;
return this;
}
AppendComment(text) {
this.comments.push(text);
return this;
}
SetEndOfLineComment(text) {
if (text.includes('\n')) {
throw Error('The end of line comment only allowed one line');
}
this.endOfLineComment = text;
return this;
}
GetEndOfLineComment() {
return this.endOfLineComment;
}
HasEndOfLineComment() {
return this.endOfLineComment.length > 0;
}
Format(tab = '') {
let text = '';
for (const comment of this.comments) {
if (comment.includes('\n')) {
const lines = comment.split('\n').map((v) => v.trimStart());
text += `${tab}/*\n`;
if (lines.some((v) => v.startsWith('*'))) {
text += lines
.map((v) => {
if (v.startsWith('*')) {
v = v.trimStart();
}
else {
v = '* ' + v.trimStart();
}
return `${tab} ` + v + '\n';
})
.join('');
text += `${tab} */\n`;
}
else {
text += lines.map((v) => `${tab}` + v + '\n').join('');
text += `${tab}*/\n`;
}
}
else {
text += `${tab}// ${comment.trimStart()}\n`;
}
}
return text;
}
}
let defaultAdapter = {
async readFile() {
return '';
},
async writeFile() { },
resolvePath(filename, basePath) {
return filename + basePath;
},
createKeyValuesID() {
return '';
},
};
function setKeyValuesAdapter(adapter) {
defaultAdapter = adapter;
}
function getKeyValuesAdapter() {
return defaultAdapter;
}
const KeyValuesRootKey = '__KeyValues_Root__';
function createID$1() {
const adapter = getKeyValuesAdapter();
return adapter.createKeyValuesID();
}
class KeyValues {
get filename() {
return this.__filename || '';
}
set filename(s) {
this.__filename = s ? s.replace(/\\/g, '/') : s;
}
constructor(Key, defaultValue) {
this.Key = Key;
/**
* Comment
*/
this.Comments = new KeyValuesComments();
/**
* The KeyValues flags, such as [$WIN32] [$X360]
*/
this.Flags = '';
/**
* Unique id of KeyValues
*/
this.ID = createID$1();
this.baseFilePath = '';
this.SetValue(defaultValue || '');
}
/**
* The parent of this KeyValues
*/
GetParent() {
return this.parent;
}
/**
* Return true that the KeyValues is root.
*/
IsRoot() {
return this.Key === KeyValuesRootKey;
}
/**
* The key is #base?
*/
IsBase() {
return this.Key === '#base';
}
/**
* Return #base's value
*/
GetBaseFilePath() {
return this.baseFilePath;
}
/**
* KeyValues list of #base
*/
GetBaseList() {
return this.FindAllKeys('#base');
}
/**
* The children of this KeyValues,
* if no children then return empty array.
*/
GetChildren() {
return this.children || [];
}
GetChildCount() {
return this.GetChildren().length;
}
GetFirstChild() {
return this.GetChildren()[0];
}
GetLastChild() {
return this.GetChildren()[this.GetChildCount() - 1];
}
/**
* Create a KeyValues to children and return it.
*/
CreateChild(key, value) {
const kv = new KeyValues(key, value);
this.Append(kv);
return kv;
}
/**
* The value of this KeyValues,
* if no value then return empty string.
*/
GetValue() {
return this.value || '';
}
/**
* Return true that the KeyValues exists children and no value.
*/
HasChildren() {
return !!this.children;
}
/**
* Set value or children.
*/
SetValue(v) {
if (Array.isArray(v)) {
this.children = v.map((c) => c.Free());
delete this.value;
for (const kv of this.children) {
if (kv === this) {
throw new Error(`SetValue(): The value can not includes self`);
}
kv.parent = this;
}
}
else {
if (this.IsRoot()) {
throw new Error('The value of the root node kv must be an array');
}
this.value = v;
delete this.children;
}
return this;
}
/**
* Append a KeyValues to children,
* if no children then throw error.
*/
Append(child) {
if (this.children) {
if (child === this) {
throw new Error(`Append(): Can not append self`);
}
this.children.push(child.Free());
child.parent = this;
}
else {
throw new Error(`The KeyValues [Key = ${this.Key}] does not have children`);
}
return this;
}
/**
* Insert a KeyValues to children,
* if no children then throw error.
*/
Insert(child, index) {
if (this.children) {
if (child === this) {
throw new Error(`Insert(): Can not insert self`);
}
this.children.splice(index, 0, child.Free());
child.parent = this;
}
else {
throw new Error(`The KeyValues [Key = ${this.Key}] does not have children`);
}
return this;
}
/**
* Find a KeyValues from children
*/
Find(callback) {
if (!this.children) {
return;
}
for (const [i, kv] of this.children.entries()) {
if (callback(kv, i, this) === true) {
return kv;
}
}
}
/**
* Find all KeyValues from children
*/
FindAll(callback) {
if (!this.children) {
return [];
}
const result = [];
for (const [i, kv] of this.children.entries()) {
if (callback(kv, i, this) === true) {
result.push(kv);
}
}
return result;
}
/**
* Find a KeyValues
*/
FindKey(key) {
return this.Find((kv) => kv.Key === key);
}
/**
* Find all KeyValues
*/
FindAllKeys(...keys) {
return this.FindAll((kv) => keys.includes(kv.Key));
}
/**
* Find a KeyValues from children and children's children...
*/
FindTraverse(callback) {
if (!this.children) {
return;
}
return KeyValues.FindTraverse(this, callback);
}
/**
* Find a KeyValues from children and children's children...
*/
static FindTraverse(root, callback) {
if (root.HasChildren()) {
for (const [i, kv] of root.GetChildren().entries()) {
if (callback(kv, i, root) === true) {
return kv;
}
const result = KeyValues.FindTraverse(kv, callback);
if (result) {
return result;
}
}
}
}
/**
* Find child from the current KeyValues
*/
FindID(id) {
return this.Find((kv) => kv.ID === id);
}
/**
* Recursively iterate through all children to find the value that matches the ID
*/
FindIDTraverse(id) {
return this.FindTraverse((kv) => kv.ID === id);
}
/**
* Delete a KeyValues from children
*/
Delete(child) {
if (!this.children) {
return;
}
let kv;
if (typeof child === 'string') {
kv = this.children.find((v) => v.Key === child);
}
else {
kv = this.children.find((v) => v === child);
}
if (kv) {
this.children = this.children.filter((v) => v !== kv);
kv.Free();
}
return kv;
}
/**
* Delete this KeyValues from parent
*/
Free() {
if (this.parent) {
this.parent.Delete(this);
delete this.parent;
}
return this;
}
/**
* Format KeyValues to file text
*/
Format(tab = '', maxLength = -1) {
if (this.IsRoot()) {
if (!this.children) {
throw new Error('The value of the root node kv must be an array');
}
return this.children.map((v) => v.Format()).join('\n');
}
let text = '';
if (this.Comments.HasComments()) {
text += this.Comments.GetComments()
.map((v) => `${tab}// ${v.trimStart()}\n`)
.join('');
}
if (this.children) {
if (this.IsBase()) {
text += `${tab}${this.Key} "${this.GetBaseFilePath()}"`;
if (this.Comments.HasEndOfLineComment()) {
text += ` // ${this.Comments.GetEndOfLineComment()}`;
}
return text;
}
const maxLength = Math.max(...this.children.map((v) => v.Key.length));
text += `${tab}"${this.Key}"`;
if (this.Comments.HasEndOfLineComment()) {
text += ` // ${this.Comments.GetEndOfLineComment()}`;
}
text += `\n${tab}{`;
for (const kv of this.children) {
text += '\n' + kv.Format(tab + ' ', maxLength);
}
text += `\n${tab}}`;
}
else {
if (this.IsBase()) {
text += `${tab}${this.Key} "${this.value}"`;
if (this.Comments.HasEndOfLineComment()) {
text += ` // ${this.Comments.GetEndOfLineComment()}`;
}
}
else {
text += `${tab}"${this.Key}"${' '.repeat(Math.max(0, maxLength - this.Key.length))}`;
text += ` "${this.value}"`;
if (this.Comments.HasEndOfLineComment()) {
text += ` // ${this.Comments.GetEndOfLineComment()}`;
}
}
}
return text;
}
toString() {
return this.Format();
}
/**
* Deep clone KeyValues
*/
Clone() {
if (!this.children) {
return new KeyValues(this.Key, this.value);
}
return new KeyValues(this.Key, this.children.map((v) => v.Clone()));
}
/**
* Create root node
*/
static CreateRoot() {
return new KeyValues(KeyValuesRootKey, []);
}
/**
* Parse string
*/
static async Parse(body, filename) {
let root = this.CreateRoot();
root.filename = filename;
this._parse({ body, pos: 0, line: 1 }, root);
// Load #base
if (filename) {
const baseList = root.FindAllKeys('#base');
const adapter = getKeyValuesAdapter();
for (const base of baseList) {
const v = base.GetValue().trim();
if (v) {
base.baseFilePath = v;
const filePath = adapter.resolvePath(filename, v);
const baseKV = await KeyValues.Load(filePath);
base.filename = baseKV.filename;
base.SetValue(baseKV.GetChildren().map((v) => v.Free()));
}
}
}
return root;
}
static _parse(data, parent) {
let kv = new KeyValues('');
let key = false;
let leftMark = false;
let inQoute = false;
let str = '';
let isEndOfLineComment = false;
let lastKV;
let matchFlag = false;
for (; data.pos < data.body.length; data.pos++) {
const c = data.body[data.pos];
const isNewLine = c === '\n';
const isSpace = isNewLine || c === ' ' || c === '\t' || c === '\r';
if (isNewLine) {
data.line += 1;
isEndOfLineComment = false;
}
// Merge flags text
if (lastKV && matchFlag) {
if (c === ']') {
lastKV.Flags = str;
matchFlag = false;
str = '';
continue;
}
str += c;
continue;
}
// If leftMark is true then merge char to str
if (leftMark) {
if (!inQoute) {
if (c === '{' || c === '"' || c === '[' || c === ']') {
throw Error(`Not readable in line ${data.line}`);
}
if (isSpace || c === '}') {
if (key) {
kv.Key = str;
}
else {
kv.SetValue(str);
parent.Append(kv);
lastKV = kv;
kv = new KeyValues('');
}
leftMark = false;
inQoute = false;
str = '';
if (c === '}') {
data.pos--;
}
continue;
}
str += c;
continue;
}
if (c === '\\') {
data.pos++;
str += c + data.body[data.pos];
continue;
}
if (c === '"') {
if (key) {
kv.Key = str;
}
else {
kv.SetValue(str);
parent.Append(kv);
lastKV = kv;
kv = new KeyValues('');
}
leftMark = false;
inQoute = false;
str = '';
continue;
}
str += c;
continue;
}
// if comment
if (c === '/') {
if (data.body[data.pos + 1] === '*') {
throw Error(`Line ${data.line}: not support multi-line comment`);
}
if (data.body[data.pos + 1] === '/') {
const endIndex = data.body.indexOf('\n', data.pos + 1);
if (endIndex < 0) {
break;
}
const comment = data.body.slice(data.pos + 2, endIndex).trim();
if (comment) {
if (isEndOfLineComment) {
if (key) {
kv.Comments.SetEndOfLineComment(comment);
}
else {
const lastChild = parent.GetLastChild();
if (lastChild) {
lastChild.Comments.SetEndOfLineComment(comment);
}
}
}
else {
kv.Comments.AppendComment(comment);
}
}
isEndOfLineComment = false;
data.pos = endIndex;
continue;
}
}
// If open breace
if (c === '{') {
data.pos++;
kv.SetValue([]);
parent.Append(kv);
this._parse(data, kv);
lastKV = kv;
kv = new KeyValues('');
key = false;
continue;
}
// If close breace
if (c === '}') {
data.pos++;
break;
}
// If space
if (isSpace) {
continue;
}
// Match flag
if (lastKV && c === '[') {
str = '';
matchFlag = true;
continue;
}
// start merge char
key = !key;
leftMark = true;
inQoute = c === '"';
str = inQoute ? '' : c;
isEndOfLineComment = true;
}
}
/**
* Convert KeyValues to object and exclude comments.
*/
toObject() {
const obj = {};
if (!this.HasChildren()) {
throw Error('Not found children in this KeyValues');
}
for (const kv of this.children) {
if (kv.Key === '#base') {
continue;
}
if (!kv.HasChildren()) {
obj[kv.Key] = kv.GetValue();
}
else {
obj[kv.Key] = kv.toObject();
}
}
if (this.IsRoot()) {
for (const base of this.GetBaseList()) {
const root = base.toObject();
for (const key in root) {
const baseChildren = root[key];
if (obj[key] === undefined) {
obj[key] = baseChildren;
}
else {
const children = obj[key];
for (const k in baseChildren) {
children[k] = baseChildren[k];
}
}
}
}
}
return obj;
}
/**
* Load KeyValues from file
*/
static async Load(filename, encoding) {
const adapter = getKeyValuesAdapter();
const text = await adapter.readFile(filename, encoding);
if (text.charCodeAt(0) === 0xfeff) {
return await this.Parse(text.slice(1), filename);
}
return await this.Parse(text, filename);
}
/**
* Save KeyValues to file
*/
async Save(otherFilename, encoding) {
const filename = otherFilename !== null && otherFilename !== void 0 ? otherFilename : this.filename;
if (!filename) {
throw new Error('Not found filename in KeyValues');
}
const adapter = getKeyValuesAdapter();
await adapter.writeFile(filename, this.Format(), encoding);
// Save #base
const baseList = this.FindAllKeys('#base');
for (const base of baseList) {
const content = base
.GetChildren()
.map((v) => v.Format())
.join('\n');
if (otherFilename) {
const filePath = adapter.resolvePath(filename, base.GetBaseFilePath());
await adapter.writeFile(filePath, content, encoding);
}
else {
await adapter.writeFile(base.filename, content, encoding);
}
}
}
}
function createID() {
const adapter = getKeyValuesAdapter();
return adapter.createKeyValuesID();
}
class KV3BaseValue {
constructor(owner) {
this.Comments = new KeyValues3Comments();
this.owner = owner;
}
Value() {
return this.value;
}
GetOwner() {
return this.owner;
}
SetOwner(owner) {
this.owner = owner;
}
IsBoolean() {
return this instanceof ValueBoolean;
}
IsInt() {
return this instanceof ValueInt;
}
IsDouble() {
return this instanceof ValueDouble;
}
IsString() {
return this instanceof ValueString;
}
IsFeature() {
return this instanceof ValueFeature;
}
IsArray() {
return this instanceof ValueArray;
}
IsObject() {
return this instanceof ValueObject;
}
IsNull() {
return this instanceof ValueNull;
}
IsFeatureObject() {
return this instanceof ValueFeatureObject;
}
Format() {
return String(this.value);
}
Clone() {
const v = new KV3BaseValue(this.owner);
v.value = this.value;
return v;
}
}
/**
* Null
*/
class ValueNull extends KV3BaseValue {
constructor() {
super();
}
Value() {
return null;
}
Format() {
return `null`;
}
Clone() {
const v = new ValueNull();
v.SetOwner(this.owner);
return v;
}
}
/**
* String
*/
class ValueString extends KV3BaseValue {
constructor(initValue) {
super();
this.value = '';
if (initValue) {
this.SetValue(initValue);
}
}
Value() {
return this.value;
}
SetValue(v) {
this.value = String(v);
return this;
}
Format() {
if (this.value.includes('\n')) {
return `"""${this.value}"""`;
}
return `"${this.value}"`;
}
Clone() {
const v = new ValueString(this.value);
v.SetOwner(this.owner);
return v;
}
}
/**
* Boolean
*/
class ValueBoolean extends KV3BaseValue {
constructor(initValue) {
super();
this.value = false;
if (initValue) {
this.SetValue(initValue);
}
}
Value() {
return this.value;
}
SetValue(v) {
this.value = v === true;
return this;
}
Clone() {
const v = new ValueBoolean(this.value);
v.SetOwner(this.owner);
return v;
}
}
/**
* Int
*/
class ValueInt extends KV3BaseValue {
constructor(initValue) {
super();
this.value = 0;
if (initValue) {
this.SetValue(initValue);
}
}
Value() {
return this.value;
}
SetValue(v) {
this.value = Math.floor(v);
return this;
}
Clone() {
const v = new ValueInt(this.value);
v.SetOwner(this.owner);
return v;
}
}
/**
* Double
*/
class ValueDouble extends KV3BaseValue {
constructor(initValue) {
super();
this.value = 0;
if (initValue) {
this.SetValue(initValue);
}
}
Value() {
return this.value;
}
SetValue(v) {
this.value = v;
return this;
}
Format() {
if (this.value < Number.MAX_SAFE_INTEGER && this.value > Number.MIN_SAFE_INTEGER) {
return this.value.toFixed(6);
}
const f = Math.floor(this.value);
const left = BigInt(f);
const right = (this.value - f).toFixed(1);
return `${left}${right.slice(1)}`;
}
Clone() {
const v = new ValueDouble(this.value);
v.SetOwner(this.owner);
return v;
}
}
/**
* Similar values:
* resource:""
* deferred_resource:""
* soundevent:""
*/
class ValueFeature extends KV3BaseValue {
constructor(Feature = 'resource', initValue) {
super();
this.Feature = Feature;
this.value = '';
if (initValue) {
this.SetValue(initValue);
}
}
Value() {
return this.value;
}
SetValue(v) {
this.value = v;
return this;
}
Format() {
return `${this.Feature}:"${this.value}"`;
}
Clone() {
const v = new ValueFeature(this.value);
v.SetOwner(this.owner);
return v;
}
}
/**
* Array
*/
class ValueArray extends KV3BaseValue {
constructor(initValue) {
super();
this.value = [];
if (initValue) {
this.SetValue(initValue);
}
}
Value() {
return this.value;
}
SetValue(list) {
this.value = list.map((v) => v);
return this;
}
Append(...kv) {
this.value.push(...kv);
return this;
}
Insert(index, ...kv) {
this.value.splice(index, 0, ...kv);
return this;
}
Delete(v) {
const i = this.value.indexOf(v);
if (i >= 0) {
this.value.splice(i, 1);
}
return this;
}
/**
* Recursively iterate through all children to find the value that matches the ID
*/
FindIDTraverse(id) {
for (const v of this.value) {
if (v.IsObject() || v.IsArray()) {
const result = v.FindIDTraverse(id);
if (result) {
return result;
}
}
}
}
/**
* Recursively iterate through all children to find the value that matches the callback
*/
Search(callback) {
for (const v of this.value) {
if (callback(v)) {
return v;
}
if (v.IsObject()) {
const result = v.Search(callback);
if (result) {
return result;
}
}
else if (v.IsArray()) {
const result = v.Search(callback);
if (result) {
return result;
}
}
}
}
Get(index) {
return this.value[index];
}
Format(tab = '') {
let text = '';
let oneLine = true;
if (this.value.some((v) => v.IsArray() ||
v.IsObject() ||
v.IsFeatureObject() ||
v.Comments.HasComments() ||
v.Comments.HasEndOfLineComment())) {
oneLine = false;
}
else {
const max = this.value.reduce((pv, v) => pv + v.Format().length, 0);
if (max > 64) {
oneLine = false;
}
}
if (oneLine) {
text = ` [ `;
text += this.value
.map((v) => {
return v.Format();
})
.join(', ');
text += ` ]`;
}
else {
text = `\n${tab}[`;
text += this.value
.map((v) => {
let comment = '';
let endComment = '';
if (v.Comments.HasComments()) {
comment = '\n' + v.Comments.Format(tab + ' ').trimEnd();
}
if (v.Comments.HasEndOfLineComment()) {
endComment = ` // ${v.Comments.GetEndOfLineComment()}`;
}
if (v.IsArray()) {
let str = v.Format(tab + ' ');
if (!str.startsWith('\n')) {
str = '\n' + tab + ' ' + str;
}
return comment + str + ',' + endComment;
}
else if (v.IsFeatureObject()) {
return (comment +
'\n' +
tab +
' ' +
v.Format(tab + ' ') +
',' +
endComment);
}
else if (v.IsObject()) {
return comment + v.Format(tab + ' ') + ',' + endComment;
}
return comment + '\n' + tab + ' ' + v.Format() + ',' + endComment;
})
.join('');
text += `\n${tab}]`;
}
return text;
}
/**
* Convert to javascript array
*/
toArray() {
const result = [];
for (const v of this.value) {
if (v.IsObject()) {
result.push(v.toObject());
}
else if (v.IsArray()) {
result.push(v.toArray());
}
else {
result.push(v.Value());
}
}
return result;
}
Clone() {
const v = new ValueArray(this.value.map((v) => v.Clone()));
v.SetOwner(this.owner);
return v;
}
}
/**
* Object
*/
class ValueObject extends KV3BaseValue {
constructor(initValue) {
super();
this.value = [];
if (initValue) {
this.SetValue(initValue);
}
}
Value() {
return this.value;
}
SetValue(list) {
this.value = [...list];
return this;
}
Create(key, value) {
const kv = new KeyValues3(key, value);
this.Append(kv);
return kv;
}
Append(...kv) {
this.value.push(...kv);
return this;
}
Insert(index, ...kv) {
this.value.splice(index, 0, ...kv);
return this;
}
Delete(v) {
let kv;
if (typeof v === 'string') {
kv = this.value.find((c) => c.Key === v);
}
else {
kv = this.value.find((c) => c === v);
}
if (kv) {
this.value.splice(this.value.indexOf(kv), 1);
}
return kv;
}
Get(index) {
return this.value[index];
}
/**
* Find a KeyValues3
*/
Find(callback) {
for (const [i, kv] of this.value.entries()) {
if (callback(kv, i, this) === true) {
return kv;
}
}
}
/**
* Find a KeyValues3
*/
FindKey(key) {
return this.Find((kv) => kv.Key === key);
}
/**
* Find a KeyValues3
*/
FindAll(callback) {
const result = [];
for (const [i, kv] of this.value.entries()) {
if (callback(kv, i, this) === true) {
result.push(kv);
}
}
return result;
}
/**
* Find a KeyValues3
*/
FindAllKeys(...keys) {
return this.FindAll((kv) => keys.includes(kv.Key));
}
/**
* Recursively iterate through all children to find the value that matches the ID
*/
FindIDTraverse(id) {
for (const kv of this.value) {
if (kv.ID === id) {
return kv;
}
const result = kv.FindIDTraverse(id);
if (result) {
return result;
}
}
}
/**
* Recursively iterate through all children to find the value that matches the callback
*/
Search(callback) {
for (const kv of this.value) {
const v = kv.GetValue();
if (callback(v) === true) {
return v;
}
if (v.IsObject()) {
const result = v.Search(callback);
if (result) {
return result;
}
}
else if (v.IsArray()) {
const result = v.Search(callback);
if (result) {
return result;
}
}
}
}
Format(tab = '') {
let text = `\n${tab}{`;
text += this.value.map((v) => '\n' + v.Format(tab + ' ')).join('');
text += `\n${tab}}`;
return text;
}
/**
* Convert to javascript object
*/
toObject() {
const result = {};
for (const kv of this.value) {
if (kv.GetValue().IsArray() || kv.GetValue().IsObject()) {
result[kv.Key] = kv.toObject();
}
else {
result[kv.Key] = kv.GetValue().Value();
}
}
return result;
}
Clone() {
const v = new ValueObject(this.value.map((v) => v.Clone()));
v.SetOwner(this.owner);
return v;
}
}
/**
* Similar values:
* ```
* m_subclassScaleFunction = subclass:
* {
* _class = "scale_function_single_stat"
* _my_subclass_name = "AbilityCooldown_scale_function"
* m_eSpecificStatScaleType = "ETechCooldown"
* }
* ```
*/
class ValueFeatureObject extends ValueObject {
constructor(Feature = 'subclass', initValue) {
super(initValue);
this.Feature = Feature;
}
Format(tab = '') {
let text = `${this.Feature}:\n${tab}{`;
text += this.value.map((v) => '\n' + v.Format(tab + ' ')).join('');
text += `\n${tab}}`;
return text;
}
/**
* Convert to javascript object
*/
toObject() {
const result = {
Feature: this.Feature,
Values: {},
};
for (const kv of this.value) {
if (kv.GetValue().IsArray() || kv.GetValue().IsObject()) {
result.Values[kv.Key] = kv.toObject();
}
else {
result.Values[kv.Key] = kv.GetValue().Value();
}
}
return result;
}
Clone() {
const v = new ValueObject(this.value.map((v) => v.Clone()));
v.SetOwner(this.owner);
return v;
}
}
const MatchKeyNoQuote = /^[0-9a-zA-Z_\.]+$/;
const MatchKeyNumber = /^\d+$/;
const MatchInt = /^-?\d+$/;
const MatchDouble = /^-?\d+(\.\d+)?$/;
const MatchDouble2 = /^-?\.\d+$/;
const MatchDouble3 = /^-?\d+\.$/;
const MatchStrangeNumber = /^[\d\+-\.]+$/;
const MatchBoolean = /^(true|false)$/;
const MatchFeature = /^[0-9a-zA-Z_]+:"(.*)"$/;
const MatchFeatureObject = /^[0-9a-zA-Z_]+:$/;
const MatchNull = /^null$/;
/**
* https://developer.valvesoftware.com/wiki/Dota_2_Workshop_Tools/KeyValues3
*/
class KeyValues3 {
static String(value) {
return new ValueString(value);
}
static Boolean(value) {
return new ValueBoolean(value);
}
static Int(value) {
return new ValueInt(value);
}
static Double(value) {
return new ValueDouble(value);
}
static Feature(feature, value) {
return new ValueFeature(feature, value);
}
static Array(value) {
return new ValueArray(value);
}
static Object(value) {
return new ValueObject(value);
}
static Null() {
return new ValueNull();
}
static FeatureObject(feature, value) {
return new ValueFeatureObject(feature, value);
}
get filename() {
return this.__filename || '';
}
set filename(s) {
this.__filename = s ? s.replace(/\\/g, '/') : s;
}
constructor(Key, defaultValue) {
this.Key = Key;
/**
* Unique id of KeyValues3
*/
this.ID = createID();
this.value = defaultValue;
this.value.SetOwner(this);
}
IsRoot() {
return !!this.header;
}
GetHeader() {
return this.header;
}
SetHeader(header) {
this.header = header;
}
static CreateRoot() {
const kv = new KeyValues3('', new ValueObject());
kv.header = this.CommonHeader;
return kv;
}
GetValue() {
return this.value;
}
/**
* Return when value is ValueObject, otherwise throw an error.
*/
GetObject() {
if (!this.value.IsObject()) {
throw Error('The value is not object');
}
return this.value;
}
/**
* Return when value is ValueArray, otherwise throw an error.
*/
GetArray() {
if (!this.value.IsArray()) {
throw Error('The value is not array');
}
return this.value;
}
SetValue(v) {
if (this.IsRoot() && !v.IsObject()) {
throw Error('The root node of KeyValues3 must be an object');
}
this.value = v;
}
CreateObjectValue(key, value) {
if (!this.value.IsObject()) {
throw Error('The KeyValues3 is not an object');
}
return this.value.Create(key, value);
}
AppendValue(...values) {
if (!this.value.IsArray()) {
throw Error('The KeyValues3 is not an array');
}
return this.value.Append(...values);
}
Find(callback) {
if (!this.value.IsObject()) {
throw Error('The KeyValues3 is not an object');
}
return this.value.Find(callback);
}
FindKey(key) {
return this.Find((kv) => kv.Key === key);
}
FindAll(callback) {
if (!this.value.IsObject()) {
throw Error('The KeyValues3 is not an object');
}
return this.value.FindAll(callback);
}
FindAllKeys(...keys) {
return this.FindAll((kv) => keys.includes(kv.Key));
}
/**
* Find child from the current KeyValues3
*/
FindID(id) {
return this.Find((kv) => kv.ID === id);
}
/**
* Recursively iterate through all children to find the value that matches the ID
*/
FindIDTraverse(id) {
if (this.value.IsObject() || this.value.IsArray()) {
return this.value.FindIDTraverse(id);
}
}
/**
* Recursively iterate through all children to find the value that matches the callback
*/
Search(callback) {
if (this.value.IsObject() || this.value.IsArray()) {
return this.value.Search(callback);
}
}
Format(tab = '') {
let text = '';
let prefix = '';
const root = this.IsRoot();
if (MatchKeyNoQuote.test(this.Key) && !MatchKeyNumber.test(this.Key)) {
prefix = `${tab}${this.Key} =`;
}
else {
prefix = `${tab}"${this.Key}" =`;
}
if (root) {
text += this.header;
}
if (this.value.Comments.HasComments()) {
text += this.value.Comments.Format(tab);
}
if (this.value.IsArray()) {
text += prefix;
text += this.value.Format(tab);
}
else if (this.value.IsObject()) {
if (root) {
text += this.value.Format(tab);
}
else {
text += prefix;
if (this.value.IsFeatureObject()) {
text += ' ';
}
text += this.value.Format(tab);
}
}
else {
text += prefix + ` ${this.value.Format()}`;
}
if (this.value.Comments.HasEndOfLineComment()) {
text += ` // ${this.value.Comments.GetEndOfLineComment()}`;
}
return text;
}
toString() {
return this.Format();
}
/**
* Convert KeyValues3 to object and exclude comments.
* If the value of KeyValues3 is not object or array, then return object,
* which has only the key and value of KeyValues3
*/
toObject() {
if (this.value.IsArray()) {
return this.value.toArray();
}
else if (this.value.IsObject()) {
return this.value.toObject();
}
return { [this.Key]: this.value.Value() };
}
/**
* Deep clone KeyValues3
*/
Clone() {
if (this.IsRoot()) {
const root = KeyValues3.CreateRoot();
root.header = this.header;
root.__filename = this.__filename;
root.SetValue(this.value.Clone());
return root;
}
return new KeyValues3(this.Key, this.value.Clone());
}
/**
* Parse text of KeyValues3
*/
static Parse(body, filename) {
let root = this.CreateRoot();
root.filename = filename;
const firstLineIndex = body.indexOf('\n');
const header = body.slice(0, firstLineIndex).trim();
if (!header.startsWith('<!--') || !header.endsWith('-->')) {
throw Error('Invalid header');
}
root.header = header;
this._parse(root, {
body,
line: 2,
pos: body.indexOf('{', firstLineIndex) + 1,
tokenCounter: 1,
});
return root;
}
static _parse(parent, data) {
var _a, _b;
if (parent.value.IsObject() || parent.value.IsFeatureObject()) {
let isKey = !parent.value.IsFeatureObject();
let startMark = false;
let inQoute = false;
let key = '';
let str = '';
let isEndOfLineComment = false;
let commentCache = [];
let lastKV;
for (; data.pos < data.body.length; data.pos++) {
const c = data.body[data.pos];
const isNewLine = c === '\n';
const isSpace = isNewLine || c === ' ' || c === '\t' || c === '\r';
if (isNewLine) {
data.line += 1;
isEndOfLineComment = false;
}
if (startMark) {
if (isKey) {
// isKey
if (inQoute) {
if (c === '\\') {
str += c + data.body[data.pos + 1];
data.pos += 1;
continue;
}
if (c === '"') {
key = str;
str = '';
startMark = false;
continue;
}
else {
str += c;
continue;
}
}
else {
if (isSpace || c === '=') {
key = str;
str = '';
startMark = false;
if (c === '=') {
data.pos -= 1;
}
continue;
}
str += c;
continue;
}
// isKey
}
else {
// not isKey
if (inQoute) {
if (c === '\\') {
str += c + data.body[data.pos + 1];
data.pos += 1;
continue;
}
if (c === '"') {
if (str.length <= 0) {
// check start on multi-line
if (data.body[data.pos + 1] === '"') {
if (data.body[data.pos + 2] !== '\n' &&
data.body[data.pos + 2] !== '\r') {
throw new Error(this._parse_error(data.line, `multi-line start identifier """ must be followed by newline`));
}
data.pos += 1;
continue;
}
}
else {
// check end on multi-line
if (data.body[data.pos + 1] === '"') {
if (data.body[data.pos + 2] === '"') {
if (data.body[data.pos - 1] !== '\n') {
throw new Error(this._parse_error(data.line, `multi-line end identifier """ must be at the beginning of line`));
}
data.pos += 2;
}
else {
throw new Error(this._parse_error(data.line, `multi-line string must be end with """`));
}
}
}
if (MatchFeature.test(str + c)) {
str += c;
inQoute = false;
continue;
}
lastKV = parent.CreateObjectValue(key, new ValueString(str));
lastKV.value.Comments.SetComments(commentCache);
commentCache = [];
key = '';
str = '';
isKey = true;
inQoute = false;
startMark = false;
continue;
}
else {
str += c;
continue;
}
}
else {
if (isSpace || c === ']' || c === '}') {
if (MatchBoolean.test(str)) {
lastKV = parent.CreateObjectValue(key, new ValueBoolean(str === 'true'));
}
else if (MatchNull.test(str)) {
lastKV = parent.CreateObjectValue(key, new ValueNull());
}
else if (MatchInt.test(str)) {
lastKV = parent.CreateObjectValue(key, new ValueInt(parseInt(str)));
}
else if (MatchDouble.test(str) ||
MatchDouble2.test(str) ||
MatchDouble3.test(str)) {
lastKV = parent.CreateObjectValue(key, new ValueDouble(Number(str)));
}
else if (MatchFeature.test(str)) {
const index = str.indexOf(':');
const feature = str.slice(0, index);
let v = str.slice(index + 2, str.length - 1);
lastKV = parent.CreateObjectValue(key, new ValueFeature(feature, v));
}
else if (MatchFeatureObject.test(str)) {
const index = str.indexOf(':');
const feature = str.slice(0, index);
const kv = new KeyValues3('', new ValueFeatureObject());
data.pos += 1;
this._parse(kv, data);
const child = parent.CreateObjectValue(key, new ValueFeatureObject(feature, [
...kv.GetObject().Value()[0].GetObject().Value(),
]));
lastKV = child;
}
else if (MatchStrangeNumber.test(str)) {
lastKV = parent.CreateObjectValue(key, new ValueString(str));
}
else {
throw new Error(this._parse_error(data.line, `Invalid value '${str}'`));
}
lastKV.value.Comments.SetComments(commentCache);
commentCache = [];
key = '';
str = '';
isKey = true;
inQoute = false;
startMark = false;
if (c === ']' || c === '}') {
data.pos -= 1;
}
continue;
}
if (str.endsWith(':') && c === '"') {
inQoute = true;
}
str += c;
continue;
}
// not isKey
}
}
if (c === '/') {
if (data.body[data.pos + 1] === '/') {
const nextIndex = data.body.in