@automattic/social-previews 2.0.0 → 2.0.1-beta.1

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.
Files changed (128) hide show
  1. package/dist/cjs/facebook-preview/custom-text.js +4 -3
  2. package/dist/cjs/facebook-preview/custom-text.js.map +1 -1
  3. package/dist/cjs/facebook-preview/helpers.js +2 -4
  4. package/dist/cjs/facebook-preview/helpers.js.map +1 -1
  5. package/dist/cjs/helpers.js +80 -17
  6. package/dist/cjs/helpers.js.map +1 -1
  7. package/dist/cjs/index.js +1 -0
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/instagram-preview/constants.js +6 -0
  10. package/dist/cjs/instagram-preview/constants.js.map +1 -0
  11. package/dist/cjs/instagram-preview/icons/bookmark.js +9 -0
  12. package/dist/cjs/instagram-preview/icons/bookmark.js.map +1 -0
  13. package/dist/cjs/instagram-preview/icons/comment.js +9 -0
  14. package/dist/cjs/instagram-preview/icons/comment.js.map +1 -0
  15. package/dist/cjs/instagram-preview/icons/default-avatar.js +9 -0
  16. package/dist/cjs/instagram-preview/icons/default-avatar.js.map +1 -0
  17. package/dist/cjs/instagram-preview/icons/heart.js +9 -0
  18. package/dist/cjs/instagram-preview/icons/heart.js.map +1 -0
  19. package/dist/cjs/instagram-preview/icons/menu.js +9 -0
  20. package/dist/cjs/instagram-preview/icons/menu.js.map +1 -0
  21. package/dist/cjs/instagram-preview/icons/share.js +9 -0
  22. package/dist/cjs/instagram-preview/icons/share.js.map +1 -0
  23. package/dist/cjs/instagram-preview/index.js +6 -0
  24. package/dist/cjs/instagram-preview/index.js.map +1 -0
  25. package/dist/cjs/instagram-preview/post-preview.js +25 -0
  26. package/dist/cjs/instagram-preview/post-preview.js.map +1 -0
  27. package/dist/cjs/instagram-preview/previews.js +15 -0
  28. package/dist/cjs/instagram-preview/previews.js.map +1 -0
  29. package/dist/cjs/instagram-preview/style.scss +102 -0
  30. package/dist/cjs/instagram-preview/types.js +3 -0
  31. package/dist/cjs/instagram-preview/types.js.map +1 -0
  32. package/dist/cjs/linkedin-preview/post-preview.js +5 -10
  33. package/dist/cjs/linkedin-preview/post-preview.js.map +1 -1
  34. package/dist/cjs/twitter-preview/post-preview.js +1 -1
  35. package/dist/cjs/twitter-preview/post-preview.js.map +1 -1
  36. package/dist/cjs/twitter-preview/text.js +1 -6
  37. package/dist/cjs/twitter-preview/text.js.map +1 -1
  38. package/dist/esm/facebook-preview/custom-text.js +6 -5
  39. package/dist/esm/facebook-preview/custom-text.js.map +1 -1
  40. package/dist/esm/facebook-preview/helpers.js +1 -2
  41. package/dist/esm/facebook-preview/helpers.js.map +1 -1
  42. package/dist/esm/helpers.js +79 -16
  43. package/dist/esm/helpers.js.map +1 -1
  44. package/dist/esm/index.js +1 -0
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/instagram-preview/constants.js +3 -0
  47. package/dist/esm/instagram-preview/constants.js.map +1 -0
  48. package/dist/esm/instagram-preview/icons/bookmark.js +5 -0
  49. package/dist/esm/instagram-preview/icons/bookmark.js.map +1 -0
  50. package/dist/esm/instagram-preview/icons/comment.js +5 -0
  51. package/dist/esm/instagram-preview/icons/comment.js.map +1 -0
  52. package/dist/esm/instagram-preview/icons/default-avatar.js +5 -0
  53. package/dist/esm/instagram-preview/icons/default-avatar.js.map +1 -0
  54. package/dist/esm/instagram-preview/icons/heart.js +5 -0
  55. package/dist/esm/instagram-preview/icons/heart.js.map +1 -0
  56. package/dist/esm/instagram-preview/icons/menu.js +5 -0
  57. package/dist/esm/instagram-preview/icons/menu.js.map +1 -0
  58. package/dist/esm/instagram-preview/icons/share.js +5 -0
  59. package/dist/esm/instagram-preview/icons/share.js.map +1 -0
  60. package/dist/esm/instagram-preview/index.js +3 -0
  61. package/dist/esm/instagram-preview/index.js.map +1 -0
  62. package/dist/esm/instagram-preview/post-preview.js +21 -0
  63. package/dist/esm/instagram-preview/post-preview.js.map +1 -0
  64. package/dist/esm/instagram-preview/previews.js +10 -0
  65. package/dist/esm/instagram-preview/previews.js.map +1 -0
  66. package/dist/esm/instagram-preview/style.scss +102 -0
  67. package/dist/esm/instagram-preview/types.js +2 -0
  68. package/dist/esm/instagram-preview/types.js.map +1 -0
  69. package/dist/esm/linkedin-preview/post-preview.js +5 -10
  70. package/dist/esm/linkedin-preview/post-preview.js.map +1 -1
  71. package/dist/esm/twitter-preview/post-preview.js +1 -1
  72. package/dist/esm/twitter-preview/post-preview.js.map +1 -1
  73. package/dist/esm/twitter-preview/text.js +1 -6
  74. package/dist/esm/twitter-preview/text.js.map +1 -1
  75. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  76. package/dist/tsconfig.tsbuildinfo +1 -1
  77. package/dist/types/facebook-preview/custom-text.d.ts.map +1 -1
  78. package/dist/types/facebook-preview/helpers.d.ts +1 -1
  79. package/dist/types/facebook-preview/helpers.d.ts.map +1 -1
  80. package/dist/types/helpers.d.ts +5 -2
  81. package/dist/types/helpers.d.ts.map +1 -1
  82. package/dist/types/index.d.ts +1 -0
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/instagram-preview/constants.d.ts +3 -0
  85. package/dist/types/instagram-preview/constants.d.ts.map +1 -0
  86. package/dist/types/instagram-preview/icons/bookmark.d.ts +2 -0
  87. package/dist/types/instagram-preview/icons/bookmark.d.ts.map +1 -0
  88. package/dist/types/instagram-preview/icons/comment.d.ts +2 -0
  89. package/dist/types/instagram-preview/icons/comment.d.ts.map +1 -0
  90. package/dist/types/instagram-preview/icons/default-avatar.d.ts +2 -0
  91. package/dist/types/instagram-preview/icons/default-avatar.d.ts.map +1 -0
  92. package/dist/types/instagram-preview/icons/heart.d.ts +2 -0
  93. package/dist/types/instagram-preview/icons/heart.d.ts.map +1 -0
  94. package/dist/types/instagram-preview/icons/menu.d.ts +2 -0
  95. package/dist/types/instagram-preview/icons/menu.d.ts.map +1 -0
  96. package/dist/types/instagram-preview/icons/share.d.ts +2 -0
  97. package/dist/types/instagram-preview/icons/share.d.ts.map +1 -0
  98. package/dist/types/instagram-preview/index.d.ts +3 -0
  99. package/dist/types/instagram-preview/index.d.ts.map +1 -0
  100. package/dist/types/instagram-preview/post-preview.d.ts +4 -0
  101. package/dist/types/instagram-preview/post-preview.d.ts.map +1 -0
  102. package/dist/types/instagram-preview/previews.d.ts +3 -0
  103. package/dist/types/instagram-preview/previews.d.ts.map +1 -0
  104. package/dist/types/instagram-preview/types.d.ts +8 -0
  105. package/dist/types/instagram-preview/types.d.ts.map +1 -0
  106. package/dist/types/linkedin-preview/post-preview.d.ts.map +1 -1
  107. package/dist/types/twitter-preview/text.d.ts.map +1 -1
  108. package/package.json +1 -1
  109. package/src/facebook-preview/custom-text.tsx +8 -6
  110. package/src/facebook-preview/helpers.ts +1 -7
  111. package/src/helpers.tsx +186 -0
  112. package/src/index.ts +1 -0
  113. package/src/instagram-preview/constants.tsx +2 -0
  114. package/src/instagram-preview/icons/bookmark.tsx +21 -0
  115. package/src/instagram-preview/icons/comment.tsx +20 -0
  116. package/src/instagram-preview/icons/default-avatar.tsx +10 -0
  117. package/src/instagram-preview/icons/heart.tsx +14 -0
  118. package/src/instagram-preview/icons/menu.tsx +24 -0
  119. package/src/instagram-preview/icons/share.tsx +30 -0
  120. package/src/instagram-preview/index.tsx +2 -0
  121. package/src/instagram-preview/post-preview.tsx +71 -0
  122. package/src/instagram-preview/previews.tsx +33 -0
  123. package/src/instagram-preview/style.scss +102 -0
  124. package/src/instagram-preview/types.ts +9 -0
  125. package/src/linkedin-preview/post-preview.tsx +7 -12
  126. package/src/twitter-preview/post-preview.tsx +1 -1
  127. package/src/twitter-preview/text.tsx +3 -9
  128. package/src/helpers.ts +0 -107
