Linkify = init: -> return if g.VIEW is 'catalog' or not Conf['Linkify'] @types = {} @types[type.key] = type for type in @ordered_types if Conf['Comment Expansion'] ExpandComment.callbacks.push @node Post.callbacks.push name: 'Linkify' cb: @node $.on d, '4chanXInitFinished PostsInserted', -> for key, service of Linkify.types when service.title?.batchSize Linkify.flushTitles service.title return events: (post) -> i = 0 items = $$ '.embedder', post.nodes.comment while el = items[i++] $.on el, 'click', Linkify.cb.toggle Linkify.cb.toggle.call el if $.hasClass el, 'embedded' return node: -> return (if Conf['Embedding'] then Linkify.events @ else null) if @isClone return unless Linkify.regString.test @info.comment test = /[^\s'"]+/g space = /[\s'"]/ snapshot = $.X './/br|.//text()', @nodes.comment i = 0 links = [] while node = snapshot.snapshotItem i++ {data} = node continue if !data or node.parentElement.nodeName is "A" while result = test.exec data {index} = result endNode = node word = result[0] # End of node, not necessarily end of space-delimited string if (length = index + word.length) is data.length test.lastIndex = 0 while (saved = snapshot.snapshotItem i++) if saved.nodeName is 'BR' break endNode = saved {data} = saved if end = space.exec data # Set our snapshot and regex to start on this node at this position when the loop resumes word += data[...end.index] test.lastIndex = length = end.index i-- break else {length} = data word += data if Linkify.regString.test word links.push Linkify.makeRange node, endNode, index, length <%= assert('word is links[links.length-1].toString()') %> break unless test.lastIndex and node is endNode i = links.length while i-- link = Linkify.makeLink links[i] unless $.x 'ancestor::pre', link Linkify.embedProcess link, @ return embedProcess: (link, post) -> if data = Linkify.services link data.post = post Linkify.embed data if Conf['Embedding'] Linkify.title data if Conf['Link Title'] regString: ///( # http, magnet, ftp, etc (https?|mailto|git|magnet|ftp|irc):( [a-z\d%/] ) | # This should account for virtually all links posted without http: ([-a-z\d]+[.])+( aero|asia|biz|cat|com|coop|info|int|jobs|mobi|moe|museum|name|net|org|post|pro|tel|travel|xxx|edu|gov|mil|[a-z]{2} )([:/]|(?![^\s'"])) | # IPv4 Addresses [\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3} | # E-mails [-\w\d.@]+@[a-z\d.-]+\.[a-z\d] )///i makeRange: (startNode, endNode, startOffset, endOffset) -> range = document.createRange() range.setStart startNode, startOffset range.setEnd endNode, endOffset range makeLink: (range) -> text = range.toString() # Clean start of range i = text.search Linkify.regString if i > 0 text = text.slice i i-- while range.startOffset + i >= range.startContainer.data.length range.setStart range.startContainer, range.startOffset + i if i # Clean end of range i = 0 while /[)\]}>.,]/.test t = text.charAt text.length - (1 + i) break unless /[.,]/.test(t) or (text.match /[()\[\]{}<>]/g).length % 2 i++ if i text = text.slice 0, -i i-- while range.endOffset - i < 0 if i range.setEnd range.endContainer, range.endOffset - i # Make our link 'valid' if it is formatted incorrectly. unless /(mailto:|.+:\/\/)/.test text text = ( if /@/.test text 'mailto:' else 'http://' ) + text a = $.el 'a', className: 'linkify' rel: 'nofollow noreferrer' target: '_blank' href: text # Insert the range into the anchor, the anchor into the range's DOM location, and destroy the range. $.add a, range.extractContents() range.insertNode a range.detach() a services: (link) -> {href} = link for type in Linkify.ordered_types when match = type.regExp.exec href return if type.dummy or type.httpOnly and location.protocol isnt 'http:' return {key: type.key, uid: match[1], options: match[2], link} return embed: (data) -> {key, uid, options, link, post} = data embed = $.el 'a', className: 'embedder' rel: 'nofollow noreferrer' href: link.href textContent: '(embed)' embed.dataset[name] = value for name, value of {key, uid, options} $.addClass link, "#{embed.dataset.key}" $.on embed, 'click', Linkify.cb.toggle $.after link, [$.tn(' '), embed] if Conf['Auto-embed'] and !post.isFetchedQuote $.asap (-> doc.contains embed), -> Linkify.cb.toggle.call embed title: (data) -> {key, uid, options, link, post} = data return unless service = Linkify.types[key].title if service.batchSize (service.queue or= []).push data if service.queue.length >= service.batchSize Linkify.flushTitles service else unless $.cache service.api(uid), (-> Linkify.cb.title @, data), {responseType: 'json'} $.extend link, <%= html('[${key}] Title Link Blocked (are you using NoScript?)') %> flushTitles: (service) -> {queue} = service return unless queue?.length service.queue = [] cb = -> Linkify.cb.title @, data for data in queue return unless $.cache service.api(data.uid for data in queue), cb, {responseType: 'json'} for data in queue $.extend data.link, <%= html('[${data.key}] Title Link Blocked (are you using NoScript?)') %> return cb: toggle: (e) -> e?.preventDefault() if $.hasClass @, "embedded" $.rm @previousElementSibling unless $.hasClass @previousElementSibling, 'linkify' @previousElementSibling.hidden = false @textContent = '(embed)' else @previousElementSibling.hidden = true $.before @, Linkify.cb.embed @ @textContent = '(unembed)' $.toggleClass @, 'embedded' embed: (a) -> # We create an element to embed el = (type = Linkify.types[a.dataset.key]).el a # Set style values. el.style.cssText = if type.style? type.style else "border: 0; width: 640px; height: 390px" return el title: (req, data) -> {key, uid, options, link, post} = data {status} = req service = Linkify.types[key].title text = "[#{key}] #{switch status when 200, 304 service.text req.response, uid when 404 "Not Found" when 403 "Forbidden or Private" else "#{status}'d" }" link.dataset.original = link.textContent link.textContent = text for post2 in post.clones for link2 in $$ 'a.linkify', post2.nodes.comment when link2.href is link.href link2.dataset.original = link2.textContent link2.textContent = text return ordered_types: [ key: 'audio' regExp: /\.(?:mp3|ogg|wav)$/i style: '' el: (a) -> $.el 'audio', controls: true preload: 'auto' src: a.href , key: 'gist' regExp: /^\w+:\/\/gist\.github\.com\/(?:[\w\-]+\/)?(\w+)/ el: (a) -> el = $.el 'iframe' el.setAttribute 'sandbox', 'allow-scripts' content = <%= html('${a.dataset.uid}') %> el.src = "data:text/html;charset=utf-8,#{encodeURIComponent content.innerHTML}" el title: api: (uid) -> "https://api.github.com/gists/#{uid}" text: ({files}) -> return file for file of files when files.hasOwnProperty file , key: 'image' regExp: /\.(?:gif|png|jpg|jpeg|bmp)$/i style: 'border: 0; width: auto; height: auto;' el: (a) -> $.el 'div', <%= html('') %> , key: 'InstallGentoo' regExp: /^\w+:\/\/paste\.installgentoo\.com\/view\/(?:raw\/|download\/|embed\/)?(\w+)/ el: (a) -> $.el 'iframe', src: "https://paste.installgentoo.com/view/embed/#{a.dataset.uid}" , key: 'Twitter' regExp: /^\w+:\/\/(?:www\.)?twitter\.com\/(\w+\/status\/\d+)/ el: (a) -> $.el 'iframe', src: "https://twitframe.com/show?url=https://twitter.com/#{a.dataset.uid}" , key: 'LiveLeak' regExp: /^\w+:\/\/(?:\w+\.)?liveleak\.com\/.*\?.*i=(\w+)/ httpOnly: true el: (a) -> el = $.el 'iframe', width: "640", height: "360", src: "http://www.liveleak.com/ll_embed?i=#{a.dataset.uid}", frameborder: "0" el.setAttribute "allowfullscreen", "true" el , key: 'MediaCrush' regExp: /^\w+:\/\/(?:www\.)?mediacru\.sh\/([\w\-]+)/ style: 'border: 0;' el: (a) -> el = $.el 'div' $.queueTask -> $.cache "https://mediacru.sh/#{a.dataset.uid}.json", -> return unless doc.contains el {status} = @ return el.textContent = "ERROR #{status}" unless status in [200, 304] {files} = @response for type in ['video/mp4', 'video/webm', 'video/ogv', 'image/svg+xml', 'image/png', 'image/gif', 'image/jpeg', 'audio/mpeg', 'audio/ogg'] for file in files if file.type is type embed = file break break if embed return div.textContent = "ERROR: Not a valid filetype" unless embed switch embed.type when 'video/mp4', 'video/webm', 'video/ogv' $.extend el, <%= html('') %> for ext, i in ['mp4', 'webm'] el.firstChild.children[i].src = "https://mediacru.sh/#{a.dataset.uid}.#{ext}" when 'image/svg+xml', 'image/png', 'image/gif', 'image/jpeg' $.extend el, <%= html('') %> when 'audio/mpeg', 'audio/ogg' $.extend el, <%= html('') %> else el.textContent = "ERROR: No valid filetype." el , key: 'pastebin' regExp: /^\w+:\/\/(?:\w+\.)?pastebin\.com\/(?!u\/)(?:[\w\.]+\?i\=)?(\w+)/ httpOnly: true el: (a) -> div = $.el 'iframe', src: "http://pastebin.com/embed_iframe.php?i=#{a.dataset.uid}" , key: 'gfycat' regExp: /^\w+:\/\/(?:www\.)?gfycat\.com\/(?:iframe\/)?(\w+)/ el: (a) -> div = $.el 'iframe', src: "//gfycat.com/iframe/#{a.dataset.uid}" , key: 'SoundCloud' regExp: /^\w+:\/\/(?:www\.)?(?:soundcloud\.com\/|snd\.sc\/)([\w\-\/]+)/ style: 'border: 0; width: 500px; height: 400px;' el: (a) -> $.el 'iframe', src: "https://w.soundcloud.com/player/?visual=true&show_comments=false&url=https%3A%2F%2Fsoundcloud.com%2F#{encodeURIComponent a.dataset.uid}" title: api: (uid) -> "//soundcloud.com/oembed?format=json&url=https%3A%2F%2Fsoundcloud.com%2F#{encodeURIComponent uid}" text: (_) -> _.title , key: 'StrawPoll' regExp: /^\w+:\/\/(?:www\.)?strawpoll\.me\/(?:embed_\d+\/)?(\d+(?:\/r)?)/ httpOnly: true style: 'border: 0; width: 600px; height: 406px;' el: (a) -> $.el 'iframe', src: "http://strawpoll.me/embed_1/#{a.dataset.uid}" , key: 'TwitchTV' regExp: /^\w+:\/\/(?:www\.)?twitch\.tv\/([^#\&\?]*)/ httpOnly: true style: "border: none; width: 640px; height: 360px;" el: (a) -> if result = /(\w+)\/([bc])\/(\d+)/i.exec a.dataset.uid [_, channel, type, id] = result idparam = {'b': 'archive_id', 'c': 'chapter_id'} obj = $.el 'object', data: 'http://www.twitch.tv/widgets/archive_embed_player.swf' $.extend obj, <%= html('') %> obj.children[1].value = "channel=#{channel}&start_volume=25&auto_play=false&#{idparam[type]}=#{id}" obj else channel = (/(\w+)/.exec a.dataset.uid)[0] obj = $.el 'object', data: "http://www.twitch.tv/widgets/live_embed_player.swf?channel=#{channel}" $.extend obj, <%= html('') %> obj.children[1].value = "hostname=www.twitch.tv&channel=#{channel}&auto_play=true&start_volume=25" obj , key: 'Vocaroo' regExp: /^\w+:\/\/(?:www\.)?vocaroo\.com\/i\/(\w+)/ style: '' el: (a) -> $.el 'audio', controls: true preload: 'auto' src: "http://vocaroo.com/media_command.php?media=#{a.dataset.uid}&command=download_ogg" , key: 'Vimeo' regExp: /^\w+:\/\/(?:www\.)?vimeo\.com\/(\d+)/ el: (a) -> $.el 'iframe', src: "//player.vimeo.com/video/#{a.dataset.uid}?wmode=opaque" title: api: (uid) -> "https://vimeo.com/api/oembed.json?url=http://vimeo.com/#{uid}" text: (_) -> _.title , key: 'Vine' regExp: /^\w+:\/\/(?:www\.)?vine\.co\/v\/(\w+)/ style: 'border: none; width: 500px; height: 500px;' el: (a) -> $.el 'iframe', src: "https://vine.co/v/#{a.dataset.uid}/card" , key: 'YouTube' regExp: /^\w+:\/\/(?:youtu.be\/|[\w\.]*youtube[\w\.]*\/.*(?:v=|\/embed\/|\/v\/|\/videos\/))([\w\-]{11})[^#\&\?]?(.*)/ el: (a) -> start = a.dataset.options.match /\b(?:star)?t\=(\w+)/ start = start[1] if start if start and !/^\d+$/.test start start += ' 0h0m0s' start = 3600 * start.match(/(\d+)h/)[1] + 60 * start.match(/(\d+)m/)[1] + 1 * start.match(/(\d+)s/)[1] el = $.el 'iframe', src: "//www.youtube.com/embed/#{a.dataset.uid}?wmode=opaque#{if start then '&start=' + start else ''}" el.setAttribute "allowfullscreen", "true" el title: batchSize: 50 api: (uids) -> ids = encodeURIComponent uids.join(',') key = '<%= meta.youtubeAPIKey %>' "https://www.googleapis.com/youtube/v3/videos?part=snippet&id=#{ids}&fields=items%28id%2Csnippet%28title%29%29&key=#{key}" text: (data, uid) -> for item in data.items when item.id is uid return item.snippet.title 'Not Found' , key: 'Loopvid' regExp: /^\w+:\/\/(?:www\.)?loopvid.appspot.com\/((?:pf|kd|lv|mc|gd|gh|db|nn)\/[\w\-]+(,[\w\-]+)*|fc\/\w+\/\d+)/ style: 'border: 0; width: auto; height: auto;' el: (a) -> el = $.el 'video', controls: true preload: 'auto' loop: true [_, host, names] = a.dataset.uid.match /(\w+)\/(.*)/ types = if host in ['gd', 'fc'] then [''] else ['.webm', '.mp4'] for name in names.split ',' for type in types base = "#{name}#{type}" url = switch host # list from src/loopvid.py at http://loopvid.appspot.com/source.html when 'pf' then "http://a.pomf.se/#{base}" when 'kd' then "http://kastden.org/loopvid/#{base}" when 'lv' then "http://loopvid.mooo.com/videos/#{base}" when 'mc' then "https://cdn.mediacru.sh/#{base}" when 'gd' then "https://docs.google.com/uc?export=download&id=#{base}" when 'gh' then "https://googledrive.com/host/#{base}" when 'db' then "https://googledrive.com/host/#{base}" when 'fc' then "//i.4cdn.org/#{base}.webm" when 'nn' then "http://naenara.eu/loopvids/#{base}" $.add el, $.el 'source', src: url el , key: 'Clyp' regExp: /^\w+:\/\/(?:www\.)?clyp\.it\/(\w+)/ style: '' el: (a) -> $.el 'audio', controls: true preload: 'auto' src: "http://clyp.it/#{a.dataset.uid}.ogg" , # dummy entries: not implemented but included to prevent them being wrongly embedded as a subsequent type key: 'Loopvid-dummy' regExp: /^\w+:\/\/(?:www\.)?loopvid.appspot.com\// dummy: true , key: 'MediaFire-dummy' regExp: /^\w+:\/\/(?:www\.)?mediafire.com\// dummy: true , key: 'video' regExp: /\.(?:ogv|webm|mp4)$/i style: 'border: 0; width: auto; height: auto;' el: (a) -> $.el 'video', controls: true preload: 'auto' src: a.href ]