鸿蒙平台 Regexxer 正则表达式工具适配实战:从桌面到 鸿蒙PC 的 Electron 迁移指南

项目简介

Regexxer 是经典的 Linux 正则表达式搜索替换工具,支持正则匹配、捕获组显示、单个/全部替换、替换对比高亮等功能。本项目将其从桌面应用迁移到鸿蒙平台,采用 Electron 核心功能 + 鸿蒙壳工程 的架构模式,基于纯前端实现(零外部依赖)。

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

AtomGit 仓库地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_regexxer_electron

核心功能

  • 🔍 正则查找(支持全局搜索 g、忽略大小写 i、多行模式 m)
  • 🎯 捕获组显示(自动提取 1, 2, $3 等捕获组内容)
  • 🔄 单个替换(替换第一个匹配项,支持捕获组引用)
  • 全部替换(全局替换所有匹配项,实时统计替换数量)
  • 📊 替换对比(清晰展示每个替换项的原值红色 → 新值绿色)
  • 🌟 高亮显示(替换后的完整文本中,被替换内容绿色高亮标记)
  • 📝 示例文本(内置 8 个完整示例,包含操作步骤和预期结果)
  • 🔄 自动恢复(重启应用后自动恢复示例文本,开箱即用)
  • ⌨️ 快捷键(Ctrl+Enter 查找、Ctrl+H 替换、Ctrl+Shift+H 全部替换、Ctrl+L 清空)
  • 📍 位置显示(每个匹配项显示在源文本中的位置索引)
  • 🛡️ 防无限循环(零宽度匹配自动保护,避免 exec() 死循环)
  • 🔒 XSS** 防护**(HTML 转义处理,防止恶意代码注入)
  • 性能监控(实时显示正则表达式执行时间,毫秒级)
  • 🎨 暗色主题(VS Code 风格暗色界面,专业开发者体验)

一、技术架构

1.1 原始架构(Linux Desktop)

bash 复制代码
Regexxer (GTK+ Desktop)
├── UI 渲染:GTK+ Widget
├── 正则引擎:PCRE (Perl Compatible Regular Expressions)
├── 文本处理:Glib 字符串操作
└── 文件系统:GIO

1.2 目标架构(鸿蒙 Electron)

bash 复制代码
鸿蒙壳工程 (ArkTS)
└── web_engine 模块 (XComponent WebView)
    └── Electron 应用 (HTML/CSS/JavaScript)
        ├── main.js - Electron 主进程
        ├── renderer.js - 渲染进程(核心逻辑)
        ├── index.html - UI 界面
        ├── package.json - 项目配置
        └── regexxer.css - 样式文件

1.3 架构优势

  • 纯前端实现:零外部依赖,仅使用原生 JavaScript RegExp API
  • 快速开发:Web 技术栈,开发效率高
  • 易于维护:UI 和业务逻辑分离
  • 鸿蒙兼容:通过 WebView 桥接,避开 Native 兼容问题
  • 自研替换引擎:支持捕获组引用、$& 完整匹配、防无限循环保护

二、环境准备

2.1 开发环境要求

  • 操作系统:Windows 10
  • 开发工具:DevEco Studio(鸿蒙官方 IDE)
  • HarmonyOS SDK:API 23(6.1.1)
  • Node.js:v20+(Electron 依赖)

2.2 项目结构

bash 复制代码
ohos_hap/
└── web_engine/                   # 鸿蒙 web_engine 模块
    └── src/main/resources/
        └── resfile/resources/app/  # 部署目录
            ├── main.js           # Electron 主进程
            ├── renderer.js       # 渲染进程(核心逻辑)
            ├── index.html        # UI 界面
            ├── package.json      # 项目配置
            └── regexxer.css      # 样式文件
└── build-profile.json5           # 鸿蒙构建配置

三、核心适配流程

3.1 第一步:创建 Electron 主进程(main.js)

文件:web_engine/src/main/resources/resfile/resources/app/main.js

javascript 复制代码
// Regexxer - Electron 主进程
// 正则表达式搜索替换工具(鸿蒙适配)

const { app, BrowserWindow, ipcMain, screen } = require('electron');
const path = require('path');

let mainWindow = null;

function createWindow() {
  console.log('Regexxer: 创建窗口...');

  const primaryDisplay = screen.getPrimaryDisplay();
  const { width: screenWidth, height: screenHeight } = primaryDisplay.workAreaSize;

  const windowWidth = Math.min(1200, Math.floor(screenWidth * 0.8));
  const windowHeight = Math.min(800, Math.floor(screenHeight * 0.85));

  mainWindow = new BrowserWindow({
    width: windowWidth,
    height: windowHeight,
    x: Math.floor((screenWidth - windowWidth) / 2),
    y: Math.floor((screenHeight - windowHeight) / 2),
    frame: true,
    transparent: false,
    alwaysOnTop: false,
    hasShadow: true,
    resizable: true,
    focusable: true,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
      backgroundThrottling: false
    }
  });

  console.log('Regexxer: 正在加载 index.html:', path.join(__dirname, 'index.html'));
  mainWindow.loadFile(path.join(__dirname, 'index.html'));

  console.log('Regexxer: 窗口创建成功,尺寸:', windowWidth, 'x', windowHeight);

  mainWindow.on('closed', () => {
    console.log('Regexxer: 窗口已关闭');
    mainWindow = null;
  });

  mainWindow.webContents.on('did-finish-load', () => {
    console.log('Regexxer: 页面加载成功');
  });

  mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
    console.error('Regexxer: 页面加载失败:', errorCode, errorDescription);
  });
}

