Skip to content

Commit 9da170a

Browse files
authored
Merge pull request #1163 from JSv4/fix/issue-1155
fix(html): give mixed-direction bordered paragraphs their own boxes when indents differ (#1155)
2 parents 5248bf4 + d1cf3fe commit 9da170a

4 files changed

Lines changed: 99 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ All notable changes to this project will be documented in this file.
163163
unit in the CSS (`in` to `pt`) but not size, beyond the removed rounding. A tab advance that ran out
164164
of tab stops now falls back to half an inch, as intended, instead of 720 inches. The page-geometry
165165
`data-*` attributes the paginator reads still carry 0.1pt.
166+
- Consecutive bordered paragraphs that mix text directions no longer share one border box when their
167+
start and end indents differ (issue #1155). The converter joined paragraphs with the same borders
168+
and indents into one box, but a right-to-left paragraph mirrors its box (its start indent is on the
169+
right), so with `w:start="720" w:end="1440"` one paragraph of the pair got its indents on the wrong
170+
sides. Such paragraphs now get a box each, as LibreOffice draws them; paragraphs whose boxes
171+
coincide in both directions (equal start and end) still join, with between borders.
166172
- Table row heights honour `w:hRule` (issue #1122). A minimum (`atLeast`, or omitted) height now
167173
bounds the row's content and the collapsed horizontal borders are added to it, as in the reference
168174
layout (a 36pt row with 0.75pt borders advances 36.75pt); an `exact` height is the whole row and

‎Docxodus.Tests/RightToLeftParagraphBorderTests.cs‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,61 @@ public void RightToLeftIndentsMirrorWhileBordersStayPhysical()
8181
Assert.Equal("67.25pt", rtl["margin-right"]);
8282
Assert.Equal("6.5pt", rtl["margin-left"]);
8383
}
84+
// Issue #1155: consecutive bordered paragraphs share a box only when their boxes coincide.
85+
private const string SingleBorders =
86+
"<w:pBdr><w:top w:val=\"single\" w:sz=\"8\" w:space=\"4\"/><w:left w:val=\"single\" w:sz=\"8\" w:space=\"4\"/>" +
87+
"<w:bottom w:val=\"single\" w:sz=\"8\" w:space=\"4\"/><w:right w:val=\"single\" w:sz=\"8\" w:space=\"4\"/>" +
88+
"<w:between w:val=\"single\" w:sz=\"8\" w:space=\"4\"/></w:pBdr>";
89+
90+
private static string Boxed(string text, string ind, bool bidi) =>
91+
$"<w:p><w:pPr>{(bidi ? "<w:bidi/>" : "")}{SingleBorders}{ind}</w:pPr><w:r><w:t>{text}</w:t></w:r></w:p>";
92+
93+
private const string Unequal = "<w:ind w:start=\"720\" w:end=\"1440\"/>";
94+
95+
[Fact]
96+
public void MixedDirectionsWithUnequalIndentsGetMirroredBoxes()
97+
{
98+
var html = Convert(Boxed("Eastward", Unequal, bidi: false), Boxed("Westward", Unequal, bidi: true));
99+
100+
Assert.Equal(2, Wrappers(html).Count);
101+
var ltr = Style(WrapperOf(html, "Eastward"));
102+
var rtl = Style(WrapperOf(html, "Westward"));
103+
// Start 36pt and end 72pt, each less the 4pt space and the 0.75pt border.
104+
Assert.Equal("31.25pt", ltr["margin-left"]);
105+
Assert.Equal("67.25pt", ltr["margin-right"]);
106+
Assert.Equal("31.25pt", rtl["margin-right"]);
107+
Assert.Equal("67.25pt", rtl["margin-left"]);
108+
}
109+
110+
[Fact]
111+
public void MixedDirectionsWithEqualIndentsStillShareOneBox()
112+
{
113+
var equal = "<w:ind w:start=\"720\" w:end=\"720\"/>";
114+
var html = Convert(Boxed("Eastward", equal, bidi: false), Boxed("Westward", equal, bidi: true),
115+
Boxed("Onward", equal, bidi: false));
116+
117+
var wrapper = Assert.Single(Wrappers(html));
118+
Assert.Equal(3, wrapper.Descendants().Count(e => e.Name.LocalName == "p"));
119+
}
120+
121+
[Fact]
122+
public void RightToLeftParagraphsWithUnequalIndentsShareOneBox()
123+
{
124+
var html = Convert(Boxed("Westward", Unequal, bidi: true), Boxed("Seaward", Unequal, bidi: true));
125+
126+
var wrapper = Assert.Single(Wrappers(html));
127+
Assert.Equal("31.25pt", Style(wrapper)["margin-right"]);
128+
Assert.Equal("67.25pt", Style(wrapper)["margin-left"]);
129+
}
130+
[Fact]
131+
public void MixedDirectionsShareOneBoxWhenAHangingIndentMakesTheBoxSymmetric()
132+
{
133+
// Start 72pt less a 36pt hanging indent puts the box's leading edge 36pt in, the same as the
134+
// 36pt end: the box is the same in either direction, so the paragraphs join (as LibreOffice joins them).
135+
var hanging = "<w:ind w:start=\"1440\" w:hanging=\"720\" w:end=\"720\"/>";
136+
var html = Convert(Boxed("Eastward", hanging, bidi: false), Boxed("Westward", hanging, bidi: true));
137+
138+
var wrapper = Assert.Single(Wrappers(html));
139+
Assert.Equal(2, wrapper.Descendants().Count(e => e.Name.LocalName == "p"));
140+
}
84141
}

‎Docxodus/WmlToHtmlConverter.cs‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10006,11 +10006,31 @@ internal static string BorderGroupKey(XElement e, XElement? pPr)
1000610006
var ind = pPr!.Element(W.ind);
1000710007
if (ind != null)
1000810008
indStr = WithoutUnids(ind);
10009-
return WithoutUnids(pBdr) + indStr;
10009+
// A right-to-left paragraph mirrors its box (leading indent on the right), so with unequal
10010+
// leading and trailing indents it cannot share a box with a left-to-right paragraph. The
10011+
// direction joins the key only then: paragraphs whose boxes coincide still join, with
10012+
// between borders, as LibreOffice joins them (issue #1155).
10013+
var direction = HasBidi(pPr) && !BorderBoxIsMirrorSymmetric(ind) ? "|rtl" : string.Empty;
10014+
return WithoutUnids(pBdr) + indStr + direction;
1001010015
}
1001110016
return e.Name == W.tbl ? "table" : string.Empty;
1001210017
}
1001310018

