Files
LittleWhiteBox/modules/variables/state2/parser.js

258 lines
7.7 KiB
JavaScript
Raw Normal View History

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') : '';
}
/**
2026-02-01 02:55:43 +08:00
* 解析 <state>
* 返回: { rules: [{path, rule}], ops: [{path, op, value, ...}] }
*/
export function parseStateBlock(content) {
const lines = String(content ?? '').split(/\r?\n/);
2026-02-01 02:55:43 +08:00
const rules = [];
const dataLines = [];
// 第一遍:分离规则行和数据行
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
// 规则行:以 $ 开头
if (trimmed.startsWith('$')) {
const parsed = parseRuleLineInternal(trimmed);
if (parsed) rules.push(parsed);
} else {
dataLines.push(line);
}
}
// 第二遍:解析数据
const ops = parseDataLines(dataLines);
return { rules, ops };
}
function parseRuleLineInternal(line) {
const tokens = line.trim().split(/\s+/);
const directives = [];
let pathStart = 0;
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].startsWith('$')) {
directives.push(tokens[i]);
pathStart = i + 1;
} else {
break;
}
}
const path = tokens.slice(pathStart).join(' ').trim();
if (!path || !directives.length) return null;
const rule = {};
for (const tok of directives) {
if (tok === '$ro') { rule.ro = true; continue; }
if (tok === '$lock') { rule.lock = true; continue; }
const rangeMatch = tok.match(/^\$range=\[\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\]$/);
if (rangeMatch) {
rule.min = Math.min(Number(rangeMatch[1]), Number(rangeMatch[2]));
rule.max = Math.max(Number(rangeMatch[1]), Number(rangeMatch[2]));
continue;
}
const stepMatch = tok.match(/^\$step=(\d+(?:\.\d+)?)$/);
if (stepMatch) { rule.step = Math.abs(Number(stepMatch[1])); continue; }
const enumMatch = tok.match(/^\$enum=\{([^}]+)\}$/);
if (enumMatch) {
rule.enum = enumMatch[1].split(/[;]/).map(s => s.trim()).filter(Boolean);
continue;
}
}
return { path, rule };
}
function parseDataLines(lines) {
const results = [];
let pendingPath = null;
let pendingLines = [];
const flushPending = () => {
if (!pendingPath) return;
2026-02-01 02:55:43 +08:00
if (!pendingLines.length) {
results.push({ path: pendingPath, op: 'set', value: '' });
pendingPath = null;
pendingLines = [];
return;
}
2026-02-01 02:55:43 +08:00
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' };
2026-02-01 02:55:43 +08:00
// (负数) 强制 set
const parenNum = t.match(/^\((-?\d+(?:\.\d+)?)\)$/);
if (parenNum) return { op: 'set', value: Number(parenNum[1]) };
2026-02-01 02:55:43 +08:00
// +N / -N
if (/^\+\d/.test(t) || /^-\d/.test(t)) {
const n = Number(t);
if (Number.isFinite(n)) return { op: 'inc', delta: n };
}
2026-02-01 02:55:43 +08:00
// +"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 };
2026-02-01 02:55:43 +08:00
} catch {}
return { op: 'set', value: t, warning: '+[] 解析失败' };
}
2026-02-01 02:55:43 +08:00
// -"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 };
2026-02-01 02:55:43 +08:00
} catch {}
return { op: 'set', value: t, warning: '-[] 解析失败' };
}
2026-02-01 02:55:43 +08:00
// 裸数字
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 };
2026-02-01 02:55:43 +08:00
// JSON array/object
if (t.startsWith('{') || t.startsWith('[')) {
try { return { op: 'set', value: JSON.parse(t) }; }
2026-02-01 02:55:43 +08:00
catch { return { op: 'set', value: t, warning: 'JSON 解析失败' }; }
}
2026-02-01 02:55:43 +08:00
// 兜底
return { op: 'set', value: t };
}