Safer property access.

This commit is contained in:
ccd0 2019-08-03 00:23:08 -07:00
parent cba171b9d0
commit e75700f5d9
60 changed files with 279 additions and 238 deletions

View File

@ -15,11 +15,11 @@ Redirect =
selectArchives: -> selectArchives: ->
o = o =
thread: {} thread: $.dict()
post: {} post: $.dict()
file: {} file: $.dict()
archives = {} archives = $.dict()
for data in Conf['archives'] for data in Conf['archives']
for key in ['boards', 'files'] for key in ['boards', 'files']
data[key] = [] unless data[key] instanceof Array data[key] = [] unless data[key] instanceof Array
@ -32,7 +32,7 @@ Redirect =
o.file[boardID] = data unless boardID of o.file or boardID not in files o.file[boardID] = data unless boardID of o.file or boardID not in files
for boardID, record of Conf['selectedArchives'] for boardID, record of Conf['selectedArchives']
for type, id of record when (archive = archives[JSON.stringify id]) for type, id of record when (archive = archives[JSON.stringify id]) and $.hasOwn(o, type)
boards = if type is 'file' then archive.files else archive.boards boards = if type is 'file' then archive.files else archive.boards
o[type][boardID] = archive if boardID in boards o[type][boardID] = archive if boardID in boards
@ -76,14 +76,14 @@ Redirect =
parse: (responses, cb) -> parse: (responses, cb) ->
archives = [] archives = []
archiveUIDs = {} archiveUIDs = $.dict()
for response in responses for response in responses
for data in response for data in response
uid = JSON.stringify(data.uid ? data.name) uid = JSON.stringify(data.uid ? data.name)
if uid of archiveUIDs if uid of archiveUIDs
$.extend archiveUIDs[uid], data $.extend archiveUIDs[uid], data
else else
archiveUIDs[uid] = data archiveUIDs[uid] = $.dict.clone data
archives.push data archives.push data
items = {archives, lastarchivecheck: Date.now()} items = {archives, lastarchivecheck: Date.now()}
$.set items $.set items
@ -98,7 +98,7 @@ Redirect =
protocol: (archive) -> protocol: (archive) ->
protocol = location.protocol protocol = location.protocol
unless archive[protocol[0...-1]] unless $.getOwn(archive, protocol[0...-1])
protocol = if protocol is 'https:' then 'http:' else 'https:' protocol = if protocol is 'https:' then 'http:' else 'https:'
"#{protocol}//" "#{protocol}//"
@ -146,10 +146,10 @@ Redirect =
type type
if type is 'capcode' if type is 'capcode'
# https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/src/Model/Search.php#L363 # https://github.com/pleebe/FoolFuuka/blob/bf4224eed04637a4d0bd4411c2bf5f9945dfec0b/src/Model/Search.php#L363
value = { value = $.getOwn({
'Developer': 'dev' 'Developer': 'dev'
'Verified': 'ver' 'Verified': 'ver'
}[value] or value.toLowerCase() }, value) or value.toLowerCase()
else if type is 'image' else if type is 'image'
value = value.replace /[+/=]/g, (c) -> ({'+': '-', '/': '_', '=': ''})[c] value = value.replace /[+/=]/g, (c) -> ({'+': '-', '/': '_', '=': ''})[c]
value = encodeURIComponent value value = encodeURIComponent value

View File

@ -1,6 +1,6 @@
Filter = Filter =
filters: {} filters: $.dict()
results: {} results: $.dict()
init: -> init: ->
return unless g.VIEW in ['index', 'thread', 'catalog'] and Conf['Filter'] return unless g.VIEW in ['index', 'thread', 'catalog'] and Conf['Filter']
return if g.VIEW is 'catalog' and not Conf['Filter in Native Catalog'] return if g.VIEW is 'catalog' and not Conf['Filter in Native Catalog']
@ -44,11 +44,11 @@ Filter =
# Filter OPs along with their threads or replies only. # Filter OPs along with their threads or replies only.
op = filter.match(/(?:^|;)\s*op:(no|only)/)?[1] or '' op = filter.match(/(?:^|;)\s*op:(no|only)/)?[1] or ''
mask = {'no': 1, 'only': 2}[op] or 0 mask = $.getOwn({'no': 1, 'only': 2}, op) or 0
# Filter only posts with/without files. # Filter only posts with/without files.
file = filter.match(/(?:^|;)\s*file:(no|only)/)?[1] or '' file = filter.match(/(?:^|;)\s*file:(no|only)/)?[1] or ''
mask = mask | ({'no': 4, 'only': 8}[file] or 0) mask = mask | ($.getOwn({'no': 4, 'only': 8}, file) or 0)
# Overrule the `Show Stubs` setting. # Overrule the `Show Stubs` setting.
# Defaults to stub showing. # Defaults to stub showing.
@ -102,7 +102,7 @@ Filter =
parseBoards: (boardsRaw) -> parseBoards: (boardsRaw) ->
return false unless boardsRaw return false unless boardsRaw
return boards if (boards = Filter.parseBoardsMemo[boardsRaw]) return boards if (boards = Filter.parseBoardsMemo[boardsRaw])
boards = {} boards = $.dict()
siteFilter = '' siteFilter = ''
for boardID in boardsRaw.split(',') for boardID in boardsRaw.split(',')
if ':' in boardID if ':' in boardID
@ -116,7 +116,7 @@ Filter =
Filter.parseBoardsMemo[boardsRaw] = boards Filter.parseBoardsMemo[boardsRaw] = boards
boards boards
parseBoardsMemo: {} parseBoardsMemo: $.dict()
test: (post, hideable=true) -> test: (post, hideable=true) ->
return post.filterResults if post.filterResults return post.filterResults if post.filterResults
@ -172,7 +172,7 @@ Filter =
catalog: -> catalog: ->
return unless (url = g.SITE.urls.catalogJSON?(g.BOARD)) return unless (url = g.SITE.urls.catalogJSON?(g.BOARD))
Filter.catalogData = {} Filter.catalogData = $.dict()
$.ajax url, $.ajax url,
onloadend: Filter.catalogParse onloadend: Filter.catalogParse
Callbacks.CatalogThreadNative.push Callbacks.CatalogThreadNative.push
@ -225,17 +225,18 @@ Filter =
MD5: (post) -> post.files.map((f) -> f.MD5) MD5: (post) -> post.files.map((f) -> f.MD5)
values: (key, post) -> values: (key, post) ->
if key of Filter.valueF if $.hasOwn(Filter.valueF, key)
Filter.valueF[key](post).filter((v) -> v?) Filter.valueF[key](post).filter((v) -> v?)
else else
[key.split('+').map((k) -> [key.split('+').map((k) ->
if (f=Filter.valueF[k]) if (f = $.getOwn(Filter.valueF, k))
f(post).map((v) -> v or '').join('\n') f(post).map((v) -> v or '').join('\n')
else else
'' ''
).join('\n')] ).join('\n')]
addFilter: (type, re, cb) -> addFilter: (type, re, cb) ->
return unless $.hasOwn(Config.filter, type)
$.get type, Conf[type], (item) -> $.get type, Conf[type], (item) ->
save = item[type] save = item[type]
# Add a new line before the regexp unless the text is empty. # Add a new line before the regexp unless the text is empty.

View File

@ -1,5 +1,5 @@
Recursive = Recursive =
recursives: {} recursives: $.dict()
init: -> init: ->
return unless g.VIEW in ['index', 'thread'] return unless g.VIEW in ['index', 'thread']
Callbacks.Post.push Callbacks.Post.push

View File

@ -15,7 +15,7 @@ ThreadHiding =
return unless $.hasStorage and g.SITE.software is 'yotsuba' return unless $.hasStorage and g.SITE.software is 'yotsuba'
hiddenThreads = ThreadHiding.db.get hiddenThreads = ThreadHiding.db.get
boardID: board.ID boardID: board.ID
defaultValue: {} defaultValue: $.dict()
hiddenThreads[threadID] = true for threadID of hiddenThreads hiddenThreads[threadID] = true for threadID of hiddenThreads
localStorage.setItem "4chan-hide-t-#{board}", JSON.stringify hiddenThreads localStorage.setItem "4chan-hide-t-#{board}", JSON.stringify hiddenThreads
@ -32,12 +32,12 @@ ThreadHiding =
catalogSave: -> catalogSave: ->
hiddenThreads2 = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} hiddenThreads2 = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
for threadID of hiddenThreads2 when !(threadID of ThreadHiding.hiddenThreads) for threadID of hiddenThreads2 when !$.hasOwn(ThreadHiding.hiddenThreads, threadID)
ThreadHiding.db.set ThreadHiding.db.set
boardID: g.BOARD.ID boardID: g.BOARD.ID
threadID: threadID threadID: threadID
val: {makeStub: Conf['Stubs']} val: {makeStub: Conf['Stubs']}
for threadID of ThreadHiding.hiddenThreads when !(threadID of hiddenThreads2) for threadID of ThreadHiding.hiddenThreads when !$.hasOwn(hiddenThreads2, threadID)
ThreadHiding.db.delete ThreadHiding.db.delete
boardID: g.BOARD.ID boardID: g.BOARD.ID
threadID: threadID threadID: threadID
@ -176,7 +176,7 @@ ThreadHiding =
toggle: (thread) -> toggle: (thread) ->
unless thread instanceof Thread unless thread instanceof Thread
thread = g.threads[@dataset.fullID] thread = g.threads.get(@dataset.fullID)
if thread.isHidden if thread.isHidden
ThreadHiding.show thread ThreadHiding.show thread
else else

View File

@ -13,7 +13,7 @@ BoardConfig =
load: -> load: ->
if @status is 200 and @response and @response.boards if @status is 200 and @response and @response.boards
boards = {} boards = $.dict()
for board in @response.boards for board in @response.boards
boards[board.board] = board boards[board.board] = board
{troll_flags} = @response {troll_flags} = @response

View File

@ -13,14 +13,14 @@ Get =
threadFromRoot: (root) -> threadFromRoot: (root) ->
return null unless root? return null unless root?
{board} = root.dataset {board} = root.dataset
g.threads["#{if board then encodeURIComponent(board) else g.BOARD.ID}.#{root.id.match(/\d*$/)[0]}"] g.threads.get("#{if board then encodeURIComponent(board) else g.BOARD.ID}.#{root.id.match(/\d*$/)[0]}")
threadFromNode: (node) -> threadFromNode: (node) ->
Get.threadFromRoot $.x "ancestor-or-self::#{g.SITE.xpath.thread}", node Get.threadFromRoot $.x "ancestor-or-self::#{g.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.get(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::#{g.SITE.xpath.postContainer}[1]", root Get.postFromRoot $.x "ancestor-or-self::#{g.SITE.xpath.postContainer}[1]", root
postDataFromLink: (link) -> postDataFromLink: (link) ->
@ -59,7 +59,7 @@ Get =
# and their clones, # and their clones,
# get all of their backlinks. # get all of their backlinks.
if Conf['Quote Backlinks'] if Conf['Quote Backlinks']
handleQuotes qPost, 'backlinks' for quote in post.quotes when qPost = posts[quote] handleQuotes qPost, 'backlinks' for quote in post.quotes when qPost = posts.get(quote)
# Third: # Third:
# Filter out irrelevant quotelinks. # Filter out irrelevant quotelinks.

View File

