@ckeditor/ckeditor5-link
Version:
Link feature for CKEditor 5.
129 lines (128 loc) • 5 kB
JavaScript
/**
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import { upperFirst } from 'lodash-es';
const ATTRIBUTE_WHITESPACES = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; // eslint-disable-line no-control-regex
const SAFE_URL = /^(?:(?:https?|ftps?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.:-]|$))/i;
// Simplified email test - should be run over previously found URL.
const EMAIL_REG_EXP = /^[\S]+@((?![-_])(?:[-\w\u00a1-\uffff]{0,63}[^-_]\.))+(?:[a-z\u00a1-\uffff]{2,})$/i;
// The regex checks for the protocol syntax ('xxxx://' or 'xxxx:')
// or non-word characters at the beginning of the link ('/', '#' etc.).
const PROTOCOL_REG_EXP = /^((\w+:(\/{2,})?)|(\W))/i;
/**
* A keystroke used by the {@link module:link/linkui~LinkUI link UI feature}.
*/
export const LINK_KEYSTROKE = 'Ctrl+K';
/**
* Returns `true` if a given view node is the link element.
*/
export function isLinkElement(node) {
return node.is('attributeElement') && !!node.getCustomProperty('link');
}
/**
* Creates a link {@link module:engine/view/attributeelement~AttributeElement} with the provided `href` attribute.
*/
export function createLinkElement(href, { writer }) {
// Priority 5 - https://github.com/ckeditor/ckeditor5-link/issues/121.
const linkElement = writer.createAttributeElement('a', { href }, { priority: 5 });
writer.setCustomProperty('link', true, linkElement);
return linkElement;
}
/**
* Returns a safe URL based on a given value.
*
* A URL is considered safe if it is safe for the user (does not contain any malicious code).
*
* If a URL is considered unsafe, a simple `"#"` is returned.
*
* @internal
*/
export function ensureSafeUrl(url) {
const urlString = String(url);
return isSafeUrl(urlString) ? urlString : '#';
}
/**
* Checks whether the given URL is safe for the user (does not contain any malicious code).
*/
function isSafeUrl(url) {
const normalizedUrl = url.replace(ATTRIBUTE_WHITESPACES, '');
return !!normalizedUrl.match(SAFE_URL);
}
/**
* Returns the {@link module:link/linkconfig~LinkConfig#decorators `config.link.decorators`} configuration processed
* to respect the locale of the editor, i.e. to display the {@link module:link/linkconfig~LinkDecoratorManualDefinition label}
* in the correct language.
*
* **Note**: Only the few most commonly used labels are translated automatically. Other labels should be manually
* translated in the {@link module:link/linkconfig~LinkConfig#decorators `config.link.decorators`} configuration.
*
* @param t Shorthand for {@link module:utils/locale~Locale#t Locale#t}.
* @param decorators The decorator reference where the label values should be localized.
*/
export function getLocalizedDecorators(t, decorators) {
const localizedDecoratorsLabels = {
'Open in a new tab': t('Open in a new tab'),
'Downloadable': t('Downloadable')
};
decorators.forEach(decorator => {
if ('label' in decorator && localizedDecoratorsLabels[decorator.label]) {
decorator.label = localizedDecoratorsLabels[decorator.label];
}
return decorator;
});
return decorators;
}
/**
* Converts an object with defined decorators to a normalized array of decorators. The `id` key is added for each decorator and
* is used as the attribute's name in the model.
*/
export function normalizeDecorators(decorators) {
const retArray = [];
if (decorators) {
for (const [key, value] of Object.entries(decorators)) {
const decorator = Object.assign({}, value, { id: `link${upperFirst(key)}` });
retArray.push(decorator);
}
}
return retArray;
}
/**
* Returns `true` if the specified `element` can be linked (the element allows the `linkHref` attribute).
*/
export function isLinkableElement(element, schema) {
if (!element) {
return false;
}
return schema.checkAttribute(element.name, 'linkHref');
}
/**
* Returns `true` if the specified `value` is an email.
*/
export function isEmail(value) {
return EMAIL_REG_EXP.test(value);
}
/**
* Adds the protocol prefix to the specified `link` when:
*
* * it does not contain it already, and there is a {@link module:link/linkconfig~LinkConfig#defaultProtocol `defaultProtocol` }
* configuration value provided,
* * or the link is an email address.
*/
export function addLinkProtocolIfApplicable(link, defaultProtocol) {
const protocol = isEmail(link) ? 'mailto:' : defaultProtocol;
const isProtocolNeeded = !!protocol && !linkHasProtocol(link);
return link && isProtocolNeeded ? protocol + link : link;
}
/**
* Checks if protocol is already included in the link.
*/
export function linkHasProtocol(link) {
return PROTOCOL_REG_EXP.test(link);
}
/**
* Opens the link in a new browser tab.
*/
export function openLink(link) {
window.open(link, '_blank', 'noopener');
}