Skip to content
Merged
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
26 changes: 20 additions & 6 deletions Source/HtmlRenderer/Core/Dom/CssBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ public bool IsPseudoElement
/// </summary>
public bool IsInline
{
get { return (Display == CssConstants.Inline || Display == CssConstants.InlineBlock) && !IsBrElement; }
get { return Display == CssConstants.Inline || Display == CssConstants.InlineBlock; }
}

/// <summary>
Expand Down Expand Up @@ -1474,9 +1474,10 @@ internal void GetMinMaxWidth(out double minWidth, out double maxWidth)
double maxSum = 0f;
double paddingSum = 0f;
double marginSum = 0f;
GetMinMaxSumWords(this, ref min, ref maxSum, ref paddingSum, ref marginSum);
double brokenLinesMax = 0f;
GetMinMaxSumWords(this, ref min, ref maxSum, ref paddingSum, ref marginSum, ref brokenLinesMax);

maxWidth = paddingSum + maxSum;
maxWidth = paddingSum + Math.Max(maxSum, brokenLinesMax);
minWidth = paddingSum + (min < 90999 ? min : 0);
}

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

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

// add the padding
Expand All @@ -1524,6 +1529,14 @@ private static void GetMinMaxSumWords(CssBox box, ref double min, ref double max
// calculate the min and max sum for all the words in the box
foreach (CssRect word in box.Words)
{
// A line break ends the line, so the next word starts a new one instead of adding to it.
if (word.IsLineBreak)
{
brokenLinesMax = Math.Max(brokenLinesMax, maxSum);
maxSum = marginSum;
continue;
}

maxSum += word.FullWidth + (word.HasSpaceBefore ? word.OwnerBox.ActualWordSpacing : 0);
min = Math.Max(min, word.Width);
}
Expand All @@ -1542,7 +1555,7 @@ private static void GetMinMaxSumWords(CssBox box, ref double min, ref double max

//maxSum += childBox.ActualMarginLeft + childBox.ActualMarginRight;
var maxSumBeforeChild = maxSum;
GetMinMaxSumWords(childBox, ref min, ref maxSum, ref paddingSum, ref marginSum);
GetMinMaxSumWords(childBox, ref min, ref maxSum, ref paddingSum, ref marginSum, ref brokenLinesMax);

// This walk otherwise never consults a box's own explicit CSS `width` at all - only
// literal word/text content. That's usually fine (explicit width constrains layout
Expand Down Expand Up @@ -1575,7 +1588,8 @@ private static void GetMinMaxSumWords(CssBox box, ref double min, ref double max
// max sum (and its matching padding contribution) is the max of all the lines in the box
if (oldSum.HasValue)
{
maxSum = Math.Max(maxSum, oldSum.Value);
maxSum = Math.Max(Math.Max(maxSum, brokenLinesMax), oldSum.Value);
brokenLinesMax = oldBrokenLinesMax;
paddingSum = Math.Max(paddingSum, oldPaddingSum!.Value);
}
}
Expand Down
7 changes: 6 additions & 1 deletion Source/HtmlRenderer/Core/Dom/CssLayoutEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,14 @@ private static void FlowBox(RGraphics g, CssBox blockbox, CssBox box, double lim
actualLimitRight = rightObstructingFloat.Location.X - rightObstructingFloat.ActualMarginLeft - rightspacing;
}

// A line break ends the current line, so the word after it starts the next one. Wrapping on
// the break itself would add an empty line after a trailing <br> and drop the one a
// leading <br> creates.
var afterLineBreak = line.Words.Count > 0 && line.Words[line.Words.Count - 1].IsLineBreak;

if ((b.WhiteSpace != CssConstants.NoWrap && b.WhiteSpace != CssConstants.Pre && curx + word.Width + rightspacing > actualLimitRight
&& (b.WhiteSpace != CssConstants.PreWrap || !word.IsSpaces))
|| word.IsLineBreak || wrapNoWrapBox)
|| afterLineBreak || wrapNoWrapBox)
{
wrapNoWrapBox = false;
curx = startx;
Expand Down
18 changes: 18 additions & 0 deletions Source/HtmlRenderer/Core/Parse/HtmlParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,29 @@ private static void AddTextBox(HtmlDataToken token, ref CssBox curBox)

if (text == null) return;

// HTML drops a single newline directly after the <pre>, <listing> or <textarea> start tag.
if (curBox.Boxes.Count == 0 && curBox.HtmlTag != null && IsNewlineStrippingElement(curBox.HtmlTag.Name))
{
if (text.StartsWith("\r\n", StringComparison.Ordinal))
text = text.Substring(2);
else if (text.StartsWith("\n", StringComparison.Ordinal))
text = text.Substring(1);

if (text.Length == 0) return;
}

var box = CssBox.CreateBox(curBox);
box.Text = text;
}


private static bool IsNewlineStrippingElement(string tagName)
{
return tagName.Equals("pre", StringComparison.OrdinalIgnoreCase)
|| tagName.Equals("listing", StringComparison.OrdinalIgnoreCase)
|| tagName.Equals("textarea", StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Parse the html part, the part from prev parsing index to the beginning of the next html tag.<br/>
/// </summary>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Source/Test/HtmlRenderer.IntegrationTest/Baselines/RTL.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified Source/Test/HtmlRenderer.IntegrationTest/Baselines/Tables.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@

private static bool _samplesLoaded;

[TestMethod]
public async Task Render_ConsecutiveBreaks_LeaveAnEmptyLine()
{
EnsureSamplesLoaded();

using var oneBreak = await RenderSample("<span>first</span><br/><span>second</span>");
using var consecutiveBreaks = await RenderSample("<span>first</span><br/><br/><span>second</span>");

Assert.IsTrue(
consecutiveBreaks.Height > oneBreak.Height,
$"Consecutive <br> elements should add an empty line. One break: {oneBreak.Height}px; two breaks: {consecutiveBreaks.Height}px.");
}

[DoNotParallelize]
[TestMethod]
[DynamicData(nameof(GetSamples), DynamicDataDisplayName = nameof(GetSampleDisplayName))]
Expand Down Expand Up @@ -119,7 +132,7 @@
return;
}

SamplesLoader.Init("Regression", typeof(HtmlRender).Assembly.GetName().Version.ToString());

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / macOS arm64 .NET 9

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / macOS arm64 .NET 9

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Ubuntu x64 .NET 9

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Ubuntu x64 .NET 9

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Ubuntu x64 .NET 10

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Ubuntu x64 .NET 10

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Windows x64 .NET 10

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Windows x64 .NET 10

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Windows x64 .NET 8

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Windows x64 .NET 8

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / macOS arm64 .NET 8

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / macOS arm64 .NET 8

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / macOS arm64 .NET 10

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / macOS arm64 .NET 10

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Windows x64 .NET 9

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Windows x64 .NET 9

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Ubuntu x64 .NET 8

Dereference of a possibly null reference.

Check warning on line 135 in Source/Test/HtmlRenderer.IntegrationTest/HtmlRenderingRegressionTests.cs

View workflow job for this annotation

GitHub Actions / Ubuntu x64 .NET 8

Dereference of a possibly null reference.

// Baselines are captured once on some machine and compared everywhere after; a sample driven
// by prefers-color-scheme must not depend on whichever machine happens to run the test having
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using HtmlRenderer.IntegrationTest.TestSupport;

namespace HtmlRenderer.IntegrationTest.Text;

/// <summary>
/// Where lines end around &lt;br&gt;, compared against what browsers render. With the mock adapter every line
/// is 20px tall.
/// </summary>
[DoNotParallelize]
[TestClass]
public sealed class LineBreakLayoutIntegrationTests
{
private const double Delta = 0.5;

[TestMethod]
[DataRow("<div id='x'>first<br>second</div>", 40)]
[DataRow("<div id='x'>first<br><br>second</div>", 60)]
[DataRow("<div id='x'><span>first</span><br/><br/><span>second</span></div>", 60)]
[DataRow("<div id='x'>a<br></div>", 20)]
[DataRow("<div id='x'><br>b</div>", 40)]
[DataRow("<div id='x'><br></div>", 20)]
public void BlockHeight_MatchesTheNumberOfLines(string html, double expectedHeight)
{
var (root, _) = LayoutHarness.Layout(LayoutHarness.Wrap(html));
var box = LayoutHarness.FindById(root, "x")!;

Assert.AreEqual(expectedHeight, box.ActualBottom - box.Location.Y, Delta);
}

[TestMethod]
public void BreakBetweenBlocks_LeavesOneEmptyLine()
{
var (root, _) = LayoutHarness.Layout(LayoutHarness.Wrap("<div id='a'>a</div><br><div id='b'>b</div>"));
var a = LayoutHarness.FindById(root, "a")!;
var b = LayoutHarness.FindById(root, "b")!;

Assert.AreEqual(20, b.Location.Y - a.ActualBottom, Delta);
}

[TestMethod]
public void PreElement_DropsTheNewlineRightAfterItsStartTag()
{
var (root, _) = LayoutHarness.Layout(LayoutHarness.Wrap("<pre id='x'>\nline</pre><pre id='y'>line</pre>"));
var withNewline = LayoutHarness.FindById(root, "x")!;
var without = LayoutHarness.FindById(root, "y")!;

Assert.AreEqual(without.ActualBottom - without.Location.Y, withNewline.ActualBottom - withNewline.Location.Y, Delta);
}

[TestMethod]
public void TableCell_MaxContentWidth_IsTheWidestLine_NotAllLinesSummed()
{
var (root, _) = LayoutHarness.Layout(LayoutHarness.Wrap(
"<table><tr><td id='broken'>aaaa<br>bbbb</td></tr></table>"
+ "<table><tr><td id='single'>aaaa</td></tr></table>"));
var broken = LayoutHarness.FindById(root, "broken")!;
var single = LayoutHarness.FindById(root, "single")!;

Assert.AreEqual(single.ActualWidth, broken.ActualWidth, Delta);
}
}
Loading