@ -52,7 +52,7 @@ Index =
# Header "Index Navigation" submenu # Header "Index Navigation" submenu
entries = [] entries = []
@inputs = inputs = {} @inputs = inputs = $.dict()
for name, arr of Config.Index when arr instanceof Array for name, arr of Config.Index when arr instanceof Array
label = UI.checkbox name, "#{name[0]}#{name[1..].toLowerCase()}" label = UI.checkbox name, "#{name[0]}#{name[1..].toLowerCase()}"
label.title = arr[1] label.title = arr[1]
@ -66,7 +66,7 @@ Index =
$.on inputs['Anchor Hidden Threads'], 'change', @cb.resort $.on inputs['Anchor Hidden Threads'], 'change', @cb.resort
watchSettings = (e) -> watchSettings = (e) ->
if (input = inputs[e.target.name]) if (input = $.getOwn(inputs, e.target.name))
input.checked = e.target.checked input.checked = e.target.checked
$.event 'change', null, input $.event 'change', null, input
$.on d, 'OpenSettings', -> $.on d, 'OpenSettings', ->
@ -283,10 +283,10 @@ Index =
Index.pageLoad false unless e?.detail?.deferred Index.pageLoad false unless e?.detail?.deferred
perBoardSort: -> perBoardSort: ->
Conf['Index Sort'] = if @checked then {} else '' Conf['Index Sort'] = if @checked then $.dict() else ''
Index.saveSort() Index.saveSort()
for i in [0...2] for i in [0...2]
Conf["Last Long Reply Thresholds #{i}"] = if @checked then {} else '' Conf["Last Long Reply Thresholds #{i}"] = if @checked then $.dict() else ''
Index.saveLastLongThresholds i Index.saveLastLongThresholds i
return return
@ -412,12 +412,12 @@ Index =
commands = hash[1..].split '/' commands = hash[1..].split '/'
leftover = [] leftover = []
for command in commands for command in commands
if (mode = Index.hashCommands.mode[command]) if (mode = $.getOwn(Index.hashCommands.mode, command))
state.mode = mode state.mode = mode
else if command is 'index' else if command is 'index'
state.mode = Conf['Previous Index Mode'] state.mode = Conf['Previous Index Mode']
state.page = 1 state.page = 1
else if (sort = Index.hashCommands.sort[command.replace(/-rev$/, '')]) else if (sort = $.getOwn(Index.hashCommands.sort, command.replace(/-rev$/, '')))
state.sort = sort state.sort = sort
state.sort += '-rev' if /-rev$/.test(command) state.sort += '-rev' if /-rev$/.test(command)
else if /^s=/.test command else if /^s=/.test command
@ -659,10 +659,10 @@ Index =
Index.threadsNumPerPage = pages[0]?.threads.length or 1 Index.threadsNumPerPage = pages[0]?.threads.length or 1
Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), [] Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), []
Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no
Index.liveThreadDict = {} Index.liveThreadDict = $.dict()
Index.threadPosition = {} Index.threadPosition = $.dict()
Index.parsedThreads = {} Index.parsedThreads = $.dict()
Index.replyData = {} Index.replyData = $.dict()
for data, i in Index.liveThreadData for data, i in Index.liveThreadData
Index.liveThreadDict[data.no] = data Index.liveThreadDict[data.no] = data
Index.threadPosition[data.no] = i Index.threadPosition[data.no] = i
@ -682,7 +682,7 @@ Index =
return return
isHidden: (threadID) -> isHidden: (threadID) ->
if (thread = g.BOARD.threads[threadID]) and thread.OP and not thread.OP.isFetchedQuote if (thread = g.BOARD.threads.get(threadID)) and thread.OP and not thread.OP.isFetchedQuote
thread.isHidden thread.isHidden
else else
Index.parsedThreads[threadID].isHidden Index.parsedThreads[threadID].isHidden
@ -698,7 +698,7 @@ Index =
try try
threadData = Index.liveThreadDict[ID] threadData = Index.liveThreadDict[ID]
if (thread = g.BOARD.threads[ID]) if (thread = g.BOARD.threads.get(ID))
isStale = (thread.json isnt threadData) and (JSON.stringify(thread.json) isnt JSON.stringify(threadData)) isStale = (thread.json isnt threadData) and (JSON.stringify(thread.json) isnt JSON.stringify(threadData))
if isStale if isStale
thread.setCount 'post', threadData.replies + 1, threadData.bumplimit thread.setCount 'post', threadData.replies + 1, threadData.bumplimit
@ -751,7 +751,7 @@ Index =
continue if not (lastReplies = Index.liveThreadDict[thread.ID].last_replies) continue if not (lastReplies = Index.liveThreadDict[thread.ID].last_replies)
nodes = [] nodes = []
for data in lastReplies for data in lastReplies
if (post = thread.posts[data.no]) and not post.isFetchedQuote if (post = thread.posts.get(data.no)) and not post.isFetchedQuote
nodes.push post.nodes.root nodes.push post.nodes.root
continue continue
nodes.push node = g.SITE.Build.postFromObject data, thread.board.ID nodes.push node = g.SITE.Build.postFromObject data, thread.board.ID
@ -822,7 +822,7 @@ Index =
if len >= Index.lastLongThresholds[+!!r.ext] if len >= Index.lastLongThresholds[+!!r.ext]
return r return r
if thread.omitted_posts then thread.last_replies[0] else thread if thread.omitted_posts then thread.last_replies[0] else thread
lastlongD = {} lastlongD = $.dict()
for thread in liveThreadData for thread in liveThreadData
lastlongD[thread.no] = lastlong(thread).no lastlongD[thread.no] = lastlong(thread).no
[liveThreadData...].sort((a, b) -> [liveThreadData...].sort((a, b) ->

View File

@ -129,8 +129,8 @@ Settings =
warning addWarning warning addWarning
$.add section, warnings $.add section, warnings
items = {} items = $.dict()
inputs = {} inputs = $.dict()
addCheckboxes = (root, obj) -> addCheckboxes = (root, obj) ->
containers = [root] containers = [root]
for key, arr of obj when arr instanceof Array for key, arr of obj when arr instanceof Array
@ -177,7 +177,7 @@ Settings =
div = $.el 'div', div = $.el 'div',
<%= html('<button></button><span class="description">: Clear manually-hidden threads and posts on all boards. Reload the page to apply.') %> <%= html('<button></button><span class="description">: Clear manually-hidden threads and posts on all boards. Reload the page to apply.') %>
button = $ 'button', div button = $ 'button', div
$.get {hiddenThreads: {}, hiddenPosts: {}}, ({hiddenThreads, hiddenPosts}) -> $.get {hiddenThreads: $.dict(), hiddenPosts: $.dict()}, ({hiddenThreads, hiddenPosts}) ->
hiddenNum = 0 hiddenNum = 0
for ID, site of hiddenThreads when ID isnt 'boards' for ID, site of hiddenThreads when ID isnt 'boards'
for ID, board of site.boards for ID, board of site.boards
@ -194,7 +194,7 @@ Settings =
button.textContent = "Hidden: #{hiddenNum}" button.textContent = "Hidden: #{hiddenNum}"
$.on button, 'click', -> $.on button, 'click', ->
@textContent = 'Hidden: 0' @textContent = 'Hidden: 0'
$.get 'hiddenThreads', {}, ({hiddenThreads}) -> $.get 'hiddenThreads', $.dict(), ({hiddenThreads}) ->
if $.hasStorage and g.SITE.software is 'yotsuba' if $.hasStorage and g.SITE.software is 'yotsuba'
for boardID of hiddenThreads.boards for boardID of hiddenThreads.boards
localStorage.removeItem "4chan-hide-t-#{boardID}" localStorage.removeItem "4chan-hide-t-#{boardID}"
@ -203,7 +203,7 @@ Settings =
export: -> export: ->
# Make sure to export the most recent data, but don't overwrite existing `Conf` object. # Make sure to export the most recent data, but don't overwrite existing `Conf` object.
Conf2 = {} Conf2 = $.dict()
$.extend Conf2, Conf $.extend Conf2, Conf
$.get Conf2, (Conf2) -> $.get Conf2, (Conf2) ->
# Don't export cached JSON data. # Don't export cached JSON data.
@ -235,7 +235,7 @@ Settings =
reader = new FileReader() reader = new FileReader()
reader.onload = (e) -> reader.onload = (e) ->
try try
Settings.loadSettings JSON.parse(e.target.result), (err) -> Settings.loadSettings $.dict.json(e.target.result), (err) ->
if err if err
output.textContent = 'Import failed due to an error.' output.textContent = 'Import failed due to an error.'
else if confirm 'Import successful. Reload now?' else if confirm 'Import successful. Reload now?'
@ -327,14 +327,14 @@ Settings =
data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, (s) -> "#{s[0].toUpperCase()}#{s[1..]}").replace /(^|.+\+)[A-Z]$/g, (s) -> data.Conf[key] = data.Conf[key].replace(/ctrl|alt|meta/g, (s) -> "#{s[0].toUpperCase()}#{s[1..]}").replace /(^|.+\+)[A-Z]$/g, (s) ->
"Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}" "Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}"
if data.WatchedThreads if data.WatchedThreads
data.Conf['watchedThreads'] = boards: {} data.Conf['watchedThreads'] = boards: $.dict()
for boardID, threads of data.WatchedThreads for boardID, threads of data.WatchedThreads
for threadID, threadData of threads for threadID, threadData of threads
(data.Conf['watchedThreads'].boards[boardID] or= {})[threadID] = excerpt: threadData.textContent (data.Conf['watchedThreads'].boards[boardID] or= $.dict())[threadID] = excerpt: threadData.textContent
data data
upgrade: (data, version) -> upgrade: (data, version) ->
changes = {} changes = $.dict()
set = (key, value) -> set = (key, value) ->
data[key] = changes[key] = value data[key] = changes[key] = value
setD = (key, value) -> setD = (key, value) ->
@ -371,7 +371,7 @@ Settings =
if data['selectedArchives']? if data['selectedArchives']?
uids = {"Moe":0,"4plebs Archive":3,"Nyafuu Archive":4,"Love is Over":5,"Rebecca Black Tech":8,"warosu":10,"fgts":15,"not4plebs":22,"DesuStorage":23,"fireden.net":24,"disabled":null} uids = {"Moe":0,"4plebs Archive":3,"Nyafuu Archive":4,"Love is Over":5,"Rebecca Black Tech":8,"warosu":10,"fgts":15,"not4plebs":22,"DesuStorage":23,"fireden.net":24,"disabled":null}
for boardID, record of data['selectedArchives'] for boardID, record of data['selectedArchives']
for type, name of record when name of uids for type, name of record when $.hasOwn(uids, name)
record[type] = uids[name] record[type] = uids[name]
set 'selectedArchives', data['selectedArchives'] set 'selectedArchives', data['selectedArchives']
if compareString < '00001.00011.00016.00000' if compareString < '00001.00011.00016.00000'
@ -468,7 +468,7 @@ Settings =
delete data[db].lastChecked delete data[db].lastChecked
set db, data[db] set db, data[db]
if data['siteSoftware']? and not data['siteProperties']? if data['siteSoftware']? and not data['siteProperties']?
siteProperties = {} siteProperties = $.dict()
for line in data['siteSoftware'].split('\n') for line in data['siteSoftware'].split('\n')
[hostname, software] = line.split(' ') [hostname, software] = line.split(' ')
siteProperties[hostname] = {software} siteProperties[hostname] = {software}
@ -523,6 +523,7 @@ Settings =
selectFilter: -> selectFilter: ->
div = @nextElementSibling div = @nextElementSibling
if (name = @value) isnt 'guide' if (name = @value) isnt 'guide'
return unless $.hasOwn(Config.filter, name)
$.rmAll div $.rmAll div
ta = $.el 'textarea', ta = $.el 'textarea',
name: name name: name
@ -551,7 +552,7 @@ Settings =
$.extend section, <%= readHTML('Advanced.html') %> $.extend section, <%= readHTML('Advanced.html') %>
warning.hidden = Conf[warning.dataset.feature] for warning in $$ '.warning', section warning.hidden = Conf[warning.dataset.feature] for warning in $$ '.warning', section
inputs = {} inputs = $.dict()
for input in $$ '[name]', section for input in $$ '[name]', section
inputs[input.name] = input inputs[input.name] = input
@ -560,7 +561,7 @@ Settings =
Conf['lastarchivecheck'] = 0 Conf['lastarchivecheck'] = 0
$.id('lastarchivecheck').textContent = 'never' $.id('lastarchivecheck').textContent = 'never'
items = {} items = $.dict()
for name, input of inputs when name not in ['captchaServiceKey', 'Interval', 'Custom CSS'] for name, input of inputs when name not in ['captchaServiceKey', 'Interval', 'Custom CSS']
items[name] = Conf[name] items[name] = Conf[name]
event = if ( event = if (
@ -602,7 +603,7 @@ Settings =
$.on customCSS, 'change', Settings.togglecss $.on customCSS, 'change', Settings.togglecss
$.on applyCSS, 'click', -> CustomCSS.update() $.on applyCSS, 'click', -> CustomCSS.update()
itemsArchive = {} itemsArchive = $.dict()
itemsArchive[name] = Conf[name] for name in ['archives', 'selectedArchives', 'lastarchivecheck'] itemsArchive[name] = Conf[name] for name in ['archives', 'selectedArchives', 'lastarchivecheck']
$.get itemsArchive, (itemsArchive) -> $.get itemsArchive, (itemsArchive) ->
$.extend Conf, itemsArchive $.extend Conf, itemsArchive
@ -634,7 +635,7 @@ Settings =
$.rmAll boardSelect $.rmAll boardSelect
$.rmAll tbody $.rmAll tbody
archBoards = {} archBoards = $.dict()
for {uid, name, boards, files, software} in Conf['archives'] for {uid, name, boards, files, software} in Conf['archives']
continue unless software in ['fuuka', 'foolfuuka'] continue unless software in ['fuuka', 'foolfuuka']
for boardID in boards for boardID in boards
@ -715,7 +716,7 @@ Settings =
saveSelectedArchive: -> saveSelectedArchive: ->
$.get 'selectedArchives', Conf['selectedArchives'], ({selectedArchives}) => $.get 'selectedArchives', Conf['selectedArchives'], ({selectedArchives}) =>
(selectedArchives[@dataset.boardid] or= {})[@dataset.type] = JSON.parse @value (selectedArchives[@dataset.boardid] or= $.dict())[@dataset.type] = JSON.parse @value
$.set 'selectedArchives', selectedArchives $.set 'selectedArchives', selectedArchives
Conf['selectedArchives'] = selectedArchives Conf['selectedArchives'] = selectedArchives
Redirect.selectArchives() Redirect.selectArchives()
@ -732,7 +733,7 @@ Settings =
Conf['captchaServiceKey'][domain] = value Conf['captchaServiceKey'][domain] = value
$.get 'captchaServiceKey', Conf['captchaServiceKey'], ({captchaServiceKey}) -> $.get 'captchaServiceKey', Conf['captchaServiceKey'], ({captchaServiceKey}) ->
captchaServiceKey[domain] = value captchaServiceKey[domain] = value
delete captchaServiceKey[domain] unless value or (domain of Config['captchaServiceKey'][0]) delete captchaServiceKey[domain] unless value or $.hasOwn(Config['captchaServiceKey'][0], domain)
Conf['captchaServiceKey'] = captchaServiceKey Conf['captchaServiceKey'] = captchaServiceKey
$.set 'captchaServiceKey', captchaServiceKey $.set 'captchaServiceKey', captchaServiceKey
Settings.captchaServiceDomainList() Settings.captchaServiceDomainList()
@ -794,8 +795,8 @@ Settings =
$('.warning', section).hidden = Conf['Keybinds'] $('.warning', section).hidden = Conf['Keybinds']
tbody = $ 'tbody', section tbody = $ 'tbody', section
items = {} items = $.dict()
inputs = {} inputs = $.dict()
for key, arr of Config.hotkeys for key, arr of Config.hotkeys
tr = $.el 'tr', tr = $.el 'tr',
<%= html('<td>${arr[1]}</td><td><input class="field"></td>') %> <%= html('<td>${arr[1]}</td><td><input class="field"></td>') %>

View File

@ -127,7 +127,7 @@ Test =
cb: cb:
testOne: -> testOne: ->
Test.testOne g.posts[@dataset.fullID] Test.testOne g.posts.get(@dataset.fullID)
Menu.menu.close() Menu.menu.close()
testAll: -> testAll: ->

View File

@ -25,7 +25,7 @@ FappeTyme =
textContent: type[0] textContent: type[0]
title: "#{type} Tyme active" title: "#{type} Tyme active"
$.on indicator, 'click', -> $.on indicator, 'click', ->
check = FappeTyme.nodes[@parentNode.id.replace('shortcut-', '')] check = $.getOwn(FappeTyme.nodes, @parentNode.id.replace('shortcut-', ''))
check.checked = !check.checked check.checked = !check.checked
$.event 'change', null, check $.event 'change', null, check
Header.addShortcut lc, indicator, 410 Header.addShortcut lc, indicator, 410

View File

@ -38,7 +38,7 @@ Gallery =
Gallery.images = [] Gallery.images = []
nodes = Gallery.nodes = {} nodes = Gallery.nodes = {}
Gallery.fileIDs = {} Gallery.fileIDs = $.dict()
Gallery.slideshow = false Gallery.slideshow = false
nodes.el = dialog = $.el 'div', nodes.el = dialog = $.el 'div',
@ -133,7 +133,7 @@ Gallery =
load: (thumb, errorCB) -> load: (thumb, errorCB) ->
ext = thumb.href.match /\w*$/ ext = thumb.href.match /\w*$/
elType = {'webm': 'video', 'mp4': 'video', 'pdf': 'iframe'}[ext] or 'img' elType = $.getOwn({'webm': 'video', 'mp4': 'video', 'pdf': 'iframe'}, ext) or 'img'
file = $.el elType file = $.el elType
$.extend file.dataset, thumb.dataset $.extend file.dataset, thumb.dataset
$.on file, 'error', errorCB $.on file, 'error', errorCB
@ -185,7 +185,7 @@ Gallery =
Gallery.cb.stop() Gallery.cb.stop()
# Scroll to post # Scroll to post
if Conf['Scroll to Post'] and (post = g.posts[file.dataset.post]) if Conf['Scroll to Post'] and (post = g.posts.get(file.dataset.post))
Header.scrollTo post.nodes.root Header.scrollTo post.nodes.root
# Preload next image # Preload next image
@ -196,11 +196,11 @@ Gallery =
if @error?.code is MediaError.MEDIA_ERR_DECODE if @error?.code is MediaError.MEDIA_ERR_DECODE
return new Notice 'error', 'Corrupt or unplayable video', 30 return new Notice 'error', 'Corrupt or unplayable video', 30
return if ImageCommon.isFromArchive @ return if ImageCommon.isFromArchive @
post = g.posts[@dataset.post] post = g.posts.get(@dataset.post)
file = post.files[@dataset.file] file = post.files[+@dataset.file]
ImageCommon.error @, post, file, null, (url) => ImageCommon.error @, post, file, null, (url) =>
return unless url return unless url
Gallery.images[@dataset.id].href = url Gallery.images[+@dataset.id].href = url
(@src = url if Gallery.nodes.current is @) (@src = url if Gallery.nodes.current is @)
cacheError: -> cacheError: ->
@ -341,7 +341,7 @@ Gallery =
{current, frame} = Gallery.nodes {current, frame} = Gallery.nodes
{style} = current {style} = current
if Conf['Stretch to Fit'] and (dim = g.posts[current.dataset.post]?.file.dimensions) if Conf['Stretch to Fit'] and (dim = g.posts.get(current.dataset.post)?.file.dimensions)
[width, height] = dim.split 'x' [width, height] = dim.split 'x'
containerWidth = frame.clientWidth containerWidth = frame.clientWidth
containerHeight = doc.clientHeight - 25 containerHeight = doc.clientHeight - 25

View File

@ -24,7 +24,7 @@ Metadata =
$.rmClass @parentNode, 'error' $.rmClass @parentNode, 'error'
$.addClass @parentNode, 'loading' $.addClass @parentNode, 'loading'
{index} = @parentNode.dataset {index} = @parentNode.dataset
CrossOrigin.binary Get.postFromNode(@).files[index].url, (data) => CrossOrigin.binary Get.postFromNode(@).files[+index].url, (data) =>
$.rmClass @parentNode, 'loading' $.rmClass @parentNode, 'loading'
if data? if data?
title = Metadata.parse data title = Metadata.parse data

View File

@ -18,7 +18,7 @@ Sauce =
parseLink: (link) -> parseLink: (link) ->
return null if not (link = link.trim()) return null if not (link = link.trim())
parts = {} parts = $.dict()
for part, i in link.split /;(?=(?:text|boards|types|regexp|sandbox):?)/ for part, i in link.split /;(?=(?:text|boards|types|regexp|sandbox):?)/
if i is 0 if i is 0
parts['url'] = part parts['url'] = part
@ -47,7 +47,7 @@ Sauce =
createSauceLink: (link, post, file) -> createSauceLink: (link, post, file) ->
ext = file.url.match(/[^.]*$/)[0] ext = file.url.match(/[^.]*$/)[0]
parts = {} parts = $.dict()
$.extend parts, link $.extend parts, link
return null unless !parts['boards'] or parts['boards']["#{post.siteID}/#{post.boardID}"] or parts['boards']["#{post.siteID}/*"] return null unless !parts['boards'] or parts['boards']["#{post.siteID}/#{post.boardID}"] or parts['boards']["#{post.siteID}/*"]

View File

@ -1,7 +1,7 @@
Embedding = Embedding =
init: -> init: ->
return unless g.VIEW in ['index', 'thread', 'archive'] and Conf['Linkify'] and (Conf['Embedding'] or Conf['Link Title'] or Conf['Cover Preview']) return unless g.VIEW in ['index', 'thread', 'archive'] and Conf['Linkify'] and (Conf['Embedding'] or Conf['Link Title'] or Conf['Cover Preview'])
@types = {} @types = $.dict()
@types[type.key] = type for type in @ordered_types @types[type.key] = type for type in @ordered_types
if Conf['Embedding'] and g.VIEW isnt 'archive' if Conf['Embedding'] and g.VIEW isnt 'archive'

View File

@ -1,5 +1,5 @@
DeleteLink = DeleteLink =
auto: [{}, {}] auto: [$.dict(), $.dict()]
init: -> init: ->
return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Delete Link'] return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Delete Link']
@ -77,7 +77,7 @@ DeleteLink =
mode: 'usrdel' mode: 'usrdel'
onlyimgdel: fileOnly onlyimgdel: fileOnly
pwd: QR.persona.getPassword() pwd: QR.persona.getPassword()
form[post.ID] = 'delete' form[+post.ID] = 'delete'
$.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"), $.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"),
responseType: 'document' responseType: 'document'
@ -110,7 +110,7 @@ DeleteLink =
link.textContent = 'Deleted' if post.fullID is DeleteLink.post.fullID link.textContent = 'Deleted' if post.fullID is DeleteLink.post.fullID
cooldown: cooldown:
seconds: {} seconds: $.dict()
start: (post, seconds) -> start: (post, seconds) ->
# Already counting. # Already counting.

View File

@ -75,7 +75,7 @@ Banner =
boardID: g.BOARD.ID boardID: g.BOARD.ID
threadID: @className threadID: @className
original: {} original: $.dict()
custom: (child) -> custom: (child) ->
{className} = child {className} = child

View File

@ -81,12 +81,12 @@ CatalogLinks =
return return
externalParse: -> externalParse: ->
CatalogLinks.externalList = {} CatalogLinks.externalList = $.dict()
for line in Conf['externalCatalogURLs'].split '\n' for line in Conf['externalCatalogURLs'].split '\n'
continue if line[0] is '#' continue if line[0] is '#'
url = line.split(';')[0] url = line.split(';')[0]
boards = Filter.parseBoards(line.match(/;boards:([^;]+)/)?[1] or '*') boards = Filter.parseBoards(line.match(/;boards:([^;]+)/)?[1] or '*')
excludes = Filter.parseBoards(line.match(/;exclude:([^;]+)/)?[1]) or {} excludes = Filter.parseBoards(line.match(/;exclude:([^;]+)/)?[1]) or $.dict()
for board of boards for board of boards
unless excludes[board] or excludes[board.split('/')[0] + '/*'] unless excludes[board] or excludes[board.split('/')[0] + '/*']
CatalogLinks.externalList[board] = url CatalogLinks.externalList[board] = url

View File

@ -1,5 +1,5 @@
ExpandThread = ExpandThread =
statuses: {} statuses: $.dict()
init: -> init: ->
return if not (g.VIEW is 'index' and Conf['Thread Expansion']) return if not (g.VIEW is 'index' and Conf['Thread Expansion'])
if Conf['JSON Index'] if Conf['JSON Index']
@ -96,7 +96,7 @@ ExpandThread =
filesCount = 0 filesCount = 0
for postData in req.response.posts for postData in req.response.posts
continue if postData.no is thread.ID continue if postData.no is thread.ID
if (post = thread.posts[postData.no]) and not post.isFetchedQuote if (post = thread.posts.get(postData.no)) and not post.isFetchedQuote
filesCount++ if 'file' of post filesCount++ if 'file' of post
{root} = post.nodes {root} = post.nodes
postsRoot.push root postsRoot.push root

View File

@ -26,7 +26,7 @@ FileInfo =
format: (formatString, post, outputNode) -> format: (formatString, post, outputNode) ->
output = [] output = []
formatString.replace /%(.)|[^%]+/g, (s, c) -> formatString.replace /%(.)|[^%]+/g, (s, c) ->
output.push if c of FileInfo.formatters output.push if $.hasOwn(FileInfo.formatters, c)
FileInfo.formatters[c].call post FileInfo.formatters[c].call post
else else
<%= html('${s}') %> <%= html('${s}') %>

View File

@ -7,8 +7,8 @@ Fourchan =
initBoard: -> initBoard: ->
if g.BOARD.config.code_tags if g.BOARD.config.code_tags
$.on window, 'prettyprint:cb', (e) -> $.on window, 'prettyprint:cb', (e) ->
return if not (post = g.posts[e.detail.ID]) return if not (post = g.posts.get(e.detail.ID))
return if not (pre = $$('.prettyprint', post.nodes.comment)[e.detail.i]) return if not (pre = $$('.prettyprint', post.nodes.comment)[+e.detail.i])
unless $.hasClass pre, 'prettyprinted' unless $.hasClass pre, 'prettyprinted'
pre.innerHTML = e.detail.html pre.innerHTML = e.detail.html
$.addClass pre, 'prettyprinted' $.addClass pre, 'prettyprinted'

View File

@ -1,9 +1,8 @@
IDColor = IDColor =
init: -> init: ->
return unless g.VIEW in ['index', 'thread'] and Conf['Color User IDs'] return unless g.VIEW in ['index', 'thread'] and Conf['Color User IDs']
@ids = { @ids = $.dict()
Heaven: [0, 0, 0, '#fff'] @ids['Heaven'] = [0, 0, 0, '#fff']
}
Callbacks.Post.push Callbacks.Post.push
name: 'Color User IDs' name: 'Color User IDs'

View File

@ -6,11 +6,11 @@ ModContact =
cb: @node cb: @node
node: -> node: ->
return if @isClone or !ModContact.specific[@info.capcode] return if @isClone or !$.hasOwn(ModContact.specific, @info.capcode)
links = $.el 'span', className: 'contact-links brackets-wrap' links = $.el 'span', className: 'contact-links brackets-wrap'
$.extend links, ModContact.template(@info.capcode) $.extend links, ModContact.template(@info.capcode)
$.after @nodes.capcode, links $.after @nodes.capcode, links
if (moved = @info.comment.match /This thread was moved to >>>\/(\w+)\//) and ModContact.moveNote[moved[1]] if (moved = @info.comment.match /This thread was moved to >>>\/(\w+)\//) and $.hasOwn(ModContact.moveNote, moved[1])
moveNote = $.el 'div', className: 'move-note' moveNote = $.el 'div', className: 'move-note'
$.extend moveNote, ModContact.moveNote[moved[1]] $.extend moveNote, ModContact.moveNote[moved[1]]
$.add @nodes.post, moveNote $.add @nodes.post, moveNote

View File

@ -39,7 +39,7 @@ Nav =
Nav.scroll +1 Nav.scroll +1
getThread: -> getThread: ->
return g.threads["#{g.BOARD}.#{g.THREADID}"].nodes.root if g.VIEW is 'thread' return g.threads.get("#{g.BOARD}.#{g.THREADID}").nodes.root if g.VIEW is 'thread'
return if $.hasClass doc, 'catalog-mode' return if $.hasClass doc, 'catalog-mode'
for threadRoot in $$ g.SITE.selectors.thread for threadRoot in $$ g.SITE.selectors.thread
thread = Get.threadFromRoot threadRoot thread = Get.threadFromRoot threadRoot

View File

@ -126,6 +126,6 @@ RelativeDates =
markStale: (data) -> markStale: (data) ->
return if data in RelativeDates.stale # We can call RelativeDates.update() multiple times. return if data in RelativeDates.stale # We can call RelativeDates.update() multiple times.
return if data instanceof Post and !g.posts[data.fullID] # collected post. return if data instanceof Post and !g.posts.get(data.fullID) # collected post.
return if data instanceof Element and !doc.contains(data) # removed catalog reply. return if data instanceof Element and !doc.contains(data) # removed catalog reply.
RelativeDates.stale.push data RelativeDates.stale.push data

View File

@ -13,7 +13,7 @@ Time =
format: (formatString, date) -> format: (formatString, date) ->
formatString.replace /%(.)/g, (s, c) -> formatString.replace /%(.)/g, (s, c) ->
if c of Time.formatters if $.hasOwn(Time.formatters, c)
Time.formatters[c].call(date) Time.formatters[c].call(date)
else else
s s

View File

@ -59,7 +59,8 @@ Favicon =
'<%= readBase64('Metro.unreadNSFW.png') %>' '<%= readBase64('Metro.unreadNSFW.png') %>'
'<%= readBase64('Metro.unreadNSFWY.png') %>' '<%= readBase64('Metro.unreadNSFWY.png') %>'
] ]
}[Conf['favicon']] }
items = $.getOwn(items, Conf['favicon'])
f = Favicon f = Favicon
t = 'data:image/png;base64,' t = 'data:image/png;base64,'

