class Post toString: -> @ID constructor: (root, @thread, @board, flags={}) -> <% if (readJSON('/.tests_enabled')) { %> @normalizedOriginal = Test.normalize root <% } %> $.extend @, flags @ID = +root.id.match(/\d*$/)[0] @postID = @ID @threadID = @thread.ID @boardID = @board.ID @siteID = g.SITE.ID @fullID = "#{@board}.#{@ID}" @context = @ @isReply = (@ID isnt @threadID) root.dataset.fullID = @fullID @nodes = @parseNodes root if not @isReply @thread.OP = @ for key in ['isSticky', 'isClosed', 'isArchived'] @thread[key] = if (selector = g.SITE.selectors.icons[key]) then !!$(selector, @nodes.info) else false if @thread.isArchived @thread.isClosed = true @thread.kill() @info = subject: @nodes.subject?.textContent or undefined name: @nodes.name?.textContent email: if @nodes.email then decodeURIComponent(@nodes.email.href.replace(/^mailto:/, '')) tripcode: @nodes.tripcode?.textContent uniqueID: @nodes.uniqueID?.textContent capcode: @nodes.capcode?.textContent.replace '## ', '' pass: @nodes.pass?.title.match(/\d*$/)[0] flagCode: @nodes.flag?.className.match(/flag-(\w+)/)?[1].toUpperCase() flagCodeTroll: @nodes.flag?.src?.match(/(\w+)\.gif$/)?[1].toUpperCase() flag: @nodes.flag?.title date: if @nodes.date then new Date(@nodes.date.getAttribute('datetime')?.trim() or (@nodes.date.dataset.utc * 1000)) if Conf['Anonymize'] @info.nameBlock = 'Anonymous' else @info.nameBlock = "#{@info.name or ''} #{@info.tripcode or ''}".trim() @info.nameBlock += " ## #{@info.capcode}" if @info.capcode @info.nameBlock += " (ID: #{@info.uniqueID})" if @info.uniqueID @parseComment() @parseQuotes() @parseFiles() @isDead = false @isHidden = false @clones = [] <% if (readJSON('/.tests_enabled')) { %> return if @forBuildTest <% } %> if g.posts[@fullID] @isRebuilt = true @clones = g.posts[@fullID].clones clone.origin = @ for clone in @clones @thread.lastPost = @ID if !@isFetchedQuote and @ID > @thread.lastPost @board.posts.push @ID, @ @thread.posts.push @ID, @ g.posts.push @fullID, @ parseNodes: (root) -> s = g.SITE.selectors post = $(s.post, root) or root info = $ s.infoRoot, post nodes = root: root bottom: if @isReply or !g.SITE.isOPContainerThread then root else $(s.opBottom, root) post: post info: info comment: $ s.comment, post quotelinks: [] archivelinks: [] embedlinks: [] for key, selector of s.info nodes[key] = $ selector, info g.SITE.parseNodes?(@, nodes) nodes.uniqueIDRoot or= nodes.uniqueID # XXX Edge invalidates HTMLCollections when an ancestor node is inserted into another node. # https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7560353/ if $.engine is 'edge' Object.defineProperty nodes, 'backlinks', configurable: true enumerable: true get: -> post.getElementsByClassName 'backlink' else nodes.backlinks = post.getElementsByClassName 'backlink' nodes parseComment: -> # Merge text nodes and remove empty ones. @nodes.comment.normalize() # Get the comment's text. #
-> \n # Remove: # 'Comment too long'... # EXIF data. (/p/) @nodes.commentClean = bq = @nodes.comment.cloneNode true g.SITE.cleanComment?(bq) @info.comment = @nodesToText bq commentDisplay: -> # Get the comment's text for display purposes (e.g. notifications, excerpts). # In addition to what's done in generating `@info.comment`, remove: # Spoilers. (filter to '[spoiler]') # Rolls. (/tg/, /qst/) # Fortunes. (/s4s/) # Preceding and following new lines. # Trailing spaces. bq = @nodes.commentClean.cloneNode true @cleanSpoilers bq unless Conf['Remove Spoilers'] or Conf['Reveal Spoilers'] g.SITE.cleanCommentDisplay?(bq) @nodesToText(bq).trim().replace(/\s+$/gm, '') commentOrig: -> # Get the comment's text for reposting purposes. bq = @nodes.commentClean.cloneNode true g.SITE.insertTags?(bq) @nodesToText bq nodesToText: (bq) -> text = "" nodes = $.X './/br|.//text()', bq i = 0 while node = nodes.snapshotItem i++ text += node.data or '\n' text cleanSpoilers: (bq) -> spoilers = $$ g.SITE.selectors.spoiler, bq for node in spoilers $.replace node, $.tn '[spoiler]' return parseQuotes: -> @quotes = [] for quotelink in $$ g.SITE.selectors.quotelink, @nodes.comment @parseQuote quotelink return parseQuote: (quotelink) -> # Only add quotes that link to posts on an imageboard. # Don't add: # - board links. (>>>/b/) # - catalog links. (>>>/b/catalog or >>>/b/search) # - rules links. (>>>/a/rules) # - text-board quotelinks. (>>>/img/1234) match = quotelink.href.match g.SITE.regexp.quotelink return unless match or (@isClone and quotelink.dataset.postID) # normal or resurrected quote @nodes.quotelinks.push quotelink return if @isClone # ES6 Set when? fullID = "#{match[1]}.#{match[3]}" @quotes.push fullID unless fullID in @quotes parseFiles: -> @files = [] fileRoots = @fileRoots() index = 0 for fileRoot, docIndex in fileRoots if (file = @parseFile fileRoot) file.index = (index++) file.docIndex = docIndex @files.push file if @files.length @file = @files[0] fileRoots: -> if g.SITE.selectors.multifile roots = $$(g.SITE.selectors.multifile, @nodes.root) return roots if roots.length [@nodes.root] parseFile: (fileRoot) -> file = {} for key, selector of g.SITE.selectors.file file[key] = $ selector, fileRoot file.thumbLink = file.thumb?.parentNode return if not (file.text and file.link) return if not g.SITE.parseFile @, file $.extend file, url: file.link.href isImage: /(jpe?g|png|gif|bmp)$/i.test file.link.href isVideo: /(webm|mp4)$/i.test file.link.href size = +file.size.match(/[\d.]+/)[0] unit = ['B', 'KB', 'MB', 'GB'].indexOf file.size.match(/\w+$/)[0] size *= 1024 while unit-- > 0 file.sizeInBytes = size file @deadMark = # \u00A0 is nbsp $.el 'span', textContent: '\u00A0(Dead)' className: 'qmark-dead' kill: (file, index=0) -> if file return if @isDead or @files[index].isDead @files[index].isDead = true $.addClass @nodes.root, 'deleted-file' else return if @isDead @isDead = true $.rmClass @nodes.root, 'deleted-file' $.addClass @nodes.root, 'deleted-post' if not (strong = $ 'strong.warning', @nodes.info) strong = $.el 'strong', className: 'warning' $.after $('input', @nodes.info), strong strong.textContent = if file then '[File deleted]' else '[Deleted]' return if @isClone for clone in @clones clone.kill file, index return if file # Get quotelinks/backlinks to this post # and paint them (Dead). for quotelink in Get.allQuotelinksLinkingTo @ when not $.hasClass quotelink, 'deadlink' $.add quotelink, Post.deadMark.cloneNode(true) $.addClass quotelink, 'deadlink' return # XXX Workaround for 4chan's racing condition # giving us false-positive dead posts. resurrect: -> @isDead = false $.rmClass @nodes.root, 'deleted-post' strong = $ 'strong.warning', @nodes.info # no false-positive files if @files.some((file) -> file.isDead) $.addClass @nodes.root, 'deleted-file' strong.textContent = '[File deleted]' else $.rm strong return if @isClone for clone in @clones clone.resurrect() for quotelink in Get.allQuotelinksLinkingTo @ when $.hasClass quotelink, 'deadlink' $.rm $('.qmark-dead', quotelink) $.rmClass quotelink, 'deadlink' return collect: -> g.posts.rm @fullID @thread.posts.rm @ @board.posts.rm @ addClone: (context, contractThumb) -> # Callbacks may not have been run yet due to anti-browser-lock delay in Main.callbackNodesDB. Callbacks.Post.execute @ new Post.Clone @, context, contractThumb rmClone: (index) -> @clones.splice index, 1 for clone in @clones[index..] clone.nodes.root.dataset.clone = index++ return setCatalogOP: (isCatalogOP) -> @nodes.root.classList.toggle 'catalog-container', isCatalogOP @nodes.root.classList.toggle 'opContainer', !isCatalogOP @nodes.post.classList.toggle 'catalog-post', isCatalogOP @nodes.post.classList.toggle 'op', !isCatalogOP @nodes.post.style.left = @nodes.post.style.right = null