UNPKG

easy-keyvalues

Version:

Parse Valve KeyValues Format and easy to use in nodejs or browser.

1,680 lines (1,675 loc) 65.8 kB
'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