View File

@ -18,10 +18,10 @@ MarkNewIPs =
when postCount - MarkNewIPs.postCount + deletedPosts.length when postCount - MarkNewIPs.postCount + deletedPosts.length
i = MarkNewIPs.ipCount i = MarkNewIPs.ipCount
for fullID in newPosts for fullID in newPosts
MarkNewIPs.markNew g.posts[fullID], ++i MarkNewIPs.markNew g.posts.get(fullID), ++i
when -deletedPosts.length when -deletedPosts.length
for fullID in newPosts for fullID in newPosts
MarkNewIPs.markOld g.posts[fullID] MarkNewIPs.markOld g.posts.get(fullID)
MarkNewIPs.ipCount = ipCount MarkNewIPs.ipCount = ipCount
MarkNewIPs.postCount = postCount MarkNewIPs.postCount = postCount

View File

@ -88,7 +88,7 @@ ReplyPruning =
return if e.detail[404] return if e.detail[404]
for fullID in e.detail.newPosts for fullID in e.detail.newPosts
ReplyPruning.total++ ReplyPruning.total++
ReplyPruning.totalFiles++ if g.posts[fullID].file ReplyPruning.totalFiles++ if g.posts.get(fullID).file
return return
update: -> update: ->
@ -105,7 +105,7 @@ ReplyPruning =
if ReplyPruning.hidden < hidden2 if ReplyPruning.hidden < hidden2
while ReplyPruning.hidden < hidden2 and ReplyPruning.position < posts.keys.length while ReplyPruning.hidden < hidden2 and ReplyPruning.position < posts.keys.length
post = posts[posts.keys[ReplyPruning.position++]] post = posts.get(posts.keys[ReplyPruning.position++])
if post.isReply and not post.isFetchedQuote if post.isReply and not post.isFetchedQuote
$.add ReplyPruning.container, node while (node = ReplyPruning.summary.nextSibling) and node isnt post.nodes.root $.add ReplyPruning.container, node while (node = ReplyPruning.summary.nextSibling) and node isnt post.nodes.root
$.add ReplyPruning.container, post.nodes.root $.add ReplyPruning.container, post.nodes.root
@ -115,7 +115,7 @@ ReplyPruning =
else if ReplyPruning.hidden > hidden2 else if ReplyPruning.hidden > hidden2
frag = $.frag() frag = $.frag()
while ReplyPruning.hidden > hidden2 and ReplyPruning.position > 0 while ReplyPruning.hidden > hidden2 and ReplyPruning.position > 0
post = posts[posts.keys[--ReplyPruning.position]] post = posts.get(posts.keys[--ReplyPruning.position])
if post.isReply and not post.isFetchedQuote if post.isReply and not post.isFetchedQuote
$.prepend frag, node while (node = ReplyPruning.container.lastChild) and node isnt post.nodes.root $.prepend frag, node while (node = ReplyPruning.container.lastChild) and node isnt post.nodes.root
$.prepend frag, post.nodes.root $.prepend frag, post.nodes.root

