@@ -434,7 +434,8 @@ func (s *SlackUI) generateBlocks(text string, includeContext bool) []slack.Block
434434func (s * SlackUI ) markdownToBlocks (text string ) []slack.Block {
435435 var blocks []slack.Block
436436
437- // First, normalize tables that might be on a single line
437+ // First, normalize inline headers and tables
438+ text = s .normalizeInlineHeaders (text )
438439 text = s .normalizeInlineTables (text )
439440
440441 lines := strings .Split (text , "\n " )
@@ -597,6 +598,148 @@ func (s *SlackUI) parseMarkdownTable(lines []string) ([]string, [][]string) {
597598 return headers , rows
598599}
599600
601+ // normalizeInlineHeaders adds line breaks after markdown headers that are followed by text without a newline.
602+ // Example: "### HeaderText here" becomes "### Header\nText here"
603+ func (s * SlackUI ) normalizeInlineHeaders (text string ) string {
604+ lines := strings .Split (text , "\n " )
605+ var result []string
606+
607+ for _ , line := range lines {
608+ trimmed := strings .TrimSpace (line )
609+
610+ // Check if line starts with markdown header
611+ if strings .HasPrefix (trimmed , "###" ) {
612+ // Find where the header text ends (after the header marker and any spaces/emojis)
613+ headerStart := strings .Index (line , "###" ) + 3
614+ restOfLine := line [headerStart :]
615+
616+ // Skip initial spaces
617+ restOfLine = strings .TrimLeft (restOfLine , " " )
618+
619+ // Check if there's text after the header that should be on a new line
620+ // Look for patterns like: "### 💡 HeaderTextWithoutSpace" or "### Header TextHere"
621+ // We want to split after the first "word" (which might include emojis)
622+
623+ // Find the first lowercase letter after uppercase/emoji sequence
624+ // This indicates where the header ends and content begins
625+ headerEnd := - 1
626+ inHeaderText := false
627+
628+ for i , r := range restOfLine {
629+ // Skip emojis and spaces at the start
630+ if i < 10 && (r >= 0x1F300 || r == ' ' ) {
631+ continue
632+ }
633+
634+ // If we see an uppercase letter or start of word, we're in header
635+ if ! inHeaderText && (r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' ) {
636+ inHeaderText = true
637+ continue
638+ }
639+
640+ // If we're in header text and see a capital letter followed by lowercase
641+ // or see text that looks like start of a sentence, that's where to split
642+ if inHeaderText && i > 0 {
643+ // Check for patterns like "PodI" -> split before "I"
644+ prevRune := rune (restOfLine [i - 1 ])
645+ if (prevRune >= 'a' && prevRune <= 'z' ) && (r >= 'A' && r <= 'Z' ) {
646+ // Lowercase followed by uppercase - likely start of new word
647+ headerEnd = i
648+ break
649+ }
650+ }
651+ }
652+
653+ if headerEnd > 0 {
654+ headerText := strings .TrimSpace (restOfLine [:headerEnd ])
655+ contentText := strings .TrimSpace (restOfLine [headerEnd :])
656+ result = append (result , "### " + headerText )
657+ if contentText != "" {
658+ result = append (result , contentText )
659+ }
660+ } else {
661+ result = append (result , line )
662+ }
663+ } else if strings .HasPrefix (trimmed , "##" ) {
664+ // Similar logic for ## headers
665+ headerStart := strings .Index (line , "##" ) + 2
666+ restOfLine := line [headerStart :]
667+ restOfLine = strings .TrimLeft (restOfLine , " " )
668+
669+ headerEnd := - 1
670+ inHeaderText := false
671+
672+ for i , r := range restOfLine {
673+ if i < 10 && (r >= 0x1F300 || r == ' ' ) {
674+ continue
675+ }
676+ if ! inHeaderText && (r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' ) {
677+ inHeaderText = true
678+ continue
679+ }
680+ if inHeaderText && i > 0 {
681+ prevRune := rune (restOfLine [i - 1 ])
682+ if (prevRune >= 'a' && prevRune <= 'z' ) && (r >= 'A' && r <= 'Z' ) {
683+ headerEnd = i
684+ break
685+ }
686+ }
687+ }
688+
689+ if headerEnd > 0 {
690+ headerText := strings .TrimSpace (restOfLine [:headerEnd ])
691+ contentText := strings .TrimSpace (restOfLine [headerEnd :])
692+ result = append (result , "## " + headerText )
693+ if contentText != "" {
694+ result = append (result , contentText )
695+ }
696+ } else {
697+ result = append (result , line )
698+ }
699+ } else if strings .HasPrefix (trimmed , "#" ) && ! strings .HasPrefix (trimmed , "##" ) {
700+ // Similar logic for # headers
701+ headerStart := strings .Index (line , "#" ) + 1
702+ restOfLine := line [headerStart :]
703+ restOfLine = strings .TrimLeft (restOfLine , " " )
704+
705+ headerEnd := - 1
706+ inHeaderText := false
707+
708+ for i , r := range restOfLine {
709+ if i < 10 && (r >= 0x1F300 || r == ' ' ) {
710+ continue
711+ }
712+ if ! inHeaderText && (r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z' ) {
713+ inHeaderText = true
714+ continue
715+ }
716+ if inHeaderText && i > 0 {
717+ prevRune := rune (restOfLine [i - 1 ])
718+ if (prevRune >= 'a' && prevRune <= 'z' ) && (r >= 'A' && r <= 'Z' ) {
719+ headerEnd = i
720+ break
721+ }
722+ }
723+ }
724+
725+ if headerEnd > 0 {
726+ headerText := strings .TrimSpace (restOfLine [:headerEnd ])
727+ contentText := strings .TrimSpace (restOfLine [headerEnd :])
728+ result = append (result , "# " + headerText )
729+ if contentText != "" {
730+ result = append (result , contentText )
731+ }
732+ } else {
733+ result = append (result , line )
734+ }
735+ } else {
736+ result = append (result , line )
737+ }
738+ }
739+
740+ return strings .Join (result , "\n " )
741+ }
742+
600743// normalizeInlineTables converts inline tables (tables without line breaks) to multi-line format
601744func (s * SlackUI ) normalizeInlineTables (text string ) string {
602745 // Pattern: | col1 | col2 || :--- | :--- || row1 | row2 |
0 commit comments