import { HELP_SECTION_IDS, type HelpSectionId } from './helpSections'; /** * Короткие имена разделов в перекрёстных ссылках * (когда в тексте не полное title, напр. «Эффекты» вместо «Эффекты поля и действий»). */ export const HELP_SECTION_LINK_ALIASES: Partial> = { effects: ['Эффекты', 'Effects'], presentation: ['Презентация', 'Presentation'], grid: ['Сетка', 'Grid'], controlPanel: ['Пульт'], players: ['Игроки', 'Players'], }; export type HelpLinkCatalogEntry = { id: HelpSectionId; label: string; }; export type HelpLinkRange = { start: number; end: number; id: HelpSectionId; text: string; }; function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** Каталог подписей → id, длинные первыми. */ export function buildHelpLinkCatalog(getTitle: (id: HelpSectionId) => string): HelpLinkCatalogEntry[] { const byLabel = new Map(); for (const id of HELP_SECTION_IDS) { const title = getTitle(id).trim(); if (title) byLabel.set(title, id); for (const alias of HELP_SECTION_LINK_ALIASES[id] ?? []) { const a = alias.trim(); if (a) byLabel.set(a, id); } } return [...byLabel.entries()] .map(([label, id]) => ({ id, label })) .sort((a, b) => b.label.length - a.label.length || a.label.localeCompare(b.label)); } function isBoundaryChar(ch: string | undefined): boolean { if (ch === undefined) return true; if (/^[\p{L}\p{N}]$/u.test(ch)) return false; return true; } function collectMatches(text: string, label: string, id: HelpSectionId): HelpLinkRange[] { const out: HelpLinkRange[] = []; if (!label) return out; const quoted = new RegExp(`«${escapeRegExp(label)}»`, 'g'); for (const m of text.matchAll(quoted)) { if (m.index === undefined) continue; out.push({ start: m.index, end: m.index + m[0].length, id, text: m[0] }); } const bare = new RegExp(escapeRegExp(label), 'g'); for (const m of text.matchAll(bare)) { if (m.index === undefined) continue; const start = m.index; const end = start + m[0].length; const before = text[start - 1]; const after = text[end]; if (!isBoundaryChar(before) || !isBoundaryChar(after)) continue; if (before === '«' && after === '»') continue; out.push({ start, end, id, text: m[0] }); } return out; } export function findHelpLinkRanges(text: string, catalog: readonly HelpLinkCatalogEntry[]): HelpLinkRange[] { const candidates: HelpLinkRange[] = []; for (const entry of catalog) { candidates.push(...collectMatches(text, entry.label, entry.id)); } candidates.sort( (a, b) => a.start - b.start || b.end - b.start - (a.end - a.start), ); const selected: HelpLinkRange[] = []; let cursor = 0; for (const range of candidates) { if (range.start < cursor) continue; selected.push(range); cursor = range.end; } return selected; } export function splitHelpTextWithLinks( text: string, catalog: readonly HelpLinkCatalogEntry[], ): Array<{ type: 'text'; value: string } | { type: 'link'; id: HelpSectionId; value: string }> { const ranges = findHelpLinkRanges(text, catalog); if (ranges.length === 0) return [{ type: 'text', value: text }]; const parts: Array<{ type: 'text'; value: string } | { type: 'link'; id: HelpSectionId; value: string }> = []; let cursor = 0; for (const range of ranges) { if (range.start > cursor) { parts.push({ type: 'text', value: text.slice(cursor, range.start) }); } parts.push({ type: 'link', id: range.id, value: range.text }); cursor = range.end; } if (cursor < text.length) { parts.push({ type: 'text', value: text.slice(cursor) }); } return parts; }