View File

@ -55,7 +55,7 @@ ThreadStats =
{posts} = ThreadStats.thread {posts} = ThreadStats.thread
n = posts.keys.length n = posts.keys.length
for i in [ThreadStats.postIndex...n] by 1 for i in [ThreadStats.postIndex...n] by 1
post = posts[posts.keys[i]] post = posts.get(posts.keys[i])
unless post.isFetchedQuote unless post.isFetchedQuote
ThreadStats.postCount++ ThreadStats.postCount++
ThreadStats.fileCount += post.files.length ThreadStats.fileCount += post.files.length
@ -132,7 +132,7 @@ ThreadStats =
ThreadStats.showPage and ThreadStats.showPage and
ThreadStats.pageCountEl.textContent isnt '1' and ThreadStats.pageCountEl.textContent isnt '1' and
!g.SITE.threadModTimeIgnoresSage and !g.SITE.threadModTimeIgnoresSage and
ThreadStats.thread.posts[ThreadStats.thread.lastPost].info.date > ThreadStats.lastPageUpdate ThreadStats.thread.posts.get(ThreadStats.thread.lastPost).info.date > ThreadStats.lastPageUpdate
) )
clearTimeout ThreadStats.timeout clearTimeout ThreadStats.timeout
ThreadStats.timeout = setTimeout ThreadStats.fetchPage, 5 * $.SECOND ThreadStats.timeout = setTimeout ThreadStats.fetchPage, 5 * $.SECOND

View File

@ -266,7 +266,7 @@ ThreadUpdater =
# XXX Reject updates that falsely delete the last post. # XXX Reject updates that falsely delete the last post.
return if postObjects[postObjects.length-1].no < lastPost and return if postObjects[postObjects.length-1].no < lastPost and
new Date(req.getResponseHeader('Last-Modified')) - thread.posts[lastPost].info.date < 30 * $.SECOND new Date(req.getResponseHeader('Last-Modified')) - thread.posts.get(lastPost).info.date < 30 * $.SECOND
g.SITE.Build.spoilerRange[board] = OP.custom_spoiler g.SITE.Build.spoilerRange[board] = OP.custom_spoiler
thread.setStatus 'Archived', !!OP.archived thread.setStatus 'Archived', !!OP.archived
@ -291,7 +291,7 @@ ThreadUpdater =
continue if ID <= lastPost continue if ID <= lastPost
# XXX Resurrect wrongly deleted posts. # XXX Resurrect wrongly deleted posts.
if (post = thread.posts[ID]) and not post.isFetchedQuote if (post = thread.posts.get(ID)) and not post.isFetchedQuote
post.resurrect() post.resurrect()
continue continue
@ -304,14 +304,14 @@ ThreadUpdater =
# Check for deleted posts. # Check for deleted posts.
deletedPosts = [] deletedPosts = []
for ID in ThreadUpdater.postIDs when ID not in index for ID in ThreadUpdater.postIDs when ID not in index
thread.posts[ID].kill() thread.posts.get(ID).kill()
deletedPosts.push "#{board}.#{ID}" deletedPosts.push "#{board}.#{ID}"
ThreadUpdater.postIDs = index ThreadUpdater.postIDs = index
# Check for deleted files. # Check for deleted files.
deletedFiles = [] deletedFiles = []
for ID in ThreadUpdater.fileIDs when not (ID in files or "#{board}.#{ID}" in deletedPosts) for ID in ThreadUpdater.fileIDs when not (ID in files or "#{board}.#{ID}" in deletedPosts)
thread.posts[ID].kill true thread.posts.get(ID).kill true
deletedFiles.push "#{board}.#{ID}" deletedFiles.push "#{board}.#{ID}"
ThreadUpdater.fileIDs = files ThreadUpdater.fileIDs = files

View File

@ -150,7 +150,7 @@ ThreadWatcher =
if Conf['Auto Watch'] if Conf['Auto Watch']
ThreadWatcher.addRaw boardID, threadID, {}, cb ThreadWatcher.addRaw boardID, threadID, {}, cb
else if Conf['Auto Watch Reply'] else if Conf['Auto Watch Reply']
ThreadWatcher.add (g.threads[boardID + '.' + threadID] or new Thread(threadID, g.boards[boardID] or new Board(boardID))), cb ThreadWatcher.add (g.threads.get(boardID + '.' + threadID) or new Thread(threadID, g.boards[boardID] or new Board(boardID))), cb
onIndexUpdate: (e) -> onIndexUpdate: (e) ->
{db} = ThreadWatcher {db} = ThreadWatcher
siteID = g.SITE.ID siteID = g.SITE.ID
@ -169,7 +169,7 @@ ThreadWatcher =
nKilled++ nKilled++
ThreadWatcher.refresh() if nKilled ThreadWatcher.refresh() if nKilled
onThreadRefresh: (e) -> onThreadRefresh: (e) ->
thread = g.threads[e.detail.threadID] thread = g.threads.get(e.detail.threadID)
return unless e.detail[404] and ThreadWatcher.isWatched thread return unless e.detail[404] and ThreadWatcher.isWatched thread
# Update dead status. # Update dead status.
ThreadWatcher.add thread ThreadWatcher.add thread
@ -215,7 +215,7 @@ ThreadWatcher =
ThreadWatcher.clearRequests() ThreadWatcher.clearRequests()
initLastModified: -> initLastModified: ->
lm = ($.lastModified['ThreadWatcher'] or= {}) lm = ($.lastModified['ThreadWatcher'] or= $.dict())
for siteID, boards of ThreadWatcher.dbLM.data for siteID, boards of ThreadWatcher.dbLM.data
for boardID, data of boards.boards for boardID, data of boards.boards
if ThreadWatcher.db.get {siteID, boardID} if ThreadWatcher.db.get {siteID, boardID}
@ -287,7 +287,7 @@ ThreadWatcher =
{siteID, boardID} = board[0] {siteID, boardID} = board[0]
lmDate = @getResponseHeader('Last-Modified') lmDate = @getResponseHeader('Last-Modified')
ThreadWatcher.dbLM.extend {siteID, boardID, val: $.item(url, lmDate)} ThreadWatcher.dbLM.extend {siteID, boardID, val: $.item(url, lmDate)}
threads = {} threads = $.dict()
pageLength = 0 pageLength = 0
nThreads = 0 nThreads = 0
oldest = null oldest = null
@ -449,7 +449,7 @@ ThreadWatcher =
div div
setPrefixes: (threads) -> setPrefixes: (threads) ->
prefixes = {} prefixes = $.dict()
for {siteID} in threads for {siteID} in threads
continue if siteID of prefixes continue if siteID of prefixes
len = 0 len = 0
@ -474,7 +474,7 @@ ThreadWatcher =
ThreadWatcher.setPrefixes threads ThreadWatcher.setPrefixes threads
for {siteID, boardID, threadID, data} in threads for {siteID, boardID, threadID, data} in threads
# Add missing excerpt for threads added by Auto Watch # Add missing excerpt for threads added by Auto Watch
if not data.excerpt? and siteID is g.SITE.ID and (thread = g.threads["#{boardID}.#{threadID}"]) and thread.OP if not data.excerpt? and siteID is g.SITE.ID and (thread = g.threads.get("#{boardID}.#{threadID}")) and thread.OP
ThreadWatcher.db.extend {boardID, threadID, val: {excerpt: Get.threadExcerpt thread}} ThreadWatcher.db.extend {boardID, threadID, val: {excerpt: Get.threadExcerpt thread}}
nodes.push ThreadWatcher.makeLine siteID, boardID, threadID, data nodes.push ThreadWatcher.makeLine siteID, boardID, threadID, data
{list} = ThreadWatcher {list} = ThreadWatcher
@ -554,7 +554,7 @@ ThreadWatcher =
ThreadWatcher.addRaw boardID, threadID, data, cb ThreadWatcher.addRaw boardID, threadID, data, cb
addRaw: (boardID, threadID, data, cb) -> addRaw: (boardID, threadID, data, cb) ->
oldData = ThreadWatcher.db.get {boardID, threadID, defaultValue: {}} oldData = ThreadWatcher.db.get {boardID, threadID, defaultValue: $.dict()}
delete oldData.last delete oldData.last
delete oldData.modified delete oldData.modified
$.extend oldData, data $.extend oldData, data
@ -594,7 +594,7 @@ ThreadWatcher =
$.rmClass entryEl, rmClass $.rmClass entryEl, rmClass
entryEl.textContent = text entryEl.textContent = text
true true
$.on entryEl, 'click', -> ThreadWatcher.toggle g.threads["#{g.BOARD}.#{g.THREADID}"] $.on entryEl, 'click', -> ThreadWatcher.toggle g.threads.get("#{g.BOARD}.#{g.THREADID}")
addMenuEntries: -> addMenuEntries: ->
entries = [] entries = []

View File

@ -131,7 +131,7 @@ Unread =
postIDs = Unread.thread.posts.keys postIDs = Unread.thread.posts.keys
for i in [Unread.readCount...postIDs.length] by 1 for i in [Unread.readCount...postIDs.length] by 1
ID = +postIDs[i] ID = +postIDs[i]
unless Unread.thread.posts[ID].isFetchedQuote unless Unread.thread.posts.get(ID).isFetchedQuote
break if ID > Unread.lastReadPost break if ID > Unread.lastReadPost
Unread.posts.delete ID Unread.posts.delete ID
Unread.postsQuotingYou.delete ID Unread.postsQuotingYou.delete ID
@ -217,7 +217,7 @@ Unread =
postIDs = Unread.thread.posts.keys postIDs = Unread.thread.posts.keys
for i in [Unread.readCount...postIDs.length] by 1 for i in [Unread.readCount...postIDs.length] by 1
ID = +postIDs[i] ID = +postIDs[i]
unless Unread.thread.posts[ID].isFetchedQuote unless Unread.thread.posts.get(ID).isFetchedQuote
break if Unread.posts.has ID break if Unread.posts.has ID
Unread.lastReadPost = ID Unread.lastReadPost = ID
Unread.readCount++ Unread.readCount++

