@liveblocks/react-ui
Version:
A set of React pre-built components for the Liveblocks products. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.
234 lines (231 loc) • 7.16 kB
JavaScript
import { Node, Path, Transforms, Range, Editor, Element } from 'slate';
import { isText, isPlainText } from '../utils/is-text.js';
import { filterActiveMarks } from '../utils/marks.js';
import { isComposerBodyCustomLink } from './custom-links.js';
function withAutoLinks(editor) {
const { isInline, normalizeNode, deleteBackward } = editor;
editor.isInline = (element) => {
return element.type === "auto-link" ? true : isInline(element);
};
editor.normalizeNode = (entry) => {
const [node, path] = entry;
if (isComposerBodyCustomLink(node)) {
return;
}
if (isText(node)) {
const parentNode = Node.parent(editor, path);
if (isComposerBodyCustomLink(parentNode)) {
return;
} else if (isComposerBodyAutoLink(parentNode)) {
const parentPath = Path.parent(path);
handleLinkEdit(editor, [parentNode, parentPath]);
if (!isPlainText(node)) {
const marks = filterActiveMarks(node);
Transforms.unsetNodes(editor, marks, { at: path });
}
} else {
handleLinkCreate(editor, [node, path]);
handleNeighbours(editor, [node, path]);
}
}
normalizeNode(entry);
};
editor.deleteBackward = (unit) => {
deleteBackward(unit);
const { selection } = editor;
if (!selection)
return;
if (!Range.isCollapsed(selection))
return;
const [match] = Editor.nodes(editor, {
at: selection,
match: isComposerBodyAutoLink,
mode: "lowest"
});
if (!match)
return;
Transforms.unwrapNodes(editor, {
match: isComposerBodyAutoLink
});
};
return editor;
}
function isComposerBodyAutoLink(node) {
return Element.isElement(node) && node.type === "auto-link";
}
const URL_REGEX = /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9().@:%_+~#?&//=]*)/;
const PUNCTUATION_OR_SPACE = /[.,;!?\s()]/;
const PERIOD_OR_QUESTION_MARK_FOLLOWED_BY_ALPHANUMERIC = /^[.?][a-zA-Z0-9]+/;
const PARENTHESES = /[()]/;
function isSeparator(char) {
return PUNCTUATION_OR_SPACE.test(char);
}
function endsWithSeparator(textContent) {
const lastCharacter = textContent[textContent.length - 1];
return lastCharacter !== void 0 ? isSeparator(lastCharacter) : false;
}
function startsWithSeparator(textContent) {
const firstCharacter = textContent[0];
return firstCharacter !== void 0 ? isSeparator(firstCharacter) : false;
}
function endsWithPeriodOrQuestionMark(textContent) {
return textContent[textContent.length - 1] === "." || textContent[textContent.length - 1] === "?";
}
function getUrlLogicalLength(url) {
if (!PARENTHESES.test(url)) {
return url.length;
}
let logicalLength = 0;
let parenthesesCount = 0;
for (const character of url) {
if (character === "(") {
parenthesesCount++;
}
if (character === ")") {
parenthesesCount--;
if (parenthesesCount < 0) {
break;
}
}
logicalLength++;
}
return logicalLength;
}
function isPreviousNodeValid(editor, path) {
const entry = Editor.previous(editor, { at: path });
if (!entry)
return true;
return isText(entry[0]) && (endsWithSeparator(entry[0].text) || entry[0].text === "");
}
function isNextNodeValid(editor, path) {
const entry = Editor.next(editor, { at: path });
if (!entry)
return true;
return isText(entry[0]) && (startsWithSeparator(entry[0].text) || entry[0].text === "");
}
function isContentAroundValid(editor, entry, start, end) {
const [node, path] = entry;
const text = node.text;
const contentBefore = text[start - 1];
const contentBeforeIsValid = start > 0 && contentBefore ? isSeparator(contentBefore) : isPreviousNodeValid(editor, path);
const contentAfter = text[end];
const contentAfterIsValid = end < text.length && contentAfter ? isSeparator(contentAfter) : isNextNodeValid(editor, path);
return contentBeforeIsValid && contentAfterIsValid;
}
const handleLinkEdit = (editor, entry) => {
const [node, path] = entry;
const children = Node.children(editor, path);
for (const [child] of children) {
if (isText(child))
continue;
Transforms.unwrapNodes(editor, { at: path });
return;
}
const text = Node.string(node);
const match = URL_REGEX.exec(text);
const matchContent = match?.[0];
if (!match || matchContent !== text) {
Transforms.unwrapNodes(editor, { at: path });
return;
}
if (endsWithPeriodOrQuestionMark(text)) {
Transforms.unwrapNodes(editor, { at: path });
const textBeforePeriod = text.slice(0, text.length - 1);
Transforms.wrapNodes(
editor,
{
type: "auto-link",
url: textBeforePeriod,
children: []
},
{
at: {
anchor: { path, offset: 0 },
focus: { path, offset: textBeforePeriod.length }
},
split: true
}
);
return;
}
const logicalLength = getUrlLogicalLength(text);
if (logicalLength < text.length) {
Transforms.unwrapNodes(editor, { at: path });
const logicalText = text.slice(0, logicalLength);
Transforms.wrapNodes(
editor,
{
type: "auto-link",
url: logicalText,
children: []
},
{
at: {
anchor: { path, offset: 0 },
focus: { path, offset: logicalText.length }
},
split: true
}
);
return;
}
if (!isPreviousNodeValid(editor, path) || !isNextNodeValid(editor, path)) {
Transforms.unwrapNodes(editor, { at: path });
return;
}
if (node.url !== text) {
Transforms.setNodes(editor, { url: matchContent }, { at: path });
return;
}
};
const handleLinkCreate = (editor, entry) => {
const [node, path] = entry;
const match = URL_REGEX.exec(node.text);
const matchContent = match?.[0];
if (!match || matchContent === void 0) {
return;
}
const start = match.index;
const end = start + matchContent.length;
if (!isContentAroundValid(editor, entry, start, end))
return;
Transforms.wrapNodes(
editor,
{
type: "auto-link",
url: matchContent,
children: []
},
{
at: {
anchor: { path, offset: start },
focus: { path, offset: end }
},
split: true
}
);
return;
};
const handleNeighbours = (editor, entry) => {
const [node, path] = entry;
const text = node.text;
const previousSibling = Editor.previous(editor, { at: path });
if (previousSibling && isComposerBodyAutoLink(previousSibling[0])) {
if (PERIOD_OR_QUESTION_MARK_FOLLOWED_BY_ALPHANUMERIC.test(text)) {
Transforms.unwrapNodes(editor, { at: previousSibling[1] });
Transforms.mergeNodes(editor, { at: path });
return;
}
if (!startsWithSeparator(text)) {
Transforms.unwrapNodes(editor, { at: previousSibling[1] });
return;
}
}
const nextSibling = Editor.next(editor, { at: path });
if (nextSibling && isComposerBodyAutoLink(nextSibling[0]) && !endsWithSeparator(text)) {
Transforms.unwrapNodes(editor, { at: nextSibling[1] });
return;
}
};
export { isComposerBodyAutoLink, withAutoLinks };
//# sourceMappingURL=auto-links.js.map