diff --git a/package-lock.json b/package-lock.json index fef96d4..6aceac1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "behavior3editor", - "version": "1.8.5", + "version": "1.8.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "behavior3editor", - "version": "1.8.5", + "version": "1.8.7", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3b47d98..20102b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "behavior3editor", - "version": "1.8.5", + "version": "1.8.7", "main": "dist-electron/main/index.js", "description": "行为树编辑器", "author": "zhandouxiaojiji & codetypes", diff --git a/src/components/inspector.tsx b/src/components/inspector.tsx index 75ce55e..2c719fe 100644 --- a/src/components/inspector.tsx +++ b/src/components/inspector.tsx @@ -549,7 +549,7 @@ const validateArg = ( } else if (value === null && !required) { value = undefined; } - if (!checkNodeArgValue(node, arg, value, true)) { + if (!checkNodeArgValue(node, arg, value, console.error)) { return Promise.reject(new Error(i18n.t("node.invalidValue"))); } return Promise.resolve(value); diff --git a/src/components/register-node.ts b/src/components/register-node.ts index 246cc7a..fff80f8 100644 --- a/src/components/register-node.ts +++ b/src/components/register-node.ts @@ -748,7 +748,7 @@ class TreeNode extends Rect { !b3util.nodeDefs.has(data.name) || (data.path && !node.children?.length) || (nodeDef.group && !nodeDef.group.some((g) => b3util.usingGroups?.[g])) || - !b3util.checkTreeData(data) || + !b3util.isValidNodeData(data) || foundUndefined(data.input) || foundUndefined(data.output) || foundUndefinedInArgs(nodeDef, data) || diff --git a/src/misc/b3type.ts b/src/misc/b3type.ts index d955289..3edbc13 100644 --- a/src/misc/b3type.ts +++ b/src/misc/b3type.ts @@ -1,6 +1,6 @@ import { NodeDef } from "../behavior3/src/behavior3"; -export const VERSION = "1.8.5"; +export const VERSION = "1.8.7"; export const keyWords = ["true", "false", "null", "undefined", "NaN", "Infinity"]; @@ -73,6 +73,7 @@ export interface TreeData { group: string[]; import: string[]; vars: VarDecl[]; + custom: Record; root: NodeData; $override: { diff --git a/src/misc/b3util.ts b/src/misc/b3util.ts index df69105..bd73059 100644 --- a/src/misc/b3util.ts +++ b/src/misc/b3util.ts @@ -20,10 +20,11 @@ import { VERSION, } from "./b3type"; import Path from "./path"; +import { stringifyJson } from "./stringify"; import { nanoid, readJson, readTree, readWorkspace } from "./util"; export class NodeDefs extends Map { - get(key: string): NodeDef { + override get(key: string): NodeDef { return super.get(key) ?? unknownNodeDef; } } @@ -188,8 +189,10 @@ export const isNodeEqual = (node1: NodeData, node2: NodeData) => { return false; }; -const error = (data: NodeData, msg: string) => { - console.error(`check ${data.id}|${data.name}: ${msg}`); +type ErrorPrinter = (msg: string) => void; + +const formatError = (data: NodeData, msg: string) => { + return `check ${data.id}|${data.name}: ${msg}`; }; export const getNodeArgRawType = (arg: NodeArg) => { @@ -208,75 +211,62 @@ export const checkNodeArgValue = ( data: NodeData, arg: NodeArg, value: unknown, - verbose?: boolean + printer?: ErrorPrinter ) => { let hasError = false; const type = getNodeArgRawType(arg); + const error = !printer ? () => {} : (msg: string) => printer(formatError(data, msg)); if (isFloatType(type)) { const isNumber = typeof value === "number"; const isOptional = value === undefined && isNodeArgOptional(arg); if (!(isNumber || isOptional)) { - if (verbose) { - error(data, `'${arg.name}=${JSON.stringify(value)}' is not a number`); - } + error(`'${arg.name}=${JSON.stringify(value)}' is not a number`); hasError = true; } } else if (isIntType(type)) { const isInt = typeof value === "number" && value === Math.floor(value); const isOptional = value === undefined && isNodeArgOptional(arg); if (!(isInt || isOptional)) { - if (verbose) { - error(data, `'${arg.name}=${JSON.stringify(value)}' is not a int`); - } + error(`'${arg.name}=${JSON.stringify(value)}' is not a int`); hasError = true; } } else if (isStringType(type)) { const isString = typeof value === "string" && value; const isOptional = (value === undefined || value === "") && isNodeArgOptional(arg); if (!(isString || isOptional)) { - if (verbose) { - error(data, `'${arg.name}=${JSON.stringify(value)}' is not a string`); - } + error(`'${arg.name}=${JSON.stringify(value)}' is not a string`); hasError = true; } } else if (isExprType(type)) { const isExpr = typeof value === "string" && value; const isOptional = (value === undefined || value === "") && isNodeArgOptional(arg); if (!(isExpr || isOptional)) { - if (verbose) { - error(data, `'${arg.name}=${JSON.stringify(value)}' is not an expr string`); - } + error(`'${arg.name}=${JSON.stringify(value)}' is not an expr string`); hasError = true; } } else if (isJsonType(type)) { const isJson = value !== undefined && value !== ""; const isOptional = isNodeArgOptional(arg); if (!(isJson || isOptional)) { - if (verbose) { - error(data, `'${arg.name}=${value}' is not an invalid object`); - } + error(`'${arg.name}=${value}' is not an invalid object`); hasError = true; } } else if (isBoolType(type)) { const isBool = typeof value === "boolean" || value === undefined; if (!isBool) { - if (verbose) { - error(data, `'${arg.name}=${JSON.stringify(value)}' is not a boolean`); - } + error(`'${arg.name}=${JSON.stringify(value)}' is not a boolean`); hasError = true; } } else { hasError = true; - error(data, `unknown arg type '${arg.type}'`); + error(`unknown arg type '${arg.type}'`); } if (hasArgOptions(arg)) { const found = !!arg.options?.find((option) => option.value === value); const isOptional = value === undefined && isNodeArgOptional(arg); if (!(found || isOptional)) { - if (verbose) { - error(data, `'${arg.name}=${JSON.stringify(value)}' is not a one of the option values`); - } + error(`'${arg.name}=${JSON.stringify(value)}' is not a one of the option values`); hasError = true; } } @@ -284,37 +274,34 @@ export const checkNodeArgValue = ( return !hasError; }; -export const checkNodeArg = (data: NodeData, conf: NodeDef, i: number, verbose?: boolean) => { +export const checkNodeArg = (data: NodeData, conf: NodeDef, i: number, printer?: ErrorPrinter) => { let hasError = false; const arg = conf.args![i] as NodeArg; const value = data.args?.[arg.name]; + const error = !printer ? () => {} : (msg: string) => printer(formatError(data, msg)); if (isNodeArgArray(arg)) { if (!Array.isArray(value) || value.length === 0) { if (!isNodeArgOptional(arg)) { - if (verbose) { - error(data, `'${arg.name}=${JSON.stringify(value)}' is not an array or empty array`); - } + error(`'${arg.name}=${JSON.stringify(value)}' is not an array or empty array`); hasError = true; } } else { for (let j = 0; j < value.length; j++) { - if (!checkNodeArgValue(data, arg, value[j], verbose)) { + if (!checkNodeArgValue(data, arg, value[j], printer)) { hasError = true; } } } - } else if (!checkNodeArgValue(data, arg, value, verbose)) { + } else if (!checkNodeArgValue(data, arg, value, printer)) { hasError = true; } if (arg.oneof !== undefined) { const idx = conf.input?.findIndex((v) => v.startsWith(arg.oneof!)) ?? -1; if (!checkOneof(arg, data.args?.[arg.name], data.input?.[idx])) { - if (verbose) { - error( - data, - `only one is allowed for between argument '${arg.name}' and input '${data.input?.[idx]}'` - ); - } + error( + `only one is allowed for between argument '${arg.name}' and input '${data.input?.[idx]}'` + ); + hasError = true; } } @@ -333,13 +320,44 @@ export const checkOneof = (arg: NodeArg, argValue: unknown, inputValue: unknown) return (argValue !== "" && inputValue === "") || (argValue === "" && inputValue !== ""); }; -export const checkNodeData = (data: NodeData | null | undefined) => { +export const isValidNodeData = (data: NodeData) => { + const def = nodeDefs.get(data.name); + if (def.input) { + for (let i = 0; i < def.input.length; i++) { + if (!isValidInputOrOutput(def.input, data.input, i)) { + return false; + } + } + } + if (def.output) { + for (let i = 0; i < def.output.length; i++) { + if (!isValidInputOrOutput(def.output, data.output, i)) { + return false; + } + } + } + if (!isValidChildren(data)) { + return false; + } + if (def.args) { + for (let i = 0; i < def.args.length; i++) { + if (!checkNodeArg(data, def, i)) { + return false; + } + } + } + + return true; +}; + +const checkNodeData = (data: NodeData | null | undefined, printer: ErrorPrinter) => { if (!data) { return false; } + const error = !printer ? () => {} : (msg: string) => printer(formatError(data, msg)); const conf = nodeDefs.get(data.name); if (conf.name === unknownNodeDef.name) { - error(data, `undefined node: ${data.name}`); + error(`undefined node: ${data.name}`); return false; } @@ -347,7 +365,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => { if (conf.group) { if (!conf.group.some((g) => usingGroups?.[g])) { - error(data, `node group '${conf.group}' is not enabled`); + error(`node group '${conf.group}' is not enabled`); hasError = true; } } @@ -356,7 +374,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => { if (data.input) { for (const v of data.input) { if (v && !usingVars[v]) { - error(data, `input variable '${v}' is not defined`); + error(`input variable '${v}' is not defined`); hasError = true; } } @@ -364,7 +382,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => { if (data.output) { for (const v of data.output) { if (v && !usingVars[v]) { - error(data, `output variable '${v}' is not defined`); + error(`output variable '${v}' is not defined`); hasError = true; } } @@ -386,7 +404,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => { } for (const v of vars) { if (v && !usingVars[v]) { - error(data, `expr variable '${arg.name}' is not defined`); + error(`expr variable '${arg.name}' is not defined`); hasError = true; } } @@ -403,11 +421,11 @@ export const checkNodeData = (data: NodeData | null | undefined) => { for (const expr of exprs) { try { if (!new ExpressionEvaluator(expr).dryRun()) { - error(data, `expr '${expr}' is not valid`); + error(`expr '${expr}' is not valid`); hasError = true; } } catch (e) { - error(data, `expr '${expr}' is not valid`); + error(`expr '${expr}' is not valid`); hasError = true; } } @@ -420,7 +438,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => { const count = data.children?.length || 0; if (conf.children !== count) { hasError = true; - error(data, `expect ${conf.children} children, but got ${count}`); + error(`expect ${conf.children} children, but got ${count}`); } } @@ -435,14 +453,13 @@ export const checkNodeData = (data: NodeData | null | undefined) => { } if (data.input[i] && !isValidVariableName(data.input[i])) { error( - data, `input field '${data.input[i]}' is not a valid variable name,` + `should start with a letter or underscore` ); hasError = true; } if (!isValidInputOrOutput(conf.input, data.input, i)) { - error(data, `intput field '${conf.input[i]}' is required`); + error(`intput field '${conf.input[i]}' is required`); hasError = true; } if (i === conf.input.length - 1 && conf.input.at(-1)?.endsWith("...")) { @@ -465,14 +482,13 @@ export const checkNodeData = (data: NodeData | null | undefined) => { } if (data.output[i] && !isValidVariableName(data.output[i])) { error( - data, `output field '${data.output[i]}' is not a valid variable name,` + `should start with a letter or underscore` ); hasError = true; } if (!isValidInputOrOutput(conf.output, data.output, i)) { - error(data, `output field '${conf.output[i]}' is required`); + error(`output field '${conf.output[i]}' is required`); hasError = true; } if (i === conf.output.length - 1 && conf.output.at(-1)?.endsWith("...")) { @@ -496,7 +512,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => { args[key] = value; } - if (!checkNodeArg(data, conf, i, true)) { + if (!checkNodeArg(data, conf, i, printer)) { hasError = true; } } @@ -505,7 +521,7 @@ export const checkNodeData = (data: NodeData | null | undefined) => { if (data.children) { for (const child of data.children) { - if (!checkNodeData(child)) { + if (!checkNodeData(child, printer)) { hasError = true; } } @@ -665,36 +681,6 @@ const isValidInputOrOutput = (def: string[], data: string[] | undefined, index: return def[index].includes("?") || data?.[index] || isVariadic(def, index); }; -export const checkTreeData = (data: NodeData) => { - const def = nodeDefs.get(data.name); - if (def.input) { - for (let i = 0; i < def.input.length; i++) { - if (!isValidInputOrOutput(def.input, data.input, i)) { - return false; - } - } - } - if (def.output) { - for (let i = 0; i < def.output.length; i++) { - if (!isValidInputOrOutput(def.output, data.output, i)) { - return false; - } - } - } - if (!isValidChildren(data)) { - return false; - } - if (def.args) { - for (let i = 0; i < def.args.length; i++) { - if (!checkNodeArg(data, def, i, false)) { - return false; - } - } - } - - return true; -}; - export const refreshNodeData = (tree: TreeData, node: NodeData, id: number) => { node.id = (id++).toString(); node.$size = calcSize(node); @@ -721,8 +707,8 @@ export const refreshNodeData = (tree: TreeData, node: NodeData, id: number) => { const subtreePath = workdir + "/" + node.path; const subtree = readTree(subtreePath); id = refreshNodeData(subtree, subtree.root, --id); - node.name = subtree.name; - node.desc = subtree.desc; + node.name = subtree.root.name; + node.desc = subtree.root.desc; node.args = subtree.root.args; node.input = subtree.root.input; node.output = subtree.root.output; @@ -761,6 +747,8 @@ export const createBuildData = (path: string) => { dfs(treeModel.root, (node) => (node.id = treeModel.prefix + node.id)); treeModel.name = Path.basenameWithoutExt(path); treeModel.root = createFileData(treeModel.root, true); + dfs(treeModel.root, (node) => delete (node as Partial).$id); + delete (treeModel as Partial).$override; return treeModel as TreeData; } catch (e) { console.log("build error:", path, e); @@ -826,6 +814,7 @@ export const buildProject = async (project: string, buildDir: string) => { }); } + const allErrors: string[] = []; for (const path of Path.ls(Path.dirname(project), true)) { if (path.endsWith(".json")) { const buildpath = buildDir + "/" + path.substring(workdir.length + 1); @@ -843,7 +832,6 @@ export const buildProject = async (project: string, buildDir: string) => { } console.log("build:", buildpath); if (errors.length) { - errors.forEach((msg) => console.error(msg)); hasError = true; } const declare: FileVarDecl = { @@ -852,14 +840,19 @@ export const buildProject = async (project: string, buildDir: string) => { subtree: [], }; refreshVarDecl(tree.root, tree.group, declare); - if (!checkNodeData(tree?.root)) { + if (!checkNodeData(tree?.root, (msg) => errors.push(msg))) { hasError = true; } + if (errors.length) { + allErrors.push(`${path}:`); + errors.forEach((v) => allErrors.push(` ${v}`)); + } buildScript?.onWriteFile?.(buildpath, tree); fs.mkdirSync(Path.dirname(buildpath), { recursive: true }); - fs.writeFileSync(buildpath, JSON.stringify(tree, null, 2)); + fs.writeFileSync(buildpath, stringifyJson(tree, { indent: 2 })); } } + allErrors.forEach((v) => console.error(v)); buildScript?.onComplete?.(hasError ? "failure" : "success"); return hasError; }; @@ -930,6 +923,7 @@ export const createNewTree = (path: string) => { name: "Sequence", $id: nanoid(), }, + custom: {}, $override: {}, }; return tree; @@ -1017,7 +1011,7 @@ const collectSubtree = (data: NodeData) => { export const refreshVarDecl = (root: NodeData, group: string[], declare: FileVarDecl) => { const filter: Record = {}; const vars: Array = new (class extends Array { - push(...items: VarDecl[]): number { + override push(...items: VarDecl[]): number { for (const v of items) { if (filter[v.name]) { continue; diff --git a/src/misc/stringify.ts b/src/misc/stringify.ts new file mode 100644 index 0000000..c17a3de --- /dev/null +++ b/src/misc/stringify.ts @@ -0,0 +1,332 @@ +import * as fs from "node:fs"; +import { basename, dirname, extname } from "node:path"; + +export type Tag = { + /** data name */ + ["!name"]?: string; + /** type tag */ + ["!type"]?: string; + /** special stringify function */ + ["!stringify"]?: (v: TValue, ctx: StringifyContext) => void; + /** ignore fields when stringify */ + ["!ignore"]?: { [k: string]: boolean }; +}; + +export type TValue = boolean | number | string | null | undefined | TObject | TArray; +export type TObject = { [k: string | number]: TValue } & Tag; +export type TArray = TValue[] & Tag; + +export class StringBuffer { + readonly data: string[] = []; + + private _indent: number; + private _indentCount: number = 0; + + constructor(indent: number) { + this._indent = indent; + } + + get indentCount() { + return this._indentCount; + } + + indent() { + this._indentCount += this._indent; + } + + unindent() { + this._indentCount -= this._indent; + } + + padding() { + if (this._indent > 0) { + this.data.push(" ".repeat(this._indentCount)); + } + } + + linefeed() { + if (this._indent > 0) { + this.data.push("\n"); + } + } + + writeLine(value: string) { + this.padding(); + this.data.push(value); + this.linefeed(); + } + + writeLines(value: string) { + for (const line of value.split("\n")) { + this.writeLine(line); + } + } + + writeString(value: string) { + this.data.push(value); + } + + toString() { + return this.data.join(""); + } +} + +export const isNumericKey = (key: string) => { + if (typeof key !== "string") return false; + + // integer or bigint + if (/^-?\d+$/.test(key)) { + try { + return String(BigInt(key)) === key; + } catch { + return false; + } + } + + // float + const num = Number(key); + return !isNaN(num) && String(num) === key; +}; + +export const escape = (value: string) => { + return value + .replaceAll("\r\n", "\n") + .replaceAll("\\", "\\\\") + .replaceAll('"', '\\"') + .replaceAll("\n", "\\n") + .replaceAll("\t", "\\t") + .replaceAll("\r", "\\r") + .replaceAll("\b", "\\b") + .replaceAll("\f", "\\f"); +}; + +export const outdent = (value: string) => { + value = value.replace(/^\n/, ""); + value = value.replace(/\n *$/, ""); + const space = value.match(/^ +/gm)?.[0]; + return space ? value.replace(new RegExp(`^${space}`, "gm"), "") : value; +}; + +export const format = (str: string, vars: Record) => { + const lines: string[] = []; + for (const line of str.split(/\n|\r\n/)) { + if (line.match(/^\s*%{\w+}\s*$/)) { + const [_, space, key] = line.match(/^(\s*)%{(\w+)}$/)!; + if (vars[key] !== undefined && vars[key] !== null) { + for (const l of vars[key].split(/\n|\r\n/)) { + lines.push(space + l); + } + } else { + throw new Error(`variable '${key}' not found`); + } + } else { + lines.push( + line.replaceAll(/%{(\w+)}/g, (_, key) => { + if (vars[key] !== undefined && vars[key] !== null) { + return vars[key]; + } + throw new Error(`variable '${key}' not found`); + }) + ); + } + } + return lines.join("\n"); +}; + +export const keys = ( + o: object, + filter?: (v: TValue) => boolean, + ignore?: { [k: string]: boolean } +) => { + const value = o as TObject; + const ks = Object.keys(value).filter( + (k) => !k.startsWith("!") && (!ignore || !ignore[k]) && (!filter || filter(value[k])) + ); + + const numKeys: string[] = []; + const strKeys: string[] = []; + for (const k of ks) { + const num = Number(k); + if (!isNaN(num) && isFinite(num)) { + numKeys.push(k); + } else { + strKeys.push(k); + } + } + numKeys.sort((a, b) => Number(a) - Number(b) || a.localeCompare(b)); + strKeys.sort((a, b) => a.localeCompare(b)); + return [...numKeys, ...strKeys]; +}; + +export const values = ( + o: TObject, + filter?: (v: TValue) => boolean, + ignore?: { [k: string]: boolean } +): T[] => { + return keys(o, filter, ignore).map((k) => o[k] as T); +}; + +export const toPascalCase = (str: string): string => { + return str + .replace(/^_+/, "") + .replace(/_([a-zA-Z])/g, (_, letter) => letter.toUpperCase()) + .replace(/^[a-zA-Z]/, (match) => match.toUpperCase()); +}; + +export const readFile = (path: string) => { + if (fs.existsSync(path)) { + return fs.readFileSync(path, "utf-8"); + } + return null; +}; + +export const writeFile = (path: string, data: string) => { + const dir = dirname(path); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + if (fs.existsSync(path) && readFile(path) === data) { + console.log(`up-to-date: ${path}`); + } else { + console.log(`write: ${path}`); + fs.writeFileSync(path, data, { encoding: "utf-8" }); + } +}; + +export const writeJson = (path: string, data: TValue, options?: JsonStringifyOption) => { + options = options ?? {}; + options.indent = options.indent ?? 2; + options.precision = options.precision ?? 10; + writeFile(path, stringifyJson(data, options)); +}; + +export const filename = (path: string, suffix: boolean = false) => { + return basename(path, !suffix ? extname(path) : undefined); +}; + +export type StringifyContext = { + readonly format: "json" | "lua" | "ts" | string; + readonly indent: number; + readonly precision?: number; + readonly buffer: StringBuffer; + readonly writeValue: (value: TValue) => void; + readonly writeArray: (value: TArray) => void; + readonly writeObject: (value: TObject) => void; +}; + +const numberToString = (value: number, precision?: number) => { + if (value === (value | 0)) { + return value.toFixed(0); + } else { + return value.toFixed(precision).replace(/\.?0+$/, ""); + } +}; + +//----------------------------------------------------------------------------- +// Json +//----------------------------------------------------------------------------- +export type JsonStringifyOption = { + indent?: number; + precision?: number; +}; + +export const stringifyJson = (data: unknown, option?: JsonStringifyOption) => { + const stacks: string[] = []; + option = option ?? {}; + option.indent = Math.max(option.indent ?? 4, 0); + option.precision = option.precision ?? 10; + const buffer = new StringBuffer(option.indent); + const ctx: StringifyContext = { + format: "json", + indent: option.indent, + precision: option.precision, + buffer, + writeValue: writeJsonValue, + writeArray: writeJsonArray, + writeObject: writeJsonObject, + }; + + function writeJsonValue(value: TValue) { + if (typeof value === "number") { + buffer.writeString(numberToString(value, ctx.precision)); + } else if (typeof value === "boolean") { + buffer.writeString(value.toString()); + } else if (value === null || value === undefined) { + buffer.writeString("null"); + } else if (typeof value === "string") { + buffer.writeString('"'); + buffer.writeString(escape(value)); + buffer.writeString('"'); + } else if (Array.isArray(value)) { + writeJsonArray(value); + } else { + if (typeof value !== "object" || value === null || value === undefined) { + writeJsonValue(value); + } else if (Array.isArray(value)) { + writeJsonArray(value); + } else { + writeJsonObject(value as TObject); + } + } + } + + function writeJsonObject(value: TObject) { + if (stacks.length > 256) { + throw new Error(`json stringify stack overflow: ${stacks.join("->")}`); + } + + if (value["!stringify"]) { + value["!stringify"](value, ctx); + return; + } + + const ks = keys(value, undefined, value["!ignore"]); + const space = ctx.indent > 0 ? " " : ""; + buffer.writeString("{"); + buffer.linefeed(); + buffer.indent(); + for (let i = 0; i < ks.length; i++) { + const k = ks[i]; + const v = value[k]; + stacks.push(k); + buffer.padding(); + buffer.writeString(`"${k}":${space}`); + writeJsonValue(v); + if (i < ks.length - 1) { + buffer.writeString(","); + } + buffer.linefeed(); + stacks.pop(); + } + buffer.unindent(); + buffer.padding(); + buffer.writeString("}"); + } + + function writeJsonArray(value: TArray) { + if (value["!stringify"]) { + value["!stringify"](value, ctx); + return; + } + + buffer.writeString("["); + buffer.linefeed(); + buffer.indent(); + for (let i = 0; i < value.length; i++) { + const v = value[i]; + buffer.padding(); + writeJsonValue(v); + if (i < value.length - 1) { + buffer.writeString(","); + } + buffer.linefeed(); + } + buffer.unindent(); + buffer.padding(); + buffer.writeString("]"); + } + + writeJsonValue(data as TValue); + + return buffer.toString(); +}; diff --git a/src/misc/util.ts b/src/misc/util.ts index e6e1fec..f167e4a 100644 --- a/src/misc/util.ts +++ b/src/misc/util.ts @@ -4,6 +4,7 @@ import { type WorkspaceModel } from "../contexts/workspace-context"; import { VERSION, type TreeData } from "./b3type"; import { dfs } from "./b3util"; import Path from "./path"; +import { stringifyJson } from "./stringify"; export const nanoid = customAlphabet( "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", @@ -16,7 +17,7 @@ export const readJson = (path: string): T => { }; export const writeJson = (path: string, data: T) => { - const str = JSON.stringify(data, undefined, 2); + const str = stringifyJson(data, { indent: 2 }); fs.writeFileSync(path, str, "utf-8"); }; @@ -34,6 +35,8 @@ export const readTree = (path: string) => { data.import = data.import || []; data.vars = data.vars || []; data.root = data.root || {}; + data.$override = data.$override || {}; + data.custom = data.custom || {}; // compatible with old version dfs(data.root, (node) => { @@ -57,6 +60,7 @@ export const writeTree = (path: string, data: TreeData) => { import: data.import, vars: data.vars, root: data.root, + custom: data.custom, $override: data.$override, }); };