View File

@ -1,7 +1,7 @@
UnreadIndex = UnreadIndex =
lastReadPost: {} lastReadPost: $.dict()
hr: {} hr: $.dict()
markReadLink: {} markReadLink: $.dict()
init: -> init: ->
return unless g.VIEW is 'index' and Conf['Remember Last Read Post'] and Conf['Unread Line in Index'] return unless g.VIEW is 'index' and Conf['Remember Last Read Post'] and Conf['Unread Line in Index']
@ -27,7 +27,7 @@ UnreadIndex =
onIndexRefresh: (e) -> onIndexRefresh: (e) ->
return if e.detail.isCatalog return if e.detail.isCatalog
for threadID in e.detail.threadIDs for threadID in e.detail.threadIDs
thread = g.threads[threadID] thread = g.threads.get(threadID)
UnreadIndex.update thread UnreadIndex.update thread
onPostsInserted: (e) -> onPostsInserted: (e) ->

View File

@ -154,7 +154,7 @@ Captcha.fixes =
else if n isnt 9 and (i = @imageKeys16.indexOf key) >= 0 and i % 4 < w and (img = @images[n - (4 - i//4)*w + (i % 4)]) else if n isnt 9 and (i = @imageKeys16.indexOf key) >= 0 and i % 4 < w and (img = @images[n - (4 - i//4)*w + (i % 4)])
img.click() img.click()
verify.focus() verify.focus()
else if dx = {'Up': n, 'Down': w, 'Left': last, 'Right': 1}[key] else if dx = $.getOwn({'Up': n, 'Down': w, 'Left': last, 'Right': 1}, key)
x = (x + dx) % (n + w) x = (x + dx) % (n + w)
if n < x < last if n < x < last
x = if dx is last then n else last x = if dx is last then n else last

View File

@ -112,7 +112,7 @@ QR =
statusCheck: -> statusCheck: ->
return unless QR.nodes return unless QR.nodes
{thread} = QR.posts[0] {thread} = QR.posts[0]
if thread isnt 'new' and g.threads["#{g.BOARD}.#{thread}"].isDead if thread isnt 'new' and g.threads.get("#{g.BOARD}.#{thread}").isDead
QR.abort() QR.abort()
else else
QR.status() QR.status()
@ -258,7 +258,7 @@ QR =
status: -> status: ->
return unless QR.nodes return unless QR.nodes
{thread} = QR.posts[0] {thread} = QR.posts[0]
if thread isnt 'new' and g.threads["#{g.BOARD}.#{thread}"].isDead if thread isnt 'new' and g.threads.get("#{g.BOARD}.#{thread}").isDead
value = 'Dead' value = 'Dead'
disabled = true disabled = true
QR.cooldown.auto = false QR.cooldown.auto = false
@ -402,7 +402,7 @@ QR =
if file if file
{type} = file {type} = file
blob = new Blob [file], {type} blob = new Blob [file], {type}
blob.name = "#{Conf['pastedname']}.#{QR.extensionFromType[type] or 'jpg'}" blob.name = "#{Conf['pastedname']}.#{$.getOwn(QR.extensionFromType, type) or 'jpg'}"
QR.open() QR.open()
QR.handleFiles [blob] QR.handleFiles [blob]
$.addClass QR.nodes.el, 'dump' $.addClass QR.nodes.el, 'dump'
@ -651,7 +651,7 @@ QR =
post = QR.posts[0] post = QR.posts[0]
post.forceSave() post.forceSave()
threadID = post.thread threadID = post.thread
thread = g.BOARD.threads[threadID] thread = g.BOARD.threads.get(threadID)
if g.BOARD.ID is 'f' and threadID is 'new' if g.BOARD.ID is 'f' and threadID is 'new'
filetag = QR.nodes.flashTag.value filetag = QR.nodes.flashTag.value
@ -662,7 +662,7 @@ QR =
err = 'New threads require a subject.' err = 'New threads require a subject.'
else unless !!g.BOARD.config.text_only or post.file else unless !!g.BOARD.config.text_only or post.file
err = 'No file selected.' err = 'No file selected.'
else if g.BOARD.threads[threadID].isClosed else if g.BOARD.threads.get(threadID).isClosed
err = 'You can\'t reply to this thread anymore.' err = 'You can\'t reply to this thread anymore.'
else unless post.com or post.file else unless post.com or post.file
err = 'No comment or file.' err = 'No comment or file.'

View File

@ -7,7 +7,7 @@ QR.cooldown =
init: -> init: ->
return unless Conf['Quick Reply'] return unless Conf['Quick Reply']
@data = Conf['cooldowns'] @data = Conf['cooldowns']
@changes = {} @changes = $.dict()
$.sync 'cooldowns', @sync $.sync 'cooldowns', @sync
# Called from QR # Called from QR
@ -35,7 +35,7 @@ QR.cooldown =
QR.cooldown.count() QR.cooldown.count()
sync: (data) -> sync: (data) ->
QR.cooldown.data = data or {} QR.cooldown.data = data or $.dict()
QR.cooldown.start() QR.cooldown.start()
add: (threadID, postID) -> add: (threadID, postID) ->
@ -63,7 +63,7 @@ QR.cooldown =
delete: (post) -> delete: (post) ->
return unless QR.cooldown.data return unless QR.cooldown.data
cooldowns = (QR.cooldown.data[post.board.ID] or= {}) cooldowns = (QR.cooldown.data[post.board.ID] or= $.dict())
for id, cooldown of cooldowns for id, cooldown of cooldowns
if !cooldown.delay? and cooldown.threadID is post.thread.ID and cooldown.postID is post.ID if !cooldown.delay? and cooldown.threadID is post.thread.ID and cooldown.postID is post.ID
QR.cooldown.set post.board.ID, id, null QR.cooldown.set post.board.ID, id, null
@ -71,7 +71,7 @@ QR.cooldown =
secondsDeletion: (post) -> secondsDeletion: (post) ->
return 0 unless QR.cooldown.data and Conf['Cooldown'] return 0 unless QR.cooldown.data and Conf['Cooldown']
cooldowns = QR.cooldown.data[post.board.ID] or {} cooldowns = QR.cooldown.data[post.board.ID] or $.dict()
for start, cooldown of cooldowns for start, cooldown of cooldowns
if !cooldown.delay? and cooldown.threadID is post.thread.ID and cooldown.postID is post.ID if !cooldown.delay? and cooldown.threadID is post.thread.ID and cooldown.postID is post.ID
seconds = QR.cooldown.delays.deletion - (Date.now() - start) // $.SECOND seconds = QR.cooldown.delays.deletion - (Date.now() - start) // $.SECOND
@ -87,25 +87,25 @@ QR.cooldown =
mergeChange: (data, scope, id, value) -> mergeChange: (data, scope, id, value) ->
if value if value
(data[scope] or= {})[id] = value (data[scope] or= $.dict())[id] = value
else if scope of data else if scope of data
delete data[scope][id] delete data[scope][id]
delete data[scope] if Object.keys(data[scope]).length is 0 delete data[scope] if Object.keys(data[scope]).length is 0
set: (scope, id, value) -> set: (scope, id, value) ->
QR.cooldown.mergeChange QR.cooldown.data, scope, id, value QR.cooldown.mergeChange QR.cooldown.data, scope, id, value
(QR.cooldown.changes[scope] or= {})[id] = value (QR.cooldown.changes[scope] or= $.dict())[id] = value
save: -> save: ->
{changes} = QR.cooldown {changes} = QR.cooldown
return unless Object.keys(changes).length return unless Object.keys(changes).length
$.get 'cooldowns', {}, ({cooldowns}) -> $.get 'cooldowns', $.dict(), ({cooldowns}) ->
for scope of QR.cooldown.changes for scope of QR.cooldown.changes
for id, value of QR.cooldown.changes[scope] for id, value of QR.cooldown.changes[scope]
QR.cooldown.mergeChange cooldowns, scope, id, value QR.cooldown.mergeChange cooldowns, scope, id, value
QR.cooldown.data = cooldowns QR.cooldown.data = cooldowns
$.set 'cooldowns', cooldowns, -> $.set 'cooldowns', cooldowns, ->
QR.cooldown.changes = {} QR.cooldown.changes = $.dict()
update: -> update: ->
return unless QR.cooldown.isCounting return unless QR.cooldown.isCounting
@ -117,7 +117,7 @@ QR.cooldown =
seconds = 0 seconds = 0
if Conf['Cooldown'] then for scope in [g.BOARD.ID, 'global'] if Conf['Cooldown'] then for scope in [g.BOARD.ID, 'global']
cooldowns = (QR.cooldown.data[scope] or= {}) cooldowns = (QR.cooldown.data[scope] or= $.dict())
for start, cooldown of cooldowns for start, cooldown of cooldowns
start = +start start = +start

View File

@ -122,6 +122,7 @@ QR.post = class
@spoiler = input.checked @spoiler = input.checked
return return
{name} = input.dataset {name} = input.dataset
return unless name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag']
prev = @[name] prev = @[name]
@[name] = input.value or input.dataset.default or null @[name] = input.value or input.dataset.default or null
switch name switch name
@ -340,7 +341,7 @@ QR.post = class
@file.newName = (@filename or '').replace /[/\\]/g, '-' @file.newName = (@filename or '').replace /[/\\]/g, '-'
unless QR.validExtension.test @filename unless QR.validExtension.test @filename
# 4chan will truncate the filename if it has no extension. # 4chan will truncate the filename if it has no extension.
@file.newName += ".#{QR.extensionFromType[@file.type] or 'jpg'}" @file.newName += ".#{$.getOwn(QR.extensionFromType, @file.type) or 'jpg'}"
updateFilename: -> updateFilename: ->
long = "#{@filename} (#{@filesize})" long = "#{@filename} (#{@filesize})"

View File

@ -10,7 +10,7 @@ QuoteBacklink =
# Second callback adds relevant containers into posts. # Second callback adds relevant containers into posts.
# This is is so that fetched posts can get their backlinks, # This is is so that fetched posts can get their backlinks,
# and that as much backlinks are appended in the background as possible. # and that as much backlinks are appended in the background as possible.
containers: {} containers: $.dict()
init: -> init: ->
return if g.VIEW not in ['index', 'thread'] or !Conf['Quote Backlinks'] return if g.VIEW not in ['index', 'thread'] or !Conf['Quote Backlinks']
@ -35,7 +35,7 @@ QuoteBacklink =
$.add a, QuoteYou.mark.cloneNode(true) if markYours $.add a, QuoteYou.mark.cloneNode(true) if markYours
for quote in @quotes for quote in @quotes
containers = [QuoteBacklink.getContainer quote] containers = [QuoteBacklink.getContainer quote]
if (post = g.posts[quote]) and post.nodes.backlinkContainer if (post = g.posts.get(quote)) and post.nodes.backlinkContainer
# Don't add OP clones when OP Backlinks is disabled, # Don't add OP clones when OP Backlinks is disabled,
# as the clones won't have the backlink containers. # as the clones won't have the backlink containers.
for clone in post.clones for clone in post.clones

View File

@ -33,7 +33,7 @@ QuoteInline =
return if $.modifiedClick e return if $.modifiedClick e
{boardID, threadID, postID} = Get.postDataFromLink @ {boardID, threadID, postID} = Get.postDataFromLink @
return if Conf['Inline Cross-thread Quotes Only'] and g.VIEW is 'thread' and g.posts["#{boardID}.#{postID}"]?.nodes.root.offsetParent # exists and not hidden return if Conf['Inline Cross-thread Quotes Only'] and g.VIEW is 'thread' and g.posts.get("#{boardID}.#{postID}")?.nodes.root.offsetParent # exists and not hidden
return if $.hasClass(doc, 'catalog-mode') return if $.hasClass(doc, 'catalog-mode')
e.preventDefault() e.preventDefault()
@ -66,7 +66,7 @@ QuoteInline =
new Fetcher boardID, threadID, postID, inline, quoter new Fetcher boardID, threadID, postID, inline, quoter
return if not ( return if not (
(post = g.posts["#{boardID}.#{postID}"]) and (post = g.posts.get("#{boardID}.#{postID}")) and
context.thread is post.thread context.thread is post.thread
) )
@ -98,13 +98,13 @@ QuoteInline =
return if not (el = root.firstElementChild) return if not (el = root.firstElementChild)
# Dereference clone. # Dereference clone.
post = g.posts["#{boardID}.#{postID}"] post = g.posts.get("#{boardID}.#{postID}")
post.rmClone el.dataset.clone post.rmClone el.dataset.clone
# Decrease forward count and unhide. # Decrease forward count and unhide.
if Conf['Forward Hiding'] and if Conf['Forward Hiding'] and
isBacklink and isBacklink and
context.thread is g.threads["#{boardID}.#{threadID}"] and context.thread is g.threads.get("#{boardID}.#{threadID}") and
not --post.forwarded not --post.forwarded
delete post.forwarded delete post.forwarded
$.rmClass post.nodes.root, 'forwarded' $.rmClass post.nodes.root, 'forwarded'

View File

@ -40,7 +40,7 @@ QuotePreview =
endEvents: 'mouseout click' endEvents: 'mouseout click'
cb: QuotePreview.mouseout cb: QuotePreview.mouseout
if Conf['Quote Highlighting'] and (origin = g.posts["#{boardID}.#{postID}"]) if Conf['Quote Highlighting'] and (origin = g.posts.get("#{boardID}.#{postID}"))
posts = [origin].concat origin.clones posts = [origin].concat origin.clones
# Remove the clone that's in the qp from the array. # Remove the clone that's in the qp from the array.
posts.pop() posts.pop()

View File

@ -11,6 +11,6 @@ QuoteStrikeThrough =
return if @isClone return if @isClone
for quotelink in @nodes.quotelinks for quotelink in @nodes.quotelinks
{boardID, postID} = Get.postDataFromLink quotelink {boardID, postID} = Get.postDataFromLink quotelink
if g.posts["#{boardID}.#{postID}"]?.isHidden if g.posts.get("#{boardID}.#{postID}")?.isHidden
$.addClass quotelink, 'filtered' $.addClass quotelink, 'filtered'
return return

View File

@ -34,9 +34,9 @@ QuoteThreading =
name: 'Quote Threading' name: 'Quote Threading'
cb: @node cb: @node
parent: {} parent: $.dict()
children: {} children: $.dict()
inserted: {} inserted: $.dict()
toggleThreading: -> toggleThreading: ->
@setThreadingState !Conf['Thread Quotes'] @setThreadingState !Conf['Thread Quotes']
@ -65,7 +65,7 @@ QuoteThreading =
parents = new Set() parents = new Set()
lastParent = null lastParent = null
for quote in @quotes when parent = g.posts[quote] for quote in @quotes when parent = g.posts.get(quote)
if not parent.isFetchedQuote and parent.isReply and parent.ID < @ID if not parent.isFetchedQuote and parent.isReply and parent.ID < @ID
parents.add parent.ID parents.add parent.ID
lastParent = parent if not lastParent or parent.ID > lastParent.ID lastParent = parent if not lastParent or parent.ID > lastParent.ID
@ -141,7 +141,7 @@ QuoteThreading =
else else
nodes = [] nodes = []
Unread.order = new RandomAccessList() Unread.order = new RandomAccessList()
QuoteThreading.inserted = {} QuoteThreading.inserted = $.dict()
posts.forEach (post) -> posts.forEach (post) ->
return if post.isFetchedQuote return if post.isFetchedQuote
Unread.order.push post Unread.order.push post

View File

@ -54,7 +54,7 @@ Quotify =
@board.ID @board.ID
quoteID = "#{boardID}.#{postID}" quoteID = "#{boardID}.#{postID}"
if post = g.posts[quoteID] if post = g.posts.get(quoteID)
unless post.isDead unless post.isDead
# Don't (Dead) when quotifying in an archived post, # Don't (Dead) when quotifying in an archived post,
# and we know the post still exists. # and we know the post still exists.

View File

@ -9,4 +9,4 @@ class CatalogThreadNative
@boardID = @nodes.thumb.parentNode.pathname.split(/\/+/)[1] @boardID = @nodes.thumb.parentNode.pathname.split(/\/+/)[1]
@board = g.boards[@boardID] or new Board(@boardID) @board = g.boards[@boardID] or new Board(@boardID)
@ID = @threadID = +(root.dataset.id or root.id).match(/\d*$/)[0] @ID = @threadID = +(root.dataset.id or root.id).match(/\d*$/)[0]
@thread = @board.threads[@ID] or new Thread(@ID, @board) @thread = @board.threads.get(@ID) or new Thread(@ID, @board)

View File

@ -17,6 +17,6 @@ class Connection
typeof e.data is 'string' and typeof e.data is 'string' and
e.data[...g.NAMESPACE.length] is g.NAMESPACE e.data[...g.NAMESPACE.length] is g.NAMESPACE
data = JSON.parse e.data[g.NAMESPACE.length..] data = JSON.parse e.data[g.NAMESPACE.length..]
for type, value of data for type, value of data when $.hasOwn(@cb, type)
@cb[type]? value @cb[type] value
return return

View File

@ -19,14 +19,14 @@ class DataBoard
@data['4chan.org'] = {boards, lastChecked} @data['4chan.org'] = {boards, lastChecked}
delete @data.boards delete @data.boards
delete @data.lastChecked delete @data.lastChecked
@data[g.SITE.ID] or= boards: {} @data[g.SITE.ID] or= boards: $.dict()
changes: [] changes: []
save: (change, cb) -> save: (change, cb) ->
change() change()
@changes.push change @changes.push change
$.get @key, {boards: {}}, (items) => $.get @key, {boards: $.dict()}, (items) =>
return unless @changes.length return unless @changes.length
needSync = ((items[@key].version or 0) > (@data.version or 0)) needSync = ((items[@key].version or 0) > (@data.version or 0))
if needSync if needSync
@ -39,7 +39,7 @@ class DataBoard
cb?() cb?()
forceSync: (cb) -> forceSync: (cb) ->
$.get @key, {boards: {}}, (items) => $.get @key, {boards: $.dict()}, (items) =>
if (items[@key].version or 0) > (@data.version or 0) if (items[@key].version or 0) > (@data.version or 0)
@initData items[@key] @initData items[@key]
change() for change in @changes change() for change in @changes
@ -78,17 +78,17 @@ class DataBoard
setUnsafe: ({siteID, boardID, threadID, postID, val}) -> setUnsafe: ({siteID, boardID, threadID, postID, val}) ->
siteID or= g.SITE.ID siteID or= g.SITE.ID
@data[siteID] or= boards: {} @data[siteID] or= boards: $.dict()
if postID isnt undefined if postID isnt undefined
((@data[siteID].boards[boardID] or= {})[threadID] or= {})[postID] = val ((@data[siteID].boards[boardID] or= $.dict())[threadID] or= $.dict())[postID] = val
else if threadID isnt undefined else if threadID isnt undefined
(@data[siteID].boards[boardID] or= {})[threadID] = val (@data[siteID].boards[boardID] or= $.dict())[threadID] = val
else else
@data[siteID].boards[boardID] = val @data[siteID].boards[boardID] = val
extend: ({siteID, boardID, threadID, postID, val}, cb) -> extend: ({siteID, boardID, threadID, postID, val}, cb) ->
@save => @save =>
oldVal = @get {siteID, boardID, threadID, postID, defaultValue: {}} oldVal = @get {siteID, boardID, threadID, postID, defaultValue: $.dict()}
for key, subVal of val for key, subVal of val
if typeof subVal is 'undefined' if typeof subVal is 'undefined'
delete oldVal[key] delete oldVal[key]
@ -147,7 +147,7 @@ class DataBoard
ajaxCleanParse: (boardID, response1, response2) -> ajaxCleanParse: (boardID, response1, response2) ->
siteID = g.SITE.ID siteID = g.SITE.ID
return if not (board = @data[siteID].boards[boardID]) return if not (board = @data[siteID].boards[boardID])
threads = {} threads = $.dict()
if response1 if response1
for page in response1 for page in response1
for thread in page.threads for thread in page.threads

View File

@ -1,11 +1,11 @@
class Fetcher class Fetcher
constructor: (@boardID, @threadID, @postID, @root, @quoter) -> constructor: (@boardID, @threadID, @postID, @root, @quoter) ->
if post = g.posts["#{@boardID}.#{@postID}"] if post = g.posts.get("#{@boardID}.#{@postID}")
@insert post @insert post
return return
# 4chan X catalog data # 4chan X catalog data
if (post = Index.replyData?["#{@boardID}.#{@postID}"]) and (thread = g.threads["#{@boardID}.#{@threadID}"]) if (post = Index.replyData?["#{@boardID}.#{@postID}"]) and (thread = g.threads.get("#{@boardID}.#{@threadID}"))
board = g.boards[@boardID] board = g.boards[@boardID]
post = new Post g.SITE.Build.postFromObject(post, @boardID), thread, board, {isFetchedQuote: true} post = new Post g.SITE.Build.postFromObject(post, @boardID), thread, board, {isFetchedQuote: true}
Main.callbackNodes 'Post', [post] Main.callbackNodes 'Post', [post]
@ -53,7 +53,7 @@ class Fetcher
fetchedPost: (req, isCached) -> fetchedPost: (req, isCached) ->
# In case of multiple callbacks for the same request, # In case of multiple callbacks for the same request,
# don't parse the same original post more than once. # don't parse the same original post more than once.
if post = g.posts["#{@boardID}.#{@postID}"] if post = g.posts.get("#{@boardID}.#{@postID}")
@insert post @insert post
return return
@ -96,7 +96,7 @@ class Fetcher
board = g.boards[@boardID] or board = g.boards[@boardID] or
new Board @boardID new Board @boardID
thread = g.threads["#{@boardID}.#{@threadID}"] or thread = g.threads.get("#{@boardID}.#{@threadID}") or
new Thread @threadID, board new Thread @threadID, board
post = new Post g.SITE.Build.postFromObject(post, @boardID), thread, board, {isFetchedQuote: true} post = new Post g.SITE.Build.postFromObject(post, @boardID), thread, board, {isFetchedQuote: true}
Main.callbackNodes 'Post', [post] Main.callbackNodes 'Post', [post]
@ -115,7 +115,7 @@ class Fetcher
for key of media when /_link$/.test key for key of media when /_link$/.test key
# Image/thumbnail URLs loaded over HTTP can be modified in transit. # Image/thumbnail URLs loaded over HTTP can be modified in transit.
# Require them to be from an HTTP host so that no referrer is sent to them from an HTTPS page. # Require them to be from an HTTP host so that no referrer is sent to them from an HTTPS page.
delete media[key] unless media[key]?.match /^http:\/\// delete media[key] unless $.getOwn(media, key)?.match /^http:\/\//
that.parseArchivedPost @response, url, archive that.parseArchivedPost @response, url, archive
return true return true
return false return false
@ -123,7 +123,7 @@ class Fetcher
parseArchivedPost: (data, url, archive) -> parseArchivedPost: (data, url, archive) ->
# In case of multiple callbacks for the same request, # In case of multiple callbacks for the same request,
# don't parse the same original post more than once. # don't parse the same original post more than once.
if post = g.posts["#{@boardID}.#{@postID}"] if post = g.posts.get("#{@boardID}.#{@postID}")
@insert post @insert post
return return
@ -210,7 +210,7 @@ class Fetcher
board = g.boards[@boardID] or board = g.boards[@boardID] or
new Board @boardID new Board @boardID
thread = g.threads["#{@boardID}.#{@threadID}"] or thread = g.threads.get("#{@boardID}.#{@threadID}") or
new Thread @threadID, board new Thread @threadID, board
post = new Post g.SITE.Build.post(o), thread, board, {isFetchedQuote: true} post = new Post g.SITE.Build.post(o), thread, board, {isFetchedQuote: true}
post.kill() post.kill()

View File

@ -59,9 +59,9 @@ class Post
<% if (readJSON('/.tests_enabled')) { %> <% if (readJSON('/.tests_enabled')) { %>
return if @forBuildTest return if @forBuildTest
<% } %> <% } %>
if g.posts[@fullID] if g.posts.get(@fullID)
@isRebuilt = true @isRebuilt = true
@clones = g.posts[@fullID].clones @clones = g.posts.get(@fullID).clones
clone.origin = @ for clone in @clones clone.origin = @ for clone in @clones
@thread.lastPost = @ID if !@isFetchedQuote and @ID > @thread.lastPost @thread.lastPost = @ID if !@isFetchedQuote and @ID > @thread.lastPost

View File

@ -1,6 +1,6 @@
class ShimSet class ShimSet
constructor: -> constructor: ->
@elements = {} @elements = $.dict()
@size = 0 @size = 0
has: (value) -> has: (value) ->
value of @elements value of @elements

View File

@ -16,3 +16,9 @@ class SimpleDict
forEach: (fn) -> forEach: (fn) ->
fn @[key] for key in [@keys...] fn @[key] for key in [@keys...]
return return
get: (key) ->
if key is 'keys'
undefined
else
$.getOwn(@, key)

View File

@ -1,7 +1,8 @@
class Thread class Thread
toString: -> @ID toString: -> @ID
constructor: (@ID, @board) -> constructor: (ID, @board) ->
@ID = +ID
@threadID = @ID @threadID = @ID
@boardID = @board.ID @boardID = @board.ID
@siteID = g.SITE.ID @siteID = g.SITE.ID

View File

@ -27,7 +27,7 @@ sub: function(css) {
var sel = variables; var sel = variables;
for (var i = 0; i < words.length; i++) { for (var i = 0; i < words.length; i++) {
if (typeof sel !== 'object') return ':not(*)'; if (typeof sel !== 'object') return ':not(*)';
sel = sel[words[i]]; sel = $.getOwn(sel, words[i]);
} }
if (typeof sel !== 'string') return ':not(*)'; if (typeof sel !== 'string') return ':not(*)';
return sel; return sel;

View File

@ -1,6 +1,6 @@
var Conf, E, c, d, doc, docSet, g; var Conf, E, c, d, doc, docSet, g;
Conf = {}; Conf = Object.create(null);
c = console; c = console;
d = document; d = document;
doc = d.documentElement; doc = d.documentElement;
@ -13,8 +13,8 @@ docSet = function() {
g = { g = {
VERSION: '<%= readJSON('/version.json').version %>', VERSION: '<%= readJSON('/version.json').version %>',
NAMESPACE: '<%= meta.name %>.', NAMESPACE: '<%= meta.name %>.',
sites: {}, sites: Object.create(null),
boards: {} boards: Object.create(null)
}; };
E = (function() { E = (function() {

View File

@ -33,7 +33,7 @@ Main =
# Flatten default values from Config into Conf # Flatten default values from Config into Conf
flatten = (parent, obj) -> flatten = (parent, obj) ->
if obj instanceof Array if obj instanceof Array
Conf[parent] = obj[0] Conf[parent] = $.dict.clone(obj[0])
else if typeof obj is 'object' else if typeof obj is 'object'
for key, val of obj for key, val of obj
flatten key, val flatten key, val
@ -57,15 +57,15 @@ Main =
flatten null, Config flatten null, Config
for db in DataBoard.keys for db in DataBoard.keys
Conf[db] = {} Conf[db] = $.dict()
Conf['customTitles'] = {'4chan.org': {boards: {'qa': {'boardTitle': {orig: '/qa/ - Question & Answer', title: '/qa/ - 2D / Random'}}}}} Conf['customTitles'] = $.dict.clone {'4chan.org': {boards: {'qa': {'boardTitle': {orig: '/qa/ - Question & Answer', title: '/qa/ - 2D / Random'}}}}}
Conf['boardConfig'] = boards: {} Conf['boardConfig'] = boards: $.dict()
Conf['archives'] = Redirect.archives Conf['archives'] = Redirect.archives
Conf['selectedArchives'] = {} Conf['selectedArchives'] = $.dict()
Conf['cooldowns'] = {} Conf['cooldowns'] = $.dict()
Conf['Index Sort'] = {} Conf['Index Sort'] = $.dict()
Conf["Last Long Reply Thresholds #{i}"] = {} for i in [0...2] Conf["Last Long Reply Thresholds #{i}"] = $.dict() for i in [0...2]
Conf['siteProperties'] = {} Conf['siteProperties'] = $.dict()
# XXX old key names # XXX old key names
Conf['Except Archives from Encryption'] = false Conf['Except Archives from Encryption'] = false
@ -84,7 +84,7 @@ Main =
$.addCSP "script-src #{jsWhitelist.replace(/^#.*$/mg, '').replace(/[\s;]+/g, ' ').trim()}" $.addCSP "script-src #{jsWhitelist.replace(/^#.*$/mg, '').replace(/[\s;]+/g, ' ').trim()}"
# Get saved values as items # Get saved values as items
items = {} items = $.dict()
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) ->
@ -363,7 +363,7 @@ Main =
else else
g.BOARD g.BOARD
threadID = +threadRoot.id.match(/\d*$/)[0] threadID = +threadRoot.id.match(/\d*$/)[0]
return if !threadID or boardObj.threads[threadID]?.nodes.root return if !threadID or boardObj.threads.get(threadID)?.nodes.root
thread = new Thread threadID, boardObj thread = new Thread threadID, boardObj
thread.nodes.root = threadRoot thread.nodes.root = threadRoot
threads.push thread threads.push thread

View File

@ -40,6 +40,32 @@ $.extend = (object, properties) ->
object[key] = val object[key] = val
return return
$.dict = ->
Object.create(null)
$.dict.clone = (obj) ->
if typeof obj isnt 'object' or obj is null
obj
else if obj instanceof Array
arr = []
for i in [0...obj.length] by 1
arr.push $.dict.clone(obj[i])
arr
else
map = Object.create(null)
for key, val of obj
map[key] = $.dict.clone(val)
map
$.dict.json = (str) ->
$.dict.clone(JSON.parse(str))
$.hasOwn = (obj, key) ->
Object::hasOwnProperty.call(obj, key)
$.getOwn = (obj, key) ->
if Object::hasOwnProperty.call(obj, key) then obj[key] else undefined
$.ajax = do -> $.ajax = do ->
if window.wrappedJSObject and not XMLHttpRequest.wrappedJSObject if window.wrappedJSObject and not XMLHttpRequest.wrappedJSObject
pageXHR = XPCNativeWrapper window.wrappedJSObject.XMLHttpRequest pageXHR = XPCNativeWrapper window.wrappedJSObject.XMLHttpRequest
@ -85,7 +111,7 @@ $.ajax = do ->
# XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638 # XXX https://bugs.chromium.org/p/chromium/issues/detail?id=920638
do -> do ->
requestID = 0 requestID = 0
requests = {} requests = $.dict()
$.ajaxPageInit = -> $.ajaxPageInit = ->
if Conf['Chromium CORB Bug'] and g.SITE.software is 'yotsuba' if Conf['Chromium CORB Bug'] and g.SITE.software is 'yotsuba'
@ -97,7 +123,7 @@ do ->
$.set 'Chromium CORB Bug', (Conf['Chromium CORB Bug'] = false) $.set 'Chromium CORB Bug', (Conf['Chromium CORB Bug'] = false)
$.global -> $.global ->
window.FCX.requests = {} window.FCX.requests = Object.create(null)
document.addEventListener '4chanXAjax', (e) -> document.addEventListener '4chanXAjax', (e) ->
{url, timeout, responseType, withCredentials, type, onprogress, form, headers, id} = e.detail {url, timeout, responseType, withCredentials, type, onprogress, form, headers, id} = e.detail
@ -144,7 +170,8 @@ do ->
return unless (req = requests[e.detail.id]) return unless (req = requests[e.detail.id])
delete requests[e.detail.id] delete requests[e.detail.id]
if e.detail.status if e.detail.status
$.extend req, e.detail for key in ['status', 'statusText', 'response', 'responseHeaderString']
req[key] = e.detail[key]
if req.responseType is 'document' if req.responseType is 'document'
req.response = new DOMParser().parseFromString(e.detail.response, 'text/html') req.response = new DOMParser().parseFromString(e.detail.response, 'text/html')
req.onloadend() req.onloadend()
@ -165,7 +192,7 @@ do ->
# Status Code 304: Not modified # Status Code 304: Not modified
# With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses. # With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses.
# This saves a lot of bandwidth and CPU time for both the users and the servers. # This saves a lot of bandwidth and CPU time for both the users and the servers.
$.lastModified = {} $.lastModified = $.dict()
$.whenModified = (url, bucket, cb, options={}) -> $.whenModified = (url, bucket, cb, options={}) ->
{timeout, ajax} = options {timeout, ajax} = options
params = [] params = []
@ -174,12 +201,12 @@ $.whenModified = (url, bucket, cb, options={}) ->
params.push "t=#{Date.now()}" if url.split('/')[2] is 'a.4cdn.org' params.push "t=#{Date.now()}" if url.split('/')[2] is 'a.4cdn.org'
url0 = url url0 = url
url += '?' + params.join('&') if params.length url += '?' + params.join('&') if params.length
headers = {} headers = $.dict()
if (t = $.lastModified[bucket]?[url0])? if (t = $.lastModified[bucket]?[url0])?
headers['If-Modified-Since'] = t headers['If-Modified-Since'] = t
r = (ajax or $.ajax) url, { r = (ajax or $.ajax) url, {
onloadend: -> onloadend: ->
($.lastModified[bucket] or= {})[url0] = @getResponseHeader('Last-Modified') ($.lastModified[bucket] or= $.dict())[url0] = @getResponseHeader('Last-Modified')
cb.call @ cb.call @
timeout timeout
headers headers
@ -187,7 +214,7 @@ $.whenModified = (url, bucket, cb, options={}) ->
r r
do -> do ->
reqs = {} reqs = $.dict()
$.cache = (url, cb, options={}) -> $.cache = (url, cb, options={}) ->
{ajax} = options {ajax} = options
if (req = reqs[url]) if (req = reqs[url])
@ -212,11 +239,13 @@ do ->
$.cb = $.cb =
checked: -> checked: ->
$.set @name, @checked if $.hasOwn(Conf, @name)
Conf[@name] = @checked $.set @name, @checked
Conf[@name] = @checked
value: -> value: ->
$.set @name, @value.trim() if $.hasOwn(Conf, @name)
Conf[@name] = @value $.set @name, @value.trim()
Conf[@name] = @value
$.asap = (test, cb) -> $.asap = (test, cb) ->
if test() if test()
@ -478,14 +507,14 @@ $.platform = '<%= type %>';
$.hasStorage = do -> $.hasStorage = do ->
try try
return true if localStorage[g.NAMESPACE + 'hasStorage'] is 'true' return true if localStorage.getItem(g.NAMESPACE + 'hasStorage') is 'true'
localStorage[g.NAMESPACE + 'hasStorage'] = 'true' localStorage.setItem(g.NAMESPACE + 'hasStorage', 'true')
return localStorage[g.NAMESPACE + 'hasStorage'] is 'true' return localStorage.getItem(g.NAMESPACE + 'hasStorage') is 'true'
catch catch
false false
$.item = (key, val) -> $.item = (key, val) ->
item = {} item = $.dict()
item[key] = val item[key] = val
item item
@ -496,7 +525,7 @@ $.oneItemSugar = (fn) ->
else else
fn key, val fn key, val
$.syncing = {} $.syncing = $.dict()
$.securityCheck = (data) -> $.securityCheck = (data) ->
if location.protocol isnt 'https:' if location.protocol isnt 'https:'
@ -505,13 +534,13 @@ $.securityCheck = (data) ->
<% if (type === 'crx') { %> <% if (type === 'crx') { %>
# https://developer.chrome.com/extensions/storage.html # https://developer.chrome.com/extensions/storage.html
$.oldValue = $.oldValue =
local: {} local: $.dict()
sync: {} sync: $.dict()
chrome.storage.onChanged.addListener (changes, area) -> chrome.storage.onChanged.addListener (changes, area) ->
for key of changes for key of changes
oldValue = $.oldValue.local[key] ? $.oldValue.sync[key] oldValue = $.oldValue.local[key] ? $.oldValue.sync[key]
$.oldValue[area][key] = changes[key].newValue $.oldValue[area][key] = $.dict.clone(changes[key].newValue)
newValue = $.oldValue.local[key] ? $.oldValue.sync[key] newValue = $.oldValue.local[key] ? $.oldValue.sync[key]
cb = $.syncing[key] cb = $.syncing[key]
if cb and JSON.stringify(newValue) isnt JSON.stringify(oldValue) if cb and JSON.stringify(newValue) isnt JSON.stringify(oldValue)
@ -542,11 +571,12 @@ $.get = $.oneItemSugar (data, cb) ->
if $.engine is 'gecko' and area is 'sync' and keys.length > 3 if $.engine is 'gecko' and area is 'sync' and keys.length > 3
keys = null keys = null
chrome.storage[area].get keys, (result) -> chrome.storage[area].get keys, (result) ->
result = $.dict.clone(result)
if chrome.runtime.lastError if chrome.runtime.lastError
c.error chrome.runtime.lastError.message c.error chrome.runtime.lastError.message
if keys is null if keys is null
result2 = {} result2 = $.dict()
result2[key] = val for key, val of result when key of data result2[key] = val for key, val of result when $.hasOwn(data, key)
result = result2 result = result2
for key of data for key of data
$.oldValue[area][key] = result[key] $.oldValue[area][key] = result[key]
@ -560,8 +590,8 @@ $.get = $.oneItemSugar (data, cb) ->
do -> do ->
items = items =
local: {} local: $.dict()
sync: {} sync: $.dict()
exceedsQuota = (key, value) -> exceedsQuota = (key, value) ->
# bytes in UTF-8 # bytes in UTF-8
@ -579,7 +609,7 @@ do ->
timeout = {} timeout = {}
setArea = (area, cb) -> setArea = (area, cb) ->
data = {} data = $.dict()
$.extend data, items[area] $.extend data, items[area]
return if !Object.keys(data).length or timeout[area] > Date.now() return if !Object.keys(data).length or timeout[area] > Date.now()
chrome.storage[area].set data, -> chrome.storage[area].set data, ->
@ -609,8 +639,8 @@ do ->
$.clear = (cb) -> $.clear = (cb) ->
return unless $.crxWorking() return unless $.crxWorking()
items.local = {} items.local = $.dict()
items.sync = {} items.sync = $.dict()
count = 2 count = 2
err = null err = null
done = -> done = ->
@ -631,7 +661,7 @@ if GM?.deleteValue? and window.BroadcastChannel and not GM_addValueChangeListene
$.on $.syncChannel, 'message', (e) -> $.on $.syncChannel, 'message', (e) ->
for key, val of e.data when (cb = $.syncing[key]) for key, val of e.data when (cb = $.syncing[key])
cb JSON.parse(JSON.stringify(val)), key cb $.dict.json(JSON.stringify(val)), key
$.sync = (key, cb) -> $.sync = (key, cb) ->
$.syncing[key] = cb $.syncing[key] = cb
@ -642,7 +672,7 @@ if GM?.deleteValue? and window.BroadcastChannel and not GM_addValueChangeListene
unless keys instanceof Array unless keys instanceof Array
keys = [keys] keys = [keys]
Promise.all(GM.deleteValue(g.NAMESPACE + key) for key in keys).then -> Promise.all(GM.deleteValue(g.NAMESPACE + key) for key in keys).then ->
items = {} items = $.dict()
items[key] = undefined for key in keys items[key] = undefined for key in keys
$.syncChannel.postMessage items $.syncChannel.postMessage items
cb?() cb?()
@ -651,7 +681,7 @@ if GM?.deleteValue? and window.BroadcastChannel and not GM_addValueChangeListene
keys = Object.keys items keys = Object.keys items
Promise.all(GM.getValue(g.NAMESPACE + key) for key in keys).then (values) -> Promise.all(GM.getValue(g.NAMESPACE + key) for key in keys).then (values) ->
for val, i in values when val for val, i in values when val
items[keys[i]] = JSON.parse val items[keys[i]] = $.dict.json val
cb items cb items
$.set = $.oneItemSugar (items, cb) -> $.set = $.oneItemSugar (items, cb) ->
@ -675,7 +705,7 @@ else
$.getValue = GM_getValue $.getValue = GM_getValue
$.listValues = -> GM_listValues() # error when called if missing $.listValues = -> GM_listValues() # error when called if missing
else if $.hasStorage else if $.hasStorage
$.getValue = (key) -> localStorage[key] $.getValue = (key) -> localStorage.getItem(key)
$.listValues = -> $.listValues = ->
key for key of localStorage when key[...g.NAMESPACE.length] is g.NAMESPACE key for key of localStorage when key[...g.NAMESPACE.length] is g.NAMESPACE
else else
@ -686,12 +716,12 @@ else
$.setValue = GM_setValue $.setValue = GM_setValue
$.deleteValue = GM_deleteValue $.deleteValue = GM_deleteValue
else if GM_deleteValue? else if GM_deleteValue?
$.oldValue = {} $.oldValue = $.dict()
$.setValue = (key, val) -> $.setValue = (key, val) ->
GM_setValue key, val GM_setValue key, val
if key of $.syncing if key of $.syncing
$.oldValue[key] = val $.oldValue[key] = val
localStorage[key] = val if $.hasStorage # for `storage` events localStorage.setItem(key, val) if $.hasStorage # for `storage` events
$.deleteValue = (key) -> $.deleteValue = (key) ->
GM_deleteValue key GM_deleteValue key
if key of $.syncing if key of $.syncing
@ -699,10 +729,10 @@ else
localStorage.removeItem key if $.hasStorage # for `storage` events localStorage.removeItem key if $.hasStorage # for `storage` events
$.cantSync = true if !$.hasStorage $.cantSync = true if !$.hasStorage
else if $.hasStorage else if $.hasStorage
$.oldValue = {} $.oldValue = $.dict()
$.setValue = (key, val) -> $.setValue = (key, val) ->
$.oldValue[key] = val if key of $.syncing $.oldValue[key] = val if key of $.syncing
localStorage[key] = val localStorage.setItem(key, val)
$.deleteValue = (key) -> $.deleteValue = (key) ->
delete $.oldValue[key] if key of $.syncing delete $.oldValue[key] if key of $.syncing
localStorage.removeItem key localStorage.removeItem key
@ -715,7 +745,7 @@ else
$.sync = (key, cb) -> $.sync = (key, cb) ->
$.syncing[key] = GM_addValueChangeListener g.NAMESPACE + key, (key2, oldValue, newValue, remote) -> $.syncing[key] = GM_addValueChangeListener g.NAMESPACE + key, (key2, oldValue, newValue, remote) ->
if remote if remote
newValue = JSON.parse newValue unless newValue is undefined newValue = $.dict.json newValue unless newValue is undefined
cb newValue, key cb newValue, key
$.forceSync = -> $.forceSync = ->
else if GM_deleteValue? or $.hasStorage else if GM_deleteValue? or $.hasStorage
@ -730,7 +760,7 @@ else
if newValue? if newValue?
return if newValue is $.oldValue[key] return if newValue is $.oldValue[key]
$.oldValue[key] = newValue $.oldValue[key] = newValue
cb JSON.parse(newValue), key[g.NAMESPACE.length..] cb $.dict.json(newValue), key[g.NAMESPACE.length..]
else else
return unless $.oldValue[key]? return unless $.oldValue[key]?
delete $.oldValue[key] delete $.oldValue[key]
@ -760,7 +790,7 @@ else
$.getSync = (items, cb) -> $.getSync = (items, cb) ->
for key of items when (val2 = $.getValue g.NAMESPACE + key) for key of items when (val2 = $.getValue g.NAMESPACE + key)
try try
items[key] = JSON.parse val2 items[key] = $.dict.json val2
catch err catch err
# XXX https://github.com/ccd0/4chan-x/issues/2218 # XXX https://github.com/ccd0/4chan-x/issues/2218
unless /^(?:undefined)*$/.test(val2) unless /^(?:undefined)*$/.test(val2)

View File

@ -10,7 +10,7 @@ eventPageRequest = do ->
<% } %> <% } %>
CrossOrigin = CrossOrigin =
binary: (url, cb, headers={}) -> binary: (url, cb, headers=$.dict()) ->
# XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310 # XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310
url = url.replace /^((?:https?:)?\/\/(?:\w+\.)?(?:4chan|4channel|4cdn)\.org)\/adv\//, '$1//adv/' url = url.replace /^((?:https?:)?\/\/(?:\w+\.)?(?:4chan|4channel|4cdn)\.org)\/adv\//, '$1//adv/'
<% if (type === 'crx') { %> <% if (type === 'crx') { %>
@ -73,7 +73,7 @@ CrossOrigin =
name = match.replace /\\"/g, '"' name = match.replace /\\"/g, '"'
if /^text\/plain;\s*charset=x-user-defined$/i.test(mime) if /^text\/plain;\s*charset=x-user-defined$/i.test(mime)
# In JS Blocker (Safari) content type comes back as 'text/plain; charset=x-user-defined'; guess from filename instead. # In JS Blocker (Safari) content type comes back as 'text/plain; charset=x-user-defined'; guess from filename instead.
mime = QR.typeFromExtension[name.match(/[^.]*$/)[0].toLowerCase()] or 'application/octet-stream' mime = $.getOwn(QR.typeFromExtension, name.match(/[^.]*$/)[0].toLowerCase()) or 'application/octet-stream'
blob = new Blob([data], {type: mime}) blob = new Blob([data], {type: mime})
blob.name = name blob.name = name
cb blob cb blob
@ -85,13 +85,13 @@ CrossOrigin =
responseHeaderString: null responseHeaderString: null
getResponseHeader: (headerName) -> getResponseHeader: (headerName) ->
if !@responseHeaders? and @responseHeaderString? if !@responseHeaders? and @responseHeaderString?
@responseHeaders = {} @responseHeaders = $.dict()
for header in @responseHeaderString.split('\r\n') for header in @responseHeaderString.split('\r\n')
if (i = header.indexOf(':')) >= 0 if (i = header.indexOf(':')) >= 0
key = header[...i].trim().toLowerCase() key = header[...i].trim().toLowerCase()
val = header[i+1..].trim() val = header[i+1..].trim()
@responseHeaders[key] = val @responseHeaders[key] = val
(@responseHeaders or {})[headerName.toLowerCase()] ? null @responseHeaders?[headerName.toLowerCase()] ? null
abort: -> abort: ->
onloadend: -> onloadend: ->

View File

@ -33,7 +33,7 @@ SW.tinyboard =
detect: -> detect: ->
for script in $$ 'script:not([src])', d.head for script in $$ 'script:not([src])', d.head
if (m = script.textContent.match(/\bvar configRoot=(".*?")/)) if (m = script.textContent.match(/\bvar configRoot=(".*?")/))
properties = {} properties = $.dict()
try try
root = JSON.parse m[1] root = JSON.parse m[1]
if root[0] is '/' if root[0] is '/'

View File

@ -1,7 +1,7 @@
Build = Build =
staticPath: '//s.4cdn.org/image/' staticPath: '//s.4cdn.org/image/'
gifIcon: if window.devicePixelRatio >= 2 then '@2x.gif' else '.gif' gifIcon: if window.devicePixelRatio >= 2 then '@2x.gif' else '.gif'
spoilerRange: {} spoilerRange: $.dict()
shortFilename: (filename) -> shortFilename: (filename) ->
ext = filename.match(/\.?[^\.]*$/)[0] ext = filename.match(/\.?[^\.]*$/)[0]
@ -69,8 +69,9 @@ Build =
o.file = SW.yotsuba.Build.parseJSONFile(data, {siteID, boardID}) o.file = SW.yotsuba.Build.parseJSONFile(data, {siteID, boardID})
o.files.push o.file o.files.push o.file
# Temporary JSON properties for events such as April 1 / Halloween # Temporary JSON properties for events such as April 1 / Halloween
o.extra = $.dict()
for key of data when key[0] is 'x' for key of data when key[0] is 'x'
o[key] = data[key] o.extra[key] = data[key]
o o
parseJSONFile: (data, {siteID, boardID}) -> parseJSONFile: (data, {siteID, boardID}) ->
@ -133,7 +134,7 @@ Build =
capcodePlural = 'Verified Users' capcodePlural = 'Verified Users'
capcodeDescription = '' capcodeDescription = ''
else else
capcodeLong = {'Admin': 'Administrator', 'Mod': 'Moderator'}[capcode] or capcode capcodeLong = $.getOwn({'Admin': 'Administrator', 'Mod': 'Moderator'}, capcode) or capcode
capcodePlural = "#{capcodeLong}s" capcodePlural = "#{capcodeLong}s"
capcodeDescription = "a 4chan #{capcodeLong}" capcodeDescription = "a 4chan #{capcodeLong}"

View File

@ -5,7 +5,7 @@
?{email}{<a href="mailto:${encodeURIComponent(email).replace(/%40/g, "@")}" class="useremail">} ?{email}{<a href="mailto:${encodeURIComponent(email).replace(/%40/g, "@")}" class="useremail">}
<span class="name?{capcode}{ capcode}">${name}</span> <span class="name?{capcode}{ capcode}">${name}</span>
?{tripcode}{ <span class="postertrip">${tripcode}</span>} ?{tripcode}{ <span class="postertrip">${tripcode}</span>}
?{o.xa19s}{ <span class="like-score">${o.xa19s}</span>} ?{o.extra.xa19s}{ <span class="like-score">${o.extra.xa19s}</span>}
?{pass}{ <span title="Pass user since ${pass}" class="n-pu"></span>} ?{pass}{ <span title="Pass user since ${pass}" class="n-pu"></span>}
?{capcode}{ <strong class="capcode hand id_${capcodeLC}" title="Highlight posts by ${capcodePlural}">## ${capcode}</strong>} ?{capcode}{ <strong class="capcode hand id_${capcodeLC}" title="Highlight posts by ${capcodePlural}">## ${capcode}</strong>}
?{email}{</a>} ?{email}{</a>}
@ -19,7 +19,7 @@
<span class="postNum?{!(boardID === "f" && !o.isReply)}{ desktop}"> <span class="postNum?{!(boardID === "f" && !o.isReply)}{ desktop}">
<a href="${postLink}" title="Link to this post">No.</a> <a href="${postLink}" title="Link to this post">No.</a>
<a href="${quoteLink}" title="Reply to this post">${ID}</a> <a href="${quoteLink}" title="Reply to this post">${ID}</a>
?{o.xa19l && o.isReply}{ <a data-cmd="like-post" href="#" class="like-btn">Like! ×${o.xa19l}</a>} ?{o.extra.xa19l && o.isReply}{ <a data-cmd="like-post" href="#" class="like-btn">Like! ×${o.extra.xa19l}</a>}
?{o.isSticky}{ <img src="${staticPath}sticky${gifIcon}" alt="Sticky" title="Sticky"?{boardID === "f"}{ style="height: 18px; width: 18px;"}{ class="stickyIcon retina"}>} ?{o.isSticky}{ <img src="${staticPath}sticky${gifIcon}" alt="Sticky" title="Sticky"?{boardID === "f"}{ style="height: 18px; width: 18px;"}{ class="stickyIcon retina"}>}
?{o.isClosed && !o.isArchived}{ <img src="${staticPath}closed${gifIcon}" alt="Closed" title="Closed"?{boardID === "f"}{ style="height: 18px; width: 18px;"}{ class="closedIcon retina"}>} ?{o.isClosed && !o.isArchived}{ <img src="${staticPath}closed${gifIcon}" alt="Closed" title="Closed"?{boardID === "f"}{ style="height: 18px; width: 18px;"}{ class="closedIcon retina"}>}
?{o.isArchived}{ <img src="${staticPath}archived${gifIcon}" alt="Archived" title="Archived" class="archivedIcon retina">} ?{o.isArchived}{ <img src="${staticPath}archived${gifIcon}" alt="Archived" title="Archived" class="archivedIcon retina">}

View File

@ -7,14 +7,14 @@ Site =
init: (cb) -> init: (cb) ->
$.extend Conf['siteProperties'], Site.defaultProperties $.extend Conf['siteProperties'], Site.defaultProperties
hostname = Site.resolve() hostname = Site.resolve()
if hostname and Conf['siteProperties'][hostname].software of SW if hostname and $.hasOwn(SW, Conf['siteProperties'][hostname].software)
@set hostname @set hostname
cb() cb()
$.onExists doc, 'body', => $.onExists doc, 'body', =>
for software of SW when (changes = SW[software].detect?()) for software of SW when (changes = SW[software].detect?())
changes.software = software changes.software = software
hostname = location.hostname.replace(/^www\./, '') hostname = location.hostname.replace(/^www\./, '')
properties = (Conf['siteProperties'][hostname] or= {}) properties = (Conf['siteProperties'][hostname] or= $.dict())
changed = 0 changed = 0
for key of changes when properties[key] isnt changes[key] for key of changes when properties[key] isnt changes[key]
properties[key] = changes[key] properties[key] = changes[key]
@ -29,7 +29,7 @@ Site =
resolve: (url=location) -> resolve: (url=location) ->
{hostname} = url {hostname} = url
while hostname and hostname not of Conf['siteProperties'] while hostname and not $.hasOwn(Conf['siteProperties'], hostname)
hostname = hostname.replace(/^[^.]*\.?/, '') hostname = hostname.replace(/^[^.]*\.?/, '')
if hostname if hostname
hostname = canonical if (canonical = Conf['siteProperties'][hostname].canonical) hostname = canonical if (canonical = Conf['siteProperties'][hostname].canonical)
@ -43,7 +43,7 @@ Site =
for ID, properties of Conf['siteProperties'] for ID, properties of Conf['siteProperties']
continue if properties.canonical continue if properties.canonical
software = properties.software software = properties.software
continue unless software and SW[software] continue unless software and $.hasOwn(SW, software)
g.sites[ID] = site = Object.create SW[software] g.sites[ID] = site = Object.create SW[software]
$.extend site, {ID, siteID: ID, properties, software} $.extend site, {ID, siteID: ID, properties, software}
g.SITE = g.sites[hostname] g.SITE = g.sites[hostname]