'heading', 'level' => strlen( $heading[1] ), 'text' => trim( $heading[2] ) ); $i++; continue; } if ( preg_match( '/^\s*(-|\d+\.)\s+(.*)$/', $line, $first_item ) ) { $ordered = (bool) preg_match( '/^\d+\.$/', $first_item[1] ); $items = array(); while ( $i < $total && preg_match( '/^\s*(-|\d+\.)\s+(.*)$/', $lines[ $i ], $item_match ) ) { $items[] = trim( $item_match[2] ); $i++; } $blocks[] = array( 'type' => 'list', 'ordered' => $ordered, 'items' => $items ); continue; } $paragraph_lines = array(); while ( $i < $total && '' !== trim( $lines[ $i ] ) && ! preg_match( '/^(#{2,4})\s+/', $lines[ $i ] ) && ! preg_match( '/^\s*(-|\d+\.)\s+/', $lines[ $i ] ) ) { $paragraph_lines[] = $lines[ $i ]; $i++; } $text = trim( implode( ' ', $paragraph_lines ) ); if ( '' !== $text ) { $blocks[] = array( 'type' => 'paragraph', 'text' => $text ); } } $html = array(); foreach ( $blocks as $block ) { if ( 'heading' === $block['type'] ) { $level = max( 2, min( 4, (int) $block['level'] ) ); $html[] = "" . self::inline( (string) $block['text'] ) . ""; continue; } if ( 'list' === $block['type'] ) { $tag = $block['ordered'] ? 'ol' : 'ul'; $items = array_map( static fn( string $item ): string => '
  • ' . self::inline( $item ) . '
  • ', $block['items'] ); $html[] = "<{$tag}>" . implode( '', $items ) . ""; continue; } $html[] = '

    ' . self::inline( (string) $block['text'] ) . '

    '; } return implode( "\n", $html ); } /** Escapes first, THEN applies bold/italic — never trust AI text as HTML. */ private static function inline( string $text ): string { $escaped = htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' ); $escaped = (string) preg_replace( '/\*\*([^*]+)\*\*/', '$1', $escaped ); $escaped = (string) preg_replace( '/\*([^*]+)\*/', '$1', $escaped ); return $escaped; } }