), so we keep those together. const splitBlocks = text => { const lines = (text || '').split('\n'); const blocks = []; let buffer = []; let fence = null; let inList = false; const flush = () => { const block = buffer.join('\n'); if (block.trim()) blocks.push(block); buffer = []; inList = false; }; lines.forEach((line, index) => { const marker = line.match(FENCE_RE)?.[1]; if (marker && !fence) fence = marker; else if (fence && line.trimStart().startsWith(fence)) fence = null; if (fence) { buffer.push(line); return; } if (LIST_ITEM_RE.test(line)) inList = true; if (line.trim() !== '') { buffer.push(line); return; } // Blank line: keep it when the current list continues on the next non-blank // line (another item or an indented continuation); otherwise end the block. const next = lines.slice(index + 1).find(other => other.trim() !== ''); if (inList && next && (LIST_ITEM_RE.test(next) || /^\s/.test(next))) { buffer.push(line); } else { flush(); } }); flush(); return blocks; }; const BLOCK_TYPE = { equal: 'equal', del: 'removed', ins: 'added' }; // Diffs the body block by block. Blocks match when they render to the same HTML // (the check staging uses), so only edits that change the page show as a diff. export const buildDiffBlocks = (oldText, newText) => { const toBlocks = text => splitBlocks(text).map(md => ({ md, key: commonmark.render(md) })); const ops = diffSequence(toBlocks(oldText), toBlocks(newText), b => b.key); return ops.map(op => ({ type: BLOCK_TYPE[op.type], md: op.item.md })); }; export const hasPendingChanges = article => article?.draftTitle != null || article?.draftContent != null;