diff --git a/html-api-debugger/html-api-debugger.php b/html-api-debugger/html-api-debugger.php
index 6a4a3f0..6aaefe3 100644
--- a/html-api-debugger/html-api-debugger.php
+++ b/html-api-debugger/html-api-debugger.php
@@ -45,6 +45,7 @@ function () {
$html = $request->get_json_params()['html'] ?: '';
$options = array(
'context_html' => $request->get_json_params()['contextHTML'] ?: null,
+ 'selector' => $request->get_json_params()['selector'] ?: null,
);
return prepare_html_result_object( $html, $options );
},
@@ -112,6 +113,7 @@ function () {
$options = array(
'context_html' => null,
+ 'selector' => null,
);
$html = '';
@@ -122,6 +124,9 @@ function () {
if ( isset( $_GET['contextHTML'] ) && is_string( $_GET['contextHTML'] ) ) {
$options['context_html'] = stripslashes( $_GET['contextHTML'] );
}
+ if ( isset( $_GET['selector'] ) && is_string( $_GET['selector'] ) ) {
+ $options['selector'] = stripslashes( $_GET['selector'] );
+ }
// phpcs:enable WordPress.Security.NonceVerification.Recommended
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
diff --git a/html-api-debugger/html-api-integration.php b/html-api-debugger/html-api-integration.php
index 0875ecb..c9a4410 100644
--- a/html-api-debugger/html-api-integration.php
+++ b/html-api-debugger/html-api-integration.php
@@ -12,6 +12,9 @@
function get_supports(): array {
return array(
'create_fragment_advanced' => method_exists( WP_HTML_Processor::class, 'create_fragment_at_current_node' ),
+ 'selectors' =>
+ class_exists( '\WP_CSS_Complex_Selector_List' )
+ || class_exists( '\WP_CSS_Compound_Selector_List' ),
);
}
@@ -70,6 +73,25 @@ function get_normalized_html( string $html, array $options ): ?string {
* @param array $options The options.
*/
function get_tree( string $html, array $options ): array {
+ /**
+ * Messages generated during parse.
+ *
+ * @var string[]
+ */
+ $warnings = array();
+ $selector = null;
+ if ( isset( $options['selector'] ) && class_exists( '\WP_CSS_Complex_Selector_List' ) ) {
+ $selector = \WP_CSS_Complex_Selector_List::from_selectors( $options['selector'] );
+ if ( null === $selector ) {
+ $warnings[] = 'The provided selector is invalid or unsupported.';
+ }
+ } elseif ( isset( $options['selector'] ) && class_exists( '\WP_CSS_Compound_Selector_List' ) ) {
+ $selector = \WP_CSS_Compound_Selector_List::from_selectors( $options['selector'] );
+ if ( null === $selector ) {
+ $warnings[] = 'The provided selector is invalid or unsupported.';
+ }
+ }
+
$processor_state = new ReflectionProperty( WP_HTML_Processor::class, 'state' );
$processor_state->setAccessible( true );
@@ -225,6 +247,8 @@ function get_tree( string $html, array $options ): array {
$document_title = $processor->get_modifiable_text();
}
+ $matches = $selector !== null && $selector->matches( $processor );
+
$attributes = array();
$attribute_names = $processor->get_attribute_names_with_prefix( '' );
if ( null !== $attribute_names ) {
@@ -261,6 +285,7 @@ function get_tree( string $html, array $options ): array {
'_virtual' => $is_virtual(),
'_depth' => $processor->get_current_depth(),
'_namespace' => $namespace,
+ '_matches' => $matches,
);
// Self-contained tags contain their inner contents as modifiable text.
@@ -440,6 +465,7 @@ function get_tree( string $html, array $options ): array {
'doctypeSystemId' => $doctype_system_identifier,
'contextNode' => $context_node,
+ 'warnings' => $warnings,
);
}
diff --git a/html-api-debugger/interactivity.php b/html-api-debugger/interactivity.php
index 4fb7e43..ae03226 100644
--- a/html-api-debugger/interactivity.php
+++ b/html-api-debugger/interactivity.php
@@ -37,6 +37,7 @@ function generate_page( string $html, array $options ): string {
'showInvisible' => false,
'showVirtual' => false,
'contextHTML' => $options['context_html'] ?? '',
+ 'selector' => $options['selector'] ?? '',
'hoverInfo' => 'breadcrumbs',
'hoverBreadcrumbs' => true,
@@ -47,6 +48,7 @@ function generate_page( string $html, array $options ): string {
'htmlApiDoctypeName' => $htmlapi_response['result']['doctypeName'] ?? null,
'htmlApiDoctypePublicId' => $htmlapi_response['result']['doctypePublicId'] ?? null,
'htmlApiDoctypeSytemId' => $htmlapi_response['result']['doctypeSystemId'] ?? null,
+ 'treeWarnings' => $htmlapi_response['result']['warnings'] ?? array(),
'normalizedHtml' => $htmlapi_response['normalizedHtml'] ?? '',
'playbackLength' => isset( $htmlapi_response['result']['playback'] )
@@ -63,6 +65,17 @@ function generate_page( string $html, array $options ): string {
data-wp-init="run"
class="html-api-debugger-container html-api-debugger--grid"
>
+
+
+
+
+
+
Processed HTML
diff --git a/html-api-debugger/print-html-tree.js b/html-api-debugger/print-html-tree.js
index 378150c..2ba33ec 100644
--- a/html-api-debugger/print-html-tree.js
+++ b/html-api-debugger/print-html-tree.js
@@ -5,6 +5,7 @@ import { replaceInvisible } from '@html-api-debugger/replace-invisible-chars';
* @property {boolean} [showClosers]
* @property {boolean} [showInvisible]
* @property {boolean} [showVirtual]
+ * @property {string|null} [selector]
* @property {'breadcrumbs'|'insertionMode'} [hoverInfo]
*/
@@ -21,6 +22,14 @@ export function printHtmlApiTree(node, ul, options = {}) {
for (let i = 0; i < node.childNodes.length; i += 1) {
const li = document.createElement('li');
li.className = `t${node.childNodes[i].nodeType}`;
+
+ if (
+ node.childNodes[i]._matches ||
+ (options.selector && node.childNodes[i].matches?.(options.selector))
+ ) {
+ li.classList.add('matches-selector');
+ }
+
if (node.childNodes[i].nodeType === Node.prototype.DOCUMENT_TYPE_NODE) {
li.appendChild(document.createTextNode('DOCTYPE: '));
}
diff --git a/html-api-debugger/style.css b/html-api-debugger/style.css
index 9094aed..433723e 100644
--- a/html-api-debugger/style.css
+++ b/html-api-debugger/style.css
@@ -7,6 +7,9 @@
}
.html-api-debugger-container {
+ --monospace-font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo,
+ Consolas, Liberation Mono, monospace;
+
width: 100%;
padding: 20px 20px 0 0;
@@ -32,6 +35,15 @@
grid-column: 1 / -1;
}
+ .matches-selector {
+ outline: 1px dotted hotpink;
+ }
+
+ code,
+ pre {
+ font-family: var(--monospace-font-family);
+ }
+
pre {
background-color: #fff;
border: inset 1px;
@@ -49,12 +61,12 @@
#input_html {
width: 100%;
min-height: 200px;
- font-family: monospace;
+ font-family: var(--monospace-font-family);
}
.context-html {
width: 100%;
- font-family: monospace;
+ font-family: var(--monospace-font-family);
&:placeholder-shown {
font-style: italic;
@@ -142,7 +154,7 @@
border: inset 1px;
padding: 0.5em 0.5em 0.5em 1em;
color: black;
- font-family: monospace;
+ font-family: var(--monospace-font-family);
background: white;
margin: 0;
@@ -165,7 +177,7 @@
.t2 {
font-style: normal;
- font-family: monospace;
+ font-family: var(--monospace-font-family);
}
.t2 .name {
diff --git a/html-api-debugger/view.js b/html-api-debugger/view.js
index c3345c0..88c1ff9 100644
--- a/html-api-debugger/view.js
+++ b/html-api-debugger/view.js
@@ -49,6 +49,7 @@ let mutationObserver = null;
*
* @typedef Supports
* @property {boolean} create_fragment_advanced
+ * @property {boolean} selectors
*
*
* @typedef HtmlApiResponse
@@ -67,6 +68,9 @@ let mutationObserver = null;
*
*
* @typedef State
+ * @property {ReadonlyArray} treeWarnings
+ * @property {string|null} selector
+ * @property {string|null} selectorErrorMessage
* @property {boolean} showClosers
* @property {boolean} showInvisible
* @property {boolean} showVirtual
@@ -139,9 +143,16 @@ const store = createStore(NS, {
showInvisible: store.state.showInvisible,
showVirtual: store.state.showVirtual,
hoverInfo: store.state.hoverInfo,
+ selector: store.state.htmlapiResponse.supports.selectors
+ ? store.state.selector
+ : '',
};
},
+ get treeWarnings() {
+ return store.state.htmlapiResponse.result?.warnings ?? [];
+ },
+
get playbackTree() {
if (store.state.playbackPoint === null) {
return undefined;
@@ -253,6 +264,9 @@ const store = createStore(NS, {
if (store.state.contextHTMLForUse) {
searchParams.set('contextHTML', store.state.contextHTMLForUse);
}
+ if (store.state.selector) {
+ searchParams.set('selector', store.state.selector);
+ }
const base = '/wp-admin/admin.php';
const u = new URL(
'https://playground.wordpress.net/?plugin=html-api-debugger',
@@ -396,10 +410,23 @@ const store = createStore(NS, {
/** @type {Element|null} */
let contextElement = null;
if (store.state.contextHTMLForUse) {
- const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT);
- while (walker.nextNode()) {
- // @ts-expect-error It's an Element!
- contextElement = walker.currentNode;
+ // An HTML document will always make HTML > HEAD + BODY.
+ // But that may not be the intended context.
+ // Guess the intended context in case the HEAD and BODY elements are empty.
+ if (doc.body.hasChildNodes() || doc.head.hasChildNodes()) {
+ const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT);
+ while (walker.nextNode()) {
+ // @ts-expect-error It's an Element!
+ contextElement = walker.currentNode;
+ }
+ } else {
+ if (/ Failed to execute 'querySelector' on 'DocumentFragment': 'foo >' is not a valid selector.
+ * Firefox:
+ * > DocumentFragment.querySelector: 'foo >' is not a valid selector
+ * Safari:
+ * > 'foo >' is not a valid selector.
+ *
+ * Try to strip the irrelevant parts.
+ */
+ let idx = msg.indexOf(val);
+ if (idx > 0) {
+ if (msg[idx - 1] === '"' || msg[idx - 1] === "'") {
+ idx -= 1;
+ }
+ msg = msg.slice(idx);
+ }
+
+ store.state.selectorErrorMessage = msg;
+ } else {
+ throw e;
+ }
+ }
+ }
+ store.state.selector = null;
+ yield store.callAPI();
+ },
});
/** @param {keyof State} stateKey */