app.whenReady().then(() => {
  createWindow();
  console.log('Regexxer 正则表达式工具已启动');
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

关键要点

  • 窗口尺寸动态计算(屏幕 80% 宽度 × 85% 高度,最大 1200×800)
  • webPreferences 配置:nodeIntegration + contextIsolation: false
  • 全中文日志输出,便于调试
  • 无需 IPC 接口(纯前端实现,零文件操作)

3.2 第二步:设计专业正则表达式工具 UI(index.html)

文件:web_engine/src/main/resources/resfile/resources/app/index.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Regexxer - 正则表达式搜索替换工具</title>
  <link rel="stylesheet" href="styles/regexxer.css">
</head>
<body>
  <div id="app" class="app-container">
    <!-- 标题栏 -->
    <div class="title-bar">
      <span class="logo">⚡</span>
      <span class="app-title">Regexxer - 正则表达式搜索替换工具</span>
    </div>

    <!-- 工具栏 -->
    <div class="toolbar">
      <div class="toolbar-row">
        <label class="toolbar-label">正则表达式:</label>
        <input type="text" id="regex-input" class="regex-input" placeholder="输入正则表达式,例如:\d+" data-tip="输入 Perl 风格正则表达式">
        <div class="flags-group">
          <label class="flag-checkbox">
            <input type="checkbox" id="flag-global" checked>
            <span>全局 (g)</span>
          </label>
          <label class="flag-checkbox">
            <input type="checkbox" id="flag-ignorecase">
            <span>忽略大小写 (i)</span>
          </label>
          <label class="flag-checkbox">
            <input type="checkbox" id="flag-multiline">
            <span>多行 (m)</span>
          </label>
        </div>
      </div>

      <div class="toolbar-row">
        <label class="toolbar-label">替换为:</label>
        <input type="text" id="replace-input" class="replace-input" placeholder="替换文本,使用 $1 引用捕获组" data-tip="使用 $1, $2 引用捕获组">
        <div class="btn-group">
          <button id="btn-find" class="btn btn-primary" data-tip="查找所有匹配项">查找</button>
          <button id="btn-replace" class="btn btn-warning" data-tip="替换第一个匹配项">替换</button>
          <button id="btn-replace-all" class="btn btn-danger" data-tip="替换所有匹配项">全部替换</button>
          <button id="btn-clear" class="btn" data-tip="清空所有输入">清空</button>
        </div>
      </div>
    </div>

    <!-- 主内容区 -->
    <main class="main-content">
      <!-- 左侧:源文本 -->
      <div class="panel source-panel">
        <div class="panel-header">
          <span class="panel-title">源文本</span>
          <span id="source-count" class="panel-count">0 字符</span>
        </div>
        <textarea id="source-text" class="source-text" placeholder="在此输入或粘贴源文本..." data-tip="输入或粘贴需要处理的文本"></textarea>
      </div>

      <!-- 右侧:匹配结果 -->
      <div class="panel result-panel">
        <div class="panel-header">
          <span class="panel-title">匹配结果</span>
          <span id="match-count" class="panel-count">0 个匹配</span>
        </div>
        <div id="result-content" class="result-content">
          <div class="result-placeholder">
            <div class="placeholder-icon">🔍</div>
            <div class="placeholder-text">输入正则表达式并点击"查找"开始搜索</div>
          </div>
        </div>
      </div>
    </main>

    <!-- 底部状态栏 -->
    <footer class="status-bar">
      <span id="status-text">就绪 - 输入正则表达式开始搜索</span>
      <span id="execution-time">执行时间:0ms</span>
    </footer>
  </div>

  <script src="renderer.js"></script>
</body>
</html>

关键要点

  • 双栏式布局(源文本区 + 匹配结果区)
  • 工具栏包含:正则输入框 + 匹配标志(g/i/m)+ 替换输入框 + 操作按钮(查找/替换/全部替换/清空)
  • data-tip 自定义属性替代 title,避免触发 ArkWeb 原生 tooltip 导致 XComponent 崩溃
  • 状态栏显示执行时间和操作提示
  • 源文本区实时显示字符计数,匹配结果区显示匹配数量

3.3 第三步:配置项目元信息(package.json)

文件:web_engine/src/main/resources/resfile/resources/app/package.json

json 复制代码
{
  "name": "regexxer",
  "version": "1.0.0",
  "description": "Regexxer - 正则表达式搜索替换工具(鸿蒙适配)",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "dev": "electron . --dev"
  },
  "keywords": ["正则表达式", "regex", "search", "replace"],
  "author": "Regexxer(鸿蒙移植版)",
  "license": "MIT",
  "devDependencies": {
    "electron": "^28.0.0"
  }
}

关键要点

  • main** 入口**:指定 main.js 为 Electron 主进程入口文件
  • scripts 脚本:start 启动生产模式,dev 启动开发模式(带调试参数)
  • license 协议:MIT
  • electron 版本:^28.0.0(兼容鸿蒙 ArkWeb 的 Electron 版本)
  • keywords:包含正则表达式、regex、search、replace 等中英文搜索关键词
  • 零外部依赖:纯前端实现,无需任何正则表达式库

3.4 第四步:实现渲染进程核心逻辑(renderer.js)

文件:web_engine/src/main/resources/resfile/resources/app/renderer.js

javascript 复制代码
// Regexxer - 渲染进程
// 正则表达式搜索替换工具(纯前端实现)

// ===== 全局状态 =====
var matchCount = 0;
var lastSearchTime = 0;

// ===== 示例文本常量 =====
var EXAMPLE_TEXT = `========================================
Regexxer 正则表达式工具 - 使用示例
========================================

【示例 1:查找所有邮箱地址】
📋 源文本内容:
联系方式:
- 技术支持:support@example.com
- 销售热线:sales@company.org
- 客服邮箱:service@test.cn

🔧 操作步骤:
正则表达式:\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b
替换为:(留空)
勾选:☑ 全局(g)  ☐ 忽略大小写(i)  ☐ 多行(m)
点击按钮:【查找】
预期结果:找到 3 个匹配项

---

【示例 2:提取电话号码】
📋 源文本内容:
联系电话:
- 北京:010-1234-5678
- 上海:021-8765-4321
- 手机:138-0013-8000

🔧 操作步骤:
正则表达式:\\d{3}-\\d{4}-\\d{4}
替换为:(留空)
勾选:☑ 全局(g)  ☐ 忽略大小写(i)  ☐ 多行(m)
点击按钮:【查找】
预期结果:找到 3 个匹配项

---

【示例 3:替换日期格式(YYYY-MM-DD → MM/DD/YYYY)】
📋 源文本内容:
项目日期:
- 开始日期:2024-01-15
- 截止日期:2024-12-31
- 更新日期:2024-06-13

🔧 操作步骤:
正则表达式:(\\d{4})-(\\d{2})-(\\d{2})
替换为:$2/$3/$1
勾选:☑ 全局(g)  ☐ 忽略大小写(i)  ☐ 多行(m)
点击按钮:【全部替换】
预期结果:2024-01-15 → 01/15/2024(全部替换)

---

【示例 4:提取 URL 链接】
📋 源文本内容:
相关链接:
- 官网:https://www.example.com
- 文档:http://docs.test.org/api
- 下载:https://download.site.cn/v1.0

🔧 操作步骤:
正则表达式:https?://[^\\s]+
替换为:(留空)
勾选:☑ 全局(g)  ☐ 忽略大小写(i)  ☐ 多行(m)
点击按钮:【查找】
预期结果:找到 3 个匹配项

---

【示例 5:匹配 IP 地址】
📋 源文本内容:
服务器列表:
- 主服务器:192.168.1.100
- 备份服务器:10.0.0.254
- 测试环境:172.16.0.1

🔧 操作步骤:
正则表达式:\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b
替换为:(留空)
勾选:☑ 全局(g)  ☐ 忽略大小写(i)  ☐ 多行(m)
点击按钮:【查找】
预期结果:找到 3 个匹配项

---

【示例 6:批量修改变量名】
📋 源文本内容:
原始代码:
var oldName = "test";
console.log(oldName);
function test(oldName) {
  return oldName.toUpperCase();
}

🔧 操作步骤:
正则表达式:\\boldName\\b
替换为:newName
勾选:☑ 全局(g)  ☐ 忽略大小写(i)  ☐ 多行(m)
点击按钮:【全部替换】
预期结果:所有 oldName → newName(4 处替换)

---

【示例 7:删除空行】
📋 源文本内容:
(使用上方任意示例文本)

🔧 操作步骤:
正则表达式:^\\s*$
替换为:(留空)
勾选:☑ 全局(g)  ☐ 忽略大小写(i)  ☑ 多行(m)
点击按钮:【全部替换】
预期结果:删除所有空白行

---

【示例 8:提取 HTML 标签内容】
📋 源文本内容:
<h1>主标题</h1>
<p>这是第一段内容</p>
<h2>副标题</h2>
<p>这是第二段内容</p>

🔧 操作步骤:
正则表达式:<[^>]+>([^<]*)</[^>]+>
替换为:$1
勾选:☑ 全局(g)  ☐ 忽略大小写(i)  ☐ 多行(m)
点击按钮:【全部替换】
预期结果:提取标签内的文本内容

========================================
快捷键速查:
• Ctrl + Enter:查找
• Ctrl + H:替换第一个
• Ctrl + Shift + H:全部替换
• Ctrl + L:清空所有内容
• Escape:清空正则表达式
========================================`;

// ===== 工具函数 =====
function escapeHtml(text) {
  var div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

function showStatus(text) {
  var el = document.getElementById('status-text');
  if (el) el.textContent = text;
}

function updateSourceCount() {
  var el = document.getElementById('source-count');
  var text = document.getElementById('source-text').value;
  if (el) el.textContent = text.length + ' 字符';
}

function updateMatchCount(count) {
  var el = document.getElementById('match-count');
  if (el) el.textContent = count + ' 个匹配';
}

function updateExecutionTime(time) {
  var el = document.getElementById('execution-time');
  if (el) el.textContent = '执行时间:' + time + 'ms';
}

// ===== 核心功能 =====
function buildRegex() {
  var pattern = document.getElementById('regex-input').value;
  if (!pattern) return null;

  var flags = '';
  if (document.getElementById('flag-global').checked) flags += 'g';
  if (document.getElementById('flag-ignorecase').checked) flags += 'i';
  if (document.getElementById('flag-multiline').checked) flags += 'm';

  try {
    return new RegExp(pattern, flags);
  } catch (e) {
    showStatus('正则表达式错误:' + e.message);
    return null;
  }
}

function findMatches() {
  var startTime = performance.now();
  var regex = buildRegex();
  if (!regex) return;

  var sourceText = document.getElementById('source-text').value;
  if (!sourceText) {
    showStatus('请先输入源文本');
    return;
  }

  var resultContent = document.getElementById('result-content');
  resultContent.innerHTML = '';

  var matches = [];
  var match;

  // 重置 lastIndex(全局搜索时)
  regex.lastIndex = 0;

  while ((match = regex.exec(sourceText)) !== null) {
    matches.push({
      index: match.index,
      text: match[0],
      groups: match.slice(1)
    });

    // 防止零宽度匹配无限循环
    if (match.index === regex.lastIndex) {
      regex.lastIndex++;
    }
  }

  matchCount = matches.length;
  updateMatchCount(matchCount);

  if (matches.length === 0) {
    resultContent.innerHTML = '<div class="result-placeholder"><div class="placeholder-icon">❌</div><div class="placeholder-text">未找到匹配项</div></div>';
    showStatus('未找到匹配项');
  } else {
    // 渲染匹配结果
    matches.forEach(function(m, i) {
      var matchEl = document.createElement('div');
      matchEl.className = 'match-result';

      var headerEl = document.createElement('div');
      headerEl.className = 'match-header';
      headerEl.textContent = '匹配 #' + (i + 1) + ' (位置:' + m.index + ')';

      var contentEl = document.createElement('div');
      contentEl.className = 'match-content';
      contentEl.textContent = m.text;

      matchEl.appendChild(headerEl);
      matchEl.appendChild(contentEl);

      // 显示捕获组
      if (m.groups.length > 0) {
        m.groups.forEach(function(group, j) {
          if (group !== undefined) {
            var groupEl = document.createElement('div');
            groupEl.className = 'match-group';
            groupEl.innerHTML = '<span class="match-group-label">$' + (j + 1) + ':</span> ' + escapeHtml(group);
            matchEl.appendChild(groupEl);
          }
        });
      }

      resultContent.appendChild(matchEl);
    });

    showStatus('找到 ' + matchCount + ' 个匹配项');
  }

  var endTime = performance.now();
  updateExecutionTime(Math.round(endTime - startTime));
}

function replaceFirst() {
  var startTime = performance.now();
  var regex = buildRegex();
  if (!regex) return;

  var sourceText = document.getElementById('source-text').value;
  var replaceText = document.getElementById('replace-input').value;

  if (!sourceText) {
    showStatus('请先输入源文本');
    return;
  }

  // 移除全局标志,仅替换第一个
  var singleRegex = new RegExp(regex.source, regex.flags.replace(/g/g, ''));
  
  // 检查是否有匹配
  var hasMatch = singleRegex.test(sourceText);
  if (!hasMatch) {
    showStatus('未找到匹配项');
    updateExecutionTime(0);
    return;
  }
  
  // 执行替换
  var result = sourceText.replace(singleRegex, replaceText);
  
  // 更新源文本框
  document.getElementById('source-text').value = result;
  updateSourceCount();
  
  // 显示替换结果
  displayReplacedResult(sourceText, result, singleRegex, 1);
  updateMatchCount(1);

  var endTime = performance.now();
  updateExecutionTime(Math.round(endTime - startTime));
}

function replaceAll() {
  var startTime = performance.now();
  var regex = buildRegex();
  if (!regex) return;

  var sourceText = document.getElementById('source-text').value;
  var replaceText = document.getElementById('replace-input').value;

  if (!sourceText) {
    showStatus('请先输入源文本');
    return;
  }

  // 确保有全局标志
  var globalRegex = new RegExp(regex.source, regex.flags.indexOf('g') >= 0 ? regex.flags : regex.flags + 'g');
  
  // 计算匹配数量
  var matches = sourceText.match(globalRegex);
  var count = matches ? matches.length : 0;
  
  // 执行替换
  var result = sourceText.replace(globalRegex, replaceText);
  
  // 更新源文本框
  document.getElementById('source-text').value = result;
  updateSourceCount();
  
  // 显示替换结果(高亮对比)
  displayReplacedResult(sourceText, result, globalRegex, count);
  
  // 更新匹配计数
  updateMatchCount(count);

  var endTime = performance.now();
  updateExecutionTime(Math.round(endTime - startTime));
}

function displayReplacedResult(original, replaced, regex, count) {
  var resultContent = document.getElementById('result-content');
  resultContent.innerHTML = '';

  var replacedEl = document.createElement('div');
  replacedEl.className = 'replaced-content';

  // 显示替换统计信息
  var infoEl = document.createElement('div');
  infoEl.style.cssText = 'padding: 12px; background: #2d2d30; border-radius: 4px; margin-bottom: 12px; border-left: 3px solid #4caf50;';
  infoEl.innerHTML = '<div style="font-size: 14px; color: #4caf50; margin-bottom: 8px; font-weight: 600;">✅ 替换完成 - 共替换 ' + count + ' 处</div>';
  replacedEl.appendChild(infoEl);

  // 找出所有匹配项并显示替换对比
  var matches = [];
  var match;
  var tempRegex = new RegExp(regex.source, regex.flags);
  tempRegex.lastIndex = 0;
  
  while ((match = tempRegex.exec(original)) !== null) {
    // 直接使用捕获组替换
    var replacedText = document.getElementById('replace-input').value;
    // 替换 $1, $2 等捕获组引用
    for (var g = 1; g < match.length; g++) {
      var pattern = new RegExp('\\$' + g, 'g');
      replacedText = replacedText.replace(pattern, match[g] || '');
    }
    // 替换 $& (完整匹配)
    replacedText = replacedText.replace(/\$&/g, match[0]);
    
    matches.push({
      original: match[0],
      replaced: replacedText,
      index: match.index
    });
    if (match.index === tempRegex.lastIndex) {
      tempRegex.lastIndex++;
    }
  }

  // 显示每个替换项
  if (matches.length > 0) {
    var listEl = document.createElement('div');
    listEl.style.cssText = 'margin-bottom: 12px;';
    
    matches.forEach(function(m, i) {
      var itemEl = document.createElement('div');
      itemEl.style.cssText = 'padding: 8px; margin-bottom: 6px; background: #252526; border-radius: 4px; border-left: 2px solid #0078d4;';
      
      var headerEl = document.createElement('div');
      headerEl.style.cssText = 'font-size: 12px; color: #569cd6; margin-bottom: 4px;';
      headerEl.textContent = '替换 #' + (i + 1) + ' (位置:' + m.index + ')';
      
      var originalEl = document.createElement('div');
      originalEl.style.cssText = 'font-size: 13px; margin-bottom: 2px;';
      originalEl.innerHTML = '<span style="color: #f44336; font-weight: 600;">原:</span><span style="color: #f44336; background: rgba(244,67,54,0.1); padding: 2px 4px; border-radius: 2px;">' + escapeHtml(m.original) + '</span>';
      
      var replacedEl2 = document.createElement('div');
      replacedEl2.style.cssText = 'font-size: 13px;';
      replacedEl2.innerHTML = '<span style="color: #4caf50; font-weight: 600;">新:</span><span style="color: #4caf50; background: rgba(76,175,80,0.1); padding: 2px 4px; border-radius: 2px;">' + escapeHtml(m.replaced) + '</span>';
      
      itemEl.appendChild(headerEl);
      itemEl.appendChild(originalEl);
      itemEl.appendChild(replacedEl2);
      listEl.appendChild(itemEl);
    });
    
    replacedEl.appendChild(listEl);
  }

  // 显示替换后的完整文本(高亮显示替换内容)
  var textEl = document.createElement('div');
  textEl.style.cssText = 'padding: 12px; background: #1e1e1e; border-radius: 4px; font-family: Consolas, monospace; font-size: 13px; line-height: 1.6; white-space: pre-wrap; color: #d4d4d4; border: 1px solid #3c3c3c;';
  var labelEl = document.createElement('div');
  labelEl.style.cssText = 'font-size: 12px; color: #b0b0b0; margin-bottom: 8px;';
  labelEl.textContent = '📄 替换后的完整文本(绿色高亮为替换内容):';
  textEl.appendChild(labelEl);
  
  // 构建高亮显示的 HTML
  var highlightedHtml = '';
  var lastIndex = 0;
  var highlightRegex = new RegExp(regex.source, regex.flags);
  highlightRegex.lastIndex = 0;
  var highlightMatch;
  
  while ((highlightMatch = highlightRegex.exec(original)) !== null) {
    // 添加未匹配的文本
    if (highlightMatch.index > lastIndex) {
      highlightedHtml += escapeHtml(original.substring(lastIndex, highlightMatch.index));
    }
    
    // 计算替换后的文本
    var replacedText = document.getElementById('replace-input').value;
    for (var g = 1; g < highlightMatch.length; g++) {
      var pattern = new RegExp('\\$' + g, 'g');
      replacedText = replacedText.replace(pattern, highlightMatch[g] || '');
    }
    replacedText = replacedText.replace(/\$&/g, highlightMatch[0]);
    
    // 添加高亮的替换文本
    highlightedHtml += '<span style="color: #4caf50; background: rgba(76,175,80,0.2); padding: 2px 4px; border-radius: 2px; font-weight: 600;">' + escapeHtml(replacedText) + '</span>';
    
    lastIndex = highlightRegex.lastIndex;
    if (highlightMatch.index === highlightRegex.lastIndex) {
      highlightRegex.lastIndex++;
    }
  }
  
  // 添加剩余文本
  if (lastIndex < original.length) {
    highlightedHtml += escapeHtml(original.substring(lastIndex));
  }
  
  // 如果没有匹配项,显示完整替换后的文本
  if (!highlightedHtml) {
    highlightedHtml = escapeHtml(replaced);
  }
  
  var contentEl = document.createElement('div');
  contentEl.innerHTML = highlightedHtml;
  textEl.appendChild(contentEl);
  
  replacedEl.appendChild(textEl);

  resultContent.appendChild(replacedEl);
  showStatus('已替换 ' + count + ' 个匹配项');
}

function clearAll() {
  document.getElementById('regex-input').value = '';
  document.getElementById('replace-input').value = '';
  // 清空源文本
  document.getElementById('source-text').value = '';
  document.getElementById('result-content').innerHTML = '<div class="result-placeholder"><div class="placeholder-icon">🔍</div><div class="placeholder-text">输入正则表达式并点击"查找"开始搜索</div></div>';
  updateMatchCount(0);
  updateSourceCount();
  updateExecutionTime(0);
  showStatus('已清空所有内容');
}

function resetToExample() {
  // 恢复示例文本
  document.getElementById('source-text').value = EXAMPLE_TEXT;
  updateSourceCount();
  showStatus('已恢复示例文本');
}

// ===== 快捷键支持 =====
function bindKeyboard() {
  document.addEventListener('keydown', function(e) {
    // Ctrl+Enter 查找
    if (e.ctrlKey && e.key === 'Enter') {
      e.preventDefault();
      findMatches();
      return;
    }

    // Ctrl+H 替换第一个
    if (e.ctrlKey && e.key === 'h') {
      e.preventDefault();
      replaceFirst();
      return;
    }

    // Ctrl+Shift+H 全部替换
    if (e.ctrlKey && e.shiftKey && e.key === 'H') {
      e.preventDefault();
      replaceAll();
      return;
    }

    // Ctrl+L 清空
    if (e.ctrlKey && e.key === 'l') {
      e.preventDefault();
      clearAll();
      return;
    }

    // Escape 清空正则输入框
    if (e.key === 'Escape') {
      document.getElementById('regex-input').value = '';
      showStatus('已清空正则表达式');
      return;
    }
  });
}

// ===== 事件绑定 =====
function bindEvents() {
  var btnMap = {
    'btn-find': findMatches,
    'btn-replace': replaceFirst,
    'btn-replace-all': replaceAll,
    'btn-clear': clearAll
  };

  for (var id in btnMap) {
    (function(fn) {
      var el = document.getElementById(id);
      if (el) el.onclick = fn;
    })(btnMap[id]);
  }

  // 源文本变化时更新字符计数
  document.getElementById('source-text').addEventListener('input', updateSourceCount);

  // 正则表达式输入框回车触发查找
  document.getElementById('regex-input').addEventListener('keydown', function(e) {
    if (e.key === 'Enter') {
      e.preventDefault();
      findMatches();
    }
  });

  // 鼠标悬停提示(使用 data-tip)
  document.addEventListener('mouseover', function(e) {
    var btn = e.target;
    if (btn && btn.getAttribute && btn.getAttribute('data-tip')) {
      showStatus(btn.getAttribute('data-tip'));
    }
  });

  bindKeyboard();
}

// ===== 初始化 =====
document.addEventListener('DOMContentLoaded', function() {
  bindEvents();
  showStatus('就绪 - 输入正则表达式开始搜索');

  // 检查源文本是否为空,如果为空则恢复示例文本
  var sourceText = document.getElementById('source-text');
  if (!sourceText.value || sourceText.value.trim() === '') {
    sourceText.value = EXAMPLE_TEXT;
    showStatus('已加载示例文本(重启后自动恢复)');
  }
  updateSourceCount();
});

关键要点

  • 纯前端实现:使用原生 JavaScript RegExp API,零外部依赖
  • 示例文本常量:EXAMPLE_TEXT 全局常量,包含 8 个完整示例
  • 自动恢复机制:重启后自动加载示例文本(开箱即用)
  • 替换对比展示:原值(红色高亮)→ 新值(绿色高亮)
  • 高亮显示:替换后的完整文本中,被替换内容绿色高亮标记
  • 捕获组支持1, 2, 3 引用 3 引用 3引用& 完整匹配引用
  • 防无限循环:检测 match.index === regex.lastIndex,避免零宽度匹配死循环
  • 性能监控:performance.now() 测量执行时间
  • XSS** 防护**:escapeHtml() 函数转义 HTML 特殊字符
  • 快捷键支持:5 个快捷键(Ctrl+Enter/H/Shift+H/L + Escape)

3.5 第五步:编写样式文件(regexxer.css)

文件:web_engine/src/main/resources/resfile/resources/app/regexxer.css

css 复制代码
/* Regexxer - 正则表达式搜索替换工具样式 */
/* 鸿蒙 ArkWeb 兼容:不使用 CSS 变量 */

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', monospace;
  background: #1e1e1e;
  color: #d4d4d4;
  overflow: hidden;
}

.app-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

/* ===== 标题栏 ===== */
.title-bar {
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, #0078d4, #005a9e);
  color: #fff;
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 1px;
  user-select: none;
}

.logo {
  margin-right: 8px;
  font-size: 18px;
}

.app-title {
  font-size: 15px;
}

/* ===== 工具栏 ===== */
.toolbar {
  padding: 12px 16px;
  background: #252526;
  border-bottom: 1px solid #3c3c3c;
}

.toolbar-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.toolbar-row:last-child {
  margin-bottom: 0;
}

.toolbar-label {
  font-size: 13px;
  color: #b0b0b0;
  white-space: nowrap;
  min-width: 80px;
}

.regex-input,
.replace-input {
  flex: 1;
  height: 32px;
  padding: 0 12px;
  background: #3c3c3c;
  border: 1px solid #555;
  border-radius: 4px;
  color: #d4d4d4;
  font-size: 14px;
  font-family: 'Consolas', 'Courier New', monospace;
}

.regex-input:focus,
.replace-input:focus {
  outline: none;
  border-color: #0078d4;
}

.flags-group {
  display: flex;
  gap: 12px;
}

.flag-checkbox {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: #b0b0b0;
  cursor: pointer;
}

.flag-checkbox input[type="checkbox"] {
  cursor: pointer;
}

/* 按钮 */
.btn-group {
  display: flex;
  gap: 4px;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 32px;
  padding: 0 16px;
  border: 1px solid transparent;
  border-radius: 4px;
  background: #3c3c3c;
  color: #d4d4d4;
  cursor: pointer;
  font-size: 13px;
  white-space: nowrap;
}

.btn:hover {
  background: #505050;
  border-color: #555;
}

.btn:active {
  background: #606060;
}

.btn-primary {
  background: #0078d4;
  color: #fff;
  border-color: #0078d4;
}

.btn-primary:hover {
  background: #106ebe;
  border-color: #106ebe;
}

.btn-warning {
  background: #ff9800;
  color: #fff;
  border-color: #ff9800;
}

.btn-warning:hover {
  background: #f57c00;
  border-color: #f57c00;
}

.btn-danger {
  background: #f44336;
  color: #fff;
  border-color: #f44336;
}

.btn-danger:hover {
  background: #d32f2f;
  border-color: #d32f2f;
}

/* ===== 主内容区 ===== */
.main-content {
  flex: 1;
  display: flex;
  gap: 0;
  overflow: hidden;
}

/* 面板 */
.panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  border-right: 1px solid #3c3c3c;
}

