Initial commit
This commit is contained in:
384
core/variable-path.js
Normal file
384
core/variable-path.js
Normal file
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user