@@ -32,20 +32,58 @@ public function format(string $markdown): string
3232 {
3333 $ text = $ markdown ;
3434
35- // Remove markdown code blocks but keep content
36- $ text = preg_replace_callback ('/```(\w+)?\n(.*?)```/s ' , function (array $ matches ): string {
35+ // First, protect code blocks (```code```) by replacing them with placeholders
36+ $ codeBlocks = [];
37+ $ codeBlockIndex = 0 ;
38+ $ text = preg_replace_callback ('/```(\w+)?\n(.*?)```/s ' , function (array $ matches ) use (&$ codeBlocks , &$ codeBlockIndex ): string {
3739 // preg_replace_callback guarantees indices 1 and 2 exist for this pattern
3840 $ code = $ matches [2 ];
3941 $ lang = $ matches [1 ] !== '' ? $ matches [1 ] : '' ;
40- return $ this ->formatCodeBlock ($ code , $ lang );
42+ $ placeholder = "\x00CODEBLOCK " . $ codeBlockIndex . "\x00" ;
43+ $ codeBlocks [$ placeholder ] = $ this ->formatCodeBlock ($ code , $ lang );
44+ $ codeBlockIndex ++;
45+ return $ placeholder ;
4146 }, $ text );
4247 if ($ text === null ) {
4348 $ text = $ markdown ;
4449 }
50+ $ text = (string )$ text ;
4551
46- // Format inline code
47- $ replaced = preg_replace ('/`([^`]+)`/ ' , $ this ->useColors ? "\033[36m \$1 \033[0m " : '[$1] ' , $ text );
48- $ text = $ replaced !== null ? $ replaced : $ text ;
52+ // Protect inline code blocks (`code`) by replacing them with placeholders
53+ // This protects content from markdown formatting (like asterisks and underscores)
54+ $ inlineCode = [];
55+ $ inlineCodeIndex = 0 ;
56+
57+ // First, handle double backticks (``code``) - these can contain single backticks
58+ // Pattern: `` followed by content (which can include `) followed by ``
59+ // Match the shortest possible sequence to avoid greedy matching
60+ $ text = preg_replace_callback ('/``(.*?)``/s ' , function (array $ matches ) use (&$ inlineCode , &$ inlineCodeIndex ): string {
61+ // preg_replace_callback guarantees index 1 exists
62+ $ code = $ matches [1 ];
63+ $ placeholder = "\x00INLINECODE " . $ inlineCodeIndex . "\x00" ;
64+ $ inlineCode [$ placeholder ] = $ this ->useColors ? "\033[36m {$ code }\033[0m " : "[ {$ code }] " ;
65+ $ inlineCodeIndex ++;
66+ return $ placeholder ;
67+ }, $ text );
68+ if ($ text === null ) {
69+ $ text = '' ;
70+ }
71+ $ text = (string )$ text ;
72+
73+ // Then handle single backticks (`code`) - these cannot contain backticks
74+ // Use non-greedy matching to handle cases correctly
75+ $ text = preg_replace_callback ('/`([^`\n]+?)`/ ' , function (array $ matches ) use (&$ inlineCode , &$ inlineCodeIndex ): string {
76+ // preg_replace_callback guarantees index 1 exists
77+ $ code = $ matches [1 ];
78+ $ placeholder = "\x00INLINECODE " . $ inlineCodeIndex . "\x00" ;
79+ $ inlineCode [$ placeholder ] = $ this ->useColors ? "\033[36m {$ code }\033[0m " : "[ {$ code }] " ;
80+ $ inlineCodeIndex ++;
81+ return $ placeholder ;
82+ }, $ text );
83+ if ($ text === null ) {
84+ $ text = '' ;
85+ }
86+ $ text = (string )$ text ;
4987
5088 // Format headers
5189 $ text = (string )preg_replace ('/^### (.*)$/m ' , $ this ->formatHeader (3 , '$1 ' ), $ text );
@@ -56,23 +94,38 @@ public function format(string $markdown): string
5694 $ text = (string )preg_replace ('/\*\*(.+?)\*\*/ ' , $ this ->useColors ? "\033[1m \$1 \033[0m " : '**$1** ' , $ text );
5795 $ text = (string )preg_replace ('/__(.+?)__/ ' , $ this ->useColors ? "\033[1m \$1 \033[0m " : '__$1__ ' , $ text );
5896
59- // Format italic text
60- $ text = (string )preg_replace ('/\*(.+?)\*/ ' , $ this ->useColors ? "\033[3m \$1 \033[0m " : '*$1* ' , $ text );
61- $ text = (string )preg_replace ('/_(.+?)_/ ' , $ this ->useColors ? "\033[3m \$1 \033[0m " : '_$1_ ' , $ text );
62-
63- // Format unordered lists
64- $ text = preg_replace_callback ('/^(\s*)[-*+] (.+)$/m ' , function (array $ matches ): string {
97+ // Format unordered lists BEFORE italic formatting to avoid conflicts
98+ // This ensures list markers are processed before italic markers
99+ $ replaced = preg_replace_callback ('/^(\s*)[-*+] (.+)$/m ' , function (array $ matches ): string {
65100 // preg_replace_callback guarantees these indices exist
66101 $ indent = $ matches [1 ];
67102 $ content = $ matches [2 ];
68103 $ bullet = $ this ->useColors ? "\033[0;33m• \033[0m " : '• ' ;
69104 return $ indent . $ bullet . ' ' . $ content ;
70105 }, $ text );
71- if ($ text === null ) {
72- $ text = '' ;
73- }
106+ $ text = $ replaced !== null ? $ replaced : $ text ;
74107 $ text = (string )$ text ;
75108
109+ // Format italic text AFTER lists to avoid interfering with list markers
110+ // Only format if underscores are around words (not inside words like users_created_at)
111+ // Pattern: _word_ but not word_word or _word_word
112+ // Exclude asterisks that are list markers (at start of line with optional whitespace)
113+ $ replaced = preg_replace ('/(?<!^|\n)(?<!\S)\*(.+?)\*(?!\S)/ ' , $ this ->useColors ? "\033[3m \$1 \033[0m " : '*$1* ' , $ text );
114+ $ text = $ replaced !== null ? $ replaced : $ text ;
115+ // Match _text_ only if not preceded/followed by word characters (letters, digits, underscores)
116+ $ replaced = preg_replace ('/(?<![a-zA-Z0-9_])_(.+?)_(?![a-zA-Z0-9_])/ ' , $ this ->useColors ? "\033[3m \$1 \033[0m " : '_$1_ ' , $ text );
117+ $ text = $ replaced !== null ? $ replaced : $ text ;
118+
119+ // Restore inline code blocks
120+ foreach ($ inlineCode as $ placeholder => $ formatted ) {
121+ $ text = str_replace ($ placeholder , $ formatted , $ text );
122+ }
123+
124+ // Restore code blocks
125+ foreach ($ codeBlocks as $ placeholder => $ formatted ) {
126+ $ text = str_replace ($ placeholder , $ formatted , $ text );
127+ }
128+
76129 // Format ordered lists
77130 $ text = preg_replace_callback ('/^(\s*)(\d+)\. (.+)$/m ' , function (array $ matches ): string {
78131 // preg_replace_callback guarantees these indices exist
@@ -363,26 +416,34 @@ protected function formatCodeBlock(string $code, string $lang = ''): string
363416 $ maxLength = max ($ maxLength , mb_strlen ($ line ));
364417 }
365418
366- $ border = str_repeat ('─ ' , min ($ maxLength + 2 , 80 ));
367419 $ langLabel = $ lang !== '' ? " {$ lang }" : '' ;
420+ // Calculate border width: max of code width + 2 (for padding) and lang label width + 2
421+ // But ensure it doesn't exceed 80 characters
422+ $ langWidth = $ lang !== '' ? mb_strlen ($ langLabel ) : 0 ;
423+ $ borderWidth = min (max ($ maxLength + 2 , $ langWidth + 2 ), 80 );
424+ $ border = str_repeat ('─ ' , $ borderWidth );
368425
369426 $ result = [];
370427 if ($ this ->useColors ) {
371428 $ result [] = "\033[0;90m┌ {$ border }┐ \033[0m " ;
372429 if ($ lang !== '' ) {
373- $ result [] = "\033[0;90m│ \033[0m \033[0;36m {$ langLabel }\033[0m " . str_repeat (' ' , max (0 , $ maxLength - mb_strlen ($ lang ) + 1 )) . "\033[0;90m│ \033[0m " ;
430+ $ langPadding = max (0 , $ borderWidth - mb_strlen ($ langLabel ) - 2 );
431+ $ result [] = "\033[0;90m│ \033[0m \033[0;36m {$ langLabel }\033[0m " . str_repeat (' ' , $ langPadding ) . "\033[0;90m│ \033[0m " ;
374432 $ result [] = "\033[0;90m├ {$ border }┤ \033[0m " ;
375433 }
376434 } else {
377435 $ result [] = "┌ {$ border }┐ " ;
378436 if ($ lang !== '' ) {
379- $ result [] = "│ {$ langLabel }" . str_repeat (' ' , max (0 , $ maxLength - mb_strlen ($ lang ) + 1 )) . '│ ' ;
437+ $ langPadding = max (0 , $ borderWidth - mb_strlen ($ langLabel ) - 2 );
438+ $ result [] = "│ {$ langLabel }" . str_repeat (' ' , $ langPadding ) . '│ ' ;
380439 $ result [] = "├ {$ border }┤ " ;
381440 }
382441 }
383442
384443 foreach ($ lines as $ line ) {
385- $ padded = $ line . str_repeat (' ' , max (0 , $ maxLength - mb_strlen ($ line )));
444+ // Pad line to border width - 2 (for left and right padding spaces)
445+ $ linePadding = max (0 , $ borderWidth - mb_strlen ($ line ) - 2 );
446+ $ padded = $ line . str_repeat (' ' , $ linePadding );
386447 if ($ this ->useColors ) {
387448 $ result [] = "\033[0;90m│ \033[0m \033[36m {$ padded }\033[0m \033[0;90m│ \033[0m " ;
388449 } else {
0 commit comments