当前位置: 首页 > 知识库问答 >
问题:

javascript - 求助一个diff对比问题?

宓诚
2024-11-29

XML字符串对比,按XML绝对路径(如root.xxx.xx.xx)忽略对比特定行,不改变原数据,完整渲染XML,仅在展示diff时,特定路径的行不高亮。

  • 已尝试diff和diff2html库
  • 目前采用暴力删除CSS方案
  • 寻求更优雅的技术实现方法

目前自己的实现

<!-- App.vue -->
<template>
  <div>
    <diff-viewer
      :old-text="parseXmlStructure(oldXml)"
      :new-text="parseXmlStructure(newXml)"
      :ignore-paths="[
        'root.company.gcc',
        'root.company.employees.employee.salary',
      ]"
      file-name="config.xml"
      @diff-rendered="onDiffRendered"
    />
  </div>
</template>

<script>
import DiffViewer from './components/diff2html.vue';
import vkbeautify from 'vkbeautify';
export default {
  components: {
    DiffViewer,
  },

  data() {
    return {
      oldXml: `<root>
<company id="123">
  <gcc/>
  <name>TechCorp International</name>
  <foundedYear>1995</foundedYear>
  <employees>
    <employee id="1">
      <name>John Smith</name>
      <position>Senior Developer</position>
      <department>Engineering</department>
      <salary>85000</salary>
      <contact>
        <email>john.smith@techcorp.com</email>
        <phone>212-555-0100</phone>
        <address>
          <street>123 Tech Avenue</street>
          <city>San Francisco</city>
          <state>CA</state>
          <country>USA</country>
          <zipCode>94105</zipCode>
        </address>
      </contact>
      <projects>
        <project>Cloud Migration</project>
        <project>Mobile App</project>
      </projects>
    </employee>
    <employee id="2">
      <name>Sarah Johnson</name>
      <position>Product Manager</position>
      <department>Product</department>
      <salary>95000</salary>
      <contact>
        <email>sarah.j@techcorp.com</email>
        <phone>212-555-0101</phone>
        <address>
          <street>456 Innovation Drive</street>
          <city>San Francisco</city>
          <state>CA</state>
          <country>USA</country>
          <zipCode>94105</zipCode>
        </address>
      </contact>
      <projects>
        <project>User Analytics</project>
        <project>Platform Redesign</project>
      </projects>
    </employee>
  </employees>
  <offices>
    <office>
      <location>San Francisco</location>
      <employeeCount>150</employeeCount>
    </office>
    <office>
      <location>New York</location>
      <employeeCount>75</employeeCount>
    </office>
  </offices>
</company>
</root>`,
      newXml: `<root>
<company id="123">
  <name>TechCorp International</name>
  <foundedYear>1995</foundedYear>
  <employees>
    <employee id="1">
      <name>John Smith</name>
      <position>Engineering Manager</position>
      <department>Engineering</department>
      <salary>95000</salary>
      <contact>
        <email>john.smith@techcorp.com</email>
        <phone>212-555-0100</phone>
        <address>
          <street>123 Tech Avenue</street>
          <city>San Jose</city>
          <state>CA</state>
          <country>USA</country>
          <zipCode>95110</zipCode>
        </address>
      </contact>
      <projects>
        <project>Cloud Migration</project>
        <project>Mobile App</project>
        <project>AI Integration</project>
      </projects>
    </employee>
    <employee id="2">
      <name>Sarah Johnson</name>
      <position>Director of Product</position>
      <department>Product</department>
      <salary>120000</salary>
      <contact>
        <email>sarah.johnson@techcorp.com</email>
        <phone>212-555-0202</phone>
        <address>
          <street>456 Innovation Drive</street>
          <city>San Jose</city>
          <state>CA</state>
          <country>USA</country>
          <zipCode>95110</zipCode>
        </address>
      </contact>
      <projects>
        <project>User Analytics 2.0</project>
        <project>Platform Redesign</project>
        <project>Customer Portal</project>
      </projects>
    </employee>
  </employees>
  <offices>
    <office>
      <location>San Jose</location>
      <employeeCount>200</employeeCount>
    </office>
    <office>
      <location>New York</location>
      <employeeCount>80</employeeCount>
    </office>
  </offices>
</company>
</root>`,
    };
  },

  methods: {
    onDiffRendered() {
      console.log('Diff rendering completed');
    },
    parseXmlStructure(content) {
      try {
        // 移除多余的空白行
        content = content.replace(/^\s*[\r\n]/gm, '');
        // 格式化XML
        return vkbeautify.xml(content, 2);
      } catch (error) {
        console.error('XML formatting error:', error);
        return content;
      }
    },
  },
};
</script>
<!-- DiffViewer.vue -->
<template>
  <div class="diff-viewer">
    <div v-if="loading" class="diff-loading">
      <div class="loading-spinner"></div>
    </div>

    <div v-else-if="error" class="diff-error">
      {{ error }}
      <button @click="retry" class="retry-btn">重试</button>
    </div>

    <div
      v-else
      ref="diffContainer"
      class="diff-container"
      v-html="diffHtml"
    ></div>

    <div class="diff-controls">
      <label>
        <input type="checkbox" v-model="sideBySide" @change="redraw" />
        并排显示
      </label>
      <select v-model="matching" @change="redraw">
        <option value="lines">按行匹配</option>
        <option value="words">按词匹配</option>
        <option value="none">禁用匹配</option>
      </select>
    </div>
  </div>
