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") 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

View File

@ -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: ->

View File

@ -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

View File

@ -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
''

View File

@ -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.

View File

@ -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) ->

View File

@ -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.

View File

@ -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()

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'] 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}/"

View File

@ -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

View File

@ -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?()

View File

@ -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

View File

@ -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

View File

@ -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
"""

View File

@ -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;
} }

View File

@ -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&#039;t load completely.<br>Some features may not work unless you <a href="javascript:;">reload</a>.') %> <%= 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() $.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) ->

View File

@ -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
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]