385 lines
10 KiB
JavaScript
385 lines
10 KiB
JavaScript
/**
|
||
* @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;
|
||
}
|
||
}
|