Bug 2059312 - Part 2: Add MathMLAnchorElement class and WebIDL bindings r=emilio,webidl,firefox-svg-reviewers

Add the MathMLAnchorElement C++ class, WebIDL binding, and feature pref (mathml.a.element.enabled).

Add basic href navigation, target attribute handling and HyperlinkElementUtils mixin implementation.

Extract a helper function in the Link base class to handle focusability decisions for HTML, SVG, and MathML anchor elements.

I2P: https://groups.google.com/u/1/a/mozilla.org/g/dev-platform/c/UHg0HwA_A6I

Differential Revision: https://phabricator.services.mozilla.com/D315266
This commit is contained in:
tannal
2026-08-11 08:04:53 +00:00
committed by ealvarez@mozilla.com
parent df4e34ff76
commit 15734edd64
24 changed files with 258 additions and 176 deletions
+35
View File
@@ -5,6 +5,7 @@
#include "Link.h"
#include "mozilla/Components.h"
#include "mozilla/FocusModel.h"
#include "mozilla/IHistory.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/dom/BindContext.h"
@@ -15,6 +16,7 @@
#include "mozilla/dom/SpeculationRules.h"
#include "nsAttrValueInlines.h"
#include "nsGkAtoms.h"
#include "nsIContentInlines.h" // for nsINode::IsInDesignMode()
#include "nsIURIMutator.h"
#include "nsIURIWithSizeOf.h"
#include "nsLayoutUtils.h"
@@ -393,6 +395,39 @@ void Link::BindToTree(const BindContext& aContext) {
ResetLinkState(false);
}
Focusable Link::IsLinkFocusableWithoutStyle(IsFocusableFlags aFlags) const {
Element* element = GetElement();
MOZ_ASSERT(element);
// Links/Elements cannot be focused if:
// 1. Not in a composed document.
// 2. In designMode (where only the document itself is focusable).
// 3. Document link handling is disabled.
// 4. Node is inside an editable region (Links in an editable region should
// never be focusable, even if in a contenteditable="false" region).
if (!element->IsInComposedDoc() || element->IsInDesignMode() ||
!element->OwnerDoc()->LinkHandlingEnabled() ||
nsContentUtils::IsNodeInEditableRegion(element)) {
return {};
}
int32_t tabIndex = element->TabIndex();
// If the element is not actually a link (e.g. <a> without href):
// Not tabbable or focusable unless forced to be via the presence of a
// tabindex attribute (Bug 17605).
if (!element->IsLink()) {
return element->GetTabIndexAttrValue().isSome() ? Focusable{true, tabIndex}
: Focusable{};
}
if (!FocusModel::IsTabFocusable(TabFocusableType::Links)) {
tabIndex = -1;
}
return {true, tabIndex};
}
void Link::ResetLinkState(bool aNotify, bool aHasHref) {
// If we have an href, we should register with the history.
mNeedsRegistration = aHasHref;
+7
View File
@@ -11,6 +11,7 @@
#include "mozilla/dom/RustTypes.h"
#include "nsCOMPtr.h"
#include "nsIContent.h"
#include "nsWrapperCache.h" // For nsWrapperCache::FlagsType
class nsIURI;
@@ -78,6 +79,12 @@ class Link : public nsISupports {
void GetPort(nsACString& aPort);
void GetHash(nsACString& aHash);
/**
* Helper function for link elements (HTML/SVG/MathML anchor elements)
* to evaluate focusability based on common link-handling constraints.
*/
Focusable IsLinkFocusableWithoutStyle(IsFocusableFlags aFlags) const;
/**
* Invalidates any link caching, and resets the state to the default.
*
+4
View File
@@ -814,6 +814,10 @@ DOMInterfaces = {
'concrete': True,
},
'MathMLElement': {
'concrete': True,
},
'SVGFEFuncAElement': {
'headerFile': 'mozilla/dom/SVGComponentTransferFunctionElement.h',
},
+3 -29
View File
@@ -90,36 +90,10 @@ bool HTMLAnchorElement::IsHTMLFocusable(IsFocusableFlags aFlags,
return true;
}
// cannot focus links if there is no link handler
if (!OwnerDoc()->LinkHandlingEnabled()) {
*aTabIndex = -1;
*aIsFocusable = false;
return false;
}
Focusable focusable = Link::IsLinkFocusableWithoutStyle(aFlags);
*aIsFocusable = focusable.mFocusable;
*aTabIndex = focusable.mTabIndex;
// Links that are in an editable region should never be focusable, even if
// they are in a contenteditable="false" region.
if (nsContentUtils::IsNodeInEditableRegion(this)) {
*aTabIndex = -1;
*aIsFocusable = false;
return true;
}
if (GetTabIndexAttrValue().isNothing()) {
// check whether we're actually a link
if (!IsLink()) {
// Not tabbable or focusable without href (bug 17605), unless
// forced to be via presence of nonnegative tabindex attribute
*aTabIndex = -1;
*aIsFocusable = false;
return false;
}
}
if (!FocusModel::IsTabFocusable(TabFocusableType::Links)) {
*aTabIndex = -1;
}
*aIsFocusable = true;
return false;
}
+45
View File
@@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/MathMLAnchorElement.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/MathMLAnchorElementBinding.h"
#include "nsContentUtils.h"
#include "nsGkAtoms.h"
namespace mozilla::dom {
NS_IMPL_CYCLE_COLLECTION_INHERITED(MathMLAnchorElement, MathMLElement)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MathMLAnchorElement)
NS_INTERFACE_MAP_ENTRY(Link)
NS_INTERFACE_MAP_END_INHERITING(MathMLElement)
NS_IMPL_ADDREF_INHERITED(MathMLAnchorElement, MathMLElement)
NS_IMPL_RELEASE_INHERITED(MathMLAnchorElement, MathMLElement)
MathMLAnchorElement::MathMLAnchorElement(
already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo)
: MathMLElement(std::move(aNodeInfo)) {}
JSObject* MathMLAnchorElement::WrapNode(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return MathMLAnchorElement_Binding::Wrap(aCx, this, aGivenProto);
}
void MathMLAnchorElement::GetBaseTarget(nsAString& aValue) const {
OwnerDoc()->GetBaseTarget(aValue);
}
void MathMLAnchorElement::GetLinkTargetImpl(nsAString& aTarget) {
GetAttr(nsGkAtoms::target, aTarget);
if (aTarget.IsEmpty()) {
GetBaseTarget(aTarget);
}
}
NS_IMPL_ELEMENT_CLONE(MathMLAnchorElement)
} // namespace mozilla::dom
+67
View File
@@ -0,0 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_dom_MathMLAnchorElement_h
#define mozilla_dom_MathMLAnchorElement_h
#include "MathMLElement.h"
#include "nsDOMTokenList.h"
namespace mozilla::dom {
// FIXME: We should also inherit from Link once we migrate link behavior from
// MathMLElement. Since MathMLElement already inherits from Link, we need to
// prevent diamond inheritance for now.
class MathMLAnchorElement final : public MathMLElement {
public:
explicit MathMLAnchorElement(
already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MathMLAnchorElement, MathMLElement)
// nsISupports
NS_DECL_ISUPPORTS_INHERITED
void GetHref(nsAString& aHref) const {
GetURIAttr(nsGkAtoms::href, nullptr, aHref);
}
void SetHref(const nsAString& aHref, ErrorResult& aRv) {
SetAttr(nsGkAtoms::href, aHref, aRv);
}
void GetTarget(nsAString& aValue) const {
if (!GetAttr(nsGkAtoms::target, aValue)) {
GetBaseTarget(aValue);
}
}
void SetTarget(const nsAString& aValue, ErrorResult& aRv) {
SetAttr(nsGkAtoms::target, aValue, aRv);
}
void GetHreflang(DOMString& aValue) const {
GetAttr(nsGkAtoms::hreflang, aValue);
}
void SetHreflang(const nsAString& aValue, mozilla::ErrorResult& rv) {
SetAttr(nsGkAtoms::hreflang, aValue, rv);
}
void GetType(DOMString& aValue) const { GetAttr(nsGkAtoms::type, aValue); }
void SetType(const nsAString& aValue, mozilla::ErrorResult& rv) {
SetAttr(nsGkAtoms::type, aValue, rv);
}
void GetBaseTarget(nsAString& aValue) const;
void GetLinkTargetImpl(nsAString& aTarget) override;
nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
protected:
virtual ~MathMLAnchorElement() = default;
JSObject* WrapNode(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
};
} // namespace mozilla::dom
#endif
+2 -31
View File
@@ -687,37 +687,8 @@ int32_t MathMLElement::TabIndexDefault() {
return mNodeInfo->Equals(nsGkAtoms::a) ? 0 : -1;
}
// XXX Bug 1586011: Share logic with other element classes.
Focusable MathMLElement::IsFocusableWithoutStyle(IsFocusableFlags) {
if (!IsInComposedDoc() || IsInDesignMode()) {
// In designMode documents we only allow focusing the document.
return {};
}
int32_t tabIndex = TabIndex();
if (!IsLink()) {
// If a tabindex is specified at all we're focusable
if (GetTabIndexAttrValue().isSome()) {
return {true, tabIndex};
}
return {};
}
if (!OwnerDoc()->LinkHandlingEnabled()) {
return {};
}
// Links that are in an editable region should never be focusable, even if
// they are in a contenteditable="false" region.
if (nsContentUtils::IsNodeInEditableRegion(this)) {
return {};
}
if (!FocusModel::IsTabFocusable(TabFocusableType::Links)) {
tabIndex = -1;
}
return {true, tabIndex};
Focusable MathMLElement::IsFocusableWithoutStyle(IsFocusableFlags aFlags) {
return Link::IsLinkFocusableWithoutStyle(aFlags);
}
already_AddRefed<nsIURI> MathMLElement::GetHrefURI() const {
+1 -1
View File
@@ -22,7 +22,7 @@ using MathMLElementBase = nsStyledElement;
/*
* The base class for MathML elements.
*/
class MathMLElement final : public MathMLElementBase, public Link {
class MathMLElement : public MathMLElementBase, public Link {
public:
explicit MathMLElement(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo);
explicit MathMLElement(already_AddRefed<mozilla::dom::NodeInfo> aNodeInfo);
+10
View File
@@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/StaticPrefs_mathml.h"
#include "mozilla/dom/MathMLAnchorElement.h"
#include "mozilla/dom/MathMLElement.h"
#include "nsContentCreatorFunctions.h"
@@ -11,7 +13,15 @@ using namespace mozilla::dom;
nsresult NS_NewMathMLElement(
Element** aResult, already_AddRefed<mozilla::dom::NodeInfo> aNodeInfo) {
RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo);
auto* nim = nodeInfo->NodeInfoManager();
if (nodeInfo->NameAtom() == nsGkAtoms::a &&
mozilla::StaticPrefs::mathml_a_element_enabled()) {
NS_ADDREF(*aResult = new (nim) MathMLAnchorElement(nodeInfo.forget()));
return NS_OK;
}
NS_ADDREF(*aResult = new (nim) MathMLElement(nodeInfo.forget()));
return NS_OK;
}
+2
View File
@@ -6,10 +6,12 @@ with Files("**"):
BUG_COMPONENT = ("Core", "MathML")
EXPORTS.mozilla.dom += [
"MathMLAnchorElement.h",
"MathMLElement.h",
]
UNIFIED_SOURCES += [
"MathMLAnchorElement.cpp",
"MathMLElement.cpp",
"MathMLElementFactory.cpp",
]
+2 -23
View File
@@ -145,34 +145,13 @@ void SVGAElement::UnbindFromTree(UnbindContext& aContext) {
int32_t SVGAElement::TabIndexDefault() { return 0; }
Focusable SVGAElement::IsFocusableWithoutStyle(IsFocusableFlags) {
Focusable SVGAElement::IsFocusableWithoutStyle(IsFocusableFlags aFlags) {
Focusable result;
if (IsSVGFocusable(&result.mFocusable, &result.mTabIndex)) {
return result;
}
if (!OwnerDoc()->LinkHandlingEnabled()) {
return {};
}
// Links that are in an editable region should never be focusable, even if
// they are in a contenteditable="false" region.
if (nsContentUtils::IsNodeInEditableRegion(this)) {
return {};
}
if (GetTabIndexAttrValue().isNothing()) {
// check whether we're actually a link
if (!IsLink()) {
// Not tabbable or focusable without href (bug 17605), unless
// forced to be via presence of nonnegative tabindex attribute
return {};
}
}
if (!FocusModel::IsTabFocusable(TabFocusableType::Links)) {
result.mTabIndex = -1;
}
return result;
return Link::IsLinkFocusableWithoutStyle(aFlags);
}
bool SVGAElement::HasHref() const {
@@ -979,6 +979,8 @@ let interfaceNamesInGlobalScope = [
// IMPORTANT: Do not change this list without review from a DOM peer!
{ name: "MIDIPort", android: false },
// IMPORTANT: Do not change this list without review from a DOM peer!
{ name: "MathMLAnchorElement", insecureContext: true, nightly: true },
// IMPORTANT: Do not change this list without review from a DOM peer!
{ name: "MathMLElement", insecureContext: true },
// IMPORTANT: Do not change this list without review from a DOM peer!
{ name: "MediaCapabilities", insecureContext: true },
+20
View File
@@ -0,0 +1,20 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*
* The origin of this IDL file is
* https://w3c.github.io/mathml-core/#dom-mathmlanchorelement
*/
[
Exposed=Window,
Pref="mathml.a.element.enabled"
]
interface MathMLAnchorElement : MathMLElement {
[HTMLConstructor] constructor();
[CEReactions, SetterThrows] attribute USVString href;
[CEReactions, SetterThrows] attribute DOMString target;
};
MathMLAnchorElement includes HyperlinkElementUtils;
+1
View File
@@ -784,6 +784,7 @@ WEBIDL_FILES = [
"Lock.webidl",
"LockManager.webidl",
"LoginStatus.webidl",
"MathMLAnchorElement.webidl",
"MathMLElement.webidl",
"MediaCapabilities.webidl",
"MediaDebugInfo.webidl",
+6
View File
@@ -12004,6 +12004,12 @@
# Prefs starting with "mathml."
#---------------------------------------------------------------------------
# Enable MathMLAnchorElement dom interface (MathML <a> element).
- name: mathml.a.element.enabled
type: bool
value: @IS_NIGHTLY_BUILD@
mirror: always
# Enable scale transform for stretchy MathML operators. See bug 414277.
- name: mathml.scale_stretchy_operators.enabled
type: bool
@@ -1,3 +0,0 @@
[a-hreflang-getter.html]
[Test anchor's hreflang getter]
expected: FAIL
@@ -1,3 +0,0 @@
[a-hreflang-setter.html]
[Test anchor's hreflang setter]
expected: FAIL
@@ -1,3 +0,0 @@
[a-type-getter-001.html]
[Test anchor's type getter when empty]
expected: FAIL
@@ -1,3 +0,0 @@
[a-type-getter-002.html]
[Test anchor's type getter]
expected: FAIL
@@ -1,3 +0,0 @@
[a-type-setter.html]
[Test anchor's type setter]
expected: FAIL
@@ -1,42 +0,0 @@
[anchor-hyperlinkutils-getter-setter.html]
[Getter and setter for attribute of anchor element(0): hash]
expected: FAIL
[Getter and setter for attribute of anchor element(1): hash]
expected: FAIL
[Getter and setter for attribute of anchor element(2): host]
expected: FAIL
[Getter and setter for attribute of anchor element(3): hostname]
expected: FAIL
[Getter and setter for attribute of anchor element(5): password]
expected: FAIL
[Getter and setter for attribute of anchor element(6): pathname]
expected: FAIL
[Getter and setter for attribute of anchor element(7): port]
expected: FAIL
[Getter and setter for attribute of anchor element(8): protocol]
expected: FAIL
[Getter and setter for attribute of anchor element(9): protocol]
expected: FAIL
[Getter and setter for attribute of anchor element(10): search]
expected: FAIL
[Getter and setter for attribute of anchor element(11): search]
expected: FAIL
[Getter and setter for attribute of anchor element(12): search]
expected: FAIL
[Getter and setter for attribute of anchor element(13): search]
expected: FAIL
[Getter and setter for attribute of anchor element(14): username]
expected: FAIL
@@ -1,30 +0,0 @@
[anchor-hyperlinkutils-getter.html]
[Getter for attribute of anchor element(0): search]
expected: FAIL
[Getter for attribute of anchor element(1): hash]
expected: FAIL
[Getter for attribute of anchor element(2): host]
expected: FAIL
[Getter for attribute of anchor element(3): hostname]
expected: FAIL
[Getter for attribute of anchor element(4): href]
expected: FAIL
[Getter for attribute of anchor element(5): password]
expected: FAIL
[Getter for attribute of anchor element(6): pathname]
expected: FAIL
[Getter for attribute of anchor element(7): port]
expected: FAIL
[Getter for attribute of anchor element(8): protocol]
expected: FAIL
[Getter for attribute of anchor element(9): username]
expected: FAIL
@@ -1,5 +0,0 @@
[href-navigation.html]
expected:
if (os == "android") and debug: [ERROR, CRASH]
if (os == "android") and not debug: [ERROR, CRASH]
ERROR
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>MathML a element target attribute navigation and base element fallback</title>
<link rel="help" href="https://html.spec.whatwg.org/multipage/links.html#get-an-element's-target">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<base target="frame_base_target">
<iframe name="frame_base_target" id="frame_base_target"></iframe>
<math>
<!-- Link with target="" (empty string); Empty target falls back to <base target> -->
<a id="link_empty_target" href="/common/blank.html?target=empty_fallback" target="">
<mtext>Link Empty Target</mtext>
</a>
</math>
<script>
function waitForFrameLoad(iframe) {
return new Promise(resolve => {
function checkAndResolve() {
const currentUrl = iframe.contentWindow.location.href;
if (currentUrl && currentUrl !== "about:blank") {
iframe.removeEventListener('load', checkAndResolve);
resolve(currentUrl);
}
}
iframe.addEventListener('load', checkAndResolve);
});
}
// Target attribute set to empty string ("") falls back to <base target> in engines
promise_test(async () => {
const frameBase = document.getElementById("frame_base_target");
const link = document.getElementById("link_empty_target");
const navigationLoaded = waitForFrameLoad(frameBase);
await test_driver.click(link);
const url = await navigationLoaded;
assert_true(
url.includes("target=empty_fallback"),
"Link with target='' falls back to <base target> when <base> is present"
);
}, "MathML <a> with target='' falls back to <base target>");
</script>