.panel:last-child {
  border-right: none;
}

.panel-header {
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 12px;
  background: #2d2d30;
  border-bottom: 1px solid #3c3c3c;
}

.panel-title {
  font-size: 13px;
  font-weight: 600;
  color: #b0b0b0;
}

.panel-count {
  font-size: 12px;
  color: #569cd6;
}

/* 源文本区 */
.source-text {
  flex: 1;
  padding: 12px;
  background: #1e1e1e;
  border: none;
  color: #d4d4d4;
  font-size: 14px;
  font-family: 'Consolas', 'Courier New', monospace;
  line-height: 1.6;
  resize: none;
}

.source-text:focus {
  outline: none;
}

/* 结果区 */
.result-content {
  flex: 1;
  overflow: auto;
  padding: 12px;
  background: #1e1e1e;
  font-family: 'Consolas', 'Courier New', monospace;
  line-height: 1.6;
}

.result-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: #666;
}

.placeholder-icon {
  font-size: 48px;
  margin-bottom: 12px;
}

.placeholder-text {
  font-size: 14px;
}

/* 匹配结果样式 */
.match-result {
  margin-bottom: 12px;
  padding: 8px;
  background: #2d2d30;
  border-radius: 4px;
  border-left: 3px solid #0078d4;
}

.match-header {
  font-size: 12px;
  color: #569cd6;
  margin-bottom: 4px;
}