</template>

<script>
import 'diff2html/bundles/css/diff2html.min.css';
import { createPatch } from 'diff';
import { parse, html } from 'diff2html';

export default {
  name: 'DiffViewer',

  props: {
    oldText: {
      type: String,
      required: true,
    },
    newText: {
      type: String,
      required: true,
    },
    fileName: {
      type: String,
      default: 'file.txt',
    },
    ignorePaths: {
      type: Array,
      default: () => [],
    },
  },

  data() {
    return {
      loading: false,
      error: null,
      sideBySide: true,
      matching: 'lines',
      diffHtml: '',
    };
  },

  watch: {
    oldText: {
      handler: 'updateDiff',
      immediate: true,
    },
    newText: {
      handler: 'updateDiff',
      immediate: true,
    },
  },

  methods: {
    async updateDiff() {
      try {
        this.loading = true;
        this.error = null;

        // 生成diff
        const diffStr = createPatch(
          this.fileName,
          this.oldText,
          this.newText,
          '',
          '',
          { context: 3 }
        );

        // 解析diff
        const diffJson = parse(diffStr);

        // 配置选项
        const config = {
          drawFileList: false,
          matching: this.matching,
          outputFormat: this.sideBySide ? 'side-by-side' : 'line-by-line',
          renderNothingWhenEmpty: true,
        };

        // 生成 HTML
        this.diffHtml = html(diffJson, config);

        // 等待 DOM 更新后处理 XML diff
        this.$nextTick(() => {
          this.processXmlDiff();
          this.$emit('diff-rendered');
        });
      } catch (err) {
        console.error('Diff generation failed:', err);
        this.error = '差异对比生成失败,请重试';
      } finally {
        this.loading = false;
      }
    },

    retry() {
      this.updateDiff();
    },

    redraw() {
      this.updateDiff();
    },

    parseXmlLine(line) {
      const result = {
        openTags: [], // 此行打开的标签
        closeTags: [], // 此行关闭的标签
        selfClosing: [], // 自闭合标签
        content: null, // 内容
        isContentLine: false, // 是否是内容行
      };

      if (!line || !line.includes('<')) {
        result.content = line ? line.trim() : '';
        result.isContentLine = true;
        return result;
      }

      const tagPattern = /<\/?([^\s>]+)[^>]*\/?>/g;
      let match;
      let lastIndex = 0;

      while ((match = tagPattern.exec(line)) !== null) {
        const fullTag = match[0];
        const tagName = match[1];

        if (match.index > lastIndex) {
          const content = line.substring(lastIndex, match.index).trim();
          if (content) {
            result.content = content;
            result.isContentLine = true;
          }
        }

        if (fullTag.endsWith('/>')) {
          result.selfClosing.push(tagName.replace(/\/$/, ''));
        } else if (fullTag.startsWith('</')) {
          result.closeTags.push(tagName);
        } else {
          result.openTags.push(tagName);
        }

        lastIndex = match.index + fullTag.length;
      }

      if (lastIndex < line.length) {
        const content = line.substring(lastIndex).trim();
        if (content) {
          result.content = content;
          result.isContentLine = true;
        }
      }

      return result;
    },

    shouldIgnorePath(path) {
      if (!path) return false;
      return this.ignorePaths.some((ignorePath) => {
        const pattern = ignorePath.replace(/\*/g, '[^.]+');
        const regex = new RegExp(`^${pattern}$`);
        return regex.test(path);
      });
    },

    removeHighlightStyle(row) {
      const lineNumCell = row.querySelector('.d2h-code-side-linenumber');
      const codeCell = row.querySelector('.d2h-del, .d2h-ins');
      if (!lineNumCell || !codeCell) return;

      lineNumCell.classList.remove('d2h-del', 'd2h-ins', 'd2h-change');
      lineNumCell.classList.add('d2h-cntx');
      codeCell.classList.remove('d2h-del', 'd2h-ins', 'd2h-change');
      codeCell.classList.add('d2h-cntx');

      const prefix = codeCell.querySelector('.d2h-code-line-prefix');
      if (prefix) {
        prefix.textContent = ' ';
      }

      const container = codeCell.querySelector('.d2h-code-line-ctn');
      if (container) {
        const delTags = container.querySelectorAll('del');
        const insTags = container.querySelectorAll('ins');

        delTags.forEach((del) => {
          const text = del.textContent;
          del.replaceWith(text);
        });

        insTags.forEach((ins) => {
          const text = ins.textContent;
          ins.replaceWith(text);
        });
      }
    },

    processXmlDiff() {
      const diffContainer = this.$refs.diffContainer;
      if (!diffContainer) return;

      const tables = diffContainer.querySelectorAll('.d2h-diff-table');
      tables.forEach((table) => {
        const rows = table.querySelectorAll('tr');
        const currentPath = [];

        rows.forEach((row) => {
          const contentCell = row.querySelector('.d2h-code-line-ctn');
          if (!contentCell) return;

          const line = contentCell.textContent;
          const xmlInfo = this.parseXmlLine(line);

          // 更新当前路径
          xmlInfo.openTags.forEach((tag) => currentPath.push(tag));

          // 处理自闭合标签
          xmlInfo.selfClosing.forEach((tag) => {
            currentPath.push(tag);
            if (this.shouldIgnorePath(currentPath.join('.'))) {
              this.removeHighlightStyle(row);
            }
            currentPath.pop();
          });

          // 处理内容行
          if (
            xmlInfo.isContentLine &&
            this.shouldIgnorePath(currentPath.join('.'))
          ) {
            this.removeHighlightStyle(row);
          }

          // 处理关闭标签
          xmlInfo.closeTags.forEach(() => currentPath.pop());
        });
      });
    },
  },
};
</script>
"diff": "^7.0.0",
"diff2html": "^3.4.48",
"vkbeautify": "^0.99.3"

