= $maxChars / 2) { throw new \InvalidArgumentException('Invalid chunk sizing.'); } } /** @return list */ public function chunk(Document $doc): array { $chunks = []; foreach ($doc->sections as $section) { foreach ($this->pack($this->pieces($section['text'])) as $text) { $chunks[] = ['seq' => count($chunks) + 1, 'anchor' => $section['anchor'], 'heading' => $section['heading'], 'text' => $text]; if (count($chunks) > self::MAX_CHUNKS_PER_SOURCE) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception messages go to logs and callers, never echoed; public errors are fixed codes throw new ExtractionException('too_many_chunks', 'The content produces more than ' . self::MAX_CHUNKS_PER_SOURCE . ' chunks. Split it into smaller sources.'); } } } return $chunks; } /** @return list paragraph- or sentence-sized pieces, each ≤ maxChars */ private function pieces(string $text): array { $out = []; foreach (preg_split('/\n\s*\n/u', $text) ?: [] as $para) { $para = trim($para); if ($para === '') { continue; } if (mb_strlen($para) <= $this->maxChars) { $out[] = $para; continue; } foreach (preg_split('/(?<=[.!?。])\s+/u', $para) ?: [$para] as $sentence) { while (mb_strlen($sentence) > $this->maxChars) { $cut = mb_strrpos(mb_substr($sentence, 0, $this->maxChars), ' ') ?: $this->maxChars; $out[] = trim(mb_substr($sentence, 0, $cut)); $sentence = trim(mb_substr($sentence, $cut)); } if ($sentence !== '') { $out[] = $sentence; } } } return $out; } /** @param list $pieces @return list */ private function pack(array $pieces): array { $chunks = []; $current = ''; foreach ($pieces as $piece) { $candidate = $current === '' ? $piece : $current . "\n\n" . $piece; if (mb_strlen($candidate) <= $this->maxChars) { $current = $candidate; continue; } $chunks[] = $current; $tail = $this->overlapChars > 0 ? $this->tail($current) : ''; $current = $tail !== '' && mb_strlen($tail . ' ' . $piece) <= $this->maxChars ? $tail . ' ' . $piece : $piece; } if ($current !== '') { $chunks[] = $current; } return $chunks; } /** Last ~overlapChars of a chunk, starting at a word boundary. */ private function tail(string $text): string { if (mb_strlen($text) <= $this->overlapChars) { return ''; } $tail = mb_substr($text, -$this->overlapChars); $space = mb_strpos($tail, ' '); return $space === false ? '' : '…' . trim(mb_substr($tail, $space)); } }