370 lines
11 KiB
TypeScript
370 lines
11 KiB
TypeScript
/**
|
||
* Collabora Online 搜索和替换功能
|
||
*
|
||
* 职责: 封装 UNO 搜索替换命令
|
||
*
|
||
* @encoding UTF-8
|
||
*/
|
||
|
||
import { sendUnoCommand } from '../Uno';
|
||
|
||
/**
|
||
* 搜索命令类型
|
||
*/
|
||
export enum SearchCommand {
|
||
/** 查找下一个 */
|
||
FindNext = 0,
|
||
/** 查找上一个 */
|
||
FindPrevious = 1,
|
||
/** 替换当前选中 */
|
||
Replace = 2,
|
||
/** 替换全部 */
|
||
ReplaceAll = 3,
|
||
}
|
||
|
||
/**
|
||
* 搜索选项
|
||
*/
|
||
export interface SearchOptions {
|
||
/** 区分大小写 */
|
||
caseSensitive?: boolean;
|
||
/** 全字匹配 */
|
||
wholeWord?: boolean;
|
||
/** 使用正则表达式 */
|
||
regexp?: boolean;
|
||
/** 向后搜索 */
|
||
backward?: boolean;
|
||
/** 静默模式(不显示搜索结果提示) */
|
||
quiet?: boolean;
|
||
}
|
||
|
||
/**
|
||
* 替换选项
|
||
*/
|
||
export interface ReplaceOptions extends SearchOptions {
|
||
/** 替换全部 */
|
||
replaceAll?: boolean;
|
||
}
|
||
|
||
/**
|
||
* 搜索文本 - 查找下一个匹配项
|
||
* @param iframeWindow - iframe 的 contentWindow
|
||
* @param text - 要搜索的文本
|
||
* @param options - 搜索选项
|
||
*/
|
||
export function unoSearchNext(
|
||
iframeWindow: Window,
|
||
text: string,
|
||
options: SearchOptions = {}
|
||
): void {
|
||
const {
|
||
caseSensitive = false,
|
||
wholeWord = false,
|
||
regexp = false,
|
||
backward = false,
|
||
quiet = true,
|
||
} = options;
|
||
|
||
// 计算 SearchFlags
|
||
let searchFlags = 0;
|
||
if (wholeWord) searchFlags |= 0x00010000; // SEARCH_WORD
|
||
|
||
sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', {
|
||
'SearchItem.SearchString': { type: 'string', value: text },
|
||
'SearchItem.Command': { type: 'long', value: backward ? SearchCommand.FindPrevious : SearchCommand.FindNext },
|
||
'SearchItem.Backward': { type: 'boolean', value: backward },
|
||
'SearchItem.SearchCaseSensitive': { type: 'boolean', value: caseSensitive },
|
||
'SearchItem.SearchRegularExpression': { type: 'boolean', value: regexp },
|
||
'SearchItem.SearchFlags': { type: 'long', value: searchFlags },
|
||
'SearchItem.AlgorithmType': { type: 'short', value: regexp ? 1 : 0 },
|
||
'Quiet': { type: 'boolean', value: quiet },
|
||
});
|
||
|
||
console.log('[SearchReplace] 搜索下一个:', text, options);
|
||
}
|
||
|
||
/**
|
||
* 搜索文本 - 查找上一个匹配项
|
||
* @param iframeWindow - iframe 的 contentWindow
|
||
* @param text - 要搜索的文本
|
||
* @param options - 搜索选项
|
||
*/
|
||
export function unoSearchPrevious(
|
||
iframeWindow: Window,
|
||
text: string,
|
||
options: SearchOptions = {}
|
||
): void {
|
||
unoSearchNext(iframeWindow, text, { ...options, backward: true });
|
||
console.log('[SearchReplace] 搜索上一个:', text);
|
||
}
|
||
|
||
/**
|
||
* 替换当前选中的匹配项
|
||
* @param iframeWindow - iframe 的 contentWindow
|
||
* @param searchText - 要搜索的文本
|
||
* @param replaceText - 替换后的文本
|
||
* @param options - 替换选项
|
||
*/
|
||
export function unoReplaceCurrent(
|
||
iframeWindow: Window,
|
||
searchText: string,
|
||
replaceText: string,
|
||
options: SearchOptions = {}
|
||
): void {
|
||
const {
|
||
caseSensitive = false,
|
||
wholeWord = false,
|
||
regexp = false,
|
||
quiet = true,
|
||
} = options;
|
||
|
||
let searchFlags = 0;
|
||
if (wholeWord) searchFlags |= 0x00010000;
|
||
|
||
sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', {
|
||
'SearchItem.SearchString': { type: 'string', value: searchText },
|
||
'SearchItem.ReplaceString': { type: 'string', value: replaceText },
|
||
'SearchItem.Command': { type: 'long', value: SearchCommand.Replace },
|
||
'SearchItem.Backward': { type: 'boolean', value: false },
|
||
'SearchItem.SearchCaseSensitive': { type: 'boolean', value: caseSensitive },
|
||
'SearchItem.SearchRegularExpression': { type: 'boolean', value: regexp },
|
||
'SearchItem.SearchFlags': { type: 'long', value: searchFlags },
|
||
'SearchItem.AlgorithmType': { type: 'short', value: regexp ? 1 : 0 },
|
||
'Quiet': { type: 'boolean', value: quiet },
|
||
});
|
||
|
||
console.log('[SearchReplace] 替换当前:', searchText, '->', replaceText);
|
||
}
|
||
|
||
/**
|
||
* 替换所有匹配项
|
||
* @param iframeWindow - iframe 的 contentWindow
|
||
* @param searchText - 要搜索的文本
|
||
* @param replaceText - 替换后的文本
|
||
* @param options - 替换选项
|
||
*/
|
||
export function unoReplaceAll(
|
||
iframeWindow: Window,
|
||
searchText: string,
|
||
replaceText: string,
|
||
options: SearchOptions = {}
|
||
): void {
|
||
const {
|
||
caseSensitive = false,
|
||
wholeWord = false,
|
||
regexp = false,
|
||
quiet = true,
|
||
} = options;
|
||
|
||
let searchFlags = 0;
|
||
if (wholeWord) searchFlags |= 0x00010000;
|
||
|
||
sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', {
|
||
'SearchItem.SearchString': { type: 'string', value: searchText },
|
||
'SearchItem.ReplaceString': { type: 'string', value: replaceText },
|
||
'SearchItem.Command': { type: 'long', value: SearchCommand.ReplaceAll },
|
||
'SearchItem.SearchAll': { type: 'boolean', value: true },
|
||
'SearchItem.Backward': { type: 'boolean', value: false },
|
||
'SearchItem.SearchCaseSensitive': { type: 'boolean', value: caseSensitive },
|
||
'SearchItem.SearchRegularExpression': { type: 'boolean', value: regexp },
|
||
'SearchItem.SearchFlags': { type: 'long', value: searchFlags },
|
||
'SearchItem.AlgorithmType': { type: 'short', value: regexp ? 1 : 0 },
|
||
'Quiet': { type: 'boolean', value: quiet },
|
||
});
|
||
|
||
console.log('[SearchReplace] 替换全部:', searchText, '->', replaceText);
|
||
}
|
||
|
||
/**
|
||
* 统一的搜索替换接口
|
||
* @param iframeWindow - iframe 的 contentWindow
|
||
* @param searchText - 要搜索的文本
|
||
* @param replaceText - 替换后的文本(可选,不提供则只搜索)
|
||
* @param options - 选项
|
||
*/
|
||
export function unoSearchAndReplace(
|
||
iframeWindow: Window,
|
||
searchText: string,
|
||
replaceText?: string,
|
||
options: ReplaceOptions = {}
|
||
): void {
|
||
if (replaceText === undefined || replaceText === null) {
|
||
// 只搜索
|
||
unoSearchNext(iframeWindow, searchText, options);
|
||
} else if (options.replaceAll) {
|
||
// 替换全部
|
||
unoReplaceAll(iframeWindow, searchText, replaceText, options);
|
||
} else {
|
||
// 替换当前
|
||
unoReplaceCurrent(iframeWindow, searchText, replaceText, options);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取消搜索选中状态
|
||
* @param iframeWindow - iframe 的 contentWindow
|
||
*/
|
||
export function unoCancelSearch(iframeWindow: Window): void {
|
||
sendUnoCommand(iframeWindow, '.uno:Escape', {});
|
||
console.log('[SearchReplace] 取消搜索');
|
||
}
|
||
|
||
/**
|
||
* 在指定页面替换文本(组合命令)
|
||
* 流程:跳转页面 -> 等待 -> 搜索选中 -> 等待 -> 替换
|
||
*
|
||
* @param iframeWindow - iframe 的 contentWindow
|
||
* @param pageNumber - 目标页码(从1开始)
|
||
* @param searchText - 要搜索的文本
|
||
* @param replaceText - 替换后的文本
|
||
* @param options - 搜索选项
|
||
* @returns Promise<boolean> - 是否成功
|
||
*/
|
||
export async function replaceTextInPage(
|
||
iframeWindow: Window,
|
||
pageNumber: number,
|
||
searchText: string,
|
||
replaceText: string,
|
||
options: SearchOptions = {}
|
||
): Promise<{ success: boolean; message: string }> {
|
||
const {
|
||
caseSensitive = false,
|
||
wholeWord = false,
|
||
regexp = false,
|
||
quiet = true,
|
||
} = options;
|
||
|
||
console.log('[SearchReplace] 开始在第', pageNumber, '页替换:', searchText, '->', replaceText);
|
||
|
||
try {
|
||
// 1. 发送自定义消息跳转到指定页面
|
||
const gotoMessage = {
|
||
MessageId: 'custompostMessage',
|
||
SendTime: Date.now(),
|
||
Values: {
|
||
Command: 'GOTO_PAGE',
|
||
Args: { pageNumber },
|
||
},
|
||
};
|
||
iframeWindow.postMessage(JSON.stringify(gotoMessage), '*');
|
||
console.log('[SearchReplace] 步骤1: 跳转到第', pageNumber, '页');
|
||
|
||
// 等待页面跳转完成
|
||
await delay(500);
|
||
|
||
// 2. 搜索文本(查找下一个)
|
||
let searchFlags = 0;
|
||
if (wholeWord) searchFlags |= 0x00010000;
|
||
|
||
sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', {
|
||
'SearchItem.SearchString': { type: 'string', value: searchText },
|
||
'SearchItem.Command': { type: 'long', value: SearchCommand.FindNext },
|
||
'SearchItem.Backward': { type: 'boolean', value: false },
|
||
'SearchItem.SearchCaseSensitive': { type: 'boolean', value: caseSensitive },
|
||
'SearchItem.SearchRegularExpression': { type: 'boolean', value: regexp },
|
||
'SearchItem.SearchFlags': { type: 'long', value: searchFlags },
|
||
'SearchItem.AlgorithmType': { type: 'short', value: regexp ? 1 : 0 },
|
||
'Quiet': { type: 'boolean', value: quiet },
|
||
});
|
||
console.log('[SearchReplace] 步骤2: 搜索文本');
|
||
|
||
// 等待搜索完成
|
||
await delay(300);
|
||
|
||
// 3. 替换选中的文本
|
||
sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', {
|
||
'SearchItem.SearchString': { type: 'string', value: searchText },
|
||
'SearchItem.ReplaceString': { type: 'string', value: replaceText },
|
||
'SearchItem.Command': { type: 'long', value: SearchCommand.Replace },
|
||
'SearchItem.Backward': { type: 'boolean', value: false },
|
||
'SearchItem.SearchCaseSensitive': { type: 'boolean', value: caseSensitive },
|
||
'SearchItem.SearchRegularExpression': { type: 'boolean', value: regexp },
|
||
'SearchItem.SearchFlags': { type: 'long', value: searchFlags },
|
||
'SearchItem.AlgorithmType': { type: 'short', value: regexp ? 1 : 0 },
|
||
'Quiet': { type: 'boolean', value: quiet },
|
||
});
|
||
console.log('[SearchReplace] 步骤3: 替换文本');
|
||
|
||
return {
|
||
success: true,
|
||
message: `已在第${pageNumber}页替换: "${searchText}" -> "${replaceText}"`,
|
||
};
|
||
} catch (e) {
|
||
console.error('[SearchReplace] 替换失败:', e);
|
||
return {
|
||
success: false,
|
||
message: e instanceof Error ? e.message : '未知错误',
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 延迟函数
|
||
*/
|
||
function delay(ms: number): Promise<void> {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
/**
|
||
* 高亮文本(使用 UNO 命令)
|
||
* 流程:先搜索选中所有匹配项,再设置背景色
|
||
*
|
||
* @param iframeWindow - iframe 的 contentWindow
|
||
* @param text - 要高亮的文本
|
||
* @param color - 高亮颜色,默认 16776960 = 黄色
|
||
*/
|
||
export function unoHighlightText(
|
||
iframeWindow: Window,
|
||
text: string,
|
||
color: number = 16776960
|
||
): void {
|
||
// 1. 查找所有匹配项(FindAll)
|
||
sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', {
|
||
'SearchItem.SearchString': { type: 'string', value: text },
|
||
'SearchItem.Command': { type: 'long', value: 1 }, // 1 = FindAll / Search Next
|
||
'SearchItem.SearchFlags': { type: 'long', value: 0 },
|
||
'SearchItem.AlgorithmType': { type: 'short', value: 0 },
|
||
'SearchItem.Backward': { type: 'boolean', value: false },
|
||
'SearchItem.SearchCaseSensitive': { type: 'boolean', value: false },
|
||
'Quiet': { type: 'boolean', value: true },
|
||
});
|
||
|
||
// 2. 设置背景色高亮
|
||
sendUnoCommand(iframeWindow, '.uno:BackColor', {
|
||
BackColor: { type: 'long', value: color },
|
||
});
|
||
|
||
console.log('[SearchReplace] 高亮文本:', text, '颜色:', color);
|
||
}
|
||
|
||
/**
|
||
* 清除高亮(使用 UNO 命令)
|
||
* 通过设置背景色为透明来清除高亮
|
||
*
|
||
* @param iframeWindow - iframe 的 contentWindow
|
||
* @param text - 要清除高亮的文本(可选,不传则清除当前选中)
|
||
*/
|
||
export function unoClearHighlight(
|
||
iframeWindow: Window,
|
||
text?: string
|
||
): void {
|
||
if (text) {
|
||
// 先搜索选中文本
|
||
sendUnoCommand(iframeWindow, '.uno:ExecuteSearch', {
|
||
'SearchItem.SearchString': { type: 'string', value: text },
|
||
'SearchItem.Command': { type: 'long', value: 1 },
|
||
'SearchItem.SearchFlags': { type: 'long', value: 0 },
|
||
'SearchItem.AlgorithmType': { type: 'short', value: 0 },
|
||
'SearchItem.Backward': { type: 'boolean', value: false },
|
||
'Quiet': { type: 'boolean', value: true },
|
||
});
|
||
}
|
||
|
||
// 设置背景色为透明(-1 表示无背景色)
|
||
sendUnoCommand(iframeWindow, '.uno:BackColor', {
|
||
BackColor: { type: 'long', value: -1 },
|
||
});
|
||
|
||
console.log('[SearchReplace] 清除高亮:', text || '当前选中');
|
||
}
|