diff --git a/html-api-debugger/html-api-debugger.php b/html-api-debugger/html-api-debugger.php index b1fa774..bda930d 100644 --- a/html-api-debugger/html-api-debugger.php +++ b/html-api-debugger/html-api-debugger.php @@ -3,9 +3,9 @@ * Plugin Name: HTML API Debugger * Plugin URI: https://github.com/sirreal/html-api-debugger * Description: Add a page to wp-admin for debugging the HTML API. - * Version: 2.1 - * Requires at least: 6.6 - * Tested up to: 6.7 + * Version: 2.2 + * Requires at least: 6.7 + * Tested up to: 6.8 * Author: Jon Surrell * Author URI: https://profiles.wordpress.org/jonsurrell/ * License: GPLv2 or later @@ -22,7 +22,7 @@ require_once __DIR__ . '/html-api-integration.php'; const SLUG = 'html-api-debugger'; -const VERSION = '2.1'; +const VERSION = '2.2'; /** Set up the plugin. */ function init() { @@ -44,8 +44,7 @@ function () { // phpcs:ignore Universal.Operators.DisallowShortTernary.Found $html = $request->get_json_params()['html'] ?: ''; $options = array( - 'quirks_mode' => $request->get_json_params()['quirksMode'] ?? false, - 'full_parser' => $request->get_json_params()['fullParser'] ?? false, + 'context_html' => $request->get_json_params()['contextHTML'] ?: null, ); return prepare_html_result_object( $html, $options ); }, @@ -111,14 +110,19 @@ function () { function () { require_once __DIR__ . '/interactivity.php'; + $options = array( + 'context_html' => null, + ); + $html = ''; // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['html'] ) && is_string( $_GET['html'] ) ) { $html = stripslashes( $_GET['html'] ); } - - $options = array(); - // @todo Add query args for other options + if ( isset( $_GET['contextHTML'] ) && is_string( $_GET['contextHTML'] ) ) { + $options['context_html'] = stripslashes( $_GET['contextHTML'] ); + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo namespace\Interactivity\generate_page( $html, $options ); @@ -141,7 +145,7 @@ function prepare_html_result_object( string $html, array $options = null ): arra 'html' => $html, 'error' => null, 'result' => null, - 'normalizedHtml' => HTML_API_Integration\get_normalized_html( $html ), + 'normalizedHtml' => HTML_API_Integration\get_normalized_html( $html, $options ), ); try { diff --git a/html-api-debugger/html-api-integration.php b/html-api-debugger/html-api-integration.php index 3e428ab..b3aa8f9 100644 --- a/html-api-debugger/html-api-integration.php +++ b/html-api-debugger/html-api-integration.php @@ -2,40 +2,55 @@ namespace HTML_API_Debugger\HTML_API_Integration; use Exception; -use ReflectionClass; use ReflectionMethod; use ReflectionProperty; use WP_HTML_Processor; -use WP_HTML_Processor_State; /** * Get information about HTML API supported features */ function get_supports(): array { - $html_processor_rc = new ReflectionClass( WP_HTML_Processor::class ); - $html_processor_state_rc = new ReflectionClass( WP_HTML_Processor_State::class ); - return array( - 'is_virtual' => $html_processor_rc->hasMethod( 'is_virtual' ), - 'full_parser' => method_exists( WP_HTML_Processor::class, 'create_full_parser' ), - 'quirks_mode' => $html_processor_rc->hasProperty( 'compat_mode' ), - 'doctype' => method_exists( WP_HTML_Processor::class, 'get_doctype_info' ), - 'normalize' => method_exists( WP_HTML_Processor::class, 'normalize' ), + 'create_fragment_advanced' => method_exists( WP_HTML_Processor::class, 'create_fragment_at_current_node' ), ); } - /** * Get the normalized HTML. * * @param string $html The HTML. + * @param array $options The options. * @return string|null The normalized HTML or null if not supported. */ -function get_normalized_html( string $html ): ?string { - if ( ! method_exists( WP_HTML_Processor::class, 'normalize' ) ) { +function get_normalized_html( string $html, array $options ): ?string { + if ( + method_exists( WP_HTML_Processor::class, 'create_fragment_at_current_node' ) && + $options['context_html'] + ) { + $context_processor = WP_HTML_Processor::create_full_parser( $options['context_html'] ); + + while ( $context_processor->next_tag() ) { + $context_processor->set_bookmark( 'final_node' ); + } + if ( $context_processor->has_bookmark( 'final_node' ) ) { + $context_processor->seek( 'final_node' ); + /** + * The main processor used for tree building. + * + * @var WP_HTML_Processor|null $processor + * @disregard P1013 + */ + $processor = $context_processor->create_fragment_at_current_node( $html ); + } + } else { + $processor = WP_HTML_Processor::create_full_parser( $html ); + } + + if ( ! isset( $processor ) ) { return null; } - return WP_HTML_Processor::normalize( $html ); + + return $processor->serialize(); } /** @@ -53,66 +68,47 @@ function get_tree( string $html, array $options ): array { $processor_bookmarks = new ReflectionProperty( WP_HTML_Processor::class, 'bookmarks' ); $processor_bookmarks->setAccessible( true ); - $use_full_parser = method_exists( WP_HTML_Processor::class, 'create_full_parser' ) && ( $options['full_parser'] ?? false ); + $is_fragment_processor = false; - $processor = $use_full_parser - ? WP_HTML_Processor::create_full_parser( $html ) - : WP_HTML_Processor::create_fragment( $html ); - - $doctype_value = $use_full_parser ? '' : 'html'; if ( - ! $use_full_parser && - ( $options['quirks_mode'] ?? false ) && - property_exists( WP_HTML_Processor::class, 'compat_mode' ) && - defined( WP_HTML_Processor::class . '::QUIRKS_MODE' ) + method_exists( WP_HTML_Processor::class, 'create_fragment_at_current_node' ) && + $options['context_html'] ) { - $processor_compat_mode = new ReflectionProperty( WP_HTML_Processor::class, 'compat_mode' ); - $processor_compat_mode->setValue( $processor, WP_HTML_Processor::QUIRKS_MODE ); - $doctype_value = ''; - } - - $rc = new ReflectionClass( WP_HTML_Processor::class ); - - $is_virtual = function () { - return null; - }; + $context_processor = WP_HTML_Processor::create_full_parser( $options['context_html'] ); - if ( $rc->hasMethod( 'is_virtual' ) ) { - $processor_is_virtual = new ReflectionMethod( WP_HTML_Processor::class, 'is_virtual' ); - $processor_is_virtual->setAccessible( true ); - $is_virtual = function () use ( $processor_is_virtual, $processor ) { - return $processor_is_virtual->invoke( $processor ); - }; - } - - $get_current_depth = method_exists( WP_HTML_Processor::class, 'get_current_depth' ) - ? function () use ( $processor ): int { - return $processor->get_current_depth(); + while ( $context_processor->next_tag() ) { + $context_processor->set_bookmark( 'final_node' ); } - : function () use ( $processor ): int { - return count( $processor->get_breadcrumbs() ); - }; - - $get_tag_name = method_exists( WP_HTML_Processor::class, 'get_qualified_tag_name' ) - ? function () use ( $processor ): string { - return $processor->get_qualified_tag_name(); + if ( $context_processor->has_bookmark( 'final_node' ) ) { + $context_processor->seek( 'final_node' ); + /** + * The main processor used for tree building. + * + * @var WP_HTML_Processor|null $processor + * @disregard P1013 + */ + $processor = $context_processor->create_fragment_at_current_node( $html ); } - : function () use ( $processor ): string { - return $processor->get_tag(); - }; - $get_attribute_name = method_exists( WP_HTML_Processor::class, 'get_qualified_attribute_name' ) - ? function ( string $attribute_name ) use ( $processor ): string { - return $processor->get_qualified_attribute_name( $attribute_name ); + if ( ! isset( $processor ) ) { + throw new Exception( 'Could not create processor from context HTML.' ); } - : function ( string $attribute_name ): string { - return $attribute_name; - }; + + $is_fragment_processor = true; + } else { + $processor = WP_HTML_Processor::create_full_parser( $html ); + } if ( null === $processor ) { - throw new Exception( 'could not process html' ); + throw new Exception( 'Could not create processor.' ); } + $processor_is_virtual = new ReflectionMethod( WP_HTML_Processor::class, 'is_virtual' ); + $processor_is_virtual->setAccessible( true ); + $is_virtual = function () use ( $processor_is_virtual, $processor ) { + return $processor_is_virtual->invoke( $processor ); + }; + $tree = array( 'nodeType' => NODE_TYPE_DOCUMENT, 'nodeName' => '#document', @@ -120,38 +116,21 @@ function get_tree( string $html, array $options ): array { ); $cursor = array( 0 ); - if ( ! $use_full_parser ) { - $tree['childNodes'][] = array( - 'nodeType' => NODE_TYPE_DOCUMENT_TYPE, - 'nodeName' => $doctype_value, - 'nodeValue' => '', - ); - $tree['childNodes'][] = array( - 'nodeType' => NODE_TYPE_ELEMENT, - 'nodeName' => 'HTML', - 'attributes' => array(), - 'childNodes' => array( - array( - 'nodeType' => NODE_TYPE_ELEMENT, - 'nodeName' => 'HEAD', - 'attributes' => array(), - 'childNodes' => array(), - ), - array( - 'nodeType' => NODE_TYPE_ELEMENT, - 'nodeName' => 'BODY', - 'attributes' => array(), - 'childNodes' => array(), - ), - ), + + if ( $is_fragment_processor ) { + $tree = array( + 'childNodes' => array(), ); - $cursor = array( 1, 1 ); + $cursor = array(); } $compat_mode = 'CSS1Compat'; $doctype_name = null; $doctype_public_identifier = null; $doctype_system_identifier = null; + $context_node = isset( $context_processor ) + ? $context_processor->get_qualified_tag_name() + : null; $playback = array(); @@ -170,7 +149,10 @@ function get_tree( string $html, array $options ): array { break; } - if ( ( count( $cursor ) + 1 ) > $get_current_depth() ) { + // Depth needs and adjustment because: + // - Nodes in a full tree are all placed under a document node. + // - Nodes in a fragment tree start at the root. + if ( ( count( $cursor ) + 1 ) > ( $processor->get_current_depth() - ( $is_fragment_processor ? 1 : 0 ) ) ) { array_pop( $cursor ); } $current = &$tree; @@ -181,31 +163,30 @@ function get_tree( string $html, array $options ): array { $token_type = $processor->get_token_type(); switch ( $token_type ) { + // @todo this should be set on the context processor if present. case '#doctype': - if ( method_exists( WP_HTML_Processor::class, 'get_doctype_info' ) ) { - $doctype = $processor->get_doctype_info(); + $doctype = $processor->get_doctype_info(); - $doctype_name = $doctype->name; - $doctype_public_identifier = $doctype->public_identifier; - $doctype_system_identifier = $doctype->system_identifier; + $doctype_name = $doctype->name; + $doctype_public_identifier = $doctype->public_identifier; + $doctype_system_identifier = $doctype->system_identifier; - if ( $doctype->indicated_compatability_mode === 'quirks' ) { - $compat_mode = 'BackCompat'; - } - - $current['childNodes'][] = array( - 'nodeType' => NODE_TYPE_DOCUMENT_TYPE, - 'nodeName' => $doctype_name, - '_span' => $bookmark, - '_mode' => $processor_state->getValue( $processor )->insertion_mode, - '_bc' => $processor->get_breadcrumbs(), - '_depth' => $get_current_depth(), - ); + if ( $doctype->indicated_compatability_mode === 'quirks' ) { + $compat_mode = 'BackCompat'; } + + $current['childNodes'][] = array( + 'nodeType' => NODE_TYPE_DOCUMENT_TYPE, + 'nodeName' => $doctype_name, + '_span' => $bookmark, + '_mode' => $processor_state->getValue( $processor )->insertion_mode, + '_bc' => $processor->get_breadcrumbs(), + '_depth' => $processor->get_current_depth(), + ); break; case '#tag': - $tag_name = $get_tag_name(); + $tag_name = $processor->get_qualified_tag_name(); $attributes = array(); $attribute_names = $processor->get_attribute_names_with_prefix( '' ); @@ -223,13 +204,13 @@ function get_tree( string $html, array $options ): array { $attributes[] = array( 'nodeType' => NODE_TYPE_ATTRIBUTE, 'specified' => true, - 'nodeName' => $get_attribute_name( $attribute_name ), + 'nodeName' => $processor->get_qualified_attribute_name( $attribute_name ), 'nodeValue' => $val, ); } } - $namespace = method_exists( WP_HTML_Processor::class, 'get_namespace' ) ? $processor->get_namespace() : 'html'; + $namespace = $processor->get_namespace(); $self = array( 'nodeType' => NODE_TYPE_ELEMENT, @@ -241,7 +222,7 @@ function get_tree( string $html, array $options ): array { '_mode' => $processor_state->getValue( $processor )->insertion_mode, '_bc' => $processor->get_breadcrumbs(), '_virtual' => $is_virtual(), - '_depth' => $get_current_depth(), + '_depth' => $processor->get_current_depth(), '_namespace' => $namespace, ); @@ -256,7 +237,7 @@ function get_tree( string $html, array $options ): array { '_mode' => $processor_state->getValue( $processor )->insertion_mode, '_bc' => array_merge( $processor->get_breadcrumbs(), array( '#text' ) ), '_virtual' => $is_virtual(), - '_depth' => $get_current_depth() + 1, + '_depth' => $processor->get_current_depth() + 1, ); } @@ -264,15 +245,13 @@ function get_tree( string $html, array $options ): array { if ( $processor->is_tag_closer() || + ( $namespace === 'html' && WP_HTML_Processor::is_void( $tag_name ) ) || ( $namespace !== 'html' && $processor->has_self_closing_flag() ) ) { break; } - if ( ! WP_HTML_Processor::is_void( $tag_name ) ) { - $cursor[] = count( $current['childNodes'] ) - 1; - } - + $cursor[] = count( $current['childNodes'] ) - 1; break; case '#text': @@ -284,7 +263,7 @@ function get_tree( string $html, array $options ): array { '_mode' => $processor_state->getValue( $processor )->insertion_mode, '_bc' => $processor->get_breadcrumbs(), '_virtual' => $is_virtual(), - '_depth' => $get_current_depth(), + '_depth' => $processor->get_current_depth(), ); $current['childNodes'][] = $self; @@ -299,7 +278,7 @@ function get_tree( string $html, array $options ): array { '_mode' => $processor_state->getValue( $processor )->insertion_mode, '_bc' => $processor->get_breadcrumbs(), '_virtual' => $is_virtual(), - '_depth' => $get_current_depth(), + '_depth' => $processor->get_current_depth(), ); $current['childNodes'][] = $self; @@ -314,7 +293,7 @@ function get_tree( string $html, array $options ): array { '_mode' => $processor_state->getValue( $processor )->insertion_mode, '_bc' => $processor->get_breadcrumbs(), '_virtual' => $is_virtual(), - '_depth' => $get_current_depth(), + '_depth' => $processor->get_current_depth(), ); $current['childNodes'][] = $self; break; @@ -328,7 +307,7 @@ function get_tree( string $html, array $options ): array { '_mode' => $processor_state->getValue( $processor )->insertion_mode, '_bc' => $processor->get_breadcrumbs(), '_virtual' => $is_virtual(), - '_depth' => $get_current_depth(), + '_depth' => $processor->get_current_depth(), ); $current['childNodes'][] = $self; break; @@ -340,7 +319,7 @@ function get_tree( string $html, array $options ): array { '_mode' => $processor_state->getValue( $processor )->insertion_mode, '_bc' => $processor->get_breadcrumbs(), '_virtual' => $is_virtual(), - '_depth' => $get_current_depth(), + '_depth' => $processor->get_current_depth(), ); switch ( $processor->get_comment_type() ) { case WP_HTML_Processor::COMMENT_AS_ABRUPTLY_CLOSED_COMMENT: @@ -388,16 +367,19 @@ function get_tree( string $html, array $options ): array { $playback[] = array( $last_html, $tree ); if ( null !== $processor->get_last_error() ) { - if ( method_exists( WP_HTML_Processor::class, 'get_unsupported_exception' ) && $processor->get_unsupported_exception() ) { + if ( $processor->get_unsupported_exception() ) { throw $processor->get_unsupported_exception(); - } else { + } elseif ( $processor->get_last_error() ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new Exception( $processor->get_last_error() ); + } else { + throw new Exception( 'Unknown error.' ); } } + // This could perhaps be ignored or surfaced in the response. if ( $processor->paused_at_incomplete_token() ) { - throw new Exception( 'Paused at incomplete token' ); + throw new Exception( 'Paused at incomplete token.' ); } return array( @@ -407,6 +389,7 @@ function get_tree( string $html, array $options ): array { 'doctypeName' => $doctype_name, 'doctypePublicId' => $doctype_public_identifier, 'doctypeSystemId' => $doctype_system_identifier, + 'contextNode' => $context_node, ); } diff --git a/html-api-debugger/interactivity.php b/html-api-debugger/interactivity.php index a406b74..01cb12e 100644 --- a/html-api-debugger/interactivity.php +++ b/html-api-debugger/interactivity.php @@ -1,27 +1,6 @@ =' ); - } - - echo $supports_async_on ? - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - "data-wp-on-async--{$on}=\"{$directive}\"" : - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - "data-wp-on--{$on}=\"{$directive}\""; -} - /** * Generate the WP Admin page HTML. * @@ -56,8 +35,7 @@ function generate_page( string $html, array $options ): string { 'showClosers' => false, 'showInvisible' => false, 'showVirtual' => false, - 'quirksMode' => false, - 'fullParser' => false, + 'contextHTML' => $options['context_html'] ?? '', 'hoverInfo' => 'breadcrumbs', 'hoverBreadcrumbs' => true, @@ -68,15 +46,19 @@ function generate_page( string $html, array $options ): string { 'htmlApiDoctypePublicId' => $htmlapi_response['result']['doctypePublicId'] ?? '[unknown]', 'htmlApiDoctypeSytemId' => $htmlapi_response['result']['doctypeSystemId'] ?? '[unknown]', 'normalizedHtml' => $htmlapi_response['normalizedHtml'] ?? '', + + 'playbackLength' => isset( $htmlapi_response['result']['playback'] ) + ? count( $htmlapi_response['result']['playback'] ) + : 0, ) ); ob_start(); ?>
@@ -87,24 +69,18 @@ class="html-api-debugger-container html-api-debugger--grid" autocomplete="off" spellcheck="false" wrap="off" - + data-wp-on-async--input="handleInput" > -

- Note: Because HTML API operates in body at this time, this will be prepended: -
- -

Rendered output

-
+
HTML API Normalized HTML

 	
@@ -116,13 +92,15 @@ class="html-api-debugger-container html-api-debugger--grid" Rendering mode: 
Doctype name: 
Doctype publicId: 
- Doctype systemId:  + Doctype systemId: 
+ Context node: 
Rendering mode: 
Doctype name: 
Doctype publicId: 
- Doctype systemId:  + Doctype systemId: 
+ Context node: 
@@ -130,10 +108,8 @@ class="html-api-debugger-container html-api-debugger--grid"

Interpreted by HTML API

+ data-wp-on-async--mouseover="handleSpanOver" + data-wp-on-async--mouseleave="clearSpan" >

 				
@@ -150,16 +126,24 @@ class="html-api-debugger-container html-api-debugger--grid"
- - - - - + + + +
+ +
@@ -192,22 +176,20 @@ class="html-api-debugger-container html-api-debugger--grid" - - -
+

- + void} clearSpan - * @property {()=>void} render * @property {()=>Promise} callAPI - * + * @property {()=>void} clearSpan + * @property {()=>void} handleContextHtmlInput + * @property {()=>void} handleCopyClick + * @property {()=>void} handleCopyPrClick + * @property {()=>void} handleCopyPrInput * @property {()=>void} handleInput - * - * @property {()=>void} handleShowInvisibleClick * @property {()=>void} handleShowClosersClick + * @property {()=>void} handleShowInvisibleClick * @property {()=>void} handleShowVirtualClick - * @property {()=>Promise} handleQuirksModeClick - * @property {()=>Promise} handleFullParserClick - * - * @property {()=>void} handleCopyClick - * @property {()=>void} handleCopyPrInput - * @property {()=>void} handleCopyPrClick - * * @property {()=>void} onRenderedIframeLoad + * @property {()=>void} redrawDOMTreeFromIframe + * @property {()=>void} render + * @property {()=>void} watch + * @property {()=>void} watchURL + * @property {State} state */ const createStore = /** @type {typeof I.store} */ (I.store); @@ -119,8 +115,6 @@ const store = createStore(NS, { showClosers: Boolean(localStorage.getItem(`${NS}-showClosers`)), showInvisible: Boolean(localStorage.getItem(`${NS}-showInvisible`)), showVirtual: Boolean(localStorage.getItem(`${NS}-showVirtual`)), - quirksMode: Boolean(localStorage.getItem(`${NS}-quirksMode`)), - fullParser: Boolean(localStorage.getItem(`${NS}-fullParser`)), playbackPoint: null, previewCorePrNumber: null, @@ -134,6 +128,7 @@ const store = createStore(NS, { store.state.playbackPoint ]?.[1]; }, + get playbackHTML() { if (store.state.playbackPoint === null) { return undefined; @@ -143,6 +138,16 @@ const store = createStore(NS, { ]?.[0]; }, + get playbackLength() { + return store.state.htmlapiResponse.result?.playback?.length; + }, + + get contextHTMLForUse() { + return store.state.htmlapiResponse.supports.create_fragment_advanced + ? store.state.contextHTML.trim() || null + : null; + }, + /** @type {Link|null} */ get previewCoreLink() { if (!store.state.previewCorePrNumber) { @@ -189,10 +194,7 @@ const store = createStore(NS, { }, get normalizedHtml() { - if ( - !store.state.htmlapiResponse.supports.normalize || - !store.state.htmlapiResponse.normalizedHtml - ) { + if (!store.state.htmlapiResponse.normalizedHtml) { return ''; } return store.state.showInvisible @@ -218,6 +220,9 @@ const store = createStore(NS, { if (store.state.html) { searchParams.set('html', store.state.html); } + if (store.state.contextHTMLForUse) { + searchParams.set('contextHTML', store.state.contextHTMLForUse); + } const base = '/wp-admin/admin.php'; const u = new URL( 'https://playground.wordpress.net/?plugin=html-api-debugger', @@ -226,23 +231,6 @@ const store = createStore(NS, { return u; }, - get htmlPreambleForProcessing() { - if (store.state.fullParser) { - return ''; - } - const doctype = ``; - return `${doctype}`; - }, - - get htmlForProcessing() { - return store.state.htmlPreambleForProcessing + store.state.html; - }, - get htmlForDisplay() { /** @type {string | undefined} */ const html = store.state.playbackHTML ?? store.state.htmlapiResponse.html; @@ -314,6 +302,10 @@ const store = createStore(NS, { }, run() { + RENDERED_IFRAME.addEventListener('load', store.onRenderedIframeLoad, { + passive: true, + }); + // The HTML parser will replace null bytes from the HTML. // Force print them if we have null bytes. if (store.state.html.includes('\0')) { @@ -325,43 +317,22 @@ const store = createStore(NS, { store.render(); // browsers "eat" some characters from search params… - // newlines seem especially problematic in chrome - // lets clean up the URL - const u = new URL(document.location.href); - if (store.state.html) { - u.searchParams.set('html', store.state.html); - history.replaceState(null, '', u); - } else if (u.searchParams.has('html')) { - u.searchParams.delete('html'); - history.replaceState(null, '', u); - } + // newlines seem especially problematic in chrome. + // Let's clean up the URL + store.watchURL(); mutationObserver = new MutationObserver(() => { store.state.hasMutatedDom = true; - store.onRenderedIframeLoad(); + store.redrawDOMTreeFromIframe(); }); }, onRenderedIframeLoad() { + store.redrawDOMTreeFromIframe(); + // @ts-expect-error It better be defined! const doc = RENDERED_IFRAME.contentWindow.document; - store.state.DOM.renderingMode = doc.compatMode; - store.state.DOM.doctypeName = doc.doctype?.name; - store.state.DOM.doctypeSystemId = doc.doctype?.systemId; - store.state.DOM.doctypePublicId = doc.doctype?.publicId; - - printHtmlApiTree( - doc, - // @ts-expect-error - document.getElementById('dom_tree'), - { - showClosers: store.state.showClosers, - showInvisible: store.state.showInvisible, - showVirtual: store.state.showVirtual, - hoverInfo: store.state.hoverInfo, - }, - ); mutationObserver?.observe(doc, { subtree: true, childList: true, @@ -382,6 +353,42 @@ const store = createStore(NS, { ); }, + redrawDOMTreeFromIframe() { + // @ts-expect-error It better be defined! + const doc = RENDERED_IFRAME.contentWindow.document; + + store.state.DOM.renderingMode = doc.compatMode; + store.state.DOM.doctypeName = doc.doctype?.name; + store.state.DOM.doctypeSystemId = doc.doctype?.systemId; + store.state.DOM.doctypePublicId = doc.doctype?.publicId; + + /** @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; + } + if (contextElement) { + store.state.DOM.contextNode = contextElement.nodeName; + contextElement.innerHTML = store.state.playbackHTML ?? store.state.html; + } + } + + printHtmlApiTree( + contextElement ?? doc, + // @ts-expect-error + document.getElementById('dom_tree'), + { + showClosers: store.state.showClosers, + showInvisible: store.state.showInvisible, + showVirtual: store.state.showVirtual, + hoverInfo: store.state.hoverInfo, + }, + ); + }, + /** @param {InputEvent} e */ handleInput: function* (e) { const val = /** @type {HTMLTextAreaElement} */ (e.target).value; @@ -429,8 +436,6 @@ const store = createStore(NS, { handleShowInvisibleClick: getToggleHandler('showInvisible'), handleShowClosersClick: getToggleHandler('showClosers'), handleShowVirtualClick: getToggleHandler('showVirtual'), - handleQuirksModeClick: getToggleHandlerWithRefetch('quirksMode'), - handleFullParserClick: getToggleHandlerWithRefetch('fullParser'), /** @param {Event} e */ hoverInfoChange: (e) => { @@ -443,8 +448,26 @@ const store = createStore(NS, { store.render(); }, - // @ts-expect-error This will be transformed by the Interactivity API runtime when called through the store. - /** @returns {Promise} */ + watchURL() { + const u = new URL(document.location.href); + let shouldReplace = false; + for (const [param, prop] of /** @type {const} */ ([ + ['html', 'html'], + ['contextHTML', 'contextHTMLForUse'], + ])) { + if (store.state[prop]) { + u.searchParams.set(param, store.state[prop]); + shouldReplace = true; + } else if (u.searchParams.has(param)) { + u.searchParams.delete(param); + shouldReplace = true; + } + } + if (shouldReplace) { + history.replaceState(null, '', u); + } + }, + callAPI: function* () { inFlightRequestAbortController?.abort('request superseded'); inFlightRequestAbortController = new AbortController(); @@ -455,8 +478,7 @@ const store = createStore(NS, { method: 'POST', body: JSON.stringify({ html: store.state.html, - quirksMode: store.state.quirksMode, - fullParser: store.state.fullParser, + contextHTML: store.state.contextHTMLForUse, }), headers: { 'Content-Type': 'application/json', @@ -471,6 +493,8 @@ const store = createStore(NS, { if (!response.ok) { throw response; } + + // @ts-expect-error It's fine. data = yield response.json(); } catch (/** @type {any} */ err) { if (err === 'request superseded' || err instanceof DOMException) { @@ -521,35 +545,20 @@ const store = createStore(NS, { /** @type {HTMLUListElement} */ ( document.getElementById('html_api_result_holder') ).innerHTML = ''; - return; } }, - watchDom() { - const doc = - // @ts-expect-error - document.getElementById('rendered_iframe').contentWindow.document; - printHtmlApiTree( - doc, - // @ts-expect-error - document.getElementById('dom_tree'), - { - showClosers: store.state.showClosers, - showInvisible: store.state.showInvisible, - showVirtual: store.state.showVirtual, - hoverInfo: store.state.hoverInfo, - }, - ); - }, - render() { // @ts-expect-error This should not be null. const iframeDocument = RENDERED_IFRAME.contentWindow.document; - mutationObserver?.disconnect(); + store.state.hasMutatedDom = false; - const html = store.state.playbackHTML ?? store.state.htmlForProcessing; + const html = + store.state.contextHTMLForUse ?? + store.state.playbackHTML ?? + store.state.html; iframeDocument.open(); iframeDocument.write(html); @@ -579,6 +588,12 @@ const store = createStore(NS, { } }, + /** @param {InputEvent} e */ + handleContextHtmlInput: function* (e) { + store.state.contextHTML = /** @type {HTMLInputElement} */ (e.target).value; + yield store.callAPI(); + }, + /** @param {InputEvent} e */ handleCopyCorePrInput(e) { const val = /** @type {HTMLInputElement} */ (e.target).valueAsNumber; @@ -644,20 +659,3 @@ function getToggleHandler(stateKey) { } }; } - -/** - * @param {keyof State} stateKey - * @return {(e: Event) => Promise} - */ -function getToggleHandlerWithRefetch(stateKey) { - const f1 = getToggleHandler(stateKey); - - /** - * @param {Event} e - */ - // @ts-expect-error The iAPI runtime transforms the generator to an async function. - return function* (e) { - f1(e); - yield store.callAPI(); - }; -}