/* Safe, deterministic editor operations. Exported for Node regression tests. */ (function (root, factory) { const api=factory(); if(typeof module==='object' && module.exports)module.exports=api; root.SiteForgeEditorHelpers=api; })(typeof window!=='undefined'?window:globalThis, function(){ 'use strict'; const deepCopy=value=>JSON.parse(JSON.stringify(value)); function reorder(items, from, to){ if(!Array.isArray(items)||!Number.isInteger(from)||!Number.isInteger(to)||from<0||to<0||from>=items.length||to>=items.length)return null; if(from===to)return items.slice(); const next=items.slice();next.splice(to,0,next.splice(from,1)[0]);return next; } function duplicate(block, idFactory){ if(!block || typeof idFactory!=='function')throw Error('无效的区块'); const item=deepCopy(block); const rewrite=node=>{ node.id=idFactory(); if(node.type==='layout')for(const col of node.columns||[])for(const child of col.blocks||[])rewrite(child); }; rewrite(item);return item; } function filterBlocks(blocks, term, names={}){ const query=String(term||'').trim().toLocaleLowerCase(); if(!query)return blocks.map((block,index)=>({block,index})); return blocks.flatMap((block,index)=>{ const description=[block.title,block.subtitle,block.type,names[block.type]?.name] .concat(block.type==='layout'?(block.columns||[]).flatMap(c=>(c.blocks||[]).map(b=>b.title||b.type)):[]) .filter(x=>typeof x==='string').join(' ').toLocaleLowerCase(); return description.includes(query)?[{block,index}]:[]; }); } function historyState(snapshots,cursor){return {canUndo:cursor>0,canRedo:cursor>=0&&cursorb.id===id):-1;} function reorderById(rows,sourceId,targetId){ const from=findTopIndex(rows,sourceId),to=findTopIndex(rows,targetId); return from<0||to<0||from===to?null:reorder(rows,from,to); } function adjacentWidths(block,widths){ if(!block||block.type!=='layout'||!Array.isArray(block.columns)||!Array.isArray(widths)||widths.length!==block.columns.length)return null; const validated=widths.map(v=>Number(v)); if(validated.some(v=>!Number.isFinite(v)||v<.5||v>4))return null; // Keep the same type and field shape expected by blueprints.validate_blocks. return validated.map(v=>Math.round(v*100)/100); } function safeCanvasCommand(command,rows,types){ if(!command||typeof command!=='object'||typeof command.blockId!=='string'||command.blockId.length>150)return false; const i=findTopIndex(rows,command.blockId); if(i<0)return false; const actions=['select','edit','up','down','add-after','duplicate','delete','reorder','resize-columns','inline-text']; if(!actions.includes(command.action))return false; if(command.action==='reorder')return findTopIndex(rows,command.targetId)>=0; if(command.action==='resize-columns')return !!adjacentWidths(rows[i],command.widths); if(command.action==='inline-text')return (command.field==='title'||command.field==='description')&& typeof command.value==='string'&&command.value.length<=500&&types?.[rows[i].type]?.fields?.includes(command.field); return true; } // V8.24: resolve the actual parent of a nested child from the validated tree. // A client message supplies stable IDs, never trusted indexes or an editable path. // Renderer accepts at most two layout levels; a bounded walk avoids malformed cycles. function findNestedChild(rows,parentId,childId){ if(typeof parentId!=='string'||typeof childId!=='string'||!Array.isArray(rows)|| !parentId||!childId||parentId===childId)return null; for(let rootIndex=0;rootIndex2)return null; for(let ci=0;ci2)return null; for(const col of layout.columns||[]){ for(const child of col.blocks||[]){ if(child?.id===childId)return findNestedChild(rows,layout.id,childId); if(child?.type==='layout'){ const match=visit(child,depth+1); if(match)return match; } } } return null; } return visit(root,0); } function findLayout(rows,layoutId){ if(typeof layoutId!=='string'||!Array.isArray(rows))return null; const topIndex=findTopIndex(rows,layoutId); if(topIndex>=0)return rows[topIndex].type==='layout'?{layout:rows[topIndex],rootIndex:topIndex,depth:0}:null; for(let i=0;i4)return null; if(preset==='equal')return Array(count).fill(1); if(count!==2)return null; if(preset==='left-wide')return [2,1]; if(preset==='right-wide')return [1,2]; return null; } function moveNested(block,columnIndex,childIndex,direction){ if(!block||block.type!=='layout'||![-1,1].includes(direction))return null; const list=block.columns?.[columnIndex]?.blocks; return Array.isArray(list)?reorder(list,childIndex,childIndex+direction):null; } return {reorder,duplicate,filterBlocks,historyState,clampNumber,shouldAcceptPreview,findTopIndex,reorderById,adjacentWidths,safeCanvasCommand,findNestedChild,findChildInRoot,findLayout,safeNestedCanvasCommand,layoutPreset,moveNested}; });