Skip to content

Commit

Permalink
Parser: Override parser to implement attributes sourcing
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Nov 9, 2019
1 parent 71ea659 commit c1e7d7e
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 1 deletion.
198 changes: 198 additions & 0 deletions lib/class-wp-sourced-attributes-block-parser.php
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;
}

}
3 changes: 2 additions & 1 deletion lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
117 changes: 117 additions & 0 deletions lib/parser.php
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;
}

0 comments on commit c1e7d7e

Please sign in to comment.