*/ public static function wrap( string $text, int $soft_limit ): array { if ( $soft_limit <= 0 ) { return [ str_replace( [ self::START_MARKER, self::END_MARKER ], '', $text ) ]; } [ $clean_text, $protected_ranges ] = self::extract_protected_ranges( $text ); return self::wrap_text_with_protected_ranges( $clean_text, max( 1, $soft_limit ), $protected_ranges ); } /** * Extract marker-protected spans from markdown text. * * @param string $text Markdown text with markers. * * @return array{0:string,1:array} */ private static function extract_protected_ranges( string $text ): array { $clean_text = ''; $offset = 0; $stack = []; $ranges = []; while ( true ) { $start_at = strpos( $text, self::START_MARKER, $offset ); $end_at = strpos( $text, self::END_MARKER, $offset ); if ( false === $start_at && false === $end_at ) { break; } $is_start = false === $end_at || ( false !== $start_at && $start_at < $end_at ); $marker_at = $is_start ? $start_at : $end_at; $clean_text .= substr( $text, $offset, $marker_at - $offset ); $clean_offset = strlen( $clean_text ); if ( $is_start ) { $stack[] = $clean_offset; $offset = $marker_at + strlen( self::START_MARKER ); continue; } $start_offset = array_pop( $stack ); if ( is_int( $start_offset ) && $clean_offset > $start_offset ) { $ranges[] = [ $start_offset, $clean_offset ]; } $offset = $marker_at + strlen( self::END_MARKER ); } $clean_text .= substr( $text, $offset ); if ( empty( $ranges ) ) { return [ $clean_text, [] ]; } usort( $ranges, static function ( array $left, array $right ): int { return $left[0] <=> $right[0]; } ); $merged = [ array_shift( $ranges ) ]; foreach ( $ranges as $range ) { $last_index = count( $merged ) - 1; $last = $merged[ $last_index ]; if ( $range[0] <= $last[1] ) { $merged[ $last_index ][1] = max( $last[1], $range[1] ); continue; } $merged[] = $range; } return [ $clean_text, $merged ]; } /** * Wrap text while preserving protected ranges as indivisible spans. * * @param string $text Clean markdown text. * @param int $soft_limit Soft wrap column. * @param array $protected_ranges Protected ranges. * * @return array */ private static function wrap_text_with_protected_ranges( string $text, int $soft_limit, array $protected_ranges ): array { $segments = self::split_into_segments( $text, $protected_ranges ); $lines = [ '' ]; foreach ( $segments as $segment ) { $chunk = $segment['text']; if ( '' === $chunk ) { continue; } if ( $segment['protected'] ) { self::append_wrapped_token( $lines, $chunk, $soft_limit, true ); continue; } $tokens = preg_split( '/(\s+)/u', $chunk, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); if ( ! is_array( $tokens ) ) { continue; } foreach ( $tokens as $token ) { self::append_wrapped_token( $lines, $token, $soft_limit, false ); } } return array_values( array_map( static function ( string $line ): string { return rtrim( $line ); }, $lines ) ); } /** * Split clean text into protected and unprotected segments. * * @param string $text Clean markdown text. * @param array $protected_ranges Protected ranges. * * @return array */ private static function split_into_segments( string $text, array $protected_ranges ): array { if ( empty( $protected_ranges ) ) { return [ [ 'text' => $text, 'protected' => false, ], ]; } $segments = []; $offset = 0; foreach ( $protected_ranges as $range ) { $start = $range[0]; $end = $range[1]; if ( $start > $offset ) { $segments[] = [ 'text' => substr( $text, $offset, $start - $offset ), 'protected' => false, ]; } $segments[] = [ 'text' => substr( $text, $start, $end - $start ), 'protected' => true, ]; $offset = $end; } if ( $offset < strlen( $text ) ) { $segments[] = [ 'text' => substr( $text, $offset ), 'protected' => false, ]; } return $segments; } /** * Append a token to wrapped lines. * * @param array $lines Current wrapped lines. * @param string $token Token text. * @param int $soft_limit Soft wrap column. * @param bool $is_protected Whether the token is protected from internal wrapping. * * @return void */ private static function append_wrapped_token( array &$lines, string $token, int $soft_limit, bool $is_protected ): void { $parts = explode( "\n", $token ); foreach ( $parts as $index => $part ) { if ( '' !== $part ) { self::append_single_token( $lines, $part, $soft_limit, $is_protected ); } if ( $index < count( $parts ) - 1 ) { $lines[] = ''; } } } /** * Append a single token without hard line breaks. * * @param array $lines Current wrapped lines. * @param string $token Token text. * @param int $soft_limit Soft wrap column. * @param bool $is_protected Whether the token is protected. * * @return void */ private static function append_single_token( array &$lines, string $token, int $soft_limit, bool $is_protected ): void { $current_index = count( $lines ) - 1; $current_line = $lines[ $current_index ]; if ( '' === trim( $token ) ) { if ( '' !== $current_line ) { $lines[ $current_index ] .= $token; } return; } $current_width = mb_strwidth( $current_line ); $token_width = mb_strwidth( $token ); if ( 0 !== $current_width && ( $current_width + $token_width ) > $soft_limit ) { $lines[] = ltrim( $token ); return; } if ( ! $is_protected && $current_width > 0 && ( $current_width + $token_width ) > $soft_limit ) { $lines[] = ltrim( $token ); return; } $lines[ $current_index ] .= $token; } }