Implement CSS selectors

This commit is contained in:
Niels Dossche 2024-04-27 23:25:37 +02:00
parent 7defc23532
commit 88da914910
50 changed files with 4073 additions and 16 deletions

View File

@ -247,6 +247,7 @@ PHP 8.4 INTERNALS UPGRADE NOTES
- Removed the "properties" HashTable field from php_libxml_node_object.
- Added a way to attached private data to a php_libxml_ref_obj.
- Added a way to fix a class type onto php_libxml_ref_obj.
- Added a way to record quirks mode in php_libxml_ref_obj.
- Added php_libxml_uses_internal_errors().
- Added a way to override document handlers (e.g. serialization) with
php_libxml_document_handlers.

View File

@ -1,3 +1,11 @@
ignore:
- "ext/dom/lexbor/lexbor" # bundled library
- "ext/pcre/pcre2lib" # bundled library
# bundled libraries
- "ext/dom/lexbor/lexbor/core"
- "ext/dom/lexbor/lexbor/css"
- "ext/dom/lexbor/lexbor/dom"
- "ext/dom/lexbor/lexbor/encoding"
- "ext/dom/lexbor/lexbor/html"
- "ext/dom/lexbor/lexbor/ns"
- "ext/dom/lexbor/lexbor/ports"
- "ext/dom/lexbor/lexbor/tag"
- "ext/pcre/pcre2lib"

View File

@ -22,12 +22,14 @@ if test "$PHP_DOM" != "no"; then
$LEXBOR_DIR/encoding/big5.c $LEXBOR_DIR/encoding/decode.c $LEXBOR_DIR/encoding/encode.c $LEXBOR_DIR/encoding/encoding.c $LEXBOR_DIR/encoding/euc_kr.c $LEXBOR_DIR/encoding/gb18030.c $LEXBOR_DIR/encoding/iso_2022_jp_katakana.c $LEXBOR_DIR/encoding/jis0208.c $LEXBOR_DIR/encoding/jis0212.c $LEXBOR_DIR/encoding/range.c $LEXBOR_DIR/encoding/res.c $LEXBOR_DIR/encoding/single.c \
$LEXBOR_DIR/html/encoding.c $LEXBOR_DIR/html/interface.c $LEXBOR_DIR/html/parser.c $LEXBOR_DIR/html/token.c $LEXBOR_DIR/html/token_attr.c $LEXBOR_DIR/html/tokenizer.c $LEXBOR_DIR/html/tree.c \
$LEXBOR_DIR/html/interfaces/anchor_element.c $LEXBOR_DIR/html/interfaces/area_element.c $LEXBOR_DIR/html/interfaces/audio_element.c $LEXBOR_DIR/html/interfaces/base_element.c $LEXBOR_DIR/html/interfaces/body_element.c $LEXBOR_DIR/html/interfaces/br_element.c $LEXBOR_DIR/html/interfaces/button_element.c $LEXBOR_DIR/html/interfaces/canvas_element.c $LEXBOR_DIR/html/interfaces/data_element.c $LEXBOR_DIR/html/interfaces/data_list_element.c $LEXBOR_DIR/html/interfaces/details_element.c $LEXBOR_DIR/html/interfaces/dialog_element.c $LEXBOR_DIR/html/interfaces/directory_element.c $LEXBOR_DIR/html/interfaces/div_element.c $LEXBOR_DIR/html/interfaces/d_list_element.c $LEXBOR_DIR/html/interfaces/document.c $LEXBOR_DIR/html/interfaces/element.c $LEXBOR_DIR/html/interfaces/embed_element.c $LEXBOR_DIR/html/interfaces/field_set_element.c $LEXBOR_DIR/html/interfaces/font_element.c $LEXBOR_DIR/html/interfaces/form_element.c $LEXBOR_DIR/html/interfaces/frame_element.c $LEXBOR_DIR/html/interfaces/frame_set_element.c $LEXBOR_DIR/html/interfaces/head_element.c $LEXBOR_DIR/html/interfaces/heading_element.c $LEXBOR_DIR/html/interfaces/hr_element.c $LEXBOR_DIR/html/interfaces/html_element.c $LEXBOR_DIR/html/interfaces/iframe_element.c $LEXBOR_DIR/html/interfaces/image_element.c $LEXBOR_DIR/html/interfaces/input_element.c $LEXBOR_DIR/html/interfaces/label_element.c $LEXBOR_DIR/html/interfaces/legend_element.c $LEXBOR_DIR/html/interfaces/li_element.c $LEXBOR_DIR/html/interfaces/link_element.c $LEXBOR_DIR/html/interfaces/map_element.c $LEXBOR_DIR/html/interfaces/marquee_element.c $LEXBOR_DIR/html/interfaces/media_element.c $LEXBOR_DIR/html/interfaces/menu_element.c $LEXBOR_DIR/html/interfaces/meta_element.c $LEXBOR_DIR/html/interfaces/meter_element.c $LEXBOR_DIR/html/interfaces/mod_element.c $LEXBOR_DIR/html/interfaces/object_element.c $LEXBOR_DIR/html/interfaces/o_list_element.c $LEXBOR_DIR/html/interfaces/opt_group_element.c $LEXBOR_DIR/html/interfaces/option_element.c $LEXBOR_DIR/html/interfaces/output_element.c $LEXBOR_DIR/html/interfaces/paragraph_element.c $LEXBOR_DIR/html/interfaces/param_element.c $LEXBOR_DIR/html/interfaces/picture_element.c $LEXBOR_DIR/html/interfaces/pre_element.c $LEXBOR_DIR/html/interfaces/progress_element.c $LEXBOR_DIR/html/interfaces/quote_element.c $LEXBOR_DIR/html/interfaces/script_element.c $LEXBOR_DIR/html/interfaces/select_element.c $LEXBOR_DIR/html/interfaces/slot_element.c $LEXBOR_DIR/html/interfaces/source_element.c $LEXBOR_DIR/html/interfaces/span_element.c $LEXBOR_DIR/html/interfaces/style_element.c $LEXBOR_DIR/html/interfaces/table_caption_element.c $LEXBOR_DIR/html/interfaces/table_cell_element.c $LEXBOR_DIR/html/interfaces/table_col_element.c $LEXBOR_DIR/html/interfaces/table_element.c $LEXBOR_DIR/html/interfaces/table_row_element.c $LEXBOR_DIR/html/interfaces/table_section_element.c $LEXBOR_DIR/html/interfaces/template_element.c $LEXBOR_DIR/html/interfaces/text_area_element.c $LEXBOR_DIR/html/interfaces/time_element.c $LEXBOR_DIR/html/interfaces/title_element.c $LEXBOR_DIR/html/interfaces/track_element.c $LEXBOR_DIR/html/interfaces/u_list_element.c $LEXBOR_DIR/html/interfaces/unknown_element.c $LEXBOR_DIR/html/interfaces/video_element.c $LEXBOR_DIR/html/interfaces/window.c \
$LEXBOR_DIR/selectors/selectors.c \
$LEXBOR_DIR/css/state.c $LEXBOR_DIR/css/log.c $LEXBOR_DIR/css/parser.c $LEXBOR_DIR/css/selectors/state.c $LEXBOR_DIR/css/selectors/selectors.c $LEXBOR_DIR/css/selectors/selector.c $LEXBOR_DIR/css/selectors/pseudo_state.c $LEXBOR_DIR/css/selectors/pseudo.c $LEXBOR_DIR/css/syntax/tokenizer/error.c $LEXBOR_DIR/css/syntax/state.c $LEXBOR_DIR/css/syntax/parser.c $LEXBOR_DIR/css/syntax/syntax.c $LEXBOR_DIR/css/syntax/anb.c $LEXBOR_DIR/css/syntax/tokenizer.c $LEXBOR_DIR/css/syntax/token.c $LEXBOR_DIR/css/css.c \
$LEXBOR_DIR/selectors-adapted/selectors.c \
$LEXBOR_DIR/ns/ns.c \
$LEXBOR_DIR/tag/tag.c"
PHP_NEW_EXTENSION(dom, [php_dom.c attr.c document.c infra.c \
xml_document.c html_document.c xml_serializer.c html5_serializer.c html5_parser.c namespace_compat.c \
domexception.c parentnode.c \
domexception.c \
parentnode/tree.c parentnode/css_selectors.c \
processinginstruction.c cdatasection.c \
documentfragment.c domimplementation.c \
element.c node.c characterdata.c \
@ -38,6 +40,7 @@ if test "$PHP_DOM" != "no"; then
namednodemap.c xpath_callbacks.c \
$LEXBOR_SOURCES],
$ext_shared,,$PHP_LEXBOR_CFLAGS)
PHP_ADD_BUILD_DIR($ext_builddir/parentnode)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/ports/posix/lexbor/core)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/core)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/dom/interfaces)
@ -45,7 +48,10 @@ if test "$PHP_DOM" != "no"; then
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/html/tokenizer)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/html/interfaces)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/encoding)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/selectors)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/css/selectors)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/css/tokenizer)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/css/syntax/tokenizer)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/selectors-adapted)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/ns)
PHP_ADD_BUILD_DIR($ext_builddir/$LEXBOR_DIR/tag)
PHP_SUBST(DOM_SHARED_LIBADD)

View File