@@ -0,0 +1,3 @@
1
+ export declare const FEED_TEXT_MAX_LENGTH = 120;
2
+ export declare const FEED_TEXT_MAX_LINES = 2;
3
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/instagram-preview/constants.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,eAAO,MAAM,mBAAmB,IAAI,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const Bookmark: React.FC;
2
+ //# sourceMappingURL=bookmark.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bookmark.d.ts","sourceRoot":"","sources":["../../../../src/instagram-preview/icons/bookmark.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,QAAQ,EAAE,KAAK,CAAC,EAoB5B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const Comment: React.FC;
2
+ //# sourceMappingURL=comment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment.d.ts","sourceRoot":"","sources":["../../../../src/instagram-preview/icons/comment.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,EAAE,KAAK,CAAC,EAmB3B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const DefaultAvatar: React.FC;
2
+ //# sourceMappingURL=default-avatar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-avatar.d.ts","sourceRoot":"","sources":["../../../../src/instagram-preview/icons/default-avatar.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EASjC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const Heart: React.FC;
2
+ //# sourceMappingURL=heart.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heart.d.ts","sourceRoot":"","sources":["../../../../src/instagram-preview/icons/heart.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAazB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const Menu: React.FC;
2
+ //# sourceMappingURL=menu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../../../../src/instagram-preview/icons/menu.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,IAAI,EAAE,KAAK,CAAC,EAuBxB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const Share: React.FC;
2
+ //# sourceMappingURL=share.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share.d.ts","sourceRoot":"","sources":["../../../../src/instagram-preview/icons/share.tsx"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EA6BzB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './post-preview';
2
+ export * from './previews';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/instagram-preview/index.tsx"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { InstagramPreviewProps } from './types';
2
+ import './style.scss';
3
+ export declare function InstagramPostPreview({ image, name, profileImage, caption, }: InstagramPreviewProps): JSX.Element;
4
+ //# sourceMappingURL=post-preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-preview.d.ts","sourceRoot":"","sources":["../../../src/instagram-preview/post-preview.tsx"],"names":[],"mappings":"AASA,OAAO,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAEhD,OAAO,cAAc,CAAC;AAEtB,wBAAgB,oBAAoB,CAAE,EACrC,KAAK,EACL,IAAI,EACJ,YAAY,EACZ,OAAO,GACP,EAAE,qBAAqB,eAoDvB"}
@@ -0,0 +1,3 @@
1
+ import type { InstagramPreviewsProps } from './types';
2
+ export declare const InstagramPreviews: React.FC<InstagramPreviewsProps>;
3
+ //# sourceMappingURL=previews.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"previews.d.ts","sourceRoot":"","sources":["../../../src/instagram-preview/previews.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAEtD,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAE,sBAAsB,CA2B/D,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { SocialPreviewBaseProps, SocialPreviewsBaseProps } from '../types';
2
+ export declare type InstagramPreviewProps = Pick<SocialPreviewBaseProps, 'image'> & {
3
+ name: string;
4
+ profileImage: string;
5
+ caption?: string;
6
+ };
7
+ export declare type InstagramPreviewsProps = InstagramPreviewProps & SocialPreviewsBaseProps;
8
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/instagram-preview/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAE3E,oBAAY,qBAAqB,GAAG,IAAI,CAAE,sBAAsB,EAAE,OAAO,CAAE,GAAG;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,oBAAY,sBAAsB,GAAG,qBAAqB,GAAG,uBAAuB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"post-preview.d.ts","sourceRoot":"","sources":["../../../src/linkedin-preview/post-preview.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,cAAc,CAAC;AAEtB,wBAAgB,mBAAmB,CAAE,EACpC,eAAmB,EACnB,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,KAAK,EACL,KAAK,EACL,GAAG,GACH,EAAE,oBAAoB,eAmGtB"}
1
+ {"version":3,"file":"post-preview.d.ts","sourceRoot":"","sources":["../../../src/linkedin-preview/post-preview.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,cAAc,CAAC;AAEtB,wBAAgB,mBAAmB,CAAE,EACpC,eAAmB,EACnB,KAAK,EACL,QAAQ,EACR,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,KAAK,EACL,KAAK,EACL,GAAG,GACH,EAAE,oBAAoB,eA8FtB"}
@@ -1 +1 @@
1
- {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../../src/twitter-preview/text.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC,eAAO,MAAM,IAAI,EAAE,KAAK,CAAC,EAAE,CAAE,SAAS,CAqBrC,CAAC"}
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../../src/twitter-preview/text.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC,eAAO,MAAM,IAAI,EAAE,KAAK,CAAC,EAAE,CAAE,SAAS,CAerC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/social-previews",
3
- "version": "2.0.0",
3
+ "version": "2.0.1-beta.1",
4
4
  "description": "A suite of components to generate previews for a post for both social and search engines.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -1,5 +1,5 @@
1
- import { hasTag } from '../helpers';
2
- import { facebookCustomText } from './helpers';
1
+ import { hasTag, preparePreviewText } from '../helpers';
2
+ import { CUSTOM_TEXT_LENGTH } from './helpers';
3
3
 
4
4
  type Props = {
5
5
  text: string;
@@ -25,10 +25,12 @@ const CustomText: React.FC< Props > = ( { text, url, forceUrlDisplay } ) => {
25
25
 
26
26
  return (
27
27
  <p className="facebook-preview__custom-text">
28
- <span
29
- // eslint-disable-next-line react/no-danger
30
- dangerouslySetInnerHTML={ { __html: facebookCustomText( text, { allowedTags: [ 'a' ] } ) } }
31
- />
28
+ <span>
29
+ { preparePreviewText( text, {
30
+ platform: 'facebook',
31
+ maxChars: CUSTOM_TEXT_LENGTH,
32
+ } ) }
33
+ </span>
32
34
  { postLink }
33
35
  </p>
34
36
  );
@@ -2,7 +2,7 @@ import { firstValid, hardTruncation, shortEnough, stripHtmlTags, Formatter } fro
2
2
 
3
3
  const TITLE_LENGTH = 110;
4
4
  const DESCRIPTION_LENGTH = 200;
5
- const CUSTOM_TEXT_LENGTH = 440;
5
+ export const CUSTOM_TEXT_LENGTH = 440;
6
6
 
7
7
  export const baseDomain: Formatter = ( url ) =>
8
8
  url
@@ -20,9 +20,3 @@ export const facebookDescription: Formatter = ( text ) =>
20
20
  shortEnough( DESCRIPTION_LENGTH ),
21
21
  hardTruncation( DESCRIPTION_LENGTH )
22
22
  )( stripHtmlTags( text ) ) || '';
23
-
24
- export const facebookCustomText: Formatter = ( text, options ) =>
25
- firstValid(
26
- shortEnough( CUSTOM_TEXT_LENGTH ),
27
- hardTruncation( CUSTOM_TEXT_LENGTH )
28
- )( stripHtmlTags( text, options?.allowedTags ) ) || '';
@@ -0,0 +1,186 @@
1
+ import { createInterpolateElement } from '@wordpress/element';
2
+ import { sprintf } from '@wordpress/i18n';
3
+
4
+ export type Formatter = ( text: string, options?: any ) => string;
5
+ type AugmentFormatterReturnType< T extends Formatter, TNewReturn > = (
6
+ ...a: Parameters< T >
7
+ ) => ReturnType< T > | TNewReturn;
8
+ type ConditionalFormatter = AugmentFormatterReturnType< Formatter, boolean >;
9
+ type NullableFormatter = AugmentFormatterReturnType< Formatter, undefined >;
10
+
11
+ export const baseDomain = ( url: string ): string =>
12
+ url
13
+ .replace( /^[^/]+[/]*/, '' ) // strip leading protocol
14
+ .replace( /\/.*$/, '' ); // strip everything after the domain
15
+
16
+ export const shortEnough: ( n: number ) => ConditionalFormatter = ( limit ) => ( title ) =>
17
+ title.length <= limit ? title : false;
18
+
19
+ export const truncatedAtSpace: ( a: number, b: number ) => ConditionalFormatter =
20
+ ( lower, upper ) => ( fullTitle ) => {
21
+ const title = fullTitle.slice( 0, upper );
22
+ const lastSpace = title.lastIndexOf( ' ' );
23
+
24
+ return lastSpace > lower && lastSpace < upper
25
+ ? title.slice( 0, lastSpace ).concat( '…' )
26
+ : false;
27
+ };
28
+
29
+ export const hardTruncation: ( n: number ) => Formatter = ( limit ) => ( title ) =>
30
+ title.slice( 0, limit ).concat( '…' );
31
+
32
+ export const firstValid: ( ...args: ConditionalFormatter[] ) => NullableFormatter =
33
+ ( ...predicates ) =>
34
+ ( a ) =>
35
+ ( predicates.find( ( p ) => false !== p( a ) ) as Formatter )?.( a );
36
+
37
+ export const stripHtmlTags: Formatter = ( description, allowedTags = [] ) => {
38
+ const pattern = new RegExp( `(<([^${ allowedTags.join( '' ) }>]+)>)`, 'gi' );
39
+
40
+ return description ? description.replace( pattern, '' ) : '';
41
+ };
42
+
43
+ export const hasTag = ( text: string, tag: string ): boolean => {
44
+ const pattern = new RegExp( `<${ tag }[^>]*>`, 'gi' );
45
+
46
+ return pattern.test( text );
47
+ };
48
+
49
+ export const formatTweetDate = new Intl.DateTimeFormat( 'en-US', {
50
+ // Result: "Apr 7", "Dec 31"
51
+ month: 'short',
52
+ day: 'numeric',
53
+ } ).format;
54
+
55
+ export type Platform = 'twitter' | 'facebook' | 'linkedin' | 'instagram';
56
+
57
+ type PreviewTextOptions = {
58
+ platform: Platform;
59
+ maxChars?: number;
60
+ maxLines?: number;
61
+ hyperlinkUrls?: boolean;
62
+ hyperlinkHashtags?: boolean;
63
+ };
64
+
65
+ export const hashtagUrlMap: Record< Platform, string > = {
66
+ twitter: 'https://twitter.com/hashtag/%s',
67
+ facebook: 'https://www.facebook.com/hashtag/%s',
68
+ linkedin: 'https://www.linkedin.com/feed/hashtag/?keywords=%s',
69
+ instagram: 'https://www.instagram.com/explore/tags/%s',
70
+ };
71
+
72
+ /**
73
+ * Prepares the text for the preview.
74
+ */
75
+ export function preparePreviewText( text: string, options: PreviewTextOptions ): React.ReactNode {
76
+ const {
77
+ platform,
78
+ maxChars,
79
+ maxLines,
80
+ hyperlinkHashtags = true,
81
+ // Instagram doesn't support hyperlink URLs at the moment.
82
+ hyperlinkUrls = 'instagram' !== platform,
83
+ } = options;
84
+
85
+ let result = stripHtmlTags( text );
86
+
87
+ if ( maxChars && result.length > maxChars ) {
88
+ result = result.substring( 0, maxChars );
89
+ }
90
+
91
+ if ( maxLines ) {
92
+ const lines = result.split( '\n' );
93
+
94
+ if ( lines.length > maxLines ) {
95
+ result = lines.slice( 0, maxLines ).join( '\n' );
96
+ }
97
+ }
98
+
99
+ const componentMap: Record< string, React.ReactNode > = {};
100
+
101
+ if ( hyperlinkUrls ) {
102
+ // Convert URLs to hyperlinks.
103
+ // TODO: Use a better regex here to match the URLs without protocol.
104
+ const urls = result.match( /(https?:\/\/\S+)/g ) || [];
105
+
106
+ /**
107
+ * BEFORE:
108
+ * result = 'Check out this cool site: https://wordpress.org and this one: https://wordpress.com'
109
+ */
110
+ urls.forEach( ( url, index ) => {
111
+ // Add the element to the component map.
112
+ componentMap[ `Link${ index }` ] = (
113
+ <a href={ url } rel="noopener noreferrer" target="_blank">
114
+ { url }
115
+ </a>
116
+ );
117
+ // Replace the URL with the component placeholder.
118
+ result = result.replace( url, `<Link${ index } />` );
119
+ } );
120
+ /**
121
+ * AFTER:
122
+ * result = 'Check out this cool site: <Link0 /> and this one: <Link1 />'
123
+ * componentMap = {
124
+ * Link0: <a href="https://wordpress.org" ...>https://wordpress.org</a>,
125
+ * Link1: <a href="https://wordpress.com" ...>https://wordpress.com</a>
126
+ * }
127
+ */
128
+ }
129
+
130
+ // Convert hashtags to hyperlinks.
131
+ if ( hyperlinkHashtags && hashtagUrlMap[ platform ] ) {
132
+ /**
133
+ * We need to ensure that only the standalone hashtags are matched.
134
+ * For example, we don't want to match the hash in the URL.
135
+ * Thus we need to match the whitespace character before the hashtag or the beginning of the string.
136
+ */
137
+ const hashtags = result.matchAll( /(^|\s)#(\w+)/g );
138
+
139
+ const hashtagUrl = hashtagUrlMap[ platform ];
140
+
141
+ /**
142
+ * BEFORE:
143
+ * result = `#breaking text with a #hashtag on the #web
144
+ * with a url https://github.com/Automattic/wp-calypso#security that has a hash in it`
145
+ */
146
+ [ ...hashtags ].forEach( ( [ fullMatch, whitespace, hashtag ], index ) => {
147
+ const url = sprintf( hashtagUrl, hashtag );
148
+
149
+ // Add the element to the component map.
150
+ componentMap[ `Hashtag${ index }` ] = (
151
+ <a href={ url } rel="noopener noreferrer" target="_blank">
152
+ { `#${ hashtag }` }
153
+ </a>
154
+ );
155
+
156
+ // Replace the hashtag with the component placeholder.
157
+ result = result.replace( fullMatch, `${ whitespace }<Hashtag${ index } />` );
158
+ } );
159
+ /**
160
+ * AFTER:
161
+ * result = `<Hashtag0 /> text with a <Hashtag1 /> on the <Hashtag2 />
162
+ * with a url https://github.com/Automattic/wp-calypso#security that has a hash in it`
163
+ *
164
+ * componentMap = {
165
+ * Hashtag0: <a href="https://twitter.com/hashtag/breaking" ...>#breaking</a>,
166
+ * Hashtag1: <a href="https://twitter.com/hashtag/hashtag" ...>#hashtag</a>,
167
+ * Hashtag2: <a href="https://twitter.com/hashtag/web" ...>#web</a>
168
+ * }
169
+ */
170
+ }
171
+
172
+ // Convert newlines to <br> tags.
173
+ /**
174
+ * BEFORE:
175
+ * result = 'This is a text\nwith a newline\nin it'
176
+ */
177
+ result = result.replace( /\n/g, '<br />' );
178
+ componentMap.br = <br />;
179
+ /**
180
+ * AFTER:
181
+ * result = 'This is a text<br />with a newline<br />in it'
182
+ * componentMap = { br: <br /> }
183
+ */
184
+
185
+ return createInterpolateElement( result, componentMap );
186
+ }
package/src/index.ts CHANGED
@@ -4,4 +4,5 @@ export * from './linkedin-preview';
4
4
  export * from './tumblr-preview';
5
5
  export * from './facebook-preview';
6
6
  export * from './constants';
7
+ export * from './instagram-preview';
7
8
  export * from './types';
@@ -0,0 +1,2 @@
1
+ export const FEED_TEXT_MAX_LENGTH = 120;
2
+ export const FEED_TEXT_MAX_LINES = 2;
@@ -0,0 +1,21 @@
1
+ export const Bookmark: React.FC = () => {
2
+ return (
3
+ <svg
4
+ color="rgb(38, 38, 38)"
5
+ fill="rgb(38, 38, 38)"
6
+ height="24"
7
+ role="img"
8
+ viewBox="0 0 24 24"
9
+ width="24"
10
+ >
11
+ <polygon
12
+ fill="none"
13
+ points="20 21 12 13.44 4 21 4 3 20 3 20 21"
14
+ stroke="currentColor"
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ strokeWidth="2"
18
+ ></polygon>
19
+ </svg>
20
+ );
21
+ };
@@ -0,0 +1,20 @@
1
+ export const Comment: React.FC = () => {
2
+ return (
3
+ <svg
4
+ color="rgb(38, 38, 38)"
5
+ fill="rgb(38, 38, 38)"
6
+ height="24"
7
+ role="img"
8
+ viewBox="0 0 24 24"
9
+ width="24"
10
+ >
11
+ <path
12
+ d="M20.656 17.008a9.993 9.993 0 1 0-3.59 3.615L22 22Z"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ strokeLinejoin="round"
16
+ strokeWidth="2"
17
+ ></path>
18
+ </svg>
19
+ );
20
+ };
@@ -0,0 +1,10 @@
1
+ export const DefaultAvatar: React.FC = () => {
2
+ return (
3
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 340 340" width="340" height="340">
4
+ <path
5
+ fill="#DDD"
6
+ d="m169,.5a169,169 0 1,0 2,0zm0,86a76,76 0 1 1-2,0zM57,287q27-35 67-35h92q40,0 67,35a164,164 0 0,1-226,0"
7
+ />
8
+ </svg>
9
+ );
10
+ };
@@ -0,0 +1,14 @@
1
+ export const Heart: React.FC = () => {
2
+ return (
3
+ <svg
4
+ color="rgb(38, 38, 38)"
5
+ fill="rgb(38, 38, 38)"
6
+ height="24"
7
+ role="img"
8
+ viewBox="0 0 24 24"
9
+ width="24"
10
+ >
11
+ <path d="M16.792 3.904A4.989 4.989 0 0 1 21.5 9.122c0 3.072-2.652 4.959-5.197 7.222-2.512 2.243-3.865 3.469-4.303 3.752-.477-.309-2.143-1.823-4.303-3.752C5.141 14.072 2.5 12.167 2.5 9.122a4.989 4.989 0 0 1 4.708-5.218 4.21 4.21 0 0 1 3.675 1.941c.84 1.175.98 1.763 1.12 1.763s.278-.588 1.11-1.766a4.17 4.17 0 0 1 3.679-1.938m0-2a6.04 6.04 0 0 0-4.797 2.127 6.052 6.052 0 0 0-4.787-2.127A6.985 6.985 0 0 0 .5 9.122c0 3.61 2.55 5.827 5.015 7.97.283.246.569.494.853.747l1.027.918a44.998 44.998 0 0 0 3.518 3.018 2 2 0 0 0 2.174 0 45.263 45.263 0 0 0 3.626-3.115l.922-.824c.293-.26.59-.519.885-.774 2.334-2.025 4.98-4.32 4.98-7.94a6.985 6.985 0 0 0-6.708-7.218Z"></path>
12
+ </svg>
13
+ );
14
+ };
@@ -0,0 +1,24 @@
1
+ export const Menu: React.FC = () => {
2
+ return (
3
+ <svg width="17" height="5" viewBox="0 0 17 5" fill="none" xmlns="http://www.w3.org/2000/svg">
4
+ <path
5
+ d="M2.11865 3.5C2.67094 3.5 3.11865 3.05228 3.11865 2.5C3.11865 1.94772 2.67094 1.5 2.11865 1.5C1.56637 1.5 1.11865 1.94772 1.11865 2.5C1.11865 3.05228 1.56637 3.5 2.11865 3.5Z"
6
+ fill="black"
7
+ stroke="black"
8
+ strokeWidth="2"
9
+ />
10
+ <path
11
+ d="M8.55933 3.5C9.11161 3.5 9.55933 3.05228 9.55933 2.5C9.55933 1.94772 9.11161 1.5 8.55933 1.5C8.00704 1.5 7.55933 1.94772 7.55933 2.5C7.55933 3.05228 8.00704 3.5 8.55933 3.5Z"
12
+ fill="black"
13
+ stroke="black"
14
+ strokeWidth="2"
15
+ />
16
+ <path
17
+ d="M15 3.5C15.5523 3.5 16 3.05228 16 2.5C16 1.94772 15.5523 1.5 15 1.5C14.4477 1.5 14 1.94772 14 2.5C14 3.05228 14.4477 3.5 15 3.5Z"
18
+ fill="black"
19
+ stroke="black"
20
+ strokeWidth="2"
21
+ />
22
+ </svg>
23
+ );
24
+ };
@@ -0,0 +1,30 @@
1
+ export const Share: React.FC = () => {
2
+ return (
3
+ <svg
4
+ color="rgb(38, 38, 38)"
5
+ fill="rgb(38, 38, 38)"
6
+ height="24"
7
+ role="img"
8
+ viewBox="0 0 24 24"
9
+ width="24"
10
+ >
11
+ <line
12
+ fill="none"
13
+ stroke="currentColor"
14
+ strokeLinejoin="round"
15
+ strokeWidth="2"
16
+ x1="22"
17
+ x2="9.218"
18
+ y1="3"
19
+ y2="10.083"
20
+ ></line>
21
+ <polygon
22
+ fill="none"
23
+ points="11.698 20.334 22 3.001 2 3.001 9.218 10.084 11.698 20.334"
24
+ stroke="currentColor"
25
+ strokeLinejoin="round"
26
+ strokeWidth="2"
27
+ ></polygon>
28
+ </svg>
29
+ );
30
+ };
@@ -0,0 +1,2 @@
1
+ export * from './post-preview';
2
+ export * from './previews';
@@ -0,0 +1,71 @@
1
+ import { __ } from '@wordpress/i18n';
2
+ import { preparePreviewText } from '../helpers';
3
+ import { FEED_TEXT_MAX_LENGTH, FEED_TEXT_MAX_LINES } from './constants';
4
+ import { Bookmark as BookmarkIcon } from './icons/bookmark';
5
+ import { Comment as CommentIcon } from './icons/comment';
6
+ import { DefaultAvatar } from './icons/default-avatar';
7
+ import { Heart as HeartIcon } from './icons/heart';
8
+ import { Menu as MenuIcon } from './icons/menu';
9
+ import { Share as ShareIcon } from './icons/share';
10
+ import { InstagramPreviewProps } from './types';
11
+
12
+ import './style.scss';
13
+
14
+ export function InstagramPostPreview( {
15
+ image,
16
+ name,
17
+ profileImage,
18
+ caption,
19
+ }: InstagramPreviewProps ) {
20
+ const username = name || 'username';
21
+
22
+ return (
23
+ <div className="instagram-preview__wrapper">
24
+ <section className="instagram-preview__container">
25
+ <div className="instagram-preview__header">
26
+ <div className="instagram-preview__header--avatar">
27
+ { profileImage ? <img src={ profileImage } alt="" /> : <DefaultAvatar /> }
28
+ </div>
29
+ <div className="instagram-preview__header--profile">
30
+ <div className="instagram-preview__header--profile-name">{ username }</div>
31
+ <div className="instagram-preview__header--profile-menu">
32
+ <MenuIcon />
33
+ </div>
34
+ </div>
35
+ </div>
36
+ <div className="instagram-preview__media">
37
+ <img className="instagram-preview__media--image" src={ image } alt="" />
38
+ </div>
39
+ <div className="instagram-preview__content">
40
+ <section className="instagram-preview__content--actions">
41
+ <div className="instagram-preview__content--actions-primary">
42
+ <HeartIcon />
43
+ <CommentIcon />
44
+ <ShareIcon />
45
+ </div>
46
+ <div className="instagram-preview__content--actions-secondary">
47
+ <BookmarkIcon />
48
+ </div>
49
+ </section>
50
+ <div className="instagram-preview__content--body">
51
+ <div className="instagram-preview__content--name">{ username }</div>
52
+ &nbsp;
53
+ { caption ? (
54
+ <div className="instagram-preview__content--text">
55
+ { preparePreviewText( caption, {
56
+ platform: 'instagram',
57
+ maxChars: FEED_TEXT_MAX_LENGTH,
58
+ maxLines: FEED_TEXT_MAX_LINES,
59
+ hyperlinkUrls: false,
60
+ } ) }
61
+ </div>
62
+ ) : null }
63
+ </div>
64
+ <div className="instagram-preview__content--footer">
65
+ <span>{ __( 'View one comment', 'social-previews' ) }</span>
66
+ </div>
67
+ </div>
68
+ </section>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,33 @@
1
+ import { __ } from '@wordpress/i18n';
2
+ import SectionHeading from '../shared/section-heading';
3
+ import { InstagramPostPreview } from './post-preview';
4
+ import type { InstagramPreviewsProps } from './types';
5
+
6
+ export const InstagramPreviews: React.FC< InstagramPreviewsProps > = ( {
7
+ headingLevel,
8
+ hideLinkPreview,
9
+ hidePostPreview,
10
+ ...props
11
+ } ) => {
12
+ return (
13
+ <div className="social-preview instagram-preview">
14
+ { ! hidePostPreview && (
15
+ <section className="social-preview__section instagram-preview__section">
16
+ <SectionHeading level={ headingLevel }>
17
+ {
18
+ // translators: refers to a social post on Instagram
19
+ __( 'Your post', 'social-previews' )
20
+ }
21
+ </SectionHeading>
22
+ <p className="social-preview__section-desc">
23
+ { __(
24
+ 'This is what your social post will look like on Instagram:',
25
+ 'social-previews'
26
+ ) }
27
+ </p>
28
+ <InstagramPostPreview { ...props } />
29
+ </section>
30
+ ) }
31
+ </div>
32
+ );
33
+ };