'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'] ) . '
'; } 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; } }