@ -9,7 +9,7 @@ if (PHP_DOM == "yes") {
) {
EXTENSION("dom", "php_dom.c attr.c document.c infra.c \
xml_document.c html_document.c xml_serializer.c html5_serializer.c html5_parser.c namespace_compat.c \
domexception.c parentnode.c processinginstruction.c \
domexception.c processinginstruction.c \
cdatasection.c documentfragment.c domimplementation.c element.c \
node.c characterdata.c documenttype.c \
entity.c nodelist.c html_collection.c text.c comment.c \
@ -17,6 +17,7 @@ if (PHP_DOM == "yes") {
notation.c xpath.c dom_iterators.c \
namednodemap.c xpath_callbacks.c", null, "-Iext/dom/lexbor");
ADD_SOURCES("ext/dom/parentnode", "tree.c css_selectors.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/ports/windows_nt/lexbor/core", "memory.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/core", "array_obj.c array.c avl.c bst.c diyfp.c conv.c dobject.c dtoa.c hash.c mem.c mraw.c print.c serialize.c shs.c str.c strtod.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/dom", "interface.c", "dom");
@ -27,7 +28,11 @@ if (PHP_DOM == "yes") {
ADD_SOURCES("ext/dom/lexbor/lexbor/html", "encoding.c interface.c parser.c token.c token_attr.c tokenizer.c tree.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/encoding", "big5.c decode.c encode.c encoding.c euc_kr.c gb18030.c iso_2022_jp_katakana.c jis0208.c jis0212.c range.c res.c single.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/html/interfaces", "anchor_element.c area_element.c audio_element.c base_element.c body_element.c br_element.c button_element.c canvas_element.c data_element.c data_list_element.c details_element.c dialog_element.c directory_element.c div_element.c d_list_element.c document.c element.c embed_element.c field_set_element.c font_element.c form_element.c frame_element.c frame_set_element.c head_element.c heading_element.c hr_element.c html_element.c iframe_element.c image_element.c input_element.c label_element.c legend_element.c li_element.c link_element.c map_element.c marquee_element.c media_element.c menu_element.c meta_element.c meter_element.c mod_element.c object_element.c o_list_element.c opt_group_element.c option_element.c output_element.c paragraph_element.c param_element.c picture_element.c pre_element.c progress_element.c quote_element.c script_element.c select_element.c slot_element.c source_element.c span_element.c style_element.c table_caption_element.c table_cell_element.c table_col_element.c table_element.c table_row_element.c table_section_element.c template_element.c text_area_element.c time_element.c title_element.c track_element.c u_list_element.c unknown_element.c video_element.c window.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/selectors", "selectors.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/selectors-adapted", "selectors.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/css", "state.c log.c parser.c css.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/css/selectors", "state.c selectors.c selector.c pseudo_state.c pseudo.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/css/syntax", "state.c parser.c syntax.c anb.c tokenizer.c token.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/css/syntax/tokenizer", "error.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/ns", "ns.c", "dom");
ADD_SOURCES("ext/dom/lexbor/lexbor/tag", "tag.c", "dom");
ADD_FLAG("CFLAGS_DOM", "/D LEXBOR_STATIC ");

View File

@ -46,4 +46,7 @@ typedef enum {
VALIDATION_ERR = 16
} dom_exception_code;
void php_dom_throw_error(dom_exception_code error_code, bool strict_error);
void php_dom_throw_error_with_message(dom_exception_code error_code, const char *error_message, bool strict_error);
#endif /* DOM_EXCEPTION_H */

View File

@ -1752,4 +1752,66 @@ out:
}
/* }}} end DOMElement::prepend */
static void php_dom_dispatch_query_selector(INTERNAL_FUNCTION_PARAMETERS, bool all)
{
zend_string *selectors_str;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STR(selectors_str)
ZEND_PARSE_PARAMETERS_END();
xmlNodePtr thisp;
dom_object *intern;
zval *id;
DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, intern);
if (all) {
dom_parent_node_query_selector_all(thisp, intern, return_value, selectors_str);
} else {
dom_parent_node_query_selector(thisp, intern, return_value, selectors_str);
}
}
PHP_METHOD(Dom_Element, querySelector)
{
php_dom_dispatch_query_selector(INTERNAL_FUNCTION_PARAM_PASSTHRU, false);
}
PHP_METHOD(Dom_Element, querySelectorAll)
{
php_dom_dispatch_query_selector(INTERNAL_FUNCTION_PARAM_PASSTHRU, true);
}
PHP_METHOD(Dom_Element, matches)
{
zend_string *selectors_str;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STR(selectors_str)
ZEND_PARSE_PARAMETERS_END();
xmlNodePtr thisp;
dom_object *intern;
zval *id;
DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, intern);
dom_element_matches(thisp, intern, return_value, selectors_str);
}
PHP_METHOD(Dom_Element, closest)
{
zend_string *selectors_str;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STR(selectors_str)
ZEND_PARSE_PARAMETERS_END();
xmlNodePtr thisp;
dom_object *intern;
zval *id;
DOM_GET_THIS_OBJ(thisp, id, xmlNodePtr, intern);
dom_element_closest(thisp, intern, return_value, selectors_str);
}
#endif

View File

@ -24,6 +24,7 @@
#include "html5_parser.h"
#include <lexbor/html/parser.h>
#include <lexbor/html/interfaces/element.h>
#include <lexbor/dom/dom.h>
#include <libxml/parserInternals.h>
#include <libxml/HTMLtree.h>
#include <Zend/zend.h>
@ -380,6 +381,7 @@ void lexbor_libxml2_bridge_copy_observations(lxb_html_tree_t *tree, lexbor_libxm
observations->has_explicit_html_tag = tree->has_explicit_html_tag;
observations->has_explicit_head_tag = tree->has_explicit_head_tag;
observations->has_explicit_body_tag = tree->has_explicit_body_tag;
observations->quirks_mode = lxb_dom_interface_document(tree->document)->compat_mode == LXB_DOM_DOCUMENT_CMODE_QUIRKS;
}
#endif /* HAVE_LIBXML && HAVE_DOM */

View File

