Merge branch 'multisite'

This commit is contained in:
ccd0 2018-01-23 22:23:02 -08:00
commit ebb9cc11e6
21 changed files with 466 additions and 207 deletions

View File

@ -32,7 +32,7 @@ $(eval $(shell node tools/pkgvars.js))
version = $(shell node -p "JSON.parse(require('fs').readFileSync('version.json')).version")
source_directories := \
globals config css platform classes \
globals config css platform classes site \
Archive Filtering General Images Linkification \
Menu Miscellaneous Monitoring Posting Quotelinks \
main

View File

@ -24,9 +24,14 @@ PostHiding =
Recursive.add PostHiding.hide, @, data.makeStub, true
return unless Conf['Reply Hiding Buttons']
sideArrows = $('.sideArrows', @nodes.root)
$.replace sideArrows.firstChild, PostHiding.makeButton @, 'hide'
sideArrows.removeAttribute 'class'
button = PostHiding.makeButton @, 'hide'
if (sa = Site.selectors.sideArrows)
sideArrows = $ sa, @nodes.root
$.replace sideArrows.firstChild, button
sideArrows.removeAttribute 'class'
else
$.prepend @nodes.root, button
menu:
init: ->

View File

@ -43,5 +43,6 @@ BoardConfig =
board for board, data of (@boards or Conf['boardConfig'].boards) when !!data.ws_board is sfw
noAudio: (boardID) ->
return false unless Site.software is 'yotsuba'
boards = @boards or Conf['boardConfig'].boards
boards and !boards[boardID].webm_audio

View File

