Skip to content

Commit

Permalink
Framework: Use a simple JS object to declare the attribute's source
Browse files Browse the repository at this point in the history
  • Loading branch information
youknowriad committed Oct 2, 2017
1 parent efd262d commit 40d3d40
Show file tree
Hide file tree
Showing 27 changed files with 271 additions and 229 deletions.
6 changes: 0 additions & 6 deletions blocks/api/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
/**
* External dependencies
*/
import * as source from './source';

export { source };
export { createBlock, switchToBlockType } from './factory';
export { default as parse } from './parser';
export { default as pasteHandler } from './paste';
Expand Down
160 changes: 71 additions & 89 deletions blocks/api/parser.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* External dependencies
*/
import { parse as hpqParse, attr } from 'hpq';
import { mapValues, reduce, pickBy } from 'lodash';
import { parse as hpqParse } from 'hpq';
import { keys } from 'lodash';

/**
* Internal dependencies
Expand All @@ -12,42 +12,7 @@ import { getBlockType, getUnknownTypeHandlerName } from './registration';
import { createBlock } from './factory';
import { isValidBlock } from './validation';
import { getCommentDelimitedContent } from './serializer';

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

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

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

return hpqParse( rawContent, sources );
}
import { attr, prop, html, text, query, node, children } from './source';

/**
* Returns value coerced to the specified JSON schema type string
Expand Down Expand Up @@ -87,6 +52,69 @@ export function asType( value, type ) {
return value;
}

/**
* Returns an hpq matcher given a source object
*
* @param {Object} source Attribute Source object
* @return {Function} hpq Matcher
*/
export function matcherFromSource( source ) {
switch ( source.type ) {
case 'attribute':
return attr( source.selector, source.attribute );
case 'property':
return prop( source.selector, source.property );
case 'html':
return html( source.selector );
case 'text':
return text( source.selector );
case 'children':
return children( source.selector );
case 'node':
return node( source.selector );
case 'query':
return query( source.selector, matcherFromSource( source.source ) );
case 'object':
return keys( source.source ).reduce( ( memo, key ) => {
memo[ key ] = matcherFromSource( source.source[ key ] );
return memo;
}, {} );
default:
// eslint-disable-next-line no-console
console.error( `Unkown source type "${ source.type }"` );
}
}

/**
* Given a blocktype, a block's raw content and the commentAttributes returns the attribute value depending on its source definition
*
* @param {string} attributeKey Attribute key
* @param {Object} attributeSchema Attribute's schema
* @param {string} rawContent Block's raw content
* @param {Object} commentAttributes Block's comment attributes
*
* @return {mixed} Attribute value
*/
export function getBlockAttribute( attributeKey, attributeSchema, rawContent, commentAttributes ) {
let value;
switch ( attributeSchema.source.type ) {
case 'meta':
break;
case 'comment':
value = commentAttributes ? commentAttributes[ attributeKey ] : undefined;
break;
default: {
// Coerce value to specified type
const matcher = matcherFromSource( attributeSchema.source );
const rawValue = hpqParse( rawContent, matcher );
value = rawValue === undefined ? rawValue : asType( rawValue, attributeSchema.type );
break;
}
}

return value === undefined ? attributeSchema.default : value;
}

