Skip to content

Commit

Permalink
#364 Yet more work on correct boxing.
Browse files Browse the repository at this point in the history
Since we wrap generated content correctly now, one can use a border on the generated content box (such as before or after) and get Chrome matching results, rather than a border on each item in the content property list.

With test proof:
+ Test for pseudo elements with different display values.
+ Test for using an image as footnote-call.
+ Test for using an image as footnote-marker.
  • Loading branch information
danfickle committed Aug 22, 2021
1 parent 25a3b34 commit b6e6d2f
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
package com.openhtmltopdf.context;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.w3c.dom.Element;

import com.openhtmltopdf.css.constants.IdentValue;
import com.openhtmltopdf.css.extend.ContentFunction;
import com.openhtmltopdf.css.parser.CSSPrimitiveValue;
Expand Down Expand Up @@ -188,7 +189,15 @@ public boolean isStatic() {

@Override
public String calculate(RenderingContext c, FSFunction function, InlineText text) {
String uri = text.getParent().getElement().getAttribute("href");
// Due to how BoxBuilder::wrapGeneratedContent works, it is likely the immediate
// parent of text is an anonymous InlineLayoutBox so we have to go up another
// level to the wrapper box which contains the element.
Element hrefElement = text.getParent().getElement() == null ?
text.getParent().getParent().getElement() :
text.getParent().getElement();

String uri = hrefElement.getAttribute("href");

if (uri != null && uri.startsWith("#")) {
String anchor = uri.substring(1);
Box target = c.getBoxById(anchor);
Expand Down Expand Up @@ -242,7 +251,15 @@ public boolean isStatic() {

@Override
public String calculate(RenderingContext c, FSFunction function, InlineText text) {
String uri = text.getParent().getElement().getAttribute("href");
// Due to how BoxBuilder::wrapGeneratedContent works, it is likely the immediate
// parent of text is an anonymous InlineLayoutBox so we have to go up another
// level to the wrapper box which contains the element.
Element hrefElement = text.getParent().getElement() == null ?
text.getParent().getParent().getElement() :
text.getParent().getElement();

String uri = hrefElement.getAttribute("href");

if (uri != null && uri.startsWith("#")) {
String anchor = uri.substring(1);
Box target = c.getBoxById(anchor);
Expand Down Expand Up @@ -302,16 +319,46 @@ public String calculate(RenderingContext c, FSFunction function, InlineText text
// Because the leader should fill up the line, we need the correct
// width and must first compute the target-counter function.
boolean dynamic = false;
Iterator<Box> childIterator = lineBox.getChildIterator();
while (childIterator.hasNext()) {
Box child = childIterator.next();
boolean dynamic2 = false;
Box wrapperBox = iB.getParent();
List<? extends Object> children = null;

// The type of wrapperBox will depend on the CSS display property
// of the pseudo element where the content property using this function exists.
// See BoxBuilder#wrapGeneratedContent.
if (wrapperBox instanceof InlineLayoutBox) {
children = ((InlineLayoutBox) wrapperBox).getInlineChildren();
} else {
children = wrapperBox.getChildren();
}

for (Object child : children) {
if (child == iB) {
// Don't call InlineLayoutBox::lookForDynamicFunctions on this box
// as we will arrive back here and end up as a stack overflow.
// Instead set the dynamic flag so we resolve subsequent dynamic functions.
dynamic = true;
} else if (dynamic && child instanceof InlineLayoutBox) {
} else if (child instanceof InlineLayoutBox) {
// This forces the computation of dynamic functions.
((InlineLayoutBox)child).lookForDynamicFunctions(c);
}
}

// We also have to evaluate subsequent dynamic functions at the line level
// in the case that we are in the ::before and subsequent functions are in ::after.
if (dynamic) {
for (Box child : lineBox.getChildren()) {
if (wrapperBox == child) {
dynamic2 = true;
} else if (dynamic2 && child instanceof InlineLayoutBox) {
((InlineLayoutBox)child).lookForDynamicFunctions(c);
}
}
}

if (dynamic) {
// Re-calculate the width of the line after subsequent dynamic functions
// have been calculated.
int totalLineWidth = InlineBoxing.positionHorizontally(c, lineBox, 0);
lineBox.setContentWidth(totalLineWidth);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
Expand Down Expand Up @@ -849,15 +848,16 @@ private static List<Styleable> createGeneratedContentList(
Element img = doc.createElement("img");

img.setAttribute("src", value.getStringValue());
// So we don't recurse into the element and create a duplicate box.
img.setAttribute("fs-ignore", "true");
creator.appendChild(img);

CalculatedStyle anon = new EmptyStyle().createAnonymousStyle(IdentValue.INLINE_BLOCK);
CalculatedStyle anon = style.createAnonymousStyle(IdentValue.INLINE_BLOCK);

BlockBox iB = new BlockBox();
iB.setElement(img);
iB.setStyle(anon);

info.setContainsBlockLevelContent(true);
iB.setPseudoElementOrClass(peName);

result.add(iB);
} else if (value.getPropertyValueType() == PropertyValue.VALUE_TYPE_FUNCTION) {
Expand Down Expand Up @@ -1027,148 +1027,116 @@ private static void insertGeneratedContent(
}
}

/**
* Creates generated content boxes for pseudo elements such as <code>::before</code>.
*
* @param element The containing element where the pseudo element appears.
* For <code>span::before</code> the element would be a <code>span</code>.
*
* @param peName Examples include <code>before</code>, <code>after</code>,
* <code>footnote-call</code> and <code>footnote-marker</code>.
*
* @param style The child style for this pseudo element. For <code>span::before</code>
* this would include all the styles set explicitly on <code>::before</code> as well as
* those that inherit from <code>span</code> following the cascade rules.
*
* @param property The values of the <code>content</code> CSS property.
* @param info In/out param. Whether the resultant box(es) contain block level content.
* @return The generated box(es). Typically one {@link BlockBox} or multiple inline boxes.
*/
private static List<Styleable> createGeneratedContent(
LayoutContext c, Element element, String peName,
CalculatedStyle style, PropertyValue property, ChildBoxInfo info) {
if (style.isDisplayNone() || style.isIdent(CSSName.DISPLAY, IdentValue.TABLE_COLUMN)
|| style.isIdent(CSSName.DISPLAY, IdentValue.TABLE_COLUMN_GROUP)) {

if (style.isDisplayNone()
|| style.isIdent(CSSName.DISPLAY, IdentValue.TABLE_COLUMN)
|| style.isIdent(CSSName.DISPLAY, IdentValue.TABLE_COLUMN_GROUP)
|| property.getValues() == null) {
return Collections.emptyList();
}

ChildBoxInfo childInfo = new ChildBoxInfo();

List<PropertyValue> values = property.getValues();

if (values == null) {
// content: normal or content: none
return Collections.emptyList();
}

List<Styleable> result = new ArrayList<>(values.size());
Element appendTo = element;
CalculatedStyle inlineStyle = style.createAnonymousStyle(IdentValue.INLINE);

if ("footnote-call".equals(peName)) {
// Wrap the ::footnote-call content with an inline anchor box.
InlineBox aStart = new InlineBox("");

aStart.setStartsHere(true);
aStart.setEndsHere(false);

Element anchor = createFootnoteCallAnchor(c, element);
createGeneratedContentList(
c, element, values, peName, style, CONTENT_LIST_DOCUMENT, childInfo, result);

aStart.setElement(anchor);
aStart.setStyle(inlineStyle);
return wrapGeneratedContent(c, element, peName, style, info, childInfo, result);
}

appendTo = anchor;
private static List<Styleable> wrapGeneratedContent(
LayoutContext c,
Element element, String peName, CalculatedStyle style,
ChildBoxInfo info, ChildBoxInfo childInfo, List<Styleable> inlineBoxes) {

result.add(aStart);
Element wrapperElement = element;
if ("footnote-call".equals(peName)) {
wrapperElement = createFootnoteCallAnchor(c, element);
} else if ("footnote-marker".equals(peName)) {
// We need a box at the start of marker to target the ::footnote-call
// link to.
Element marker = createFootnoteTarget(c, element);

InlineBox targetStart = new InlineBox("");

targetStart.setStartsHere(true);
targetStart.setEndsHere(false);
targetStart.setElement(marker);
targetStart.setStyle(inlineStyle);
targetStart.setPseudoElementOrClass(peName);

result.add(targetStart);

appendTo = marker;
wrapperElement = createFootnoteTarget(c, element);
}

createGeneratedContentList(
c, appendTo, values, peName, style, CONTENT_LIST_DOCUMENT, childInfo, result);

if ("footnote-call".equals(peName)) {
// Wrap the ::footnote-call content with an inline anchor box.
InlineBox aEnd = new InlineBox("");

aEnd.setElement(appendTo);
aEnd.setStartsHere(false);
aEnd.setEndsHere(true);
aEnd.setStyle(inlineStyle);

result.add(aEnd);
} else if ("footnote-marker".equals(peName)) {
InlineBox targetEnd = new InlineBox("");
if (style.isInline()) {
// Because a content property like: content: counter(page) ". ";
// will generate multiple inline boxes we have to wrap them in a inline-box
// and use a child style for the generated boxes. Otherwise, if we use the
// pseudo style directly and it has a border, etc we will incorrectly get a border
// around every content box.

targetEnd.setStartsHere(false);
targetEnd.setEndsHere(true);
targetEnd.setElement(appendTo);
targetEnd.setStyle(inlineStyle);
List<Styleable> pseudoInlines = new ArrayList<>(inlineBoxes.size() + 2);

result.add(targetEnd);
}
InlineBox pseudoStart = new InlineBox("");
pseudoStart.setStartsHere(true);
pseudoStart.setEndsHere(false);
pseudoStart.setStyle(style);
pseudoStart.setElement(wrapperElement);
pseudoStart.setPseudoElementOrClass(peName);

return wrapGeneratedContent(element, peName, style, info, childInfo, result);
}
pseudoInlines.add(pseudoStart);

private static List<Styleable> wrapGeneratedContent(Element element, String peName, CalculatedStyle style,
ChildBoxInfo info, ChildBoxInfo childInfo, List<Styleable> inlineBoxes) {
if (childInfo.isContainsBlockLevelContent()) {
List<Styleable> inlines = new ArrayList<>();
CalculatedStyle inlineContent = style.createAnonymousStyle(IdentValue.INLINE);

CalculatedStyle anonStyle = style.isInlineBlock() ?
style : style.overrideStyle(IdentValue.INLINE_BLOCK);
for (Styleable styleable : inlineBoxes) {
if (styleable instanceof InlineBox) {
InlineBox iB = (InlineBox) styleable;

BlockBox result = createBlockBox(anonStyle, info, true);
result.setStyle(anonStyle);
result.setElement(element);
result.setChildrenContentType(BlockBox.ContentType.INLINE);
result.setPseudoElementOrClass(peName);
iB.setElement(null);
iB.setStyle(inlineContent);
iB.applyTextTransform();
}

CalculatedStyle anon = style.createAnonymousStyle(IdentValue.INLINE);
for (Iterator<Styleable> i = inlineBoxes.iterator(); i.hasNext();) {
Styleable b = i.next();

if (b instanceof BlockBox) {
inlines.add(b);
} else {
InlineBox iB = (InlineBox) b;

iB.setStyle(anon);
iB.applyTextTransform();
if (!isMustKeepElement(iB)) {
iB.setElement(null);
}

inlines.add(iB);
}
pseudoInlines.add(styleable);
}

if (!inlines.isEmpty()) {
result.setInlineContent(inlines);
}
InlineBox pseudoEnd = new InlineBox("");
pseudoEnd.setStartsHere(false);
pseudoEnd.setEndsHere(true);
pseudoEnd.setStyle(style);
pseudoEnd.setElement(wrapperElement);
pseudoEnd.setPseudoElementOrClass(peName);

return Collections.singletonList(result);
} else if (style.isInline()) {
for (Iterator<Styleable> i = inlineBoxes.iterator(); i.hasNext();) {
InlineBox iB = (InlineBox) i.next();
iB.setStyle(style);
iB.applyTextTransform();
}
return inlineBoxes;
pseudoInlines.add(pseudoEnd);

return pseudoInlines;
} else {
CalculatedStyle anon = style.createAnonymousStyle(IdentValue.INLINE);
for (Iterator<Styleable> i = inlineBoxes.iterator(); i.hasNext();) {
InlineBox iB = (InlineBox) i.next();
iB.setStyle(anon);
iB.applyTextTransform();

if (!isMustKeepElement(iB)) {
for (Styleable styleable : inlineBoxes) {
if (styleable instanceof InlineBox) {
InlineBox iB = (InlineBox) styleable;

iB.setElement(null);
iB.setStyle(anon);
iB.applyTextTransform();
}
}

BlockBox result = createBlockBox(style, info, true);
result.setStyle(style);
result.setInlineContent(inlineBoxes);
result.setElement(element);
result.setElement(wrapperElement);
result.setChildrenContentType(BlockBox.ContentType.INLINE);
result.setPseudoElementOrClass(peName);

Expand All @@ -1180,12 +1148,6 @@ private static List<Styleable> wrapGeneratedContent(Element element, String peNa
}
}

private static boolean isMustKeepElement(InlineBox iB) {
return iB.getElement() != null &&
("fs-footnote-marker".equals(iB.getElement().getNodeName()) ||
iB.getElement().getAttribute("href").startsWith("#fs-footnote-"));
}

private static List<Styleable> createGeneratedMarginBoxContent(
LayoutContext c, Element element, PropertyValue property,
CalculatedStyle style, ChildBoxInfo info) {
Expand Down Expand Up @@ -1337,9 +1299,11 @@ private static void createElementChild(
}

String tag = element.getNodeName();
if ("fs-footnote-marker".equals(tag) ||
("a".equals(tag) && element.getAttribute("href").startsWith("#fs-footnote"))) {
// Don't output elements that have been artificially created to support footnotes.
if (("fs-footnote-marker".equals(tag)) ||
("a".equals(tag) && element.getAttribute("href").startsWith("#fs-footnote")) ||
("img".equals(tag) && element.getAttribute("fs-ignore").equals("true"))) {
// Don't output elements that have been artificially created to support
// footnotes and content property images.
return;
}

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
</style>
</head>
<body>
<p style="margin-top: 0;">
<p style="margin: 0;">
This text needs some footnotes.
<div id="1" class="footnote">
This is a footnote.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
</style>
</head>
<body>
<p style="margin-top: 0;">
<p style="margin: 0;">
This text needs some footnotes.
<div id="1" class="footnote">
This is a footnote.
Expand Down
Loading

0 comments on commit b6e6d2f

Please sign in to comment.