@wordpress/format-library
Version:
Format library for the WordPress editor.
263 lines (223 loc) • 6.89 kB
JavaScript
/**
* WordPress dependencies
*/
import {
getProtocol,
isValidProtocol,
getAuthority,
isValidAuthority,
getPath,
isValidPath,
getQueryString,
isValidQueryString,
getFragment,
isValidFragment,
} from '@wordpress/url';
/**
* Check for issues with the provided href.
*
* @param {string} href The href.
*
* @return {boolean} Is the href invalid?
*/
export function isValidHref( href ) {
if ( ! href ) {
return false;
}
const trimmedHref = href.trim();
if ( ! trimmedHref ) {
return false;
}
// Does the href start with something that looks like a URL protocol?
if ( /^\S+:/.test( trimmedHref ) ) {
const protocol = getProtocol( trimmedHref );
if ( ! isValidProtocol( protocol ) ) {
return false;
}
// Add some extra checks for http(s) URIs, since these are the most common use-case.
// This ensures URIs with an http protocol have exactly two forward slashes following the protocol.
if (
protocol.startsWith( 'http' ) &&
! /^https?:\/\/[^\/\s]/i.test( trimmedHref )
) {
return false;
}
const authority = getAuthority( trimmedHref );
if ( ! isValidAuthority( authority ) ) {
return false;
}
const path = getPath( trimmedHref );
if ( path && ! isValidPath( path ) ) {
return false;
}
const queryString = getQueryString( trimmedHref );
if ( queryString && ! isValidQueryString( queryString ) ) {
return false;
}
const fragment = getFragment( trimmedHref );
if ( fragment && ! isValidFragment( fragment ) ) {
return false;
}
}
// Validate anchor links.
if ( trimmedHref.startsWith( '#' ) && ! isValidFragment( trimmedHref ) ) {
return false;
}
return true;
}
/**
* Generates the format object that will be applied to the link text.
*
* @param {Object} options
* @param {string} options.url The href of the link.
* @param {string} options.type The type of the link.
* @param {string} options.id The ID of the link.
* @param {boolean} options.opensInNewWindow Whether this link will open in a new window.
* @param {boolean} options.nofollow Whether this link is marked as no follow relationship.
* @return {Object} The final format object.
*/
export function createLinkFormat( {
url,
type,
id,
opensInNewWindow,
nofollow,
} ) {
const format = {
type: 'core/link',
attributes: {
url,
},
};
if ( type ) {
format.attributes.type = type;
}
if ( id ) {
format.attributes.id = id;
}
if ( opensInNewWindow ) {
format.attributes.target = '_blank';
format.attributes.rel = format.attributes.rel
? format.attributes.rel + ' noreferrer noopener'
: 'noreferrer noopener';
}
if ( nofollow ) {
format.attributes.rel = format.attributes.rel
? format.attributes.rel + ' nofollow'
: 'nofollow';
}
return format;
}
/* eslint-disable jsdoc/no-undefined-types */
/**
* Get the start and end boundaries of a given format from a rich text value.
*
*
* @param {RichTextValue} value the rich text value to interrogate.
* @param {string} format the identifier for the target format (e.g. `core/link`, `core/bold`).
* @param {number?} startIndex optional startIndex to seek from.
* @param {number?} endIndex optional endIndex to seek from.
* @return {Object} object containing start and end values for the given format.
*/
/* eslint-enable jsdoc/no-undefined-types */
export function getFormatBoundary(
value,
format,
startIndex = value.start,
endIndex = value.end
) {
const EMPTY_BOUNDARIES = {
start: null,
end: null,
};
const { formats } = value;
let targetFormat;
let initialIndex;
if ( ! formats?.length ) {
return EMPTY_BOUNDARIES;
}
// Clone formats to avoid modifying source formats.
const newFormats = formats.slice();
const formatAtStart = newFormats[ startIndex ]?.find(
( { type } ) => type === format.type
);
const formatAtEnd = newFormats[ endIndex ]?.find(
( { type } ) => type === format.type
);
const formatAtEndMinusOne = newFormats[ endIndex - 1 ]?.find(
( { type } ) => type === format.type
);
if ( !! formatAtStart ) {
// Set values to conform to "start"
targetFormat = formatAtStart;
initialIndex = startIndex;
} else if ( !! formatAtEnd ) {
// Set values to conform to "end"
targetFormat = formatAtEnd;
initialIndex = endIndex;
} else if ( !! formatAtEndMinusOne ) {
// This is an edge case which will occur if you create a format, then place
// the caret just before the format and hit the back ARROW key. The resulting
// value object will have start and end +1 beyond the edge of the format boundary.
targetFormat = formatAtEndMinusOne;
initialIndex = endIndex - 1;
} else {
return EMPTY_BOUNDARIES;
}
const index = newFormats[ initialIndex ].indexOf( targetFormat );
const walkingArgs = [ newFormats, initialIndex, targetFormat, index ];
// Walk the startIndex "backwards" to the leading "edge" of the matching format.
startIndex = walkToStart( ...walkingArgs );
// Walk the endIndex "forwards" until the trailing "edge" of the matching format.
endIndex = walkToEnd( ...walkingArgs );
// Safe guard: start index cannot be less than 0.
startIndex = startIndex < 0 ? 0 : startIndex;
// // Return the indices of the "edges" as the boundaries.
return {
start: startIndex,
end: endIndex,
};
}
/**
* Walks forwards/backwards towards the boundary of a given format within an
* array of format objects. Returns the index of the boundary.
*
* @param {Array} formats the formats to search for the given format type.
* @param {number} initialIndex the starting index from which to walk.
* @param {Object} targetFormatRef a reference to the format type object being sought.
* @param {number} formatIndex the index at which we expect the target format object to be.
* @param {string} direction either 'forwards' or 'backwards' to indicate the direction.
* @return {number} the index of the boundary of the given format.
*/
function walkToBoundary(
formats,
initialIndex,
targetFormatRef,
formatIndex,
direction
) {
let index = initialIndex;
const directions = {
forwards: 1,
backwards: -1,
};
const directionIncrement = directions[ direction ] || 1; // invalid direction arg default to forwards
const inverseDirectionIncrement = directionIncrement * -1;
while (
formats[ index ] &&
formats[ index ][ formatIndex ] === targetFormatRef
) {
// Increment/decrement in the direction of operation.
index = index + directionIncrement;
}
// Restore by one in inverse direction of operation
// to avoid out of bounds.
index = index + inverseDirectionIncrement;
return index;
}
const partialRight =
( fn, ...partialArgs ) =>
( ...args ) =>
fn( ...args, ...partialArgs );
const walkToStart = partialRight( walkToBoundary, 'backwards' );
const walkToEnd = partialRight( walkToBoundary, 'forwards' );