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 }; }