@ -10,25 +10,24 @@ Get =
excerpt
threadFromRoot: (root) ->
return null unless root?
g.threads["#{g.BOARD}.#{root.id[1..]}"]
g.threads["#{g.BOARD}.#{root.id.match(/\d*$/)[0]}"]
threadFromNode: (node) ->
Get.threadFromRoot $.x 'ancestor-or-self::div[contains(concat(" ",@class," ")," thread ")]', node
Get.threadFromRoot $.x "ancestor-or-self::#{Site.xpath.thread}", node
postFromRoot: (root) ->
return null unless root?
post = g.posts[root.dataset.fullID]
index = root.dataset.clone
if index then post.clones[index] else post
postFromNode: (root) ->
Get.postFromRoot $.x 'ancestor-or-self::div[contains(@class,"postContainer")][1]', root
Get.postFromRoot $.x "ancestor-or-self::#{Site.xpath.postContainer}[1]", root
postDataFromLink: (link) ->
if link.hostname is 'boards.4chan.org'
path = link.pathname.split /\/+/
boardID = path[1]
threadID = path[3]
postID = if link.hash then link.hash[2..] else path[3]
else # resurrected quote
if link.dataset.postID # resurrected quote
{boardID, threadID, postID} = link.dataset
threadID or= 0
else
match = link.href.match Site.regexp.quotelink
[boardID, threadID, postID] = match[1..]
postID or= threadID
return {
boardID: boardID
threadID: +threadID
@ -64,8 +63,3 @@ Get =
quotelinks.filter (quotelink) ->
{boardID, postID} = Get.postDataFromLink quotelink
boardID is post.board.ID and postID is post.ID
scriptData: ->
for script in $$ 'script:not([src])', d.head
return script.textContent if /\bcooldowns *=/.test script.textContent
''

View File

@ -84,15 +84,15 @@ Header =
$.on window, 'load popstate', Header.hashScroll
$.on d, 'CreateNotification', @createNotification
@setBoardList()
$.onExists doc, 'body', =>
return unless Main.isThisPageLegit()
$.prepend d.body, @bar
$.add d.body, Header.hover
@setBarPosition Conf['Bottom Header']
# Wait for #boardNavMobile instead of #boardNavDesktop,
# it might be incomplete otherwise.
$.onExists doc, '#boardNavMobile', Header.setBoardList
$.onExists doc, "#{Site.selectors.boardList} + *", Header.generateFullBoardList
Main.ready ->
if not (footer = $.id 'boardNavDesktopFoot')
@ -154,9 +154,20 @@ Header =
btn = $('.hide-board-list-button', boardList)
$.on btn, 'click', Header.toggleBoardList
$.add Header.bar, [Header.boardList, Header.shortcuts, Header.noticesRoot, Header.toggle]
Header.setCustomNav Conf['Custom Board Navigation']
Header.generateBoardList Conf['boardnav']
$.sync 'Custom Board Navigation', Header.setCustomNav
$.sync 'boardnav', Header.generateBoardList
generateFullBoardList: ->
nodes = []
spacer = -> $.el 'span', className: 'spacer'
for node in $('#boardNavDesktop > .boardList').childNodes
items = $.X './/a|.//text()[not(ancestor::a)]', $(Site.selectors.boardList)
i = 0
while node = items.snapshotItem i++
switch node.nodeName
when '#text'
for chr in node.nodeValue
@ -169,18 +180,10 @@ Header =
a = node.cloneNode true
a.className = 'current' if a.pathname.split('/')[1] is g.BOARD.ID
nodes.push a
fullBoardList = $ '.boardList', boardList
fullBoardList = $ '.boardList', Header.boardList
$.add fullBoardList, nodes
CatalogLinks.setLinks fullBoardList
$.add Header.bar, [Header.boardList, Header.shortcuts, Header.noticesRoot, Header.toggle]
Header.setCustomNav Conf['Custom Board Navigation']
Header.generateBoardList Conf['boardnav']
$.sync 'Custom Board Navigation', Header.setCustomNav
$.sync 'boardnav', Header.generateBoardList
generateBoardList: (boardnav) ->
list = $ '#custom-board-list', Header.boardList
$.rmAll list
@ -224,7 +227,17 @@ Header =
return a
boardID = t.split('-')[0]
boardID = g.BOARD.ID if boardID is 'current'
if boardID is 'current'
if location.hostname is 'boards.4chan.org'
boardID = g.BOARD.ID
else
a = $.el 'a',
href: "/#{g.BOARD.ID}/"
textContent: text or g.BOARD.ID
className: 'current'
if /-(catalog|archive|expired)/.test(t)
a = a.firstChild # Its text node.
return a
a = do ->
if boardID is '@'
@ -237,13 +250,13 @@ Header =
return a.cloneNode true
a = $.el 'a',
href: "/#{boardID}/"
href: "//boards.4chan.org/#{boardID}/"
textContent: boardID
a.href += g.VIEW if g.VIEW in ['catalog', 'archive']
a.className = 'current' if boardID is g.BOARD.ID
a.className = 'current' if a.hostname is location.hostname and boardID is g.BOARD.ID
a
a.textContent = if /-title/.test(t) or /-replace/.test(t) and boardID is g.BOARD.ID
a.textContent = if /-title/.test(t) or /-replace/.test(t) and a.hostname is location.hostname and boardID is g.BOARD.ID
a.title or a.textContent
else if /-full/.test t
("/#{boardID}/") + (if a.title then " - #{a.title}" else '')
@ -271,7 +284,7 @@ Header =
if /-expired/.test t
if boardID not in ['b', 'f', 'trash', 'bant']
a.href = "/#{boardID}/archive"
a.href = "//boards.4chan.org/#{boardID}/archive"
else
return a.firstChild # Its text node.

View File

@ -169,7 +169,7 @@ Settings =
# Unsupported options
if $.engine isnt 'gecko'
$('div[data-name="Remember QR Size"]', section).hidden = true
if $.perProtocolSettings
if $.perProtocolSettings or location.protocol isnt 'https:'
$('div[data-name="Redirect to HTTPS"]', section).hidden = true
$.get items, (items) ->

View File

@ -82,8 +82,8 @@ Gallery =
$.on window, 'resize', Gallery.cb.setHeight
for file in $$ '.post .file'
post = Get.postFromNode file
for postThumb in $$ Site.selectors.file.thumb
post = Get.postFromNode postThumb
continue unless post.file?.thumb
Gallery.generateThumb post
# If no image to open is given, pick image we have scrolled to.

View File

@ -23,7 +23,7 @@ ImageHover =
return unless doc.contains @
{file} = post
{isVideo} = file
return if file.isExpanding or file.isExpanded
return if file.isExpanding or file.isExpanded or Site.isThumbExpanded?(file)
error = ImageHover.error post
if ImageCommon.cache?.dataset.fullID is post.fullID
el = ImageCommon.popCache()

View File

@ -78,12 +78,12 @@ CatalogLinks =
if Conf['External Catalog'] and board in ['a', 'c', 'g', 'biz', 'k', 'm', 'o', 'p', 'v', 'vg', 'vr', 'w', 'wg', 'cm', '3', 'adv', 'an', 'asp', 'cgl', 'ck', 'co', 'diy', 'fa', 'fit', 'gd', 'int', 'jp', 'lit', 'mlp', 'mu', 'n', 'out', 'po', 'sci', 'sp', 'tg', 'toy', 'trv', 'tv', 'vp', 'wsg', 'x', 'f', 'pol', 's4s', 'lgbt']
"//catalog.neet.tv/#{board}/"
else if Conf['JSON Index'] and Conf['Use <%= meta.name %> Catalog']
if g.BOARD.ID is board and g.VIEW is 'index' then '#catalog' else "/#{board}/#catalog"
if location.hostname is 'boards.4chan.org' and g.BOARD.ID is board and g.VIEW is 'index' then '#catalog' else "//boards.4chan.org/#{board}/#catalog"
else
"/#{board}/catalog"
"//boards.4chan.org/#{board}/catalog"
index: (board=g.BOARD.ID) ->
if Conf['JSON Index'] and board isnt 'f'
if g.BOARD.ID is board and g.VIEW is 'index' then '#index' else "/#{board}/#index"
if location.hostname is 'boards.4chan.org' and g.BOARD.ID is board and g.VIEW is 'index' then '#index' else "//boards.4chan.org/#{board}/#index"
else
"/#{board}/"
"//boards.4chan.org/#{board}/"

View File

@ -326,7 +326,7 @@ QR =
$.replace node, $.tn '\n'
for node in $$ 'br', frag
$.replace node, $.tn '\n>' unless node is frag.lastChild
Post::insertTags frag
Site.insertTags?(frag)
for node in $$ '.linkify[data-original]', frag
$.replace node, $.tn node.dataset.original
for node in $$ '.embedder', frag

View File

@ -2,7 +2,7 @@ class DataBoard
@keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles']
constructor: (@key, sync, dontClean) ->
@data = Conf[@key]
@initData Conf[@key]
$.sync @key, @onSync
@clean() unless dontClean
return unless sync
@ -13,28 +13,34 @@ class DataBoard
@sync = sync
$.on d, '4chanXInitFinished', init
initData: (@allData) ->
if Site.hostname is '4chan.org' and @allData.boards
@data = @allData
else
@data = (@allData[Site.hostname] or= boards: {})
changes: []
save: (change, cb) ->
snapshot1 = JSON.stringify @data
snapshot1 = JSON.stringify @allData
change()
{changes} = @
changes.push change
$.get @key, {boards: {}}, (items) =>
@data = items[@key]
snapshot2 = JSON.stringify @data
@initData items[@key]
snapshot2 = JSON.stringify @allData
c() for c in changes
$.set @key, @data, =>
$.set @key, @allData, =>
@changes = []
@sync?() if snapshot1 isnt snapshot2
cb?()
forceSync: (cb) ->
snapshot1 = JSON.stringify @data
snapshot1 = JSON.stringify @allData
{changes} = @
$.get @key, {boards: {}}, (items) =>
@data = items[@key]
snapshot2 = JSON.stringify @data
@initData items[@key]
snapshot2 = JSON.stringify @allData
c() for c in changes
@sync?() if snapshot1 isnt snapshot2
cb?()
@ -103,6 +109,9 @@ class DataBoard
val or defaultValue
clean: ->
# XXX not yet multisite ready
return unless Site.software is 'yotsuba'
for boardID, val of @data.boards
@deleteIfEmpty {boardID}
@ -133,8 +142,8 @@ class DataBoard
threads[ID] = board[ID] if ID of board
@data.boards[boardID] = threads
@deleteIfEmpty {boardID}
$.set @key, @data
$.set @key, @allData
onSync: (data) =>
@data = data or boards: {}
@initData data
@sync?()

View File

@ -11,10 +11,10 @@ Post.Clone = class extends Post
@cloneWithoutVideo nodes.root
else
nodes.root.cloneNode true
Post.Clone.prefix or= 0
Post.Clone.suffix or= 0
for node in [root, $$('[id]', root)...]
node.id = Post.Clone.prefix + node.id
Post.Clone.prefix++
node.id += "_#{Post.Clone.suffix}"
Post.Clone.suffix++
# Remove inlined posts inside of this post.
for inline in $$ '.inline', root
@ -44,12 +44,10 @@ Post.Clone = class extends Post
@file = {}
for key, val of @origin.file
@file[key] = val
{fileRoot} = @nodes
@file.text = fileRoot.firstElementChild
@file.link = $ '.fileText > a, .fileText-original', fileRoot
@file.thumb = $ 'a.fileThumb > [data-md5]', fileRoot
for key, selector of Site.selectors.file
@file[key] = $ selector, @nodes.root
@file.thumbLink = @file.thumb?.parentNode
@file.fullImage = $ '.full-image', fileRoot
@file.fullImage = $ '.full-image', @file.thumbLink if @file.thumbLink
@file.videoControls = $ '.video-controls', @file.text
@file.thumb.muted = true if @file.videoThumb

View File

@ -6,7 +6,7 @@ class Post
@normalizedOriginal = Build.Test.normalize root
<% } %>
@ID = +root.id[2..]
@ID = +root.id.match(/\d*$/)[0]
@threadID = @thread.ID
@boardID = @board.ID
@fullID = "#{@board}.#{@ID}"
@ -16,15 +16,13 @@ class Post
@nodes = @parseNodes root
if not (@isReply = $.hasClass @nodes.post, 'reply')
if not (@isReply = @ID isnt @threadID)
@thread.OP = @
if @boardID is 'f'
for type in ['Sticky', 'Closed'] when (icon = $ "img[alt=#{type}]", @nodes.info)
$.addClass icon, "#{type.toLowerCase()}Icon", 'retina'
@thread.isArchived = !!$ '.archivedIcon', @nodes.info
@thread.isSticky = !!$ '.stickyIcon', @nodes.info
@thread.isClosed = @thread.isArchived or !!$ '.closedIcon', @nodes.info
@thread.kill() if @thread.isArchived
for key in ['isSticky', 'isClosed', 'isArchived']
@thread[key] = if (selector = 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
@ -36,7 +34,7 @@ class Post
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.dataset.utc * 1000)
date: if @nodes.date then new Date(@nodes.date.getAttribute('datetime')?.trim() or (@nodes.date.dataset.utc * 1000))
if Conf['Anonymize']
@info.nameBlock = 'Anonymous'
@ -66,30 +64,21 @@ class Post
g.posts.push @fullID, @
parseNodes: (root) ->
post = $ '.post', root
info = $ '.postInfo', post
s = Site.selectors
post = $(s.post, root) or root
info = $ s.infoRoot, post
nodes =
root: root
post: post
info: info
subject: $ '.subject', info
name: $ '.name', info
email: $ '.useremail', info
tripcode: $ '.postertrip', info
uniqueIDRoot: $ '.posteruid', info
uniqueID: $ '.posteruid > .hand', info
capcode: $ '.capcode.hand', info
pass: $ '.n-pu', info
flag: $ '.flag, .countryFlag', info
date: $ '.dateTime', info
nameBlock: $ '.nameBlock', info
quote: $ '.postNum > a:nth-of-type(2)', info
reply: $ '.replylink', info
fileRoot: $ '.file', post
comment: $ '.postMessage', post
quotelinks: []
root: root
post: post
info: info
comment: $ s.comment, post
quotelinks: []
archivelinks: []
embedlinks: []
for key, selector of s.info
nodes[key] = $ selector, info
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/
@ -113,7 +102,7 @@ class Post
# 'Comment too long'...
# EXIF data. (/p/)
@nodes.commentClean = bq = @nodes.comment.cloneNode true
@cleanComment bq
Site.cleanComment?(bq)
@info.comment = @nodesToText bq
commentDisplay: ->
@ -126,13 +115,13 @@ class Post
# Trailing spaces.
bq = @nodes.commentClean.cloneNode true
@cleanSpoilers bq unless Conf['Remove Spoilers'] or Conf['Reveal Spoilers']
@cleanCommentDisplay bq
Site.cleanCommentDisplay?(bq)
@nodesToText(bq).trim().replace(/\s+$/gm, '')
commentOrig: ->
# Get the comment's text for reposting purposes.
bq = @nodes.commentClean.cloneNode true
@insertTags bq
Site.insertTags?(bq)
@nodesToText bq
nodesToText: (bq) ->
@ -143,36 +132,15 @@ class Post
text += node.data or '\n'
text
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
cleanSpoilers: (bq) ->
spoilers = $$ 's', bq
spoilers = $$ Site.selectors.spoiler, bq
for node in spoilers
$.replace node, $.tn '[spoiler]'
return
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
parseQuotes: ->
@quotes = []
# XXX https://github.com/4chan/4chan-JS/issues/77
# 4chan currently creates quote links inside [code] tags; ignore them
for quotelink in $$ ':not(pre) > .quotelink', @nodes.comment
for quotelink in $$ Site.selectors.quotelink, @nodes.comment
@parseQuote quotelink
return
@ -183,13 +151,7 @@ class Post
# - catalog links. (>>>/b/catalog or >>>/b/search)
# - rules links. (>>>/a/rules)
# - text-board quotelinks. (>>>/img/1234)
match = quotelink.href.match ///
^https?://boards\.4chan\.org/+
([^/]+) # boardID
/+(?:res|thread)/+\d+(?:[/?][^#]*)?#p
(\d+) # postID
$
///
match = quotelink.href.match Site.regexp.quotelink
return unless match or (@isClone and quotelink.dataset.postID) # normal or resurrected quote
@nodes.quotelinks.push quotelink
@ -197,39 +159,28 @@ class Post
return if @isClone
# ES6 Set when?
fullID = "#{match[1]}.#{match[2]}"
fullID = "#{match[1]}.#{match[3]}"
@quotes.push fullID unless fullID in @quotes
parseFile: ->
{fileRoot} = @nodes
return unless fileRoot
return if not (link = $ '.fileText > a, .fileText-original > a', fileRoot)
return if not (info = link.nextSibling?.textContent.match /\(([\d.]+ [KMG]?B).*\)/)
fileText = fileRoot.firstElementChild
@file =
text: fileText
link: link
url: link.href
name: fileText.title or link.title or link.textContent
size: info[1]
isImage: /(jpg|png|gif)$/i.test link.href
isVideo: /webm$/i.test link.href
dimensions: info[0].match(/\d+x\d+/)?[0]
tag: info[0].match(/,[^,]*, ([a-z]+)\)/i)?[1]
MD5: fileText.dataset.md5
size = +@file.size.match(/[\d.]+/)[0]
unit = ['B', 'KB', 'MB', 'GB'].indexOf @file.size.match(/\w+$/)[0]
file = {}
for key, selector of Site.selectors.file
file[key] = $ selector, @nodes.root
file.thumbLink = file.thumb?.parentNode
return if not (file.text and file.link)
return if not Site.parseFile @, file
$.extend file,
url: file.link.href
isImage: /(jpg|png|gif)$/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
if (thumb = $ 'a.fileThumb > [data-md5]', fileRoot)
$.extend @file,
thumb: thumb
thumbLink: thumb.parentNode
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()}/#{@board}/#{m[0]}s.jpg"
file.sizeInBytes = size
@file = file
@deadMark =
# \u00A0 is nbsp