@ -47,6 +47,7 @@ typedef struct _lexbor_libxml2_bridge_extracted_observations {
bool has_explicit_html_tag;
bool has_explicit_head_tag;
bool has_explicit_body_tag;
bool quirks_mode;
} lexbor_libxml2_bridge_extracted_observations;
typedef struct _lexbor_libxml2_bridge_parse_context {

View File

@ -917,6 +917,7 @@ PHP_METHOD(Dom_HTMLDocument, createFromString)
NULL
);
dom_set_xml_class(intern->document);
intern->document->quirks_mode = ctx.observations.quirks_mode;
intern->document->private_data = php_dom_libxml_ns_mapper_header(ns_mapper);
return;
@ -1137,6 +1138,7 @@ PHP_METHOD(Dom_HTMLDocument, createFromFile)
NULL
);
dom_set_xml_class(intern->document);
intern->document->quirks_mode = ctx.observations.quirks_mode;
intern->document->private_data = php_dom_libxml_ns_mapper_header(ns_mapper);
return;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,254 @@
/*
* Copyright (C) 2021-2024 Alexander Borisov
*
* Author: Alexander Borisov <borisov@lexbor.com>
* Adapted for PHP libxml2 by: Niels Dossche <nielsdos@php.net>
*/
#ifndef LEXBOR_SELECTORS_H
#define LEXBOR_SELECTORS_H
#ifdef __cplusplus
extern "C" {
#endif
#include "lexbor/selectors/base.h"
#include "lexbor/dom/dom.h"
#include "lexbor/css/selectors/selectors.h"
#include "lexbor/core/array_obj.h"
#include <libxml/tree.h>
typedef enum {
LXB_SELECTORS_OPT_DEFAULT = 0x00,
/*
* Includes the passed (root) node in the search.
*
* By default, the root node does not participate in selector searches,
* only its children.
*
* This behavior is logical, if you have found a node and then you want to
* search for other nodes in it, you don't need to check it again.
*
* But there are cases when it is necessary for root node to participate
* in the search. That's what this option is for.
*/
LXB_SELECTORS_OPT_MATCH_ROOT = 1 << 1,
/*
* Stop searching after the first match with any of the selectors
* in the list.
*
* By default, the callback will be triggered for each selector list.
* That is, if your node matches different selector lists, it will be
* returned multiple times in the callback.
*
* For example:
* HTML: <div id="ok"><span>test</span></div>
* Selectors: div, div[id="ok"], div:has(:not(a))
*
* The default behavior will cause three callbacks with the same node (div).
* Because it will be found by every selector in the list.
*
* This option allows you to end the element check after the first match on
* any of the selectors. That is, the callback will be called only once
* for example above. This way we get rid of duplicates in the search.
*/
LXB_SELECTORS_OPT_MATCH_FIRST = 1 << 2,
/* Quirks mode (sigh) */
LXB_SELECTORS_OPT_QUIRKS_MODE = 1 << 3,
}
lxb_selectors_opt_t;
typedef struct lxb_selectors lxb_selectors_t;
typedef struct lxb_selectors_entry lxb_selectors_entry_t;
typedef struct lxb_selectors_nested lxb_selectors_nested_t;
typedef lxb_status_t
(*lxb_selectors_cb_f)(const xmlNode *node,
lxb_css_selector_specificity_t spec, void *ctx);
typedef lxb_selectors_entry_t *
(*lxb_selectors_state_cb_f)(lxb_selectors_t *selectors,
lxb_selectors_entry_t *entry);
typedef struct {
const xmlChar *name;
bool interned;
} lxb_selectors_adapted_id;
struct lxb_selectors_entry {
lxb_selectors_adapted_id id;
lxb_css_selector_combinator_t combinator;
const lxb_css_selector_t *selector;
const xmlNode *node;
lxb_selectors_entry_t *next;
lxb_selectors_entry_t *prev;
lxb_selectors_entry_t *following;
lxb_selectors_nested_t *nested;
};
struct lxb_selectors_nested {
lxb_selectors_entry_t *entry;
lxb_selectors_state_cb_f return_state;
lxb_selectors_cb_f cb;
void *ctx;
const xmlNode *root;
lxb_selectors_entry_t *last;
lxb_selectors_nested_t *parent;
size_t index;
bool found;
};
struct lxb_selectors {
lxb_selectors_state_cb_f state;
lexbor_dobject_t *objs;
lexbor_dobject_t *nested;
lxb_selectors_nested_t *current;
lxb_selectors_entry_t *first;
lxb_selectors_opt_t options;
lxb_status_t status;
};
/*
* Initialization of lxb_selectors_t object.
*
* Caches are initialized in this function.
*
* @param[in] lxb_selectors_t *
*
* @return LXB_STATUS_OK if successful, otherwise an error status value.
*/
LXB_API lxb_status_t
lxb_selectors_init(lxb_selectors_t *selectors);
/*
* Clears the object. Returns object to states as after initialization.
*
* After each call to lxb_selectors_find() and lxb_selectors_find_for_node(),
* the lxb_selectors_t object is cleared. That is, you don't need to call this
* function every time after searching by a selector.
*
* @param[in] lxb_url_parser_t *
*/
LXB_API void
lxb_selectors_clean(lxb_selectors_t *selectors);
/*
* Destroy lxb_selectors_t object.
*
* Destroying all caches.
*
* @param[in] lxb_selectors_t *. Can be NULL.
* if true: destroys the lxb_selectors_t object and all internal caches.
*/
LXB_API void
lxb_selectors_destroy(lxb_selectors_t *selectors);
/*
* Search for nodes by selector list.
*
* Default Behavior:
* 1. The root node does not participate in the search, only its child nodes.
* 2. If a node matches multiple selector lists, a callback with that node
* will be called on each list.
* For example:
* HTML: <div id="ok"><span></span></div>
* Selectors: div, div[id="ok"], div:has(:not(a))
* For each selector list, a callback with a "div" node will be called.
*
* To change the search behavior, see lxb_selectors_opt_set().
*
* @param[in] lxb_selectors_t *.
* @param[in] const xmlNode *. The node from which the search will begin.
* @param[in] const lxb_css_selector_list_t *. Selectors List.
* @param[in] lxb_selectors_cb_f. Callback for a found node.
* @param[in] void *. Context for the callback.
* if true: destroys the lxb_selectors_t object and all internal caches.
*
* @return LXB_STATUS_OK if successful, otherwise an error status value.
*/
LXB_API lxb_status_t
lxb_selectors_find(lxb_selectors_t *selectors, const xmlNode *root,
const lxb_css_selector_list_t *list,
lxb_selectors_cb_f cb, void *ctx);
/*
* Match a node to a Selectors List.
*
* In other words, the function checks which selector lists will find the
* specified node.
*
* Default Behavior:
* 1. If a node matches multiple selector lists, a callback with that node
* will be called on each list.
* For example:
* HTML: <div id="ok"><span></span></div>
* Node: div
* Selectors: div, div[id="ok"], div:has(:not(a))
* For each selector list, a callback with a "div" node will be called.
*
* To change the search behavior, see lxb_selectors_opt_set().
*
* @param[in] lxb_selectors_t *.
* @param[in] const xmlNode *. The node from which the search will begin.
* @param[in] const lxb_css_selector_list_t *. Selectors List.
* @param[in] lxb_selectors_cb_f. Callback for a found node.
* @param[in] void *. Context for the callback.
* if true: destroys the lxb_selectors_t object and all internal caches.
*
* @return LXB_STATUS_OK if successful, otherwise an error status value.
*/
LXB_API lxb_status_t
lxb_selectors_match_node(lxb_selectors_t *selectors, const xmlNode *node,
const lxb_css_selector_list_t *list,
lxb_selectors_cb_f cb, void *ctx);
/*
* Inline functions.
*/
/*
* The function sets the node search options.
*
* For more information, see lxb_selectors_opt_t.
*
* @param[in] lxb_selectors_t *.
* @param[in] lxb_selectors_opt_t.
*/
lxb_inline void
lxb_selectors_opt_set(lxb_selectors_t *selectors, lxb_selectors_opt_t opt)
{
selectors->options = opt;
}
/*
* Get the current selector.
*
* Function to get the selector by which the node was found.
* Use context (void *ctx) to pass the lxb_selectors_t object to the callback.
*
* @param[in] const lxb_selectors_t *.
*
* @return const lxb_css_selector_list_t *.
*/
lxb_inline const lxb_css_selector_list_t *
lxb_selectors_selector(const lxb_selectors_t *selectors)
{
return selectors->current->entry->selector->list;
}
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* LEXBOR_SELECTORS_H */

View File

@ -0,0 +1,282 @@
/*
+----------------------------------------------------------------------+
| Copyright (c) The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| https://www.php.net/license/3_01.txt |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
| Authors: Niels Dossche <nielsdos@php.net> |
+----------------------------------------------------------------------+
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
#include "../php_dom.h"
#include "lexbor/css/parser.h"
#include "lexbor/selectors-adapted/selectors.h"
// TODO: optimization idea: cache the parsed selectors in an LRU fashion?
typedef struct {
HashTable *list;
dom_object *intern;
} dom_query_selector_all_ctx;
typedef struct {
const xmlNode *reference;
bool result;
} dom_query_selector_matches_ctx;
static lxb_selectors_opt_t dom_quirks_opt(lxb_selectors_opt_t options, const dom_object *intern)
{
if (intern->document != NULL && intern->document->quirks_mode) {
options |= LXB_SELECTORS_OPT_QUIRKS_MODE;
}
return options;
}
lxb_status_t dom_query_selector_find_single_callback(const xmlNode *node, lxb_css_selector_specificity_t spec, void *ctx)
{
xmlNodePtr *result = (xmlNodePtr *) ctx;
*result = (xmlNodePtr) node;
return LXB_STATUS_STOP;
}
lxb_status_t dom_query_selector_find_array_callback(const xmlNode *node, lxb_css_selector_specificity_t spec, void *ctx)
{
dom_query_selector_all_ctx *qsa_ctx = (dom_query_selector_all_ctx *) ctx;
zval object;
php_dom_create_object((xmlNodePtr) node, &object, qsa_ctx->intern);
zend_hash_next_index_insert_new(qsa_ctx->list, &object);
return LXB_STATUS_OK;
}
lxb_status_t dom_query_selector_find_matches_callback(const xmlNode *node, lxb_css_selector_specificity_t spec, void *ctx)
{
dom_query_selector_matches_ctx *matches_ctx = (dom_query_selector_matches_ctx *) ctx;
if (node == matches_ctx->reference) {
matches_ctx->result = true;
return LXB_STATUS_STOP;
}
return LXB_STATUS_OK;
}
static lxb_css_selector_list_t *dom_parse_selector(
lxb_css_parser_t *parser,
lxb_selectors_t *selectors,
const zend_string *selectors_str,
lxb_selectors_opt_t options,
const dom_object *intern
)
{
lxb_status_t status;
memset(parser, 0, sizeof(lxb_css_parser_t));
status = lxb_css_parser_init(parser, NULL);
ZEND_ASSERT(status == LXB_STATUS_OK);
memset(selectors, 0, sizeof(lxb_selectors_t));
status = lxb_selectors_init(selectors);
ZEND_ASSERT(status == LXB_STATUS_OK);
lxb_selectors_opt_set(selectors, dom_quirks_opt(options, intern));
lxb_css_selector_list_t *list = lxb_css_selectors_parse(parser, (const lxb_char_t *) ZSTR_VAL(selectors_str), ZSTR_LEN(selectors_str));
if (UNEXPECTED(list == NULL)) {
size_t nr_of_messages = lexbor_array_obj_length(&parser->log->messages);
if (nr_of_messages > 0) {
lxb_css_log_message_t *msg = lexbor_array_obj_get(&parser->log->messages, 0);
char *error;
zend_spprintf(&error, 0, "Invalid selector (%.*s)", (int) msg->text.length, msg->text.data);
php_dom_throw_error_with_message(SYNTAX_ERR, error, true);
efree(error);
} else {
php_dom_throw_error_with_message(SYNTAX_ERR, "Invalid selector", true);
}
}
return list;
}
static lxb_status_t dom_check_css_execution_status(lxb_status_t status)
{
if (UNEXPECTED(status != LXB_STATUS_OK && status != LXB_STATUS_STOP)) {
zend_argument_value_error(1, "contains an unsupported selector");
return status;
}
return LXB_STATUS_OK;
}
static void dom_selector_cleanup(lxb_css_parser_t *parser, lxb_selectors_t *selectors, lxb_css_selector_list_t *list)
{
lxb_css_selector_list_destroy_memory(list);
lxb_selectors_destroy(selectors);
(void) lxb_css_parser_destroy(parser, false);
}
static lxb_status_t dom_query_selector_common(
const xmlNode *root,
const dom_object *intern,
const zend_string *selectors_str,
lxb_selectors_cb_f cb,
void *ctx,
lxb_selectors_opt_t options
)
{
lxb_status_t status;
lxb_css_parser_t parser;
lxb_selectors_t selectors;
lxb_css_selector_list_t *list = dom_parse_selector(&parser, &selectors, selectors_str, options, intern);
if (UNEXPECTED(list == NULL)) {
status = LXB_STATUS_ERROR;
} else {
status = lxb_selectors_find(&selectors, root, list, cb, ctx);
status = dom_check_css_execution_status(status);
}
dom_selector_cleanup(&parser, &selectors, list);
return status;
}
static lxb_status_t dom_query_matches(
const xmlNode *root,
const dom_object *intern,
const zend_string *selectors_str,
void *ctx
)
{
lxb_status_t status;
lxb_css_parser_t parser;
lxb_selectors_t selectors;
lxb_css_selector_list_t *list = dom_parse_selector(&parser, &selectors, selectors_str, LXB_SELECTORS_OPT_MATCH_FIRST, intern);
if (UNEXPECTED(list == NULL)) {
status = LXB_STATUS_ERROR;
} else {
status = lxb_selectors_match_node(&selectors, root, list, dom_query_selector_find_matches_callback, ctx);
status = dom_check_css_execution_status(status);
}
dom_selector_cleanup(&parser, &selectors, list);
return status;
}
static const xmlNode *dom_query_closest(
const xmlNode *root,
const dom_object *intern,
const zend_string *selectors_str
)
{
const xmlNode *ret = NULL;
lxb_css_parser_t parser;
lxb_selectors_t selectors;
lxb_css_selector_list_t *list = dom_parse_selector(&parser, &selectors, selectors_str, LXB_SELECTORS_OPT_MATCH_FIRST, intern);
if (EXPECTED(list != NULL)) {
const xmlNode *current = root;
while (current != NULL) {
dom_query_selector_matches_ctx ctx = { current, false };
lxb_status_t status = lxb_selectors_match_node(&selectors, current, list, dom_query_selector_find_matches_callback, &ctx);
status = dom_check_css_execution_status(status);
if (UNEXPECTED(status != LXB_STATUS_OK)) {
break;
}
if (ctx.result) {
ret = current;
break;
}
current = current->parent;
}
}
dom_selector_cleanup(&parser, &selectors, list);
return ret;
}
/* https://dom.spec.whatwg.org/#dom-parentnode-queryselector */
void dom_parent_node_query_selector(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
{
xmlNodePtr result = NULL;
if (dom_query_selector_common(
thisp,
intern,
selectors_str,
dom_query_selector_find_single_callback,
&result,
LXB_SELECTORS_OPT_MATCH_FIRST
) != LXB_STATUS_OK || result == NULL) {
RETURN_NULL();
} else {
DOM_RET_OBJ(result, intern);
}
}
/* https://dom.spec.whatwg.org/#dom-parentnode-queryselectorall */
void dom_parent_node_query_selector_all(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
{
HashTable *list = zend_new_array(0);
dom_query_selector_all_ctx ctx = { list, intern };
if (dom_query_selector_common(
thisp,
intern,
selectors_str,
dom_query_selector_find_array_callback,
&ctx,
LXB_SELECTORS_OPT_DEFAULT
) != LXB_STATUS_OK) {
zend_array_destroy(list);
RETURN_THROWS();
} else {
php_dom_create_iterator(return_value, DOM_NODELIST, true);
dom_object *ret_obj = Z_DOMOBJ_P(return_value);
dom_nnodemap_object *mapptr = (dom_nnodemap_object *) ret_obj->ptr;
ZVAL_ARR(&mapptr->baseobj_zv, list);
mapptr->nodetype = DOM_NODESET;
}
}
/* https://dom.spec.whatwg.org/#dom-element-matches */
void dom_element_matches(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
{
dom_query_selector_matches_ctx ctx = { thisp, false };
if (dom_query_matches(
thisp,
intern,
selectors_str,
&ctx
) != LXB_STATUS_OK) {
RETURN_THROWS();
} else {
RETURN_BOOL(ctx.result);
}
}
/* https://dom.spec.whatwg.org/#dom-element-closest */
void dom_element_closest(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str)
{
const xmlNode *result = dom_query_closest(thisp, intern, selectors_str);
if (EXPECTED(result != NULL)) {
DOM_RET_OBJ((xmlNodePtr) result, intern);
}
}
#endif

View File

@ -1,6 +1,4 @@
/*
+----------------------------------------------------------------------+
| PHP Version 7 |
+----------------------------------------------------------------------+
| Copyright (c) The PHP Group |
+----------------------------------------------------------------------+
@ -23,9 +21,9 @@
#include "php.h"
#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
#include "php_dom.h"
#include "internal_helpers.h"
#include "dom_properties.h"
#include "../php_dom.h"
#include "../internal_helpers.h"
#include "../dom_properties.h"
/* {{{ firstElementChild DomParentNode
readonly=yes

View File

@ -130,8 +130,6 @@ zend_object *dom_nnodemap_objects_new(zend_class_entry *class_type);
zend_object *dom_xpath_objects_new(zend_class_entry *class_type);
#endif
bool dom_get_strict_error(php_libxml_ref_obj *document);
void php_dom_throw_error(dom_exception_code error_code, bool strict_error);
void php_dom_throw_error_with_message(dom_exception_code error_code, const char *error_message, bool strict_error);
void node_list_unlink(xmlNodePtr node);
int dom_check_qname(char *qname, char **localname, char **prefix, int uri_len, int name_len);
xmlNsPtr dom_get_ns(xmlNodePtr node, char *uri, int *errorcode, char *prefix);
@ -199,6 +197,10 @@ bool php_dom_fragment_insertion_hierarchy_check_replace(xmlNodePtr parent, xmlNo
void php_dom_node_append(php_libxml_ref_obj *document, xmlNodePtr node, xmlNodePtr parent);
bool php_dom_pre_insert(php_libxml_ref_obj *document, xmlNodePtr node, xmlNodePtr parent, xmlNodePtr insertion_point);
bool php_dom_pre_insert_is_parent_invalid(xmlNodePtr parent);
void dom_parent_node_query_selector(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str);
void dom_parent_node_query_selector_all(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str);
void dom_element_matches(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str);
void dom_element_closest(xmlNodePtr thisp, dom_object *intern, zval *return_value, const zend_string *selectors_str);
/* nodemap and nodelist APIs */
xmlNodePtr php_dom_named_node_map_get_named_item(dom_nnodemap_object *objmap, const zend_string *named, bool may_transform);

View File

@ -1106,6 +1106,9 @@ namespace Dom
public function append(Node|string ...$nodes): void;
public function prepend(Node|string ...$nodes): void;
public function replaceChildren(Node|string ...$nodes): void;
public function querySelector(string $selectors): ?Element;
public function querySelectorAll(string $selectors): NodeList;
}
interface ChildNode
@ -1372,6 +1375,11 @@ namespace Dom
public function prepend(Node|string ...$nodes): void {}
/** @implementation-alias DOMElement::replaceChildren */
public function replaceChildren(Node|string ...$nodes): void {}
public function querySelector(string $selectors): ?Element {}
public function querySelectorAll(string $selectors): NodeList {}
public function closest(string $selectors): ?Element {}
public function matches(string $selectors): bool {}
}
class HTMLElement extends Element
@ -1492,6 +1500,11 @@ namespace Dom
public function prepend(Node|string ...$nodes): void {}
/** @implementation-alias DOMElement::replaceChildren */
public function replaceChildren(Node|string ...$nodes): void {}
/** @implementation-alias Dom\Element::querySelector */
public function querySelector(string $selectors): ?Element {}
/** @implementation-alias Dom\Element::querySelectorAll */
public function querySelectorAll(string $selectors): NodeList {}
}
class Entity extends Node
@ -1585,6 +1598,11 @@ namespace Dom
public function importLegacyNode(\DOMNode $node, bool $deep = false): Node {}
/** @implementation-alias Dom\Element::querySelector */
public function querySelector(string $selectors): ?Element {}
/** @implementation-alias Dom\Element::querySelectorAll */
public function querySelectorAll(string $selectors): NodeList {}
public ?HTMLElement $body;
/** @readonly */
public ?HTMLElement $head;

View File

@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: fd650b1e64c4ed4ce66b0dad9c681a51cb8ff1ae */
* Stub hash: 28365949d78a2d0254cfdb0da6549e282d2eb436 */
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_dom_import_simplexml, 0, 1, DOMElement, 0)
ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0)
@ -567,6 +567,14 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Dom_ParentNode_replaceChildren arginfo_class_Dom_ParentNode_append
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Dom_ParentNode_querySelector, 0, 1, Dom\\Element, 1)
ZEND_ARG_TYPE_INFO(0, selectors, IS_STRING, 0)
ZEND_END_ARG_INFO()
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Dom_ParentNode_querySelectorAll, 0, 1, Dom\\\116odeList, 0)
ZEND_ARG_TYPE_INFO(0, selectors, IS_STRING, 0)
ZEND_END_ARG_INFO()
#define arginfo_class_Dom_ChildNode_remove arginfo_class_DOMChildNode_remove
#define arginfo_class_Dom_ChildNode_before arginfo_class_Dom_ParentNode_append
@ -836,6 +844,16 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Dom_Element_replaceChildren arginfo_class_Dom_ParentNode_append
#define arginfo_class_Dom_Element_querySelector arginfo_class_Dom_ParentNode_querySelector
#define arginfo_class_Dom_Element_querySelectorAll arginfo_class_Dom_ParentNode_querySelectorAll
#define arginfo_class_Dom_Element_closest arginfo_class_Dom_ParentNode_querySelector
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_Element_matches, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, selectors, IS_STRING, 0)
ZEND_END_ARG_INFO()
#define arginfo_class_Dom_Attr_isId arginfo_class_Dom_Node_hasChildNodes
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Dom_CharacterData_substringData, 0, 2, IS_STRING, 0)
@ -893,6 +911,10 @@ ZEND_END_ARG_INFO()
#define arginfo_class_Dom_DocumentFragment_replaceChildren arginfo_class_Dom_ParentNode_append
#define arginfo_class_Dom_DocumentFragment_querySelector arginfo_class_Dom_ParentNode_querySelector
#define arginfo_class_Dom_DocumentFragment_querySelectorAll arginfo_class_Dom_ParentNode_querySelectorAll
#define arginfo_class_Dom_Document_getElementsByTagName arginfo_class_Dom_Element_getElementsByTagName
#define arginfo_class_Dom_Document_getElementsByTagNameNS arginfo_class_Dom_Element_getElementsByTagNameNS
@ -988,6 +1010,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Dom_Document_importLegacyNo
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, deep, _IS_BOOL, 0, "false")
ZEND_END_ARG_INFO()
#define arginfo_class_Dom_Document_querySelector arginfo_class_Dom_ParentNode_querySelector
#define arginfo_class_Dom_Document_querySelectorAll arginfo_class_Dom_ParentNode_querySelectorAll
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Dom_HTMLDocument_createEmpty, 0, 0, Dom\\HTMLDocument, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, encoding, IS_STRING, 0, "\"UTF-8\"")
ZEND_END_ARG_INFO()
@ -1272,6 +1298,10 @@ ZEND_METHOD(Dom_Element, getElementsByTagNameNS);
ZEND_METHOD(Dom_Element, insertAdjacentElement);
ZEND_METHOD(Dom_Element, insertAdjacentText);
ZEND_METHOD(Dom_Element, setIdAttributeNode);
ZEND_METHOD(Dom_Element, querySelector);
ZEND_METHOD(Dom_Element, querySelectorAll);
ZEND_METHOD(Dom_Element, closest);
ZEND_METHOD(Dom_Element, matches);
ZEND_METHOD(Dom_CharacterData, appendData);
ZEND_METHOD(Dom_CharacterData, insertData);
ZEND_METHOD(Dom_CharacterData, deleteData);
@ -1559,6 +1589,8 @@ static const zend_function_entry class_Dom_ParentNode_methods[] = {
ZEND_RAW_FENTRY("append", NULL, arginfo_class_Dom_ParentNode_append, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL)
ZEND_RAW_FENTRY("prepend", NULL, arginfo_class_Dom_ParentNode_prepend, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL)
ZEND_RAW_FENTRY("replaceChildren", NULL, arginfo_class_Dom_ParentNode_replaceChildren, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL)
ZEND_RAW_FENTRY("querySelector", NULL, arginfo_class_Dom_ParentNode_querySelector, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL)
ZEND_RAW_FENTRY("querySelectorAll", NULL, arginfo_class_Dom_ParentNode_querySelectorAll, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL)
ZEND_FE_END
};
@ -1671,6 +1703,10 @@ static const zend_function_entry class_Dom_Element_methods[] = {
ZEND_RAW_FENTRY("append", zim_DOMElement_append, arginfo_class_Dom_Element_append, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_RAW_FENTRY("prepend", zim_DOMElement_prepend, arginfo_class_Dom_Element_prepend, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_RAW_FENTRY("replaceChildren", zim_DOMElement_replaceChildren, arginfo_class_Dom_Element_replaceChildren, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_ME(Dom_Element, querySelector, arginfo_class_Dom_Element_querySelector, ZEND_ACC_PUBLIC)
ZEND_ME(Dom_Element, querySelectorAll, arginfo_class_Dom_Element_querySelectorAll, ZEND_ACC_PUBLIC)
ZEND_ME(Dom_Element, closest, arginfo_class_Dom_Element_closest, ZEND_ACC_PUBLIC)
ZEND_ME(Dom_Element, matches, arginfo_class_Dom_Element_matches, ZEND_ACC_PUBLIC)
ZEND_FE_END
};
@ -1726,6 +1762,8 @@ static const zend_function_entry class_Dom_DocumentFragment_methods[] = {
ZEND_RAW_FENTRY("append", zim_DOMElement_append, arginfo_class_Dom_DocumentFragment_append, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_RAW_FENTRY("prepend", zim_DOMElement_prepend, arginfo_class_Dom_DocumentFragment_prepend, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_RAW_FENTRY("replaceChildren", zim_DOMElement_replaceChildren, arginfo_class_Dom_DocumentFragment_replaceChildren, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_RAW_FENTRY("querySelector", zim_Dom_Element_querySelector, arginfo_class_Dom_DocumentFragment_querySelector, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_RAW_FENTRY("querySelectorAll", zim_Dom_Element_querySelectorAll, arginfo_class_Dom_DocumentFragment_querySelectorAll, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_FE_END
};
@ -1773,6 +1811,8 @@ static const zend_function_entry class_Dom_Document_methods[] = {
ZEND_RAW_FENTRY("prepend", zim_DOMElement_prepend, arginfo_class_Dom_Document_prepend, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_RAW_FENTRY("replaceChildren", zim_DOMDocument_replaceChildren, arginfo_class_Dom_Document_replaceChildren, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_ME(Dom_Document, importLegacyNode, arginfo_class_Dom_Document_importLegacyNode, ZEND_ACC_PUBLIC)
ZEND_RAW_FENTRY("querySelector", zim_Dom_Element_querySelector, arginfo_class_Dom_Document_querySelector, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_RAW_FENTRY("querySelectorAll", zim_Dom_Element_querySelectorAll, arginfo_class_Dom_Document_querySelectorAll, ZEND_ACC_PUBLIC, NULL, NULL)
ZEND_FE_END
};

View File

@ -0,0 +1,118 @@
--TEST--
CSS Selectors - Attribute
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<a title="http://example.com" lang="en-us"/>
<a title="http://example.be"/>
<a title="ftp://example.be" lang="nl-be" tokens="abc def ghi"/>
<a title="ftp://example.nl" lang="nl-be"/>
</container>
XML);
echo "=== Case sensitive ===\n";
test_helper($dom, 'a[title]');
test_helper($dom, 'a[title="http://example.com"]');
test_helper($dom, 'a[title="http://example."]');
test_helper($dom, 'a[title*="example"]');
test_helper($dom, 'a[title*=""]');
test_helper($dom, 'a[title^="HTTP"]');
test_helper($dom, 'a[title^="http"]');
test_helper($dom, 'a[title^="http"][title$=".be"]');
test_helper($dom, 'a[title$=".com"]');
test_helper($dom, 'a[title$=".foo"]');
test_helper($dom, 'a[lang|="nl"]');
test_helper($dom, 'a[lang|="nl-be"]');
test_helper($dom, 'a[tokens~="def"]');
test_helper($dom, 'a[tokens~="de"]');
test_helper($dom, 'a[tokens~="def ghi"]');
echo "=== Case insensitive ===\n";
test_helper($dom, 'a[title]');
test_helper($dom, 'a[title="http://example.COM" i]');
test_helper($dom, 'a[title="http://EXAMPLE." i]');
test_helper($dom, 'a[title*="ExAmPlE" i]');
test_helper($dom, 'a[title^="HTTP" i]');
test_helper($dom, 'a[title^="HTTP" i][title$=".be"]');
test_helper($dom, 'a[title$=".COM" i]');
test_helper($dom, 'a[lang|="NL" i]');
test_helper($dom, 'a[lang|="NL-BE" i]');
test_helper($dom, 'a[tokens~="DE" i]');
test_helper($dom, 'a[tokens~="DEF" i]');
test_helper($dom, 'a[tokens~="DEF GHI" i]');
?>
--EXPECT--
=== Case sensitive ===
--- Selector: a[title] ---
<a title="http://example.com" lang="en-us"/>
<a title="http://example.be"/>
<a title="ftp://example.be" lang="nl-be" tokens="abc def ghi"/>
<a title="ftp://example.nl" lang="nl-be"/>
--- Selector: a[title="http://example.com"] ---
<a title="http://example.com" lang="en-us"/>
--- Selector: a[title="http://example."] ---
--- Selector: a[title*="example"] ---
<a title="http://example.com" lang="en-us"/>
<a title="http://example.be"/>
<a title="ftp://example.be" lang="nl-be" tokens="abc def ghi"/>
<a title="ftp://example.nl" lang="nl-be"/>
--- Selector: a[title*=""] ---
--- Selector: a[title^="HTTP"] ---
--- Selector: a[title^="http"] ---
<a title="http://example.com" lang="en-us"/>
<a title="http://example.be"/>
--- Selector: a[title^="http"][title$=".be"] ---
<a title="http://example.be"/>
--- Selector: a[title$=".com"] ---
<a title="http://example.com" lang="en-us"/>
--- Selector: a[title$=".foo"] ---
--- Selector: a[lang|="nl"] ---
<a title="ftp://example.be" lang="nl-be" tokens="abc def ghi"/>
<a title="ftp://example.nl" lang="nl-be"/>
--- Selector: a[lang|="nl-be"] ---
<a title="ftp://example.be" lang="nl-be" tokens="abc def ghi"/>
<a title="ftp://example.nl" lang="nl-be"/>
--- Selector: a[tokens~="def"] ---
<a title="ftp://example.be" lang="nl-be" tokens="abc def ghi"/>
--- Selector: a[tokens~="de"] ---
--- Selector: a[tokens~="def ghi"] ---
=== Case insensitive ===
--- Selector: a[title] ---
<a title="http://example.com" lang="en-us"/>
<a title="http://example.be"/>
<a title="ftp://example.be" lang="nl-be" tokens="abc def ghi"/>
<a title="ftp://example.nl" lang="nl-be"/>
--- Selector: a[title="http://example.COM" i] ---
<a title="http://example.com" lang="en-us"/>
--- Selector: a[title="http://EXAMPLE." i] ---
--- Selector: a[title*="ExAmPlE" i] ---
<a title="http://example.com" lang="en-us"/>
<a title="http://example.be"/>
<a title="ftp://example.be" lang="nl-be" tokens="abc def ghi"/>
<a title="ftp://example.nl" lang="nl-be"/>
--- Selector: a[title^="HTTP" i] ---
<a title="http://example.com" lang="en-us"/>
<a title="http://example.be"/>
--- Selector: a[title^="HTTP" i][title$=".be"] ---
<a title="http://example.be"/>
--- Selector: a[title$=".COM" i] ---
<a title="http://example.com" lang="en-us"/>
--- Selector: a[lang|="NL" i] ---
<a title="ftp://example.be" lang="nl-be" tokens="abc def ghi"/>
<a title="ftp://example.nl" lang="nl-be"/>
--- Selector: a[lang|="NL-BE" i] ---
<a title="ftp://example.be" lang="nl-be" tokens="abc def ghi"/>
<a title="ftp://example.nl" lang="nl-be"/>
--- Selector: a[tokens~="DE" i] ---
--- Selector: a[tokens~="DEF" i] ---
<a title="ftp://example.be" lang="nl-be" tokens="abc def ghi"/>
--- Selector: a[tokens~="DEF GHI" i] ---

View File

@ -0,0 +1,49 @@
--TEST--
Test DOM\Element::closest() method: legit cases
--EXTENSIONS--
dom
--FILE--
<?php
$xml = <<<XML
<root>
<a/>
<div class="foo" xml:id="div1">
<div xml:id="div2">
<div class="bar" xml:id="div3"/>
</div>
</div>
</root>
XML;
$dom = DOM\XMLDocument::createFromString($xml);
function test($el, $selector) {
echo "--- Selector: $selector ---\n";
var_dump($el->closest($selector)?->getAttribute('xml:id'));
}
test($dom->getElementById('div3'), 'div');
test($dom->getElementById('div3'), '[class="foo"]');
test($dom->getElementById('div3'), ':not(root)');
test($dom->getElementById('div3'), ':not(div)');
test($dom->getElementById('div3'), 'a');
test($dom->getElementById('div3'), 'root :not(div[class])');
test($dom->getElementById('div3'), 'root > :not(div[class])');
?>
--EXPECT--
--- Selector: div ---
string(4) "div3"
--- Selector: [class="foo"] ---
string(4) "div1"
--- Selector: :not(root) ---
string(4) "div3"
--- Selector: :not(div) ---
NULL
--- Selector: a ---
NULL
--- Selector: root :not(div[class]) ---
string(4) "div2"
--- Selector: root > :not(div[class]) ---
NULL

View File

@ -0,0 +1,18 @@
--TEST--
Test DOM\Element::closest() method: invalid selector
--EXTENSIONS--
dom
--FILE--
<?php
$dom = DOM\XMLDocument::createFromString("<root/>");
try {
var_dump($dom->documentElement->closest('@invalid'));
} catch (DOMException $e) {
echo $e->getMessage();
}
?>
--EXPECT--
Invalid selector (Selectors. Unexpected token: @invalid)

View File

@ -0,0 +1,113 @@
--TEST--
CSS Selectors - Combinators
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\HTMLDocument::createFromString(<<<HTML
<!DOCTYPE html>
<html>
<body>
<p>First p</p>
<p>Second p</p>
<img src="1.png">
<div class="">
<p>Third p</p>
<img src="2.png">
<img src="3.png">
<div class="foo bar baz">
<p>Fourth p</p>
</div>
</div>
<article title="foo" class="bar">
<p class="bar">Fifth p</p>
</article>
<table border="1">
<colgroup>
<col class="selected">
</colgroup>
<tbody>
<tr>
<td>A</td>
<td>B</td>
<td>C</td>
</tr>
</tbody>
</table>
</body>
</html>
HTML);
test_helper($dom, 'nonsense');
test_helper($dom, 'p');
test_helper($dom, 'p, img');
test_helper($dom, 'body p');
test_helper($dom, 'body div p');
test_helper($dom, 'div > *');
test_helper($dom, 'div > p');
test_helper($dom, 'div > p + img');
test_helper($dom, 'div > p ~ img');
test_helper($dom, 'body > img');
test_helper($dom, 'div.bar.baz > p');
test_helper($dom, 'article[title].bar p');
try {
test_helper($dom, 'col.selected||td');
} catch (ValueError $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
--- Selector: nonsense ---
--- Selector: p ---
<p xmlns="http://www.w3.org/1999/xhtml">First p</p>
<p xmlns="http://www.w3.org/1999/xhtml">Second p</p>
<p xmlns="http://www.w3.org/1999/xhtml">Third p</p>
<p xmlns="http://www.w3.org/1999/xhtml">Fourth p</p>
<p xmlns="http://www.w3.org/1999/xhtml" class="bar">Fifth p</p>
--- Selector: p, img ---
<p xmlns="http://www.w3.org/1999/xhtml">First p</p>
<p xmlns="http://www.w3.org/1999/xhtml">Second p</p>
<img xmlns="http://www.w3.org/1999/xhtml" src="1.png" />
<p xmlns="http://www.w3.org/1999/xhtml">Third p</p>
<img xmlns="http://www.w3.org/1999/xhtml" src="2.png" />
<img xmlns="http://www.w3.org/1999/xhtml" src="3.png" />
<p xmlns="http://www.w3.org/1999/xhtml">Fourth p</p>
<p xmlns="http://www.w3.org/1999/xhtml" class="bar">Fifth p</p>
--- Selector: body p ---
<p xmlns="http://www.w3.org/1999/xhtml">First p</p>
<p xmlns="http://www.w3.org/1999/xhtml">Second p</p>
<p xmlns="http://www.w3.org/1999/xhtml">Third p</p>
<p xmlns="http://www.w3.org/1999/xhtml">Fourth p</p>
<p xmlns="http://www.w3.org/1999/xhtml" class="bar">Fifth p</p>
--- Selector: body div p ---
<p xmlns="http://www.w3.org/1999/xhtml">Third p</p>
<p xmlns="http://www.w3.org/1999/xhtml">Fourth p</p>
--- Selector: div > * ---
<p xmlns="http://www.w3.org/1999/xhtml">Third p</p>
<img xmlns="http://www.w3.org/1999/xhtml" src="2.png" />
<img xmlns="http://www.w3.org/1999/xhtml" src="3.png" />
<div xmlns="http://www.w3.org/1999/xhtml" class="foo bar baz">
<p>Fourth p</p>
</div>
<p xmlns="http://www.w3.org/1999/xhtml">Fourth p</p>
--- Selector: div > p ---
<p xmlns="http://www.w3.org/1999/xhtml">Third p</p>
<p xmlns="http://www.w3.org/1999/xhtml">Fourth p</p>
--- Selector: div > p + img ---
<img xmlns="http://www.w3.org/1999/xhtml" src="2.png" />
--- Selector: div > p ~ img ---
<img xmlns="http://www.w3.org/1999/xhtml" src="2.png" />
<img xmlns="http://www.w3.org/1999/xhtml" src="3.png" />
--- Selector: body > img ---
<img xmlns="http://www.w3.org/1999/xhtml" src="1.png" />
--- Selector: div.bar.baz > p ---
<p xmlns="http://www.w3.org/1999/xhtml">Fourth p</p>
--- Selector: article[title].bar p ---
<p xmlns="http://www.w3.org/1999/xhtml" class="bar">Fifth p</p>
--- Selector: col.selected||td ---
Dom\Document::querySelectorAll(): Argument #1 ($selectors) contains an unsupported selector

View File

@ -0,0 +1,26 @@
--TEST--
CSS Selectors - Handling entities
--EXTENSIONS--
dom
--FILE--
<?php
$dom = Dom\XMLDocument::createFromString(<<<XML
<!DOCTYPE root [
<!ENTITY ent "box">
]>
<root>
<input type="check&ent;" checked="" class="a&ent;a" id="a&ent;a" xmlns="http://www.w3.org/1999/xhtml"/>
</root>
XML);
var_dump($dom->querySelector('input:checked')->nodeName);
var_dump($dom->querySelector('input[type$="ox"]')->nodeName);
var_dump($dom->querySelector('input.aboxa')->nodeName);
var_dump($dom->querySelector('input#aboxa')->nodeName);
?>
--EXPECT--
string(5) "input"
string(5) "input"
string(5) "input"
string(5) "input"

View File

@ -0,0 +1,35 @@
--TEST--
CSS Selectors - ID
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<test id="test1"/>
<test/>
<test id="test2"/>
<test id="test3"/>
<test xmlns:x="urn:x" x:id="test4"/>
</container>
XML);
test_helper($dom, '#test');
test_helper($dom, '#test1');
test_helper($dom, '#test2');
test_helper($dom, '#test3');
test_helper($dom, '#test4');
?>
--EXPECT--
--- Selector: #test ---
--- Selector: #test1 ---
<test id="test1"/>
--- Selector: #test2 ---
<test id="test2"/>
--- Selector: #test3 ---
<test id="test3"/>
--- Selector: #test4 ---

View File

@ -0,0 +1,18 @@
--TEST--
Test DOM\Element::matches() method: invalid selector
--EXTENSIONS--
dom
--FILE--
<?php
$dom = DOM\XMLDocument::createFromString("<root/>");
try {
var_dump($dom->documentElement->matches('@invalid'));
} catch (DOMException $e) {
echo $e->getMessage();
}
?>
--EXPECT--
Invalid selector (Selectors. Unexpected token: @invalid)

View File

@ -0,0 +1,54 @@
--TEST--
CSS Selectors - Namespaces
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<root>
<container align="left"/>
<only>
<a xmlns="urn:a"/>
<a xmlns="urn:a"/>
<a xmlns="urn:b"/>
<a xmlns=""/>
<a/>
</only>
</root>
XML);
$container = $dom->documentElement->firstElementChild;
$container->setAttribute("foo:bar", "baz");
$container->setAttributeNS("urn:a", "a:bar", "baz");
test_helper($dom, 'container[align]');
test_helper($dom, 'container[foo\\:bar]');
test_helper($dom, 'container[a\\:bar]');
test_helper($dom, 'container[bar]');
test_helper($dom, 'a:first-of-type');
test_helper($dom, 'a:last-of-type');
test_failure($dom, 'container[* | bar]');
?>
--EXPECT--
--- Selector: container[align] ---
<container align="left" foo:bar="baz" xmlns:a="urn:a" a:bar="baz"/>
--- Selector: container[foo\:bar] ---
<container align="left" foo:bar="baz" xmlns:a="urn:a" a:bar="baz"/>
--- Selector: container[a\:bar] ---
--- Selector: container[bar] ---
--- Selector: a:first-of-type ---
<a xmlns="urn:a"/>
<a xmlns="urn:b"/>
<a xmlns=""/>
--- Selector: a:last-of-type ---
<a xmlns="urn:a"/>
<a xmlns="urn:b"/>
<a/>
--- Selector: container[* | bar] ---
Code 12 Invalid selector (Selectors. Unexpected token: *)

View File

@ -0,0 +1,23 @@
--TEST--
CSS Selectors - Pseudo classes: blank
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container/>
XML);
try {
test_helper($dom, ':blank');
} catch (DOMException $e) {
echo $e->getMessage(), "\n";
}
?>
--EXPECT--
--- Selector: :blank ---
:blank selector is not implemented because CSSWG has not yet decided its semantics (https://github.com/w3c/csswg-drafts/issues/1967)

View File

@ -0,0 +1,27 @@
--TEST--
CSS Selectors - Pseudo classes: checked
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<html xmlns="http://www.w3.org/1999/xhtml">
<input type="checkbox" checked="checked" />
<input type="radio" checked="checked" />
<option selected="" />
<option xmlns="" selected="" />
<input/>
</html>
XML);
test_helper($dom, ':checked');
?>
--EXPECT--
--- Selector: :checked ---
<input xmlns="http://www.w3.org/1999/xhtml" type="checkbox" checked="checked" />
<input xmlns="http://www.w3.org/1999/xhtml" type="radio" checked="checked" />
<option xmlns="http://www.w3.org/1999/xhtml" selected=""></option>

View File

@ -0,0 +1,21 @@
--TEST--
CSS Selectors - Pseudo classes: current
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<html xmlns="http://www.w3.org/1999/xhtml">
<div/>
</html>
XML);
test_helper($dom, ':current(div)');
?>
--EXPECT--
--- Selector: :current(div) ---
<div xmlns="http://www.w3.org/1999/xhtml"></div>

View File

@ -0,0 +1,27 @@
--TEST--
CSS Selectors - Pseudo classes: dir
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container dir="rtl">
<p>1</p>
<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr">
<p>2</p>
</html>
</container>
XML);
test_failure($dom, ':dir(rtl)', true);
test_failure($dom, ':dir(ltr)', true);
?>
--EXPECT--
--- Selector: :dir(rtl) ---
Code 12 Invalid selector (Selectors. Not supported: dir)
--- Selector: :dir(ltr) ---
Code 12 Invalid selector (Selectors. Not supported: dir)

View File

@ -0,0 +1,107 @@
--TEST--
CSS Selectors - Pseudo classes: disabled/enabled
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<html xmlns="http://www.w3.org/1999/xhtml">
<button/>
<button xmlns="" disabled="disabled"/>
<button disabled="disabled"/>
<input disabled="disabled"/>
<select disabled="disabled"/>
<textarea disabled="disabled"/>
<optgroup disabled="disabled"/>
<!-- fieldset rules are complicated -->
<fieldset disabled="disabled"/>
<fieldset disabled="disabled">
<fieldset id="1"/>
</fieldset>
<fieldset disabled="disabled">
<!-- foo -->
<legend>
<div>
<fieldset id="2"/>
</div>
</legend>
<div>
<fieldset id="3"/>
</div>
</fieldset>
</html>
XML);
test_helper($dom, '*:disabled');
test_helper($dom, '*:enabled');
?>
--EXPECT--
--- Selector: *:disabled ---
<button xmlns="http://www.w3.org/1999/xhtml" disabled="disabled"></button>
<input xmlns="http://www.w3.org/1999/xhtml" disabled="disabled" />
<select xmlns="http://www.w3.org/1999/xhtml" disabled="disabled"></select>
<textarea xmlns="http://www.w3.org/1999/xhtml" disabled="disabled"></textarea>
<optgroup xmlns="http://www.w3.org/1999/xhtml" disabled="disabled"></optgroup>
<fieldset xmlns="http://www.w3.org/1999/xhtml" disabled="disabled"></fieldset>
<fieldset xmlns="http://www.w3.org/1999/xhtml" disabled="disabled">
<fieldset id="1"></fieldset>
</fieldset>
<fieldset xmlns="http://www.w3.org/1999/xhtml" disabled="disabled">
<!-- foo -->
<legend>
<div>
<fieldset id="2"></fieldset>
</div>
</legend>
<div>
<fieldset id="3"></fieldset>
</div>
</fieldset>
<fieldset xmlns="http://www.w3.org/1999/xhtml" id="3"></fieldset>
--- Selector: *:enabled ---
<html xmlns="http://www.w3.org/1999/xhtml">
<button></button>
<button xmlns="" disabled="disabled"/>
<button disabled="disabled"></button>
<input disabled="disabled" />
<select disabled="disabled"></select>
<textarea disabled="disabled"></textarea>
<optgroup disabled="disabled"></optgroup>
<!-- fieldset rules are complicated -->
<fieldset disabled="disabled"></fieldset>
<fieldset disabled="disabled">
<fieldset id="1"></fieldset>
</fieldset>
<fieldset disabled="disabled">
<!-- foo -->
<legend>
<div>
<fieldset id="2"></fieldset>
</div>
</legend>
<div>
<fieldset id="3"></fieldset>
</div>
</fieldset>
</html>
<button xmlns="http://www.w3.org/1999/xhtml"></button>
<button xmlns="" disabled="disabled"/>
<fieldset xmlns="http://www.w3.org/1999/xhtml" id="1"></fieldset>
<legend xmlns="http://www.w3.org/1999/xhtml">
<div>
<fieldset id="2"></fieldset>
</div>
</legend>
<div xmlns="http://www.w3.org/1999/xhtml">
<fieldset id="2"></fieldset>
</div>
<fieldset xmlns="http://www.w3.org/1999/xhtml" id="2"></fieldset>
<div xmlns="http://www.w3.org/1999/xhtml">
<fieldset id="3"></fieldset>
</div>

View File

@ -0,0 +1,38 @@
--TEST--
CSS Selectors - Pseudo classes: empty
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<div class="empty">
<p>Element with no content:</p>
<div></div>
<p>Element with comment:</p>
<div><!-- Simple Comment --></div>
<p>Element with PI:</p>
<div><?foo?></div>
<p>Element with CDATA:</p>
<div><![CDATA[foo]]></div>
<p>Element with nested empty element:</p>
<div><p></p></div>
</div>
</container>
XML);
test_helper($dom, '.empty > div:empty');
?>
--EXPECT--
--- Selector: .empty > div:empty ---
<div/>
<div><!-- Simple Comment --></div>
<div><?foo ?></div>

View File

@ -0,0 +1,29 @@
--TEST--
CSS Selectors - Pseudo classes: first/last child
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<?foo?>
<!--bar-->
<first />
&amp;
<last/>
<![CDATA[skip me]]>
</container>
XML);
test_helper($dom, 'container > *:first-child');
test_helper($dom, 'container > *:last-child');
?>
--EXPECT--
--- Selector: container > *:first-child ---
<first/>
--- Selector: container > *:last-child ---
<last/>

View File

@ -0,0 +1,33 @@
--TEST--
CSS Selectors - Pseudo classes: has
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<div>
<p class="foo"/>
</div>
<div>
<p/>
</div>
</container>
XML);
test_helper($dom, 'div:has(p.foo)');
test_helper($dom, 'div:has(:not(p.foo))');
?>
--EXPECT--
--- Selector: div:has(p.foo) ---
<div>
<p class="foo"/>
</div>
--- Selector: div:has(:not(p.foo)) ---
<div>
<p/>
</div>

View File

@ -0,0 +1,34 @@
--TEST--
CSS Selectors - Pseudo classes: is/where
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<article>
<p>1</p>
</article>
<main>
<p>2</p>
</main>
<div>
<p>3</p>
</div>
</container>
XML);
test_helper($dom, ':is(article, main) p');
test_helper($dom, ':where(article, main) p');
?>
--EXPECT--
--- Selector: :is(article, main) p ---
<p>1</p>
<p>2</p>
--- Selector: :where(article, main) p ---
<p>1</p>
<p>2</p>

View File

@ -0,0 +1,27 @@
--TEST--
CSS Selectors - Pseudo classes: lang
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container lang="en">
<p>1</p>
<html xmlns="http://www.w3.org/1999/xhtml" lang="nl">
<p>2</p>
</html>
</container>
XML);
test_failure($dom, ':lang(en)', true);
test_failure($dom, ':lang(nl)', true);
?>
--EXPECT--
--- Selector: :lang(en) ---
Code 12 Invalid selector (Selectors. Not supported: lang)
--- Selector: :lang(nl) ---
Code 12 Invalid selector (Selectors. Not supported: lang)

