From 2368bdbcd79fe6a18794f3e5e21339b8e3e818e5 Mon Sep 17 00:00:00 2001
From: Andrew Duthie Content Content This is content. Can get out of control. This is content. Can get out of control. This is a paragraph.
- Some preformatted text...
+
And more!Some preformatted text...
diff --git a/core-blocks/test/fixtures/core__verse.serialized.html b/core-blocks/test/fixtures/core__verse.serialized.html
index ff4983491f13d..8fe10de33da6e 100644
--- a/core-blocks/test/fixtures/core__verse.serialized.html
+++ b/core-blocks/test/fixtures/core__verse.serialized.html
@@ -1,3 +1,3 @@
-
And more!A verse…
+
And more!A verse…
diff --git a/packages/element/src/serialize.js b/packages/element/src/serialize.js
index 6ef93817d8c93..ac278292aa150 100644
--- a/packages/element/src/serialize.js
+++ b/packages/element/src/serialize.js
@@ -28,13 +28,27 @@
/**
* External dependencies
*/
-import { isEmpty, castArray, omit, kebabCase } from 'lodash';
+import { isEmpty, castArray, omit, repeat, kebabCase } from 'lodash';
/**
* Internal dependencies
*/
import { Fragment, RawHTML } from './';
+/**
+ * Default options considered by `renderToString`.
+ *
+ * @property {Object} context Component context.
+ * @property {boolean} beautify Whether output should include indented newlines
+ * on non-inline element types.
+ *
+ * @type {Object}
+ */
+const DEFAULT_OPTIONS = {
+ context: {},
+ beautify: false,
+};
+
/**
* Valid attribute types.
*
@@ -70,6 +84,55 @@ const SELF_CLOSING_TAGS = new Set( [
'wbr',
] );
+/**
+ * Inline element tags.
+ *
+ * Extracted from:
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
+ *
+ * Regenerate by:
+ *
+ * [ ...document.querySelectorAll( '.threecolumns code' ) ]
+ * .map( ( el ) => el.textContent.replace( /(^<|>$)/g, '' ) )
+ *
+ * @type {Set}
+ */
+const INLINE_TAGS = new Set( [
+ 'a',
+ 'abbr',
+ 'acronym',
+ 'b',
+ 'bdo',
+ 'big',
+ 'br',
+ 'button',
+ 'cite',
+ 'code',
+ 'dfn',
+ 'em',
+ 'i',
+ 'img',
+ 'input',
+ 'kbd',
+ 'label',
+ 'map',
+ 'object',
+ 'q',
+ 'samp',
+ 'script',
+ 'select',
+ 'small',
+ 'span',
+ 'strong',
+ 'sub',
+ 'sup',
+ 'textarea',
+ 'time',
+ 'tt',
+ 'var',
+] );
+
/**
* Boolean attributes are attributes whose presence as being assigned is
* meaningful, even if only empty.
@@ -332,20 +395,34 @@ function getNormalStyleValue( property, value ) {
}
/**
- * Serializes a React element to string.
+ * Returns an indentation at the given level, if greater than zero. Different
+ * from default repeat implementation in that an undefined level is treated as
+ * though it were passed as zero.
*
- * @param {WPElement} element Element to serialize.
- * @param {?Object} context Context object.
+ * @param {?number} level Level to indent. Default zero.
+ *
+ * @return {string} Indentation.
+ */
+function indent( level = 0 ) {
+ return repeat( '\t', level );
+}
+
+/**
+ * Serializes an element to string.
+ *
+ * @param {WPElement} element Element to serialize.
+ * @param {?Object} context Context object.
+ * @param {?number} indentLevel In recursion, level at which to indent.
*
* @return {string} Serialized element.
*/
-export function renderElement( element, context = {} ) {
+export function renderElement( element, context, indentLevel ) {
if ( null === element || undefined === element || false === element ) {
return '';
}
if ( Array.isArray( element ) ) {
- return renderChildren( element, context );
+ return renderChildren( element, context, indentLevel );
}
switch ( typeof element ) {
@@ -360,7 +437,7 @@ export function renderElement( element, context = {} ) {
switch ( tagName ) {
case Fragment:
- return renderChildren( props.children, context );
+ return renderChildren( props.children, context, indentLevel );
case RawHTML:
const { children, ...wrapperProps } = props;
@@ -371,20 +448,21 @@ export function renderElement( element, context = {} ) {
...wrapperProps,
dangerouslySetInnerHTML: { __html: children },
},
- context
+ context,
+ indentLevel
);
}
switch ( typeof tagName ) {
case 'string':
- return renderNativeComponent( tagName, props, context );
+ return renderNativeComponent( tagName, props, context, indentLevel );
case 'function':
if ( tagName.prototype && typeof tagName.prototype.render === 'function' ) {
- return renderComponent( tagName, props, context );
+ return renderComponent( tagName, props, context, indentLevel );
}
- return renderElement( tagName( props, context ), context );
+ return renderElement( tagName( props, context ), context, indentLevel );
}
return '';
@@ -393,52 +471,83 @@ export function renderElement( element, context = {} ) {
/**
* Serializes a native component type to string.
*
- * @param {?string} type Native component type to serialize, or null if
- * rendering as fragment of children content.
- * @param {Object} props Props object.
- * @param {?Object} context Context object.
+ * @param {string} type Native component type to serialize.
+ * @param {Object} props Props object.
+ * @param {?Object} context Context object.
+ * @param {?number} indentLevel In recursion, level at which to indent.
*
* @return {string} Serialized element.
*/
-export function renderNativeComponent( type, props, context = {} ) {
- let content = '';
+export function renderNativeComponent( type, props, context, indentLevel ) {
+ let childrenContent = '';
+
+ let childrenIndentLevel = indentLevel;
+ if ( ! isInlineTag ) {
+ childrenIndentLevel++;
+ }
+
if ( type === 'textarea' && props.hasOwnProperty( 'value' ) ) {
// Textarea children can be assigned as value prop. If it is, render in
// place of children. Ensure to omit so it is not assigned as attribute
// as well.
- content = renderChildren( props.value, context );
+ childrenContent = renderChildren( props.value, context, childrenIndentLevel );
props = omit( props, 'value' );
} else if ( props.dangerouslySetInnerHTML &&
typeof props.dangerouslySetInnerHTML.__html === 'string' ) {
// Dangerous content is left unescaped.
- content = props.dangerouslySetInnerHTML.__html;
+ childrenContent = props.dangerouslySetInnerHTML.__html;
} else if ( typeof props.children !== 'undefined' ) {
- content = renderChildren( props.children, context );
+ childrenContent = renderChildren( props.children, context, childrenIndentLevel );
}
if ( ! type ) {
- return content;
+ return childrenContent;
+ }
+
+ let content = '';
+
+ // Place non-inline tag on own line with indentation.
+ const isInlineTag = INLINE_TAGS.has( type );
+ if ( ! isInlineTag && indentLevel > 0 ) {
+ content += '\n' + indent( indentLevel );
}
const attributes = renderAttributes( props );
- if ( SELF_CLOSING_TAGS.has( type ) ) {
- return '<' + type + attributes + '/>';
+ if ( type ) {
+ content += '<' + type + attributes;
+
+ if ( SELF_CLOSING_TAGS.has( type ) ) {
+ return content + ' />';
+ }
+
+ content += '>';
}
- return '<' + type + attributes + '>' + content + '' + type + '>';
+ content += childrenContent;
+
+ // For closing tag, if non-inline element rendered its own children,
+ // closing tag should be placed on its own line.
+ if ( ! isInlineTag && type !== 'pre' && /\n\t./.test( childrenContent ) ) {
+ content += '\n' + indent( indentLevel );
+ }
+
+ content += '' + type + '>';
+
+ return content;
}
/**
* Serializes a non-native component type to string.
*
- * @param {Function} Component Component type to serialize.
- * @param {Object} props Props object.
- * @param {?Object} context Context object.
+ * @param {Function} Component Component type to serialize.
+ * @param {Object} props Props object.
+ * @param {?Object} context Context object.
+ * @param {?number} indentLevel In recursion, level at which to indent.
*
* @return {string} Serialized element
*/
-export function renderComponent( Component, props, context = {} ) {
+export function renderComponent( Component, props, context, indentLevel ) {
const instance = new Component( props, context );
if ( typeof instance.componentWillMount === 'function' ) {
@@ -449,7 +558,7 @@ export function renderComponent( Component, props, context = {} ) {
Object.assign( context, instance.getChildContext() );
}
- const html = renderElement( instance.render(), context );
+ const html = renderElement( instance.render(), context, indentLevel );
return html;
}
@@ -457,12 +566,13 @@ export function renderComponent( Component, props, context = {} ) {
/**
* Serializes an array of children to string.
*
- * @param {Array} children Children to serialize.
- * @param {?Object} context Context object.
+ * @param {Array} children Children to serialize.
+ * @param {?Object} context Context object.
+ * @param {?number} indentLevel In recursion, level at which to indent.
*
* @return {string} Serialized children.
*/
-function renderChildren( children, context = {} ) {
+function renderChildren( children, context, indentLevel ) {
let result = '';
children = castArray( children );
@@ -470,7 +580,13 @@ function renderChildren( children, context = {} ) {
for ( let i = 0; i < children.length; i++ ) {
const child = children[ i ];
- result += renderElement( child, context );
+ result += renderElement( child, context, indentLevel );
+
+ // If rendering children from top-level (e.g. fragment), avoid leading
+ // newline for first non-inline tag.
+ if ( i === 0 && indentLevel === 0 ) {
+ result = result.replace( /^\n/, '' );
+ }
}
return result;
@@ -564,4 +680,28 @@ export function renderStyle( style ) {
return result;
}
-export default renderElement;
+/**
+ * Serializes an element to string, given options.
+ *
+ * @param {WPElement} element Element to serialize.
+ * @param {Object} options Serialization options.
+ *
+ * @return {string} Serialized element.
+ */
+export function renderToString( element, options ) {
+ options = {
+ ...DEFAULT_OPTIONS,
+ ...options,
+ };
+
+ const { context, beautify } = options;
+
+ let indentLevel;
+ if ( beautify ) {
+ indentLevel = 0;
+ }
+
+ return renderElement( element, context, indentLevel );
+}
+
+export default renderToString;
diff --git a/packages/element/src/test/serialize.js b/packages/element/src/test/serialize.js
index 134371161ec57..74805f9e68d39 100644
--- a/packages/element/src/test/serialize.js
+++ b/packages/element/src/test/serialize.js
@@ -170,6 +170,50 @@ describe( 'renderElement()', () => {
expect( result ).toBe( '
And more!Hello World!
+ On previous line
+
+ { 'foo\nbar\t' }
Hello World!
On previous line\n' +
+ '\t
\n' +
+ 'foo\n' +
+ 'bar
This is a link.
-
This is a link.
+
This is a title
This is a title
This is a paragraph with a link.
This is a paragraph with a link.
An image:
An image:
Preserve
line breaks please.
Preserve
line breaks please.
test
test
test
test
test
test
Column One, Paragraph One
- +Column One, Paragraph One
+ - -Column One, Paragraph Two
- + +Column One, Paragraph Two
+ - -Column Two, Paragraph One
- + +Column Two, Paragraph One
+ - -Column Three, Paragraph One
- -Column Three, Paragraph One
+-+Testing pullquote block...
...with a caption
Testing pullquote block...
...with a caption + diff --git a/core-blocks/test/fixtures/core__pullquote__multi-paragraph.serialized.html b/core-blocks/test/fixtures/core__pullquote__multi-paragraph.serialized.html index 58d4022398773..8e1d8cdb83259 100644 --- a/core-blocks/test/fixtures/core__pullquote__multi-paragraph.serialized.html +++ b/core-blocks/test/fixtures/core__pullquote__multi-paragraph.serialized.html @@ -1,5 +1,6 @@+Paragraph one
-Paragraph two
by whomever
Paragraph two
by whomever + diff --git a/core-blocks/test/fixtures/core__quote__style-1.serialized.html b/core-blocks/test/fixtures/core__quote__style-1.serialized.html index a56c5859bd335..81c99a70d33b6 100644 --- a/core-blocks/test/fixtures/core__quote__style-1.serialized.html +++ b/core-blocks/test/fixtures/core__quote__style-1.serialized.html @@ -1,4 +1,5 @@-+The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery.
Matt Mullenweg, 2017
The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery.
Matt Mullenweg, 2017 + diff --git a/core-blocks/test/fixtures/core__quote__style-2.serialized.html b/core-blocks/test/fixtures/core__quote__style-2.serialized.html index e715726fb9cc6..bb5d03efd5c2b 100644 --- a/core-blocks/test/fixtures/core__quote__style-2.serialized.html +++ b/core-blocks/test/fixtures/core__quote__style-2.serialized.html @@ -1,4 +1,5 @@-+There is no greater agony than bearing an untold story inside you.
Maya Angelou
There is no greater agony than bearing an untold story inside you.
Maya Angelou + diff --git a/core-blocks/test/fixtures/core__text__converts-to-paragraph.serialized.html b/core-blocks/test/fixtures/core__text__converts-to-paragraph.serialized.html index 7a11c004984d1..61fd699dace63 100644 --- a/core-blocks/test/fixtures/core__text__converts-to-paragraph.serialized.html +++ b/core-blocks/test/fixtures/core__text__converts-to-paragraph.serialized.html @@ -1,3 +1,3 @@ -This is an old-style text block. Changed to paragraph
in #2135.
This is an old-style text block. Changed to paragraph
in #2135.
Invalid - -
- - +Invalid +
+ " `; diff --git a/editor/components/inner-blocks/test/index.js b/editor/components/inner-blocks/test/index.js index 1472b8817b9cd..2af394e48ec1e 100644 --- a/editor/components/inner-blocks/test/index.js +++ b/editor/components/inner-blocks/test/index.js @@ -9,6 +9,7 @@ import { getSaveElement, registerBlockType, serialize, + parse, unregisterBlockType, } from '@wordpress/blocks'; import { renderToString } from '@wordpress/element'; @@ -106,6 +107,10 @@ describe( 'InnerBlocks', () => { block.isValid = false; block.originalContent = 'Original'; - expect( serialize( block ) ).toMatchSnapshot(); + const serialized = serialize( block ); + expect( serialized ).toMatchSnapshot(); + // Ensure beautification doesn't impact (trailing, leading whitespace) + // re-parsed content: + expect( parse( serialized )[ 0 ].attributes.content ).toEqual( block.attributes.content ); } ); } ); diff --git a/package-lock.json b/package-lock.json index 634d743e6d2b9..93f2d5c3bed30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -616,7 +616,8 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true }, "acorn": { "version": "5.5.3", @@ -2145,7 +2146,8 @@ "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true }, "bn.js": { "version": "4.11.8", @@ -3040,7 +3042,8 @@ "commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true }, "comment-parser": { "version": "0.4.2", @@ -3188,15 +3191,6 @@ } } }, - "config-chain": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", - "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, "console-browserify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", @@ -4392,18 +4386,6 @@ "integrity": "sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==", "dev": true }, - "editorconfig": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.13.3.tgz", - "integrity": "sha512-WkjsUNVCu+ITKDj73QDvi0trvpdDWdkDyHybDGSXPfekLCqwmpD7CP7iPbvBgosNuLcI96XTDwNa75JyFl7tEQ==", - "requires": { - "bluebird": "^3.0.5", - "commander": "^2.9.0", - "lru-cache": "^3.2.0", - "semver": "^5.1.0", - "sigmund": "^1.0.1" - } - }, "ejs": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", @@ -8418,17 +8400,6 @@ "integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ==", "dev": true }, - "js-beautify": { - "version": "1.6.14", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.6.14.tgz", - "integrity": "sha1-07j3Mi0CuSd9WL0jgmTDJ+WARM0=", - "requires": { - "config-chain": "~1.1.5", - "editorconfig": "^0.13.2", - "mkdirp": "~0.5.0", - "nopt": "~3.0.1" - } - }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -9357,14 +9328,6 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" }, - "lru-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-3.2.0.tgz", - "integrity": "sha1-cXibO39Tmb7IVl3aOKow0qCX7+4=", - "requires": { - "pseudomap": "^1.0.1" - } - }, "make-dir": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", @@ -10151,6 +10114,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, "requires": { "abbrev": "1" } @@ -12916,11 +12880,6 @@ "loose-envify": "^1.3.1" } }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" - }, "proxy-from-env": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", @@ -14359,11 +14318,6 @@ } } }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" - }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", diff --git a/package.json b/package.json index bf3fddda7da00..5f5d77549af99 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "eslint-plugin-wordpress": "git://github.com/WordPress-Coding-Standards/eslint-plugin-wordpress.git#1774343f6226052a46b081e01db3fca8793cc9f1", "hpq": "1.2.0", "jquery": "3.2.1", - "js-beautify": "1.6.14", "lerna": "2.11.0", "lodash": "4.17.5", "memize": "1.0.5", diff --git a/test/integration/fixtures/apple-out.html b/test/integration/fixtures/apple-out.html index 84bdfad50c7e1..f85a469f61198 100644 --- a/test/integration/fixtures/apple-out.html +++ b/test/integration/fixtures/apple-out.html @@ -35,36 +35,36 @@Fourth paragraph
+Fourth paragraph
diff --git a/test/integration/fixtures/evernote-out.html b/test/integration/fixtures/evernote-out.html index ad356841db682..e06e1c471eb85 100644 --- a/test/integration/fixtures/evernote-out.html +++ b/test/integration/fixtures/evernote-out.html @@ -1,7 +1,7 @@This is a paragraph.
-
This is a link.
-
Preserve
line breaks please.
Preserve
+line breaks please.
This is a title -
+This is a +title
-This is a subtitle -
+This is a +subtitle
@@ -45,36 +45,36 @@