"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// packages/editor/src/components/post-revisions-preview/block-diff.js
var block_diff_exports = {};
__export(block_diff_exports, {
  diffRevisionContent: () => diffRevisionContent
});
module.exports = __toCommonJS(block_diff_exports);
var import_array = require("diff/lib/diff/array");
var import_word = require("diff/lib/diff/word");
var import_block_serialization_default_parser = require("@wordpress/block-serialization-default-parser");
var import_blocks = require("@wordpress/blocks");
var import_rich_text = require("@wordpress/rich-text");
var import_i18n = require("@wordpress/i18n");
var import_lock_unlock = require("../../lock-unlock.cjs");
var { parseRawBlock } = (0, import_lock_unlock.unlock)(import_blocks.privateApis);
function stringifyValue(value) {
  if (value === null || value === void 0) {
    return "";
  }
  if (typeof value === "object") {
    return JSON.stringify(value, null, 2);
  }
  return String(value);
}
function textSimilarity(text1, text2) {
  if (!text1 && !text2) {
    return 1;
  }
  if (!text1 || !text2) {
    return 0;
  }
  const segmenter = new Intl.Segmenter(void 0, {
    granularity: "word"
  });
  const wordLikeRegex = /[\p{L}\p{N}]/u;
  const getWords = (text) => {
    const words = [];
    for (const { segment, isWordLike } of segmenter.segment(text)) {
      if (isWordLike || wordLikeRegex.test(segment)) {
        words.push(segment);
      }
    }
    return words;
  };
  const words1 = getWords(text1);
  const words2 = getWords(text2);
  if (words1.length === 0 && words2.length === 0) {
    return 1;
  }
  const set1 = new Set(words1);
  let intersection = 0;
  for (const word of words2) {
    if (set1.has(word)) {
      intersection++;
    }
  }
  const total = Math.max(words1.length, words2.length);
  return total > 0 ? intersection / total : 0;
}
function pairSimilarBlocks(blocks) {
  const removed = [];
  const added = [];
  blocks.forEach((block, index) => {
    const status = block.__revisionDiffStatus?.status;
    if (status === "removed") {
      removed.push({ block, index });
    } else if (status === "added") {
      added.push({ block, index });
    }
  });
  if (removed.length === 0 || added.length === 0) {
    return blocks;
  }
  const pairedRemoved = /* @__PURE__ */ new Set();
  const pairedAdded = /* @__PURE__ */ new Set();
  const modifications = /* @__PURE__ */ new Map();
  const SIMILARITY_THRESHOLD = 0.5;
  const addedByName = /* @__PURE__ */ new Map();
  for (const add of added) {
    const name = add.block.blockName;
    if (!addedByName.has(name)) {
      addedByName.set(name, []);
    }
    addedByName.get(name).push(add);
  }
  const removedByName = /* @__PURE__ */ new Map();
  for (const rem of removed) {
    const name = rem.block.blockName;
    if (!removedByName.has(name)) {
      removedByName.set(name, []);
    }
    removedByName.get(name).push(rem);
  }
  let maxPairedAddedIndex = -1;
  for (const rem of removed) {
    const candidates = addedByName.get(rem.block.blockName) || [];
    const sameNameRemoved = removedByName.get(rem.block.blockName) || [];
    const unpaired = candidates.filter(
      (add) => !modifications.has(add.index) && add.index > maxPairedAddedIndex
    );
    if (unpaired.length === 0) {
      continue;
    }
    let bestMatch = null;
    if (sameNameRemoved.length === 1 && unpaired.length === 1) {
      const add = unpaired[0];
      const attrsMatch = JSON.stringify(rem.block.attrs) === JSON.stringify(add.block.attrs);
      const contentMatch = (rem.block.innerHTML || "") === (add.block.innerHTML || "");
      if (!contentMatch || !attrsMatch) {
        bestMatch = add;
      }
    } else {
      let bestScore = 0;
      for (const add of unpaired) {
        const score = textSimilarity(
          rem.block.innerHTML || "",
          add.block.innerHTML || ""
        );
        const attrsMatch = JSON.stringify(rem.block.attrs) === JSON.stringify(add.block.attrs);
        if (score > bestScore && score > SIMILARITY_THRESHOLD && (score < 1 || !attrsMatch)) {
          bestScore = score;
          bestMatch = add;
        }
      }
    }
    if (bestMatch) {
      maxPairedAddedIndex = bestMatch.index;
      const modifiedBlock = {
        ...bestMatch.block,
        __revisionDiffStatus: { status: "modified" },
        __previousRawBlock: rem.block
      };
      const lo = Math.min(rem.index, bestMatch.index);
      const hi = Math.max(rem.index, bestMatch.index);
      let hasAddedBetween = false;
      for (let i = lo + 1; i < hi; i++) {
        if (blocks[i].__revisionDiffStatus?.status === "added" && !pairedAdded.has(i)) {
          hasAddedBetween = true;
          break;
        }
      }
      if (hasAddedBetween) {
        modifications.set(bestMatch.index, modifiedBlock);
        pairedRemoved.add(rem.index);
      } else {
        modifications.set(rem.index, modifiedBlock);
        pairedAdded.add(bestMatch.index);
      }
    }
  }
  return blocks.map((block, index) => {
    if (pairedRemoved.has(index) || pairedAdded.has(index)) {
      return null;
    }
    if (modifications.has(index)) {
      return modifications.get(index);
    }
    return block;
  }).filter(Boolean);
}
function diffRawBlocks(currentRaw, previousRaw) {
  const createBlockSignature = (rawBlock) => JSON.stringify({
    name: rawBlock.blockName,
    attrs: rawBlock.attrs,
    // Use innerContent filtered to non-null and non-whitespace-only strings.
    // This excludes whitespace between inner blocks which changes based on count.
    html: (rawBlock.innerContent || []).filter(
      (c) => c !== null && c.trim() !== ""
    )
  });
  const currentSigs = currentRaw.map(createBlockSignature);
  const previousSigs = previousRaw.map(createBlockSignature);
  const diff = (0, import_array.diffArrays)(previousSigs, currentSigs);
  const result = [];
  let currIdx = 0;
  let prevIdx = 0;
  for (const part of diff) {
    if (part.added) {
      for (let i = 0; i < part.count; i++) {
        result.push({
          ...currentRaw[currIdx++],
          __revisionDiffStatus: { status: "added" }
        });
      }
    } else if (part.removed) {
      for (let i = 0; i < part.count; i++) {
        result.push({
          ...previousRaw[prevIdx++],
          __revisionDiffStatus: { status: "removed" }
        });
      }
    } else {
      for (let i = 0; i < part.count; i++) {
        const currBlock = currentRaw[currIdx++];
        const prevBlock = previousRaw[prevIdx++];
        const diffedInnerBlocks = diffRawBlocks(
          currBlock.innerBlocks || [],
          prevBlock.innerBlocks || []
        );
        result.push({
          ...currBlock,
          innerBlocks: diffedInnerBlocks
        });
      }
    }
  }
  return pairSimilarBlocks(result);
}
function hasFormatChangedAtIndex(currentFormats, previousFormats, currentIndex, previousIndex) {
  const currFmts = currentFormats[currentIndex] || [];
  const prevFmts = previousFormats[previousIndex] || [];
  if (currFmts.length !== prevFmts.length) {
    return true;
  }
  for (const fmt of currFmts) {
    const match = prevFmts.find(
      (pf) => pf.type === fmt.type && JSON.stringify(pf.attributes) === JSON.stringify(fmt.attributes)
    );
    if (!match) {
      return true;
    }
  }
  return false;
}
function describeFormatChange(currentFormats, previousFormats, currIdx, prevIdx) {
  const currFmts = currentFormats[currIdx] || [];
  const prevFmts = previousFormats[prevIdx] || [];
  let addedCount = 0;
  let removedCount = 0;
  let changedCount = 0;
  for (const fmt of currFmts) {
    const match = prevFmts.find((pf) => pf.type === fmt.type);
    if (!match) {
      addedCount++;
    } else if (JSON.stringify(fmt.attributes) !== JSON.stringify(match.attributes)) {
      changedCount++;
    }
  }
  for (const fmt of prevFmts) {
    const match = currFmts.find((cf) => cf.type === fmt.type);
    if (!match) {
      removedCount++;
    }
  }
  if (addedCount > 0 && removedCount === 0 && changedCount === 0) {
    return {
      type: "added",
      description: (0, import_i18n.sprintf)(
        /* translators: %d: number of formats added */
        (0, import_i18n._n)("%d format added", "%d formats added", addedCount),
        addedCount
      )
    };
  }
  if (removedCount > 0 && addedCount === 0 && changedCount === 0) {
    return {
      type: "removed",
      description: (0, import_i18n.sprintf)(
        /* translators: %d: number of formats removed */
        (0, import_i18n._n)("%d format removed", "%d formats removed", removedCount),
        removedCount
      )
    };
  }
  const parts = [];
  if (addedCount > 0) {
    parts.push(
      (0, import_i18n.sprintf)(
        /* translators: %d: number of formats added */
        (0, import_i18n._n)("%d format added", "%d formats added", addedCount),
        addedCount
      )
    );
  }
  if (removedCount > 0) {
    parts.push(
      (0, import_i18n.sprintf)(
        /* translators: %d: number of formats removed */
        (0, import_i18n._n)("%d format removed", "%d formats removed", removedCount),
        removedCount
      )
    );
  }
  if (changedCount > 0) {
    parts.push(
      (0, import_i18n.sprintf)(
        /* translators: %d: number of formats changed */
        (0, import_i18n._n)("%d format changed", "%d formats changed", changedCount),
        changedCount
      )
    );
  }
  return {
    type: "changed",
    description: parts.join(", ") || (0, import_i18n.__)("Formatting changed")
  };
}
function applyRichTextDiff(currentRichText, previousRichText) {
  const currentText = currentRichText.toPlainText();
  const previousText = previousRichText.toPlainText();
  const textDiff = (0, import_word.diffWords)(previousText, currentText);
  let result = (0, import_rich_text.create)({ text: "" });
  let currentIdx = 0;
  let previousIdx = 0;
  for (const part of textDiff) {
    if (part.removed) {
      const removedSlice = (0, import_rich_text.slice)(
        previousRichText,
        previousIdx,
        previousIdx + part.value.length
      );
      const formatted = (0, import_rich_text.applyFormat)(
        removedSlice,
        {
          type: "revision/diff-removed",
          attributes: { title: (0, import_i18n.__)("Removed") }
        },
        0,
        part.value.length
      );
      result = (0, import_rich_text.concat)(result, formatted);
      previousIdx += part.value.length;
    } else if (part.added) {
      const addedSlice = (0, import_rich_text.slice)(
        currentRichText,
        currentIdx,
        currentIdx + part.value.length
      );
      const formatted = (0, import_rich_text.applyFormat)(
        addedSlice,
        {
          type: "revision/diff-added",
          attributes: { title: (0, import_i18n.__)("Added") }
        },
        0,
        part.value.length
      );
      result = (0, import_rich_text.concat)(result, formatted);
      currentIdx += part.value.length;
    } else {
      const currentFormats = currentRichText.formats || [];
      const previousFormats = previousRichText.formats || [];
      const len = part.value.length;
      const checkFormatChanged = (offset) => hasFormatChangedAtIndex(
        currentFormats,
        previousFormats,
        currentIdx + offset,
        previousIdx + offset
      );
      let rangeStart = 0;
      let rangeFormatChanged = checkFormatChanged(0);
      for (let i = 1; i <= len; i++) {
        const formatChanged = i < len && checkFormatChanged(i);
        if (i === len || formatChanged !== rangeFormatChanged) {
          const rangeSlice = (0, import_rich_text.slice)(
            currentRichText,
            currentIdx + rangeStart,
            currentIdx + i
          );
          if (rangeFormatChanged) {
            const { type, description } = describeFormatChange(
              currentFormats,
              previousFormats,
              currentIdx + rangeStart,
              previousIdx + rangeStart
            );
            const formatType = {
              added: "revision/diff-format-added",
              removed: "revision/diff-format-removed",
              changed: "revision/diff-format-changed"
            }[type];
            const marked = (0, import_rich_text.applyFormat)(
              rangeSlice,
              {
                type: formatType,
                attributes: { title: description }
              },
              0,
              i - rangeStart
            );
            result = (0, import_rich_text.concat)(result, marked);
          } else {
            result = (0, import_rich_text.concat)(result, rangeSlice);
          }
          rangeStart = i;
          rangeFormatChanged = formatChanged;
        }
      }
      currentIdx += part.value.length;
      previousIdx += part.value.length;
    }
  }
  return new import_rich_text.RichTextData(result);
}
function applyDiffToBlock(currentBlock, previousBlock, diffStatus) {
  const blockType = (0, import_blocks.getBlockType)(currentBlock.name);
  if (!blockType) {
    return;
  }
  const changedAttributes = {};
  for (const [attrName, attrDef] of Object.entries(
    blockType.attributes
  )) {
    if (attrDef.source === "rich-text") {
      const currentRichText = currentBlock.attributes[attrName];
      const previousRichText = previousBlock.attributes[attrName];
      if (currentRichText instanceof import_rich_text.RichTextData && previousRichText instanceof import_rich_text.RichTextData) {
        currentBlock.attributes[attrName] = applyRichTextDiff(
          currentRichText,
          previousRichText
        );
      }
    } else {
      const currStr = stringifyValue(
        currentBlock.attributes[attrName]
      );
      const prevStr = stringifyValue(
        previousBlock.attributes[attrName]
      );
      if (currStr !== prevStr) {
        changedAttributes[attrName] = (0, import_word.diffWords)(prevStr, currStr);
      }
    }
  }
  if (Object.keys(changedAttributes).length > 0) {
    diffStatus.changedAttributes = changedAttributes;
  }
}
function applyDiffRecursively(parsedBlock, rawBlock) {
  if (rawBlock.__revisionDiffStatus) {
    if (rawBlock.__revisionDiffStatus.status === "modified" && rawBlock.__previousRawBlock) {
      const previousParsed = parseRawBlock(rawBlock.__previousRawBlock);
      if (previousParsed) {
        applyDiffToBlock(
          parsedBlock,
          previousParsed,
          rawBlock.__revisionDiffStatus
        );
      }
    }
    parsedBlock.__revisionDiffStatus = rawBlock.__revisionDiffStatus;
    parsedBlock.attributes.__revisionDiffStatus = rawBlock.__revisionDiffStatus;
  }
  if (parsedBlock.innerBlocks && rawBlock.innerBlocks) {
    for (let i = 0; i < parsedBlock.innerBlocks.length; i++) {
      const parsedInner = parsedBlock.innerBlocks[i];
      const rawInner = rawBlock.innerBlocks[i];
      if (parsedInner && rawInner) {
        applyDiffRecursively(parsedInner, rawInner);
      }
    }
  }
}
function diffRevisionContent(currentContent, previousContent) {
  const currentRaw = (0, import_block_serialization_default_parser.parse)(currentContent || "");
  const previousRaw = (0, import_block_serialization_default_parser.parse)(previousContent || "");
  const mergedRaw = diffRawBlocks(currentRaw, previousRaw);
  return mergedRaw.map((rawBlock) => {
    const parsed = parseRawBlock(rawBlock);
    if (parsed) {
      applyDiffRecursively(parsed, rawBlock);
    }
    return parsed;
  }).filter(Boolean);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  diffRevisionContent
});
//# sourceMappingURL=block-diff.cjs.map