共有1个答案

蒋鹏鹍
2024-11-29
### 回答

要实现 XML 字符串对比,并且按 XML 绝对路径忽略对比特定行,同时不改变原数据且完整渲染 XML,你可以在对比过程中自定义 diff 渲染逻辑。以下是一个可能的解决方案,使用 JavaScript 和一些 XML 处理库:

1. **解析 XML 字符串**:使用 XML 解析器(如 DOMParser)将 XML 字符串解析为 DOM 树。

2. **遍历 XML 树**:根据绝对路径忽略特定节点或属性。

3. **生成 diff**:使用 diff 库(如 `jsdiff`)对未忽略的部分进行 diff 对比。

4. **自定义渲染**:使用 diff2html 或其他 diff 渲染库,但修改其渲染逻辑,以忽略特定路径的高亮。

#### 具体步骤

1. **解析 XML**:
const parser = new DOMParser();
const xmlDoc1 = parser.parseFromString(xmlString1, "application/xml");
const xmlDoc2 = parser.parseFromString(xmlString2, "application/xml");
```
  1. 遍历 XML 并标记忽略路径

    function markIgnoredPaths(xmlDoc, paths) {
        const walk = (node, path = []) => {
            path.push(node.nodeName);
            if (paths.includes(path.join('.'))) {
                node._ignore = true;  // 添加自定义属性标记忽略
            }
            node.childNodes.forEach(child => walk(child, path));
            path.pop();
        };
        xmlDoc.documentElement.childNodes.forEach(child => walk(child));
    }
    
    const pathsToIgnore = ['root.xxx.xx.xx'];  // 绝对路径数组
    markIgnoredPaths(xmlDoc1, pathsToIgnore);
    markIgnoredPaths(xmlDoc2, pathsToIgnore);
  2. 生成 diff

    const jsdiff = require('jsdiff');
    function diffXmlStrings(xmlDoc1, xmlDoc2) {
        const serializer = new XMLSerializer();
        const xmlString1 = serializer.serializeToString(xmlDoc1);
        const xmlString2 = serializer.serializeToString(xmlDoc2);
        return jsdiff.diffLines(xmlString1, xmlString2);
    }
    
    const diffs = diffXmlStrings(xmlDoc1, xmlDoc2);
  3. 自定义渲染

    function renderDiff(diffs) {
        const diffHtml = Diff2Html.getPrettyHtml(diffs, {
            drawFileList: false,
            matching: 'words',
            renderNothingWhenEmpty: true,
            showNonMatching: true,
            customInlineDiffRenderer: (part, type) => {
                if (part.originalElement && part.originalElement._ignore) {
                    return `<span>${part.text}</span>`;  // 不高亮忽略部分
                }
                return Diff2Html.getInlineDiffLineHtml(part, type);
            }
        });
        document.getElementById('diff-output').innerHTML = diffHtml;
    }
    
    renderDiff(diffs);

这种方法通过解析 XML,标记需要忽略的路径,生成 diff,并在渲染时自定义忽略部分的高亮,从而实现了你的需求。

 类似资料:
  • 请问如何使用git diff 对比两个文件的差异呢? 我从1.txt 复制内容到2.txt 并修改了内容信息, 我想要使用git diff 它们,输出差异的位置信息。 但是我执行如下的命令: 没有得到任何响应内容。

  • 模板中{{index}}显示的是12、16,按照手册中的说法index是数组对应的下标。但是面对这样的data数据,我想显示1、2、3.....这样的序号。该怎么处理?

  • 打开网址1:域名/article/359.html ,正常的内容页。 打开网址2:域名//////article/359.html ,还是正常的内容页。 正常来说 应该是 404页面或者自动跳到只有一个 /开头的该页面。 APACHE规则如下: NGINX规则如下: 伪静态处理时: 请问如果修改规则? 打开网址2,跳转到404错误或者301跳转到网址1

  • 求一个数学问题�� 已知或者能够计算出来的相关条件: 1.ABCD是梯形,F是梯形内一点(不是中心点) 2.OC、OE、OF、CF、FE、OM、MC的长度均已知 3.OC、OE的夹角α为60°,OF是α的角度平分线 求 AB、CD的长度

  • 你可以用 git diff 来比较项目中任意两个版本的差异。 $ git diff master..test 上面这条命令只显示两个分支间的差异,如果你想找出‘master’,‘test’的共有 父分支和'test'分支之间的差异,你用3个‘.'来取代前面的两个'.' 。 $ git diff master...test git diff 是一个难以置信的有用的工具,可以找出你项目上任意两点间

  • 怎么求b相对于a点的弧度,js中通过鼠标点来求.

  • 请问上面这段代码,我想封装成Promise 这种 直接调用this.home_barlist1().then 该怎么改呢? 我改成下面这样 好像不行

  • 我想匹配一段字符串中所有的input 并使用replace进行替换,如果input里面有类似data-* 这种自定义属性的就跳过 不知道这种正则该怎么写,我也阅读了文档并使用google。都没找到 比如 <input type='text' /> 这种Input就匹配,<input data-xxx /> 带有自定义属性的input 正则则不匹配