Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion packages/core/lib/src/internal/ops/tag_br.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,44 @@ class TagBrBit extends BuildBit {
BuildBit copyWith({BuildTree? parent}) => TagBrBit(parent ?? this.parent);

@override
void flatten(Flattened f) => f.write(text: '\n');
void flatten(Flattened f) {
final next = _nextNonWhitespace;
if (next != null && next.isInline == false) {
// Keep behavior of skipping a single BR before a block, but convert
// additional BRs to vertical spacing outside RichText to avoid
// WidgetSpan line-metric artifacts.
final prev = _prevNonWhitespace;
if (prev is TagBrBit || prev?.isInline == false) {
const oneEm = CssLength(1, CssLengthUnit.em);
f.widget(
HeightPlaceholder(
oneEm,
parent.inheritanceResolvers,
debugLabel: '${parent.element.localName}--$oneEm',
),
);
}
return;
}

f.write(text: '\n');
}

BuildBit? get _nextNonWhitespace {
var next = this.next;
while (next is WhitespaceBit) {
next = next.next;
}
return next;
}

BuildBit? get _prevNonWhitespace {
var prev = this.prev;
while (prev is WhitespaceBit) {
prev = prev.prev;
}
return prev;
}

@override
String toString() => '<BR />';
Expand Down
51 changes: 51 additions & 0 deletions packages/core/test/tag_br_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
import 'package:html/dom.dart' as dom;

import '_.dart' as helper;

void main() {
// Some special test cases for BR handling, note that most of the tests for BR are in `core_test.dart`
group('BR', () {
testWidgets(
'avoids WidgetSpan newline artifact before block',
(WidgetTester tester) async {
Widget? customWidgetBuilder(dom.Element element) {
if (element.localName != 'x-inline') {
return null;
}

return const InlineCustomWidget(
child: SizedBox(width: 20, height: 20),
);
}

// Keep BR spacing outside RichText for inline widgets before a block:
// if a trailing newline remains in the same paragraph as WidgetSpan,
// paragraph line metrics may produce unexpectedly large vertical gaps.
const html = '<x-inline></x-inline><br><br><div>Next line!</div>';
final explained = await helper.explain(
tester,
null,
hw: HtmlWidget(
html,
customWidgetBuilder: customWidgetBuilder,
key: helper.hwKey,
),
);

expect(
explained,
allOf(
startsWith('[Column:children=['),
contains('[SizedBox:20.0x20.0],'),
contains('[SizedBox:0.0x10.0],'),
contains('[CssBlock:child=[RichText:(:Next line!)]]'),
isNot(contains('[RichText:(:[SizedBox:20.0x20.0]')),
),
);
},
);
});
}
Loading