-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Parser: Override parser to implement attributes sourcing
- Loading branch information
Showing
3 changed files
with
317 additions
and
1 deletion.
There are no files selected for viewing
This file contains 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,198 @@ | ||
<?php | ||
/** | ||
* Block Serialization Parser | ||
* | ||
* @package Gutenberg | ||
*/ | ||
|
||
/** | ||
* Class WP_Sourced_Attributes_Block_Parser | ||
* | ||
* Parses a document and constructs a list of parsed block objects | ||
* | ||
* @since 6.9.0 | ||
*/ | ||
class WP_Sourced_Attributes_Block_Parser extends WP_Block_Parser { | ||
|
||
/** | ||
* Parses a document and returns a list of block structures | ||
* | ||
* When encountering an invalid parse will return a best-effort | ||
* parse. In contrast to the specification parser this does not | ||
* return an error on invalid inputs. | ||
* | ||
* @since 6.9.0 | ||
* | ||
* @param string $document Input document being parsed. | ||
* @param WP_Block_Type_Registry $registry Block type registry from which | ||
* block attributes schema can be | ||
* retrieved. | ||
* @param int|null $post_id Optional post ID. | ||
* @return WP_Block_Parser_Block[] | ||
*/ | ||
function parse( $document, $registry = null, $post_id = null ) { | ||
if ( is_null( $registry ) ) { | ||
$registry = WP_Block_Type_Registry::get_instance(); | ||
} | ||
|
||
$blocks = parent::parse( $document ); | ||
|
||
foreach ( $blocks as $i => $block ) { | ||
$block_type = $registry->get_registered( $block['blockName'] ); | ||
if ( is_null( $block_type ) || ! isset( $block_type->attributes ) ) { | ||
continue; | ||
} | ||
|
||
$sourced_attributes = $this->get_sourced_attributes( | ||
$block, | ||
$block_type->attributes, | ||
$post_id | ||
); | ||
|
||
$blocks[ $i ]['attrs'] = array_merge( $block['attrs'], $sourced_attributes ); | ||
} | ||
|
||
return $blocks; | ||
} | ||
|
||
/** | ||
* Returns an array of sourced attribute values for a block. | ||
* | ||
* @param WP_Block_Parser_Block $block Parsed block object. | ||
* @param array $attributes_schema Attributes of registered | ||
* block type for block. | ||
* @param int|null $post_id Optional post ID. | ||
* @return array Sourced attribute values. | ||
*/ | ||
function get_sourced_attributes( $block, $attributes_schema, $post_id ) { | ||
$attributes = array(); | ||
|
||
foreach ( $attributes_schema as $key => $attribute_schema ) { | ||
if ( isset( $attribute_schema['source'] ) ) { | ||
$attributes[ $key ] = $this->get_sourced_attribute( | ||
$block, | ||
$attribute_schema, | ||
$post_id | ||
); | ||
} | ||
} | ||
|
||
return $attributes; | ||
} | ||
|
||
/** | ||
* Returns a sourced attribute value for a block, for attribute type which | ||
* sources from HTML. | ||
* | ||
* @param WP_Block_Parser_Block $block Parsed block object. | ||
* @param array $attribute_schema Attribute schema for | ||
* individual attribute to | ||
* be parsed. | ||
* @return mixed Sourced attribute value. | ||
*/ | ||
function get_html_sourced_attribute( $block, $attribute_schema ) { | ||
$document = new DOMDocument(); | ||
try { | ||
$document->loadHTML( '<html><body>' . $block['innerHTML'] . '</body></html>' ); | ||
} catch ( Exception $e ) { | ||
return null; | ||
} | ||
|
||
$selector = 'body'; | ||
if ( isset( $attribute_schema['selector'] ) ) { | ||
$selector .= ' ' . $attribute_schema['selector']; | ||
} | ||
|
||
$xpath_selector = _wp_css_selector_to_xpath( $selector ); | ||
$xpath = new DOMXpath( $document ); | ||
$match = $xpath->evaluate( $xpath_selector ); | ||
|
||
if ( 0 === $match->count() ) { | ||
return null; | ||
} | ||
|
||
$element = $match->item( 0 ); | ||
|
||
switch ( $attribute_schema['source'] ) { | ||
case 'text': | ||
/* | ||
* See: https://github.com/WordPress/WordPress-Coding-Standards/issues/574 | ||
*/ | ||
// phpcs:ignore | ||
return $element->textContent; | ||
|
||
case 'html': | ||
$inner_html = ''; | ||
|
||
/* | ||
* See: https://github.com/WordPress/WordPress-Coding-Standards/issues/574 | ||
*/ | ||
// phpcs:ignore | ||
foreach ( $element->childNodes as $child ) { | ||
/* | ||
* See: https://github.com/WordPress/WordPress-Coding-Standards/issues/574 | ||
*/ | ||
// phpcs:ignore | ||
$inner_html .= $child->ownerDocument->saveXML( $child ); | ||
} | ||
|
||
return $inner_html; | ||
|
||
case 'attribute': | ||
if ( ! isset( $attribute_schema['attribute'] ) || | ||
is_null( $element->attributes ) ) { | ||
return null; | ||
} | ||
|
||
$attribute = $element->attributes->getNamedItem( $attribute_schema['attribute'] ); | ||
|
||
/* | ||
* See: https://github.com/WordPress/WordPress-Coding-Standards/issues/574 | ||
*/ | ||
// phpcs:ignore | ||
return is_null( $attribute ) ? null : $attribute->nodeValue; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* Returns a sourced attribute value for a block. | ||
* | ||
* @param WP_Block_Parser_Block $block Parsed block object. | ||
* @param array $attribute_schema Attribute schema for | ||
* individual attribute to | ||
* be parsed. | ||
* @param int|null $post_id Optional post ID. | ||
* @return mixed Sourced attribute value. | ||
*/ | ||
function get_sourced_attribute( $block, $attribute_schema, $post_id ) { | ||
switch ( $attribute_schema['source'] ) { | ||
case 'text': | ||
case 'html': | ||
case 'attribute': | ||
return $this->get_html_sourced_attribute( $block, $attribute_schema ); | ||
|
||
case 'query': | ||
// TODO: Implement. | ||
return null; | ||
|
||
case 'property': | ||
case 'node': | ||
case 'children': | ||
case 'tag': | ||
// Unsupported or deprecated. | ||
return null; | ||
|
||
case 'meta': | ||
if ( ! is_null( $post_id ) && isset( $attribute_schema['meta'] ) ) { | ||
return get_post_meta( $post_id, $attribute_schema['meta'] ); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
} |
This file contains 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
This file contains 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,117 @@ | ||
<?php | ||
/** | ||
* Includes revisions to the block parser not yet proposed for core adoption. | ||
* | ||
* @package gutenberg | ||
*/ | ||
|
||
/** | ||
* Filters to override default block parser with Gutenberg's custom | ||
* implementation. | ||
* | ||
* @since 6.9.0 | ||
*/ | ||
function gutenberg_replace_block_parser_class() { | ||
return 'WP_Sourced_Attributes_Block_Parser'; | ||
} | ||
add_filter( 'block_parser_class', 'gutenberg_replace_block_parser_class' ); | ||
|
||
/** | ||
* Given a CSS selector string, returns an equivalent XPath selector. | ||
* | ||
* @access private | ||
* @since 6.9.0 | ||
* | ||
* @param string $selector CSS selector. | ||
* @return string Equivalent XPath selector. | ||
*/ | ||
function _wp_css_selector_to_xpath( $selector ) { | ||
/* | ||
* Modified from PHP Selector, released under the MIT License. | ||
* https://github.com/tj/php-selector | ||
* Copyright © 2008 - 2009 TJ Holowaychuk <tj@vision-media.ca> | ||
*/ | ||
|
||
// Remove spaces around operators. | ||
$selector = preg_replace( '/\s*>\s*/', '>', $selector ); | ||
$selector = preg_replace( '/\s*~\s*/', '~', $selector ); | ||
$selector = preg_replace( '/\s*\+\s*/', '+', $selector ); | ||
$selector = preg_replace( '/\s*,\s*/', ',', $selector ); | ||
$selectors = preg_split( '/\s+(?![^\[]+\])/', $selector ); | ||
|
||
foreach ( $selectors as &$selector ) { | ||
/* , */ | ||
$selector = preg_replace( '/,/', '|descendant-or-self::', $selector ); | ||
/* input:checked, :disabled, etc. */ | ||
$selector = preg_replace( '/(.+)?:(checked|disabled|required|autofocus)/', '\1[@\2="\2"]', $selector ); | ||
/* input:autocomplete, :autocomplete */ | ||
$selector = preg_replace( '/(.+)?:(autocomplete)/', '\1[@\2="on"]', $selector ); | ||
/* input:button, input:submit, etc. */ | ||
$selector = preg_replace( '/:(text|password|checkbox|radio|button|submit|reset|file|hidden|image|datetime|datetime-local|date|month|time|week|number|range|email|url|search|tel|color)/', 'input[@type="\1"]', $selector ); | ||
/* foo[id] */ | ||
$selector = preg_replace( '/(\w+)\[([_\w-]+[_\w\d-]*)\]/', '\1[@\2]', $selector ); | ||
/* [id] */ | ||
$selector = preg_replace( '/\[([_\w-]+[_\w\d-]*)\]/', '*[@\1]', $selector ); | ||
/* foo[id=foo] */ | ||
$selector = preg_replace( '/\[([_\w-]+[_\w\d-]*)=[\'"]?(.*?)[\'"]?\]/', '[@\1="\2"]', $selector ); | ||
/* [id=foo] */ | ||
$selector = preg_replace( '/^\[/', '*[', $selector ); | ||
/* div#foo */ | ||
$selector = preg_replace( '/([_\w-]+[_\w\d-]*)\#([_\w-]+[_\w\d-]*)/', '\1[@id="\2"]', $selector ); | ||
/* #foo */ | ||
$selector = preg_replace( '/\#([_\w-]+[_\w\d-]*)/', '*[@id="\1"]', $selector ); | ||
/* div.foo */ | ||
$selector = preg_replace( '/([_\w-]+[_\w\d-]*)\.([_\w-]+[_\w\d-]*)/', '\1[contains(concat(" ",@class," ")," \2 ")]', $selector ); | ||
/* .foo */ | ||
$selector = preg_replace( '/\.([_\w-]+[_\w\d-]*)/', '*[contains(concat(" ",@class," ")," \1 ")]', $selector ); | ||
/* div:first-child */ | ||
$selector = preg_replace( '/([_\w-]+[_\w\d-]*):first-child/', '*/\1[position()=1]', $selector ); | ||
/* div:last-child */ | ||
$selector = preg_replace( '/([_\w-]+[_\w\d-]*):last-child/', '*/\1[position()=last()]', $selector ); | ||
/* :first-child */ | ||
$selector = str_replace( ':first-child', '*/*[position()=1]', $selector ); | ||
/* :last-child */ | ||
$selector = str_replace( ':last-child', '*/*[position()=last()]', $selector ); | ||
/* :nth-last-child */ | ||
$selector = preg_replace( '/:nth-last-child\((\d+)\)/', '[position()=(last() - (\1 - 1))]', $selector ); | ||
/* div:nth-child */ | ||
$selector = preg_replace( '/([_\w-]+[_\w\d-]*):nth-child\((\d+)\)/', '*/*[position()=\2 and self::\1]', $selector ); | ||
/* :nth-child */ | ||
$selector = preg_replace( '/:nth-child\((\d+)\)/', '*/*[position()=\1]', $selector ); | ||
/* :contains(Foo) */ | ||
$selector = preg_replace( '/([_\w-]+[_\w\d-]*):contains\((.*?)\)/', '\1[contains(string(.),"\2")]', $selector ); | ||
/* > */ | ||
$selector = preg_replace( '/>/', '/', $selector ); | ||
/* ~ */ | ||
$selector = preg_replace( '/~/', '/following-sibling::', $selector ); | ||
/* + */ | ||
$selector = preg_replace( '/\+([_\w-]+[_\w\d-]*)/', '/following-sibling::\1[position()=1]', $selector ); | ||
$selector = str_replace( ']*', ']', $selector ); | ||
$selector = str_replace( ']/*', ']', $selector ); | ||
} | ||
|
||
// ' ' | ||
$selector = implode( '/descendant::', $selectors ); | ||
$selector = 'descendant-or-self::' . $selector; | ||
// :scope | ||
$selector = preg_replace( '/(((\|)?descendant-or-self::):scope)/', '.\3', $selector ); | ||
// $element | ||
$sub_selectors = explode( ',', $selector ); | ||
|
||
foreach ( $sub_selectors as $key => $sub_selector ) { | ||
$parts = explode( '$', $sub_selector ); | ||
$sub_selector = array_shift( $parts ); | ||
|
||
if ( count( $parts ) && preg_match_all( '/((?:[^\/]*\/?\/?)|$)/', $parts[0], $matches ) ) { | ||
$results = $matches[0]; | ||
$results[] = str_repeat( '/..', count( $results ) - 2 ); | ||
$sub_selector .= implode( '', $results ); | ||
} | ||
|
||
$sub_selectors[ $key ] = $sub_selector; | ||
} | ||
|
||
$selector = implode( ',', $sub_selectors ); | ||
|
||
return $selector; | ||
} |