Word XML 批注范围克隆处理器

该类用于处理 Word 文档(XML 结构)中被批注标记的文本范围,

实现指定内容的深度克隆,并将其插入到目标节点之后。

适用于在生成或修改 .docx 文件时复制批注内容块。

复制代码
/**
 * Word XML 批注范围克隆处理器
 * 
 * 该类用于处理 Word 文档(XML 结构)中被批注标记的文本范围,
 * 实现指定内容的深度克隆,并将其插入到目标节点之后。
 * 适用于在生成或修改 .docx 文件时复制批注内容块。
 */
class WordCommentRangeCloner
{
    /**
     * 存储文档节点的线性列表,用于通过索引快速访问
     * @var array
     */
    private $domList = [];

    /**
     * 记录每个节点索引关联的命名信息,用于模板或块引用
     * 格式: [index] => [['type', 'name'], ...]
     * @var array
     */
    private $domIdxToName = [];

    /**
     * 存储按名称组织的块引用,支持多实例
     * 格式: ['blockName#1'] => ['type' => [index1, index2, ...]]
     * @var array
     */
    private $blocks = [];

    /**
     * 扩展索引映射,用于记录节点间的附加关联关系
     * 格式: [index] => [relatedIndex1, relatedIndex2, ...]
     * @var array
     */
    private $idxExtendIdxs = [];

    /**
     * 将克隆的节点插入到目标节点之后
     *
     * 如果目标节点有下一个兄弟节点,则插入到其前;
     * 否则作为父节点的最后一个子节点追加。
     *
     * @param \DOMNode $copy 要插入的已克隆节点
     * @param \DOMNode $targetNode 目标位置的参考节点
     * @return void
     */
    public function insertAfter($copy, $targetNode)
    {
        if ($nextSibling = $targetNode->nextSibling) {
            // 如果存在下一个兄弟节点,则插入到它前面
            if ($parentNode = $nextSibling->parentNode) {
                $parentNode->insertBefore($copy, $nextSibling);
            }
        } else {
            // 否则作为父节点的最后一个子节点添加
            if ($parentNode = $targetNode->parentNode) {
                $parentNode->appendChild($copy);
            }
        }
    }

    /**
     * 克隆指定索引的节点及其子树,并插入到指定位置后
     *
     * 此方法用于复制 Word 文档中被批注包裹的内容块。
     * 支持维护索引、命名、关联等元数据一致性。
     *
     * @param int $nodeIdx 要克隆的原始节点在 domList 中的索引
     * @param int $endNodeIdx 插入位置的参考节点索引(插入到该节点之后)
     * @param string $name 原始块名称(用于命名新实例)
     * @param int $idx 克隆实例编号(用于区分多个副本,如 #1, #2)
     * @param int $reIdx 递归层级编号(嵌套克隆时使用)
     * @return int 返回克隆子树在 domList 中的起始索引
     */
    private function cloneNode($nodeIdx, $endNodeIdx, $name, $idx, $reIdx = 0)
    {
        $originalNode = $this->domList[$nodeIdx];
        $clonedNode = clone $originalNode;

        // 为克隆节点重建树索引(使用引用传递维护连续索引)
        $startIndex = count($this->domList);
        $this->treeToList($clonedNode, $startIndex);

        // 构建原始索引到新索引的映射表
        $oldToNewMap = $this->buildIndexMap($originalNode, $clonedNode);

        // 更新命名映射关系(domIdxToName 和 blocks)
        $this->updateNameMappings($originalNode, $oldToNewMap, $idx);

        // 更新扩展索引关联关系
        $this->updateExtendIdxs($originalNode, $oldToNewMap);

        // 将克隆的节点插入到目标节点之后
        $this->insertAfter($clonedNode, $this->domList[$endNodeIdx]);

        return $startIndex;
    }

    /**
     * 遍历节点树并生成线性节点列表,同时设置每个节点的起止索引
     *
     * 用于重建克隆节点或新插入节点的索引结构。
     *
     * @param \DOMNode $node 当前处理的节点
     * @param int|null $index 引用传递的当前索引值
     * @return void
     */
    protected function treeToList($node, &$index = null)
    {
        if ($index === null) {
            $index = count($this->domList);
        }

        if (is_null($node)) {
            return;
        }

        $node->idxBegin = $index;
        $node->tagList = [];
        $this->domList[$index++] = $node;

        if ($node->hasChildNodes()) {
            foreach ($node->childNodes as $childNode) {
                if ($childNode->nodeType !== XML_TEXT_NODE) { // 非文本节点
                    $tagName = $childNode->tagName;
                    $node->tagList[$tagName][] = $childNode;
                    $this->treeToList($childNode, $index);
                    // 合并后代标签列表
                    foreach ($childNode->tagList as $tag => $nodes) {
                        foreach ($nodes as $n) {
                            $node->tagList[$tag][] = $n;
                        }
                    }
                }
            }
        }

        $node->idxEnd = $index - 1;
    }

