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];
                    }
                }
            }
        }
    }
}
相关推荐
Volunteer Technology1 分钟前
Lua基础+Lua数据类型
开发语言·junit·lua
在打豆豆的小潘学长1 小时前
【R语言】多样本单细胞分析_SCTransform+Harmony方案(2)
开发语言·r语言
C4程序员3 小时前
北京JAVA基础面试30天打卡06
java·开发语言·面试
teeeeeeemo3 小时前
一些js数组去重的实现算法
开发语言·前端·javascript·笔记·算法
啊森要自信4 小时前
【QT】常⽤控件详解(七)容器类控件 GroupBox && TabWidget && 布局管理器 && Spacer
linux·开发语言·c++·qt·adb
SEO-狼术4 小时前
SmartClient 14.1 improves Crack
开发语言
码界筑梦坊4 小时前
97-基于Python的大众点评数据分析预测系统
开发语言·python·数据分析
teeeeeeemo4 小时前
JS实现数组扁平化
开发语言·前端·javascript·笔记·算法
源代码•宸4 小时前
C++高频知识点(二十)
开发语言·c++·经验分享·epoll·拆包封包·名称修饰