-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathget-block-attributes.js
297 lines (272 loc) · 7.68 KB
/
get-block-attributes.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
/**
* External dependencies
*/
import { parse as hpqParse } from 'hpq';
import memoize from 'memize';
/**
* WordPress dependencies
*/
import { pipe } from '@wordpress/compose';
import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies
*/
import { attr, html, text, query, node, children, prop } from '../matchers';
import { normalizeBlockType } from '../utils';
/**
* 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 ) =>
pipe( [
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 true if value is of the given JSON schema type, or false otherwise.
*
* @see http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.25
*
* @param {*} value Value to test.
* @param {string} type Type to test.
*
* @return {boolean} Whether value is of type.
*/
export function isOfType( value, type ) {
switch ( type ) {
case 'string':
return typeof value === 'string';
case 'boolean':
return typeof value === 'boolean';
case 'object':
return !! value && value.constructor === Object;
case 'null':
return value === null;
case 'array':
return Array.isArray( value );
case 'integer':
case 'number':
return typeof value === 'number';
}
return true;
}
/**
* Returns true if value is of an array of given JSON schema types, or false
* otherwise.
*
* @see http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.25
*
* @param {*} value Value to test.
* @param {string[]} types Types to test.
*
* @return {boolean} Whether value is of types.
*/
export function isOfTypes( value, types ) {
return types.some( ( type ) => isOfType( value, type ) );
}
/**
* 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 {Node} innerDOM Parsed DOM of block's inner HTML.
* @param {Object} commentAttributes Block's comment attributes.
* @param {string} innerHTML Raw HTML from block node's innerHTML property.
*
* @return {*} Attribute value.
*/
export function getBlockAttribute(
attributeKey,
attributeSchema,
innerDOM,
commentAttributes,
innerHTML
) {
let value;
switch ( attributeSchema.source ) {
// An undefined source means that it's an attribute serialized to the
// block's "comment".
case undefined:
value = commentAttributes
? commentAttributes[ attributeKey ]
: undefined;
break;
// raw source means that it's the original raw block content.
case 'raw':
value = innerHTML;
break;
case 'attribute':
case 'property':
case 'html':
case 'text':
case 'children':
case 'node':
case 'query':
case 'tag':
value = parseWithAttributeSchema( innerDOM, attributeSchema );
break;
}
if (
! isValidByType( value, attributeSchema.type ) ||
! isValidByEnum( value, attributeSchema.enum )
) {
// Reject the value if it is not valid. Reverting to the undefined
// value ensures the default is respected, if applicable.
value = undefined;
}
if ( value === undefined ) {
value = attributeSchema.default;
}
return value;
}
/**
* Returns true if value is valid per the given block attribute schema type
* definition, or false otherwise.
*
* @see https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.1
*
* @param {*} value Value to test.
* @param {?(Array<string>|string)} type Block attribute schema type.
*
* @return {boolean} Whether value is valid.
*/
export function isValidByType( value, type ) {
return (
type === undefined ||
isOfTypes( value, Array.isArray( type ) ? type : [ type ] )
);
}
/**
* Returns true if value is valid per the given block attribute schema enum
* definition, or false otherwise.
*
* @see https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.2
*
* @param {*} value Value to test.
* @param {?Array} enumSet Block attribute schema enum.
*
* @return {boolean} Whether value is valid.
*/
export function isValidByEnum( value, enumSet ) {
return ! Array.isArray( enumSet ) || enumSet.includes( value );
}
/**
* Returns an hpq matcher given a source object.
*
* @param {Object} sourceConfig Attribute Source object.
*
* @return {Function} A hpq Matcher.
*/
export const matcherFromSource = memoize( ( sourceConfig ) => {
switch ( sourceConfig.source ) {
case 'attribute':
let matcher = attr( sourceConfig.selector, sourceConfig.attribute );
if ( sourceConfig.type === 'boolean' ) {
matcher = toBooleanAttributeMatcher( matcher );
}
return matcher;
case 'html':
return html( sourceConfig.selector, sourceConfig.multiline );
case 'text':
return text( sourceConfig.selector );
case 'children':
return children( sourceConfig.selector );
case 'node':
return node( sourceConfig.selector );
case 'query':
const subMatchers = Object.fromEntries(
Object.entries( sourceConfig.query ).map(
( [ key, subSourceConfig ] ) => [
key,
matcherFromSource( subSourceConfig ),
]
)
);
return query( sourceConfig.selector, subMatchers );
case 'tag':
return pipe( [
prop( sourceConfig.selector, 'nodeName' ),
( nodeName ) =>
nodeName ? nodeName.toLowerCase() : undefined,
] );
default:
// eslint-disable-next-line no-console
console.error( `Unknown source type "${ sourceConfig.source }"` );
}
} );
/**
* Parse a HTML string into DOM tree.
*
* @param {string|Node} innerHTML HTML string or already parsed DOM node.
*
* @return {Node} Parsed DOM node.
*/
function parseHtml( innerHTML ) {
return hpqParse( innerHTML, ( h ) => h );
}
/**
* Given a block's raw content and an attribute's schema returns the attribute's
* value depending on its source.
*
* @param {string|Node} innerHTML Block's raw content.
* @param {Object} attributeSchema Attribute's schema.
*
* @return {*} Attribute value.
*/
export function parseWithAttributeSchema( innerHTML, attributeSchema ) {
return matcherFromSource( attributeSchema )( parseHtml( innerHTML ) );
}
/**
* Returns the block attributes of a registered block node given its type.
*
* @param {string|Object} blockTypeOrName Block type or name.
* @param {string|Node} innerHTML Raw block content.
* @param {?Object} attributes Known block attributes (from delimiters).
*
* @return {Object} All block attributes.
*/
export function getBlockAttributes(
blockTypeOrName,
innerHTML,
attributes = {}
) {
const doc = parseHtml( innerHTML );
const blockType = normalizeBlockType( blockTypeOrName );
const blockAttributes = Object.fromEntries(
Object.entries( blockType.attributes ?? {} ).map(
( [ key, schema ] ) => [
key,
getBlockAttribute( key, schema, doc, attributes, innerHTML ),
]
)
);
return applyFilters(
'blocks.getBlockAttributes',
blockAttributes,
blockType,
innerHTML,
attributes
);
}