View File

@ -1119,3 +1119,7 @@ Config =
'updater.position': 'bottom: 0px; left: 0px;'
'thread-watcher.position': 'top: 50px; left: 0px;'
'qr.position': 'top: 50px; right: 0px;'
siteSoftware: """
4chan.org yotsuba
"""

View File

@ -1272,7 +1272,7 @@ span.hide-announcement {
.expanded-image > .post > .file > .fileThumb > img[data-md5] {
display: none;
}
.full-image {
.full-image[data-full-i-d] {
display: none;
cursor: pointer;
}

View File

@ -56,7 +56,7 @@ Main =
flatten null, Config
for db in DataBoard.keys
Conf[db] = boards: {}
Conf[db] = {}
Conf['boardConfig'] = boards: {}
Conf['archives'] = Redirect.archives
Conf['selectedArchives'] = {}
@ -74,15 +74,16 @@ Main =
Conf['Toggleable Thread Watcher'] = true
# Enforce JS whitelist
($.getSync or $.get) {'jsWhitelist': Conf['jsWhitelist']}, ({jsWhitelist}) ->
$.addCSP "script-src #{jsWhitelist.replace(/^#.*$/mg, '').replace(/[\s;]+/g, ' ').trim()}"
if /\.4chan\.org$/.test(location.hostname)
($.getSync or $.get) {'jsWhitelist': Conf['jsWhitelist']}, ({jsWhitelist}) ->
$.addCSP "script-src #{jsWhitelist.replace(/^#.*$/mg, '').replace(/[\s;]+/g, ' ').trim()}"
# Get saved values as items
items = {}
items[key] = undefined for key of Conf
items['previousversion'] = undefined
($.getSync or $.get) items, (items) ->
if !$.perProtocolSettings and (items['Redirect to HTTPS'] ? Conf['Redirect to HTTPS']) and location.protocol isnt 'https:'
if !$.perProtocolSettings and /\.4chan\.org$/.test(location.hostname) and (items['Redirect to HTTPS'] ? Conf['Redirect to HTTPS']) and location.protocol isnt 'https:'
location.replace('https:' + location.host + location.pathname + location.search + location.hash)
return
$.asap docSet, ->
@ -105,7 +106,7 @@ Main =
for key, val of Conf
Conf[key] = items[key] ? val
Main.initFeatures()
Site.init Main.initFeatures
upgrade: (items) ->
{previousversion} = items
@ -122,11 +123,10 @@ Main =
pathname = location.pathname.split /\/+/
g.BOARD = new Board pathname[1] unless hostname is 'www.4chan.org'
if hostname in ['boards.4chan.org', 'sys.4chan.org', 'www.4chan.org']
$.global ->
document.documentElement.classList.add 'js-enabled'
window.FCX = {}
Main.jsEnabled = $.hasClass doc, 'js-enabled'
$.global ->
document.documentElement.classList.add 'js-enabled'
window.FCX = {}
Main.jsEnabled = $.hasClass doc, 'js-enabled'
switch hostname
when 'www.4chan.org'
@ -151,7 +151,7 @@ Main =
if ImageHost.test hostname
return unless pathname[2] and not /[sm]\.jpg$/.test(pathname[2])
$.asap (-> d.readyState isnt 'loading'), ->
if Conf['404 Redirect'] and d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
if Conf['404 Redirect'] and Site.is404?()
Redirect.navigate 'file', {
boardID: g.BOARD.ID
filename: pathname[pathname.length - 1]
@ -166,14 +166,14 @@ Main =
ImageCommon.addControls video
return
return unless hostname is 'boards.4chan.org'
return if Site.isAuxiliaryPage?()
if pathname[2] in ['thread', 'res']
g.VIEW = 'thread'
g.THREADID = +pathname[3]
else if pathname[2] in ['catalog', 'archive']
g.VIEW = pathname[2]
else if pathname[2].match /^\d*$/
else if /^(?:catalog|archive)(?:\.html)?$/.test(pathname[2])
g.VIEW = pathname[2].replace('.html', '')
else if /^(?:index|\d*)(?:\.html)?$/.test(pathname[2])
g.VIEW = 'index'
else
return
@ -186,6 +186,7 @@ Main =
# c.time 'All initializations'
for [name, feature] in Main.features
continue if Site.disabledFeatures and name in Site.disabledFeatures
# c.time "#{name} initialization"
try
feature.init()
@ -245,9 +246,9 @@ Main =
$.rm Main.bgColorStyle
else
# Determine proper background color for dialogs if 4chan is using a special stylesheet.
div = $.el 'div',
className: 'reply'
div.style.cssText = 'position: absolute; visibility: hidden;'
div = Site.bgColoredEl()
div.style.position = 'absolute';
div.style.visibility = 'hidden';
$.add d.body, div
bgColor = window.getComputedStyle(div).backgroundColor
$.rm div
@ -265,42 +266,44 @@ Main =
}
initReady: ->
# XXX Sometimes threads don't 404 but are left over as stubs containing one garbage reply post.
if g.VIEW is 'thread' and (d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] or ($('.board') and not $('.opContainer')))
ThreadWatcher.set404 g.BOARD.ID, g.THREADID, ->
if Conf['404 Redirect']
Redirect.navigate 'thread',
boardID: g.BOARD.ID
threadID: g.THREADID
postID: +location.hash.match /\d+/ # post number or 0
, "/#{g.BOARD}/"
if Site.is404?()
if g.VIEW is 'thread'
ThreadWatcher.set404 g.BOARD.ID, g.THREADID, ->
if Conf['404 Redirect']
Redirect.navigate 'thread',
boardID: g.BOARD.ID
threadID: g.THREADID
postID: +location.hash.match /\d+/ # post number or 0
, "/#{g.BOARD}/"
return
return if d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
if g.VIEW in ['index', 'thread'] and not $('.board + *')
if Site.isIncomplete?()
msg = $.el 'div',
<%= html('The page didn&#039;t load completely.<br>Some features may not work unless you <a href="javascript:;">reload</a>.') %>
$.on $('a', msg), 'click', -> location.reload()
new Notice 'warning', msg
# Parse HTML or skip it and start building from JSON.
unless Conf['JSON Index'] and g.VIEW is 'index'
unless Index.enabled
Main.initThread()
else
Main.expectInitFinished = true
$.event '4chanXInitFinished'
initThread: ->
if (board = $ '.board')
s = Site.selectors
if (board = $ s.board)
threads = []
posts = []
for threadRoot in $$ '.board > .thread', board
thread = new Thread +threadRoot.id[1..], g.BOARD
for threadRoot in $$(s.thread, board)
thread = new Thread +threadRoot.id.match(/\d*$/)[0], g.BOARD
thread.nodes.root = threadRoot
threads.push thread
for postRoot in $$('.thread > .postContainer', threadRoot) when $('.postMessage', postRoot)
postRoots = $$ s.postContainer, threadRoot
postRoots.unshift threadRoot if Site.isOPContainerThread
for postRoot in postRoots when $(s.comment, postRoot)
try
posts.push new Post postRoot, thread, g.BOARD
catch err
@ -313,17 +316,7 @@ Main =
Main.handleErrors errors if errors
if g.VIEW is 'thread'
scriptData = Get.scriptData()
threads[0].postLimit = /\bbumplimit *= *1\b/.test scriptData
threads[0].fileLimit = /\bimagelimit *= *1\b/.test scriptData
threads[0].ipCount = if m = scriptData.match /\bunique_ips *= *(\d+)\b/ then +m[1]
if g.BOARD.ID is 'f' and g.VIEW is 'thread'
$.ajax "#{location.protocol}//a.4cdn.org/f/thread/#{g.THREADID}.json",
timeout: $.MINUTE
onloadend: ->
if @response and posts[0].file
posts[0].file.text.dataset.md5 = posts[0].file.MD5 = @response.posts[0].md5
Site.parseThreadMetadata?(threads[0])
Main.callbackNodes 'Thread', threads
Main.callbackNodesDB 'Post', posts, ->
@ -424,11 +417,12 @@ Main =
<%= html('<span class="report-error"> [<a href="${url}" target="_blank">report</a>]</span>') %>
isThisPageLegit: ->
# 404 error page or similar.
# not 404 error page or similar.
unless 'thisPageIsLegit' of Main
Main.thisPageIsLegit = location.hostname is 'boards.4chan.org' and
!$('link[href*="favicon-status.ico"]', d.head) and
d.title not in ['4chan - Temporarily Offline', '4chan - Error', '504 Gateway Time-out']
Main.thisPageIsLegit = if Site.isThisPageLegit
Site.isThisPageLegit()
else
!/^[45]\d\d\b/.test(document.title)
Main.thisPageIsLegit
ready: (cb) ->

View File

@ -384,6 +384,10 @@ $.oneItemSugar = (fn) ->
$.syncing = {}
$.securityCheck = (data) ->
if location.protocol isnt 'https:'
delete data['Redirect to HTTPS']
<% if (type === 'crx') { %>
# https://developer.chrome.com/extensions/storage.html
$.oldValue =
@ -485,6 +489,7 @@ do ->
$.set = $.oneItemSugar (data, cb) ->
return unless $.crxWorking()
$.securityCheck data
$.extend items.local, data
setArea 'local', cb
@ -536,6 +541,7 @@ if GM?.deleteValue? and window.BroadcastChannel and not GM_addValueChangeListene
cb items
$.set = $.oneItemSugar (items, cb) ->
$.securityCheck items
Promise.all(GM.setValue(g.NAMESPACE + key, JSON.stringify(val)) for key, val of items).then ->
$.syncChannel.postMessage items
cb?()
@ -655,6 +661,7 @@ else
cb items
$.set = $.oneItemSugar (items, cb) ->
$.securityCheck items
$.queueTask ->
for key, value of items
$.setValue(g.NAMESPACE + key, JSON.stringify value)

1
src/site/SW.js Normal file
View File

@ -0,0 +1 @@
SW = {};

View File

@ -0,0 +1,123 @@
SW.tinyboard =
isOPContainerThread: true
disabledFeatures: [
'Board Configuration'
'Normalize URL'
'Captcha Configuration'
'Image Host Rewriting'
'Index Generator'
'Announcement Hiding'
'Fourchan thingies'
'Custom CSS'
'Resurrect Quotes'
'Quick Reply Personas'
'Quick Reply'
'Cooldown'
'Pass Link'
'Index Generator (Menu)'
'Edit Link'
'Archive Link'
'Quote Inlining'
'Quote Previewing'
'Quote Backlinks'
'File Info Formatting'
'Image Expansion'
'Image Expansion (Menu)'
'Comment Expansion'
'Thread Expansion'
'Thread Stats'
'Thread Updater'
'Mark New IPs'
'Banner'
'Flash Features'
'Reply Pruning'
<% if (readJSON('/.tests_enabled')) { %>
'Build Test'
<% } %>
]
detect: ->
for script in $$ 'script:not([src])', d.head
return true if /\bvar configRoot=".*?"/.test(script.textContent)
false
selectors:
board: 'form[name="postcontrols"]'
thread: 'div[id^="thread_"]'
postContainer: '.reply' # postContainer is thread for OP
infoRoot: '.intro'
info:
subject: '.subject'
name: '.name'
email: '.email'
tripcode: '.trip'
uniqueID: '.poster_id'
capcode: '.capcode'
flag: '.flag'
date: 'time'
nameBlock: 'label'
quote: 'a[href*="#q"]'
reply: 'a[href*="/res/"]:not([href*="#"])'
icons:
isSticky: '.fa-thumb-tack'
isClosed: '.fa-lock'
file:
text: '.fileinfo'
link: '.fileinfo > a'
thumb: 'a > .post-image'
comment: '.body'
spoiler: '.spoiler'
quotelink: 'a[onclick^="highlightReply("]'
boardList: '.boardlist'
xpath:
thread: 'div[starts-with(@id,"thread_")]'
postContainer: 'div[starts-with(@id,"reply_") or starts-with(@id,"thread_")]'
regexp:
quotelink:
///
/
([^/]+) # boardID
/res/
(\d+) # threadID
\.html#
(\d+) # postID
$
///
bgColoredEl: ->
$.el 'div', className: 'post reply'
parseNodes: (post, nodes) ->
# Add vichan's span.poster_id around the ID if not already present.
return if nodes.uniqueID
nodes.info.normalize()
{nextSibling} = nodes.nameBlock
if nextSibling.nodeType is 3 and (m = nextSibling.textContent.match /(\s*ID:\s*)(\S+)/)
nextSibling = nextSibling.splitText m[1].length
nextSibling.splitText m[2].length
nodes.uniqueID = uniqueID = $.el 'span', {className: 'poster_id'}
$.replace nextSibling, uniqueID
$.add uniqueID, nextSibling
parseFile: (post, file) ->
{text, link, thumb} = file
return false if $.x("ancestor::#{Site.xpath.postContainer}[1]", text) isnt post.nodes.root # file belongs to a reply
return false if not (infoNode = link.nextElementSibling)
return false if not (info = infoNode.textContent.match /\((Spoiler Image, )?([\d.]+ [KMG]?B).*\)/)
nameNode = $ '.postfilename', text
$.extend file,
name: if nameNode then (nameNode.title or nameNode.textContent) else link.pathname.match(/[^/]*$/)[0]
size: info[2]
dimensions: info[0].match(/\d+x\d+/)?[0]
if thumb
$.extend file,
thumbURL: if '/static/' in thumb.src then link.href else thumb.src
isSpoiler: !!info[1]
true
isThumbExpanded: (file) ->
# Detect old Tinyboard image expansion that changes src attribute on thumbnail.
$.hasClass file.thumb.parentNode, 'expanded'

135
src/site/SW.yotsuba.coffee Normal file
View File

@ -0,0 +1,135 @@
SW.yotsuba =
isOPContainerThread: false
selectors:
board: '.board'
thread: '.thread'
postContainer: '.postContainer'
sideArrows: '.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, .countryFlag'
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]'
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
boardList: '#boardNavDesktop > .boardList'
xpath:
thread: 'div[contains(concat(" ",@class," ")," thread ")]'
postContainer: 'div[contains(@class,"postContainer")]'
regexp:
quotelink:
///
^https?://boards\.4chan\.org/+
([^/]+) # boardID
/+thread/+
(\d+) # threadID
(?:[/?][^#]*)?
(?:#p
(\d+) # postID
)?
$
///
bgColoredEl: ->
$.el 'div', className: 'reply'
isThisPageLegit: ->
# not 404 error page or similar.
location.hostname is 'boards.4chan.org' and
!$('link[href*="favicon-status.ico"]', d.head) and
d.title not in ['4chan - Temporarily Offline', '4chan - Error', '504 Gateway Time-out']
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 + *')
isAuxiliaryPage: ->
location.hostname isnt 'boards.4chan.org'
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 "#{location.protocol}//a.4cdn.org/f/thread/#{thread}.json",
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'
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

24
src/site/Site.coffee Normal file
View File

@ -0,0 +1,24 @@
Site =
init: (cb) ->
swDict = {}
for line in Conf['siteSoftware'].split('\n') when line[0] isnt '#'
[hostname, software] = line.split(' ')
swDict[hostname] = software if software of SW
{hostname} = location
while hostname and hostname not of swDict
hostname = hostname.replace(/^[^.]*\.?/, '')
if hostname
@set hostname, swDict[hostname]
cb()
else
$.onExists doc, 'body', =>
for software of SW
if SW[software].detect?()
@set location.hostname.replace(/^www\./, ''), software
Conf['siteSoftware'] += "\n#{@hostname} #{@software}"
$.set 'siteSoftware', Conf['siteSoftware']
cb()
return
set: (@hostname, @software) ->
$.extend @, SW[@software]