/**
* Returns the block attributes of a registered block node given its type.
*
Expand All @@ -96,57 +124,11 @@ export function asType( value, type ) {
* @return {Object} All block attributes
*/
export function getBlockAttributes( blockType, rawContent, attributes ) {
// Retrieve additional attributes sourced from content
const sourcedAttributes = getSourcedAttributes(
rawContent,
blockType.attributes
);

const blockAttributes = reduce( blockType.attributes, ( result, source, key ) => {
let value;
if ( sourcedAttributes.hasOwnProperty( key ) ) {
value = sourcedAttributes[ key ];
} else if ( attributes ) {
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 ( 'default' in source ) {
value = source.default;
} else {
return result;
}
}

// Coerce value to specified type
const coercedValue = asType( value, source.type );

if ( 'development' === process.env.NODE_ENV &&
! sourcedAttributes.hasOwnProperty( key ) &&
value !== coercedValue ) {
// Only in case of sourcing attribute from content do we want to
// allow coercion, as comment attributes are serialized respecting
// original data type. In development environments, log if value
// coerced to specified type is not strictly equal. We still allow
// coerced value to be assigned into attributes to avoid errors.
//
// Example:
// Number( 5 ) === 5
// Number( '5' ) !== '5'

// eslint-disable-next-line no-console
console.error(
`Expected attribute "${ key }" of type ${ source.type } for ` +
`block type "${ blockType.name }" but received ${ typeof value }.`
);
}

result[ key ] = coercedValue;
return result;
}, {} );
const blockAttributes = keys( blockType.attributes ).reduce( ( memo, attributeKey ) => {
const attributeSchema = blockType.attributes[ attributeKey ];
memo[ attributeKey ] = getBlockAttribute( attributeKey, attributeSchema, rawContent, attributes );
return memo;
}, {} ) || {};

// If the block supports anchor, parse the id
if ( blockType.supportAnchor ) {
Expand Down
17 changes: 14 additions & 3 deletions blocks/api/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* External dependencies
*/
import { get, isFunction, some } from 'lodash';
import { get, isFunction, some, keys } from 'lodash';

/**
* WordPress dependencies
Expand Down Expand Up @@ -104,10 +104,21 @@ export function registerBlockType( name, settings ) {
);
return;
}

const attributes = settings.attributes
? settings.attributes
: get( window._wpBlocksAttributes, name, );

const block = blocks[ name ] = {
name,
attributes: get( window._wpBlocksAttributes, name ),
...settings,
name,
attributes: keys( attributes ).reduce( ( memo, attributeKey ) => {
memo[ attributeKey ] = {
source: { type: 'comment' },
...attributes[ attributeKey ],
};
return memo;
}, {} ),
};

return block;
Expand Down
2 changes: 1 addition & 1 deletion blocks/api/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export function getCommentAttributes( allAttributes, schema ) {
}

// Ignore values sources from content and post meta
if ( attributeSchema.source || attributeSchema.meta ) {
if ( attributeSchema.source.type !== 'comment' ) {
return result;
}

Expand Down
37 changes: 6 additions & 31 deletions blocks/api/source.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,9 @@ import { createElement } from '@wordpress/element';
* External dependencies
*/
import { nodeListToReact, nodeToReact } from 'dom-react';
import { flow } from 'lodash';
import {
attr as originalAttr,
prop as originalProp,
html as originalHtml,
text as originalText,
query as originalQuery,
} from 'hpq';
export { attr, prop, html, text, query } from 'hpq';

/**
* Given a source function creator, returns a new function which applies an
* internal flag to the created source.
*
* @param {Function} fn Original source function creator
* @return {Function} Modified source function creator
*/
function withKnownSourceFlag( fn ) {
return flow( fn, ( source ) => {
source._wpBlocksKnownSource = true;
return source;
} );
}

export const attr = withKnownSourceFlag( originalAttr );
export const prop = withKnownSourceFlag( originalProp );
export const html = withKnownSourceFlag( originalHtml );
export const text = withKnownSourceFlag( originalText );
export const query = withKnownSourceFlag( originalQuery );
export const children = withKnownSourceFlag( ( selector ) => {
export const children = ( selector ) => {
return ( domNode ) => {
let match = domNode;

Expand All @@ -49,8 +23,9 @@ export const children = withKnownSourceFlag( ( selector ) => {

return [];
};
} );
export const node = withKnownSourceFlag( ( selector ) => {
};

export const node = ( selector ) => {
return ( domNode ) => {
let match = domNode;

Expand All @@ -60,4 +35,4 @@ export const node = withKnownSourceFlag( ( selector ) => {

return nodeToReact( match, createElement );
};
} );
};
10 changes: 6 additions & 4 deletions blocks/library/audio/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ import { Component } from '@wordpress/element';
* Internal dependencies
*/
import './style.scss';
import { registerBlockType, source } from '../../api';
import { registerBlockType } from '../../api';
import MediaUploadButton from '../../media-upload-button';
import BlockControls from '../../block-controls';
import BlockAlignmentToolbar from '../../block-alignment-toolbar';
import InspectorControls from '../../inspector-controls';
import BlockDescription from '../../block-description';

const { attr } = source;

registerBlockType( 'core/audio', {
title: __( 'Audio' ),

Expand All @@ -32,7 +30,11 @@ registerBlockType( 'core/audio', {
attributes: {
src: {
type: 'string',
source: attr( 'audio', 'src' ),
source: {
type: 'atttribute',
selector: 'audio',
attribute: 'src',
},
},
align: {
type: 'string',
Expand Down
21 changes: 15 additions & 6 deletions blocks/library/button/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IconButton } from '@wordpress/components';
*/
import './editor.scss';
import './style.scss';
import { registerBlockType, source } from '../../api';
import { registerBlockType } from '../../api';
import Editable from '../../editable';
import UrlInput from '../../url-input';
import BlockControls from '../../block-controls';
Expand All @@ -18,8 +18,6 @@ import ColorPalette from '../../color-palette';
import InspectorControls from '../../inspector-controls';
import BlockDescription from '../../block-description';

const { attr, children } = source;

registerBlockType( 'core/button', {
title: __( 'Button' ),

Expand All @@ -30,15 +28,26 @@ registerBlockType( 'core/button', {
attributes: {
url: {
type: 'string',
source: attr( 'a', 'href' ),
source: {
type: 'attribute',
selector: 'a',
attribute: 'href',
},
},
title: {
type: 'string',
source: attr( 'a', 'title' ),
source: {
type: 'attribute',
selector: 'a',
attribute: 'title',
},
},
text: {
type: 'array',
source: children( 'a' ),
source: {
type: 'children',
selector: 'a',
},
},
align: {
type: 'string',
Expand Down
Loading

0 comments on commit 40d3d40

Please sign in to comment.