-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathparser.js
386 lines (334 loc) · 10.9 KB
/
parser.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
/**
* External dependencies
*/
import { parse as hpqParse } from 'hpq';
import { flow, castArray, mapValues, omit, stubFalse } from 'lodash';
/**
* WordPress dependencies
*/
import { autop } from '@wordpress/autop';
import { applyFilters } from '@wordpress/hooks';
import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
import { parse as grammarParse } from './post-parser';
import { getBlockType, getUnknownTypeHandlerName } from './registration';
import { createBlock } from './factory';
import { isValidBlock } from './validation';
import { getCommentDelimitedContent } from './serializer';
import { attr, prop, html, text, query, node, children } from './matchers';
/**
* Higher-order hpq matcher which enhances an attribute matcher to return true
* or false depending on whether the original matcher returns undefined. This
* is useful for boolean attributes (e.g. disabled) whose attribute values may
* be technically falsey (empty string), though their mere presence should be
* enough to infer as true.
*
* @param {Function} matcher Original hpq matcher.
*
* @return {Function} Enhanced hpq matcher.
*/
export const toBooleanAttributeMatcher = ( matcher ) => flow( [
matcher,
// Expected values from `attr( 'disabled' )`:
//
// <input>
// - Value: `undefined`
// - Transformed: `false`
//
// <input disabled>
// - Value: `''`
// - Transformed: `true`
//
// <input disabled="disabled">
// - Value: `'disabled'`
// - Transformed: `true`
( value ) => value !== undefined,
] );
/**
* Returns value coerced to the specified JSON schema type string.
*
* @see http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.25
*
* @param {*} value Original value.
* @param {string} type Type to coerce.
*
* @return {*} Coerced value.
*/
export function asType( value, type ) {
switch ( type ) {
case 'string':
return String( value );
case 'boolean':
return Boolean( value );
case 'object':
return Object( value );
case 'null':
return null;
case 'array':
if ( Array.isArray( value ) ) {
return value;
}
return Array.from( value );
case 'integer':
case 'number':
return Number( value );
}
return value;
}
/**
* Returns an hpq matcher given a source object.
*
* @param {Object} sourceConfig Attribute Source object.
*
* @return {Function} A hpq Matcher.
*/
export function matcherFromSource( sourceConfig ) {
switch ( sourceConfig.source ) {
case 'attribute':
let matcher = attr( sourceConfig.selector, sourceConfig.attribute );
if ( sourceConfig.type === 'boolean' ) {
matcher = toBooleanAttributeMatcher( matcher );
}
return matcher;
case 'property':
deprecated( '`property` source', {
version: '3.4',
alternative: 'equivalent `text`, `html`, or `attribute` source, or comment attribute',
plugin: 'Gutenberg',
} );
return prop( sourceConfig.selector, sourceConfig.property );
case 'html':
return html( sourceConfig.selector );
case 'text':
return text( sourceConfig.selector );
case 'children':
return children( sourceConfig.selector );
case 'node':
return node( sourceConfig.selector );
case 'query':
const subMatchers = mapValues( sourceConfig.query, matcherFromSource );
return query( sourceConfig.selector, subMatchers );
default:
// eslint-disable-next-line no-console
console.error( `Unknown source type "${ sourceConfig.source }"` );
}
}
/**
* Given a block's raw content and an attribute's schema returns the attribute's
* value depending on its source.
*
* @param {string} innerHTML Block's raw content.
* @param {Object} attributeSchema Attribute's schema.
*
* @return {*} Attribute value.
*/
export function parseWithAttributeSchema( innerHTML, attributeSchema ) {
return hpqParse( innerHTML, matcherFromSource( attributeSchema ) );
}
/**
* Given an attribute key, an attribute's schema, a block's raw content and the
* commentAttributes returns the attribute value depending on its source
* definition of the given attribute key.
*
* @param {string} attributeKey Attribute key.
* @param {Object} attributeSchema Attribute's schema.
* @param {string} innerHTML Block's raw content.
* @param {Object} commentAttributes Block's comment attributes.
*
* @return {*} Attribute value.
*/
export function getBlockAttribute( attributeKey, attributeSchema, innerHTML, commentAttributes ) {
let value;
switch ( attributeSchema.source ) {
// undefined source means that it's an attribute serialized to the block's "comment"
case undefined:
value = commentAttributes ? commentAttributes[ attributeKey ] : undefined;
break;
case 'attribute':
case 'property':
case 'html':
case 'text':
case 'children':
case 'node':
case 'query':
value = parseWithAttributeSchema( innerHTML, attributeSchema );
break;
}
return value === undefined ? attributeSchema.default : asType( value, attributeSchema.type );
}
/**
* Returns the block attributes of a registered block node given its type.
*
* @param {?Object} blockType Block type.
* @param {string} innerHTML Raw block content.
* @param {?Object} attributes Known block attributes (from delimiters).
*
* @return {Object} All block attributes.
*/
export function getBlockAttributes( blockType, innerHTML, attributes ) {
const blockAttributes = mapValues( blockType.attributes, ( attributeSchema, attributeKey ) => {
return getBlockAttribute( attributeKey, attributeSchema, innerHTML, attributes );
} );
return applyFilters(
'blocks.getBlockAttributes',
blockAttributes,
blockType,
innerHTML,
attributes
);
}
/**
* Given a block object, returns a new copy of the block with any applicable
* deprecated migrations applied, or the original block if it was both valid
* and no eligible migrations exist.
*
* @param {WPBlock} block Original block object.
*
* @return {WPBlock} Migrated block object.
*/
export function getMigratedBlock( block ) {
const blockType = getBlockType( block.name );
const { deprecated: deprecatedDefinitions } = blockType;
if ( ! deprecatedDefinitions || ! deprecatedDefinitions.length ) {
return block;
}
const { originalContent, attributes, innerBlocks } = block;
for ( let i = 0; i < deprecatedDefinitions.length; i++ ) {
// A block can opt into a migration even if the block is valid by
// defining isEligible on its deprecation. If the block is both valid
// and does not opt to migrate, skip.
const { isEligible = stubFalse } = deprecatedDefinitions[ i ];
if ( block.isValid && ! isEligible( attributes, innerBlocks ) ) {
continue;
}
// Block type properties which could impact either serialization or
// parsing are not considered in the deprecated block type by default,
// and must be explicitly provided.
const deprecatedBlockType = Object.assign(
omit( blockType, [ 'attributes', 'save', 'supports' ] ),
deprecatedDefinitions[ i ]
);
let migratedAttributes = getBlockAttributes(
deprecatedBlockType,
originalContent,
attributes
);
// Ignore the deprecation if it produces a block which is not valid.
const isValid = isValidBlock(
originalContent,
deprecatedBlockType,
migratedAttributes
);
if ( ! isValid ) {
continue;
}
block = {
...block,
isValid: true,
};
let migratedInnerBlocks = innerBlocks;
// A block may provide custom behavior to assign new attributes and/or
// inner blocks.
const { migrate } = deprecatedBlockType;
if ( migrate ) {
( [
migratedAttributes = attributes,
migratedInnerBlocks = innerBlocks,
] = castArray( migrate( migratedAttributes, innerBlocks ) ) );
}
block.attributes = migratedAttributes;
block.innerBlocks = migratedInnerBlocks;
}
return block;
}
/**
* Creates a block with fallback to the unknown type handler.
*
* @param {Object} blockNode Parsed block node.
*
* @return {?Object} An initialized block object (if possible).
*/
export function createBlockWithFallback( blockNode ) {
let {
blockName: name,
attrs: attributes,
innerBlocks = [],
innerHTML,
} = blockNode;
attributes = attributes || {};
// Trim content to avoid creation of intermediary freeform segments.
innerHTML = innerHTML.trim();
// Use type from block content, otherwise find unknown handler.
name = name || getUnknownTypeHandlerName();
// Convert 'core/text' blocks in existing content to 'core/paragraph'.
if ( 'core/text' === name || 'core/cover-text' === name ) {
name = 'core/paragraph';
}
// Try finding the type for known block name, else fall back again.
let blockType = getBlockType( name );
const fallbackBlock = getUnknownTypeHandlerName();
// Fallback content may be upgraded from classic editor expecting implicit
// automatic paragraphs, so preserve them. Assumes wpautop is idempotent,
// meaning there are no negative consequences to repeated autop calls.
if ( name === fallbackBlock ) {
innerHTML = autop( innerHTML ).trim();
}
if ( ! blockType ) {
// If detected as a block which is not registered, preserve comment
// delimiters in content of unknown type handler.
if ( name ) {
innerHTML = getCommentDelimitedContent( name, attributes, innerHTML );
}
name = fallbackBlock;
blockType = getBlockType( name );
}
// Coerce inner blocks from parsed form to canonical form.
innerBlocks = innerBlocks.map( createBlockWithFallback );
// Include in set only if type were determined.
if ( ! blockType || ( ! innerHTML && name === fallbackBlock ) ) {
return;
}
let block = createBlock(
name,
getBlockAttributes( blockType, innerHTML, attributes ),
innerBlocks
);
// Block validation assumes an idempotent operation from source block to serialized block
// provided there are no changes in attributes. The validation procedure thus compares the
// provided source value with the serialized output before there are any modifications to
// the block. When both match, the block is marked as valid.
if ( name !== fallbackBlock ) {
block.isValid = isValidBlock( innerHTML, blockType, block.attributes );
}
// Preserve original content for future use in case the block is parsed as
// invalid, or future serialization attempt results in an error.
block.originalContent = innerHTML;
block = getMigratedBlock( block );
return block;
}
/**
* Creates a parse implementation for the post content which returns a list of blocks.
*
* @param {Function} parseImplementation Parse implementation.
*
* @return {Function} An implementation which parses the post content.
*/
export const createParse = ( parseImplementation ) =>
( content ) => parseImplementation( content ).reduce( ( memo, blockNode ) => {
const block = createBlockWithFallback( blockNode );
if ( block ) {
memo.push( block );
}
return memo;
}, [] );
/**
* Parses the post content with a PegJS grammar and returns a list of blocks.
*
* @param {string} content The post content.
*
* @return {Array} Block list.
*/
export const parseWithGrammar = createParse( grammarParse );
export default parseWithGrammar;