View File

@ -0,0 +1,31 @@
--TEST--
CSS Selectors - Pseudo classes: links
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<a href="http://example.com">Link</a>
<a xmlns="http://www.w3.org/1999/xhtml" href="http://example.com">Link</a>
<area xmlns="http://www.w3.org/1999/xhtml" href="http://example.com">Link</area>
</container>
XML);
test_helper($dom, ':any-link');
test_helper($dom, ':link');
test_helper($dom, 'a:not(:any-link)');
?>
--EXPECT--
--- Selector: :any-link ---
<a xmlns="http://www.w3.org/1999/xhtml" href="http://example.com">Link</a>
<area xmlns="http://www.w3.org/1999/xhtml" href="http://example.com">Link</area>
--- Selector: :link ---
<a xmlns="http://www.w3.org/1999/xhtml" href="http://example.com">Link</a>
<area xmlns="http://www.w3.org/1999/xhtml" href="http://example.com">Link</area>
--- Selector: a:not(:any-link) ---
<a href="http://example.com">Link</a>

View File

@ -0,0 +1,83 @@
--TEST--
CSS Selectors - Pseudo classes: no-ops
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<html xmlns="http://www.w3.org/1999/xhtml">
<div/>
<a href="#foo"/>
</html>
XML);
// These will always fail or return an empty list because they are display-specific or require user interaction
test_failure($dom, ':default');
test_failure($dom, ':focus');
test_failure($dom, ':focus-visible');
test_failure($dom, ':focus-within');
test_failure($dom, ':fullscreen');
test_failure($dom, ':hover');
test_failure($dom, ':in-range');
test_failure($dom, ':indeterminate');
test_failure($dom, ':invalid');
test_failure($dom, ':out-of-range');
test_failure($dom, ':past');
test_failure($dom, ':future');
test_failure($dom, ':scope');
test_failure($dom, ':target');
test_failure($dom, ':target-within');
test_failure($dom, ':user-invalid');
test_failure($dom, ':valid');
test_failure($dom, ':visited');
test_failure($dom, ':warning');
test_failure($dom, ':local-link');
test_failure($dom, ':active');
?>
--EXPECT--
--- Selector: :default ---
Code 12 Invalid selector (Selectors. Not supported: default)
--- Selector: :focus ---
int(0)
--- Selector: :focus-visible ---
Code 12 Invalid selector (Selectors. Not supported: focus-visible)
--- Selector: :focus-within ---
Code 12 Invalid selector (Selectors. Not supported: focus-within)
--- Selector: :fullscreen ---
Code 12 Invalid selector (Selectors. Not supported: fullscreen)
--- Selector: :hover ---
int(0)
--- Selector: :in-range ---
Code 12 Invalid selector (Selectors. Not supported: in-range)
--- Selector: :indeterminate ---
Code 12 Invalid selector (Selectors. Not supported: indeterminate)
--- Selector: :invalid ---
Code 12 Invalid selector (Selectors. Not supported: invalid)
--- Selector: :out-of-range ---
Code 12 Invalid selector (Selectors. Not supported: out-of-range)
--- Selector: :past ---
Code 12 Invalid selector (Selectors. Not supported: past)
--- Selector: :future ---
Code 12 Invalid selector (Selectors. Not supported: future)
--- Selector: :scope ---
Code 12 Invalid selector (Selectors. Not supported: scope)
--- Selector: :target ---
Code 12 Invalid selector (Selectors. Not supported: target)
--- Selector: :target-within ---
Code 12 Invalid selector (Selectors. Not supported: target-within)
--- Selector: :user-invalid ---
Code 12 Invalid selector (Selectors. Not supported: user-invalid)
--- Selector: :valid ---
Code 12 Invalid selector (Selectors. Not supported: valid)
--- Selector: :visited ---
Code 12 Invalid selector (Selectors. Not supported: visited)
--- Selector: :warning ---
Code 12 Invalid selector (Selectors. Not supported: warning)
--- Selector: :local-link ---
Code 12 Invalid selector (Selectors. Not supported: local-link)
--- Selector: :active ---
int(0)

