291 lines
8.8 KiB
CoffeeScript
291 lines
8.8 KiB
CoffeeScript
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))
|
|
|
|
delete @info.date if @info.date and isNaN(@info.date.getTime())
|
|
|
|
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.get(@fullID)
|
|
@isRebuilt = true
|
|
@clones = g.posts.get(@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.
|
|
# <br> -> \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
|