blob: c6c36ae67f50397136d70fc1f1be300a4b2cf638 [file] [log] [blame]
/*
* Copyright (C) 1997 Martin Jones (mjones@kde.org)
* (C) 1997 Torben Weis (weis@kde.org)
* (C) 1998 Waldo Bastian (bastian@kde.org)
* (C) 1999 Lars Knoll (knoll@kde.org)
* (C) 1999 Antti Koivisto (koivisto@kde.org)
* Copyright (C) 2003, 2004, 2005, 2006, 2008, 2010, 2011 Apple Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "config.h"
#include "HTMLTableElement.h"
#include "CSSImageValue.h"
#include "CSSPropertyNames.h"
#include "CSSValueKeywords.h"
#include "CSSValuePool.h"
#include "ElementChildIterator.h"
#include "GenericCachedHTMLCollection.h"
#include "HTMLNames.h"
#include "HTMLParserIdioms.h"
#include "HTMLTableCaptionElement.h"
#include "HTMLTableRowElement.h"
#include "HTMLTableRowsCollection.h"
#include "HTMLTableSectionElement.h"
#include "NodeRareData.h"
#include "RenderTable.h"
#include "StyleProperties.h"
#include <wtf/IsoMallocInlines.h>
#include <wtf/Ref.h>
namespace WebCore {
WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLTableElement);
using namespace HTMLNames;
HTMLTableElement::HTMLTableElement(const QualifiedName& tagName, Document& document)
: HTMLElement(tagName, document)
{
ASSERT(hasTagName(tableTag));
}
Ref<HTMLTableElement> HTMLTableElement::create(Document& document)
{
return adoptRef(*new HTMLTableElement(tableTag, document));
}
Ref<HTMLTableElement> HTMLTableElement::create(const QualifiedName& tagName, Document& document)
{
return adoptRef(*new HTMLTableElement(tagName, document));
}
RefPtr<HTMLTableCaptionElement> HTMLTableElement::caption() const
{
return childrenOfType<HTMLTableCaptionElement>(const_cast<HTMLTableElement&>(*this)).first();
}
ExceptionOr<void> HTMLTableElement::setCaption(RefPtr<HTMLTableCaptionElement>&& newCaption)
{
deleteCaption();
if (!newCaption)
return { };
return insertBefore(*newCaption, firstChild());
}
RefPtr<HTMLTableSectionElement> HTMLTableElement::tHead() const
{
for (RefPtr<Node> child = firstChild(); child; child = child->nextSibling()) {
if (child->hasTagName(theadTag))
return downcast<HTMLTableSectionElement>(child.get());
}
return nullptr;
}
ExceptionOr<void> HTMLTableElement::setTHead(RefPtr<HTMLTableSectionElement>&& newHead)
{
if (UNLIKELY(newHead && !newHead->hasTagName(theadTag)))
return Exception { HierarchyRequestError };
deleteTHead();
if (!newHead)
return { };
RefPtr<Node> child;
for (child = firstChild(); child; child = child->nextSibling()) {
if (child->isElementNode() && !child->hasTagName(captionTag) && !child->hasTagName(colgroupTag))
break;
}
return insertBefore(*newHead, child.get());
}
RefPtr<HTMLTableSectionElement> HTMLTableElement::tFoot() const
{
for (RefPtr<Node> child = firstChild(); child; child = child->nextSibling()) {
if (child->hasTagName(tfootTag))
return downcast<HTMLTableSectionElement>(child.get());
}
return nullptr;
}
ExceptionOr<void> HTMLTableElement::setTFoot(RefPtr<HTMLTableSectionElement>&& newFoot)
{
if (UNLIKELY(newFoot && !newFoot->hasTagName(tfootTag)))
return Exception { HierarchyRequestError };
deleteTFoot();
if (!newFoot)
return { };
return appendChild(*newFoot);
}
Ref<HTMLTableSectionElement> HTMLTableElement::createTHead()
{
if (auto existingHead = tHead())
return existingHead.releaseNonNull();
auto head = HTMLTableSectionElement::create(theadTag, document());
setTHead(head.copyRef());
return head;
}
void HTMLTableElement::deleteTHead()
{
if (auto head = tHead())
removeChild(*head);
}
Ref<HTMLTableSectionElement> HTMLTableElement::createTFoot()
{
if (auto existingFoot = tFoot())
return existingFoot.releaseNonNull();
auto foot = HTMLTableSectionElement::create(tfootTag, document());
setTFoot(foot.copyRef());
return foot;
}
void HTMLTableElement::deleteTFoot()
{
if (auto foot = tFoot())
removeChild(*foot);
}
Ref<HTMLTableSectionElement> HTMLTableElement::createTBody()
{
auto body = HTMLTableSectionElement::create(tbodyTag, document());
RefPtr<Node> referenceElement = lastBody() ? lastBody()->nextSibling() : nullptr;
insertBefore(body, referenceElement.get());
return body;
}
Ref<HTMLTableCaptionElement> HTMLTableElement::createCaption()
{
if (auto existingCaption = caption())
return existingCaption.releaseNonNull();
auto caption = HTMLTableCaptionElement::create(captionTag, document());
setCaption(caption.copyRef());
return caption;
}
void HTMLTableElement::deleteCaption()
{
if (auto caption = this->caption())
removeChild(*caption);
}
HTMLTableSectionElement* HTMLTableElement::lastBody() const
{
for (RefPtr<Node> child = lastChild(); child; child = child->previousSibling()) {
if (child->hasTagName(tbodyTag))
return downcast<HTMLTableSectionElement>(child.get());
}
return nullptr;
}
ExceptionOr<Ref<HTMLElement>> HTMLTableElement::insertRow(int index)
{
if (index < -1)
return Exception { IndexSizeError };
Ref<HTMLTableElement> protectedThis(*this);
RefPtr<HTMLTableRowElement> lastRow;
RefPtr<HTMLTableRowElement> row;
if (index == -1)
lastRow = HTMLTableRowsCollection::lastRow(*this);
else {
for (int i = 0; i <= index; ++i) {
row = HTMLTableRowsCollection::rowAfter(*this, lastRow.get());
if (!row) {
if (i != index)
return Exception { IndexSizeError };
break;
}
lastRow = row;
}
}
RefPtr<ContainerNode> parent;
if (lastRow)
parent = row ? row->parentNode() : lastRow->parentNode();
else {
parent = lastBody();
if (!parent) {
auto newBody = HTMLTableSectionElement::create(tbodyTag, document());
auto newRow = HTMLTableRowElement::create(document());
newBody->appendChild(newRow);
// FIXME: Why ignore the exception if the first appendChild failed?
auto result = appendChild(newBody);
if (result.hasException())
return result.releaseException();
return Ref<HTMLElement> { WTFMove(newRow) };
}
}
auto newRow = HTMLTableRowElement::create(document());
auto result = parent->insertBefore(newRow, row.get());
if (result.hasException())
return result.releaseException();
return Ref<HTMLElement> { WTFMove(newRow) };
}
ExceptionOr<void> HTMLTableElement::deleteRow(int index)
{
RefPtr<HTMLTableRowElement> row;
if (index == -1) {
row = HTMLTableRowsCollection::lastRow(*this);
if (!row)
return { };
} else {
for (int i = 0; i <= index; ++i) {
row = HTMLTableRowsCollection::rowAfter(*this, row.get());
if (!row)
break;
}
if (!row)
return Exception { IndexSizeError };
}
return row->remove();
}
static inline bool isTableCellAncestor(const Element& element)
{
return element.hasTagName(theadTag)
|| element.hasTagName(tbodyTag)
|| element.hasTagName(tfootTag)
|| element.hasTagName(trTag)
|| element.hasTagName(thTag);
}
static bool setTableCellsChanged(Element& element)
{
bool cellChanged = false;
if (element.hasTagName(tdTag))
cellChanged = true;
else if (isTableCellAncestor(element)) {
for (auto& child : childrenOfType<Element>(element))
cellChanged |= setTableCellsChanged(child);
}
if (cellChanged)
element.invalidateStyleForSubtree();
return cellChanged;
}
static bool getBordersFromFrameAttributeValue(const AtomString& value, bool& borderTop, bool& borderRight, bool& borderBottom, bool& borderLeft)
{
borderTop = false;
borderRight = false;
borderBottom = false;
borderLeft = false;
if (equalLettersIgnoringASCIICase(value, "above"))
borderTop = true;
else if (equalLettersIgnoringASCIICase(value, "below"))
borderBottom = true;
else if (equalLettersIgnoringASCIICase(value, "hsides"))
borderTop = borderBottom = true;
else if (equalLettersIgnoringASCIICase(value, "vsides"))
borderLeft = borderRight = true;
else if (equalLettersIgnoringASCIICase(value, "lhs"))
borderLeft = true;
else if (equalLettersIgnoringASCIICase(value, "rhs"))
borderRight = true;
else if (equalLettersIgnoringASCIICase(value, "box") || equalLettersIgnoringASCIICase(value, "border"))
borderTop = borderBottom = borderLeft = borderRight = true;
else if (!equalLettersIgnoringASCIICase(value, "void"))
return false;
return true;
}
void HTMLTableElement::collectPresentationalHintsForAttribute(const QualifiedName& name, const AtomString& value, MutableStyleProperties& style)
{
if (name == widthAttr)
addHTMLLengthToStyle(style, CSSPropertyWidth, value);
else if (name == heightAttr)
addHTMLLengthToStyle(style, CSSPropertyHeight, value);
else if (name == borderAttr)
addPropertyToPresentationalHintStyle(style, CSSPropertyBorderWidth, parseBorderWidthAttribute(value), CSSUnitType::CSS_PX);
else if (name == bordercolorAttr) {
if (!value.isEmpty())
addHTMLColorToStyle(style, CSSPropertyBorderColor, value);
} else if (name == bgcolorAttr)
addHTMLColorToStyle(style, CSSPropertyBackgroundColor, value);
else if (name == backgroundAttr) {
String url = stripLeadingAndTrailingHTMLSpaces(value);
if (!url.isEmpty())
style.setProperty(CSSProperty(CSSPropertyBackgroundImage, CSSImageValue::create(document().completeURL(url), LoadedFromOpaqueSource::No)));
} else if (name == valignAttr) {
if (!value.isEmpty())
addPropertyToPresentationalHintStyle(style, CSSPropertyVerticalAlign, value);
} else if (name == cellspacingAttr) {
if (!value.isEmpty())
addHTMLPixelsToStyle(style, CSSPropertyBorderSpacing, value);
} else if (name == vspaceAttr) {
addHTMLLengthToStyle(style, CSSPropertyMarginTop, value);
addHTMLLengthToStyle(style, CSSPropertyMarginBottom, value);
} else if (name == hspaceAttr) {
addHTMLLengthToStyle(style, CSSPropertyMarginLeft, value);
addHTMLLengthToStyle(style, CSSPropertyMarginRight, value);
} else if (name == alignAttr) {
if (!value.isEmpty()) {
if (equalLettersIgnoringASCIICase(value, "center")) {
addPropertyToPresentationalHintStyle(style, CSSPropertyMarginInlineStart, CSSValueAuto);
addPropertyToPresentationalHintStyle(style, CSSPropertyMarginInlineEnd, CSSValueAuto);
} else
addPropertyToPresentationalHintStyle(style, CSSPropertyFloat, value);
}
} else if (name == rulesAttr) {
// The presence of a valid rules attribute causes border collapsing to be enabled.
if (m_rulesAttr != UnsetRules)
addPropertyToPresentationalHintStyle(style, CSSPropertyBorderCollapse, CSSValueCollapse);
} else if (name == frameAttr) {
bool borderTop;
bool borderRight;
bool borderBottom;
bool borderLeft;
if (getBordersFromFrameAttributeValue(value, borderTop, borderRight, borderBottom, borderLeft)) {
addPropertyToPresentationalHintStyle(style, CSSPropertyBorderWidth, CSSValueThin);
addPropertyToPresentationalHintStyle(style, CSSPropertyBorderTopStyle, borderTop ? CSSValueSolid : CSSValueHidden);
addPropertyToPresentationalHintStyle(style, CSSPropertyBorderBottomStyle, borderBottom ? CSSValueSolid : CSSValueHidden);
addPropertyToPresentationalHintStyle(style, CSSPropertyBorderLeftStyle, borderLeft ? CSSValueSolid : CSSValueHidden);
addPropertyToPresentationalHintStyle(style, CSSPropertyBorderRightStyle, borderRight ? CSSValueSolid : CSSValueHidden);
}
} else
HTMLElement::collectPresentationalHintsForAttribute(name, value, style);
}
bool HTMLTableElement::hasPresentationalHintsForAttribute(const QualifiedName& name) const
{
if (name == widthAttr || name == heightAttr || name == bgcolorAttr || name == backgroundAttr || name == valignAttr || name == vspaceAttr || name == hspaceAttr || name == cellspacingAttr || name == borderAttr || name == bordercolorAttr || name == frameAttr || name == rulesAttr)
return true;
return HTMLElement::hasPresentationalHintsForAttribute(name);
}
void HTMLTableElement::parseAttribute(const QualifiedName& name, const AtomString& value)
{
CellBorders bordersBefore = cellBorders();
unsigned short oldPadding = m_padding;
if (name == borderAttr) {
// FIXME: This attribute is a mess.
m_borderAttr = parseBorderWidthAttribute(value);
} else if (name == bordercolorAttr) {
m_borderColorAttr = !value.isEmpty();
} else if (name == frameAttr) {
// FIXME: This attribute is a mess.
bool borderTop;
bool borderRight;
bool borderBottom;
bool borderLeft;
m_frameAttr = getBordersFromFrameAttributeValue(value, borderTop, borderRight, borderBottom, borderLeft);
} else if (name == rulesAttr) {
m_rulesAttr = UnsetRules;
if (equalLettersIgnoringASCIICase(value, "none"))
m_rulesAttr = NoneRules;
else if (equalLettersIgnoringASCIICase(value, "groups"))
m_rulesAttr = GroupsRules;
else if (equalLettersIgnoringASCIICase(value, "rows"))
m_rulesAttr = RowsRules;
else if (equalLettersIgnoringASCIICase(value, "cols"))
m_rulesAttr = ColsRules;
else if (equalLettersIgnoringASCIICase(value, "all"))
m_rulesAttr = AllRules;
} else if (name == cellpaddingAttr) {
if (!value.isEmpty())
m_padding = std::max(0, parseHTMLInteger(value).value_or(0));
else
m_padding = 1;
} else if (name == colsAttr) {
// ###
} else
HTMLElement::parseAttribute(name, value);
if (bordersBefore != cellBorders() || oldPadding != m_padding) {
m_sharedCellStyle = nullptr;
bool cellChanged = false;
for (auto& child : childrenOfType<Element>(*this))
cellChanged |= setTableCellsChanged(child);
if (cellChanged)
invalidateStyleForSubtree();
}
}
static StyleProperties* leakBorderStyle(CSSValueID value)
{
auto style = MutableStyleProperties::create();
style->setProperty(CSSPropertyBorderTopStyle, value);
style->setProperty(CSSPropertyBorderBottomStyle, value);
style->setProperty(CSSPropertyBorderLeftStyle, value);
style->setProperty(CSSPropertyBorderRightStyle, value);
return &style.leakRef();
}
const StyleProperties* HTMLTableElement::additionalPresentationalHintStyle() const
{
if (m_frameAttr)
return 0;
if (!m_borderAttr && !m_borderColorAttr) {
// Setting the border to 'hidden' allows it to win over any border
// set on the table's cells during border-conflict resolution.
if (m_rulesAttr != UnsetRules) {
static StyleProperties* solidBorderStyle = leakBorderStyle(CSSValueHidden);
return solidBorderStyle;
}
return 0;
}
if (m_borderColorAttr) {
static StyleProperties* solidBorderStyle = leakBorderStyle(CSSValueSolid);
return solidBorderStyle;
}
static StyleProperties* outsetBorderStyle = leakBorderStyle(CSSValueOutset);
return outsetBorderStyle;
}
HTMLTableElement::CellBorders HTMLTableElement::cellBorders() const
{
switch (m_rulesAttr) {
case NoneRules:
case GroupsRules:
return NoBorders;
case AllRules:
return SolidBorders;
case ColsRules:
return SolidBordersColsOnly;
case RowsRules:
return SolidBordersRowsOnly;
case UnsetRules:
if (!m_borderAttr)
return NoBorders;
if (m_borderColorAttr)
return SolidBorders;
return InsetBorders;
}
ASSERT_NOT_REACHED();
return NoBorders;
}
Ref<StyleProperties> HTMLTableElement::createSharedCellStyle() const
{
auto style = MutableStyleProperties::create();
auto& cssValuePool = CSSValuePool::singleton();
switch (cellBorders()) {
case SolidBordersColsOnly:
style->setProperty(CSSPropertyBorderLeftWidth, CSSValueThin);
style->setProperty(CSSPropertyBorderRightWidth, CSSValueThin);
style->setProperty(CSSPropertyBorderLeftStyle, CSSValueSolid);
style->setProperty(CSSPropertyBorderRightStyle, CSSValueSolid);
style->setProperty(CSSPropertyBorderColor, cssValuePool.createInheritedValue());
break;
case SolidBordersRowsOnly:
style->setProperty(CSSPropertyBorderTopWidth, CSSValueThin);
style->setProperty(CSSPropertyBorderBottomWidth, CSSValueThin);
style->setProperty(CSSPropertyBorderTopStyle, CSSValueSolid);
style->setProperty(CSSPropertyBorderBottomStyle, CSSValueSolid);
style->setProperty(CSSPropertyBorderColor, cssValuePool.createInheritedValue());
break;
case SolidBorders:
style->setProperty(CSSPropertyBorderWidth, cssValuePool.createValue(1, CSSUnitType::CSS_PX));
style->setProperty(CSSPropertyBorderStyle, cssValuePool.createIdentifierValue(CSSValueSolid));
style->setProperty(CSSPropertyBorderColor, cssValuePool.createInheritedValue());
break;
case InsetBorders:
style->setProperty(CSSPropertyBorderWidth, cssValuePool.createValue(1, CSSUnitType::CSS_PX));
style->setProperty(CSSPropertyBorderStyle, cssValuePool.createIdentifierValue(CSSValueInset));
style->setProperty(CSSPropertyBorderColor, cssValuePool.createInheritedValue());
break;
case NoBorders:
// If 'rules=none' then allow any borders set at cell level to take effect.
break;
}
if (m_padding)
style->setProperty(CSSPropertyPadding, cssValuePool.createValue(m_padding, CSSUnitType::CSS_PX));
return style;
}
const StyleProperties* HTMLTableElement::additionalCellStyle() const
{
if (!m_sharedCellStyle)
m_sharedCellStyle = createSharedCellStyle();
return m_sharedCellStyle.get();
}
static StyleProperties* leakGroupBorderStyle(int rows)
{
auto style = MutableStyleProperties::create();
if (rows) {
style->setProperty(CSSPropertyBorderTopWidth, CSSValueThin);
style->setProperty(CSSPropertyBorderBottomWidth, CSSValueThin);
style->setProperty(CSSPropertyBorderTopStyle, CSSValueSolid);
style->setProperty(CSSPropertyBorderBottomStyle, CSSValueSolid);
} else {
style->setProperty(CSSPropertyBorderLeftWidth, CSSValueThin);
style->setProperty(CSSPropertyBorderRightWidth, CSSValueThin);
style->setProperty(CSSPropertyBorderLeftStyle, CSSValueSolid);
style->setProperty(CSSPropertyBorderRightStyle, CSSValueSolid);
}
return &style.leakRef();
}
const StyleProperties* HTMLTableElement::additionalGroupStyle(bool rows) const
{
if (m_rulesAttr != GroupsRules)
return nullptr;
if (rows) {
static StyleProperties* rowBorderStyle = leakGroupBorderStyle(true);
return rowBorderStyle;
}
static StyleProperties* columnBorderStyle = leakGroupBorderStyle(false);
return columnBorderStyle;
}
bool HTMLTableElement::isURLAttribute(const Attribute& attribute) const
{
return attribute.name() == backgroundAttr || HTMLElement::isURLAttribute(attribute);
}
Ref<HTMLCollection> HTMLTableElement::rows()
{
return ensureRareData().ensureNodeLists().addCachedCollection<HTMLTableRowsCollection>(*this, TableRows);
}
Ref<HTMLCollection> HTMLTableElement::tBodies()
{
return ensureRareData().ensureNodeLists().addCachedCollection<GenericCachedHTMLCollection<CollectionTypeTraits<TableTBodies>::traversalType>>(*this, TableTBodies);
}
const AtomString& HTMLTableElement::rules() const
{
return attributeWithoutSynchronization(rulesAttr);
}
const AtomString& HTMLTableElement::summary() const
{
return attributeWithoutSynchronization(summaryAttr);
}
void HTMLTableElement::addSubresourceAttributeURLs(ListHashSet<URL>& urls) const
{
HTMLElement::addSubresourceAttributeURLs(urls);
addSubresourceURL(urls, document().completeURL(attributeWithoutSynchronization(backgroundAttr)));
}
}