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

View File

@ -1,6 +1,6 @@
Filter =
filters: {}
results: {}
filters: $.dict()
results: $.dict()
init: ->
return unless g.VIEW in ['index', 'thread', 'catalog'] and Conf['Filter']
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.
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.
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.
# Defaults to stub showing.
@ -102,7 +102,7 @@ Filter =
parseBoards: (boardsRaw) ->
return false unless boardsRaw
return boards if (boards = Filter.parseBoardsMemo[boardsRaw])
boards = {}
boards = $.dict()
siteFilter = ''
for boardID in boardsRaw.split(',')
if ':' in boardID
@ -116,7 +116,7 @@ Filter =
Filter.parseBoardsMemo[boardsRaw] = boards
boards
parseBoardsMemo: {}
parseBoardsMemo: $.dict()
test: (post, hideable=true) ->
return post.filterResults if post.filterResults
@ -172,7 +172,7 @@ Filter =
catalog: ->
return unless (url = g.SITE.urls.catalogJSON?(g.BOARD))
Filter.catalogData = {}
Filter.catalogData = $.dict()
$.ajax url,
onloadend: Filter.catalogParse
Callbacks.CatalogThreadNative.push
@ -225,17 +225,18 @@ Filter =
MD5: (post) -> post.files.map((f) -> f.MD5)
values: (key, post) ->
if key of Filter.valueF
if $.hasOwn(Filter.valueF, key)
Filter.valueF[key](post).filter((v) -> v?)
else
[key.split('+').map((k) ->
if (f=Filter.valueF[k])
if (f = $.getOwn(Filter.valueF, k))
f(post).map((v) -> v or '').join('\n')
else
''
).join('\n')]
addFilter: (type, re, cb) ->
return unless $.hasOwn(Config.filter, type)
$.get type, Conf[type], (item) ->
save = item[type]
# Add a new line before the regexp unless the text is empty.

View File

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

View File

@ -15,7 +15,7 @@ ThreadHiding =
return unless $.hasStorage and g.SITE.software is 'yotsuba'
hiddenThreads = ThreadHiding.db.get
boardID: board.ID
defaultValue: {}
defaultValue: $.dict()
hiddenThreads[threadID] = true for threadID of hiddenThreads
localStorage.setItem "4chan-hide-t-#{board}", JSON.stringify hiddenThreads
@ -32,12 +32,12 @@ ThreadHiding =
catalogSave: ->
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
boardID: g.BOARD.ID
threadID: threadID
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
boardID: g.BOARD.ID
threadID: threadID
@ -176,7 +176,7 @@ ThreadHiding =
toggle: (thread) ->
unless thread instanceof Thread
thread = g.threads[@dataset.fullID]
thread = g.threads.get(@dataset.fullID)
if thread.isHidden
ThreadHiding.show thread
else

View File

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

View File

