Skip to content

Commit 12c2081

Browse files
committed
Implement DOMElement::toggleAttribute()
ref: https://dom.spec.whatwg.org/#dom-element-toggleattribute
1 parent c502c58 commit 12c2081

File tree

6 files changed

+270
-1
lines changed

6 files changed

+270
-1
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ PHP NEWS
1616

1717
- DOM:
1818
. Added DOMNode::contains() and DOMNameSpaceNode::contains(). (nielsdos)
19+
. Added DOMElement::toggleAttribute(). (nielsdos)
1920

2021
- Intl:
2122
. Fix memory leak in MessageFormatter::format() on failure. (Girgias)

UPGRADING

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ PHP 8.3 UPGRADE NOTES
240240

241241
- DOM:
242242
. Added DOMNode::contains() and DOMNameSpaceNode::contains().
243+
. Added DOMElement::toggleAttribute().
243244

244245
- JSON:
245246
. Added json_validate(), which returns whether the json is valid for

ext/dom/element.c

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,4 +1218,111 @@ PHP_METHOD(DOMElement, replaceWith)
12181218
}
12191219
/* }}} end DOMElement::prepend */
12201220

1221+
/* {{{ URL: https://dom.spec.whatwg.org/#dom-element-toggleattribute
1222+
Since:
1223+
*/
1224+
PHP_METHOD(DOMElement, toggleAttribute)
1225+
{
1226+
char *qname, *qname_tmp = NULL;
1227+
size_t qname_length;
1228+
bool force, force_is_null = true;
1229+
xmlNodePtr thisp;
1230+
zval *id;
1231+
dom_object *intern;
1232+
bool retval;
1233+
1234+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|b!", &qname, &qname_length, &force, &force_is_null) == FAILURE) {
1235+
RETURN_THROWS();
1236+
}
1237+
1238+
DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, intern);
1239+
1240+
/* Step 1 */
1241+
if (xmlValidateName((xmlChar *) qname, 0) != 0) {
1242+
php_dom_throw_error(INVALID_CHARACTER_ERR, 1);
1243+
RETURN_THROWS();
1244+
}
1245+
1246+
/* Step 2 */
1247+
if (thisp->doc->type == XML_HTML_DOCUMENT_NODE && (thisp->ns == NULL || xmlStrEqual(thisp->ns->href, (const xmlChar *) "http://www.w3.org/1999/xhtml"))) {
1248+
qname_tmp = zend_str_tolower_dup_ex(qname, qname_length);
1249+
if (qname_tmp != NULL) {
1250+
qname = qname_tmp;
1251+
}
1252+
}
1253+
1254+
/* Step 3 */
1255+
xmlNodePtr attribute = dom_get_dom1_attribute(thisp, (xmlChar *) qname);
1256+
1257+
/* Step 4 */
1258+
if (attribute == NULL) {
1259+
/* Step 4.1 */
1260+
if (force_is_null || force) {
1261+
/* The behaviour for namespaces isn't defined by spec, but this is based on observing browers behaviour.
1262+
* It follows the same rules when you'd manually add an attribute using the other APIs. */
1263+
int len;
1264+
const xmlChar *split = xmlSplitQName3((const xmlChar *) qname, &len);
1265+
if (split == NULL || strncmp(qname, "xmlns:", len + 1) != 0) {
1266+
/* unqualified name, or qualified name with no xml namespace declaration */
1267+
dom_create_attribute(thisp, qname, "");
1268+
} else {
1269+
/* qualified name with xml namespace declaration */
1270+
xmlNewNs(thisp, (const xmlChar *) "", (const xmlChar *) (qname + len + 1));
1271+
}
1272+
retval = true;
1273+
goto out;
1274+
}
1275+
/* Step 4.2 */
1276+
retval = false;
1277+
goto out;
1278+
}
1279+
1280+
/* Step 5 */
1281+
if (force_is_null || !force) {
1282+
if (attribute->type == XML_NAMESPACE_DECL) {
1283+
/* The behaviour isn't defined by spec, but by observing browsers I found
1284+
* that you can remove the nodes, but they'll get reconciled.
1285+
* So if any reference was left to the namespace, the only effect is that
1286+
* the definition is potentially moved closer to the element using it.
1287+
* If no reference was left, it is actually removed. */
1288+
xmlNsPtr ns = (xmlNsPtr) attribute;
1289+
if (thisp->nsDef == ns) {
1290+
thisp->nsDef = ns->next;
1291+
} else if (thisp->nsDef != NULL) {
1292+
xmlNsPtr prev = thisp->nsDef;
1293+
xmlNsPtr cur = prev->next;
1294+
while (cur) {
1295+
if (cur == ns) {
1296+
prev->next = cur->next;
1297+
break;
1298+
}
1299+
prev = cur;
1300+
cur = cur->next;
1301+
}
1302+
}
1303+
1304+
ns->next = NULL;
1305+
dom_set_old_ns(thisp->doc, ns);
1306+
dom_reconcile_ns(thisp->doc, thisp);
1307+
} else {
1308+
/* TODO: in the future when namespace bugs are fixed,
1309+
* the above if-branch should be merged into this called function
1310+
* such that the removal will work properly with all APIs. */
1311+
dom_remove_attribute(attribute);
1312+
}
1313+
retval = false;
1314+
goto out;
1315+
}
1316+
1317+
/* Step 6 */
1318+
retval = true;
1319+
1320+
out:
1321+
if (qname_tmp) {
1322+
efree(qname_tmp);
1323+
}
1324+
RETURN_BOOL(retval);
1325+
}
1326+
/* }}} end DOMElement::prepend */
1327+
12211328
#endif