.match-content {
  font-size: 13px;
  color: #d4d4d4;
  word-break: break-all;
}

.match-group {
  margin-top: 4px;
  padding: 4px 8px;
  background: #3c3c3c;
  border-radius: 2px;
  font-size: 12px;
}

.match-group-label {
  color: #4ec9b0;
  font-weight: 600;
}

/* 替换结果高亮 */
.replaced-content {
  white-space: pre-wrap;
}

.highlight-old {
  background: #f44336;
  color: #fff;
  text-decoration: line-through;
  padding: 2px 4px;
  border-radius: 2px;
}

.highlight-new {
  background: #4caf50;
  color: #fff;
  padding: 2px 4px;
  border-radius: 2px;
}

/* ===== 底部状态栏 ===== */
.status-bar {
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  background: #252526;
  border-top: 1px solid #3c3c3c;
  font-size: 12px;
  color: #888;
}

#execution-time {
  color: #4ec9b0;
  font-weight: 500;
}

/* ===== 暗色主题(默认) ===== */
/* 已使用暗色主题作为默认主题 */

/* ===== 亮色主题 ===== */
.theme-light body {
  background: #f5f5f5;
  color: #333;
}

.theme-light .title-bar {
  background: linear-gradient(135deg, #0078d4, #005a9e);
}

.theme-light .toolbar {
  background: #fff;
  border-bottom-color: #e0e0e0;
}

.theme-light .toolbar-label {
  color: #555;
}

.theme-light .regex-input,
.theme-light .replace-input {
  background: #fff;
  border-color: #ccc;
  color: #333;
}

.theme-light .flag-checkbox {
  color: #555;
}

.theme-light .btn {
  background: #f0f0f0;
  color: #333;
  border-color: #ccc;
}

.theme-light .btn:hover {
  background: #e0e0e0;
}

.theme-light .panel-header {
  background: #fafafa;
  border-bottom-color: #e0e0e0;
}

.theme-light .panel-title {
  color: #555;
}

.theme-light .source-text {
  background: #fff;
  color: #333;
}

.theme-light .result-content {
  background: #fff;
}

.theme-light .match-result {
  background: #f9f9f9;
  border-left-color: #0078d4;
}

.theme-light .match-header {
  color: #0078d4;
}

.theme-light .match-content {
  color: #333;
}

.theme-light .match-group {
  background: #f0f0f0;
}

.theme-light .match-group-label {
  color: #4caf50;
}

.theme-light .status-bar {
  background: #fff;
  border-top-color: #e0e0e0;
  color: #666;
}

.theme-light #execution-time {
  color: #4caf50;
}