10019+
private static bool HasBidi(XElement pPr) =>
10020+
pPr.Elements(W.bidi).Any(b => b.Attribute(W.val) == null || b.Attribute(W.val).ToBoolean() == true);
10021+
10022+
/// <summary>Whether a border box laid out from <paramref name="ind"/> is the same in either
10023+
/// direction: its leading edge (the leading indent less any hanging indent, as
10024+
/// <see cref="CreateBorderDivs"/> places it) and its trailing edge are equally far in.</summary>
10025+
private static bool BorderBoxIsMirrorSymmetric(XElement? ind)
10026+
{
10027+
if (ind == null) return true;
10028+
var leading = WordprocessingMLUtil.AttributeToDecimalTwips(WordprocessingMLUtil.IndLeadingAttribute(ind)) ?? 0m;
10029+
var hanging = WordprocessingMLUtil.AttributeToDecimalTwips(ind.Attribute(W.hanging)) ?? 0m;
10030+
var trailing = WordprocessingMLUtil.AttributeToDecimalTwips(WordprocessingMLUtil.IndTrailingAttribute(ind)) ?? 0m;
10031+
return Math.Max(0m, leading - hanging) == Math.Max(0m, trailing);
10032+
}
10033+
1001410034
/// <summary>The element's markup with the projector's per-element Unid bookkeeping
1001510035
/// removed. Two paragraphs with the same borders must key alike whether or not a session
1001610036
/// has stamped their property elements — otherwise a document rendered from a session

‎docs/ooxml_corner_cases.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3069,6 +3069,21 @@ Docxodus already behaved this way; `RightToLeftParagraphBorderTests` pins it. `C
30693069
the sides the paragraph's direction gives them, so each margin subtracts the outset of the border that
30703070
is physically on that side. A Word reference for this fixture would settle the last row.
30713071

3072+
### Mixed directions in one bordered group (issue #1155)
3073+
3074+
Consecutive paragraphs with identical borders and indents join into one box with `w:between` borders.
3075+
With unequal start and end indents, a left-to-right and a right-to-left paragraph have mirror-image
3076+
boxes, so they cannot share one.
3077+
3078+
| Two paragraphs, `w:ind w:start="720"`, second `w:bidi` | LibreOffice 25.8.7.3 | Docxodus before | Docxodus after |
3079+
|---|---|---|---|
3080+
| `w:end="720"` (equal) | one box, between border | one box | one box |
3081+
| `w:end="1440"` (unequal) | a box per paragraph, each mirrored | one box: one paragraph's indents on the wrong sides | a box per paragraph, each mirrored |
3082+
3083+
`BorderGroupKey` adds the direction to the key only when the box is not mirror-symmetric
3084+
(`BorderBoxIsMirrorSymmetric`), so grouping is otherwise unchanged. Word's grouping for this case has
3085+
not been measured.
3086+
30723087
## Contributing
30733088

30743089
When adding new corner cases to this document:

0 commit comments

Comments
 (0)