Merge branch 'multisite'
This commit is contained in:
commit
ebb9cc11e6
2
Makefile
2
Makefile
@ -32,7 +32,7 @@ $(eval $(shell node tools/pkgvars.js))
|
|||||||
version = $(shell node -p "JSON.parse(require('fs').readFileSync('version.json')).version")
|
version = $(shell node -p "JSON.parse(require('fs').readFileSync('version.json')).version")
|
||||||
|
|
||||||
source_directories := \
|
source_directories := \
|
||||||
globals config css platform classes \
|
globals config css platform classes site \
|
||||||
Archive Filtering General Images Linkification \
|
Archive Filtering General Images Linkification \
|
||||||
Menu Miscellaneous Monitoring Posting Quotelinks \
|
Menu Miscellaneous Monitoring Posting Quotelinks \
|
||||||
main
|
main
|
||||||
|
|||||||
@ -24,9 +24,14 @@ PostHiding =
|
|||||||
Recursive.add PostHiding.hide, @, data.makeStub, true
|
Recursive.add PostHiding.hide, @, data.makeStub, true
|
||||||
|
|
||||||
return unless Conf['Reply Hiding Buttons']
|
return unless Conf['Reply Hiding Buttons']
|
||||||
sideArrows = $('.sideArrows', @nodes.root)
|
|
||||||
$.replace sideArrows.firstChild, PostHiding.makeButton @, 'hide'
|
button = PostHiding.makeButton @, 'hide'
|
||||||
sideArrows.removeAttribute 'class'
|
if (sa = Site.selectors.sideArrows)
|
||||||
|
sideArrows = $ sa, @nodes.root
|
||||||
|
$.replace sideArrows.firstChild, button
|
||||||
|
sideArrows.removeAttribute 'class'
|
||||||
|
else
|
||||||
|
$.prepend @nodes.root, button
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
init: ->
|
init: ->
|
||||||
|
|||||||
@ -43,5 +43,6 @@ BoardConfig =
|
|||||||
board for board, data of (@boards or Conf['boardConfig'].boards) when !!data.ws_board is sfw
|
board for board, data of (@boards or Conf['boardConfig'].boards) when !!data.ws_board is sfw
|
||||||
|
|
||||||
noAudio: (boardID) ->
|
noAudio: (boardID) ->
|
||||||
|
return false unless Site.software is 'yotsuba'
|
||||||
boards = @boards or Conf['boardConfig'].boards
|
boards = @boards or Conf['boardConfig'].boards
|
||||||
boards and !boards[boardID].webm_audio
|
boards and !boards[boardID].webm_audio
|
||||||
|
|||||||
@ -10,25 +10,24 @@ Get =
|
|||||||
excerpt
|
excerpt
|
||||||
threadFromRoot: (root) ->
|
threadFromRoot: (root) ->
|
||||||
return null unless root?
|
return null unless root?
|
||||||
g.threads["#{g.BOARD}.#{root.id[1..]}"]
|
g.threads["#{g.BOARD}.#{root.id.match(/\d*$/)[0]}"]
|
||||||
threadFromNode: (node) ->
|
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) ->
|
postFromRoot: (root) ->
|
||||||
return null unless root?
|
return null unless root?
|
||||||
post = g.posts[root.dataset.fullID]
|
post = g.posts[root.dataset.fullID]
|
||||||
index = root.dataset.clone
|
index = root.dataset.clone
|
||||||
if index then post.clones[index] else post
|
if index then post.clones[index] else post
|
||||||
postFromNode: (root) ->
|
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) ->
|
postDataFromLink: (link) ->
|
||||||
if link.hostname is 'boards.4chan.org'
|
if link.dataset.postID # resurrected quote
|
||||||
path = link.pathname.split /\/+/
|
|
||||||
boardID = path[1]
|
|
||||||
threadID = path[3]
|
|
||||||
postID = if link.hash then link.hash[2..] else path[3]
|
|
||||||
else # resurrected quote
|
|
||||||
{boardID, threadID, postID} = link.dataset
|
{boardID, threadID, postID} = link.dataset
|
||||||
threadID or= 0
|
threadID or= 0
|
||||||
|
else
|
||||||
|
match = link.href.match Site.regexp.quotelink
|
||||||
|
[boardID, threadID, postID] = match[1..]
|
||||||
|
postID or= threadID
|
||||||
return {
|
return {
|
||||||
boardID: boardID
|
boardID: boardID
|
||||||
threadID: +threadID
|
threadID: +threadID
|
||||||
@ -64,8 +63,3 @@ Get =
|
|||||||
quotelinks.filter (quotelink) ->
|
quotelinks.filter (quotelink) ->
|
||||||
{boardID, postID} = Get.postDataFromLink quotelink
|
{boardID, postID} = Get.postDataFromLink quotelink
|
||||||
boardID is post.board.ID and postID is post.ID
|
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
|
|
||||||
''
|
|
||||||
|
|||||||
@ -84,15 +84,15 @@ Header =
|
|||||||
$.on window, 'load popstate', Header.hashScroll
|
$.on window, 'load popstate', Header.hashScroll
|
||||||
$.on d, 'CreateNotification', @createNotification
|
$.on d, 'CreateNotification', @createNotification
|
||||||
|
|
||||||
|
@setBoardList()
|
||||||
|
|
||||||
$.onExists doc, 'body', =>
|
$.onExists doc, 'body', =>
|
||||||
return unless Main.isThisPageLegit()
|
return unless Main.isThisPageLegit()
|
||||||
$.prepend d.body, @bar
|
$.prepend d.body, @bar
|
||||||
$.add d.body, Header.hover
|
$.add d.body, Header.hover
|
||||||
@setBarPosition Conf['Bottom Header']
|
@setBarPosition Conf['Bottom Header']
|
||||||
|
|
||||||
# Wait for #boardNavMobile instead of #boardNavDesktop,
|
$.onExists doc, "#{Site.selectors.boardList} + *", Header.generateFullBoardList
|
||||||
# it might be incomplete otherwise.
|
|
||||||
$.onExists doc, '#boardNavMobile', Header.setBoardList
|
|
||||||
|
|
||||||
Main.ready ->
|
Main.ready ->
|
||||||
if not (footer = $.id 'boardNavDesktopFoot')
|
if not (footer = $.id 'boardNavDesktopFoot')
|
||||||
@ -154,9 +154,20 @@ Header =
|
|||||||
btn = $('.hide-board-list-button', boardList)
|
btn = $('.hide-board-list-button', boardList)
|
||||||
$.on btn, 'click', Header.toggleBoardList
|
$.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 = []
|
nodes = []
|
||||||
spacer = -> $.el 'span', className: 'spacer'
|
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
|
switch node.nodeName
|
||||||
when '#text'
|
when '#text'
|
||||||
for chr in node.nodeValue
|
for chr in node.nodeValue
|
||||||
@ -169,18 +180,10 @@ Header =
|
|||||||
a = node.cloneNode true
|
a = node.cloneNode true
|
||||||
a.className = 'current' if a.pathname.split('/')[1] is g.BOARD.ID
|
a.className = 'current' if a.pathname.split('/')[1] is g.BOARD.ID
|
||||||
nodes.push a
|
nodes.push a
|
||||||
fullBoardList = $ '.boardList', boardList
|
fullBoardList = $ '.boardList', Header.boardList
|
||||||
$.add fullBoardList, nodes
|
$.add fullBoardList, nodes
|
||||||
CatalogLinks.setLinks fullBoardList
|
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) ->
|
generateBoardList: (boardnav) ->
|
||||||
list = $ '#custom-board-list', Header.boardList
|
list = $ '#custom-board-list', Header.boardList
|
||||||
$.rmAll list
|
$.rmAll list
|
||||||
@ -224,7 +227,17 @@ Header =
|
|||||||
return a
|
return a
|
||||||
|
|
||||||
boardID = t.split('-')[0]
|
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 ->
|
a = do ->
|
||||||
if boardID is '@'
|
if boardID is '@'
|
||||||
@ -237,13 +250,13 @@ Header =
|
|||||||
return a.cloneNode true
|
return a.cloneNode true
|
||||||
|
|
||||||
a = $.el 'a',
|
a = $.el 'a',
|
||||||
href: "/#{boardID}/"
|
href: "//boards.4chan.org/#{boardID}/"
|
||||||
textContent: boardID
|
textContent: boardID
|
||||||
a.href += g.VIEW if g.VIEW in ['catalog', 'archive']
|
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
|
||||||
|
|
||||||
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
|
a.title or a.textContent
|
||||||
else if /-full/.test t
|
else if /-full/.test t
|
||||||
("/#{boardID}/") + (if a.title then " - #{a.title}" else '')
|
("/#{boardID}/") + (if a.title then " - #{a.title}" else '')
|
||||||
@ -271,7 +284,7 @@ Header =
|
|||||||
|
|
||||||
if /-expired/.test t
|
if /-expired/.test t
|
||||||
if boardID not in ['b', 'f', 'trash', 'bant']
|
if boardID not in ['b', 'f', 'trash', 'bant']
|
||||||
a.href = "/#{boardID}/archive"
|
a.href = "//boards.4chan.org/#{boardID}/archive"
|
||||||
else
|
else
|
||||||
return a.firstChild # Its text node.
|
return a.firstChild # Its text node.
|
||||||
|
|
||||||
|
|||||||
@ -169,7 +169,7 @@ Settings =
|
|||||||
# Unsupported options
|
# Unsupported options
|
||||||
if $.engine isnt 'gecko'
|
if $.engine isnt 'gecko'
|
||||||
$('div[data-name="Remember QR Size"]', section).hidden = true
|
$('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
|
$('div[data-name="Redirect to HTTPS"]', section).hidden = true
|
||||||
|
|
||||||
$.get items, (items) ->
|
$.get items, (items) ->
|
||||||
|
|||||||
@ -82,8 +82,8 @@ Gallery =
|
|||||||
|
|
||||||
$.on window, 'resize', Gallery.cb.setHeight
|
$.on window, 'resize', Gallery.cb.setHeight
|
||||||
|
|
||||||
for file in $$ '.post .file'
|
for postThumb in $$ Site.selectors.file.thumb
|
||||||
post = Get.postFromNode file
|
post = Get.postFromNode postThumb
|
||||||
continue unless post.file?.thumb
|
continue unless post.file?.thumb
|
||||||
Gallery.generateThumb post
|
Gallery.generateThumb post
|
||||||
# If no image to open is given, pick image we have scrolled to.
|
# If no image to open is given, pick image we have scrolled to.
|
||||||
|
|||||||
@ -23,7 +23,7 @@ ImageHover =
|
|||||||
return unless doc.contains @
|
return unless doc.contains @
|
||||||
{file} = post
|
{file} = post
|
||||||
{isVideo} = file
|
{isVideo} = file
|
||||||
return if file.isExpanding or file.isExpanded
|
return if file.isExpanding or file.isExpanded or Site.isThumbExpanded?(file)
|
||||||
error = ImageHover.error post
|
error = ImageHover.error post
|
||||||
if ImageCommon.cache?.dataset.fullID is post.fullID
|
if ImageCommon.cache?.dataset.fullID is post.fullID
|
||||||
el = ImageCommon.popCache()
|
el = ImageCommon.popCache()
|
||||||
|
|||||||
@ -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']
|
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}/"
|
"//catalog.neet.tv/#{board}/"
|
||||||
else if Conf['JSON Index'] and Conf['Use <%= meta.name %> Catalog']
|
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
|
else
|
||||||
"/#{board}/catalog"
|
"//boards.4chan.org/#{board}/catalog"
|
||||||
|
|
||||||
index: (board=g.BOARD.ID) ->
|
index: (board=g.BOARD.ID) ->
|
||||||
if Conf['JSON Index'] and board isnt 'f'
|
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
|
else
|
||||||
"/#{board}/"
|
"//boards.4chan.org/#{board}/"
|
||||||
|
|||||||
@ -326,7 +326,7 @@ QR =
|
|||||||
$.replace node, $.tn '\n'
|
$.replace node, $.tn '\n'
|
||||||
for node in $$ 'br', frag
|
for node in $$ 'br', frag
|
||||||
$.replace node, $.tn '\n>' unless node is frag.lastChild
|
$.replace node, $.tn '\n>' unless node is frag.lastChild
|
||||||
Post::insertTags frag
|
Site.insertTags?(frag)
|
||||||
for node in $$ '.linkify[data-original]', frag
|
for node in $$ '.linkify[data-original]', frag
|
||||||
$.replace node, $.tn node.dataset.original
|
$.replace node, $.tn node.dataset.original
|
||||||
for node in $$ '.embedder', frag
|
for node in $$ '.embedder', frag
|
||||||
|
|||||||
@ -2,7 +2,7 @@ class DataBoard
|
|||||||
@keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles']
|
@keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads', 'customTitles']
|
||||||
|
|
||||||
constructor: (@key, sync, dontClean) ->
|
constructor: (@key, sync, dontClean) ->
|
||||||
@data = Conf[@key]
|
@initData Conf[@key]
|
||||||
$.sync @key, @onSync
|
$.sync @key, @onSync
|
||||||
@clean() unless dontClean
|
@clean() unless dontClean
|
||||||
return unless sync
|
return unless sync
|
||||||
@ -13,28 +13,34 @@ class DataBoard
|
|||||||
@sync = sync
|
@sync = sync
|
||||||
$.on d, '4chanXInitFinished', init
|
$.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: []
|
changes: []
|
||||||
|
|
||||||
save: (change, cb) ->
|
save: (change, cb) ->
|
||||||
snapshot1 = JSON.stringify @data
|
snapshot1 = JSON.stringify @allData
|
||||||
change()
|
change()
|
||||||
{changes} = @
|
{changes} = @
|
||||||
changes.push change
|
changes.push change
|
||||||
$.get @key, {boards: {}}, (items) =>
|
$.get @key, {boards: {}}, (items) =>
|
||||||
@data = items[@key]
|
@initData items[@key]
|
||||||
snapshot2 = JSON.stringify @data
|
snapshot2 = JSON.stringify @allData
|
||||||
c() for c in changes
|
c() for c in changes
|
||||||
$.set @key, @data, =>
|
$.set @key, @allData, =>
|
||||||
@changes = []
|
@changes = []
|
||||||
@sync?() if snapshot1 isnt snapshot2
|
@sync?() if snapshot1 isnt snapshot2
|
||||||
cb?()
|
cb?()
|
||||||
|
|
||||||
forceSync: (cb) ->
|
forceSync: (cb) ->
|
||||||
snapshot1 = JSON.stringify @data
|
snapshot1 = JSON.stringify @allData
|
||||||
{changes} = @
|
{changes} = @
|
||||||
$.get @key, {boards: {}}, (items) =>
|
$.get @key, {boards: {}}, (items) =>
|
||||||
@data = items[@key]
|
@initData items[@key]
|
||||||
snapshot2 = JSON.stringify @data
|
snapshot2 = JSON.stringify @allData
|
||||||
c() for c in changes
|
c() for c in changes
|
||||||
@sync?() if snapshot1 isnt snapshot2
|
@sync?() if snapshot1 isnt snapshot2
|
||||||
cb?()
|
cb?()
|
||||||
@ -103,6 +109,9 @@ class DataBoard
|
|||||||
val or defaultValue
|
val or defaultValue
|
||||||
|
|
||||||
clean: ->
|
clean: ->
|
||||||
|
# XXX not yet multisite ready
|
||||||
|
return unless Site.software is 'yotsuba'
|
||||||
|
|
||||||
for boardID, val of @data.boards
|
for boardID, val of @data.boards
|
||||||
@deleteIfEmpty {boardID}
|
@deleteIfEmpty {boardID}
|
||||||
|
|
||||||
@ -133,8 +142,8 @@ class DataBoard
|
|||||||
threads[ID] = board[ID] if ID of board
|
threads[ID] = board[ID] if ID of board
|
||||||
@data.boards[boardID] = threads
|
@data.boards[boardID] = threads
|
||||||
@deleteIfEmpty {boardID}
|
@deleteIfEmpty {boardID}
|
||||||
$.set @key, @data
|
$.set @key, @allData
|
||||||
|
|
||||||
onSync: (data) =>
|
onSync: (data) =>
|
||||||
@data = data or boards: {}
|
@initData data
|
||||||
@sync?()
|
@sync?()
|
||||||
|
|||||||
@ -11,10 +11,10 @@ Post.Clone = class extends Post
|
|||||||
@cloneWithoutVideo nodes.root
|
@cloneWithoutVideo nodes.root
|
||||||
else
|
else
|
||||||
nodes.root.cloneNode true
|
nodes.root.cloneNode true
|
||||||
Post.Clone.prefix or= 0
|
Post.Clone.suffix or= 0
|
||||||
for node in [root, $$('[id]', root)...]
|
for node in [root, $$('[id]', root)...]
|
||||||
node.id = Post.Clone.prefix + node.id
|
node.id += "_#{Post.Clone.suffix}"
|
||||||
Post.Clone.prefix++
|
Post.Clone.suffix++
|
||||||
|
|
||||||
# Remove inlined posts inside of this post.
|
# Remove inlined posts inside of this post.
|
||||||
for inline in $$ '.inline', root
|
for inline in $$ '.inline', root
|
||||||
@ -44,12 +44,10 @@ Post.Clone = class extends Post
|
|||||||
@file = {}
|
@file = {}
|
||||||
for key, val of @origin.file
|
for key, val of @origin.file
|
||||||
@file[key] = val
|
@file[key] = val
|
||||||
{fileRoot} = @nodes
|
for key, selector of Site.selectors.file
|
||||||
@file.text = fileRoot.firstElementChild
|
@file[key] = $ selector, @nodes.root
|
||||||
@file.link = $ '.fileText > a, .fileText-original', fileRoot
|
|
||||||
@file.thumb = $ 'a.fileThumb > [data-md5]', fileRoot
|
|
||||||
@file.thumbLink = @file.thumb?.parentNode
|
@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.videoControls = $ '.video-controls', @file.text
|
||||||
|
|
||||||
@file.thumb.muted = true if @file.videoThumb
|
@file.thumb.muted = true if @file.videoThumb
|
||||||
|
|||||||
@ -6,7 +6,7 @@ class Post
|
|||||||
@normalizedOriginal = Build.Test.normalize root
|
@normalizedOriginal = Build.Test.normalize root
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
@ID = +root.id[2..]
|
@ID = +root.id.match(/\d*$/)[0]
|
||||||
@threadID = @thread.ID
|
@threadID = @thread.ID
|
||||||
@boardID = @board.ID
|
@boardID = @board.ID
|
||||||
@fullID = "#{@board}.#{@ID}"
|
@fullID = "#{@board}.#{@ID}"
|
||||||
@ -16,15 +16,13 @@ class Post
|
|||||||
|
|
||||||
@nodes = @parseNodes root
|
@nodes = @parseNodes root
|
||||||
|
|
||||||
if not (@isReply = $.hasClass @nodes.post, 'reply')
|
if not (@isReply = @ID isnt @threadID)
|
||||||
@thread.OP = @
|
@thread.OP = @
|
||||||
if @boardID is 'f'
|
for key in ['isSticky', 'isClosed', 'isArchived']
|
||||||
for type in ['Sticky', 'Closed'] when (icon = $ "img[alt=#{type}]", @nodes.info)
|
@thread[key] = if (selector = Site.selectors.icons[key]) then !!$(selector, @nodes.info) else false
|
||||||
$.addClass icon, "#{type.toLowerCase()}Icon", 'retina'
|
if @thread.isArchived
|
||||||
@thread.isArchived = !!$ '.archivedIcon', @nodes.info
|
@thread.isClosed = true
|
||||||
@thread.isSticky = !!$ '.stickyIcon', @nodes.info
|
@thread.kill()
|
||||||
@thread.isClosed = @thread.isArchived or !!$ '.closedIcon', @nodes.info
|
|
||||||
@thread.kill() if @thread.isArchived
|
|
||||||
|
|
||||||
@info =
|
@info =
|
||||||
subject: @nodes.subject?.textContent or undefined
|
subject: @nodes.subject?.textContent or undefined
|
||||||
@ -36,7 +34,7 @@ class Post
|
|||||||
flagCode: @nodes.flag?.className.match(/flag-(\w+)/)?[1].toUpperCase()
|
flagCode: @nodes.flag?.className.match(/flag-(\w+)/)?[1].toUpperCase()
|
||||||
flagCodeTroll: @nodes.flag?.src?.match(/(\w+)\.gif$/)?[1].toUpperCase()
|
flagCodeTroll: @nodes.flag?.src?.match(/(\w+)\.gif$/)?[1].toUpperCase()
|
||||||
flag: @nodes.flag?.title
|
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']
|
if Conf['Anonymize']
|
||||||
@info.nameBlock = 'Anonymous'
|
@info.nameBlock = 'Anonymous'
|
||||||
@ -66,30 +64,21 @@ class Post
|
|||||||
g.posts.push @fullID, @
|
g.posts.push @fullID, @
|
||||||
|
|
||||||
parseNodes: (root) ->
|
parseNodes: (root) ->
|
||||||
post = $ '.post', root
|
s = Site.selectors
|
||||||
info = $ '.postInfo', post
|
post = $(s.post, root) or root
|
||||||
|
info = $ s.infoRoot, post
|
||||||
nodes =
|
nodes =
|
||||||
root: root
|
root: root
|
||||||
post: post
|
post: post
|
||||||
info: info
|
info: info
|
||||||
subject: $ '.subject', info
|
comment: $ s.comment, post
|
||||||
name: $ '.name', info
|
quotelinks: []
|
||||||
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: []
|
|
||||||
archivelinks: []
|
archivelinks: []
|
||||||
embedlinks: []
|
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.
|
# XXX Edge invalidates HTMLCollections when an ancestor node is inserted into another node.
|
||||||
# https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7560353/
|
# https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7560353/
|
||||||
@ -113,7 +102,7 @@ class Post
|
|||||||
# 'Comment too long'...
|
# 'Comment too long'...
|
||||||
# EXIF data. (/p/)
|
# EXIF data. (/p/)
|
||||||
@nodes.commentClean = bq = @nodes.comment.cloneNode true
|
@nodes.commentClean = bq = @nodes.comment.cloneNode true
|
||||||
@cleanComment bq
|
Site.cleanComment?(bq)
|
||||||
@info.comment = @nodesToText bq
|
@info.comment = @nodesToText bq
|
||||||
|
|
||||||
commentDisplay: ->
|
commentDisplay: ->
|
||||||
@ -126,13 +115,13 @@ class Post
|
|||||||
# Trailing spaces.
|
# Trailing spaces.
|
||||||
bq = @nodes.commentClean.cloneNode true
|
bq = @nodes.commentClean.cloneNode true
|
||||||
@cleanSpoilers bq unless Conf['Remove Spoilers'] or Conf['Reveal Spoilers']
|
@cleanSpoilers bq unless Conf['Remove Spoilers'] or Conf['Reveal Spoilers']
|
||||||
@cleanCommentDisplay bq
|
Site.cleanCommentDisplay?(bq)
|
||||||
@nodesToText(bq).trim().replace(/\s+$/gm, '')
|
@nodesToText(bq).trim().replace(/\s+$/gm, '')
|
||||||
|
|
||||||
commentOrig: ->
|
commentOrig: ->
|
||||||
# Get the comment's text for reposting purposes.
|
# Get the comment's text for reposting purposes.
|
||||||
bq = @nodes.commentClean.cloneNode true
|
bq = @nodes.commentClean.cloneNode true
|
||||||
@insertTags bq
|
Site.insertTags?(bq)
|
||||||
@nodesToText bq
|
@nodesToText bq
|
||||||
|
|
||||||
nodesToText: (bq) ->
|
nodesToText: (bq) ->
|
||||||
@ -143,36 +132,15 @@ class Post
|
|||||||
text += node.data or '\n'
|
text += node.data or '\n'
|
||||||
text
|
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) ->
|
cleanSpoilers: (bq) ->
|
||||||
spoilers = $$ 's', bq
|
spoilers = $$ Site.selectors.spoiler, bq
|
||||||
for node in spoilers
|
for node in spoilers
|
||||||
$.replace node, $.tn '[spoiler]'
|
$.replace node, $.tn '[spoiler]'
|
||||||
return
|
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: ->
|
parseQuotes: ->
|
||||||
@quotes = []
|
@quotes = []
|
||||||
# XXX https://github.com/4chan/4chan-JS/issues/77
|
for quotelink in $$ Site.selectors.quotelink, @nodes.comment
|
||||||
# 4chan currently creates quote links inside [code] tags; ignore them
|
|
||||||
for quotelink in $$ ':not(pre) > .quotelink', @nodes.comment
|
|
||||||
@parseQuote quotelink
|
@parseQuote quotelink
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -183,13 +151,7 @@ class Post
|
|||||||
# - catalog links. (>>>/b/catalog or >>>/b/search)
|
# - catalog links. (>>>/b/catalog or >>>/b/search)
|
||||||
# - rules links. (>>>/a/rules)
|
# - rules links. (>>>/a/rules)
|
||||||
# - text-board quotelinks. (>>>/img/1234)
|
# - text-board quotelinks. (>>>/img/1234)
|
||||||
match = quotelink.href.match ///
|
match = quotelink.href.match Site.regexp.quotelink
|
||||||
^https?://boards\.4chan\.org/+
|
|
||||||
([^/]+) # boardID
|
|
||||||
/+(?:res|thread)/+\d+(?:[/?][^#]*)?#p
|
|
||||||
(\d+) # postID
|
|
||||||
$
|
|
||||||
///
|
|
||||||
return unless match or (@isClone and quotelink.dataset.postID) # normal or resurrected quote
|
return unless match or (@isClone and quotelink.dataset.postID) # normal or resurrected quote
|
||||||
|
|
||||||
@nodes.quotelinks.push quotelink
|
@nodes.quotelinks.push quotelink
|
||||||
@ -197,39 +159,28 @@ class Post
|
|||||||
return if @isClone
|
return if @isClone
|
||||||
|
|
||||||
# ES6 Set when?
|
# ES6 Set when?
|
||||||
fullID = "#{match[1]}.#{match[2]}"
|
fullID = "#{match[1]}.#{match[3]}"
|
||||||
@quotes.push fullID unless fullID in @quotes
|
@quotes.push fullID unless fullID in @quotes
|
||||||
|
|
||||||
parseFile: ->
|
parseFile: ->
|
||||||
{fileRoot} = @nodes
|
file = {}
|
||||||
return unless fileRoot
|
for key, selector of Site.selectors.file
|
||||||
return if not (link = $ '.fileText > a, .fileText-original > a', fileRoot)
|
file[key] = $ selector, @nodes.root
|
||||||
return if not (info = link.nextSibling?.textContent.match /\(([\d.]+ [KMG]?B).*\)/)
|
file.thumbLink = file.thumb?.parentNode
|
||||||
fileText = fileRoot.firstElementChild
|
|
||||||
@file =
|
return if not (file.text and file.link)
|
||||||
text: fileText
|
return if not Site.parseFile @, file
|
||||||
link: link
|
|
||||||
url: link.href
|
$.extend file,
|
||||||
name: fileText.title or link.title or link.textContent
|
url: file.link.href
|
||||||
size: info[1]
|
isImage: /(jpg|png|gif)$/i.test file.link.href
|
||||||
isImage: /(jpg|png|gif)$/i.test link.href
|
isVideo: /(webm|mp4)$/i.test file.link.href
|
||||||
isVideo: /webm$/i.test link.href
|
size = +file.size.match(/[\d.]+/)[0]
|
||||||
dimensions: info[0].match(/\d+x\d+/)?[0]
|
unit = ['B', 'KB', 'MB', 'GB'].indexOf file.size.match(/\w+$/)[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]
|
|
||||||
size *= 1024 while unit-- > 0
|
size *= 1024 while unit-- > 0
|
||||||
@file.sizeInBytes = size
|
file.sizeInBytes = size
|
||||||
if (thumb = $ 'a.fileThumb > [data-md5]', fileRoot)
|
|
||||||
$.extend @file,
|
@file = 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"
|
|
||||||
|
|
||||||
@deadMark =
|
@deadMark =
|
||||||
# \u00A0 is nbsp
|
# \u00A0 is nbsp
|
||||||
|
|||||||
@ -1119,3 +1119,7 @@ Config =
|
|||||||
'updater.position': 'bottom: 0px; left: 0px;'
|
'updater.position': 'bottom: 0px; left: 0px;'
|
||||||
'thread-watcher.position': 'top: 50px; left: 0px;'
|
'thread-watcher.position': 'top: 50px; left: 0px;'
|
||||||
'qr.position': 'top: 50px; right: 0px;'
|
'qr.position': 'top: 50px; right: 0px;'
|
||||||
|
|
||||||
|
siteSoftware: """
|
||||||
|
4chan.org yotsuba
|
||||||
|
"""
|
||||||
|
|||||||
@ -1272,7 +1272,7 @@ span.hide-announcement {
|
|||||||
.expanded-image > .post > .file > .fileThumb > img[data-md5] {
|
.expanded-image > .post > .file > .fileThumb > img[data-md5] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.full-image {
|
.full-image[data-full-i-d] {
|
||||||
display: none;
|
display: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,7 @@ Main =
|
|||||||
flatten null, Config
|
flatten null, Config
|
||||||
|
|
||||||
for db in DataBoard.keys
|
for db in DataBoard.keys
|
||||||
Conf[db] = boards: {}
|
Conf[db] = {}
|
||||||
Conf['boardConfig'] = boards: {}
|
Conf['boardConfig'] = boards: {}
|
||||||
Conf['archives'] = Redirect.archives
|
Conf['archives'] = Redirect.archives
|
||||||
Conf['selectedArchives'] = {}
|
Conf['selectedArchives'] = {}
|
||||||
@ -74,15 +74,16 @@ Main =
|
|||||||
Conf['Toggleable Thread Watcher'] = true
|
Conf['Toggleable Thread Watcher'] = true
|
||||||
|
|
||||||
# Enforce JS whitelist
|
# Enforce JS whitelist
|
||||||
($.getSync or $.get) {'jsWhitelist': Conf['jsWhitelist']}, ({jsWhitelist}) ->
|
if /\.4chan\.org$/.test(location.hostname)
|
||||||
$.addCSP "script-src #{jsWhitelist.replace(/^#.*$/mg, '').replace(/[\s;]+/g, ' ').trim()}"
|
($.getSync or $.get) {'jsWhitelist': Conf['jsWhitelist']}, ({jsWhitelist}) ->
|
||||||
|
$.addCSP "script-src #{jsWhitelist.replace(/^#.*$/mg, '').replace(/[\s;]+/g, ' ').trim()}"
|
||||||
|
|
||||||
# Get saved values as items
|
# Get saved values as items
|
||||||
items = {}
|
items = {}
|
||||||
items[key] = undefined for key of Conf
|
items[key] = undefined for key of Conf
|
||||||
items['previousversion'] = undefined
|
items['previousversion'] = undefined
|
||||||
($.getSync or $.get) items, (items) ->
|
($.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)
|
location.replace('https:' + location.host + location.pathname + location.search + location.hash)
|
||||||
return
|
return
|
||||||
$.asap docSet, ->
|
$.asap docSet, ->
|
||||||
@ -105,7 +106,7 @@ Main =
|
|||||||
for key, val of Conf
|
for key, val of Conf
|
||||||
Conf[key] = items[key] ? val
|
Conf[key] = items[key] ? val
|
||||||
|
|
||||||
Main.initFeatures()
|
Site.init Main.initFeatures
|
||||||
|
|
||||||
upgrade: (items) ->
|
upgrade: (items) ->
|
||||||
{previousversion} = items
|
{previousversion} = items
|
||||||
@ -122,11 +123,10 @@ Main =
|
|||||||
pathname = location.pathname.split /\/+/
|
pathname = location.pathname.split /\/+/
|
||||||
g.BOARD = new Board pathname[1] unless hostname is 'www.4chan.org'
|
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 ->
|
||||||
$.global ->
|
document.documentElement.classList.add 'js-enabled'
|
||||||
document.documentElement.classList.add 'js-enabled'
|
window.FCX = {}
|
||||||
window.FCX = {}
|
Main.jsEnabled = $.hasClass doc, 'js-enabled'
|
||||||
Main.jsEnabled = $.hasClass doc, 'js-enabled'
|
|
||||||
|
|
||||||
switch hostname
|
switch hostname
|
||||||
when 'www.4chan.org'
|
when 'www.4chan.org'
|
||||||
@ -151,7 +151,7 @@ Main =
|
|||||||
if ImageHost.test hostname
|
if ImageHost.test hostname
|
||||||
return unless pathname[2] and not /[sm]\.jpg$/.test(pathname[2])
|
return unless pathname[2] and not /[sm]\.jpg$/.test(pathname[2])
|
||||||
$.asap (-> d.readyState isnt 'loading'), ->
|
$.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', {
|
Redirect.navigate 'file', {
|
||||||
boardID: g.BOARD.ID
|
boardID: g.BOARD.ID
|
||||||
filename: pathname[pathname.length - 1]
|
filename: pathname[pathname.length - 1]
|
||||||
@ -166,14 +166,14 @@ Main =
|
|||||||
ImageCommon.addControls video
|
ImageCommon.addControls video
|
||||||
return
|
return
|
||||||
|
|
||||||
return unless hostname is 'boards.4chan.org'
|
return if Site.isAuxiliaryPage?()
|
||||||
|
|
||||||
if pathname[2] in ['thread', 'res']
|
if pathname[2] in ['thread', 'res']
|
||||||
g.VIEW = 'thread'
|
g.VIEW = 'thread'
|
||||||
g.THREADID = +pathname[3]
|
g.THREADID = +pathname[3]
|
||||||
else if pathname[2] in ['catalog', 'archive']
|
else if /^(?:catalog|archive)(?:\.html)?$/.test(pathname[2])
|
||||||
g.VIEW = pathname[2]
|
g.VIEW = pathname[2].replace('.html', '')
|
||||||
else if pathname[2].match /^\d*$/
|
else if /^(?:index|\d*)(?:\.html)?$/.test(pathname[2])
|
||||||
g.VIEW = 'index'
|
g.VIEW = 'index'
|
||||||
else
|
else
|
||||||
return
|
return
|
||||||
@ -186,6 +186,7 @@ Main =
|
|||||||
|
|
||||||
# c.time 'All initializations'
|
# c.time 'All initializations'
|
||||||
for [name, feature] in Main.features
|
for [name, feature] in Main.features
|
||||||
|
continue if Site.disabledFeatures and name in Site.disabledFeatures
|
||||||
# c.time "#{name} initialization"
|
# c.time "#{name} initialization"
|
||||||
try
|
try
|
||||||
feature.init()
|
feature.init()
|
||||||
@ -245,9 +246,9 @@ Main =
|
|||||||
$.rm Main.bgColorStyle
|
$.rm Main.bgColorStyle
|
||||||
else
|
else
|
||||||
# Determine proper background color for dialogs if 4chan is using a special stylesheet.
|
# Determine proper background color for dialogs if 4chan is using a special stylesheet.
|
||||||
div = $.el 'div',
|
div = Site.bgColoredEl()
|
||||||
className: 'reply'
|
div.style.position = 'absolute';
|
||||||
div.style.cssText = 'position: absolute; visibility: hidden;'
|
div.style.visibility = 'hidden';
|
||||||
$.add d.body, div
|
$.add d.body, div
|
||||||
bgColor = window.getComputedStyle(div).backgroundColor
|
bgColor = window.getComputedStyle(div).backgroundColor
|
||||||
$.rm div
|
$.rm div
|
||||||
@ -265,42 +266,44 @@ Main =
|
|||||||
}
|
}
|
||||||
|
|
||||||
initReady: ->
|
initReady: ->
|
||||||
# XXX Sometimes threads don't 404 but are left over as stubs containing one garbage reply post.
|
if Site.is404?()
|
||||||
if g.VIEW is 'thread' and (d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found'] or ($('.board') and not $('.opContainer')))
|
if g.VIEW is 'thread'
|
||||||
ThreadWatcher.set404 g.BOARD.ID, g.THREADID, ->
|
ThreadWatcher.set404 g.BOARD.ID, g.THREADID, ->
|
||||||
if Conf['404 Redirect']
|
if Conf['404 Redirect']
|
||||||
Redirect.navigate 'thread',
|
Redirect.navigate 'thread',
|
||||||
boardID: g.BOARD.ID
|
boardID: g.BOARD.ID
|
||||||
threadID: g.THREADID
|
threadID: g.THREADID
|
||||||
postID: +location.hash.match /\d+/ # post number or 0
|
postID: +location.hash.match /\d+/ # post number or 0
|
||||||
, "/#{g.BOARD}/"
|
, "/#{g.BOARD}/"
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
return if d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
|
if Site.isIncomplete?()
|
||||||
|
|
||||||
if g.VIEW in ['index', 'thread'] and not $('.board + *')
|
|
||||||
msg = $.el 'div',
|
msg = $.el 'div',
|
||||||
<%= html('The page didn't load completely.<br>Some features may not work unless you <a href="javascript:;">reload</a>.') %>
|
<%= html('The page didn't load completely.<br>Some features may not work unless you <a href="javascript:;">reload</a>.') %>
|
||||||
$.on $('a', msg), 'click', -> location.reload()
|
$.on $('a', msg), 'click', -> location.reload()
|
||||||
new Notice 'warning', msg
|
new Notice 'warning', msg
|
||||||
|
|
||||||
# Parse HTML or skip it and start building from JSON.
|
# Parse HTML or skip it and start building from JSON.
|
||||||
unless Conf['JSON Index'] and g.VIEW is 'index'
|
unless Index.enabled
|
||||||
Main.initThread()
|
Main.initThread()
|
||||||
else
|
else
|
||||||
Main.expectInitFinished = true
|
Main.expectInitFinished = true
|
||||||
$.event '4chanXInitFinished'
|
$.event '4chanXInitFinished'
|
||||||
|
|
||||||
initThread: ->
|
initThread: ->
|
||||||
if (board = $ '.board')
|
s = Site.selectors
|
||||||
|
if (board = $ s.board)
|
||||||
threads = []
|
threads = []
|
||||||
posts = []
|
posts = []
|
||||||
|
|
||||||
for threadRoot in $$ '.board > .thread', board
|
for threadRoot in $$(s.thread, board)
|
||||||
thread = new Thread +threadRoot.id[1..], g.BOARD
|
thread = new Thread +threadRoot.id.match(/\d*$/)[0], g.BOARD
|
||||||
thread.nodes.root = threadRoot
|
thread.nodes.root = threadRoot
|
||||||
threads.push thread
|
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
|
try
|
||||||
posts.push new Post postRoot, thread, g.BOARD
|
posts.push new Post postRoot, thread, g.BOARD
|
||||||
catch err
|
catch err
|
||||||
@ -313,17 +316,7 @@ Main =
|
|||||||
Main.handleErrors errors if errors
|
Main.handleErrors errors if errors
|
||||||
|
|
||||||
if g.VIEW is 'thread'
|
if g.VIEW is 'thread'
|
||||||
scriptData = Get.scriptData()
|
Site.parseThreadMetadata?(threads[0])
|
||||||
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
|
|
||||||
|
|
||||||
Main.callbackNodes 'Thread', threads
|
Main.callbackNodes 'Thread', threads
|
||||||
Main.callbackNodesDB 'Post', posts, ->
|
Main.callbackNodesDB 'Post', posts, ->
|
||||||
@ -424,11 +417,12 @@ Main =
|
|||||||
<%= html('<span class="report-error"> [<a href="${url}" target="_blank">report</a>]</span>') %>
|
<%= html('<span class="report-error"> [<a href="${url}" target="_blank">report</a>]</span>') %>
|
||||||
|
|
||||||
isThisPageLegit: ->
|
isThisPageLegit: ->
|
||||||
# 404 error page or similar.
|
# not 404 error page or similar.
|
||||||
unless 'thisPageIsLegit' of Main
|
unless 'thisPageIsLegit' of Main
|
||||||
Main.thisPageIsLegit = location.hostname is 'boards.4chan.org' and
|
Main.thisPageIsLegit = if Site.isThisPageLegit
|
||||||
!$('link[href*="favicon-status.ico"]', d.head) and
|
Site.isThisPageLegit()
|
||||||
d.title not in ['4chan - Temporarily Offline', '4chan - Error', '504 Gateway Time-out']
|
else
|
||||||
|
!/^[45]\d\d\b/.test(document.title)
|
||||||
Main.thisPageIsLegit
|
Main.thisPageIsLegit
|
||||||
|
|
||||||
ready: (cb) ->
|
ready: (cb) ->
|
||||||
|
|||||||
@ -384,6 +384,10 @@ $.oneItemSugar = (fn) ->
|
|||||||
|
|
||||||
$.syncing = {}
|
$.syncing = {}
|
||||||
|
|
||||||
|
$.securityCheck = (data) ->
|
||||||
|
if location.protocol isnt 'https:'
|
||||||
|
delete data['Redirect to HTTPS']
|
||||||
|
|
||||||
<% if (type === 'crx') { %>
|
<% if (type === 'crx') { %>
|
||||||
# https://developer.chrome.com/extensions/storage.html
|
# https://developer.chrome.com/extensions/storage.html
|
||||||
$.oldValue =
|
$.oldValue =
|
||||||
@ -485,6 +489,7 @@ do ->
|
|||||||
|
|
||||||
$.set = $.oneItemSugar (data, cb) ->
|
$.set = $.oneItemSugar (data, cb) ->
|
||||||
return unless $.crxWorking()
|
return unless $.crxWorking()
|
||||||
|
$.securityCheck data
|
||||||
$.extend items.local, data
|
$.extend items.local, data
|
||||||
setArea 'local', cb
|
setArea 'local', cb
|
||||||
|
|
||||||
@ -536,6 +541,7 @@ if GM?.deleteValue? and window.BroadcastChannel and not GM_addValueChangeListene
|
|||||||
cb items
|
cb items
|
||||||
|
|
||||||
$.set = $.oneItemSugar (items, cb) ->
|
$.set = $.oneItemSugar (items, cb) ->
|
||||||
|
$.securityCheck items
|
||||||
Promise.all(GM.setValue(g.NAMESPACE + key, JSON.stringify(val)) for key, val of items).then ->
|
Promise.all(GM.setValue(g.NAMESPACE + key, JSON.stringify(val)) for key, val of items).then ->
|
||||||
$.syncChannel.postMessage items
|
$.syncChannel.postMessage items
|
||||||
cb?()
|
cb?()
|
||||||
@ -655,6 +661,7 @@ else
|
|||||||
cb items
|
cb items
|
||||||
|
|
||||||
$.set = $.oneItemSugar (items, cb) ->
|
$.set = $.oneItemSugar (items, cb) ->
|
||||||
|
$.securityCheck items
|
||||||
$.queueTask ->
|
$.queueTask ->
|
||||||
for key, value of items
|
for key, value of items
|
||||||
$.setValue(g.NAMESPACE + key, JSON.stringify value)
|
$.setValue(g.NAMESPACE + key, JSON.stringify value)
|
||||||
|
|||||||
1
src/site/SW.js
Normal file
1
src/site/SW.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
SW = {};
|
||||||
123
src/site/SW.tinyboard.coffee
Normal file
123
src/site/SW.tinyboard.coffee
Normal 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
135
src/site/SW.yotsuba.coffee
Normal 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
24
src/site/Site.coffee
Normal 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]
|
||||||
Loading…
x
Reference in New Issue
Block a user