@automattic/social-previews 3.2.3 → 3.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -50,6 +50,43 @@ var formatMastodonDate = new Intl.DateTimeFormat("en-US", {
50
50
  day: "numeric",
51
51
  year: "numeric"
52
52
  }).format;
53
+ var collapseWhitespace = (text) => text.replace(/\s+/g, " ").trim();
54
+ var countOccurrences = (haystack, needle) => {
55
+ let count = 0;
56
+ for (let pos = haystack.indexOf(needle); pos !== -1; pos = haystack.indexOf(needle, pos + 1)) {
57
+ count++;
58
+ }
59
+ return count;
60
+ };
61
+ var nthIndexOf = (haystack, needle, n) => {
62
+ let pos = haystack.indexOf(needle);
63
+ while (pos !== -1 && n > 0) {
64
+ n--;
65
+ pos = haystack.indexOf(needle, pos + 1);
66
+ }
67
+ return pos;
68
+ };
69
+ function parseHyperlinks(html) {
70
+ if (!html) {
71
+ return [];
72
+ }
73
+ const doc = document.implementation.createHTMLDocument("");
74
+ doc.body.innerHTML = html;
75
+ const links = [];
76
+ for (const anchor of Array.from(doc.body.querySelectorAll("a[href]"))) {
77
+ const href = anchor.getAttribute("href") ?? "";
78
+ const text = collapseWhitespace(anchor.textContent ?? "");
79
+ if (!/^https?:\/\//i.test(href) || "" === text || text === href) {
80
+ continue;
81
+ }
82
+ const range = doc.createRange();
83
+ range.selectNodeContents(doc.body);
84
+ range.setEndBefore(anchor);
85
+ const occurrence = countOccurrences(collapseWhitespace(range.toString()), text);
86
+ links.push({ text, href, occurrence });
87
+ }
88
+ return links;
89
+ }
53
90
  var hashtagUrlMap = {
54
91
  twitter: "https://twitter.com/hashtag/%1$s",
55
92
  facebook: "https://www.facebook.com/hashtag/%1$s",
@@ -68,7 +105,8 @@ function preparePreviewText(text, options) {
68
105
  maxLines,
69
106
  hyperlinkHashtags = true,
70
107
  // Instagram doesn't support hyperlink URLs at the moment.
71
- hyperlinkUrls = "instagram" !== platform
108
+ hyperlinkUrls = "instagram" !== platform,
109
+ hyperlinks
72
110
  } = options;
73
111
  let result = stripHtmlTags(text);
74
112
  result = result.replaceAll(/(?:\s*[\n\r]){2,}/g, "\n\n");
@@ -98,6 +136,31 @@ function preparePreviewText(text, options) {
98
136
  result = result.replace(fullMatch, `${whitespace}<Hashtag${index} />`);
99
137
  });
100
138
  }
139
+ if (hyperlinks?.length) {
140
+ const matches = [];
141
+ hyperlinks.forEach(({ text: anchorText, href, occurrence = 0 }, index) => {
142
+ if (!anchorText) {
143
+ return;
144
+ }
145
+ const pos = nthIndexOf(result, anchorText, occurrence);
146
+ if (pos === -1) {
147
+ return;
148
+ }
149
+ const overlaps = matches.some(
150
+ (match) => pos < match.pos + match.text.length && match.pos < pos + anchorText.length
151
+ );
152
+ if (!overlaps) {
153
+ matches.push({ pos, text: anchorText, href, index });
154
+ }
155
+ });
156
+ matches.sort((a, b) => b.pos - a.pos);
157
+ for (const { pos, text: anchorText, href, index } of matches) {
158
+ const token = `Hyperlink${index}`;
159
+ componentMap[token] = /* @__PURE__ */ jsx("a", { href, rel: "noopener noreferrer", target: "_blank" });
160
+ const wrapped = `<${token}>${anchorText}</${token}>`;
161
+ result = result.slice(0, pos) + wrapped + result.slice(pos + anchorText.length);
162
+ }
163
+ }
101
164
  result = result.replace(/\n/g, "<br />");
