blob: 5c15eb44a8b9f8910ae8c9bb02011331e7c8c7c3 [file] [log] [blame]
/*
* Copyright (C) 2010 Alex Milowski (alex@milowski.com). All rights reserved.
* Copyright (C) 2013 The MathJax Consortium.
* Copyright (C) 2016 Igalia S.L.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "RenderMathMLScripts.h"
#if ENABLE(MATHML)
#include "MathMLElement.h"
#include "MathMLScriptsElement.h"
#include "RenderMathMLOperator.h"
#include <wtf/IsoMallocInlines.h>
namespace WebCore {
WTF_MAKE_ISO_ALLOCATED_IMPL(RenderMathMLScripts);
static bool isPrescriptDelimiter(const RenderObject& renderObject)
{
return renderObject.node() && renderObject.node()->hasTagName(MathMLNames::mprescriptsTag);
}
RenderMathMLScripts::RenderMathMLScripts(MathMLScriptsElement& element, RenderStyle&& style)
: RenderMathMLBlock(element, WTFMove(style))
{
}
MathMLScriptsElement& RenderMathMLScripts::element() const
{
return static_cast<MathMLScriptsElement&>(nodeForNonAnonymous());
}
MathMLScriptsElement::ScriptType RenderMathMLScripts::scriptType() const
{
return element().scriptType();
}
RenderMathMLOperator* RenderMathMLScripts::unembellishedOperator() const
{
auto base = firstChildBox();
if (!is<RenderMathMLBlock>(base))
return nullptr;
return downcast<RenderMathMLBlock>(base)->unembellishedOperator();
}
Optional<RenderMathMLScripts::ReferenceChildren> RenderMathMLScripts::validateAndGetReferenceChildren()
{
// All scripted elements must have at least one child.
// The first child is the base.
auto base = firstChildBox();
if (!base)
return WTF::nullopt;
ReferenceChildren reference;
reference.base = base;
reference.firstPostScript = nullptr;
reference.firstPreScript = nullptr;
reference.prescriptDelimiter = nullptr;
switch (scriptType()) {
case MathMLScriptsElement::ScriptType::Sub:
case MathMLScriptsElement::ScriptType::Super:
case MathMLScriptsElement::ScriptType::Under:
case MathMLScriptsElement::ScriptType::Over: {
// These elements must have exactly two children.
// The second child is a postscript and there are no prescripts.
// <msub> base subscript </msub>
// <msup> base superscript </msup>
// <munder> base underscript </munder>
// <mover> base overscript </mover>
auto script = base->nextSiblingBox();
if (!script || isPrescriptDelimiter(*script) || script->nextSiblingBox())
return WTF::nullopt;
reference.firstPostScript = script;
return reference;
}
case MathMLScriptsElement::ScriptType::SubSup:
case MathMLScriptsElement::ScriptType::UnderOver: {
// These elements must have exactly three children.
// The second and third children are postscripts and there are no prescripts.
// <msubsup> base subscript superscript </msubsup>
// <munderover> base subscript superscript </munderover>
auto subScript = base->nextSiblingBox();
if (!subScript || isPrescriptDelimiter(*subScript))
return WTF::nullopt;
auto superScript = subScript->nextSiblingBox();
if (!superScript || isPrescriptDelimiter(*superScript) || superScript->nextSiblingBox())
return WTF::nullopt;
reference.firstPostScript = subScript;
return reference;
}
case MathMLScriptsElement::ScriptType::Multiscripts: {
// This element accepts the following syntax:
//
// <mmultiscripts>
// base
// (subscript superscript)*
// [ <mprescripts/> (presubscript presuperscript)* ]
// </mmultiscripts>
//
// https://www.w3.org/TR/MathML3/chapter3.html#presm.mmultiscripts
//
// We set the first postscript, unless (subscript superscript)* is empty.
if (base->nextSiblingBox() && !isPrescriptDelimiter(*base->nextSiblingBox()))
reference.firstPostScript = base->nextSiblingBox();
// We browse the children in order to
// 1) Set the first prescript, unless (presubscript presuperscript)* is empty.
// 2) Ensure the validity of the element i.e.
// a) That the list of postscripts can be grouped into pairs of subscript/superscript.
// b) That the list of prescripts can be grouped into pairs of subscript/superscript.
// c) That there is at most one <mprescripts/>.
bool numberOfScriptIsEven = true;
for (auto script = base->nextSiblingBox(); script; script = script->nextSiblingBox()) {
if (isPrescriptDelimiter(*script)) {
// This is a <mprescripts/>. Let's check 2a) and 2c).
if (!numberOfScriptIsEven || reference.firstPreScript)
return WTF::nullopt;
reference.firstPreScript = script->nextSiblingBox(); // We do 1).
reference.prescriptDelimiter = script;
continue;
}
numberOfScriptIsEven = !numberOfScriptIsEven;
}
return numberOfScriptIsEven ? Optional<ReferenceChildren>(reference) : WTF::nullopt; // We verify 2b).
}
}
ASSERT_NOT_REACHED();
return WTF::nullopt;
}
LayoutUnit RenderMathMLScripts::spaceAfterScript()
{
const auto& primaryFont = style().fontCascade().primaryFont();
if (auto* mathData = primaryFont.mathData())
return LayoutUnit(mathData->getMathConstant(primaryFont, OpenTypeMathData::SpaceAfterScript));
return LayoutUnit(style().fontCascade().size() / 5);
}
LayoutUnit RenderMathMLScripts::italicCorrection(const ReferenceChildren& reference)
{
if (is<RenderMathMLBlock>(*reference.base)) {
if (auto* renderOperator = downcast<RenderMathMLBlock>(*reference.base).unembellishedOperator())
return renderOperator->italicCorrection();
}
return 0;
}
void RenderMathMLScripts::computePreferredLogicalWidths()
{
ASSERT(preferredLogicalWidthsDirty());
m_minPreferredLogicalWidth = 0;
m_maxPreferredLogicalWidth = 0;
auto possibleReference = validateAndGetReferenceChildren();
if (!possibleReference) {
setPreferredLogicalWidthsDirty(false);
return;
}
auto& reference = possibleReference.value();
LayoutUnit baseItalicCorrection = std::min(reference.base->maxPreferredLogicalWidth(), italicCorrection(reference));
LayoutUnit space = spaceAfterScript();
switch (scriptType()) {
case MathMLScriptsElement::ScriptType::Sub:
case MathMLScriptsElement::ScriptType::Under:
m_maxPreferredLogicalWidth += reference.base->maxPreferredLogicalWidth();
m_maxPreferredLogicalWidth += std::max(0_lu, reference.firstPostScript->maxPreferredLogicalWidth() - baseItalicCorrection + space);
break;
case MathMLScriptsElement::ScriptType::Super:
case MathMLScriptsElement::ScriptType::Over:
m_maxPreferredLogicalWidth += reference.base->maxPreferredLogicalWidth();
m_maxPreferredLogicalWidth += std::max(0_lu, reference.firstPostScript->maxPreferredLogicalWidth() + space);
break;
case MathMLScriptsElement::ScriptType::SubSup:
case MathMLScriptsElement::ScriptType::UnderOver:
case MathMLScriptsElement::ScriptType::Multiscripts: {
auto subScript = reference.firstPreScript;
while (subScript) {
auto supScript = subScript->nextSiblingBox();
ASSERT(supScript);
LayoutUnit subSupPairWidth = std::max(subScript->maxPreferredLogicalWidth(), supScript->maxPreferredLogicalWidth());
m_maxPreferredLogicalWidth += subSupPairWidth + space;
subScript = supScript->nextSiblingBox();
}
m_maxPreferredLogicalWidth += reference.base->maxPreferredLogicalWidth();
subScript = reference.firstPostScript;
while (subScript && subScript != reference.prescriptDelimiter) {
auto supScript = subScript->nextSiblingBox();
ASSERT(supScript);
LayoutUnit subSupPairWidth = std::max(std::max(0_lu, subScript->maxPreferredLogicalWidth() - baseItalicCorrection), supScript->maxPreferredLogicalWidth());
m_maxPreferredLogicalWidth += subSupPairWidth + space;
subScript = supScript->nextSiblingBox();
}
}
}
m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth;
setPreferredLogicalWidthsDirty(false);
}
auto RenderMathMLScripts::verticalParameters() const -> VerticalParameters
{
VerticalParameters parameters;
const auto& primaryFont = style().fontCascade().primaryFont();
if (auto* mathData = primaryFont.mathData()) {
parameters.subscriptShiftDown = mathData->getMathConstant(primaryFont, OpenTypeMathData::SubscriptShiftDown);
parameters.superscriptShiftUp = mathData->getMathConstant(primaryFont, OpenTypeMathData::SuperscriptShiftUp);
parameters.subscriptBaselineDropMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::SubscriptBaselineDropMin);
parameters.superScriptBaselineDropMax = mathData->getMathConstant(primaryFont, OpenTypeMathData::SuperscriptBaselineDropMax);
parameters.subSuperscriptGapMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::SubSuperscriptGapMin);
parameters.superscriptBottomMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::SuperscriptBottomMin);
parameters.subscriptTopMax = mathData->getMathConstant(primaryFont, OpenTypeMathData::SubscriptTopMax);
parameters.superscriptBottomMaxWithSubscript = mathData->getMathConstant(primaryFont, OpenTypeMathData::SuperscriptBottomMaxWithSubscript);
} else {
// Default heuristic values when you do not have a font.
parameters.subscriptShiftDown = style().fontMetrics().xHeight() / 3;
parameters.superscriptShiftUp = style().fontMetrics().xHeight();
parameters.subscriptBaselineDropMin = style().fontMetrics().xHeight() / 2;
parameters.superScriptBaselineDropMax = style().fontMetrics().xHeight() / 2;
parameters.subSuperscriptGapMin = style().fontCascade().size() / 5;
parameters.superscriptBottomMin = style().fontMetrics().xHeight() / 4;
parameters.subscriptTopMax = 4 * style().fontMetrics().xHeight() / 5;
parameters.superscriptBottomMaxWithSubscript = 4 * style().fontMetrics().xHeight() / 5;
}
return parameters;
}
RenderMathMLScripts::VerticalMetrics RenderMathMLScripts::verticalMetrics(const ReferenceChildren& reference)
{
VerticalParameters parameters = verticalParameters();
VerticalMetrics metrics = { 0, 0, 0, 0 };
LayoutUnit baseAscent = ascentForChild(*reference.base);
LayoutUnit baseDescent = reference.base->logicalHeight() - baseAscent;
if (scriptType() == MathMLScriptsElement::ScriptType::Sub || scriptType() == MathMLScriptsElement::ScriptType::SubSup || scriptType() == MathMLScriptsElement::ScriptType::Multiscripts || scriptType() == MathMLScriptsElement::ScriptType::Under || scriptType() == MathMLScriptsElement::ScriptType::UnderOver) {
metrics.subShift = std::max(parameters.subscriptShiftDown, baseDescent + parameters.subscriptBaselineDropMin);
if (!isRenderMathMLUnderOver()) {
// It is not clear how to interpret the default shift and it is not available yet anyway.
// Hence we just pass 0 as the default value used by toUserUnits.
LayoutUnit specifiedMinSubShift = toUserUnits(element().subscriptShift(), style(), 0);
metrics.subShift = std::max(metrics.subShift, specifiedMinSubShift);
}
}
if (scriptType() == MathMLScriptsElement::ScriptType::Super || scriptType() == MathMLScriptsElement::ScriptType::SubSup || scriptType() == MathMLScriptsElement::ScriptType::Multiscripts || scriptType() == MathMLScriptsElement::ScriptType::Over || scriptType() == MathMLScriptsElement::ScriptType::UnderOver) {
metrics.supShift = std::max(parameters.superscriptShiftUp, baseAscent - parameters.superScriptBaselineDropMax);
if (!isRenderMathMLUnderOver()) {
// It is not clear how to interpret the default shift and it is not available yet anyway.
// Hence we just pass 0 as the default value used by toUserUnits.
LayoutUnit specifiedMinSupShift = toUserUnits(element().superscriptShift(), style(), 0);
metrics.supShift = std::max(metrics.supShift, specifiedMinSupShift);
}
}
switch (scriptType()) {
case MathMLScriptsElement::ScriptType::Sub:
case MathMLScriptsElement::ScriptType::Under: {
LayoutUnit subAscent = ascentForChild(*reference.firstPostScript);
LayoutUnit subDescent = reference.firstPostScript->logicalHeight() - subAscent;
metrics.descent = subDescent;
metrics.subShift = std::max(metrics.subShift, subAscent - parameters.subscriptTopMax);
}
break;
case MathMLScriptsElement::ScriptType::Super:
case MathMLScriptsElement::ScriptType::Over: {
LayoutUnit supAscent = ascentForChild(*reference.firstPostScript);
LayoutUnit supDescent = reference.firstPostScript->logicalHeight() - supAscent;
metrics.ascent = supAscent;
metrics.supShift = std::max(metrics.supShift, parameters.superscriptBottomMin + supDescent);
}
break;
case MathMLScriptsElement::ScriptType::SubSup:
case MathMLScriptsElement::ScriptType::UnderOver:
case MathMLScriptsElement::ScriptType::Multiscripts: {
// FIXME: We should move the code updating VerticalMetrics for each sub/sup pair in a helper
// function. That way, SubSup/UnderOver can just make one call and the loop for Multiscripts
// can be rewritten in a more readable.
auto subScript = reference.firstPostScript ? reference.firstPostScript : reference.firstPreScript;
while (subScript) {
auto supScript = subScript->nextSiblingBox();
ASSERT(supScript);
LayoutUnit subAscent = ascentForChild(*subScript);
LayoutUnit subDescent = subScript->logicalHeight() - subAscent;
LayoutUnit supAscent = ascentForChild(*supScript);
LayoutUnit supDescent = supScript->logicalHeight() - supAscent;
metrics.ascent = std::max(metrics.ascent, supAscent);
metrics.descent = std::max(metrics.descent, subDescent);
LayoutUnit subScriptShift = std::max(parameters.subscriptShiftDown, baseDescent + parameters.subscriptBaselineDropMin);
subScriptShift = std::max(subScriptShift, subAscent - parameters.subscriptTopMax);
LayoutUnit supScriptShift = std::max(parameters.superscriptShiftUp, baseAscent - parameters.superScriptBaselineDropMax);
supScriptShift = std::max(supScriptShift, parameters.superscriptBottomMin + supDescent);
LayoutUnit subSuperscriptGap = (subScriptShift - subAscent) + (supScriptShift - supDescent);
if (subSuperscriptGap < parameters.subSuperscriptGapMin) {
// First, we try and push the superscript up.
LayoutUnit delta = parameters.superscriptBottomMaxWithSubscript - (supScriptShift - supDescent);
if (delta > 0) {
delta = std::min(delta, parameters.subSuperscriptGapMin - subSuperscriptGap);
supScriptShift += delta;
subSuperscriptGap += delta;
}
// If that is not enough, we push the subscript down.
if (subSuperscriptGap < parameters.subSuperscriptGapMin)
subScriptShift += parameters.subSuperscriptGapMin - subSuperscriptGap;
}
metrics.subShift = std::max(metrics.subShift, subScriptShift);
metrics.supShift = std::max(metrics.supShift, supScriptShift);
subScript = supScript->nextSiblingBox();
if (subScript == reference.prescriptDelimiter)
subScript = reference.firstPreScript;
}
}
}
return metrics;
}
void RenderMathMLScripts::layoutBlock(bool relayoutChildren, LayoutUnit)
{
ASSERT(needsLayout());
if (!relayoutChildren && simplifiedLayout())
return;
auto possibleReference = validateAndGetReferenceChildren();
if (!possibleReference) {
layoutInvalidMarkup(relayoutChildren);
return;
}
auto& reference = possibleReference.value();
recomputeLogicalWidth();
for (auto child = firstChildBox(); child; child = child->nextSiblingBox())
child->layoutIfNeeded();
LayoutUnit space = spaceAfterScript();
// We determine the minimal shift/size of each script and take the maximum of the values.
VerticalMetrics metrics = verticalMetrics(reference);
LayoutUnit baseAscent = ascentForChild(*reference.base);
LayoutUnit baseDescent = reference.base->logicalHeight() - baseAscent;
LayoutUnit baseItalicCorrection = std::min(reference.base->logicalWidth(), italicCorrection(reference));
LayoutUnit horizontalOffset;
LayoutUnit ascent = std::max(baseAscent, metrics.ascent + metrics.supShift);
LayoutUnit descent = std::max(baseDescent, metrics.descent + metrics.subShift);
setLogicalHeight(ascent + descent);
switch (scriptType()) {
case MathMLScriptsElement::ScriptType::Sub:
case MathMLScriptsElement::ScriptType::Under: {
setLogicalWidth(reference.base->logicalWidth() + std::max(0_lu, reference.firstPostScript->logicalWidth() - baseItalicCorrection + space));
LayoutPoint baseLocation(mirrorIfNeeded(horizontalOffset, *reference.base), ascent - baseAscent);
reference.base->setLocation(baseLocation);
horizontalOffset += reference.base->logicalWidth();
LayoutUnit scriptAscent = ascentForChild(*reference.firstPostScript);
LayoutPoint scriptLocation(mirrorIfNeeded(horizontalOffset - baseItalicCorrection, *reference.firstPostScript), ascent + metrics.subShift - scriptAscent);
reference.firstPostScript->setLocation(scriptLocation);
}
break;
case MathMLScriptsElement::ScriptType::Super:
case MathMLScriptsElement::ScriptType::Over: {
setLogicalWidth(reference.base->logicalWidth() + std::max(0_lu, reference.firstPostScript->logicalWidth() + space));
LayoutPoint baseLocation(mirrorIfNeeded(horizontalOffset, *reference.base), ascent - baseAscent);
reference.base->setLocation(baseLocation);
horizontalOffset += reference.base->logicalWidth();
LayoutUnit scriptAscent = ascentForChild(*reference.firstPostScript);
LayoutPoint scriptLocation(mirrorIfNeeded(horizontalOffset, *reference.firstPostScript), ascent - metrics.supShift - scriptAscent);
reference.firstPostScript->setLocation(scriptLocation);
}
break;
case MathMLScriptsElement::ScriptType::SubSup:
case MathMLScriptsElement::ScriptType::UnderOver:
case MathMLScriptsElement::ScriptType::Multiscripts: {
// Calculate the logical width.
LayoutUnit logicalWidth;
auto subScript = reference.firstPreScript;
while (subScript) {
auto supScript = subScript->nextSiblingBox();
ASSERT(supScript);
LayoutUnit subSupPairWidth = std::max(subScript->logicalWidth(), supScript->logicalWidth());
logicalWidth += subSupPairWidth + space;
subScript = supScript->nextSiblingBox();
}
logicalWidth += reference.base->logicalWidth();
subScript = reference.firstPostScript;
while (subScript && subScript != reference.prescriptDelimiter) {
auto supScript = subScript->nextSiblingBox();
ASSERT(supScript);
LayoutUnit subSupPairWidth = std::max(std::max(0_lu, subScript->logicalWidth() - baseItalicCorrection), supScript->logicalWidth());
logicalWidth += subSupPairWidth + space;
subScript = supScript->nextSiblingBox();
}
setLogicalWidth(logicalWidth);
subScript = reference.firstPreScript;
while (subScript) {
auto supScript = subScript->nextSiblingBox();
ASSERT(supScript);
LayoutUnit subSupPairWidth = std::max(subScript->logicalWidth(), supScript->logicalWidth());
horizontalOffset += space + subSupPairWidth;
LayoutUnit subAscent = ascentForChild(*subScript);
LayoutPoint subScriptLocation(mirrorIfNeeded(horizontalOffset - subScript->logicalWidth(), *subScript), ascent + metrics.subShift - subAscent);
subScript->setLocation(subScriptLocation);
LayoutUnit supAscent = ascentForChild(*supScript);
LayoutPoint supScriptLocation(mirrorIfNeeded(horizontalOffset - supScript->logicalWidth(), *supScript), ascent - metrics.supShift - supAscent);
supScript->setLocation(supScriptLocation);
subScript = supScript->nextSiblingBox();
}
LayoutPoint baseLocation(mirrorIfNeeded(horizontalOffset, *reference.base), ascent - baseAscent);
reference.base->setLocation(baseLocation);
horizontalOffset += reference.base->logicalWidth();
subScript = reference.firstPostScript;
while (subScript && subScript != reference.prescriptDelimiter) {
auto supScript = subScript->nextSiblingBox();
ASSERT(supScript);
LayoutUnit subAscent = ascentForChild(*subScript);
LayoutPoint subScriptLocation(mirrorIfNeeded(horizontalOffset - baseItalicCorrection, *subScript), ascent + metrics.subShift - subAscent);
subScript->setLocation(subScriptLocation);
LayoutUnit supAscent = ascentForChild(*supScript);
LayoutPoint supScriptLocation(mirrorIfNeeded(horizontalOffset, *supScript), ascent - metrics.supShift - supAscent);
supScript->setLocation(supScriptLocation);
LayoutUnit subSupPairWidth = std::max(subScript->logicalWidth(), supScript->logicalWidth());
horizontalOffset += subSupPairWidth + space;
subScript = supScript->nextSiblingBox();
}
}
}
layoutPositionedObjects(relayoutChildren);
updateScrollInfoAfterLayout();
clearNeedsLayout();
}
Optional<int> RenderMathMLScripts::firstLineBaseline() const
{
auto* base = firstChildBox();
if (!base)
return Optional<int>();
return Optional<int>(static_cast<int>(lroundf(ascentForChild(*base) + base->logicalTop())));
}
}
#endif // ENABLE(MATHML)