UNPKG

@automattic/social-previews

Version:

A suite of components to generate previews for a post for both social and search engines.

169 lines 7.51 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.hashtagUrlMap = exports.formatMastodonDate = exports.formatTweetDate = exports.formatThreadsDate = exports.formatNextdoorDate = exports.hasTag = exports.getTitleFromDescription = exports.stripHtmlTags = exports.firstValid = exports.hardTruncation = exports.truncatedAtSpace = exports.shortEnough = exports.baseDomain = void 0; exports.preparePreviewText = preparePreviewText; const jsx_runtime_1 = require("react/jsx-runtime"); const element_1 = require("@wordpress/element"); const i18n_1 = require("@wordpress/i18n"); const baseDomain = (url) => url .replace(/^[^/]+[/]*/, '') // strip leading protocol .replace(/\/.*$/, ''); // strip everything after the domain exports.baseDomain = baseDomain; const shortEnough = (limit) => (title) => title.length <= limit ? title : false; exports.shortEnough = shortEnough; const truncatedAtSpace = (lower, upper) => (fullTitle) => { const title = fullTitle.slice(0, upper); const lastSpace = title.lastIndexOf(' '); return lastSpace > lower && lastSpace < upper ? title.slice(0, lastSpace).concat('…') : false; }; exports.truncatedAtSpace = truncatedAtSpace; const hardTruncation = (limit) => (title) => title.slice(0, limit).concat('…'); exports.hardTruncation = hardTruncation; const firstValid = (...predicates) => (a) => predicates.find((p) => false !== p(a))?.(a); exports.firstValid = firstValid; const stripHtmlTags = (description, allowedTags = []) => { const pattern = new RegExp(`(<([^${allowedTags.join('')}>]+)>)`, 'gi'); return description ? description.replace(pattern, '') : ''; }; exports.stripHtmlTags = stripHtmlTags; /** * For social note posts we use the first 50 characters of the description. * @param description The post description. * @returns The first 50 characters of the description. */ const getTitleFromDescription = (description) => { return (0, exports.stripHtmlTags)(description).substring(0, 50); }; exports.getTitleFromDescription = getTitleFromDescription; const hasTag = (text, tag) => { const pattern = new RegExp(`<${tag}[^>]*>`, 'gi'); return pattern.test(text); }; exports.hasTag = hasTag; exports.formatNextdoorDate = new Intl.DateTimeFormat('en-GB', { // Result: "7 Oct", "31 Dec" day: 'numeric', month: 'short', }).format; exports.formatThreadsDate = new Intl.DateTimeFormat('en-US', { // Result: "'06/21/2024" day: '2-digit', month: '2-digit', year: 'numeric', }).format; exports.formatTweetDate = new Intl.DateTimeFormat('en-US', { // Result: "Apr 7", "Dec 31" month: 'short', day: 'numeric', }).format; exports.formatMastodonDate = new Intl.DateTimeFormat('en-US', { // Result: "Apr 7, 2024", "Dec 31, 2023" month: 'short', day: 'numeric', year: 'numeric', }).format; exports.hashtagUrlMap = { twitter: 'https://twitter.com/hashtag/%1$s', facebook: 'https://www.facebook.com/hashtag/%1$s', linkedin: 'https://www.linkedin.com/feed/hashtag/?keywords=%1$s', instagram: 'https://www.instagram.com/explore/tags/%1$s', mastodon: 'https://%2$s/tags/%1$s', nextdoor: 'https://nextdoor.com/hashtag/%1$s', threads: 'https://www.threads.net/search?q=%1$s&serp_type=tags', bluesky: 'https://bsky.app/hashtag/%1$s', }; /** * Prepares the text for the preview. */ function preparePreviewText(text, options) { const { platform, maxChars, maxLines, hyperlinkHashtags = true, // Instagram doesn't support hyperlink URLs at the moment. hyperlinkUrls = 'instagram' !== platform, } = options; let result = (0, exports.stripHtmlTags)(text); // Replace multiple new lines (2+) with 2 new lines // There can be any whitespace characters in empty lines // That is why "\s*" result = result.replaceAll(/(?:\s*[\n\r]){2,}/g, '\n\n'); if (maxChars && result.length > maxChars) { result = (0, exports.hardTruncation)(maxChars)(result); } if (maxLines) { const lines = result.split('\n'); if (lines.length > maxLines) { result = lines.slice(0, maxLines).join('\n'); } } const componentMap = {}; if (hyperlinkUrls) { // Convert URLs to hyperlinks. // TODO: Use a better regex here to match the URLs without protocol. const urls = result.match(/(https?:\/\/\S+)/g) || []; /** * BEFORE: * result = 'Check out this cool site: https://wordpress.org and this one: https://wordpress.com' */ urls.forEach((url, index) => { // Add the element to the component map. componentMap[`Link${index}`] = ((0, jsx_runtime_1.jsx)("a", { href: url, rel: "noopener noreferrer", target: "_blank", children: url })); // Replace the URL with the component placeholder. result = result.replace(url, `<Link${index} />`); }); /** * AFTER: * result = 'Check out this cool site: <Link0 /> and this one: <Link1 />' * componentMap = { * Link0: <a href="https://wordpress.org" ...>https://wordpress.org</a>, * Link1: <a href="https://wordpress.com" ...>https://wordpress.com</a> * } */ } // Convert hashtags to hyperlinks. if (hyperlinkHashtags && exports.hashtagUrlMap[platform]) { /** * We need to ensure that only the standalone hashtags are matched. * For example, we don't want to match the hash in the URL. * Thus we need to match the whitespace character before the hashtag or the beginning of the string. */ const hashtags = result.matchAll(/(^|\s)#(\w+)/g); const hashtagUrl = exports.hashtagUrlMap[platform]; /** * BEFORE: * result = `#breaking text with a #hashtag on the #web * with a url https://github.com/Automattic/wp-calypso#security that has a hash in it` */ [...hashtags].forEach(([fullMatch, whitespace, hashtag], index) => { const url = (0, i18n_1.sprintf)(hashtagUrl, hashtag, options.hashtagDomain); // Add the element to the component map. componentMap[`Hashtag${index}`] = ((0, jsx_runtime_1.jsx)("a", { href: url, rel: "noopener noreferrer", target: "_blank", children: `#${hashtag}` })); // Replace the hashtag with the component placeholder. result = result.replace(fullMatch, `${whitespace}<Hashtag${index} />`); }); /** * AFTER: * result = `<Hashtag0 /> text with a <Hashtag1 /> on the <Hashtag2 /> * with a url https://github.com/Automattic/wp-calypso#security that has a hash in it` * * componentMap = { * Hashtag0: <a href="https://twitter.com/hashtag/breaking" ...>#breaking</a>, * Hashtag1: <a href="https://twitter.com/hashtag/hashtag" ...>#hashtag</a>, * Hashtag2: <a href="https://twitter.com/hashtag/web" ...>#web</a> * } */ } // Convert newlines to <br> tags. /** * BEFORE: * result = 'This is a text\nwith a newline\nin it' */ result = result.replace(/\n/g, '<br />'); componentMap.br = (0, jsx_runtime_1.jsx)("br", {}); /** * AFTER: * result = 'This is a text<br />with a newline<br />in it' * componentMap = { br: <br /> } */ return (0, element_1.createInterpolateElement)(result, componentMap); } //# sourceMappingURL=helpers.js.map