flappa-doormal 2.11.1 → 2.11.3
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.
- package/AGENTS.md +6 -0
- package/README.md +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +214 -51
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["processPattern","skipWhitespace","passesRuleConstraints","createSegment","TOKEN_PRIORITY_ORDER","isArabicLetter","resolveOptions","word"],"sources":["../src/segmentation/fuzzy.ts","../src/segmentation/types.ts","../src/segmentation/optimize-rules.ts","../src/segmentation/tokens.ts","../src/segmentation/pattern-validator.ts","../src/segmentation/replace.ts","../src/segmentation/breakpoint-utils.ts","../src/segmentation/debug-meta.ts","../src/segmentation/breakpoint-processor.ts","../src/segmentation/match-utils.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/textUtils.ts","../src/segmentation/segmenter.ts","../src/analysis/shared.ts","../src/analysis/line-starts.ts","../src/analysis/repeating-sequences.ts","../src/detection.ts","../src/recovery.ts"],"sourcesContent":["/**\n * Fuzzy matching utilities for Arabic text.\n *\n * Provides diacritic-insensitive and character-equivalence matching for Arabic text.\n * This allows matching text regardless of:\n * - Diacritical marks (harakat/tashkeel): فَتْحَة، ضَمَّة، كَسْرَة، سُكُون، شَدَّة، تَنْوين\n * - Character equivalences: ا↔آ↔أ↔إ, ة↔ه, ى↔ي\n *\n * @module fuzzy\n *\n * @example\n * // Make a pattern diacritic-insensitive\n * const pattern = makeDiacriticInsensitive('حدثنا');\n * new RegExp(pattern, 'u').test('حَدَّثَنَا') // → true\n */\n\n/**\n * Character class matching all Arabic diacritics (Tashkeel/Harakat).\n *\n * Includes the following diacritical marks:\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 = '[\\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: string[][] = [\n ['\\u0627', '\\u0622', '\\u0623', '\\u0625'], // ا, آ, أ, إ\n ['\\u0629', '\\u0647'], // ة <-> ه\n ['\\u0649', '\\u064A'], // ى <-> ي\n];\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): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/**\n * Returns a regex character class for all equivalents of a given character.\n *\n * If the character belongs to one of the predefined equivalence groups\n * (e.g., ا/آ/أ/إ), the returned class will match any member of that group.\n * Otherwise, the original character is simply escaped for safe regex inclusion.\n *\n * @param ch - A single character to expand into its equivalence class\n * @returns A RegExp-safe string representing the character and its equivalents\n *\n * @example\n * getEquivClass('ا') // → '[اآأإ]' (matches any alef variant)\n * getEquivClass('ب') // → 'ب' (no equivalents, just escaped)\n * getEquivClass('.') // → '\\\\.' (regex metachar escaped)\n *\n * @internal\n */\nconst getEquivClass = (ch: string): string => {\n for (const group of EQUIV_GROUPS) {\n if (group.includes(ch)) {\n // join the group's members into a character class\n return `[${group.map((c) => escapeRegex(c)).join('')}]`;\n }\n }\n // not in equivalence groups -> return escaped character\n return escapeRegex(ch);\n};\n\n/**\n * Performs light normalization on Arabic text for consistent matching.\n *\n * Normalization steps:\n * 1. NFC normalization (canonical decomposition then composition)\n * 2. Remove Zero-Width Joiner (U+200D) and Zero-Width Non-Joiner (U+200C)\n * 3. Collapse multiple whitespace characters to single space\n * 4. Trim leading and trailing whitespace\n *\n * This normalization preserves diacritics and letter forms while removing\n * invisible characters that could interfere with matching.\n *\n * @param str - Arabic text to normalize\n * @returns Normalized string\n *\n * @example\n * normalizeArabicLight('حَدَّثَنَا') // → 'حَدَّثَنَا' (diacritics preserved)\n * normalizeArabicLight('بسم الله') // → 'بسم الله' (spaces collapsed)\n * normalizeArabicLight(' text ') // → 'text' (trimmed)\n *\n * @internal\n */\nconst normalizeArabicLight = (str: string) => {\n return str\n .normalize('NFC')\n .replace(/[\\u200C\\u200D]/g, '') // remove ZWJ/ZWNJ\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\n/**\n * Creates a diacritic-insensitive regex pattern for Arabic text matching.\n *\n * Transforms input text into a regex pattern that matches the text regardless\n * of diacritical marks (harakat) and character variations. Each character in\n * the input is:\n * 1. Expanded to its equivalence class (if applicable)\n * 2. Followed by an optional diacritics matcher\n *\n * This allows matching:\n * - `حدثنا` with `حَدَّثَنَا` (with full diacritics)\n * - `الإيمان` with `الايمان` (alef variants)\n * - `صلاة` with `صلاه` (ta marbuta ↔ ha)\n *\n * @param text - Input Arabic text to make diacritic-insensitive\n * @returns Regex pattern string that matches the text with or without diacritics\n *\n * @example\n * const pattern = makeDiacriticInsensitive('حدثنا');\n * // Each char gets equivalence class + optional diacritics\n * // Result matches: حدثنا, حَدَّثَنَا, حَدَثَنَا, etc.\n *\n * @example\n * const pattern = makeDiacriticInsensitive('باب');\n * new RegExp(pattern, 'u').test('بَابٌ') // → true\n * new RegExp(pattern, 'u').test('باب') // → true\n *\n * @example\n * // Using with split rules\n * {\n * lineStartsWith: ['باب'],\n * split: 'at',\n * fuzzy: true // Applies makeDiacriticInsensitive internally\n * }\n */\nexport const makeDiacriticInsensitive = (text: string) => {\n const diacriticsMatcher = `${DIACRITICS_CLASS}*`;\n const norm = normalizeArabicLight(text);\n // Use Array.from to iterate grapheme-safe over the string (works fine for Arabic letters)\n return Array.from(norm)\n .map((ch) => getEquivClass(ch) + diacriticsMatcher)\n .join('');\n};\n","// ─────────────────────────────────────────────────────────────\n// Pattern Types (mutually exclusive - only ONE per rule)\n// ─────────────────────────────────────────────────────────────\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 * 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 */\ntype PatternType =\n | RegexPattern\n | TemplatePattern\n | LineStartsWithPattern\n | LineStartsAfterPattern\n | LineEndsWithPattern;\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 = ['lineStartsWith', 'lineStartsAfter', 'lineEndsWith', 'template', 'regex'] 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// ─────────────────────────────────────────────────────────────\n// Split Behavior\n// ─────────────────────────────────────────────────────────────\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// Page Range Types\n// ─────────────────────────────────────────────────────────────\n\n/**\n * A single page ID or a range of page IDs.\n *\n * - `number`: A single page ID\n * - `[number, number]`: A range from first to second (inclusive)\n *\n * @example\n * 5 // Single page 5\n * [10, 20] // Pages 10 through 20 (inclusive)\n */\nexport type PageRange = number | [number, number];\n\n// ─────────────────────────────────────────────────────────────\n// Constraints & Metadata\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 */\ntype RuleConstraints = {\n /**\n * Minimum page ID for this rule to apply.\n *\n * Matches on pages with `id < min` are ignored.\n *\n * @example\n * // Only apply rule starting from page 10\n * { min: 10, lineStartsWith: ['##'], split: 'before' }\n */\n min?: number;\n\n /**\n * Maximum page ID for this rule to apply.\n *\n * Matches on pages with `id > max` are ignored.\n *\n * @example\n * // Only apply rule up to page 100\n * { max: 100, lineStartsWith: ['##'], split: 'before' }\n */\n max?: number;\n\n /**\n * Specific pages or page ranges to exclude from this rule.\n *\n * Use this to skip the rule for specific pages without needing\n * to repeat the rule with different min/max values.\n *\n * @example\n * // Exclude specific pages\n * { exclude: [1, 2, 5] }\n *\n * @example\n * // Exclude page ranges\n * { exclude: [[1, 10], [50, 100]] }\n *\n * @example\n * // Mix single pages and ranges\n * { exclude: [1, [5, 10], 50] }\n */\n exclude?: PageRange[];\n\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// ─────────────────────────────────────────────────────────────\n// Combined Rule Type\n// ─────────────────────────────────────────────────────────────\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`, or `lineEndsWith`\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\n// ─────────────────────────────────────────────────────────────\n// Input & Output\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Input page structure for segmentation.\n *\n * Each page represents a logical unit of content (e.g., a book page,\n * a document section) that can be tracked across segment boundaries.\n *\n * @example\n * const pages: Page[] = [\n * { id: 1, content: '## Chapter 1\\nFirst paragraph...' },\n * { id: 2, content: 'Continued text...\\n## Chapter 2' },\n * ];\n */\nexport type Page = {\n /**\n * Unique page/entry ID used for:\n * - `min`/`max` constraint filtering\n * - `from`/`to` tracking in output segments\n */\n id: number;\n\n /**\n * Raw page content (may contain HTML).\n *\n * Line endings are normalized internally (`\\r\\n` and `\\r` → `\\n`).\n * Use a utility to convert html to markdown or `stripHtmlTags()` to preprocess HTML.\n */\n content: string;\n};\n\n// ─────────────────────────────────────────────────────────────\n// Breakpoint Types\n// ─────────────────────────────────────────────────────────────\n\n/**\n * A breakpoint pattern with optional page constraints.\n *\n * Use this to control which pages a breakpoint pattern applies to.\n * Patterns outside the specified range are skipped, allowing\n * the next breakpoint pattern (or fallback) to be tried.\n *\n * @example\n * // Only apply punctuation-based breaking from page 10 onwards\n * { pattern: '{{tarqim}}\\\\s*', min: 10 }\n *\n * @example\n * // Apply to specific page range (pages 10-50)\n * { pattern: '{{tarqim}}\\\\s*', min: 10, max: 50 }\n */\nexport type BreakpointRule = {\n /**\n * Regex pattern for breaking (supports token expansion).\n * Empty string `''` means fall back to page boundary.\n */\n pattern: string;\n\n /**\n * Minimum page ID for this breakpoint to apply.\n * Segments starting before this page skip this pattern.\n */\n min?: number;\n\n /**\n * Maximum page ID for this breakpoint to apply.\n * Segments starting after this page skip this pattern.\n */\n max?: number;\n\n /**\n * Specific pages or page ranges to exclude from this breakpoint.\n *\n * Use this to skip the breakpoint for specific pages without needing\n * to repeat the breakpoint with different min/max values.\n *\n * @example\n * // Exclude specific pages\n * { pattern: '\\\\.\\\\s*', exclude: [1, 2, 5] }\n *\n * @example\n * // Exclude page ranges (front matter pages 1-10)\n * { pattern: '{{tarqim}}\\\\s*', exclude: [[1, 10]] }\n *\n * @example\n * // Mix single pages and ranges\n * { pattern: '\\\\.\\\\s*', exclude: [1, [5, 10], 50] }\n */\n exclude?: PageRange[];\n\n /**\n * Skip this breakpoint if the segment content matches this pattern.\n *\n * Supports token expansion (e.g., `{{kitab}}`). When the segment's\n * remaining content matches this regex, the breakpoint pattern is\n * skipped and the next breakpoint in the array is tried.\n *\n * Useful for excluding title pages or front matter without needing\n * to specify explicit page ranges.\n *\n * @example\n * // Skip punctuation breakpoint for short content (likely titles)\n * { pattern: '{{tarqim}}\\\\s*', skipWhen: '^.{1,20}$' }\n *\n * @example\n * // Skip for content containing \"kitab\" (book) marker\n * { pattern: '\\\\.\\\\s*', skipWhen: '{{kitab}}' }\n */\n skipWhen?: string;\n};\n\n/**\n * A breakpoint can be a simple string pattern or an object with constraints.\n *\n * String breakpoints apply to all pages. Object breakpoints can specify\n * `min`/`max` to limit which pages they apply to.\n *\n * @example\n * // String (applies everywhere)\n * '{{tarqim}}\\\\s*'\n *\n * @example\n * // Object with constraints (only from page 10+)\n * { pattern: '{{tarqim}}\\\\s*', min: 10 }\n */\nexport type Breakpoint = string | BreakpointRule;\n\n// ─────────────────────────────────────────────────────────────\n// Logger Interface\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Logger interface for custom logging implementations.\n *\n * All methods are optional - only implement the verbosity levels you need.\n * When no logger is provided, no logging overhead is incurred.\n *\n * Compatible with the Logger interface from ffmpeg-simplified and similar libraries.\n *\n * @example\n * // Simple console logger\n * const logger: Logger = {\n * debug: console.debug,\n * info: console.info,\n * warn: console.warn,\n * error: console.error,\n * };\n *\n * @example\n * // Production logger (only warnings and errors)\n * const prodLogger: Logger = {\n * warn: (msg, ...args) => myLoggingService.warn(msg, args),\n * error: (msg, ...args) => myLoggingService.error(msg, args),\n * };\n */\nexport interface Logger {\n /** Log a debug message (verbose debugging output) */\n debug?: (message: string, ...args: unknown[]) => void;\n /** Log an error message (critical failures) */\n error?: (message: string, ...args: unknown[]) => void;\n /** Log an informational message (key progress points) */\n info?: (message: string, ...args: unknown[]) => void;\n /** Log a trace message (extremely verbose, per-iteration details) */\n trace?: (message: string, ...args: unknown[]) => void;\n /** Log a warning message (potential issues) */\n warn?: (message: string, ...args: unknown[]) => void;\n}\n\n/**\n * - Default regex flags: `gu` (global + unicode)\n * - If `flags` is provided, it is validated and merged with required flags:\n * `g` and `u` are always enforced.\n *\n * `pageIds` controls which pages a rule applies to:\n * - `undefined`: apply to all pages\n * - `[]`: apply to no pages (rule is skipped)\n * - `[id1, id2, ...]`: apply only to those pages\n */\nexport type Replacement = {\n /** Raw regex source string (no token expansion). Compiled with `u` (and always `g`). */\n regex: string;\n /** Replacement string (passed to `String.prototype.replace`). */\n replacement: string;\n /** Optional regex flags; `g` and `u` are always enforced. */\n flags?: string;\n /** Optional list of page IDs to apply this replacement to. Empty array means skip. */\n pageIds?: number[];\n};\n\n// ─────────────────────────────────────────────────────────────\n// Segmentation Options\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Segmentation options controlling how pages are split.\n *\n * @example\n * // Basic structural rules only\n * const options: SegmentationOptions = {\n * rules: [\n * { lineStartsWith: ['## '], split: 'at', meta: { type: 'chapter' } },\n * { lineStartsWith: ['### '], split: 'at', meta: { type: 'section' } },\n * ]\n * };\n *\n * @example\n * // With breakpoints for oversized segments\n * const options: SegmentationOptions = {\n * rules: [{ lineStartsWith: ['{{fasl}}'], split: 'at' }],\n * maxPages: 2,\n * breakpoints: ['{{tarqim}}\\\\s*', '\\\\n', ''],\n * prefer: 'longer'\n * };\n *\n * @example\n * // With custom logger for debugging\n * const options: SegmentationOptions = {\n * rules: [...],\n * logger: {\n * debug: console.debug,\n * info: console.info,\n * warn: console.warn,\n * }\n * };\n */\nexport type SegmentationOptions = {\n /**\n * Optional pre-processing replacements applied to page content BEFORE segmentation.\n *\n * Replacements are applied per-page (not on concatenated content), in array order.\n */\n replace?: Replacement[];\n\n /**\n * Rules applied in order to find split points.\n *\n * All rules are evaluated against the content, and their matches\n * are combined to determine final split points. The first matching\n * rule's metadata is used for each segment.\n */\n rules?: SplitRule[];\n\n /**\n * Attach debugging provenance into `segment.meta` indicating which rule and/or breakpoint\n * created the segment boundary.\n *\n * This is opt-in because it increases output size.\n *\n * When enabled (default metaKey: `_flappa`), segments may include:\n * `meta._flappa.rule` and/or `meta._flappa.breakpoint`.\n */\n debug?:\n | boolean\n | {\n /** Where to store provenance in meta. @default '_flappa' */\n metaKey?: string;\n /** Which kinds of provenance to include. @default ['rule','breakpoint'] */\n include?: Array<'rule' | 'breakpoint'>;\n };\n\n /**\n * Maximum pages per segment before breakpoints are applied.\n *\n * When a segment spans more pages than this limit, the `breakpoints`\n * patterns are tried (in order) to find a suitable break point within\n * the allowed window.\n *\n * Structural markers (from rules) always take precedence - segments\n * are only broken within their rule-defined boundaries, never across them.\n *\n * @example\n * // Break segments that exceed 2 pages\n * { maxPages: 2, breakpoints: ['{{tarqim}}', ''] }\n */\n maxPages?: number;\n\n /**\n * Maximum length (in characters) per segment.\n *\n * When a segment exceeds this length, breakpoints are applied to split it.\n * This can typically be used in conjunction with `maxPages`, where the\n * strictest constraint (intersection) determines the split window.\n *\n * @example\n * // Break segments that exceed 2000 chars\n * { maxContentLength: 2000, breakpoints: ['{{tarqim}}'] }\n */\n maxContentLength?: number;\n\n /**\n * Patterns tried in order to break oversized segments.\n *\n * Each pattern is tried until one matches within the allowed page window.\n * Supports token expansion (e.g., `{{tarqim}}`). An empty string `''`\n * matches the page boundary (always succeeds as ultimate fallback).\n *\n * Patterns can be simple strings (apply everywhere) or objects with\n * `min`/`max` constraints to limit which pages they apply to.\n *\n * Patterns are checked in order - put preferred break styles first:\n * - `{{tarqim}}\\\\s*` - Break at sentence-ending punctuation\n * - `\\\\n` - Break at line breaks (useful for OCR content)\n * - `''` - Break at page boundary (always works)\n *\n * Only applied to segments exceeding `maxPages`.\n *\n * @example\n * // Simple patterns (backward compatible)\n * breakpoints: ['{{tarqim}}\\\\s*', '\\\\n', '']\n *\n * @example\n * // Object patterns with page constraints\n * breakpoints: [\n * { pattern: '{{tarqim}}\\\\s*', min: 10 }, // Only from page 10+\n * '' // Fallback for pages 1-9\n * ]\n */\n breakpoints?: Breakpoint[];\n\n /**\n * When multiple matches exist for a breakpoint pattern, select:\n * - `'longer'` - Last match in window (prefers longer segments)\n * - `'shorter'` - First match in window (prefers shorter segments)\n *\n * @default 'longer'\n */\n prefer?: 'longer' | 'shorter';\n\n /**\n * How to join content across page boundaries in OUTPUT segments.\n *\n * Internally, pages are still concatenated with `\\\\n` for matching (multiline regex),\n * but when a segment spans multiple pages, the inserted page-boundary separator is\n * normalized for output.\n *\n * - `'space'`: Join pages with a single space (default)\n * - `'newline'`: Preserve page boundary as a newline\n *\n * @default 'space'\n */\n pageJoiner?: 'space' | 'newline';\n\n /**\n * Optional logger for debugging segmentation.\n *\n * Provide a logger to receive detailed information about the segmentation\n * process. Useful for debugging pattern matching, page tracking, and\n * breakpoint processing issues.\n *\n * When not provided, no logging overhead is incurred (methods are not called).\n *\n * Verbosity levels:\n * - `trace`: Per-iteration details (very verbose)\n * - `debug`: Detailed operation information\n * - `info`: Key progress points\n * - `warn`: Potential issues\n * - `error`: Critical failures\n *\n * @example\n * // Console logger for development\n * logger: {\n * debug: console.debug,\n * info: console.info,\n * warn: console.warn,\n * }\n *\n * @example\n * // Custom logger integration\n * logger: {\n * debug: (msg, ...args) => winston.debug(msg, { meta: args }),\n * error: (msg, ...args) => winston.error(msg, { meta: args }),\n * }\n */\n logger?: Logger;\n};\n\n/**\n * Output segment produced by `segmentPages()`.\n *\n * Each segment contains extracted content, page references, and\n * optional metadata from the matched rule and captured groups.\n *\n * @example\n * // Simple segment on a single page\n * { content: '## Chapter 1\\nIntroduction...', from: 1, meta: { type: 'chapter' } }\n *\n * @example\n * // Segment spanning pages 5-7 with captured hadith number\n * { content: 'Hadith text...', from: 5, to: 7, meta: { type: 'hadith', hadithNum: '٤٢' } }\n */\nexport type Segment = {\n /**\n * Segment content with:\n * - Leading/trailing whitespace trimmed\n * - Page breaks converted to spaces (for multi-page segments)\n * - Markers stripped (for `lineStartsAfter` patterns)\n */\n content: string;\n\n /**\n * Starting page ID (from `Page.id`).\n */\n from: number;\n\n /**\n * Ending page ID if segment spans multiple pages.\n *\n * Only present when the segment content extends across page boundaries.\n * When `undefined`, the segment is contained within a single page.\n */\n to?: number;\n\n /**\n * Combined metadata from:\n * 1. Rule's `meta` property (static metadata)\n * 2. Named captures from patterns (e.g., `{{raqms:num}}` → `{ num: '٤٢' }`)\n *\n * Named captures override static metadata with the same key.\n */\n meta?: Record<string, unknown>;\n};\n","/**\n * Rule optimization utilities for merging and sorting split rules.\n *\n * Provides `optimizeRules()` to:\n * 1. Merge compatible rules with same pattern type and options\n * 2. Deduplicate patterns within each rule\n * 3. Sort rules by specificity (longer patterns first)\n *\n * @module optimize-rules\n */\n\nimport { PATTERN_TYPE_KEYS, type PatternTypeKey, type SplitRule } from './types.js';\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): PatternTypeKey => {\n for (const key of PATTERN_TYPE_KEYS) {\n if (key in rule) {\n return key;\n }\n }\n return 'regex'; // fallback\n};\n\n/**\n * Get the pattern array for a mergeable rule.\n */\nconst getPatternArray = (rule: SplitRule, key: PatternTypeKey): string[] => {\n const value = (rule as Record<string, unknown>)[key];\n return Array.isArray(value) ? (value as string[]) : [];\n};\n\n/**\n * Get a string representation of the pattern value (for specificity scoring).\n */\nconst getPatternString = (rule: SplitRule, key: PatternTypeKey): string => {\n const value = (rule as Record<string, unknown>)[key];\n if (typeof value === 'string') {\n return value;\n }\n if (Array.isArray(value)) {\n return value.join('\\n');\n }\n return '';\n};\n\n/**\n * Deduplicate and sort patterns by length (longest first).\n */\nconst normalizePatterns = (patterns: string[]): string[] => {\n const unique = [...new Set(patterns)];\n return unique.sort((a, b) => b.length - a.length || a.localeCompare(b));\n};\n\n/**\n * Calculate specificity score for a rule (higher = more specific).\n * Based on the longest pattern length.\n */\nconst getSpecificityScore = (rule: SplitRule): number => {\n const key = getPatternKey(rule);\n\n if (MERGEABLE_KEYS.has(key)) {\n const patterns = getPatternArray(rule, key);\n return patterns.reduce((max, p) => Math.max(max, p.length), 0);\n }\n\n return getPatternString(rule, key).length;\n};\n\n/**\n * Create a merge key for a rule based on pattern type and all non-pattern properties.\n * Rules with the same merge key can have their patterns combined.\n */\nconst createMergeKey = (rule: SplitRule): string => {\n const patternKey = getPatternKey(rule);\n const { [patternKey]: _pattern, ...rest } = rule as Record<string, unknown>;\n return `${patternKey}|${JSON.stringify(rest)}`;\n};\n\n/**\n * Optimize split rules by merging compatible rules and sorting by specificity.\n *\n * This function:\n * 1. **Merges compatible rules**: Rules with the same pattern type and identical\n * options (meta, fuzzy, min/max, etc.) have their pattern arrays combined\n * 2. **Deduplicates patterns**: Removes duplicate patterns within each rule\n * 3. **Sorts by specificity**: Rules with longer patterns come first\n *\n * Only array-based pattern types (`lineStartsWith`, `lineStartsAfter`, `lineEndsWith`)\n * can be merged. `template` and `regex` rules are kept separate.\n *\n * @param rules - Array of split rules to optimize\n * @returns Optimized rules and count of merged rules\n *\n * @example\n * import { optimizeRules } from 'flappa-doormal';\n *\n * const { rules, mergedCount } = optimizeRules([\n * { lineStartsWith: ['{{kitab}}'], fuzzy: true, meta: { type: 'header' } },\n * { lineStartsWith: ['{{bab}}'], fuzzy: true, meta: { type: 'header' } },\n * { lineStartsAfter: ['{{numbered}}'], meta: { type: 'entry' } },\n * ]);\n *\n * // rules[0] = { lineStartsWith: ['{{kitab}}', '{{bab}}'], fuzzy: true, meta: { type: 'header' } }\n * // rules[1] = { lineStartsAfter: ['{{numbered}}'], meta: { type: 'entry' } }\n * // mergedCount = 1\n */\nexport const optimizeRules = (rules: SplitRule[]): OptimizeResult => {\n const output: SplitRule[] = [];\n const indexByMergeKey = new Map<string, number>();\n let mergedCount = 0;\n\n for (const rule of rules) {\n const patternKey = getPatternKey(rule);\n\n // Only merge array-pattern rules\n if (!MERGEABLE_KEYS.has(patternKey)) {\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 // New rule - normalize patterns and add\n indexByMergeKey.set(mergeKey, output.length);\n output.push({\n ...rule,\n [patternKey]: normalizePatterns(getPatternArray(rule, patternKey)),\n } as SplitRule);\n continue;\n }\n\n // Merge patterns into existing rule\n const existing = output[existingIndex] as Record<string, unknown>;\n existing[patternKey] = normalizePatterns([\n ...getPatternArray(existing as SplitRule, patternKey),\n ...getPatternArray(rule, patternKey),\n ]);\n mergedCount++;\n }\n\n // Sort by specificity (most specific first)\n output.sort((a, b) => getSpecificityScore(b) - getSpecificityScore(a));\n\n return { mergedCount, rules: output };\n};\n","/**\n * Token-based template system for Arabic text pattern matching.\n *\n * This module provides a human-readable way to define regex patterns using\n * `{{token}}` placeholders that expand to their regex equivalents. It supports\n * named capture groups for extracting matched values into metadata.\n *\n * @module tokens\n *\n * @example\n * // Simple token expansion\n * expandTokens('{{raqms}} {{dash}}')\n * // → '[\\\\u0660-\\\\u0669]+ [-–—ـ]'\n *\n * @example\n * // Named capture groups\n * expandTokensWithCaptures('{{raqms:num}} {{dash}}')\n * // → { pattern: '(?<num>[\\\\u0660-\\\\u0669]+) [-–—ـ]', captureNames: ['num'], hasCaptures: true }\n */\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// ─────────────────────────────────────────────────────────────\n// Auto-escaping for template patterns\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): string => {\n // Match either a token ({{...}}) or a bracket character\n // Tokens are preserved as-is, brackets are escaped\n return pattern.replace(/(\\{\\{[^}]*\\}\\})|([()[\\]])/g, (_match, token, bracket) => {\n if (token) {\n return token; // Leave tokens intact\n }\n return `\\\\${bracket}`; // Escape the bracket\n });\n};\n\n// ─────────────────────────────────────────────────────────────\n// Base tokens - raw regex patterns (no template references)\n// ─────────────────────────────────────────────────────────────\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أ-ي])';\nconst RUMUZ_FOUR = '(?<![\\\\u0660-\\\\u0669])٤(?![\\\\u0660-\\\\u0669])';\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('|')})`;\nconst RUMUZ_BLOCK = `${RUMUZ_ATOM}(?:\\\\s+${RUMUZ_ATOM})*`;\n\nconst BASE_TOKENS: Record<string, string> = {\n /**\n * Chapter marker - Arabic word for \"chapter\" (باب).\n *\n * Commonly used in hadith collections to mark chapter divisions.\n *\n * @example 'باب ما جاء في الصلاة' (Chapter on what came regarding prayer)\n */\n bab: 'باب',\n\n /**\n * Basmala pattern - Arabic invocation \"In the name of Allah\" (بسم الله).\n *\n * Matches the beginning of the basmala formula, commonly appearing\n * at the start of chapters, books, or documents.\n *\n * @example 'بسم الله الرحمن الرحيم' (In the name of Allah, the Most Gracious, the Most Merciful)\n */\n basmalah: ['بسم الله', '﷽'].join('|'),\n\n /**\n * Bullet point variants - common bullet characters.\n *\n * Character class matching: `•` (bullet), `*` (asterisk), `°` (degree).\n *\n * @example '• First item'\n */\n bullet: '[•*°]',\n\n /**\n * Dash variants - various dash and separator characters.\n *\n * Character class matching:\n * - `-` (hyphen-minus U+002D)\n * - `–` (en-dash U+2013)\n * - `—` (em-dash U+2014)\n * - `ـ` (tatweel U+0640, Arabic elongation character)\n *\n * @example '٦٦٩٦ - حدثنا' or '٦٦٩٦ ـ حدثنا'\n */\n dash: '[-–—ـ]',\n\n /**\n * Section marker - Arabic word for \"section/issue\".\n * Commonly used for fiqh books.\n */\n fasl: ['مسألة', 'فصل'].join('|'),\n\n /**\n * Single Arabic letter - matches any Arabic letter character.\n *\n * Character range from أ (alef with hamza) to ي (ya).\n * Does NOT include diacritics (harakat/tashkeel).\n *\n * @example '{{harf}}' matches 'ب' in 'باب'\n */\n harf: '[أ-ي]',\n\n /**\n * One or more Arabic letters separated by spaces - matches sequences like \"د ت س ي ق\".\n *\n * Useful for matching abbreviation *lists* that are encoded as single-letter tokens\n * separated by spaces.\n *\n * IMPORTANT:\n * - This token intentionally matches **single letters only** (with optional spacing).\n * - It does NOT match multi-letter rumuz like \"سي\" or \"خت\". For those, use `{{rumuz}}`.\n *\n * @example '{{harfs}}' matches 'د ت س ي ق' in '١١١٨ د ت س ي ق: حجاج'\n * @example '{{raqms:num}} {{harfs}}:' matches number + abbreviations + colon\n */\n // Example matches: \"د ت س ي ق\"\n // Example non-matches: \"وعلامة ...\", \"في\", \"لا\", \"سي\", \"خت\"\n harfs: '[أ-ي](?:\\\\s+[أ-ي])*',\n\n /**\n * Book marker - Arabic word for \"book\" (كتاب).\n *\n * Commonly used in hadith collections to mark major book divisions.\n *\n * @example 'كتاب الإيمان' (Book of Faith)\n */\n kitab: 'كتاب',\n\n /**\n * Naql (transmission) phrases - common hadith transmission phrases.\n *\n * Alternation of Arabic phrases used to indicate narration chains:\n * - حدثنا (he narrated to us)\n * - أخبرنا (he informed us)\n * - حدثني (he narrated to me)\n * - وحدثنا (and he narrated to us)\n * - أنبأنا (he reported to us)\n * - سمعت (I heard)\n *\n * @example '{{naql}}' matches any of the above phrases\n */\n naql: ['حدثني', 'وأخبرنا', 'حدثنا', 'سمعت', 'أنبأنا', 'وحدثنا', 'أخبرنا', 'وحدثني', 'وحدثنيه'].join('|'),\n\n /**\n * Single Arabic-Indic digit - matches one digit (٠-٩).\n *\n * Unicode range: U+0660 to U+0669 (Arabic-Indic digits).\n * Use `{{raqms}}` for one or more digits.\n *\n * @example '{{raqm}}' matches '٥' in '٥ - '\n */\n raqm: '[\\\\u0660-\\\\u0669]',\n\n /**\n * One or more Arabic-Indic digits - matches digit sequences (٠-٩)+.\n *\n * Unicode range: U+0660 to U+0669 (Arabic-Indic digits).\n * Commonly used for hadith numbers, verse numbers, etc.\n *\n * @example '{{raqms}}' matches '٦٦٩٦' in '٦٦٩٦ - حدثنا'\n */\n raqms: '[\\\\u0660-\\\\u0669]+',\n\n /**\n * Rumuz (source abbreviations) used in rijāl / takhrīj texts.\n *\n * This token matches the known abbreviation set used to denote sources like:\n * - All six books: (ع)\n * - The four Sunan: (٤)\n * - Bukhari: خ / خت / خغ / بخ / عخ / ز / ي\n * - Muslim: م / مق / مت\n * - Nasa'i: س / ن / ص / عس / سي / كن\n * - Abu Dawud: د / مد / قد / خد / ف / فد / ل / دل / كد / غد / صد\n * - Tirmidhi: ت / تم\n * - Ibn Majah: ق / فق\n *\n * Notes:\n * - Order matters: longer alternatives must come before shorter ones (e.g., \"خد\" before \"خ\")\n * - This token matches a rumuz *block*: one or more codes separated by whitespace\n * (e.g., \"خ سي\", \"خ فق\", \"خت ٤\", \"د ت سي ق\")\n */\n rumuz: RUMUZ_BLOCK,\n\n /**\n * Punctuation characters.\n * Use {{tarqim}} which is especially useful when splitting using split: 'after' on punctuation marks.\n */\n tarqim: '[.!?؟؛]',\n};\n\n// ─────────────────────────────────────────────────────────────\n// Composite tokens - templates that reference base tokens\n// These are pre-expanded at module load time for performance\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Composite token definitions using template syntax.\n *\n * These tokens reference base tokens using `{{token}}` syntax and are\n * automatically expanded to their final regex patterns at module load time.\n *\n * This provides better abstraction - if base tokens change, composites\n * automatically update on the next build.\n *\n * @internal\n */\nconst COMPOSITE_TOKENS: Record<string, string> = {\n /**\n * Numbered hadith marker - common format for hadith numbering.\n *\n * Matches patterns like \"٢٢ - \" (number, space, dash, space).\n * This is the most common format in hadith collections.\n *\n * Use with `lineStartsAfter` to cleanly extract hadith content:\n * ```typescript\n * { lineStartsAfter: ['{{numbered}}'], split: 'at' }\n * ```\n *\n * For capturing the hadith number, use explicit capture syntax:\n * ```typescript\n * { lineStartsAfter: ['{{raqms:hadithNum}} {{dash}} '], split: 'at' }\n * ```\n *\n * @example '٢٢ - حدثنا' matches, content starts after '٢٢ - '\n * @example '٦٦٩٦ – أخبرنا' matches (with en-dash)\n */\n numbered: '{{raqms}} {{dash}} ',\n};\n\n/**\n * Expands any *composite* tokens (like `{{numbered}}`) into their underlying template form\n * (like `{{raqms}} {{dash}} `).\n *\n * This is useful when you want to take a signature produced by `analyzeCommonLineStarts()`\n * and turn it into an editable template where you can add named captures, e.g.:\n *\n * - `{{numbered}}` → `{{raqms}} {{dash}} `\n * - then: `{{raqms:num}} {{dash}} ` to capture the number\n *\n * Notes:\n * - This only expands the plain `{{token}}` form (not `{{token:name}}`).\n * - Expansion is repeated a few times to support nested composites.\n */\nexport const expandCompositeTokensInTemplate = (template: string): string => {\n let out = template;\n for (let i = 0; i < 10; i++) {\n const next = out.replace(/\\{\\{(\\w+)\\}\\}/g, (m, tokenName: string) => {\n const replacement = COMPOSITE_TOKENS[tokenName];\n return replacement ?? m;\n });\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): string => {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, tokenName) => {\n return BASE_TOKENS[tokenName] ?? `{{${tokenName}}}`;\n });\n};\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: Record<string, string> = {\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};\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): boolean => {\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): TemplateSegment[] => {\n const segments: TemplateSegment[] = [];\n let lastIndex = 0;\n TOKEN_WITH_CAPTURE_REGEX.lastIndex = 0;\n let match: RegExpExecArray | null;\n\n // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop pattern\n while ((match = TOKEN_WITH_CAPTURE_REGEX.exec(query)) !== null) {\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\n return segments;\n};\n\nconst maybeApplyFuzzyToText = (text: string, fuzzyTransform?: (pattern: string) => string): string => {\n if (fuzzyTransform && /[\\u0600-\\u06FF]/u.test(text)) {\n return fuzzyTransform(text);\n }\n return text;\n};\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): string => {\n if (!fuzzyTransform) {\n return tokenPattern;\n }\n return tokenPattern\n .split('|')\n .map((part) => (/[\\u0600-\\u06FF]/u.test(part) ? fuzzyTransform(part) : part))\n .join('|');\n};\n\nconst parseTokenLiteral = (literal: string): { tokenName: string; captureName: string } | null => {\n TOKEN_WITH_CAPTURE_REGEX.lastIndex = 0;\n const tokenMatch = TOKEN_WITH_CAPTURE_REGEX.exec(literal);\n if (!tokenMatch) {\n return null;\n }\n const [, tokenName, captureName] = tokenMatch;\n return { captureName, tokenName };\n};\n\nconst createCaptureRegistry = (capturePrefix?: string) => {\n const captureNames: string[] = [];\n const captureNameCounts = new Map<string, number>();\n\n const register = (baseName: string): 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): string => {\n const parsed = parseTokenLiteral(literal);\n if (!parsed) {\n return literal;\n }\n\n const { tokenName, captureName } = parsed;\n\n // {{:name}} - capture anything with name\n if (!tokenName && captureName) {\n const name = opts.registerCapture(captureName);\n return `(?<${name}>.+)`;\n }\n\n let tokenPattern = TOKEN_PATTERNS[tokenName];\n if (!tokenPattern) {\n // Unknown token - leave as-is\n return literal;\n }\n\n tokenPattern = maybeApplyFuzzyToTokenPattern(tokenPattern, opts.fuzzyTransform);\n\n // {{token:name}} - capture with name\n if (captureName) {\n const name = opts.registerCapture(captureName);\n return `(?<${name}>${tokenPattern})`;\n }\n\n // {{token}} - no capture, just expand\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): ExpandResult => {\n const segments = splitTemplateIntoSegments(query);\n const registry = createCaptureRegistry(capturePrefix);\n\n const processedParts = segments.map((segment) => {\n if (segment.type === 'text') {\n return maybeApplyFuzzyToText(segment.value, fuzzyTransform);\n }\n return expandTokenLiteral(segment.value, {\n capturePrefix,\n fuzzyTransform,\n registerCapture: registry.register,\n });\n });\n\n return {\n captureNames: registry.captureNames,\n hasCaptures: registry.captureNames.length > 0,\n pattern: processedParts.join(''),\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', 'basmala', 'bullet', ...]`)\n *\n * @example\n * getAvailableTokens()\n * // → ['bab', 'basmala', 'bullet', 'dash', 'harf', 'kitab', 'naql', 'raqm', 'raqms']\n */\nexport const getAvailableTokens = () => Object.keys(TOKEN_PATTERNS);\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')\n * @returns The regex pattern string, or `undefined` if token doesn't exist\n *\n * @example\n * getTokenPattern('raqms') // → '[\\\\u0660-\\\\u0669]+'\n * getTokenPattern('dash') // → '[-–—ـ]'\n * getTokenPattern('unknown') // → undefined\n */\nexport const getTokenPattern = (tokenName: string): string | undefined => 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[]): boolean => {\n const arr = Array.isArray(patterns) ? patterns : [patterns];\n return arr.some((p) => {\n FUZZY_TOKEN_REGEX.lastIndex = 0; // Reset stateful regex\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): string => {\n // Match {{token:name}} and replace with {{token}}\n return template.replace(/\\{\\{([^:}]+):[^}]+\\}\\}/g, '{{$1}}');\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 { getAvailableTokens } from './tokens.js';\nimport type { SplitRule } from './types.js';\n\n/**\n * Types of validation issues that can be detected.\n */\nexport type ValidationIssueType = 'missing_braces' | 'unknown_token' | 'duplicate' | 'empty_pattern';\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};\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 = (): RegExp => {\n // Sort by length descending to match longer tokens first\n const tokens = [...KNOWN_TOKENS].sort((a, b) => b.length - a.length);\n // Match token name followed by optional :name, but NOT inside {{}}\n // Use negative lookbehind for {{ and negative lookahead for }}\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>): ValidationIssue | undefined => {\n if (!pattern.trim()) {\n return { message: 'Empty pattern is not allowed', type: 'empty_pattern' };\n }\n // Check for duplicates\n if (seenPatterns.has(pattern)) {\n return {\n message: `Duplicate pattern: \"${pattern}\"`,\n pattern,\n type: 'duplicate',\n };\n }\n seenPatterns.add(pattern);\n\n // Check for unknown tokens inside {{}}\n const tokensInBraces = [...pattern.matchAll(TOKEN_INSIDE_BRACES)];\n for (const match of tokensInBraces) {\n const tokenName = match[1];\n if (!KNOWN_TOKENS.has(tokenName)) {\n return {\n message: `Unknown token: {{${tokenName}}}. Available tokens: ${[...KNOWN_TOKENS].slice(0, 5).join(', ')}...`,\n suggestion: `Check spelling or use a known token`,\n token: tokenName,\n type: 'unknown_token',\n };\n }\n }\n\n // Check for bare token names not inside {{}}\n const bareTokenRegex = buildBareTokenRegex();\n const bareMatches = [...pattern.matchAll(bareTokenRegex)];\n for (const match of bareMatches) {\n const tokenName = match[1];\n const fullMatch = match[0];\n // Make sure this isn't inside {{}} by checking the original pattern\n const matchIndex = match.index!;\n const before = pattern.slice(Math.max(0, matchIndex - 2), matchIndex);\n const after = pattern.slice(matchIndex + fullMatch.length, matchIndex + fullMatch.length + 2);\n if (before !== '{{' && after !== '}}') {\n return {\n message: `Token \"${tokenName}\" appears to be missing {{}}. Did you mean \"{{${fullMatch}}}\"?`,\n suggestion: `{{${fullMatch}}}`,\n token: tokenName,\n type: 'missing_braces',\n };\n }\n }\n\n return undefined;\n};\n\n/**\n * Validates an array of patterns, returning parallel array of issues.\n */\nconst validatePatternArray = (patterns: string[]): (ValidationIssue | undefined)[] | undefined => {\n const seenPatterns = new Set<string>();\n const issues = patterns.map((p) => validatePattern(p, seenPatterns));\n\n // If all undefined, return undefined for the whole array\n if (issues.every((i) => i === undefined)) {\n return undefined;\n }\n return issues;\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[]): (RuleValidationResult | undefined)[] => {\n return rules.map((rule) => {\n const result: RuleValidationResult = {};\n let hasIssues = false;\n\n if ('lineStartsWith' in rule && rule.lineStartsWith) {\n const issues = validatePatternArray(rule.lineStartsWith);\n if (issues) {\n result.lineStartsWith = issues;\n hasIssues = true;\n }\n }\n\n if ('lineStartsAfter' in rule && rule.lineStartsAfter) {\n const issues = validatePatternArray(rule.lineStartsAfter);\n if (issues) {\n result.lineStartsAfter = issues;\n hasIssues = true;\n }\n }\n\n if ('lineEndsWith' in rule && rule.lineEndsWith) {\n const issues = validatePatternArray(rule.lineEndsWith);\n if (issues) {\n result.lineEndsWith = issues;\n hasIssues = true;\n }\n }\n\n if ('template' in rule && rule.template !== undefined) {\n const seenPatterns = new Set<string>();\n const issue = validatePattern(rule.template, seenPatterns);\n if (issue) {\n result.template = issue;\n hasIssues = true;\n }\n }\n\n // Note: We don't validate `regex` patterns as they are raw regex, not templates\n\n return hasIssues ? result : undefined;\n });\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)[]): string[] => {\n const errors: string[] = [];\n\n results.forEach((result, ruleIndex) => {\n if (!result) {\n return;\n }\n\n // Helper to format a single issue\n // eslint-disable-next-line\n const formatIssue = (issue: any, location: string) => {\n if (!issue) {\n return;\n }\n const type = issue.type as ValidationIssueType;\n\n if (type === 'missing_braces' && issue.token) {\n errors.push(`${location}: Missing {{}} around token \"${issue.token}\"`);\n } else if (type === 'unknown_token' && issue.token) {\n errors.push(`${location}: Unknown token \"{{${issue.token}}}\"`);\n } else if (type === 'duplicate' && issue.pattern) {\n errors.push(`${location}: Duplicate pattern \"${issue.pattern}\"`);\n } else if (issue.message) {\n errors.push(`${location}: ${issue.message}`);\n } else {\n errors.push(`${location}: ${type}`);\n }\n };\n\n // Each result is a Record with pattern types as keys\n for (const [patternType, issues] of Object.entries(result)) {\n const list = Array.isArray(issues) ? issues : [issues];\n for (const issue of list) {\n if (issue) {\n formatIssue(issue, `Rule ${ruleIndex + 1}, ${patternType}`);\n }\n }\n }\n });\n\n return errors;\n};\n","import type { Page, SegmentationOptions } from './types.js';\n\n/**\n * A single replacement rule applied by `applyReplacements()` / `SegmentationOptions.replace`.\n *\n * Notes:\n * - `regex` is a raw JavaScript regex source string (no token expansion).\n * - Default flags are `gu` (global + unicode).\n * - If `flags` is provided, it is validated and `g` + `u` are always enforced.\n * - If `pageIds` is omitted, the rule applies to all pages.\n * - If `pageIds` is `[]`, the rule applies to no pages (rule is skipped).\n */\nexport type ReplaceRule = NonNullable<SegmentationOptions['replace']>[number];\n\nconst DEFAULT_REPLACE_FLAGS = 'gu';\n\nconst normalizeReplaceFlags = (flags?: string): string => {\n if (!flags) {\n return DEFAULT_REPLACE_FLAGS;\n }\n // Validate and de-duplicate flags. Force include g + u.\n const allowed = new Set(['g', 'i', 'm', 's', 'u', 'y']);\n const set = new Set<string>();\n for (const ch of flags) {\n if (!allowed.has(ch)) {\n throw new Error(`Invalid replace regex flag: \"${ch}\" (allowed: gimsyu)`);\n }\n set.add(ch);\n }\n set.add('g');\n set.add('u');\n\n // Stable ordering for reproducibility\n const order = ['g', 'i', 'm', 's', 'y', 'u'];\n return order.filter((c) => set.has(c)).join('');\n};\n\ntype CompiledReplaceRule = {\n re: RegExp;\n replacement: string;\n pageIdSet?: ReadonlySet<number>;\n};\n\nconst compileReplaceRules = (rules: ReplaceRule[]): CompiledReplaceRule[] => {\n const compiled: CompiledReplaceRule[] = [];\n for (const r of rules) {\n if (r.pageIds && r.pageIds.length === 0) {\n // Empty list means \"apply to no pages\"\n continue;\n }\n const flags = normalizeReplaceFlags(r.flags);\n const re = new RegExp(r.regex, flags);\n compiled.push({\n pageIdSet: r.pageIds ? new Set(r.pageIds) : undefined,\n re,\n replacement: r.replacement,\n });\n }\n return compiled;\n};\n\n/**\n * Applies ordered regex replacements to page content (per page).\n *\n * - Replacement rules are applied in array order.\n * - Each rule is applied globally (flag `g` enforced) with unicode mode (flag `u` enforced).\n * - `pageIds` can scope a rule to specific pages. `pageIds: []` skips the rule entirely.\n *\n * This function is intentionally **pure**:\n * it returns a new pages array only when changes are needed, otherwise it returns the original pages.\n */\nexport const applyReplacements = (pages: Page[], rules?: ReplaceRule[]): Page[] => {\n if (!rules || rules.length === 0 || pages.length === 0) {\n return pages;\n }\n const compiled = compileReplaceRules(rules);\n if (compiled.length === 0) {\n return pages;\n }\n\n return pages.map((p) => {\n let content = p.content;\n for (const rule of compiled) {\n if (rule.pageIdSet && !rule.pageIdSet.has(p.id)) {\n continue;\n }\n content = content.replace(rule.re, rule.replacement);\n }\n if (content === p.content) {\n return p;\n }\n return { ...p, content };\n });\n};\n\n\n","/**\n * Utility functions for breakpoint processing in the segmentation engine.\n *\n * These functions handle breakpoint normalization, page exclusion checking,\n * and segment creation. Extracted for independent testing and reuse.\n *\n * @module breakpoint-utils\n */\n\nimport type { Breakpoint, BreakpointRule, Logger, PageRange, Segment } from './types.js';\n\nconst WINDOW_PREFIX_LENGTHS = [80, 60, 40, 30, 20, 15] as const;\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.\nconst JOINER_PREFIX_LENGTHS = [80, 60, 40, 30, 20, 15, 12, 10, 8, 6] as const;\n\n/**\n * Normalizes a breakpoint to the object form.\n * Strings are converted to { pattern: str } with no constraints.\n *\n * @param bp - Breakpoint as string or object\n * @returns Normalized BreakpointRule object\n *\n * @example\n * normalizeBreakpoint('\\\\n\\\\n')\n * // → { pattern: '\\\\n\\\\n' }\n *\n * normalizeBreakpoint({ pattern: '\\\\n', min: 10 })\n * // → { pattern: '\\\\n', min: 10 }\n */\nexport const normalizeBreakpoint = (bp: Breakpoint): BreakpointRule => (typeof bp === 'string' ? { pattern: bp } : bp);\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): boolean => {\n if (!excludeList || excludeList.length === 0) {\n return false;\n }\n for (const item of excludeList) {\n if (typeof item === 'number') {\n if (pageId === item) {\n return true;\n }\n } else {\n const [from, to] = item;\n if (pageId >= from && pageId <= to) {\n return true;\n }\n }\n }\n return false;\n};\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): boolean => {\n if (rule.min !== undefined && pageId < rule.min) {\n return false;\n }\n if (rule.max !== undefined && pageId > rule.max) {\n return false;\n }\n return !isPageExcluded(pageId, rule.exclude);\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): Set<number> => {\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 const seg: Segment = { content: trimmed, from: fromPageId };\n if (toPageId !== undefined && toPageId !== fromPageId) {\n seg.to = toPageId;\n }\n if (meta) {\n seg.meta = meta;\n }\n return seg;\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};\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 */\nexport const expandBreakpoints = (breakpoints: Breakpoint[], processPattern: PatternProcessor): ExpandedBreakpoint[] =>\n breakpoints.map((bp) => {\n const rule = normalizeBreakpoint(bp);\n const excludeSet = buildExcludeSet(rule.exclude);\n const skipWhenRegex =\n rule.skipWhen !== undefined\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 : null;\n if (rule.pattern === '') {\n return { excludeSet, regex: null, rule, skipWhenRegex };\n }\n const expanded = processPattern(rule.pattern);\n try {\n return { excludeSet, regex: new RegExp(expanded, 'gmu'), rule, skipWhenRegex };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Invalid breakpoint regex: ${rule.pattern}\\n Cause: ${message}`);\n }\n });\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): string => {\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): 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): number => {\n const currentPageData = normalizedPages.get(pageIds[currentFromIdx]);\n if (!currentPageData) {\n return 0;\n }\n\n const remStart = remainingContent.trimStart().slice(0, Math.min(60, remainingContent.length));\n const needle = remStart.slice(0, Math.min(30, remStart.length));\n if (!needle) {\n return 0;\n }\n\n const idx = currentPageData.content.indexOf(needle);\n return idx > 0 ? idx : 0;\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 _currentFromIdx: number, // unused but kept for API compatibility\n targetPageIdx: number,\n expectedBoundary: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n logger?: Logger,\n): number => {\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 // The target page content might be truncated in the current segment due to structural split points\n // early in that page (e.g. headings). Use progressively shorter prefixes.\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\n // Collect all candidate positions within the search range\n const candidates: { pos: number; isNewline: boolean }[] = [];\n let pos = remainingContent.indexOf(prefix, searchStart);\n while (pos !== -1 && pos <= searchEnd) {\n if (pos > 0) {\n const charBefore = remainingContent[pos - 1];\n if (charBefore === '\\n') {\n // Page boundaries are marked by newlines - this is the strongest signal\n candidates.push({ isNewline: true, pos });\n } else if (/\\s/.test(charBefore)) {\n // Other whitespace is acceptable but less preferred\n candidates.push({ isNewline: false, pos });\n }\n }\n pos = remainingContent.indexOf(prefix, pos + 1);\n }\n\n if (candidates.length > 0) {\n // Prioritize: 1) newline-preceded matches, 2) closest to expected boundary\n const newlineCandidates = candidates.filter((c) => c.isNewline);\n const pool = newlineCandidates.length > 0 ? newlineCandidates : candidates;\n\n // Select the candidate closest to the expected boundary\n let bestCandidate = pool[0];\n let bestDistance = Math.abs(pool[0].pos - expectedBoundary);\n for (let i = 1; i < pool.length; i++) {\n const dist = Math.abs(pool[i].pos - expectedBoundary);\n if (dist < bestDistance) {\n bestDistance = dist;\n bestCandidate = pool[i];\n }\n }\n\n // Only accept the match if it's within MAX_DEVIATION of the expected boundary.\n // This prevents false positives when content is duplicated within pages.\n // MAX_DEVIATION of 2000 chars allows ~50-100% variance for typical\n // Arabic book pages (1000-3000 chars) while rejecting false positives\n // from duplicated content appearing mid-page.\n const MAX_DEVIATION = 2000;\n if (bestDistance <= MAX_DEVIATION) {\n return bestCandidate.pos;\n }\n\n logger?.debug?.('[breakpoints] findPageStartNearExpectedBoundary: Rejected match exceeding deviation', {\n bestDistance,\n expectedBoundary,\n matchPos: bestCandidate.pos,\n maxDeviation: MAX_DEVIATION,\n prefixLength: len,\n targetPageIdx,\n });\n\n // If best match is too far, continue to try shorter prefixes or return -1\n }\n }\n\n return -1;\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): number[] => {\n const boundaryPositions: number[] = [0];\n const startOffsetInFromPage = estimateStartOffsetInCurrentPage(segmentContent, fromIdx, pageIds, normalizedPages);\n\n for (let i = fromIdx + 1; i <= toIdx; i++) {\n const expectedBoundary =\n cumulativeOffsets[i] !== undefined && cumulativeOffsets[fromIdx] !== undefined\n ? Math.max(0, cumulativeOffsets[i] - cumulativeOffsets[fromIdx] - startOffsetInFromPage)\n : segmentContent.length;\n\n const pos = findPageStartNearExpectedBoundary(\n segmentContent,\n fromIdx,\n i,\n expectedBoundary,\n pageIds,\n normalizedPages,\n logger,\n );\n\n const prevBoundary = boundaryPositions[boundaryPositions.length - 1];\n\n // Strict > prevents duplicate boundaries when pages have identical content\n const MAX_DEVIATION = 2000;\n const isValidPosition = pos > 0 && pos > prevBoundary && Math.abs(pos - expectedBoundary) < MAX_DEVIATION;\n\n if (isValidPosition) {\n boundaryPositions.push(pos);\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, expectedBoundary);\n boundaryPositions.push(Math.min(estimate, segmentContent.length));\n }\n }\n\n boundaryPositions.push(segmentContent.length); // sentinel\n return boundaryPositions;\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): 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): number => {\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 currentFromIdx,\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): number => {\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 = (\n excludeSet: Set<number>,\n pageIds: number[],\n fromIdx: number,\n toIdx: number,\n): boolean => {\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): number => {\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.\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 * @returns Break position after the selected match, or -1 if no matches\n */\nexport const findPatternBreakPosition = (\n windowContent: string,\n regex: RegExp,\n prefer: 'longer' | 'shorter',\n): number => {\n // OPTIMIZATION: Stream matches instead of collecting all into an array.\n // Only track first and last match to avoid allocating large arrays for dense patterns.\n let first: { index: number; length: number } | undefined;\n let last: { index: number; length: number } | undefined;\n for (const m of windowContent.matchAll(regex)) {\n const match = { index: m.index, length: m[0].length };\n if (!first) {\n first = match;\n }\n last = match;\n }\n if (!first) {\n return -1;\n }\n const selected = prefer === 'longer' ? last! : first;\n return selected.index + selected.length;\n};\n\n/**\n * Handles page boundary breakpoint (empty pattern).\n * Returns break position or -1 if no valid position found.\n */\nconst handlePageBoundaryBreak = (\n remainingContent: string,\n windowEndIdx: number,\n windowEndPosition: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n): number => {\n const nextPageIdx = windowEndIdx + 1;\n if (nextPageIdx <= toIdx) {\n const nextPageData = normalizedPages.get(pageIds[nextPageIdx]);\n if (nextPageData) {\n const pos = findNextPagePosition(remainingContent, nextPageData);\n // Only trust findNextPagePosition if the result is reasonably close to windowEndPosition.\n // This prevents incorrect breaks when content is duplicated within pages.\n // Use a generous tolerance (2000 chars or 50% of windowEndPosition, whichever is larger).\n const tolerance = Math.max(2000, windowEndPosition * 0.5);\n if (pos > 0 && Math.abs(pos - windowEndPosition) <= tolerance) {\n return Math.min(pos, windowEndPosition, remainingContent.length);\n }\n }\n }\n // Fall back to windowEndPosition which is computed from cumulative offsets\n return Math.min(windowEndPosition, remainingContent.length);\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): { breakpointIndex: number; breakPos: number; rule: BreakpointRule } | null => {\n const { pageIds, normalizedPages, expandedBreakpoints, prefer } = ctx;\n\n for (let i = 0; i < expandedBreakpoints.length; i++) {\n const { rule, regex, excludeSet, skipWhenRegex } = expandedBreakpoints[i];\n // Check if this breakpoint applies to the current segment's starting page\n if (!isInBreakpointRange(pageIds[currentFromIdx], rule)) {\n continue;\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 continue;\n }\n\n // Check if content matches skipWhen pattern (pre-compiled)\n if (skipWhenRegex?.test(remainingContent)) {\n continue;\n }\n\n // Handle page boundary (empty pattern)\n if (regex === null) {\n return {\n breakPos: handlePageBoundaryBreak(\n remainingContent,\n windowEndIdx,\n windowEndPosition,\n toIdx,\n pageIds,\n normalizedPages,\n ),\n breakpointIndex: i,\n rule,\n };\n }\n\n // Find matches within window\n const windowContent = remainingContent.slice(0, Math.min(windowEndPosition, remainingContent.length));\n const breakPos = findPatternBreakPosition(windowContent, regex, prefer);\n if (breakPos > 0) {\n return { breakPos, breakpointIndex: i, rule };\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): number => {\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 // Includes Arabic comma (،), semicolon (؛), full stop (.), etc.\n if (/[\\s\\n.,;!?؛،۔]/.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): 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\n","import type { BreakpointRule, SplitRule } from './types.js';\n\nexport type DebugConfig = { includeBreakpoint: boolean; includeRule: boolean; metaKey: string } | null;\n\nexport const resolveDebugConfig = (debug: unknown): DebugConfig => {\n if (!debug) {\n return null;\n }\n if (debug === true) {\n return { includeBreakpoint: true, includeRule: true, metaKey: '_flappa' };\n }\n if (typeof debug !== 'object') {\n return null;\n }\n const metaKey = (debug as any).metaKey;\n const include = (debug as any).include;\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 if ('lineStartsWith' in rule) {\n return 'lineStartsWith';\n }\n if ('lineStartsAfter' in rule) {\n return 'lineStartsAfter';\n }\n if ('lineEndsWith' in rule) {\n return 'lineEndsWith';\n }\n if ('template' in rule) {\n return 'template';\n }\n return '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): Record<string, unknown> => {\n const out = meta ? { ...meta } : {};\n const existing = out[metaKey];\n const existingObj = isPlainObject(existing) ? existing : {};\n out[metaKey] = { ...existingObj, ...patch };\n return out;\n};\n\nexport const buildRuleDebugPatch = (ruleIndex: number, rule: SplitRule) => ({\n rule: { index: ruleIndex, patternType: getRulePatternType(rule) },\n});\n\nexport const buildBreakpointDebugPatch = (breakpointIndex: number, rule: BreakpointRule) => ({\n breakpoint: {\n index: breakpointIndex,\n kind: rule.pattern === '' ? 'pageBoundary' : 'pattern',\n pattern: rule.pattern,\n },\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 {\n adjustForSurrogate,\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';\nimport type { Breakpoint, Logger, Page, Segment } from './types.js';\n\nexport type BreakpointPatternProcessor = (pattern: string) => string;\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 ? pageData.length : 0;\n if (i < pageIds.length - 1) {\n totalOffset += 1; // separator between pages\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): boolean => expandedBreakpoints.some((bp) => hasExcludedPageInRange(bp.excludeSet, pageIds, fromIdx, toIdx));\n\nexport const computeWindowEndIdx = (currentFromIdx: number, toIdx: number, pageIds: number[], maxPages: number) => {\n const currentPageId = pageIds[currentFromIdx];\n const maxWindowPageId = currentPageId + 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\ntype PiecePages = { actualEndIdx: number; actualStartIdx: number };\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): PiecePages => {\n const actualStartIdx = findPageIndexForPosition(pieceStartPos, boundaryPositions, baseFromIdx);\n // For end position, use pieceEndPos - 1 to get the page containing the last character\n // (since pieceEndPos is exclusive)\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, Math.min(30, nextPageData.length));\n const remainingPrefix = remainingContent.trimStart().slice(0, Math.min(30, remainingContent.length));\n // Check both directions:\n // 1. remainingContent starts with page prefix (page is longer or equal)\n // 2. page content starts with remaining prefix (remaining is shorter)\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): { breakpointIndex?: number; breakOffset: number; breakpointRule?: { pattern: string } } => {\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 );\n\n if (patternMatch && patternMatch.breakPos > 0) {\n return {\n breakOffset: patternMatch.breakPos,\n breakpointIndex: patternMatch.breakpointIndex,\n breakpointRule: patternMatch.rule,\n };\n }\n\n // Fallback: If hitting maxContentLength, try to find a safe break position\n if (maxContentLength && windowEndPosition === maxContentLength) {\n const safeOffset = findSafeBreakPosition(remainingContent, windowEndPosition);\n if (safeOffset !== -1) {\n return { breakOffset: safeOffset };\n }\n // If no safe break (whitespace) found, ensure we don't split a surrogate pair\n const adjustedOffset = adjustForSurrogate(remainingContent, windowEndPosition);\n return { breakOffset: adjustedOffset };\n }\n\n return { breakOffset: windowEndPosition };\n};\n\n/**\n * Advances cursor position past any leading whitespace.\n */\nconst skipWhitespace = (content: string, startPos: number): number => {\n let pos = startPos;\n while (pos < content.length && /\\s/.test(content[pos])) {\n pos++;\n }\n return pos;\n};\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 result: Segment[] = [];\n const fullContent = segment.content;\n let cursorPos = 0;\n let currentFromIdx = fromIdx;\n let isFirstPiece = true;\n let lastBreakpoint: { breakpointIndex: number; rule: { pattern: string } } | 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 let i = 0;\n const MAX_SAFE_ITERATIONS = 100_000;\n while (cursorPos < fullContent.length && currentFromIdx <= toIdx && i < MAX_SAFE_ITERATIONS) {\n i++;\n const remainingContent = fullContent.slice(cursorPos);\n if (!remainingContent.trim()) {\n break;\n }\n\n if (\n handleOversizedSegmentFit(\n remainingContent,\n currentFromIdx,\n toIdx,\n pageIds,\n expandedBreakpoints,\n maxPages,\n maxContentLength,\n isFirstPiece,\n debugMetaKey,\n segment.meta,\n lastBreakpoint,\n result,\n )\n ) {\n break;\n }\n\n const windowEndIdx = computeWindowEndIdx(currentFromIdx, toIdx, pageIds, maxPages);\n const windowEndPosition = getWindowEndPosition(\n remainingContent,\n currentFromIdx,\n windowEndIdx,\n toIdx,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\n maxContentLength,\n logger,\n );\n\n logger?.debug?.(`[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 // 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 let breakOffset = found.breakOffset;\n if (breakOffset <= 0) {\n const fallbackPos = maxContentLength ? Math.min(maxContentLength, remainingContent.length) : 1;\n breakOffset = Math.max(1, fallbackPos);\n logger?.warn?.('[breakpoints] No progress from findBreakOffsetForWindow; forcing forward movement', {\n breakOffset,\n cursorPos,\n });\n }\n\n if (found.breakpointIndex !== undefined && found.breakpointRule) {\n lastBreakpoint = { breakpointIndex: found.breakpointIndex, rule: found.breakpointRule };\n }\n\n const breakPos = cursorPos + breakOffset;\n const pieceContent = fullContent.slice(cursorPos, breakPos).trim();\n\n if (pieceContent) {\n const { actualEndIdx, actualStartIdx } = computePiecePages(\n cursorPos,\n breakPos,\n boundaryPositions,\n fromIdx,\n toIdx,\n );\n const meta = getSegmentMetaWithDebug(isFirstPiece, debugMetaKey, segment.meta, lastBreakpoint);\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 cursorPos = next.cursorPos;\n currentFromIdx = next.currentFromIdx;\n } else {\n cursorPos = breakPos;\n }\n\n isFirstPiece = false;\n }\n\n if (i >= MAX_SAFE_ITERATIONS) {\n logger?.error?.('[breakpoints] Stopped processing oversized segment: reached MAX_SAFE_ITERATIONS', {\n cursorPos,\n fullContentLength: fullContent.length,\n iterations: i,\n });\n }\n\n logger?.debug?.('[breakpoints] done', { resultCount: result.length });\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 */\nconst handleOversizedSegmentFit = (\n remainingContent: string,\n currentFromIdx: number,\n toIdx: 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: { pattern: string } } | null,\n result: Segment[],\n): boolean => {\n const remainingSpan = computeRemainingSpan(currentFromIdx, toIdx, pageIds);\n const remainingHasExclusions = hasAnyExclusionsInRange(expandedBreakpoints, pageIds, currentFromIdx, toIdx);\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(remainingContent, currentFromIdx, toIdx, pageIds, meta, includeMeta);\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: { pattern: string } } | null,\n): Segment['meta'] | undefined => {\n const includeMeta = isFirstPiece || Boolean(debugMetaKey);\n if (!includeMeta) {\n return undefined;\n }\n\n if (debugMetaKey && lastBreakpoint) {\n return mergeDebugIntoMeta(\n isFirstPiece ? originalMeta : undefined,\n debugMetaKey,\n buildBreakpointDebugPatch(lastBreakpoint.breakpointIndex, lastBreakpoint.rule as any),\n );\n }\n return isFirstPiece ? originalMeta : undefined;\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): number => {\n let windowEndPosition = findBreakpointWindowEndPosition(\n remainingContent,\n currentFromIdx,\n windowEndIdx,\n toIdx,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\n logger,\n );\n\n if (maxContentLength && maxContentLength < windowEndPosition) {\n windowEndPosition = maxContentLength;\n }\n return windowEndPosition;\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): { currentFromIdx: number; cursorPos: number } => {\n const nextCursorPos = skipWhitespace(fullContent, breakPos);\n const nextFromIdx = computeNextFromIdx(\n fullContent.slice(nextCursorPos),\n actualEndIdx,\n toIdx,\n pageIds,\n normalizedPages,\n );\n return { currentFromIdx: nextFromIdx, cursorPos: nextCursorPos };\n};\n\n/**\n * Applies breakpoints to oversized segments.\n *\n * Note: This is an internal engine used by `segmentPages()`.\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: BreakpointPatternProcessor,\n logger?: Logger,\n pageJoiner: 'space' | 'newline' = 'space',\n debugMetaKey?: string,\n maxContentLength?: number,\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);\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 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 // 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 * Utility functions for regex matching and result processing.\n *\n * These functions were extracted from `segmenter.ts` to reduce complexity\n * and enable independent testing. They handle match filtering, capture\n * extraction, and occurrence-based selection.\n *\n * @module match-utils\n */\n\nimport { isPageExcluded } from './breakpoint-utils.js';\nimport type { SplitRule } from './types.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/**\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 = (\n groups: Record<string, string> | undefined,\n captureNames: string[],\n): Record<string, string> | undefined => {\n if (!groups || captureNames.length === 0) {\n return undefined;\n }\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\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): string | undefined => {\n if (match.length <= 1) {\n return undefined;\n }\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): MatchResult[] => {\n return matches.filter((m) => {\n const id = getId(m.start);\n if (rule.min !== undefined && id < rule.min) {\n return false;\n }\n if (rule.max !== undefined && id > rule.max) {\n return false;\n }\n if (isPageExcluded(id, rule.exclude)) {\n return false;\n }\n return true;\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'): MatchResult[] => {\n if (!matches.length) {\n return [];\n }\n if (occurrence === 'first') {\n return [matches[0]];\n }\n if (occurrence === 'last') {\n return [matches[matches.length - 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): boolean => {\n return rules.some((r) => {\n const minOk = r.min === undefined || pageId >= r.min;\n const maxOk = r.max === undefined || pageId <= r.max;\n return minOk && maxOk;\n });\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 { makeDiacriticInsensitive } from './fuzzy.js';\nimport { escapeTemplateBrackets, expandTokensWithCaptures, shouldDefaultToFuzzy } from './tokens.js';\nimport type { SplitRule } from './types.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\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 => {\n // Match ( that is NOT followed by ? (excludes non-capturing and named groups)\n return /\\((?!\\?)/.test(pattern);\n};\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 const names: string[] = [];\n // Match (?<name> where name is the capture group name\n const namedGroupRegex = /\\(\\?<([^>]+)>/g;\n for (const match of pattern.matchAll(namedGroupRegex)) {\n names.push(match[1]);\n }\n return names;\n};\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 const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Invalid regex pattern: ${pattern}\\n Cause: ${message}`);\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 escaped = escapeTemplateBrackets(pattern);\n const fuzzyTransform = fuzzy ? makeDiacriticInsensitive : undefined;\n const { pattern: expanded, captureNames } = expandTokensWithCaptures(escaped, fuzzyTransform, capturePrefix);\n return { captureNames, pattern: expanded };\n};\n\nexport const buildLineStartsAfterRegexSource = (\n patterns: string[],\n fuzzy: boolean,\n capturePrefix?: string,\n): { regex: string; captureNames: string[] } => {\n const processed = patterns.map((p) => processPattern(p, fuzzy, capturePrefix));\n const union = processed.map((p) => p.pattern).join('|');\n const captureNames = processed.flatMap((p) => p.captureNames);\n // For lineStartsAfter, we need to capture the content.\n // If we have a prefix (combined-regex mode), we name the internal content capture so the caller\n // can compute marker length. IMPORTANT: this internal group is not a \"user capture\", so it must\n // NOT be included in `captureNames` (otherwise it leaks into segment.meta as `content`).\n const contentCapture = capturePrefix ? `(?<${capturePrefix}__content>.*)` : '(.*)';\n // Allow zero-width formatters (LRM, RLM, ALM, etc.) at line start before matching content.\n const zeroWidthPrefix = '[\\\\u200E\\\\u200F\\\\u061C\\\\u200B\\\\uFEFF]*';\n return { captureNames, regex: `^${zeroWidthPrefix}(?:${union})${contentCapture}` };\n};\n\nexport const buildLineStartsWithRegexSource = (\n patterns: string[],\n fuzzy: boolean,\n capturePrefix?: string,\n): { regex: string; captureNames: string[] } => {\n const processed = patterns.map((p) => processPattern(p, fuzzy, capturePrefix));\n const union = processed.map((p) => p.pattern).join('|');\n const captureNames = processed.flatMap((p) => p.captureNames);\n // Allow zero-width formatters (LRM, RLM, ALM, etc.) at line start before matching content.\n // These invisible Unicode characters are common in Arabic text and shouldn't affect semantics.\n // U+200E (LRM), U+200F (RLM), U+061C (ALM), U+200B (ZWSP), U+FEFF (BOM/ZWNBSP)\n const zeroWidthPrefix = '[\\\\u200E\\\\u200F\\\\u061C\\\\u200B\\\\uFEFF]*';\n return { captureNames, regex: `^${zeroWidthPrefix}(?:${union})` };\n};\n\nexport const buildLineEndsWithRegexSource = (\n patterns: string[],\n fuzzy: boolean,\n capturePrefix?: string,\n): { regex: string; captureNames: string[] } => {\n const processed = patterns.map((p) => processPattern(p, fuzzy, capturePrefix));\n const union = processed.map((p) => p.pattern).join('|');\n const captureNames = processed.flatMap((p) => p.captureNames);\n return { captureNames, regex: `(?:${union})$` };\n};\n\nexport const buildTemplateRegexSource = (\n template: string,\n capturePrefix?: string,\n): { regex: string; captureNames: string[] } => {\n const escaped = escapeTemplateBrackets(template);\n const { pattern, captureNames } = expandTokensWithCaptures(escaped, undefined, capturePrefix);\n return { captureNames, regex: pattern };\n};\n\nexport const determineUsesCapture = (regexSource: string, _captureNames: string[]): boolean =>\n hasCapturingGroup(regexSource);\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 s: {\n lineStartsWith?: string[];\n lineStartsAfter?: string[];\n lineEndsWith?: string[];\n template?: string;\n regex?: string;\n } = { ...rule };\n\n // Auto-detect fuzzy for tokens like bab, kitab, etc. unless explicitly set\n const allPatterns = [...(s.lineStartsWith ?? []), ...(s.lineStartsAfter ?? []), ...(s.lineEndsWith ?? [])];\n const explicitFuzzy = (rule as { fuzzy?: boolean }).fuzzy;\n const fuzzy = explicitFuzzy ?? shouldDefaultToFuzzy(allPatterns);\n let allCaptureNames: string[] = [];\n\n // lineStartsAfter: creates a capturing group to exclude the marker from content\n if (s.lineStartsAfter?.length) {\n const { regex, captureNames } = buildLineStartsAfterRegexSource(s.lineStartsAfter, fuzzy, capturePrefix);\n allCaptureNames = captureNames;\n return {\n captureNames: allCaptureNames,\n regex: compileRuleRegex(regex),\n usesCapture: true,\n usesLineStartsAfter: true,\n };\n }\n\n if (s.lineStartsWith?.length) {\n const { regex, captureNames } = buildLineStartsWithRegexSource(s.lineStartsWith, fuzzy, capturePrefix);\n s.regex = regex;\n allCaptureNames = captureNames;\n }\n if (s.lineEndsWith?.length) {\n const { regex, captureNames } = buildLineEndsWithRegexSource(s.lineEndsWith, fuzzy, capturePrefix);\n s.regex = regex;\n allCaptureNames = captureNames;\n }\n if (s.template) {\n const { regex, captureNames } = buildTemplateRegexSource(s.template, capturePrefix);\n s.regex = regex;\n allCaptureNames = [...allCaptureNames, ...captureNames];\n }\n\n if (!s.regex) {\n throw new Error(\n 'Rule must specify exactly one pattern type: regex, template, lineStartsWith, lineStartsAfter, or lineEndsWith',\n );\n }\n\n // Extract named capture groups from raw regex patterns if not already populated\n if (allCaptureNames.length === 0) {\n allCaptureNames = extractNamedCaptureNames(s.regex);\n }\n\n const usesCapture = determineUsesCapture(s.regex, allCaptureNames);\n return {\n captureNames: allCaptureNames,\n regex: compileRuleRegex(s.regex),\n usesCapture,\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 } from './tokens.js';\n\n// U+064B..U+0652 (tashkeel/harakat)\nconst isArabicDiacriticCode = (code: number): boolean => code >= 0x064b && code <= 0x0652;\n\n// Map a char to a representative equivalence class key.\n// Keep this in sync with EQUIV_GROUPS in fuzzy.ts.\nconst equivKey = (ch: string): 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\n/**\n * Match a fuzzy literal prefix at a given offset.\n *\n * - Skips diacritics in the content\n * - Applies equivalence groups on both content and literal\n *\n * @returns endOffset (exclusive) in CONTENT if matched; otherwise null.\n */\nexport const matchFuzzyLiteralPrefixAt = (content: string, offset: number, literal: string): number | null => {\n let i = offset;\n // Skip leading diacritics in content (rare but possible)\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\n // In literal, we treat whitespace literally (no collapsing).\n // (Tokens like kitab/bab/fasl/naql/basmalah do not rely on fuzzy spaces.)\n // Skip diacritics in content before matching each char.\n while (i < content.length && isArabicDiacriticCode(content.charCodeAt(i))) {\n i++;\n }\n\n if (i >= content.length) {\n return null;\n }\n\n const cCh = content[i];\n if (equivKey(cCh) !== equivKey(litCh)) {\n return null;\n }\n i++;\n }\n\n // Allow trailing diacritics immediately after the matched prefix.\n while (i < content.length && isArabicDiacriticCode(content.charCodeAt(i))) {\n i++;\n }\n return i;\n};\n\nconst isLiteralOnly = (s: string): boolean => {\n // Reject anything that looks like regex syntax.\n // We allow only plain text (including Arabic, spaces) and the alternation separator `|`.\n // This intentionally rejects tokens like `tarqim: '[.!?؟؛]'`, which are not literal.\n return !/[\\\\[\\]{}()^$.*+?]/.test(s);\n};\n\nexport type CompiledLiteralAlternation = {\n alternatives: string[];\n};\n\nexport const compileLiteralAlternation = (pattern: string): CompiledLiteralAlternation | null => {\n if (!pattern) {\n return null;\n }\n if (!isLiteralOnly(pattern)) {\n return null;\n }\n const alternatives = pattern\n .split('|')\n .map((s) => s.trim())\n .filter(Boolean);\n if (!alternatives.length) {\n return null;\n }\n return { alternatives };\n};\n\nexport type FastFuzzyTokenRule = {\n token: string; // token name, e.g. 'kitab'\n alternatives: string[]; // resolved literal alternatives\n};\n\n/**\n * Attempt to compile a fast fuzzy rule from a single-token pattern like `{{kitab}}`.\n * Returns null if not eligible.\n */\nexport const compileFastFuzzyTokenRule = (tokenTemplate: string): FastFuzzyTokenRule | null => {\n const m = tokenTemplate.match(/^\\{\\{(\\w+)\\}\\}$/);\n if (!m) {\n return null;\n }\n const token = m[1];\n const tokenPattern = getTokenPattern(token);\n if (!tokenPattern) {\n return null;\n }\n const compiled = compileLiteralAlternation(tokenPattern);\n if (!compiled) {\n return null;\n }\n return { alternatives: compiled.alternatives, token };\n};\n\n/**\n * Try matching any alternative for a compiled token at a line-start offset.\n * Returns endOffset (exclusive) on match, else null.\n */\nexport const matchFastFuzzyTokenAt = (content: string, offset: number, compiled: FastFuzzyTokenRule): number | null => {\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 { isPageExcluded } from './breakpoint-utils.js';\nimport { compileFastFuzzyTokenRule, type FastFuzzyTokenRule, matchFastFuzzyTokenAt } from './fast-fuzzy-prefix.js';\nimport { extractNamedCaptureNames, hasCapturingGroup, processPattern } from './rule-regex.js';\nimport type { PageMap, SplitPoint } from './segmenter-types.js';\nimport type { SplitRule } from './types.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: SplitRule[];\n fastFuzzyRules: FastFuzzyRule[];\n};\n\nexport const partitionRulesForMatching = (rules: SplitRule[]): PartitionedRules => {\n const combinableRules: { rule: SplitRule; prefix: string; index: number }[] = [];\n const standaloneRules: SplitRule[] = [];\n const fastFuzzyRules: FastFuzzyRule[] = [];\n\n // Separate rules into combinable, standalone, and fast-fuzzy\n rules.forEach((rule, index) => {\n // Fast-path: fuzzy + lineStartsWith + single token pattern like {{kitab}}\n if ((rule as { fuzzy?: boolean }).fuzzy && 'lineStartsWith' in rule) {\n const compiled =\n rule.lineStartsWith.length === 1 ? compileFastFuzzyTokenRule(rule.lineStartsWith[0]) : null;\n if (compiled) {\n fastFuzzyRules.push({ compiled, kind: 'startsWith', rule, ruleIndex: index });\n return; // handled by fast path\n }\n }\n\n // Fast-path: fuzzy + lineStartsAfter + single token pattern like {{naql}}\n if ((rule as { fuzzy?: boolean }).fuzzy && 'lineStartsAfter' in rule) {\n const compiled =\n rule.lineStartsAfter.length === 1 ? compileFastFuzzyTokenRule(rule.lineStartsAfter[0]) : null;\n if (compiled) {\n fastFuzzyRules.push({ compiled, kind: 'startsAfter', rule, ruleIndex: index });\n return; // handled by fast path\n }\n }\n\n let isCombinable = true;\n\n // Raw regex rules are combinable ONLY if they don't use named captures, backreferences, or anonymous captures\n if ('regex' in rule && rule.regex) {\n const hasNamedCaptures = extractNamedCaptureNames(rule.regex).length > 0;\n const hasBackreferences = /\\\\[1-9]/.test(rule.regex);\n const hasAnonymousCaptures = hasCapturingGroup(rule.regex);\n if (hasNamedCaptures || hasBackreferences || hasAnonymousCaptures) {\n isCombinable = false;\n }\n }\n\n if (isCombinable) {\n combinableRules.push({ index, prefix: `r${index}_`, rule });\n } else {\n standaloneRules.push(rule);\n }\n });\n\n return { combinableRules, fastFuzzyRules, standaloneRules };\n};\n\nexport type PageStartGuardChecker = (rule: SplitRule, ruleIndex: number, matchStart: number) => boolean;\n\nexport const createPageStartGuardChecker = (matchContent: string, pageMap: PageMap): PageStartGuardChecker => {\n const pageStartToBoundaryIndex = new Map<number, number>();\n for (let i = 0; i < pageMap.boundaries.length; i++) {\n pageStartToBoundaryIndex.set(pageMap.boundaries[i].start, i);\n }\n\n const compiledPageStartPrev = new Map<number, RegExp | null>();\n const getPageStartPrevRegex = (rule: SplitRule, ruleIndex: number): RegExp | null => {\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 expanded = processPattern(pattern, false).pattern;\n const re = new RegExp(`(?:${expanded})$`, 'u');\n compiledPageStartPrev.set(ruleIndex, re);\n return re;\n };\n\n const getPrevPageLastNonWsChar = (boundaryIndex: number): string => {\n if (boundaryIndex <= 0) {\n return '';\n }\n const prevBoundary = pageMap.boundaries[boundaryIndex - 1];\n // prevBoundary.end points at the inserted page-break newline; the last content char is end-1.\n for (let i = prevBoundary.end - 1; i >= prevBoundary.start; i--) {\n const ch = matchContent[i];\n if (!ch) {\n continue;\n }\n if (/\\s/u.test(ch)) {\n continue;\n }\n return ch;\n }\n return '';\n };\n\n return (rule: SplitRule, ruleIndex: number, matchStart: number): boolean => {\n const boundaryIndex = pageStartToBoundaryIndex.get(matchStart);\n if (boundaryIndex === undefined || boundaryIndex === 0) {\n return true; // not a page start, or the very first page\n }\n const prevReq = getPageStartPrevRegex(rule, ruleIndex);\n if (!prevReq) {\n return true;\n }\n const lastChar = getPrevPageLastNonWsChar(boundaryIndex);\n if (!lastChar) {\n return false;\n }\n return prevReq.test(lastChar);\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): boolean => {\n return (\n (rule.min === undefined || pageId >= rule.min) &&\n (rule.max === undefined || pageId <= rule.max) &&\n !isPageExcluded(pageId, rule.exclude)\n );\n};\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 return;\n }\n arr.push(sp);\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 isPageStart: boolean,\n splitPointsByRule: Map<number, SplitPoint[]>,\n) => {\n for (const { compiled, kind, rule, ruleIndex } of fastFuzzyRules) {\n if (!passesRuleConstraints(rule, pageId)) {\n continue;\n }\n\n if (isPageStart && !passesPageStartGuard(rule, ruleIndex, lineStart)) {\n continue;\n }\n\n const end = matchFastFuzzyTokenAt(matchContent, lineStart, compiled);\n if (end === null) {\n continue;\n }\n\n const splitIndex = (rule.split ?? 'at') === 'at' ? lineStart : end;\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: (rule.split ?? 'at') === 'at' ? markerLength : undefined,\n index: splitIndex,\n meta: rule.meta,\n });\n }\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 const isPageStart = (offset: number): boolean => offset === currentBoundary?.start;\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 isPageStart(lineStart),\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 { isPageExcluded } from './breakpoint-utils.js';\nimport {\n extractNamedCaptures,\n filterByConstraints,\n getLastPositionalCapture,\n type MatchResult,\n} from './match-utils.js';\nimport { buildRuleRegex, type RuleRegex } from './rule-regex.js';\nimport type { PageMap, SplitPoint } from './segmenter-types.js';\nimport type { Logger, SplitRule } from './types.js';\nimport { buildRuleDebugPatch, mergeDebugIntoMeta } from './debug-meta.js';\n\n// Maximum iterations before throwing to prevent infinite loops\nconst MAX_REGEX_ITERATIONS = 100000;\n\ntype CombinableRule = { rule: SplitRule; prefix: string; index: number };\n\ntype RuleRegexInfo = RuleRegex & { prefix: string; source: string };\n\n// ─────────────────────────────────────────────────────────────\n// Combined regex matching\n// ─────────────────────────────────────────────────────────────\n\nconst extractNamedCapturesForRule = (\n groups: Record<string, string> | undefined,\n captureNames: string[],\n prefix: string,\n): Record<string, string> => {\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): boolean =>\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 { contentStartOffset } = buildContentOffsets(match, ruleInfo);\n\n return {\n capturedContent: undefined,\n 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 };\n};\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): void => {\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 iterations++;\n\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\n if (iterations % 10000 === 0) {\n logger?.warn?.('[segmenter] high iteration count', { iterations, position: m.index });\n }\n\n const matchedIndex = combinableRules.findIndex(({ prefix }) => m?.groups?.[prefix] !== undefined);\n\n if (matchedIndex !== -1) {\n const { rule, index: originalIndex } = combinableRules[matchedIndex];\n const ruleInfo = ruleRegexes[matchedIndex];\n const pageId = pageMap.getId(m.index);\n\n if (passesRuleConstraints(rule, pageId) && passesPageStartGuard(rule, originalIndex, m.index)) {\n const sp = createSplitPointFromMatch(m, rule, ruleInfo);\n\n if (!splitPointsByRule.has(originalIndex)) {\n splitPointsByRule.set(originalIndex, []);\n }\n splitPointsByRule.get(originalIndex)!.push(sp);\n }\n }\n\n if (m[0].length === 0) {\n combinedRegex.lastIndex++;\n }\n m = combinedRegex.exec(matchContent);\n }\n};\n\nexport const buildRuleRegexes = (combinableRules: CombinableRule[]): RuleRegexInfo[] =>\n combinableRules.map(({ rule, prefix }) => {\n const built = buildRuleRegex(rule, prefix);\n return { ...built, prefix, source: `(?<${prefix}>${built.regex.source})` };\n });\n\n// ─────────────────────────────────────────────────────────────\n// Standalone rule processing\n// ─────────────────────────────────────────────────────────────\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): void => {\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 guarded = constrained.filter((m) => passesPageStartGuard(rule, ruleIndex, m.start));\n\n const points = guarded.map((m) => {\n const isLSA = usesLineStartsAfter && m.captured !== undefined;\n const markerLen = isLSA ? m.end - m.captured!.length - m.start : 0;\n return {\n capturedContent: isLSA ? undefined : m.captured,\n contentStartOffset: isLSA ? markerLen : undefined,\n index: (rule.split ?? 'at') === 'at' ? m.start : m.end,\n meta: rule.meta,\n namedCaptures: m.namedCaptures,\n };\n });\n\n if (!splitPointsByRule.has(ruleIndex)) {\n splitPointsByRule.set(ruleIndex, []);\n }\n splitPointsByRule.get(ruleIndex)!.push(...points);\n};\n\nconst findMatchesInContent = (\n content: string,\n regex: RegExp,\n usesCapture: boolean,\n captureNames: string[],\n): MatchResult[] => {\n const matches: MatchResult[] = [];\n let m = regex.exec(content);\n\n while (m !== null) {\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 });\n if (m[0].length === 0) {\n regex.lastIndex++;\n }\n m = regex.exec(content);\n }\n\n return matches;\n};\n\n// ─────────────────────────────────────────────────────────────\n// Occurrence filtering\n// ─────────────────────────────────────────────────────────────\n\nexport const applyOccurrenceFilter = (\n rules: SplitRule[],\n splitPointsByRule: Map<number, SplitPoint[]>,\n debugMetaKey?: string,\n): SplitPoint[] => {\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 if (!debugMetaKey) {\n result.push(...filtered.map((p) => ({ ...p, ruleIndex: index })));\n return;\n }\n\n const debugPatch = buildRuleDebugPatch(index, rule);\n result.push(\n ...filtered.map((p) => ({\n ...p,\n meta: mergeDebugIntoMeta(p.meta, debugMetaKey, debugPatch),\n ruleIndex: index,\n })),\n );\n });\n\n return result;\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 * Core segmentation engine for splitting Arabic text pages into logical segments.\n *\n * The segmenter takes an array of pages and applies pattern-based rules to\n * identify split points, producing segments with content, page references,\n * and optional metadata.\n *\n * @module segmenter\n */\n\nimport { applyBreakpoints } from './breakpoint-processor.js';\nimport { resolveDebugConfig } from './debug-meta.js';\nimport { anyRuleAllowsId } from './match-utils.js';\nimport { applyReplacements } from './replace.js';\nimport { processPattern } from './rule-regex.js';\nimport {\n collectFastFuzzySplitPoints,\n createPageStartGuardChecker,\n partitionRulesForMatching,\n} from './segmenter-rule-utils.js';\nimport type { PageBoundary, PageMap, SplitPoint } from './segmenter-types.js';\nimport {\n applyOccurrenceFilter,\n buildRuleRegexes,\n processCombinedMatches,\n processStandaloneRule,\n} from './split-point-helpers.js';\nimport { normalizeLineEndings } from './textUtils.js';\nimport type { Logger, Page, Segment, SegmentationOptions, SplitRule } from './types.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[]): { content: string; normalizedPages: string[]; pageMap: PageMap } => {\n const boundaries: PageBoundary[] = [];\n const pageBreaks: number[] = []; // Sorted array for binary search\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); // Already in sorted order\n offset += normalized.length + 1;\n } else {\n offset += normalized.length;\n }\n }\n\n /**\n * Finds the page boundary containing the given offset using binary search.\n * O(log n) complexity for efficient lookup with many pages.\n *\n * @param off - Character offset to look up\n * @returns Page boundary or the last boundary as fallback\n */\n const findBoundary = (off: number): PageBoundary | undefined => {\n let lo = 0;\n let hi = boundaries.length - 1;\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1; // Unsigned right shift for floor division\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 // Fallback to last boundary if not found\n return boundaries[boundaries.length - 1];\n };\n\n return {\n content: parts.join('\\n'),\n normalizedPages: parts, // OPTIMIZATION: Return already-normalized content for reuse\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 const hasMoreInfo =\n (p.contentStartOffset !== undefined && existing.contentStartOffset === undefined) ||\n (p.meta !== undefined && existing.meta === undefined);\n if (hasMoreInfo) {\n byIndex.set(p.index, p);\n }\n }\n const unique = [...byIndex.values()];\n unique.sort((a, b) => a.index - b.index);\n return unique;\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 const firstPage = pages[0];\n const lastPage = pages.at(-1)!;\n const joinChar = pageJoiner === 'newline' ? '\\n' : ' ';\n const allContent = normalizedContent.join(joinChar).trim();\n if (!allContent) {\n return segments;\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 // Start with fast-fuzzy matches\n const splitPointsByRule = collectFastFuzzySplitPoints(matchContent, pageMap, fastFuzzyRules, passesPageStartGuard);\n\n // Process combinable rules in a single pass\n if (combinableRules.length > 0) {\n const ruleRegexes = buildRuleRegexes(combinableRules);\n processCombinedMatches(\n matchContent,\n combinableRules,\n ruleRegexes,\n pageMap,\n passesPageStartGuard,\n splitPointsByRule,\n logger,\n );\n }\n\n // Process standalone rules\n for (const rule of standaloneRules) {\n const originalIndex = rules.indexOf(rule);\n processStandaloneRule(rule, originalIndex, matchContent, pageMap, passesPageStartGuard, splitPointsByRule);\n }\n\n // Apply occurrence filtering and flatten\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 // Binary search for first break >= startOffset\n let lo = 0;\n let 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 // Collect breaks until we exceed endOffset\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 * @returns Content with page-break newlines converted to spaces\n */\nconst convertPageBreaks = (content: string, startOffset: number, pageBreaks: number[]) => {\n // OPTIMIZATION: Fast-path for empty or no-newline content (common cases)\n if (!content || !content.includes('\\n')) {\n return content;\n }\n\n const endOffset = startOffset + content.length;\n const breaksInRange = findBreaksInRange(startOffset, endOffset, pageBreaks);\n\n // No page breaks in this segment - return as-is (most common case)\n if (breaksInRange.length === 0) {\n return content;\n }\n\n // Convert ONLY page-break newlines (the ones inserted during concatenation) to spaces.\n //\n // NOTE: Offsets from findBreaksInRange are string indices (code units). Using Array.from()\n // would index by Unicode code points and can desync indices if surrogate pairs appear.\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 { rules = [], breakpoints = [], prefer = 'longer', pageJoiner = 'space', logger, maxContentLength } = options;\n\n if (maxContentLength && maxContentLength < 50) {\n throw new Error(`maxContentLength must be at least 50 characters.`);\n }\n\n // Default maxPages to 0 (single page) unless maxContentLength is provided\n const maxPages = options.maxPages ?? (maxContentLength ? Number.MAX_SAFE_INTEGER : 0);\n\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 ruleCount: rules.length,\n });\n\n const processedPages = options.replace ? applyReplacements(pages, options.replace) : pages;\n const { content: matchContent, normalizedPages: normalizedContent, pageMap } = buildPageMap(processedPages);\n\n logger?.debug?.('[segmenter] content built', {\n pageIds: pageMap.pageIds,\n totalContentLength: matchContent.length,\n });\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 // Build initial segments from structural rules\n let segments = buildSegments(unique, matchContent, pageMap, rules);\n\n logger?.debug?.('[segmenter] structural segments built', {\n segmentCount: segments.length,\n segments: segments.map((s) => ({ contentLength: s.content.length, from: s.from, to: s.to })),\n });\n\n segments = ensureFallbackSegment(segments, processedPages, normalizedContent, pageJoiner);\n\n // Apply breakpoints post-processing for oversized segments\n if ((maxPages >= 0 || (maxContentLength && maxContentLength > 0)) && breakpoints.length) {\n logger?.debug?.('[segmenter] applying breakpoints to oversized segments');\n const patternProcessor = (p: string) => processPattern(p, false).pattern;\n const result = applyBreakpoints(\n segments,\n processedPages,\n normalizedContent,\n maxPages,\n breakpoints,\n prefer,\n patternProcessor,\n logger,\n pageJoiner,\n debug?.includeBreakpoint ? debug.metaKey : undefined,\n maxContentLength,\n );\n logger?.info?.('[segmenter] segmentation complete (with breakpoints)', {\n finalSegmentCount: result.length,\n });\n return result;\n }\n logger?.info?.('[segmenter] segmentation complete (structural only)', {\n finalSegmentCount: segments.length,\n });\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 = (splitPoints: SplitPoint[], content: string, pageMap: PageMap, rules: SplitRule[]) => {\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 ): Segment | null => {\n // For lineStartsAfter, skip the marker by using contentStartOffset\n const actualStart = start + (contentStartOffset ?? 0);\n // For lineStartsAfter (contentStartOffset set), trim leading whitespace after marker\n // For other rules, only trim trailing whitespace to preserve intentional leading spaces\n const sliced = content.slice(actualStart, end);\n let text = capturedContent?.trim() ?? (contentStartOffset ? sliced.trim() : sliced.replace(/[\\s\\n]+$/, ''));\n if (!text) {\n return null;\n }\n if (!capturedContent) {\n text = convertPageBreaks(text, actualStart, pageMap.pageBreaks);\n }\n const from = pageMap.getId(actualStart);\n const to = capturedContent ? pageMap.getId(end - 1) : pageMap.getId(actualStart + text.length - 1);\n const seg: Segment = { content: text, from };\n if (to !== from) {\n seg.to = to;\n }\n if (meta || namedCaptures) {\n seg.meta = { ...meta, ...namedCaptures };\n }\n return seg;\n };\n\n /**\n * Creates segments from an array of split points.\n */\n const createSegmentsFromSplitPoints = (): Segment[] => {\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","// Shared utilities for analysis functions\n\nimport { getAvailableTokens, TOKEN_PATTERNS } from '../segmentation/tokens.js';\n\n// ─────────────────────────────────────────────────────────────\n// Helpers shared across analysis modules\n// ─────────────────────────────────────────────────────────────\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 const compiled: CompiledTokenRegex[] = [];\n for (const token of tokenNames) {\n const pat = TOKEN_PATTERNS[token];\n if (!pat) {\n continue;\n }\n try {\n compiled.push({ re: new RegExp(pat, 'uy'), token });\n } catch {\n // Ignore invalid patterns\n }\n }\n return compiled;\n};\n\nexport const appendWs = (out: string, mode: 'regex' | 'space'): string => {\n if (!out) {\n return out;\n }\n if (mode === 'space') {\n return out.endsWith(' ') ? out : `${out} `;\n }\n return out.endsWith('\\\\s*') ? out : `${out}\\\\s*`;\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 { normalizeLineEndings } from '../segmentation/textUtils.js';\nimport type { Page } from '../segmentation/types.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// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Options resolution\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Specificity & sorting\n// ─────────────────────────────────────────────────────────────\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\n// ─────────────────────────────────────────────────────────────\n// Signature building helpers\n// ─────────────────────────────────────────────────────────────\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/** Consume prefix matchers at current position */\nconst consumePrefixes = (\n s: string,\n pos: number,\n out: string,\n matchers: RegExp[],\n ws: 'regex' | 'space',\n): { pos: number; out: string; matched: boolean } => {\n let matched = false;\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 matched = true;\n const wsm = /^[ \\t]+/u.exec(s.slice(pos));\n if (wsm) {\n pos += wsm[0].length;\n out = appendWs(out, ws);\n }\n }\n }\n return { matched, out, pos };\n};\n\n/** Try to match a token at current position and append to signature */\nconst tryMatchToken = (\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 if (!best) {\n return { matched: false, out, pos };\n }\n return { matched: true, out: `${out}{{${best.token}}}`, pos: pos + best.text.length };\n};\n\n/** Try to match a delimiter at current position */\nconst tryMatchDelimiter = (s: string, pos: number, out: string): { pos: number; out: string; matched: boolean } => {\n const ch = s[pos];\n if (!ch || !isCommonDelimiter(ch)) {\n return { matched: false, out, pos };\n }\n return { matched: true, out: out + escapeSignatureLiteral(ch), pos: pos + 1 };\n};\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// ─────────────────────────────────────────────────────────────\n// Main tokenization\n// ─────────────────────────────────────────────────────────────\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 = consumePrefixes(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 // Skip whitespace\n const ws = skipWhitespace(s, pos, out, opts.whitespace);\n if (ws.skipped) {\n pos = ws.pos;\n out = ws.out;\n continue;\n }\n\n // Try token\n const tok = tryMatchToken(s, pos, out, compiled);\n if (tok.matched) {\n pos = tok.pos;\n out = tok.out;\n matchedAny = matchedToken = true;\n steps++;\n continue;\n }\n\n // Try delimiter (only after matching something)\n if (matchedAny) {\n const delim = tryMatchDelimiter(s, pos, out);\n if (delim.matched) {\n pos = delim.pos;\n out = delim.out;\n continue;\n }\n }\n\n // Fallback logic\n if (matchedAny) {\n if (opts.includeFirstWordFallback && !matchedToken) {\n const word = extractFirstWord(s.slice(pos));\n if (word) {\n out += escapeSignatureLiteral(word);\n steps++;\n }\n }\n break;\n }\n\n if (!opts.includeFirstWordFallback) {\n return null;\n }\n\n const word = extractFirstWord(s.slice(pos));\n if (!word) {\n return null;\n }\n return escapeSignatureLiteral(word);\n }\n\n return matchedAny ? trimTrailingWs(out, opts.whitespace) : null;\n};\n\n// ─────────────────────────────────────────────────────────────\n// Page processing\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Main export\n// ─────────────────────────────────────────────────────────────\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 '../segmentation/types.js';\nimport {\n buildTokenPriority,\n compileTokenRegexes,\n escapeSignatureLiteral,\n findBestTokenMatchAt,\n isArabicLetter,\n isCommonDelimiter,\n stripArabicDiacritics,\n} from './shared.js';\n\n// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Resolved options with defaults\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Raw position tracking for diacritic normalization\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Token content scanner\n// ─────────────────────────────────────────────────────────────\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// N-gram pattern extraction\n// ─────────────────────────────────────────────────────────────\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\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 for (let n = opts.minElements; n <= Math.min(opts.maxElements, items.length - i); n++) {\n const window = items.slice(i, i + n);\n\n if (opts.requireToken && !hasTokenInWindow(window)) {\n continue;\n }\n\n const pattern = buildPattern(window, opts.whitespace);\n\n if (!stats.has(pattern)) {\n if (stats.size >= opts.maxUniquePatterns) {\n continue;\n }\n stats.set(pattern, { count: 0, examples: [], ...computeWindowStats(window) });\n }\n\n const entry = stats.get(pattern)!;\n entry.count++;\n\n if (entry.examples.length < opts.maxExamples) {\n entry.examples.push(buildExample(page, window, opts.contextChars));\n }\n }\n }\n};\n\n// ─────────────────────────────────────────────────────────────\n// Main export\n// ─────────────────────────────────────────────────────────────\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","/**\n * Pattern detection utilities for recognizing template tokens in Arabic text.\n * Used to auto-detect patterns from user-highlighted text in the segmentation dialog.\n *\n * @module pattern-detection\n */\n\nimport { 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 { applyReplacements } from './segmentation/replace.js';\nimport { buildRuleRegex } from './segmentation/rule-regex.js';\nimport { segmentPages } from './segmentation/segmenter.js';\nimport { normalizeLineEndings } from './segmentation/textUtils.js';\nimport type { Page, Segment, SegmentationOptions, SplitRule } from './segmentation/types.js';\n\nexport type MarkerRecoverySelector =\n | { type: 'rule_indices'; indices: number[] }\n | { type: 'lineStartsAfter_patterns'; match?: 'exact' | 'normalized'; patterns: string[] }\n | { type: 'predicate'; predicate: (rule: SplitRule, index: number) => boolean };\n\nexport type MarkerRecoveryRun = {\n options: SegmentationOptions;\n pages: Page[];\n segments: Segment[];\n selector: MarkerRecoverySelector;\n};\n\nexport type MarkerRecoveryReport = {\n summary: {\n mode: 'rerun_only' | 'best_effort_then_rerun';\n recovered: number;\n totalSegments: number;\n unchanged: number;\n unresolved: number;\n };\n byRun?: Array<{\n recovered: number;\n runIndex: number;\n totalSegments: number;\n unresolved: number;\n }>;\n details: Array<{\n from: number;\n originalStartPreview: string;\n recoveredPrefixPreview?: string;\n recoveredStartPreview?: string;\n segmentIndex: number;\n status: 'recovered' | 'skipped_idempotent' | 'unchanged' | 'unresolved_alignment' | 'unresolved_selector';\n strategy: 'rerun' | 'stage1' | 'none';\n to?: number;\n notes?: string[];\n }>;\n errors: string[];\n warnings: string[];\n};\n\ntype NormalizeCompareMode = 'none' | 'whitespace' | 'whitespace_and_nfkc';\n\nconst preview = (s: string, max = 40): string => (s.length <= max ? s : `${s.slice(0, max)}…`);\n\nconst normalizeForCompare = (s: string, mode: NormalizeCompareMode): string => {\n if (mode === 'none') {\n return s;\n }\n let out = s;\n if (mode === 'whitespace_and_nfkc') {\n // Use alternation (not a character class) to satisfy Biome's noMisleadingCharacterClass rule.\n out = out.normalize('NFKC').replace(/(?:\\u200C|\\u200D|\\uFEFF)/gu, '');\n }\n // Collapse whitespace and normalize line endings\n out = out.replace(/\\r\\n?/gu, '\\n').replace(/\\s+/gu, ' ').trim();\n return out;\n};\n\nconst segmentRangeKey = (s: Pick<Segment, 'from' | 'to'>): string => `${s.from}|${s.to ?? s.from}`;\n\nconst buildFixedOptions = (options: SegmentationOptions, selectedRuleIndices: Set<number>): SegmentationOptions => {\n const rules = options.rules ?? [];\n const fixedRules: SplitRule[] = rules.map((r, idx) => {\n if (!selectedRuleIndices.has(idx)) {\n return r;\n }\n if (!('lineStartsAfter' in r) || !r.lineStartsAfter) {\n return r;\n }\n\n // Convert: lineStartsAfter -> lineStartsWith, keep all other fields.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { lineStartsAfter, ...rest } = r as SplitRule & { lineStartsAfter: string[] };\n return { ...(rest as Omit<SplitRule, 'lineStartsAfter'>), lineStartsWith: lineStartsAfter };\n });\n\n return { ...options, rules: fixedRules };\n};\n\nconst buildPageIdToIndex = (pages: Page[]): Map<number, number> => new Map(pages.map((p, i) => [p.id, i]));\n\ntype RangeContent = {\n matchContent: string;\n outputContent: string;\n};\n\nconst buildRangeContent = (\n processedPages: Page[],\n fromIdx: number,\n toIdx: number,\n pageJoiner: 'space' | 'newline',\n): RangeContent => {\n const parts: string[] = [];\n for (let i = fromIdx; i <= toIdx; i++) {\n parts.push(normalizeLineEndings(processedPages[i].content));\n }\n const matchContent = parts.join('\\n');\n if (pageJoiner === 'newline') {\n return { matchContent, outputContent: matchContent };\n }\n // Only convert the inserted page-boundary separators (exactly those join '\\n's) to spaces.\n // In-page newlines remain as-is.\n // Since we built matchContent by joining pages with '\\n', the separators are exactly between each part.\n // Replacing all '\\n' would corrupt in-page newlines, so we rebuild outputContent explicitly.\n const outputContent = parts.join(' ');\n return { matchContent, outputContent };\n};\n\ntype CompiledMistakenRule = {\n ruleIndex: number;\n // A regex that matches the marker at a line start (equivalent to lineStartsWith).\n startsWithRegex: RegExp;\n};\n\nconst compileMistakenRulesAsStartsWith = (\n options: SegmentationOptions,\n selectedRuleIndices: Set<number>,\n): CompiledMistakenRule[] => {\n const rules = options.rules ?? [];\n const compiled: CompiledMistakenRule[] = [];\n\n for (const idx of selectedRuleIndices) {\n const r = rules[idx];\n if (!r || !('lineStartsAfter' in r) || !r.lineStartsAfter?.length) {\n continue;\n }\n // Convert cleanly without using `delete` (keeps TS happy with discriminated unions).\n const { lineStartsAfter, ...rest } = r as SplitRule & { lineStartsAfter: string[] };\n const converted: SplitRule = {\n ...(rest as Omit<SplitRule, 'lineStartsAfter'>),\n lineStartsWith: lineStartsAfter,\n };\n\n const built = buildRuleRegex(converted);\n // built.regex has flags gmu; we want a stable, non-global matcher.\n compiled.push({ ruleIndex: idx, startsWithRegex: new RegExp(built.regex.source, 'mu') });\n }\n\n return compiled;\n};\n\ntype Stage1Result =\n | { kind: 'recovered'; recoveredContent: string; recoveredPrefix: string }\n | { kind: 'skipped_idempotent' }\n | { kind: 'unresolved'; reason: string };\n\nconst findUniqueAnchorPos = (outputContent: string, segmentContent: string): number | null => {\n const prefixLens = [80, 60, 40, 30, 20, 15] as const;\n\n for (const len of prefixLens) {\n const needle = segmentContent.slice(0, Math.min(len, segmentContent.length));\n if (!needle.trim()) {\n continue;\n }\n\n const first = outputContent.indexOf(needle);\n if (first === -1) {\n continue;\n }\n const second = outputContent.indexOf(needle, first + 1);\n if (second === -1) {\n return first;\n }\n }\n\n return null;\n};\n\nconst findRecoveredPrefixAtLineStart = (\n segmentContent: string,\n matchContent: string,\n lineStart: number,\n anchorPos: number,\n compiledMistaken: CompiledMistakenRule[],\n): { prefix: string } | { reason: string } => {\n const line = matchContent.slice(lineStart);\n\n for (const mr of compiledMistaken) {\n mr.startsWithRegex.lastIndex = 0;\n const m = mr.startsWithRegex.exec(line);\n if (!m || m.index !== 0) {\n continue;\n }\n\n const markerMatch = m[0];\n const markerEnd = lineStart + markerMatch.length;\n if (anchorPos < markerEnd) {\n continue; // anchor is inside marker; unsafe\n }\n\n // If there is whitespace between the marker match and the anchored content (common when lineStartsAfter trims),\n // include it in the recovered prefix.\n const gap = matchContent.slice(markerEnd, anchorPos);\n const recoveredPrefix = /^\\s*$/u.test(gap) ? `${markerMatch}${gap}` : markerMatch;\n\n // Idempotency: if content already starts with the marker/prefix, don’t prepend.\n if (segmentContent.startsWith(markerMatch) || segmentContent.startsWith(recoveredPrefix)) {\n return { reason: 'content already starts with selected marker' };\n }\n\n return { prefix: recoveredPrefix };\n }\n\n return { reason: 'no selected marker pattern matched at anchored line start' };\n};\n\nconst tryBestEffortRecoverOneSegment = (\n segment: Segment,\n processedPages: Page[],\n pageIdToIndex: Map<number, number>,\n compiledMistaken: CompiledMistakenRule[],\n pageJoiner: 'space' | 'newline',\n): Stage1Result => {\n const fromIdx = pageIdToIndex.get(segment.from);\n const toIdx = pageIdToIndex.get(segment.to ?? segment.from) ?? fromIdx;\n if (fromIdx === undefined || toIdx === undefined || fromIdx < 0 || toIdx < fromIdx) {\n return { kind: 'unresolved', reason: 'segment page range not found in pages' };\n }\n\n const { matchContent, outputContent } = buildRangeContent(processedPages, fromIdx, toIdx, pageJoiner);\n if (!segment.content) {\n return { kind: 'unresolved', reason: 'empty segment content' };\n }\n\n const anchorPos = findUniqueAnchorPos(outputContent, segment.content);\n if (anchorPos === null) {\n return { kind: 'unresolved', reason: 'could not uniquely anchor segment content in page range' };\n }\n\n // Find line start in matchContent. (Positions align because outputContent differs only by page-boundary joiner.)\n const lineStart = matchContent.lastIndexOf('\\n', Math.max(0, anchorPos - 1)) + 1;\n const found = findRecoveredPrefixAtLineStart(segment.content, matchContent, lineStart, anchorPos, compiledMistaken);\n if ('reason' in found) {\n return found.reason.includes('already starts')\n ? { kind: 'skipped_idempotent' }\n : { kind: 'unresolved', reason: found.reason };\n }\n return { kind: 'recovered', recoveredContent: `${found.prefix}${segment.content}`, recoveredPrefix: found.prefix };\n};\n\nconst resolveRuleIndicesSelector = (rules: SplitRule[], indicesIn: number[]) => {\n const errors: string[] = [];\n const indices = new Set<number>();\n for (const idx of indicesIn) {\n if (!Number.isInteger(idx) || idx < 0 || idx >= rules.length) {\n errors.push(`Selector index out of range: ${idx}`);\n continue;\n }\n const rule = rules[idx];\n if (!rule || !('lineStartsAfter' in rule)) {\n errors.push(`Selector index ${idx} is not a lineStartsAfter rule`);\n continue;\n }\n indices.add(idx);\n }\n return { errors, indices, warnings: [] as string[] };\n};\n\nconst resolvePredicateSelector = (rules: SplitRule[], predicate: (rule: SplitRule, index: number) => boolean) => {\n const errors: string[] = [];\n const warnings: string[] = [];\n const indices = new Set<number>();\n\n rules.forEach((r, i) => {\n try {\n if (!predicate(r, i)) {\n return;\n }\n if ('lineStartsAfter' in r && r.lineStartsAfter?.length) {\n indices.add(i);\n return;\n }\n warnings.push(`Predicate selected rule ${i}, but it is not a lineStartsAfter rule; skipping`);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n errors.push(`Predicate threw at rule ${i}: ${msg}`);\n }\n });\n\n if (indices.size === 0) {\n warnings.push('Predicate did not select any lineStartsAfter rules');\n }\n\n return { errors, indices, warnings };\n};\n\nconst resolvePatternsSelector = (\n rules: SplitRule[],\n patterns: string[],\n matchMode: 'exact' | 'normalized' | undefined,\n): { errors: string[]; indices: Set<number>; warnings: string[] } => {\n const errors: string[] = [];\n const warnings: string[] = [];\n const indices = new Set<number>();\n\n const normalizePattern = (p: string) =>\n normalizeForCompare(p, (matchMode ?? 'exact') === 'normalized' ? 'whitespace_and_nfkc' : 'none');\n const targets = patterns.map(normalizePattern);\n\n for (let pi = 0; pi < patterns.length; pi++) {\n const rawPattern = patterns[pi];\n const pat = targets[pi];\n const matched: number[] = [];\n\n for (let i = 0; i < rules.length; i++) {\n const r = rules[i];\n if (!('lineStartsAfter' in r) || !r.lineStartsAfter?.length) {\n continue;\n }\n if (r.lineStartsAfter.some((rp) => normalizePattern(rp) === pat)) {\n matched.push(i);\n }\n }\n\n if (matched.length === 0) {\n errors.push(`Pattern \"${rawPattern}\" did not match any lineStartsAfter rule`);\n continue;\n }\n if (matched.length > 1) {\n warnings.push(`Pattern \"${rawPattern}\" matched multiple lineStartsAfter rules: [${matched.join(', ')}]`);\n }\n matched.forEach((i) => {\n indices.add(i);\n });\n }\n\n return { errors, indices, warnings };\n};\n\nconst resolveSelectorToRuleIndices = (options: SegmentationOptions, selector: MarkerRecoverySelector) => {\n const rules = options.rules ?? [];\n if (selector.type === 'rule_indices') {\n return resolveRuleIndicesSelector(rules, selector.indices);\n }\n if (selector.type === 'predicate') {\n return resolvePredicateSelector(rules, selector.predicate);\n }\n return resolvePatternsSelector(rules, selector.patterns, selector.match);\n};\n\ntype AlignmentCandidate = { fixedIndex: number; kind: 'exact' | 'exact_suffix' | 'normalized_suffix'; score: number };\n\nconst longestCommonSuffixLength = (a: string, b: string): number => {\n const max = Math.min(a.length, b.length);\n let i = 0;\n while (i < max) {\n if (a[a.length - 1 - i] !== b[b.length - 1 - i]) {\n break;\n }\n i++;\n }\n return i;\n};\n\n// Minimum score difference required to confidently disambiguate between candidates.\n// If the gap between best and second-best is smaller, we consider the match ambiguous.\nconst AMBIGUITY_SCORE_GAP = 5;\n\nconst scoreCandidate = (\n orig: Segment,\n fixed: Segment,\n normalizeMode: NormalizeCompareMode,\n): AlignmentCandidate | null => {\n // Scoring hierarchy:\n // exact (100) - Content is identical, no recovery needed\n // exact_suffix (90-120) - Fixed ends with original; fixed = marker + orig (most reliable)\n // normalized_suffix (70-90) - Suffix match after whitespace/NFKC normalization\n // Higher scores indicate more confident alignment.\n\n if (fixed.content === orig.content) {\n return { fixedIndex: -1, kind: 'exact', score: 100 };\n }\n\n if (fixed.content.endsWith(orig.content)) {\n // Most reliable case: fixed = marker + orig.\n // Bonus points for longer markers (up to 30) to prefer substantive recovery.\n const markerLen = fixed.content.length - orig.content.length;\n const bonus = Math.min(30, markerLen);\n return { fixedIndex: -1, kind: 'exact_suffix', score: 90 + bonus };\n }\n\n if (normalizeMode !== 'none') {\n const normFixed = normalizeForCompare(fixed.content, normalizeMode);\n const normOrig = normalizeForCompare(orig.content, normalizeMode);\n if (normFixed.endsWith(normOrig) && normOrig.length > 0) {\n // Base score 70, plus up to 20 bonus based on overlap ratio\n const overlap = longestCommonSuffixLength(normFixed, normOrig) / normOrig.length;\n return { fixedIndex: -1, kind: 'normalized_suffix', score: 70 + Math.floor(overlap * 20) };\n }\n }\n\n return null;\n};\n\nconst buildNoSelectionResult = (\n segments: Segment[],\n reportBase: Omit<MarkerRecoveryReport, 'details' | 'summary'>,\n mode: MarkerRecoveryReport['summary']['mode'],\n selectorErrors: string[],\n): { report: MarkerRecoveryReport; segments: Segment[] } => {\n const warnings = [...reportBase.warnings];\n warnings.push('No lineStartsAfter rules selected for recovery; returning segments unchanged');\n\n const details: MarkerRecoveryReport['details'] = segments.map((s, i) => {\n const status: MarkerRecoveryReport['details'][number]['status'] = selectorErrors.length\n ? 'unresolved_selector'\n : 'unchanged';\n return {\n from: s.from,\n notes: selectorErrors.length ? (['selector did not resolve'] as string[]) : undefined,\n originalStartPreview: preview(s.content),\n segmentIndex: i,\n status,\n strategy: 'none',\n to: s.to,\n };\n });\n\n return {\n report: {\n ...reportBase,\n details,\n summary: {\n mode,\n recovered: 0,\n totalSegments: segments.length,\n unchanged: segments.length,\n unresolved: selectorErrors.length ? segments.length : 0,\n },\n warnings,\n },\n segments,\n };\n};\n\nconst runStage1IfEnabled = (\n pages: Page[],\n segments: Segment[],\n options: SegmentationOptions,\n selectedRuleIndices: Set<number>,\n mode: MarkerRecoveryReport['summary']['mode'],\n): {\n recoveredAtIndex: Map<number, Segment>;\n recoveredDetailAtIndex: Map<number, MarkerRecoveryReport['details'][number]>;\n} => {\n const recoveredAtIndex = new Map<number, Segment>();\n const recoveredDetailAtIndex = new Map<number, MarkerRecoveryReport['details'][number]>();\n\n if (mode !== 'best_effort_then_rerun') {\n return { recoveredAtIndex, recoveredDetailAtIndex };\n }\n\n const processedPages = options.replace ? applyReplacements(pages, options.replace) : pages;\n const pageIdToIndex = buildPageIdToIndex(processedPages);\n const pageJoiner = options.pageJoiner ?? 'space';\n const compiledMistaken = compileMistakenRulesAsStartsWith(options, selectedRuleIndices);\n\n for (let i = 0; i < segments.length; i++) {\n const orig = segments[i];\n const r = tryBestEffortRecoverOneSegment(orig, processedPages, pageIdToIndex, compiledMistaken, pageJoiner);\n if (r.kind !== 'recovered') {\n continue;\n }\n\n const seg: Segment = { ...orig, content: r.recoveredContent };\n recoveredAtIndex.set(i, seg);\n recoveredDetailAtIndex.set(i, {\n from: orig.from,\n originalStartPreview: preview(orig.content),\n recoveredPrefixPreview: preview(r.recoveredPrefix),\n recoveredStartPreview: preview(seg.content),\n segmentIndex: i,\n status: 'recovered',\n strategy: 'stage1',\n to: orig.to,\n });\n }\n\n return { recoveredAtIndex, recoveredDetailAtIndex };\n};\n\nconst buildFixedBuckets = (fixedSegments: Segment[]): Map<string, number[]> => {\n const buckets = new Map<string, number[]>();\n for (let i = 0; i < fixedSegments.length; i++) {\n const k = segmentRangeKey(fixedSegments[i]);\n const arr = buckets.get(k);\n if (!arr) {\n buckets.set(k, [i]);\n } else {\n arr.push(i);\n }\n }\n return buckets;\n};\n\ntype BestFixedMatch = { kind: 'none' } | { kind: 'ambiguous' } | { kind: 'match'; fixedIdx: number };\n\nconst findBestFixedMatch = (\n orig: Segment,\n candidates: number[],\n fixedSegments: Segment[],\n usedFixed: Set<number>,\n normalizeCompare: NormalizeCompareMode,\n): BestFixedMatch => {\n let best: { fixedIdx: number; score: number } | null = null;\n let secondBestScore = -Infinity;\n\n for (const fixedIdx of candidates) {\n if (usedFixed.has(fixedIdx)) {\n continue;\n }\n const fixed = fixedSegments[fixedIdx];\n const scored = scoreCandidate(orig, fixed, normalizeCompare);\n if (!scored) {\n continue;\n }\n const candidateScore = scored.score;\n if (!best || candidateScore > best.score) {\n secondBestScore = best?.score ?? -Infinity;\n best = { fixedIdx, score: candidateScore };\n } else if (candidateScore > secondBestScore) {\n secondBestScore = candidateScore;\n }\n }\n\n if (!best) {\n return { kind: 'none' };\n }\n if (best.score - secondBestScore < AMBIGUITY_SCORE_GAP && candidates.length > 1) {\n return { kind: 'ambiguous' };\n }\n return { fixedIdx: best.fixedIdx, kind: 'match' };\n};\n\nconst detailUnresolved = (\n orig: Segment,\n segmentIndex: number,\n notes: string[],\n): MarkerRecoveryReport['details'][number] => ({\n from: orig.from,\n notes,\n originalStartPreview: preview(orig.content),\n segmentIndex,\n status: 'unresolved_alignment',\n strategy: 'rerun',\n to: orig.to,\n});\n\nconst detailSkippedIdempotent = (\n orig: Segment,\n segmentIndex: number,\n notes: string[],\n): MarkerRecoveryReport['details'][number] => ({\n from: orig.from,\n notes,\n originalStartPreview: preview(orig.content),\n segmentIndex,\n status: 'skipped_idempotent',\n strategy: 'rerun',\n to: orig.to,\n});\n\nconst detailRecoveredRerun = (\n orig: Segment,\n fixed: Segment,\n segmentIndex: number,\n): MarkerRecoveryReport['details'][number] => {\n let recoveredPrefixPreview: string | undefined;\n if (fixed.content.endsWith(orig.content)) {\n recoveredPrefixPreview = preview(fixed.content.slice(0, fixed.content.length - orig.content.length));\n }\n return {\n from: orig.from,\n originalStartPreview: preview(orig.content),\n recoveredPrefixPreview,\n recoveredStartPreview: preview(fixed.content),\n segmentIndex,\n status: 'recovered',\n strategy: 'rerun',\n to: orig.to,\n };\n};\n\nconst mergeWithRerun = (params: {\n fixedBuckets: Map<string, number[]>;\n fixedSegments: Segment[];\n normalizeCompare: NormalizeCompareMode;\n originalSegments: Segment[];\n recoveredDetailAtIndex: Map<number, MarkerRecoveryReport['details'][number]>;\n stage1RecoveredAtIndex: Map<number, Segment>;\n}): {\n details: MarkerRecoveryReport['details'];\n segments: Segment[];\n summary: Omit<MarkerRecoveryReport['summary'], 'mode' | 'totalSegments'>;\n} => {\n const {\n fixedBuckets,\n fixedSegments,\n normalizeCompare,\n originalSegments,\n stage1RecoveredAtIndex,\n recoveredDetailAtIndex,\n } = params;\n\n const usedFixed = new Set<number>();\n const out: Segment[] = [];\n const details: MarkerRecoveryReport['details'] = [];\n let recovered = 0;\n let unresolved = 0;\n let unchanged = 0;\n\n for (let i = 0; i < originalSegments.length; i++) {\n const stage1Recovered = stage1RecoveredAtIndex.get(i);\n if (stage1Recovered) {\n out.push(stage1Recovered);\n recovered++;\n details.push(\n recoveredDetailAtIndex.get(i) ?? {\n from: stage1Recovered.from,\n originalStartPreview: preview(originalSegments[i].content),\n recoveredStartPreview: preview(stage1Recovered.content),\n segmentIndex: i,\n status: 'recovered',\n strategy: 'stage1',\n to: stage1Recovered.to,\n },\n );\n continue;\n }\n\n const orig = originalSegments[i];\n const candidates = fixedBuckets.get(segmentRangeKey(orig)) ?? [];\n\n const best = findBestFixedMatch(orig, candidates, fixedSegments, usedFixed, normalizeCompare);\n if (best.kind === 'none') {\n out.push(orig);\n unresolved++;\n details.push(detailUnresolved(orig, i, ['no alignment candidate in rerun output for same (from,to)']));\n continue;\n }\n if (best.kind === 'ambiguous') {\n out.push(orig);\n unresolved++;\n details.push(detailUnresolved(orig, i, ['ambiguous alignment (score gap too small)']));\n continue;\n }\n\n usedFixed.add(best.fixedIdx);\n const fixed = fixedSegments[best.fixedIdx];\n\n if (fixed.content === orig.content) {\n out.push(orig);\n unchanged++;\n details.push(detailSkippedIdempotent(orig, i, ['content already matches rerun output']));\n continue;\n }\n\n out.push({ ...orig, content: fixed.content });\n recovered++;\n details.push(detailRecoveredRerun(orig, fixed, i));\n }\n\n return { details, segments: out, summary: { recovered, unchanged, unresolved } };\n};\n\nexport function recoverMistakenLineStartsAfterMarkers(\n pages: Page[],\n segments: Segment[],\n options: SegmentationOptions,\n selector: MarkerRecoverySelector,\n opts?: {\n mode?: 'rerun_only' | 'best_effort_then_rerun';\n normalizeCompare?: NormalizeCompareMode;\n },\n): { report: MarkerRecoveryReport; segments: Segment[] } {\n const mode = opts?.mode ?? 'rerun_only';\n const normalizeCompare = opts?.normalizeCompare ?? 'whitespace';\n\n const resolved = resolveSelectorToRuleIndices(options, selector);\n const reportBase: Omit<MarkerRecoveryReport, 'details' | 'summary'> = {\n byRun: undefined,\n errors: resolved.errors,\n warnings: resolved.warnings,\n };\n\n if (resolved.indices.size === 0) {\n return buildNoSelectionResult(segments, reportBase, mode, resolved.errors);\n }\n\n const stage1 = runStage1IfEnabled(pages, segments, options, resolved.indices, mode);\n\n const fixedOptions = buildFixedOptions(options, resolved.indices);\n const fixedSegments = segmentPages(pages, fixedOptions);\n const fixedBuckets = buildFixedBuckets(fixedSegments);\n const merged = mergeWithRerun({\n fixedBuckets,\n fixedSegments,\n normalizeCompare,\n originalSegments: segments,\n recoveredDetailAtIndex: stage1.recoveredDetailAtIndex,\n stage1RecoveredAtIndex: stage1.recoveredAtIndex,\n });\n\n return {\n report: {\n ...reportBase,\n details: merged.details,\n summary: {\n mode,\n recovered: merged.summary.recovered,\n totalSegments: segments.length,\n unchanged: merged.summary.unchanged,\n unresolved: merged.summary.unresolved,\n },\n },\n segments: merged.segments,\n };\n}\n\nexport function recoverMistakenMarkersForRuns(\n runs: MarkerRecoveryRun[],\n opts?: { mode?: 'rerun_only' | 'best_effort_then_rerun'; normalizeCompare?: NormalizeCompareMode },\n): { report: MarkerRecoveryReport; segments: Segment[] } {\n const allSegments: Segment[] = [];\n const byRun: NonNullable<MarkerRecoveryReport['byRun']> = [];\n const details: MarkerRecoveryReport['details'] = [];\n const warnings: string[] = [];\n const errors: string[] = [];\n\n let recovered = 0;\n let unchanged = 0;\n let unresolved = 0;\n let offset = 0;\n\n for (let i = 0; i < runs.length; i++) {\n const run = runs[i];\n const res = recoverMistakenLineStartsAfterMarkers(run.pages, run.segments, run.options, run.selector, opts);\n allSegments.push(...res.segments);\n\n // Adjust indices in details to be global\n for (const d of res.report.details) {\n details.push({ ...d, segmentIndex: d.segmentIndex + offset });\n }\n offset += run.segments.length;\n\n recovered += res.report.summary.recovered;\n unchanged += res.report.summary.unchanged;\n unresolved += res.report.summary.unresolved;\n\n warnings.push(...res.report.warnings);\n errors.push(...res.report.errors);\n\n byRun.push({\n recovered: res.report.summary.recovered,\n runIndex: i,\n totalSegments: run.segments.length,\n unresolved: res.report.summary.unresolved,\n });\n }\n\n const report: MarkerRecoveryReport = {\n byRun,\n details,\n errors,\n summary: {\n mode: opts?.mode ?? 'rerun_only',\n recovered,\n totalSegments: offset,\n unchanged,\n unresolved,\n },\n warnings,\n };\n\n return { report, segments: allSegments };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["processPattern","skipWhitespace","passesRuleConstraints","createSegment","TOKEN_PRIORITY_ORDER","isArabicLetter","resolveOptions","word"],"sources":["../src/segmentation/fuzzy.ts","../src/segmentation/types.ts","../src/segmentation/optimize-rules.ts","../src/segmentation/tokens.ts","../src/segmentation/pattern-validator.ts","../src/segmentation/replace.ts","../src/segmentation/breakpoint-constants.ts","../src/segmentation/breakpoint-utils.ts","../src/segmentation/debug-meta.ts","../src/segmentation/breakpoint-processor.ts","../src/segmentation/match-utils.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/textUtils.ts","../src/segmentation/segmenter.ts","../src/analysis/shared.ts","../src/analysis/line-starts.ts","../src/analysis/repeating-sequences.ts","../src/detection.ts","../src/recovery.ts"],"sourcesContent":["/**\n * Fuzzy matching utilities for Arabic text.\n *\n * Provides diacritic-insensitive and character-equivalence matching for Arabic text.\n * This allows matching text regardless of:\n * - Diacritical marks (harakat/tashkeel): فَتْحَة، ضَمَّة، كَسْرَة، سُكُون، شَدَّة، تَنْوين\n * - Character equivalences: ا↔آ↔أ↔إ, ة↔ه, ى↔ي\n *\n * @module fuzzy\n *\n * @example\n * // Make a pattern diacritic-insensitive\n * const pattern = makeDiacriticInsensitive('حدثنا');\n * new RegExp(pattern, 'u').test('حَدَّثَنَا') // → true\n */\n\n/**\n * Character class matching all Arabic diacritics (Tashkeel/Harakat).\n *\n * Includes the following diacritical marks:\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 = '[\\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: string[][] = [\n ['\\u0627', '\\u0622', '\\u0623', '\\u0625'], // ا, آ, أ, إ\n ['\\u0629', '\\u0647'], // ة <-> ه\n ['\\u0649', '\\u064A'], // ى <-> ي\n];\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): string => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/**\n * Returns a regex character class for all equivalents of a given character.\n *\n * If the character belongs to one of the predefined equivalence groups\n * (e.g., ا/آ/أ/إ), the returned class will match any member of that group.\n * Otherwise, the original character is simply escaped for safe regex inclusion.\n *\n * @param ch - A single character to expand into its equivalence class\n * @returns A RegExp-safe string representing the character and its equivalents\n *\n * @example\n * getEquivClass('ا') // → '[اآأإ]' (matches any alef variant)\n * getEquivClass('ب') // → 'ب' (no equivalents, just escaped)\n * getEquivClass('.') // → '\\\\.' (regex metachar escaped)\n *\n * @internal\n */\nconst getEquivClass = (ch: string): string => {\n for (const group of EQUIV_GROUPS) {\n if (group.includes(ch)) {\n // join the group's members into a character class\n return `[${group.map((c) => escapeRegex(c)).join('')}]`;\n }\n }\n // not in equivalence groups -> return escaped character\n return escapeRegex(ch);\n};\n\n/**\n * Performs light normalization on Arabic text for consistent matching.\n *\n * Normalization steps:\n * 1. NFC normalization (canonical decomposition then composition)\n * 2. Remove Zero-Width Joiner (U+200D) and Zero-Width Non-Joiner (U+200C)\n * 3. Collapse multiple whitespace characters to single space\n * 4. Trim leading and trailing whitespace\n *\n * This normalization preserves diacritics and letter forms while removing\n * invisible characters that could interfere with matching.\n *\n * @param str - Arabic text to normalize\n * @returns Normalized string\n *\n * @example\n * normalizeArabicLight('حَدَّثَنَا') // → 'حَدَّثَنَا' (diacritics preserved)\n * normalizeArabicLight('بسم الله') // → 'بسم الله' (spaces collapsed)\n * normalizeArabicLight(' text ') // → 'text' (trimmed)\n *\n * @internal\n */\nconst normalizeArabicLight = (str: string) => {\n return str\n .normalize('NFC')\n .replace(/[\\u200C\\u200D]/g, '') // remove ZWJ/ZWNJ\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\n/**\n * Creates a diacritic-insensitive regex pattern for Arabic text matching.\n *\n * Transforms input text into a regex pattern that matches the text regardless\n * of diacritical marks (harakat) and character variations. Each character in\n * the input is:\n * 1. Expanded to its equivalence class (if applicable)\n * 2. Followed by an optional diacritics matcher\n *\n * This allows matching:\n * - `حدثنا` with `حَدَّثَنَا` (with full diacritics)\n * - `الإيمان` with `الايمان` (alef variants)\n * - `صلاة` with `صلاه` (ta marbuta ↔ ha)\n *\n * @param text - Input Arabic text to make diacritic-insensitive\n * @returns Regex pattern string that matches the text with or without diacritics\n *\n * @example\n * const pattern = makeDiacriticInsensitive('حدثنا');\n * // Each char gets equivalence class + optional diacritics\n * // Result matches: حدثنا, حَدَّثَنَا, حَدَثَنَا, etc.\n *\n * @example\n * const pattern = makeDiacriticInsensitive('باب');\n * new RegExp(pattern, 'u').test('بَابٌ') // → true\n * new RegExp(pattern, 'u').test('باب') // → true\n *\n * @example\n * // Using with split rules\n * {\n * lineStartsWith: ['باب'],\n * split: 'at',\n * fuzzy: true // Applies makeDiacriticInsensitive internally\n * }\n */\nexport const makeDiacriticInsensitive = (text: string) => {\n const diacriticsMatcher = `${DIACRITICS_CLASS}*`;\n const norm = normalizeArabicLight(text);\n // Use Array.from to iterate grapheme-safe over the string (works fine for Arabic letters)\n return Array.from(norm)\n .map((ch) => getEquivClass(ch) + diacriticsMatcher)\n .join('');\n};\n","// ─────────────────────────────────────────────────────────────\n// Pattern Types (mutually exclusive - only ONE per rule)\n// ─────────────────────────────────────────────────────────────\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 * 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 */\ntype PatternType =\n | RegexPattern\n | TemplatePattern\n | LineStartsWithPattern\n | LineStartsAfterPattern\n | LineEndsWithPattern;\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 = ['lineStartsWith', 'lineStartsAfter', 'lineEndsWith', 'template', 'regex'] 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// ─────────────────────────────────────────────────────────────\n// Split Behavior\n// ─────────────────────────────────────────────────────────────\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// Page Range Types\n// ─────────────────────────────────────────────────────────────\n\n/**\n * A single page ID or a range of page IDs.\n *\n * - `number`: A single page ID\n * - `[number, number]`: A range from first to second (inclusive)\n *\n * @example\n * 5 // Single page 5\n * [10, 20] // Pages 10 through 20 (inclusive)\n */\nexport type PageRange = number | [number, number];\n\n// ─────────────────────────────────────────────────────────────\n// Constraints & Metadata\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 */\ntype RuleConstraints = {\n /**\n * Minimum page ID for this rule to apply.\n *\n * Matches on pages with `id < min` are ignored.\n *\n * @example\n * // Only apply rule starting from page 10\n * { min: 10, lineStartsWith: ['##'], split: 'before' }\n */\n min?: number;\n\n /**\n * Maximum page ID for this rule to apply.\n *\n * Matches on pages with `id > max` are ignored.\n *\n * @example\n * // Only apply rule up to page 100\n * { max: 100, lineStartsWith: ['##'], split: 'before' }\n */\n max?: number;\n\n /**\n * Specific pages or page ranges to exclude from this rule.\n *\n * Use this to skip the rule for specific pages without needing\n * to repeat the rule with different min/max values.\n *\n * @example\n * // Exclude specific pages\n * { exclude: [1, 2, 5] }\n *\n * @example\n * // Exclude page ranges\n * { exclude: [[1, 10], [50, 100]] }\n *\n * @example\n * // Mix single pages and ranges\n * { exclude: [1, [5, 10], 50] }\n */\n exclude?: PageRange[];\n\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// ─────────────────────────────────────────────────────────────\n// Combined Rule Type\n// ─────────────────────────────────────────────────────────────\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`, or `lineEndsWith`\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\n// ─────────────────────────────────────────────────────────────\n// Input & Output\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Input page structure for segmentation.\n *\n * Each page represents a logical unit of content (e.g., a book page,\n * a document section) that can be tracked across segment boundaries.\n *\n * @example\n * const pages: Page[] = [\n * { id: 1, content: '## Chapter 1\\nFirst paragraph...' },\n * { id: 2, content: 'Continued text...\\n## Chapter 2' },\n * ];\n */\nexport type Page = {\n /**\n * Unique page/entry ID used for:\n * - `min`/`max` constraint filtering\n * - `from`/`to` tracking in output segments\n */\n id: number;\n\n /**\n * Raw page content (may contain HTML).\n *\n * Line endings are normalized internally (`\\r\\n` and `\\r` → `\\n`).\n * Use a utility to convert html to markdown or `stripHtmlTags()` to preprocess HTML.\n */\n content: string;\n};\n\n// ─────────────────────────────────────────────────────────────\n// Breakpoint Types\n// ─────────────────────────────────────────────────────────────\n\n/**\n * A breakpoint pattern with optional page constraints.\n *\n * Use this to control which pages a breakpoint pattern applies to.\n * Patterns outside the specified range are skipped, allowing\n * the next breakpoint pattern (or fallback) to be tried.\n *\n * @example\n * // Only apply punctuation-based breaking from page 10 onwards\n * { pattern: '{{tarqim}}\\\\s*', min: 10 }\n *\n * @example\n * // Apply to specific page range (pages 10-50)\n * { pattern: '{{tarqim}}\\\\s*', min: 10, max: 50 }\n */\nexport type BreakpointRule = {\n /**\n * Regex pattern for breaking (supports token expansion).\n * Empty string `''` means fall back to page boundary.\n */\n pattern: string;\n\n /**\n * Minimum page ID for this breakpoint to apply.\n * Segments starting before this page skip this pattern.\n */\n min?: number;\n\n /**\n * Maximum page ID for this breakpoint to apply.\n * Segments starting after this page skip this pattern.\n */\n max?: number;\n\n /**\n * Specific pages or page ranges to exclude from this breakpoint.\n *\n * Use this to skip the breakpoint for specific pages without needing\n * to repeat the breakpoint with different min/max values.\n *\n * @example\n * // Exclude specific pages\n * { pattern: '\\\\.\\\\s*', exclude: [1, 2, 5] }\n *\n * @example\n * // Exclude page ranges (front matter pages 1-10)\n * { pattern: '{{tarqim}}\\\\s*', exclude: [[1, 10]] }\n *\n * @example\n * // Mix single pages and ranges\n * { pattern: '\\\\.\\\\s*', exclude: [1, [5, 10], 50] }\n */\n exclude?: PageRange[];\n\n /**\n * Skip this breakpoint if the segment content matches this pattern.\n *\n * Supports token expansion (e.g., `{{kitab}}`). When the segment's\n * remaining content matches this regex, the breakpoint pattern is\n * skipped and the next breakpoint in the array is tried.\n *\n * Useful for excluding title pages or front matter without needing\n * to specify explicit page ranges.\n *\n * @example\n * // Skip punctuation breakpoint for short content (likely titles)\n * { pattern: '{{tarqim}}\\\\s*', skipWhen: '^.{1,20}$' }\n *\n * @example\n * // Skip for content containing \"kitab\" (book) marker\n * { pattern: '\\\\.\\\\s*', skipWhen: '{{kitab}}' }\n */\n skipWhen?: string;\n};\n\n/**\n * A breakpoint can be a simple string pattern or an object with constraints.\n *\n * String breakpoints apply to all pages. Object breakpoints can specify\n * `min`/`max` to limit which pages they apply to.\n *\n * @example\n * // String (applies everywhere)\n * '{{tarqim}}\\\\s*'\n *\n * @example\n * // Object with constraints (only from page 10+)\n * { pattern: '{{tarqim}}\\\\s*', min: 10 }\n */\nexport type Breakpoint = string | BreakpointRule;\n\n// ─────────────────────────────────────────────────────────────\n// Logger Interface\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Logger interface for custom logging implementations.\n *\n * All methods are optional - only implement the verbosity levels you need.\n * When no logger is provided, no logging overhead is incurred.\n *\n * Compatible with the Logger interface from ffmpeg-simplified and similar libraries.\n *\n * @example\n * // Simple console logger\n * const logger: Logger = {\n * debug: console.debug,\n * info: console.info,\n * warn: console.warn,\n * error: console.error,\n * };\n *\n * @example\n * // Production logger (only warnings and errors)\n * const prodLogger: Logger = {\n * warn: (msg, ...args) => myLoggingService.warn(msg, args),\n * error: (msg, ...args) => myLoggingService.error(msg, args),\n * };\n */\nexport interface Logger {\n /** Log a debug message (verbose debugging output) */\n debug?: (message: string, ...args: unknown[]) => void;\n /** Log an error message (critical failures) */\n error?: (message: string, ...args: unknown[]) => void;\n /** Log an informational message (key progress points) */\n info?: (message: string, ...args: unknown[]) => void;\n /** Log a trace message (extremely verbose, per-iteration details) */\n trace?: (message: string, ...args: unknown[]) => void;\n /** Log a warning message (potential issues) */\n warn?: (message: string, ...args: unknown[]) => void;\n}\n\n/**\n * - Default regex flags: `gu` (global + unicode)\n * - If `flags` is provided, it is validated and merged with required flags:\n * `g` and `u` are always enforced.\n *\n * `pageIds` controls which pages a rule applies to:\n * - `undefined`: apply to all pages\n * - `[]`: apply to no pages (rule is skipped)\n * - `[id1, id2, ...]`: apply only to those pages\n */\nexport type Replacement = {\n /** Raw regex source string (no token expansion). Compiled with `u` (and always `g`). */\n regex: string;\n /** Replacement string (passed to `String.prototype.replace`). */\n replacement: string;\n /** Optional regex flags; `g` and `u` are always enforced. */\n flags?: string;\n /** Optional list of page IDs to apply this replacement to. Empty array means skip. */\n pageIds?: number[];\n};\n\n// ─────────────────────────────────────────────────────────────\n// Segmentation Options\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Segmentation options controlling how pages are split.\n *\n * @example\n * // Basic structural rules only\n * const options: SegmentationOptions = {\n * rules: [\n * { lineStartsWith: ['## '], split: 'at', meta: { type: 'chapter' } },\n * { lineStartsWith: ['### '], split: 'at', meta: { type: 'section' } },\n * ]\n * };\n *\n * @example\n * // With breakpoints for oversized segments\n * const options: SegmentationOptions = {\n * rules: [{ lineStartsWith: ['{{fasl}}'], split: 'at' }],\n * maxPages: 2,\n * breakpoints: ['{{tarqim}}\\\\s*', '\\\\n', ''],\n * prefer: 'longer'\n * };\n *\n * @example\n * // With custom logger for debugging\n * const options: SegmentationOptions = {\n * rules: [...],\n * logger: {\n * debug: console.debug,\n * info: console.info,\n * warn: console.warn,\n * }\n * };\n */\nexport type SegmentationOptions = {\n /**\n * Optional pre-processing replacements applied to page content BEFORE segmentation.\n *\n * Replacements are applied per-page (not on concatenated content), in array order.\n */\n replace?: Replacement[];\n\n /**\n * Rules applied in order to find split points.\n *\n * All rules are evaluated against the content, and their matches\n * are combined to determine final split points. The first matching\n * rule's metadata is used for each segment.\n */\n rules?: SplitRule[];\n\n /**\n * Attach debugging provenance into `segment.meta` indicating which rule and/or breakpoint\n * created the segment boundary.\n *\n * This is opt-in because it increases output size.\n *\n * When enabled (default metaKey: `_flappa`), segments may include:\n * `meta._flappa.rule` and/or `meta._flappa.breakpoint`.\n */\n debug?:\n | boolean\n | {\n /** Where to store provenance in meta. @default '_flappa' */\n metaKey?: string;\n /** Which kinds of provenance to include. @default ['rule','breakpoint'] */\n include?: Array<'rule' | 'breakpoint'>;\n };\n\n /**\n * Maximum pages per segment before breakpoints are applied.\n *\n * When a segment spans more pages than this limit, the `breakpoints`\n * patterns are tried (in order) to find a suitable break point within\n * the allowed window.\n *\n * Structural markers (from rules) always take precedence - segments\n * are only broken within their rule-defined boundaries, never across them.\n *\n * @example\n * // Break segments that exceed 2 pages\n * { maxPages: 2, breakpoints: ['{{tarqim}}', ''] }\n */\n maxPages?: number;\n\n /**\n * Maximum length (in characters) per segment.\n *\n * When a segment exceeds this length, breakpoints are applied to split it.\n * This can typically be used in conjunction with `maxPages`, where the\n * strictest constraint (intersection) determines the split window.\n *\n * @example\n * // Break segments that exceed 2000 chars\n * { maxContentLength: 2000, breakpoints: ['{{tarqim}}'] }\n */\n maxContentLength?: number;\n\n /**\n * Patterns tried in order to break oversized segments.\n *\n * Each pattern is tried until one matches within the allowed page window.\n * Supports token expansion (e.g., `{{tarqim}}`). An empty string `''`\n * matches the page boundary (always succeeds as ultimate fallback).\n *\n * Patterns can be simple strings (apply everywhere) or objects with\n * `min`/`max` constraints to limit which pages they apply to.\n *\n * Patterns are checked in order - put preferred break styles first:\n * - `{{tarqim}}\\\\s*` - Break at sentence-ending punctuation\n * - `\\\\n` - Break at line breaks (useful for OCR content)\n * - `''` - Break at page boundary (always works)\n *\n * Only applied to segments exceeding `maxPages`.\n *\n * @example\n * // Simple patterns (backward compatible)\n * breakpoints: ['{{tarqim}}\\\\s*', '\\\\n', '']\n *\n * @example\n * // Object patterns with page constraints\n * breakpoints: [\n * { pattern: '{{tarqim}}\\\\s*', min: 10 }, // Only from page 10+\n * '' // Fallback for pages 1-9\n * ]\n */\n breakpoints?: Breakpoint[];\n\n /**\n * When multiple matches exist for a breakpoint pattern, select:\n * - `'longer'` - Last match in window (prefers longer segments)\n * - `'shorter'` - First match in window (prefers shorter segments)\n *\n * @default 'longer'\n */\n prefer?: 'longer' | 'shorter';\n\n /**\n * How to join content across page boundaries in OUTPUT segments.\n *\n * Internally, pages are still concatenated with `\\\\n` for matching (multiline regex),\n * but when a segment spans multiple pages, the inserted page-boundary separator is\n * normalized for output.\n *\n * - `'space'`: Join pages with a single space (default)\n * - `'newline'`: Preserve page boundary as a newline\n *\n * @default 'space'\n */\n pageJoiner?: 'space' | 'newline';\n\n /**\n * Optional logger for debugging segmentation.\n *\n * Provide a logger to receive detailed information about the segmentation\n * process. Useful for debugging pattern matching, page tracking, and\n * breakpoint processing issues.\n *\n * When not provided, no logging overhead is incurred (methods are not called).\n *\n * Verbosity levels:\n * - `trace`: Per-iteration details (very verbose)\n * - `debug`: Detailed operation information\n * - `info`: Key progress points\n * - `warn`: Potential issues\n * - `error`: Critical failures\n *\n * @example\n * // Console logger for development\n * logger: {\n * debug: console.debug,\n * info: console.info,\n * warn: console.warn,\n * }\n *\n * @example\n * // Custom logger integration\n * logger: {\n * debug: (msg, ...args) => winston.debug(msg, { meta: args }),\n * error: (msg, ...args) => winston.error(msg, { meta: args }),\n * }\n */\n logger?: Logger;\n};\n\n/**\n * Output segment produced by `segmentPages()`.\n *\n * Each segment contains extracted content, page references, and\n * optional metadata from the matched rule and captured groups.\n *\n * @example\n * // Simple segment on a single page\n * { content: '## Chapter 1\\nIntroduction...', from: 1, meta: { type: 'chapter' } }\n *\n * @example\n * // Segment spanning pages 5-7 with captured hadith number\n * { content: 'Hadith text...', from: 5, to: 7, meta: { type: 'hadith', hadithNum: '٤٢' } }\n */\nexport type Segment = {\n /**\n * Segment content with:\n * - Leading/trailing whitespace trimmed\n * - Page breaks converted to spaces (for multi-page segments)\n * - Markers stripped (for `lineStartsAfter` patterns)\n */\n content: string;\n\n /**\n * Starting page ID (from `Page.id`).\n */\n from: number;\n\n /**\n * Ending page ID if segment spans multiple pages.\n *\n * Only present when the segment content extends across page boundaries.\n * When `undefined`, the segment is contained within a single page.\n */\n to?: number;\n\n /**\n * Combined metadata from:\n * 1. Rule's `meta` property (static metadata)\n * 2. Named captures from patterns (e.g., `{{raqms:num}}` → `{ num: '٤٢' }`)\n *\n * Named captures override static metadata with the same key.\n */\n meta?: Record<string, unknown>;\n};\n","/**\n * Rule optimization utilities for merging and sorting split rules.\n *\n * Provides `optimizeRules()` to:\n * 1. Merge compatible rules with same pattern type and options\n * 2. Deduplicate patterns within each rule\n * 3. Sort rules by specificity (longer patterns first)\n *\n * @module optimize-rules\n */\n\nimport { PATTERN_TYPE_KEYS, type PatternTypeKey, type SplitRule } from './types.js';\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): PatternTypeKey => {\n for (const key of PATTERN_TYPE_KEYS) {\n if (key in rule) {\n return key;\n }\n }\n return 'regex'; // fallback\n};\n\n/**\n * Get the pattern array for a mergeable rule.\n */\nconst getPatternArray = (rule: SplitRule, key: PatternTypeKey): string[] => {\n const value = (rule as Record<string, unknown>)[key];\n return Array.isArray(value) ? (value as string[]) : [];\n};\n\n/**\n * Get a string representation of the pattern value (for specificity scoring).\n */\nconst getPatternString = (rule: SplitRule, key: PatternTypeKey): string => {\n const value = (rule as Record<string, unknown>)[key];\n if (typeof value === 'string') {\n return value;\n }\n if (Array.isArray(value)) {\n return value.join('\\n');\n }\n return '';\n};\n\n/**\n * Deduplicate and sort patterns by length (longest first).\n */\nconst normalizePatterns = (patterns: string[]): string[] => {\n const unique = [...new Set(patterns)];\n return unique.sort((a, b) => b.length - a.length || a.localeCompare(b));\n};\n\n/**\n * Calculate specificity score for a rule (higher = more specific).\n * Based on the longest pattern length.\n */\nconst getSpecificityScore = (rule: SplitRule): number => {\n const key = getPatternKey(rule);\n\n if (MERGEABLE_KEYS.has(key)) {\n const patterns = getPatternArray(rule, key);\n return patterns.reduce((max, p) => Math.max(max, p.length), 0);\n }\n\n return getPatternString(rule, key).length;\n};\n\n/**\n * Create a merge key for a rule based on pattern type and all non-pattern properties.\n * Rules with the same merge key can have their patterns combined.\n */\nconst createMergeKey = (rule: SplitRule): string => {\n const patternKey = getPatternKey(rule);\n const { [patternKey]: _pattern, ...rest } = rule as Record<string, unknown>;\n return `${patternKey}|${JSON.stringify(rest)}`;\n};\n\n/**\n * Optimize split rules by merging compatible rules and sorting by specificity.\n *\n * This function:\n * 1. **Merges compatible rules**: Rules with the same pattern type and identical\n * options (meta, fuzzy, min/max, etc.) have their pattern arrays combined\n * 2. **Deduplicates patterns**: Removes duplicate patterns within each rule\n * 3. **Sorts by specificity**: Rules with longer patterns come first\n *\n * Only array-based pattern types (`lineStartsWith`, `lineStartsAfter`, `lineEndsWith`)\n * can be merged. `template` and `regex` rules are kept separate.\n *\n * @param rules - Array of split rules to optimize\n * @returns Optimized rules and count of merged rules\n *\n * @example\n * import { optimizeRules } from 'flappa-doormal';\n *\n * const { rules, mergedCount } = optimizeRules([\n * { lineStartsWith: ['{{kitab}}'], fuzzy: true, meta: { type: 'header' } },\n * { lineStartsWith: ['{{bab}}'], fuzzy: true, meta: { type: 'header' } },\n * { lineStartsAfter: ['{{numbered}}'], meta: { type: 'entry' } },\n * ]);\n *\n * // rules[0] = { lineStartsWith: ['{{kitab}}', '{{bab}}'], fuzzy: true, meta: { type: 'header' } }\n * // rules[1] = { lineStartsAfter: ['{{numbered}}'], meta: { type: 'entry' } }\n * // mergedCount = 1\n */\nexport const optimizeRules = (rules: SplitRule[]): OptimizeResult => {\n const output: SplitRule[] = [];\n const indexByMergeKey = new Map<string, number>();\n let mergedCount = 0;\n\n for (const rule of rules) {\n const patternKey = getPatternKey(rule);\n\n // Only merge array-pattern rules\n if (!MERGEABLE_KEYS.has(patternKey)) {\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 // New rule - normalize patterns and add\n indexByMergeKey.set(mergeKey, output.length);\n output.push({\n ...rule,\n [patternKey]: normalizePatterns(getPatternArray(rule, patternKey)),\n } as SplitRule);\n continue;\n }\n\n // Merge patterns into existing rule\n const existing = output[existingIndex] as Record<string, unknown>;\n existing[patternKey] = normalizePatterns([\n ...getPatternArray(existing as SplitRule, patternKey),\n ...getPatternArray(rule, patternKey),\n ]);\n mergedCount++;\n }\n\n // Sort by specificity (most specific first)\n output.sort((a, b) => getSpecificityScore(b) - getSpecificityScore(a));\n\n return { mergedCount, rules: output };\n};\n","/**\n * Token-based template system for Arabic text pattern matching.\n *\n * This module provides a human-readable way to define regex patterns using\n * `{{token}}` placeholders that expand to their regex equivalents. It supports\n * named capture groups for extracting matched values into metadata.\n *\n * @module tokens\n *\n * @example\n * // Simple token expansion\n * expandTokens('{{raqms}} {{dash}}')\n * // → '[\\\\u0660-\\\\u0669]+ [-–—ـ]'\n *\n * @example\n * // Named capture groups\n * expandTokensWithCaptures('{{raqms:num}} {{dash}}')\n * // → { pattern: '(?<num>[\\\\u0660-\\\\u0669]+) [-–—ـ]', captureNames: ['num'], hasCaptures: true }\n */\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// ─────────────────────────────────────────────────────────────\n// Auto-escaping for template patterns\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): string => {\n // Match either a token ({{...}}) or a bracket character\n // Tokens are preserved as-is, brackets are escaped\n return pattern.replace(/(\\{\\{[^}]*\\}\\})|([()[\\]])/g, (_match, token, bracket) => {\n if (token) {\n return token; // Leave tokens intact\n }\n return `\\\\${bracket}`; // Escape the bracket\n });\n};\n\n// ─────────────────────────────────────────────────────────────\n// Base tokens - raw regex patterns (no template references)\n// ─────────────────────────────────────────────────────────────\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أ-ي])';\nconst RUMUZ_FOUR = '(?<![\\\\u0660-\\\\u0669])٤(?![\\\\u0660-\\\\u0669])';\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('|')})`;\nconst RUMUZ_BLOCK = `${RUMUZ_ATOM}(?:\\\\s+${RUMUZ_ATOM})*`;\n\nconst BASE_TOKENS: Record<string, string> = {\n /**\n * Chapter marker - Arabic word for \"chapter\" (باب).\n *\n * Commonly used in hadith collections to mark chapter divisions.\n *\n * @example 'باب ما جاء في الصلاة' (Chapter on what came regarding prayer)\n */\n bab: 'باب',\n\n /**\n * Basmala pattern - Arabic invocation \"In the name of Allah\" (بسم الله).\n *\n * Matches the beginning of the basmala formula, commonly appearing\n * at the start of chapters, books, or documents.\n *\n * @example 'بسم الله الرحمن الرحيم' (In the name of Allah, the Most Gracious, the Most Merciful)\n */\n basmalah: ['بسم الله', '﷽'].join('|'),\n\n /**\n * Bullet point variants - common bullet characters.\n *\n * Character class matching: `•` (bullet), `*` (asterisk), `°` (degree).\n *\n * @example '• First item'\n */\n bullet: '[•*°]',\n\n /**\n * Dash variants - various dash and separator characters.\n *\n * Character class matching:\n * - `-` (hyphen-minus U+002D)\n * - `–` (en-dash U+2013)\n * - `—` (em-dash U+2014)\n * - `ـ` (tatweel U+0640, Arabic elongation character)\n *\n * @example '٦٦٩٦ - حدثنا' or '٦٦٩٦ ـ حدثنا'\n */\n dash: '[-–—ـ]',\n\n /**\n * Section marker - Arabic word for \"section/issue\".\n * Commonly used for fiqh books.\n */\n fasl: ['مسألة', 'فصل'].join('|'),\n\n /**\n * Single Arabic letter - matches any Arabic letter character.\n *\n * Character range from أ (alef with hamza) to ي (ya).\n * Does NOT include diacritics (harakat/tashkeel).\n *\n * @example '{{harf}}' matches 'ب' in 'باب'\n */\n harf: '[أ-ي]',\n\n /**\n * One or more Arabic letters separated by spaces - matches sequences like \"د ت س ي ق\".\n *\n * Useful for matching abbreviation *lists* that are encoded as single-letter tokens\n * separated by spaces.\n *\n * IMPORTANT:\n * - This token intentionally matches **single letters only** (with optional spacing).\n * - It does NOT match multi-letter rumuz like \"سي\" or \"خت\". For those, use `{{rumuz}}`.\n *\n * @example '{{harfs}}' matches 'د ت س ي ق' in '١١١٨ د ت س ي ق: حجاج'\n * @example '{{raqms:num}} {{harfs}}:' matches number + abbreviations + colon\n */\n // Example matches: \"د ت س ي ق\"\n // Example non-matches: \"وعلامة ...\", \"في\", \"لا\", \"سي\", \"خت\"\n harfs: '[أ-ي](?:\\\\s+[أ-ي])*',\n\n /**\n * Book marker - Arabic word for \"book\" (كتاب).\n *\n * Commonly used in hadith collections to mark major book divisions.\n *\n * @example 'كتاب الإيمان' (Book of Faith)\n */\n kitab: 'كتاب',\n\n /**\n * Naql (transmission) phrases - common hadith transmission phrases.\n *\n * Alternation of Arabic phrases used to indicate narration chains:\n * - حدثنا (he narrated to us)\n * - أخبرنا (he informed us)\n * - حدثني (he narrated to me)\n * - وحدثنا (and he narrated to us)\n * - أنبأنا (he reported to us)\n * - سمعت (I heard)\n *\n * @example '{{naql}}' matches any of the above phrases\n */\n naql: ['حدثني', 'وأخبرنا', 'حدثنا', 'سمعت', 'أنبأنا', 'وحدثنا', 'أخبرنا', 'وحدثني', 'وحدثنيه'].join('|'),\n\n /**\n * Single ASCII digit - matches one digit (0-9).\n *\n * @example '{{num}}' matches '5' in '5 - '\n */\n num: '\\\\d',\n\n /**\n * One or more ASCII digits - matches digit sequences (0-9)+.\n *\n * @example '{{nums}}' matches '123' in '123 - '\n */\n nums: '\\\\d+',\n\n /**\n * Single Arabic-Indic digit - matches one digit (٠-٩).\n *\n * Unicode range: U+0660 to U+0669 (Arabic-Indic digits).\n * Use `{{raqms}}` for one or more digits.\n *\n * @example '{{raqm}}' matches '٥' in '٥ - '\n */\n raqm: '[\\\\u0660-\\\\u0669]',\n\n /**\n * One or more Arabic-Indic digits - matches digit sequences (٠-٩)+.\n *\n * Unicode range: U+0660 to U+0669 (Arabic-Indic digits).\n * Commonly used for hadith numbers, verse numbers, etc.\n *\n * @example '{{raqms}}' matches '٦٦٩٦' in '٦٦٩٦ - حدثنا'\n */\n raqms: '[\\\\u0660-\\\\u0669]+',\n\n /**\n * Rumuz (source abbreviations) used in rijāl / takhrīj texts.\n *\n * This token matches the known abbreviation set used to denote sources like:\n * - All six books: (ع)\n * - The four Sunan: (٤)\n * - Bukhari: خ / خت / خغ / بخ / عخ / ز / ي\n * - Muslim: م / مق / مت\n * - Nasa'i: س / ن / ص / عس / سي / كن\n * - Abu Dawud: د / مد / قد / خد / ف / فد / ل / دل / كد / غد / صد\n * - Tirmidhi: ت / تم\n * - Ibn Majah: ق / فق\n *\n * Notes:\n * - Order matters: longer alternatives must come before shorter ones (e.g., \"خد\" before \"خ\")\n * - This token matches a rumuz *block*: one or more codes separated by whitespace\n * (e.g., \"خ سي\", \"خ فق\", \"خت ٤\", \"د ت سي ق\")\n */\n rumuz: RUMUZ_BLOCK,\n\n /**\n * Punctuation characters.\n * Use {{tarqim}} which is especially useful when splitting using split: 'after' on punctuation marks.\n */\n tarqim: '[.!?؟؛]',\n};\n\n// ─────────────────────────────────────────────────────────────\n// Composite tokens - templates that reference base tokens\n// These are pre-expanded at module load time for performance\n// ─────────────────────────────────────────────────────────────\n\n/**\n * Composite token definitions using template syntax.\n *\n * These tokens reference base tokens using `{{token}}` syntax and are\n * automatically expanded to their final regex patterns at module load time.\n *\n * This provides better abstraction - if base tokens change, composites\n * automatically update on the next build.\n *\n * @internal\n */\nconst COMPOSITE_TOKENS: Record<string, string> = {\n /**\n * Numbered hadith marker - common format for hadith numbering.\n *\n * Matches patterns like \"٢٢ - \" (number, space, dash, space).\n * This is the most common format in hadith collections.\n *\n * Use with `lineStartsAfter` to cleanly extract hadith content:\n * ```typescript\n * { lineStartsAfter: ['{{numbered}}'], split: 'at' }\n * ```\n *\n * For capturing the hadith number, use explicit capture syntax:\n * ```typescript\n * { lineStartsAfter: ['{{raqms:hadithNum}} {{dash}} '], split: 'at' }\n * ```\n *\n * @example '٢٢ - حدثنا' matches, content starts after '٢٢ - '\n * @example '٦٦٩٦ – أخبرنا' matches (with en-dash)\n */\n numbered: '{{raqms}} {{dash}} ',\n};\n\n/**\n * Expands any *composite* tokens (like `{{numbered}}`) into their underlying template form\n * (like `{{raqms}} {{dash}} `).\n *\n * This is useful when you want to take a signature produced by `analyzeCommonLineStarts()`\n * and turn it into an editable template where you can add named captures, e.g.:\n *\n * - `{{numbered}}` → `{{raqms}} {{dash}} `\n * - then: `{{raqms:num}} {{dash}} ` to capture the number\n *\n * Notes:\n * - This only expands the plain `{{token}}` form (not `{{token:name}}`).\n * - Expansion is repeated a few times to support nested composites.\n */\nexport const expandCompositeTokensInTemplate = (template: string): string => {\n let out = template;\n for (let i = 0; i < 10; i++) {\n const next = out.replace(/\\{\\{(\\w+)\\}\\}/g, (m, tokenName: string) => {\n const replacement = COMPOSITE_TOKENS[tokenName];\n return replacement ?? m;\n });\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): string => {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, tokenName) => {\n return BASE_TOKENS[tokenName] ?? `{{${tokenName}}}`;\n });\n};\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: Record<string, string> = {\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};\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): boolean => {\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): TemplateSegment[] => {\n const segments: TemplateSegment[] = [];\n let lastIndex = 0;\n TOKEN_WITH_CAPTURE_REGEX.lastIndex = 0;\n let match: RegExpExecArray | null;\n\n // biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop pattern\n while ((match = TOKEN_WITH_CAPTURE_REGEX.exec(query)) !== null) {\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\n return segments;\n};\n\nconst maybeApplyFuzzyToText = (text: string, fuzzyTransform?: (pattern: string) => string): string => {\n if (fuzzyTransform && /[\\u0600-\\u06FF]/u.test(text)) {\n return fuzzyTransform(text);\n }\n return text;\n};\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): string => {\n if (!fuzzyTransform) {\n return tokenPattern;\n }\n return tokenPattern\n .split('|')\n .map((part) => (/[\\u0600-\\u06FF]/u.test(part) ? fuzzyTransform(part) : part))\n .join('|');\n};\n\nconst parseTokenLiteral = (literal: string): { tokenName: string; captureName: string } | null => {\n TOKEN_WITH_CAPTURE_REGEX.lastIndex = 0;\n const tokenMatch = TOKEN_WITH_CAPTURE_REGEX.exec(literal);\n if (!tokenMatch) {\n return null;\n }\n const [, tokenName, captureName] = tokenMatch;\n return { captureName, tokenName };\n};\n\nconst createCaptureRegistry = (capturePrefix?: string) => {\n const captureNames: string[] = [];\n const captureNameCounts = new Map<string, number>();\n\n const register = (baseName: string): 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): string => {\n const parsed = parseTokenLiteral(literal);\n if (!parsed) {\n return literal;\n }\n\n const { tokenName, captureName } = parsed;\n\n // {{:name}} - capture anything with name\n if (!tokenName && captureName) {\n const name = opts.registerCapture(captureName);\n return `(?<${name}>.+)`;\n }\n\n let tokenPattern = TOKEN_PATTERNS[tokenName];\n if (!tokenPattern) {\n // Unknown token - leave as-is\n return literal;\n }\n\n tokenPattern = maybeApplyFuzzyToTokenPattern(tokenPattern, opts.fuzzyTransform);\n\n // {{token:name}} - capture with name\n if (captureName) {\n const name = opts.registerCapture(captureName);\n return `(?<${name}>${tokenPattern})`;\n }\n\n // {{token}} - no capture, just expand\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): ExpandResult => {\n const segments = splitTemplateIntoSegments(query);\n const registry = createCaptureRegistry(capturePrefix);\n\n const processedParts = segments.map((segment) => {\n if (segment.type === 'text') {\n return maybeApplyFuzzyToText(segment.value, fuzzyTransform);\n }\n return expandTokenLiteral(segment.value, {\n capturePrefix,\n fuzzyTransform,\n registerCapture: registry.register,\n });\n });\n\n return {\n captureNames: registry.captureNames,\n hasCaptures: registry.captureNames.length > 0,\n pattern: processedParts.join(''),\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', 'basmala', 'bullet', ...]`)\n *\n * @example\n * getAvailableTokens()\n * // → ['bab', 'basmala', 'bullet', 'dash', 'harf', 'kitab', 'naql', 'raqm', 'raqms']\n */\nexport const getAvailableTokens = () => Object.keys(TOKEN_PATTERNS);\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')\n * @returns The regex pattern string, or `undefined` if token doesn't exist\n *\n * @example\n * getTokenPattern('raqms') // → '[\\\\u0660-\\\\u0669]+'\n * getTokenPattern('dash') // → '[-–—ـ]'\n * getTokenPattern('unknown') // → undefined\n */\nexport const getTokenPattern = (tokenName: string): string | undefined => 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[]): boolean => {\n const arr = Array.isArray(patterns) ? patterns : [patterns];\n return arr.some((p) => {\n FUZZY_TOKEN_REGEX.lastIndex = 0; // Reset stateful regex\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): string => {\n // Match {{token:name}} and replace with {{token}}\n return template.replace(/\\{\\{([^:}]+):[^}]+\\}\\}/g, '{{$1}}');\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 { getAvailableTokens } from './tokens.js';\nimport type { SplitRule } from './types.js';\n\n/**\n * Types of validation issues that can be detected.\n */\nexport type ValidationIssueType = 'missing_braces' | 'unknown_token' | 'duplicate' | 'empty_pattern';\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};\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 = (): RegExp => {\n // Sort by length descending to match longer tokens first\n const tokens = [...KNOWN_TOKENS].sort((a, b) => b.length - a.length);\n // Match token name followed by optional :name, but NOT inside {{}}\n // Use negative lookbehind for {{ and negative lookahead for }}\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>): ValidationIssue | undefined => {\n if (!pattern.trim()) {\n return { message: 'Empty pattern is not allowed', type: 'empty_pattern' };\n }\n // Check for duplicates\n if (seenPatterns.has(pattern)) {\n return {\n message: `Duplicate pattern: \"${pattern}\"`,\n pattern,\n type: 'duplicate',\n };\n }\n seenPatterns.add(pattern);\n\n // Check for unknown tokens inside {{}}\n const tokensInBraces = [...pattern.matchAll(TOKEN_INSIDE_BRACES)];\n for (const match of tokensInBraces) {\n const tokenName = match[1];\n if (!KNOWN_TOKENS.has(tokenName)) {\n return {\n message: `Unknown token: {{${tokenName}}}. Available tokens: ${[...KNOWN_TOKENS].slice(0, 5).join(', ')}...`,\n suggestion: `Check spelling or use a known token`,\n token: tokenName,\n type: 'unknown_token',\n };\n }\n }\n\n // Check for bare token names not inside {{}}\n const bareTokenRegex = buildBareTokenRegex();\n const bareMatches = [...pattern.matchAll(bareTokenRegex)];\n for (const match of bareMatches) {\n const tokenName = match[1];\n const fullMatch = match[0];\n // Make sure this isn't inside {{}} by checking the original pattern\n const matchIndex = match.index!;\n const before = pattern.slice(Math.max(0, matchIndex - 2), matchIndex);\n const after = pattern.slice(matchIndex + fullMatch.length, matchIndex + fullMatch.length + 2);\n if (before !== '{{' && after !== '}}') {\n return {\n message: `Token \"${tokenName}\" appears to be missing {{}}. Did you mean \"{{${fullMatch}}}\"?`,\n suggestion: `{{${fullMatch}}}`,\n token: tokenName,\n type: 'missing_braces',\n };\n }\n }\n\n return undefined;\n};\n\n/**\n * Validates an array of patterns, returning parallel array of issues.\n */\nconst validatePatternArray = (patterns: string[]): (ValidationIssue | undefined)[] | undefined => {\n const seenPatterns = new Set<string>();\n const issues = patterns.map((p) => validatePattern(p, seenPatterns));\n\n // If all undefined, return undefined for the whole array\n if (issues.every((i) => i === undefined)) {\n return undefined;\n }\n return issues;\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[]): (RuleValidationResult | undefined)[] => {\n return rules.map((rule) => {\n const result: RuleValidationResult = {};\n let hasIssues = false;\n\n if ('lineStartsWith' in rule && rule.lineStartsWith) {\n const issues = validatePatternArray(rule.lineStartsWith);\n if (issues) {\n result.lineStartsWith = issues;\n hasIssues = true;\n }\n }\n\n if ('lineStartsAfter' in rule && rule.lineStartsAfter) {\n const issues = validatePatternArray(rule.lineStartsAfter);\n if (issues) {\n result.lineStartsAfter = issues;\n hasIssues = true;\n }\n }\n\n if ('lineEndsWith' in rule && rule.lineEndsWith) {\n const issues = validatePatternArray(rule.lineEndsWith);\n if (issues) {\n result.lineEndsWith = issues;\n hasIssues = true;\n }\n }\n\n if ('template' in rule && rule.template !== undefined) {\n const seenPatterns = new Set<string>();\n const issue = validatePattern(rule.template, seenPatterns);\n if (issue) {\n result.template = issue;\n hasIssues = true;\n }\n }\n\n // Note: We don't validate `regex` patterns as they are raw regex, not templates\n\n return hasIssues ? result : undefined;\n });\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)[]): string[] => {\n const errors: string[] = [];\n\n results.forEach((result, ruleIndex) => {\n if (!result) {\n return;\n }\n\n // Helper to format a single issue\n // eslint-disable-next-line\n const formatIssue = (issue: any, location: string) => {\n if (!issue) {\n return;\n }\n const type = issue.type as ValidationIssueType;\n\n if (type === 'missing_braces' && issue.token) {\n errors.push(`${location}: Missing {{}} around token \"${issue.token}\"`);\n } else if (type === 'unknown_token' && issue.token) {\n errors.push(`${location}: Unknown token \"{{${issue.token}}}\"`);\n } else if (type === 'duplicate' && issue.pattern) {\n errors.push(`${location}: Duplicate pattern \"${issue.pattern}\"`);\n } else if (issue.message) {\n errors.push(`${location}: ${issue.message}`);\n } else {\n errors.push(`${location}: ${type}`);\n }\n };\n\n // Each result is a Record with pattern types as keys\n for (const [patternType, issues] of Object.entries(result)) {\n const list = Array.isArray(issues) ? issues : [issues];\n for (const issue of list) {\n if (issue) {\n formatIssue(issue, `Rule ${ruleIndex + 1}, ${patternType}`);\n }\n }\n }\n });\n\n return errors;\n};\n","import type { Page, SegmentationOptions } from './types.js';\n\n/**\n * A single replacement rule applied by `applyReplacements()` / `SegmentationOptions.replace`.\n *\n * Notes:\n * - `regex` is a raw JavaScript regex source string (no token expansion).\n * - Default flags are `gu` (global + unicode).\n * - If `flags` is provided, it is validated and `g` + `u` are always enforced.\n * - If `pageIds` is omitted, the rule applies to all pages.\n * - If `pageIds` is `[]`, the rule applies to no pages (rule is skipped).\n */\nexport type ReplaceRule = NonNullable<SegmentationOptions['replace']>[number];\n\nconst DEFAULT_REPLACE_FLAGS = 'gu';\n\nconst normalizeReplaceFlags = (flags?: string): string => {\n if (!flags) {\n return DEFAULT_REPLACE_FLAGS;\n }\n // Validate and de-duplicate flags. Force include g + u.\n const allowed = new Set(['g', 'i', 'm', 's', 'u', 'y']);\n const set = new Set<string>();\n for (const ch of flags) {\n if (!allowed.has(ch)) {\n throw new Error(`Invalid replace regex flag: \"${ch}\" (allowed: gimsyu)`);\n }\n set.add(ch);\n }\n set.add('g');\n set.add('u');\n\n // Stable ordering for reproducibility\n const order = ['g', 'i', 'm', 's', 'y', 'u'];\n return order.filter((c) => set.has(c)).join('');\n};\n\ntype CompiledReplaceRule = {\n re: RegExp;\n replacement: string;\n pageIdSet?: ReadonlySet<number>;\n};\n\nconst compileReplaceRules = (rules: ReplaceRule[]): CompiledReplaceRule[] => {\n const compiled: CompiledReplaceRule[] = [];\n for (const r of rules) {\n if (r.pageIds && r.pageIds.length === 0) {\n // Empty list means \"apply to no pages\"\n continue;\n }\n const flags = normalizeReplaceFlags(r.flags);\n const re = new RegExp(r.regex, flags);\n compiled.push({\n pageIdSet: r.pageIds ? new Set(r.pageIds) : undefined,\n re,\n replacement: r.replacement,\n });\n }\n return compiled;\n};\n\n/**\n * Applies ordered regex replacements to page content (per page).\n *\n * - Replacement rules are applied in array order.\n * - Each rule is applied globally (flag `g` enforced) with unicode mode (flag `u` enforced).\n * - `pageIds` can scope a rule to specific pages. `pageIds: []` skips the rule entirely.\n *\n * This function is intentionally **pure**:\n * it returns a new pages array only when changes are needed, otherwise it returns the original pages.\n */\nexport const applyReplacements = (pages: Page[], rules?: ReplaceRule[]): Page[] => {\n if (!rules || rules.length === 0 || pages.length === 0) {\n return pages;\n }\n const compiled = compileReplaceRules(rules);\n if (compiled.length === 0) {\n return pages;\n }\n\n return pages.map((p) => {\n let content = p.content;\n for (const rule of compiled) {\n if (rule.pageIdSet && !rule.pageIdSet.has(p.id)) {\n continue;\n }\n content = content.replace(rule.re, rule.replacement);\n }\n if (content === p.content) {\n return p;\n }\n return { ...p, content };\n });\n};\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","/**\n * Utility functions for breakpoint processing in the segmentation engine.\n *\n * These functions handle breakpoint normalization, page exclusion checking,\n * and segment creation. Extracted for independent testing and reuse.\n *\n * @module breakpoint-utils\n */\n\nimport { FAST_PATH_THRESHOLD } from './breakpoint-constants.js';\nimport type { Breakpoint, BreakpointRule, Logger, PageRange, Segment } from './types.js';\n\nconst WINDOW_PREFIX_LENGTHS = [80, 60, 40, 30, 20, 15] as const;\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.\nconst JOINER_PREFIX_LENGTHS = [80, 60, 40, 30, 20, 15, 12, 10, 8, 6] as const;\n\n/**\n * Normalizes a breakpoint to the object form.\n * Strings are converted to { pattern: str } with no constraints.\n *\n * @param bp - Breakpoint as string or object\n * @returns Normalized BreakpointRule object\n *\n * @example\n * normalizeBreakpoint('\\\\n\\\\n')\n * // → { pattern: '\\\\n\\\\n' }\n *\n * normalizeBreakpoint({ pattern: '\\\\n', min: 10 })\n * // → { pattern: '\\\\n', min: 10 }\n */\nexport const normalizeBreakpoint = (bp: Breakpoint): BreakpointRule => (typeof bp === 'string' ? { pattern: bp } : bp);\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): boolean => {\n if (!excludeList || excludeList.length === 0) {\n return false;\n }\n for (const item of excludeList) {\n if (typeof item === 'number') {\n if (pageId === item) {\n return true;\n }\n } else {\n const [from, to] = item;\n if (pageId >= from && pageId <= to) {\n return true;\n }\n }\n }\n return false;\n};\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): boolean => {\n if (rule.min !== undefined && pageId < rule.min) {\n return false;\n }\n if (rule.max !== undefined && pageId > rule.max) {\n return false;\n }\n return !isPageExcluded(pageId, rule.exclude);\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): Set<number> => {\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 const seg: Segment = { content: trimmed, from: fromPageId };\n if (toPageId !== undefined && toPageId !== fromPageId) {\n seg.to = toPageId;\n }\n if (meta) {\n seg.meta = meta;\n }\n return seg;\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};\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 */\nexport const expandBreakpoints = (breakpoints: Breakpoint[], processPattern: PatternProcessor): ExpandedBreakpoint[] =>\n breakpoints.map((bp) => {\n const rule = normalizeBreakpoint(bp);\n const excludeSet = buildExcludeSet(rule.exclude);\n const skipWhenRegex =\n rule.skipWhen !== undefined\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 : null;\n if (rule.pattern === '') {\n return { excludeSet, regex: null, rule, skipWhenRegex };\n }\n const expanded = processPattern(rule.pattern);\n try {\n return { excludeSet, regex: new RegExp(expanded, 'gmu'), rule, skipWhenRegex };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Invalid breakpoint regex: ${rule.pattern}\\n Cause: ${message}`);\n }\n });\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): string => {\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): 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): number => {\n const currentPageData = normalizedPages.get(pageIds[currentFromIdx]);\n if (!currentPageData) {\n return 0;\n }\n\n const remStart = remainingContent.trimStart().slice(0, Math.min(60, remainingContent.length));\n const needle = remStart.slice(0, Math.min(30, remStart.length));\n if (!needle) {\n return 0;\n }\n\n const idx = currentPageData.content.indexOf(needle);\n return idx > 0 ? idx : 0;\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 _currentFromIdx: number, // unused but kept for API compatibility\n targetPageIdx: number,\n expectedBoundary: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n logger?: Logger,\n): number => {\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 // The target page content might be truncated in the current segment due to structural split points\n // early in that page (e.g. headings). Use progressively shorter prefixes.\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\n // Collect all candidate positions within the search range\n const candidates: { pos: number; isNewline: boolean }[] = [];\n let pos = remainingContent.indexOf(prefix, searchStart);\n while (pos !== -1 && pos <= searchEnd) {\n if (pos > 0) {\n const charBefore = remainingContent[pos - 1];\n if (charBefore === '\\n') {\n // Page boundaries are marked by newlines - this is the strongest signal\n candidates.push({ isNewline: true, pos });\n } else if (/\\s/.test(charBefore)) {\n // Other whitespace is acceptable but less preferred\n candidates.push({ isNewline: false, pos });\n }\n }\n pos = remainingContent.indexOf(prefix, pos + 1);\n }\n\n if (candidates.length > 0) {\n // Prioritize: 1) newline-preceded matches, 2) closest to expected boundary\n const newlineCandidates = candidates.filter((c) => c.isNewline);\n const pool = newlineCandidates.length > 0 ? newlineCandidates : candidates;\n\n // Select the candidate closest to the expected boundary\n let bestCandidate = pool[0];\n let bestDistance = Math.abs(pool[0].pos - expectedBoundary);\n for (let i = 1; i < pool.length; i++) {\n const dist = Math.abs(pool[i].pos - expectedBoundary);\n if (dist < bestDistance) {\n bestDistance = dist;\n bestCandidate = pool[i];\n }\n }\n\n // Only accept the match if it's within MAX_DEVIATION of the expected boundary.\n // This prevents false positives when content is duplicated within pages.\n // MAX_DEVIATION of 2000 chars allows ~50-100% variance for typical\n // Arabic book pages (1000-3000 chars) while rejecting false positives\n // from duplicated content appearing mid-page.\n const MAX_DEVIATION = 2000;\n if (bestDistance <= MAX_DEVIATION) {\n return bestCandidate.pos;\n }\n\n logger?.debug?.('[breakpoints] findPageStartNearExpectedBoundary: Rejected match exceeding deviation', {\n bestDistance,\n expectedBoundary,\n matchPos: bestCandidate.pos,\n maxDeviation: MAX_DEVIATION,\n prefixLength: len,\n targetPageIdx,\n });\n\n // If best match is too far, continue to try shorter prefixes or return -1\n }\n }\n\n return -1;\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): number[] => {\n const boundaryPositions: number[] = [0];\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 if (pageCount >= FAST_PATH_THRESHOLD) {\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\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 logger?.debug?.('[breakpoints] buildBoundaryPositions: Using accurate string-search path', {\n contentLength: segmentContent.length,\n fromIdx,\n pageCount,\n toIdx,\n });\n const startOffsetInFromPage = estimateStartOffsetInCurrentPage(segmentContent, fromIdx, pageIds, normalizedPages);\n\n for (let i = fromIdx + 1; i <= toIdx; i++) {\n const expectedBoundary =\n cumulativeOffsets[i] !== undefined && cumulativeOffsets[fromIdx] !== undefined\n ? Math.max(0, cumulativeOffsets[i] - cumulativeOffsets[fromIdx] - startOffsetInFromPage)\n : segmentContent.length;\n\n const pos = findPageStartNearExpectedBoundary(\n segmentContent,\n fromIdx,\n i,\n expectedBoundary,\n pageIds,\n normalizedPages,\n logger,\n );\n\n const prevBoundary = boundaryPositions[boundaryPositions.length - 1];\n\n // Strict > prevents duplicate boundaries when pages have identical content\n const MAX_DEVIATION = 2000;\n const isValidPosition = pos > 0 && pos > prevBoundary && Math.abs(pos - expectedBoundary) < MAX_DEVIATION;\n\n if (isValidPosition) {\n boundaryPositions.push(pos);\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, 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 * 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): 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): number => {\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 currentFromIdx,\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): number => {\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 = (\n excludeSet: Set<number>,\n pageIds: number[],\n fromIdx: number,\n toIdx: number,\n): boolean => {\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): number => {\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.\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 * @returns Break position after the selected match, or -1 if no matches\n */\nexport const findPatternBreakPosition = (\n windowContent: string,\n regex: RegExp,\n prefer: 'longer' | 'shorter',\n): number => {\n // OPTIMIZATION: Stream matches instead of collecting all into an array.\n // Only track first and last match to avoid allocating large arrays for dense patterns.\n let first: { index: number; length: number } | undefined;\n let last: { index: number; length: number } | undefined;\n for (const m of windowContent.matchAll(regex)) {\n const match = { index: m.index, length: m[0].length };\n if (!first) {\n first = match;\n }\n last = match;\n }\n if (!first) {\n return -1;\n }\n const selected = prefer === 'longer' ? last! : first;\n return selected.index + selected.length;\n};\n\n/**\n * Handles page boundary breakpoint (empty pattern).\n * Returns break position or -1 if no valid position found.\n */\nconst handlePageBoundaryBreak = (\n remainingContent: string,\n windowEndIdx: number,\n windowEndPosition: number,\n toIdx: number,\n pageIds: number[],\n normalizedPages: Map<number, NormalizedPage>,\n): number => {\n const nextPageIdx = windowEndIdx + 1;\n if (nextPageIdx <= toIdx) {\n const nextPageData = normalizedPages.get(pageIds[nextPageIdx]);\n if (nextPageData) {\n const pos = findNextPagePosition(remainingContent, nextPageData);\n // Only trust findNextPagePosition if the result is reasonably close to windowEndPosition.\n // This prevents incorrect breaks when content is duplicated within pages.\n // Use a generous tolerance (2000 chars or 50% of windowEndPosition, whichever is larger).\n const tolerance = Math.max(2000, windowEndPosition * 0.5);\n if (pos > 0 && Math.abs(pos - windowEndPosition) <= tolerance) {\n return Math.min(pos, windowEndPosition, remainingContent.length);\n }\n }\n }\n // Fall back to windowEndPosition which is computed from cumulative offsets\n return Math.min(windowEndPosition, remainingContent.length);\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): { breakpointIndex: number; breakPos: number; rule: BreakpointRule } | null => {\n const { pageIds, normalizedPages, expandedBreakpoints, prefer } = ctx;\n\n for (let i = 0; i < expandedBreakpoints.length; i++) {\n const { rule, regex, excludeSet, skipWhenRegex } = expandedBreakpoints[i];\n // Check if this breakpoint applies to the current segment's starting page\n if (!isInBreakpointRange(pageIds[currentFromIdx], rule)) {\n continue;\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 continue;\n }\n\n // Check if content matches skipWhen pattern (pre-compiled)\n if (skipWhenRegex?.test(remainingContent)) {\n continue;\n }\n\n // Handle page boundary (empty pattern)\n if (regex === null) {\n return {\n breakPos: handlePageBoundaryBreak(\n remainingContent,\n windowEndIdx,\n windowEndPosition,\n toIdx,\n pageIds,\n normalizedPages,\n ),\n breakpointIndex: i,\n rule,\n };\n }\n\n // Find matches within window\n const windowContent = remainingContent.slice(0, Math.min(windowEndPosition, remainingContent.length));\n const breakPos = findPatternBreakPosition(windowContent, regex, prefer);\n if (breakPos > 0) {\n return { breakPos, breakpointIndex: i, rule };\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): number => {\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 // Includes Arabic comma (،), semicolon (؛), full stop (.), etc.\n if (/[\\s\\n.,;!?؛،۔]/.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): 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, SplitRule } from './types.js';\n\nexport type DebugConfig = { includeBreakpoint: boolean; includeRule: boolean; metaKey: string } | null;\n\nexport const resolveDebugConfig = (debug: unknown): DebugConfig => {\n if (!debug) {\n return null;\n }\n if (debug === true) {\n return { includeBreakpoint: true, includeRule: true, metaKey: '_flappa' };\n }\n if (typeof debug !== 'object') {\n return null;\n }\n const metaKey = (debug as any).metaKey;\n const include = (debug as any).include;\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 if ('lineStartsWith' in rule) {\n return 'lineStartsWith';\n }\n if ('lineStartsAfter' in rule) {\n return 'lineStartsAfter';\n }\n if ('lineEndsWith' in rule) {\n return 'lineEndsWith';\n }\n if ('template' in rule) {\n return 'template';\n }\n return '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): Record<string, unknown> => {\n const out = meta ? { ...meta } : {};\n const existing = out[metaKey];\n const existingObj = isPlainObject(existing) ? existing : {};\n out[metaKey] = { ...existingObj, ...patch };\n return out;\n};\n\nexport const buildRuleDebugPatch = (ruleIndex: number, rule: SplitRule) => ({\n rule: { index: ruleIndex, patternType: getRulePatternType(rule) },\n});\n\nexport const buildBreakpointDebugPatch = (breakpointIndex: number, rule: BreakpointRule) => ({\n breakpoint: {\n index: breakpointIndex,\n kind: rule.pattern === '' ? 'pageBoundary' : 'pattern',\n pattern: rule.pattern,\n },\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 { FAST_PATH_THRESHOLD } from './breakpoint-constants.js';\nimport {\n adjustForSurrogate,\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';\nimport type { Breakpoint, Logger, Page, Segment } from './types.js';\n\nexport type BreakpointPatternProcessor = (pattern: string) => string;\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 ? pageData.length : 0;\n if (i < pageIds.length - 1) {\n totalOffset += 1; // separator between pages\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): boolean => expandedBreakpoints.some((bp) => hasExcludedPageInRange(bp.excludeSet, pageIds, fromIdx, toIdx));\n\nexport const computeWindowEndIdx = (currentFromIdx: number, toIdx: number, pageIds: number[], maxPages: number) => {\n const currentPageId = pageIds[currentFromIdx];\n const maxWindowPageId = currentPageId + 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\ntype PiecePages = { actualEndIdx: number; actualStartIdx: number };\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): PiecePages => {\n const actualStartIdx = findPageIndexForPosition(pieceStartPos, boundaryPositions, baseFromIdx);\n // For end position, use pieceEndPos - 1 to get the page containing the last character\n // (since pieceEndPos is exclusive)\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, Math.min(30, nextPageData.length));\n const remainingPrefix = remainingContent.trimStart().slice(0, Math.min(30, remainingContent.length));\n // Check both directions:\n // 1. remainingContent starts with page prefix (page is longer or equal)\n // 2. page content starts with remaining prefix (remaining is shorter)\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): { breakpointIndex?: number; breakOffset: number; breakpointRule?: { pattern: string } } => {\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 );\n\n if (patternMatch && patternMatch.breakPos > 0) {\n return {\n breakOffset: patternMatch.breakPos,\n breakpointIndex: patternMatch.breakpointIndex,\n breakpointRule: patternMatch.rule,\n };\n }\n\n // Fallback: If hitting maxContentLength, try to find a safe break position\n if (maxContentLength && windowEndPosition === maxContentLength) {\n const safeOffset = findSafeBreakPosition(remainingContent, windowEndPosition);\n if (safeOffset !== -1) {\n return { breakOffset: safeOffset };\n }\n // If no safe break (whitespace) found, ensure we don't split a surrogate pair\n const adjustedOffset = adjustForSurrogate(remainingContent, windowEndPosition);\n return { breakOffset: adjustedOffset };\n }\n\n return { breakOffset: windowEndPosition };\n};\n\n/**\n * Advances cursor position past any leading whitespace.\n */\nconst skipWhitespace = (content: string, startPos: number): 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): boolean => {\n const expectedLength = (cumulativeOffsets[toIdx + 1] ?? fullContent.length) - (cumulativeOffsets[fromIdx] ?? 0);\n const actualLength = fullContent.length;\n const driftTolerance = Math.max(100, actualLength * 0.01); // 1% or 100 chars tolerance\n\n const isAligned = Math.abs(expectedLength - actualLength) <= 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,\n drift: Math.abs(expectedLength - actualLength),\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): Segment[] => {\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 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): Segment[] => {\n const result: Segment[] = [];\n const effectiveMaxPages = maxPages + 1;\n const pageCount = toIdx - fromIdx + 1;\n\n logger?.debug?.('[breakpoints] Using offset-based fast-path for large segment', {\n effectiveMaxPages,\n fromIdx,\n maxPages,\n pageCount,\n toIdx,\n });\n\n const baseOffset = cumulativeOffsets[fromIdx] ?? 0;\n\n for (let segStart = fromIdx; segStart <= toIdx; segStart += effectiveMaxPages) {\n const segEnd = Math.min(segStart + effectiveMaxPages - 1, toIdx);\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\n const rawContent = fullContent.slice(startOffset, endOffset).trim();\n if (rawContent) {\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 if (segEnd > segStart) {\n seg.to = pageIds[segEnd];\n }\n if (meta) {\n seg.meta = meta;\n }\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 */\nconst handleOversizedSegmentFit = (\n remainingContent: string,\n currentFromIdx: number,\n toIdx: 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: { pattern: string } } | null,\n result: Segment[],\n): boolean => {\n const remainingSpan = computeRemainingSpan(currentFromIdx, toIdx, pageIds);\n const remainingHasExclusions = hasAnyExclusionsInRange(expandedBreakpoints, pageIds, currentFromIdx, toIdx);\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(remainingContent, currentFromIdx, toIdx, pageIds, meta, includeMeta);\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: { pattern: string } } | null,\n): Segment['meta'] | undefined => {\n const includeMeta = isFirstPiece || Boolean(debugMetaKey);\n if (!includeMeta) {\n return undefined;\n }\n\n if (debugMetaKey && lastBreakpoint) {\n return mergeDebugIntoMeta(\n isFirstPiece ? originalMeta : undefined,\n debugMetaKey,\n buildBreakpointDebugPatch(lastBreakpoint.breakpointIndex, lastBreakpoint.rule as any),\n );\n }\n return isFirstPiece ? originalMeta : undefined;\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): number => {\n let windowEndPosition = findBreakpointWindowEndPosition(\n remainingContent,\n currentFromIdx,\n windowEndIdx,\n toIdx,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\n logger,\n );\n\n if (maxContentLength && maxContentLength < windowEndPosition) {\n windowEndPosition = maxContentLength;\n }\n return windowEndPosition;\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): { currentFromIdx: number; cursorPos: number } => {\n const nextCursorPos = skipWhitespace(fullContent, breakPos);\n const nextFromIdx = computeNextFromIdx(\n fullContent.slice(nextCursorPos, nextCursorPos + 500), // Optimization: only need prefix\n actualEndIdx,\n toIdx,\n pageIds,\n normalizedPages,\n );\n return { currentFromIdx: nextFromIdx, cursorPos: nextCursorPos };\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 result: Segment[] = [];\n const fullContent = segment.content;\n const pageCount = toIdx - fromIdx + 1;\n\n // FAST PATH LOGIC\n // -------------------------------------------------------------------------\n // For large segments (1000+ pages), use cumulative offsets directly to avoid O(n²) processing.\n // We skip this optimization if:\n // 1. debugMetaKey is set (we need full provenance)\n // 2. maxContentLength is set (requires character-accurate checks)\n // 3. Offset drift is detected (structural rules modified content length)\n\n const isAligned = checkFastPathAlignment(cumulativeOffsets, fullContent, fromIdx, toIdx, pageCount, logger);\n\n if (pageCount >= FAST_PATH_THRESHOLD && isAligned && !maxContentLength && !debugMetaKey) {\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 return processOffsetFastPath(\n fullContent,\n fromIdx,\n toIdx,\n pageIds,\n cumulativeOffsets,\n maxPages,\n segment.meta,\n debugMetaKey,\n logger,\n );\n }\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: { pattern: string } } | 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 let i = 0;\n const MAX_SAFE_ITERATIONS = 100_000;\n while (cursorPos < fullContent.length && currentFromIdx <= toIdx && i < MAX_SAFE_ITERATIONS) {\n i++;\n // Optimization: slice only what's needed to avoid O(N^2) copying for large content\n const safeSliceLen = maxContentLength ? maxContentLength + 4000 : undefined;\n const remainingContent = safeSliceLen\n ? fullContent.slice(cursorPos, cursorPos + safeSliceLen)\n : fullContent.slice(cursorPos);\n\n if (!remainingContent.trim()) {\n break;\n }\n\n if (\n handleOversizedSegmentFit(\n remainingContent,\n currentFromIdx,\n toIdx,\n pageIds,\n expandedBreakpoints,\n maxPages,\n maxContentLength,\n isFirstPiece,\n debugMetaKey,\n segment.meta,\n lastBreakpoint,\n result,\n )\n ) {\n break;\n }\n\n const windowEndIdx = computeWindowEndIdx(currentFromIdx, toIdx, pageIds, maxPages);\n const windowEndPosition = getWindowEndPosition(\n remainingContent,\n currentFromIdx,\n windowEndIdx,\n toIdx,\n pageIds,\n normalizedPages,\n cumulativeOffsets,\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 // 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 let breakOffset = found.breakOffset;\n if (breakOffset <= 0) {\n const fallbackPos = maxContentLength ? Math.min(maxContentLength, remainingContent.length) : 1;\n breakOffset = Math.max(1, fallbackPos);\n logger?.warn?.('[breakpoints] No progress from findBreakOffsetForWindow; forcing forward movement', {\n breakOffset,\n cursorPos,\n });\n }\n\n if (found.breakpointIndex !== undefined && found.breakpointRule) {\n lastBreakpoint = { breakpointIndex: found.breakpointIndex, rule: found.breakpointRule };\n }\n\n const breakPos = cursorPos + breakOffset;\n const pieceContent = fullContent.slice(cursorPos, breakPos).trim();\n\n if (pieceContent) {\n const { actualEndIdx, actualStartIdx } = computePiecePages(\n cursorPos,\n breakPos,\n boundaryPositions,\n fromIdx,\n toIdx,\n );\n const meta = getSegmentMetaWithDebug(isFirstPiece, debugMetaKey, segment.meta, lastBreakpoint);\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 cursorPos = next.cursorPos;\n currentFromIdx = next.currentFromIdx;\n } else {\n cursorPos = breakPos;\n }\n\n isFirstPiece = false;\n }\n\n if (i >= MAX_SAFE_ITERATIONS) {\n logger?.error?.('[breakpoints] Stopped processing oversized segment: reached MAX_SAFE_ITERATIONS', {\n cursorPos,\n fullContentLength: fullContent.length,\n iterations: i,\n });\n }\n\n logger?.debug?.('[breakpoints] processOversizedSegment: Complete', { iterations: i, resultCount: result.length });\n return result;\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: BreakpointPatternProcessor,\n logger?: Logger,\n pageJoiner: 'space' | 'newline' = 'space',\n debugMetaKey?: string,\n maxContentLength?: number,\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);\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 * Utility functions for regex matching and result processing.\n *\n * These functions were extracted from `segmenter.ts` to reduce complexity\n * and enable independent testing. They handle match filtering, capture\n * extraction, and occurrence-based selection.\n *\n * @module match-utils\n */\n\nimport { isPageExcluded } from './breakpoint-utils.js';\nimport type { SplitRule } from './types.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/**\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 = (\n groups: Record<string, string> | undefined,\n captureNames: string[],\n): Record<string, string> | undefined => {\n if (!groups || captureNames.length === 0) {\n return undefined;\n }\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\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): string | undefined => {\n if (match.length <= 1) {\n return undefined;\n }\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): MatchResult[] => {\n return matches.filter((m) => {\n const id = getId(m.start);\n if (rule.min !== undefined && id < rule.min) {\n return false;\n }\n if (rule.max !== undefined && id > rule.max) {\n return false;\n }\n if (isPageExcluded(id, rule.exclude)) {\n return false;\n }\n return true;\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'): MatchResult[] => {\n if (!matches.length) {\n return [];\n }\n if (occurrence === 'first') {\n return [matches[0]];\n }\n if (occurrence === 'last') {\n return [matches[matches.length - 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): boolean => {\n return rules.some((r) => {\n const minOk = r.min === undefined || pageId >= r.min;\n const maxOk = r.max === undefined || pageId <= r.max;\n return minOk && maxOk;\n });\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 { makeDiacriticInsensitive } from './fuzzy.js';\nimport { escapeTemplateBrackets, expandTokensWithCaptures, shouldDefaultToFuzzy } from './tokens.js';\nimport type { SplitRule } from './types.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\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 => {\n // Match ( that is NOT followed by ? (excludes non-capturing and named groups)\n return /\\((?!\\?)/.test(pattern);\n};\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 const names: string[] = [];\n // Match (?<name> where name is the capture group name\n const namedGroupRegex = /\\(\\?<([^>]+)>/g;\n for (const match of pattern.matchAll(namedGroupRegex)) {\n names.push(match[1]);\n }\n return names;\n};\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 const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Invalid regex pattern: ${pattern}\\n Cause: ${message}`);\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 escaped = escapeTemplateBrackets(pattern);\n const fuzzyTransform = fuzzy ? makeDiacriticInsensitive : undefined;\n const { pattern: expanded, captureNames } = expandTokensWithCaptures(escaped, fuzzyTransform, capturePrefix);\n return { captureNames, pattern: expanded };\n};\n\nexport const buildLineStartsAfterRegexSource = (\n patterns: string[],\n fuzzy: boolean,\n capturePrefix?: string,\n): { regex: string; captureNames: string[] } => {\n const processed = patterns.map((p) => processPattern(p, fuzzy, capturePrefix));\n const union = processed.map((p) => p.pattern).join('|');\n const captureNames = processed.flatMap((p) => p.captureNames);\n // For lineStartsAfter, we need to capture the content.\n // If we have a prefix (combined-regex mode), we name the internal content capture so the caller\n // can compute marker length. IMPORTANT: this internal group is not a \"user capture\", so it must\n // NOT be included in `captureNames` (otherwise it leaks into segment.meta as `content`).\n const contentCapture = capturePrefix ? `(?<${capturePrefix}__content>.*)` : '(.*)';\n // Allow zero-width formatters (LRM, RLM, ALM, etc.) at line start before matching content.\n const zeroWidthPrefix = '[\\\\u200E\\\\u200F\\\\u061C\\\\u200B\\\\uFEFF]*';\n return { captureNames, regex: `^${zeroWidthPrefix}(?:${union})${contentCapture}` };\n};\n\nexport const buildLineStartsWithRegexSource = (\n patterns: string[],\n fuzzy: boolean,\n capturePrefix?: string,\n): { regex: string; captureNames: string[] } => {\n const processed = patterns.map((p) => processPattern(p, fuzzy, capturePrefix));\n const union = processed.map((p) => p.pattern).join('|');\n const captureNames = processed.flatMap((p) => p.captureNames);\n // Allow zero-width formatters (LRM, RLM, ALM, etc.) at line start before matching content.\n // These invisible Unicode characters are common in Arabic text and shouldn't affect semantics.\n // U+200E (LRM), U+200F (RLM), U+061C (ALM), U+200B (ZWSP), U+FEFF (BOM/ZWNBSP)\n const zeroWidthPrefix = '[\\\\u200E\\\\u200F\\\\u061C\\\\u200B\\\\uFEFF]*';\n return { captureNames, regex: `^${zeroWidthPrefix}(?:${union})` };\n};\n\nexport const buildLineEndsWithRegexSource = (\n patterns: string[],\n fuzzy: boolean,\n capturePrefix?: string,\n): { regex: string; captureNames: string[] } => {\n const processed = patterns.map((p) => processPattern(p, fuzzy, capturePrefix));\n const union = processed.map((p) => p.pattern).join('|');\n const captureNames = processed.flatMap((p) => p.captureNames);\n return { captureNames, regex: `(?:${union})$` };\n};\n\nexport const buildTemplateRegexSource = (\n template: string,\n capturePrefix?: string,\n): { regex: string; captureNames: string[] } => {\n const escaped = escapeTemplateBrackets(template);\n const { pattern, captureNames } = expandTokensWithCaptures(escaped, undefined, capturePrefix);\n return { captureNames, regex: pattern };\n};\n\nexport const determineUsesCapture = (regexSource: string, _captureNames: string[]): boolean =>\n hasCapturingGroup(regexSource);\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 s: {\n lineStartsWith?: string[];\n lineStartsAfter?: string[];\n lineEndsWith?: string[];\n template?: string;\n regex?: string;\n } = { ...rule };\n\n // Auto-detect fuzzy for tokens like bab, kitab, etc. unless explicitly set\n const allPatterns = [...(s.lineStartsWith ?? []), ...(s.lineStartsAfter ?? []), ...(s.lineEndsWith ?? [])];\n const explicitFuzzy = (rule as { fuzzy?: boolean }).fuzzy;\n const fuzzy = explicitFuzzy ?? shouldDefaultToFuzzy(allPatterns);\n let allCaptureNames: string[] = [];\n\n // lineStartsAfter: creates a capturing group to exclude the marker from content\n if (s.lineStartsAfter?.length) {\n const { regex, captureNames } = buildLineStartsAfterRegexSource(s.lineStartsAfter, fuzzy, capturePrefix);\n allCaptureNames = captureNames;\n return {\n captureNames: allCaptureNames,\n regex: compileRuleRegex(regex),\n usesCapture: true,\n usesLineStartsAfter: true,\n };\n }\n\n if (s.lineStartsWith?.length) {\n const { regex, captureNames } = buildLineStartsWithRegexSource(s.lineStartsWith, fuzzy, capturePrefix);\n s.regex = regex;\n allCaptureNames = captureNames;\n }\n if (s.lineEndsWith?.length) {\n const { regex, captureNames } = buildLineEndsWithRegexSource(s.lineEndsWith, fuzzy, capturePrefix);\n s.regex = regex;\n allCaptureNames = captureNames;\n }\n if (s.template) {\n const { regex, captureNames } = buildTemplateRegexSource(s.template, capturePrefix);\n s.regex = regex;\n allCaptureNames = [...allCaptureNames, ...captureNames];\n }\n\n if (!s.regex) {\n throw new Error(\n 'Rule must specify exactly one pattern type: regex, template, lineStartsWith, lineStartsAfter, or lineEndsWith',\n );\n }\n\n // Extract named capture groups from raw regex patterns if not already populated\n if (allCaptureNames.length === 0) {\n allCaptureNames = extractNamedCaptureNames(s.regex);\n }\n\n const usesCapture = determineUsesCapture(s.regex, allCaptureNames);\n return {\n captureNames: allCaptureNames,\n regex: compileRuleRegex(s.regex),\n usesCapture,\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 } from './tokens.js';\n\n// U+064B..U+0652 (tashkeel/harakat)\nconst isArabicDiacriticCode = (code: number): boolean => code >= 0x064b && code <= 0x0652;\n\n// Map a char to a representative equivalence class key.\n// Keep this in sync with EQUIV_GROUPS in fuzzy.ts.\nconst equivKey = (ch: string): 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\n/**\n * Match a fuzzy literal prefix at a given offset.\n *\n * - Skips diacritics in the content\n * - Applies equivalence groups on both content and literal\n *\n * @returns endOffset (exclusive) in CONTENT if matched; otherwise null.\n */\nexport const matchFuzzyLiteralPrefixAt = (content: string, offset: number, literal: string): number | null => {\n let i = offset;\n // Skip leading diacritics in content (rare but possible)\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\n // In literal, we treat whitespace literally (no collapsing).\n // (Tokens like kitab/bab/fasl/naql/basmalah do not rely on fuzzy spaces.)\n // Skip diacritics in content before matching each char.\n while (i < content.length && isArabicDiacriticCode(content.charCodeAt(i))) {\n i++;\n }\n\n if (i >= content.length) {\n return null;\n }\n\n const cCh = content[i];\n if (equivKey(cCh) !== equivKey(litCh)) {\n return null;\n }\n i++;\n }\n\n // Allow trailing diacritics immediately after the matched prefix.\n while (i < content.length && isArabicDiacriticCode(content.charCodeAt(i))) {\n i++;\n }\n return i;\n};\n\nconst isLiteralOnly = (s: string): boolean => {\n // Reject anything that looks like regex syntax.\n // We allow only plain text (including Arabic, spaces) and the alternation separator `|`.\n // This intentionally rejects tokens like `tarqim: '[.!?؟؛]'`, which are not literal.\n return !/[\\\\[\\]{}()^$.*+?]/.test(s);\n};\n\nexport type CompiledLiteralAlternation = {\n alternatives: string[];\n};\n\nexport const compileLiteralAlternation = (pattern: string): CompiledLiteralAlternation | null => {\n if (!pattern) {\n return null;\n }\n if (!isLiteralOnly(pattern)) {\n return null;\n }\n const alternatives = pattern\n .split('|')\n .map((s) => s.trim())\n .filter(Boolean);\n if (!alternatives.length) {\n return null;\n }\n return { alternatives };\n};\n\nexport type FastFuzzyTokenRule = {\n token: string; // token name, e.g. 'kitab'\n alternatives: string[]; // resolved literal alternatives\n};\n\n/**\n * Attempt to compile a fast fuzzy rule from a single-token pattern like `{{kitab}}`.\n * Returns null if not eligible.\n */\nexport const compileFastFuzzyTokenRule = (tokenTemplate: string): FastFuzzyTokenRule | null => {\n const m = tokenTemplate.match(/^\\{\\{(\\w+)\\}\\}$/);\n if (!m) {\n return null;\n }\n const token = m[1];\n const tokenPattern = getTokenPattern(token);\n if (!tokenPattern) {\n return null;\n }\n const compiled = compileLiteralAlternation(tokenPattern);\n if (!compiled) {\n return null;\n }\n return { alternatives: compiled.alternatives, token };\n};\n\n/**\n * Try matching any alternative for a compiled token at a line-start offset.\n * Returns endOffset (exclusive) on match, else null.\n */\nexport const matchFastFuzzyTokenAt = (content: string, offset: number, compiled: FastFuzzyTokenRule): number | null => {\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 { isPageExcluded } from './breakpoint-utils.js';\nimport { compileFastFuzzyTokenRule, type FastFuzzyTokenRule, matchFastFuzzyTokenAt } from './fast-fuzzy-prefix.js';\nimport { extractNamedCaptureNames, hasCapturingGroup, processPattern } from './rule-regex.js';\nimport type { PageMap, SplitPoint } from './segmenter-types.js';\nimport type { SplitRule } from './types.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: SplitRule[];\n fastFuzzyRules: FastFuzzyRule[];\n};\n\nexport const partitionRulesForMatching = (rules: SplitRule[]): PartitionedRules => {\n const combinableRules: { rule: SplitRule; prefix: string; index: number }[] = [];\n const standaloneRules: SplitRule[] = [];\n const fastFuzzyRules: FastFuzzyRule[] = [];\n\n // Separate rules into combinable, standalone, and fast-fuzzy\n rules.forEach((rule, index) => {\n // Fast-path: fuzzy + lineStartsWith + single token pattern like {{kitab}}\n if ((rule as { fuzzy?: boolean }).fuzzy && 'lineStartsWith' in rule) {\n const compiled =\n rule.lineStartsWith.length === 1 ? compileFastFuzzyTokenRule(rule.lineStartsWith[0]) : null;\n if (compiled) {\n fastFuzzyRules.push({ compiled, kind: 'startsWith', rule, ruleIndex: index });\n return; // handled by fast path\n }\n }\n\n // Fast-path: fuzzy + lineStartsAfter + single token pattern like {{naql}}\n if ((rule as { fuzzy?: boolean }).fuzzy && 'lineStartsAfter' in rule) {\n const compiled =\n rule.lineStartsAfter.length === 1 ? compileFastFuzzyTokenRule(rule.lineStartsAfter[0]) : null;\n if (compiled) {\n fastFuzzyRules.push({ compiled, kind: 'startsAfter', rule, ruleIndex: index });\n return; // handled by fast path\n }\n }\n\n let isCombinable = true;\n\n // Raw regex rules are combinable ONLY if they don't use named captures, backreferences, or anonymous captures\n if ('regex' in rule && rule.regex) {\n const hasNamedCaptures = extractNamedCaptureNames(rule.regex).length > 0;\n const hasBackreferences = /\\\\[1-9]/.test(rule.regex);\n const hasAnonymousCaptures = hasCapturingGroup(rule.regex);\n if (hasNamedCaptures || hasBackreferences || hasAnonymousCaptures) {\n isCombinable = false;\n }\n }\n\n if (isCombinable) {\n combinableRules.push({ index, prefix: `r${index}_`, rule });\n } else {\n standaloneRules.push(rule);\n }\n });\n\n return { combinableRules, fastFuzzyRules, standaloneRules };\n};\n\nexport type PageStartGuardChecker = (rule: SplitRule, ruleIndex: number, matchStart: number) => boolean;\n\nexport const createPageStartGuardChecker = (matchContent: string, pageMap: PageMap): PageStartGuardChecker => {\n const pageStartToBoundaryIndex = new Map<number, number>();\n for (let i = 0; i < pageMap.boundaries.length; i++) {\n pageStartToBoundaryIndex.set(pageMap.boundaries[i].start, i);\n }\n\n const compiledPageStartPrev = new Map<number, RegExp | null>();\n const getPageStartPrevRegex = (rule: SplitRule, ruleIndex: number): RegExp | null => {\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 expanded = processPattern(pattern, false).pattern;\n const re = new RegExp(`(?:${expanded})$`, 'u');\n compiledPageStartPrev.set(ruleIndex, re);\n return re;\n };\n\n const getPrevPageLastNonWsChar = (boundaryIndex: number): string => {\n if (boundaryIndex <= 0) {\n return '';\n }\n const prevBoundary = pageMap.boundaries[boundaryIndex - 1];\n // prevBoundary.end points at the inserted page-break newline; the last content char is end-1.\n for (let i = prevBoundary.end - 1; i >= prevBoundary.start; i--) {\n const ch = matchContent[i];\n if (!ch) {\n continue;\n }\n if (/\\s/u.test(ch)) {\n continue;\n }\n return ch;\n }\n return '';\n };\n\n return (rule: SplitRule, ruleIndex: number, matchStart: number): boolean => {\n const boundaryIndex = pageStartToBoundaryIndex.get(matchStart);\n if (boundaryIndex === undefined || boundaryIndex === 0) {\n return true; // not a page start, or the very first page\n }\n const prevReq = getPageStartPrevRegex(rule, ruleIndex);\n if (!prevReq) {\n return true;\n }\n const lastChar = getPrevPageLastNonWsChar(boundaryIndex);\n if (!lastChar) {\n return false;\n }\n return prevReq.test(lastChar);\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): boolean => {\n return (\n (rule.min === undefined || pageId >= rule.min) &&\n (rule.max === undefined || pageId <= rule.max) &&\n !isPageExcluded(pageId, rule.exclude)\n );\n};\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 return;\n }\n arr.push(sp);\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 isPageStart: boolean,\n splitPointsByRule: Map<number, SplitPoint[]>,\n) => {\n for (const { compiled, kind, rule, ruleIndex } of fastFuzzyRules) {\n if (!passesRuleConstraints(rule, pageId)) {\n continue;\n }\n\n if (isPageStart && !passesPageStartGuard(rule, ruleIndex, lineStart)) {\n continue;\n }\n\n const end = matchFastFuzzyTokenAt(matchContent, lineStart, compiled);\n if (end === null) {\n continue;\n }\n\n const splitIndex = (rule.split ?? 'at') === 'at' ? lineStart : end;\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: (rule.split ?? 'at') === 'at' ? markerLength : undefined,\n index: splitIndex,\n meta: rule.meta,\n });\n }\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 const isPageStart = (offset: number): boolean => offset === currentBoundary?.start;\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 isPageStart(lineStart),\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 { isPageExcluded } from './breakpoint-utils.js';\nimport {\n extractNamedCaptures,\n filterByConstraints,\n getLastPositionalCapture,\n type MatchResult,\n} from './match-utils.js';\nimport { buildRuleRegex, type RuleRegex } from './rule-regex.js';\nimport type { PageMap, SplitPoint } from './segmenter-types.js';\nimport type { Logger, SplitRule } from './types.js';\nimport { buildRuleDebugPatch, mergeDebugIntoMeta } from './debug-meta.js';\n\n// Maximum iterations before throwing to prevent infinite loops\nconst MAX_REGEX_ITERATIONS = 100000;\n\ntype CombinableRule = { rule: SplitRule; prefix: string; index: number };\n\ntype RuleRegexInfo = RuleRegex & { prefix: string; source: string };\n\n// ─────────────────────────────────────────────────────────────\n// Combined regex matching\n// ─────────────────────────────────────────────────────────────\n\nconst extractNamedCapturesForRule = (\n groups: Record<string, string> | undefined,\n captureNames: string[],\n prefix: string,\n): Record<string, string> => {\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): boolean =>\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 { contentStartOffset } = buildContentOffsets(match, ruleInfo);\n\n return {\n capturedContent: undefined,\n 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 };\n};\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): void => {\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 iterations++;\n\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\n if (iterations % 10000 === 0) {\n logger?.warn?.('[segmenter] high iteration count', { iterations, position: m.index });\n }\n\n const matchedIndex = combinableRules.findIndex(({ prefix }) => m?.groups?.[prefix] !== undefined);\n\n if (matchedIndex !== -1) {\n const { rule, index: originalIndex } = combinableRules[matchedIndex];\n const ruleInfo = ruleRegexes[matchedIndex];\n const pageId = pageMap.getId(m.index);\n\n if (passesRuleConstraints(rule, pageId) && passesPageStartGuard(rule, originalIndex, m.index)) {\n const sp = createSplitPointFromMatch(m, rule, ruleInfo);\n\n if (!splitPointsByRule.has(originalIndex)) {\n splitPointsByRule.set(originalIndex, []);\n }\n splitPointsByRule.get(originalIndex)!.push(sp);\n }\n }\n\n if (m[0].length === 0) {\n combinedRegex.lastIndex++;\n }\n m = combinedRegex.exec(matchContent);\n }\n};\n\nexport const buildRuleRegexes = (combinableRules: CombinableRule[]): RuleRegexInfo[] =>\n combinableRules.map(({ rule, prefix }) => {\n const built = buildRuleRegex(rule, prefix);\n return { ...built, prefix, source: `(?<${prefix}>${built.regex.source})` };\n });\n\n// ─────────────────────────────────────────────────────────────\n// Standalone rule processing\n// ─────────────────────────────────────────────────────────────\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): void => {\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 guarded = constrained.filter((m) => passesPageStartGuard(rule, ruleIndex, m.start));\n\n const points = guarded.map((m) => {\n const isLSA = usesLineStartsAfter && m.captured !== undefined;\n const markerLen = isLSA ? m.end - m.captured!.length - m.start : 0;\n return {\n capturedContent: isLSA ? undefined : m.captured,\n contentStartOffset: isLSA ? markerLen : undefined,\n index: (rule.split ?? 'at') === 'at' ? m.start : m.end,\n meta: rule.meta,\n namedCaptures: m.namedCaptures,\n };\n });\n\n if (!splitPointsByRule.has(ruleIndex)) {\n splitPointsByRule.set(ruleIndex, []);\n }\n splitPointsByRule.get(ruleIndex)!.push(...points);\n};\n\nconst findMatchesInContent = (\n content: string,\n regex: RegExp,\n usesCapture: boolean,\n captureNames: string[],\n): MatchResult[] => {\n const matches: MatchResult[] = [];\n let m = regex.exec(content);\n\n while (m !== null) {\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 });\n if (m[0].length === 0) {\n regex.lastIndex++;\n }\n m = regex.exec(content);\n }\n\n return matches;\n};\n\n// ─────────────────────────────────────────────────────────────\n// Occurrence filtering\n// ─────────────────────────────────────────────────────────────\n\nexport const applyOccurrenceFilter = (\n rules: SplitRule[],\n splitPointsByRule: Map<number, SplitPoint[]>,\n debugMetaKey?: string,\n): SplitPoint[] => {\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 if (!debugMetaKey) {\n result.push(...filtered.map((p) => ({ ...p, ruleIndex: index })));\n return;\n }\n\n const debugPatch = buildRuleDebugPatch(index, rule);\n result.push(\n ...filtered.map((p) => ({\n ...p,\n meta: mergeDebugIntoMeta(p.meta, debugMetaKey, debugPatch),\n ruleIndex: index,\n })),\n );\n });\n\n return result;\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 * Core segmentation engine for splitting Arabic text pages into logical segments.\n *\n * The segmenter takes an array of pages and applies pattern-based rules to\n * identify split points, producing segments with content, page references,\n * and optional metadata.\n *\n * @module segmenter\n */\n\nimport { applyBreakpoints } from './breakpoint-processor.js';\nimport { resolveDebugConfig } from './debug-meta.js';\nimport { anyRuleAllowsId } from './match-utils.js';\nimport { applyReplacements } from './replace.js';\nimport { processPattern } from './rule-regex.js';\nimport {\n collectFastFuzzySplitPoints,\n createPageStartGuardChecker,\n partitionRulesForMatching,\n} from './segmenter-rule-utils.js';\nimport type { PageBoundary, PageMap, SplitPoint } from './segmenter-types.js';\nimport {\n applyOccurrenceFilter,\n buildRuleRegexes,\n processCombinedMatches,\n processStandaloneRule,\n} from './split-point-helpers.js';\nimport { normalizeLineEndings } from './textUtils.js';\nimport type { Logger, Page, Segment, SegmentationOptions, SplitRule } from './types.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[]): { content: string; normalizedPages: string[]; pageMap: PageMap } => {\n const boundaries: PageBoundary[] = [];\n const pageBreaks: number[] = []; // Sorted array for binary search\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); // Already in sorted order\n offset += normalized.length + 1;\n } else {\n offset += normalized.length;\n }\n }\n\n /**\n * Finds the page boundary containing the given offset using binary search.\n * O(log n) complexity for efficient lookup with many pages.\n *\n * @param off - Character offset to look up\n * @returns Page boundary or the last boundary as fallback\n */\n const findBoundary = (off: number): PageBoundary | undefined => {\n let lo = 0;\n let hi = boundaries.length - 1;\n\n while (lo <= hi) {\n const mid = (lo + hi) >>> 1; // Unsigned right shift for floor division\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 // Fallback to last boundary if not found\n return boundaries[boundaries.length - 1];\n };\n\n return {\n content: parts.join('\\n'),\n normalizedPages: parts, // OPTIMIZATION: Return already-normalized content for reuse\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 const hasMoreInfo =\n (p.contentStartOffset !== undefined && existing.contentStartOffset === undefined) ||\n (p.meta !== undefined && existing.meta === undefined);\n if (hasMoreInfo) {\n byIndex.set(p.index, p);\n }\n }\n const unique = [...byIndex.values()];\n unique.sort((a, b) => a.index - b.index);\n return unique;\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 const firstPage = pages[0];\n const lastPage = pages.at(-1)!;\n const joinChar = pageJoiner === 'newline' ? '\\n' : ' ';\n const allContent = normalizedContent.join(joinChar).trim();\n if (!allContent) {\n return segments;\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 // Start with fast-fuzzy matches\n const splitPointsByRule = collectFastFuzzySplitPoints(matchContent, pageMap, fastFuzzyRules, passesPageStartGuard);\n\n // Process combinable rules in a single pass\n if (combinableRules.length > 0) {\n const ruleRegexes = buildRuleRegexes(combinableRules);\n processCombinedMatches(\n matchContent,\n combinableRules,\n ruleRegexes,\n pageMap,\n passesPageStartGuard,\n splitPointsByRule,\n logger,\n );\n }\n\n // Process standalone rules\n for (const rule of standaloneRules) {\n const originalIndex = rules.indexOf(rule);\n processStandaloneRule(rule, originalIndex, matchContent, pageMap, passesPageStartGuard, splitPointsByRule);\n }\n\n // Apply occurrence filtering and flatten\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 // Binary search for first break >= startOffset\n let lo = 0;\n let 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 // Collect breaks until we exceed endOffset\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 * @returns Content with page-break newlines converted to spaces\n */\nconst convertPageBreaks = (content: string, startOffset: number, pageBreaks: number[]) => {\n // OPTIMIZATION: Fast-path for empty or no-newline content (common cases)\n if (!content || !content.includes('\\n')) {\n return content;\n }\n\n const endOffset = startOffset + content.length;\n const breaksInRange = findBreaksInRange(startOffset, endOffset, pageBreaks);\n\n // No page breaks in this segment - return as-is (most common case)\n if (breaksInRange.length === 0) {\n return content;\n }\n\n // Convert ONLY page-break newlines (the ones inserted during concatenation) to spaces.\n //\n // NOTE: Offsets from findBreaksInRange are string indices (code units). Using Array.from()\n // would index by Unicode code points and can desync indices if surrogate pairs appear.\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 { rules = [], breakpoints = [], prefer = 'longer', pageJoiner = 'space', logger, maxContentLength } = options;\n\n if (maxContentLength && maxContentLength < 50) {\n throw new Error(`maxContentLength must be at least 50 characters.`);\n }\n\n // Default maxPages to 0 (single page) unless maxContentLength is provided\n const maxPages = options.maxPages ?? (maxContentLength ? Number.MAX_SAFE_INTEGER : 0);\n\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 ruleCount: rules.length,\n });\n\n const processedPages = options.replace ? applyReplacements(pages, options.replace) : pages;\n const { content: matchContent, normalizedPages: normalizedContent, pageMap } = buildPageMap(processedPages);\n\n logger?.debug?.('[segmenter] content built', {\n pageIds: pageMap.pageIds,\n totalContentLength: matchContent.length,\n });\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 // Build initial segments from structural rules\n let segments = buildSegments(unique, matchContent, pageMap, rules);\n\n logger?.debug?.('[segmenter] structural segments built', {\n segmentCount: segments.length,\n segments: segments.map((s) => ({ contentLength: s.content.length, from: s.from, to: s.to })),\n });\n\n segments = ensureFallbackSegment(segments, processedPages, normalizedContent, pageJoiner);\n\n // Apply breakpoints post-processing for oversized segments\n if ((maxPages >= 0 || (maxContentLength && maxContentLength > 0)) && breakpoints.length) {\n logger?.debug?.('[segmenter] applying breakpoints to oversized segments');\n const patternProcessor = (p: string) => processPattern(p, false).pattern;\n const result = applyBreakpoints(\n segments,\n processedPages,\n normalizedContent,\n maxPages,\n breakpoints,\n prefer,\n patternProcessor,\n logger,\n pageJoiner,\n debug?.includeBreakpoint ? debug.metaKey : undefined,\n maxContentLength,\n );\n logger?.info?.('[segmenter] segmentation complete (with breakpoints)', {\n finalSegmentCount: result.length,\n });\n return result;\n }\n logger?.info?.('[segmenter] segmentation complete (structural only)', {\n finalSegmentCount: segments.length,\n });\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 = (splitPoints: SplitPoint[], content: string, pageMap: PageMap, rules: SplitRule[]) => {\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 ): Segment | null => {\n // For lineStartsAfter, skip the marker by using contentStartOffset\n const actualStart = start + (contentStartOffset ?? 0);\n // For lineStartsAfter (contentStartOffset set), trim leading whitespace after marker\n // For other rules, only trim trailing whitespace to preserve intentional leading spaces\n const sliced = content.slice(actualStart, end);\n let text = capturedContent?.trim() ?? (contentStartOffset ? sliced.trim() : sliced.replace(/[\\s\\n]+$/, ''));\n if (!text) {\n return null;\n }\n if (!capturedContent) {\n text = convertPageBreaks(text, actualStart, pageMap.pageBreaks);\n }\n const from = pageMap.getId(actualStart);\n const to = capturedContent ? pageMap.getId(end - 1) : pageMap.getId(actualStart + text.length - 1);\n const seg: Segment = { content: text, from };\n if (to !== from) {\n seg.to = to;\n }\n if (meta || namedCaptures) {\n seg.meta = { ...meta, ...namedCaptures };\n }\n return seg;\n };\n\n /**\n * Creates segments from an array of split points.\n */\n const createSegmentsFromSplitPoints = (): Segment[] => {\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","// Shared utilities for analysis functions\n\nimport { getAvailableTokens, TOKEN_PATTERNS } from '../segmentation/tokens.js';\n\n// ─────────────────────────────────────────────────────────────\n// Helpers shared across analysis modules\n// ─────────────────────────────────────────────────────────────\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 const compiled: CompiledTokenRegex[] = [];\n for (const token of tokenNames) {\n const pat = TOKEN_PATTERNS[token];\n if (!pat) {\n continue;\n }\n try {\n compiled.push({ re: new RegExp(pat, 'uy'), token });\n } catch {\n // Ignore invalid patterns\n }\n }\n return compiled;\n};\n\nexport const appendWs = (out: string, mode: 'regex' | 'space'): string => {\n if (!out) {\n return out;\n }\n if (mode === 'space') {\n return out.endsWith(' ') ? out : `${out} `;\n }\n return out.endsWith('\\\\s*') ? out : `${out}\\\\s*`;\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 { normalizeLineEndings } from '../segmentation/textUtils.js';\nimport type { Page } from '../segmentation/types.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// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Options resolution\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Specificity & sorting\n// ─────────────────────────────────────────────────────────────\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\n// ─────────────────────────────────────────────────────────────\n// Signature building helpers\n// ─────────────────────────────────────────────────────────────\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/** Consume prefix matchers at current position */\nconst consumePrefixes = (\n s: string,\n pos: number,\n out: string,\n matchers: RegExp[],\n ws: 'regex' | 'space',\n): { pos: number; out: string; matched: boolean } => {\n let matched = false;\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 matched = true;\n const wsm = /^[ \\t]+/u.exec(s.slice(pos));\n if (wsm) {\n pos += wsm[0].length;\n out = appendWs(out, ws);\n }\n }\n }\n return { matched, out, pos };\n};\n\n/** Try to match a token at current position and append to signature */\nconst tryMatchToken = (\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 if (!best) {\n return { matched: false, out, pos };\n }\n return { matched: true, out: `${out}{{${best.token}}}`, pos: pos + best.text.length };\n};\n\n/** Try to match a delimiter at current position */\nconst tryMatchDelimiter = (s: string, pos: number, out: string): { pos: number; out: string; matched: boolean } => {\n const ch = s[pos];\n if (!ch || !isCommonDelimiter(ch)) {\n return { matched: false, out, pos };\n }\n return { matched: true, out: out + escapeSignatureLiteral(ch), pos: pos + 1 };\n};\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// ─────────────────────────────────────────────────────────────\n// Main tokenization\n// ─────────────────────────────────────────────────────────────\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 = consumePrefixes(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 // Skip whitespace\n const ws = skipWhitespace(s, pos, out, opts.whitespace);\n if (ws.skipped) {\n pos = ws.pos;\n out = ws.out;\n continue;\n }\n\n // Try token\n const tok = tryMatchToken(s, pos, out, compiled);\n if (tok.matched) {\n pos = tok.pos;\n out = tok.out;\n matchedAny = matchedToken = true;\n steps++;\n continue;\n }\n\n // Try delimiter (only after matching something)\n if (matchedAny) {\n const delim = tryMatchDelimiter(s, pos, out);\n if (delim.matched) {\n pos = delim.pos;\n out = delim.out;\n continue;\n }\n }\n\n // Fallback logic\n if (matchedAny) {\n if (opts.includeFirstWordFallback && !matchedToken) {\n const word = extractFirstWord(s.slice(pos));\n if (word) {\n out += escapeSignatureLiteral(word);\n steps++;\n }\n }\n break;\n }\n\n if (!opts.includeFirstWordFallback) {\n return null;\n }\n\n const word = extractFirstWord(s.slice(pos));\n if (!word) {\n return null;\n }\n return escapeSignatureLiteral(word);\n }\n\n return matchedAny ? trimTrailingWs(out, opts.whitespace) : null;\n};\n\n// ─────────────────────────────────────────────────────────────\n// Page processing\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Main export\n// ─────────────────────────────────────────────────────────────\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 '../segmentation/types.js';\nimport {\n buildTokenPriority,\n compileTokenRegexes,\n escapeSignatureLiteral,\n findBestTokenMatchAt,\n isArabicLetter,\n isCommonDelimiter,\n stripArabicDiacritics,\n} from './shared.js';\n\n// ─────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Resolved options with defaults\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Raw position tracking for diacritic normalization\n// ─────────────────────────────────────────────────────────────\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// ─────────────────────────────────────────────────────────────\n// Token content scanner\n// ─────────────────────────────────────────────────────────────\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// N-gram pattern extraction\n// ─────────────────────────────────────────────────────────────\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\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 for (let n = opts.minElements; n <= Math.min(opts.maxElements, items.length - i); n++) {\n const window = items.slice(i, i + n);\n\n if (opts.requireToken && !hasTokenInWindow(window)) {\n continue;\n }\n\n const pattern = buildPattern(window, opts.whitespace);\n\n if (!stats.has(pattern)) {\n if (stats.size >= opts.maxUniquePatterns) {\n continue;\n }\n stats.set(pattern, { count: 0, examples: [], ...computeWindowStats(window) });\n }\n\n const entry = stats.get(pattern)!;\n entry.count++;\n\n if (entry.examples.length < opts.maxExamples) {\n entry.examples.push(buildExample(page, window, opts.contextChars));\n }\n }\n }\n};\n\n// ─────────────────────────────────────────────────────────────\n// Main export\n// ─────────────────────────────────────────────────────────────\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","/**\n * Pattern detection utilities for recognizing template tokens in Arabic text.\n * Used to auto-detect patterns from user-highlighted text in the segmentation dialog.\n *\n * @module pattern-detection\n */\n\nimport { 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 { applyReplacements } from './segmentation/replace.js';\nimport { buildRuleRegex } from './segmentation/rule-regex.js';\nimport { segmentPages } from './segmentation/segmenter.js';\nimport { normalizeLineEndings } from './segmentation/textUtils.js';\nimport type { Page, Segment, SegmentationOptions, SplitRule } from './segmentation/types.js';\n\nexport type MarkerRecoverySelector =\n | { type: 'rule_indices'; indices: number[] }\n | { type: 'lineStartsAfter_patterns'; match?: 'exact' | 'normalized'; patterns: string[] }\n | { type: 'predicate'; predicate: (rule: SplitRule, index: number) => boolean };\n\nexport type MarkerRecoveryRun = {\n options: SegmentationOptions;\n pages: Page[];\n segments: Segment[];\n selector: MarkerRecoverySelector;\n};\n\nexport type MarkerRecoveryReport = {\n summary: {\n mode: 'rerun_only' | 'best_effort_then_rerun';\n recovered: number;\n totalSegments: number;\n unchanged: number;\n unresolved: number;\n };\n byRun?: Array<{\n recovered: number;\n runIndex: number;\n totalSegments: number;\n unresolved: number;\n }>;\n details: Array<{\n from: number;\n originalStartPreview: string;\n recoveredPrefixPreview?: string;\n recoveredStartPreview?: string;\n segmentIndex: number;\n status: 'recovered' | 'skipped_idempotent' | 'unchanged' | 'unresolved_alignment' | 'unresolved_selector';\n strategy: 'rerun' | 'stage1' | 'none';\n to?: number;\n notes?: string[];\n }>;\n errors: string[];\n warnings: string[];\n};\n\ntype NormalizeCompareMode = 'none' | 'whitespace' | 'whitespace_and_nfkc';\n\nconst preview = (s: string, max = 40): string => (s.length <= max ? s : `${s.slice(0, max)}…`);\n\nconst normalizeForCompare = (s: string, mode: NormalizeCompareMode): string => {\n if (mode === 'none') {\n return s;\n }\n let out = s;\n if (mode === 'whitespace_and_nfkc') {\n // Use alternation (not a character class) to satisfy Biome's noMisleadingCharacterClass rule.\n out = out.normalize('NFKC').replace(/(?:\\u200C|\\u200D|\\uFEFF)/gu, '');\n }\n // Collapse whitespace and normalize line endings\n out = out.replace(/\\r\\n?/gu, '\\n').replace(/\\s+/gu, ' ').trim();\n return out;\n};\n\nconst segmentRangeKey = (s: Pick<Segment, 'from' | 'to'>): string => `${s.from}|${s.to ?? s.from}`;\n\nconst buildFixedOptions = (options: SegmentationOptions, selectedRuleIndices: Set<number>): SegmentationOptions => {\n const rules = options.rules ?? [];\n const fixedRules: SplitRule[] = rules.map((r, idx) => {\n if (!selectedRuleIndices.has(idx)) {\n return r;\n }\n if (!('lineStartsAfter' in r) || !r.lineStartsAfter) {\n return r;\n }\n\n // Convert: lineStartsAfter -> lineStartsWith, keep all other fields.\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { lineStartsAfter, ...rest } = r as SplitRule & { lineStartsAfter: string[] };\n return { ...(rest as Omit<SplitRule, 'lineStartsAfter'>), lineStartsWith: lineStartsAfter };\n });\n\n return { ...options, rules: fixedRules };\n};\n\nconst buildPageIdToIndex = (pages: Page[]): Map<number, number> => new Map(pages.map((p, i) => [p.id, i]));\n\ntype RangeContent = {\n matchContent: string;\n outputContent: string;\n};\n\nconst buildRangeContent = (\n processedPages: Page[],\n fromIdx: number,\n toIdx: number,\n pageJoiner: 'space' | 'newline',\n): RangeContent => {\n const parts: string[] = [];\n for (let i = fromIdx; i <= toIdx; i++) {\n parts.push(normalizeLineEndings(processedPages[i].content));\n }\n const matchContent = parts.join('\\n');\n if (pageJoiner === 'newline') {\n return { matchContent, outputContent: matchContent };\n }\n // Only convert the inserted page-boundary separators (exactly those join '\\n's) to spaces.\n // In-page newlines remain as-is.\n // Since we built matchContent by joining pages with '\\n', the separators are exactly between each part.\n // Replacing all '\\n' would corrupt in-page newlines, so we rebuild outputContent explicitly.\n const outputContent = parts.join(' ');\n return { matchContent, outputContent };\n};\n\ntype CompiledMistakenRule = {\n ruleIndex: number;\n // A regex that matches the marker at a line start (equivalent to lineStartsWith).\n startsWithRegex: RegExp;\n};\n\nconst compileMistakenRulesAsStartsWith = (\n options: SegmentationOptions,\n selectedRuleIndices: Set<number>,\n): CompiledMistakenRule[] => {\n const rules = options.rules ?? [];\n const compiled: CompiledMistakenRule[] = [];\n\n for (const idx of selectedRuleIndices) {\n const r = rules[idx];\n if (!r || !('lineStartsAfter' in r) || !r.lineStartsAfter?.length) {\n continue;\n }\n // Convert cleanly without using `delete` (keeps TS happy with discriminated unions).\n const { lineStartsAfter, ...rest } = r as SplitRule & { lineStartsAfter: string[] };\n const converted: SplitRule = {\n ...(rest as Omit<SplitRule, 'lineStartsAfter'>),\n lineStartsWith: lineStartsAfter,\n };\n\n const built = buildRuleRegex(converted);\n // built.regex has flags gmu; we want a stable, non-global matcher.\n compiled.push({ ruleIndex: idx, startsWithRegex: new RegExp(built.regex.source, 'mu') });\n }\n\n return compiled;\n};\n\ntype Stage1Result =\n | { kind: 'recovered'; recoveredContent: string; recoveredPrefix: string }\n | { kind: 'skipped_idempotent' }\n | { kind: 'unresolved'; reason: string };\n\nconst findUniqueAnchorPos = (outputContent: string, segmentContent: string): number | null => {\n const prefixLens = [80, 60, 40, 30, 20, 15] as const;\n\n for (const len of prefixLens) {\n const needle = segmentContent.slice(0, Math.min(len, segmentContent.length));\n if (!needle.trim()) {\n continue;\n }\n\n const first = outputContent.indexOf(needle);\n if (first === -1) {\n continue;\n }\n const second = outputContent.indexOf(needle, first + 1);\n if (second === -1) {\n return first;\n }\n }\n\n return null;\n};\n\nconst findRecoveredPrefixAtLineStart = (\n segmentContent: string,\n matchContent: string,\n lineStart: number,\n anchorPos: number,\n compiledMistaken: CompiledMistakenRule[],\n): { prefix: string } | { reason: string } => {\n const line = matchContent.slice(lineStart);\n\n for (const mr of compiledMistaken) {\n mr.startsWithRegex.lastIndex = 0;\n const m = mr.startsWithRegex.exec(line);\n if (!m || m.index !== 0) {\n continue;\n }\n\n const markerMatch = m[0];\n const markerEnd = lineStart + markerMatch.length;\n if (anchorPos < markerEnd) {\n continue; // anchor is inside marker; unsafe\n }\n\n // If there is whitespace between the marker match and the anchored content (common when lineStartsAfter trims),\n // include it in the recovered prefix.\n const gap = matchContent.slice(markerEnd, anchorPos);\n const recoveredPrefix = /^\\s*$/u.test(gap) ? `${markerMatch}${gap}` : markerMatch;\n\n // Idempotency: if content already starts with the marker/prefix, don’t prepend.\n if (segmentContent.startsWith(markerMatch) || segmentContent.startsWith(recoveredPrefix)) {\n return { reason: 'content already starts with selected marker' };\n }\n\n return { prefix: recoveredPrefix };\n }\n\n return { reason: 'no selected marker pattern matched at anchored line start' };\n};\n\nconst tryBestEffortRecoverOneSegment = (\n segment: Segment,\n processedPages: Page[],\n pageIdToIndex: Map<number, number>,\n compiledMistaken: CompiledMistakenRule[],\n pageJoiner: 'space' | 'newline',\n): Stage1Result => {\n const fromIdx = pageIdToIndex.get(segment.from);\n const toIdx = pageIdToIndex.get(segment.to ?? segment.from) ?? fromIdx;\n if (fromIdx === undefined || toIdx === undefined || fromIdx < 0 || toIdx < fromIdx) {\n return { kind: 'unresolved', reason: 'segment page range not found in pages' };\n }\n\n const { matchContent, outputContent } = buildRangeContent(processedPages, fromIdx, toIdx, pageJoiner);\n if (!segment.content) {\n return { kind: 'unresolved', reason: 'empty segment content' };\n }\n\n const anchorPos = findUniqueAnchorPos(outputContent, segment.content);\n if (anchorPos === null) {\n return { kind: 'unresolved', reason: 'could not uniquely anchor segment content in page range' };\n }\n\n // Find line start in matchContent. (Positions align because outputContent differs only by page-boundary joiner.)\n const lineStart = matchContent.lastIndexOf('\\n', Math.max(0, anchorPos - 1)) + 1;\n const found = findRecoveredPrefixAtLineStart(segment.content, matchContent, lineStart, anchorPos, compiledMistaken);\n if ('reason' in found) {\n return found.reason.includes('already starts')\n ? { kind: 'skipped_idempotent' }\n : { kind: 'unresolved', reason: found.reason };\n }\n return { kind: 'recovered', recoveredContent: `${found.prefix}${segment.content}`, recoveredPrefix: found.prefix };\n};\n\nconst resolveRuleIndicesSelector = (rules: SplitRule[], indicesIn: number[]) => {\n const errors: string[] = [];\n const indices = new Set<number>();\n for (const idx of indicesIn) {\n if (!Number.isInteger(idx) || idx < 0 || idx >= rules.length) {\n errors.push(`Selector index out of range: ${idx}`);\n continue;\n }\n const rule = rules[idx];\n if (!rule || !('lineStartsAfter' in rule)) {\n errors.push(`Selector index ${idx} is not a lineStartsAfter rule`);\n continue;\n }\n indices.add(idx);\n }\n return { errors, indices, warnings: [] as string[] };\n};\n\nconst resolvePredicateSelector = (rules: SplitRule[], predicate: (rule: SplitRule, index: number) => boolean) => {\n const errors: string[] = [];\n const warnings: string[] = [];\n const indices = new Set<number>();\n\n rules.forEach((r, i) => {\n try {\n if (!predicate(r, i)) {\n return;\n }\n if ('lineStartsAfter' in r && r.lineStartsAfter?.length) {\n indices.add(i);\n return;\n }\n warnings.push(`Predicate selected rule ${i}, but it is not a lineStartsAfter rule; skipping`);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n errors.push(`Predicate threw at rule ${i}: ${msg}`);\n }\n });\n\n if (indices.size === 0) {\n warnings.push('Predicate did not select any lineStartsAfter rules');\n }\n\n return { errors, indices, warnings };\n};\n\nconst resolvePatternsSelector = (\n rules: SplitRule[],\n patterns: string[],\n matchMode: 'exact' | 'normalized' | undefined,\n): { errors: string[]; indices: Set<number>; warnings: string[] } => {\n const errors: string[] = [];\n const warnings: string[] = [];\n const indices = new Set<number>();\n\n const normalizePattern = (p: string) =>\n normalizeForCompare(p, (matchMode ?? 'exact') === 'normalized' ? 'whitespace_and_nfkc' : 'none');\n const targets = patterns.map(normalizePattern);\n\n for (let pi = 0; pi < patterns.length; pi++) {\n const rawPattern = patterns[pi];\n const pat = targets[pi];\n const matched: number[] = [];\n\n for (let i = 0; i < rules.length; i++) {\n const r = rules[i];\n if (!('lineStartsAfter' in r) || !r.lineStartsAfter?.length) {\n continue;\n }\n if (r.lineStartsAfter.some((rp) => normalizePattern(rp) === pat)) {\n matched.push(i);\n }\n }\n\n if (matched.length === 0) {\n errors.push(`Pattern \"${rawPattern}\" did not match any lineStartsAfter rule`);\n continue;\n }\n if (matched.length > 1) {\n warnings.push(`Pattern \"${rawPattern}\" matched multiple lineStartsAfter rules: [${matched.join(', ')}]`);\n }\n matched.forEach((i) => {\n indices.add(i);\n });\n }\n\n return { errors, indices, warnings };\n};\n\nconst resolveSelectorToRuleIndices = (options: SegmentationOptions, selector: MarkerRecoverySelector) => {\n const rules = options.rules ?? [];\n if (selector.type === 'rule_indices') {\n return resolveRuleIndicesSelector(rules, selector.indices);\n }\n if (selector.type === 'predicate') {\n return resolvePredicateSelector(rules, selector.predicate);\n }\n return resolvePatternsSelector(rules, selector.patterns, selector.match);\n};\n\ntype AlignmentCandidate = { fixedIndex: number; kind: 'exact' | 'exact_suffix' | 'normalized_suffix'; score: number };\n\nconst longestCommonSuffixLength = (a: string, b: string): number => {\n const max = Math.min(a.length, b.length);\n let i = 0;\n while (i < max) {\n if (a[a.length - 1 - i] !== b[b.length - 1 - i]) {\n break;\n }\n i++;\n }\n return i;\n};\n\n// Minimum score difference required to confidently disambiguate between candidates.\n// If the gap between best and second-best is smaller, we consider the match ambiguous.\nconst AMBIGUITY_SCORE_GAP = 5;\n\nconst scoreCandidate = (\n orig: Segment,\n fixed: Segment,\n normalizeMode: NormalizeCompareMode,\n): AlignmentCandidate | null => {\n // Scoring hierarchy:\n // exact (100) - Content is identical, no recovery needed\n // exact_suffix (90-120) - Fixed ends with original; fixed = marker + orig (most reliable)\n // normalized_suffix (70-90) - Suffix match after whitespace/NFKC normalization\n // Higher scores indicate more confident alignment.\n\n if (fixed.content === orig.content) {\n return { fixedIndex: -1, kind: 'exact', score: 100 };\n }\n\n if (fixed.content.endsWith(orig.content)) {\n // Most reliable case: fixed = marker + orig.\n // Bonus points for longer markers (up to 30) to prefer substantive recovery.\n const markerLen = fixed.content.length - orig.content.length;\n const bonus = Math.min(30, markerLen);\n return { fixedIndex: -1, kind: 'exact_suffix', score: 90 + bonus };\n }\n\n if (normalizeMode !== 'none') {\n const normFixed = normalizeForCompare(fixed.content, normalizeMode);\n const normOrig = normalizeForCompare(orig.content, normalizeMode);\n if (normFixed.endsWith(normOrig) && normOrig.length > 0) {\n // Base score 70, plus up to 20 bonus based on overlap ratio\n const overlap = longestCommonSuffixLength(normFixed, normOrig) / normOrig.length;\n return { fixedIndex: -1, kind: 'normalized_suffix', score: 70 + Math.floor(overlap * 20) };\n }\n }\n\n return null;\n};\n\nconst buildNoSelectionResult = (\n segments: Segment[],\n reportBase: Omit<MarkerRecoveryReport, 'details' | 'summary'>,\n mode: MarkerRecoveryReport['summary']['mode'],\n selectorErrors: string[],\n): { report: MarkerRecoveryReport; segments: Segment[] } => {\n const warnings = [...reportBase.warnings];\n warnings.push('No lineStartsAfter rules selected for recovery; returning segments unchanged');\n\n const details: MarkerRecoveryReport['details'] = segments.map((s, i) => {\n const status: MarkerRecoveryReport['details'][number]['status'] = selectorErrors.length\n ? 'unresolved_selector'\n : 'unchanged';\n return {\n from: s.from,\n notes: selectorErrors.length ? (['selector did not resolve'] as string[]) : undefined,\n originalStartPreview: preview(s.content),\n segmentIndex: i,\n status,\n strategy: 'none',\n to: s.to,\n };\n });\n\n return {\n report: {\n ...reportBase,\n details,\n summary: {\n mode,\n recovered: 0,\n totalSegments: segments.length,\n unchanged: segments.length,\n unresolved: selectorErrors.length ? segments.length : 0,\n },\n warnings,\n },\n segments,\n };\n};\n\nconst runStage1IfEnabled = (\n pages: Page[],\n segments: Segment[],\n options: SegmentationOptions,\n selectedRuleIndices: Set<number>,\n mode: MarkerRecoveryReport['summary']['mode'],\n): {\n recoveredAtIndex: Map<number, Segment>;\n recoveredDetailAtIndex: Map<number, MarkerRecoveryReport['details'][number]>;\n} => {\n const recoveredAtIndex = new Map<number, Segment>();\n const recoveredDetailAtIndex = new Map<number, MarkerRecoveryReport['details'][number]>();\n\n if (mode !== 'best_effort_then_rerun') {\n return { recoveredAtIndex, recoveredDetailAtIndex };\n }\n\n const processedPages = options.replace ? applyReplacements(pages, options.replace) : pages;\n const pageIdToIndex = buildPageIdToIndex(processedPages);\n const pageJoiner = options.pageJoiner ?? 'space';\n const compiledMistaken = compileMistakenRulesAsStartsWith(options, selectedRuleIndices);\n\n for (let i = 0; i < segments.length; i++) {\n const orig = segments[i];\n const r = tryBestEffortRecoverOneSegment(orig, processedPages, pageIdToIndex, compiledMistaken, pageJoiner);\n if (r.kind !== 'recovered') {\n continue;\n }\n\n const seg: Segment = { ...orig, content: r.recoveredContent };\n recoveredAtIndex.set(i, seg);\n recoveredDetailAtIndex.set(i, {\n from: orig.from,\n originalStartPreview: preview(orig.content),\n recoveredPrefixPreview: preview(r.recoveredPrefix),\n recoveredStartPreview: preview(seg.content),\n segmentIndex: i,\n status: 'recovered',\n strategy: 'stage1',\n to: orig.to,\n });\n }\n\n return { recoveredAtIndex, recoveredDetailAtIndex };\n};\n\nconst buildFixedBuckets = (fixedSegments: Segment[]): Map<string, number[]> => {\n const buckets = new Map<string, number[]>();\n for (let i = 0; i < fixedSegments.length; i++) {\n const k = segmentRangeKey(fixedSegments[i]);\n const arr = buckets.get(k);\n if (!arr) {\n buckets.set(k, [i]);\n } else {\n arr.push(i);\n }\n }\n return buckets;\n};\n\ntype BestFixedMatch = { kind: 'none' } | { kind: 'ambiguous' } | { kind: 'match'; fixedIdx: number };\n\nconst findBestFixedMatch = (\n orig: Segment,\n candidates: number[],\n fixedSegments: Segment[],\n usedFixed: Set<number>,\n normalizeCompare: NormalizeCompareMode,\n): BestFixedMatch => {\n let best: { fixedIdx: number; score: number } | null = null;\n let secondBestScore = -Infinity;\n\n for (const fixedIdx of candidates) {\n if (usedFixed.has(fixedIdx)) {\n continue;\n }\n const fixed = fixedSegments[fixedIdx];\n const scored = scoreCandidate(orig, fixed, normalizeCompare);\n if (!scored) {\n continue;\n }\n const candidateScore = scored.score;\n if (!best || candidateScore > best.score) {\n secondBestScore = best?.score ?? -Infinity;\n best = { fixedIdx, score: candidateScore };\n } else if (candidateScore > secondBestScore) {\n secondBestScore = candidateScore;\n }\n }\n\n if (!best) {\n return { kind: 'none' };\n }\n if (best.score - secondBestScore < AMBIGUITY_SCORE_GAP && candidates.length > 1) {\n return { kind: 'ambiguous' };\n }\n return { fixedIdx: best.fixedIdx, kind: 'match' };\n};\n\nconst detailUnresolved = (\n orig: Segment,\n segmentIndex: number,\n notes: string[],\n): MarkerRecoveryReport['details'][number] => ({\n from: orig.from,\n notes,\n originalStartPreview: preview(orig.content),\n segmentIndex,\n status: 'unresolved_alignment',\n strategy: 'rerun',\n to: orig.to,\n});\n\nconst detailSkippedIdempotent = (\n orig: Segment,\n segmentIndex: number,\n notes: string[],\n): MarkerRecoveryReport['details'][number] => ({\n from: orig.from,\n notes,\n originalStartPreview: preview(orig.content),\n segmentIndex,\n status: 'skipped_idempotent',\n strategy: 'rerun',\n to: orig.to,\n});\n\nconst detailRecoveredRerun = (\n orig: Segment,\n fixed: Segment,\n segmentIndex: number,\n): MarkerRecoveryReport['details'][number] => {\n let recoveredPrefixPreview: string | undefined;\n if (fixed.content.endsWith(orig.content)) {\n recoveredPrefixPreview = preview(fixed.content.slice(0, fixed.content.length - orig.content.length));\n }\n return {\n from: orig.from,\n originalStartPreview: preview(orig.content),\n recoveredPrefixPreview,\n recoveredStartPreview: preview(fixed.content),\n segmentIndex,\n status: 'recovered',\n strategy: 'rerun',\n to: orig.to,\n };\n};\n\nconst mergeWithRerun = (params: {\n fixedBuckets: Map<string, number[]>;\n fixedSegments: Segment[];\n normalizeCompare: NormalizeCompareMode;\n originalSegments: Segment[];\n recoveredDetailAtIndex: Map<number, MarkerRecoveryReport['details'][number]>;\n stage1RecoveredAtIndex: Map<number, Segment>;\n}): {\n details: MarkerRecoveryReport['details'];\n segments: Segment[];\n summary: Omit<MarkerRecoveryReport['summary'], 'mode' | 'totalSegments'>;\n} => {\n const {\n fixedBuckets,\n fixedSegments,\n normalizeCompare,\n originalSegments,\n stage1RecoveredAtIndex,\n recoveredDetailAtIndex,\n } = params;\n\n const usedFixed = new Set<number>();\n const out: Segment[] = [];\n const details: MarkerRecoveryReport['details'] = [];\n let recovered = 0;\n let unresolved = 0;\n let unchanged = 0;\n\n for (let i = 0; i < originalSegments.length; i++) {\n const stage1Recovered = stage1RecoveredAtIndex.get(i);\n if (stage1Recovered) {\n out.push(stage1Recovered);\n recovered++;\n details.push(\n recoveredDetailAtIndex.get(i) ?? {\n from: stage1Recovered.from,\n originalStartPreview: preview(originalSegments[i].content),\n recoveredStartPreview: preview(stage1Recovered.content),\n segmentIndex: i,\n status: 'recovered',\n strategy: 'stage1',\n to: stage1Recovered.to,\n },\n );\n continue;\n }\n\n const orig = originalSegments[i];\n const candidates = fixedBuckets.get(segmentRangeKey(orig)) ?? [];\n\n const best = findBestFixedMatch(orig, candidates, fixedSegments, usedFixed, normalizeCompare);\n if (best.kind === 'none') {\n out.push(orig);\n unresolved++;\n details.push(detailUnresolved(orig, i, ['no alignment candidate in rerun output for same (from,to)']));\n continue;\n }\n if (best.kind === 'ambiguous') {\n out.push(orig);\n unresolved++;\n details.push(detailUnresolved(orig, i, ['ambiguous alignment (score gap too small)']));\n continue;\n }\n\n usedFixed.add(best.fixedIdx);\n const fixed = fixedSegments[best.fixedIdx];\n\n if (fixed.content === orig.content) {\n out.push(orig);\n unchanged++;\n details.push(detailSkippedIdempotent(orig, i, ['content already matches rerun output']));\n continue;\n }\n\n out.push({ ...orig, content: fixed.content });\n recovered++;\n details.push(detailRecoveredRerun(orig, fixed, i));\n }\n\n return { details, segments: out, summary: { recovered, unchanged, unresolved } };\n};\n\nexport function recoverMistakenLineStartsAfterMarkers(\n pages: Page[],\n segments: Segment[],\n options: SegmentationOptions,\n selector: MarkerRecoverySelector,\n opts?: {\n mode?: 'rerun_only' | 'best_effort_then_rerun';\n normalizeCompare?: NormalizeCompareMode;\n },\n): { report: MarkerRecoveryReport; segments: Segment[] } {\n const mode = opts?.mode ?? 'rerun_only';\n const normalizeCompare = opts?.normalizeCompare ?? 'whitespace';\n\n const resolved = resolveSelectorToRuleIndices(options, selector);\n const reportBase: Omit<MarkerRecoveryReport, 'details' | 'summary'> = {\n byRun: undefined,\n errors: resolved.errors,\n warnings: resolved.warnings,\n };\n\n if (resolved.indices.size === 0) {\n return buildNoSelectionResult(segments, reportBase, mode, resolved.errors);\n }\n\n const stage1 = runStage1IfEnabled(pages, segments, options, resolved.indices, mode);\n\n const fixedOptions = buildFixedOptions(options, resolved.indices);\n const fixedSegments = segmentPages(pages, fixedOptions);\n const fixedBuckets = buildFixedBuckets(fixedSegments);\n const merged = mergeWithRerun({\n fixedBuckets,\n fixedSegments,\n normalizeCompare,\n originalSegments: segments,\n recoveredDetailAtIndex: stage1.recoveredDetailAtIndex,\n stage1RecoveredAtIndex: stage1.recoveredAtIndex,\n });\n\n return {\n report: {\n ...reportBase,\n details: merged.details,\n summary: {\n mode,\n recovered: merged.summary.recovered,\n totalSegments: segments.length,\n unchanged: merged.summary.unchanged,\n unresolved: merged.summary.unresolved,\n },\n },\n segments: merged.segments,\n };\n}\n\nexport function recoverMistakenMarkersForRuns(\n runs: MarkerRecoveryRun[],\n opts?: { mode?: 'rerun_only' | 'best_effort_then_rerun'; normalizeCompare?: NormalizeCompareMode },\n): { report: MarkerRecoveryReport; segments: Segment[] } {\n const allSegments: Segment[] = [];\n const byRun: NonNullable<MarkerRecoveryReport['byRun']> = [];\n const details: MarkerRecoveryReport['details'] = [];\n const warnings: string[] = [];\n const errors: string[] = [];\n\n let recovered = 0;\n let unchanged = 0;\n let unresolved = 0;\n let offset = 0;\n\n for (let i = 0; i < runs.length; i++) {\n const run = runs[i];\n const res = recoverMistakenLineStartsAfterMarkers(run.pages, run.segments, run.options, run.selector, opts);\n allSegments.push(...res.segments);\n\n // Adjust indices in details to be global\n for (const d of res.report.details) {\n details.push({ ...d, segmentIndex: d.segmentIndex + offset });\n }\n offset += run.segments.length;\n\n recovered += res.report.summary.recovered;\n unchanged += res.report.summary.unchanged;\n unresolved += res.report.summary.unresolved;\n\n warnings.push(...res.report.warnings);\n errors.push(...res.report.errors);\n\n byRun.push({\n recovered: res.report.summary.recovered,\n runIndex: i,\n totalSegments: run.segments.length,\n unresolved: res.report.summary.unresolved,\n });\n }\n\n const report: MarkerRecoveryReport = {\n byRun,\n details,\n errors,\n summary: {\n mode: opts?.mode ?? 'rerun_only',\n recovered,\n totalSegments: offset,\n unchanged,\n unresolved,\n },\n warnings,\n };\n\n return { report, segments: allSegments };\n}\n"],"mappings":"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"}
|