com.openhtmltopdf.layout.BoxBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openhtmltopdf-core Show documentation
Show all versions of openhtmltopdf-core Show documentation
Open HTML to PDF is a CSS 2.1 renderer written in Java. This artifact contains the core rendering and layout code.
The newest version!
/*
* {{{ header & license
* Copyright (c) 2004, 2005 Torbjoern Gannholm, Joshua Marinacci
* Copyright (c) 2006 Wisconsin Court System
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
* }}}
*/
package com.openhtmltopdf.layout;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Text;
import com.openhtmltopdf.bidi.BidiSplitter;
import com.openhtmltopdf.bidi.BidiTextRun;
import com.openhtmltopdf.bidi.ParagraphSplitter.Paragraph;
import com.openhtmltopdf.css.constants.CSSName;
import com.openhtmltopdf.css.constants.IdentValue;
import com.openhtmltopdf.css.constants.MarginBoxName;
import com.openhtmltopdf.css.constants.PageElementPosition;
import com.openhtmltopdf.css.extend.ContentFunction;
import com.openhtmltopdf.css.newmatch.CascadedStyle;
import com.openhtmltopdf.css.newmatch.PageInfo;
import com.openhtmltopdf.css.parser.CSSPrimitiveValue;
import com.openhtmltopdf.css.parser.FSFunction;
import com.openhtmltopdf.css.parser.PropertyValue;
import com.openhtmltopdf.css.sheet.PropertyDeclaration;
import com.openhtmltopdf.css.sheet.StylesheetInfo;
import com.openhtmltopdf.css.style.CalculatedStyle;
import com.openhtmltopdf.css.style.EmptyStyle;
import com.openhtmltopdf.css.style.FSDerivedValue;
import com.openhtmltopdf.layout.counter.AbstractCounterContext;
import com.openhtmltopdf.layout.counter.RootCounterContext;
import com.openhtmltopdf.newtable.TableBox;
import com.openhtmltopdf.newtable.TableCellBox;
import com.openhtmltopdf.newtable.TableColumn;
import com.openhtmltopdf.newtable.TableRowBox;
import com.openhtmltopdf.newtable.TableSectionBox;
import com.openhtmltopdf.render.AnonymousBlockBox;
import com.openhtmltopdf.render.BlockBox;
import com.openhtmltopdf.render.Box;
import com.openhtmltopdf.render.FloatedBoxData;
import com.openhtmltopdf.render.FlowingColumnBox;
import com.openhtmltopdf.render.FlowingColumnContainerBox;
import com.openhtmltopdf.render.InlineBox;
import com.openhtmltopdf.util.OpenUtil;
import com.openhtmltopdf.util.XRLog;
import com.openhtmltopdf.util.LogMessageId;
/**
* This class is responsible for creating the box tree from the DOM. This is
* mostly just a one-to-one translation from the Element
to an
* InlineBox
or a BlockBox
(or some subclass of
* BlockBox
), but the tree is reorganized according to the CSS rules.
* This includes inserting anonymous block and inline boxes, anonymous table
* content, and :before
and :after
content. White
* space is also normalized at this point. Table columns and table column groups
* are added to the table which owns them, but are not created as regular boxes.
* Floated and absolutely positioned content is always treated as inline
* content for purposes of inserting anonymous block boxes and calculating
* the kind of content contained in a given block box.
*/
public class BoxBuilder {
public static final int MARGIN_BOX_VERTICAL = 1;
public static final int MARGIN_BOX_HORIZONTAL = 2;
private static final int CONTENT_LIST_DOCUMENT = 1;
private static final int CONTENT_LIST_MARGIN_BOX = 2;
/**
* Split the document into paragraphs for use in analyzing bi-directional text runs.
* @param c
* @param document
*/
private static void splitParagraphs(LayoutContext c, Document document) {
c.getParagraphSplitter().splitRoot(c, document);
c.getParagraphSplitter().runBidiOnParagraphs(c);
}
public static BlockBox createRootBox(LayoutContext c, Document document) {
splitParagraphs(c, document);
Element root = document.getDocumentElement();
CalculatedStyle style = c.getSharedContext().getStyle(root);
BlockBox result;
if (style.isTable() || style.isInlineTable()) {
result = new TableBox();
} else {
result = new BlockBox();
}
result.setStyle(style);
result.setElement(root);
c.addLayoutBoxId(root, result);
c.resolveCounters(style);
return result;
}
public static void createChildren(LayoutContext c, BlockBox parent) {
if (parent.shouldBeReplaced()) {
// Don't create boxes for elements in a SVG element.
// This avoids many warnings and improves performance.
parent.setChildrenContentType(BlockBox.ContentType.EMPTY);
return;
} else if (isInsertedBoxIgnored(parent.getElement())) {
return;
}
List children = new ArrayList<>();
ChildBoxInfo info = new ChildBoxInfo();
CalculatedStyle parentStyle = parent.getStyle();
boolean oldAllowFootnotes = c.isFootnoteAllowed();
if (parentStyle.isFixed() || parentStyle.isRunning()) {
c.setFootnoteAllowed(false);
}
createChildren(c, parent, parent.getElement(), children, info, false);
boolean parentIsNestingTableContent = isNestingTableContent(parentStyle.getIdent(
CSSName.DISPLAY));
if (!parentIsNestingTableContent && !info.isContainsTableContent()) {
resolveChildren(c, parent, children, info);
} else {
stripAllWhitespace(children);
if (parentIsNestingTableContent) {
resolveTableContent(c, parent, children, info);
} else {
resolveChildTableContent(c, parent, children, info, IdentValue.TABLE_CELL);
}
}
c.setFootnoteAllowed(oldAllowFootnotes);
// The following is very useful for debugging.
// It shows the contents of the box tree before layout.
// if (parent == c.getRootLayer().getMaster()) {
// System.out.println(com.openhtmltopdf.util.LambdaUtil.descendantDump(parent));
// }
}
private static boolean isInsertedBoxIgnored(Element element) {
if (element == null) {
return false;
}
String tag = element.getTagName();
if (!tag.startsWith("fs-")) {
return false;
}
switch (tag) {
case "fs-footnote":
case "fs-footnote-body":
return true;
default:
return false;
}
}
public static TableBox createMarginTable(
LayoutContext c,
PageInfo pageInfo,
MarginBoxName[] names,
int height,
int direction)
{
if (! pageInfo.hasAny(names)) {
return null;
}
Element source = c.getRootLayer().getMaster().getElement(); // HACK
ChildBoxInfo info = new ChildBoxInfo();
CalculatedStyle pageStyle = new EmptyStyle().deriveStyle(pageInfo.getPageStyle());
CalculatedStyle tableStyle = pageStyle.deriveStyle(
CascadedStyle.createLayoutStyle(new PropertyDeclaration[] {
new PropertyDeclaration(
CSSName.DISPLAY,
new PropertyValue(IdentValue.TABLE),
true,
StylesheetInfo.USER),
new PropertyDeclaration(
CSSName.WIDTH,
new PropertyValue(CSSPrimitiveValue.CSS_PERCENTAGE, 100.0f, "100%"),
true,
StylesheetInfo.USER),
}));
TableBox result = (TableBox)createBlockBox(tableStyle, info, false);
result.setMarginAreaRoot(true);
result.setStyle(tableStyle);
result.setElement(source);
result.setAnonymous(true);
result.setChildrenContentType(BlockBox.ContentType.BLOCK);
CalculatedStyle tableSectionStyle = pageStyle.createAnonymousStyle(IdentValue.TABLE_ROW_GROUP);
TableSectionBox section = (TableSectionBox)createBlockBox(tableSectionStyle, info, false);
section.setStyle(tableSectionStyle);
section.setElement(source);
section.setAnonymous(true);
section.setChildrenContentType(BlockBox.ContentType.BLOCK);
result.addChild(section);
TableRowBox row = null;
if (direction == MARGIN_BOX_HORIZONTAL) {
CalculatedStyle tableRowStyle = pageStyle.createAnonymousStyle(IdentValue.TABLE_ROW);
row = (TableRowBox)createBlockBox(tableRowStyle, info, false);
row.setStyle(tableRowStyle);
row.setElement(source);
row.setAnonymous(true);
row.setChildrenContentType(BlockBox.ContentType.BLOCK);
row.setHeightOverride(height);
section.addChild(row);
}
int cellCount = 0;
boolean alwaysCreate = names.length > 1 && direction == MARGIN_BOX_HORIZONTAL;
for (int i = 0; i < names.length; i++) {
CascadedStyle cellStyle = pageInfo.createMarginBoxStyle(names[i], alwaysCreate);
if (cellStyle != null) {
TableCellBox cell = createMarginBox(c, cellStyle, alwaysCreate);
if (cell != null) {
if (direction == MARGIN_BOX_VERTICAL) {
CalculatedStyle tableRowStyle = pageStyle.createAnonymousStyle(IdentValue.TABLE_ROW);
row = (TableRowBox)createBlockBox(tableRowStyle, info, false);
row.setStyle(tableRowStyle);
row.setElement(source);
row.setAnonymous(true);
row.setChildrenContentType(BlockBox.ContentType.BLOCK);
row.setHeightOverride(height);
section.addChild(row);
}
row.addChild(cell);
cellCount++;
}
}
}
if (direction == MARGIN_BOX_VERTICAL && cellCount > 0) {
int rHeight = 0;
for (Iterator i = section.getChildIterator(); i.hasNext(); ) {
TableRowBox r = (TableRowBox)i.next();
r.setHeightOverride(height / cellCount);
rHeight += r.getHeightOverride();
}
for (Iterator i = section.getChildIterator(); i.hasNext() && rHeight < height; ) {
TableRowBox r = (TableRowBox)i.next();
r.setHeightOverride(r.getHeightOverride()+1);
rHeight++;
}
}
return cellCount > 0 ? result : null;
}
private static TableCellBox createMarginBox(
LayoutContext c,
CascadedStyle cascadedStyle,
boolean alwaysCreate) {
boolean hasContent = true;
PropertyDeclaration contentDecl = cascadedStyle.propertyByName(CSSName.CONTENT);
CalculatedStyle style = new EmptyStyle().deriveStyle(cascadedStyle);
if (style.isDisplayNone() && ! alwaysCreate) {
return null;
}
if (style.isIdent(CSSName.CONTENT, IdentValue.NONE) ||
style.isIdent(CSSName.CONTENT, IdentValue.NORMAL)) {
hasContent = false;
}
if (style.isAutoWidth() && ! alwaysCreate && ! hasContent) {
return null;
}
List children = new ArrayList<>();
ChildBoxInfo info = new ChildBoxInfo();
info.setContainsTableContent(true);
info.setLayoutRunningBlocks(true);
TableCellBox result = new TableCellBox();
result.setAnonymous(true);
result.setStyle(style);
result.setElement(c.getRootLayer().getMaster().getElement()); // XXX Doesn't make sense, but we need something here
if (hasContent && ! style.isDisplayNone()) {
children.addAll(createGeneratedMarginBoxContent(
c,
c.getRootLayer().getMaster().getElement(),
(PropertyValue)contentDecl.getValue(),
style,
info));
stripAllWhitespace(children);
}
resolveChildTableContent(c, result, children, info, IdentValue.TABLE_CELL);
return result;
}
private static void resolveChildren(
LayoutContext c, BlockBox owner, List children, ChildBoxInfo info) {
if (children.size() > 0) {
if (info.isContainsBlockLevelContent()) {
insertAnonymousBlocks(
c.getSharedContext(), owner, children, info.isLayoutRunningBlocks());
owner.setChildrenContentType(BlockBox.ContentType.BLOCK);
} else {
WhitespaceStripper.stripInlineContent(children);
if (children.size() > 0) {
owner.setInlineContent(children);
owner.setChildrenContentType(BlockBox.ContentType.INLINE);
} else {
owner.setChildrenContentType(BlockBox.ContentType.EMPTY);
}
}
} else {
owner.setChildrenContentType(BlockBox.ContentType.EMPTY);
}
}
private static boolean isAllProperTableNesting(IdentValue parentDisplay, List children) {
return children.stream().allMatch(child -> isProperTableNesting(parentDisplay, child.getStyle().getIdent(CSSName.DISPLAY)));
}
/**
* Handles the situation when we find table content, but our parent is not
* table related. For example, div
-> td
.
* Anonymous tables are then constructed by repeatedly pulling together
* consecutive same-table-level siblings and wrapping them in the next
* highest table level (e.g. consecutive td
elements will
* be wrapped in an anonymous tr
, then a tbody
, and
* finally a table
).
*/
private static void resolveChildTableContent(
LayoutContext c, BlockBox parent, List children, ChildBoxInfo info, IdentValue target) {
List childrenForAnonymous = new ArrayList<>();
List childrenWithAnonymous = new ArrayList<>();
IdentValue nextUp = getPreviousTableNestingLevel(target);
for (Styleable styleable : children) {
if (matchesTableLevel(target, styleable.getStyle().getIdent(CSSName.DISPLAY))) {
childrenForAnonymous.add(styleable);
} else {
if (childrenForAnonymous.size() > 0) {
createAnonymousTableContent(c, (BlockBox) childrenForAnonymous.get(0), nextUp,
childrenForAnonymous, childrenWithAnonymous);
childrenForAnonymous = new ArrayList<>();
}
childrenWithAnonymous.add(styleable);
}
}
if (childrenForAnonymous.size() > 0) {
createAnonymousTableContent(c, (BlockBox) childrenForAnonymous.get(0), nextUp,
childrenForAnonymous, childrenWithAnonymous);
}
if (nextUp == IdentValue.TABLE) {
rebalanceInlineContent(childrenWithAnonymous);
info.setContainsBlockLevelContent(true);
resolveChildren(c, parent, childrenWithAnonymous, info);
} else {
resolveChildTableContent(c, parent, childrenWithAnonymous, info, nextUp);
}
}
private static boolean matchesTableLevel(IdentValue target, IdentValue value) {
if (target == IdentValue.TABLE_ROW_GROUP) {
return value == IdentValue.TABLE_ROW_GROUP || value == IdentValue.TABLE_HEADER_GROUP
|| value == IdentValue.TABLE_FOOTER_GROUP || value == IdentValue.TABLE_CAPTION;
} else {
return target == value;
}
}
/**
* Makes sure that any InlineBox
in content
* both starts and ends within content
. Used to ensure that
* it is always possible to construct anonymous blocks once an element's
* children has been distributed among anonymous table objects.
*/
private static void rebalanceInlineContent(List content) {
Map boxesByElement = new HashMap<>();
for (Styleable styleable : content) {
if (styleable instanceof InlineBox) {
InlineBox iB = (InlineBox) styleable;
Element elem = iB.getElement();
if (!boxesByElement.containsKey(elem)) {
iB.setStartsHere(true);
}
boxesByElement.put(elem, iB);
}
}
for (InlineBox iB : boxesByElement.values()) {
iB.setEndsHere(true);
}
}
private static void stripAllWhitespace(List content) {
int start = 0;
int current = 0;
boolean started = false;
for (current = 0; current < content.size(); current++) {
Styleable styleable = content.get(current);
if (! styleable.getStyle().isLayedOutInInlineContext()) {
if (started) {
int before = content.size();
WhitespaceStripper.stripInlineContent(content.subList(start, current));
int after = content.size();
current -= (before - after);
}
started = false;
} else {
if (! started) {
started = true;
start = current;
}
}
}
if (started) {
WhitespaceStripper.stripInlineContent(content.subList(start, current));
}
}
/**
* Handles the situation when our current parent is table related. If
* everything is properly nested (e.g. a tr
contains only
* td
elements), nothing is done. Otherwise anonymous boxes
* are inserted to ensure the integrity of the table model.
*/
private static void resolveTableContent(
LayoutContext c, BlockBox parent, List children, ChildBoxInfo info) {
IdentValue parentDisplay = parent.getStyle().getIdent(CSSName.DISPLAY);
IdentValue next = getNextTableNestingLevel(parentDisplay);
if (next == null && parent.isAnonymous() && containsOrphanedTableContent(children)) {
resolveChildTableContent(c, parent, children, info, IdentValue.TABLE_CELL);
} else if (next == null || isAllProperTableNesting(parentDisplay, children)) {
if (parent.isAnonymous()) {
rebalanceInlineContent(children);
}
resolveChildren(c, parent, children, info);
} else {
List childrenForAnonymous = new ArrayList<>();
List childrenWithAnonymous = new ArrayList<>();
for (Styleable child : children) {
IdentValue childDisplay = child.getStyle().getIdent(CSSName.DISPLAY);
if (isProperTableNesting(parentDisplay, childDisplay)) {
if (childrenForAnonymous.size() > 0) {
createAnonymousTableContent(c, parent, next, childrenForAnonymous,
childrenWithAnonymous);
childrenForAnonymous = new ArrayList<>();
}
childrenWithAnonymous.add(child);
} else {
childrenForAnonymous.add(child);
}
}
if (childrenForAnonymous.size() > 0) {
createAnonymousTableContent(c, parent, next, childrenForAnonymous,
childrenWithAnonymous);
}
info.setContainsBlockLevelContent(true);
resolveChildren(c, parent, childrenWithAnonymous, info);
}
}
private static boolean isTableRowOrRowGroup(Styleable child) {
IdentValue display = child.getStyle().getIdent(CSSName.DISPLAY);
return (display == IdentValue.TABLE_HEADER_GROUP ||
display == IdentValue.TABLE_ROW_GROUP ||
display == IdentValue.TABLE_FOOTER_GROUP ||
display == IdentValue.TABLE_ROW);
}
private static boolean containsOrphanedTableContent(List children) {
return children.stream().anyMatch(BoxBuilder::isTableRowOrRowGroup);
}
private static boolean isParentInline(BlockBox box) {
CalculatedStyle parentStyle = box.getStyle().getParent();
return parentStyle != null && parentStyle.isInline();
}
private static void createAnonymousTableContent(LayoutContext c, BlockBox source,
IdentValue next, List childrenForAnonymous, List childrenWithAnonymous) {
ChildBoxInfo nested = lookForBlockContent(childrenForAnonymous);
IdentValue anonDisplay;
if (isParentInline(source) && next == IdentValue.TABLE) {
anonDisplay = IdentValue.INLINE_TABLE;
} else {
anonDisplay = next;
}
CalculatedStyle anonStyle = source.getStyle().createAnonymousStyle(anonDisplay);
BlockBox anonBox = createBlockBox(anonStyle, nested, false);
anonBox.setStyle(anonStyle);
anonBox.setAnonymous(true);
// XXX Doesn't really make sense, but what to do?
anonBox.setElement(source.getElement());
resolveTableContent(c, anonBox, childrenForAnonymous, nested);
if (next == IdentValue.TABLE) {
childrenWithAnonymous.add(reorderTableContent(c, (TableBox) anonBox));
} else {
childrenWithAnonymous.add(anonBox);
}
}
/**
* Reorganizes a table so that the header is the first row group and the
* footer the last. If the table has caption boxes, they will be pulled
* out and added to an anonymous block box along with the table itself.
* If not, the table is returned.
*/
private static BlockBox reorderTableContent(LayoutContext c, TableBox table) {
List topCaptions = new ArrayList<>();
Box header = null;
List bodies = new ArrayList<>();
Box footer = null;
List bottomCaptions = new ArrayList<>();
for (Box b : table.getChildren()) {
IdentValue display = b.getStyle().getIdent(CSSName.DISPLAY);
if (display == IdentValue.TABLE_CAPTION) {
IdentValue side = b.getStyle().getIdent(CSSName.CAPTION_SIDE);
if (side == IdentValue.BOTTOM) {
bottomCaptions.add(b);
} else { /* side == IdentValue.TOP */
topCaptions.add(b);
}
} else if (display == IdentValue.TABLE_HEADER_GROUP && header == null) {
header = b;
} else if (display == IdentValue.TABLE_FOOTER_GROUP && footer == null) {
footer = b;
} else {
bodies.add(b);
}
}
table.removeAllChildren();
if (header != null) {
((TableSectionBox)header).setHeader(true);
table.addChild(header);
}
table.addAllChildren(bodies);
if (footer != null) {
((TableSectionBox)footer).setFooter(true);
table.addChild(footer);
}
if (topCaptions.size() == 0 && bottomCaptions.size() == 0) {
return table;
} else {
// If we have a floated table with a caption, we need to float the
// outer anonymous box and not the table
CalculatedStyle anonStyle;
if (table.getStyle().isFloated()) {
CascadedStyle cascadedStyle = CascadedStyle.createLayoutStyle(
new PropertyDeclaration[]{
CascadedStyle.createLayoutPropertyDeclaration(
CSSName.DISPLAY, IdentValue.BLOCK),
CascadedStyle.createLayoutPropertyDeclaration(
CSSName.FLOAT, table.getStyle().getIdent(CSSName.FLOAT))});
anonStyle = table.getStyle().deriveStyle(cascadedStyle);
} else {
anonStyle = table.getStyle().createAnonymousStyle(IdentValue.BLOCK);
}
BlockBox anonBox = new BlockBox();
anonBox.setStyle(anonStyle);
anonBox.setAnonymous(true);
anonBox.setFromCaptionedTable(true);
anonBox.setElement(table.getElement());
anonBox.setChildrenContentType(BlockBox.ContentType.BLOCK);
anonBox.addAllChildren(topCaptions);
anonBox.addChild(table);
anonBox.addAllChildren(bottomCaptions);
if (table.getStyle().isFloated()) {
anonBox.setFloatedBoxData(new FloatedBoxData());
table.setFloatedBoxData(null);
CascadedStyle original = c.getSharedContext().getCss().getCascadedStyle(
table.getElement(), false);
CascadedStyle modified = CascadedStyle.createLayoutStyle(
original,
new PropertyDeclaration[]{
CascadedStyle.createLayoutPropertyDeclaration(
CSSName.FLOAT, IdentValue.NONE)
});
table.setStyle(table.getStyle().getParent().deriveStyle(modified));
}
return anonBox;
}
}
private static ChildBoxInfo lookForBlockContent(List styleables) {
ChildBoxInfo result = new ChildBoxInfo();
if (styleables.stream().anyMatch(s -> !s.getStyle().isLayedOutInInlineContext())) {
result.setContainsBlockLevelContent(true);
}
return result;
}
private static IdentValue getNextTableNestingLevel(IdentValue display) {
if (display == IdentValue.TABLE || display == IdentValue.INLINE_TABLE) {
return IdentValue.TABLE_ROW_GROUP;
} else if (display == IdentValue.TABLE_HEADER_GROUP
|| display == IdentValue.TABLE_ROW_GROUP
|| display == IdentValue.TABLE_FOOTER_GROUP) {
return IdentValue.TABLE_ROW;
} else if (display == IdentValue.TABLE_ROW) {
return IdentValue.TABLE_CELL;
} else {
return null;
}
}
private static IdentValue getPreviousTableNestingLevel(IdentValue display) {
if (display == IdentValue.TABLE_CELL) {
return IdentValue.TABLE_ROW;
} else if (display == IdentValue.TABLE_ROW) {
return IdentValue.TABLE_ROW_GROUP;
} else if (display == IdentValue.TABLE_HEADER_GROUP
|| display == IdentValue.TABLE_ROW_GROUP
|| display == IdentValue.TABLE_FOOTER_GROUP) {
return IdentValue.TABLE;
} else {
return null;
}
}
private static boolean isProperTableNesting(IdentValue parent, IdentValue child) {
return (parent == IdentValue.TABLE && (child == IdentValue.TABLE_HEADER_GROUP ||
child == IdentValue.TABLE_ROW_GROUP ||
child == IdentValue.TABLE_FOOTER_GROUP ||
child == IdentValue.TABLE_CAPTION))
|| ((parent == IdentValue.TABLE_HEADER_GROUP ||
parent == IdentValue.TABLE_ROW_GROUP ||
parent == IdentValue.TABLE_FOOTER_GROUP) &&
child == IdentValue.TABLE_ROW)
|| (parent == IdentValue.TABLE_ROW && child == IdentValue.TABLE_CELL)
|| (parent == IdentValue.INLINE_TABLE && (child == IdentValue.TABLE_HEADER_GROUP ||
child == IdentValue.TABLE_ROW_GROUP ||
child == IdentValue.TABLE_FOOTER_GROUP));
}
private static boolean isNestingTableContent(IdentValue display) {
return display == IdentValue.TABLE || display == IdentValue.INLINE_TABLE ||
display == IdentValue.TABLE_HEADER_GROUP || display == IdentValue.TABLE_ROW_GROUP ||
display == IdentValue.TABLE_FOOTER_GROUP || display == IdentValue.TABLE_ROW;
}
private static boolean isAttrFunction(FSFunction function) {
if (function.getName().equals("attr")) {
List params = function.getParameters();
if (params.size() == 1) {
PropertyValue value = params.get(0);
return value.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT;
}
}
return false;
}
public static boolean isElementFunction(FSFunction function) {
if (function.getName().equals("element")) {
List params = function.getParameters();
if (params.size() < 1 || params.size() > 2) {
return false;
}
boolean ok = true;
PropertyValue value1 = params.get(0);
ok = value1.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT;
if (ok && params.size() == 2) {
PropertyValue value2 = params.get(1);
ok = value2.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT;
}
return ok;
}
return false;
}
private static CounterFunction makeCounterFunction(
FSFunction function, LayoutContext c, CalculatedStyle style) {
if (function.getName().equals("counter")) {
List params = function.getParameters();
if (params.size() < 1 || params.size() > 2) {
return null;
}
PropertyValue value = params.get(0);
if (value.getPrimitiveType() != CSSPrimitiveValue.CSS_IDENT) {
return null;
}
String s = value.getStringValue();
// counter(page) and counter(pages) are handled separately
if (s.equals("page") || s.equals("pages")) {
return null;
}
String counter = value.getStringValue();
IdentValue listStyleType = IdentValue.DECIMAL;
if (params.size() == 2) {
value = params.get(1);
if (value.getPrimitiveType() != CSSPrimitiveValue.CSS_IDENT) {
return null;
}
IdentValue identValue = IdentValue.valueOf(value.getStringValue());
if (identValue != null) {
value.setIdentValue(identValue);
listStyleType = identValue;
}
}
if ("footnote".equals(s)) {
RootCounterContext rootCc = c.getSharedContext().getGlobalCounterContext();
int counterValue = rootCc.getCurrentCounterValue(s);
return new CounterFunction(counterValue, listStyleType);
}
AbstractCounterContext cc = c.getCounterContext(style);
int counterValue = cc.getCurrentCounterValue(counter);
return new CounterFunction(counterValue, listStyleType);
} else if (function.getName().equals("counters")) {
List params = function.getParameters();
if (params.size() < 2 || params.size() > 3) {
return null;
}
PropertyValue value = params.get(0);
if (value.getPrimitiveType() != CSSPrimitiveValue.CSS_IDENT) {
return null;
}
String counter = value.getStringValue();
value = params.get(1);
if (value.getPrimitiveType() != CSSPrimitiveValue.CSS_STRING) {
return null;
}
String separator = value.getStringValue();
IdentValue listStyleType = IdentValue.DECIMAL;
if (params.size() == 3) {
value = params.get(2);
if (value.getPrimitiveType() != CSSPrimitiveValue.CSS_IDENT) {
return null;
}
IdentValue identValue = IdentValue.valueOf(value.getStringValue());
if (identValue != null) {
value.setIdentValue(identValue);
listStyleType = identValue;
}
}
List counterValues = c.getCounterContext(style).getCurrentCounterValues(counter);
return new CounterFunction(counterValues, separator, listStyleType);
} else {
return null;
}
}
private static String getAttributeValue(FSFunction attrFunc, Element e) {
PropertyValue value = attrFunc.getParameters().get(0);
return e.getAttribute(value.getStringValue());
}
private static List createGeneratedContentList(
LayoutContext c, Element element, List values,
String peName, CalculatedStyle style, int mode, ChildBoxInfo info,
List result) {
for (PropertyValue value : values) {
ContentFunction contentFunction = null;
FSFunction function = null;
String content = null;
short type = value.getPrimitiveType();
if (type == CSSPrimitiveValue.CSS_STRING) {
content = value.getStringValue();
} else if (type == CSSPrimitiveValue.CSS_URI) {
Element creator = element != null ? element : c.getRootLayer().getMaster().getElement();
Document doc = creator.getOwnerDocument();
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 = style.createAnonymousStyle(IdentValue.INLINE_BLOCK);
BlockBox iB = new BlockBox();
iB.setElement(img);
iB.setStyle(anon);
iB.setPseudoElementOrClass(peName);
result.add(iB);
} else if (value.getPropertyValueType() == PropertyValue.VALUE_TYPE_FUNCTION) {
if (mode == CONTENT_LIST_DOCUMENT && isAttrFunction(value.getFunction())) {
content = getAttributeValue(value.getFunction(), element);
} else {
CounterFunction cFunc = null;
if (mode == CONTENT_LIST_DOCUMENT) {
cFunc = makeCounterFunction(value.getFunction(), c, style);
}
if (cFunc != null) {
//TODO: counter functions may be called with non-ordered list-style-types, e.g. disc
content = cFunc.evaluate();
contentFunction = null;
function = null;
} else if (mode == CONTENT_LIST_MARGIN_BOX && isElementFunction(value.getFunction())) {
BlockBox target = getRunningBlock(c, value);
if (target != null) {
result.add(target.copyOf());
info.setContainsBlockLevelContent(true);
}
} else {
contentFunction =
c.getContentFunctionFactory().lookupFunction(c, value.getFunction());
if (contentFunction != null) {
function = value.getFunction();
if (contentFunction.isStatic()) {
content = contentFunction.calculate(c, function);
contentFunction = null;
function = null;
} else {
content = contentFunction.getLayoutReplacementText();
}
}
}
}
} else if (type == CSSPrimitiveValue.CSS_IDENT) {
FSDerivedValue dv = style.valueByName(CSSName.QUOTES);
if (dv != IdentValue.NONE) {
IdentValue ident = value.getIdentValue();
if (ident == IdentValue.OPEN_QUOTE) {
String[] quotes = style.asStringArray(CSSName.QUOTES);
content = quotes[0];
} else if (ident == IdentValue.CLOSE_QUOTE) {
String[] quotes = style.asStringArray(CSSName.QUOTES);
content = quotes[1];
}
}
}
if (content != null) {
InlineBox iB = new InlineBox(content);
iB.setContentFunction(contentFunction);
iB.setFunction(function);
iB.setElement(element);
iB.setPseudoElementOrClass(peName);
iB.setStartsHere(true);
iB.setEndsHere(true);
c.addLayoutBoxId(element, iB);
result.add(iB);
}
}
return result;
}
/**
* Creates an element with id for the footnote-marker pseudo element
* so we can link to it from the footnote-call pseduo element.
*
* See {@link #createFootnoteCallAnchor(LayoutContext, Element)}
*/
private static Element createFootnoteTarget(LayoutContext c, Element parent) {
if (parent == null) {
return null;
}
Document doc = parent.getOwnerDocument();
Element target = doc.createElement("fs-footnote-marker");
target.setAttribute("id", "fs-footnote-" + c.getFootnoteIndex());
parent.appendChild(target);
return target;
}
/**
* Used to create the anchor element to link to the footnote body for
* the footnote-call pseudo element.
*
* See {@link #createFootnoteTarget(LayoutContext, Element)}
*/
private static Element createFootnoteCallAnchor(LayoutContext c, Element parent) {
if (parent == null) {
return null;
}
Document doc = parent.getOwnerDocument();
Element anchor = doc.createElement("a");
anchor.setAttribute("href", "#fs-footnote-" + c.getFootnoteIndex());
parent.appendChild(anchor);
return anchor;
}
public static BlockBox getRunningBlock(LayoutContext c, PropertyValue value) {
List params = value.getFunction().getParameters();
String ident = params.get(0).getStringValue();
PageElementPosition position = null;
if (params.size() == 2) {
position = PageElementPosition.valueOf(params.get(1).getStringValue());
}
if (position == null) {
position = PageElementPosition.FIRST;
}
BlockBox target = c.getRootDocumentLayer().getRunningBlock(ident, c.getPage(), position);
return target;
}
private static void insertGeneratedContent(
LayoutContext c, Element element, CalculatedStyle parentStyle,
String peName, List children, ChildBoxInfo info) {
CascadedStyle peStyle = c.getCss().getPseudoElementStyle(element, peName);
if (peStyle != null) {
PropertyDeclaration contentDecl = peStyle.propertyByName(CSSName.CONTENT);
PropertyDeclaration counterResetDecl = peStyle.propertyByName(CSSName.COUNTER_RESET);
PropertyDeclaration counterIncrDecl = peStyle.propertyByName(CSSName.COUNTER_INCREMENT);
CalculatedStyle calculatedStyle = null;
if (contentDecl != null || counterResetDecl != null || counterIncrDecl != null) {
calculatedStyle = parentStyle.deriveStyle(peStyle);
if (calculatedStyle.isDisplayNone() ||
calculatedStyle.isIdent(CSSName.CONTENT, IdentValue.NONE) ||
(calculatedStyle.isIdent(CSSName.CONTENT, IdentValue.NORMAL) &&
(peName.equals("before") || peName.equals("after")))) {
return;
}
if (calculatedStyle.isTable() ||
calculatedStyle.isTableRow() ||
calculatedStyle.isTableSection()) {
calculatedStyle = parentStyle.createAnonymousStyle(IdentValue.BLOCK);
}
c.resolveCounters(calculatedStyle);
}
if (contentDecl != null) {
CSSPrimitiveValue propValue = contentDecl.getValue();
children.addAll(createGeneratedContent(c, element, peName, calculatedStyle,
(PropertyValue) propValue, info));
}
}
}
/**
* Creates generated content boxes for pseudo elements such as ::before
.
*
* @param element The containing element where the pseudo element appears.
* For span::before
the element would be a span
.
*
* @param peName Examples include before
, after
,
* footnote-call
and footnote-marker
.
*
* @param style The child style for this pseudo element. For span::before
* this would include all the styles set explicitly on ::before
as well as
* those that inherit from span
following the cascade rules.
*
* @param property The values of the content
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 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)
|| property.getValues() == null) {
return Collections.emptyList();
}
if ("footnote-call".equals(peName) || "footnote-marker".equals(peName)) {
if (!isValidFootnotePseudo(style)) {
logInvalidFootnotePseudo(peName, style);
return Collections.emptyList();
}
} else {
if (c.isInFloatBottom() && !isValidFootnotePseudo(style)) {
// ::before or ::after in footnote.
logInvalidFootnotePseudo(peName, style);
return Collections.emptyList();
} else if (style.isFootnote()) {
// ::before or ::after trying to be a footnote.
XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.GENERAL_FOOTNOTE_CAN_NOT_BE_PSEUDO, peName);
return Collections.emptyList();
}
}
ChildBoxInfo childInfo = new ChildBoxInfo();
List values = property.getValues();
List result = new ArrayList<>(values.size());
createGeneratedContentList(
c, element, values, peName, style, CONTENT_LIST_DOCUMENT, childInfo, result);
return wrapGeneratedContent(c, element, peName, style, info, childInfo, result);
}
private static List wrapGeneratedContent(
LayoutContext c,
Element element, String peName, CalculatedStyle style,
ChildBoxInfo info, ChildBoxInfo childInfo, List inlineBoxes) {
Element wrapperElement = element;
if ("footnote-call".equals(peName)) {
wrapperElement = createFootnoteCallAnchor(c, element);
} else if ("footnote-marker".equals(peName)) {
wrapperElement = createFootnoteTarget(c, element);
}
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.
List pseudoInlines = new ArrayList<>(inlineBoxes.size() + 2);
InlineBox pseudoStart = new InlineBox("");
pseudoStart.setStartsHere(true);
pseudoStart.setEndsHere(false);
pseudoStart.setStyle(style);
pseudoStart.setElement(wrapperElement);
pseudoStart.setPseudoElementOrClass(peName);
c.addLayoutBoxId(wrapperElement, pseudoStart);
pseudoInlines.add(pseudoStart);
CalculatedStyle inlineContent = style.createAnonymousStyle(IdentValue.INLINE);
for (Styleable styleable : inlineBoxes) {
if (styleable instanceof InlineBox) {
InlineBox iB = (InlineBox) styleable;
iB.setElement(null);
iB.setStyle(inlineContent);
iB.applyTextTransform();
}
pseudoInlines.add(styleable);
}
InlineBox pseudoEnd = new InlineBox("");
pseudoEnd.setStartsHere(false);
pseudoEnd.setEndsHere(true);
pseudoEnd.setStyle(style);
pseudoEnd.setElement(wrapperElement);
pseudoEnd.setPseudoElementOrClass(peName);
pseudoInlines.add(pseudoEnd);
return pseudoInlines;
} else {
CalculatedStyle anon = style.createAnonymousStyle(IdentValue.INLINE);
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(wrapperElement);
result.setChildrenContentType(BlockBox.ContentType.INLINE);
result.setPseudoElementOrClass(peName);
if (! style.isLayedOutInInlineContext()) {
info.setContainsBlockLevelContent(true);
}
c.addLayoutBoxId(wrapperElement, result);
return new ArrayList<>(Collections.singletonList(result));
}
}
private static List createGeneratedMarginBoxContent(
LayoutContext c, Element element, PropertyValue property,
CalculatedStyle style, ChildBoxInfo info) {
List values = property.getValues();
if (values == null) {
return Collections.emptyList();
}
List result = new ArrayList<>(values.size());
createGeneratedContentList(
c, element, values, null, style, CONTENT_LIST_MARGIN_BOX, info, result);
CalculatedStyle anon = style.createAnonymousStyle(IdentValue.INLINE);
for (Styleable s : result) {
if (s instanceof InlineBox) {
InlineBox iB = (InlineBox)s;
iB.setElement(null);
iB.setStyle(anon);
iB.applyTextTransform();
}
}
return result;
}
private static BlockBox createBlockBox(
CalculatedStyle style, ChildBoxInfo info, boolean generated) {
if (style.isFloated() && !(style.isAbsolute() || style.isFixed())) {
BlockBox result;
if (style.isTable() || style.isInlineTable()) {
result = new TableBox();
} else if (style.isTableCell()) {
info.setContainsTableContent(true);
result = new TableCellBox();
} else {
result = new BlockBox();
}
result.setFloatedBoxData(new FloatedBoxData());
return result;
} else if (style.isSpecifiedAsBlock()) {
return new BlockBox();
} else if (! generated && (style.isTable() || style.isInlineTable())) {
return new TableBox();
} else if (style.isTableCell()) {
info.setContainsTableContent(true);
return new TableCellBox();
} else if (! generated && style.isTableRow()) {
info.setContainsTableContent(true);
return new TableRowBox();
} else if (! generated && style.isTableSection()) {
info.setContainsTableContent(true);
return new TableSectionBox();
} else if (style.isTableCaption()) {
info.setContainsTableContent(true);
return new BlockBox();
} else {
return new BlockBox();
}
}
private static void addColumns(LayoutContext c, TableBox table, TableColumn parent) {
SharedContext sharedContext = c.getSharedContext();
Node working = parent.getElement().getFirstChild();
boolean found = false;
while (working != null) {
if (working.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) working;
CalculatedStyle style = sharedContext.getStyle(element);
if (style.isIdent(CSSName.DISPLAY, IdentValue.TABLE_COLUMN)) {
found = true;
TableColumn col = new TableColumn(element, style);
col.setParent(parent);
table.addStyleColumn(col);
}
}
working = working.getNextSibling();
}
if (! found) {
table.addStyleColumn(parent);
}
}
private static void addColumnOrColumnGroup(
LayoutContext c, TableBox table, Element e, CalculatedStyle style) {
if (style.isIdent(CSSName.DISPLAY, IdentValue.TABLE_COLUMN)) {
table.addStyleColumn(new TableColumn(e, style));
} else { /* style.isIdent(CSSName.DISPLAY, IdentValue.TABLE_COLUMN_GROUP) */
addColumns(c, table, new TableColumn(e, style));
}
}
private static InlineBox createInlineBox(
String text, Element parent, CalculatedStyle parentStyle, Text node) {
InlineBox result = new InlineBox(text);
if (parentStyle.isInline() && ! (parent.getParentNode() instanceof Document)) {
result.setStyle(parentStyle);
result.setElement(parent);
} else {
result.setStyle(parentStyle.createAnonymousStyle(IdentValue.INLINE));
}
result.applyTextTransform();
return result;
}
private static class CreateChildrenContext {
CreateChildrenContext(
boolean needStartText, boolean needEndText,
CalculatedStyle parentStyle, boolean inline) {
this.needStartText = needStartText;
this.needEndText = needEndText;
this.parentStyle = parentStyle;
this.inline = inline;
}
boolean needStartText;
boolean needEndText;
boolean inline;
InlineBox previousIB = null;
final CalculatedStyle parentStyle;
}
private static boolean isValidFootnote(
LayoutContext c, Element element, CalculatedStyle style) {
return c.isPrint() &&
(style.isInline() || style.isSpecifiedAsBlock()) &&
!style.isPostionedOrFloated() &&
!style.isRunning() &&
!c.getSharedContext().getReplacedElementFactory().isReplacedElement(element);
}
private static void logInvalidFootnoteStyle(
LayoutContext c, Element element, CalculatedStyle style) {
String cause = "";
if (!style.isInline() && !style.isSpecifiedAsBlock()) {
cause = "The footnote element should be display: block (such as )";
} else if (style.isFloated()) {
cause = "The footnote element must not be floated";
} else if (c.getSharedContext().getReplacedElementFactory().isReplacedElement(element)) {
cause = "The footnote element must not be replaced (such as )";
} else if (style.isPositioned() || style.isRunning()) {
cause = "The footnote element must have position: static (not absolute, relative, running or fixed)";
}
XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.GENERAL_FOOTNOTE_INVALID, cause);
}
private static boolean isValidFootnotePseudo(CalculatedStyle style) {
return !style.isFixed() && !style.isFootnote();
}
private static void logInvalidFootnotePseudo(String peName, CalculatedStyle style) {
String cause = "";
if (style.isFixed()) {
cause = "Footnote pseudo element (" + peName + ") may not have fixed position";
} else if (style.isFootnote()) {
cause = "Footnote pseudo element (" + peName + ") may not have float: footnote set itself";
}
XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.GENERAL_FOOTNOTE_PSEUDO_INVALID, cause);
}
/**
* Don't output elements that have been artificially created to support
* footnotes and content property images.
*/
private static boolean isGeneratedElement(Element element) {
String tag = element.getNodeName();
return ("fs-footnote-marker".equals(tag)) ||
("a".equals(tag) && element.getAttribute("href").startsWith("#fs-footnote")) ||
("img".equals(tag) && element.getAttribute("fs-ignore").equals("true"));
}
private static void createElementChild(
LayoutContext c,
Element parent,
BlockBox blockParent,
Node working,
List children,
ChildBoxInfo info,
CreateChildrenContext context) {
Styleable child = null;
SharedContext sharedContext = c.getSharedContext();
Element element = (Element) working;
CalculatedStyle style = sharedContext.getStyle(element);
if (style.isDisplayNone() || isGeneratedElement(element)) {
return;
}
resolveElementCounters(c, working, element, style);
if (style.isIdent(CSSName.DISPLAY, IdentValue.TABLE_COLUMN) ||
style.isIdent(CSSName.DISPLAY, IdentValue.TABLE_COLUMN_GROUP)) {
if ((blockParent != null) &&
(blockParent.getStyle().isTable() || blockParent.getStyle().isInlineTable())) {
TableBox table = (TableBox) blockParent;
addColumnOrColumnGroup(c, table, element, style);
}
return;
}
if (style.isFootnote() && !c.isInFloatBottom() && c.isFootnoteAllowed()) {
if (isValidFootnote(c, element, style)) {
c.setFootnoteIndex(c.getFootnoteIndex() + 1);
// This is the official footnote call content that can generate zero or more boxes
// depending on user for ::footnote-call pseudo element.
insertGeneratedContent(c, element, style, "footnote-call", children, info);
BlockBox footnoteBody = createFootnoteBody(c, element, style);
// This is purely a marker box for the footnote so we
// can figure out in layout when to add the footnote body.
InlineBox iB = createInlineBox("", parent, context.parentStyle, null);
iB.setStartsHere(true);
iB.setEndsHere(true);
iB.setFootnote(footnoteBody);
children.add(iB);
return;
} else {
logInvalidFootnoteStyle(c, element, style);
}
}
if (style.isInline()) {
createInlineChildren(c, parent, children, info, context, element);
} else {
child = createChildBlockBox(c, info, element, style);
}
if (child != null) {
children.add(child);
}
}
private static void createInlineChildren(
LayoutContext c,
Element parent,
List children,
ChildBoxInfo info,
CreateChildrenContext context,
Element element) {
if (context.needStartText) {
context.needStartText = false;
InlineBox iB = createInlineBox("", parent, context.parentStyle, null);
iB.setStartsHere(true);
iB.setEndsHere(false);
children.add(iB);
context.previousIB = iB;
}
createChildren(c, null, element, children, info, true);
if (context.inline) {
if (context.previousIB != null) {
context.previousIB.setEndsHere(false);
}
context.needEndText = true;
}
}
private static Styleable createChildBlockBox(
LayoutContext c, ChildBoxInfo info, Element element, CalculatedStyle style) {
Styleable child;
if (style.hasColumns() && c.isPrint()) {
child = new FlowingColumnContainerBox();
} else {
child = createBlockBox(style, info, false);
}
child.setStyle(style);
child.setElement(element);
c.addLayoutBoxId(element, child);
if (style.hasColumns() && c.isPrint()) {
createColumnContainer(c, child, element, style);
}
if (style.isListItem()) {
BlockBox block = (BlockBox) child;
block.setListCounter(c.getCounterContext(style).getCurrentCounterValue("list-item"));
}
if (style.isTable() || style.isInlineTable()) {
TableBox table = (TableBox) child;
table.ensureChildren(c);
child = reorderTableContent(c, table);
}
if (!info.isContainsBlockLevelContent()
&& !style.isLayedOutInInlineContext()) {
info.setContainsBlockLevelContent(true);
}
BlockBox block = (BlockBox) child;
if (block.getStyle().mayHaveFirstLine()) {
block.setFirstLineStyle(c.getCss().getPseudoElementStyle(element,
"first-line"));
}
if (block.getStyle().mayHaveFirstLetter()) {
block.setFirstLetterStyle(c.getCss().getPseudoElementStyle(element,
"first-letter"));
}
// I think we need to do this to evaluate counters correctly
block.ensureChildren(c);
return child;
}
/**
* Creates the footnote body to put at the bottom of the page inside a
* page's footnote area.
*/
private static BlockBox createFootnoteBody(
LayoutContext c, Element element, CalculatedStyle style) {
List footnoteChildren = new ArrayList<>();
ChildBoxInfo footnoteChildInfo = new ChildBoxInfo();
// Create the out-of-flow footnote-body box as a block box.
BlockBox footnoteBody = new BlockBox();
CalculatedStyle footnoteBodyStyle = style.createAnonymousStyle(IdentValue.BLOCK);
// Create a dummy element for the footnote-body.
Element fnBodyElement = element.getOwnerDocument().createElement("fs-footnote-body");
c.getRootLayer().getMaster().getElement().appendChild(fnBodyElement);
footnoteBody.setElement(fnBodyElement);
footnoteBody.setStyle(footnoteBodyStyle);
// This will be set to the same as the footnote area when we add it to the page.
footnoteBody.setContainingBlock(null);
// Create an isolated layer for now. This will be changed to the footnote area
// layer when we add this footnote body to a page.
Layer layer = new Layer(footnoteBody, c, true);
footnoteBody.setLayer(layer);
footnoteBody.setContainingLayer(layer);
c.pushLayer(layer);
c.setIsInFloatBottom(true);
CreateChildrenContext context = new CreateChildrenContext(false, false, style.getParent(), false);
createElementChild(c, (Element) element.getParentNode(), footnoteBody, element, footnoteChildren, footnoteChildInfo, context);
resolveChildren(c, footnoteBody, footnoteChildren, footnoteChildInfo);
c.setFootnoteAllowed(true);
c.setIsInFloatBottom(false);
c.popLayer();
// System.out.println();
// System.out.println(com.openhtmltopdf.util.LambdaUtil.descendantDump(footnoteBody));
// System.out.println();
return footnoteBody;
}
private static void createColumnContainer(
LayoutContext c, Styleable child, Element element, CalculatedStyle style) {
FlowingColumnContainerBox cont = (FlowingColumnContainerBox) child;
cont.setOnlyChild(c, new FlowingColumnBox(cont));
cont.getChild().setStyle(style.createAnonymousStyle(IdentValue.BLOCK));
cont.getChild().setElement(element);
cont.getChild().ensureChildren(c);
}
private static void resolveElementCounters(
LayoutContext c, Node working, Element element, CalculatedStyle style) {
Integer attrValue = null;
if ("ol".equals(working.getNodeName()) && element.hasAttribute("start")) {
attrValue = OpenUtil.parseIntegerOrNull(element.getAttribute("start"));
} else if ("li".equals(working.getNodeName()) && element.hasAttribute("value")) {
attrValue = OpenUtil.parseIntegerOrNull(element.getAttribute("value"));
}
if (attrValue != null) {
c.resolveCounters(style, attrValue - 1);
} else {
c.resolveCounters(style, null);
}
}
private static void createChildren(
LayoutContext c, BlockBox blockParent, Element parent,
List children, ChildBoxInfo info, boolean inline) {
if (isInsertedBoxIgnored(parent)) {
return;
}
SharedContext sharedContext = c.getSharedContext();
CalculatedStyle parentStyle = sharedContext.getStyle(parent);
insertGeneratedContent(c, parent, parentStyle, "before", children, info);
if (parentStyle.isFootnote()) {
if (c.isFootnoteAllowed() && isValidFootnote(c, parent, parentStyle)) {
insertGeneratedContent(c, parent, parentStyle, "footnote-marker", children, info);
// Ban further footnote content until we bubble back up to createFootnoteBody.
c.setFootnoteAllowed(false);
} else if (!c.isFootnoteAllowed()) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId0Param.GENERAL_NO_FOOTNOTES_INSIDE_FOOTNOTES);
}
}
Node working = parent.getFirstChild();
CreateChildrenContext context = null;
if (working != null) {
context = new CreateChildrenContext(inline, inline, parentStyle, inline);
do {
short nodeType = working.getNodeType();
if (nodeType == Node.ELEMENT_NODE) {
createElementChild(
c, parent, blockParent, working, children, info, context);
} else if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) {
context.needStartText = false;
context.needEndText = false;
Text textNode = (Text) working;
// Ignore the text belonging to a textarea.
if (!textNode.getParentNode().getNodeName().equals("textarea")) {
context.previousIB = doBidi(c, textNode, parent, parentStyle, context.previousIB, children);
}
}
} while ((working = working.getNextSibling()) != null);
}
boolean needStartText = context != null ? context.needStartText : inline;
boolean needEndText = context != null ? context.needEndText : inline;
if (needStartText || needEndText) {
InlineBox iB = createInlineBox("", parent, parentStyle, null);
iB.setStartsHere(needStartText);
iB.setEndsHere(needEndText);
children.add(iB);
}
insertGeneratedContent(c, parent, parentStyle, "after", children, info);
}
private static InlineBox setupInlineChild(InlineBox child, InlineBox previousIB) {
child.setEndsHere(true);
if (previousIB == null) {
child.setStartsHere(true);
} else {
previousIB.setEndsHere(false);
}
return child;
}
private static InlineBox doFakeBidi(LayoutContext c, Text textNode, Element parent, CalculatedStyle parentStyle, InlineBox previousIB, List children) {
String runText = textNode.getData();
InlineBox child = createInlineBox(runText, parent, parentStyle, textNode);
child.setTextDirection(BidiSplitter.LTR);
previousIB = setupInlineChild(child, previousIB);
children.add(child);
return previousIB;
}
/**
* Attempts to divide a Text node further into directional text runs, either LTR or RTL.
* @param c
* @param textNode
* @param parent
* @param parentStyle
* @return the previousIB.
*/
private static InlineBox doBidi(LayoutContext c, Text textNode, Element parent, CalculatedStyle parentStyle, InlineBox previousIB, List children) {
Paragraph para = c.getParagraphSplitter().lookupParagraph(textNode);
if (para == null) {
// Must be no implementation of BIDI for this Text node.
return doFakeBidi(c, textNode, parent, parentStyle, previousIB, children);
}
int startIndex = para.getFirstCharIndexInParagraph(textNode); // Index into the paragraph.
if (startIndex < 0) {
// Must be a fake implementation of BIDI.
return doFakeBidi(c, textNode, parent, parentStyle, previousIB, children);
}
int nodeIndex = 0; // Index into the text node.
String runText; // Calculated text for the directional run.
BidiTextRun prevSplit = para.prevSplit(startIndex); // Get directional run at or before startIndex.
assert(prevSplit != null); // There should always be a split at zero (start of paragraph) to fall back on.
assert(prevSplit.getStart() <= startIndex); // Split should always be before or at the start of this text node.
// When calculating length, remember that it may overlap the start and/or end of the text node.
int maxRunLength = prevSplit.getLength() - (startIndex - prevSplit.getStart());
int splitLength = Math.min(maxRunLength, textNode.getLength());
// Advance char indexes.
nodeIndex += splitLength;
startIndex += splitLength;
assert(prevSplit.getDirection() == BidiSplitter.LTR || prevSplit.getDirection() == BidiSplitter.RTL);
if (splitLength == textNode.getLength()) {
// The simple case: the entire text node is part of a single direction run.
runText = textNode.getData();
}
else {
// The complex case: the first directional run only encompasses part of the text node.
runText = textNode.getData().substring(0, nodeIndex);
}
// Shape here, so the layout will get the right visual length for the run.
if (prevSplit.getDirection() == BidiSplitter.RTL) {
runText = c.getBidiReorderer().shapeText(runText);
}
InlineBox child = createInlineBox(runText, parent, parentStyle, textNode);
child.setTextDirection(prevSplit.getDirection());
previousIB = setupInlineChild(child, previousIB);
children.add(child);
if (splitLength != textNode.getLength()) {
// We have more directional runs to extract.
do {
BidiTextRun newSplit = para.nextSplit(startIndex);
assert(newSplit != null); // There should always be enough splits to completely cover the text node.
int newLength;
if (newSplit != null) {
// When calculating length, remember that it may overlap the start and/or end of the text node.
int newMaxRunLength = newSplit.getLength() - (startIndex - newSplit.getStart());
newLength = Math.min(newMaxRunLength, textNode.getLength() - nodeIndex);
runText = textNode.getData().substring(nodeIndex, nodeIndex + newLength);
// Shape here, so the layout will get the right visual length for the run.
if (newSplit.getDirection() == BidiSplitter.RTL) {
runText = c.getBidiReorderer().shapeText(runText);
}
startIndex += newLength;
nodeIndex += newLength;
child = createInlineBox(runText, parent, parentStyle, textNode);
child.setTextDirection(newSplit.getDirection());
previousIB = setupInlineChild(child, previousIB);
children.add(child);
}
else {
// We should never get here, but handle it just in case.
newLength = textNode.getLength() - nodeIndex;
runText = textNode.getData().substring(nodeIndex, newLength);
child = createInlineBox(runText, parent, parentStyle, textNode);
child.setTextDirection(c.getDefaultTextDirection());
previousIB = setupInlineChild(child, previousIB);
children.add(child);
startIndex += newLength;
nodeIndex += newLength;
}
} while(nodeIndex < textNode.getLength());
}
return previousIB;
}
private static void insertAnonymousBlocks(
SharedContext c, Box parent, List children, boolean layoutRunningBlocks) {
List inline = new ArrayList<>();
Deque parents = new ArrayDeque<>();
List savedParents = null;
for (Styleable child : children) {
if (child.getStyle().isLayedOutInInlineContext() &&
! (layoutRunningBlocks && child.getStyle().isRunning()) &&
!child.getStyle().isTableCell() //see issue https://github.com/danfickle/openhtmltopdf/issues/309
) {
inline.add(child);
if (child.getStyle().isInline()) {
InlineBox iB = (InlineBox) child;
if (iB.isStartsHere()) {
parents.add(iB);
}
if (iB.isEndsHere()) {
parents.removeLast();
}
}
} else {
if (inline.size() > 0) {
createAnonymousBlock(c, parent, inline, savedParents);
inline = new ArrayList<>();
savedParents = new ArrayList<>(parents);
}
parent.addChild((Box) child);
}
}
createAnonymousBlock(c, parent, inline, savedParents);
}
private static void createAnonymousBlock(SharedContext c, Box parent, List inline, List savedParents) {
createAnonymousBlock(c, parent, inline, savedParents, IdentValue.BLOCK);
}
private static void createAnonymousBlock(SharedContext c, Box parent, List inline, List savedParents, IdentValue display) {
WhitespaceStripper.stripInlineContent(inline);
if (inline.size() > 0) {
AnonymousBlockBox anon = new AnonymousBlockBox(parent.getElement());
anon.setStyle(parent.getStyle().createAnonymousStyle(display));
anon.setAnonymous(true);
if (savedParents != null && savedParents.size() > 0) {
anon.setOpenInlineBoxes(savedParents);
}
parent.addChild(anon);
anon.setChildrenContentType(BlockBox.ContentType.INLINE);
anon.setInlineContent(inline);
}
}
private static class ChildBoxInfo {
private boolean _containsBlockLevelContent;
private boolean _containsTableContent;
private boolean _layoutRunningBlocks;
public ChildBoxInfo() {
}
public boolean isContainsBlockLevelContent() {
return _containsBlockLevelContent;
}
public void setContainsBlockLevelContent(boolean containsBlockLevelContent) {
_containsBlockLevelContent = containsBlockLevelContent;
}
public boolean isContainsTableContent() {
return _containsTableContent;
}
public void setContainsTableContent(boolean containsTableContent) {
_containsTableContent = containsTableContent;
}
public boolean isLayoutRunningBlocks() {
return _layoutRunningBlocks;
}
public void setLayoutRunningBlocks(boolean layoutRunningBlocks) {
_layoutRunningBlocks = layoutRunningBlocks;
}
@Override
public String toString() {
return String.format(
"ChildBoxInfo [_containsBlockLevelContent=%s, _containsTableContent=%s, _layoutRunningBlocks=%s]",
_containsBlockLevelContent, _containsTableContent, _layoutRunningBlocks);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy