<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[58191] trunk: HTML API: Add method to report depth of currently-matched node.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/58191">58191</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/58191","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>dmsnell</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-05-23 23:35:52 +0000 (Thu, 23 May 2024)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>HTML API: Add method to report depth of currently-matched node.

The HTML Processor maintains a stack of open elements, where every element,
every `#text` node, every HTML comment, and other node is pushed and popped while
traversing the document. The "depth" of each of these nodes represents how deep
that stack is where the node appears. Unfortunately this information isn't
exposed to calling code, which has led different projects to attempt to
calculate this value externally. This isn't always trivial, but the HTML
Processor could make it so by exposing the internal knowledge in a new method.

In this patch the `get_current_depth()` method returns just that. Since the
processor always exists within a context, the depth includes nesting from the
always-present html element and also the body, since currently the HTML
Processor only supports parsing in the IN BODY context.

This means that the depth reported for the `DIV` in `<div>` is 3, not 1, because
its breadcrumbs path is `HTML > BODY > DIV`.

Developed in https://github.com/WordPress/wordpress-develop/pull/6589
Discussed in https://core.trac.wordpress.org/ticket/61255

Fixes <a href="https://core.trac.wordpress.org/ticket/61255">#61255</a>.
Props dmsnell, jonsurrell.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludeshtmlapiclasswphtmlprocessorphp">trunk/src/wp-includes/html-api/class-wp-html-processor.php</a></li>
<li><a href="#trunktestsphpunittestshtmlapiwpHtmlProcessorphp">trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludeshtmlapiclasswphtmlprocessorphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/html-api/class-wp-html-processor.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/html-api/class-wp-html-processor.php        2024-05-23 22:43:27 UTC (rev 58190)
+++ trunk/src/wp-includes/html-api/class-wp-html-processor.php  2024-05-23 23:35:52 UTC (rev 58191)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -624,6 +624,35 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * Returns the nesting depth of the current location in the document.
+        *
+        * Example:
+        *
+        *     $processor = WP_HTML_Processor::create_fragment( '<div><p></p></div>' );
+        *     // The processor starts in the BODY context, meaning it has depth from the start: HTML > BODY.
+        *     2 === $processor->get_current_depth();
+        *
+        *     // Opening the DIV element increases the depth.
+        *     $processor->next_token();
+        *     3 === $processor->get_current_depth();
+        *
+        *     // Opening the P element increases the depth.
+        *     $processor->next_token();
+        *     4 === $processor->get_current_depth();
+        *
+        *     // The P element is closed during `next_token()` so the depth is decreased to reflect that.
+        *     $processor->next_token();
+        *     3 === $processor->get_current_depth();
+        *
+        * @since 6.6.0
+        *
+        * @return int Nesting-depth of current location in the document.
+        */
+       public function get_current_depth() {
+               return $this->state->stack_of_open_elements->count();
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Parses next element in the 'in body' insertion mode.
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * This internal function performs the 'in body' insertion mode
</span></span></pre></div>
<a id="trunktestsphpunittestshtmlapiwpHtmlProcessorphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php    2024-05-23 22:43:27 UTC (rev 58190)
+++ trunk/tests/phpunit/tests/html-api/wpHtmlProcessor.php      2024-05-23 23:35:52 UTC (rev 58191)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -334,4 +334,90 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'XMP'       => array( 'XMP' ),
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * Ensures that the HTML Processor properly reports the depth of a given element.
+        *
+        * @ticket 61255
+        *
+        * @dataProvider data_html_with_target_element_and_depth_in_body
+        *
+        * @param string $html_with_target_element HTML containing element with `target` class.
+        * @param int    $depth_at_element         Depth into document at target node.
+        */
+       public function test_reports_proper_element_depth_in_body( $html_with_target_element, $depth_at_element ) {
+               $processor = WP_HTML_Processor::create_fragment( $html_with_target_element );
+
+               $this->assertTrue(
+                       $processor->next_tag( array( 'class_name' => 'target' ) ),
+                       'Failed to find target element: check test data provider.'
+               );
+
+               $this->assertSame(
+                       $depth_at_element,
+                       $processor->get_current_depth(),
+                       'HTML Processor reported the wrong depth at the matched element.'
+               );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @return array[].
+        */
+       public static function data_html_with_target_element_and_depth_in_body() {
+               return array(
+                       'Single element'                    => array( '<div class="target">', 3 ),
+                       'Basic layout and formatting stack' => array( '<div><span><p><b><em class="target">', 7 ),
+                       'Adjacent elements'                 => array( '<div><span></span><span class="target"></div>', 4 ),
+               );
+       }
+
+       /**
+        * Ensures that the HTML Processor properly reports the depth of a given non-element.
+        *
+        * @ticket 61255
+        *
+        * @dataProvider data_html_with_target_element_and_depth_of_next_node_in_body
+        *
+        * @param string $html_with_target_element HTML containing element with `target` class.
+        * @param int    $depth_after_element      Depth into document immediately after target node.
+        */
+       public function test_reports_proper_non_element_depth_in_body( $html_with_target_element, $depth_after_element ) {
+               $processor = WP_HTML_Processor::create_fragment( $html_with_target_element );
+
+               $this->assertTrue(
+                       $processor->next_tag( array( 'class_name' => 'target' ) ),
+                       'Failed to find target element: check test data provider.'
+               );
+
+               $this->assertTrue(
+                       $processor->next_token(),
+                       'Failed to find next node after target element: check tests data provider.'
+               );
+
+               $this->assertSame(
+                       $depth_after_element,
+                       $processor->get_current_depth(),
+                       'HTML Processor reported the wrong depth after the matched element.'
+               );
+       }
+
+       /**
+        * Data provider.
+        *
+        * @return array[].
+        */
+       public static function data_html_with_target_element_and_depth_of_next_node_in_body() {
+               return array(
+                       'Element then text'                 => array( '<div class="target">One Deeper', 4 ),
+                       'Basic layout and formatting stack' => array( '<div><span><p><b><em class="target">Formatted', 8 ),
+                       'Basic layout with text'            => array( '<div>a<span>b<p>c<b>e<em class="target">e', 8 ),
+                       'Adjacent elements'                 => array( '<div><span></span><span class="target">Here</div>', 5 ),
+                       'Adjacent text'                     => array( '<p>Before<img class="target">After</p>', 4 ),
+                       'HTML comment'                      => array( '<img class="target"><!-- this is inside the BODY -->', 3 ),
+                       'HTML comment in DIV'               => array( '<div class="target"><!-- this is inside the BODY -->', 4 ),
+                       'Funky comment'                     => array( '<div><p>What <br class="target"><//wp:post-author></p></div>', 5 ),
+               );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre>
</div>
</div>

</body>
</html>