@ -13,14 +13,14 @@ Get =
threadFromRoot: (root) ->
return null unless root?
{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) ->
Get.threadFromRoot $.x "ancestor-or-self::#{g.SITE.xpath.thread}", node
postFromRoot: (root) ->
return null unless root?
post = g.posts[root.dataset.fullID]
post = g.posts.get(root.dataset.fullID)
index = root.dataset.clone
if index then post.clones[index] else post
if index then post.clones[+index] else post
postFromNode: (root) ->
Get.postFromRoot $.x "ancestor-or-self::#{g.SITE.xpath.postContainer}[1]", root
postDataFromLink: (link) ->
@ -59,7 +59,7 @@ Get =
# and their clones,
# get all of their 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:
# Filter out irrelevant quotelinks.

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ Gallery =
Gallery.images = []
nodes = Gallery.nodes = {}
Gallery.fileIDs = {}
Gallery.fileIDs = $.dict()
Gallery.slideshow = false
nodes.el = dialog = $.el 'div',
@ -133,7 +133,7 @@ Gallery =
load: (thumb, errorCB) ->
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
$.extend file.dataset, thumb.dataset
$.on file, 'error', errorCB
@ -185,7 +185,7 @@ Gallery =
Gallery.cb.stop()
# 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
# Preload next image
@ -196,11 +196,11 @@ Gallery =
if @error?.code is MediaError.MEDIA_ERR_DECODE
return new Notice 'error', 'Corrupt or unplayable video', 30
return if ImageCommon.isFromArchive @
post = g.posts[@dataset.post]
file = post.files[@dataset.file]
post = g.posts.get(@dataset.post)
file = post.files[+@dataset.file]
ImageCommon.error @, post, file, null, (url) =>
return unless url
Gallery.images[@dataset.id].href = url
Gallery.images[+@dataset.id].href = url
(@src = url if Gallery.nodes.current is @)
cacheError: ->
@ -341,7 +341,7 @@ Gallery =
{current, frame} = Gallery.nodes
{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'
containerWidth = frame.clientWidth
containerHeight = doc.clientHeight - 25

View File

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

View File

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

View File

@ -1,7 +1,7 @@
Embedding =
init: ->
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
if Conf['Embedding'] and g.VIEW isnt 'archive'

View File

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

View File

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

View File

@ -81,12 +81,12 @@ CatalogLinks =
return
externalParse: ->
CatalogLinks.externalList = {}
CatalogLinks.externalList = $.dict()
for line in Conf['externalCatalogURLs'].split '\n'
continue if line[0] is '#'
url = line.split(';')[0]
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
unless excludes[board] or excludes[board.split('/')[0] + '/*']
CatalogLinks.externalList[board] = url

View File

@ -1,5 +1,5 @@
ExpandThread =
statuses: {}
statuses: $.dict()
init: ->
return if not (g.VIEW is 'index' and Conf['Thread Expansion'])
if Conf['JSON Index']
@ -96,7 +96,7 @@ ExpandThread =
filesCount = 0
for postData in req.response.posts
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
{root} = post.nodes
postsRoot.push root

View File

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

View File

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

View File

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

View File

@ -6,11 +6,11 @@ ModContact =
cb: @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'
$.extend links, ModContact.template(@info.capcode)
$.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'
$.extend moveNote, ModContact.moveNote[moved[1]]
$.add @nodes.post, moveNote

View File

@ -39,7 +39,7 @@ Nav =
Nav.scroll +1
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'
for threadRoot in $$ g.SITE.selectors.thread
thread = Get.threadFromRoot threadRoot

View File

@ -126,6 +126,6 @@ RelativeDates =
markStale: (data) ->
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.
RelativeDates.stale.push data

View File

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

View File

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

View File

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

View File

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

View File

@ -55,7 +55,7 @@ ThreadStats =
{posts} = ThreadStats.thread
n = posts.keys.length
for i in [ThreadStats.postIndex...n] by 1
post = posts[posts.keys[i]]
post = posts.get(posts.keys[i])
unless post.isFetchedQuote
ThreadStats.postCount++
ThreadStats.fileCount += post.files.length
@ -132,7 +132,7 @@ ThreadStats =
ThreadStats.showPage and
ThreadStats.pageCountEl.textContent isnt '1' 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
ThreadStats.timeout = setTimeout ThreadStats.fetchPage, 5 * $.SECOND

View File

@ -266,7 +266,7 @@ ThreadUpdater =
# XXX Reject updates that falsely delete the last post.
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
thread.setStatus 'Archived', !!OP.archived
@ -291,7 +291,7 @@ ThreadUpdater =
continue if ID <= lastPost
# 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()
continue
@ -304,14 +304,14 @@ ThreadUpdater =
# Check for deleted posts.
deletedPosts = []
for ID in ThreadUpdater.postIDs when ID not in index
thread.posts[ID].kill()
thread.posts.get(ID).kill()
deletedPosts.push "#{board}.#{ID}"
ThreadUpdater.postIDs = index
# Check for deleted files.
deletedFiles = []
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}"
ThreadUpdater.fileIDs = files

View File