    /**
     * 构建原始节点索引到克隆节点索引的映射表
     *
     * @param \DOMNode $orig 原始节点
     * @param \DOMNode $clone 克隆节点
     * @return array 映射表 [oldIndex => newIndex]
     */
    private function buildIndexMap($orig, $clone)
    {
        $map = [];
        $this->walkNodeAndMap($orig, $clone, $map);
        return $map;
    }

    /**
     * 递归遍历原始与克隆节点,建立索引映射
     *
     * @param \DOMNode $orig
     * @param \DOMNode $clone
     * @param array &$map 引用传递的映射表
     * @return void
     */
    private function walkNodeAndMap($orig, $clone, &$map)
    {
        $map[$orig->idxBegin] = $clone->idxBegin;

        if ($orig->hasChildNodes()) {
            $origChildren = iterator_to_array($orig->childNodes);
            $cloneChildren = iterator_to_array($clone->childNodes);
            $j = 0;
            foreach ($origChildren as $child) {
                if ($child->nodeType !== XML_TEXT_NODE) {
                    $map = $j < count($cloneChildren) ? $cloneChildren[$j] : null;
                    if ($map) {
                        $this->walkNodeAndMap($child, $map, $map);
                        $j++;
                    }
                }
            }
        }
    }

    /**
     * 更新命名映射表(domIdxToName 和 blocks)
     *
     * 为克隆节点分配新名称(如 name#1),并注册到 blocks 中
     *
     * @param \DOMNode $originalNode 原始节点
     * @param array $oldToNewMap 索引映射表
     * @param int $cloneIdx 克隆编号
     * @return void
     */
    private function updateNameMappings($originalNode, $oldToNewMap, $cloneIdx)
    {
        $begin = $originalNode->idxBegin;
        $end = $originalNode->idxEnd;

        for ($i = $begin; $i <= $end; $i++) {
            if (isset($this->domIdxToName[$i])) {
                $newIdx = $oldToNewMap[$i];
                $nameTemps = $this->domIdxToName[$i];

                foreach ($nameTemps as $key => $nameTemp) {
                    $newName = $nameTemp[1] . '#' . $cloneIdx;
                    $nameTemps[$key] = [$nameTemp[0], $newName];
                    $this->blocks[$newName][$nameTemp[0]][] = $newIdx;
                }
                $this->domIdxToName[$newIdx] = $nameTemps;
            }
        }
    }

    /**
     * 更新扩展索引关联关系
     *
     * 将原始节点的 idxExtendIdxs 映射到克隆节点
     *
     * @param \DOMNode $originalNode 原始节点
     * @param array $oldToNewMap 索引映射表
     * @return void
     */
    private function updateExtendIdxs($originalNode, $oldToNewMap)
    {
        $begin = $originalNode->idxBegin;
        $end = $originalNode->idxEnd;

        for ($i = $begin; $i <= $end; $i++) {
            if (isset($this->idxExtendIdxs[$i])) {
                $newIdx = $oldToNewMap[$i];
                $this->idxExtendIdxs[$newIdx] = [];
                foreach ($this->idxExtendIdxs[$i] as $oldRefId) {
                    if (isset($oldToNewMap[$oldRefId])) {
                        $this->idxExtendIdxs[$newIdx][] = $oldToNewMap[$oldRefId];
                    }
                }
            }
        }
    }
}
相关推荐
Morpheon21 分钟前
Intro to R Programming - Lesson 4 (Graphs)
开发语言·r语言
代码AI弗森22 分钟前
使用 JavaScript 构建 RAG(检索增强生成)库:原理与实现
开发语言·javascript·ecmascript
Tipriest_1 小时前
C++ 中 ::(作用域解析运算符)的用途
开发语言·c++·作用域解析
Swift社区2 小时前
Java 常见异常系列:ClassNotFoundException 类找不到
java·开发语言
Tipriest_2 小时前
求一个整数x的平方根到指定精度[C++][Python]
开发语言·c++·python
蓝倾9763 小时前
淘宝/天猫店铺商品搜索API(taobao.item_search_shop)返回值详解
android·大数据·开发语言·python·开放api接口·淘宝开放平台
John_ToDebug4 小时前
从源码看浏览器弹窗消息机制:SetDefaultView 的创建、消息转发与本地/在线页通用实践
开发语言·c++·chrome
菌王5 小时前
EXCEL 2 word 的一些案例。excel通过一些策略将内容写入word中。
开发语言·c#
励志不掉头发的内向程序员5 小时前
STL库——list(类模拟实现)
开发语言·c++·学习
Swift社区6 小时前
Swift 解法详解:LeetCode 367《有效的完全平方数》
开发语言·leetcode·swift