View File

@ -0,0 +1,56 @@
--TEST--
CSS Selectors - Pseudo classes: nth-child
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<h2>1</h2>
<h2>2</h2>
<h2>3</h2>
<h2>4</h2>
<h2>5</h2>
</container>
XML);
test_helper($dom, 'h2:nth-of-type(n+2):nth-last-of-type(n+2)');
test_helper($dom, 'h2:not(:first-of-type):not(:last-of-type)'); // Equivalent to the above
test_helper($dom, 'h2:nth-child(2)');
test_helper($dom, 'h2:nth-last-child(2)');
test_helper($dom, 'h2:nth-child(2n + 1)');
test_helper($dom, 'h2:nth-last-child(2n + 1)');
test_helper($dom, 'h2:nth-child(3n - 2)');
test_helper($dom, 'h2:nth-last-child(3n - 2)');
?>
--EXPECT--
--- Selector: h2:nth-of-type(n+2):nth-last-of-type(n+2) ---
<h2>2</h2>
<h2>3</h2>
<h2>4</h2>
--- Selector: h2:not(:first-of-type):not(:last-of-type) ---
<h2>2</h2>
<h2>3</h2>
<h2>4</h2>
--- Selector: h2:nth-child(2) ---
<h2>2</h2>
--- Selector: h2:nth-last-child(2) ---
<h2>4</h2>
--- Selector: h2:nth-child(2n + 1) ---
<h2>1</h2>
<h2>3</h2>
<h2>5</h2>
--- Selector: h2:nth-last-child(2n + 1) ---
<h2>1</h2>
<h2>3</h2>
<h2>5</h2>
--- Selector: h2:nth-child(3n - 2) ---
<h2>1</h2>
<h2>4</h2>
--- Selector: h2:nth-last-child(3n - 2) ---
<h2>2</h2>
<h2>5</h2>