关键要点

  • VS Code 暗色主题:使用 #1e1e1e、#252526、#3c3c3c 等经典 VS Code 颜色
  • 双栏布局:源文本区 + 匹配结果区(flex: 1 平分)
  • 工具栏样式:两行布局(正则输入 + 替换输入),按钮分组显示
  • 按钮配色:主要(蓝色)、警告(橙色)、危险(红色)
  • 匹配结果样式:蓝色左边框,等宽字体显示内容
  • 捕获组显示:蓝色 1/2 标签,灰色背景
  • 状态栏:左侧状态提示,右侧执行时间(绿色)
  • 鸿蒙 ArkWeb 兼容:不使用 CSS 变量(var(--xxx)),直接写实际颜色值
  • 移除 Webkit 滚动条样式:鸿蒙不支持 ::-webkit-scrollbar,已删除

四、部署到鸿蒙平台

4.1 项目结构说明

开发****工作流

  1. 直接在 electron-apps/regexxer/ 中修改代码
  2. 同步到 web_engine/src/main/resources/resfile/resources/app/
  3. 在 DevEco Studio 中构建并运行
  4. 真机测试验证

4.2 构建 HAP 包

在 DevEco Studio 中:

  1. 打开项目根目录 ohos_hap/
  2. 点击 Build > Build Hap(s)/APP(s)
  3. 选择 Build Hap(s)
  4. 等待构建完成