102
165
  componentMap.br = /* @__PURE__ */ jsx("br", {});
103
166
  return createInterpolateElement(result, componentMap);
@@ -836,7 +899,8 @@ var TumblrPostPreview = ({
836
899
  image,
837
900
  user,
838
901
  url,
839
- media
902
+ media,
903
+ hyperlinks
840
904
  }) => {
841
905
  const avatarUrl = user?.avatarUrl;
842
906
  const mediaItem = media?.[0];
@@ -847,7 +911,8 @@ var TumblrPostPreview = ({
847
911
  /* @__PURE__ */ jsxs16("div", { className: "tumblr-preview__body", children: [
848
912
  title ? /* @__PURE__ */ jsx25("div", { className: "tumblr-preview__title", children: tumblrTitle(title) }) : null,
849
913
  description && /* @__PURE__ */ jsx25("div", { className: "tumblr-preview__description", children: /* @__PURE__ */ jsx25(ExpandableText, { text: description, children: (visibleText) => preparePreviewText(tumblrDescription(visibleText), {
850
- platform: "tumblr"
914
+ platform: "tumblr",
915
+ hyperlinks
851
916
  }) }) }),
852
917
  mediaItem ? /* @__PURE__ */ jsx25("div", { className: "tumblr-preview__media-item", children: mediaItem.type.startsWith("video/") ? /* @__PURE__ */ jsx25("video", { controls: true, className: "tumblr-preview__media--video", children: /* @__PURE__ */ jsx25("source", { src: mediaItem.url, type: mediaItem.type }) }) : /* @__PURE__ */ jsx25("img", { className: "tumblr-preview__image", src: mediaItem.url, alt: "" }) }) : image && /* @__PURE__ */ jsx25(
853
918
  "img",
@@ -1275,8 +1340,6 @@ var DEFAULT_MASTODON_INSTANCE = "mastodon.social";
1275
1340
  // src/mastodon-preview/helpers.ts
1276
1341
  var TITLE_LENGTH4 = 200;
1277
1342
  var BODY_LENGTH = 500;
1278
- var URL_LENGTH2 = 30;
1279
- var BODY_CHAR_LIMIT = BODY_LENGTH - URL_LENGTH2;
1280
1343
  var ADDRESS_PATTERN = /^@([^@]*)@([^@]*)$/i;
1281
1344
  var mastodonTitle = (text) => firstValid(
1282
1345
  shortEnough(TITLE_LENGTH4),
@@ -1286,11 +1349,10 @@ var mastodonBody = (text, options) => {
1286
1349
  const { instance, offset } = options;
1287
1350
  return preparePreviewText(text, {
1288
1351
  platform: "mastodon",
1289
- maxChars: BODY_LENGTH - URL_LENGTH2 - offset,
1352
+ maxChars: BODY_LENGTH - offset,
1290
1353
  hashtagDomain: instance
1291
1354
  });
1292
1355
  };
1293
- var mastodonUrl = (text) => firstValid(shortEnough(URL_LENGTH2), hardTruncation(URL_LENGTH2))(stripHtmlTags(text)) || "";
1294
1356
  var getMastodonAddressDetails = (address) => {
1295
1357
  const matches = address.match(ADDRESS_PATTERN);
1296
1358
  return {
@@ -1393,7 +1455,7 @@ import clsx4 from "clsx";
1393
1455
  // src/mastodon-preview/post/body/index.tsx
1394
1456
  import { Fragment as Fragment4, jsx as jsx40, jsxs as jsxs29 } from "react/jsx-runtime";
1395
1457
  var MastonPostBody = (props) => {
1396
- const { title, description, customText, url, user, children } = props;
1458
+ const { title, description, customText, user, children } = props;
1397
1459
  const instance = user?.address ? getMastodonAddressDetails(user.address).instance : "";
1398
1460
  const options = {
1399
1461
  instance,
@@ -1418,7 +1480,6 @@ var MastonPostBody = (props) => {
1418
1480
  }
1419
1481
  return /* @__PURE__ */ jsxs29("div", { className: "mastodon-preview__body", children: [
1420
1482
  bodyTxt,
1421
- /* @__PURE__ */ jsx40("a", { href: url, target: "_blank", rel: "noreferrer noopener", children: mastodonUrl(url.replace(/^https?:\/\//, "")) }),
1422
1483
  children
1423
1484
  ] });
1424
1485
  };
@@ -1817,28 +1878,35 @@ var actions_default4 = BlueskyPostActions;
1817
1878
  // src/bluesky-preview/helpers.ts
1818
1879
  var TITLE_LENGTH5 = 200;
1819
1880
  var BODY_LENGTH2 = 300;
1820
- var URL_LENGTH3 = 40;
1821
- var BODY_CHAR_LIMIT2 = BODY_LENGTH2 - URL_LENGTH3;
1881
+ var URL_LENGTH2 = 40;
1882
+ var BODY_CHAR_LIMIT = BODY_LENGTH2 - URL_LENGTH2;
1822
1883
  var blueskyTitle = (text) => firstValid(
1823
1884
  shortEnough(TITLE_LENGTH5),
1824
1885
  hardTruncation(TITLE_LENGTH5)
1825
1886
  )(stripHtmlTags(text)) || "";
1826
1887
  var blueskyBody = (text, options = {}) => {
1827
- const { offset = 0, reserveUrlSpace = true } = options;
1888
+ const { offset = 0, reserveUrlSpace = true, hyperlinks } = options;
1828
1889
  return preparePreviewText(text, {
1829
1890
  platform: "bluesky",
1830
- maxChars: BODY_LENGTH2 - (reserveUrlSpace ? URL_LENGTH3 : 0) - offset
1891
+ maxChars: BODY_LENGTH2 - (reserveUrlSpace ? URL_LENGTH2 : 0) - offset,
1892
+ hyperlinks
1831
1893
  });
1832
1894
  };
1833
- var blueskyUrl = (text) => firstValid(shortEnough(URL_LENGTH3), hardTruncation(URL_LENGTH3))(stripHtmlTags(text)) || "";
1895
+ var blueskyUrl = (text) => firstValid(shortEnough(URL_LENGTH2), hardTruncation(URL_LENGTH2))(stripHtmlTags(text)) || "";
1834
1896
 
1835
1897
  // src/bluesky-preview/post/body/index.tsx
1836
1898
  import { Fragment as Fragment6, jsx as jsx54, jsxs as jsxs37 } from "react/jsx-runtime";
1837
- var BlueskyPostBody = ({ customText, url, children, appendUrl }) => {
1899
+ var BlueskyPostBody = ({
1900
+ customText,
1901
+ url,
1902
+ children,
1903
+ appendUrl,
1904
+ hyperlinks
1905
+ }) => {
1838
1906
  const showUrl = appendUrl && !!url && !customText?.includes(url);
1839
1907
  return /* @__PURE__ */ jsxs37("div", { className: "bluesky-preview__body", children: [
1840
1908
  customText ? /* @__PURE__ */ jsxs37(Fragment6, { children: [
1841
- /* @__PURE__ */ jsx54("div", { children: blueskyBody(customText, { reserveUrlSpace: showUrl }) }),
1909
+ /* @__PURE__ */ jsx54("div", { children: blueskyBody(customText, { reserveUrlSpace: showUrl, hyperlinks }) }),
1842
1910
  showUrl ? /* @__PURE__ */ jsxs37(Fragment6, { children: [
1843
1911
  /* @__PURE__ */ jsx54("br", {}),
1844
1912
  /* @__PURE__ */ jsx54("a", { href: url, target: "_blank", rel: "noreferrer noopener", children: blueskyUrl(url.replace(/^https?:\/\//, "")) })
@@ -2445,6 +2513,7 @@ export {
2445
2513
  TumblrPreviews,
2446
2514
  TwitterLinkPreview,
2447
2515
  TwitterPostPreview,
2448
- TwitterPreviews
2516
+ TwitterPreviews,
2517
+ parseHyperlinks
2449
2518
  };
2450
2519
  //# sourceMappingURL=index.mjs.map