View File

@ -0,0 +1,56 @@
--TEST--
CSS Selectors - Pseudo classes: nth-child of
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<h2 class="hi">1</h2>
<h2>2</h2>
<h2 class="hi">3</h2>
<h2 class="hi">4</h2>
<h2>5</h2>
<h2 class="hi">6</h2>
</container>
XML);
test_helper($dom, 'h2:nth-child(even of .hi)');
test_helper($dom, 'h2.hi:nth-child(even)');
test_helper($dom, 'h2:nth-child(odd of .hi)');
test_helper($dom, 'h2.hi:nth-child(odd)');
test_helper($dom, 'h2:nth-last-child(even of .hi)');
test_helper($dom, 'h2.hi:nth-last-child(even)');
test_helper($dom, 'h2:nth-last-child(odd of .hi)');
test_helper($dom, 'h2.hi:nth-last-child(odd)');
?>
--EXPECT--
--- Selector: h2:nth-child(even of .hi) ---
<h2 class="hi">3</h2>
<h2 class="hi">6</h2>
--- Selector: h2.hi:nth-child(even) ---
<h2 class="hi">4</h2>
<h2 class="hi">6</h2>
--- Selector: h2:nth-child(odd of .hi) ---
<h2 class="hi">1</h2>
<h2 class="hi">4</h2>
--- Selector: h2.hi:nth-child(odd) ---
<h2 class="hi">1</h2>
<h2 class="hi">3</h2>
--- Selector: h2:nth-last-child(even of .hi) ---
<h2 class="hi">1</h2>
<h2 class="hi">4</h2>
--- Selector: h2.hi:nth-last-child(even) ---
<h2 class="hi">1</h2>
<h2 class="hi">3</h2>
--- Selector: h2:nth-last-child(odd of .hi) ---
<h2 class="hi">3</h2>
<h2 class="hi">6</h2>
--- Selector: h2.hi:nth-last-child(odd) ---
<h2 class="hi">4</h2>
<h2 class="hi">6</h2>