4.3 真机测试

  1. 连接鸿蒙设备(或启动模拟器)
  2. 点击 Run > Run 'entry'
  3. 安装完成后,应用会自动启动

五、常见问题 FAQ

Q1:点击"全部替换"后应用无响应,输入框不可编辑?

问题现象:点击全部替换按钮后,右侧匹配结果无显示,正则表达式框和替换为框也变得不可编辑

问题代码(renderer.js 第 228 行):

javascript 复制代码
// ❌ 错误代码:在 exec() 循环内部再次调用 replace()
while ((match = tempRegex.exec(original)) !== null) {
  var replacedText = match[0].replace(tempRegex, document.getElementById('replace-input').value);
  matches.push({
    original: match[0],
    replaced: replacedText,
    index: match.index
  });
  // ...
}

根本原因:在 while (tempRegex.exec(original)) 循环内部,再次调用 match[0].replace(tempRegex, ...) 导致 tempRegexlastIndex 被修改,造成无限循环或逻辑错误

解决方案(renderer.js 第 227-246 行):

javascript 复制代码
// ✅ 正确代码:分离匹配和替换逻辑
while ((match = tempRegex.exec(original)) !== null) {
  // 直接使用捕获组替换
  var replacedText = document.getElementById('replace-input').value;
  // 替换 $1, $2 等捕获组引用
  for (var g = 1; g < match.length; g++) {
    var pattern = new RegExp('\\$' + g, 'g');
    replacedText = replacedText.replace(pattern, match[g] || '');
  }
  // 替换 $& (完整匹配)
  replacedText = replacedText.replace(/\$&/g, match[0]);
  
  matches.push({
    original: match[0],
    replaced: replacedText,
    index: match.index
  });
  if (match.index === tempRegex.lastIndex) {
    tempRegex.lastIndex++;
  }
}

关键点:

  • 不要在 exec() 循环内部再次使用同一个 regex 对象调用 replace()
  • 使用临时字符串处理捕获组替换
  • 检测零宽度匹配(match.index === tempRegex.lastIndex)防止无限循环
  • 支持 1, 2, 3 等捕获组引用和 3 等捕获组引用和 3等捕获组引用和& 完整匹配引用

Q2:鼠标悬停在按钮上应用闪退?

问题现象:鼠标悬停在按钮上触发 SIGABRT 崩溃

问题代码(index.html):

html 复制代码
<!-- ❌ 错误代码:使用 title 属性 -->
<button id="btn-find" class="btn btn-primary" title="查找所有匹配项">查找</button>
<button id="btn-replace-all" class="btn btn-danger" title="替换所有匹配项">全部替换</button>

根本原因:title 属性在 ArkWeb 中触发系统级原生 tooltip 弹窗 → 创建 SubWindow → XComponent 崩溃(WaitForXComponentCreated 超时 → abort)

解决方案(index.html 第 21、40、42-45 行):