@ -150,7 +150,7 @@ ThreadWatcher =
if Conf['Auto Watch']
ThreadWatcher.addRaw boardID, threadID, {}, cb
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) ->
{db} = ThreadWatcher
siteID = g.SITE.ID
@ -169,7 +169,7 @@ ThreadWatcher =
nKilled++
ThreadWatcher.refresh() if nKilled
onThreadRefresh: (e) ->
thread = g.threads[e.detail.threadID]
thread = g.threads.get(e.detail.threadID)
return unless e.detail[404] and ThreadWatcher.isWatched thread
# Update dead status.
ThreadWatcher.add thread
@ -215,7 +215,7 @@ ThreadWatcher =
ThreadWatcher.clearRequests()
initLastModified: ->
lm = ($.lastModified['ThreadWatcher'] or= {})
lm = ($.lastModified['ThreadWatcher'] or= $.dict())
for siteID, boards of ThreadWatcher.dbLM.data
for boardID, data of boards.boards
if ThreadWatcher.db.get {siteID, boardID}
@ -287,7 +287,7 @@ ThreadWatcher =
{siteID, boardID} = board[0]
lmDate = @getResponseHeader('Last-Modified')
ThreadWatcher.dbLM.extend {siteID, boardID, val: $.item(url, lmDate)}
threads = {}
threads = $.dict()
pageLength = 0
nThreads = 0
oldest = null
@ -449,7 +449,7 @@ ThreadWatcher =
div
setPrefixes: (threads) ->
prefixes = {}
prefixes = $.dict()
for {siteID} in threads
continue if siteID of prefixes
len = 0
@ -474,7 +474,7 @@ ThreadWatcher =
ThreadWatcher.setPrefixes threads
for {siteID, boardID, threadID, data} in threads
# 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}}
nodes.push ThreadWatcher.makeLine siteID, boardID, threadID, data
{list} = ThreadWatcher
@ -554,7 +554,7 @@ ThreadWatcher =
ThreadWatcher.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.modified
$.extend oldData, data
@ -594,7 +594,7 @@ ThreadWatcher =
$.rmClass entryEl, rmClass
entryEl.textContent = text
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: ->
entries = []

View File

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

View File

@ -1,7 +1,7 @@
UnreadIndex =
lastReadPost: {}
hr: {}
markReadLink: {}
lastReadPost: $.dict()
hr: $.dict()
markReadLink: $.dict()
init: ->
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) ->
return if e.detail.isCatalog
for threadID in e.detail.threadIDs
thread = g.threads[threadID]
thread = g.threads.get(threadID)
UnreadIndex.update thread
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)])
img.click()
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)
if n < x < last
x = if dx is last then n else last

View File