View File

@ -0,0 +1,36 @@
--TEST--
CSS Selectors - Pseudo classes: nth-(last-)col
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\HTMLDocument::createFromString(<<<HTML
<!DOCTYPE html>
<html>
<body>
<table>
<tr>
<th>Col 1</th>
<th>Col 2</th>
</tr>
<tr>
<td>1</td>
<td>2</td>
</tr>
</table>
</body>
</html>
HTML);
test_failure($dom, ':nth-col(1)', true);
test_failure($dom, ':nth-last-col(1)', true);
?>
--EXPECT--
--- Selector: :nth-col(1) ---
Code 12 Invalid selector (Selectors. Not supported: nth-col)
--- Selector: :nth-last-col(1) ---
Code 12 Invalid selector (Selectors. Not supported: nth-last-col)

View File

@ -0,0 +1,29 @@
--TEST--
CSS Selectors - Pseudo classes: only-child
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<div class="only-child1">
<p>Lonely</p>
</div>
<div class="only-child2">
<p>With 2</p>
<p>With 2</p>
</div>
</container>
XML);
test_helper($dom, '.only-child1 p:only-child');
test_helper($dom, '.only-child2 p:only-child');
?>
--EXPECT--
--- Selector: .only-child1 p:only-child ---
<p>Lonely</p>
--- Selector: .only-child2 p:only-child ---

View File

