Skip to content

Commit ccdedc5

Browse files
committed
Treat <br> as inline and end the line after a line break
- Keep <br> in the surrounding inline run instead of splitting it into its own block - Start the new line with the word after a line break, so a trailing <br> adds no line and a leading one does - Ignore line breaks when summing the max-content width of a line - Drop the newline right after a <pre>, <listing> or <textarea> start tag - Revert the CorrectLineBreaksBlocks change, it's not needed anymore - Update the five baselines that now show the empty lines
1 parent 11c91bc commit ccdedc5

10 files changed

Lines changed: 107 additions & 9 deletions

File tree

‎Source/HtmlRenderer/Core/Dom/CssBox.cs‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ public bool IsPseudoElement
238238
/// </summary>
239239
public bool IsInline
240240
{
241-
get { return (Display == CssConstants.Inline || Display == CssConstants.InlineBlock) && !IsBrElement; }
241+
get { return Display == CssConstants.Inline || Display == CssConstants.InlineBlock; }
242242
}
243243

244244
/// <summary>
@@ -1474,9 +1474,10 @@ internal void GetMinMaxWidth(out double minWidth, out double maxWidth)
14741474
double maxSum = 0f;
14751475
double paddingSum = 0f;
14761476
double marginSum = 0f;
1477-
GetMinMaxSumWords(this, ref min, ref maxSum, ref paddingSum, ref marginSum);
1477+
double brokenLinesMax = 0f;
1478+
GetMinMaxSumWords(this, ref min, ref maxSum, ref paddingSum, ref marginSum, ref brokenLinesMax);
14781479

1479-
maxWidth = paddingSum + maxSum;
1480+
maxWidth = paddingSum + Math.Max(maxSum, brokenLinesMax);
14801481
minWidth = paddingSum + (min < 90999 ? min : 0);
14811482
}
14821483

