Files
LittleWhiteBox/core/variable-path.js
2026-01-17 16:34:39 +08:00

385 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file core/variable-path.js
* @description 变量路径解析与深层操作工具
* @description 零依赖的纯函数模块,供多个变量相关模块使用
*/
/* ============= 路径解析 ============= */
/**
* 解析带中括号的路径
* @param {string} path - 路径字符串,如 "a.b[0].c" 或 "a['key'].b"
* @returns {Array<string|number>} 路径段数组,如 ["a", "b", 0, "c"]
* @example
* lwbSplitPathWithBrackets("a.b[0].c") // ["a", "b", 0, "c"]
* lwbSplitPathWithBrackets("a['key'].b") // ["a", "key", "b"]
* lwbSplitPathWithBrackets("a[\"0\"].b") // ["a", "0", "b"] (字符串"0")
*/
export function lwbSplitPathWithBrackets(path) {
const s = String(path || '');
const segs = [];
let i = 0;
let buf = '';
const flushBuf = () => {
if (buf.length) {
const pushed = /^\d+$/.test(buf) ? Number(buf) : buf;
segs.push(pushed);
buf = '';
}
};
while (i < s.length) {
const ch = s[i];
if (ch === '.') {
flushBuf();
i++;
continue;
}
if (ch === '[') {
flushBuf();
i++;
// 跳过空白
while (i < s.length && /\s/.test(s[i])) i++;
let val;
if (s[i] === '"' || s[i] === "'") {
// 引号包裹的字符串键
const quote = s[i++];
let str = '';
let esc = false;
while (i < s.length) {
const c = s[i++];
if (esc) {
str += c;
esc = false;
continue;
}
if (c === '\\') {
esc = true;
continue;
}
if (c === quote) break;
str += c;
}
val = str;
while (i < s.length && /\s/.test(s[i])) i++;
if (s[i] === ']') i++;
} else {
// 无引号,可能是数字索引或普通键
let raw = '';
while (i < s.length && s[i] !== ']') raw += s[i++];
if (s[i] === ']') i++;
const trimmed = String(raw).trim();
val = /^-?\d+$/.test(trimmed) ? Number(trimmed) : trimmed;
}
segs.push(val);
continue;
}
buf += ch;
i++;
}
flushBuf();
return segs;
}
/**
* 分离路径和值(用于命令解析)
* @param {string} raw - 原始字符串,如 "a.b[0] some value"
* @returns {{path: string, value: string}} 路径和值
* @example
* lwbSplitPathAndValue("a.b[0] hello") // { path: "a.b[0]", value: "hello" }
* lwbSplitPathAndValue("a.b") // { path: "a.b", value: "" }
*/
export function lwbSplitPathAndValue(raw) {
const s = String(raw || '');
let i = 0;
let depth = 0; // 中括号深度
let inQ = false; // 是否在引号内
let qch = ''; // 当前引号字符
for (; i < s.length; i++) {
const ch = s[i];
if (inQ) {
if (ch === '\\') {
i++;
continue;
}
if (ch === qch) {
inQ = false;
qch = '';
}
continue;
}
if (ch === '"' || ch === "'") {
inQ = true;
qch = ch;
continue;
}
if (ch === '[') {
depth++;
continue;
}
if (ch === ']') {
depth = Math.max(0, depth - 1);
continue;
}
// 在顶层遇到空白,分割
if (depth === 0 && /\s/.test(ch)) {
const path = s.slice(0, i).trim();
const value = s.slice(i + 1).trim();
return { path, value };
}
}
return { path: s.trim(), value: '' };
}
/**
* 简单分割路径段(仅支持点号分隔)
* @param {string} path - 路径字符串
* @returns {Array<string|number>} 路径段数组
*/
export function splitPathSegments(path) {
return String(path || '')
.split('.')
.map(s => s.trim())
.filter(Boolean)
.map(seg => /^\d+$/.test(seg) ? Number(seg) : seg);
}
/**
* 规范化路径(统一为点号分隔格式)
* @param {string} path - 路径字符串
* @returns {string} 规范化后的路径
* @example
* normalizePath("a[0].b['c']") // "a.0.b.c"
*/
export function normalizePath(path) {
try {
const segs = lwbSplitPathWithBrackets(path);
return segs.map(s => String(s)).join('.');
} catch {
return String(path || '').trim();
}
}
/**
* 获取根变量名和子路径
* @param {string} name - 完整路径
* @returns {{root: string, subPath: string}}
* @example
* getRootAndPath("a.b.c") // { root: "a", subPath: "b.c" }
* getRootAndPath("a") // { root: "a", subPath: "" }
*/
export function getRootAndPath(name) {
const segs = String(name || '').split('.').map(s => s.trim()).filter(Boolean);
if (segs.length <= 1) {
return { root: String(name || '').trim(), subPath: '' };
}
return { root: segs[0], subPath: segs.slice(1).join('.') };
}
/**
* 拼接路径
* @param {string} base - 基础路径
* @param {string} more - 追加路径
* @returns {string} 拼接后的路径
*/
export function joinPath(base, more) {
return base ? (more ? base + '.' + more : base) : more;
}
/* ============= 深层对象操作 ============= */
/**
* 确保深层容器存在
* @param {Object|Array} root - 根对象
* @param {Array<string|number>} segs - 路径段数组
* @returns {{parent: Object|Array, lastKey: string|number}} 父容器和最后一个键
*/
export function ensureDeepContainer(root, segs) {
let cur = root;
for (let i = 0; i < segs.length - 1; i++) {
const key = segs[i];
const nextKey = segs[i + 1];
const shouldBeArray = typeof nextKey === 'number';
let val = cur?.[key];
if (val === undefined || val === null || typeof val !== 'object') {
cur[key] = shouldBeArray ? [] : {};
}
cur = cur[key];
}
return {
parent: cur,
lastKey: segs[segs.length - 1]
};
}
/**
* 设置深层值
* @param {Object} root - 根对象
* @param {string} path - 路径(点号分隔)
* @param {*} value - 要设置的值
* @returns {boolean} 是否有变化
*/
export function setDeepValue(root, path, value) {
const segs = splitPathSegments(path);
if (segs.length === 0) return false;
const { parent, lastKey } = ensureDeepContainer(root, segs);
const prev = parent[lastKey];
if (prev !== value) {
parent[lastKey] = value;
return true;
}
return false;
}
/**
* 向深层数组推入值(去重)
* @param {Object} root - 根对象
* @param {string} path - 路径
* @param {*|Array} values - 要推入的值
* @returns {boolean} 是否有变化
*/
export function pushDeepValue(root, path, values) {
const segs = splitPathSegments(path);
if (segs.length === 0) return false;
const { parent, lastKey } = ensureDeepContainer(root, segs);
let arr = parent[lastKey];
let changed = false;
// 确保是数组
if (!Array.isArray(arr)) {
arr = arr === undefined ? [] : [arr];
}
const incoming = Array.isArray(values) ? values : [values];
for (const v of incoming) {
if (!arr.includes(v)) {
arr.push(v);
changed = true;
}
}
if (changed) {
parent[lastKey] = arr;
}
return changed;
}
/**
* 删除深层键
* @param {Object} root - 根对象
* @param {string} path - 路径
* @returns {boolean} 是否成功删除
*/
export function deleteDeepKey(root, path) {
const segs = splitPathSegments(path);
if (segs.length === 0) return false;
const { parent, lastKey } = ensureDeepContainer(root, segs);
// 父级是数组
if (Array.isArray(parent)) {
// 数字索引:直接删除
if (typeof lastKey === 'number' && lastKey >= 0 && lastKey < parent.length) {
parent.splice(lastKey, 1);
return true;
}
// 值匹配:删除所有匹配项
const equal = (a, b) => a === b || a == b || String(a) === String(b);
let changed = false;
for (let i = parent.length - 1; i >= 0; i--) {
if (equal(parent[i], lastKey)) {
parent.splice(i, 1);
changed = true;
}
}
return changed;
}
// 父级是对象
if (Object.prototype.hasOwnProperty.call(parent, lastKey)) {
delete parent[lastKey];
return true;
}
return false;
}
/* ============= 值处理工具 ============= */
/**
* 安全的 JSON 序列化
* @param {*} v - 要序列化的值
* @returns {string} JSON 字符串,失败返回空字符串
*/
export function safeJSONStringify(v) {
try {
return JSON.stringify(v);
} catch {
return '';
}
}
/**
* 尝试将原始值解析为对象
* @param {*} rootRaw - 原始值(可能是字符串或对象)
* @returns {Object|Array|null} 解析后的对象,失败返回 null
*/
export function maybeParseObject(rootRaw) {
if (typeof rootRaw === 'string') {
try {
const s = rootRaw.trim();
return (s && (s[0] === '{' || s[0] === '[')) ? JSON.parse(s) : null;
} catch {
return null;
}
}
return (rootRaw && typeof rootRaw === 'object') ? rootRaw : null;
}
/**
* 将值转换为输出字符串
* @param {*} v - 任意值
* @returns {string} 字符串表示
*/
export function valueToString(v) {
if (v == null) return '';
if (typeof v === 'object') return safeJSONStringify(v) || '';
return String(v);
}
/**
* 深度克隆对象(使用 structuredClone 或 JSON
* @param {*} obj - 要克隆的对象
* @returns {*} 克隆后的对象
*/
export function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
try {
return typeof structuredClone === 'function'
? structuredClone(obj)
: JSON.parse(JSON.stringify(obj));
} catch {
return obj;
}
}