Skip to content

Commit

Permalink
Parsing: Assign attributes value as schema
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Aug 4, 2017
1 parent c196f72 commit b64317c
Show file tree
Hide file tree
Showing 30 changed files with 380 additions and 202 deletions.
17 changes: 11 additions & 6 deletions blocks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ add_action( 'enqueue_block_editor_assets', 'random_image_enqueue_block_editor_as
category: 'media',

attributes: {
category: query.attr( 'img', 'alt' )
category: {
type: 'string',
source: query.attr( 'img', 'alt' )
}
},

edit: function( props ) {
Expand Down Expand Up @@ -231,11 +234,13 @@ editor interface where blocks are implemented.
[Dashicon](https://developer.wordpress.org/resource/dashicons/#awards)
to be shown in the control's button, or an element (or function returning an
element) if you choose to render your own SVG.
- `attributes: Object | Function` - An object of
[matchers](http://github.com/aduth/hpq) or a function which, when passed the
raw content of the block, returns block attributes as an object. When defined
as an object of matchers, the attributes object is generated with values
corresponding to the shape of the matcher object keys.
- `attributes: Object | Function` - An object of attribute schemas, where the
keys of the object define the shape of attributes, and each value an object
schema describing the `type`, `default` (optional), and
[`source`](http://gutenberg-devdoc.surge.sh/reference/attribute-matchers/)
(optional) of the attribute. If `source` is omitted, the attribute is
serialized into the block's comment delimiters. Alternatively, define
`attributes` as a function which returns the attributes object.
- `category: string` - Slug of the block's category. The category is used to
organize the blocks in the block inserter.
- `edit( { attributes: Object, setAttributes: Function } ): WPElement` -
Expand Down
8 changes: 2 additions & 6 deletions blocks/api/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
/**
* Internal dependencies
*/
import { getNormalizedAttributeSource } from './parser';
import { getBlockType } from './registration';

/**
Expand All @@ -34,11 +33,8 @@ export function createBlock( name, attributes = {} ) {
const value = attributes[ key ];
if ( undefined !== value ) {
result[ key ] = value;
} else {
source = getNormalizedAttributeSource( source );
if ( source.defaultValue ) {
result[ key ] = source.defaultValue;
}
} else if ( source.default ) {
result[ key ] = source.default;
}

return result;
Expand Down
110 changes: 57 additions & 53 deletions blocks/api/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import { parse as hpqParse } from 'hpq';
import { isPlainObject, mapValues, pickBy } from 'lodash';
import { mapValues, reduce, pickBy } from 'lodash';

/**
* Internal dependencies
Expand All @@ -13,61 +13,39 @@ import { createBlock } from './factory';
import { isValidBlock } from './validation';

/**
* Returns true if the provided function is a valid attribute matcher, or false
* Returns true if the provided function is a valid attribute source, or false
* otherwise.
*
* Matchers are implemented as functions receiving a DOM node to select data
* Sources are implemented as functions receiving a DOM node to select data
* from. Using the DOM is incidental and we shouldn't guarantee a contract that
* this be provided, else block implementers may feel inclined to use the node.
* Instead, matchers are intended as a generic interface to query data from any
* tree shape. Here we pick only matchers which include an internal flag.
* Instead, sources are intended as a generic interface to query data from any
* tree shape. Here we pick only sources which include an internal flag.
*
* @param {Function} matcher Function to test
* @return {Boolean} Whether function is an attribute matcher
* @param {Function} source Function to test
* @return {Boolean} Whether function is an attribute source
*/
export function isValidMatcher( matcher ) {
return !! matcher && '_wpBlocksKnownMatcher' in matcher;
export function isValidSource( source ) {
return !! source && '_wpBlocksKnownMatcher' in source;
}

/**
* Returns the block attributes parsed from raw content.
*
* @param {String} rawContent Raw block content
* @param {Object} sources Block attribute matchers
* @return {Object} Block attributes
* @param {Object} schema Block attribute schema
* @return {Object} Block attribute values
*/
export function getMatcherAttributes( rawContent, sources ) {
const matchers = mapValues(
// Parse only sources with matcher defined
pickBy( sources, ( source ) => isValidMatcher( source.matcher ) ),
export function getSourcedAttributes( rawContent, schema ) {
const sources = mapValues(
// Parse only sources with source defined
pickBy( schema, ( attributeSchema ) => isValidSource( attributeSchema.source ) ),

// Transform to object where matcher is value
( source ) => source.matcher
// Transform to object where source is value
( attributeSchema ) => attributeSchema.source
);

return hpqParse( rawContent, matchers );
}

/**
* Returns an attribute source in normalized (object) form. A source may be
* specified in shorthand form as a constructor or attribute matcher, or in its
* expanded form as an object with any of `type`, `matcher`, and `defaultValue`
* values.
*
* @param {(Object|Function)} source Source to normalize
* @return {Object} Normalized source
*/
export function getNormalizedAttributeSource( source ) {
if ( isPlainObject( source ) ) {
return source;
} if ( 'function' === typeof source ) {
// Function may be either matcher or a constructor. Quack quack.
if ( isValidMatcher( source ) ) {
return { matcher: source };
}

return { type: source };
}
return hpqParse( rawContent, sources );
}

/**
Expand All @@ -79,31 +57,57 @@ export function getNormalizedAttributeSource( source ) {
* @return {Object} All block attributes
*/
export function getBlockAttributes( blockType, rawContent, attributes ) {
const sources = mapValues( blockType.attributes, getNormalizedAttributeSource );

// Merge matcher values into attributes parsed from comment delimiters
// Merge source values into attributes parsed from comment delimiters
attributes = {
...attributes,
...getMatcherAttributes( rawContent, sources ),
...getSourcedAttributes( rawContent, blockType.attributes ),
};

return mapValues( sources, ( source, key ) => {
const value = attributes[ key ];
return reduce( blockType.attributes, ( result, source, key ) => {
let value = attributes[ key ];

// Return default if attribute value not assigned
if ( undefined === value ) {
// Nest the condition so that constructor coercion never occurs if
// value is undefined and block type doesn't specify default value
if ( 'defaultValue' in source ) {
return source.defaultValue;
if ( 'default' in source ) {
value = source.default;
} else {
return result;
}
} else if ( source.type && source.type.prototype.valueOf ) {
// Coerce to constructor value if source type
return ( new source.type( value ) ).valueOf();
}

return value;
} );
// Coerce to constructor value if source type
switch ( source.type ) {
case 'string':
value = String( value );
break;

case 'boolean':
value = Boolean( value );
break;

case 'object':
value = Object( value );
break;

case 'null':
value = null;
break;

case 'array':
value = Array.from( value );
break;

case 'integer':
case 'number':
value = Number( value );
break;
}

result[ key ] = value;
return result;
}, {} );
}

/**
Expand Down
6 changes: 3 additions & 3 deletions blocks/api/paste.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { find, get, mapValues, flowRight as compose } from 'lodash';
import { find, get, flowRight as compose } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -13,7 +13,7 @@ import { nodetypes } from '@wordpress/utils';
*/
import { createBlock } from './factory';
import { getBlockTypes, getUnknownTypeHandler } from './registration';
import { getMatcherAttributes, getNormalizedAttributeSource } from './parser';
import { getMatcherAttributes } from './parser';
import stripAttributes from './paste/strip-attributes';
import removeSpans from './paste/remove-spans';

Expand Down Expand Up @@ -96,7 +96,7 @@ export default function( nodes ) {

const attributes = getMatcherAttributes(
node.outerHTML,
mapValues( transform.attributes, getNormalizedAttributeSource )
transform.attributes,
);

return createBlock( blockType.name, attributes );
Expand Down
12 changes: 5 additions & 7 deletions blocks/api/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { Component, createElement, renderToString, cloneElement, Children } from
* Internal dependencies
*/
import { getBlockType } from './registration';
import { getNormalizedAttributeSource } from './parser';

/**
* Returns the block's default classname from its name
Expand Down Expand Up @@ -82,11 +81,11 @@ export function getSaveContent( blockType, attributes ) {
* which cannot be matched from the block content.
*
* @param {Object<String,*>} allAttributes Attributes from in-memory block data
* @param {Object<String,*>} sources Block type attributes definition
* @param {Object<String,*>} schema Block type schema
* @returns {Object<String,*>} Subset of attributes for comment serialization
*/
export function getCommentAttributes( allAttributes, sources ) {
return reduce( sources, ( result, source, key ) => {
export function getCommentAttributes( allAttributes, schema ) {
return reduce( schema, ( result, attributeSchema, key ) => {
const value = allAttributes[ key ];

// Ignore undefined values
Expand All @@ -95,13 +94,12 @@ export function getCommentAttributes( allAttributes, sources ) {
}

// Ignore values sources from content
source = getNormalizedAttributeSource( source );
if ( source.matcher ) {
if ( attributeSchema.source ) {
return result;
}

// Ignore default value
if ( 'defaultValue' in source && source.defaultValue === value ) {
if ( 'default' in attributeSchema && attributeSchema.default === value ) {
return result;
}

Expand Down
Loading

0 comments on commit b64317c

Please sign in to comment.