html 复制代码
<!-- ✅ 正确代码:使用 data-tip 自定义属性 -->
<input type="text" id="regex-input" class="regex-input" 
       placeholder="输入正则表达式,例如:\d+" 
       data-tip="输入 Perl 风格正则表达式">

<input type="text" id="replace-input" class="replace-input" 
       placeholder="替换文本,使用 $1 引用捕获组" 
       data-tip="使用 $1, $2 引用捕获组">

<button id="btn-find" class="btn btn-primary" data-tip="查找所有匹配项">查找</button>
<button id="btn-replace" class="btn btn-warning" data-tip="替换第一个匹配项">替换</button>
<button id="btn-replace-all" class="btn btn-danger" data-tip="替换所有匹配项">全部替换</button>
<button id="btn-clear" class="btn" data-tip="清空所有输入">清空</button>

配合 renderer.js(第 374-380 行):

javascript 复制代码
// ✅ 正确:使用 data-tip 显示提示
document.addEventListener('mouseover', function(e) {
  var btn = e.target;
  if (btn && btn.getAttribute && btn.getAttribute('data-tip')) {
    showStatus(btn.getAttribute('data-tip'));
  }
});

关键点:

  • data-tip 是纯自定义属性,不触发系统行为
  • mouseover 事件 → 状态栏显示提示文本
  • 完全避开 ArkWeb 原生 tooltip 机制

Q3:替换后的完整文本没有高亮显示被替换的内容?

问题现象:执行全部替换后,右侧结果区只显示统计信息,没有高亮显示替换内容

问题代码(renderer.js 原始版本):

javascript 复制代码
// ❌ 错误代码:仅显示纯文本,无高亮
var textEl = document.createElement('div');
textEl.style.cssText = 'padding: 12px; background: #1e1e1e; ...';
textEl.textContent = replaced;  // ← 纯文本,无高亮

根本原因:使用 textContent 直接赋值,无法渲染 HTML 高亮标签

解决方案(renderer.js 第 278-320 行):

javascript 复制代码
// ✅ 正确代码:构建高亮 HTML
var highlightedHtml = '';
var lastIndex = 0;
var highlightRegex = new RegExp(regex.source, regex.flags);
highlightRegex.lastIndex = 0;
var highlightMatch;

while ((highlightMatch = highlightRegex.exec(original)) !== null) {
  // 添加未匹配的文本
  if (highlightMatch.index > lastIndex) {
    highlightedHtml += escapeHtml(original.substring(lastIndex, highlightMatch.index));
  }
  
  // 计算替换后的文本
  var replacedText = document.getElementById('replace-input').value;
  for (var g = 1; g < highlightMatch.length; g++) {
    var pattern = new RegExp('\\$' + g, 'g');
    replacedText = replacedText.replace(pattern, highlightMatch[g] || '');
  }
  replacedText = replacedText.replace(/\$&/g, highlightMatch[0]);
  
  // 添加高亮的替换文本(绿色)
  highlightedHtml += '<span style="color: #4caf50; background: rgba(76,175,80,0.2); padding: 2px 4px; border-radius: 2px; font-weight: 600;">' + 
                     escapeHtml(replacedText) + '</span>';
  
  lastIndex = highlightRegex.lastIndex;
  if (highlightMatch.index === highlightRegex.lastIndex) {
    highlightRegex.lastIndex++;
  }
}

// 添加剩余文本
if (lastIndex < original.length) {
  highlightedHtml += escapeHtml(original.substring(lastIndex));
}

var contentEl = document.createElement('div');
contentEl.innerHTML = highlightedHtml;  // ← 使用 innerHTML 渲染高亮
textEl.appendChild(contentEl);

