263 lines
9.3 KiB
CoffeeScript
263 lines
9.3 KiB
CoffeeScript
SW.yotsuba =
|
|
isOPContainerThread: false
|
|
hasIPCount: true
|
|
archivedBoardsKnown: true
|
|
|
|
urls:
|
|
thread: ({boardID, threadID}) -> "#{location.protocol}//#{BoardConfig.domain(boardID)}/#{boardID}/thread/#{threadID}"
|
|
post: ({postID}) -> "#p#{postID}"
|
|
index: ({boardID}) -> "#{location.protocol}//#{BoardConfig.domain(boardID)}/#{boardID}/"
|
|
catalog: ({boardID}) -> if boardID is 'f' then undefined else "#{location.protocol}//#{BoardConfig.domain(boardID)}/#{boardID}/catalog"
|
|
archive: ({boardID}) -> if BoardConfig.isArchived(boardID) then "#{location.protocol}//#{BoardConfig.domain(boardID)}/#{boardID}/archive" else undefined
|
|
threadJSON: ({boardID, threadID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/thread/#{threadID}.json"
|
|
threadsListJSON: ({boardID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/threads.json"
|
|
archiveListJSON: ({boardID}) -> if BoardConfig.isArchived(boardID) then "#{location.protocol}//a.4cdn.org/#{boardID}/archive.json" else ''
|
|
catalogJSON: ({boardID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/catalog.json"
|
|
file: ({boardID}, filename) ->
|
|
hostname = if boardID is 'f' then ImageHost.flashHost() else ImageHost.host()
|
|
"#{location.protocol}//#{hostname}/#{boardID}/#{filename}"
|
|
thumb: ({boardID}, filename) ->
|
|
"#{location.protocol}//#{ImageHost.thumbHost()}/#{boardID}/#{filename}"
|
|
|
|
isPrunedByAge: ({boardID}) -> boardID is 'f'
|
|
areMD5sDeferred: ({boardID}) -> boardID is 'f'
|
|
isOnePage: ({boardID}) -> boardID is 'f'
|
|
noAudio: ({boardID}) -> BoardConfig.noAudio(boardID)
|
|
|
|
selectors:
|
|
board: '.board'
|
|
thread: '.thread'
|
|
threadDivider: '.board > hr'
|
|
summary: '.summary'
|
|
postContainer: '.postContainer'
|
|
replyOriginal: '.replyContainer:not([data-clone])'
|
|
sideArrows: 'div.sideArrows'
|
|
post: '.post'
|
|
infoRoot: '.postInfo'
|
|
info:
|
|
subject: '.subject'
|
|
name: '.name'
|
|
email: '.useremail'
|
|
tripcode: '.postertrip'
|
|
uniqueIDRoot: '.posteruid'
|
|
uniqueID: '.posteruid > .hand'
|
|
capcode: '.capcode.hand'
|
|
pass: '.n-pu'
|
|
flag: '.flag, .bfl'
|
|
date: '.dateTime'
|
|
nameBlock: '.nameBlock'
|
|
quote: '.postNum > a:nth-of-type(2)'
|
|
reply: '.replylink'
|
|
icons:
|
|
isSticky: '.stickyIcon'
|
|
isClosed: '.closedIcon'
|
|
isArchived: '.archivedIcon'
|
|
file:
|
|
text: '.file > :first-child'
|
|
link: '.fileText > a'
|
|
thumb: 'a.fileThumb > [data-md5]'
|
|
thumbLink: 'a.fileThumb'
|
|
highlightable:
|
|
op: '.opContainer'
|
|
reply: ' > .reply'
|
|
catalog: ''
|
|
comment: '.postMessage'
|
|
spoiler: 's'
|
|
quotelink: ':not(pre) > .quotelink' # XXX https://github.com/4chan/4chan-JS/issues/77: 4chan currently creates quote links inside [code] tags; ignore them
|
|
catalog:
|
|
board: '#threads'
|
|
thread: '.thread'
|
|
thumb: '.thumb'
|
|
boardList: '#boardNavDesktop > .boardList'
|
|
boardListBottom: '#boardNavDesktopFoot > .boardList'
|
|
styleSheet: 'link[title=switch]'
|
|
psa: '#globalMessage'
|
|
psaTop: '#globalToggle'
|
|
searchBox: '#search-box'
|
|
nav:
|
|
prev: '.prev > form > [type=submit]'
|
|
next: '.next > form > [type=submit]'
|
|
|
|
classes:
|
|
highlight: 'highlight'
|
|
|
|
xpath:
|
|
thread: 'div[contains(concat(" ",@class," ")," thread ")]'
|
|
postContainer: 'div[contains(@class,"postContainer")]'
|
|
replyContainer: 'div[contains(@class,"replyContainer")]'
|
|
|
|
regexp:
|
|
quotelink:
|
|
///
|
|
^https?://boards\.4chan(?:nel)?\.org/+
|
|
([^/]+) # boardID
|
|
/+thread/+
|
|
(\d+) # threadID
|
|
(?:[/?][^#]*)?
|
|
(?:#p
|
|
(\d+) # postID
|
|
)?
|
|
$
|
|
///
|
|
quotelinkHTML:
|
|
/<a [^>]*\bhref="(?:(?:\/\/boards\.4chan(?:nel)?\.org)?\/([^\/]+)\/thread\/)?(\d+)?(?:#p(\d+))?"/g
|
|
pass:
|
|
/^https?:\/\/www\.4chan(?:nel)?\.org\/+pass(?:$|[?#])/
|
|
captcha:
|
|
/^https?:\/\/sys\.4chan(?:nel)?\.org\/+captcha(?:$|[?#])/
|
|
|
|
bgColoredEl: ->
|
|
$.el 'div', className: 'reply'
|
|
|
|
isThisPageLegit: ->
|
|
# not 404 error page or similar.
|
|
location.hostname in ['boards.4chan.org', 'boards.4channel.org'] and
|
|
d.doctype and
|
|
!$('link[href*="favicon-status.ico"]', d.head) and
|
|
d.title not in ['4chan - Temporarily Offline', '4chan - Error', '504 Gateway Time-out', 'MathJax Equation Source']
|
|
|
|
is404: ->
|
|
# XXX Sometimes threads don't 404 but are left over as stubs containing one garbage reply post.
|
|
d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] or (g.VIEW is 'thread' and $('.board') and not $('.opContainer'))
|
|
|
|
isIncomplete: ->
|
|
return g.VIEW in ['index', 'thread'] and not $('.board + *')
|
|
|
|
isBoardlessPage: (url) ->
|
|
url.hostname in ['www.4chan.org', 'www.4channel.org']
|
|
|
|
isAuxiliaryPage: (url) ->
|
|
url.hostname not in ['boards.4chan.org', 'boards.4channel.org']
|
|
|
|
isFileURL: (url) ->
|
|
ImageHost.test(url.hostname)
|
|
|
|
initAuxiliary: ->
|
|
switch location.hostname
|
|
when 'www.4chan.org', 'www.4channel.org'
|
|
if SW.yotsuba.regexp.pass.test(location.href)
|
|
PassMessage.init()
|
|
else
|
|
$.onExists doc, 'body', -> $.addStyle CSS.www
|
|
Captcha.replace.init()
|
|
return
|
|
when 'sys.4chan.org', 'sys.4channel.org'
|
|
pathname = location.pathname.split /\/+/
|
|
if pathname[2] is 'imgboard.php'
|
|
if /\bmode=report\b/.test location.search
|
|
Report.init()
|
|
else if (match = location.search.match /\bres=(\d+)/)
|
|
$.ready ->
|
|
if Conf['404 Redirect'] and $.id('errmsg')?.textContent is 'Error: Specified thread does not exist.'
|
|
Redirect.navigate 'thread', {
|
|
boardID: g.BOARD.ID
|
|
postID: +match[1]
|
|
}
|
|
else if pathname[2] is 'post'
|
|
PostSuccessful.init()
|
|
return
|
|
|
|
scriptData: ->
|
|
for script in $$ 'script:not([src])', d.head
|
|
return script.textContent if /\bcooldowns *=/.test script.textContent
|
|
''
|
|
|
|
parseThreadMetadata: (thread) ->
|
|
scriptData = @scriptData()
|
|
thread.postLimit = /\bbumplimit *= *1\b/.test scriptData
|
|
thread.fileLimit = /\bimagelimit *= *1\b/.test scriptData
|
|
thread.ipCount = if (m = scriptData.match /\bunique_ips *= *(\d+)\b/) then +m[1]
|
|
|
|
if g.BOARD.ID is 'f' and thread.OP.file
|
|
{file} = thread.OP
|
|
$.ajax @urls.threadJSON({boardID: 'f', threadID: thread.ID}),
|
|
timeout: $.MINUTE
|
|
onloadend: ->
|
|
if @response
|
|
file.text.dataset.md5 = file.MD5 = @response.posts[0].md5
|
|
|
|
parseNodes: (post, nodes) ->
|
|
# Add CSS classes to sticky/closed icons on /f/ to match other boards.
|
|
if post.boardID is 'f'
|
|
for type in ['Sticky', 'Closed'] when (icon = $ "img[alt=#{type}]", nodes.info)
|
|
$.addClass icon, "#{type.toLowerCase()}Icon", 'retina'
|
|
|
|
parseDate: (node) ->
|
|
new Date(node.dataset.utc * 1000)
|
|
|
|
parseFile: (post, file) ->
|
|
{text, link, thumb} = file
|
|
return false if not (info = link.nextSibling?.textContent.match /\(([\d.]+ [KMG]?B).*\)/)
|
|
$.extend file,
|
|
name: text.title or link.title or link.textContent
|
|
size: info[1]
|
|
dimensions: info[0].match(/\d+x\d+/)?[0]
|
|
tag: info[0].match(/,[^,]*, ([a-z]+)\)/i)?[1]
|
|
MD5: text.dataset.md5
|
|
if thumb
|
|
$.extend file,
|
|
thumbURL: thumb.src
|
|
MD5: thumb.dataset.md5
|
|
isSpoiler: $.hasClass thumb.parentNode, 'imgspoiler'
|
|
if file.isSpoiler
|
|
file.thumbURL = if (m = link.href.match /\d+(?=\.\w+$)/) then "#{location.protocol}//#{ImageHost.thumbHost()}/#{post.board}/#{m[0]}s.jpg"
|
|
true
|
|
|
|
cleanComment: (bq) ->
|
|
if (abbr = $ '.abbr', bq) # 'Comment too long' or 'EXIF data available'
|
|
for node in $$ '.abbr + br, .exif', bq
|
|
$.rm node
|
|
for i in [0...2]
|
|
$.rm br if (br = abbr.previousSibling) and br.nodeName is 'BR'
|
|
$.rm abbr
|
|
|
|
cleanCommentDisplay: (bq) ->
|
|
$.rm b if (b = $ 'b', bq) and /^Rolled /.test(b.textContent)
|
|
$.rm $('.fortune', bq)
|
|
|
|
insertTags: (bq) ->
|
|
for node in $$ 's, .removed-spoiler', bq
|
|
$.replace node, [$.tn('[spoiler]'), node.childNodes..., $.tn '[/spoiler]']
|
|
for node in $$ '.prettyprint', bq
|
|
$.replace node, [$.tn('[code]'), node.childNodes..., $.tn '[/code]']
|
|
return
|
|
|
|
hasCORS: (url) ->
|
|
url.split('/')[...3].join('/') is location.protocol + '//a.4cdn.org'
|
|
|
|
sfwBoards: (sfw) ->
|
|
BoardConfig.sfwBoards(sfw)
|
|
|
|
uidColor: (uid) ->
|
|
msg = 0
|
|
i = 0
|
|
while i < 8
|
|
msg = (msg << 5) - msg + uid.charCodeAt i++
|
|
(msg >> 8) & 0xFFFFFF
|
|
|
|
isLinkified: (link) ->
|
|
ImageHost.test(link.hostname)
|
|
|
|
testNativeExtension: ->
|
|
$.global ->
|
|
@enabled = 'true' if window.Parser.postMenuIcon
|
|
|
|
transformBoardList: ->
|
|
nodes = []
|
|
spacer = -> $.el 'span', className: 'spacer'
|
|
items = $.X './/a|.//text()[not(ancestor::a)]', $(SW.yotsuba.selectors.boardList)
|
|
i = 0
|
|
while node = items.snapshotItem i++
|
|
switch node.nodeName
|
|
when '#text'
|
|
for chr in node.nodeValue
|
|
span = $.el 'span', textContent: chr
|
|
span.className = 'space' if chr is ' '
|
|
nodes.push spacer() if chr is ']'
|
|
nodes.push span
|
|
nodes.push spacer() if chr is '['
|
|
when 'A'
|
|
a = node.cloneNode true
|
|
nodes.push a
|
|
return nodes
|