Skip to content

Commit

Permalink
Enhance attributes to specify types and defaults
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Jul 14, 2017
1 parent 19582ef commit c102331
Show file tree
Hide file tree
Showing 30 changed files with 371 additions and 189 deletions.
35 changes: 25 additions & 10 deletions blocks/api/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
* External dependencies
*/
import uuid from 'uuid/v4';
import { get, castArray, findIndex, isObjectLike, find } from 'lodash';
import {
get,
reduce,
castArray,
findIndex,
isObjectLike,
find,
} from 'lodash';

/**
* Internal dependencies
*/
import { getNormalizedAttributeSource } from './parser';
import { getBlockType } from './registration';

/**
Expand All @@ -20,21 +28,28 @@ export function createBlock( name, attributes = {} ) {
// Get the type definition associated with a registered block.
const blockType = getBlockType( name );

// Do we need this? What purpose does it have?
let defaultAttributes;
if ( blockType ) {
defaultAttributes = blockType.defaultAttributes;
}
// Ensure attributes contains only values defined by block type, and merge
// default values for missing attributes.
attributes = reduce( blockType.attributes, ( result, source, key ) => {
const value = attributes[ key ];
if ( undefined !== value ) {
result[ key ] = value;
} else {
source = getNormalizedAttributeSource( source );
if ( source.defaultValue ) {
result[ key ] = source.defaultValue;
}
}

return result;
}, {} );

// Blocks are stored with a unique ID, the assigned type name,
// and the block attributes.
return {
uid: uuid(),
name,
attributes: {
...defaultAttributes,
...attributes,
},
attributes,
};
}

Expand Down
106 changes: 76 additions & 30 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 { pickBy } from 'lodash';
import { isPlainObject, mapValues, pickBy } from 'lodash';

/**
* Internal dependencies
Expand All @@ -11,29 +11,62 @@ import { parse as grammarParse } from './post.pegjs';
import { getBlockType, getUnknownTypeHandler } from './registration';
import { createBlock } from './factory';

/**
* Returns true if the provided function is a valid attribute matcher, or false
* otherwise.
*
* Matchers 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.
*
* @param {Function} matcher Function to test
* @return {Boolean} Whether function is an attribute matcher
*/
export function isValidMatcher( matcher ) {
return !! matcher && '_wpBlocksKnownMatcher' in matcher;
}

/**
* Returns the block attributes parsed from raw content.
*
* @param {String} rawContent Raw block content
* @param {Object} attributes Block attribute matchers
* @return {Object} Block attributes
* @param {String} rawContent Raw block content
* @param {Object} sources Block attribute matchers
* @return {Object} Block attributes
*/
export function parseBlockAttributes( rawContent, attributes ) {
if ( 'function' === typeof attributes ) {
return attributes( rawContent );
} else if ( attributes ) {
// Matchers are implemented as functions that receive a DOM node from
// which to select data. Use of the DOM is incidental and we shouldn't
// guarantee a contract that this be provided, else block implementers
// may feel compelled 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.
const knownMatchers = pickBy( attributes, '_wpBlocksKnownMatcher' );

return hpqParse( rawContent, knownMatchers );
}
export function getMatcherAttributes( rawContent, sources ) {
const matchers = mapValues(
// Parse only sources with matcher defined
pickBy( sources, ( source ) => isValidMatcher( source.matcher ) ),

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

return {};
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 };
}
}

/**
Expand All @@ -45,18 +78,31 @@ export function parseBlockAttributes( rawContent, attributes ) {
* @return {Object} All block attributes
*/
export function getBlockAttributes( blockType, rawContent, attributes ) {
// Merge any attributes present in comment delimiters with those
// that are specified in the block implementation.
attributes = attributes || {};
if ( blockType ) {
attributes = {
...blockType.defaultAttributes,
...attributes,
...parseBlockAttributes( rawContent, blockType.attributes ),
};
}
const sources = mapValues( blockType.attributes, getNormalizedAttributeSource );

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

return mapValues( sources, ( source, key ) => {
const 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;
}
} else if ( source.type && source.type.prototype.valueOf ) {
// Coerce to constructor value if source type
return ( new source.type( value ) ).valueOf();
}

return attributes;
return value;
} );
}

