@@ -1099,6 +1099,117 @@ export class FormatPrinter extends Printer {
10991099 } )
11001100 }
11011101
1102+ /**
1103+ * Check if there's a blank line (double newline) in the nodes at the given index
1104+ */
1105+ private hasBlankLineBetween ( body : Node [ ] , index : number ) : boolean {
1106+ for ( let lookbackIndex = index - 1 ; lookbackIndex >= 0 && lookbackIndex >= index - 2 ; lookbackIndex -- ) {
1107+ const node = body [ lookbackIndex ]
1108+
1109+ if ( isNode ( node , HTMLTextNode ) && node . content . includes ( '\n\n' ) ) {
1110+ return true
1111+ }
1112+
1113+ if ( isNode ( node , WhitespaceNode ) ) {
1114+ continue
1115+ }
1116+
1117+ break
1118+ }
1119+
1120+ for ( let lookaheadIndex = index ; lookaheadIndex < body . length && lookaheadIndex <= index + 1 ; lookaheadIndex ++ ) {
1121+ const node = body [ lookaheadIndex ]
1122+
1123+ if ( isNode ( node , HTMLTextNode ) && node . content . includes ( '\n\n' ) ) {
1124+ return true
1125+ }
1126+
1127+ if ( isNode ( node , WhitespaceNode ) ) {
1128+ continue
1129+ }
1130+
1131+ break
1132+ }
1133+
1134+ return false
1135+ }
1136+
1137+ /**
1138+ * Check if a node is part of a text flow run (text, ERB, or inline element)
1139+ */
1140+ private isTextFlowNode ( node : Node ) : boolean {
1141+ if ( isNode ( node , ERBContentNode ) ) return true
1142+ if ( isNode ( node , HTMLTextNode ) && node . content . trim ( ) !== "" ) return true
1143+ if ( isNode ( node , HTMLElementNode ) && isInlineElement ( getTagName ( node ) ) ) return true
1144+
1145+ return false
1146+ }
1147+
1148+ /**
1149+ * Check if a node is whitespace that can appear within a text flow run
1150+ */
1151+ private isTextFlowWhitespace ( node : Node ) : boolean {
1152+ if ( isNode ( node , WhitespaceNode ) ) return true
1153+ if ( isNode ( node , HTMLTextNode ) && node . content . trim ( ) === "" && ! node . content . includes ( '\n\n' ) ) return true
1154+
1155+ return false
1156+ }
1157+
1158+ /**
1159+ * Collect a run of text flow nodes starting at the given index.
1160+ * Returns the nodes in the run and the index after the last node.
1161+ */
1162+ private collectTextFlowRun ( body : Node [ ] , startIndex : number ) : { nodes : Node [ ] , endIndex : number } | null {
1163+ const nodes : Node [ ] = [ ]
1164+ let index = startIndex
1165+ let textFlowCount = 0
1166+
1167+ while ( index < body . length ) {
1168+ const child = body [ index ]
1169+
1170+ if ( this . isTextFlowNode ( child ) ) {
1171+ nodes . push ( child )
1172+ textFlowCount ++
1173+ index ++
1174+ } else if ( this . isTextFlowWhitespace ( child ) ) {
1175+ let hasMoreTextFlow = false
1176+
1177+ for ( let lookaheadIndex = index + 1 ; lookaheadIndex < body . length ; lookaheadIndex ++ ) {
1178+ if ( this . isTextFlowNode ( body [ lookaheadIndex ] ) ) {
1179+ hasMoreTextFlow = true
1180+ break
1181+ }
1182+
1183+ if ( this . isTextFlowWhitespace ( body [ lookaheadIndex ] ) ) {
1184+ continue
1185+ }
1186+
1187+ break
1188+ }
1189+
1190+ if ( hasMoreTextFlow ) {
1191+ nodes . push ( child )
1192+ index ++
1193+ } else {
1194+ break
1195+ }
1196+ } else {
1197+ break
1198+ }
1199+ }
1200+
1201+ if ( textFlowCount >= 2 ) {
1202+ const hasText = nodes . some ( node => isNode ( node , HTMLTextNode ) && node . content . trim ( ) !== "" )
1203+ const hasAtomicContent = nodes . some ( node => isNode ( node , ERBContentNode ) || ( isNode ( node , HTMLElementNode ) && isInlineElement ( getTagName ( node ) ) ) )
1204+
1205+ if ( hasText && hasAtomicContent ) {
1206+ return { nodes, endIndex : index }
1207+ }
1208+ }
1209+
1210+ return null
1211+ }
1212+
11021213 /**
11031214 * Visit element children with intelligent spacing logic
11041215 *
@@ -1130,6 +1241,41 @@ export class FormatPrinter extends Printer {
11301241
11311242 if ( ! isNonWhitespaceNode ( child ) ) continue
11321243
1244+ if ( this . isTextFlowNode ( child ) ) {
1245+ const run = this . collectTextFlowRun ( body , index )
1246+
1247+ if ( run ) {
1248+ if ( lastMeaningfulNode && ! hasHandledSpacing ) {
1249+ const hasBlankLineBefore = this . hasBlankLineBetween ( body , index )
1250+
1251+ if ( hasBlankLineBefore ) {
1252+ this . push ( "" )
1253+ }
1254+ }
1255+
1256+ this . visitTextFlowChildren ( run . nodes )
1257+
1258+ const lastRunNode = run . nodes [ run . nodes . length - 1 ]
1259+ const hasBlankLineInTrailing = isNode ( lastRunNode , HTMLTextNode ) && lastRunNode . content . includes ( '\n\n' )
1260+ const hasBlankLineAfter = hasBlankLineInTrailing || this . hasBlankLineBetween ( body , run . endIndex )
1261+
1262+ if ( hasBlankLineAfter ) {
1263+ this . push ( "" )
1264+ hasHandledSpacing = true
1265+ }
1266+
1267+ lastMeaningfulNode = run . nodes [ run . nodes . length - 1 ]
1268+
1269+ if ( ! hasBlankLineAfter ) {
1270+ hasHandledSpacing = false
1271+ }
1272+
1273+ index = run . endIndex - 1
1274+
1275+ continue
1276+ }
1277+ }
1278+
11331279 let hasTrailingHerbDisable = false
11341280
11351281 if ( isNode ( child , HTMLElementNode ) && child . close_tag ) {
0 commit comments