197 lines
6.2 KiB
JavaScript
197 lines
6.2 KiB
JavaScript
|
|
import jsyaml from '../../../libs/js-yaml.mjs';
|
|||
|
|
|
|||
|
|
const STATE_TAG_RE = /<\s*state\b[^>]*>([\s\S]*?)<\s*\/\s*state\s*>/gi;
|
|||
|
|
|
|||
|
|
export function extractStateBlocks(text) {
|
|||
|
|
const s = String(text ?? '');
|
|||
|
|
if (!s || s.toLowerCase().indexOf('<state') === -1) return [];
|
|||
|
|
const out = [];
|
|||
|
|
STATE_TAG_RE.lastIndex = 0;
|
|||
|
|
let m;
|
|||
|
|
while ((m = STATE_TAG_RE.exec(s)) !== null) {
|
|||
|
|
const inner = String(m[1] ?? '');
|
|||
|
|
if (inner.trim()) out.push(inner);
|
|||
|
|
}
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function computeStateSignature(text) {
|
|||
|
|
const s = String(text ?? '');
|
|||
|
|
if (!s || s.toLowerCase().indexOf('<state') === -1) return '';
|
|||
|
|
const chunks = [];
|
|||
|
|
STATE_TAG_RE.lastIndex = 0;
|
|||
|
|
let m;
|
|||
|
|
while ((m = STATE_TAG_RE.exec(s)) !== null) chunks.push(String(m[0] ?? '').trim());
|
|||
|
|
return chunks.length ? chunks.join('\n---\n') : '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 解析 <state> 块内容 -> ops[]
|
|||
|
|
* 单行支持运算符,多行只支持覆盖 set(YAML)
|
|||
|
|
*/
|
|||
|
|
export function parseStateBlock(content) {
|
|||
|
|
const results = [];
|
|||
|
|
const lines = String(content ?? '').split(/\r?\n/);
|
|||
|
|
|
|||
|
|
let pendingPath = null;
|
|||
|
|
let pendingLines = [];
|
|||
|
|
|
|||
|
|
const flushPending = () => {
|
|||
|
|
if (!pendingPath) return;
|
|||
|
|
// 没有任何缩进行:视为 set 空字符串
|
|||
|
|
if (!pendingLines.length) {
|
|||
|
|
results.push({ path: pendingPath, op: 'set', value: '' });
|
|||
|
|
pendingPath = null;
|
|||
|
|
pendingLines = [];
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
try {
|
|||
|
|
// 去除公共缩进
|
|||
|
|
const nonEmpty = pendingLines.filter(l => l.trim());
|
|||
|
|
const minIndent = nonEmpty.length
|
|||
|
|
? Math.min(...nonEmpty.map(l => l.search(/\S/)))
|
|||
|
|
: 0;
|
|||
|
|
|
|||
|
|
const yamlText = pendingLines
|
|||
|
|
.map(l => (l.trim() ? l.slice(minIndent) : ''))
|
|||
|
|
.join('\n');
|
|||
|
|
|
|||
|
|
const obj = jsyaml.load(yamlText);
|
|||
|
|
results.push({ path: pendingPath, op: 'set', value: obj });
|
|||
|
|
} catch (e) {
|
|||
|
|
results.push({ path: pendingPath, op: 'set', value: null, warning: `YAML 解析失败: ${e.message}` });
|
|||
|
|
} finally {
|
|||
|
|
pendingPath = null;
|
|||
|
|
pendingLines = [];
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
for (const raw of lines) {
|
|||
|
|
const trimmed = raw.trim();
|
|||
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|||
|
|
|
|||
|
|
const indent = raw.search(/\S/);
|
|||
|
|
|
|||
|
|
if (indent === 0) {
|
|||
|
|
flushPending();
|
|||
|
|
const colonIdx = findTopLevelColon(trimmed);
|
|||
|
|
if (colonIdx === -1) continue;
|
|||
|
|
|
|||
|
|
const path = trimmed.slice(0, colonIdx).trim();
|
|||
|
|
const rhs = trimmed.slice(colonIdx + 1).trim();
|
|||
|
|
if (!path) continue;
|
|||
|
|
|
|||
|
|
if (!rhs) {
|
|||
|
|
pendingPath = path;
|
|||
|
|
pendingLines = [];
|
|||
|
|
} else {
|
|||
|
|
results.push({ path, ...parseInlineValue(rhs) });
|
|||
|
|
}
|
|||
|
|
} else if (pendingPath) {
|
|||
|
|
pendingLines.push(raw);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
flushPending();
|
|||
|
|
return results;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function findTopLevelColon(line) {
|
|||
|
|
let inQuote = false;
|
|||
|
|
let q = '';
|
|||
|
|
let esc = false;
|
|||
|
|
for (let i = 0; i < line.length; i++) {
|
|||
|
|
const ch = line[i];
|
|||
|
|
if (esc) { esc = false; continue; }
|
|||
|
|
if (ch === '\\') { esc = true; continue; }
|
|||
|
|
if (!inQuote && (ch === '"' || ch === "'")) { inQuote = true; q = ch; continue; }
|
|||
|
|
if (inQuote && ch === q) { inQuote = false; q = ''; continue; }
|
|||
|
|
if (!inQuote && ch === ':') return i;
|
|||
|
|
}
|
|||
|
|
return -1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function unescapeString(s) {
|
|||
|
|
return String(s ?? '')
|
|||
|
|
.replace(/\\n/g, '\n')
|
|||
|
|
.replace(/\\t/g, '\t')
|
|||
|
|
.replace(/\\r/g, '\r')
|
|||
|
|
.replace(/\\"/g, '"')
|
|||
|
|
.replace(/\\'/g, "'")
|
|||
|
|
.replace(/\\\\/g, '\\');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 单行内联值解析
|
|||
|
|
*/
|
|||
|
|
export function parseInlineValue(raw) {
|
|||
|
|
const t = String(raw ?? '').trim();
|
|||
|
|
|
|||
|
|
if (t === 'null') return { op: 'del' };
|
|||
|
|
|
|||
|
|
// (负数) 用于强制 set -5,而不是 inc -5
|
|||
|
|
const parenNum = t.match(/^\((-?\d+(?:\.\d+)?)\)$/);
|
|||
|
|
if (parenNum) return { op: 'set', value: Number(parenNum[1]) };
|
|||
|
|
|
|||
|
|
// +10 / -20
|
|||
|
|
if (/^\+\d/.test(t) || /^-\d/.test(t)) {
|
|||
|
|
const n = Number(t);
|
|||
|
|
if (Number.isFinite(n)) return { op: 'inc', delta: n };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// +"str" / +'str'
|
|||
|
|
const pushD = t.match(/^\+"((?:[^"\\]|\\.)*)"\s*$/);
|
|||
|
|
if (pushD) return { op: 'push', value: unescapeString(pushD[1]) };
|
|||
|
|
const pushS = t.match(/^\+'((?:[^'\\]|\\.)*)'\s*$/);
|
|||
|
|
if (pushS) return { op: 'push', value: unescapeString(pushS[1]) };
|
|||
|
|
|
|||
|
|
// +[...]
|
|||
|
|
if (t.startsWith('+[')) {
|
|||
|
|
try {
|
|||
|
|
const arr = JSON.parse(t.slice(1));
|
|||
|
|
if (Array.isArray(arr)) return { op: 'push', value: arr };
|
|||
|
|
return { op: 'set', value: t, warning: '+[] 不是数组,作为字符串' };
|
|||
|
|
} catch {
|
|||
|
|
return { op: 'set', value: t, warning: '+[] JSON 解析失败,作为字符串' };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// -"str" / -'str'
|
|||
|
|
const popD = t.match(/^-"((?:[^"\\]|\\.)*)"\s*$/);
|
|||
|
|
if (popD) return { op: 'pop', value: unescapeString(popD[1]) };
|
|||
|
|
const popS = t.match(/^-'((?:[^'\\]|\\.)*)'\s*$/);
|
|||
|
|
if (popS) return { op: 'pop', value: unescapeString(popS[1]) };
|
|||
|
|
|
|||
|
|
// -[...]
|
|||
|
|
if (t.startsWith('-[')) {
|
|||
|
|
try {
|
|||
|
|
const arr = JSON.parse(t.slice(1));
|
|||
|
|
if (Array.isArray(arr)) return { op: 'pop', value: arr };
|
|||
|
|
return { op: 'set', value: t, warning: '-[] 不是数组,作为字符串' };
|
|||
|
|
} catch {
|
|||
|
|
return { op: 'set', value: t, warning: '-[] JSON 解析失败,作为字符串' };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 裸数字 set
|
|||
|
|
if (/^-?\d+(?:\.\d+)?$/.test(t)) return { op: 'set', value: Number(t) };
|
|||
|
|
|
|||
|
|
// "str" / 'str'
|
|||
|
|
const strD = t.match(/^"((?:[^"\\]|\\.)*)"\s*$/);
|
|||
|
|
if (strD) return { op: 'set', value: unescapeString(strD[1]) };
|
|||
|
|
const strS = t.match(/^'((?:[^'\\]|\\.)*)'\s*$/);
|
|||
|
|
if (strS) return { op: 'set', value: unescapeString(strS[1]) };
|
|||
|
|
|
|||
|
|
if (t === 'true') return { op: 'set', value: true };
|
|||
|
|
if (t === 'false') return { op: 'set', value: false };
|
|||
|
|
|
|||
|
|
// JSON set
|
|||
|
|
if (t.startsWith('{') || t.startsWith('[')) {
|
|||
|
|
try { return { op: 'set', value: JSON.parse(t) }; }
|
|||
|
|
catch { return { op: 'set', value: t, warning: 'JSON 解析失败,作为字符串' }; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 兜底 set 原文本
|
|||
|
|
return { op: 'set', value: t };
|
|||
|
|
}
|