// 在终稿里定位某条冲突对应的正文区间(供编辑器 setSelectionRange 高亮)。 // 纯逻辑(可单测,不依赖 DOM)。定位优先级:精确 original → where 引号内原文 → 段级 snippet。 import type { ReviewConflict } from "./sse"; import { conflictSnippet } from "./snippet"; export interface DraftRange { start: number; end: number; } // 抽取一段文本里所有成对引号内的内容(「」/“”/‘’/ 直双引号 / 直单引号)。 // where 多形如 草稿中写"<草稿原文>" —— 引号里就是正文精确子串,是最可靠的定位锚。 // 返回去重后按长度降序(长串更具体、误命中概率低)。 const QUOTE_PATTERNS: RegExp[] = [ /「([^」]{2,})」/g, /“([^”]{2,})”/g, /"([^"]{2,})"/g, /‘([^’]{2,})’/g, /'([^']{2,})'/g, ]; export function extractDraftQuotes(where: string): string[] { const found = new Set(); for (const re of QUOTE_PATTERNS) { for (const m of where.matchAll(re)) { const inner = m[1]?.trim(); if (inner && inner.length >= 2) found.add(inner); } } return [...found].sort((a, b) => b.length - a.length); } // 返回冲突在 text 中的字符区间;定位不到(无 original/引号/段号,或已被改动)→ null。 export function locateInDraft( text: string, conflict: Pick, ): DraftRange | null { // 1) 精确原文片段(审稿提议的 original)逐字匹配——最可靠。 if (conflict.original) { const idx = text.indexOf(conflict.original); if (idx !== -1) { return { start: idx, end: idx + conflict.original.length }; } } // 2) 从 where 的引号里抽出草稿原文逐字匹配(覆盖 `草稿中写"…"` 这类定位)。 for (const quote of extractDraftQuotes(conflict.where)) { const idx = text.indexOf(quote); if (idx !== -1) { return { start: idx, end: idx + quote.length }; } } // 3) 回退:按 where 段号取段级预览,定位该段。snippet 超长会带省略号, // 去掉末尾「…」后用其前缀(仍是该段真实前缀)做 indexOf。 const snippet = conflictSnippet(text, conflict.where); if (snippet) { const needle = snippet.endsWith("…") ? snippet.slice(0, -1) : snippet; if (needle.length > 0) { const idx = text.indexOf(needle); if (idx !== -1) { return { start: idx, end: idx + needle.length }; } } } return null; }