Skip to content

Commit

Permalink
fix: Correctly parse nested attribute and tag sources.
Browse files Browse the repository at this point in the history
  • Loading branch information
DDEV User committed Sep 23, 2024
1 parent a9ad853 commit 664f264
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 44 deletions.
5 changes: 5 additions & 0 deletions .changeset/neat-books-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wpengine/wp-graphql-content-blocks": patch
---

fix: Correctly parse nested attribute and tag sources.
40 changes: 17 additions & 23 deletions includes/Data/BlockAttributeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,16 @@ public static function resolve_block_attribute( $attribute, string $html, $attri
$value = null;

if ( isset( $attribute['source'] ) ) {
// @todo parse remaining sources: https://github.com/WordPress/gutenberg/blob/trunk/packages/blocks/src/api/parser/get-block-attributes.js#L198
switch ( $attribute['source'] ) {
case 'attribute':
$value = self::parse_attribute_source( $html, $attribute );
break;
case 'html':
case 'rich-text':
// If there is no selector, we are dealing with single source.
// If there is no selector, the source is the node's innerHTML.
if ( ! isset( $attribute['selector'] ) ) {
$value = self::parse_single_source( $html, $attribute['source'] );
$value = ! empty( $html ) ? DOMHelpers::find_nodes( $html )->innerHTML() : null;
break;
}
$value = self::parse_html_source( $html, $attribute );
Expand All @@ -47,6 +48,9 @@ public static function resolve_block_attribute( $attribute, string $html, $attri
case 'query':
$value = self::parse_query_source( $html, $attribute, $attribute_value );
break;
case 'tag':
$value = self::parse_tag_source( $html );
break;
case 'meta':
$value = self::parse_meta_source( $attribute );
break;
Expand Down Expand Up @@ -76,25 +80,6 @@ public static function resolve_block_attribute( $attribute, string $html, $attri
return $value;
}

/**
* Parses the block content of a source only block type
*
* @param string $html The html value
* @param string $source The source type
*/
private static function parse_single_source( string $html, $source ): ?string {
if ( empty( $html ) ) {
return null;
}

switch ( $source ) {
case 'html':
return DOMHelpers::find_nodes( $html )->innerHTML();
}

return null;
}

/**
* Parses the block content of an HTML source block type.
*
Expand Down Expand Up @@ -125,11 +110,11 @@ private static function parse_html_source( string $html, array $config ): ?strin
* @param array<string,mixed> $config The value configuration.
*/
private static function parse_attribute_source( string $html, array $config ): ?string {
if ( empty( $html ) || ! isset( $config['selector'] ) || ! isset( $config['attribute'] ) ) {
if ( empty( $html ) || ! isset( $config['attribute'] ) ) {
return null;
}

return DOMHelpers::parse_attribute( $html, $config['selector'], $config['attribute'] );
return DOMHelpers::parse_attribute( $html, $config['selector'] ?? '', $config['attribute'] );
}

/**
Expand Down Expand Up @@ -204,4 +189,13 @@ private static function parse_meta_source( array $config ): ?string {

return get_post_meta( $post_id, $config['meta'], true );
}

/**
* Parses a tag source.
*
* @param string $html The html value.
*/
private static function parse_tag_source( string $html ): ?string {
return DOMHelpers::get_first_node_tag_name( $html );
}
}
27 changes: 27 additions & 0 deletions includes/Utilities/DOMHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,33 @@ public static function parse_text( string $html, string $selector ): ?string {
return $nodes[0]->text();
}

/**
* Gets the tag name of the first node.
*
* @internal This method should only be used internally. There are no guarantees for backwards compatibility.
*
* @param string $html The HTML string to parse.
*/
public static function get_first_node_tag_name( string $html ): ?string {
// Bail early if there's no html to parse.
if ( empty( trim( $html ) ) ) {
return null;
}

$doc = new Document();
$doc->loadHtml( $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );

/** @var \DiDom\Element[] $nodes */
$nodes = $doc->find( '*' );

if ( count( $nodes ) === 0 ) {
return null;
}

// Lowercase the tag name.
return strtolower( $nodes[0]->tagName() );
}

/**
* Parses the html into DOMElement and searches the DOM tree for a given XPath expression or CSS selector.
*
Expand Down
187 changes: 166 additions & 21 deletions tests/unit/CoreTableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ className
* Tested attributes:
* - caption
* - hasFixedLayout
* - body > cells > tag (Rest are not working)
* - body > cells:
* - content
* - tag
*/
public function test_retrieve_core_table_attribute_fields() {
$block_content = '
Expand Down Expand Up @@ -184,10 +186,10 @@ public function test_retrieve_core_table_attribute_fields() {
// Test the first row
$this->assertCount( 2, $body[0]['cells'], 'There should be 2 cells in the first row' );
$this->assertEquals(
[ // @todo These should be filled in
[
'align' => null,
'colspan' => null,
'content' => null,
'content' => 'Cell 1',
'rowspan' => null,
'scope' => null,
'tag' => 'td',
Expand All @@ -199,7 +201,7 @@ public function test_retrieve_core_table_attribute_fields() {
[ // @todo These should be filled in
'align' => null,
'colspan' => null,
'content' => null,
'content' => 'Cell 2',
'rowspan' => null,
'scope' => null,
'tag' => 'td',
Expand All @@ -211,10 +213,10 @@ public function test_retrieve_core_table_attribute_fields() {
// Test the second row
$this->assertCount( 2, $body[1]['cells'], 'There should be 2 cells in the second row' );
$this->assertEquals(
[ // @todo These should be filled in
[
'align' => null,
'colspan' => null,
'content' => null,
'content' => 'Cell 3',
'rowspan' => null,
'scope' => null,
'tag' => 'td',
Expand All @@ -223,10 +225,10 @@ public function test_retrieve_core_table_attribute_fields() {
'The first cell in the second row does not match'
);
$this->assertEquals(
[ // @todo These should be filled in
[
'align' => null,
'colspan' => null,
'content' => null,
'content' => 'Cell 4',
'rowspan' => null,
'scope' => null,
'tag' => 'td',
Expand All @@ -245,8 +247,15 @@ public function test_retrieve_core_table_attribute_fields() {
* - fontFamily
* - fontSize
* - style
* - head > cells > tag (Rest are not working)
* - foot > cells > tag (Rest are not working)
* - body > cells > align
* - head > cells:
* - align
* - content
* - tag
* - foot > cells:
* - align
* - content
* - tag
*/
public function test_retrieve_core_table_attribute_fields_header_footer() {
$block_content = '
Expand Down Expand Up @@ -316,15 +325,7 @@ public function test_retrieve_core_table_attribute_fields_header_footer() {
$this->assertCount( 1, $head, 'There should be 1 row in the head' );
$this->assertCount( 2, $head[0]['cells'], 'There should be 2 cells in the head' );

// Test the foot cells.
// @todo This is duplicated after the `markTestIncomplete` call and should be removed once fixed.
$this->assertNotEmpty( $foot, 'The foot should have cells' );
$this->assertCount( 1, $foot, 'There should be 1 row in the foot' );
$this->assertCount( 2, $foot[0]['cells'], 'There should be 2 cells in the foot' );

$this->markTestIncomplete( 'Cell attributes are not returned' );

$this->assertEquals(
$this->assertEquals( // Previously untested
[
'align' => 'left',
'colspan' => null,
Expand Down Expand Up @@ -356,7 +357,7 @@ public function test_retrieve_core_table_attribute_fields_header_footer() {
// Test the left cell.
$this->assertEquals(
[
'align' => 'left',
'align' => 'left', // Previously untested
'colspan' => null,
'content' => 'This column has "align column left"',
'rowspan' => null,
Expand All @@ -377,14 +378,16 @@ public function test_retrieve_core_table_attribute_fields_header_footer() {
'scope' => null,
'tag' => 'td',
],
$body[0]['cells'][1],
'The second cell in the first row does not match'
);

// Test the foot cells.
$this->assertNotEmpty( $foot, 'The foot should have cells' );
$this->assertCount( 1, $foot, 'There should be 1 row in the foot' );
$this->assertCount( 2, $foot[0]['cells'], 'There should be 2 cells in the foot' );

$this->assertEquals(
$this->assertEquals( // Previously untested
[
'align' => 'left',
'colspan' => null,
Expand Down Expand Up @@ -556,4 +559,146 @@ public function test_retrieve_core_table_attribute_lock_gradient(): void {
'The block attributes do not match'
);
}

/**
* Test custom cell markup in the CoreTable block.
*
* Tested attributes:
* - body > cells:
* - colspan
* - rowspan
* - scope
* - foot > cells:
* - colspan
* - rowspan
* - scope
* - head > cells:
* - colspan
* - rowspan
* - scope
*/
public function test_retrieve_core_table_custom_cell_markup(): void {
$block_markup = '
<!-- wp:table -->
<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th scope="col" colspan="2">Header label</th><th>Header label</th></tr></thead><tbody><tr><td rowspan="2">Cell 1</td><td colspan="2">Cell 2</td></tr><tr><td>Cell 3</td><td>Cell 4</td></tr></tbody><tfoot><tr><td colspan="3">Footer label</td></tr></tfoot></table><figcaption class="wp-element-caption">Caption</figcaption></figure>
<!-- /wp:table -->
';

wp_update_post(
[
'ID' => $this->post_id,
'post_content' => $block_markup,
]
);

$query = $this->query();
$variables = [
'id' => $this->post_id,
];

$actual = graphql( compact( 'query', 'variables' ) );

$this->assertArrayNotHasKey( 'errors', $actual, 'There should not be any errors' );
$this->assertArrayHasKey( 'data', $actual, 'The data key should be present' );
$this->assertArrayHasKey( 'post', $actual['data'], 'The post key should be present' );

$this->assertEquals( $this->post_id, $actual['data']['post']['databaseId'], 'The post ID should match' );

$block = $actual['data']['post']['editorBlocks'][0];

// We only need to test the cells.
$body = $block['attributes']['body'];
$head = $block['attributes']['head'];
$foot = $block['attributes']['foot'];

// No need to test the cells again.

// Test the head cells.
$this->assertNotEmpty( $head, 'The head should have cells' );
$this->assertCount( 1, $head, 'There should be 1 row in the head' );
$this->assertCount( 2, $head[0]['cells'], 'There should be 2 cells in the head' );

// Test the first cell in the head.
$this->assertEquals(
[
'align' => null,
'colspan' => 2, // Previously untested
'content' => 'Header label',
'rowspan' => null,
'scope' => 'col', // Previously untested
'tag' => 'th',
],
$head[0]['cells'][0],
'The first cell in the head does not match'
);

// Test the second cell in the head.
$this->assertEquals(
[
'align' => null,
'colspan' => null,
'content' => 'Header label',
'rowspan' => null,
'scope' => null,
'tag' => 'th',
],
$head[0]['cells'][1],
'The second cell in the head does not match'
);

// Test the body cells.
$this->assertNotEmpty( $body, 'The body should have cells' );
$this->assertCount( 2, $body, 'There should be 2 rows' );

// Test the first row.
$this->assertCount( 2, $body[0]['cells'], 'There should be 2 cells in the first row' );

// Test the first cell in the first row.
$this->assertEquals(
[
'align' => null,
'colspan' => null,
'content' => 'Cell 1',
'rowspan' => 2, // Previously untested
'scope' => null,
'tag' => 'td',
],
$body[0]['cells'][0],
'The first cell in the first row does not match'
);

// Test the second cell in the first row.

$this->assertEquals(
[
'align' => null,
'colspan' => 2,
'content' => 'Cell 2',
'rowspan' => null,
'scope' => null,
'tag' => 'td',
],
$body[0]['cells'][1],
'The second cell in the first row does not match'
);

// Test the footer cells.
$this->assertNotEmpty( $foot, 'The foot should have cells' );
$this->assertCount( 1, $foot, 'There should be 1 row in the foot' );
$this->assertCount( 1, $foot[0]['cells'], 'There should be 1 cell in the foot' );

// Test the first cell in the foot.
$this->assertEquals(
[
'align' => null,
'colspan' => 3,
'content' => 'Footer label',
'rowspan' => null,
'scope' => null,
'tag' => 'td',
],
$foot[0]['cells'][0],
'The first cell in the foot does not match'
);
}
}

0 comments on commit 664f264

Please sign in to comment.