@@ -46,7 +46,7 @@ class DiffService {
4646 'info ' => 'ℹ️ ' ,
4747 'success ' => '✅ ' ,
4848 'warn ' => '⚠️ ' ,
49- 'error ' => '🔴 ' ,
49+ 'error ' => '❗ ' ,
5050 ];
5151
5252 /**
@@ -426,6 +426,11 @@ private function generateWordLevelDiff(string $oldLine, string $newLine): string
426426 return $ this ->generateCalloutBlockDiff ($ oldLine , $ newLine );
427427 }
428428
429+ // Check if either line is a quote line
430+ if ($ this ->isQuoteLine ($ oldLine ) || $ this ->isQuoteLine ($ newLine )) {
431+ return $ this ->generateQuoteDiff ($ oldLine , $ newLine );
432+ }
433+
429434 // Split lines into words for comparison
430435 $ oldWords = $ this ->splitIntoWords ($ oldLine );
431436 $ newWords = $ this ->splitIntoWords ($ newLine );
@@ -515,6 +520,42 @@ private function generateCalloutBlockDiff(string $oldLine, string $newLine): str
515520 return $ oldEmoji . '→ ' . $ newEmoji ;
516521 }
517522
523+ /**
524+ * Check if a line is a quote line
525+ *
526+ * @param string $line
527+ * @return bool
528+ */
529+ private function isQuoteLine (string $ line ): bool {
530+ return preg_match (self ::QUOTE_PATTERN , trim ($ line )) === 1 ;
531+ }
532+
533+ /**
534+ * Generate diff for quote line changes
535+ *
536+ * @param string $oldLine
537+ * @param string $newLine
538+ * @return string
539+ */
540+ private function generateQuoteDiff (string $ oldLine , string $ newLine ): string {
541+ $ oldText = preg_replace (self ::QUOTE_PATTERN , '' , $ oldLine );
542+ $ newText = preg_replace (self ::QUOTE_PATTERN , '' , $ newLine );
543+
544+ // If both are quotes, show the text change
545+ if ($ this ->isQuoteLine ($ oldLine ) && $ this ->isQuoteLine ($ newLine )) {
546+ return '→❞ ' . htmlspecialchars (trim ($ oldText ), ENT_QUOTES , 'UTF-8 ' ) .
547+ '→ ' . htmlspecialchars (trim ($ newText ), ENT_QUOTES , 'UTF-8 ' );
548+ }
549+
550+ // If only new line is a quote
551+ if ($ this ->isQuoteLine ($ newLine )) {
552+ return '→❞ ' . htmlspecialchars (trim ($ newText ), ENT_QUOTES , 'UTF-8 ' );
553+ }
554+
555+ // If only old line was a quote
556+ return '❞→ ' . htmlspecialchars (trim ($ newText ), ENT_QUOTES , 'UTF-8 ' );
557+ }
558+
518559 /**
519560 * Format special lines (code blocks, callouts, quotes) with emojis
520561 *
@@ -531,7 +572,7 @@ private function formatSpecialLine(string $line): ?string {
531572
532573 // Format code block markers
533574 if (preg_match (self ::CODE_BLOCK_PATTERN , $ trimmed )) {
534- return '→📝 ' ;
575+ return '→‹› ' ;
535576 }
536577
537578 // Format callout block markers
@@ -545,7 +586,7 @@ private function formatSpecialLine(string $line): ?string {
545586 if (preg_match (self ::QUOTE_PATTERN , $ trimmed )) {
546587 // Remove the > marker and return the quoted text with emoji
547588 $ quotedText = preg_replace (self ::QUOTE_PATTERN , '' , $ line );
548- return '→💬 ' . htmlspecialchars ($ quotedText , ENT_QUOTES , 'UTF-8 ' );
589+ return '→❞ ' . htmlspecialchars ($ quotedText , ENT_QUOTES , 'UTF-8 ' );
549590 }
550591
551592 // Return original line if not a special pattern
@@ -582,31 +623,60 @@ private function splitIntoWords(string $text): array {
582623 */
583624 private function renderWordLevelHtml (array $ operations , array $ oldWords , array $ newWords ): string {
584625 $ html = '' ;
585- $ lastWasDel = false ;
586-
626+ $ buffer = '' ;
627+ $ lastType = null ;
628+
587629 foreach ($ operations as $ operation ) {
588- switch ($ operation ['type ' ]) {
630+ $ currentType = $ operation ['type ' ];
631+ $ word = '' ;
632+
633+ // Get the word for this operation
634+ switch ($ currentType ) {
589635 case 'add ' :
590636 $ word = $ newWords [$ operation ['new_line ' ]] ?? '' ;
591- // Add arrow if previous operation was a deletion (showing replacement)
592- if ($ lastWasDel ) {
593- $ html .= '→ ' . htmlspecialchars ($ word , ENT_QUOTES , 'UTF-8 ' );
594- } else {
595- $ html .= htmlspecialchars ($ word , ENT_QUOTES , 'UTF-8 ' );
596- }
597- $ lastWasDel = false ;
598637 break ;
599638 case 'remove ' :
600639 $ word = $ oldWords [$ operation ['old_line ' ]] ?? '' ;
601- $ html .= htmlspecialchars ($ word , ENT_QUOTES , 'UTF-8 ' );
602- $ lastWasDel = true ;
603640 break ;
604641 case 'keep ' :
605642 $ word = $ oldWords [$ operation ['old_line ' ]] ?? '' ;
606- $ html .= htmlspecialchars ($ word , ENT_QUOTES , 'UTF-8 ' );
607- $ lastWasDel = false ;
608643 break ;
609644 }
645+
646+ // If current operation is 'keep' and it's only whitespace, and we have a buffer of add/remove,
647+ // then add it to the buffer to merge tags
648+ if ($ currentType === 'keep ' && trim ($ word ) === '' && $ lastType !== null && $ lastType !== 'keep ' ) {
649+ $ buffer .= htmlspecialchars ($ word , ENT_QUOTES , 'UTF-8 ' );
650+ // Don't change lastType, so we continue with the same operation type
651+ continue ;
652+ }
653+
654+ // If type changed (and not a whitespace-only keep), flush the buffer with appropriate tags
655+ if ($ lastType !== null && $ lastType !== $ currentType && $ buffer !== '' ) {
656+ if ($ lastType === 'add ' ) {
657+ $ html .= '<ins> ' . $ buffer . '</ins> ' ;
658+ } elseif ($ lastType === 'remove ' ) {
659+ $ html .= '<del> ' . $ buffer . '</del> ' ;
660+ } else {
661+ $ html .= $ buffer ;
662+ }
663+ $ buffer = '' ;
664+ }
665+
666+ // Add current word to buffer
667+ $ buffer .= htmlspecialchars ($ word , ENT_QUOTES , 'UTF-8 ' );
668+ $ lastType = $ currentType ;
669+ }
670+
671+ // Flush remaining buffer
672+ if ($ buffer !== '' ) {
673+ if ($ lastType === 'add ' ) {
674+ $ html .= '<ins> ' . $ buffer . '</ins> ' ;
675+ } elseif ($ lastType === 'remove ' ) {
676+ $ html .= '<del> ' . $ buffer . '</del> ' ;
677+ } else {
678+ $ html .= $ buffer ;
679+ }
610680 }
611681
612682 return $ html ;
0 commit comments