From c1e7d7edb39ec78a448b42bc055b82d46c19734b Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 8 Nov 2019 19:07:09 -0500 Subject: [PATCH] Parser: Override parser to implement attributes sourcing --- ...ass-wp-sourced-attributes-block-parser.php | 198 ++++++++++++++++++ lib/load.php | 3 +- lib/parser.php | 117 +++++++++++ 3 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 lib/class-wp-sourced-attributes-block-parser.php create mode 100644 lib/parser.php diff --git a/lib/class-wp-sourced-attributes-block-parser.php b/lib/class-wp-sourced-attributes-block-parser.php new file mode 100644 index 0000000000000..97f6161b93e30 --- /dev/null +++ b/lib/class-wp-sourced-attributes-block-parser.php @@ -0,0 +1,198 @@ + $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( '' . $block['innerHTML'] . '' ); + } 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; + } + +} diff --git a/lib/load.php b/lib/load.php index 58e545b38a6f7..54cfe25350a1e 100644 --- a/lib/load.php +++ b/lib/load.php @@ -51,7 +51,8 @@ function gutenberg_is_experiment_enabled( $name ) { } require dirname( __FILE__ ) . '/compat.php'; - +require dirname( __FILE__ ) . '/class-wp-sourced-attributes-block-parser.php'; +require dirname( __FILE__ ) . '/parser.php'; require dirname( __FILE__ ) . '/blocks.php'; require dirname( __FILE__ ) . '/templates.php'; require dirname( __FILE__ ) . '/template-loader.php'; diff --git a/lib/parser.php b/lib/parser.php new file mode 100644 index 0000000000000..5c9cfb0fde29c --- /dev/null +++ b/lib/parser.php @@ -0,0 +1,117 @@ + + */ + + // 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; +}