@ -0,0 +1,36 @@
--TEST--
CSS Selectors - Pseudo classes: only-of-type
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<div class="only-of-type1">
<p>Alone</p>
</div>
<div class="only-of-type2">
<p>With 2</p>
<p>With 2</p>
</div>
<div class="only-of-type3">
<p>With 2</p>
<div/>
<p>With 2</p>
</div>
</container>
XML);
test_helper($dom, '.only-of-type1 p:only-of-type');
test_helper($dom, '.only-of-type2 p:only-of-type');
test_helper($dom, '.only-of-type3 p:only-of-type');
?>
--EXPECT--
--- Selector: .only-of-type1 p:only-of-type ---
<p>Alone</p>
--- Selector: .only-of-type2 p:only-of-type ---
--- Selector: .only-of-type3 p:only-of-type ---

View File

@ -0,0 +1,35 @@
--TEST--
CSS Selectors - Pseudo classes: optional/required
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<html xmlns="http://www.w3.org/1999/xhtml">
<input type="checkbox" required="required" />
<select required="required" />
<textarea required="" />
<input type="checkbox" />
<select />
<textarea />
<input xmlns=""/>
<input xmlns="" required="" />
</html>
XML);
test_helper($dom, ':required');
test_helper($dom, ':optional');
?>
--EXPECT--
--- Selector: :required ---
<input xmlns="http://www.w3.org/1999/xhtml" type="checkbox" required="required" />
<select xmlns="http://www.w3.org/1999/xhtml" required="required"></select>
<textarea xmlns="http://www.w3.org/1999/xhtml" required=""></textarea>
--- Selector: :optional ---
<input xmlns="http://www.w3.org/1999/xhtml" type="checkbox" />
<select xmlns="http://www.w3.org/1999/xhtml"></select>
<textarea xmlns="http://www.w3.org/1999/xhtml"></textarea>

View File

@ -0,0 +1,27 @@
--TEST--
CSS Selectors - Pseudo classes: placeholder-shown
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<html xmlns="http://www.w3.org/1999/xhtml">
<input type="text" placeholder="" />
<textarea placeholder="" />
<input xmlns="" type="text" placeholder="" />
<textarea xmlns="" placeholder="" />
<input type="text" />
<textarea />
</html>
XML);
test_helper($dom, ':placeholder-shown');
?>
--EXPECT--
--- Selector: :placeholder-shown ---
<input xmlns="http://www.w3.org/1999/xhtml" type="text" placeholder="" />
<textarea xmlns="http://www.w3.org/1999/xhtml" placeholder=""></textarea>

View File

@ -0,0 +1,53 @@
--TEST--
CSS Selectors - Pseudo classes: read-write/read-only
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<html xmlns="http://www.w3.org/1999/xhtml">
<input type="text" readonly="" />
<textarea readonly="" />
<input type="text" disabled="" />
<textarea disabled="" />
<input type="text" xmlns="" />
<textarea xmlns="" />
<input type="text" />
<textarea />
<p contenteditable="" />
<p contenteditable="false" />
</html>
XML);
test_helper($dom, ':read-write');
test_helper($dom, ':read-only');
?>
--EXPECT--
--- Selector: :read-write ---
<input xmlns="http://www.w3.org/1999/xhtml" type="text" />
<textarea xmlns="http://www.w3.org/1999/xhtml"></textarea>
<p xmlns="http://www.w3.org/1999/xhtml" contenteditable=""></p>
--- Selector: :read-only ---
<html xmlns="http://www.w3.org/1999/xhtml">
<input type="text" readonly="" />
<textarea readonly=""></textarea>
<input type="text" disabled="" />
<textarea disabled=""></textarea>
<input xmlns="" type="text"/>
<textarea xmlns=""/>
<input type="text" />
<textarea></textarea>
<p contenteditable=""></p>
<p contenteditable="false"></p>
</html>
<input xmlns="http://www.w3.org/1999/xhtml" type="text" readonly="" />
<textarea xmlns="http://www.w3.org/1999/xhtml" readonly=""></textarea>
<input xmlns="http://www.w3.org/1999/xhtml" type="text" disabled="" />
<textarea xmlns="http://www.w3.org/1999/xhtml" disabled=""></textarea>
<input xmlns="" type="text"/>
<textarea xmlns=""/>
<p xmlns="http://www.w3.org/1999/xhtml" contenteditable="false"></p>

View File

@ -0,0 +1,29 @@
--TEST--
CSS Selectors - Pseudo classes: root
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container/>
XML);
test_helper($dom, ':root', true);
$fragment = $dom->createDocumentFragment();
$fragment->appendXML('<div><p></p></div>');
test_helper($fragment, ':root', true);
test_helper($dom->createElement("foo"), ':root', true);
test_helper($dom->createElement("not-a-root"), ":root", true);
?>
--EXPECT--
--- Selector: :root ---
container
--- Selector: :root ---
div
--- Selector: :root ---
--- Selector: :root ---

View File

@ -0,0 +1,55 @@
--TEST--
CSS Selectors - Pseudo elements
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
$dom = DOM\XMLDocument::createFromString(<<<XML
<container>
<a/>
</container>
XML);
// Pseudo-elements can't work in a static DOM
test_failure($dom, 'a::after');
test_failure($dom, 'a::before');
test_failure($dom, 'a::backdrop');
test_failure($dom, 'a::first-letter');
test_failure($dom, 'a::first-line');
test_failure($dom, 'a::grammar-error');
test_failure($dom, 'a::inactive-selection');
test_failure($dom, 'a::marker');
test_failure($dom, 'a::placeholder');
test_failure($dom, 'a::selection');
test_failure($dom, 'a::spelling-error');
test_failure($dom, 'a::target-text');
?>
--EXPECT--
--- Selector: a::after ---
Code 12 Invalid selector (Selectors. Not supported: after)
--- Selector: a::before ---
Code 12 Invalid selector (Selectors. Not supported: before)
--- Selector: a::backdrop ---
Code 12 Invalid selector (Selectors. Not supported: backdrop)
--- Selector: a::first-letter ---
Code 12 Invalid selector (Selectors. Not supported: first-letter)
--- Selector: a::first-line ---
Code 12 Invalid selector (Selectors. Not supported: first-line)
--- Selector: a::grammar-error ---
Code 12 Invalid selector (Selectors. Not supported: grammar-error)
--- Selector: a::inactive-selection ---
Code 12 Invalid selector (Selectors. Not supported: inactive-selection)
--- Selector: a::marker ---
Code 12 Invalid selector (Selectors. Not supported: marker)
--- Selector: a::placeholder ---
Code 12 Invalid selector (Selectors. Not supported: placeholder)
--- Selector: a::selection ---
Code 12 Invalid selector (Selectors. Not supported: selection)
--- Selector: a::spelling-error ---
Code 12 Invalid selector (Selectors. Not supported: spelling-error)
--- Selector: a::target-text ---
Code 12 Invalid selector (Selectors. Not supported: target-text)

View File

@ -0,0 +1,68 @@
--TEST--
CSS Selectors - Quirks mode test
--EXTENSIONS--
dom
--FILE--
<?php
require __DIR__ . '/test_utils.inc';
echo "\n=== Document in quirks mode ===\n\n";
$dom = DOM\HTMLDocument::createFromString(<<<HTML
<html>
<div class="HElLoWorLD"/>
<div id="hI"/>
</html>
HTML, LIBXML_NOERROR);
test_helper($dom, 'div.helloworld');
test_helper($dom, 'div.HElLoWorLD');
test_helper($dom, '#hi');
test_helper($dom, '#hI');
echo "\n=== Document not in quirks mode ===\n\n";
$dom = DOM\HTMLDocument::createFromString(<<<HTML
<!DOCTYPE html>
<html>
<div class="HElLoWorLD"/>
<div id="hI"/>
</html>
HTML, LIBXML_NOERROR);
test_helper($dom, 'div.helloworld');
test_helper($dom, 'div.HElLoWorLD');
test_helper($dom, '#hi');
test_helper($dom, '#hI');
?>
--EXPECT--
=== Document in quirks mode ===
--- Selector: div.helloworld ---
<div xmlns="http://www.w3.org/1999/xhtml" class="HElLoWorLD">
<div id="hI">
</div></div>
--- Selector: div.HElLoWorLD ---
<div xmlns="http://www.w3.org/1999/xhtml" class="HElLoWorLD">
<div id="hI">
</div></div>
--- Selector: #hi ---
<div xmlns="http://www.w3.org/1999/xhtml" id="hI">
</div>
--- Selector: #hI ---
<div xmlns="http://www.w3.org/1999/xhtml" id="hI">
</div>
=== Document not in quirks mode ===
--- Selector: div.helloworld ---
--- Selector: div.HElLoWorLD ---
<div xmlns="http://www.w3.org/1999/xhtml" class="HElLoWorLD">
<div id="hI">
</div></div>
--- Selector: #hi ---
--- Selector: #hI ---
<div xmlns="http://www.w3.org/1999/xhtml" id="hI">
</div>

View File

@ -0,0 +1,46 @@
<?php
function test_helper(DOM\ParentNode $dom, string $selector, bool $only_name = false)
{
echo "--- Selector: $selector ---\n";
$all = $dom->querySelectorAll($selector);
$single = $dom->querySelector($selector);
if ((count($all) === 0 && $single !== null) || ($all[0] !== $single)) {
throw new Exception('Mismatch in querySelector and querySelectorAll');
}
$list = [];
foreach ($all as $node) {
$list[] = $node;
if ($only_name) {
echo $node->nodeName, "\n";
continue;
}
echo $dom->saveXML($node), "\n";
}
// If the element is in the list, then it must match, otherwise it must not
// This loops over all the elements in the document and checks them
foreach ($dom->querySelectorAll('*') as $node) {
if (in_array($node, $list, true) !== $node->matches($selector)) {
var_dump($node, $selector, in_array($node, $list, true), $node->matches($selector));
echo $dom->saveXML($node), "\n";
throw new Exception('Bug in Element::matches()');
}
}
}
function test_failure(DOM\ParentNode $dom, string $selector)
{
echo "--- Selector: $selector ---\n";
try {
var_dump(count($dom->querySelectorAll($selector)));
} catch (DOMException $e) {
echo "Code ", $e->getCode(), " ", $e->getMessage(), "\n";
}
}

View File

@ -1350,6 +1350,7 @@ PHP_LIBXML_API int php_libxml_increment_doc_ref(php_libxml_node_object *object,
object->document->private_data = NULL;
object->document->class_type = PHP_LIBXML_CLASS_UNSET;
object->document->handlers = &php_libxml_default_document_handlers;
object->document->quirks_mode = false;
}
return ret_refcount;

View File

@ -102,7 +102,8 @@ typedef struct _php_libxml_ref_obj {
php_libxml_private_data_header *private_data;
const php_libxml_document_handlers *handlers;
int refcount;
php_libxml_class_type class_type;
php_libxml_class_type class_type : 8;
bool quirks_mode;
} php_libxml_ref_obj;
typedef struct _php_libxml_node_ptr {