{"version":3,"file":"segmentDocument.mjs","names":[],"sources":["../../../src/docReview/segmentDocument.ts"],"sourcesContent":["import type { Block, BlockType } from './types';\n\nconst HEADING_PATTERN = /^\\s*(#{1,6})\\s+/;\n\nconst isBlankLine = (line: string): boolean => line.trim().length === 0;\nconst isFencedCodeDelimiter = (line: string): boolean => /^\\s*```/.test(line);\n\n/**\n * Read the depth of an ATX markdown heading (`#` → 1, `######` → 6).\n *\n * @param line - The line to inspect.\n * @returns The heading depth, or `null` when the line is not a heading.\n */\nconst parseHeadingDepth = (line: string): number | null => {\n  const match = HEADING_PATTERN.exec(line);\n\n  return match?.[1]?.length ?? null;\n};\n\nconst isHeading = (line: string): boolean => parseHeadingDepth(line) !== null;\nconst isFrontmatterDelimiter = (line: string): boolean =>\n  /^\\s*---\\s*$/.test(line);\n\n/**\n * A content unit (heading, paragraph, code block or frontmatter) spanning a\n * 0-based, inclusive line range. Blank-line runs are not units of their own;\n * they are folded into the preceding unit as trailing separators (see\n * {@link segmentDocument}) so that the concatenation of every block's content\n * reproduces the document byte-for-byte.\n */\ntype ContentUnit = {\n  type: BlockType;\n  /** Depth of the ATX heading opening the unit, `null` when it is not a heading. */\n  headingDepth: number | null;\n  startIndex: number;\n  endIndex: number;\n};\n\n/**\n * Split a markdown document into fine-grained blocks.\n *\n * Boundaries are drawn at headings, blank lines (paragraph breaks) and fenced\n * code blocks, while frontmatter and the inside of code fences are kept intact.\n * Each blank-line run is appended to the block that precedes it, so the blocks\n * form an exact partition of the document: concatenating every `content` (in\n * order) yields the original text unchanged. This is what lets the block-aware\n * review re-translate only the paragraphs/snippets that actually changed instead\n * of the whole heading section.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of blocks with their 1-based line ranges.\n */\nexport const segmentDocument = (text: string): Block[] => {\n  const lines = text.split('\\n');\n  const lineCount = lines.length;\n\n  // 1. Tokenize into content units, skipping blank-line runs (folded in below).\n  const units: ContentUnit[] = [];\n  let index = 0;\n\n  while (index < lineCount) {\n    const currentLine = lines[index];\n\n    if (isBlankLine(currentLine)) {\n      index += 1;\n      continue;\n    }\n\n    // Frontmatter: only when it opens the document.\n    if (units.length === 0 && isFrontmatterDelimiter(currentLine)) {\n      const startIndex = index;\n      index += 1;\n      while (index < lineCount && !isFrontmatterDelimiter(lines[index])) {\n        index += 1;\n      }\n      // Include the closing delimiter when present.\n      if (index < lineCount) index += 1;\n      units.push({\n        type: 'unknown',\n        headingDepth: null,\n        startIndex,\n        endIndex: index - 1,\n      });\n      continue;\n    }\n\n    // Fenced code block: consumed whole so inner blank lines and `#` lines are\n    // never treated as boundaries.\n    if (isFencedCodeDelimiter(currentLine)) {\n      const startIndex = index;\n      index += 1;\n      while (index < lineCount && !isFencedCodeDelimiter(lines[index])) {\n        index += 1;\n      }\n      // Include the closing fence when present.\n      if (index < lineCount) index += 1;\n      units.push({\n        type: 'code_block',\n        headingDepth: null,\n        startIndex,\n        endIndex: index - 1,\n      });\n      continue;\n    }\n\n    // Heading: a single self-contained line.\n    const headingDepth = parseHeadingDepth(currentLine);\n\n    if (headingDepth !== null) {\n      units.push({\n        type: 'heading',\n        headingDepth,\n        startIndex: index,\n        endIndex: index,\n      });\n      index += 1;\n      continue;\n    }\n\n    // Paragraph: a run of consecutive lines until a blank line, a heading or a\n    // code fence. Tables and tight lists stay together (no blank line between\n    // their rows/items).\n    const startIndex = index;\n    while (\n      index < lineCount &&\n      !isBlankLine(lines[index]) &&\n      !isHeading(lines[index]) &&\n      !isFencedCodeDelimiter(lines[index])\n    ) {\n      index += 1;\n    }\n    units.push({\n      type: 'paragraph',\n      headingDepth: null,\n      startIndex,\n      endIndex: index - 1,\n    });\n  }\n\n  if (units.length === 0) return [];\n\n  // 2. Turn each unit into a block whose line range extends to just before the\n  //    next unit, so the trailing blank-line run is owned by it. The first block\n  //    also absorbs any leading blank lines, and the last block runs to EOF.\n  return units.map((unit, unitIndex): Block => {\n    const blockStartIndex = unitIndex === 0 ? 0 : unit.startIndex;\n    const blockEndIndex =\n      unitIndex === units.length - 1\n        ? lineCount - 1\n        : units[unitIndex + 1].startIndex - 1;\n\n    const blockLines = lines.slice(blockStartIndex, blockEndIndex + 1);\n    // Re-append the boundary newline dropped by `split` for every block but the\n    // one ending at EOF, so concatenating all blocks rebuilds the document.\n    const content =\n      blockEndIndex < lineCount - 1\n        ? `${blockLines.join('\\n')}\\n`\n        : blockLines.join('\\n');\n\n    return {\n      type: unit.type,\n      content,\n      headingDepth: unit.headingDepth,\n      lineStart: blockStartIndex + 1,\n      lineEnd: blockEndIndex + 1,\n    };\n  });\n};\n\n/**\n * Split a markdown document into coarse, heading-anchored sections.\n *\n * Built by grouping the fine blocks of {@link segmentDocument}: frontmatter and\n * each heading open a new section, and the following non-heading blocks are\n * folded into it. Because it only concatenates adjacent fine blocks, the result\n * is still an exact partition of the document (sections concatenate back to the\n * source unchanged).\n *\n * Sections are the robust alignment unit between a base document and its\n * translation — both share the same heading structure, so they align almost\n * perfectly and a translation that splits its prose into a different number of\n * paragraphs never causes a section to be dropped. Fine-grained review happens\n * within a section once it is known to have changed.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of sections with their 1-based line ranges.\n */\nexport const segmentSections = (text: string): Block[] => {\n  const fineBlocks = segmentDocument(text);\n  const sections: Block[] = [];\n  let currentBlocks: Block[] = [];\n\n  const flushSection = (): void => {\n    if (currentBlocks.length === 0) return;\n\n    const [firstBlock] = currentBlocks;\n    const lastBlock = currentBlocks[currentBlocks.length - 1];\n\n    sections.push({\n      type: firstBlock.type,\n      content: currentBlocks.map((block) => block.content).join(''),\n      // A section is identified by the heading that opens it, so it inherits its\n      // depth — this is what keeps a `##` section from aligning with a `###` one.\n      headingDepth: firstBlock.headingDepth,\n      lineStart: firstBlock.lineStart,\n      lineEnd: lastBlock.lineEnd,\n    });\n    currentBlocks = [];\n  };\n\n  for (const block of fineBlocks) {\n    // Frontmatter (a leading `unknown` block) and every heading open a section.\n    const opensSection =\n      block.type === 'heading' ||\n      (block.type === 'unknown' && sections.length === 0);\n\n    if (opensSection) flushSection();\n    currentBlocks.push(block);\n    if (block.type === 'unknown' && sections.length === 0) flushSection();\n  }\n\n  flushSection();\n\n  return sections;\n};\n"],"mappings":"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"}