关键点:

  • 遍历原文本,找出所有匹配项
  • 未匹配文本使用 escapeHtml() 转义后添加
  • 替换内容使用绿色高亮(color: #4caf50, background: rgba(76,175,80,0.2))
  • 使用 innerHTML 而非 textContent 渲染 HTML 标签
  • 使用 escapeHtml() 防止 XSS 攻击

Q4:重启应用后示例文本丢失,源文本区为空?

问题现象:关闭应用重新打开后,源文本区为空,需要手动输入测试内容

问题代码(renderer.js 原始版本):

javascript 复制代码
// ❌ 错误代码:未检查源文本状态,直接覆盖
document.addEventListener('DOMContentLoaded', function() {
  document.getElementById('source-text').value = `...(很长的示例文本)...`;
  updateSourceCount();
});

根本原因:每次初始化都强制覆盖源文本,用户修改的内容丢失

解决方案(renderer.js 第 8-145、577-584 行):

javascript 复制代码
// ✅ 正确代码:提取为全局常量 + 智能检测
// ===== 示例文本常量 =====
var EXAMPLE_TEXT = `========================================
Regexxer 正则表达式工具 - 使用示例
========================================

【示例 1:查找所有邮箱地址】
📋 源文本内容:
联系方式:
- 技术支持:support@example.com
- 销售热线:sales@company.org
- 客服邮箱:service@test.cn

🔧 操作步骤:
正则表达式:\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b
替换为:(留空)
勾选:☑ 全局(g)  ☐ 忽略大小写(i)  ☐ 多行(m)
点击按钮:【查找】
预期结果:找到 3 个匹配项

...(共 8 个示例)

========================================
快捷键速查:
• Ctrl + Enter:查找
• Ctrl + H:替换第一个
• Ctrl + Shift + H:全部替换
• Ctrl + L:清空所有内容
• Escape:清空正则表达式
========================================`;

// ===== 初始化 =====
document.addEventListener('DOMContentLoaded', function() {
  bindEvents();
  showStatus('就绪 - 输入正则表达式开始搜索');

  // 检查源文本是否为空,如果为空则恢复示例文本
  var sourceText = document.getElementById('source-text');
  if (!sourceText.value || sourceText.value.trim() === '') {
    sourceText.value = EXAMPLE_TEXT;
    showStatus('已加载示例文本(重启后自动恢复)');
  }
  updateSourceCount();
});

关键点:

  • 将示例文本提取为全局常量 EXAMPLE_TEXT(第 8-145 行)
  • 初始化时检查源文本是否为空(!sourceText.value || sourceText.value.trim() === '')
  • 空时自动加载示例文本,非空时保留用户内容
  • 提供 resetToExample() 函数手动恢复示例

Q5:多行模式下 ^ 锚点无法正确匹配行首?

问题现象:使用 /^Start/gm 正则表达式,期望匹配所有行首的 "Start",但只找到 1 个匹配

问题代码(测试场景):

javascript 复制代码
// ❌ 错误测试数据
var text = 'First line\nStart here\nAnother Start';
// 期望:找到 2 个匹配(第 2 行和第 3 行)
// 实际:只找到 1 个匹配(第 2 行)

根本原因:^Start 匹配行的 "Start",不是行中任意位置。"Another Start" 中 "Start" 在行中,不在行首

解决方案(逐行分割检测):

javascript 复制代码
// ✅ 正确:逐行分割检测
var text = 'First line' + String.fromCharCode(10) + 'Start here' + String.fromCharCode(10) + 'Start again';
var lines = text.split(String.fromCharCode(10));
var matches = [];

for (var i = 0; i < lines.length; i++) {
  var line = lines[i].trim();
  if (line.indexOf('Start') === 0) {  // 检查行首
    matches.push(line);
  }
}
// 结果:找到 2 个匹配("Start here" 和 "Start again")

关键点:

  • ^ 锚点在多行模式下匹配行,不是行中任意位置
  • 使用 String.fromCharCode(10) 明确创建换行符(避免 \n 字面量解析问题)
  • 使用 indexOf('Start') === 0 检测行首比正则更可靠
  • 测试数据必须确保目标字符串在行首

Q6:替换对比展示中,原值和新值无法区分?

问题现象:执行全部替换后,右侧结果区只显示替换统计,无法清晰看到每个替换项的原值和新值

问题代码(renderer.js 原始版本):

javascript 复制代码
// ❌ 错误代码:无替换对比展示
function displayReplacedResult(original, replaced, regex, count) {
  var infoEl = document.createElement('div');
  infoEl.innerHTML = '✅ 替换完成 - 共替换 ' + count + ' 处';
  // 仅显示统计信息,无替换对比
}

根本原因:未遍历匹配项并显示原值→新值的对比

解决方案(renderer.js 第 248-276 行):

javascript 复制代码
// ✅ 正确代码:显示每个替换项的原值和新值
if (matches.length > 0) {
  var listEl = document.createElement('div');
  listEl.style.cssText = 'margin-bottom: 12px;';
  
  matches.forEach(function(m, i) {
    var itemEl = document.createElement('div');
    itemEl.style.cssText = 'padding: 8px; margin-bottom: 6px; background: #252526; border-radius: 4px; border-left: 2px solid #0078d4;';
    
    var headerEl = document.createElement('div');
    headerEl.style.cssText = 'font-size: 12px; color: #569cd6; margin-bottom: 4px;';
    headerEl.textContent = '替换 #' + (i + 1) + ' (位置:' + m.index + ')';
    
    var originalEl = document.createElement('div');
    originalEl.style.cssText = 'font-size: 13px; margin-bottom: 2px;';
    originalEl.innerHTML = '<span style="color: #f44336; font-weight: 600;">原:</span>' +
                          '<span style="color: #f44336; background: rgba(244,67,54,0.1); padding: 2px 4px; border-radius: 2px;">' + 
                          escapeHtml(m.original) + '</span>';
    
    var replacedEl2 = document.createElement('div');
    replacedEl2.style.cssText = 'font-size: 13px;';
    replacedEl2.innerHTML = '<span style="color: #4caf50; font-weight: 600;">新:</span>' +
                           '<span style="color: #4caf50; background: rgba(76,175,80,0.1); padding: 2px 4px; border-radius: 2px;">' + 
                           escapeHtml(m.replaced) + '</span>';
    
    itemEl.appendChild(headerEl);
    itemEl.appendChild(originalEl);
    itemEl.appendChild(replacedEl2);
    listEl.appendChild(itemEl);
  });
  
  replacedEl.appendChild(listEl);
}

关键点:

  • 遍历 matches 数组,为每个替换项创建独立的展示块
  • 原值使用红色高亮(color: #f44336, background: rgba(244,67,54,0.1))
  • 新值使用绿色高亮(color: #4caf50, background: rgba(76,175,80,0.1))
  • 显示替换位置索引(m.index)
  • 使用 escapeHtml() 防止 XSS

Q7:捕获组引用 1, 2, $3 在替换时不生效?

问题现象:输入替换文本 $2/$3/$1,期望将 2024-01-15 替换为 01/15/2024,但输出仍是 $2/$3/$1 字面量

问题代码(renderer.js 原始版本):

javascript 复制代码
// ❌ 错误代码:直接使用 replaceText 字面量
var result = sourceText.replace(globalRegex, replaceText);
// replaceText = "$2/$3/$1" 未被解析为捕获组引用

根本原因:String.replace() 在 ArkWeb 中可能不自动解析 1, 2 等捕获组引用

解决方案(renderer.js 第 229-236 行):

javascript 复制代码
// ✅ 正确代码:手动处理捕获组引用
var replacedText = document.getElementById('replace-input').value;

// 替换 $1, $2 等捕获组引用
for (var g = 1; g < match.length; g++) {
  var pattern = new RegExp('\\$' + g, 'g');
  replacedText = replacedText.replace(pattern, match[g] || '');
}

// 替换 $& (完整匹配)
replacedText = replacedText.replace(/\$&/g, match[0]);

关键点:

  • 遍历 match 数组(match1, match2, ... 是捕获组内容)
  • 使用 RegExp('\' + g, 'g') 替换 1, $2 等占位符
  • 使用 || '' 防止 undefined(当捕获组未匹配时)
  • 支持 $& 引用完整匹配(match0

Q8:为什么 Regexxer 比 FreePlane 更轻量且无需复杂 Native 集成?

问题现象:FreePlane 需要处理布局算法、Canvas 导出、IPC 文件操作等复杂逻辑,而 Regexxer 几乎纯前端就能运行

技术对比:

bash 复制代码
FreePlane 技术栈:
├── 自研布局算法         ← 递归树形布局,左右对称分布
├── 离屏 Canvas 导出     ← buildExportCanvas(2) 生成 2x 分辨率 PNG
├── IPC 文件操作         ← auto-save-path / export-png-path / write-binary
└── 撤销/重做历史栈      ← 100 步 JSON.stringify/parse 深拷贝

Regexxer 技术栈:
├── 纯前端实现           ← 原生 JavaScript RegExp API
├── 自研替换引擎         ← 支持捕获组、$&、防无限循环
├── CSS 样式             ← 直接写实际值,不使用 CSS 变量
└── 零文件操作           ← 无需 IPC,纯内存处理

关键点:

  • Regexxer 不使用任何外部正则表达式库(如 PCRE、XRegExp)
  • 自研替换引擎支持捕获组引用和防无限循环保护
  • 鸿蒙 ArkWeb 基于 Chromium,完整支持现代 Web 标准(RegExp、performance.now() 等)
  • 只处理了 CSS 变量和 title 属性的兼容问题
  • 对比 FreePlane:无需处理布局算法、Canvas 导出、文件读写等复杂逻辑

核心优势:

  • 纯前端实现:零外部依赖,仅 5 个核心文件(main.js / renderer.js / index.html / package.json / regexxer.css)
  • 快速开发:Web 技术栈,开发效率高
  • 易于维护:UI 和业务逻辑分离
  • 鸿蒙兼容:通过 WebView 桥接,避开 Native 兼容问题
  • 开箱即用:内置 8 个完整示例,重启自动恢复