: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* 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.
function createLinkFormat({
format.attributes.type = type;
format.attributes.id = id;
format.attributes.target = '_blank';
format.attributes.rel = format.attributes.rel ? format.attributes.rel + ' noreferrer noopener' : 'noreferrer noopener';
format.attributes.rel = format.attributes.rel ? format.attributes.rel + ' nofollow' : 'nofollow';
/* 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 */
function getFormatBoundary(value, format, startIndex = value.start, endIndex = value.end) {
const EMPTY_BOUNDARIES = {
// Clone formats to avoid modifying source formats.
const newFormats = formats.slice();
const formatAtStart = newFormats[startIndex]?.find(({
}) => type === format.type);
const formatAtEnd = newFormats[endIndex]?.find(({
}) => type === format.type);
const formatAtEndMinusOne = newFormats[endIndex - 1]?.find(({
}) => type === format.type);
// Set values to conform to "start"
targetFormat = formatAtStart;
initialIndex = startIndex;
} else if (!!formatAtEnd) {
// Set values to conform to "end"
targetFormat = formatAtEnd;
} 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;
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 indicies of the "edges" as the boundaries.
* 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 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;
const partialRight = (fn, ...partialArgs) => (...args) => fn(...args, ...partialArgs);
const walkToStart = partialRight(walkToBoundary, 'backwards');
const walkToEnd = partialRight(walkToBoundary, 'forwards');
;// CONCATENATED MODULE: ./node_modules/@wordpress/format-library/build-module/link/inline.js
const LINK_SETTINGS = [...external_wp_blockEditor_namespaceObject.__experimentalLinkControl.DEFAULT_LINK_SETTINGS, {
title: (0,external_wp_i18n_namespaceObject.__)('Mark as nofollow')
const richLinkTextValue = getRichTextValueFromSelection(value, isActive);
// Get the text content minus any HTML tags.
const richTextText = richLinkTextValue.text;
} = (0,external_wp_data_namespaceObject.useDispatch)(external_wp_blockEditor_namespaceObject.store);
} = (0,external_wp_data_namespaceObject.useSelect)(select => {
} = select(external_wp_blockEditor_namespaceObject.store);
const _settings = getSettings();
createPageEntity: _settings.__experimentalCreatePageEntity,
userCanCreatePages: _settings.__experimentalUserCanCreatePages,
selectionStart: getSelectionStart()
const linkValue = (0,external_wp_element_namespaceObject.useMemo)(() => ({
url: activeAttributes.url,
type: activeAttributes.type,
opensInNewTab: activeAttributes.target === '_blank',
nofollow: activeAttributes.rel?.includes('nofollow'),
}), [activeAttributes.id, activeAttributes.rel, activeAttributes.target, activeAttributes.type, activeAttributes.url, richTextText]);
const newValue = (0,external_wp_richText_namespaceObject.removeFormat)(value, 'core/link');
(0,external_wp_a11y_namespaceObject.speak)((0,external_wp_i18n_namespaceObject.__)('Link removed.'), 'assertive');
function onChangeLink(nextValue) {
const hasLink = linkValue?.url;
const isNewLink = !hasLink;
// Merge the next value with the current link value.
const newUrl = (0,external_wp_url_namespaceObject.prependHTTP)(nextValue.url);
const linkFormat = createLinkFormat({
id: nextValue.id !== undefined && nextValue.id !== null ? String(nextValue.id) : undefined,
opensInNewWindow: nextValue.opensInNewTab,
nofollow: nextValue.nofollow
const newText = nextValue.title || newUrl;
// Scenario: we have any active text selection or an active format.
if ((0,external_wp_richText_namespaceObject.isCollapsed)(value) && !isActive) {
// Scenario: we don't have any actively selected text or formats.
const inserted = (0,external_wp_richText_namespaceObject.insert)(value, newText);
newValue = (0,external_wp_richText_namespaceObject.applyFormat)(inserted, linkFormat, value.start, value.start + newText.length);
// Move the selection to the end of the inserted link outside of the format boundary
// so the user can continue typing after the link.
clientId: selectionStart.clientId,
identifier: selectionStart.attributeKey,
start: value.start + newText.length + 1
} else if (newText === richTextText) {
newValue = (0,external_wp_richText_namespaceObject.applyFormat)(value, linkFormat);
// Scenario: Editing an existing link.
// Create new RichText value for the new text in order that we
// can apply formats to it.
newValue = (0,external_wp_richText_namespaceObject.create)({
// Apply the new Link format to this new text value.
newValue = (0,external_wp_richText_namespaceObject.applyFormat)(newValue, linkFormat, 0, newText.length);
// Get the boundaries of the active link format.
const boundary = getFormatBoundary(value, {
// Split the value at the start of the active link format.
// Passing "start" as the 3rd parameter is required to ensure
// the second half of the split value is split at the format's
// start boundary and avoids relying on the value's "end" property
// which may not correspond correctly.
const [valBefore, valAfter] = (0,external_wp_richText_namespaceObject.split)(value, boundary.start, boundary.start);
// Update the original (full) RichTextValue replacing the
// target text with the *new* RichTextValue containing:
// 1. The new text content.
// 2. The new link format.
// As "replace" will operate on the first match only, it is
// run only against the second half of the value which was
// split at the active format's boundary. This avoids a bug
// with incorrectly targetted replacements.
// See: https://github.com/WordPress/gutenberg/issues/41771.
// Note original formats will be lost when applying this change.
// That is expected behaviour.
// See: https://github.com/WordPress/gutenberg/pull/33849#issuecomment-936134179.
const newValAfter = (0,external_wp_richText_namespaceObject.replace)(valAfter, richTextText, newValue);
newValue = (0,external_wp_richText_namespaceObject.concat)(valBefore, newValAfter);
// Focus should only be returned to the rich text on submit if this link is not
// being created for the first time. If it is then focus should remain within the
// Link UI because it should remain open for the user to modify the link they have
if (!isValidHref(newUrl)) {
(0,external_wp_a11y_namespaceObject.speak)((0,external_wp_i18n_namespaceObject.__)('Warning: the link has been inserted but may have errors. Please test it.'), 'assertive');
(0,external_wp_a11y_namespaceObject.speak)((0,external_wp_i18n_namespaceObject.__)('Link edited.'), 'assertive');
(0,external_wp_a11y_namespaceObject.speak)((0,external_wp_i18n_namespaceObject.__)('Link inserted.'), 'assertive');
const popoverAnchor = (0,external_wp_richText_namespaceObject.useAnchor)({
editableContentElement: contentRef.current,
...build_module_link_link,
async function handleCreate(pageTitle) {
const page = await createPageEntity({
title: page.title.rendered,
function createButtonText(searchTerm) {
return (0,external_wp_element_namespaceObject.createInterpolateElement)((0,external_wp_i18n_namespaceObject.sprintf)( /* translators: %s: search term. */
(0,external_wp_i18n_namespaceObject.__)('Create page: <mark>%s</mark>'), searchTerm), {
mark: /*#__PURE__*/(0,external_ReactJSXRuntime_namespaceObject.jsx)("mark", {})
return /*#__PURE__*/(0,external_ReactJSXRuntime_namespaceObject.jsx)(external_wp_components_namespaceObject.Popover, {
onFocusOutside: onFocusOutside,
focusOnMount: focusOnMount,
children: /*#__PURE__*/(0,external_ReactJSXRuntime_namespaceObject.jsx)(external_wp_blockEditor_namespaceObject.__experimentalLinkControl, {
createSuggestion: createPageEntity && handleCreate,
withCreateSuggestion: userCanCreatePages,
createSuggestionButtonText: createButtonText,
showInitialSuggestions: true,
// always show Pages as initial suggestions
initialSuggestionsSearchOptions: {
function getRichTextValueFromSelection(value, isActive) {
// Default to the selection ranges on the RichTextValue object.
let textStart = value.start;
// If the format is currently active then the rich text value
// should always be taken from the bounds of the active format
// and not the selected text.
const boundary = getFormatBoundary(value, {
textStart = boundary.start;
// Text *selection* always extends +1 beyond the edge of the format.
// We account for that here.
textEnd = boundary.end + 1;
// Get a RichTextValue containing the selected text content.
return (0,external_wp_richText_namespaceObject.slice)(value, textStart, textEnd);
/* harmony default export */ const inline = (InlineLinkUI);
;// CONCATENATED MODULE: ./node_modules/@wordpress/format-library/build-module/link/index.js
const link_name = 'core/link';
const link_title = (0,external_wp_i18n_namespaceObject.__)('Link');
const [addingLink, setAddingLink] = (0,external_wp_element_namespaceObject.useState)(false);
// We only need to store the button element that opened the popover. We can ignore the other states, as they will be handled by the onFocus prop to return to the rich text field.
const [openedBy, setOpenedBy] = (0,external_wp_element_namespaceObject.useState)(null);
(0,external_wp_element_namespaceObject.useEffect)(() => {
// When the link becomes inactive (i.e. isActive is false), reset the editingLink state
// and the creatingLink state. This means that if the Link UI is displayed and the link
// becomes inactive (e.g. used arrow keys to move cursor outside of link bounds), the UI will close.
(0,external_wp_element_namespaceObject.useLayoutEffect)(() => {
const editableContentElement = contentRef.current;
if (!editableContentElement) {
function handleClick(event) {
// There is a situation whereby there is an existing link in the rich text
// and the user clicks on the leftmost edge of that link and fails to activate
// the link format, but the click event still fires on the `<a>` element.
// This causes the `editingLink` state to be set to `true` and the link UI
// to be rendered in "creating" mode. We need to check isActive to see if
// we have an active link format.
const link = event.target.closest('[contenteditable] a');
// other formats (e.g. bold) may be nested within the link.
editableContentElement.addEventListener('click', handleClick);
editableContentElement.removeEventListener('click', handleClick);
}, [contentRef, isActive]);
function addLink(target) {
const text = (0,external_wp_richText_namespaceObject.getTextContent)((0,external_wp_richText_namespaceObject.slice)(value));
if (!isActive && text && (0,external_wp_url_namespaceObject.isURL)(text) && isValidHref(text)) {
onChange((0,external_wp_richText_namespaceObject.applyFormat)(value, {
} else if (!isActive && text && (0,external_wp_url_namespaceObject.isEmail)(text)) {
onChange((0,external_wp_richText_namespaceObject.applyFormat)(value, {
action: null // We don't need to distinguish between click or keyboard here
* Runs when the popover is closed via escape keypress, unlinking the selected text,
* but _not_ on a click outside the popover. onFocusOutside handles that.