11class ExtLinks {
22 static ExternalDomains = [
3- {
3+ { // XKCD
44 expression : `xkcd\\.com` ,
55 should_fetch : true ,
6- fetch_transform : ( ( post ) => post . url ) ,
7- function : function ( url , data ) {
8- const returnData = { } ;
6+ post_only : false ,
7+ fetch_transform : ( ( url ) => url ) ,
8+ link_data : function ( url , data ) {
99 // The image is located in the #comic div and is in the format "//imgs.xkcd.com/comics/.*?"
1010 const expression = / < d i v i d = \" c o m i c \" > .* ?< i m g s r c = \" ( .* ?i m g s \. x k c d \. c o m \/ c o m i c s \/ .* ?) \" .* ?\/ > / gis;
1111 const matches = [ ...data . matchAll ( expression ) ] ;
1212
1313 // We only care about the first match
1414 // and index 1 for the capture group
1515 if ( matches [ 0 ] && matches [ 0 ] [ 1 ] ) {
16+ return "https:" + matches [ 0 ] [ 1 ] ;
17+ }
18+
19+ return null ;
20+ } ,
21+ post_data : function ( url , data ) {
22+ const returnData = { } ;
23+
24+ if ( url ) {
1625 returnData . post_hint = "image" ;
17- returnData . url = "https:" + matches [ 0 ] [ 1 ] ;
26+ returnData . url = url ;
1827 returnData . thumbnail = returnData . url ;
1928 return returnData ;
2029 }
2130 return null ;
2231 } ,
32+ inline_replacement : ( ( url ) => `<a href="${ url } "><img class="inline" src="${ url } "></a>` ) ,
2333 } ,
24- {
25- expression : `imgur\\.com` ,
34+ { // Generic imgur/reddit links
35+ // imgur.com, preview.reddi.it, i.redd.it, v.redd.it
36+ expression : `(?:imgur\\.com|(?:preview|[iv])\\.redd\\.it|imgs\\.xkcd\\.com)` ,
2637 should_fetch : false ,
27- fetch_transform : ( ( post ) => post . url ) ,
28- function : function ( url , data ) {
38+ post_only : false ,
39+ fetch_transform : ( ( url ) => url ) ,
40+ link_data : function ( url , data ) {
41+ return url ;
42+ } ,
43+ post_data : function ( url , data ) {
2944 const returnData = { } ;
3045 returnData . url = url ;
3146 const expression = / (?: g i f v | m p 4 ) / i;
@@ -37,43 +52,93 @@ class ExtLinks {
3752 returnData . secure_media . reddit_video . hls_url =
3853 url . replace ( expression , 'mp4' ) ;
3954 returnData . thumbnail = returnData . url ;
55+ returnData . preview = { images : [ { source : { url : returnData . thumbnail } } ] } ;
4056 returnData . post_hint = "hosted:video" ;
4157 } else {
4258 // Probably just a normal image
4359 returnData . post_hint = "image" ;
4460 }
4561 return returnData ;
4662 } ,
63+ inline_replacement : function ( url ) {
64+ const expression = / (?: g i f v | m p 4 ) / i;
65+ const gifExpression = / g i f v / i;
66+ if ( url . match ( expression ) ) {
67+ // Probably a gif / video
68+ // If it's a gifv, enable autoplay
69+ return `<video controls muted data-dashjs-player preload="metadata" poster="${ url } "${ url . match ( gifExpression ) ? "autoplay" : "" } ><source src="${ url } "></video>` ;
70+ } else {
71+ // Probably just a normal image
72+ return `<a href="${ url } "><img class="inline" src="${ url } "></a>` ;
73+ }
74+ } ,
4775 } ,
48- {
76+ { // Old-style imgur gallery
4977 expression : `imgur\\.com\/a\/` ,
5078 should_fetch : true ,
51- fetch_transform : ( ( post ) => post . url . includes ( "/gallery" ) ? post . url : post . url + "/embed" ) ,
52- function : function ( url , data ) {
79+ post_only : true ,
80+ fetch_transform : ( ( url ) => url . includes ( "/gallery" ) ? url : url + "/embed" ) ,
81+ link_data : function ( url , data ) {
82+ return url ;
83+ } ,
84+ post_data : function ( url , data ) {
5385 const returnData = { } ;
5486 returnData . url = url ;
5587 const expression = / ^ \s + ?i m a g e s \s + ?: \s + ?( \{ \" c o u n t \" : \d + ?, \" i m a g e s \" : \[ \{ .* \} , ? \] \} ) / gm;
56- data . matchAll ( expression ) . forEach ( ( match ) => {
57- // Build a gallery with these images
58- returnData . is_gallery = true ;
59- returnData . gallery_data = {
60- items : [ ] ,
61- } ;
62- JSON . parse ( match [ 1 ] ) . images . map ( ( img ) => {
63- returnData . gallery_data . items . push ( {
64- media_id : "https://i.imgur.com/" + img . hash + ".jpg" ,
88+ const matches = data . matchAll ( expression ) ;
89+ if ( matches ) {
90+ const matchArray = Array . from ( matches ) ;
91+ if ( matchArray . length == 1 ) {
92+ // Single-item, don't treat it as a gallery
93+ const video_expression = / (?: g i f v | m p 4 ) / i;
94+ JSON . parse ( matchArray [ 0 ] [ 1 ] ) . images . map ( ( img ) => {
95+ if ( img . ext . match ( video_expression ) ) {
96+ // Probably a gif / video
97+ returnData . secure_media = { reddit_video : { } } ;
98+ returnData . secure_media . reddit_video . fallback_url =
99+ returnData . secure_media . reddit_video . dash_url =
100+ returnData . secure_media . reddit_video . hls_url =
101+ "https://i.imgur.com/" + img . hash + img . ext ;
102+ returnData . thumbnail = "https://i.imgur.com/" + img . hash + ".jpg" ;
103+ returnData . preview = { images : [ { source : { url : returnData . thumbnail } } ] } ;
104+ returnData . post_hint = "hosted:video" ;
105+ } else {
106+ // Probably just a normal image
107+ returnData . post_hint = "image" ;
108+ returnData . url = "https://i.imgur.com/" + img . hash + ".jpg" ;
109+ }
65110 } ) ;
66- } ) ;
67- } ) ;
111+ } else {
112+ // Build a gallery with these images
113+ returnData . is_gallery = true ;
114+ returnData . gallery_data = {
115+ items : [ ] ,
116+ } ;
117+ matchArray . forEach ( ( match ) => {
118+ JSON . parse ( match [ 1 ] ) . images . map ( ( img ) => {
119+ returnData . gallery_data . items . push ( {
120+ media_id : "https://i.imgur.com/" + img . hash + ".jpg" ,
121+ } ) ;
122+ } ) ;
123+ } ) ;
124+ }
125+ }
68126
69127 return returnData ;
70128 } ,
129+ inline_replacement : function ( url ) {
130+ return null ;
131+ } ,
71132 } ,
72- {
133+ { // New-style imgur gallery
73134 expression : `imgur\\.com\/gallery\/` ,
74135 should_fetch : true ,
75- fetch_transform : ( ( post ) => post . url ) ,
76- function : function ( url , data ) {
136+ post_only : true ,
137+ fetch_transform : ( ( url ) => url ) ,
138+ link_data : function ( url , data ) {
139+ return url ;
140+ } ,
141+ post_data : function ( url , data ) {
77142 const returnData = { } ;
78143 returnData . url = url ;
79144 const expression = / h t t p .{ 1 , 2 } \/ \/ i \. i m g u r \. c o m \/ \w + \. j p e g / g;
@@ -90,6 +155,9 @@ class ExtLinks {
90155
91156 return returnData ;
92157 } ,
158+ inline_replacement : function ( url ) {
159+ return null ;
160+ } ,
93161 } ,
94162 ] ;
95163
@@ -115,38 +183,118 @@ class ExtLinks {
115183 . catch ( ( err ) => null ) ;
116184 }
117185
118- static async parseExternal ( post ) {
119- const domain = ExtLinks . getExternalDomain ( post . url ) ;
186+ static async parseExternalLink ( url ) {
187+ const domain = ExtLinks . getExternalDomain ( url ) ;
120188 if ( domain ) {
189+ var fetch_data = null ;
121190 if ( domain . should_fetch ) {
122- const data = await ExtLinks . fetchExternal ( domain . fetch_transform ( post ) ) ;
191+ fetch_data = await ExtLinks . fetchExternal ( domain . fetch_transform ( url ) ) ;
192+ }
193+
194+ return domain . link_data ( url , fetch_data ) ;
195+ }
123196
124- if ( data ) {
125- const result = domain . function ( post . url , data ) ;
126- return result ;
197+ return null ;
198+ }
199+
200+ static async parseExternalInlineLink ( url ) {
201+ const domain = ExtLinks . getExternalDomain ( url ) ;
202+ if ( domain && ! domain . post_only ) {
203+ var fetch_data = null ;
204+ if ( domain . should_fetch ) {
205+ fetch_data = await ExtLinks . fetchExternal ( domain . fetch_transform ( url ) ) ;
206+ }
207+
208+ const link_data = domain . link_data ( url , fetch_data ) ;
209+ const inline_replacement = domain . inline_replacement ( link_data ) ;
210+ return inline_replacement ;
211+ }
212+
213+ return null ;
214+ }
215+
216+ static async parseExternalPost ( post ) {
217+ if ( post . url ) {
218+ const domain = ExtLinks . getExternalDomain ( post . url ) ;
219+ if ( domain ) {
220+ const link_data = await ExtLinks . parseExternalLink ( post . url ) ;
221+ if ( link_data ) {
222+ var fetch_data = null ;
223+
224+ if ( domain . post_only ) {
225+ if ( domain . should_fetch ) {
226+ fetch_data = await ExtLinks . fetchExternal ( domain . fetch_transform ( post . url ) ) ;
227+ }
228+ else {
229+ fetch_data = post . secure_media_embed ;
230+ }
231+ }
232+
233+ return domain . post_data ( link_data , fetch_data ) ;
127234 }
128- } else {
129- const result = domain . function ( post . url , post . secure_media_embed ) ;
130- return result ;
131235 }
132236 }
133237
134238 return null ;
135239 }
136240
241+ static async resolveExternalInlineLinks ( html ) {
242+ const expression = / < a h r e f = " ( h t t p [ s ] ? : \/ \/ .* ?) " > \1? < \/ a > / g;
243+ const matches = Array . from ( html . matchAll ( expression ) ) ;
244+
245+ var result = html ;
246+ for ( var i = 0 ; i < matches . length ; i ++ ) {
247+ const match = matches [ i ] ;
248+ const replacement = await ExtLinks . parseExternalInlineLink ( match [ 1 ] ) ;
249+ if ( replacement ) {
250+ result = result . replace ( match [ 0 ] , replacement ) ;
251+ }
252+ }
253+
254+ return result ;
255+ }
256+
137257 static async resolveExternalLinks ( post ) {
138- if ( post . url ) {
139- const data = await ExtLinks . parseExternal ( post ) ;
258+ // First, check for a link post
259+ if ( post . post_hint == 'link' ) {
260+ const data = await ExtLinks . parseExternalPost ( post ) ;
140261 if ( data ) {
141262 return data ;
142263 }
143- }
144- return null ;
145- }
264+ }
265+
266+ // Otherwise, if the first thing in this self post is a link, or it's only a link or a group of links, treat it as a link post
267+ // Easier to perform the regex on the markdown instead of the HTML version
268+ if ( post . selftext ) {
269+ const expression = / ^ \[ ( .* ?) \] \( \1? \) / g;
270+ const matches = Array . from ( post . selftext . matchAll ( expression ) ) ;
271+
272+ if ( matches . length > 0 ) {
273+ // Treat this as a link post with the first match
274+ post . post_hint = 'link' ;
275+ post . url = matches [ 0 ] [ 1 ] ;
276+ const data = await ExtLinks . parseExternalPost ( post ) ;
277+ if ( data ) {
278+ // Strip out the matching link from the self text
279+ data . selftext = post . selftext . replace ( `[${ post . url } ](${ post . url } )` , '' ) ;
280+ const expr = new RegExp ( `(<|<)a href="${ post . url } "(>|>)${ post . url } (<|<)/a(>|>)` , "g" )
281+ data . selftext_html = post . selftext_html . replace ( expr , '' ) ;
282+ }
283+
284+ return data ;
285+ }
286+ }
287+
288+ // Finally, find all anchors that href to known domains and contain just a link to the same href
289+ if ( post . selftext_html ) {
290+ const result = await ExtLinks . resolveExternalInlineLinks ( post . selftext_html ) ;
146291
147- static updatePost ( post , data ) {
148- // Update the post approrpriately
149- post = Object . assign ( post , data ) ;
292+ if ( result ) {
293+ return { selftext_html : result } ;
294+ }
295+ }
296+
297+ return null ;
150298 }
151299}
152300
0 commit comments