@ -112,7 +112,7 @@ QR =
statusCheck: ->
return unless QR.nodes
{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()
else
QR.status()
@ -258,7 +258,7 @@ QR =
status: ->
return unless QR.nodes
{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'
disabled = true
QR.cooldown.auto = false
@ -402,7 +402,7 @@ QR =
if file
{type} = file
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.handleFiles [blob]
$.addClass QR.nodes.el, 'dump'
@ -651,7 +651,7 @@ QR =
post = QR.posts[0]
post.forceSave()
threadID = post.thread
thread = g.BOARD.threads[threadID]
thread = g.BOARD.threads.get(threadID)
if g.BOARD.ID is 'f' and threadID is 'new'
filetag = QR.nodes.flashTag.value
@ -662,7 +662,7 @@ QR =
err = 'New threads require a subject.'
else unless !!g.BOARD.config.text_only or post.file
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.'
else unless post.com or post.file
err = 'No comment or file.'

View File

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

View File

@ -122,6 +122,7 @@ QR.post = class
@spoiler = input.checked
return
{name} = input.dataset
return unless name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag']
prev = @[name]
@[name] = input.value or input.dataset.default or null
switch name
@ -340,7 +341,7 @@ QR.post = class
@file.newName = (@filename or '').replace /[/\\]/g, '-'
unless QR.validExtension.test @filename
# 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: ->
long = "#{@filename} (#{@filesize})"

View File

@ -10,7 +10,7 @@ QuoteBacklink =
# Second callback adds relevant containers into posts.
# This is is so that fetched posts can get their backlinks,
# and that as much backlinks are appended in the background as possible.
containers: {}
containers: $.dict()
init: ->
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
for quote in @quotes
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,
# as the clones won't have the backlink containers.
for clone in post.clones

View File

@ -33,7 +33,7 @@ QuoteInline =
return if $.modifiedClick e
{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')
e.preventDefault()
@ -66,7 +66,7 @@ QuoteInline =
new Fetcher boardID, threadID, postID, inline, quoter
return if not (
(post = g.posts["#{boardID}.#{postID}"]) and
(post = g.posts.get("#{boardID}.#{postID}")) and
context.thread is post.thread
)
@ -98,13 +98,13 @@ QuoteInline =
return if not (el = root.firstElementChild)
# Dereference clone.
post = g.posts["#{boardID}.#{postID}"]
post = g.posts.get("#{boardID}.#{postID}")
post.rmClone el.dataset.clone
# Decrease forward count and unhide.
if Conf['Forward Hiding'] and
isBacklink and
context.thread is g.threads["#{boardID}.#{threadID}"] and
context.thread is g.threads.get("#{boardID}.#{threadID}") and
not --post.forwarded
delete post.forwarded
$.rmClass post.nodes.root, 'forwarded'

View File

@ -40,7 +40,7 @@ QuotePreview =
endEvents: 'mouseout click'
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
# Remove the clone that's in the qp from the array.
posts.pop()

View File

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

View File

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

View File

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

View File

@ -9,4 +9,4 @@ class CatalogThreadNative
@boardID = @nodes.thumb.parentNode.pathname.split(/\/+/)[1]
@board = g.boards[@boardID] or new Board(@boardID)
@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
e.data[...g.NAMESPACE.length] is g.NAMESPACE
data = JSON.parse e.data[g.NAMESPACE.length..]
for type, value of data
@cb[type]? value
for type, value of data when $.hasOwn(@cb, type)
@cb[type] value
return

View File

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

View File

@ -1,11 +1,11 @@
class Fetcher
constructor: (@boardID, @threadID, @postID, @root, @quoter) ->
if post = g.posts["#{@boardID}.#{@postID}"]
if post = g.posts.get("#{@boardID}.#{@postID}")
@insert post
return
# 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]
post = new Post g.SITE.Build.postFromObject(post, @boardID), thread, board, {isFetchedQuote: true}
Main.callbackNodes 'Post', [post]
@ -53,7 +53,7 @@ class Fetcher
fetchedPost: (req, isCached) ->
# In case of multiple callbacks for the same request,
# 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
return
@ -96,7 +96,7 @@ class Fetcher
board = g.boards[@boardID] or
new Board @boardID
thread = g.threads["#{@boardID}.#{@threadID}"] or
thread = g.threads.get("#{@boardID}.#{@threadID}") or
new Thread @threadID, board
post = new Post g.SITE.Build.postFromObject(post, @boardID), thread, board, {isFetchedQuote: true}
Main.callbackNodes 'Post', [post]
@ -115,7 +115,7 @@ class Fetcher
for key of media when /_link$/.test key
# 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.
delete media[key] unless media[key]?.match /^http:\/\//
delete media[key] unless $.getOwn(media, key)?.match /^http:\/\//
that.parseArchivedPost @response, url, archive
return true
return false
@ -123,7 +123,7 @@ class Fetcher
parseArchivedPost: (data, url, archive) ->
# In case of multiple callbacks for the same request,
# 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
return
@ -210,7 +210,7 @@ class Fetcher
board = g.boards[@boardID] or
new Board @boardID
thread = g.threads["#{@boardID}.#{@threadID}"] or
thread = g.threads.get("#{@boardID}.#{@threadID}") or
new Thread @threadID, board
post = new Post g.SITE.Build.post(o), thread, board, {isFetchedQuote: true}
post.kill()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ eventPageRequest = do ->
<% } %>
CrossOrigin =
binary: (url, cb, headers={}) ->
binary: (url, cb, headers=$.dict()) ->
# 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/'
<% if (type === 'crx') { %>
@ -73,7 +73,7 @@ CrossOrigin =
name = match.replace /\\"/g, '"'
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.
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.name = name
cb blob
@ -85,13 +85,13 @@ CrossOrigin =
responseHeaderString: null
getResponseHeader: (headerName) ->
if !@responseHeaders? and @responseHeaderString?
@responseHeaders = {}
@responseHeaders = $.dict()
for header in @responseHeaderString.split('\r\n')
if (i = header.indexOf(':')) >= 0
key = header[...i].trim().toLowerCase()
val = header[i+1..].trim()
@responseHeaders[key] = val
(@responseHeaders or {})[headerName.toLowerCase()] ? null
@responseHeaders?[headerName.toLowerCase()] ? null
abort: ->
onloadend: ->

View File

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

View File

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

View File

@ -5,7 +5,7 @@
?{email}{<a href="mailto:${encodeURIComponent(email).replace(/%40/g, "@")}" class="useremail">}
<span class="name?{capcode}{ capcode}">${name}</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>}
?{capcode}{ <strong class="capcode hand id_${capcodeLC}" title="Highlight posts by ${capcodePlural}">## ${capcode}</strong>}
?{email}{</a>}
@ -19,7 +19,7 @@
<span class="postNum?{!(boardID === "f" && !o.isReply)}{ desktop}">
<a href="${postLink}" title="Link to this post">No.</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.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">}

View File

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