import jsyaml from '../../../libs/js-yaml.mjs';
/**
* Robust block matcher (no regex)
* - Pairs each with the nearest preceding
* - Ignores unclosed
*/
function isValidOpenTagAt(s, i) {
if (s[i] !== '<') return false;
const head = s.slice(i, i + 6).toLowerCase();
if (head !== '' || next === '/' || /\s/.test(next))) return false;
return true;
}
function isValidCloseTagAt(s, i) {
if (s[i] !== '<') return false;
if (s[i + 1] !== '/') return false;
const head = s.slice(i, i + 7).toLowerCase();
if (head !== '';
}
function findTagEnd(s, openIndex) {
const end = s.indexOf('>', openIndex);
return end === -1 ? -1 : end;
}
function findStateBlockSpans(text) {
const s = String(text ?? '');
const closes = [];
for (let i = 0; i < s.length; i++) {
if (s[i] !== '<') continue;
if (isValidCloseTagAt(s, i)) closes.push(i);
}
if (!closes.length) return [];
const spans = [];
let searchEnd = s.length;
for (let cIdx = closes.length - 1; cIdx >= 0; cIdx--) {
const closeStart = closes[cIdx];
if (closeStart >= searchEnd) continue;
let closeEnd = closeStart + 7;
while (closeEnd < s.length && s[closeEnd] !== '>') closeEnd++;
if (s[closeEnd] !== '>') continue;
closeEnd += 1;
let openStart = -1;
for (let i = closeStart - 1; i >= 0; i--) {
if (s[i] !== '<') continue;
if (!isValidOpenTagAt(s, i)) continue;
const tagEnd = findTagEnd(s, i);
if (tagEnd === -1) continue;
if (tagEnd >= closeStart) continue;
openStart = i;
break;
}
if (openStart === -1) continue;
const openTagEnd = findTagEnd(s, openStart);
if (openTagEnd === -1) continue;
spans.push({
openStart,
openTagEnd: openTagEnd + 1,
closeStart,
closeEnd,
});
searchEnd = openStart;
}
spans.reverse();
return spans;
}
export function extractStateBlocks(text) {
const s = String(text ?? '');
const spans = findStateBlockSpans(s);
const out = [];
for (const sp of spans) {
const inner = s.slice(sp.openTagEnd, sp.closeStart);
if (inner.trim()) out.push(inner);
}
return out;
}
export function computeStateSignature(text) {
const s = String(text ?? '');
const spans = findStateBlockSpans(s);
if (!spans.length) return '';
const chunks = spans.map(sp => s.slice(sp.openStart, sp.closeEnd).trim());
return chunks.join('\n---\n');
}
export function parseStateBlock(content) {
const lines = String(content ?? '').split(/\r?\n/);
const rules = [];
const dataLines = [];
let inSchema = false;
let schemaPath = '';
let schemaLines = [];
let schemaBaseIndent = -1;
const flushSchema = () => {
if (schemaLines.length) {
const parsed = parseSchemaBlock(schemaPath, schemaLines);
rules.push(...parsed);
}
inSchema = false;
schemaPath = '';
schemaLines = [];
schemaBaseIndent = -1;
};
for (let i = 0; i < lines.length; i++) {
const raw = lines[i];
const trimmed = raw.trim();
const indent = raw.search(/\S/);
if (!trimmed || trimmed.startsWith('#')) {
if (inSchema && schemaBaseIndent >= 0) schemaLines.push(raw);
continue;
}
// $schema 开始
if (trimmed.startsWith('$schema')) {
flushSchema();
const rest = trimmed.slice(7).trim();
schemaPath = rest || '';
inSchema = true;
schemaBaseIndent = -1;
continue;
}
if (inSchema) {
if (schemaBaseIndent < 0) {
schemaBaseIndent = indent;
}
// 缩进回退 => schema 结束
if (indent < schemaBaseIndent && indent >= 0 && trimmed) {
flushSchema();
i--;
continue;
}
schemaLines.push(raw);
continue;
}
// 普通 $rule($ro, $range, $step, $enum)
if (trimmed.startsWith('$')) {
const parsed = parseRuleLine(trimmed);
if (parsed) rules.push(parsed);
continue;
}
dataLines.push(raw);
}
flushSchema();
const ops = parseDataLines(dataLines);
return { rules, ops };
}
/**
* 解析数据行
*/
function stripYamlInlineComment(s) {
const text = String(s ?? '');
if (!text) return '';
let inSingle = false;
let inDouble = false;
let escaped = false;
for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (inSingle) {
if (ch === "'") {
if (text[i + 1] === "'") { i++; continue; }
inSingle = false;
}
continue;
}
if (inDouble) {
if (escaped) { escaped = false; continue; }
if (ch === '\\') { escaped = true; continue; }
if (ch === '"') inDouble = false;
continue;
}
if (ch === "'") { inSingle = true; continue; }
if (ch === '"') { inDouble = true; continue; }
if (ch === '#') {
const prev = i > 0 ? text[i - 1] : '';
if (i === 0 || /\s/.test(prev)) {
return text.slice(0, i).trimEnd();
}
}
}
return text.trimEnd();
}
function parseDataLines(lines) {
const results = [];
let pendingPath = null;
let pendingLines = [];
const flushPending = () => {
if (!pendingPath) return;
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();
let rhs = trimmed.slice(colonIdx + 1).trim();
rhs = stripYamlInlineComment(rhs);
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' };
const parenNum = t.match(/^\((-?\d+(?:\.\d+)?)\)$/);
if (parenNum) return { op: 'set', value: Number(parenNum[1]) };
if (/^\+\d/.test(t) || /^-\d/.test(t)) {
const n = Number(t);
if (Number.isFinite(n)) return { op: 'inc', delta: n };
}
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 };
} catch {}
return { op: 'set', value: t, warning: '+[] 解析失败' };
}
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 };
} catch {}
return { op: 'set', value: t, warning: '-[] 解析失败' };
}
if (/^-?\d+(?:\.\d+)?$/.test(t)) return { op: 'set', value: Number(t) };
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 };
if (t.startsWith('{') || t.startsWith('[')) {
try { return { op: 'set', value: JSON.parse(t) }; }
catch { return { op: 'set', value: t, warning: 'JSON 解析失败' }; }
}
return { op: 'set', value: t };
}