Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dom: Add types to clean-node-list #30412

Merged
merged 3 commits into from
Apr 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/dom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ the latter.

_Parameters_

- _newNode_ `Element`: Node to be inserted.
- _referenceNode_ `Element`: Node after which to perform the insertion.
- _newNode_ `Node`: Node to be inserted.
- _referenceNode_ `Node`: Node after which to perform the insertion.

_Returns_

Expand Down Expand Up @@ -290,7 +290,7 @@ Given a DOM node, removes it from the DOM.

_Parameters_

- _node_ `Element`: Node to be removed.
- _node_ `Node`: Node to be removed.

_Returns_

Expand Down
34 changes: 27 additions & 7 deletions packages/dom/src/dom/clean-node-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,44 @@ import remove from './remove';
import unwrap from './unwrap';
import { isPhrasingContent } from '../phrasing-content';
import insertAfter from './insert-after';
import isElement from './is-element';

/* eslint-disable jsdoc/valid-types */
sarayourfriend marked this conversation as resolved.
Show resolved Hide resolved
/**
* @typedef SchemaItem
* @property {string[]} [attributes] Attributes.
* @property {(string | RegExp)[]} [classes] Classnames or RegExp to test against.
* @property {'*' | { [tag: string]: SchemaItem }} [children] Child schemas.
* @property {string[]} [require] Selectors to test required children against. Leave empty or undefined if there are no requirements.
* @property {boolean} allowEmpty Whether to allow nodes without children.
* @property {(node: Node) => boolean} [isMatch] Function to test whether a node is a match. If left undefined any node will be assumed to match.
*/

/** @typedef {{ [tag: string]: SchemaItem }} Schema */
/* eslint-enable jsdoc/valid-types */

/**
* Given a schema, unwraps or removes nodes, attributes and classes on a node
* list.
*
* @param {NodeList} nodeList The nodeList to filter.
* @param {Document} doc The document of the nodeList.
* @param {Object} schema An array of functions that can mutate with the provided node.
* @param {Object} inline Whether to clean for inline mode.
* @param {Schema} schema An array of functions that can mutate with the provided node.
* @param {boolean} inline Whether to clean for inline mode.
*/
export default function cleanNodeList( nodeList, doc, schema, inline ) {
Array.from( nodeList ).forEach( ( node ) => {
Array.from( nodeList ).forEach( (
/** @type {Node & { nextElementSibling?: unknown }} */ node
) => {
const tag = node.nodeName.toLowerCase();

// It's a valid child, if the tag exists in the schema without an isMatch
// function, or with an isMatch function that matches the node.
if (
schema.hasOwnProperty( tag ) &&
( ! schema[ tag ].isMatch || schema[ tag ].isMatch( node ) )
( ! schema[ tag ].isMatch || schema[ tag ].isMatch?.( node ) )
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

! schema[ tag ].isMatch || doesn't all the way refine isMatch's presence. There might be a better way to do this but I'm not sure about it 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the optional chaining will do a good job here 👍

) {
if ( node.nodeType === node.ELEMENT_NODE ) {
if ( isElement( node ) ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a type guard that does the same thing as the previous check but also refines the type to Element from Node.

const {
attributes = [],
classes = [],
Expand Down Expand Up @@ -64,9 +81,11 @@ export default function cleanNodeList( nodeList, doc, schema, inline ) {
if ( node.classList && node.classList.length ) {
const mattchers = classes.map( ( item ) => {
if ( typeof item === 'string' ) {
return ( className ) => className === item;
return ( /** @type {string} */ className ) =>
className === item;
} else if ( item instanceof RegExp ) {
return ( className ) => item.test( className );
return ( /** @type {string} */ className ) =>
item.test( className );
}

return noop;
Expand Down Expand Up @@ -113,6 +132,7 @@ export default function cleanNodeList( nodeList, doc, schema, inline ) {
// contains children that are block content, unwrap
// the node because it is invalid.
} else if (
node.parentNode &&
sarayourfriend marked this conversation as resolved.
Show resolved Hide resolved
node.parentNode.nodeName === 'BODY' &&
isPhrasingContent( node )
) {
Expand Down
10 changes: 8 additions & 2 deletions packages/dom/src/dom/insert-after.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';

/**
* Given two DOM nodes, inserts the former in the DOM as the next sibling of
* the latter.
*
* @param {Element} newNode Node to be inserted.
* @param {Element} referenceNode Node after which to perform the insertion.
* @param {Node} newNode Node to be inserted.
* @param {Node} referenceNode Node after which to perform the insertion.
* @return {void}
*/
export default function insertAfter( newNode, referenceNode ) {
assertIsDefined( referenceNode.parentNode, 'referenceNode.parentNode' );
referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling );
}
9 changes: 9 additions & 0 deletions packages/dom/src/dom/is-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* eslint-disable jsdoc/valid-types */
/**
* @param {Node | null | undefined} node
* @return {node is Element} True if node is an Element node
*/
export default function isElement( node ) {
/* eslint-enable jsdoc/valid-types */
return !! node && node.nodeType === node.ELEMENT_NODE;
}
6 changes: 4 additions & 2 deletions packages/dom/src/dom/is-empty.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ export default function isEmpty( element ) {
case element.TEXT_NODE:
// We cannot use \s since it includes special spaces which we want
// to preserve.
return /^[ \f\n\r\t\v\u00a0]*$/.test( element.nodeValue );
return /^[ \f\n\r\t\v\u00a0]*$/.test( element.nodeValue || '' );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nodeValue could be null so this defaults it to ''.

case element.ELEMENT_NODE:
if ( element.hasAttributes() ) {
return false;
} else if ( ! element.hasChildNodes() ) {
return true;
}

return Array.from( element.childNodes ).every( isEmpty );
return /** @type {Element[]} */ ( Array.from(
element.childNodes
) ).every( isEmpty );
default:
return true;
}
Expand Down
8 changes: 7 additions & 1 deletion packages/dom/src/dom/remove.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';

/**
* Given a DOM node, removes it from the DOM.
*
* @param {Element} node Node to be removed.
* @param {Node} node Node to be removed.
* @return {void}
*/
export default function remove( node ) {
assertIsDefined( node.parentNode, 'node.parentNode' );
node.parentNode.removeChild( node );
}
7 changes: 7 additions & 0 deletions packages/dom/src/dom/unwrap.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';

/**
* Unwrap the given node. This means any child nodes are moved to the parent.
*
Expand All @@ -8,6 +13,8 @@
export default function unwrap( node ) {
const parent = node.parentNode;

assertIsDefined( parent, 'node.parentNode' );

while ( node.firstChild ) {
parent.insertBefore( node.firstChild, node );
}
Expand Down
6 changes: 6 additions & 0 deletions packages/dom/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@
"include": [
"src/data-transfer.js",
"src/dom/caret-range-from-point.js",
"src/dom/clean-node-list.js",
"src/dom/compute-caret-rect.js",
"src/dom/get-rectangle-from-range.js",
"src/dom/is-empty.js",
"src/dom/is-element.js",
"src/dom/insert-after.js",
"src/dom/remove.js",
"src/dom/unwrap.js",
"src/utils/**/*",
"src/focusable.js",
"src/phrasing-content.js",
Expand Down