ext/dom/php_dom.stub.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,8 @@ public function setIdAttributeNS(string $namespace, string $qualifiedName, bool
614614
/** @tentative-return-type */
615615
public function setIdAttributeNode(DOMAttr $attr, bool $isId): void {}
616616

617+
public function toggleAttribute(string $qualifiedName, ?bool $force = null): bool {}
618+
617619
public function remove(): void {}
618620

619621
/** @param DOMNode|string $nodes */

ext/dom/php_dom_arginfo.h

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
--TEST--
2+
DOMElement::toggleAttribute()
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
$html = new DOMDocument();
9+
$html->loadHTML('<!DOCTYPE HTML><html id="test"></html>');
10+
$xml = new DOMDocument();
11+
$xml->loadXML('<!DOCTYPE HTML><html id="test"></html>');
12+
13+
try {
14+
var_dump($html->documentElement->toggleAttribute("\0"));
15+
} catch (DOMException $e) {
16+
echo $e->getMessage(), "\n";
17+
}
18+
19+
echo "--- Selected attribute tests (HTML) ---\n";
20+
21+
var_dump($html->documentElement->toggleAttribute("SELECTED", false));
22+
echo $html->saveHTML();
23+
var_dump($html->documentElement->toggleAttribute("SELECTED"));
24+
echo $html->saveHTML();
25+
var_dump($html->documentElement->toggleAttribute("selected", true));
26+
echo $html->saveHTML();
27+
var_dump($html->documentElement->toggleAttribute("selected"));
28+
echo $html->saveHTML();
29+
30+
echo "--- Selected attribute tests (XML) ---\n";
31+
32+
var_dump($xml->documentElement->toggleAttribute("SELECTED", false));
33+
echo $xml->saveXML();
34+
var_dump($xml->documentElement->toggleAttribute("SELECTED"));
35+
echo $xml->saveXML();
36+
var_dump($xml->documentElement->toggleAttribute("selected", true));
37+
echo $xml->saveXML();
38+
var_dump($xml->documentElement->toggleAttribute("selected"));
39+
echo $xml->saveXML();
40+
41+
echo "--- id attribute tests ---\n";
42+
43+
var_dump($html->getElementById("test") === NULL);
44+
var_dump($html->documentElement->toggleAttribute("id"));
45+
var_dump($html->getElementById("test") === NULL);
46+
47+
echo "--- Namespace tests ---\n";
48+
49+
$dom = new DOMDocument();
50+
$dom->loadXML("<?xml version='1.0'?><container xmlns='some:ns' xmlns:foo='some:ns2' xmlns:anotherone='some:ns3'><foo:bar/><baz/></container>");
51+
52+
echo "Toggling namespaces:\n";
53+
var_dump($dom->documentElement->toggleAttribute('xmlns'));
54+
echo $dom->saveXML();
55+
var_dump($dom->documentElement->toggleAttribute('xmlns:anotherone'));
56+
echo $dom->saveXML();
57+
var_dump($dom->documentElement->toggleAttribute('xmlns:anotherone'));
58+
echo $dom->saveXML();
59+
var_dump($dom->documentElement->toggleAttribute('xmlns:foo'));
60+
echo $dom->saveXML();
61+
62+
echo "Toggling namespaced attributes:\n";
63+
var_dump($dom->documentElement->toggleAttribute('test:test'));
64+
var_dump($dom->documentElement->firstElementChild->toggleAttribute('foo:test'));
65+
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test'));
66+
echo $dom->saveXML();
67+
68+
echo "namespace of test:test = ";
69+
var_dump($dom->documentElement->getAttributeNode('test:test')->namespaceURI);
70+
echo "namespace of foo:test = ";
71+
var_dump($dom->documentElement->firstElementChild->getAttributeNode('foo:test')->namespaceURI);
72+
echo "namespace of doesnotexist:test = ";
73+
var_dump($dom->documentElement->firstElementChild->getAttributeNode('doesnotexist:test')->namespaceURI);
74+
75+
echo "Toggling namespaced attributes:\n";
76+
var_dump($dom->documentElement->toggleAttribute('test:test'));
77+
var_dump($dom->documentElement->firstElementChild->toggleAttribute('foo:test'));
78+
var_dump($dom->documentElement->firstElementChild->toggleAttribute('doesnotexist:test'));
79+
echo $dom->saveXML();
80+
81+
echo "Checking toggled namespace:\n";
82+
var_dump($dom->documentElement->getAttribute('xmlns:anotheron'));
83+
84+
?>
85+
--EXPECT--
86+
Invalid Character Error
87+
--- Selected attribute tests (HTML) ---
88+
bool(false)
89+
<!DOCTYPE HTML>
90+
<html id="test"></html>
91+
bool(true)
92+
<!DOCTYPE HTML>
93+
<html id="test" selected></html>
94+
bool(true)
95+
<!DOCTYPE HTML>
96+
<html id="test" selected></html>
97+
bool(false)
98+
<!DOCTYPE HTML>
99+
<html id="test"></html>
100+
--- Selected attribute tests (XML) ---
101+
bool(false)
102+
<?xml version="1.0"?>
103+
<!DOCTYPE HTML>
104+
<html id="test"/>
105+
bool(true)
106+
<?xml version="1.0"?>
107+
<!DOCTYPE HTML>
108+
<html id="test" SELECTED=""/>
109+
bool(true)
110+
<?xml version="1.0"?>
111+
<!DOCTYPE HTML>
112+
<html id="test" SELECTED="" selected=""/>
113+
bool(false)
114+
<?xml version="1.0"?>
115+
<!DOCTYPE HTML>
116+
<html id="test" SELECTED=""/>
117+
--- id attribute tests ---
118+
bool(false)
119+
bool(false)
120+
bool(true)
121+
--- Namespace tests ---
122+
Toggling namespaces:
123+
bool(false)
124+
<?xml version="1.0"?>
125+
<container xmlns:foo="some:ns2" xmlns:anotherone="some:ns3" xmlns="some:ns"><foo:bar/><baz/></container>
126+
bool(false)
127+
<?xml version="1.0"?>
128+
<container xmlns:foo="some:ns2" xmlns="some:ns"><foo:bar/><baz/></container>
129+
bool(true)
130+
<?xml version="1.0"?>
131+
<container xmlns:foo="some:ns2" xmlns="some:ns" xmlns:anotherone=""><foo:bar/><baz/></container>
132+
bool(false)
133+
<?xml version="1.0"?>
134+
<container xmlns="some:ns" xmlns:anotherone=""><foo:bar xmlns:foo="some:ns2"/><baz/></container>
135+
Toggling namespaced attributes:
136+
bool(true)
137+
bool(true)
138+
bool(true)
139+
<?xml version="1.0"?>
140+
<container xmlns="some:ns" xmlns:anotherone="" test:test=""><foo:bar xmlns:foo="some:ns2" foo:test="" doesnotexist:test=""/><baz/></container>
141+
namespace of test:test = NULL
142+
namespace of foo:test = string(8) "some:ns2"
143+
namespace of doesnotexist:test = NULL
144+
Toggling namespaced attributes:
145+
bool(false)
146+
bool(false)
147+
bool(false)
148+
<?xml version="1.0"?>
149+
<container xmlns="some:ns" xmlns:anotherone=""><foo:bar xmlns:foo="some:ns2"/><baz/></container>
150+
Checking toggled namespace:
151+
string(0) ""

0 commit comments

Comments
 (0)