This repository was archived by the owner on Jul 28, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9
SSR: Add basic directive processor #169
Merged
ockham
merged 20 commits into
main-wp-directives-plugin
from
add/directive-processor-and-unit-tests
Mar 15, 2023
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
beda2c3
WIP: Introduce directive processor
dmsnell 4e4b983
Move to directives/ dir
ockham f85a7d3
Add basic test coverage
ockham 9c25acb
Add basic test for next_balanced_closer
ockham 3c95937
More test coverage for next_balanced_tag
ockham 52d15d7
TEMP: Use GB v15.3.0 RC2
ockham 9d7b1e1
Revert "TEMP: Use GB v15.3.0 RC2"
ockham 3964533
In {g|s}et_inner_html, return to original position
ockham a32cfa5
Run get_updated_html in set_inner_html
ockham 054c579
Skip bookmark invalidation unit test
ockham fc85032
Move is_html_void_element into WP_Directive_Processor
ockham 1a23ad9
Handle void elements in next_balanced_closer
ockham 351635e
Add PHPDoc
ockham 90ad441
Allow setting prefix
ockham 05fcd77
Needle <-> haystack
ockham 749f110
Scrape directive specific stuff for now
ockham 9b31d7b
set_inner_html: Make sure bookmarks don't collide
ockham 0a16325
Introduce get_balanced_bookmarks helper
ockham c738c55
Use get_balanced_tag_bookmarks helper in get_inner_html()
ockham eecd268
Format
ockham File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| <?php | ||
| /** | ||
| * `WP_Directive_Processor` class test. | ||
| */ | ||
| require_once __DIR__ . '/../../src/directives/class-wp-directive-processor.php'; | ||
|
|
||
| /** | ||
| * @group html-processor | ||
| * | ||
| * @coversDefaultClass WP_Directive_Processor | ||
| */ | ||
| class WP_Directive_Processor_Test extends WP_UnitTestCase { | ||
| const HTML = '<div>outside</div><section><div><img>inside</div></section>'; | ||
|
|
||
| public function test_next_balanced_closer_stays_on_void_tag() { | ||
| $tags = new WP_Directive_Processor( self::HTML ); | ||
|
|
||
| $tags->next_tag( 'img' ); | ||
| $result = $tags->next_balanced_closer(); | ||
| $this->assertSame( 'IMG', $tags->get_tag() ); | ||
| $this->assertFalse( $result ); | ||
| } | ||
|
|
||
| public function test_next_balanced_closer_proceeds_to_correct_tag() { | ||
| $tags = new WP_Directive_Processor( self::HTML ); | ||
|
|
||
| $tags->next_tag( 'section' ); | ||
| $tags->next_balanced_closer(); | ||
| $this->assertSame( 'SECTION', $tags->get_tag() ); | ||
| $this->assertTrue( $tags->is_tag_closer() ); | ||
| } | ||
|
|
||
| public function test_next_balanced_closer_proceeds_to_correct_tag_for_nested_tag() { | ||
| $tags = new WP_Directive_Processor( self::HTML ); | ||
|
|
||
| $tags->next_tag( 'div' ); | ||
| $tags->next_tag( 'div' ); | ||
| $tags->next_balanced_closer(); | ||
| $this->assertSame( 'DIV', $tags->get_tag() ); | ||
| $this->assertTrue( $tags->is_tag_closer() ); | ||
| } | ||
|
|
||
| public function test_get_inner_html_returns_correct_result() { | ||
| $tags = new WP_Directive_Processor( self::HTML ); | ||
|
|
||
| $tags->next_tag( 'section' ); | ||
| $this->assertSame( '<div><img>inside</div>', $tags->get_inner_html() ); | ||
| } | ||
|
|
||
| public function test_set_inner_html_on_void_element_has_no_effect() { | ||
| $tags = new WP_Directive_Processor( self::HTML ); | ||
|
|
||
| $tags->next_tag( 'img' ); | ||
| $content = $tags->set_inner_html( 'This is the new img content' ); | ||
| $this->assertFalse( $content ); | ||
| $this->assertSame( self::HTML, $tags->get_updated_html() ); | ||
| } | ||
|
|
||
| public function test_set_inner_html_sets_content_correctly() { | ||
| $tags = new WP_Directive_Processor( self::HTML ); | ||
|
|
||
| $tags->next_tag( 'section' ); | ||
| $tags->set_inner_html( 'This is the new section content.' ); | ||
| $this->assertSame( '<div>outside</div><section>This is the new section content.</section>', $tags->get_updated_html() ); | ||
| } | ||
|
|
||
| public function test_set_inner_html_updates_bookmarks_correctly() { | ||
| $tags = new WP_Directive_Processor( self::HTML ); | ||
|
|
||
| $tags->next_tag( 'div' ); | ||
| $tags->set_bookmark( 'start' ); | ||
| $tags->next_tag( 'img' ); | ||
| $this->assertSame( 'IMG', $tags->get_tag() ); | ||
| $tags->set_bookmark( 'after' ); | ||
| $tags->seek( 'start' ); | ||
|
|
||
| $tags->set_inner_html( 'This is the new div content.' ); | ||
| $this->assertSame( '<div>This is the new div content.</div><section><div><img>inside</div></section>', $tags->get_updated_html() ); | ||
| $tags->seek( 'after' ); | ||
| $this->assertSame( 'IMG', $tags->get_tag() ); | ||
| } | ||
|
|
||
| public function test_set_inner_html_subsequent_updates_on_the_same_tag_work() { | ||
| $tags = new WP_Directive_Processor( self::HTML ); | ||
|
|
||
| $tags->next_tag( 'section' ); | ||
| $tags->set_inner_html( 'This is the new section content.' ); | ||
| $tags->set_inner_html( 'This is the even newer section content.' ); | ||
| $this->assertSame( '<div>outside</div><section>This is the even newer section content.</section>', $tags->get_updated_html() ); | ||
| } | ||
|
|
||
| public function test_set_inner_html_followed_by_set_attribute_works() { | ||
| $tags = new WP_Directive_Processor( self::HTML ); | ||
|
|
||
| $tags->next_tag( 'section' ); | ||
| $tags->set_inner_html( 'This is the new section content.' ); | ||
| $tags->set_attribute( 'id', 'thesection' ); | ||
| $this->assertSame( '<div>outside</div><section id="thesection">This is the new section content.</section>', $tags->get_updated_html() ); | ||
| } | ||
|
|
||
| public function test_set_inner_html_preceded_by_set_attribute_works() { | ||
| $tags = new WP_Directive_Processor( self::HTML ); | ||
|
|
||
| $tags->next_tag( 'section' ); | ||
| $tags->set_attribute( 'id', 'thesection' ); | ||
| $tags->set_inner_html( 'This is the new section content.' ); | ||
| $this->assertSame( '<div>outside</div><section id="thesection">This is the new section content.</section>', $tags->get_updated_html() ); | ||
| } | ||
|
|
||
| public function test_set_inner_html_invalidates_bookmarks_that_point_to_replaced_content() { | ||
| $this->markTestSkipped( "This requires on bookmark invalidation, which is only in GB's WP 6.3 compat layer." ); | ||
|
|
||
| $tags = new WP_Directive_Processor( self::HTML ); | ||
|
|
||
| $tags->next_tag( 'section' ); | ||
| $tags->set_bookmark( 'start' ); | ||
| $tags->next_tag( 'img' ); | ||
| $tags->set_bookmark( 'replaced' ); | ||
| $tags->seek( 'start' ); | ||
|
|
||
| $tags->set_inner_html( 'This is the new section content.' ); | ||
| $this->assertSame( '<div>outside</div><section>This is the new section content.</section>', $tags->get_updated_html() ); | ||
|
|
||
| $this->expectExceptionMessage( 'Invalid bookmark name' ); | ||
| $successful_seek = $tags->seek( 'replaced' ); | ||
| $this->assertFalse( $successful_seek ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| <?php | ||
|
|
||
| class WP_Directive_Processor extends WP_HTML_Tag_Processor { | ||
| /** | ||
| * Find the matching closing tag for an opening tag. | ||
| * | ||
| * When called while on an open tag, traverse the HTML until we find | ||
| * the matching closing tag, respecting any in-between content, including | ||
| * nested tags of the same name. Return false when called on a closing or | ||
| * void tag, or if no matching closing tag was found. | ||
| * | ||
| * @return bool True if a matching closing tag was found. | ||
| */ | ||
| public function next_balanced_closer() { | ||
| $depth = 0; | ||
|
|
||
| $tag_name = $this->get_tag(); | ||
|
|
||
| if ( self::is_html_void_element( $tag_name ) ) { | ||
| return false; | ||
| } | ||
|
|
||
| while ( $this->next_tag( | ||
| array( | ||
| 'tag_name' => $tag_name, | ||
| 'tag_closers' => 'visit', | ||
| ) | ||
| ) ) { | ||
| if ( ! $this->is_tag_closer() ) { | ||
| $depth++; | ||
| continue; | ||
| } | ||
|
|
||
| if ( 0 === $depth ) { | ||
| return true; | ||
| } | ||
|
|
||
| $depth--; | ||
|
ockham marked this conversation as resolved.
|
||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Return the content between two balanced tags. | ||
| * | ||
| * When called on an opening tag, return the HTML content found between | ||
| * that opening tag and its matching closing tag. | ||
| * | ||
| * @return string The content between the current opening and its matching closing tag. | ||
| */ | ||
| public function get_inner_html() { | ||
| $bookmarks = $this->get_balanced_tag_bookmarks(); | ||
| if ( ! $bookmarks ) { | ||
| return false; | ||
| } | ||
| list( $start_name, $end_name ) = $bookmarks; | ||
|
|
||
| $start = $this->bookmarks[ $start_name ]->end + 1; | ||
| $end = $this->bookmarks[ $end_name ]->start; | ||
|
|
||
| $this->seek( $start_name ); // Return to original position. | ||
| $this->release_bookmark( $start_name ); | ||
| $this->release_bookmark( $end_name ); | ||
|
|
||
| return substr( $this->html, $start, $end - $start ); | ||
| } | ||
|
|
||
| /** | ||
| * Set the content between two balanced tags. | ||
| * | ||
| * When called on an opening tag, set the HTML content found between | ||
| * that opening tag and its matching closing tag. | ||
| * | ||
| * @param string $new_html The string to replace the content between the matching tags with. | ||
| * @return bool Whether the content was successfully replaced. | ||
| */ | ||
| public function set_inner_html( $new_html ) { | ||
| $this->get_updated_html(); // Apply potential previous updates. | ||
|
|
||
| $bookmarks = $this->get_balanced_tag_bookmarks(); | ||
| if ( ! $bookmarks ) { | ||
| return false; | ||
| } | ||
| list( $start_name, $end_name ) = $bookmarks; | ||
|
|
||
| $start = $this->bookmarks[ $start_name ]->end + 1; | ||
| $end = $this->bookmarks[ $end_name ]->start; | ||
|
|
||
| $this->seek( $start_name ); // Return to original position. | ||
| $this->release_bookmark( $start_name ); | ||
| $this->release_bookmark( $end_name ); | ||
|
|
||
| $this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $new_html ); | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Return a pair of bookmarks for the current opening tag and the matching closing tag. | ||
| * | ||
| * @return array|false A pair of bookmarks, or false if there's no matching closing tag. | ||
| */ | ||
| public function get_balanced_tag_bookmarks() { | ||
| $i = 0; | ||
| while ( array_key_exists( 'start' . $i, $this->bookmarks ) ) { | ||
| ++$i; | ||
| } | ||
| $start_name = 'start' . $i; | ||
|
|
||
| $this->set_bookmark( $start_name ); | ||
| if ( ! $this->next_balanced_closer() ) { | ||
| $this->release_bookmark( $start_name ); | ||
| return false; | ||
| } | ||
|
|
||
| $i = 0; | ||
| while ( array_key_exists( 'end' . $i, $this->bookmarks ) ) { | ||
| ++$i; | ||
| } | ||
| $end_name = 'end' . $i; | ||
| $this->set_bookmark( $end_name ); | ||
|
|
||
| return array( $start_name, $end_name ); | ||
| } | ||
|
|
||
| /** | ||
| * Whether a given HTML element is void (e.g. <br>). | ||
| * | ||
| * @param string $tag_name The element in question. | ||
| * @return bool True if the element is void. | ||
| * | ||
| * @see https://html.spec.whatwg.org/#elements-2 | ||
| */ | ||
| public static function is_html_void_element( $tag_name ) { | ||
| switch ( $tag_name ) { | ||
| case 'AREA': | ||
| case 'BASE': | ||
| case 'BR': | ||
| case 'COL': | ||
| case 'EMBED': | ||
| case 'HR': | ||
| case 'IMG': | ||
| case 'INPUT': | ||
| case 'LINK': | ||
| case 'META': | ||
| case 'SOURCE': | ||
| case 'TRACK': | ||
| case 'WBR': | ||
| return true; | ||
|
|
||
| default: | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#156 also had some more directive-specific logic (such as a
next_directivemethod) that I removed. The reason is that it’ll need some more work where we’d need to move some of the logic that’s currently inwp_process_directivesinto this class. This isn’t currently a priority, so I’ve opted to defer it.As a consequence however,
WP_Directive_Processordoesn’t have any directive-specific code at all — it’s really more of aWP_HTML_Tag_Processor-derived class that allows finding a balanced closing tag, and getting/setting inner HTML. It might thus be worth considering renaming that class.