flappa-doormal 2.19.0 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["TOKEN_PRIORITY_ORDER","resolveOptions","skipWhitespace","passesRuleConstraints"],"sources":["../src/segmentation/tokens.ts","../src/utils/textUtils.ts","../src/analysis/shared.ts","../src/analysis/line-starts.ts","../src/analysis/repeating-sequences.ts","../src/detection.ts","../src/types/rules.ts","../src/optimization/optimize-rules.ts","../src/preprocessing/transforms.ts","../src/segmentation/arabic-dictionary-rule.ts","../src/segmentation/breakpoint-constants.ts","../src/segmentation/match-utils.ts","../src/segmentation/breakpoint-utils.ts","../src/segmentation/debug-meta.ts","../src/segmentation/pattern-validator.ts","../src/segmentation/breakpoint-processor.ts","../src/segmentation/rule-regex.ts","../src/segmentation/fast-fuzzy-prefix.ts","../src/segmentation/segmenter-rule-utils.ts","../src/segmentation/split-point-helpers.ts","../src/segmentation/segmenter.ts","../src/validation/validate-segments.ts"],"sourcesContent":["/**\n * Arabic base letters used by low-level dictionary-style regex helpers.\n *\n * This is intentionally broader than `{{harf}}`:\n * - includes standalone hamza `ء`\n * - stays as a raw regex fragment rather than a template token\n */\nexport const ARABIC_BASE_LETTER_CLASS = '[ء-غف-ي]';\n\n/**\n * Arabic combining marks / annotation signs used by low-level regex helpers.\n */\nexport const ARABIC_MARKS_CLASS = '[\\\\u0610-\\\\u061A\\\\u0640\\\\u064B-\\\\u065F\\\\u0670\\\\u06D6-\\\\u06ED]';\n\n/**\n * A single Arabic base letter followed by zero or more combining marks.\n */\nexport const ARABIC_LETTER_WITH_OPTIONAL_MARKS_PATTERN = `${ARABIC_BASE_LETTER_CLASS}${ARABIC_MARKS_CLASS}*`;\n\n/**\n * One or more Arabic letters, where each letter may carry combining marks.\n */\nexport const ARABIC_WORD_WITH_OPTIONAL_MARKS_PATTERN = `(?:${ARABIC_LETTER_WITH_OPTIONAL_MARKS_PATTERN})+`;\n\nconst ARABIC_SPACED_CODE_ATOM = `[أ-غف-ي]${ARABIC_MARKS_CLASS}*`;\n\n/**\n * Base token definitions mapping human-readable token names to regex patterns.\n *\n * These tokens contain raw regex patterns and do not reference other tokens.\n * For composite tokens that build on these, see `COMPOSITE_TOKENS`.\n *\n * @internal\n */\n// IMPORTANT:\n// - We include the Arabic-Indic digit `٤` as a rumuz code, but we do NOT match it when it's part of a larger number (e.g. \"٣٤\").\n// - We intentionally do NOT match ASCII `4`.\n// - For performance/clarity, the single-letter rumuz are represented as a character class.\n// - Single-letter codes must NOT be followed by Arabic diacritics (\\u064B-\\u0652, \\u0670) or letters (أ-ي),\n// otherwise we'd incorrectly match the first letter of Arabic words like عَن as rumuz ع.\nconst RUMUZ_SINGLE_LETTER = '[خرزيمنصسدفلتقع](?![\\\\u064B-\\\\u0652\\\\u0670أ-ي])';\n\nconst RUMUZ_FOUR = '(?<![\\\\u0660-\\\\u0669])٤(?![\\\\u0660-\\\\u0669])';\n\n// IMPORTANT: order matters. Put longer/more specific codes before shorter ones.\nconst RUMUZ_ATOMS: string[] = [\n // Multi-letter word codes (must NOT be followed by diacritics or letters)\n 'تمييز(?![\\\\u064B-\\\\u0652\\\\u0670أ-ي])',\n // 2-letter codes\n 'خت',\n 'خغ',\n 'بخ',\n 'عخ',\n 'مق',\n 'مت',\n 'عس',\n 'سي',\n 'سن',\n 'كن',\n 'مد',\n 'قد',\n 'خد',\n 'فد',\n 'دل',\n 'كد',\n 'غد',\n 'صد',\n 'دت',\n 'دس',\n 'تم',\n 'فق',\n 'دق',\n // Single-letter codes (character class) + special digit atom\n RUMUZ_SINGLE_LETTER,\n RUMUZ_FOUR,\n];\n\nconst RUMUZ_ATOM = `(?:${RUMUZ_ATOMS.join('|')})`;\n\nconst RUMUZ_BLOCK = `${RUMUZ_ATOM}(?:\\\\s+${RUMUZ_ATOM})*`;\n\nconst BASE_TOKENS = {\n /** Chapter marker (باب). */\n bab: 'باب',\n\n /** Basmala (بسم الله). Also matches ﷽. */\n basmalah: ['بسم الله', '﷽'].join('|'),\n\n /** Bullet point variants: `•`, `*`, `°`. */\n bullet: '[•*°]',\n\n /** Dash variants: `-` (U+002D), `–` (U+2013), `—` (U+2014), `ـ` (tatweel U+0640). */\n dash: '[-–—ـ]',\n\n /** Section marker (فصل / مسألة). */\n fasl: ['مسألة', 'فصل'].join('|'),\n\n /** Single Arabic letter (أ-ي). Does NOT include diacritics. */\n harf: '[أ-ي]',\n\n /** One or more single Arabic letters separated by spaces, allowing marks/tatweel on each isolated letter (e.g. `د ت س`, `هـ ث`). For multi-letter codes use `{{rumuz}}`. */\n harfs: `${ARABIC_SPACED_CODE_ATOM}(?:\\\\s+${ARABIC_SPACED_CODE_ATOM})*`,\n\n /** Horizontal rule / separator: 5+ repeated dashes, underscores, equals, or tatweels. Mixed allowed. */\n hr: '[-–—ـ_=]{5,}',\n\n /** Book marker (كتاب). */\n kitab: 'كتاب',\n\n /** Hadith transmission phrases (حدثنا, أخبرنا, حدثني, etc.). */\n naql: ['حدثني', 'وأخبرنا', 'حدثنا', 'سمعت', 'أنبأنا', 'وحدثنا', 'أخبرنا', 'وحدثني', 'وحدثنيه'].join('|'),\n\n /** Newline character. Useful for breakpoints that split on line boundaries. */\n newline: '\\\\n',\n\n /** Single ASCII digit (0-9). */\n num: '\\\\d',\n\n /** One or more ASCII digits (0-9)+. */\n nums: '\\\\d+',\n\n /** Single Arabic-Indic digit (٠-٩, U+0660-U+0669). */\n raqm: '[\\\\u0660-\\\\u0669]',\n\n /** One or more Arabic-Indic digits (٠-٩)+. */\n raqms: '[\\\\u0660-\\\\u0669]+',\n\n /** Rijāl/takhrīj source abbreviations. Matches one or more codes separated by whitespace. */\n rumuz: RUMUZ_BLOCK,\n\n /** Arabic/common punctuation: `.`, `!`, `?`, `؟`, `؛`. */\n tarqim: '[.!?؟؛]',\n} as const satisfies Record<string, string>;\n\n/** Pre-defined token constants for use in patterns. */\nexport const Token = {\n /** Chapter marker - باب */\n BAB: '{{bab}}',\n /** Basmala - بسم الله */\n BASMALAH: '{{basmalah}}',\n /** Bullet point variants */\n BULLET: '{{bullet}}',\n /** Dash variants (hyphen, en-dash, em-dash, tatweel) */\n DASH: '{{dash}}',\n /** Section marker - فصل / مسألة */\n FASL: '{{fasl}}',\n /** Single Arabic letter */\n HARF: '{{harf}}',\n /** Multiple Arabic letters separated by spaces, allowing marks/tatweel on each isolated letter */\n HARFS: '{{harfs}}',\n /** Horizontal rule / separator (repeated dashes) */\n HR: '{{hr}}',\n /** Book marker - كتاب */\n KITAB: '{{kitab}}',\n /** Hadith transmission phrases */\n NAQL: '{{naql}}',\n /** Newline character (for breakpoints) */\n NEWLINE: '{{newline}}',\n /** Single ASCII digit */\n NUM: '{{num}}',\n /** Composite: {{raqms}} {{dash}} (space) */\n NUMBERED: '{{numbered}}',\n /** One or more ASCII digits */\n NUMS: '{{nums}}',\n /** Single Arabic-Indic digit */\n RAQM: '{{raqm}}',\n /** One or more Arabic-Indic digits */\n RAQMS: '{{raqms}}',\n /** Source abbreviations (rijāl/takhrīj) */\n RUMUZ: '{{rumuz}}',\n /** Punctuation marks */\n TARQIM: '{{tarqim}}',\n} as const;\n\n/**\n * Type representing valid token constant keys.\n */\nexport type TokenKey = keyof typeof Token;\n\n/**\n * Type representing valid token pattern names for `getTokenPattern()`.\n */\nexport type TokenPatternName = keyof typeof TOKEN_PATTERNS;\n\n/** Wraps a token constant with a named capture: `{{token}}` → `{{token:name}}`. */\nexport const withCapture = (token: string, name: string): string => {\n // Extract token name from {{token}} format\n const match = token.match(/^\\{\\{(\\w+)\\}\\}$/);\n if (!match) {\n // If not a valid token format, return capture-only syntax\n return `{{:${name}}}`;\n }\n return `{{${match[1]}:${name}}}`;\n};\n\n/** Composite tokens that reference base tokens. Pre-expanded at load time. @internal */\nconst COMPOSITE_TOKENS = {\n /** Common hadith numbering format: Arabic-Indic digits + dash + space. */\n numbered: '{{raqms}} {{dash}} ',\n} as const satisfies Record<string, string>;\n\n/** Expands composite tokens (e.g. `{{numbered}}`) to their underlying template form. */\nexport const expandCompositeTokensInTemplate = (template: string) => {\n let out = template;\n for (let i = 0; i < 10; i++) {\n const next = out.replace(/\\{\\{(\\w+)\\}\\}/g, (m, tokenName: string) => COMPOSITE_TOKENS[tokenName] ?? m);\n if (next === out) {\n break;\n }\n out = next;\n }\n return out;\n};\n\n/**\n * Expands base tokens in a template string.\n * Used internally to pre-expand composite tokens.\n *\n * @param template - Template string with `{{token}}` placeholders\n * @returns Expanded pattern with base tokens replaced\n * @internal\n */\nconst expandBaseTokens = (template: string) =>\n template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, tokenName) => BASE_TOKENS[tokenName] ?? `{{${tokenName}}}`);\n\n/**\n * Token definitions mapping human-readable token names to regex patterns.\n *\n * Tokens are used in template strings with double-brace syntax:\n * - `{{token}}` - Expands to the pattern (non-capturing in context)\n * - `{{token:name}}` - Expands to a named capture group `(?<name>pattern)`\n * - `{{:name}}` - Captures any content with the given name `(?<name>.+)`\n *\n * @remarks\n * These patterns are designed for Arabic text matching. For diacritic-insensitive\n * matching of Arabic patterns, use the `fuzzy: true` option in split rules,\n * which applies `makeDiacriticInsensitive()` to the expanded patterns.\n *\n * @example\n * // Using tokens in a split rule\n * { lineStartsWith: ['{{kitab}}', '{{bab}}'], split: 'at', fuzzy: true }\n *\n * @example\n * // Using tokens with named captures\n * { lineStartsAfter: ['{{raqms:hadithNum}} {{dash}} '], split: 'at' }\n *\n * @example\n * // Using the numbered convenience token\n * { lineStartsAfter: ['{{numbered}}'], split: 'at' }\n */\nexport const TOKEN_PATTERNS = {\n ...BASE_TOKENS,\n // Pre-expand composite tokens at module load time\n ...Object.fromEntries(Object.entries(COMPOSITE_TOKENS).map(([k, v]) => [k, expandBaseTokens(v)])),\n} as const satisfies Record<string, string>;\n\n/**\n * Regex pattern for matching tokens with optional named capture syntax.\n *\n * Matches:\n * - `{{token}}` - Simple token (group 1 = token name, group 2 = empty)\n * - `{{token:name}}` - Token with capture (group 1 = token, group 2 = name)\n * - `{{:name}}` - Capture-only (group 1 = empty, group 2 = name)\n *\n * @internal\n */\nconst TOKEN_WITH_CAPTURE_REGEX = /\\{\\{(\\w*):?(\\w*)\\}\\}/g;\n\n/**\n * Regex pattern for simple token matching (no capture syntax).\n *\n * Matches only `{{token}}` format where token is one or more word characters.\n * Used by `containsTokens()` for quick detection.\n *\n * @internal\n */\nconst SIMPLE_TOKEN_REGEX = /\\{\\{(\\w+)\\}\\}/g;\n\n/**\n * Checks if a query string contains template tokens.\n *\n * Performs a quick test for `{{token}}` patterns without actually\n * expanding them. Useful for determining whether to apply token\n * expansion to a string.\n *\n * @param query - String to check for tokens\n * @returns `true` if the string contains at least one `{{token}}` pattern\n *\n * @example\n * containsTokens('{{raqms}} {{dash}}') // → true\n * containsTokens('plain text') // → false\n * containsTokens('[٠-٩]+ - ') // → false (raw regex, no tokens)\n */\nexport const containsTokens = (query: string) => {\n SIMPLE_TOKEN_REGEX.lastIndex = 0;\n return SIMPLE_TOKEN_REGEX.test(query);\n};\n\n/**\n * Result from expanding tokens with capture information.\n *\n * Contains the expanded pattern string along with metadata about\n * any named capture groups that were created.\n */\nexport type ExpandResult = {\n /**\n * The expanded regex pattern string with all tokens replaced.\n *\n * Named captures use the `(?<name>pattern)` syntax.\n */\n pattern: string;\n\n /**\n * Names of captured groups extracted from `{{token:name}}` syntax.\n *\n * Empty array if no named captures were found.\n */\n captureNames: string[];\n\n /**\n * Whether the pattern has any named capturing groups.\n *\n * Equivalent to `captureNames.length > 0`.\n */\n hasCaptures: boolean;\n};\n\ntype TemplateSegment = { type: 'token' | 'text'; value: string };\n\nconst splitTemplateIntoSegments = (query: string) => {\n const segments: TemplateSegment[] = [];\n let lastIndex = 0;\n TOKEN_WITH_CAPTURE_REGEX.lastIndex = 0;\n\n for (const match of query.matchAll(TOKEN_WITH_CAPTURE_REGEX)) {\n if (match.index! > lastIndex) {\n segments.push({ type: 'text', value: query.slice(lastIndex, match.index) });\n }\n segments.push({ type: 'token', value: match[0] });\n lastIndex = match.index! + match[0].length;\n }\n\n if (lastIndex < query.length) {\n segments.push({ type: 'text', value: query.slice(lastIndex) });\n }\n return segments;\n};\n\nconst maybeApplyFuzzyToText = (text: string, fuzzyTransform?: (pattern: string) => string) =>\n fuzzyTransform && /[\\u0600-\\u06FF]/u.test(text) ? fuzzyTransform(text) : text;\n\n// NOTE: This intentionally preserves the previous behavior:\n// it applies fuzzy per `|`-separated alternative (best-effort) to avoid corrupting regex metacharacters.\nconst maybeApplyFuzzyToTokenPattern = (tokenPattern: string, fuzzyTransform?: (pattern: string) => string) =>\n !fuzzyTransform\n ? tokenPattern\n : tokenPattern\n .split('|')\n .map((part) => (/[\\u0600-\\u06FF]/u.test(part) ? fuzzyTransform(part) : part))\n .join('|');\n\nconst parseTokenLiteral = (literal: string) => {\n TOKEN_WITH_CAPTURE_REGEX.lastIndex = 0;\n const m = TOKEN_WITH_CAPTURE_REGEX.exec(literal);\n return m ? { captureName: m[2], tokenName: m[1] } : null;\n};\n\nconst createCaptureRegistry = (capturePrefix?: string) => {\n const captureNames: string[] = [];\n const captureNameCounts = new Map<string, number>();\n\n const register = (baseName: string) => {\n const count = captureNameCounts.get(baseName) ?? 0;\n captureNameCounts.set(baseName, count + 1);\n const uniqueName = count === 0 ? baseName : `${baseName}_${count + 1}`;\n const prefixedName = capturePrefix ? `${capturePrefix}${uniqueName}` : uniqueName;\n captureNames.push(prefixedName);\n return prefixedName;\n };\n\n return { captureNames, register };\n};\n\nconst expandTokenLiteral = (\n literal: string,\n opts: {\n fuzzyTransform?: (pattern: string) => string;\n registerCapture: (baseName: string) => string;\n capturePrefix?: string;\n },\n) => {\n const parsed = parseTokenLiteral(literal);\n if (!parsed) {\n return literal;\n }\n\n const { tokenName, captureName } = parsed;\n if (!tokenName && captureName) {\n return `(?<${opts.registerCapture(captureName)}>.+)`;\n }\n\n let tokenPattern = TOKEN_PATTERNS[tokenName];\n if (!tokenPattern) {\n return literal;\n }\n\n tokenPattern = maybeApplyFuzzyToTokenPattern(tokenPattern, opts.fuzzyTransform);\n if (captureName) {\n return `(?<${opts.registerCapture(captureName)}>${tokenPattern})`;\n }\n\n return tokenPattern;\n};\n\n/**\n * Expands template tokens with support for named captures.\n *\n * This is the primary token expansion function that handles all token syntax:\n * - `{{token}}` → Expands to the token's pattern (no capture group)\n * - `{{token:name}}` → Expands to `(?<name>pattern)` (named capture)\n * - `{{:name}}` → Expands to `(?<name>.+)` (capture anything)\n *\n * Unknown tokens are left as-is in the output, allowing for partial templates.\n *\n * @param query - The template string containing tokens\n * @param fuzzyTransform - Optional function to transform Arabic text for fuzzy matching.\n * Applied to both token patterns and plain Arabic text between tokens.\n * Typically `makeDiacriticInsensitive` from the fuzzy module.\n * @returns Object with expanded pattern, capture names, and capture flag\n *\n * @example\n * // Simple token expansion\n * expandTokensWithCaptures('{{raqms}} {{dash}}')\n * // → { pattern: '[\\\\u0660-\\\\u0669]+ [-–—ـ]', captureNames: [], hasCaptures: false }\n *\n * @example\n * // Named capture\n * expandTokensWithCaptures('{{raqms:num}} {{dash}}')\n * // → { pattern: '(?<num>[\\\\u0660-\\\\u0669]+) [-–—ـ]', captureNames: ['num'], hasCaptures: true }\n *\n * @example\n * // Capture-only token\n * expandTokensWithCaptures('{{raqms:num}} {{dash}} {{:content}}')\n * // → { pattern: '(?<num>[٠-٩]+) [-–—ـ] (?<content>.+)', captureNames: ['num', 'content'], hasCaptures: true }\n *\n * @example\n * // With fuzzy transform\n * expandTokensWithCaptures('{{bab}}', makeDiacriticInsensitive)\n * // → { pattern: 'بَ?ا?بٌ?', captureNames: [], hasCaptures: false }\n */\nexport const expandTokensWithCaptures = (\n query: string,\n fuzzyTransform?: (pattern: string) => string,\n capturePrefix?: string,\n) => {\n const segments = splitTemplateIntoSegments(query);\n const registry = createCaptureRegistry(capturePrefix);\n\n const pattern = segments\n .map((segment) =>\n segment.type === 'text'\n ? maybeApplyFuzzyToText(segment.value, fuzzyTransform)\n : expandTokenLiteral(segment.value, {\n capturePrefix,\n fuzzyTransform,\n registerCapture: registry.register,\n }),\n )\n .join('');\n\n return {\n captureNames: registry.captureNames,\n hasCaptures: registry.captureNames.length > 0,\n pattern,\n };\n};\n\n/**\n * Expands template tokens in a query string to their regex equivalents.\n *\n * This is the simple version without capture support. It returns only the\n * expanded pattern string, not capture metadata.\n *\n * Unknown tokens are left as-is, allowing for partial templates.\n *\n * @param query - Template string containing `{{token}}` placeholders\n * @returns Expanded regex pattern string\n *\n * @example\n * expandTokens('، {{raqms}}') // → '، [\\\\u0660-\\\\u0669]+'\n * expandTokens('{{raqm}}*') // → '[\\\\u0660-\\\\u0669]*'\n * expandTokens('{{dash}}{{raqm}}') // → '[-–—ـ][\\\\u0660-\\\\u0669]'\n * expandTokens('{{unknown}}') // → '{{unknown}}' (left as-is)\n *\n * @see expandTokensWithCaptures for full capture group support\n */\nexport const expandTokens = (query: string) => expandTokensWithCaptures(query).pattern;\n\n/**\n * Converts a template string to a compiled RegExp.\n *\n * Expands all tokens and attempts to compile the result as a RegExp\n * with Unicode flag. Returns `null` if the resulting pattern is invalid.\n *\n * @remarks\n * This function dynamically compiles regular expressions from template strings.\n * If templates may come from untrusted sources, be aware of potential ReDoS\n * (Regular Expression Denial of Service) risks due to catastrophic backtracking.\n * Consider validating pattern complexity or applying execution timeouts when\n * running user-submitted patterns.\n *\n * @param template - Template string containing `{{token}}` placeholders\n * @returns Compiled RegExp with 'u' flag, or `null` if invalid\n *\n * @example\n * templateToRegex('، {{raqms}}') // → /، [٠-٩]+/u\n * templateToRegex('{{raqms}}+') // → /[٠-٩]++/u (might be invalid in some engines)\n * templateToRegex('(((') // → null (invalid regex)\n */\nexport const templateToRegex = (template: string) => {\n const expanded = expandTokens(template);\n try {\n return new RegExp(expanded, 'u');\n } catch {\n return null;\n }\n};\n\n/**\n * Lists all available token names defined in `TOKEN_PATTERNS`.\n *\n * Useful for documentation, validation, or building user interfaces\n * that show available tokens.\n *\n * @returns Array of token names (e.g., `['bab', 'basmalah', 'bullet', ...]`)\n *\n * @example\n * getAvailableTokens()\n * // → ['bab', 'basmalah', 'bullet', 'dash', 'harf', 'kitab', 'naql', 'raqm', 'raqms']\n */\nexport const getAvailableTokens = (): TokenPatternName[] => Object.keys(TOKEN_PATTERNS) as TokenPatternName[];\n\n/**\n * Gets the regex pattern for a specific token name.\n *\n * Returns the raw pattern string as defined in `TOKEN_PATTERNS`,\n * without any expansion or capture group wrapping.\n *\n * @param tokenName - The token name to look up (e.g., `'raqms'`, `'dash'`, `'harfs'`)\n * @returns The regex pattern string for that known token\n *\n * @example\n * getTokenPattern('raqms') // → '[\\\\u0660-\\\\u0669]+'\n * getTokenPattern('dash') // → '[-–—ـ]'\n * getTokenPattern('harfs') // → pattern for spaced isolated Arabic letter codes\n */\nexport const getTokenPattern = (tokenName: TokenPatternName) => TOKEN_PATTERNS[tokenName];\n\n/**\n * Tokens that should default to fuzzy matching when used in rules.\n *\n * These are Arabic phrase tokens where diacritic-insensitive matching\n * is almost always desired. Users can still override with `fuzzy: false`.\n */\nconst FUZZY_DEFAULT_TOKENS: (keyof typeof BASE_TOKENS)[] = ['bab', 'basmalah', 'fasl', 'kitab', 'naql'];\n\n/**\n * Regex to detect fuzzy-default tokens in a pattern string.\n * Matches {{token}} or {{token:name}} syntax.\n */\nconst FUZZY_TOKEN_REGEX = new RegExp(`\\\\{\\\\{(?:${FUZZY_DEFAULT_TOKENS.join('|')})(?::\\\\w+)?\\\\}\\\\}`, 'g');\n\n/**\n * Checks if a pattern (or array of patterns) contains tokens that should\n * default to fuzzy matching.\n *\n * Fuzzy-default tokens are: bab, basmalah, fasl, kitab, naql\n *\n * @param patterns - Single pattern string or array of pattern strings\n * @returns `true` if any pattern contains a fuzzy-default token\n *\n * @example\n * shouldDefaultToFuzzy('{{bab}} الإيمان') // true\n * shouldDefaultToFuzzy('{{raqms}} {{dash}}') // false\n * shouldDefaultToFuzzy(['{{kitab}}', '{{raqms}}']) // true\n */\nexport const shouldDefaultToFuzzy = (patterns: string | string[]) => {\n const arr = Array.isArray(patterns) ? patterns : [patterns];\n return arr.some((p) => {\n FUZZY_TOKEN_REGEX.lastIndex = 0;\n return FUZZY_TOKEN_REGEX.test(p);\n });\n};\n\n/**\n * Structure for mapping a token to a capture name.\n */\nexport type TokenMapping = { token: string; name: string };\n\n/**\n * Apply token mappings to a template string.\n *\n * Transforms `{{token}}` into `{{token:name}}` based on the provided mappings.\n * Useful for applying user-configured capture names to a raw template.\n *\n * - Only affects exact matches of `{{token}}`.\n * - Does NOT affect tokens that already have a capture name (e.g. `{{token:existing}}`).\n * - Does NOT affect capture-only tokens (e.g. `{{:name}}`).\n *\n * @param template - The template string to transform\n * @param mappings - Array of mappings from token name to capture name\n * @returns Transformed template string with captures applied\n *\n * @example\n * applyTokenMappings('{{raqms}} {{dash}}', [{ token: 'raqms', name: 'num' }])\n * // → '{{raqms:num}} {{dash}}'\n */\nexport const applyTokenMappings = (template: string, mappings: TokenMapping[]): string => {\n let result = template;\n for (const { token, name } of mappings) {\n if (!token || !name) {\n continue;\n }\n // Match {{token}} but ensure it doesn't already have a suffix like :name\n // We use a regex dealing with the brace syntax\n const regex = new RegExp(`\\\\{\\\\{${token}\\\\}\\\\}`, 'g');\n result = result.replace(regex, `{{${token}:${name}}}`);\n }\n return result;\n};\n\n/**\n * Strip token mappings from a template string.\n *\n * Transforms `{{token:name}}` back into `{{token}}`.\n * Also transforms `{{:name}}` patterns (capture-only) into `{{}}` (which is invalid/empty).\n *\n * Useful for normalizing templates for storage or comparison.\n *\n * @param template - The template string to strip\n * @returns Template string with capture names removed\n *\n * @example\n * stripTokenMappings('{{raqms:num}} {{dash}}')\n * // → '{{raqms}} {{dash}}'\n */\nexport const stripTokenMappings = (template: string) => {\n // Match {{token:name}} and replace with {{token}}\n return template.replace(/\\{\\{([^:}]+):[^}]+\\}\\}/g, '{{$1}}');\n};\n","import { ARABIC_MARKS_CLASS } from '@/segmentation/tokens.js';\n\n/**\n * Normalizes line endings to Unix-style (`\\n`).\n *\n * Converts Windows (`\\r\\n`) and old Mac (`\\r`) line endings to Unix style\n * for consistent pattern matching across platforms.\n *\n * @param content - Raw content with potentially mixed line endings\n * @returns Content with all line endings normalized to `\\n`\n */\n// OPTIMIZATION: Fast-path when no \\r present (common case for Unix/Mac content)\nexport const normalizeLineEndings = (content: string) => {\n return content.includes('\\r') ? content.replace(/\\r\\n?/g, '\\n') : content;\n};\n\n/**\n * Escapes regex metacharacters (parentheses and brackets) in template patterns,\n * but preserves content inside `{{...}}` token delimiters.\n *\n * This allows users to write intuitive patterns like `({{harf}}):` instead of\n * the verbose `\\\\({{harf}}\\\\):`. The escaping is applied BEFORE token expansion,\n * so tokens like `{{harf}}` which expand to `[أ-ي]` work correctly.\n *\n * @param pattern - Template pattern that may contain `()[]` and `{{tokens}}`\n * @returns Pattern with `()[]` escaped outside of `{{...}}` delimiters\n *\n * @example\n * escapeTemplateBrackets('({{harf}}): ')\n * // → '\\\\({{harf}}\\\\): '\n *\n * @example\n * escapeTemplateBrackets('[{{raqm}}] ')\n * // → '\\\\[{{raqm}}\\\\] '\n *\n * @example\n * escapeTemplateBrackets('{{harf}}')\n * // → '{{harf}}' (unchanged - no brackets outside tokens)\n */\nexport const escapeTemplateBrackets = (pattern: string) => {\n return pattern.replace(/(\\{\\{[^}]*\\}\\})|([()[\\]])/g, (_match, token, bracket) => token || `\\\\${bracket}`);\n};\n\n/**\n * Character class matching all Arabic diacritics (Tashkeel/Harakat).\n *\n * Includes the following diacritical marks:\n * - U+0640: ـ (tatweel / kashida)\n * - U+064B: ً (fathatan - double fatha)\n * - U+064C: ٌ (dammatan - double damma)\n * - U+064D: ٍ (kasratan - double kasra)\n * - U+064E: َ (fatha - short a)\n * - U+064F: ُ (damma - short u)\n * - U+0650: ِ (kasra - short i)\n * - U+0651: ّ (shadda - gemination)\n * - U+0652: ْ (sukun - no vowel)\n *\n * @internal\n */\nconst DIACRITICS_CLASS = '[\\u0640\\u064B\\u064C\\u064D\\u064E\\u064F\\u0650\\u0651\\u0652]';\n\n/**\n * Groups of equivalent Arabic characters.\n *\n * Characters within the same group are considered equivalent for matching purposes.\n * This handles common variations in Arabic text where different characters are\n * used interchangeably or have the same underlying meaning.\n *\n * Equivalence groups:\n * - Alef variants: ا (bare), آ (with madda), أ (with hamza above), إ (with hamza below)\n * - Ta marbuta and Ha: ة ↔ ه (often interchangeable at word endings)\n * - Alef maqsura and Ya: ى ↔ ي (often interchangeable at word endings)\n *\n * @internal\n */\nconst EQUIV_GROUPS = [\n ['\\u0627', '\\u0622', '\\u0623', '\\u0625'], // ا, آ, أ, إ\n ['\\u0629', '\\u0647'], // ة <-> ه\n ['\\u0649', '\\u064A'], // ى <-> ي\n];\n\nconst DIACRITICS_AND_MARKS_REGEX = new RegExp(ARABIC_MARKS_CLASS, 'g');\n\n/**\n * Escapes a string for safe inclusion in a regular expression.\n *\n * Escapes all regex metacharacters: `.*+?^${}()|[\\]\\\\`\n *\n * @param s - Any string to escape\n * @returns String with regex metacharacters escaped\n *\n * @example\n * escapeRegex('hello.world') // → 'hello\\\\.world'\n * escapeRegex('[test]') // → '\\\\[test\\\\]'\n * escapeRegex('a+b*c?') // → 'a\\\\+b\\\\*c\\\\?'\n */\nexport const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nconst getEquivClass = (ch: string) => {\n const group = EQUIV_GROUPS.find((g) => g.includes(ch));\n return group ? `[${group.map(escapeRegex).join('')}]` : escapeRegex(ch);\n};\n\nconst normalizeArabicLight = (str: string) => {\n return str\n .normalize('NFC')\n .replace(/[\\u200C\\u200D]/g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\n/**\n * Normalizes Arabic text for exact comparisons while tolerating common variants.\n *\n * This removes Arabic diacritics, collapses whitespace, removes joiners, and\n * maps common equivalent letters to a shared canonical form:\n * - ا/آ/أ/إ -> ا\n * - ة/ه -> ه\n * - ى/ي -> ي\n */\nexport const normalizeArabicForComparison = (text: string) => {\n return Array.from(normalizeArabicLight(text).replace(DIACRITICS_AND_MARKS_REGEX, ''))\n .map((ch) => {\n if (ch === '\\u0622' || ch === '\\u0623' || ch === '\\u0625') {\n return '\\u0627';\n }\n if (ch === '\\u0629') {\n return '\\u0647';\n }\n if (ch === '\\u0649') {\n return '\\u064A';\n }\n return ch;\n })\n .join('');\n};\n\nexport const makeDiacriticInsensitive = (text: string) => {\n const diacriticsMatcher = `${DIACRITICS_CLASS}*`;\n return Array.from(normalizeArabicLight(text))\n .map((ch) => getEquivClass(ch) + diacriticsMatcher)\n .join('');\n};\n\nconst isCombiningMarkOrSelector = (char: string | undefined) => {\n if (!char) {\n return false;\n }\n // \\p{M} = Unicode combining mark category (includes Arabic harakat)\n // FE0E/FE0F = variation selectors\n return /\\p{M}/u.test(char) || char === '\\uFE0E' || char === '\\uFE0F';\n};\n\nconst isJoiner = (char: string | undefined) => char === '\\u200C' || char === '\\u200D';\n\n/**\n * Ensures the position does not split a grapheme cluster (surrogate pairs,\n * combining marks, or zero-width joiners / variation selectors).\n *\n * This is only used as a last-resort fallback when we are forced to split\n * near a hard limit (e.g. maxContentLength with no safe whitespace/punctuation).\n */\nexport const adjustForUnicodeBoundary = (content: string, position: number) => {\n let adjusted = position;\n\n while (adjusted > 0) {\n // 1. Ensure we don't split a surrogate pair\n // (High surrogate at adjusted-1, Low surrogate at adjusted)\n const high = content.charCodeAt(adjusted - 1);\n const low = content.charCodeAt(adjusted);\n if (high >= 0xd800 && high <= 0xdbff && low >= 0xdc00 && low <= 0xdfff) {\n adjusted -= 1;\n continue;\n }\n\n const nextChar = content[adjusted];\n const prevChar = content[adjusted - 1];\n // 2. If we'd start the next segment with a combining mark / selector / joiner, back up.\n // For joiners, also avoid ending the previous segment with a joiner.\n // (Splitting AFTER combining marks / selectors is safe; splitting before them is not.)\n if (isCombiningMarkOrSelector(nextChar) || isJoiner(nextChar) || isJoiner(prevChar)) {\n adjusted -= 1;\n continue;\n }\n\n break;\n }\n return adjusted;\n};\n","// Shared utilities for analysis functions\n\nimport { getAvailableTokens, TOKEN_PATTERNS } from '../segmentation/tokens.js';\n\n// Helpers shared across analysis modules\n\n// For analysis signatures we avoid escaping ()[] because:\n// - These are commonly used literally in texts (e.g., \"(ح)\")\n// - When signatures are later used in template patterns, ()[] are auto-escaped there\n// We still escape other regex metacharacters to keep signatures safe if reused as templates.\nexport const escapeSignatureLiteral = (s: string): string => s.replace(/[.*+?^${}|\\\\{}]/g, '\\\\$&');\n\n// Keep this intentionally focused on \"useful at line start\" tokens, avoiding overly-generic tokens like {{harf}}.\nexport const TOKEN_PRIORITY_ORDER: string[] = [\n 'basmalah',\n 'kitab',\n 'bab',\n 'fasl',\n 'naql',\n 'rumuz',\n 'numbered',\n 'raqms',\n 'raqm',\n 'dash',\n 'bullet',\n 'tarqim',\n];\n\nexport const buildTokenPriority = (): string[] => {\n const allTokens = new Set(getAvailableTokens());\n // IMPORTANT: We only use an explicit allow-list here.\n // Including \"all remaining tokens\" adds overly-generic tokens (e.g., harf) which makes signatures noisy.\n return TOKEN_PRIORITY_ORDER.filter((t) => allTokens.has(t));\n};\n\nexport const collapseWhitespace = (s: string): string => s.replace(/\\s+/g, ' ').trim();\n\n// Arabic diacritics / tashkeel marks that commonly appear in Shamela texts.\n// This is intentionally conservative: remove combining marks but keep letters.\n// NOTE: Tatweel (U+0640) is NOT stripped here because it's used semantically as a dash/separator.\nexport const stripArabicDiacritics = (s: string): string =>\n // harakat + common Quranic marks (no tatweel - it's used as a dash)\n s.replace(/[\\u064B-\\u065F\\u0670\\u06D6-\\u06ED]/gu, '');\n\nexport type CompiledTokenRegex = { token: string; re: RegExp };\n\nexport const compileTokenRegexes = (tokenNames: string[]): CompiledTokenRegex[] =>\n tokenNames\n .map((token) => {\n const pat = TOKEN_PATTERNS[token];\n if (!pat) {\n return null;\n }\n try {\n return { re: new RegExp(pat, 'uy'), token };\n } catch {\n return null;\n }\n })\n .filter((x): x is CompiledTokenRegex => x !== null);\n\nexport const appendWs = (out: string, mode: 'regex' | 'space'): string => {\n if (!out) {\n return out;\n }\n const suffix = mode === 'space' ? ' ' : '\\\\s*';\n return out.endsWith(suffix) ? out : `${out}${suffix}`;\n};\n\nexport const findBestTokenMatchAt = (\n s: string,\n pos: number,\n compiled: CompiledTokenRegex[],\n isArabicLetter: (ch: string) => boolean,\n): { token: string; text: string } | null => {\n let best: { token: string; text: string } | null = null;\n for (const { token, re } of compiled) {\n re.lastIndex = pos;\n const m = re.exec(s);\n if (!m || m.index !== pos) {\n continue;\n }\n if (!best || m[0].length > best.text.length) {\n best = { text: m[0], token };\n }\n }\n\n if (best?.token === 'rumuz') {\n const end = pos + best.text.length;\n const next = end < s.length ? s[end] : '';\n if (next && isArabicLetter(next) && !/\\s/u.test(next)) {\n return null;\n }\n }\n\n return best;\n};\n\n// IMPORTANT: do NOT treat all Arabic-block codepoints as \"letters\" (it includes punctuation like \"،\").\n// We only want to consider actual letters here for the rumuz boundary guard.\nexport const isArabicLetter = (ch: string): boolean => /\\p{Script=Arabic}/u.test(ch) && /\\p{L}/u.test(ch);\nexport const isCommonDelimiter = (ch: string): boolean => /[::\\-–—ـ،؛.?!؟()[\\]{}]/u.test(ch);\n","// Line-starts analysis module\n\nimport type { Page } from '@/types/index.js';\nimport { normalizeLineEndings } from '@/utils/textUtils.js';\nimport {\n appendWs,\n buildTokenPriority,\n type CompiledTokenRegex,\n collapseWhitespace,\n compileTokenRegexes,\n escapeSignatureLiteral,\n findBestTokenMatchAt,\n isArabicLetter,\n isCommonDelimiter,\n stripArabicDiacritics,\n} from './shared.js';\n\n// Types\n\nexport type LineStartAnalysisOptions = {\n topK?: number;\n prefixChars?: number;\n minLineLength?: number;\n minCount?: number;\n maxExamples?: number;\n includeFirstWordFallback?: boolean;\n normalizeArabicDiacritics?: boolean;\n sortBy?: 'specificity' | 'count';\n lineFilter?: (line: string, pageId: number) => boolean;\n prefixMatchers?: RegExp[];\n whitespace?: 'regex' | 'space';\n};\n\nexport type LineStartPatternExample = { line: string; pageId: number };\n\nexport type CommonLineStartPattern = {\n pattern: string;\n count: number;\n examples: LineStartPatternExample[];\n};\n\n// Options resolution\n\ntype ResolvedOptions = Required<Omit<LineStartAnalysisOptions, 'lineFilter'>> & {\n lineFilter?: LineStartAnalysisOptions['lineFilter'];\n};\n\nconst resolveOptions = (options: LineStartAnalysisOptions = {}): ResolvedOptions => ({\n includeFirstWordFallback: options.includeFirstWordFallback ?? true,\n lineFilter: options.lineFilter,\n maxExamples: options.maxExamples ?? 1,\n minCount: options.minCount ?? 3,\n minLineLength: options.minLineLength ?? 6,\n normalizeArabicDiacritics: options.normalizeArabicDiacritics ?? true,\n prefixChars: options.prefixChars ?? 60,\n prefixMatchers: options.prefixMatchers ?? [/^#+/u],\n sortBy: options.sortBy ?? 'specificity',\n topK: options.topK ?? 40,\n whitespace: options.whitespace ?? 'regex',\n});\n\n// Specificity & sorting\n\nconst countTokenMarkers = (pattern: string): number => (pattern.match(/\\{\\{/g) ?? []).length;\n\nconst computeSpecificity = (pattern: string) => ({\n literalLen: pattern.replace(/\\\\s\\*/g, '').replace(/[ \\t]+/g, '').length,\n tokenCount: countTokenMarkers(pattern),\n});\n\nconst compareBySpecificity = (a: CommonLineStartPattern, b: CommonLineStartPattern): number => {\n const sa = computeSpecificity(a.pattern),\n sb = computeSpecificity(b.pattern);\n return (\n sb.tokenCount - sa.tokenCount ||\n sb.literalLen - sa.literalLen ||\n b.count - a.count ||\n a.pattern.localeCompare(b.pattern)\n );\n};\n\nconst compareByCount = (a: CommonLineStartPattern, b: CommonLineStartPattern): number =>\n b.count !== a.count ? b.count - a.count : compareBySpecificity(a, b);\n\nconst appendPrefix = (\n s: string,\n pos: number,\n out: string,\n matchers: RegExp[],\n ws: 'regex' | 'space',\n): { pos: number; out: string; matched: boolean } => {\n for (const re of matchers) {\n if (pos >= s.length) {\n break;\n }\n const m = re.exec(s.slice(pos));\n if (!m?.index && m?.[0]) {\n out += escapeSignatureLiteral(m[0]);\n pos += m[0].length;\n const wsm = /^[ \\t]+/u.exec(s.slice(pos));\n if (wsm) {\n pos += wsm[0].length;\n out = appendWs(out, ws);\n }\n return { matched: true, out, pos };\n }\n }\n return { matched: false, out, pos };\n};\n\nconst appendToken = (\n s: string,\n pos: number,\n out: string,\n compiled: CompiledTokenRegex[],\n): { pos: number; out: string; matched: boolean } => {\n const best = findBestTokenMatchAt(s, pos, compiled, isArabicLetter);\n return best\n ? { matched: true, out: `${out}{{${best.token}}}`, pos: pos + best.text.length }\n : { matched: false, out, pos };\n};\n\nconst appendDelimiter = (s: string, pos: number, out: string): { pos: number; out: string; matched: boolean } => {\n const ch = s[pos];\n return ch && isCommonDelimiter(ch)\n ? { matched: true, out: `${out}${escapeSignatureLiteral(ch)}`, pos: pos + 1 }\n : { matched: false, out, pos };\n};\n\nconst appendFallbackWord = (s: string, pos: number, out: string): string | null => {\n const word = extractFirstWord(s.slice(pos));\n return word ? `${out}${escapeSignatureLiteral(word)}` : null;\n};\n\nconst consumeLineStartStep = (\n s: string,\n pos: number,\n out: string,\n compiled: CompiledTokenRegex[],\n opts: ResolvedOptions,\n matchedAny: boolean,\n matchedToken: boolean,\n): { pos: number; out: string; matchedAny: boolean; matchedToken: boolean; steps: number; done: boolean } => {\n const ws = skipWhitespace(s, pos, out, opts.whitespace);\n if (ws.skipped) {\n return { done: false, matchedAny, matchedToken, out: ws.out, pos: ws.pos, steps: 0 };\n }\n\n const tok = appendToken(s, pos, out, compiled);\n if (tok.matched) {\n return { done: false, matchedAny: true, matchedToken: true, out: tok.out, pos: tok.pos, steps: 1 };\n }\n\n if (matchedAny) {\n const delim = appendDelimiter(s, pos, out);\n if (delim.matched) {\n return { done: false, matchedAny, matchedToken, out: delim.out, pos: delim.pos, steps: 0 };\n }\n\n if (opts.includeFirstWordFallback && !matchedToken) {\n const fallback = appendFallbackWord(s, pos, out);\n if (fallback) {\n return { done: true, matchedAny, matchedToken, out: fallback, pos, steps: 1 };\n }\n }\n\n return { done: true, matchedAny, matchedToken, out, pos, steps: 0 };\n }\n\n if (!opts.includeFirstWordFallback) {\n return { done: true, matchedAny, matchedToken, out, pos, steps: 0 };\n }\n\n const fallback = appendFallbackWord(s, pos, out);\n return fallback\n ? { done: true, matchedAny: true, matchedToken, out: fallback, pos, steps: 0 }\n : { done: true, matchedAny, matchedToken, out, pos, steps: 0 };\n};\n\n// Signature building helpers\n\n/** Remove trailing whitespace placeholders */\nconst trimTrailingWs = (out: string, mode: 'regex' | 'space'): string => {\n const suffix = mode === 'regex' ? '\\\\s*' : ' ';\n while (out.endsWith(suffix)) {\n out = out.slice(0, -suffix.length);\n }\n return out;\n};\n\n/** Try to extract first word for fallback */\nconst extractFirstWord = (s: string): string | null => (s.match(/^[^\\s:،؛.?!؟]+/u) ?? [])[0] ?? null;\n\n/** Skip whitespace at position */\nconst skipWhitespace = (\n s: string,\n pos: number,\n out: string,\n ws: 'regex' | 'space',\n): { pos: number; out: string; skipped: boolean } => {\n const m = /^[ \\t]+/u.exec(s.slice(pos));\n if (!m) {\n return { out, pos, skipped: false };\n }\n return { out: appendWs(out, ws), pos: pos + m[0].length, skipped: true };\n};\n\n// Main tokenization\n\nconst tokenizeLineStart = (line: string, tokenNames: string[], opts: ResolvedOptions): string | null => {\n const trimmed = collapseWhitespace(line);\n if (!trimmed) {\n return null;\n }\n\n const s = (opts.normalizeArabicDiacritics ? stripArabicDiacritics(trimmed) : trimmed).slice(0, opts.prefixChars);\n const compiled = compileTokenRegexes(tokenNames);\n\n let pos = 0,\n out = '',\n matchedAny = false,\n matchedToken = false,\n steps = 0;\n\n // Consume prefixes\n const prefix = appendPrefix(s, pos, out, opts.prefixMatchers, opts.whitespace);\n pos = prefix.pos;\n out = prefix.out;\n matchedAny = prefix.matched;\n\n while (steps < 6 && pos < s.length) {\n const next = consumeLineStartStep(s, pos, out, compiled, opts, matchedAny, matchedToken);\n if (next.done) {\n if (!next.matchedAny && !next.matchedToken && next.out === out && next.pos === pos) {\n return null;\n }\n if (next.steps > 0) {\n steps += next.steps;\n }\n matchedAny = next.matchedAny;\n matchedToken = next.matchedToken;\n out = next.out;\n break;\n }\n\n pos = next.pos;\n out = next.out;\n matchedAny = next.matchedAny;\n matchedToken = next.matchedToken;\n steps += next.steps;\n }\n\n return matchedAny ? trimTrailingWs(out, opts.whitespace) : null;\n};\n\n// Page processing\n\ntype PatternAccumulator = Map<string, { count: number; examples: LineStartPatternExample[] }>;\n\nconst processLine = (\n line: string,\n pageId: number,\n tokenPriority: string[],\n opts: ResolvedOptions,\n acc: PatternAccumulator,\n): void => {\n const trimmed = collapseWhitespace(line);\n if (trimmed.length < opts.minLineLength) {\n return;\n }\n if (opts.lineFilter && !opts.lineFilter(trimmed, pageId)) {\n return;\n }\n\n const sig = tokenizeLineStart(trimmed, tokenPriority, opts);\n if (!sig) {\n return;\n }\n\n const entry = acc.get(sig);\n if (!entry) {\n acc.set(sig, { count: 1, examples: [{ line: trimmed, pageId }] });\n } else {\n entry.count++;\n if (entry.examples.length < opts.maxExamples) {\n entry.examples.push({ line: trimmed, pageId });\n }\n }\n};\n\nconst processPage = (page: Page, tokenPriority: string[], opts: ResolvedOptions, acc: PatternAccumulator): void => {\n for (const line of normalizeLineEndings(page.content ?? '').split('\\n')) {\n processLine(line, page.id, tokenPriority, opts, acc);\n }\n};\n\n// Main export\n\n/**\n * Analyze pages and return the most common line-start patterns (top K).\n */\nexport const analyzeCommonLineStarts = (\n pages: Page[],\n options: LineStartAnalysisOptions = {},\n): CommonLineStartPattern[] => {\n const opts = resolveOptions(options);\n const tokenPriority = buildTokenPriority();\n const acc: PatternAccumulator = new Map();\n\n for (const page of pages) {\n processPage(page, tokenPriority, opts, acc);\n }\n\n const comparator = opts.sortBy === 'count' ? compareByCount : compareBySpecificity;\n\n return [...acc.entries()]\n .map(([pattern, v]) => ({ count: v.count, examples: v.examples, pattern }))\n .filter((p) => p.count >= opts.minCount)\n .sort(comparator)\n .slice(0, opts.topK);\n};\n","// Repeating sequences analysis module\n\nimport type { Page } from '@/types/index.js';\nimport {\n buildTokenPriority,\n compileTokenRegexes,\n escapeSignatureLiteral,\n findBestTokenMatchAt,\n isArabicLetter,\n isCommonDelimiter,\n stripArabicDiacritics,\n} from './shared.js';\n\n// Types\n\nexport type TokenStreamItem = {\n type: 'token' | 'literal';\n /** The represented value (e.g. \"{{naql}}\" or \"hello\") */\n text: string;\n /** The original raw text (e.g. \"حَدَّثَنَا\") */\n raw: string;\n start: number;\n end: number;\n};\n\nexport type RepeatingSequenceOptions = {\n minElements?: number;\n maxElements?: number;\n minCount?: number;\n topK?: number;\n normalizeArabicDiacritics?: boolean;\n requireToken?: boolean;\n whitespace?: 'regex' | 'space';\n maxExamples?: number;\n contextChars?: number;\n maxUniquePatterns?: number;\n};\n\nexport type RepeatingSequenceExample = {\n text: string;\n context: string;\n pageId: number;\n startIndices: number[];\n};\n\nexport type RepeatingSequencePattern = {\n pattern: string;\n count: number;\n examples: RepeatingSequenceExample[];\n};\n\ntype PatternStats = {\n count: number;\n examples: RepeatingSequenceExample[];\n tokenCount: number;\n literalLen: number;\n};\n\n// Resolved options with defaults\n\ntype ResolvedOptions = Required<RepeatingSequenceOptions>;\n\nconst resolveOptions = (options?: RepeatingSequenceOptions): ResolvedOptions => {\n const minElements = Math.max(1, options?.minElements ?? 1);\n return {\n contextChars: options?.contextChars ?? 50,\n maxElements: Math.max(minElements, options?.maxElements ?? 3),\n maxExamples: options?.maxExamples ?? 3,\n maxUniquePatterns: options?.maxUniquePatterns ?? 1000,\n minCount: Math.max(1, options?.minCount ?? 3),\n minElements,\n normalizeArabicDiacritics: options?.normalizeArabicDiacritics ?? true,\n requireToken: options?.requireToken ?? true,\n topK: Math.max(1, options?.topK ?? 20),\n whitespace: options?.whitespace ?? 'regex',\n };\n};\n\n// Raw position tracking for diacritic normalization\n\n/** Creates a cursor that tracks position in both normalized and raw text */\nconst createRawCursor = (text: string, normalize: boolean) => {\n let rawPos = 0;\n\n return {\n /** Advance cursor, returning the raw text chunk consumed */\n advance(normalizedLen: number): string {\n if (!normalize) {\n const chunk = text.slice(rawPos, rawPos + normalizedLen);\n rawPos += normalizedLen;\n return chunk;\n }\n\n const start = rawPos;\n let matchedLen = 0;\n\n // Match normalized characters\n while (matchedLen < normalizedLen && rawPos < text.length) {\n if (stripArabicDiacritics(text[rawPos]).length > 0) {\n matchedLen++;\n }\n rawPos++;\n }\n\n // Consume trailing diacritics (belong to last character)\n while (rawPos < text.length && stripArabicDiacritics(text[rawPos]).length === 0) {\n rawPos++;\n }\n\n return text.slice(start, rawPos);\n },\n get pos() {\n return rawPos;\n },\n };\n};\n\n// Token content scanner\n\n/** Scans text and produces a stream of tokens and literals. */\nexport const tokenizeContent = (text: string, normalize: boolean): TokenStreamItem[] => {\n const normalized = normalize ? stripArabicDiacritics(text) : text;\n const compiled = compileTokenRegexes(buildTokenPriority());\n const cursor = createRawCursor(text, normalize);\n const items: TokenStreamItem[] = [];\n let pos = 0;\n\n while (pos < normalized.length) {\n // Skip whitespace\n const ws = /^\\s+/u.exec(normalized.slice(pos));\n if (ws) {\n pos += ws[0].length;\n cursor.advance(ws[0].length);\n continue;\n }\n\n // Try token\n const token = findBestTokenMatchAt(normalized, pos, compiled, isArabicLetter);\n if (token) {\n const raw = cursor.advance(token.text.length);\n items.push({\n end: cursor.pos,\n raw,\n start: cursor.pos - raw.length,\n text: `{{${token.token}}}`,\n type: 'token',\n });\n pos += token.text.length;\n continue;\n }\n\n // Try delimiter\n if (isCommonDelimiter(normalized[pos])) {\n const raw = cursor.advance(1);\n items.push({\n end: cursor.pos,\n raw,\n start: cursor.pos - 1,\n text: escapeSignatureLiteral(normalized[pos]),\n type: 'literal',\n });\n pos++;\n continue;\n }\n\n // Literal word\n const word = /^[^\\s::\\-–—ـ،؛.?!؟()[\\]{}]+/u.exec(normalized.slice(pos));\n if (word) {\n const raw = cursor.advance(word[0].length);\n items.push({\n end: cursor.pos,\n raw,\n start: cursor.pos - raw.length,\n text: escapeSignatureLiteral(word[0]),\n type: 'literal',\n });\n pos += word[0].length;\n continue;\n }\n\n cursor.advance(1);\n pos++;\n }\n\n return items;\n};\n\n// N-gram pattern extraction\n\n/** Build pattern string from window items */\nconst buildPattern = (window: TokenStreamItem[], whitespace: 'regex' | 'space'): string =>\n window.map((i) => i.text).join(whitespace === 'space' ? ' ' : '\\\\s*');\n\n/** Check if window contains at least one token */\nconst hasTokenInWindow = (window: TokenStreamItem[]): boolean => window.some((i) => i.type === 'token');\n\n/** Compute token count and literal length for a window */\nconst computeWindowStats = (window: TokenStreamItem[]) => {\n let tokenCount = 0,\n literalLen = 0;\n for (const item of window) {\n if (item.type === 'token') {\n tokenCount++;\n } else {\n literalLen += item.text.length;\n }\n }\n return { literalLen, tokenCount };\n};\n\n/** Build example from page content and window */\nconst buildExample = (page: Page, window: TokenStreamItem[], contextChars: number): RepeatingSequenceExample => {\n const start = window[0].start;\n const end = window.at(-1)!.end;\n const ctxStart = Math.max(0, start - contextChars);\n const ctxEnd = Math.min(page.content.length, end + contextChars);\n\n return {\n context:\n (ctxStart > 0 ? '...' : '') +\n page.content.slice(ctxStart, ctxEnd) +\n (ctxEnd < page.content.length ? '...' : ''),\n pageId: page.id,\n startIndices: window.map((w) => w.start),\n text: page.content.slice(start, end),\n };\n};\n\nconst recordPattern = (\n page: Page,\n window: TokenStreamItem[],\n opts: ResolvedOptions,\n stats: Map<string, PatternStats>,\n): void => {\n if (opts.requireToken && !hasTokenInWindow(window)) {\n return;\n }\n\n const pattern = buildPattern(window, opts.whitespace);\n let entry = stats.get(pattern);\n if (!entry) {\n if (stats.size >= opts.maxUniquePatterns) {\n return;\n }\n entry = { count: 0, examples: [], ...computeWindowStats(window) };\n stats.set(pattern, entry);\n }\n\n entry.count++;\n if (entry.examples.length < opts.maxExamples) {\n entry.examples.push(buildExample(page, window, opts.contextChars));\n }\n};\n\n/** Extract N-grams from a single page */\nconst extractPageNgrams = (\n page: Page,\n items: TokenStreamItem[],\n opts: ResolvedOptions,\n stats: Map<string, PatternStats>,\n): void => {\n for (let i = 0; i <= items.length - opts.minElements; i++) {\n const maxWindowSize = Math.min(opts.maxElements, items.length - i);\n for (let n = opts.minElements; n <= maxWindowSize; n++) {\n recordPattern(page, items.slice(i, i + n), opts, stats);\n }\n }\n};\n\n// Main export\n\n/**\n * Analyze pages for commonly repeating word sequences.\n *\n * Use for continuous text without line breaks. For line-based analysis,\n * use `analyzeCommonLineStarts()` instead.\n */\nexport const analyzeRepeatingSequences = (\n pages: Page[],\n options?: RepeatingSequenceOptions,\n): RepeatingSequencePattern[] => {\n const opts = resolveOptions(options);\n const stats = new Map<string, PatternStats>();\n\n for (const page of pages) {\n if (!page.content) {\n continue;\n }\n extractPageNgrams(page, tokenizeContent(page.content, opts.normalizeArabicDiacritics), opts, stats);\n }\n\n return [...stats.entries()]\n .filter(([, s]) => s.count >= opts.minCount)\n .sort(\n (a, b) => b[1].count - a[1].count || b[1].tokenCount - a[1].tokenCount || b[1].literalLen - a[1].literalLen,\n )\n .slice(0, opts.topK)\n .map(([pattern, s]) => ({ count: s.count, examples: s.examples, pattern }));\n};\n","import { getAvailableTokens, TOKEN_PATTERNS } from './segmentation/tokens.js';\n\n/**\n * Result of detecting a token pattern in text\n */\nexport type DetectedPattern = {\n /** Token name from TOKEN_PATTERNS (e.g., 'raqms', 'dash') */\n token: string;\n /** The matched text */\n match: string;\n /** Start index in the original text */\n index: number;\n /** End index (exclusive) */\n endIndex: number;\n};\n\n/**\n * Token detection order - more specific patterns first to avoid partial matches.\n * Example: 'raqms' before 'raqm' so \"٣٤\" matches 'raqms' not just the first digit.\n *\n * Tokens not in this list are appended in alphabetical order from TOKEN_PATTERNS.\n */\nconst TOKEN_PRIORITY_ORDER = [\n 'basmalah', // Most specific - full phrase\n 'kitab',\n 'bab',\n 'fasl',\n 'naql',\n 'rumuz', // Source abbreviations (e.g., \"خت\", \"خ سي\", \"٤\")\n 'numbered', // Composite: raqms + dash\n 'raqms', // Multiple digits before single digit\n 'raqm',\n 'tarqim',\n 'bullet',\n 'dash',\n 'harf',\n];\n\n/**\n * Gets the token detection priority order.\n * Returns tokens in priority order, with any TOKEN_PATTERNS not in the priority list appended.\n */\nconst getTokenPriority = () => {\n const allTokens = getAvailableTokens();\n const prioritized = TOKEN_PRIORITY_ORDER.filter((t) => allTokens.includes(t));\n const remaining = allTokens.filter((t) => !TOKEN_PRIORITY_ORDER.includes(t)).sort();\n return [...prioritized, ...remaining];\n};\n\nconst isRumuzStandalone = (text: string, startIndex: number, endIndex: number): boolean => {\n // We want rumuz to behave like a standalone marker (e.g. \"س:\" or \"خت ٤:\"),\n // not a substring match inside normal Arabic words (e.g. \"إِبْرَاهِيم\").\n const before = startIndex > 0 ? text[startIndex - 1] : '';\n const after = endIndex < text.length ? text[endIndex] : '';\n\n const isWhitespace = (ch: string): boolean => !!ch && /\\s/u.test(ch);\n const isOpenBracket = (ch: string): boolean => !!ch && /[([{]/u.test(ch);\n const isRightDelimiter = (ch: string): boolean => !!ch && /[::\\-–—ـ،؛.?!؟)\\]}]/u.test(ch);\n\n // Treat any Arabic-block codepoint (letters + diacritics + digits) as \"wordy\" context.\n // Unicode Script properties can classify some combining marks as \"Inherited\", so we avoid \\p{Script=Arabic}.\n const isArabicWordy = (ch: string): boolean => !!ch && /[\\u0600-\\u06FF]/u.test(ch);\n\n const leftOk = !before || isWhitespace(before) || isOpenBracket(before) || !isArabicWordy(before);\n const rightOk = !after || isWhitespace(after) || isRightDelimiter(after) || !isArabicWordy(after);\n\n return leftOk && rightOk;\n};\n\n/**\n * Analyzes text and returns all detected token patterns with their positions.\n * Patterns are detected in priority order to avoid partial matches.\n *\n * @param text - The text to analyze for token patterns\n * @returns Array of detected patterns sorted by position\n *\n * @example\n * detectTokenPatterns(\"٣٤ - حدثنا\")\n * // Returns: [\n * // { token: 'raqms', match: '٣٤', index: 0, endIndex: 2 },\n * // { token: 'dash', match: '-', index: 3, endIndex: 4 },\n * // { token: 'naql', match: 'حدثنا', index: 5, endIndex: 10 }\n * // ]\n */\nexport const detectTokenPatterns = (text: string) => {\n if (!text) {\n return [];\n }\n\n const results: DetectedPattern[] = [];\n const coveredRanges: Array<[number, number]> = [];\n\n // Check if a position is already covered by a detected pattern\n const isPositionCovered = (start: number, end: number): boolean => {\n return coveredRanges.some(\n ([s, e]) => (start >= s && start < e) || (end > s && end <= e) || (start <= s && end >= e),\n );\n };\n\n // Process tokens in priority order\n for (const tokenName of getTokenPriority()) {\n const pattern = TOKEN_PATTERNS[tokenName];\n if (!pattern) {\n continue;\n }\n\n try {\n // Create a global regex to find all matches\n const regex = new RegExp(`(${pattern})`, 'gu');\n let match: RegExpExecArray | null;\n\n // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop pattern\n while ((match = regex.exec(text)) !== null) {\n const startIndex = match.index;\n const endIndex = startIndex + match[0].length;\n\n if (tokenName === 'rumuz' && !isRumuzStandalone(text, startIndex, endIndex)) {\n continue;\n }\n\n // Skip if this range overlaps with an already detected pattern\n if (isPositionCovered(startIndex, endIndex)) {\n continue;\n }\n\n results.push({ endIndex, index: startIndex, match: match[0], token: tokenName });\n\n coveredRanges.push([startIndex, endIndex]);\n }\n } catch {}\n }\n\n return results.sort((a, b) => a.index - b.index);\n};\n\n/**\n * Generates a template pattern from text using detected tokens.\n * Replaces matched portions with {{token}} syntax.\n *\n * @param text - Original text\n * @param detected - Array of detected patterns from detectTokenPatterns\n * @returns Template string with tokens, e.g., \"{{raqms}} {{dash}} \"\n *\n * @example\n * const detected = detectTokenPatterns(\"٣٤ - \");\n * generateTemplateFromText(\"٣٤ - \", detected);\n * // Returns: \"{{raqms}} {{dash}} \"\n */\nexport const generateTemplateFromText = (text: string, detected: DetectedPattern[]) => {\n if (!text || detected.length === 0) {\n return text;\n }\n\n // Build template by replacing detected patterns with tokens\n // Process in reverse order to preserve indices\n let template = text;\n const sortedByIndexDesc = [...detected].sort((a, b) => b.index - a.index);\n\n for (const d of sortedByIndexDesc) {\n template = `${template.slice(0, d.index)}{{${d.token}}}${template.slice(d.endIndex)}`;\n }\n\n return template;\n};\n\n/**\n * Determines the best pattern type for auto-generated rules based on detected patterns.\n *\n * @param detected - Array of detected patterns\n * @returns Suggested pattern type and whether to use fuzzy matching\n */\nexport const suggestPatternConfig = (\n detected: DetectedPattern[],\n): { patternType: 'lineStartsWith' | 'lineStartsAfter'; fuzzy: boolean; metaType?: string } => {\n // Check if the detected patterns suggest a structural marker (chapter, book, etc.)\n const hasStructuralToken = detected.some((d) => ['basmalah', 'kitab', 'bab', 'fasl'].includes(d.token));\n\n // Check if the pattern is numbered (hadith-style)\n const hasNumberedPattern = detected.some((d) => ['raqms', 'raqm', 'numbered'].includes(d.token));\n\n // If it starts with a structural token, use lineStartsWith (keep marker in content)\n if (hasStructuralToken) {\n return {\n fuzzy: true,\n metaType: detected.find((d) => ['kitab', 'bab', 'fasl'].includes(d.token))?.token || 'chapter',\n patternType: 'lineStartsWith',\n };\n }\n\n // If it's a numbered pattern (like hadith numbers), use lineStartsAfter (strip prefix)\n if (hasNumberedPattern) {\n return { fuzzy: false, metaType: 'hadith', patternType: 'lineStartsAfter' };\n }\n\n // Default: use lineStartsAfter without fuzzy\n return { fuzzy: false, patternType: 'lineStartsAfter' };\n};\n\n/**\n * Analyzes text and generates a complete suggested rule configuration.\n *\n * @param text - Highlighted text from the page\n * @returns Suggested rule configuration or null if no patterns detected\n */\nexport const analyzeTextForRule = (\n text: string,\n): {\n template: string;\n patternType: 'lineStartsWith' | 'lineStartsAfter';\n fuzzy: boolean;\n metaType?: string;\n detected: DetectedPattern[];\n} | null => {\n const detected = detectTokenPatterns(text);\n\n if (detected.length === 0) {\n return null;\n }\n\n const template = generateTemplateFromText(text, detected);\n const config = suggestPatternConfig(detected);\n\n return { detected, template, ...config };\n};\n","import type { PageRangeConstraintWithExclude } from './index.js';\n\n/**\n * Literal regex pattern rule - no token expansion or auto-escaping is applied.\n *\n * Use this when you need full control over the regex pattern, including:\n * - Character classes like `[أب]` to match أ or ب\n * - Capturing groups like `(test|text)` for alternation\n * - Any other regex syntax that would be escaped in template patterns\n *\n * If the regex contains capturing groups, the captured content\n * will be used as the segment content.\n *\n * **Note**: Unlike `template`, `lineStartsWith`, etc., this pattern type\n * does NOT auto-escape `()[]`. You have full regex control.\n *\n * @example\n * // Match Arabic-Indic numbers followed by a dash\n * { regex: '^[٠-٩]+ - ', split: 'at' }\n *\n * @example\n * // Character class - matches أ or ب\n * { regex: '^[أب] ', split: 'at' }\n *\n * @example\n * // Capture group - content after the marker becomes segment content\n * { regex: '^[٠-٩]+ - (.*)', split: 'at' }\n */\ntype RegexPattern = {\n /** Raw regex pattern string (no token expansion, no auto-escaping) */\n regex: string;\n};\n\n/**\n * Template pattern rule - expands `{{tokens}}` before compiling to regex.\n *\n * Supports all tokens defined in `TOKEN_PATTERNS` and named capture syntax.\n *\n * **Auto-escaping**: Parentheses `()` and square brackets `[]` outside of\n * `{{tokens}}` are automatically escaped. Write `({{harf}}):` instead of\n * `\\\\({{harf}}\\\\):`. For raw regex control, use `regex` pattern type.\n *\n * @example\n * // Using tokens for Arabic-Indic digits\n * { template: '^{{raqms}} {{dash}}', split: 'at' }\n *\n * @example\n * // Named capture to extract hadith number into metadata\n * { template: '^{{raqms:hadithNum}} {{dash}}', split: 'at' }\n *\n * @example\n * // Auto-escaped brackets - matches literal (أ):\n * { template: '^({{harf}}): ', split: 'at' }\n *\n * @see TOKEN_PATTERNS for available tokens\n */\ntype TemplatePattern = {\n /** Template string with `{{token}}` or `{{token:name}}` placeholders. Brackets `()[]` are auto-escaped. */\n template: string;\n};\n\n/**\n * Line-start pattern rule - matches lines starting with any of the given patterns.\n *\n * Syntactic sugar for `^(?:pattern1|pattern2|...)`. The matched marker\n * is **included** in the segment content.\n *\n * Token expansion is applied to each pattern. Use `fuzzy: true` for\n * diacritic-insensitive Arabic matching.\n *\n * **Auto-escaping**: Parentheses `()` and square brackets `[]` outside of\n * `{{tokens}}` are automatically escaped. Write `({{harf}})` instead of\n * `\\\\({{harf}}\\\\)`. For raw regex control, use `regex` pattern type.\n *\n * @example\n * // Split at chapter headings (marker included in content)\n * { lineStartsWith: ['## ', '### '], split: 'at' }\n *\n * @example\n * // Split at Arabic book/chapter markers with fuzzy matching\n * { lineStartsWith: ['{{kitab}}', '{{bab}}'], split: 'at', fuzzy: true }\n *\n * @example\n * // Auto-escaped brackets - matches literal (أ)\n * { lineStartsWith: ['({{harf}}) '], split: 'at' }\n */\ntype LineStartsWithPattern = {\n /** Array of patterns that mark line beginnings (marker included in content). Brackets `()[]` are auto-escaped. */\n lineStartsWith: string[];\n};\n\n/**\n * Line-start-after pattern rule - matches lines starting with patterns,\n * but **excludes** the marker from the segment content.\n *\n * Behaves like `lineStartsWith` but strips the marker from the output.\n * The segment content starts after the marker and extends to the next split point\n * (not just the end of the matching line).\n *\n * Token expansion is applied to each pattern. Use `fuzzy: true` for\n * diacritic-insensitive Arabic matching.\n *\n * **Auto-escaping**: Parentheses `()` and square brackets `[]` outside of\n * `{{tokens}}` are automatically escaped. Write `({{harf}}):` instead of\n * `\\\\({{harf}}\\\\):`. For raw regex control, use `regex` pattern type.\n *\n * @example\n * // Split at numbered hadiths, capturing content without the number prefix\n * // Content extends to next split, not just end of that line\n * { lineStartsAfter: ['{{raqms}} {{dash}} '], split: 'at' }\n *\n * @example\n * // Extract hadith number to metadata while stripping the prefix\n * { lineStartsAfter: ['{{raqms:num}} {{dash}} '], split: 'at' }\n *\n * @example\n * // Auto-escaped brackets - matches literal (أ): prefix\n * { lineStartsAfter: ['({{harf}}): '], split: 'at' }\n */\ntype LineStartsAfterPattern = {\n /** Array of patterns that mark line beginnings (marker excluded from content). Brackets `()[]` are auto-escaped. */\n lineStartsAfter: string[];\n};\n\n/**\n * Line-end pattern rule - matches lines ending with any of the given patterns.\n *\n * Syntactic sugar for `(?:pattern1|pattern2|...)$`.\n *\n * Token expansion is applied to each pattern. Use `fuzzy: true` for\n * diacritic-insensitive Arabic matching.\n *\n * **Auto-escaping**: Parentheses `()` and square brackets `[]` outside of\n * `{{tokens}}` are automatically escaped. For raw regex control, use `regex` pattern type.\n *\n * @example\n * // Split at lines ending with Arabic sentence-ending punctuation\n * { lineEndsWith: ['۔', '؟', '!'], split: 'after' }\n *\n * @example\n * // Auto-escaped brackets - matches literal (انتهى) suffix\n * { lineEndsWith: ['(انتهى)'], split: 'after' }\n */\ntype LineEndsWithPattern = {\n /** Array of patterns that mark line endings. Brackets `()[]` are auto-escaped. */\n lineEndsWith: string[];\n};\n\n/**\n * Dictionary entry pattern options for Arabic lexicon-style headword matching.\n *\n * This captures authoring intent in a serializable shape and is compiled into\n * a regex internally by the rule compiler.\n */\nexport interface DictionaryEntryPatternOptions {\n /**\n * Words that should never be treated as lemmas when followed by a colon.\n *\n * Matching is Arabic-normalized, diacritic-insensitive, and exact. Callers\n * should provide canonical forms only; vocalized variants do not need to be\n * listed separately.\n */\n stopWords: string[];\n\n /**\n * Allow balanced parenthesized headwords like `(عنبر):` or `(عنبر) :`.\n * @default false\n */\n allowParenthesized?: boolean;\n\n /**\n * Allow optional whitespace before the trailing colon.\n * @default false\n */\n allowWhitespaceBeforeColon?: boolean;\n\n /**\n * Allow comma-separated headword lists like `سبد، دبس:`.\n * @default false\n */\n allowCommaSeparated?: boolean;\n\n /**\n * Allow conservative mid-line subentries that begin with `و`.\n * Disable this when the rule should only split true line/page starts.\n * @default true\n */\n midLineSubentries?: boolean;\n\n /**\n * Named capture key for the matched lemma metadata.\n * @default 'lemma'\n */\n captureName?: string;\n\n /**\n * Minimum number of Arabic base letters in a lemma.\n * @default 2\n */\n minLetters?: number;\n\n /**\n * Maximum number of Arabic base letters in a lemma.\n * @default 10\n */\n maxLetters?: number;\n}\n\n/**\n * Arabic dictionary entry pattern rule - serializable headword matcher compiled internally.\n *\n * @example\n * {\n * dictionaryEntry: {\n * stopWords: ['قال', 'وقيل'],\n * allowCommaSeparated: true,\n * },\n * meta: { type: 'entry' }\n * }\n */\ntype DictionaryEntryPattern = {\n dictionaryEntry: DictionaryEntryPatternOptions;\n};\n\n/**\n * Union of all pattern types for split rules.\n *\n * Each rule must have exactly ONE pattern type:\n * - `regex` - Raw regex pattern (no token expansion)\n * - `template` - Pattern with `{{token}}` expansion\n * - `lineStartsWith` - Match line beginnings (marker included)\n * - `lineStartsAfter` - Match line beginnings (marker excluded)\n * - `lineEndsWith` - Match line endings\n * - `dictionaryEntry` - Arabic dictionary headword matching\n */\ntype PatternType =\n | RegexPattern\n | TemplatePattern\n | LineStartsWithPattern\n | LineStartsAfterPattern\n | LineEndsWithPattern\n | DictionaryEntryPattern;\n\n/**\n * Pattern type key names for split rules.\n *\n * Use this array to dynamically iterate over pattern types in UIs,\n * or use the `PatternTypeKey` type for type-safe string unions.\n *\n * @example\n * // Build a dropdown/select in UI\n * PATTERN_TYPE_KEYS.map(key => <option value={key}>{key}</option>)\n *\n * @example\n * // Type-safe pattern key validation\n * const validateKey = (k: string): k is PatternTypeKey =>\n * (PATTERN_TYPE_KEYS as readonly string[]).includes(k);\n */\nexport const PATTERN_TYPE_KEYS = [\n 'lineStartsWith',\n 'lineStartsAfter',\n 'lineEndsWith',\n 'template',\n 'regex',\n 'dictionaryEntry',\n] as const;\n\n/**\n * String union of pattern type key names.\n *\n * Derived from `PATTERN_TYPE_KEYS` to stay in sync automatically.\n */\nexport type PatternTypeKey = (typeof PATTERN_TYPE_KEYS)[number];\n\n// Split Behavior\n\n/**\n * Configuration for how and where to split content when a pattern matches.\n *\n * Controls the split position relative to matches, which occurrences to\n * split on, page span limits, and fuzzy matching for Arabic text.\n */\ntype SplitBehavior = {\n /**\n * Where to split relative to the match.\n * - `'at'`: New segment starts at the match position\n * - `'after'`: New segment starts after the match ends\n * @default 'at'\n */\n split?: 'at' | 'after';\n\n /**\n * Which occurrence(s) to split on.\n * - `'all'`: Split at every match (default)\n * - `'first'`: Only split at the first match\n * - `'last'`: Only split at the last match\n *\n * @default 'all'\n */\n occurrence?: 'first' | 'last' | 'all';\n\n /**\n * Enable diacritic-insensitive matching for Arabic text.\n *\n * When `true`, patterns in `lineStartsWith`, `lineEndsWith`, and\n * `lineStartsAfter` are transformed to match text regardless of:\n * - Diacritics (harakat/tashkeel): فَتْحَة، ضَمَّة، كَسْرَة، etc.\n * - Character equivalences: ا/آ/أ/إ, ة/ه, ى/ي\n *\n * **Note**: Does NOT apply to `regex` or `template` patterns.\n * For templates, apply fuzzy manually using `makeDiacriticInsensitive()`.\n *\n * @default false\n */\n fuzzy?: boolean;\n};\n\n/**\n * Optional constraints and metadata for a split rule.\n *\n * Use constraints to limit which pages a rule applies to, and\n * metadata to attach arbitrary data to resulting segments.\n *\n * Extends `PageRangeConstraintWithExclude` for `min`, `max`, and `exclude` properties.\n */\ntype RuleConstraints = PageRangeConstraintWithExclude & {\n /**\n * Arbitrary metadata attached to segments matching this rule.\n *\n * This metadata is merged with any named captures from the pattern.\n * Named captures (e.g., `{{raqms:num}}`) take precedence over\n * static metadata with the same key.\n *\n * @example\n * // Tag segments as chapters\n * { lineStartsWith: ['{{bab}}'], split: 'before', meta: { type: 'chapter' } }\n */\n meta?: Record<string, unknown>;\n\n /**\n * Page-start guard: only allow this rule to match at the START of a page if the\n * previous page's last non-whitespace character matches this pattern.\n *\n * This is useful for avoiding false positives caused purely by page wrap.\n *\n * Example use-case:\n * - Split on `{{naql}}` at line starts (e.g. \"أخبرنا ...\")\n * - BUT if a new page starts with \"أخبرنا ...\" and the previous page did NOT\n * end with sentence-ending punctuation, treat it as a continuation and do not split.\n *\n * Notes:\n * - This guard applies ONLY at page starts, not mid-page line starts.\n * - This is a template pattern (tokens allowed). It is checked against the LAST\n * non-whitespace character of the previous page's content.\n *\n * @example\n * // Allow split at page start only if previous page ends with sentence punctuation\n * { lineStartsWith: ['{{naql}}'], fuzzy: true, pageStartGuard: '{{tarqim}}' }\n */\n pageStartGuard?: string;\n\n /**\n * Suppress page-start matches when the previous page's last Arabic word\n * is in this stoplist, unless that page ends with strong sentence punctuation.\n *\n * This is useful for dictionary-like content where a page break can split\n * a phrase such as `قال` / `العجاج:` across two pages, causing a false entry\n * start at the top of the next page.\n *\n * Notes:\n * - Applies ONLY at page starts, not to mid-page matches.\n * - Matching is exact after Arabic normalization:\n * diacritics are ignored and common variants like ا/أ/إ/آ are tolerated.\n *\n * @example\n * {\n * regex: '^(?<lemma>[ء-غف-ي]+):',\n * pageStartPrevWordStoplist: ['قال', 'وقيل', 'ويقال']\n * }\n */\n pageStartPrevWordStoplist?: string[];\n\n /**\n * Suppress matches when the immediately previous Arabic word on the SAME page\n * is in this stoplist.\n *\n * This is useful for dictionary-like content where phrases such as\n * `جلّ وعزّ:` should not be treated as a new entry starting at `وعزّ:`.\n *\n * Notes:\n * - Applies only to non-page-start matches.\n * - Matching is exact after Arabic normalization:\n * diacritics are ignored and common variants like ا/أ/إ/آ are tolerated.\n *\n * @example\n * {\n * regex: '(?<lemma>وعزّ):',\n * samePagePrevWordStoplist: ['جل']\n * }\n */\n samePagePrevWordStoplist?: string[];\n};\n\n// Combined Rule Type\n\n/**\n * A complete split rule combining pattern, behavior, and constraints.\n *\n * Each rule must specify:\n * - **Pattern** (exactly one): `regex`, `template`, `lineStartsWith`,\n * `lineStartsAfter`, `lineEndsWith`, or `dictionaryEntry`\n * - **Split behavior**: `split` (optional, defaults to `'at'`), `occurrence`, `fuzzy`\n * - **Constraints** (optional): `min`, `max`, `meta`\n *\n * @example\n * // Basic rule: split at markdown headers (split defaults to 'at')\n * const rule: SplitRule = {\n * lineStartsWith: ['## ', '### '],\n * meta: { type: 'section' }\n * };\n *\n * @example\n * // Advanced rule: extract hadith numbers with fuzzy Arabic matching\n * const rule: SplitRule = {\n * lineStartsAfter: ['{{raqms:hadithNum}} {{dash}} '],\n * fuzzy: true,\n * min: 5,\n * max: 500,\n * meta: { type: 'hadith' }\n * };\n */\nexport type SplitRule = PatternType & SplitBehavior & RuleConstraints;\n","import { PATTERN_TYPE_KEYS, type PatternTypeKey, type SplitRule } from '@/types/rules';\n\n// Keys that support array patterns and can be merged\nconst MERGEABLE_KEYS = new Set<PatternTypeKey>(['lineStartsWith', 'lineStartsAfter', 'lineEndsWith']);\n\n/**\n * Result from optimizing rules.\n */\nexport type OptimizeResult = {\n /** Optimized rules (merged and sorted by specificity) */\n rules: SplitRule[];\n /** Number of rules that were merged into existing rules */\n mergedCount: number;\n};\n\n/**\n * Get the pattern type key for a rule.\n */\nconst getPatternKey = (rule: SplitRule) => PATTERN_TYPE_KEYS.find((key) => key in rule) ?? 'regex';\n\nconst getPatternArray = (rule: SplitRule, key: PatternTypeKey) => {\n const value = (rule as Record<string, unknown>)[key];\n return Array.isArray(value) ? (value as string[]) : [];\n};\n\nconst getPatternString = (rule: SplitRule, key: PatternTypeKey) => {\n const value = (rule as Record<string, unknown>)[key];\n return typeof value === 'string'\n ? value\n : Array.isArray(value)\n ? value.join('\\n')\n : value\n ? JSON.stringify(value)\n : '';\n};\n\nconst normalizePatterns = (patterns: string[]) =>\n [...new Set(patterns)].sort((a, b) => b.length - a.length || a.localeCompare(b));\n\nconst getDictionaryEntrySpecificityScore = (rule: SplitRule) => {\n if (!('dictionaryEntry' in rule)) {\n return 0;\n }\n\n const {\n allowCommaSeparated = false,\n allowParenthesized = false,\n allowWhitespaceBeforeColon = false,\n maxLetters = 10,\n midLineSubentries = true,\n minLetters = 2,\n stopWords,\n } = rule.dictionaryEntry;\n\n return (\n minLetters * 20 +\n maxLetters +\n (allowCommaSeparated ? 0 : 120) +\n (allowParenthesized ? 0 : 60) +\n (allowWhitespaceBeforeColon ? 0 : 20) +\n (midLineSubentries ? 0 : 160) +\n Math.min(stopWords.length, 25)\n );\n};\n\nconst getSpecificityScore = (rule: SplitRule) => {\n const key = getPatternKey(rule);\n if (key === 'dictionaryEntry') {\n return getDictionaryEntrySpecificityScore(rule);\n }\n return MERGEABLE_KEYS.has(key)\n ? getPatternArray(rule, key).reduce((max, p) => Math.max(max, p.length), 0)\n : getPatternString(rule, key).length;\n};\n\nconst createMergeKey = (rule: SplitRule) => {\n const key = getPatternKey(rule);\n const { [key]: _, ...rest } = rule as any;\n return `${key}|${JSON.stringify(rest)}`;\n};\n\nexport const optimizeRules = (rules: SplitRule[]) => {\n const output: SplitRule[] = [];\n const indexByMergeKey = new Map<string, number>();\n let mergedCount = 0;\n\n for (const rule of rules) {\n const key = getPatternKey(rule);\n if (!MERGEABLE_KEYS.has(key)) {\n output.push(rule);\n continue;\n }\n\n const mergeKey = createMergeKey(rule);\n const existingIndex = indexByMergeKey.get(mergeKey);\n\n if (existingIndex === undefined) {\n indexByMergeKey.set(mergeKey, output.length);\n output.push({ ...rule, [key]: normalizePatterns(getPatternArray(rule, key)) } as SplitRule);\n } else {\n const existing = output[existingIndex] as any;\n existing[key] = normalizePatterns([...getPatternArray(existing, key), ...getPatternArray(rule, key)]);\n mergedCount++;\n }\n }\n\n return { mergedCount, rules: output.sort((a, b) => getSpecificityScore(b) - getSpecificityScore(a)) };\n};\n","import type { PageRangeConstraint, PreprocessTransform } from '../types/index.js';\n\n/** Helper for exhaustive switch checking - TypeScript will error if a case is missed */\nconst assertNever = (x: never): never => {\n throw new Error(`Unknown preprocess transform type: ${JSON.stringify(x)}`);\n};\n\n/** Check if a character is whitespace (space, newline, tab, etc.) */\nconst isWhitespace = (char: string): boolean => /\\s/.test(char);\n\n/**\n * Check if a character code is a zero-width control character.\n *\n * Covers:\n * - U+200B–U+200F (Zero Width Space, Joiners, Direction Marks)\n * - U+202A–U+202E (Bidirectional Formatting)\n * - U+2060–U+2064 (Word Joiner, Invisible Operators)\n * - U+FEFF (Byte Order Mark / Zero Width No-Break Space)\n */\nexport const isZeroWidth = (code: number): boolean =>\n (code >= 0x200b && code <= 0x200f) ||\n (code >= 0x202a && code <= 0x202e) ||\n (code >= 0x2060 && code <= 0x2064) ||\n code === 0xfeff;\n\n/**\n * Remove zero-width control characters from text.\n *\n * @param text - Input text\n * @param mode - 'strip' (default) removes entirely, 'space' replaces with space\n * @returns Text with zero-width characters removed or replaced\n */\nexport const removeZeroWidth = (text: string, mode: 'strip' | 'space' = 'strip'): string => {\n if (mode === 'space') {\n // Use array builder for O(n) performance instead of string concatenation\n const parts: string[] = [];\n let lastWasWhitespace = true; // Treat start as \"after whitespace\" to avoid leading space\n for (let i = 0; i < text.length; i++) {\n const code = text.charCodeAt(i);\n if (isZeroWidth(code)) {\n if (!lastWasWhitespace && parts.length > 0) {\n parts.push(' ');\n lastWasWhitespace = true;\n }\n } else {\n const char = text[i];\n parts.push(char);\n lastWasWhitespace = isWhitespace(char);\n }\n }\n return parts.join('');\n }\n return text.replace(/[\\u200B-\\u200F\\u202A-\\u202E\\u2060-\\u2064\\uFEFF]/g, '');\n};\n\n/**\n * Condense multiple periods (...) into ellipsis character (…).\n *\n * Prevents `{{tarqim}}` from false-matching inside ellipsis since\n * the `.` in tarqim matches individual periods.\n *\n * @param text - Input text\n * @returns Text with period sequences replaced by ellipsis\n */\nexport const condenseEllipsis = (text: string): string => text.replace(/\\.{2,}/g, '…');\n\n/**\n * Join trailing و (waw) to the next word.\n *\n * Fixes OCR/digitization artifacts: ' و ' → ' و' (waw joined to next word)\n *\n * @param text - Input text\n * @returns Text with trailing waw joined to following word\n */\nexport const fixTrailingWaw = (text: string): string => text.replace(/ و /g, ' و');\n\n/**\n * Check if a page ID is within a constraint range.\n */\nconst isInRange = (pageId: number, constraint: PageRangeConstraint): boolean => {\n if (constraint.min !== undefined && pageId < constraint.min) {\n return false;\n }\n if (constraint.max !== undefined && pageId > constraint.max) {\n return false;\n }\n return true;\n};\n\n/**\n * Normalize a transform to its object form.\n */\nconst normalizeTransform = (\n transform: PreprocessTransform,\n): {\n type: 'removeZeroWidth' | 'condenseEllipsis' | 'fixTrailingWaw';\n mode?: 'strip' | 'space';\n min?: number;\n max?: number;\n} => {\n if (typeof transform === 'string') {\n return { type: transform };\n }\n return transform;\n};\n\n/**\n * Apply preprocessing transforms to a page's content.\n *\n * Transforms run in array order. Each can be limited to specific pages\n * via `min`/`max` constraints.\n *\n * @param content - Page content to transform\n * @param pageId - Page ID for constraint checking\n * @param transforms - Array of transforms to apply\n * @returns Transformed content\n */\nexport const applyPreprocessToPage = (content: string, pageId: number, transforms: PreprocessTransform[]): string => {\n let result = content;\n\n for (const transform of transforms) {\n const rule = normalizeTransform(transform);\n\n // Check page constraints\n if (!isInRange(pageId, rule)) {\n continue;\n }\n\n // Apply transform\n switch (rule.type) {\n case 'removeZeroWidth':\n result = removeZeroWidth(result, rule.mode ?? 'strip');\n break;\n case 'condenseEllipsis':\n result = condenseEllipsis(result);\n break;\n case 'fixTrailingWaw':\n result = fixTrailingWaw(result);\n break;\n default:\n // TypeScript will error if a new transform type is added but not handled\n assertNever(rule.type);\n }\n }\n\n return result;\n};\n","import type { DictionaryEntryPatternOptions, SplitRule } from '@/types/rules.js';\nimport { makeDiacriticInsensitive, normalizeArabicForComparison } from '@/utils/textUtils.js';\nimport { ARABIC_LETTER_WITH_OPTIONAL_MARKS_PATTERN, ARABIC_MARKS_CLASS } from './tokens.js';\n\nexport interface ArabicDictionaryEntryRuleOptions extends DictionaryEntryPatternOptions {\n /**\n * Suppress page-start matches when the previous page's last Arabic word\n * is in this stoplist, unless that page ends with strong sentence punctuation.\n */\n pageStartPrevWordStoplist?: string[];\n\n /**\n * Suppress non-page-start matches when the immediately previous Arabic word\n * on the same page is in this stoplist.\n */\n samePagePrevWordStoplist?: string[];\n\n /**\n * Static metadata merged into matching segments.\n */\n meta?: Record<string, unknown>;\n}\n\ntype DictionaryEntryRegexSource = {\n captureNames: string[];\n regex: string;\n};\n\nconst uniqueCanonicalWords = (words: string[]) => {\n const seen = new Set<string>();\n const result: string[] = [];\n\n for (const word of words) {\n const normalized = normalizeArabicForComparison(word);\n if (!normalized || seen.has(normalized)) {\n continue;\n }\n seen.add(normalized);\n result.push(word);\n }\n\n return result;\n};\n\nconst buildStopAlternation = (stopWords: string[]) => {\n const unique = uniqueCanonicalWords(stopWords);\n if (unique.length === 0) {\n return '';\n }\n return unique.map((word) => makeDiacriticInsensitive(normalizeArabicForComparison(word))).join('|');\n};\n\nconst buildHeadwordBody = ({\n allowCommaSeparated,\n colonPattern,\n stopAlternation,\n stopwordBody,\n unit,\n}: {\n allowCommaSeparated: boolean;\n colonPattern: string;\n stopAlternation: string;\n stopwordBody: string;\n unit: string;\n}) => {\n if (!stopAlternation) {\n return allowCommaSeparated ? `${unit}(?:\\\\s*[،,]\\\\s*${unit})*` : unit;\n }\n\n const stopwordBoundary = allowCommaSeparated ? `(?:\\\\s*[،,]\\\\s*|${colonPattern})` : colonPattern;\n const guardedUnit = `(?!(?:${stopwordBody})${stopwordBoundary})${unit}`;\n\n return allowCommaSeparated ? `${guardedUnit}(?:\\\\s*[،,]\\\\s*${guardedUnit})*` : guardedUnit;\n};\n\nconst buildBalancedMarker = ({\n allowParenthesized,\n allowWhitespaceBeforeColon,\n captureName,\n headwordBody,\n}: {\n allowParenthesized: boolean;\n allowWhitespaceBeforeColon: boolean;\n captureName: string;\n headwordBody: string;\n}) => {\n const colon = allowWhitespaceBeforeColon ? '\\\\s*:' : ':';\n const withCapture = `(?<${captureName}>${headwordBody})`;\n\n if (!allowParenthesized) {\n return `${withCapture}${colon}`;\n }\n\n return `(?:\\\\(\\\\s*${withCapture}\\\\s*\\\\)|${withCapture})${colon}`;\n};\n\nconst validateDictionaryEntryOptions = ({\n captureName = 'lemma',\n maxLetters = 10,\n minLetters = 2,\n}: Pick<DictionaryEntryPatternOptions, 'captureName' | 'maxLetters' | 'minLetters'>) => {\n if (!Number.isInteger(minLetters) || minLetters < 1) {\n throw new Error(`createArabicDictionaryEntryRule: minLetters must be an integer >= 1, got ${minLetters}`);\n }\n if (!Number.isInteger(maxLetters) || maxLetters < minLetters) {\n throw new Error(\n `createArabicDictionaryEntryRule: maxLetters must be an integer >= minLetters, got ${maxLetters}`,\n );\n }\n if (!captureName.match(/^[A-Za-z_]\\w*$/)) {\n throw new Error(`createArabicDictionaryEntryRule: invalid captureName \"${captureName}\"`);\n }\n};\n\nexport const buildArabicDictionaryEntryRegexSource = (\n {\n allowCommaSeparated = false,\n allowParenthesized = false,\n allowWhitespaceBeforeColon = false,\n captureName = 'lemma',\n maxLetters = 10,\n midLineSubentries = true,\n minLetters = 2,\n stopWords,\n }: DictionaryEntryPatternOptions,\n capturePrefix?: string,\n): DictionaryEntryRegexSource => {\n validateDictionaryEntryOptions({ captureName, maxLetters, minLetters });\n\n const zeroWidthPrefix = '[\\\\u200E\\\\u200F\\\\u061C\\\\u200B\\\\u200C\\\\u200D\\\\uFEFF]*';\n const wawWithMarks = `و${ARABIC_MARKS_CLASS}*`;\n const alWithMarks = `ا${ARABIC_MARKS_CLASS}*ل${ARABIC_MARKS_CLASS}*`;\n const stem = `${ARABIC_LETTER_WITH_OPTIONAL_MARKS_PATTERN}(?:${ARABIC_LETTER_WITH_OPTIONAL_MARKS_PATTERN}){${minLetters - 1},${maxLetters - 1}}`;\n const lemmaUnit = `(?:${wawWithMarks})?(?:${alWithMarks})?${stem}`;\n const stopAlternation = buildStopAlternation(stopWords);\n const colonPattern = allowWhitespaceBeforeColon ? '\\\\s*:' : ':';\n const stopwordBody = stopAlternation ? `(?:${wawWithMarks})?(?:${stopAlternation})` : '';\n const lemmaBody = buildHeadwordBody({\n allowCommaSeparated,\n colonPattern,\n stopAlternation,\n stopwordBody,\n unit: lemmaUnit,\n });\n const lineStartBoundary = `(?:(?<=^)|(?<=\\\\n))${zeroWidthPrefix}`;\n const midLineTrigger = allowParenthesized\n ? `(?<=\\\\s)(?=(?:\\\\(\\\\s*)?${wawWithMarks}(?:${alWithMarks})?)`\n : `(?<=\\\\s)(?=${wawWithMarks}(?:${alWithMarks})?)`;\n const prefixedCaptureName = capturePrefix ? `${capturePrefix}${captureName}` : captureName;\n const regex =\n `(?:${lineStartBoundary}${midLineSubentries ? `|${midLineTrigger}` : ''})` +\n buildBalancedMarker({\n allowParenthesized,\n allowWhitespaceBeforeColon,\n captureName: prefixedCaptureName,\n headwordBody: lemmaBody,\n });\n\n return {\n captureNames: [prefixedCaptureName],\n regex,\n };\n};\n\n/**\n * Creates a reusable split rule for Arabic dictionary entries.\n *\n * The returned rule preserves authoring intent as a serializable\n * `{ dictionaryEntry: ... }` pattern rather than eagerly compiling to a raw\n * regex string.\n *\n * @example\n * createArabicDictionaryEntryRule({\n * stopWords: ['وقيل', 'ويقال', 'قال'],\n * pageStartPrevWordStoplist: ['قال', 'وقيل', 'ويقال'],\n * })\n *\n * @example\n * createArabicDictionaryEntryRule({\n * allowParenthesized: true,\n * allowWhitespaceBeforeColon: true,\n * allowCommaSeparated: true,\n * stopWords: ['الليث', 'العجاج'],\n * })\n */\nexport const createArabicDictionaryEntryRule = ({\n allowCommaSeparated = false,\n allowParenthesized = false,\n allowWhitespaceBeforeColon = false,\n captureName = 'lemma',\n maxLetters = 10,\n meta,\n midLineSubentries = true,\n minLetters = 2,\n pageStartPrevWordStoplist,\n samePagePrevWordStoplist,\n stopWords,\n}: ArabicDictionaryEntryRuleOptions): SplitRule => {\n validateDictionaryEntryOptions({ captureName, maxLetters, minLetters });\n\n return {\n dictionaryEntry: {\n allowCommaSeparated,\n allowParenthesized,\n allowWhitespaceBeforeColon,\n captureName,\n maxLetters,\n midLineSubentries,\n minLetters,\n stopWords: uniqueCanonicalWords(stopWords),\n },\n meta,\n pageStartPrevWordStoplist,\n samePagePrevWordStoplist,\n };\n};\n","/**\n * Shared constants for segmentation breakpoint processing.\n */\n\n/**\n * Threshold for using offset-based fast path in boundary processing.\n *\n * Below this: accurate string-search (handles offset drift from structural rules).\n * At or above this: O(n) arithmetic (performance critical for large books).\n *\n * The value of 1000 is chosen based on typical Arabic book sizes:\n * - Sahih al-Bukhari: ~1000-3000 pages\n * - Standard hadith collections: 1000-7000 pages\n * - Large aggregated corpora: 10k-50k pages\n *\n * For segments ≥1000 pages, the performance gain from offset-based slicing\n * outweighs the minor accuracy loss from potential offset drift.\n *\n * @remarks\n * Fast path is skipped when:\n * - `maxContentLength` is set (requires character-accurate splitting)\n * - `debugMetaKey` is set (requires proper provenance tracking)\n * - Content was structurally modified by marker stripping (offsets may drift)\n */\nexport const FAST_PATH_THRESHOLD = 1000;\n\nexport const WINDOW_PREFIX_LENGTHS = [80, 60, 40, 30, 20, 15] as const;\n\n// For page-join normalization we need to handle cases where only the very beginning of the next page\n// is present in the current segment (e.g. the segment ends right before the next structural marker).\n// That can be as short as a few words, so we allow shorter prefixes here.\nexport const JOINER_PREFIX_LENGTHS = [80, 60, 40, 30, 20, 15, 12, 10, 8, 6] as const;\n\n// Includes Arabic comma (،), semicolon (؛), full stop (.), Quranic marks (۝۞), etc.\nexport const STOP_CHARACTERS = /[\\s\\n.,;!?؛،۔۝۞]/;\n\n/**\n * Buffer size for fuzzy searching around expected boundary positions (characters).\n * Used when exact matches fail due to minor content variations.\n */\nexport const BUFFER_SIZE = 1000;\n\n/**\n * Maximum allowed deviation between expected and actual boundary positions (characters).\n * Matches outside this range are rejected unless `ignoreDeviation` is active.\n */\nexport const MAX_DEVIATION = 2000;\n\n/**\n * Penalty score applied to non-newline anchor candidates.\n *\n * Designed to prioritize newline-aligned boundaries unless a whitespace match is\n * significantly closer (within 20 chars). Handles cases where marker stripping\n * shifts the boundary slightly.\n */\nexport const NON_NEWLINE_PENALTY = 20;\n\n/**\n * Limit for inferring start offset from a relaxed search (characters).\n *\n * If the relaxed search finds a match more than this distance away from the\n * expected position, we assume it's a false positive (e.g. repeated content)\n * and do not use it to infer the start offset.\n */\nexport const INFERENCE_PROXIMITY_LIMIT = 500;\n","import type { SplitRule } from '@/types/rules.js';\nimport { isPageExcluded } from './breakpoint-utils.js';\n\n/**\n * Result of a regex match with position and optional capture information.\n *\n * Represents a single match found by the segmentation engine, including\n * its position in the concatenated content and any captured values.\n */\nexport type MatchResult = {\n /**\n * Start offset (inclusive) of the match in the content string.\n */\n start: number;\n\n /**\n * End offset (exclusive) of the match in the content string.\n *\n * The matched text is `content.slice(start, end)`.\n */\n end: number;\n\n /**\n * Content captured by `lineStartsAfter` patterns.\n *\n * For patterns like `^٦٦٩٦ - (.*)`, this contains the text\n * matched by the `(.*)` group (the rest of the line after the marker).\n */\n captured?: string;\n\n /**\n * Named capture group values from `{{token:name}}` syntax.\n *\n * Keys are the capture names, values are the matched strings.\n *\n * @example\n * // For pattern '{{raqms:num}} {{dash}}'\n * { num: '٦٦٩٦' }\n */\n namedCaptures?: Record<string, string>;\n\n /**\n * Optional index of the word from a words/patterns array that caused the match.\n */\n wordIndex?: number;\n};\n\n/**\n * Extracts named capture groups from a regex match.\n *\n * Only includes groups that are in the `captureNames` list and have\n * defined values. This filters out positional captures and ensures\n * only explicitly requested named captures are returned.\n *\n * @param groups - The `match.groups` object from `RegExp.exec()`\n * @param captureNames - List of capture names to extract (from `{{token:name}}` syntax)\n * @returns Object with capture name → value pairs, or `undefined` if none found\n *\n * @example\n * const match = /(?<num>[٠-٩]+) -/.exec('٦٦٩٦ - text');\n * extractNamedCaptures(match.groups, ['num'])\n * // → { num: '٦٦٩٦' }\n *\n * @example\n * // No matching captures\n * extractNamedCaptures({}, ['num'])\n * // → undefined\n *\n * @example\n * // Undefined groups\n * extractNamedCaptures(undefined, ['num'])\n * // → undefined\n */\nexport const extractNamedCaptures = (groups: Record<string, string> | undefined, captureNames: string[]) => {\n if (!groups || captureNames.length === 0) {\n return undefined;\n }\n const namedCaptures: Record<string, string> = {};\n for (const name of captureNames) {\n if (groups[name] !== undefined) {\n namedCaptures[name] = groups[name];\n }\n }\n return Object.keys(namedCaptures).length > 0 ? namedCaptures : undefined;\n};\n\n/**\n * Gets the last defined positional capture group from a match array.\n *\n * Used for `lineStartsAfter` patterns where the content capture (`.*`)\n * is always at the end of the pattern. Named captures may shift the\n * positional indices, so we iterate backward to find the actual content.\n *\n * @param match - RegExp exec result array\n * @returns The last defined capture group value, or `undefined` if none\n *\n * @example\n * // Pattern: ^(?:(?<num>[٠-٩]+) - )(.*)\n * // Match array: ['٦٦٩٦ - content', '٦٦٩٦', 'content']\n * getLastPositionalCapture(match)\n * // → 'content'\n *\n * @example\n * // No captures\n * getLastPositionalCapture(['full match'])\n * // → undefined\n */\nexport const getLastPositionalCapture = (match: RegExpExecArray) => {\n if (match.length <= 1) {\n return undefined;\n }\n for (let i = match.length - 1; i >= 1; i--) {\n if (match[i] !== undefined) {\n return match[i];\n }\n }\n return undefined;\n};\n\n/**\n * Filters matches to only include those within page ID constraints.\n *\n * Applies the `min`, `max`, and `exclude` constraints from a rule to filter out\n * matches that occur on pages outside the allowed range or explicitly excluded.\n *\n * @param matches - Array of match results to filter\n * @param rule - Rule containing `min`, `max`, and/or `exclude` page constraints\n * @param getId - Function that returns the page ID for a given offset\n * @returns Filtered array containing only matches within constraints\n *\n * @example\n * const matches = [\n * { start: 0, end: 10 }, // Page 1\n * { start: 100, end: 110 }, // Page 5\n * { start: 200, end: 210 }, // Page 10\n * ];\n * filterByConstraints(matches, { min: 3, max: 8 }, getId)\n * // → [{ start: 100, end: 110 }] (only page 5 match)\n */\nexport const filterByConstraints = (\n matches: MatchResult[],\n rule: Pick<SplitRule, 'min' | 'max' | 'exclude'>,\n getId: (offset: number) => number,\n) =>\n matches.filter((m) => {\n const id = getId(m.start);\n return (\n (rule.min === undefined || id >= rule.min) &&\n (rule.max === undefined || id <= rule.max) &&\n !isPageExcluded(id, rule.exclude)\n );\n });\n\n/**\n * Filters matches based on occurrence setting (first, last, or all).\n *\n * Applies occurrence-based selection to a list of matches:\n * - `'all'` or `undefined`: Return all matches (default)\n * - `'first'`: Return only the first match\n * - `'last'`: Return only the last match\n *\n * @param matches - Array of match results to filter\n * @param occurrence - Which occurrence(s) to keep\n * @returns Filtered array based on occurrence setting\n *\n * @example\n * const matches = [{ start: 0 }, { start: 10 }, { start: 20 }];\n *\n * filterByOccurrence(matches, 'first')\n * // → [{ start: 0 }]\n *\n * filterByOccurrence(matches, 'last')\n * // → [{ start: 20 }]\n *\n * filterByOccurrence(matches, 'all')\n * // → [{ start: 0 }, { start: 10 }, { start: 20 }]\n *\n * filterByOccurrence(matches, undefined)\n * // → [{ start: 0 }, { start: 10 }, { start: 20 }] (default: all)\n */\nexport const filterByOccurrence = (matches: MatchResult[], occurrence?: 'first' | 'last' | 'all') => {\n if (!matches.length) {\n return [];\n }\n if (occurrence === 'first') {\n return [matches[0]];\n }\n if (occurrence === 'last') {\n return [matches.at(-1)!];\n }\n return matches;\n};\n\n/**\n * Checks if any rule in the list allows the given page ID.\n *\n * A rule allows an ID if it falls within the rule's `min`/`max` constraints.\n * Rules without constraints allow all page IDs.\n *\n * This is used to determine whether to create a segment for content\n * that appears before any split points (the \"first segment\").\n *\n * @param rules - Array of rules with optional `min` and `max` constraints\n * @param pageId - Page ID to check\n * @returns `true` if at least one rule allows the page ID\n *\n * @example\n * const rules = [\n * { min: 5, max: 10 }, // Allows pages 5-10\n * { min: 20 }, // Allows pages 20+\n * ];\n *\n * anyRuleAllowsId(rules, 7) // → true (first rule allows)\n * anyRuleAllowsId(rules, 3) // → false (no rule allows)\n * anyRuleAllowsId(rules, 25) // → true (second rule allows)\n *\n * @example\n * // Rules without constraints allow everything\n * anyRuleAllowsId([{}], 999) // → true\n */\nexport const anyRuleAllowsId = (rules: Pick<SplitRule, 'min' | 'max'>[], pageId: number) =>\n rules.some((r) => (r.min === undefined || pageId >= r.min) && (r.max === undefined || pageId <= r.max));\n\nexport const extractDebugIndex = (groups: Record<string, string> | undefined, prefix: string): number | undefined => {\n if (!groups) {\n return undefined;\n }\n for (const key in groups) {\n if (key.startsWith(prefix) && groups[key] !== undefined) {\n const idx = Number.parseInt(key.slice(prefix.length), 10);\n if (!Number.isNaN(idx)) {\n return idx;\n }\n }\n }\n return undefined;\n};\n","import type { Breakpoint, BreakpointRule } from '@/types/breakpoints.js';\nimport type { PageRange, Segment } from '@/types/index.js';\nimport type { Logger } from '@/types/options.js';\nimport { adjustForUnicodeBoundary } from '@/utils/textUtils.js';\nimport {\n FAST_PATH_THRESHOLD,\n INFERENCE_PROXIMITY_LIMIT,\n JOINER_PREFIX_LENGTHS,\n MAX_DEVIATION,\n NON_NEWLINE_PENALTY,\n STOP_CHARACTERS,\n WINDOW_PREFIX_LENGTHS,\n} from './breakpoint-constants.js';\nimport { extractDebugIndex } from './match-utils.js';\n\nexport type BreakpointMatch = {\n breakPos: number;\n breakpointIndex: number;\n rule: BreakpointRule;\n contentLengthSplit?: { maxContentLength: number; reason: 'whitespace' | 'unicode_boundary' };\n wordIndex?: number;\n};\n\n/**\n * Escapes regex metacharacters outside of `{{token}}` delimiters.\n *\n * This allows words in the `words` field to contain tokens while treating\n * most other characters as literals.\n *\n * Note: `()[]` are NOT escaped here because `processPattern` will handle them\n * via `escapeTemplateBrackets`. This avoids double-escaping.\n *\n * @param word - Word string that may contain {{tokens}}\n * @returns String with metacharacters escaped outside tokens (except ()[] which are escaped by processPattern)\n *\n * @example\n * escapeWordsOutsideTokens('a.*b')\n * // → 'a\\\\.\\\\*b'\n *\n * escapeWordsOutsideTokens('{{naql}}.test')\n * // → '{{naql}}\\\\.test'\n *\n * escapeWordsOutsideTokens('(literal)')\n * // → '(literal)' (not escaped here - processPattern handles it)\n */\nexport const escapeWordsOutsideTokens = (word: string): string =>\n word\n .split(/(\\{\\{[^}]+\\}\\})/g)\n .map((part) => (part.startsWith('{{') && part.endsWith('}}') ? part : part.replace(/[.*+?^${}|\\\\]/g, '\\\\$&')))\n .join('');\n\n/**\n * Normalizes a breakpoint to the object form.\n * Strings are converted to { pattern: str, split: 'after' } with no constraints.\n * Invalid `split` values are treated as `'after'` for backward compatibility.\n * If both `pattern` and `regex` are specified, `regex` takes precedence.\n *\n * When `words` is specified:\n * - Defaults `split` to `'at'` (can be overridden)\n * - Throws if combined with `pattern` or `regex`\n *\n * @param bp - Breakpoint as string or object\n * @returns Normalized BreakpointRule object with resolved pattern/regex\n *\n * @example\n * normalizeBreakpoint('\\\\n\\\\n')\n * // → { pattern: '\\\\n\\\\n', split: 'after' }\n *\n * normalizeBreakpoint({ pattern: '\\\\n', min: 10 })\n * // → { pattern: '\\\\n', min: 10, split: 'after' }\n *\n * normalizeBreakpoint({ pattern: 'X', split: 'at' })\n * // → { pattern: 'X', split: 'at' }\n *\n * normalizeBreakpoint({ words: ['فهذا', 'ثم'] })\n * // → { words: ['فهذا', 'ثم'], split: 'at' }\n */\nexport const normalizeBreakpoint = (bp: Breakpoint): BreakpointRule => {\n if (typeof bp === 'string') {\n return { pattern: bp, split: 'after' };\n }\n\n // Validate words mutual exclusivity\n if (bp.words && (bp.pattern !== undefined || bp.regex !== undefined)) {\n throw new Error('BreakpointRule: \"words\" cannot be combined with \"pattern\" or \"regex\"');\n }\n\n // Determine default split based on whether words is present\n const defaultSplit = bp.words ? 'at' : 'after';\n\n // Validate split value - treat invalid as default for backward compatibility\n const split = bp.split === 'at' || bp.split === 'after' ? bp.split : defaultSplit;\n\n return { ...bp, split };\n};\n\n/**\n * Checks if a page ID is in an excluded list (single pages or ranges).\n *\n * @param pageId - Page ID to check\n * @param excludeList - List of page IDs or [from, to] ranges to exclude\n * @returns True if page is excluded\n *\n * @example\n * isPageExcluded(5, [1, 5, 10])\n * // → true\n *\n * isPageExcluded(5, [[3, 7]])\n * // → true\n *\n * isPageExcluded(5, [[10, 20]])\n * // → false\n */\nexport const isPageExcluded = (pageId: number, excludeList: PageRange[] | undefined) =>\n excludeList?.some((item) =>\n typeof item === 'number' ? pageId === item : pageId >= item[0] && pageId <= item[1],\n ) ?? false;\n\n/**\n * Checks if a page ID is within a breakpoint's min/max range and not excluded.\n *\n * @param pageId - Page ID to check\n * @param rule - Breakpoint rule with optional min/max/exclude constraints\n * @returns True if page is within valid range\n *\n * @example\n * isInBreakpointRange(50, { pattern: '\\\\n', min: 10, max: 100 })\n * // → true\n *\n * isInBreakpointRange(5, { pattern: '\\\\n', min: 10 })\n * // → false (below min)\n */\nexport const isInBreakpointRange = (pageId: number, rule: BreakpointRule) => {\n const { min, max, exclude } = rule;\n return (\n (min === undefined || pageId >= min) && (max === undefined || pageId <= max) && !isPageExcluded(pageId, exclude)\n );\n};\n\n/**\n * Builds an exclude set from a PageRange array for O(1) lookups.\n *\n * @param excludeList - List of page IDs or [from, to] ranges\n * @returns Set of all excluded page IDs\n *\n * @remarks\n * This expands ranges into explicit page IDs for fast membership checks. For typical\n * book-scale inputs (thousands of pages), this is small and keeps downstream logic\n * simple and fast. If you expect extremely large ranges (e.g., millions of pages),\n * consider avoiding broad excludes or introducing a range-based membership structure.\n *\n * @example\n * buildExcludeSet([1, 5, [10, 12]])\n * // → Set { 1, 5, 10, 11, 12 }\n */\nexport const buildExcludeSet = (excludeList: PageRange[] | undefined) => {\n const excludeSet = new Set<number>();\n for (const item of excludeList || []) {\n if (typeof item === 'number') {\n excludeSet.add(item);\n } else {\n for (let i = item[0]; i <= item[1]; i++) {\n excludeSet.add(i);\n }\n }\n }\n return excludeSet;\n};\n\n/**\n * Creates a segment with optional to and meta fields.\n * Returns null if content is empty after trimming.\n *\n * @param content - Segment content\n * @param fromPageId - Starting page ID\n * @param toPageId - Optional ending page ID (omitted if same as from)\n * @param meta - Optional metadata to attach\n * @returns Segment object or null if empty\n *\n * @example\n * createSegment('Hello world', 1, 3, { chapter: 1 })\n * // → { content: 'Hello world', from: 1, to: 3, meta: { chapter: 1 } }\n *\n * createSegment(' ', 1, undefined, undefined)\n * // → null (empty content)\n */\nexport const createSegment = (\n content: string,\n fromPageId: number,\n toPageId: number | undefined,\n meta: Record<string, unknown> | undefined,\n): Segment | null => {\n const trimmed = content.trim();\n if (!trimmed) {\n return null;\n }\n return {\n content: trimmed,\n from: fromPageId,\n ...(toPageId !== undefined && toPageId !== fromPageId && { to: toPageId }),\n ...(meta && { meta }),\n };\n};\n\n/** Expanded breakpoint with pre-compiled regex and exclude set */\nexport type ExpandedBreakpoint = {\n rule: BreakpointRule;\n regex: RegExp | null;\n excludeSet: Set<number>;\n skipWhenRegex: RegExp | null;\n /** true = split AT match (new segment starts with match), false = split AFTER (default) */\n splitAt: boolean;\n};\n\n/** Function type for pattern processing */\nexport type PatternProcessor = (pattern: string) => string;\n\n/**\n * Expands breakpoint patterns and pre-computes exclude sets.\n *\n * @param breakpoints - Array of breakpoint patterns or rules\n * @param processPattern - Function to expand tokens in patterns\n * @returns Array of expanded breakpoints with compiled regexes\n *\n * @remarks\n * This function compiles regex patterns dynamically. This can be a ReDoS vector\n * if patterns come from untrusted sources. In typical usage, breakpoint rules\n * are application configuration, not user input.\n/**\n * @param processPattern - Function to expand tokens in patterns (with bracket escaping)\n * @param processRawPattern - Function to expand tokens without bracket escaping (for regex field)\n */\n/**\n * Builds regex source from words array.\n * Words are escaped, processed, sorted by length, and joined with alternation.\n */\nexport const buildWordsRegex = (words: string[], processPattern: PatternProcessor): string | null => {\n const processed = words\n .map((w, i) => ({ originalIndex: i, w: w.trimStart() }))\n .filter(({ w }) => w.length > 0)\n .map(({ w, originalIndex }) => ({\n originalIndex,\n pattern: processPattern(escapeWordsOutsideTokens(w)),\n }));\n\n if (processed.length === 0) {\n return null;\n }\n\n const seen = new Set<string>();\n const unique: typeof processed = [];\n\n for (const item of processed) {\n if (!seen.has(item.pattern)) {\n seen.add(item.pattern);\n unique.push(item);\n }\n }\n\n unique.sort((a, b) => b.pattern.length - a.pattern.length);\n\n const alternatives = unique.map((item) => `(?<_w${item.originalIndex}>${item.pattern})`);\n return `\\\\s+(?:${alternatives.join('|')})`;\n};\n\n/** Compiles skipWhen pattern to regex, or null if not present. */\nconst compileSkipWhenRegex = (rule: BreakpointRule, processPattern: PatternProcessor): RegExp | null => {\n if (rule.skipWhen === undefined) {\n return null;\n }\n const expandedSkip = processPattern(rule.skipWhen);\n try {\n return new RegExp(expandedSkip, 'mu');\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Invalid breakpoint skipWhen regex: ${rule.skipWhen}\\n Cause: ${message}`);\n }\n};\n\n/** Compiles a regex from a pattern string, throws descriptive error on failure. */\nconst compilePatternRegex = (pattern: string, fieldName: string): RegExp => {\n try {\n return new RegExp(pattern, 'gmu');\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Invalid breakpoint ${fieldName}: ${pattern}\\n Cause: ${message}`);\n }\n};\n\n/** Expands a single breakpoint to its expanded form. */\nconst expandSingleBreakpoint = (\n bp: Breakpoint,\n processPattern: PatternProcessor,\n processRawPattern?: PatternProcessor,\n): ExpandedBreakpoint | null => {\n const rule = normalizeBreakpoint(bp);\n const excludeSet = buildExcludeSet(rule.exclude);\n const skipWhenRegex = compileSkipWhenRegex(rule, processPattern);\n\n // Handle words field\n if (rule.words !== undefined) {\n const wordsPattern = buildWordsRegex(rule.words, processPattern);\n if (wordsPattern === null) {\n // Empty words array = no-op breakpoint (filter out)\n // This is NOT the same as '' which is page-boundary fallback\n return null;\n }\n const regex = compilePatternRegex(wordsPattern, `words: ${rule.words.join(', ')}`);\n return { excludeSet, regex, rule, skipWhenRegex, splitAt: rule.split === 'at' };\n }\n\n // Determine which pattern to use: regex takes precedence over pattern\n const rawPattern = rule.regex ?? rule.pattern;\n\n // Empty pattern = page boundary fallback\n if (rawPattern === '' || rawPattern === undefined) {\n return { excludeSet, regex: null, rule, skipWhenRegex, splitAt: false };\n }\n\n // Use raw processor if regex field is set and rawPatternProcessor is provided\n const useRawProcessor = rule.regex !== undefined && processRawPattern;\n const expanded = useRawProcessor ? processRawPattern(rawPattern) : processPattern(rawPattern);\n const fieldUsed = rule.regex !== undefined ? 'regex' : 'pattern';\n const regex = compilePatternRegex(expanded, fieldUsed);\n\n return { excludeSet, regex, rule, skipWhenRegex, splitAt: rule.split === 'at' };\n};\n\nexport const expandBreakpoints = (\n breakpoints: Breakpoint[],\n processPattern: PatternProcessor,\n processRawPattern?: PatternProcessor,\n): ExpandedBreakpoint[] =>\n breakpoints\n .map((bp) => expandSingleBreakpoint(bp, processPattern, processRawPattern))\n .filter((bp): bp is ExpandedBreakpoint => bp !== null);\n\n/** Normalized page data for efficient lookups */\nexport type NormalizedPage = { content: string; length: number; index: number };\n\n/**\n * Applies a configured joiner at detected page boundaries within a multi-page content chunk.\n *\n * This is used for breakpoint-generated segments which don't have access to the original\n * `pageMap.pageBreaks` offsets. We detect page starts sequentially by searching for each page's\n * prefix after the previous boundary, then replace ONLY the single newline immediately before\n * that page start.\n *\n * This avoids converting real in-page newlines, while still normalizing page joins consistently.\n */\nexport const applyPageJoinerBetweenPages = (\n content: string,\n fromIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n joiner: 'space' | 'newline',\n) => {\n if (joiner === 'newline' || fromIdx >= toIdx || !content.includes('\\n')) {\n return content;\n }\n\n let updated = content;\n let searchFrom = 0;\n\n for (let pi = fromIdx + 1; pi <= toIdx; pi++) {\n const pageData = normalizedPages.get(pageIds[pi]);\n if (!pageData) {\n continue;\n }\n\n const found = findPrefixPositionInContent(updated, pageData.content.trimStart(), searchFrom);\n if (found > 0 && updated[found - 1] === '\\n') {\n updated = `${updated.slice(0, found - 1)} ${updated.slice(found)}`;\n }\n if (found > 0) {\n searchFrom = found;\n }\n }\n\n return updated;\n};\n\n/**\n * Finds the position of a page prefix in content, trying multiple prefix lengths.\n */\nconst findPrefixPositionInContent = (content: string, trimmedPageContent: string, searchFrom: number) => {\n for (const len of JOINER_PREFIX_LENGTHS) {\n const prefix = trimmedPageContent.slice(0, Math.min(len, trimmedPageContent.length)).trim();\n if (!prefix) {\n continue;\n }\n const pos = content.indexOf(prefix, searchFrom);\n if (pos > 0) {\n return pos;\n }\n }\n return -1;\n};\n\n/**\n * Estimates how far into the current page `remainingContent` begins.\n *\n * During breakpoint processing, `remainingContent` can begin mid-page after a previous split.\n * When that happens, raw cumulative page offsets (computed from full page starts) can overestimate\n * expected boundary positions. This helper computes an approximate starting offset by matching\n * a short prefix of `remainingContent` inside the current page content.\n */\nexport const estimateStartOffsetInCurrentPage = (\n remainingContent: string,\n currentFromIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n) => {\n const currentPageData = normalizedPages.get(pageIds[currentFromIdx]);\n if (!currentPageData) {\n return 0;\n }\n\n // Optimization: slice a small chunk first to avoid trimStart() on potentially huge strings\n const remPrefix = remainingContent.slice(0, 500).trimStart();\n if (!remPrefix) {\n return 0;\n }\n\n // Try progressively shorter prefixes. This handles cases where remainingContent starts\n // near the end of the current page, causing a long needle to span across the page boundary\n // and fail to match. We start with longer prefixes for better uniqueness, falling back\n // to shorter ones if needed.\n const maxNeedleLen = Math.min(30, remPrefix.length);\n for (let len = maxNeedleLen; len >= 5; len -= 5) {\n const needle = remPrefix.slice(0, len);\n const idx = currentPageData.content.indexOf(needle);\n if (idx >= 0) {\n return idx;\n }\n }\n\n // Last resort: try very short prefix (3 chars) which has higher collision risk\n // but is better than returning 0 when we're truly at the end of a page\n if (remPrefix.length >= 3) {\n const needle = remPrefix.slice(0, 3);\n const idx = currentPageData.content.indexOf(needle);\n if (idx >= 0) {\n return idx;\n }\n }\n\n return 0;\n};\n\nconst estimateStartOffsetInCurrentPageFromEnd = (\n remainingContent: string,\n currentFromIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n) => {\n const currentPageData = normalizedPages.get(pageIds[currentFromIdx]);\n if (!currentPageData) {\n return 0;\n }\n\n const remPrefix = remainingContent.slice(0, 500).trimStart();\n if (!remPrefix) {\n return 0;\n }\n\n const maxNeedleLen = Math.min(30, remPrefix.length);\n for (let len = maxNeedleLen; len >= 5; len -= 5) {\n const needle = remPrefix.slice(0, len);\n const idx = currentPageData.content.lastIndexOf(needle);\n if (idx >= 0) {\n return idx;\n }\n }\n\n if (remPrefix.length >= 3) {\n const needle = remPrefix.slice(0, 3);\n const idx = currentPageData.content.lastIndexOf(needle);\n if (idx >= 0) {\n return idx;\n }\n }\n\n return 0;\n};\n\nconst selectStartOffsetInCurrentPage = (\n segmentContent: string,\n fromIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n cumulativeOffsets: number[],\n logger?: Logger,\n) => {\n const first = estimateStartOffsetInCurrentPage(segmentContent, fromIdx, pageIds, normalizedPages);\n const last = estimateStartOffsetInCurrentPageFromEnd(segmentContent, fromIdx, pageIds, normalizedPages);\n const candidates = [...new Set([first, last])];\n if (candidates.length <= 1 || fromIdx + 1 > toIdx) {\n return candidates[0] ?? 0;\n }\n\n const rawBoundary =\n cumulativeOffsets[fromIdx + 1] !== undefined && cumulativeOffsets[fromIdx] !== undefined\n ? Math.max(0, cumulativeOffsets[fromIdx + 1] - cumulativeOffsets[fromIdx])\n : undefined;\n if (rawBoundary === undefined) {\n return candidates[0] ?? 0;\n }\n\n let best = candidates[0] ?? 0;\n let bestScore = Number.POSITIVE_INFINITY;\n for (const candidate of candidates) {\n const expectedBoundary = Math.max(0, rawBoundary - candidate);\n const pos = findPageStartNearExpectedBoundary(\n segmentContent,\n fromIdx + 1,\n expectedBoundary,\n pageIds,\n normalizedPages,\n logger,\n );\n if (pos > 0) {\n const score = Math.abs(pos - expectedBoundary);\n if (score < bestScore) {\n bestScore = score;\n best = candidate;\n }\n }\n }\n\n return best;\n};\n\n/**\n * Attempts to find the start position of a target page within remainingContent,\n * anchored near an expected boundary position to reduce collisions.\n *\n * This is used to define breakpoint windows in terms of actual content being split, rather than\n * raw per-page offsets which can desync when structural rules strip markers.\n */\nexport const findPageStartNearExpectedBoundary = (\n remainingContent: string,\n targetPageIdx: number,\n expectedBoundary: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n logger?: Logger,\n) => {\n const targetPageData = normalizedPages.get(pageIds[targetPageIdx]);\n if (!targetPageData) {\n return -1;\n }\n\n // Anchor search near the expected boundary to avoid matching repeated phrases earlier in content.\n const approx = Math.min(Math.max(0, expectedBoundary), remainingContent.length);\n const searchStart = Math.max(0, approx - 10_000);\n const searchEnd = Math.min(remainingContent.length, approx + 2_000);\n\n const targetTrimmed = targetPageData.content.trimStart();\n const ignoreDeviation = expectedBoundary >= remainingContent.length;\n const scanStart = ignoreDeviation ? 0 : searchStart;\n const scanEnd = ignoreDeviation ? remainingContent.length : searchEnd;\n const expectedForRanking = ignoreDeviation ? 0 : expectedBoundary;\n for (const len of WINDOW_PREFIX_LENGTHS) {\n const prefix = targetTrimmed.slice(0, Math.min(len, targetTrimmed.length)).trim();\n if (!prefix) {\n continue;\n }\n\n const candidates = findAnchorCandidates(remainingContent, prefix, scanStart, scanEnd);\n if (candidates.length === 0) {\n continue;\n }\n\n // Only accept matches within MAX_DEVIATION of the expected boundary.\n // Prefer newline-preceded candidates *among valid matches*, otherwise choose the closest.\n const deviationLimit = ignoreDeviation ? Number.POSITIVE_INFINITY : MAX_DEVIATION;\n const inRange = candidates.filter((c) => Math.abs(c.pos - expectedBoundary) <= deviationLimit);\n if (inRange.length > 0) {\n const best = selectBestAnchor(inRange, expectedForRanking);\n return best.pos;\n }\n\n const bestOverall = selectBestAnchor(candidates, expectedForRanking);\n logger?.debug?.('[breakpoints] findPageStartNearExpectedBoundary: Rejected match exceeding deviation', {\n bestDistance: Math.abs(bestOverall.pos - expectedForRanking),\n expectedBoundary,\n matchPos: bestOverall.pos,\n maxDeviation: deviationLimit,\n prefixLength: len,\n targetPageIdx,\n });\n }\n\n return -1;\n};\n\n/** Internal candidate for page start anchoring */\ninterface AnchorCandidate {\n pos: number;\n isNewline: boolean;\n}\n\n/** Finds all whitespace-preceded occurrences of a prefix within a search range */\nconst findAnchorCandidates = (content: string, prefix: string, start: number, end: number) => {\n const candidates: AnchorCandidate[] = [];\n let pos = content.indexOf(prefix, start);\n\n while (pos !== -1 && pos <= end) {\n if (pos > 0) {\n const charBefore = content[pos - 1];\n if (charBefore === '\\n') {\n candidates.push({ isNewline: true, pos });\n } else if (/\\s/.test(charBefore)) {\n candidates.push({ isNewline: false, pos });\n }\n }\n pos = content.indexOf(prefix, pos + 1);\n }\n\n return candidates;\n};\n\n/** Selects the best anchor candidate, prioritizing newlines then proximity to boundary */\nconst selectBestAnchor = (candidates: AnchorCandidate[], expectedBoundary: number) => {\n // Penalty for not being a newline. This allows a newline candidate to \"win\"\n // against a whitespace candidate if it is reasonably close (e.g. within 20 chars),\n // which handles cases like offset drift from marker stripping.\n // However, it prevents a distant newline (e.g. 300+ chars away) from overriding\n // an exact whitespace match, ensuring we don't skip valid page boundaries just\n // because they were normalized to spaces.\n return candidates.reduce((best, curr) => {\n const bestScore = Math.abs(best.pos - expectedBoundary) + (best.isNewline ? 0 : NON_NEWLINE_PENALTY);\n const currScore = Math.abs(curr.pos - expectedBoundary) + (curr.isNewline ? 0 : NON_NEWLINE_PENALTY);\n return currScore < bestScore ? curr : best;\n });\n};\n\n/**\n * Finds the start position of a target page after a minimum position.\n * Used to avoid duplicate earlier matches when content repeats.\n */\nconst findPageStartAfterPosition = (\n remainingContent: string,\n targetPageIdx: number,\n minPos: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n) => {\n const targetPageData = normalizedPages.get(pageIds[targetPageIdx]);\n if (!targetPageData) {\n return -1;\n }\n\n const targetTrimmed = targetPageData.content.trimStart();\n for (const len of WINDOW_PREFIX_LENGTHS) {\n const prefix = targetTrimmed.slice(0, Math.min(len, targetTrimmed.length)).trim();\n if (!prefix) {\n continue;\n }\n const candidates = findAnchorCandidates(remainingContent, prefix, Math.max(0, minPos), remainingContent.length);\n const after = candidates.filter((c) => c.pos > minPos);\n if (after.length > 0) {\n return selectBestAnchor(after, minPos).pos;\n }\n }\n\n return -1;\n};\n\nconst buildBoundaryPositionsFastPath = (\n segmentContent: string,\n fromIdx: number,\n toIdx: number,\n pageCount: number,\n cumulativeOffsets: number[],\n logger?: Logger,\n) => {\n const boundaryPositions: number[] = [0];\n logger?.debug?.('[breakpoints] Using fast-path for large segment in buildBoundaryPositions', {\n fromIdx,\n pageCount,\n toIdx,\n });\n\n const baseOffset = cumulativeOffsets[fromIdx] ?? 0;\n for (let i = fromIdx + 1; i <= toIdx; i++) {\n const offset = cumulativeOffsets[i];\n if (offset !== undefined) {\n const boundary = Math.max(0, offset - baseOffset);\n const prevBoundary = boundaryPositions[boundaryPositions.length - 1];\n // Ensure strictly increasing boundaries\n boundaryPositions.push(Math.max(prevBoundary + 1, Math.min(boundary, segmentContent.length)));\n }\n }\n boundaryPositions.push(segmentContent.length); // sentinel\n return boundaryPositions;\n};\n\nconst isBoundaryPositionValid = (\n pos: number,\n prevBoundary: number,\n expectedBoundary: number,\n segmentLength: number,\n ignoreDeviation = false,\n) => {\n if (pos <= 0 || pos <= prevBoundary) {\n return false;\n }\n\n if (ignoreDeviation) {\n return true;\n }\n\n if (expectedBoundary >= segmentLength) {\n return true;\n }\n\n const deviationLimit = MAX_DEVIATION;\n return Math.abs(pos - expectedBoundary) < deviationLimit;\n};\n\nconst resolveBoundaryMatch = (\n segmentContent: string,\n pageIdx: number,\n rawBoundary: number | undefined,\n startOffsetInFromPage: number,\n canInferStartOffset: boolean,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n logger?: Logger,\n) => {\n let expectedBoundary =\n rawBoundary !== undefined ? Math.max(0, rawBoundary - startOffsetInFromPage) : segmentContent.length;\n let pos = findPageStartNearExpectedBoundary(\n segmentContent,\n pageIdx,\n expectedBoundary,\n pageIds,\n normalizedPages,\n logger,\n );\n let didInferStartOffset = false;\n\n if (pos < 0 && canInferStartOffset && rawBoundary !== undefined) {\n const relaxedPos = findPageStartNearExpectedBoundary(\n segmentContent,\n pageIdx,\n segmentContent.length,\n pageIds,\n normalizedPages,\n logger,\n );\n if (relaxedPos > 0) {\n const inferredStartOffset = rawBoundary - relaxedPos;\n const currentExpected = Math.max(0, rawBoundary - startOffsetInFromPage);\n\n // Only infer if match is reasonably close to expected position\n // This prevents inferring from early duplicates in content\n if (inferredStartOffset >= 0 && Math.abs(relaxedPos - currentExpected) < INFERENCE_PROXIMITY_LIMIT) {\n startOffsetInFromPage = inferredStartOffset;\n expectedBoundary = Math.max(0, rawBoundary - startOffsetInFromPage);\n pos = relaxedPos;\n didInferStartOffset = true;\n }\n }\n }\n\n return { didInferStartOffset, expectedBoundary, pos, startOffsetInFromPage };\n};\n\nconst buildBoundaryPositionsAccurate = (\n segmentContent: string,\n fromIdx: number,\n toIdx: number,\n pageCount: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n cumulativeOffsets: number[],\n logger?: Logger,\n) => {\n const boundaryPositions: number[] = [0];\n\n logger?.debug?.('[breakpoints] buildBoundaryPositions: Using accurate string-search path', {\n contentLength: segmentContent.length,\n fromIdx,\n pageCount,\n toIdx,\n });\n let startOffsetInFromPage = selectStartOffsetInCurrentPage(\n segmentContent,\n fromIdx,\n toIdx,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\n logger,\n );\n let didInferStartOffset = false;\n\n for (let i = fromIdx + 1; i <= toIdx; i++) {\n const rawBoundary =\n cumulativeOffsets[i] !== undefined && cumulativeOffsets[fromIdx] !== undefined\n ? Math.max(0, cumulativeOffsets[i] - cumulativeOffsets[fromIdx])\n : undefined;\n const resolved = resolveBoundaryMatch(\n segmentContent,\n i,\n rawBoundary,\n startOffsetInFromPage,\n !didInferStartOffset && i === fromIdx + 1,\n pageIds,\n normalizedPages,\n logger,\n );\n startOffsetInFromPage = resolved.startOffsetInFromPage;\n didInferStartOffset = didInferStartOffset || resolved.didInferStartOffset;\n\n const prevBoundary = boundaryPositions[boundaryPositions.length - 1];\n let resolvedPos = resolved.pos;\n if (resolvedPos <= prevBoundary) {\n const afterPos = findPageStartAfterPosition(segmentContent, i, prevBoundary + 1, pageIds, normalizedPages);\n if (afterPos > prevBoundary) {\n resolvedPos = afterPos;\n }\n }\n if (isBoundaryPositionValid(resolvedPos, prevBoundary, resolved.expectedBoundary, segmentContent.length)) {\n boundaryPositions.push(resolvedPos);\n } else {\n // Fallback for whitespace-only pages, identical content, or stripped markers.\n // Ensure estimate is strictly > prevBoundary to prevent duplicate zero-length\n // boundaries, which would break binary-search page-attribution logic.\n const estimate = Math.max(prevBoundary + 1, resolved.expectedBoundary);\n boundaryPositions.push(Math.min(estimate, segmentContent.length));\n }\n }\n\n boundaryPositions.push(segmentContent.length); // sentinel\n logger?.debug?.('[breakpoints] buildBoundaryPositions: Complete', { boundaryCount: boundaryPositions.length });\n return boundaryPositions;\n};\n\n/**\n * Builds a boundary position map for pages within the given range.\n *\n * This function computes page boundaries once per segment and enables\n * O(log n) page lookups via binary search with `findPageIndexForPosition`.\n *\n * Boundaries are derived from segmentContent (post-structural-rules).\n * When the segment starts mid-page, an offset correction is applied to\n * keep boundary estimates aligned with the segment's actual content space.\n *\n * @param segmentContent - Full segment content (already processed by structural rules)\n * @param fromIdx - Starting page index\n * @param toIdx - Ending page index\n * @param pageIds - Array of all page IDs\n * @param normalizedPages - Map of page ID to normalized content\n * @param cumulativeOffsets - Cumulative character offsets (for estimates)\n * @param logger - Optional logger for debugging\n * @returns Array where boundaryPositions[i] = start position of page (fromIdx + i),\n * with a sentinel boundary at segmentContent.length as the last element\n *\n * @example\n * // For a 3-page segment:\n * buildBoundaryPositions(content, 0, 2, pageIds, normalizedPages, offsets)\n * // → [0, 23, 45, 67] where 67 is content.length (sentinel)\n */\nexport const buildBoundaryPositions = (\n segmentContent: string,\n fromIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n cumulativeOffsets: number[],\n logger?: Logger,\n) => {\n const pageCount = toIdx - fromIdx + 1;\n\n // FAST PATH: For large segments (1000+ pages), use cumulative offsets directly.\n // The expensive string-search verification is only useful when structural rules\n // have stripped content causing offset drift. For large books with simple breakpoints,\n // the precomputed offsets are accurate and O(n) vs O(n×m) string searching.\n const expectedLength = (cumulativeOffsets[toIdx + 1] ?? 0) - (cumulativeOffsets[fromIdx] ?? 0);\n if (pageCount >= FAST_PATH_THRESHOLD && segmentContent.length === expectedLength) {\n return buildBoundaryPositionsFastPath(segmentContent, fromIdx, toIdx, pageCount, cumulativeOffsets, logger);\n }\n\n // ACCURATE PATH: For smaller segments, verify boundaries with string search\n // This handles cases where structural rules stripped markers causing offset drift\n // WARNING: This path is O(n×m) - if this log appears for large pageCount, investigate!\n return buildBoundaryPositionsAccurate(\n segmentContent,\n fromIdx,\n toIdx,\n pageCount,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\n logger,\n );\n};\n\n/**\n * Binary search to find which page a position falls within.\n * Uses \"largest i where boundaryPositions[i] <= position\" semantics.\n *\n * @param position - Character position in segmentContent\n * @param boundaryPositions - Precomputed boundary positions (from buildBoundaryPositions)\n * @param fromIdx - Base page index (boundaryPositions[0] corresponds to pageIds[fromIdx])\n * @returns Page index in pageIds array\n *\n * @example\n * // With boundaries [0, 20, 40, 60] and fromIdx=0:\n * findPageIndexForPosition(15, boundaries, 0) // → 0 (first page)\n * findPageIndexForPosition(25, boundaries, 0) // → 1 (second page)\n * findPageIndexForPosition(40, boundaries, 0) // → 2 (exactly on boundary = that page)\n */\nexport const findPageIndexForPosition = (position: number, boundaryPositions: number[], fromIdx: number) => {\n // Handle edge cases\n if (boundaryPositions.length <= 1) {\n return fromIdx;\n }\n\n // Binary search for largest i where boundaryPositions[i] <= position\n let left = 0;\n let right = boundaryPositions.length - 2; // Exclude sentinel\n\n while (left < right) {\n const mid = Math.ceil((left + right) / 2);\n if (boundaryPositions[mid] <= position) {\n left = mid;\n } else {\n right = mid - 1;\n }\n }\n\n return fromIdx + left;\n};\n/**\n * Finds the end position of a breakpoint window inside `remainingContent`.\n *\n * The window end is defined as the start of the page AFTER `windowEndIdx` (i.e. `windowEndIdx + 1`),\n * found within the actual `remainingContent` string being split. This avoids relying on raw page offsets\n * that can diverge when structural rules strip markers (e.g. `lineStartsAfter`).\n */\nexport const findBreakpointWindowEndPosition = (\n remainingContent: string,\n currentFromIdx: number,\n windowEndIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n cumulativeOffsets: number[],\n logger?: Logger,\n) => {\n // If the window already reaches the end of the segment, the window is the remaining content.\n if (windowEndIdx >= toIdx) {\n return remainingContent.length;\n }\n\n const desiredNextIdx = windowEndIdx + 1;\n const minNextIdx = currentFromIdx + 1;\n const maxNextIdx = Math.min(desiredNextIdx, toIdx);\n\n const startOffsetInCurrentPage = estimateStartOffsetInCurrentPage(\n remainingContent,\n currentFromIdx,\n pageIds,\n normalizedPages,\n );\n\n // Track the best expected boundary for fallback\n let bestExpectedBoundary = remainingContent.length;\n\n // If we can't find the boundary for the desired next page, progressively fall back\n // to earlier page boundaries (smaller window), which is conservative but still correct.\n for (let nextIdx = maxNextIdx; nextIdx >= minNextIdx; nextIdx--) {\n const expectedBoundary =\n cumulativeOffsets[nextIdx] !== undefined && cumulativeOffsets[currentFromIdx] !== undefined\n ? Math.max(0, cumulativeOffsets[nextIdx] - cumulativeOffsets[currentFromIdx] - startOffsetInCurrentPage)\n : remainingContent.length;\n\n // Keep track of the expected boundary for fallback\n if (nextIdx === maxNextIdx) {\n bestExpectedBoundary = expectedBoundary;\n }\n\n const pos = findPageStartNearExpectedBoundary(\n remainingContent,\n nextIdx,\n expectedBoundary,\n pageIds,\n normalizedPages,\n logger,\n );\n if (pos > 0) {\n return pos;\n }\n }\n\n // Fallback: Use the expected boundary from cumulative offsets.\n // This is more accurate than returning remainingContent.length, which would\n // merge all remaining pages into one segment.\n return Math.min(bestExpectedBoundary, remainingContent.length);\n};\n\n/**\n * Finds exclusion-based break position using raw cumulative offsets.\n *\n * This is used to ensure pages excluded by breakpoints are never merged into the same output segment.\n * Returns a break position relative to the start of `remainingContent` (i.e. the currentFromIdx start).\n */\nexport const findExclusionBreakPosition = (\n currentFromIdx: number,\n windowEndIdx: number,\n toIdx: number,\n pageIds: number[],\n expandedBreakpoints: Array<{ excludeSet: Set<number> }>,\n cumulativeOffsets: number[],\n) => {\n const startingPageId = pageIds[currentFromIdx];\n const startingPageExcluded = expandedBreakpoints.some((bp) => bp.excludeSet.has(startingPageId));\n if (startingPageExcluded && currentFromIdx < toIdx) {\n // Output just this one page as a segment (break at next page boundary)\n return cumulativeOffsets[currentFromIdx + 1] - cumulativeOffsets[currentFromIdx];\n }\n\n // Find the first excluded page AFTER the starting page (within window) and split BEFORE it\n for (let pageIdx = currentFromIdx + 1; pageIdx <= windowEndIdx; pageIdx++) {\n const pageId = pageIds[pageIdx];\n const isExcluded = expandedBreakpoints.some((bp) => bp.excludeSet.has(pageId));\n if (isExcluded) {\n return cumulativeOffsets[pageIdx] - cumulativeOffsets[currentFromIdx];\n }\n }\n return -1;\n};\n\n/** Context required for finding break positions */\nexport type BreakpointContext = {\n pageIds: number[];\n normalizedPages: Map<number, NormalizedPage>;\n expandedBreakpoints: ExpandedBreakpoint[];\n prefer: 'longer' | 'shorter';\n};\n\n/**\n * Checks if any page in a range is excluded by the given exclude set.\n *\n * @param excludeSet - Set of excluded page IDs\n * @param pageIds - Array of page IDs\n * @param fromIdx - Start index (inclusive)\n * @param toIdx - End index (inclusive)\n * @returns True if any page in range is excluded\n */\nexport const hasExcludedPageInRange = (excludeSet: Set<number>, pageIds: number[], fromIdx: number, toIdx: number) => {\n if (excludeSet.size === 0) {\n return false;\n }\n for (let pageIdx = fromIdx; pageIdx <= toIdx; pageIdx++) {\n if (excludeSet.has(pageIds[pageIdx])) {\n return true;\n }\n }\n return false;\n};\n\n/**\n * Finds the position of the next page content within remaining content.\n * Returns -1 if not found.\n *\n * @param remainingContent - Content to search in\n * @param nextPageData - Normalized data for the next page\n * @returns Position of next page content, or -1 if not found\n */\nexport const findNextPagePosition = (remainingContent: string, nextPageData: NormalizedPage) => {\n const searchPrefix = nextPageData.content.trim().slice(0, Math.min(30, nextPageData.length));\n if (searchPrefix.length === 0) {\n return -1;\n }\n const pos = remainingContent.indexOf(searchPrefix);\n return pos > 0 ? pos : -1;\n};\n\n/**\n * Finds matches within a window and returns the selected position based on preference and split mode.\n *\n * @param windowContent - Content to search\n * @param regex - Regex to match\n * @param prefer - 'longer' for last match, 'shorter' for first match\n * @param splitAt - If true, return position BEFORE match (at index). If false, return position AFTER match (at index + length).\n * @returns Break position, or -1 if no valid matches\n *\n * @remarks\n * - Matches with length 0 are skipped (prevents infinite loops with lookahead patterns)\n * - Matches that would result in position 0 are skipped (prevents empty first segments)\n * - For prefer:'shorter', returns immediately on first valid match (optimization)\n */\nexport const findPatternBreakPosition = (\n windowContent: string,\n regex: RegExp,\n prefer: 'longer' | 'shorter',\n splitAt = false,\n): { pos: number; groups?: Record<string, string> } => {\n // Track last valid match for 'longer' preference\n let last: { index: number; length: number; groups?: Record<string, string> } | undefined;\n\n for (const m of windowContent.matchAll(regex)) {\n const idx = m.index ?? -1;\n const len = m[0]?.length ?? 0;\n\n // Skip invalid matches: negative index, zero-length (lookahead)\n if (idx < 0 || len === 0) {\n continue;\n }\n\n // Compute break position based on split mode\n const pos = splitAt ? idx : idx + len;\n\n // Skip position 0 - would create empty first segment\n if (pos === 0) {\n continue;\n }\n\n last = { groups: m.groups, index: idx, length: len };\n\n // Early return for 'shorter' (first valid match)\n if (prefer === 'shorter') {\n return { groups: m.groups, pos };\n }\n }\n\n if (!last) {\n return { pos: -1 };\n }\n\n // For 'longer', use last valid match\n const finalPos = splitAt ? last.index : last.index + last.length;\n return { groups: last.groups, pos: finalPos };\n};\n\n/**\n * Handles page boundary breakpoint (empty pattern).\n * Returns break position or -1 if no valid position found.\n */\nconst findStartOfNextPageInWindow = (\n remainingContent: string,\n currentFromIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n targetPos: number,\n) => {\n const targetNextPageIdx = currentFromIdx + 1;\n\n // Progressively try to find the boundary if detection fails (conservative fallback).\n for (let nextIdx = targetNextPageIdx; nextIdx > currentFromIdx; nextIdx--) {\n if (nextIdx <= toIdx) {\n const nextPageData = normalizedPages.get(pageIds[nextIdx]);\n if (nextPageData) {\n const boundaryPos = findNextPagePosition(remainingContent, nextPageData);\n if (boundaryPos > 0 && boundaryPos <= targetPos) {\n return boundaryPos;\n }\n }\n }\n }\n return -1;\n};\nconst handlePageBoundaryBreak = (\n remainingContent: string,\n currentFromIdx: number,\n windowEndPosition: number,\n maxContentLength: number | undefined,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n) => {\n // Page-boundary breakpoint (empty pattern '').\n //\n // Semantics: when no other breakpoint patterns match, break at the NEXT PAGE boundary\n // (i.e. swallow the remainder of the current page), not at the end of the maxPages window.\n //\n // This ensures that with maxPages=0 each page stays isolated, and with maxPages>0\n // we don't accidentally swallow the next page when no pattern matches.\n const targetPos = Math.min(windowEndPosition, remainingContent.length);\n\n // If the window is currently bounded by maxContentLength, do NOT force an early page-boundary break.\n // In length-bounded mode, we want the best possible split *near* the length limit (using safe-break fallbacks),\n // even if that spans a page boundary.\n const isLengthBounded = maxContentLength !== undefined && windowEndPosition === maxContentLength;\n\n if (!isLengthBounded) {\n // Page-bounded window semantics: swallow the remainder of the CURRENT page.\n // Even if the maxPages window could include additional pages, an empty breakpoint ('')\n // must not consume into the next page when no real breakpoint patterns matched.\n const boundaryPos = findStartOfNextPageInWindow(\n remainingContent,\n currentFromIdx,\n toIdx,\n pageIds,\n normalizedPages,\n targetPos,\n );\n if (boundaryPos > 0) {\n return { pos: boundaryPos };\n }\n }\n\n // If we couldn't reliably detect the boundary (or we're at the end), fall back to a safe split\n // within the window to avoid mid-word / surrogate corruption.\n if (targetPos < remainingContent.length) {\n const safePos = findSafeBreakPosition(remainingContent, targetPos);\n if (safePos !== -1) {\n return {\n pos: safePos,\n splitReason: isLengthBounded ? ('whitespace' as const) : undefined,\n };\n }\n return {\n pos: adjustForUnicodeBoundary(remainingContent, targetPos),\n splitReason: isLengthBounded ? ('unicode_boundary' as const) : undefined,\n };\n }\n return { pos: targetPos };\n};\n\nconst checkBreakpointMatch = (\n i: number,\n remainingContent: string,\n currentFromIdx: number,\n toIdx: number,\n windowEndIdx: number,\n windowEndPosition: number,\n ctx: BreakpointContext,\n maxContentLength?: number,\n) => {\n const { pageIds, normalizedPages, expandedBreakpoints, prefer } = ctx;\n const bpCtx = expandedBreakpoints[i];\n const { rule, regex, excludeSet, skipWhenRegex } = bpCtx;\n\n // Check if this breakpoint applies to the current segment's starting page\n if (!isInBreakpointRange(pageIds[currentFromIdx], rule)) {\n return null;\n }\n\n // Check if ANY page in the current WINDOW is excluded (not the entire segment)\n if (hasExcludedPageInRange(excludeSet, pageIds, currentFromIdx, windowEndIdx)) {\n return null;\n }\n\n // Check if content matches skipWhen pattern (pre-compiled)\n if (skipWhenRegex?.test(remainingContent)) {\n return null;\n }\n\n // Handle page boundary (empty pattern)\n if (regex === null) {\n const result = handlePageBoundaryBreak(\n remainingContent,\n currentFromIdx,\n windowEndPosition,\n maxContentLength,\n toIdx,\n pageIds,\n normalizedPages,\n );\n return {\n breakPos: result.pos,\n breakpointIndex: i,\n contentLengthSplit:\n result.splitReason && maxContentLength ? { maxContentLength, reason: result.splitReason } : undefined,\n rule,\n };\n }\n\n // Find matches within window\n const windowContent = remainingContent.slice(0, Math.min(windowEndPosition, remainingContent.length));\n const { pos: breakPos, groups } = findPatternBreakPosition(windowContent, regex, prefer, bpCtx.splitAt);\n\n if (breakPos > 0) {\n const wordIndex = extractDebugIndex(groups, '_w');\n return { breakPos, breakpointIndex: i, rule, wordIndex };\n }\n\n return null;\n};\n\n/**\n * Tries to find a break position within the current window using breakpoint patterns.\n * Returns the break position or -1 if no suitable break was found.\n *\n * @param remainingContent - Content remaining to be segmented\n * @param currentFromIdx - Current starting page index\n * @param toIdx - Ending page index\n * @param windowEndIdx - Maximum window end index\n * @param ctx - Breakpoint context with page data and patterns\n * @returns Break position in the content, or -1 if no break found\n */\nexport const findBreakPosition = (\n remainingContent: string,\n currentFromIdx: number,\n toIdx: number,\n windowEndIdx: number,\n windowEndPosition: number,\n ctx: BreakpointContext,\n maxContentLength?: number,\n): BreakpointMatch | null => {\n const { expandedBreakpoints } = ctx;\n\n for (let i = 0; i < expandedBreakpoints.length; i++) {\n const match = checkBreakpointMatch(\n i,\n remainingContent,\n currentFromIdx,\n toIdx,\n windowEndIdx,\n windowEndPosition,\n ctx,\n maxContentLength,\n );\n if (match) {\n return match;\n }\n }\n\n return null;\n};\n\n/**\n * Searches backward from a target position to find a \"safe\" split point.\n * A safe split point is after whitespace or punctuation.\n *\n * @param content The text content\n * @param targetPosition The desired split position (hard limit)\n * @param lookbackChars How far back to search for a safe break\n * @returns The new split position (index), or -1 if no safe break found\n */\nexport const findSafeBreakPosition = (content: string, targetPosition: number, lookbackChars = 100) => {\n // 1. Sanity check bounds\n const startSearch = Math.max(0, targetPosition - lookbackChars);\n\n // 2. Iterate backward\n for (let i = targetPosition - 1; i >= startSearch; i--) {\n const char = content[i];\n\n // Check for safe delimiter: Whitespace or Punctuation\n if (STOP_CHARACTERS.test(char)) {\n return i + 1;\n }\n }\n return -1;\n};\n\n/**\n * Ensures the position does not split a surrogate pair.\n * If position is between High and Low surrogate, returns position - 1.\n */\nexport const adjustForSurrogate = (content: string, position: number) => {\n if (position <= 0 || position >= content.length) {\n return position;\n }\n\n const high = content.charCodeAt(position - 1);\n const low = content.charCodeAt(position);\n\n // Check if previous char is High Surrogate (0xD800–0xDBFF)\n // AND current char is Low Surrogate (0xDC00–0xDFFF)\n if (high >= 0xd800 && high <= 0xdbff && low >= 0xdc00 && low <= 0xdfff) {\n return position - 1;\n }\n\n return position;\n};\n","import type { BreakpointRule } from '@/types/breakpoints';\nimport type { Segment } from '@/types/index.js';\nimport { PATTERN_TYPE_KEYS, type SplitRule } from '@/types/rules.js';\n\nexport type DebugConfig = { includeBreakpoint: boolean; includeRule: boolean; metaKey: string } | null;\n\nexport const resolveDebugConfig = (debug: unknown) => {\n if (debug === true) {\n return { includeBreakpoint: true, includeRule: true, metaKey: '_flappa' };\n }\n\n if (!debug || typeof debug !== 'object') {\n return null;\n }\n\n const { metaKey, include } = debug as any;\n const includeRule = Array.isArray(include) ? include.includes('rule') : true;\n const includeBreakpoint = Array.isArray(include) ? include.includes('breakpoint') : true;\n return { includeBreakpoint, includeRule, metaKey: typeof metaKey === 'string' && metaKey ? metaKey : '_flappa' };\n};\n\nexport const getRulePatternType = (rule: SplitRule) => {\n return PATTERN_TYPE_KEYS.find((key) => key in rule) ?? 'regex';\n};\n\nconst isPlainObject = (v: unknown): v is Record<string, unknown> =>\n Boolean(v) && typeof v === 'object' && !Array.isArray(v);\n\nexport const mergeDebugIntoMeta = (\n meta: Record<string, unknown> | undefined,\n metaKey: string,\n patch: Record<string, unknown>,\n) => {\n const out = meta ? { ...meta } : {};\n const existing = out[metaKey];\n out[metaKey] = { ...(isPlainObject(existing) ? existing : {}), ...patch };\n return out;\n};\n\nexport const buildRuleDebugPatch = (ruleIndex: number, rule: SplitRule, wordIndex?: number) => {\n const patternType = getRulePatternType(rule);\n const patterns = (rule as any)[patternType];\n const word =\n wordIndex !== undefined && Array.isArray(patterns) && patterns[wordIndex] !== undefined\n ? patterns[wordIndex]\n : undefined;\n\n return {\n rule: {\n index: ruleIndex,\n patternType,\n ...(wordIndex !== undefined ? { wordIndex } : {}),\n ...(word !== undefined ? { word } : {}),\n },\n };\n};\n\nexport const buildBreakpointDebugPatch = (breakpointIndex: number, rule: BreakpointRule, wordIndex?: number) => ({\n breakpoint: {\n index: breakpointIndex,\n kind: rule.pattern === '' ? 'pageBoundary' : rule.regex ? 'regex' : 'pattern',\n pattern: rule.pattern ?? rule.regex,\n ...(wordIndex !== undefined ? { wordIndex } : {}),\n ...(wordIndex !== undefined && rule.words ? { word: rule.words[wordIndex] } : {}),\n },\n});\n\nexport type ContentLengthSplitReason = 'whitespace' | 'unicode_boundary' | 'grapheme_cluster';\n\nexport const buildContentLengthDebugPatch = (\n maxContentLength: number,\n actualLength: number,\n splitReason: ContentLengthSplitReason = 'whitespace',\n) => ({\n contentLengthSplit: {\n actualLength,\n maxContentLength,\n splitReason,\n },\n});\n\n/**\n * Options for formatting the debug reason.\n */\nexport type DebugReasonOptions = {\n /**\n * If true, returns a concise string representation.\n * e.g. 'Rule: \"Chapter\"' instead of 'Rule #1 (lineStartsWith) [idx:0] (Matched: \"Chapter\")'\n */\n concise?: boolean;\n};\n\n/**\n * Helper to format the debug info into a human-readable string.\n * @param meta - The segment metadata object\n * @param options - Formatting options\n */\nconst formatRuleReason = (rule: any, concise?: boolean) => {\n const { index, patternType, wordIndex, word } = rule;\n\n if (concise) {\n // \"Rule: <value>\" (value is word or patternType)\n const val = word ? `\"${word}\"` : patternType;\n return `Rule: ${val}`;\n }\n\n const wordInfo = word ? ` (Matched: \"${word}\")` : '';\n const indexInfo = wordIndex !== undefined ? ` [idx:${wordIndex}]` : '';\n return `Rule #${index} (${patternType})${indexInfo}${wordInfo}`;\n};\n\nconst formatBreakpointReason = (breakpoint: any, concise?: boolean) => {\n const { index, kind, pattern, wordIndex, word } = breakpoint;\n\n if (kind === 'pageBoundary') {\n return concise ? 'Breakpoint: <page-boundary>' : 'Page Boundary (Fallback)';\n }\n\n if (concise) {\n // \"Breakpoint: <value>\" (value is word or pattern)\n const val = word ? `\"${word}\"` : `\"${pattern}\"`;\n return `Breakpoint: ${val}`;\n }\n\n // For words array matches\n if (word) {\n return `Breakpoint #${index} (Words) [idx:${wordIndex}] - \"${word}\"`;\n }\n\n // For standard patterns\n return `Breakpoint #${index} (${kind}) - \"${pattern}\"`;\n};\n\nconst formatContentLengthReason = (split: any, concise?: boolean) => {\n const { maxContentLength, splitReason } = split;\n if (concise) {\n return `> ${maxContentLength} (${splitReason})`;\n }\n return `Safety Split (${splitReason}) > ${maxContentLength}`;\n};\n\n/**\n * Helper to format the debug info into a human-readable string.\n * @param meta - The segment metadata object\n * @param options - Formatting options\n */\nexport const getDebugReason = (meta: Record<string, any> | undefined, options?: DebugReasonOptions) => {\n const debug = meta?._flappa;\n if (!debug) {\n return '-';\n }\n\n const concise = options?.concise;\n\n if (debug.rule) {\n return formatRuleReason(debug.rule, concise);\n }\n\n if (debug.breakpoint) {\n return formatBreakpointReason(debug.breakpoint, concise);\n }\n\n if (debug.contentLengthSplit) {\n return formatContentLengthReason(debug.contentLengthSplit, concise);\n }\n\n return 'Unknown';\n};\n\n/**\n * Convenience helper to get the formatted debug reason directly from a segment.\n * @param segment - The segment object\n * @param options - Formatting options\n */\nexport const getSegmentDebugReason = (segment: Segment, options?: DebugReasonOptions) => {\n return getDebugReason(segment.meta, options);\n};\n","/**\n * Pattern validation utilities for detecting common mistakes in rule patterns.\n *\n * These utilities help catch typos and issues early, before rules are used\n * for segmentation.\n */\n\nimport type { DictionaryEntryPatternOptions, SplitRule } from '@/types/rules.js';\nimport { getAvailableTokens } from './tokens.js';\n\n/**\n * Types of validation issues that can be detected.\n */\nexport type ValidationIssueType =\n | 'missing_braces'\n | 'unknown_token'\n | 'duplicate'\n | 'empty_pattern'\n | 'invalid_regex'\n | 'invalid_option';\n\n/**\n * A validation issue found in a pattern.\n */\nexport type ValidationIssue = {\n type: ValidationIssueType;\n message: string;\n suggestion?: string;\n /** The token name involved in the issue (for unknown_token / missing_braces) */\n token?: string;\n /** The specific pattern involved (for duplicate) */\n pattern?: string;\n};\n\n/**\n * Validation result for a single rule, with issues keyed by pattern type.\n * Arrays parallel the input pattern arrays - undefined means no issue.\n */\nexport type RuleValidationResult = {\n lineStartsWith?: (ValidationIssue | undefined)[];\n lineStartsAfter?: (ValidationIssue | undefined)[];\n lineEndsWith?: (ValidationIssue | undefined)[];\n template?: ValidationIssue;\n regex?: ValidationIssue;\n dictionaryEntry?: Partial<Record<keyof DictionaryEntryPatternOptions, ValidationIssue>>;\n};\n\n// Known token names from the tokens module\nconst KNOWN_TOKENS = new Set(getAvailableTokens());\n\n// Regex to find tokens inside {{}} - both with and without capture syntax\nconst TOKEN_INSIDE_BRACES = /\\{\\{(\\w+)(?::\\w+)?\\}\\}/g;\n\n// Regex to find potential token names NOT inside {{}}\n// Matches word boundaries around known token names\nconst buildBareTokenRegex = () => {\n const tokens = [...KNOWN_TOKENS].sort((a, b) => b.length - a.length);\n return new RegExp(`(?<!\\\\{\\\\{)(${tokens.join('|')})(?::\\\\w+)?(?!\\\\}\\\\})`, 'g');\n};\n\n/**\n * Validates a single pattern for common issues.\n */\nconst validatePattern = (pattern: string, seenPatterns: Set<string>) => {\n if (!pattern.trim()) {\n return { message: 'Empty pattern is not allowed', type: 'empty_pattern' } as const;\n }\n if (seenPatterns.has(pattern)) {\n return { message: `Duplicate pattern: \"${pattern}\"`, pattern, type: 'duplicate' } as const;\n }\n seenPatterns.add(pattern);\n\n // TOKEN_INSIDE_BRACES is a global /g regex. Ensure lastIndex does not leak between calls.\n TOKEN_INSIDE_BRACES.lastIndex = 0;\n for (const match of pattern.matchAll(TOKEN_INSIDE_BRACES)) {\n const name = match[1];\n if (!KNOWN_TOKENS.has(name)) {\n return {\n message: `Unknown token: {{${name}}}. Available tokens: ${[...KNOWN_TOKENS].slice(0, 5).join(', ')}...`,\n suggestion: 'Check spelling or use a known token',\n token: name,\n type: 'unknown_token',\n } as const;\n }\n }\n\n for (const match of pattern.matchAll(buildBareTokenRegex())) {\n const [full, name] = match;\n const idx = match.index!;\n if (\n pattern.slice(Math.max(0, idx - 2), idx) !== '{{' ||\n pattern.slice(idx + full.length, idx + full.length + 2) !== '}}'\n ) {\n return {\n message: `Token \"${name}\" appears to be missing {{}}. Did you mean \"{{${full}}}\"?`,\n suggestion: `{{${full}}}`,\n token: name,\n type: 'missing_braces',\n } as const;\n }\n }\n};\n\n/**\n * Validates an array of patterns, returning parallel array of issues.\n */\nconst validatePatternArray = (patterns: string[]) => {\n const seen = new Set<string>();\n const issues = patterns.map((p) => validatePattern(p, seen));\n return issues.some(Boolean) ? issues : undefined;\n};\n\nconst applyRulePatternValidation = (\n result: RuleValidationResult,\n key: 'lineStartsWith' | 'lineStartsAfter' | 'lineEndsWith',\n patterns: string[] | undefined,\n): boolean => {\n if (!patterns) {\n return false;\n }\n const issues = validatePatternArray(patterns);\n if (!issues) {\n return false;\n }\n result[key] = issues;\n return true;\n};\n\nconst validateTemplateRule = (rule: SplitRule, result: RuleValidationResult) => {\n if (rule.template === undefined) {\n return false;\n }\n\n const issue = validatePattern(rule.template, new Set());\n if (!issue) {\n return false;\n }\n\n result.template = issue;\n return true;\n};\n\nconst validateRegexRule = (rule: SplitRule, result: RuleValidationResult) => {\n if (rule.regex === undefined) {\n return false;\n }\n\n if (!rule.regex.trim()) {\n result.regex = { message: 'Empty pattern is not allowed', type: 'empty_pattern' };\n return true;\n }\n\n try {\n new RegExp(rule.regex, 'u');\n return false;\n } catch (error) {\n result.regex = {\n message: error instanceof Error ? error.message : String(error),\n pattern: rule.regex,\n type: 'invalid_regex',\n };\n return true;\n }\n};\n\nconst invalidDictionaryEntryIssue = (message: string): ValidationIssue => ({\n message,\n type: 'invalid_option',\n});\n\nconst validateDictionaryEntryRule = (rule: SplitRule, result: RuleValidationResult) => {\n if (!('dictionaryEntry' in rule) || !rule.dictionaryEntry) {\n return false;\n }\n\n const issues: Partial<Record<keyof DictionaryEntryPatternOptions, ValidationIssue>> = {};\n const {\n allowCommaSeparated,\n allowParenthesized,\n allowWhitespaceBeforeColon,\n captureName,\n maxLetters,\n midLineSubentries,\n minLetters,\n stopWords,\n } = rule.dictionaryEntry;\n\n if (!Array.isArray(stopWords) || stopWords.some((word) => typeof word !== 'string' || !word.trim())) {\n issues.stopWords = invalidDictionaryEntryIssue('stopWords must be a string[] with non-empty entries');\n }\n if (allowCommaSeparated !== undefined && typeof allowCommaSeparated !== 'boolean') {\n issues.allowCommaSeparated = invalidDictionaryEntryIssue('allowCommaSeparated must be a boolean');\n }\n if (allowParenthesized !== undefined && typeof allowParenthesized !== 'boolean') {\n issues.allowParenthesized = invalidDictionaryEntryIssue('allowParenthesized must be a boolean');\n }\n if (allowWhitespaceBeforeColon !== undefined && typeof allowWhitespaceBeforeColon !== 'boolean') {\n issues.allowWhitespaceBeforeColon = invalidDictionaryEntryIssue('allowWhitespaceBeforeColon must be a boolean');\n }\n if (midLineSubentries !== undefined && typeof midLineSubentries !== 'boolean') {\n issues.midLineSubentries = invalidDictionaryEntryIssue('midLineSubentries must be a boolean');\n }\n if (captureName !== undefined && !captureName.match(/^[A-Za-z_]\\w*$/)) {\n issues.captureName = invalidDictionaryEntryIssue(\n `captureName must match /^[A-Za-z_]\\\\w*$/, got \"${captureName}\"`,\n );\n }\n if (minLetters !== undefined && (!Number.isInteger(minLetters) || minLetters < 1)) {\n issues.minLetters = invalidDictionaryEntryIssue('minLetters must be an integer >= 1');\n }\n if (maxLetters !== undefined && (!Number.isInteger(maxLetters) || maxLetters < (minLetters ?? 2))) {\n issues.maxLetters = invalidDictionaryEntryIssue(`maxLetters must be an integer >= ${minLetters ?? 2}`);\n }\n\n if (Object.keys(issues).length === 0) {\n return false;\n }\n\n result.dictionaryEntry = issues;\n return true;\n};\n\nconst formatValidationIssue = (_type: string, issue: ValidationIssue | undefined, loc: string): string | null => {\n if (!issue) {\n return null;\n }\n if (issue.type === 'missing_braces') {\n return `${loc}: Missing {{}} around token \"${issue.token}\"`;\n }\n if (issue.type === 'unknown_token') {\n return `${loc}: Unknown token \"{{${issue.token}}}\"`;\n }\n if (issue.type === 'duplicate') {\n return `${loc}: Duplicate pattern \"${issue.pattern}\"`;\n }\n if (issue.type === 'invalid_regex') {\n return `${loc}: Invalid regex (${issue.message})`;\n }\n return `${loc}: ${issue.message || issue.type}`;\n};\n\n/**\n * Validates split rules for common pattern issues.\n *\n * Checks for:\n * - Missing `{{}}` around known token names (e.g., `raqms:num` instead of `{{raqms:num}}`)\n * - Unknown token names inside `{{}}` (e.g., `{{nonexistent}}`)\n * - Duplicate patterns within the same rule\n *\n * @param rules - Array of split rules to validate\n * @returns Array parallel to input with validation results (undefined if no issues)\n *\n * @example\n * const issues = validateRules([\n * { lineStartsAfter: ['raqms:num'] }, // Missing braces\n * { lineStartsWith: ['{{unknown}}'] }, // Unknown token\n * ]);\n * // issues[0]?.lineStartsAfter?.[0]?.type === 'missing_braces'\n * // issues[1]?.lineStartsWith?.[0]?.type === 'unknown_token'\n */\nexport const validateRules = (rules: SplitRule[]) =>\n rules.map((rule) => {\n const result: RuleValidationResult = {};\n const startsWithIssues = applyRulePatternValidation(result, 'lineStartsWith', rule.lineStartsWith);\n const startsAfterIssues = applyRulePatternValidation(result, 'lineStartsAfter', rule.lineStartsAfter);\n const endsWithIssues = applyRulePatternValidation(result, 'lineEndsWith', rule.lineEndsWith);\n const templateIssues = validateTemplateRule(rule, result);\n const regexIssues = validateRegexRule(rule, result);\n const dictionaryEntryIssues = validateDictionaryEntryRule(rule, result);\n const hasIssues =\n startsWithIssues ||\n startsAfterIssues ||\n endsWithIssues ||\n templateIssues ||\n regexIssues ||\n dictionaryEntryIssues;\n\n return hasIssues ? result : undefined;\n });\n/**\n * Formats a validation result array into a list of human-readable error messages.\n *\n * Useful for displaying validation errors in UIs.\n *\n * @param results - The result array from `validateRules()`\n * @returns Array of formatted error strings\n *\n * @example\n * const issues = validateRules(rules);\n * const errors = formatValidationReport(issues);\n * // [\"Rule 1, lineStartsWith: Missing {{}} around token...\"]\n */\nexport const formatValidationReport = (results: (RuleValidationResult | undefined)[]) =>\n results.flatMap((result, i) => {\n if (!result) {\n return [];\n }\n return Object.entries(result).flatMap(([type, issues]) => formatValidationIssues(type, issues, i + 1));\n });\n\nconst formatValidationIssues = (type: string, issues: unknown, ruleNumber: number) => {\n if (type === 'dictionaryEntry' && issues && typeof issues === 'object' && !Array.isArray(issues)) {\n return Object.entries(issues)\n .map(([field, issue]) =>\n formatValidationIssue(\n type,\n issue as ValidationIssue | undefined,\n `Rule ${ruleNumber}, ${type}.${field}`,\n ),\n )\n .filter((msg): msg is string => msg !== null);\n }\n\n return (Array.isArray(issues) ? issues : [issues])\n .map((issue) =>\n formatValidationIssue(type, issue as ValidationIssue | undefined, `Rule ${ruleNumber}, ${type}`),\n )\n .filter((msg): msg is string => msg !== null);\n};\n","/**\n * Breakpoint post-processing engine extracted from segmenter.ts.\n *\n * This module is intentionally split into small helpers to reduce cognitive complexity\n * and allow unit testing of tricky edge cases (window sizing, next-page advancement, etc.).\n */\n\nimport type { Breakpoint, BreakpointRule } from '@/types/breakpoints.js';\nimport type { Page, Segment } from '@/types/index.js';\nimport type { Logger } from '@/types/options.js';\nimport { adjustForUnicodeBoundary } from '@/utils/textUtils.js';\nimport { FAST_PATH_THRESHOLD } from './breakpoint-constants.js';\nimport type { PatternProcessor } from './breakpoint-utils.js';\nimport {\n applyPageJoinerBetweenPages,\n type BreakpointContext,\n buildBoundaryPositions,\n createSegment,\n expandBreakpoints,\n findBreakPosition,\n findBreakpointWindowEndPosition,\n findExclusionBreakPosition,\n findPageIndexForPosition,\n findSafeBreakPosition,\n hasExcludedPageInRange,\n type NormalizedPage,\n} from './breakpoint-utils.js';\nimport { buildBreakpointDebugPatch, mergeDebugIntoMeta } from './debug-meta.js';\n\nconst buildPageIdToIndexMap = (pageIds: number[]) => new Map(pageIds.map((id, i) => [id, i]));\n\nconst buildNormalizedPagesMap = (pages: Page[], normalizedContent: string[]) => {\n const normalizedPages = new Map<number, NormalizedPage>();\n for (let i = 0; i < pages.length; i++) {\n const content = normalizedContent[i];\n normalizedPages.set(pages[i].id, { content, index: i, length: content.length });\n }\n return normalizedPages;\n};\n\nconst buildCumulativeOffsets = (pageIds: number[], normalizedPages: Map<number, NormalizedPage>) => {\n const cumulativeOffsets: number[] = [0];\n let totalOffset = 0;\n for (let i = 0; i < pageIds.length; i++) {\n const pageData = normalizedPages.get(pageIds[i]);\n totalOffset += pageData?.length ?? 0;\n if (i < pageIds.length - 1) {\n totalOffset += 1;\n }\n cumulativeOffsets.push(totalOffset);\n }\n return cumulativeOffsets;\n};\n\nconst hasAnyExclusionsInRange = (\n expandedBreakpoints: Array<{ excludeSet: Set<number> }>,\n pageIds: number[],\n fromIdx: number,\n toIdx: number,\n) => expandedBreakpoints.some((bp) => hasExcludedPageInRange(bp.excludeSet, pageIds, fromIdx, toIdx));\n\nexport const computeWindowEndIdx = (currentFromIdx: number, toIdx: number, pageIds: number[], maxPages: number) => {\n const maxWindowPageId = pageIds[currentFromIdx] + maxPages;\n let windowEndIdx = currentFromIdx;\n for (let i = currentFromIdx; i <= toIdx; i++) {\n if (pageIds[i] <= maxWindowPageId) {\n windowEndIdx = i;\n } else {\n break;\n }\n }\n return windowEndIdx;\n};\n\nconst computeRemainingSpan = (currentFromIdx: number, toIdx: number, pageIds: number[]) =>\n pageIds[toIdx] - pageIds[currentFromIdx];\n\nconst createFinalSegment = (\n remainingContent: string,\n currentFromIdx: number,\n toIdx: number,\n pageIds: number[],\n meta: Segment['meta'] | undefined,\n includeMeta: boolean,\n) =>\n createSegment(\n remainingContent,\n pageIds[currentFromIdx],\n currentFromIdx !== toIdx ? pageIds[toIdx] : undefined,\n includeMeta ? meta : undefined,\n );\n\n/**\n * Computes the actual start and end page indices for a piece using\n * precomputed boundary positions and binary search.\n *\n * @param pieceStartPos - Start position of the piece in the full segment content\n * @param pieceEndPos - End position (exclusive) of the piece\n * @param boundaryPositions - Precomputed boundary positions from buildBoundaryPositions\n * @param baseFromIdx - Base page index (boundaryPositions[0] corresponds to pageIds[baseFromIdx])\n * @param toIdx - Maximum page index\n * @returns Object with actualStartIdx and actualEndIdx\n */\nconst computePiecePages = (\n pieceStartPos: number,\n pieceEndPos: number,\n boundaryPositions: number[],\n baseFromIdx: number,\n toIdx: number,\n) => {\n const actualStartIdx = findPageIndexForPosition(pieceStartPos, boundaryPositions, baseFromIdx);\n const endPos = Math.max(pieceStartPos, pieceEndPos - 1);\n const actualEndIdx = Math.min(findPageIndexForPosition(endPos, boundaryPositions, baseFromIdx), toIdx);\n return { actualEndIdx, actualStartIdx };\n};\n\nexport const computeNextFromIdx = (\n remainingContent: string,\n actualEndIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n) => {\n let nextFromIdx = actualEndIdx;\n if (remainingContent && actualEndIdx + 1 <= toIdx) {\n const nextPageData = normalizedPages.get(pageIds[actualEndIdx + 1]);\n if (nextPageData) {\n const nextPrefix = nextPageData.content.slice(0, 30);\n const remainingPrefix = remainingContent.trimStart().slice(0, 30);\n if (\n nextPrefix &&\n (remainingContent.startsWith(nextPrefix) || nextPageData.content.startsWith(remainingPrefix))\n ) {\n nextFromIdx = actualEndIdx + 1;\n }\n }\n }\n return nextFromIdx;\n};\n\nconst createPieceSegment = (\n pieceContent: string,\n actualStartIdx: number,\n actualEndIdx: number,\n pageIds: number[],\n meta: Segment['meta'] | undefined,\n includeMeta: boolean,\n): Segment | null =>\n createSegment(\n pieceContent,\n pageIds[actualStartIdx],\n actualEndIdx > actualStartIdx ? pageIds[actualEndIdx] : undefined,\n includeMeta ? meta : undefined,\n );\n\n/**\n * Finds the break offset within a window, trying exclusions first, then patterns.\n *\n * @returns Break offset relative to remainingContent, or windowEndPosition as fallback\n */\nconst findBreakOffsetForWindow = (\n remainingContent: string,\n currentFromIdx: number,\n windowEndIdx: number,\n toIdx: number,\n windowEndPosition: number,\n pageIds: number[],\n expandedBreakpoints: ReturnType<typeof expandBreakpoints>,\n cumulativeOffsets: number[],\n normalizedPages: Map<number, NormalizedPage>,\n prefer: 'longer' | 'shorter',\n maxContentLength?: number,\n) => {\n const windowHasExclusions = hasAnyExclusionsInRange(expandedBreakpoints, pageIds, currentFromIdx, windowEndIdx);\n\n if (windowHasExclusions) {\n const exclusionBreak = findExclusionBreakPosition(\n currentFromIdx,\n windowEndIdx,\n toIdx,\n pageIds,\n expandedBreakpoints,\n cumulativeOffsets,\n );\n if (exclusionBreak > 0) {\n return { breakOffset: exclusionBreak };\n }\n }\n\n const breakpointCtx: BreakpointContext = { expandedBreakpoints, normalizedPages, pageIds, prefer };\n const patternMatch = findBreakPosition(\n remainingContent,\n currentFromIdx,\n toIdx,\n windowEndIdx,\n windowEndPosition,\n breakpointCtx,\n maxContentLength,\n );\n\n if (patternMatch && patternMatch.breakPos > 0) {\n return {\n breakOffset: patternMatch.breakPos,\n breakpointIndex: patternMatch.breakpointIndex,\n breakpointRule: patternMatch.rule,\n contentLengthSplit: patternMatch.contentLengthSplit,\n wordIndex: patternMatch.wordIndex,\n };\n }\n\n // Fallback: Always try to find a safe break position (avoid mid-word splits)\n if (windowEndPosition < remainingContent.length) {\n const safeOffset = findSafeBreakPosition(remainingContent, windowEndPosition);\n if (safeOffset !== -1) {\n return {\n breakOffset: safeOffset,\n contentLengthSplit: maxContentLength ? { maxContentLength, reason: 'whitespace' as const } : undefined,\n };\n }\n // If no safe break (whitespace) found, ensure we don't split a surrogate pair\n const adjustedOffset = adjustForUnicodeBoundary(remainingContent, windowEndPosition);\n return {\n breakOffset: adjustedOffset,\n contentLengthSplit: maxContentLength\n ? { maxContentLength, reason: 'unicode_boundary' as const }\n : undefined,\n };\n }\n\n return { breakOffset: windowEndPosition };\n};\n\n/**\n * Advances cursor position past any leading whitespace.\n */\nconst skipWhitespace = (content: string, startPos: number) => {\n let pos = startPos;\n while (pos < content.length && /\\s/.test(content[pos])) {\n pos++;\n }\n return pos;\n};\n\n/**\n * Validates that cumulative offsets match actual content length within a tolerance.\n * Required to detect if structural rules (like `lineStartsAfter`) have stripped content\n * which would make offset-based calculations inaccurate.\n */\nconst checkFastPathAlignment = (\n cumulativeOffsets: number[],\n fullContent: string,\n fromIdx: number,\n toIdx: number,\n pageCount: number,\n logger?: Logger,\n) => {\n const expectedLength = (cumulativeOffsets[toIdx + 1] ?? fullContent.length) - (cumulativeOffsets[fromIdx] ?? 0);\n const driftTolerance = Math.max(100, fullContent.length * 0.01);\n const isAligned = Math.abs(expectedLength - fullContent.length) <= driftTolerance;\n\n if (!isAligned && pageCount >= FAST_PATH_THRESHOLD) {\n logger?.warn?.('[breakpoints] Offset drift detected in fast-path candidate, falling back to slow path', {\n actualLength: fullContent.length,\n drift: Math.abs(expectedLength - fullContent.length),\n expectedLength,\n pageCount,\n });\n }\n return isAligned;\n};\n\n/**\n * Handles the special optimized case for maxPages=0 (1 page per segment).\n * This is O(n) and safer than offset arithmetic as it uses source pages directly.\n */\nconst processTrivialFastPath = (\n fromIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n pageCount: number,\n originalMeta?: Segment['meta'],\n debugMetaKey?: string,\n logger?: Logger,\n) => {\n logger?.debug?.('[breakpoints] Using trivial per-page fast-path (maxPages=0)', { fromIdx, pageCount, toIdx });\n const result: Segment[] = [];\n for (let i = fromIdx; i <= toIdx; i++) {\n const pageData = normalizedPages.get(pageIds[i]);\n if (pageData?.content.trim()) {\n const isFirstPiece = i === fromIdx;\n const meta = getSegmentMetaWithDebug(isFirstPiece, debugMetaKey, originalMeta, null);\n const seg = createSegment(pageData.content.trim(), pageIds[i], undefined, meta);\n if (seg) {\n result.push(seg);\n }\n }\n }\n return result;\n};\n\n/**\n * Handles fast-path segmentation for maxPages > 0 using cumulative offsets.\n * Avoids O(n²) string searching but requires accurate offsets.\n */\nconst buildFastPathRawContent = (\n fullContent: string,\n baseOffset: number,\n cumulativeOffsets: number[],\n segStart: number,\n segEnd: number,\n toIdx: number,\n) => {\n const startOffset = Math.max(0, (cumulativeOffsets[segStart] ?? 0) - baseOffset);\n const endOffset =\n segEnd < toIdx\n ? Math.max(0, (cumulativeOffsets[segEnd + 1] ?? fullContent.length) - baseOffset)\n : fullContent.length;\n return fullContent.slice(startOffset, endOffset).trim();\n};\n\nconst buildFastPathSegment = (\n fullContent: string,\n baseOffset: number,\n cumulativeOffsets: number[],\n segStart: number,\n segEnd: number,\n fromIdx: number,\n toIdx: number,\n pageIds: number[],\n originalMeta?: Segment['meta'],\n debugMetaKey?: string,\n) => {\n const rawContent = buildFastPathRawContent(fullContent, baseOffset, cumulativeOffsets, segStart, segEnd, toIdx);\n if (!rawContent) {\n return null;\n }\n\n const isFirstPiece = segStart === fromIdx;\n const meta = getSegmentMetaWithDebug(isFirstPiece, debugMetaKey, originalMeta, null);\n\n const seg: Segment = {\n content: rawContent,\n from: pageIds[segStart],\n };\n\n if (segEnd > segStart) {\n seg.to = pageIds[segEnd];\n }\n if (meta) {\n seg.meta = meta;\n }\n return seg;\n};\n\nconst processOffsetFastPath = (\n fullContent: string,\n fromIdx: number,\n toIdx: number,\n pageIds: number[],\n cumulativeOffsets: number[],\n maxPages: number,\n originalMeta?: Segment['meta'],\n debugMetaKey?: string,\n logger?: Logger,\n) => {\n const result: Segment[] = [];\n const pageCount = toIdx - fromIdx + 1;\n\n logger?.debug?.('[breakpoints] Using offset-based fast-path for large segment', {\n fromIdx,\n maxPages,\n pageCount,\n toIdx,\n });\n\n const baseOffset = cumulativeOffsets[fromIdx] ?? 0;\n\n // IMPORTANT: This fast path is only valid when breakpoint behavior is effectively\n // \"page boundary fallback\" (empty breakpoint ''), which breaks oversized segments\n // at the NEXT page boundary (end of the current page) until the remaining span fits.\n //\n // That means the output shape is:\n // - many single-page pieces, then\n // - one final segment that includes the remaining pages (<= maxPages ID span).\n //\n // This mirrors the iterative breakpoint semantics and avoids \"threshold flips\" where\n // results change at FAST_PATH_THRESHOLD.\n let segStart = fromIdx;\n const needsPeel = (startIdx: number) => pageIds[toIdx] - pageIds[startIdx] > maxPages;\n\n for (; segStart <= toIdx && needsPeel(segStart); segStart++) {\n const seg = buildFastPathSegment(\n fullContent,\n baseOffset,\n cumulativeOffsets,\n segStart,\n segStart,\n fromIdx,\n toIdx,\n pageIds,\n originalMeta,\n debugMetaKey,\n );\n if (seg) {\n result.push(seg);\n }\n }\n\n // Final remainder (fits maxPages by ID span)\n if (segStart <= toIdx) {\n const seg = buildFastPathSegment(\n fullContent,\n baseOffset,\n cumulativeOffsets,\n segStart,\n toIdx,\n fromIdx,\n toIdx,\n pageIds,\n originalMeta,\n debugMetaKey,\n );\n if (seg) {\n result.push(seg);\n }\n }\n return result;\n};\n\n/**\n * Checks if the remaining content fits within paged/length limits.\n * If so, pushes the final segment and returns true.\n *\n * @param actualRemainingEndIdx - The actual end page index of the remaining content\n * (computed from boundaryPositions), NOT the original segment's toIdx. This is critical\n * for maxPages=0 scenarios where remaining content may end before toIdx.\n */\nconst handleOversizedSegmentFit = (\n remainingContent: string,\n currentFromIdx: number,\n actualRemainingEndIdx: number,\n pageIds: number[],\n expandedBreakpoints: Array<{ excludeSet: Set<number> }>,\n maxPages: number,\n maxContentLength: number | undefined,\n isFirstPiece: boolean,\n debugMetaKey: string | undefined,\n originalMeta: Segment['meta'] | undefined,\n lastBreakpoint: { breakpointIndex: number; rule: BreakpointRule; wordIndex?: number } | null,\n result: Segment[],\n) => {\n const remainingSpan = computeRemainingSpan(currentFromIdx, actualRemainingEndIdx, pageIds);\n const remainingHasExclusions = hasAnyExclusionsInRange(\n expandedBreakpoints,\n pageIds,\n currentFromIdx,\n actualRemainingEndIdx,\n );\n\n const fitsInPages = remainingSpan <= maxPages;\n const fitsInLength = !maxContentLength || remainingContent.length <= maxContentLength;\n\n if (fitsInPages && fitsInLength && !remainingHasExclusions) {\n const includeMeta = isFirstPiece || Boolean(debugMetaKey);\n const meta = getSegmentMetaWithDebug(isFirstPiece, debugMetaKey, originalMeta, lastBreakpoint);\n const finalSeg = createFinalSegment(\n remainingContent,\n currentFromIdx,\n actualRemainingEndIdx,\n pageIds,\n meta,\n includeMeta,\n );\n if (finalSeg) {\n result.push(finalSeg);\n }\n return true;\n }\n return false;\n};\n\n/**\n * Builds metadata for a segment piece, optionally including debug info.\n */\nconst getSegmentMetaWithDebug = (\n isFirstPiece: boolean,\n debugMetaKey: string | undefined,\n originalMeta: Segment['meta'] | undefined,\n lastBreakpoint: { breakpointIndex: number; rule: BreakpointRule; wordIndex?: number } | null,\n contentLengthSplit?: { reason: 'whitespace' | 'unicode_boundary'; maxContentLength: number },\n) => {\n const includeMeta = isFirstPiece || Boolean(debugMetaKey);\n if (!includeMeta) {\n return undefined;\n }\n\n let meta = isFirstPiece ? originalMeta : undefined;\n\n if (debugMetaKey) {\n if (lastBreakpoint) {\n meta = mergeDebugIntoMeta(\n meta,\n debugMetaKey,\n buildBreakpointDebugPatch(\n lastBreakpoint.breakpointIndex,\n lastBreakpoint.rule,\n lastBreakpoint.wordIndex,\n ),\n );\n }\n if (contentLengthSplit) {\n meta = mergeDebugIntoMeta(meta, debugMetaKey, {\n contentLengthSplit: {\n maxContentLength: contentLengthSplit.maxContentLength,\n splitReason: contentLengthSplit.reason,\n },\n });\n }\n }\n\n return meta;\n};\n\n/**\n * Calculates window end position, capped by maxContentLength if present.\n */\nconst getWindowEndPosition = (\n remainingContent: string,\n currentFromIdx: number,\n windowEndIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n cumulativeOffsets: number[],\n maxContentLength: number | undefined,\n logger?: Logger,\n) => {\n const pos = findBreakpointWindowEndPosition(\n remainingContent,\n currentFromIdx,\n windowEndIdx,\n toIdx,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\n logger,\n );\n return maxContentLength ? Math.min(pos, maxContentLength) : pos;\n};\n\n/**\n * Advances cursorPos and currentFromIdx for the next iteration.\n */\nconst advanceCursorAndIndex = (\n fullContent: string,\n breakPos: number,\n actualEndIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n) => {\n const nextCursorPos = skipWhitespace(fullContent, breakPos);\n const nextFromIdx = computeNextFromIdx(\n fullContent.slice(nextCursorPos, nextCursorPos + 500),\n actualEndIdx,\n toIdx,\n pageIds,\n normalizedPages,\n );\n return { currentFromIdx: nextFromIdx, cursorPos: nextCursorPos };\n};\n\nconst computeIterationWindow = (\n fullContent: string,\n cursorPos: number,\n currentFromIdx: number,\n fromIdx: number,\n toIdx: number,\n pageIds: number[],\n boundaryPositions: number[],\n maxPages: number,\n maxContentLength: number | undefined,\n) => {\n const windowEndIdx = computeWindowEndIdx(currentFromIdx, toIdx, pageIds, maxPages);\n\n // Optimization: slice only the active \"window\" plus a small padding.\n // This avoids O(N^2) copying when maxContentLength is unset (e.g. debug mode forces iterative path).\n const windowEndBoundaryIdx = windowEndIdx - fromIdx + 1; // boundaryPositions[0] is fromIdx start\n const windowEndAbsPos = boundaryPositions[windowEndBoundaryIdx] ?? fullContent.length;\n const sliceEndByPages = Math.min(fullContent.length, windowEndAbsPos + 4000);\n const sliceEndByLength = maxContentLength\n ? Math.min(fullContent.length, cursorPos + maxContentLength + 4000)\n : fullContent.length;\n const sliceEnd = Math.max(cursorPos + 1, Math.min(sliceEndByPages, sliceEndByLength));\n\n const remainingContent = fullContent.slice(cursorPos, sliceEnd);\n return { remainingContent, sliceEnd, windowEndIdx };\n};\n\nconst computeWindowEndPositionForIteration = (\n remainingContent: string,\n cursorPos: number,\n currentFromIdx: number,\n fromIdx: number,\n windowEndIdx: number,\n toIdx: number,\n pageIds: number[],\n boundaryPositions: number[],\n normalizedPages: Map<number, NormalizedPage>,\n cumulativeOffsets: number[],\n maxPages: number,\n maxContentLength: number | undefined,\n logger?: Logger,\n) => {\n // When maxPages=0, the window MUST NOT extend beyond the current page boundary.\n // Otherwise, breakpoint matching can \"see\" into the next page and create segments spanning pages,\n // even though maxPages=0 semantically means each segment must stay within a single page.\n if (maxPages === 0) {\n const boundaryIdx = currentFromIdx - fromIdx + 1; // boundaryPositions[0] is fromIdx start\n const nextPageStartPos = boundaryPositions[boundaryIdx] ?? Number.POSITIVE_INFINITY;\n const remainingInCurrentPage = Math.max(0, nextPageStartPos - cursorPos);\n const capped = maxContentLength ? Math.min(remainingInCurrentPage, maxContentLength) : remainingInCurrentPage;\n return Math.min(capped, remainingContent.length);\n }\n\n const pos = getWindowEndPosition(\n remainingContent,\n currentFromIdx,\n windowEndIdx,\n toIdx,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\n maxContentLength,\n logger,\n );\n return Math.min(pos, remainingContent.length);\n};\n\nconst ensureProgressingBreakOffset = (\n foundBreakOffset: number,\n remainingContent: string,\n cursorPos: number,\n maxContentLength: number | undefined,\n logger?: Logger,\n) => {\n if (foundBreakOffset > 0) {\n return foundBreakOffset;\n }\n\n // Progress safeguard: Ensure we advance by at least one character to prevent infinite loops.\n // This is critical if findBreakOffsetForWindow returns 0 (e.g. from an empty windowEndPosition).\n const fallbackPos = maxContentLength ? Math.min(maxContentLength, remainingContent.length) : 1;\n const breakOffset = Math.max(1, fallbackPos);\n logger?.warn?.('[breakpoints] No progress from findBreakOffsetForWindow; forcing forward movement', {\n breakOffset,\n cursorPos,\n });\n return breakOffset;\n};\n\nconst updateLastBreakpointFromFound = (\n found: ReturnType<typeof findBreakOffsetForWindow>,\n lastBreakpoint: { breakpointIndex: number; rule: BreakpointRule; wordIndex?: number } | null,\n) => {\n if (found.breakpointIndex !== undefined && found.breakpointRule) {\n return {\n breakpointIndex: found.breakpointIndex,\n rule: found.breakpointRule,\n wordIndex: found.wordIndex,\n };\n }\n return lastBreakpoint;\n};\n\nconst appendPieceAndAdvance = (\n fullContent: string,\n cursorPos: number,\n breakPos: number,\n pieceContent: string,\n currentFromIdx: number,\n fromIdx: number,\n toIdx: number,\n pageIds: number[],\n boundaryPositions: number[],\n normalizedPages: Map<number, NormalizedPage>,\n maxPages: number,\n isFirstPiece: boolean,\n debugMetaKey: string | undefined,\n originalMeta: Segment['meta'] | undefined,\n lastBreakpoint: { breakpointIndex: number; rule: BreakpointRule } | null,\n result: Segment[],\n logger?: Logger,\n contentLengthSplit?: { reason: 'whitespace' | 'unicode_boundary'; maxContentLength: number },\n) => {\n let { actualEndIdx, actualStartIdx } = computePiecePages(cursorPos, breakPos, boundaryPositions, fromIdx, toIdx);\n\n // Safety: boundaryPositions can be slightly misaligned in rare cases for very large segments\n // (e.g. if upstream content was trimmed/normalized). Never allow a piece to \"start\" before\n // the current page cursor, as that can violate maxPages constraints by inflating from/to span.\n if (actualStartIdx < currentFromIdx) {\n logger?.warn?.('[breakpoints] Page attribution drift detected; clamping actualStartIdx', {\n actualStartIdx,\n currentFromIdx,\n });\n actualStartIdx = currentFromIdx;\n }\n\n // When maxPages=0, enforce that the piece cannot span beyond the current page.\n // This is necessary because boundaryPositions-based page detection can be confused\n // when pages have duplicate/overlapping content at boundaries.\n if (maxPages === 0) {\n actualEndIdx = Math.min(actualEndIdx, currentFromIdx);\n actualStartIdx = Math.min(actualStartIdx, currentFromIdx);\n } else if (maxPages > 0) {\n // Enforce ID-span-based maxPages for page attribution too (handles drift).\n const maxAllowedEndIdx = computeWindowEndIdx(actualStartIdx, toIdx, pageIds, maxPages);\n actualEndIdx = Math.min(actualEndIdx, maxAllowedEndIdx);\n }\n\n const meta = getSegmentMetaWithDebug(isFirstPiece, debugMetaKey, originalMeta, lastBreakpoint, contentLengthSplit);\n const pieceSeg = createPieceSegment(pieceContent, actualStartIdx, actualEndIdx, pageIds, meta, true);\n if (pieceSeg) {\n result.push(pieceSeg);\n }\n\n const next = advanceCursorAndIndex(fullContent, breakPos, actualEndIdx, toIdx, pageIds, normalizedPages);\n let nextFromIdx = next.currentFromIdx;\n if (maxPages === 0) {\n // When maxPages=0, content-based detection can be confused by overlapping content; use positions.\n nextFromIdx = findPageIndexForPosition(next.cursorPos, boundaryPositions, fromIdx);\n }\n return { currentFromIdx: nextFromIdx, cursorPos: next.cursorPos };\n};\n\nconst tryProcessOversizedSegmentFastPath = (\n segment: Segment,\n fromIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n cumulativeOffsets: number[],\n expandedBreakpoints: ReturnType<typeof expandBreakpoints>,\n maxPages: number,\n logger?: Logger,\n debugMetaKey?: string,\n maxContentLength?: number,\n) => {\n const fullContent = segment.content;\n const pageCount = toIdx - fromIdx + 1;\n\n const isAligned = checkFastPathAlignment(cumulativeOffsets, fullContent, fromIdx, toIdx, pageCount, logger);\n const isPageBoundaryOnly = expandedBreakpoints.every(\n // Note: compileSkipWhenRegex returns null (not undefined) when skipWhen is not set\n (bp) => bp.regex === null && bp.excludeSet.size === 0 && bp.skipWhenRegex === null,\n );\n if (pageCount < FAST_PATH_THRESHOLD || !isAligned || !isPageBoundaryOnly || maxContentLength || debugMetaKey) {\n return null;\n }\n\n if (maxPages === 0) {\n return processTrivialFastPath(\n fromIdx,\n toIdx,\n pageIds,\n normalizedPages,\n pageCount,\n segment.meta,\n debugMetaKey,\n logger,\n );\n }\n\n return processOffsetFastPath(\n fullContent,\n fromIdx,\n toIdx,\n pageIds,\n cumulativeOffsets,\n maxPages,\n segment.meta,\n debugMetaKey,\n logger,\n );\n};\n\ntype CurrentPageFitResult =\n | {\n handled: true;\n newCursorPos: number;\n newFromIdx: number;\n newLastBreakpoint: { breakpointIndex: number; rule: BreakpointRule; wordIndex?: number } | null;\n }\n | { handled: false };\n\n/**\n * For maxPages=0 with maxContentLength: if current page's remaining content fits,\n * create a segment and advance to next page without applying breakpoints.\n */\nconst tryHandleCurrentPageFit = (\n fullContent: string,\n cursorPos: number,\n currentFromIdx: number,\n fromIdx: number,\n actualRemainingEndIdx: number,\n boundaryPositions: number[],\n pageIds: number[],\n expandedBreakpoints: ReturnType<typeof expandBreakpoints>,\n maxPages: number,\n maxContentLength: number | undefined,\n isFirstPiece: boolean,\n debugMetaKey: string | undefined,\n segmentMeta: Record<string, unknown> | undefined,\n lastBreakpoint: { breakpointIndex: number; rule: BreakpointRule; wordIndex?: number } | null,\n result: Segment[],\n): CurrentPageFitResult => {\n // Only applies when maxPages=0 AND maxContentLength is set AND we span multiple pages\n if (maxPages !== 0 || !maxContentLength || currentFromIdx >= actualRemainingEndIdx) {\n return { handled: false };\n }\n\n const boundaryIdx = currentFromIdx - fromIdx + 1;\n const currentPageEndPos = boundaryPositions[boundaryIdx] ?? fullContent.length;\n const currentPageRemainingContent = fullContent.slice(cursorPos, currentPageEndPos).trim();\n\n if (!currentPageRemainingContent) {\n return { handled: false };\n }\n\n const currentPageFitsInLength = currentPageRemainingContent.length <= maxContentLength;\n const currentPageHasExclusions = hasAnyExclusionsInRange(\n expandedBreakpoints,\n pageIds,\n currentFromIdx,\n currentFromIdx,\n );\n\n if (!currentPageFitsInLength || currentPageHasExclusions) {\n return { handled: false };\n }\n\n // Find the page boundary breakpoint ('') for debug metadata\n const pageBoundaryIdx = expandedBreakpoints.findIndex((bp) => bp.regex === null);\n const pageBoundaryBreakpoint: { breakpointIndex: number; rule: BreakpointRule; wordIndex?: number } | null =\n pageBoundaryIdx >= 0\n ? { breakpointIndex: pageBoundaryIdx, rule: { pattern: '' } as BreakpointRule }\n : lastBreakpoint;\n\n // Create segment for current page's remaining content\n const includeMeta = isFirstPiece || Boolean(debugMetaKey);\n const meta = getSegmentMetaWithDebug(isFirstPiece, debugMetaKey, segmentMeta, pageBoundaryBreakpoint);\n const seg = createSegment(\n currentPageRemainingContent,\n pageIds[currentFromIdx],\n undefined,\n includeMeta ? meta : undefined,\n );\n if (seg) {\n result.push(seg);\n }\n\n // Skip whitespace after page boundary\n let newCursorPos = currentPageEndPos;\n while (newCursorPos < fullContent.length && /\\s/.test(fullContent[newCursorPos])) {\n newCursorPos++;\n }\n\n return {\n handled: true,\n newCursorPos,\n newFromIdx: currentFromIdx + 1,\n newLastBreakpoint: pageBoundaryBreakpoint,\n };\n};\n\nconst processOversizedSegmentIterative = (\n segment: Segment,\n fromIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n cumulativeOffsets: number[],\n expandedBreakpoints: ReturnType<typeof expandBreakpoints>,\n maxPages: number,\n prefer: 'longer' | 'shorter',\n logger?: Logger,\n debugMetaKey?: string,\n maxContentLength?: number,\n) => {\n const result: Segment[] = [];\n const fullContent = segment.content;\n const pageCount = toIdx - fromIdx + 1;\n\n // SLOW PATH: Iterative breakpoint processing\n // WARNING: This path can be slow for large segments - if this log shows large pageCount, investigate!\n logger?.debug?.('[breakpoints] processOversizedSegment: Using iterative path', {\n contentLength: fullContent.length,\n fromIdx,\n maxContentLength,\n maxPages,\n pageCount,\n toIdx,\n });\n\n let cursorPos = 0;\n let currentFromIdx = fromIdx;\n let isFirstPiece = true;\n let lastBreakpoint: { breakpointIndex: number; rule: BreakpointRule; wordIndex?: number } | null = null;\n\n const boundaryPositions = buildBoundaryPositions(\n fullContent,\n fromIdx,\n toIdx,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\n logger,\n );\n\n logger?.debug?.('[breakpoints] boundaryPositions built', {\n boundaryPositions,\n fromIdx,\n fullContentLength: fullContent.length,\n toIdx,\n });\n\n const MAX_SAFE_ITERATIONS = 100_000;\n let didHitMaxIterations = true;\n\n for (let i = 1; i <= MAX_SAFE_ITERATIONS; i++) {\n if (cursorPos >= fullContent.length || currentFromIdx > toIdx) {\n didHitMaxIterations = false;\n break;\n }\n\n const { remainingContent, windowEndIdx } = computeIterationWindow(\n fullContent,\n cursorPos,\n currentFromIdx,\n fromIdx,\n toIdx,\n pageIds,\n boundaryPositions,\n maxPages,\n maxContentLength,\n );\n\n if (!remainingContent.trim()) {\n didHitMaxIterations = false;\n break;\n }\n\n // Compute the actual remaining content (full remaining, not windowed) and its actual end page.\n // This fixes the bug where remainingSpan was computed using toIdx even when remaining\n // content only spans fewer pages.\n const actualRemainingContent = fullContent.slice(cursorPos);\n const actualEndPos = Math.max(cursorPos, fullContent.length - 1);\n const actualRemainingEndIdx = Math.min(\n findPageIndexForPosition(actualEndPos, boundaryPositions, fromIdx),\n toIdx,\n );\n\n // Special handling for maxPages=0 WITH maxContentLength: check if remaining on CURRENT PAGE fits.\n // If so, create a segment for current page content and CONTINUE to next page.\n const currentPageFit = tryHandleCurrentPageFit(\n fullContent,\n cursorPos,\n currentFromIdx,\n fromIdx,\n actualRemainingEndIdx,\n boundaryPositions,\n pageIds,\n expandedBreakpoints,\n maxPages,\n maxContentLength,\n isFirstPiece,\n debugMetaKey,\n segment.meta,\n lastBreakpoint,\n result,\n );\n if (currentPageFit.handled) {\n cursorPos = currentPageFit.newCursorPos;\n currentFromIdx = currentPageFit.newFromIdx;\n lastBreakpoint = currentPageFit.newLastBreakpoint;\n isFirstPiece = false;\n continue;\n }\n\n if (\n handleOversizedSegmentFit(\n actualRemainingContent,\n currentFromIdx,\n actualRemainingEndIdx,\n pageIds,\n expandedBreakpoints,\n maxPages,\n maxContentLength,\n isFirstPiece,\n debugMetaKey,\n segment.meta,\n lastBreakpoint,\n result,\n )\n ) {\n didHitMaxIterations = false;\n break;\n }\n\n const windowEndPosition = computeWindowEndPositionForIteration(\n remainingContent,\n cursorPos,\n currentFromIdx,\n fromIdx,\n windowEndIdx,\n toIdx,\n pageIds,\n boundaryPositions,\n normalizedPages,\n cumulativeOffsets,\n maxPages,\n maxContentLength,\n logger,\n );\n\n // Per-iteration log at trace level to avoid spam in debug mode\n logger?.trace?.(`[breakpoints] iteration=${i}`, { currentFromIdx, cursorPos, windowEndIdx, windowEndPosition });\n\n const found = findBreakOffsetForWindow(\n remainingContent,\n currentFromIdx,\n windowEndIdx,\n toIdx,\n windowEndPosition,\n pageIds,\n expandedBreakpoints,\n cumulativeOffsets,\n normalizedPages,\n prefer,\n maxContentLength,\n );\n\n const breakOffset = ensureProgressingBreakOffset(\n found.breakOffset,\n remainingContent,\n cursorPos,\n maxContentLength,\n logger,\n );\n lastBreakpoint = updateLastBreakpointFromFound(found, lastBreakpoint);\n\n const breakPos = cursorPos + breakOffset;\n const pieceContent = fullContent.slice(cursorPos, breakPos).trim();\n if (!pieceContent) {\n cursorPos = breakPos;\n isFirstPiece = false;\n continue;\n }\n\n const next = appendPieceAndAdvance(\n fullContent,\n cursorPos,\n breakPos,\n pieceContent,\n currentFromIdx,\n fromIdx,\n toIdx,\n pageIds,\n boundaryPositions,\n normalizedPages,\n maxPages,\n isFirstPiece,\n debugMetaKey,\n segment.meta,\n lastBreakpoint,\n result,\n logger,\n found.contentLengthSplit,\n );\n cursorPos = next.cursorPos;\n currentFromIdx = next.currentFromIdx;\n isFirstPiece = false;\n }\n\n if (didHitMaxIterations) {\n logger?.error?.('[breakpoints] Stopped processing oversized segment: reached MAX_SAFE_ITERATIONS', {\n cursorPos,\n fullContentLength: fullContent.length,\n iterations: MAX_SAFE_ITERATIONS,\n });\n }\n\n logger?.debug?.('[breakpoints] processOversizedSegment: Complete', { resultCount: result.length });\n return result;\n};\n\n/**\n * Applies breakpoints to oversized segments.\n *\n * Note: This is an internal engine used by `segmentPages()`.\n */\n/**\n * Processes an oversized segment by iterating through the content and\n * breaking it into smaller pieces that fit within maxPages constraints.\n *\n * Uses precomputed boundary positions for O(log n) page attribution lookups.\n */\nconst processOversizedSegment = (\n segment: Segment,\n fromIdx: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n cumulativeOffsets: number[],\n expandedBreakpoints: ReturnType<typeof expandBreakpoints>,\n maxPages: number,\n prefer: 'longer' | 'shorter',\n logger?: Logger,\n debugMetaKey?: string,\n maxContentLength?: number,\n) => {\n const fast = tryProcessOversizedSegmentFastPath(\n segment,\n fromIdx,\n toIdx,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\n expandedBreakpoints,\n maxPages,\n logger,\n debugMetaKey,\n maxContentLength,\n );\n if (fast) {\n return fast;\n }\n\n return processOversizedSegmentIterative(\n segment,\n fromIdx,\n toIdx,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\n expandedBreakpoints,\n maxPages,\n prefer,\n logger,\n debugMetaKey,\n maxContentLength,\n );\n};\n\nexport const applyBreakpoints = (\n segments: Segment[],\n pages: Page[],\n normalizedContent: string[],\n maxPages: number,\n breakpoints: Breakpoint[],\n prefer: 'longer' | 'shorter',\n patternProcessor: PatternProcessor,\n logger?: Logger,\n pageJoiner: 'space' | 'newline' = 'space',\n debugMetaKey?: string,\n maxContentLength?: number,\n rawPatternProcessor?: PatternProcessor,\n) => {\n const pageIds = pages.map((p) => p.id);\n const pageIdToIndex = buildPageIdToIndexMap(pageIds);\n const normalizedPages = buildNormalizedPagesMap(pages, normalizedContent);\n const cumulativeOffsets = buildCumulativeOffsets(pageIds, normalizedPages);\n const expandedBreakpoints = expandBreakpoints(breakpoints, patternProcessor, rawPatternProcessor);\n\n const result: Segment[] = [];\n\n logger?.info?.('Starting breakpoint processing', { maxPages, segmentCount: segments.length });\n\n logger?.debug?.('[breakpoints] inputSegments', {\n segmentCount: segments.length,\n segments: segments.map((s) => ({ contentLength: s.content.length, from: s.from, to: s.to })),\n });\n\n for (const segment of segments) {\n const fromIdx = pageIdToIndex.get(segment.from) ?? -1;\n const toIdx = segment.to !== undefined ? (pageIdToIndex.get(segment.to) ?? fromIdx) : fromIdx;\n\n const segmentSpan = (segment.to ?? segment.from) - segment.from;\n const hasExclusions = hasAnyExclusionsInRange(expandedBreakpoints, pageIds, fromIdx, toIdx);\n\n const fitsInPages = segmentSpan <= maxPages;\n const fitsInLength = !maxContentLength || segment.content.length <= maxContentLength;\n\n if (fitsInPages && fitsInLength && !hasExclusions) {\n result.push(segment);\n continue;\n }\n\n // Log details about why this segment needs breaking up\n logger?.debug?.('[breakpoints] Processing oversized segment', {\n contentLength: segment.content.length,\n from: segment.from,\n hasExclusions,\n pageSpan: toIdx - fromIdx + 1,\n reasonFitsInLength: fitsInLength,\n reasonFitsInPages: fitsInPages,\n to: segment.to,\n });\n\n const broken = processOversizedSegment(\n segment,\n fromIdx,\n toIdx,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\n expandedBreakpoints,\n maxPages,\n prefer,\n logger,\n debugMetaKey,\n maxContentLength,\n );\n\n // Normalize page joins for breakpoint-created pieces\n result.push(\n ...broken.map((s) => {\n const segFromIdx = pageIdToIndex.get(s.from) ?? -1;\n const segToIdx = s.to !== undefined ? (pageIdToIndex.get(s.to) ?? segFromIdx) : segFromIdx;\n if (segFromIdx >= 0 && segToIdx > segFromIdx) {\n return {\n ...s,\n content: applyPageJoinerBetweenPages(\n s.content,\n segFromIdx,\n segToIdx,\n pageIds,\n normalizedPages,\n pageJoiner,\n ),\n };\n }\n return s;\n }),\n );\n }\n\n logger?.info?.('Breakpoint processing completed', { resultCount: result.length });\n return result;\n};\n","/**\n * Split rule → compiled regex builder.\n *\n * Extracted from `segmenter.ts` to reduce cognitive complexity and enable\n * independent unit testing of regex compilation and token expansion behavior.\n */\n\nimport type { SplitRule } from '@/types/rules.js';\nimport { escapeTemplateBrackets, makeDiacriticInsensitive } from '@/utils/textUtils.js';\nimport { buildArabicDictionaryEntryRegexSource } from './arabic-dictionary-rule.js';\nimport { expandTokensWithCaptures, shouldDefaultToFuzzy } from './tokens.js';\n\n/**\n * Result of processing a pattern with token expansion and optional fuzzy matching.\n */\nexport type ProcessedPattern = {\n /** The expanded regex pattern string (tokens replaced with regex) */\n pattern: string;\n /** Names of captured groups extracted from `{{token:name}}` syntax */\n captureNames: string[];\n};\n\n/**\n * Compiled regex and metadata for a split rule.\n */\nexport type RuleRegex = {\n /** Compiled RegExp with 'gmu' flags (global, multiline, unicode) */\n regex: RegExp;\n /** Whether the regex uses capturing groups for content extraction */\n usesCapture: boolean;\n /** Names of captured groups from `{{token:name}}` syntax */\n captureNames: string[];\n /** Whether this rule uses `lineStartsAfter` (content capture at end) */\n usesLineStartsAfter: boolean;\n};\n\ninterface RuleRegexSource {\n captureNames: string[];\n regex: string;\n}\n\n/**\n * Checks if a regex pattern contains standard (anonymous) capturing groups.\n *\n * Detects standard capturing groups `(...)` while excluding:\n * - Non-capturing groups `(?:...)`\n * - Lookahead assertions `(?=...)` and `(?!...)`\n * - Lookbehind assertions `(?<=...)` and `(?<!...)`\n * - Named groups `(?<name>...)` (start with `(?` so excluded here)\n *\n * NOTE: Named capture groups are still captures, but they're tracked via `captureNames`.\n */\nexport const hasCapturingGroup = (pattern: string): boolean => /\\((?!\\?)/.test(pattern);\n\n/**\n * Extracts named capture group names from a regex pattern.\n *\n * Parses patterns like `(?<num>[0-9]+)` and returns `['num']`.\n *\n * @example\n * extractNamedCaptureNames('^(?<num>[٠-٩]+)\\\\s+') // ['num']\n * extractNamedCaptureNames('^(?<a>\\\\d+)(?<b>\\\\w+)') // ['a', 'b']\n * extractNamedCaptureNames('^\\\\d+') // []\n */\nexport const extractNamedCaptureNames = (pattern: string): string[] =>\n [...pattern.matchAll(/\\(\\?<([A-Za-z_]\\w*)>/g)]\n .map((m) => m[1])\n .filter((n) => !n.startsWith('_r') && !n.startsWith('_w'));\n\n/**\n * Safely compiles a regex pattern, throwing a helpful error if invalid.\n */\nexport const compileRuleRegex = (pattern: string): RegExp => {\n try {\n return new RegExp(pattern, 'gmu');\n } catch (error) {\n throw new Error(\n `Invalid regex pattern: ${pattern}\\n Cause: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n};\n\n/**\n * Processes a pattern string by expanding tokens and optionally applying fuzzy matching.\n *\n * Brackets `()[]` outside `{{tokens}}` are auto-escaped.\n */\nexport const processPattern = (pattern: string, fuzzy: boolean, capturePrefix?: string): ProcessedPattern => {\n const { pattern: expanded, captureNames } = expandTokensWithCaptures(\n escapeTemplateBrackets(pattern),\n fuzzy ? makeDiacriticInsensitive : undefined,\n capturePrefix,\n );\n return { captureNames, pattern: expanded };\n};\n\n/**\n * Processes a breakpoint pattern by expanding tokens only.\n *\n * Unlike `processPattern`, this does NOT escape brackets because breakpoints\n * are treated as raw regex patterns (like the `regex` rule type).\n * Users have full control over regex syntax including `(?:...)` groups.\n */\nexport const processBreakpointPattern = (pattern: string): string => {\n const { pattern: expanded } = expandTokensWithCaptures(pattern);\n return expanded;\n};\n\n/**\n * Builds the raw regex source for a `lineStartsAfter` rule.\n *\n * Expands each pattern through `processPattern()`, combines them into an\n * alternation at the start of a line, and appends a trailing content capture.\n *\n * @param patterns - Template-like line-start markers to match\n * @param fuzzy - Whether Arabic fuzzy matching should be applied during expansion\n * @param capturePrefix - Optional prefix used for internal named captures\n * @returns Regex source plus the named captures extracted from the patterns\n */\nexport const buildLineStartsAfterRegexSource = (\n patterns: string[],\n fuzzy: boolean,\n capturePrefix?: string,\n): RuleRegexSource => {\n const processed = patterns.map((p) => processPattern(p, fuzzy, capturePrefix));\n const alternatives = processed.map((p, i) => `(?<_r${i}>${p.pattern})`).join('|');\n return {\n captureNames: processed.flatMap((p) => p.captureNames),\n regex: `^[\\\\u200E\\\\u200F\\\\u061C\\\\u200B\\\\u200C\\\\u200D\\\\uFEFF]*(?:${alternatives})${capturePrefix ? `(?<${capturePrefix}__content>.*)` : '(.*)'}`,\n };\n};\n\n/**\n * Builds the raw regex source for a `lineStartsWith` rule.\n *\n * Expands each pattern through `processPattern()` and combines them into an\n * alternation anchored at the start of a line.\n *\n * @param patterns - Template-like line-start markers to match\n * @param fuzzy - Whether Arabic fuzzy matching should be applied during expansion\n * @param capturePrefix - Optional prefix used for internal named captures\n * @returns Regex source plus the named captures extracted from the patterns\n */\nexport const buildLineStartsWithRegexSource = (\n patterns: string[],\n fuzzy: boolean,\n capturePrefix?: string,\n): RuleRegexSource => {\n const processed = patterns.map((p) => processPattern(p, fuzzy, capturePrefix));\n const alternatives = processed.map((p, i) => `(?<_r${i}>${p.pattern})`).join('|');\n return {\n captureNames: processed.flatMap((p) => p.captureNames),\n regex: `^[\\\\u200E\\\\u200F\\\\u061C\\\\u200B\\\\u200C\\\\u200D\\\\uFEFF]*(?:${alternatives})`,\n };\n};\n\n/**\n * Builds the raw regex source for a `lineEndsWith` rule.\n *\n * Expands each pattern through `processPattern()` and combines them into an\n * end-anchored alternation.\n *\n * @param patterns - Template-like line-end markers to match\n * @param fuzzy - Whether Arabic fuzzy matching should be applied during expansion\n * @param capturePrefix - Optional prefix used for internal named captures\n * @returns Regex source plus the named captures extracted from the patterns\n */\nexport const buildLineEndsWithRegexSource = (\n patterns: string[],\n fuzzy: boolean,\n capturePrefix?: string,\n): RuleRegexSource => {\n const processed = patterns.map((p) => processPattern(p, fuzzy, capturePrefix));\n const alternatives = processed.map((p, i) => `(?<_r${i}>${p.pattern})`).join('|');\n return {\n captureNames: processed.flatMap((p) => p.captureNames),\n regex: `(?:${alternatives})$`,\n };\n};\n\n/**\n * Builds the raw regex source for a `template` rule.\n *\n * Expands tokens and named captures via `expandTokensWithCaptures()` after\n * applying `escapeTemplateBrackets()` to non-token brackets.\n *\n * @param template - Template string containing optional `{{token}}` markers\n * @param capturePrefix - Optional prefix used for internal named captures\n * @returns Regex source plus the named captures extracted from the template\n */\nexport const buildTemplateRegexSource = (template: string, capturePrefix?: string): RuleRegexSource => {\n const { pattern, captureNames } = expandTokensWithCaptures(\n escapeTemplateBrackets(template),\n undefined,\n capturePrefix,\n );\n return { captureNames, regex: pattern };\n};\n\nconst getFuzzyCandidatePatterns = (rule: SplitRule): string[] => [\n ...('lineStartsWith' in rule && Array.isArray(rule.lineStartsWith) ? rule.lineStartsWith : []),\n ...('lineStartsAfter' in rule && Array.isArray(rule.lineStartsAfter) ? rule.lineStartsAfter : []),\n ...('lineEndsWith' in rule && Array.isArray(rule.lineEndsWith) ? rule.lineEndsWith : []),\n];\n\nconst buildLineBasedRuleRegex = (rule: SplitRule, fuzzy: boolean, capturePrefix?: string): RuleRegexSource | null => {\n if ('lineStartsWith' in rule && Array.isArray(rule.lineStartsWith) && rule.lineStartsWith.length > 0) {\n return buildLineStartsWithRegexSource(rule.lineStartsWith, fuzzy, capturePrefix);\n }\n if ('lineEndsWith' in rule && Array.isArray(rule.lineEndsWith) && rule.lineEndsWith.length > 0) {\n return buildLineEndsWithRegexSource(rule.lineEndsWith, fuzzy, capturePrefix);\n }\n if ('template' in rule && typeof rule.template === 'string') {\n return buildTemplateRegexSource(rule.template, capturePrefix);\n }\n if ('dictionaryEntry' in rule && rule.dictionaryEntry) {\n return buildArabicDictionaryEntryRegexSource(rule.dictionaryEntry, capturePrefix);\n }\n return null;\n};\n\n/**\n * Builds a compiled regex and metadata from a split rule.\n *\n * Behavior mirrors the previous implementation in `segmenter.ts`.\n */\nexport const buildRuleRegex = (rule: SplitRule, capturePrefix?: string): RuleRegex => {\n const fuzzy = rule.fuzzy ?? shouldDefaultToFuzzy(getFuzzyCandidatePatterns(rule));\n\n if ('lineStartsAfter' in rule && Array.isArray(rule.lineStartsAfter) && rule.lineStartsAfter.length > 0) {\n const { regex: lsaRegex, captureNames } = buildLineStartsAfterRegexSource(\n rule.lineStartsAfter,\n fuzzy,\n capturePrefix,\n );\n return { captureNames, regex: compileRuleRegex(lsaRegex), usesCapture: true, usesLineStartsAfter: true };\n }\n\n const ruleRegexSource = buildLineBasedRuleRegex(rule, fuzzy, capturePrefix);\n let finalRegex: string | undefined = ruleRegexSource?.regex;\n let allCaptureNames: string[] = ruleRegexSource?.captureNames ?? [];\n if (!finalRegex && 'regex' in rule && typeof rule.regex === 'string') {\n finalRegex = rule.regex;\n }\n\n if (!finalRegex) {\n throw new Error(\n 'Rule must specify exactly one pattern type: regex, template, lineStartsWith, lineStartsAfter, lineEndsWith, or dictionaryEntry',\n );\n }\n if (allCaptureNames.length === 0) {\n allCaptureNames = extractNamedCaptureNames(finalRegex);\n }\n\n return {\n captureNames: allCaptureNames,\n regex: compileRuleRegex(finalRegex),\n usesCapture: hasCapturingGroup(finalRegex),\n usesLineStartsAfter: false,\n };\n};\n","/**\n * Fast-path fuzzy prefix matching for common Arabic line-start markers.\n *\n * This exists to avoid running expensive fuzzy-expanded regex alternations over\n * a giant concatenated string. Instead, we match only at known line-start\n * offsets and perform a small deterministic comparison:\n * - Skip Arabic diacritics in the CONTENT\n * - Treat common equivalence groups as equal (ا/آ/أ/إ, ة/ه, ى/ي)\n *\n * This module is intentionally conservative: it only supports \"literal\"\n * token patterns (plain text alternation via `|`), not general regex.\n */\n\nimport { getTokenPattern, TOKEN_PATTERNS, type TokenPatternName } from './tokens.js';\n\nexport type FastFuzzyTokenRule = {\n token: string;\n alternatives: string[];\n};\n\n// U+064B..U+0652 (tashkeel/harakat)\nconst isArabicDiacriticCode = (code: number) => code >= 0x064b && code <= 0x0652;\n\nconst equivKey = (ch: string) => {\n switch (ch) {\n case '\\u0622': // آ\n case '\\u0623': // أ\n case '\\u0625': // إ\n return '\\u0627'; // ا\n case '\\u0647': // ه\n return '\\u0629'; // ة\n case '\\u064a': // ي\n return '\\u0649'; // ى\n default:\n return ch;\n }\n};\n\nexport const matchFuzzyLiteralPrefixAt = (content: string, offset: number, literal: string) => {\n let i = offset;\n while (i < content.length && isArabicDiacriticCode(content.charCodeAt(i))) {\n i++;\n }\n\n for (let j = 0; j < literal.length; j++) {\n const litCh = literal[j];\n while (i < content.length && isArabicDiacriticCode(content.charCodeAt(i))) {\n i++;\n }\n if (i >= content.length || equivKey(content[i]) !== equivKey(litCh)) {\n return null;\n }\n i++;\n }\n\n while (i < content.length && isArabicDiacriticCode(content.charCodeAt(i))) {\n i++;\n }\n return i;\n};\n\nconst isLiteralOnly = (s: string) => !/[\\\\[\\]{}()^$.*+?]/.test(s);\n\nexport const compileLiteralAlternation = (pattern: string) => {\n if (!pattern || !isLiteralOnly(pattern)) {\n return null;\n }\n const alternatives = pattern\n .split('|')\n .map((s) => s.trim())\n .filter(Boolean);\n return alternatives.length ? { alternatives } : null;\n};\n\nexport const compileFastFuzzyTokenRule = (tokenTemplate: string) => {\n const m = tokenTemplate.match(/^\\{\\{(\\w+)\\}\\}$/);\n if (!m) {\n return null;\n }\n const token = m[1];\n if (!(token in TOKEN_PATTERNS)) {\n return null;\n }\n const tokenPattern = getTokenPattern(token as TokenPatternName);\n const compiled = compileLiteralAlternation(tokenPattern);\n return compiled ? { alternatives: compiled.alternatives, token } : null;\n};\n\nexport const matchFastFuzzyTokenAt = (content: string, offset: number, compiled: FastFuzzyTokenRule) => {\n for (const alt of compiled.alternatives) {\n const end = matchFuzzyLiteralPrefixAt(content, offset, alt);\n if (end !== null) {\n return end;\n }\n }\n return null;\n};\n","import type { SplitRule } from '@/types/rules.js';\nimport type { PageMap, SplitPoint } from '@/types/segmenter.js';\nimport { normalizeArabicForComparison } from '@/utils/textUtils.js';\nimport { isPageExcluded } from './breakpoint-utils.js';\nimport { compileFastFuzzyTokenRule, type FastFuzzyTokenRule, matchFastFuzzyTokenAt } from './fast-fuzzy-prefix.js';\nimport { extractNamedCaptureNames, hasCapturingGroup, processPattern } from './rule-regex.js';\nimport { ARABIC_WORD_WITH_OPTIONAL_MARKS_PATTERN, shouldDefaultToFuzzy } from './tokens.js';\n\nexport type FastFuzzyRule = {\n compiled: FastFuzzyTokenRule;\n rule: SplitRule;\n ruleIndex: number;\n kind: 'startsWith' | 'startsAfter';\n};\n\nexport type PartitionedRules = {\n combinableRules: Array<{ rule: SplitRule; prefix: string; index: number }>;\n standaloneRules: Array<{ rule: SplitRule; index: number }>;\n fastFuzzyRules: FastFuzzyRule[];\n};\n\nconst tryCompileFastFuzzyRule = (\n rule: SplitRule,\n): { compiled: FastFuzzyTokenRule; kind: 'startsWith' | 'startsAfter' } | null => {\n const fuzzyCandidatePatterns = [\n ...('lineStartsWith' in rule ? rule.lineStartsWith : []),\n ...('lineStartsAfter' in rule ? rule.lineStartsAfter : []),\n ];\n const fuzzy = rule.fuzzy ?? shouldDefaultToFuzzy(fuzzyCandidatePatterns);\n if (!fuzzy) {\n return null;\n }\n\n if ('lineStartsWith' in rule && rule.lineStartsWith?.length === 1) {\n const compiled = compileFastFuzzyTokenRule(rule.lineStartsWith[0]);\n if (compiled) {\n return { compiled, kind: 'startsWith' };\n }\n }\n if ('lineStartsAfter' in rule && rule.lineStartsAfter?.length === 1) {\n const compiled = compileFastFuzzyTokenRule(rule.lineStartsAfter[0]);\n if (compiled) {\n return { compiled, kind: 'startsAfter' };\n }\n }\n return null;\n};\n\nconst isCombinableRule = (rule: SplitRule): boolean => {\n if ('regex' in rule && rule.regex) {\n return (\n extractNamedCaptureNames(rule.regex).length === 0 &&\n !/\\\\[1-9]/.test(rule.regex) &&\n !hasCapturingGroup(rule.regex)\n );\n }\n return true;\n};\n\nexport const partitionRulesForMatching = (rules: SplitRule[]) => {\n const combinableRules: { rule: SplitRule; prefix: string; index: number }[] = [];\n const standaloneRules: Array<{ rule: SplitRule; index: number }> = [];\n const fastFuzzyRules: FastFuzzyRule[] = [];\n\n for (let index = 0; index < rules.length; index++) {\n const rule = rules[index];\n const fuzzyComp = tryCompileFastFuzzyRule(rule);\n\n if (fuzzyComp) {\n fastFuzzyRules.push({\n compiled: fuzzyComp.compiled,\n kind: fuzzyComp.kind,\n rule,\n ruleIndex: index,\n });\n continue;\n }\n\n if (isCombinableRule(rule)) {\n combinableRules.push({ index, prefix: `r${index}_`, rule });\n } else {\n standaloneRules.push({ index, rule });\n }\n }\n\n return { combinableRules, fastFuzzyRules, standaloneRules };\n};\n\nexport type PageStartGuardChecker = (rule: SplitRule, ruleIndex: number, matchStart: number) => boolean;\n\nconst STRONG_SENTENCE_TERMINATORS = /[.!?؟؛۔…]$/u;\nconst TRAILING_PAGE_WRAP_NOISE = /[\\s\\u0660-\\u0669\\d«»\"“”'‘’()[\\]{}<>]+$/u;\nconst TRAILING_WORD_DELIMITERS = /[\\s\\u0660-\\u0669\\d«»\"“”'‘’()[\\]{}<>.,!?؟؛،:]+$/u;\nconst ARABIC_WORD_REGEX = new RegExp(ARABIC_WORD_WITH_OPTIONAL_MARKS_PATTERN, 'gu');\n\nconst trimTrailingPageWrapNoise = (text: string) => {\n let trimmed = text.trimEnd();\n while (trimmed !== trimmed.replace(TRAILING_PAGE_WRAP_NOISE, '')) {\n trimmed = trimmed.replace(TRAILING_PAGE_WRAP_NOISE, '');\n }\n return trimmed;\n};\n\nconst endsWithStrongSentenceTerminator = (pageContent: string) => {\n return STRONG_SENTENCE_TERMINATORS.test(trimTrailingPageWrapNoise(pageContent));\n};\n\nconst extractLastArabicWord = (pageContent: string) => {\n const withoutTrailingDelimiters = trimTrailingPageWrapNoise(pageContent).replace(TRAILING_WORD_DELIMITERS, '');\n const matches = [...withoutTrailingDelimiters.matchAll(ARABIC_WORD_REGEX)];\n return matches.at(-1)?.[0] ?? '';\n};\n\nconst shouldAllowPageStartMatch = (previousPageContent: string, prevWordStoplist: Set<string> | null): boolean => {\n if (!prevWordStoplist || endsWithStrongSentenceTerminator(previousPageContent)) {\n return true;\n }\n\n const lastWord = extractLastArabicWord(previousPageContent);\n return !lastWord || !prevWordStoplist.has(normalizeArabicForComparison(lastWord));\n};\n\nconst shouldAllowSamePageMatch = (contentBeforeMatch: string, stoplist: Set<string> | null): boolean => {\n if (!stoplist) {\n return true;\n }\n\n const lastWord = extractLastArabicWord(contentBeforeMatch);\n return !lastWord || !stoplist.has(normalizeArabicForComparison(lastWord));\n};\n\nexport const createPageStartGuardChecker = (matchContent: string, pageMap: PageMap) => {\n const pageStartToBoundaryIndex = new Map(pageMap.boundaries.map((b, i) => [b.start, i]));\n const compiledPageStartPrev = new Map<number, RegExp | null>();\n const compiledPrevWordStoplists = new Map<number, Set<string> | null>();\n const compiledSamePagePrevWordStoplists = new Map<number, Set<string> | null>();\n const pageIdToBoundaryIndex = new Map(pageMap.boundaries.map((b, i) => [b.id, i]));\n\n const getPageStartPrevRegex = (rule: SplitRule, ruleIndex: number) => {\n if (compiledPageStartPrev.has(ruleIndex)) {\n return compiledPageStartPrev.get(ruleIndex) ?? null;\n }\n const pattern = (rule as { pageStartGuard?: string }).pageStartGuard;\n if (!pattern) {\n compiledPageStartPrev.set(ruleIndex, null);\n return null;\n }\n const re = new RegExp(`(?:${processPattern(pattern, false).pattern})$`, 'u');\n compiledPageStartPrev.set(ruleIndex, re);\n return re;\n };\n\n const getPrevWordStoplist = (rule: SplitRule, ruleIndex: number) => {\n if (compiledPrevWordStoplists.has(ruleIndex)) {\n return compiledPrevWordStoplists.get(ruleIndex) ?? null;\n }\n\n const stoplist = (rule as { pageStartPrevWordStoplist?: string[] }).pageStartPrevWordStoplist;\n if (!stoplist?.length) {\n compiledPrevWordStoplists.set(ruleIndex, null);\n return null;\n }\n\n const normalized = new Set(stoplist.map((word) => normalizeArabicForComparison(word)).filter(Boolean));\n compiledPrevWordStoplists.set(ruleIndex, normalized);\n return normalized;\n };\n\n const getSamePagePrevWordStoplist = (rule: SplitRule, ruleIndex: number) => {\n if (compiledSamePagePrevWordStoplists.has(ruleIndex)) {\n return compiledSamePagePrevWordStoplists.get(ruleIndex) ?? null;\n }\n\n const stoplist = (rule as { samePagePrevWordStoplist?: string[] }).samePagePrevWordStoplist;\n if (!stoplist?.length) {\n compiledSamePagePrevWordStoplists.set(ruleIndex, null);\n return null;\n }\n\n const normalized = new Set(stoplist.map((word) => normalizeArabicForComparison(word)).filter(Boolean));\n compiledSamePagePrevWordStoplists.set(ruleIndex, normalized);\n return normalized;\n };\n\n const getPreviousPageContent = (boundaryIndex: number) => {\n if (boundaryIndex <= 0) {\n return '';\n }\n const prevBoundary = pageMap.boundaries[boundaryIndex - 1];\n return matchContent.slice(prevBoundary.start, prevBoundary.end);\n };\n\n const getPrevPageLastNonWsChar = (boundaryIndex: number) => {\n if (boundaryIndex <= 0) {\n return '';\n }\n const prevBoundary = pageMap.boundaries[boundaryIndex - 1];\n for (let i = prevBoundary.end - 1; i >= prevBoundary.start; i--) {\n const ch = matchContent[i];\n if (ch && !/\\s/u.test(ch)) {\n return ch;\n }\n }\n return '';\n };\n\n const getCurrentPageContentBeforeMatch = (matchStart: number) => {\n const pageId = pageMap.getId(matchStart);\n const boundaryIndex = pageIdToBoundaryIndex.get(pageId);\n if (boundaryIndex === undefined) {\n return '';\n }\n const boundary = pageMap.boundaries[boundaryIndex];\n return matchContent.slice(boundary.start, matchStart);\n };\n\n return (rule: SplitRule, ruleIndex: number, matchStart: number) => {\n const boundaryIndex = pageStartToBoundaryIndex.get(matchStart);\n const isNonFirstPageStart = boundaryIndex !== undefined && boundaryIndex !== 0;\n\n if (isNonFirstPageStart) {\n const prevReq = getPageStartPrevRegex(rule, ruleIndex);\n if (prevReq) {\n const lastChar = getPrevPageLastNonWsChar(boundaryIndex);\n if (!lastChar || !prevReq.test(lastChar)) {\n return false;\n }\n }\n\n return shouldAllowPageStartMatch(\n getPreviousPageContent(boundaryIndex),\n getPrevWordStoplist(rule, ruleIndex),\n );\n }\n\n return shouldAllowSamePageMatch(\n getCurrentPageContentBeforeMatch(matchStart),\n getSamePagePrevWordStoplist(rule, ruleIndex),\n );\n };\n};\n\n/**\n * Checks if a pageId matches the min/max/exclude constraints of a rule.\n */\nconst passesRuleConstraints = (rule: SplitRule, pageId: number) =>\n (rule.min === undefined || pageId >= rule.min) &&\n (rule.max === undefined || pageId <= rule.max) &&\n !isPageExcluded(pageId, rule.exclude);\n\n/**\n * Records a split point for a specific rule.\n */\nconst recordSplitPointAt = (splitPointsByRule: Map<number, SplitPoint[]>, ruleIndex: number, sp: SplitPoint) => {\n const arr = splitPointsByRule.get(ruleIndex);\n if (!arr) {\n splitPointsByRule.set(ruleIndex, [sp]);\n } else {\n arr.push(sp);\n }\n};\n\nconst attemptFastFuzzyMatch = (\n matchContent: string,\n lineStart: number,\n { compiled, kind, rule, ruleIndex }: FastFuzzyRule,\n splitPointsByRule: Map<number, SplitPoint[]>,\n) => {\n const end = matchFastFuzzyTokenAt(matchContent, lineStart, compiled);\n if (end === null) {\n return;\n }\n\n const splitAt = rule.split ?? 'at';\n const splitIndex = splitAt === 'at' ? lineStart : end;\n\n if (kind === 'startsWith') {\n recordSplitPointAt(splitPointsByRule, ruleIndex, { index: splitIndex, meta: rule.meta });\n } else {\n const markerLength = end - lineStart;\n recordSplitPointAt(splitPointsByRule, ruleIndex, {\n contentStartOffset: splitAt === 'at' ? markerLength : undefined,\n index: splitIndex,\n meta: rule.meta,\n });\n }\n};\n\n/**\n * Processes matches for all fast-fuzzy rules at a specific line start.\n */\nconst processFastFuzzyMatchesAt = (\n matchContent: string,\n lineStart: number,\n pageId: number,\n fastFuzzyRules: FastFuzzyRule[],\n passesPageStartGuard: PageStartGuardChecker,\n splitPointsByRule: Map<number, SplitPoint[]>,\n) => {\n for (const ffRule of fastFuzzyRules) {\n if (!passesRuleConstraints(ffRule.rule, pageId)) {\n continue;\n }\n\n if (!passesPageStartGuard(ffRule.rule, ffRule.ruleIndex, lineStart)) {\n continue;\n }\n\n attemptFastFuzzyMatch(matchContent, lineStart, ffRule, splitPointsByRule);\n }\n};\n\nexport const collectFastFuzzySplitPoints = (\n matchContent: string,\n pageMap: PageMap,\n fastFuzzyRules: FastFuzzyRule[],\n passesPageStartGuard: PageStartGuardChecker,\n) => {\n const splitPointsByRule = new Map<number, SplitPoint[]>();\n if (fastFuzzyRules.length === 0 || pageMap.boundaries.length === 0) {\n return splitPointsByRule;\n }\n\n // Stream page boundary cursor to avoid O(log n) getId calls in hot loop.\n let boundaryIdx = 0;\n let currentBoundary = pageMap.boundaries[boundaryIdx];\n const advanceBoundaryTo = (offset: number) => {\n while (currentBoundary && offset > currentBoundary.end && boundaryIdx < pageMap.boundaries.length - 1) {\n boundaryIdx++;\n currentBoundary = pageMap.boundaries[boundaryIdx];\n }\n };\n\n // Line starts are offset 0 and any char after '\\n'\n for (let lineStart = 0; lineStart <= matchContent.length; ) {\n advanceBoundaryTo(lineStart);\n const pageId = currentBoundary?.id ?? 0;\n\n if (lineStart >= matchContent.length) {\n break;\n }\n\n processFastFuzzyMatchesAt(\n matchContent,\n lineStart,\n pageId,\n fastFuzzyRules,\n passesPageStartGuard,\n splitPointsByRule,\n );\n\n const nextNl = matchContent.indexOf('\\n', lineStart);\n if (nextNl === -1) {\n break;\n }\n lineStart = nextNl + 1;\n }\n\n return splitPointsByRule;\n};\n","/**\n * Helper module for collectSplitPointsFromRules to reduce complexity.\n * Handles combined regex matching and split point creation.\n */\n\nimport type { Logger } from '@/types/options.js';\nimport type { SplitRule } from '@/types/rules.js';\nimport type { PageMap, SplitPoint } from '../types/segmenter.js';\nimport { isPageExcluded } from './breakpoint-utils.js';\nimport { buildRuleDebugPatch, mergeDebugIntoMeta } from './debug-meta.js';\nimport {\n extractDebugIndex,\n extractNamedCaptures,\n filterByConstraints,\n getLastPositionalCapture,\n type MatchResult,\n} from './match-utils.js';\nimport { buildRuleRegex, type RuleRegex } from './rule-regex.js';\n\n// Maximum iterations before throwing to prevent infinite loops\nconst MAX_REGEX_ITERATIONS = 100000;\n\ninterface CombinableRule {\n rule: SplitRule;\n prefix: string;\n index: number;\n}\n\ntype RuleRegexInfo = RuleRegex & { prefix: string; source: string };\n\n// Combined regex matching\n\nconst extractNamedCapturesForRule = (\n groups: Record<string, string> | undefined,\n captureNames: string[],\n prefix: string,\n) => {\n const result: Record<string, string> = {};\n if (!groups) {\n return result;\n }\n for (const name of captureNames) {\n if (groups[name] !== undefined) {\n result[name.slice(prefix.length)] = groups[name];\n }\n }\n return result;\n};\n\nconst buildContentOffsets = (\n match: RegExpExecArray,\n ruleInfo: RuleRegexInfo,\n): { capturedContent?: string; contentStartOffset?: number } => {\n if (!ruleInfo.usesLineStartsAfter) {\n return {};\n }\n\n const captured = match.groups?.[`${ruleInfo.prefix}__content`];\n if (captured === undefined) {\n return {};\n }\n\n const fullMatch = match.groups?.[ruleInfo.prefix] ?? match[0];\n return { contentStartOffset: fullMatch.length - captured.length };\n};\n\nconst passesRuleConstraints = (rule: SplitRule, pageId: number) =>\n (rule.min === undefined || pageId >= rule.min) &&\n (rule.max === undefined || pageId <= rule.max) &&\n !isPageExcluded(pageId, rule.exclude);\n\nconst createSplitPointFromMatch = (match: RegExpExecArray, rule: SplitRule, ruleInfo: RuleRegexInfo): SplitPoint => {\n const namedCaptures = extractNamedCapturesForRule(match.groups, ruleInfo.captureNames, ruleInfo.prefix);\n const wordIndex = extractDebugIndex(match.groups, '_r');\n\n return {\n capturedContent: undefined,\n contentStartOffset: buildContentOffsets(match, ruleInfo).contentStartOffset,\n index: (rule.split ?? 'at') === 'at' ? match.index : match.index + match[0].length,\n meta: rule.meta,\n namedCaptures: Object.keys(namedCaptures).length > 0 ? namedCaptures : undefined,\n wordIndex,\n };\n};\n\nconst addSplitPoint = (\n splitPointsByRule: Map<number, SplitPoint[]>,\n originalIndex: number,\n point: SplitPoint,\n): void => {\n const arr = splitPointsByRule.get(originalIndex);\n if (!arr) {\n splitPointsByRule.set(originalIndex, [point]);\n return;\n }\n arr.push(point);\n};\n\n/**\n * Executes a combined regex over the content for combinable rules and records\n * any resulting split points into `splitPointsByRule`.\n *\n * This function mutates `splitPointsByRule` in place and throws if the regex\n * iteration guard is exceeded.\n *\n * @param matchContent - Concatenated content being segmented\n * @param combinableRules - Rules that can be combined into a single alternation\n * @param ruleRegexes - Compiled regex metadata aligned with `combinableRules`\n * @param pageMap - Page boundary mapping utilities for the content\n * @param passesPageStartGuard - Callback that decides whether a match is allowed\n * @param splitPointsByRule - Mutable map collecting split points by rule index\n * @param logger - Optional logger for iteration diagnostics\n * @returns Nothing; results are written into `splitPointsByRule`\n */\nexport const processCombinedMatches = (\n matchContent: string,\n combinableRules: CombinableRule[],\n ruleRegexes: RuleRegexInfo[],\n pageMap: PageMap,\n passesPageStartGuard: (rule: SplitRule, index: number, pos: number) => boolean,\n splitPointsByRule: Map<number, SplitPoint[]>,\n logger?: Logger,\n) => {\n assertCombinedRuleAlignment(combinableRules, ruleRegexes);\n\n const combinedSource = ruleRegexes.map((r) => r.source).join('|');\n const combinedRegex = new RegExp(combinedSource, 'gm');\n\n logger?.debug?.('[segmenter] combined regex built', {\n combinableRuleCount: combinableRules.length,\n combinedSourceLength: combinedSource.length,\n });\n\n let m = combinedRegex.exec(matchContent);\n let iterations = 0;\n\n while (m !== null) {\n if (++iterations > MAX_REGEX_ITERATIONS) {\n throw new Error(\n `[segmenter] Possible infinite loop: exceeded ${MAX_REGEX_ITERATIONS} iterations at position ${m.index}.`,\n );\n }\n if (iterations % 10000 === 0) {\n logger?.warn?.('[segmenter] high iteration count', { iterations, position: m.index });\n }\n\n processCombinedMatch(combinableRules, ruleRegexes, pageMap, passesPageStartGuard, splitPointsByRule, m);\n if (m[0].length === 0) {\n combinedRegex.lastIndex++;\n }\n m = combinedRegex.exec(matchContent);\n }\n};\n\nconst assertCombinedRuleAlignment = (combinableRules: CombinableRule[], ruleRegexes: RuleRegexInfo[]) => {\n if (combinableRules.length !== ruleRegexes.length) {\n throw new Error(\n `processCombinedMatches: combinableRules/ruleRegexes length mismatch (${combinableRules.length} !== ${ruleRegexes.length})`,\n );\n }\n for (let i = 0; i < combinableRules.length; i++) {\n if (!ruleRegexes[i].source.includes(`(?<${combinableRules[i].prefix}>`)) {\n throw new Error(\n `processCombinedMatches: regex alignment mismatch for prefix \"${combinableRules[i].prefix}\" at index ${i}`,\n );\n }\n }\n};\n\nconst processCombinedMatch = (\n combinableRules: CombinableRule[],\n ruleRegexes: RuleRegexInfo[],\n pageMap: PageMap,\n passesPageStartGuard: (rule: SplitRule, index: number, pos: number) => boolean,\n splitPointsByRule: Map<number, SplitPoint[]>,\n match: RegExpExecArray,\n) => {\n const matchedIndex = combinableRules.findIndex(({ prefix }) => match.groups?.[prefix] !== undefined);\n if (matchedIndex === -1) {\n return;\n }\n\n const { rule, index: originalIndex } = combinableRules[matchedIndex];\n if (\n !passesRuleConstraints(rule, pageMap.getId(match.index)) ||\n !passesPageStartGuard(rule, originalIndex, match.index)\n ) {\n return;\n }\n\n addSplitPoint(splitPointsByRule, originalIndex, createSplitPointFromMatch(match, rule, ruleRegexes[matchedIndex]));\n};\n\n/**\n * Builds compiled regex metadata for each combinable rule while preserving the\n * prefix used to identify the matching branch inside a combined alternation.\n *\n * @param combinableRules - Rules eligible for combined-regex processing\n * @returns Rule regex metadata aligned with the input order\n */\nexport const buildRuleRegexes = (combinableRules: CombinableRule[]) =>\n combinableRules.map(({ rule, prefix }) => {\n const built = buildRuleRegex(rule, prefix);\n return { ...built, prefix, source: `(?<${prefix}>${built.regex.source})` };\n });\n\n/**\n * Processes a standalone rule by matching it independently and appending its\n * resulting split points into `splitPointsByRule`.\n *\n * @param rule - The standalone split rule to evaluate\n * @param ruleIndex - Original rule index in the caller's rules array\n * @param matchContent - Concatenated content being segmented\n * @param pageMap - Page boundary mapping utilities for the content\n * @param passesPageStartGuard - Callback that decides whether a match is allowed\n * @param splitPointsByRule - Mutable map collecting split points by rule index\n * @returns Nothing; results are written into `splitPointsByRule`\n */\nexport const processStandaloneRule = (\n rule: SplitRule,\n ruleIndex: number,\n matchContent: string,\n pageMap: PageMap,\n passesPageStartGuard: (rule: SplitRule, index: number, pos: number) => boolean,\n splitPointsByRule: Map<number, SplitPoint[]>,\n) => {\n const { regex, usesCapture, captureNames, usesLineStartsAfter } = buildRuleRegex(rule);\n const allMatches = findMatchesInContent(matchContent, regex, usesCapture, captureNames);\n const constrained = filterByConstraints(allMatches, rule, pageMap.getId);\n const points = constrained\n .filter((m) => passesPageStartGuard(rule, ruleIndex, m.start))\n .map((m) => {\n const isLSA = usesLineStartsAfter && m.captured !== undefined;\n return {\n capturedContent: isLSA ? undefined : m.captured,\n contentStartOffset: isLSA ? m.end - m.captured!.length - m.start : undefined,\n index: (rule.split ?? 'at') === 'at' ? m.start : m.end,\n meta: rule.meta,\n namedCaptures: m.namedCaptures,\n wordIndex: m.wordIndex,\n };\n });\n\n const arr = splitPointsByRule.get(ruleIndex);\n if (!arr) {\n splitPointsByRule.set(ruleIndex, points);\n } else {\n arr.push(...points);\n }\n};\n\nconst findMatchesInContent = (content: string, regex: RegExp, usesCapture: boolean, captureNames: string[]) => {\n const matches: MatchResult[] = [];\n let m = regex.exec(content);\n\n while (m !== null) {\n const wordIndex = extractDebugIndex(m.groups, '_r');\n\n matches.push({\n captured: usesCapture ? getLastPositionalCapture(m) : undefined,\n end: m.index + m[0].length,\n namedCaptures: extractNamedCaptures(m.groups, captureNames),\n start: m.index,\n wordIndex,\n });\n if (m[0].length === 0) {\n regex.lastIndex++;\n }\n m = regex.exec(content);\n }\n\n return matches;\n};\n\n// Occurrence filtering\n\n/**\n * Applies per-rule occurrence filtering and optional debug metadata patches to\n * the collected split points.\n *\n * @param rules - Full rule list in original order\n * @param splitPointsByRule - Split points grouped by originating rule index\n * @param debugMetaKey - Optional metadata key used for debug provenance patches\n * @returns Flattened split points after occurrence filtering and debug merging\n */\nexport const applyOccurrenceFilter = (\n rules: SplitRule[],\n splitPointsByRule: Map<number, SplitPoint[]>,\n debugMetaKey?: string,\n) => {\n const result: SplitPoint[] = [];\n\n rules.forEach((rule, index) => {\n const points = splitPointsByRule.get(index);\n if (!points?.length) {\n return;\n }\n\n const filtered =\n rule.occurrence === 'first' ? [points[0]] : rule.occurrence === 'last' ? [points.at(-1)!] : points;\n\n result.push(\n ...filtered.map((p) => {\n const debugPatch = debugMetaKey ? buildRuleDebugPatch(index, rule, p.wordIndex) : null;\n return {\n ...p,\n meta: debugMetaKey ? mergeDebugIntoMeta(p.meta, debugMetaKey, debugPatch!) : p.meta,\n ruleIndex: index,\n };\n }),\n );\n });\n return result;\n};\n","import { applyPreprocessToPage } from '@/preprocessing/transforms.js';\nimport type { Page, Segment } from '@/types';\nimport type { Logger, SegmentationOptions } from '@/types/options.js';\nimport type { SplitRule } from '@/types/rules.js';\nimport { normalizeLineEndings } from '@/utils/textUtils.js';\nimport type { PageBoundary, PageMap, SplitPoint } from '../types/segmenter.js';\nimport { applyBreakpoints } from './breakpoint-processor.js';\nimport { resolveDebugConfig } from './debug-meta.js';\nimport { anyRuleAllowsId } from './match-utils.js';\nimport { processBreakpointPattern, processPattern } from './rule-regex.js';\nimport {\n collectFastFuzzySplitPoints,\n createPageStartGuardChecker,\n partitionRulesForMatching,\n} from './segmenter-rule-utils.js';\nimport {\n applyOccurrenceFilter,\n buildRuleRegexes,\n processCombinedMatches,\n processStandaloneRule,\n} from './split-point-helpers.js';\n\n/**\n * Builds a concatenated content string and page mapping from input pages.\n *\n * Pages are joined with newline characters, and a page map is created to\n * track which page each offset belongs to. This allows pattern matching\n * across page boundaries while preserving page reference information.\n *\n * @param pages - Array of input pages with id and content\n * @returns Concatenated content string and page mapping utilities\n *\n * @example\n * const pages = [\n * { id: 1, content: 'Page 1 text' },\n * { id: 2, content: 'Page 2 text' }\n * ];\n * const { content, pageMap } = buildPageMap(pages);\n * // content = 'Page 1 text\\nPage 2 text'\n * // pageMap.getId(0) = 1\n * // pageMap.getId(12) = 2\n */\nconst buildPageMap = (pages: Page[]) => {\n const boundaries: PageBoundary[] = [];\n const pageBreaks: number[] = [];\n let offset = 0;\n const parts: string[] = [];\n\n for (let i = 0; i < pages.length; i++) {\n const normalized = normalizeLineEndings(pages[i].content);\n boundaries.push({ end: offset + normalized.length, id: pages[i].id, start: offset });\n parts.push(normalized);\n if (i < pages.length - 1) {\n pageBreaks.push(offset + normalized.length);\n offset += normalized.length + 1;\n } else {\n offset += normalized.length;\n }\n }\n\n const findBoundary = (off: number) => {\n let lo = 0,\n hi = boundaries.length - 1;\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1;\n const b = boundaries[mid];\n if (off < b.start) {\n hi = mid - 1;\n } else if (off > b.end) {\n lo = mid + 1;\n } else {\n return b;\n }\n }\n return boundaries.at(-1);\n };\n\n return {\n content: parts.join('\\n'),\n normalizedPages: parts,\n pageMap: {\n boundaries,\n getId: (off: number) => findBoundary(off)?.id ?? 0,\n pageBreaks,\n pageIds: boundaries.map((b) => b.id),\n },\n };\n};\n\n/**\n * Deduplicate split points by index, preferring ones with more information.\n *\n * Preference rules (when same index):\n * - Prefer a split with `contentStartOffset` (needed for `lineStartsAfter` marker stripping)\n * - Otherwise prefer a split with `meta` over one without\n */\nexport const dedupeSplitPoints = (splitPoints: SplitPoint[]) => {\n const byIndex = new Map<number, SplitPoint>();\n for (const p of splitPoints) {\n const existing = byIndex.get(p.index);\n if (!existing) {\n byIndex.set(p.index, p);\n continue;\n }\n\n byIndex.set(p.index, mergeSplitPoints(existing, p));\n }\n return [...byIndex.values()].sort((a, b) => a.index - b.index);\n};\n\nconst prefersIncomingSplitPoint = (existing: SplitPoint, incoming: SplitPoint) =>\n (incoming.contentStartOffset !== undefined && existing.contentStartOffset === undefined) ||\n (incoming.meta !== undefined && existing.meta === undefined);\n\nconst mergeRecord = (\n existing: Record<string, unknown> | undefined,\n incoming: Record<string, unknown> | undefined,\n): Record<string, unknown> | undefined =>\n existing || incoming\n ? {\n ...(existing ?? {}),\n ...(incoming ?? {}),\n }\n : undefined;\n\nconst mergeSplitPoints = (existing: SplitPoint, incoming: SplitPoint): SplitPoint => {\n const preferred = prefersIncomingSplitPoint(existing, incoming) ? incoming : existing;\n const fallback = preferred === incoming ? existing : incoming;\n\n return {\n ...fallback,\n ...preferred,\n contentStartOffset: preferred.contentStartOffset ?? fallback.contentStartOffset,\n meta: mergeRecord(existing.meta, incoming.meta),\n namedCaptures: mergeRecord(existing.namedCaptures, incoming.namedCaptures) as\n | Record<string, string>\n | undefined,\n };\n};\n\n/**\n * If no structural rules produced segments, create a single segment spanning all pages.\n * This allows breakpoint processing to still run.\n */\nexport const ensureFallbackSegment = (\n segments: Segment[],\n pages: Page[],\n normalizedContent: string[],\n pageJoiner: 'space' | 'newline',\n) => {\n if (segments.length > 0 || pages.length === 0) {\n return segments;\n }\n\n const firstPage = pages[0];\n const lastPage = pages.at(-1)!;\n const joiner = pageJoiner === 'newline' ? '\\n' : ' ';\n const joined = normalizedContent.join(joiner);\n // Important: do NOT trimStart here.\n // Trimming the leading content can desync cumulative offsets/boundary positions in breakpoint processing\n // for very large fallback segments, causing incorrect page attribution and maxPages violations.\n const allContent = joined.replace(/\\s+$/u, '');\n if (!allContent.trim()) {\n return segments;\n }\n\n const initialSeg: Segment = { content: allContent, from: firstPage.id };\n if (lastPage.id !== firstPage.id) {\n initialSeg.to = lastPage.id;\n }\n return [initialSeg];\n};\n\nconst collectSplitPointsFromRules = (\n rules: SplitRule[],\n matchContent: string,\n pageMap: PageMap,\n debugMetaKey: string | undefined,\n logger?: Logger,\n) => {\n logger?.debug?.('[segmenter] collecting split points from rules', {\n contentLength: matchContent.length,\n ruleCount: rules.length,\n });\n\n const passesPageStartGuard = createPageStartGuardChecker(matchContent, pageMap);\n const { combinableRules, fastFuzzyRules, standaloneRules } = partitionRulesForMatching(rules);\n\n logger?.debug?.('[segmenter] rules partitioned', {\n combinableCount: combinableRules.length,\n fastFuzzyCount: fastFuzzyRules.length,\n standaloneCount: standaloneRules.length,\n });\n\n const splitPointsByRule = collectFastFuzzySplitPoints(matchContent, pageMap, fastFuzzyRules, passesPageStartGuard);\n\n if (combinableRules.length > 0) {\n processCombinedMatches(\n matchContent,\n combinableRules,\n buildRuleRegexes(combinableRules),\n pageMap,\n passesPageStartGuard,\n splitPointsByRule,\n logger,\n );\n }\n\n for (const { rule, index } of standaloneRules) {\n processStandaloneRule(rule, index, matchContent, pageMap, passesPageStartGuard, splitPointsByRule);\n }\n\n return applyOccurrenceFilter(rules, splitPointsByRule, debugMetaKey);\n};\n\n/**\n * Finds page breaks within a given offset range using binary search.\n * O(log n + k) where n = total breaks, k = breaks in range.\n *\n * @param startOffset - Start of range (inclusive)\n * @param endOffset - End of range (exclusive)\n * @param sortedBreaks - Sorted array of page break offsets\n * @returns Array of break offsets relative to startOffset\n */\nconst findBreaksInRange = (startOffset: number, endOffset: number, sortedBreaks: number[]) => {\n if (sortedBreaks.length === 0) {\n return [];\n }\n\n let lo = 0,\n hi = sortedBreaks.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n if (sortedBreaks[mid] < startOffset) {\n lo = mid + 1;\n } else {\n hi = mid;\n }\n }\n\n const result: number[] = [];\n for (let i = lo; i < sortedBreaks.length && sortedBreaks[i] < endOffset; i++) {\n result.push(sortedBreaks[i] - startOffset);\n }\n return result;\n};\n\n/**\n * Converts page-break newlines to spaces in segment content.\n *\n * When a segment spans multiple pages, the newline characters that were\n * inserted as page separators during concatenation are converted to spaces\n * for more natural reading.\n *\n * Uses binary search for O(log n + k) lookup instead of O(n) iteration.\n *\n * @param content - Segment content string\n * @param startOffset - Starting offset of this content in concatenated string\n * @param pageBreaks - Sorted array of page break offsets\n * @param pageJoiner - How to represent page boundaries in output (`space` vs `newline`)\n * @returns Content with page-break newlines converted to spaces (or left as-is for `newline`)\n */\nconst convertPageBreaks = (\n content: string,\n startOffset: number,\n pageBreaks: number[],\n pageJoiner: 'space' | 'newline',\n) => {\n if (!content?.includes('\\n')) {\n return content;\n }\n\n // If the caller wants newlines preserved between pages, no conversion is needed.\n if (pageJoiner === 'newline') {\n return content;\n }\n\n const breaksInRange = findBreaksInRange(startOffset, startOffset + content.length, pageBreaks);\n if (breaksInRange.length === 0) {\n return content;\n }\n\n const breakSet = new Set(breaksInRange);\n return content.replace(/\\n/g, (match, offset: number) => (breakSet.has(offset) ? ' ' : match));\n};\n\n/**\n * Segments pages of content based on pattern-matching rules.\n *\n * This is the main entry point for the segmentation engine. It takes an array\n * of pages and applies the provided rules to identify split points, producing\n * an array of segments with content, page references, and metadata.\n *\n * @param pages - Array of pages with id and content\n * @param options - Segmentation options including splitting rules\n * @returns Array of segments with content, from/to page references, and optional metadata\n *\n * @example\n * // Split markdown by headers\n * const segments = segmentPages(pages, {\n * rules: [\n * { lineStartsWith: ['## '], split: 'at', meta: { type: 'chapter' } }\n * ]\n * });\n *\n * @example\n * // Split Arabic hadith text with number extraction\n * const segments = segmentPages(pages, {\n * rules: [\n * {\n * lineStartsAfter: ['{{raqms:hadithNum}} {{dash}} '],\n * split: 'at',\n * fuzzy: true,\n * meta: { type: 'hadith' }\n * }\n * ]\n * });\n *\n * @example\n * // Multiple rules with page constraints\n * const segments = segmentPages(pages, {\n * rules: [\n * { lineStartsWith: ['{{kitab}}'], split: 'at', meta: { type: 'book' } },\n * { lineStartsWith: ['{{bab}}'], split: 'at', min: 10, meta: { type: 'chapter' } },\n * { regex: '^[٠-٩]+ - ', split: 'at', meta: { type: 'hadith' } }\n * ]\n * });\n */\nexport const segmentPages = (pages: Page[], options: SegmentationOptions) => {\n const {\n rules = [],\n breakpoints = [],\n prefer = 'longer',\n pageJoiner = 'space',\n logger,\n maxContentLength,\n preprocess,\n } = options;\n\n if (maxContentLength && maxContentLength < 50) {\n throw new Error(`maxContentLength must be at least 50 characters.`);\n }\n\n const maxPages = options.maxPages ?? Number.MAX_SAFE_INTEGER;\n const hasLimits = options.maxPages !== undefined || maxContentLength !== undefined;\n const debug = resolveDebugConfig((options as any).debug);\n const debugMetaKey = debug?.includeRule ? debug.metaKey : undefined;\n\n logger?.info?.('[segmenter] starting segmentation', {\n breakpointCount: breakpoints.length,\n maxContentLength,\n maxPages,\n pageCount: pages.length,\n prefer,\n preprocessCount: preprocess?.length ?? 0,\n ruleCount: rules.length,\n });\n\n // Apply preprocessing transforms to each page\n const preprocessedPages: Page[] =\n preprocess && preprocess.length > 0\n ? pages.map((page) => ({\n ...page,\n content: applyPreprocessToPage(page.content, page.id, preprocess),\n }))\n : pages;\n\n const { content: matchContent, normalizedPages: normalizedContent, pageMap } = buildPageMap(preprocessedPages);\n\n logger?.debug?.('[segmenter] content built', { pageIds: pageMap.pageIds, totalContentLength: matchContent.length });\n\n const splitPoints = collectSplitPointsFromRules(rules, matchContent, pageMap, debugMetaKey, logger);\n const unique = dedupeSplitPoints(splitPoints);\n\n logger?.debug?.('[segmenter] split points collected', {\n rawSplitPoints: splitPoints.length,\n uniqueSplitPoints: unique.length,\n });\n\n let segments = buildSegments(unique, matchContent, pageMap, rules, pageJoiner);\n logger?.debug?.('[segmenter] structural segments built', { segmentCount: segments.length });\n\n segments = ensureFallbackSegment(segments, preprocessedPages, normalizedContent, pageJoiner);\n\n if (hasLimits) {\n logger?.debug?.('[segmenter] applying breakpoints to oversized segments');\n const result = applyBreakpoints(\n segments,\n preprocessedPages,\n normalizedContent,\n maxPages,\n breakpoints,\n prefer,\n (p: string) => processPattern(p, false).pattern,\n logger,\n pageJoiner,\n debug?.includeBreakpoint ? debug.metaKey : undefined,\n maxContentLength,\n processBreakpointPattern,\n );\n logger?.info?.('[segmenter] segmentation complete (with breakpoints)', { finalSegmentCount: result.length });\n return result;\n }\n logger?.info?.('[segmenter] segmentation complete (structural only)', { finalSegmentCount: segments.length });\n return segments;\n};\n\n/**\n * Creates segment objects from split points.\n *\n * Handles segment creation including:\n * - Content extraction (with captured content for `lineStartsAfter`)\n * - Page break conversion to spaces\n * - From/to page reference calculation\n * - Metadata merging (static + named captures)\n *\n * @param splitPoints - Sorted, unique split points\n * @param content - Full concatenated content string\n * @param pageMap - Page mapping utilities\n * @param rules - Original rules (for constraint checking on first segment)\n * @returns Array of segment objects\n */\nconst buildSegments = (\n splitPoints: SplitPoint[],\n content: string,\n pageMap: PageMap,\n rules: SplitRule[],\n pageJoiner: 'space' | 'newline',\n) => {\n const getActualStart = (start: number, contentStartOffset?: number) => start + (contentStartOffset ?? 0);\n const trimSegmentText = (sliced: string, capturedContent?: string, contentStartOffset?: number) =>\n capturedContent?.trim() ?? (contentStartOffset ? sliced.trim() : sliced.replace(/[\\s\\n]+$/, ''));\n const getAdjustedStart = (actualStart: number, sliced: string, contentStartOffset?: number) =>\n actualStart + (contentStartOffset ? sliced.length - sliced.trimStart().length : 0);\n const applyMeta = (meta?: Record<string, unknown>, namedCaptures?: Record<string, string>) =>\n meta || namedCaptures ? { ...meta, ...namedCaptures } : undefined;\n\n /**\n * Creates a single segment from a content range.\n */\n const createSegment = (\n start: number,\n end: number,\n meta?: Record<string, unknown>,\n capturedContent?: string,\n namedCaptures?: Record<string, string>,\n contentStartOffset?: number,\n ) => {\n const actualStart = getActualStart(start, contentStartOffset);\n const sliced = content.slice(actualStart, end);\n let text = trimSegmentText(sliced, capturedContent, contentStartOffset);\n if (!text) {\n return null;\n }\n\n if (!capturedContent) {\n text = convertPageBreaks(text, actualStart, pageMap.pageBreaks, pageJoiner);\n }\n\n // Calculate how much leading whitespace was trimmed to get the correct 'from' page.\n // This is critical for lineStartsAfter rules where the content after the marker\n // may start on a different page than where the marker was matched.\n const adjustedStart = getAdjustedStart(actualStart, sliced, contentStartOffset);\n\n const from = pageMap.getId(adjustedStart);\n const to = capturedContent ? pageMap.getId(end - 1) : pageMap.getId(adjustedStart + text.length - 1);\n const seg: Segment = { content: text, from };\n if (to !== from) {\n seg.to = to;\n }\n const mergedMeta = applyMeta(meta, namedCaptures);\n if (mergedMeta) {\n seg.meta = mergedMeta;\n }\n return seg;\n };\n\n /**\n * Creates segments from an array of split points.\n */\n const createSegmentsFromSplitPoints = () => {\n const result: Segment[] = [];\n for (let i = 0; i < splitPoints.length; i++) {\n const sp = splitPoints[i];\n const end = splitPoints[i + 1]?.index ?? content.length;\n const s = createSegment(\n sp.index,\n end,\n sp.meta,\n sp.capturedContent,\n sp.namedCaptures,\n sp.contentStartOffset,\n );\n if (s) {\n result.push(s);\n }\n }\n return result;\n };\n\n const segments: Segment[] = [];\n\n // Handle case with no split points\n if (!splitPoints.length) {\n const firstId = pageMap.getId(0);\n if (anyRuleAllowsId(rules, firstId)) {\n const s = createSegment(0, content.length);\n if (s) {\n segments.push(s);\n }\n }\n return segments;\n }\n\n // Add first segment if there's content before first split\n if (splitPoints[0].index > 0) {\n const firstId = pageMap.getId(0);\n if (anyRuleAllowsId(rules, firstId)) {\n const s = createSegment(0, splitPoints[0].index);\n if (s) {\n segments.push(s);\n }\n }\n }\n\n // Create segments from split points using extracted utility\n return [...segments, ...createSegmentsFromSplitPoints()];\n};\n","import { applyPreprocessToPage } from '@/preprocessing/transforms.js';\nimport type { Page, Segment } from '@/types';\nimport type { SegmentationOptions } from '@/types/options.js';\nimport type { SegmentValidationIssue, SegmentValidationReport } from '@/types/validation.js';\nimport { normalizeLineEndings } from '@/utils/textUtils.js';\nimport { FULL_SEARCH_THRESHOLD, PREVIEW_LIMIT } from './validation-constants.js';\n\ntype JoinedBoundary = {\n id: number;\n start: number;\n end: number;\n};\n\n/**\n * Creates a short preview string of text content for error reporting.\n * Truncates content exceeding PREVIEW_LIMIT.\n */\nconst buildPreview = (text: string) => {\n const normalized = text.replace(/\\s+/g, ' ').trim();\n if (normalized.length <= PREVIEW_LIMIT) {\n return normalized;\n }\n return `${normalized.slice(0, PREVIEW_LIMIT)}...`;\n};\n\n/**\n * Creates a lightweight snapshot of a segment for inclusion in validation checks.\n */\nconst buildSegmentSnapshot = (segment: Segment) => ({\n contentPreview: buildPreview(segment.content),\n from: segment.from,\n to: segment.to,\n});\n\n/**\n * Normalizes page content by applying preprocessing transforms and standardizing line endings.\n */\nconst normalizePages = (pages: Page[], options: SegmentationOptions): Page[] => {\n const transforms = options.preprocess ?? [];\n return pages.map((page) => {\n const preprocessed = transforms.length\n ? applyPreprocessToPage(page.content, page.id, transforms)\n : page.content;\n return {\n content: normalizeLineEndings(preprocessed),\n id: page.id,\n };\n });\n};\n\n/**\n * Joins all page content into a single string with boundary tracking.\n * Returns the joined string and a list of boundary mappings (start/end indices for each page).\n */\nconst buildJoinedContent = (pages: Page[], joiner: string) => {\n const boundaries: JoinedBoundary[] = [];\n const joined = pages.map((p) => p.content).join(joiner);\n\n let offset = 0;\n for (let i = 0; i < pages.length; i++) {\n const content = pages[i].content;\n const start = offset;\n const end = start + content.length;\n boundaries.push({ end, id: pages[i].id, start });\n offset += content.length + (i < pages.length - 1 ? joiner.length : 0);\n }\n return { boundaries, joined };\n};\n\n/**\n * Binary search to find which page ID corresponds to a character offset in the joined content.\n * Returns undefined if the offset falls within a joiner gap or outside bounds.\n */\nconst findBoundaryIdForOffset = (offset: number, boundaries: JoinedBoundary[]) => {\n let lo = 0;\n let hi = boundaries.length - 1;\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1;\n const boundary = boundaries[mid];\n if (offset < boundary.start) {\n hi = mid - 1;\n } else if (offset > boundary.end) {\n lo = mid + 1;\n } else {\n return boundary.id;\n }\n }\n\n if (boundaries.length === 0) {\n return undefined;\n }\n\n const last = boundaries.at(-1)!;\n return offset > last.end ? last.id : undefined;\n};\n\nexport type ValidationOptions = {\n /**\n * Threshold for short segment content (characters).\n * Segments shorter than this will trigger a full-document search fallback.\n * @default 500\n */\n fullSearchThreshold?: number;\n};\n\ntype IssueOverrides = Partial<Omit<SegmentValidationIssue, 'type' | 'segment' | 'segmentIndex' | 'severity'>> & {\n matchIndex?: number;\n};\n\n/**\n * Helper to construct a standardized validation issue object.\n */\nconst createIssue = (\n type: SegmentValidationIssue['type'],\n segment: Segment,\n segmentIndex: number,\n overrides: IssueOverrides = {},\n pageMap?: Map<number, Page>,\n): SegmentValidationIssue => {\n const segmentSnapshot = buildSegmentSnapshot(segment);\n const page = pageMap?.get(segment.from);\n\n const matchIndex = overrides.matchIndex;\n const { matchIndex: _ignored, ...restOverrides } = overrides;\n\n const base: Omit<SegmentValidationIssue, 'type' | 'severity'> = {\n actual: { from: segment.from, to: segment.to },\n segment: segmentSnapshot,\n segmentIndex,\n ...restOverrides,\n };\n\n switch (type) {\n case 'page_not_found':\n return {\n ...base,\n evidence: overrides.evidence ?? `Segment.from=${segment.from} does not exist in input pages.`,\n hint: 'Check page IDs passed into segmentPages() and validateSegments().',\n severity: 'error',\n type,\n };\n case 'content_not_found':\n return {\n ...base,\n evidence: overrides.evidence ?? 'Segment content not found in any page content.',\n hint: overrides.hint ?? 'Check preprocessing options, joiner settings, or whitespace normalization.',\n pageContext: page ? { pageId: page.id, pagePreview: buildPreview(page.content) } : undefined,\n severity: 'error',\n type,\n };\n case 'page_attribution_mismatch': {\n const matchedFromId = overrides.expected?.from ?? overrides.actual?.from ?? segment.from;\n const actualPage = pageMap?.get(matchedFromId);\n return {\n ...base,\n evidence:\n overrides.evidence ??\n `Content found in joined content at page ${matchedFromId}, but segment.from=${segment.from}.`,\n hint: overrides.hint ?? 'Check duplicate content handling and boundary detection rules.',\n pageContext: actualPage\n ? {\n matchIndex: matchIndex ?? -1,\n pageId: actualPage.id,\n pagePreview: buildPreview(actualPage.content),\n }\n : undefined,\n severity: 'error',\n type,\n };\n }\n case 'max_pages_violation':\n return {\n ...base,\n evidence: overrides.evidence ?? `Segment spans pages ${segment.from}-${overrides.actual?.to}.`,\n hint: overrides.hint ?? 'Check maxPages windowing in breakpoint-processor.ts and page constraints.',\n severity: 'error',\n type,\n };\n default:\n return { ...base, severity: 'error', type };\n }\n};\n\n/**\n * Finds all occurrences of a content string within the joined text.\n * Respects search limits to avoid performance cliffs on highly repetitive content.\n */\nconst findJoinedMatches = (\n content: string,\n joined: string,\n searchStart: number,\n searchEnd: number,\n limit: number = Infinity,\n): { start: number; end: number }[] => {\n const matches: { start: number; end: number }[] = [];\n if (!content || searchStart >= searchEnd) {\n return matches;\n }\n let idx = joined.indexOf(content, searchStart);\n let count = 0;\n while (idx >= 0 && idx < searchEnd && count < limit) {\n matches.push({ end: idx + content.length - 1, start: idx });\n idx = joined.indexOf(content, idx + 1);\n if (idx >= searchEnd) {\n break;\n }\n count++;\n }\n return matches;\n};\n\n/**\n * Verifies that a matched segment falls within the allowed maxTerms/maxPages constraints.\n * Checks both implicit spans (calculated from match end) and explicit segment.to claims.\n */\nconst checkMaxPagesViolation = (\n segment: Segment,\n segmentIndex: number,\n maxPages: number | undefined,\n matchEnd: number,\n _expectedBoundaryEnd: number,\n boundaries: JoinedBoundary[],\n): SegmentValidationIssue[] => {\n // If maxPages is undefined (no limit) and we trust the segment.to if present (no we verify it now),\n // actually if maxPages is undefined we still might want to verify segment.to integrity?\n // But the issue specifically flagged max_pages_violation.\n // Let's stick to max_pages / boundary enforcement.\n\n // 1. Identify which page the match extends to\n const actualToId = findBoundaryIdForOffset(matchEnd, boundaries);\n if (actualToId === undefined) {\n return []; // Should not happen if match found\n }\n\n // 2. Check strict single-page constraint (maxPages=0)\n if (maxPages === 0) {\n // Violation if it spans to a different page\n if (actualToId !== segment.from) {\n return [\n createIssue('max_pages_violation', segment, segmentIndex, {\n actual: { from: segment.from, to: actualToId },\n evidence: `Segment spans pages ${segment.from}-${actualToId} in joined content (maxPages=0).`,\n expected: { from: segment.from, to: segment.from },\n }),\n ];\n }\n }\n\n // 3. Check explicit segment.to constraint\n if (segment.to !== undefined) {\n if (actualToId > segment.to) {\n return [\n createIssue('max_pages_violation', segment, segmentIndex, {\n actual: { from: segment.from, to: actualToId },\n evidence: `Segment content ends on page ${actualToId} but segment.to is ${segment.to}.`,\n expected: { from: segment.from, to: segment.to },\n }),\n ];\n }\n }\n // 4. Check dynamic maxPages constraint (if segment.to was undefined)\n else if (maxPages !== undefined) {\n const span = actualToId - segment.from;\n if (span > maxPages) {\n return [\n createIssue('max_pages_violation', segment, segmentIndex, {\n actual: { from: segment.from, to: actualToId },\n evidence: `Segment spans ${span} pages (maxPages=${maxPages}).`,\n expected: { from: segment.from, to: segment.from + maxPages },\n }),\n ];\n }\n }\n\n // Original legacy check (can be removed or kept as fallback?)\n // The above logic covers the original case:\n // maxPages=0, to=undefined, matchEnd implies actualToId > from.\n // -> Matches step 2.\n\n return [];\n};\n\n/**\n * Handles validation when content is not found in the expected boundary window.\n * Fallback strategy: search entire document if segment matches existing content elsewhere.\n */\nconst handleMissingBoundary = (\n segment: Segment,\n segmentIndex: number,\n joined: string,\n boundaries: JoinedBoundary[],\n pageMap: Map<number, Page>,\n): SegmentValidationIssue[] => {\n // Search full text to see if content exists anywhere\n const matches = findJoinedMatches(segment.content, joined, 0, joined.length, 1);\n if (matches.length === 0) {\n return [\n createIssue(\n 'content_not_found',\n segment,\n segmentIndex,\n { evidence: 'Segment content not found in any page content.' },\n pageMap,\n ),\n ];\n }\n // Content exists, but claimed page doesn't - this is a mismatch\n const match = matches[0];\n const actualFromId = findBoundaryIdForOffset(match.start, boundaries);\n const actualToId = findBoundaryIdForOffset(match.end, boundaries);\n return [\n createIssue(\n 'page_attribution_mismatch',\n segment,\n segmentIndex,\n {\n actual: { from: segment.from, to: segment.to },\n evidence: `Content found in joined content at page ${actualFromId}, but segment.from=${segment.from}.`,\n expected: { from: actualFromId, to: actualToId },\n matchIndex: match.start,\n },\n pageMap,\n ),\n ];\n};\n\n/**\n * Performs a widened search when the direct check fails.\n * Includes a small buffer around the expected position, and optionally a full-document search for short segments.\n */\nconst handleFallbackSearch = (\n segment: Segment,\n segmentIndex: number,\n joined: string,\n searchStart: number,\n searchEnd: number,\n expectedBoundary: JoinedBoundary,\n boundaries: JoinedBoundary[],\n pageMap: Map<number, Page>,\n maxPages: number | undefined,\n validationOptions?: ValidationOptions,\n): SegmentValidationIssue[] => {\n const content = segment.content;\n const bufferSize = 1000;\n const slowSearchStart = Math.max(0, searchStart - bufferSize);\n const slowSearchEnd = Math.min(joined.length, searchEnd + bufferSize);\n\n const rawMatches = findJoinedMatches(content, joined, slowSearchStart, slowSearchEnd, 5);\n\n if (rawMatches.length === 0) {\n // Fallback: search entire document only for short segments\n const threshold = validationOptions?.fullSearchThreshold ?? FULL_SEARCH_THRESHOLD;\n if (content.length < threshold) {\n // Fix: Check all matches (limit 50) to find one that attributes to the correct from page\n const fullMatches = findJoinedMatches(content, joined, 0, joined.length, 50);\n\n // Check if ANY match aligns with the expected page\n const validMatch = fullMatches.find((m) => {\n const matchFromId = findBoundaryIdForOffset(m.start, boundaries);\n return matchFromId === segment.from;\n });\n\n if (validMatch) {\n return checkMaxPagesViolation(\n segment,\n segmentIndex,\n maxPages,\n validMatch.end,\n expectedBoundary.end,\n boundaries,\n );\n }\n\n if (fullMatches.length > 0) {\n // Found matches but none on the correct page. Report attribution mismatch on the first one.\n const match = fullMatches[0];\n const actualFromId = findBoundaryIdForOffset(match.start, boundaries);\n const actualToId = findBoundaryIdForOffset(match.end, boundaries);\n return [\n createIssue(\n 'page_attribution_mismatch',\n segment,\n segmentIndex,\n {\n actual: { from: segment.from, to: segment.to },\n evidence: `Content found in joined content at page ${actualFromId}, but segment.from=${segment.from}.`,\n expected: { from: actualFromId, to: actualToId },\n matchIndex: match.start,\n },\n pageMap,\n ),\n ];\n }\n }\n\n return [\n createIssue(\n 'content_not_found',\n segment,\n segmentIndex,\n {\n evidence: `Segment content (${content.length} chars) not found in expected window.`,\n hint: 'Check page boundary attribution in segmenter.ts.',\n },\n pageMap,\n ),\n ];\n }\n\n // Check if any match aligns with expected page\n const alignedMatches = rawMatches.filter(\n (m) => m.start >= expectedBoundary.start && m.start <= expectedBoundary.end,\n );\n\n if (alignedMatches.length > 0) {\n const primary = alignedMatches[0];\n return checkMaxPagesViolation(segment, segmentIndex, maxPages, primary.end, expectedBoundary.end, boundaries);\n }\n\n // No aligned matches - report mismatch\n const primary = rawMatches[0];\n const actualFromId = findBoundaryIdForOffset(primary.start, boundaries);\n const actualToId = findBoundaryIdForOffset(primary.end, boundaries);\n return [\n createIssue(\n 'page_attribution_mismatch',\n segment,\n segmentIndex,\n {\n actual: { from: segment.from, to: segment.to },\n evidence: `Content found in joined content at page ${actualFromId}, but segment.from=${segment.from}.`,\n expected: { from: actualFromId, to: actualToId },\n matchIndex: primary.start,\n },\n pageMap,\n ),\n ];\n};\n\n/**\n * Calculates the search range end index based on segment.to or strict bounds.\n */\nconst getSearchRange = (\n segment: Segment,\n expectedBoundary: JoinedBoundary,\n boundaryMap: Map<number, JoinedBoundary>,\n joinedLength: number,\n) => {\n let searchEnd = expectedBoundary.end + 1;\n if (segment.to !== undefined) {\n const endBoundary = boundaryMap.get(segment.to);\n if (endBoundary) {\n searchEnd = endBoundary.end + 1;\n } else {\n searchEnd = Math.min(joinedLength, expectedBoundary.end + 50000);\n }\n }\n return searchEnd;\n};\n\n/**\n * Validates attribution for a single segment by searching for its content in the joined text.\n * Returns issues if content is missing, mis-attributed, or violates page limits.\n */\nconst getAttributionIssues = (\n segment: Segment,\n segmentIndex: number,\n maxPages: number | undefined,\n joined: string,\n boundaries: JoinedBoundary[],\n boundaryMap: Map<number, JoinedBoundary>,\n pageMap: Map<number, Page>,\n validationOptions?: ValidationOptions,\n): SegmentValidationIssue[] => {\n if (!segment.content) {\n return [\n createIssue('content_not_found', segment, segmentIndex, { evidence: 'Segment content is empty.' }, pageMap),\n ];\n }\n\n const expectedBoundary = boundaryMap.get(segment.from);\n if (!expectedBoundary) {\n return handleMissingBoundary(segment, segmentIndex, joined, boundaries, pageMap);\n }\n\n const searchEnd = getSearchRange(segment, expectedBoundary, boundaryMap, joined.length);\n const searchStart = expectedBoundary.start;\n\n // Fast path: direct check\n const idx = joined.indexOf(segment.content, searchStart);\n if (idx !== -1 && idx < searchEnd) {\n const matchEnd = idx + segment.content.length - 1;\n return checkMaxPagesViolation(segment, segmentIndex, maxPages, matchEnd, expectedBoundary.end, boundaries);\n }\n\n // Slow path\n return handleFallbackSearch(\n segment,\n segmentIndex,\n joined,\n searchStart,\n searchEnd,\n expectedBoundary,\n boundaries,\n pageMap,\n maxPages,\n validationOptions,\n );\n};\n\n/**\n * Performs purely static checks on the segment metadata (Ids and spans) before expensive content searching.\n */\nconst checkStaticMaxPages = (segment: Segment, index: number, maxPages: number | undefined) => {\n if (maxPages === undefined || segment.to === undefined) {\n return null;\n }\n\n if (maxPages === 0) {\n if (segment.to !== segment.from) {\n return createIssue('max_pages_violation', segment, index, {\n evidence: 'maxPages=0 requires all segments to stay within one page.',\n expected: { from: segment.from, to: segment.from },\n hint: 'Check boundary detection in breakpoint-utils.ts.',\n });\n }\n return null;\n }\n\n const span = segment.to - segment.from;\n if (span > maxPages) {\n return createIssue('max_pages_violation', segment, index, {\n evidence: `Segment spans ${span} pages (maxPages=${maxPages}).`,\n expected: { from: segment.from, to: segment.from + maxPages },\n hint: 'Check breakpoint windowing and page attribution in breakpoint-processor.ts.',\n });\n }\n return null;\n};\n\n/**\n * Validates a list of segments against the source pages.\n * checks for:\n * - Page existence (invalid IDs)\n * - Content fidelity (content must exist in pages)\n * - Page attribution (from/to must match content location)\n * - Page constraints (maxPages violations)\n *\n * @param pages Input pages used for segmentation\n * @param options Operations used during segmentation (for preprocessing/joining consistency)\n * @param segments The output segments to validate\n * @param validationOptions Optional settings for validation behavior\n * @returns A detailed validation report\n */\nexport const validateSegments = (\n pages: Page[],\n options: SegmentationOptions,\n segments: Segment[],\n validationOptions?: ValidationOptions,\n): SegmentValidationReport => {\n const normalizedPages = normalizePages(pages, options);\n const joiner = options.pageJoiner === 'newline' ? '\\n' : ' ';\n const { boundaries, joined } = buildJoinedContent(normalizedPages, joiner);\n\n const boundaryMap = new Map<number, JoinedBoundary>();\n const pageMap = new Map<number, Page>();\n\n for (const b of boundaries) {\n boundaryMap.set(b.id, b);\n }\n for (const p of normalizedPages) {\n pageMap.set(p.id, p);\n }\n\n const pageIds = new Set(normalizedPages.map((p) => p.id));\n const maxPages = options.maxPages;\n\n const issues: SegmentValidationIssue[] = [];\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n\n if (!pageIds.has(segment.from)) {\n issues.push(createIssue('page_not_found', segment, i));\n continue;\n }\n if (segment.to !== undefined && !pageIds.has(segment.to)) {\n issues.push(\n createIssue('page_not_found', segment, i, {\n evidence: `Segment.to=${segment.to} does not exist in input pages.`,\n }),\n );\n }\n\n // Check maxPages constraint\n const staticMaxPageIssue = checkStaticMaxPages(segment, i, maxPages);\n if (staticMaxPageIssue) {\n issues.push(staticMaxPageIssue);\n }\n\n // Attribution check\n const attributionIssues = getAttributionIssues(\n segment,\n i,\n maxPages,\n joined,\n boundaries,\n boundaryMap,\n pageMap,\n validationOptions,\n );\n issues.push(...attributionIssues);\n }\n\n const errors = issues.filter((issue) => issue.severity === 'error').length;\n const warnings = issues.filter((issue) => issue.severity === 'warn').length;\n\n return {\n issues,\n ok: issues.length === 0,\n summary: {\n errors,\n issues: issues.length,\n pageCount: pages.length,\n segmentCount: segments.length,\n warnings,\n },\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/detection.ts"],"sourcesContent":["import { getAvailableTokens, TOKEN_PATTERNS, type TokenPatternName } from './segmentation/tokens.js';\n\ntype TokenName = TokenPatternName;\n\n/**\n * Result of detecting a token pattern in text\n */\nexport type DetectedPattern = {\n /** Token name from TOKEN_PATTERNS (e.g., 'raqms', 'dash') */\n token: string;\n /** The matched text */\n match: string;\n /** Start index in the original text */\n index: number;\n /** End index (exclusive) */\n endIndex: number;\n};\n\n/**\n * Token detection order - more specific patterns first to avoid partial matches.\n * Example: 'raqms' before 'raqm' so \"٣٤\" matches 'raqms' not just the first digit.\n *\n * Tokens not in this list are appended in alphabetical order from TOKEN_PATTERNS.\n */\nconst TOKEN_PRIORITY_ORDER: TokenName[] = [\n 'basmalah', // Most specific - full phrase\n 'kitab',\n 'bab',\n 'fasl',\n 'naql',\n 'rumuz', // Source abbreviations (e.g., \"خت\", \"خ سي\", \"٤\")\n 'numbered', // Composite: raqms + dash\n 'raqms', // Multiple digits before single digit\n 'raqm',\n 'tarqim',\n 'bullet',\n 'dash',\n 'harf',\n];\n\n/**\n * Gets the token detection priority order.\n * Returns tokens in priority order, with any TOKEN_PATTERNS not in the priority list appended.\n */\nconst getTokenPriority = () => {\n const allTokens = getAvailableTokens();\n const prioritized = TOKEN_PRIORITY_ORDER.filter((t) => allTokens.includes(t));\n const remaining = allTokens.filter((t) => !TOKEN_PRIORITY_ORDER.includes(t)).sort();\n return [...prioritized, ...remaining];\n};\n\nconst isRumuzStandalone = (text: string, startIndex: number, endIndex: number): boolean => {\n // We want rumuz to behave like a standalone marker (e.g. \"س:\" or \"خت ٤:\"),\n // not a substring match inside normal Arabic words (e.g. \"إِبْرَاهِيم\").\n const before = startIndex > 0 ? text[startIndex - 1] : '';\n const after = endIndex < text.length ? text[endIndex] : '';\n\n const isWhitespace = (ch: string): boolean => !!ch && /\\s/u.test(ch);\n const isOpenBracket = (ch: string): boolean => !!ch && /[([{]/u.test(ch);\n const isRightDelimiter = (ch: string): boolean => !!ch && /[::\\-–—ـ،؛.?!؟)\\]}]/u.test(ch);\n\n // Treat any Arabic-block codepoint (letters + diacritics + digits) as \"wordy\" context.\n // Unicode Script properties can classify some combining marks as \"Inherited\", so we avoid \\p{Script=Arabic}.\n const isArabicWordy = (ch: string): boolean => !!ch && /[\\u0600-\\u06FF]/u.test(ch);\n\n const leftOk = !before || isWhitespace(before) || isOpenBracket(before) || !isArabicWordy(before);\n const rightOk = !after || isWhitespace(after) || isRightDelimiter(after) || !isArabicWordy(after);\n\n return leftOk && rightOk;\n};\n\n/**\n * Analyzes text and returns all detected token patterns with their positions.\n * Patterns are detected in priority order to avoid partial matches.\n *\n * @param text - The text to analyze for token patterns\n * @returns Array of detected patterns sorted by position\n *\n * @example\n * detectTokenPatterns(\"٣٤ - حدثنا\")\n * // Returns: [\n * // { token: 'raqms', match: '٣٤', index: 0, endIndex: 2 },\n * // { token: 'dash', match: '-', index: 3, endIndex: 4 },\n * // { token: 'naql', match: 'حدثنا', index: 5, endIndex: 10 }\n * // ]\n */\nexport const detectTokenPatterns = (text: string) => {\n if (!text) {\n return [];\n }\n\n const results: DetectedPattern[] = [];\n const coveredRanges: Array<[number, number]> = [];\n\n // Check if a position is already covered by a detected pattern\n const isPositionCovered = (start: number, end: number): boolean => {\n return coveredRanges.some(\n ([s, e]) => (start >= s && start < e) || (end > s && end <= e) || (start <= s && end >= e),\n );\n };\n\n // Process tokens in priority order\n for (const tokenName of getTokenPriority()) {\n const pattern = TOKEN_PATTERNS[tokenName as keyof typeof TOKEN_PATTERNS];\n if (!pattern) {\n continue;\n }\n\n try {\n // Create a global regex to find all matches\n const regex = new RegExp(`(${pattern})`, 'gu');\n let match: RegExpExecArray | null;\n\n while ((match = regex.exec(text)) !== null) {\n const startIndex = match.index;\n const endIndex = startIndex + match[0].length;\n\n if (tokenName === 'rumuz' && !isRumuzStandalone(text, startIndex, endIndex)) {\n continue;\n }\n\n // Skip if this range overlaps with an already detected pattern\n if (isPositionCovered(startIndex, endIndex)) {\n continue;\n }\n\n results.push({ endIndex, index: startIndex, match: match[0], token: tokenName });\n\n coveredRanges.push([startIndex, endIndex]);\n }\n } catch {}\n }\n\n return results.sort((a, b) => a.index - b.index);\n};\n\n/**\n * Generates a template pattern from text using detected tokens.\n * Replaces matched portions with {{token}} syntax.\n *\n * @param text - Original text\n * @param detected - Array of detected patterns from detectTokenPatterns\n * @returns Template string with tokens, e.g., \"{{raqms}} {{dash}} \"\n *\n * @example\n * const detected = detectTokenPatterns(\"٣٤ - \");\n * generateTemplateFromText(\"٣٤ - \", detected);\n * // Returns: \"{{raqms}} {{dash}} \"\n */\nexport const generateTemplateFromText = (text: string, detected: DetectedPattern[]) => {\n if (!text || detected.length === 0) {\n return text;\n }\n\n // Build template by replacing detected patterns with tokens\n // Process in reverse order to preserve indices\n let template = text;\n const sortedByIndexDesc = [...detected].sort((a, b) => b.index - a.index);\n\n for (const d of sortedByIndexDesc) {\n template = `${template.slice(0, d.index)}{{${d.token}}}${template.slice(d.endIndex)}`;\n }\n\n return template;\n};\n\n/**\n * Determines the best pattern type for auto-generated rules based on detected patterns.\n *\n * @param detected - Array of detected patterns\n * @returns Suggested pattern type and whether to use fuzzy matching\n */\nexport const suggestPatternConfig = (\n detected: DetectedPattern[],\n): { patternType: 'lineStartsWith' | 'lineStartsAfter'; fuzzy: boolean; metaType?: string } => {\n // Check if the detected patterns suggest a structural marker (chapter, book, etc.)\n const hasStructuralToken = detected.some((d) => ['basmalah', 'kitab', 'bab', 'fasl'].includes(d.token));\n\n // Check if the pattern is numbered (hadith-style)\n const hasNumberedPattern = detected.some((d) => ['raqms', 'raqm', 'numbered'].includes(d.token));\n\n // If it starts with a structural token, use lineStartsWith (keep marker in content)\n if (hasStructuralToken) {\n return {\n fuzzy: true,\n metaType: detected.find((d) => ['kitab', 'bab', 'fasl'].includes(d.token))?.token || 'chapter',\n patternType: 'lineStartsWith',\n };\n }\n\n // If it's a numbered pattern (like hadith numbers), use lineStartsAfter (strip prefix)\n if (hasNumberedPattern) {\n return { fuzzy: false, metaType: 'hadith', patternType: 'lineStartsAfter' };\n }\n\n // Default: use lineStartsAfter without fuzzy\n return { fuzzy: false, patternType: 'lineStartsAfter' };\n};\n\n/**\n * Analyzes text and generates a complete suggested rule configuration.\n *\n * @param text - Highlighted text from the page\n * @returns Suggested rule configuration or null if no patterns detected\n */\nexport const analyzeTextForRule = (\n text: string,\n): {\n template: string;\n patternType: 'lineStartsWith' | 'lineStartsAfter';\n fuzzy: boolean;\n metaType?: string;\n detected: DetectedPattern[];\n} | null => {\n const detected = detectTokenPatterns(text);\n\n if (detected.length === 0) {\n return null;\n }\n\n const template = generateTemplateFromText(text, detected);\n const config = suggestPatternConfig(detected);\n\n return { detected, template, ...config };\n};\n"],"mappings":"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"}