/**
Expand Down
12 changes: 7 additions & 5 deletions blocks/api/paste.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import { nodeListToReact } from 'dom-react';
import { find, get } from 'lodash';
import { find, get, mapValues } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -14,7 +14,7 @@ import { createElement } from 'element';
*/
import { createBlock } from './factory';
import { getBlockTypes, getUnknownTypeHandler } from './registration';
import { parseBlockAttributes } from './parser';
import { getMatcherAttributes, getNormalizedAttributeSource } from './parser';

/**
* Normalises array nodes of any node type to an array of block level nodes.
Expand Down Expand Up @@ -89,10 +89,12 @@ export default function( nodes ) {
return acc;
}

const { name, defaultAttributes = [] } = blockType;
const attributes = parseBlockAttributes( node.outerHTML, transform.attributes );
const attributes = getMatcherAttributes(
node.outerHTML,
mapValues( transform.attributes, getNormalizedAttributeSource )
);

return createBlock( name, { ...defaultAttributes, ...attributes } );
return createBlock( blockType.name, attributes );
}, null );

if ( block ) {
Expand Down
59 changes: 31 additions & 28 deletions blocks/api/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Component, createElement, renderToString, cloneElement, Children } from
* Internal dependencies
*/
import { getBlockType } from './registration';
import { parseBlockAttributes } from './parser';
import { getNormalizedAttributeSource } from './parser';

/**
* Returns the block's default classname from its name
Expand Down Expand Up @@ -71,36 +71,39 @@ export function getSaveContent( blockType, attributes ) {
}

/**
* Returns attributes which ought to be saved
* and serialized into the block comment header
* Returns attributes which are to be saved and serialized into the block
* comment delimiter.
*
* When a block exists in memory it contains as its attributes
* both those which come from the block comment header _and_
* those which come from parsing the contents of the block.
* When a block exists in memory it contains as its attributes both those
* parsed the block comment delimiter _and_ those which matched from the
* contents of the block.
*
* This function returns only those attributes which are
* needed to persist and which cannot already be inferred
* from the block content.
* This function returns only those attributes which are needed to persist and
* which cannot be matched from the block content.
*
* @param {Object<String,*>} allAttributes Attributes from in-memory block data
* @param {Object<String,*>} attributesFromContent Attributes which are inferred from block content
* @returns {Object<String,*>} filtered set of attributes for minimum save/serialization
* @param {Object<String,*>} allAttributes Attributes from in-memory block data
* @param {Object<String,*>} sources Block type attributes definition
* @returns {Object<String,*>} Subset of attributes for comment serialization
*/
export function getCommentAttributes( allAttributes, attributesFromContent ) {
// Iterate over attributes and produce the set to save
return reduce(
Object.keys( allAttributes ),
( toSave, key ) => {
const allValue = allAttributes[ key ];
const contentValue = attributesFromContent[ key ];

// save only if attribute if not inferred from the content and if valued
return ! ( contentValue !== undefined || allValue === undefined )
? Object.assign( toSave, { [ key ]: allValue } )
: toSave;
},
{},
);
export function getCommentAttributes( allAttributes, sources ) {
return reduce( sources, ( result, source, key ) => {
const value = allAttributes[ key ];

// Ignore undefined values
if ( undefined === value ) {
return result;
}

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

// Otherwise, include in comment set
result[ key ] = value;
return result;
}, {} );
}

export function serializeAttributes( attrs ) {
Expand All @@ -115,7 +118,7 @@ export function serializeBlock( block ) {
const blockName = block.name;
const blockType = getBlockType( blockName );
const saveContent = getSaveContent( blockType, block.attributes );
const saveAttributes = getCommentAttributes( block.attributes, parseBlockAttributes( saveContent, blockType.attributes ) );
const saveAttributes = getCommentAttributes( block.attributes, blockType.attributes );

if ( 'wp:core/more' === blockName ) {
return `<!-- more ${ saveAttributes.customText ? `${ saveAttributes.customText } ` : '' }-->${ saveAttributes.noTeaser ? '\n<!--noteaser-->' : '' }`;
Expand Down
Loading

0 comments on commit c102331

Please sign in to comment.