-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathindex.js
314 lines (284 loc) · 10.8 KB
/
index.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
/**
* WordPress dependencies
*/
import { parse as grammarParse } from '@wordpress/block-serialization-default-parser';
import { autop } from '@wordpress/autop';
/**
* Internal dependencies
*/
import {
getFreeformContentHandlerName,
getUnregisteredTypeHandlerName,
getBlockType,
} from '../registration';
import { getSaveContent } from '../serializer';
import { validateBlock } from '../validation';
import { createBlock } from '../factory';
import { convertLegacyBlockNameAndAttributes } from './convert-legacy-block';
import { serializeRawBlock } from './serialize-raw-block';
import { getBlockAttributes } from './get-block-attributes';
import { applyBlockDeprecatedVersions } from './apply-block-deprecated-versions';
import { applyBuiltInValidationFixes } from './apply-built-in-validation-fixes';
/**
* The raw structure of a block includes its attributes, inner
* blocks, and inner HTML. It is important to distinguish inner blocks from
* the HTML content of the block as only the latter is relevant for block
* validation and edit operations.
*
* @typedef WPRawBlock
*
* @property {string=} blockName Block name
* @property {Object=} attrs Block raw or comment attributes.
* @property {string} innerHTML HTML content of the block.
* @property {(string|null)[]} innerContent Content without inner blocks.
* @property {WPRawBlock[]} innerBlocks Inner Blocks.
*/
/**
* Fully parsed block object.
*
* @typedef WPBlock
*
* @property {string} name Block name
* @property {Object} attributes Block raw or comment attributes.
* @property {WPBlock[]} innerBlocks Inner Blocks.
* @property {string} originalContent Original content of the block before validation fixes.
* @property {boolean} isValid Whether the block is valid.
* @property {Object[]} validationIssues Validation issues.
* @property {WPRawBlock} [__unstableBlockSource] Un-processed original copy of block if created through parser.
*/
/**
* @typedef {Object} ParseOptions
* @property {boolean} __unstableSkipMigrationLogs If a block is migrated from a deprecated version, skip logging the migration details.
*/
/**
* Convert legacy blocks to their canonical form. This function is used
* both in the parser level for previous content and to convert such blocks
* used in Custom Post Types templates.
*
* @param {WPRawBlock} rawBlock
*
* @return {WPRawBlock} The block's name and attributes, changed accordingly if a match was found
*/
function convertLegacyBlocks( rawBlock ) {
const [
correctName,
correctedAttributes,
] = convertLegacyBlockNameAndAttributes(
rawBlock.blockName,
rawBlock.attrs
);
return {
...rawBlock,
blockName: correctName,
attrs: correctedAttributes,
};
}
/**
* Normalize the raw block by applying the fallback block name if none given,
* sanitize the parsed HTML...
*
* @param {WPRawBlock} rawBlock The raw block object.
*
* @return {WPRawBlock} The normalized block object.
*/
export function normalizeRawBlock( rawBlock ) {
const fallbackBlockName = getFreeformContentHandlerName();
// If the grammar parsing don't produce any block name, use the freeform block.
const rawBlockName = rawBlock.blockName || getFreeformContentHandlerName();
const rawAttributes = rawBlock.attrs || {};
const rawInnerBlocks = rawBlock.innerBlocks || [];
let rawInnerHTML = rawBlock.innerHTML.trim();
// Fallback content may be upgraded from classic content expecting implicit
// automatic paragraphs, so preserve them. Assumes wpautop is idempotent,
// meaning there are no negative consequences to repeated autop calls.
if ( rawBlockName === fallbackBlockName ) {
rawInnerHTML = autop( rawInnerHTML ).trim();
}
return {
...rawBlock,
blockName: rawBlockName,
attrs: rawAttributes,
innerHTML: rawInnerHTML,
innerBlocks: rawInnerBlocks,
};
}
/**
* Uses the "unregistered blockType" to create a block object.
*
* @param {WPRawBlock} rawBlock block.
*
* @return {WPRawBlock} The unregistered block object.
*/
function createMissingBlockType( rawBlock ) {
const unregisteredFallbackBlock =
getUnregisteredTypeHandlerName() || getFreeformContentHandlerName();
// Preserve undelimited content for use by the unregistered type
// handler. A block node's `innerHTML` isn't enough, as that field only
// carries the block's own HTML and not its nested blocks.
const originalUndelimitedContent = serializeRawBlock( rawBlock, {
isCommentDelimited: false,
} );
// Preserve full block content for use by the unregistered type
// handler, block boundaries included.
const originalContent = serializeRawBlock( rawBlock, {
isCommentDelimited: true,
} );
return {
blockName: unregisteredFallbackBlock,
attrs: {
originalName: rawBlock.blockName,
originalContent,
originalUndelimitedContent,
},
innerHTML: rawBlock.blockName ? originalContent : rawBlock.innerHTML,
innerBlocks: rawBlock.innerBlocks,
innerContent: rawBlock.innerContent,
};
}
/**
* Validates a block and wraps with validation meta.
*
* The name here is regrettable but `validateBlock` is already taken.
*
* @param {WPBlock} unvalidatedBlock
* @param {import('../registration').WPBlockType} blockType
* @return {WPBlock} validated block, with auto-fixes if initially invalid
*/
function applyBlockValidation( unvalidatedBlock, blockType ) {
// Attempt to validate the block.
const [ isValid ] = validateBlock( unvalidatedBlock, blockType );
if ( isValid ) {
return { ...unvalidatedBlock, isValid, validationIssues: [] };
}
// If the block is invalid, attempt some built-in fixes
// like custom classNames handling.
const fixedBlock = applyBuiltInValidationFixes(
unvalidatedBlock,
blockType
);
// Attempt to validate the block once again after the built-in fixes.
const [ isFixedValid, validationIssues ] = validateBlock(
unvalidatedBlock,
blockType
);
return { ...fixedBlock, isValid: isFixedValid, validationIssues };
}
/**
* Given a raw block returned by grammar parsing, returns a fully parsed block.
*
* @param {WPRawBlock} rawBlock The raw block object.
* @param {ParseOptions} options Extra options for handling block parsing.
*
* @return {WPBlock} Fully parsed block.
*/
export function parseRawBlock( rawBlock, options ) {
let normalizedBlock = normalizeRawBlock( rawBlock );
// During the lifecycle of the project, we renamed some old blocks
// and transformed others to new blocks. To avoid breaking existing content,
// we added this function to properly parse the old content.
normalizedBlock = convertLegacyBlocks( normalizedBlock );
// Try finding the type for known block name.
let blockType = getBlockType( normalizedBlock.blockName );
// If not blockType is found for the specified name, fallback to the "unregistedBlockType".
if ( ! blockType ) {
normalizedBlock = createMissingBlockType( normalizedBlock );
blockType = getBlockType( normalizedBlock.blockName );
}
// If it's an empty freeform block or there's no blockType (no missing block handler)
// Then, just ignore the block.
// It might be a good idea to throw a warning here.
// TODO: I'm unsure about the unregisteredFallbackBlock check,
// it might ignore some dynamic unregistered third party blocks wrongly.
const isFallbackBlock =
normalizedBlock.blockName === getFreeformContentHandlerName() ||
normalizedBlock.blockName === getUnregisteredTypeHandlerName();
if ( ! blockType || ( ! normalizedBlock.innerHTML && isFallbackBlock ) ) {
return;
}
// Parse inner blocks recursively.
const parsedInnerBlocks = normalizedBlock.innerBlocks
.map( ( innerBlock ) => parseRawBlock( innerBlock, options ) )
// See https://github.com/WordPress/gutenberg/pull/17164.
.filter( ( innerBlock ) => !! innerBlock );
// Get the fully parsed block.
const parsedBlock = createBlock(
normalizedBlock.blockName,
getBlockAttributes(
blockType,
normalizedBlock.innerHTML,
normalizedBlock.attrs
),
parsedInnerBlocks
);
parsedBlock.originalContent = normalizedBlock.innerHTML;
const validatedBlock = applyBlockValidation( parsedBlock, blockType );
const { validationIssues } = validatedBlock;
// Run the block deprecation and migrations.
// This is performed on both invalid and valid blocks because
// migration using the `migrate` functions should run even
// if the output is deemed valid.
const updatedBlock = applyBlockDeprecatedVersions(
validatedBlock,
normalizedBlock,
blockType
);
if ( ! updatedBlock.isValid ) {
// Preserve the original unprocessed version of the block
// that we received (no fixes, no deprecations) so that
// we can save it as close to exactly the same way as
// we loaded it. This is important to avoid corruption
// and data loss caused by block implementations trying
// to process data that isn't fully recognized.
updatedBlock.__unstableBlockSource = rawBlock;
}
if (
! validatedBlock.isValid &&
updatedBlock.isValid &&
! options?.__unstableSkipMigrationLogs
) {
/* eslint-disable no-console */
console.groupCollapsed( 'Updated Block: %s', blockType.name );
console.info(
'Block successfully updated for `%s` (%o).\n\nNew content generated by `save` function:\n\n%s\n\nContent retrieved from post body:\n\n%s',
blockType.name,
blockType,
getSaveContent( blockType, updatedBlock.attributes ),
updatedBlock.originalContent
);
console.groupEnd();
/* eslint-enable no-console */
} else if ( ! validatedBlock.isValid && ! updatedBlock.isValid ) {
validationIssues.forEach( ( { log, args } ) => log( ...args ) );
}
return updatedBlock;
}
/**
* Utilizes an optimized token-driven parser based on the Gutenberg grammar spec
* defined through a parsing expression grammar to take advantage of the regular
* cadence provided by block delimiters -- composed syntactically through HTML
* comments -- which, given a general HTML document as an input, returns a block
* list array representation.
*
* This is a recursive-descent parser that scans linearly once through the input
* document. Instead of directly recursing it utilizes a trampoline mechanism to
* prevent stack overflow. This initial pass is mainly interested in separating
* and isolating the blocks serialized in the document and manifestly not in the
* content within the blocks.
*
* @see
* https://developer.wordpress.org/block-editor/packages/packages-block-serialization-default-parser/
*
* @param {string} content The post content.
* @param {ParseOptions} options Extra options for handling block parsing.
*
* @return {Array} Block list.
*/
export default function parse( content, options ) {
return grammarParse( content ).reduce( ( accumulator, rawBlock ) => {
const block = parseRawBlock( rawBlock, options );
if ( block ) {
accumulator.push( block );
}
return accumulator;
}, [] );
}