@@ -1488,10 +1489,12 @@ internal void GetMinMaxWidth(out double minWidth, out double maxWidth)
14881489
/// <param name="maxSum">the max width a single line of words can take without wrapping</param>
14891490
/// <param name="paddingSum">the total amount of padding the content has </param>
14901491
/// <param name="marginSum"></param>
1492+
/// <param name="brokenLinesMax">the widest line already ended by a line break in the current block</param>
14911493
/// <returns></returns>
1492-
private static void GetMinMaxSumWords(CssBox box, ref double min, ref double maxSum, ref double paddingSum, ref double marginSum)
1494+
private static void GetMinMaxSumWords(CssBox box, ref double min, ref double maxSum, ref double paddingSum, ref double marginSum, ref double brokenLinesMax)
14931495
{
14941496
double? oldSum = null;
1497+
double oldBrokenLinesMax = 0;
14951498

14961499
// paddingSum must be scoped per "line" the same way maxSum is (see the oldSum save/restore
14971500
// below) - it represents the border/padding belonging to the WIDEST line found so far, not a
@@ -1509,6 +1512,8 @@ private static void GetMinMaxSumWords(CssBox box, ref double min, ref double max
15091512
maxSum = marginSum;
15101513
oldPaddingSum = paddingSum;
15111514
paddingSum = 0;
1515+
oldBrokenLinesMax = brokenLinesMax;
1516+
brokenLinesMax = 0;
15121517
}
15131518

15141519
// add the padding
@@ -1524,6 +1529,14 @@ private static void GetMinMaxSumWords(CssBox box, ref double min, ref double max
15241529
// calculate the min and max sum for all the words in the box
15251530
foreach (CssRect word in box.Words)
15261531
{
1532+
// A line break ends the line, so the next word starts a new one instead of adding to it.
1533+
if (word.IsLineBreak)
1534+
{
1535+
brokenLinesMax = Math.Max(brokenLinesMax, maxSum);
1536+
maxSum = marginSum;
1537+
continue;
1538+
}
1539+
15271540
maxSum += word.FullWidth + (word.HasSpaceBefore ? word.OwnerBox.ActualWordSpacing : 0);
15281541
min = Math.Max(min, word.Width);
15291542
}
@@ -1542,7 +1555,7 @@ private static void GetMinMaxSumWords(CssBox box, ref double min, ref double max
15421555

15431556
//maxSum += childBox.ActualMarginLeft + childBox.ActualMarginRight;
15441557
var maxSumBeforeChild = maxSum;
1545-
GetMinMaxSumWords(childBox, ref min, ref maxSum, ref paddingSum, ref marginSum);
1558+
GetMinMaxSumWords(childBox, ref min, ref maxSum, ref paddingSum, ref marginSum, ref brokenLinesMax);
15461559

15471560
// This walk otherwise never consults a box's own explicit CSS `width` at all - only
15481561
// literal word/text content. That's usually fine (explicit width constrains layout
@@ -1575,7 +1588,8 @@ private static void GetMinMaxSumWords(CssBox box, ref double min, ref double max
15751588
// max sum (and its matching padding contribution) is the max of all the lines in the box
15761589
if (oldSum.HasValue)
15771590
{
1578-
maxSum = Math.Max(maxSum, oldSum.Value);
1591+
maxSum = Math.Max(Math.Max(maxSum, brokenLinesMax), oldSum.Value);
1592+
brokenLinesMax = oldBrokenLinesMax;
15791593
paddingSum = Math.Max(paddingSum, oldPaddingSum!.Value);
15801594
}
15811595
}

‎Source/HtmlRenderer/Core/Dom/CssLayoutEngine.cs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,9 +467,14 @@ private static void FlowBox(RGraphics g, CssBox blockbox, CssBox box, double lim
467467
actualLimitRight = rightObstructingFloat.Location.X - rightObstructingFloat.ActualMarginLeft - rightspacing;
468468
}
469469

470+
// A line break ends the current line, so the word after it starts the next one. Wrapping on
471+
// the break itself would add an empty line after a trailing <br> and drop the one a
472+
// leading <br> creates.
473+
var afterLineBreak = line.Words.Count > 0 && line.Words[line.Words.Count - 1].IsLineBreak;
474+
470475
if ((b.WhiteSpace != CssConstants.NoWrap && b.WhiteSpace != CssConstants.Pre && curx + word.Width + rightspacing > actualLimitRight
471476
&& (b.WhiteSpace != CssConstants.PreWrap || !word.IsSpaces))
472-
|| word.IsLineBreak || wrapNoWrapBox)
477+
|| afterLineBreak || wrapNoWrapBox)
473478
{
474479
wrapNoWrapBox = false;
475480
curx = startx;

‎Source/HtmlRenderer/Core/Parse/DomParser.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,10 +1174,10 @@ private static void CorrectLineBreaksBlocks(CssBox box)
11741174
if (!box.IsBrElement) return;
11751175

11761176
var previousSibling = DomUtils.GetPreviousSibling(box);
1177-
if (previousSibling == null || previousSibling.IsBlock || previousSibling.IsBrElement)
1177+
if (previousSibling == null || previousSibling.IsBlock)
11781178
{
11791179
var nextSibling = DomUtils.GetFollowingSiblings(box, b => b.IsInline && !b.IsBrElement, true).FirstOrDefault();
1180-
if (nextSibling == null || previousSibling?.IsBrElement == true)
1180+
if (nextSibling == null)
11811181
{
11821182
box.Text = "\n";
11831183
box.ParseToWords();

‎Source/HtmlRenderer/Core/Parse/HtmlParser.cs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,29 @@ private static void AddTextBox(HtmlDataToken token, ref CssBox curBox)
9898

9999
if (text == null) return;
100100

101+
// HTML drops a single newline directly after the <pre>, <listing> or <textarea> start tag.
102+
if (curBox.Boxes.Count == 0 && curBox.HtmlTag != null && IsNewlineStrippingElement(curBox.HtmlTag.Name))
103+
{
104+
if (text.StartsWith("\r\n", StringComparison.Ordinal))
105+
text = text.Substring(2);
106+
else if (text.StartsWith("\n", StringComparison.Ordinal))
107+
text = text.Substring(1);
108+
109+
if (text.Length == 0) return;
110+
}
111+
101112
var box = CssBox.CreateBox(curBox);
102113
box.Text = text;
103114
}
104115

105116

117+
private static bool IsNewlineStrippingElement(string tagName)
118+
{
119+
return tagName.Equals("pre", StringComparison.OrdinalIgnoreCase)
120+
|| tagName.Equals("listing", StringComparison.OrdinalIgnoreCase)
121+
|| tagName.Equals("textarea", StringComparison.OrdinalIgnoreCase);
122+
}
123+
106124
/// <summary>
107125
/// Parse the html part, the part from prev parsing index to the beginning of the next html tag.<br/>
108126
/// </summary>
443 Bytes
Loading
170 Bytes
Loading
1.12 KB
Loading
312 Bytes
Loading
168 Bytes
Loading
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using HtmlRenderer.IntegrationTest.TestSupport;
2+
3+
namespace HtmlRenderer.IntegrationTest.Text;
4+
5+
/// <summary>
6+
/// Where lines end around &lt;br&gt;, compared against what browsers render. With the mock adapter every line
7+
/// is 20px tall.
8+
/// </summary>
9+
[DoNotParallelize]
10+
[TestClass]
11+
public sealed class LineBreakLayoutIntegrationTests
12+
{
13+
private const double Delta = 0.5;
14+
15+
[TestMethod]
16+
[DataRow("<div id='x'>first<br>second</div>", 40)]
17+
[DataRow("<div id='x'>first<br><br>second</div>", 60)]
18+
[DataRow("<div id='x'><span>first</span><br/><br/><span>second</span></div>", 60)]
19+
[DataRow("<div id='x'>a<br></div>", 20)]
20+
[DataRow("<div id='x'><br>b</div>", 40)]
21+
[DataRow("<div id='x'><br></div>", 20)]
22+
public void BlockHeight_MatchesTheNumberOfLines(string html, double expectedHeight)
23+
{
24+
var (root, _) = LayoutHarness.Layout(LayoutHarness.Wrap(html));
25+
var box = LayoutHarness.FindById(root, "x")!;
26+
27+
Assert.AreEqual(expectedHeight, box.ActualBottom - box.Location.Y, Delta);
28+
}
29+
30+
[TestMethod]
31+
public void BreakBetweenBlocks_LeavesOneEmptyLine()
32+
{
33+
var (root, _) = LayoutHarness.Layout(LayoutHarness.Wrap("<div id='a'>a</div><br><div id='b'>b</div>"));
34+
var a = LayoutHarness.FindById(root, "a")!;
35+
var b = LayoutHarness.FindById(root, "b")!;
36+
37+
Assert.AreEqual(20, b.Location.Y - a.ActualBottom, Delta);
38+
}
39+
40+
[TestMethod]
41+
public void PreElement_DropsTheNewlineRightAfterItsStartTag()
42+
{
43+
var (root, _) = LayoutHarness.Layout(LayoutHarness.Wrap("<pre id='x'>\nline</pre><pre id='y'>line</pre>"));
44+
var withNewline = LayoutHarness.FindById(root, "x")!;
45+
var without = LayoutHarness.FindById(root, "y")!;
46+
47+
Assert.AreEqual(without.ActualBottom - without.Location.Y, withNewline.ActualBottom - withNewline.Location.Y, Delta);
48+
}
49+
50+
[TestMethod]
51+
public void TableCell_MaxContentWidth_IsTheWidestLine_NotAllLinesSummed()
52+
{
53+
var (root, _) = LayoutHarness.Layout(LayoutHarness.Wrap(
54+
"<table><tr><td id='broken'>aaaa<br>bbbb</td></tr></table>"
55+
+ "<table><tr><td id='single'>aaaa</td></tr></table>"));
56+
var broken = LayoutHarness.FindById(root, "broken")!;
57+
var single = LayoutHarness.FindById(root, "single")!;
58+
59+
Assert.AreEqual(single.ActualWidth, broken.ActualWidth, Delta);
60+
}
61+
}

0 commit comments

Comments
 (0)