Move Build to SW.yotsuba.Build.

This commit is contained in:
ccd0 2019-04-09 06:49:13 -07:00
parent 794027e355
commit a214f51755
20 changed files with 55 additions and 43 deletions

View File

@ -178,7 +178,7 @@ Filter =
pass: (post) -> post.info.pass pass: (post) -> post.info.pass
email: (post) -> post.info.email email: (post) -> post.info.email
subject: (post) -> post.info.subject or (if post.isReply then undefined else '') subject: (post) -> post.info.subject or (if post.isReply then undefined else '')
comment: (post) -> (post.info.comment ?= Build.parseComment post.info.commentHTML.innerHTML) comment: (post) -> (post.info.comment ?= g.sites[post.siteID]?.Build?.parseComment?(post.info.commentHTML.innerHTML))
flag: (post) -> post.info.flag flag: (post) -> post.info.flag
filename: (post) -> post.file?.name filename: (post) -> post.file?.name
dimensions: (post) -> post.file?.dimensions dimensions: (post) -> post.file?.dimensions

View File

@ -141,13 +141,13 @@ Index =
d.title = d.title.replace /\ -\ Page\ \d+/, '' d.title = d.title.replace /\ -\ Page\ \d+/, ''
$.onExists doc, '.board > .thread > .postContainer, .board + *', -> $.onExists doc, '.board > .thread > .postContainer, .board + *', ->
Build.hat = $ '.board > .thread > img:first-child' g.SITE.Build.hat = $ '.board > .thread > img:first-child'
if Build.hat if g.SITE.Build.hat
g.BOARD.threads.forEach (thread) -> g.BOARD.threads.forEach (thread) ->
if thread.nodes.root if thread.nodes.root
$.prepend thread.nodes.root, Build.hat.cloneNode false $.prepend thread.nodes.root, g.SITE.Build.hat.cloneNode false
$.addClass doc, 'hats-enabled' $.addClass doc, 'hats-enabled'
$.addStyle ".catalog-thread::after {background-image: url(#{Build.hat.src});}" $.addStyle ".catalog-thread::after {background-image: url(#{g.SITE.Build.hat.src});}"
board = $ '.board' board = $ '.board'
$.replace board, Index.root $.replace board, Index.root
@ -663,7 +663,7 @@ Index =
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
Index.parsedThreads[data.no] = obj = Build.parseJSON data, g.BOARD.ID Index.parsedThreads[data.no] = obj = g.SITE.Build.parseJSON data, g.BOARD.ID
obj.filterResults = results = Filter.test obj obj.filterResults = results = Filter.test obj
obj.isOnTop = results.top obj.isOnTop = results.top
obj.isHidden = results.hide or ThreadHiding.isHidden(obj.boardID, obj.threadID) obj.isHidden = results.hide or ThreadHiding.isHidden(obj.boardID, obj.threadID)
@ -671,7 +671,7 @@ Index =
for reply in data.last_replies for reply in data.last_replies
Index.replyData["#{g.BOARD}.#{reply.no}"] = reply Index.replyData["#{g.BOARD}.#{reply.no}"] = reply
if Index.liveThreadData[0] if Index.liveThreadData[0]
Build.spoilerRange[g.BOARD.ID] = Index.liveThreadData[0].custom_spoiler g.SITE.Build.spoilerRange[g.BOARD.ID] = Index.liveThreadData[0].custom_spoiler
g.BOARD.threads.forEach (thread) -> g.BOARD.threads.forEach (thread) ->
(thread.collect() unless thread.ID in Index.liveThreadIDs) (thread.collect() unless thread.ID in Index.liveThreadIDs)
$.event 'IndexUpdate', $.event 'IndexUpdate',
@ -685,7 +685,7 @@ Index =
Index.parsedThreads[threadID].isHidden Index.parsedThreads[threadID].isHidden
isHiddenReply: (threadID, replyData) -> isHiddenReply: (threadID, replyData) ->
PostHiding.isHidden(g.BOARD.ID, threadID, replyData.no) or Filter.isHidden(Build.parseJSON replyData, g.BOARD.ID) PostHiding.isHidden(g.BOARD.ID, threadID, replyData.no) or Filter.isHidden(g.SITE.Build.parseJSON replyData, g.BOARD.ID)
lastPost: (threadID) -> lastPost: (threadID) ->
threadData = Index.liveThreadDict[threadID] threadData = Index.liveThreadDict[threadID]
@ -720,12 +720,12 @@ Index =
thread.setPage(Index.threadPosition[ID] // Index.threadsNumPerPage + 1) thread.setPage(Index.threadPosition[ID] // Index.threadsNumPerPage + 1)
else else
obj = Index.parsedThreads[ID] obj = Index.parsedThreads[ID]
OP = new Post Build.post(obj), thread, g.BOARD OP = new Post g.SITE.Build.post(obj), thread, g.BOARD
OP.filterResults = obj.filterResults OP.filterResults = obj.filterResults
newPosts.push OP newPosts.push OP
unless isCatalog and thread.nodes.root unless isCatalog and thread.nodes.root
Build.thread thread, threadData, withReplies g.SITE.Build.thread thread, threadData, withReplies
catch err catch err
# Skip posts that we failed to parse. # Skip posts that we failed to parse.
errors = [] unless errors errors = [] unless errors
@ -753,7 +753,7 @@ Index =
if (post = thread.posts[data.no]) and not post.isFetchedQuote if (post = thread.posts[data.no]) and not post.isFetchedQuote
nodes.push post.nodes.root nodes.push post.nodes.root
continue continue
nodes.push node = Build.postFromObject data, thread.board.ID nodes.push node = g.SITE.Build.postFromObject data, thread.board.ID
try try
posts.push new Post node, thread, thread.board posts.push new Post node, thread, thread.board
catch err catch err
@ -772,7 +772,7 @@ Index =
for thread in threads when !thread.catalogView for thread in threads when !thread.catalogView
{ID} = thread {ID} = thread
page = Index.threadPosition[ID] // Index.threadsNumPerPage + 1 page = Index.threadPosition[ID] // Index.threadsNumPerPage + 1
root = Build.catalogThread thread, Index.liveThreadDict[ID], page root = g.SITE.Build.catalogThread thread, Index.liveThreadDict[ID], page
catalogThreads.push new CatalogThread root, thread catalogThreads.push new CatalogThread root, thread
Main.callbackNodes 'CatalogThread', catalogThreads Main.callbackNodes 'CatalogThread', catalogThreads
return return
@ -796,7 +796,7 @@ Index =
replies = [] replies = []
for data in lastReplies for data in lastReplies
continue if Index.isHiddenReply thread.ID, data continue if Index.isHiddenReply thread.ID, data
reply = Build.catalogReply thread, data reply = g.SITE.Build.catalogReply thread, data
RelativeDates.update $('time', reply) RelativeDates.update $('time', reply)
$.on $('.catalog-reply-preview', reply), 'mouseover', QuotePreview.mouseover $.on $('.catalog-reply-preview', reply), 'mouseover', QuotePreview.mouseover
replies.push reply replies.push reply
@ -820,7 +820,7 @@ Index =
lastlong = (thread) -> lastlong = (thread) ->
for r, i in (thread.last_replies or []) by -1 for r, i in (thread.last_replies or []) by -1
continue if Index.isHiddenReply thread.no, r continue if Index.isHiddenReply thread.no, r
len = if r.com then Build.parseComment(r.com).replace(/[^a-z]/ig, '').length else 0 len = if r.com then g.SITE.Build.parseComment(r.com).replace(/[^a-z]/ig, '').length else 0
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
@ -949,7 +949,7 @@ Index =
searchMatch: (obj, keywords) -> searchMatch: (obj, keywords) ->
{info, file} = obj {info, file} = obj
info.comment ?= Build.parseComment info.commentHTML.innerHTML info.comment ?= g.SITE.Build.parseComment info.commentHTML.innerHTML
text = [] text = []
for key in ['comment', 'subject', 'name', 'tripcode'] for key in ['comment', 'subject', 'name', 'tripcode']
text.push info[key] if key of info text.push info[key] if key of info

View File

@ -68,12 +68,12 @@ Test =
$.cache g.SITE.urls.threadJSON({boardID: post.boardID, threadID: post.threadID}), -> $.cache g.SITE.urls.threadJSON({boardID: post.boardID, threadID: post.threadID}), ->
return unless @response return unless @response
{posts} = @response {posts} = @response
Build.spoilerRange[post.board.ID] = posts[0].custom_spoiler g.SITE.Build.spoilerRange[post.board.ID] = posts[0].custom_spoiler
for postData in posts for postData in posts
if postData.no is post.ID if postData.no is post.ID
t1 = new Date().getTime() t1 = new Date().getTime()
obj = Build.parseJSON postData, post.board.ID obj = g.SITE.Build.parseJSON postData, post.board.ID
root = Build.post obj root = g.SITE.Build.post obj
t2 = new Date().getTime() t2 = new Date().getTime()
Test.time += t2 - t1 Test.time += t2 - t1
post2 = new Post root, post.thread, post.board, 'forBuildTest' post2 = new Post root, post.thread, post.board, 'forBuildTest'

View File

@ -40,7 +40,7 @@ ExpandComment =
posts = req.response.posts posts = req.response.posts
if spoilerRange = posts[0].custom_spoiler if spoilerRange = posts[0].custom_spoiler
Build.spoilerRange[g.BOARD] = spoilerRange g.SITE.Build.spoilerRange[g.BOARD] = spoilerRange
for postObj in posts for postObj in posts
break if postObj.no is post.ID break if postObj.no is post.ID

View File

@ -11,7 +11,7 @@ ExpandThread =
setButton: (thread) -> setButton: (thread) ->
return if not (thread.nodes.root and (a = $ '.summary', thread.nodes.root)) return if not (thread.nodes.root and (a = $ '.summary', thread.nodes.root))
a.textContent = Build.summaryText '+', a.textContent.match(/\d+/g)... a.textContent = g.SITE.Build.summaryText '+', a.textContent.match(/\d+/g)...
a.style.cursor = 'pointer' a.style.cursor = 'pointer'
$.on a, 'click', ExpandThread.cbToggle $.on a, 'click', ExpandThread.cbToggle
@ -53,7 +53,7 @@ ExpandThread =
expand: (thread, a) -> expand: (thread, a) ->
ExpandThread.statuses[thread] = status = {} ExpandThread.statuses[thread] = status = {}
a.textContent = Build.summaryText '...', a.textContent.match(/\d+/g)... a.textContent = g.SITE.Build.summaryText '...', a.textContent.match(/\d+/g)...
status.req = $.cache g.SITE.urls.threadJSON({boardID: thread.board.ID, threadID: thread.ID}), -> status.req = $.cache g.SITE.urls.threadJSON({boardID: thread.board.ID, threadID: thread.ID}), ->
return if @ isnt status.req # aborted return if @ isnt status.req # aborted
delete status.req delete status.req
@ -65,7 +65,7 @@ ExpandThread =
if (oldReq = status.req) if (oldReq = status.req)
delete status.req delete status.req
oldReq.abort() oldReq.abort()
a.textContent = Build.summaryText '+', a.textContent.match(/\d+/g)... if a a.textContent = g.SITE.Build.summaryText '+', a.textContent.match(/\d+/g)... if a
return return
replies = $$ '.thread > .replyContainer', threadRoot replies = $$ '.thread > .replyContainer', threadRoot
@ -88,7 +88,7 @@ ExpandThread =
$.rm reply $.rm reply
if Index.enabled # otherwise handled by Main.addPosts if Index.enabled # otherwise handled by Main.addPosts
$.event 'PostsRemoved', null, a.parentNode $.event 'PostsRemoved', null, a.parentNode
a.textContent = Build.summaryText '+', postsCount, filesCount a.textContent = g.SITE.Build.summaryText '+', postsCount, filesCount
$.rm $('.summary-bottom', threadRoot) $.rm $('.summary-bottom', threadRoot)
parse: (req, thread, a) -> parse: (req, thread, a) ->
@ -96,7 +96,7 @@ ExpandThread =
a.textContent = if req.status then "Error #{req.statusText} (#{req.status})" else 'Connection Error' a.textContent = if req.status then "Error #{req.statusText} (#{req.status})" else 'Connection Error'
return return
Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler g.SITE.Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler
posts = [] posts = []
postsRoot = [] postsRoot = []
@ -108,7 +108,7 @@ ExpandThread =
{root} = post.nodes {root} = post.nodes
postsRoot.push root postsRoot.push root
continue continue
root = Build.postFromObject postData, thread.board.ID root = g.SITE.Build.postFromObject postData, thread.board.ID
post = new Post root, thread, thread.board post = new Post root, thread, thread.board
filesCount++ if 'file' of post filesCount++ if 'file' of post
posts.push post posts.push post
@ -118,7 +118,7 @@ ExpandThread =
$.event 'PostsInserted', null, a.parentNode $.event 'PostsInserted', null, a.parentNode
postsCount = postsRoot.length postsCount = postsRoot.length
a.textContent = Build.summaryText '-', postsCount, filesCount a.textContent = g.SITE.Build.summaryText '-', postsCount, filesCount
if root if root
a2 = a.cloneNode true a2 = a.cloneNode true

View File

@ -45,7 +45,7 @@ FileInfo =
L: -> <%= html('<a href="${this.file.url}" target="_blank">&{FileInfo.formatters.N.call(this)}</a>') %> L: -> <%= html('<a href="${this.file.url}" target="_blank">&{FileInfo.formatters.N.call(this)}</a>') %>
n: -> n: ->
fullname = @file.name fullname = @file.name
shortname = Build.shortFilename @file.name, @isReply shortname = g.SITE.Build.shortFilename @file.name, @isReply
if fullname is shortname if fullname is shortname
<%= html('${fullname}') %> <%= html('${fullname}') %>
else else

View File

@ -125,9 +125,9 @@ ReplyPruning =
$.event 'PostsInserted', null, ReplyPruning.summary.parentNode $.event 'PostsInserted', null, ReplyPruning.summary.parentNode
ReplyPruning.summary.textContent = if ReplyPruning.active ReplyPruning.summary.textContent = if ReplyPruning.active
Build.summaryText '+', ReplyPruning.hidden, ReplyPruning.hiddenFiles g.SITE.Build.summaryText '+', ReplyPruning.hidden, ReplyPruning.hiddenFiles
else else
Build.summaryText '-', ReplyPruning.total, ReplyPruning.totalFiles g.SITE.Build.summaryText '-', ReplyPruning.total, ReplyPruning.totalFiles
ReplyPruning.summary.hidden = (ReplyPruning.total <= +Conf["Max Replies"]) ReplyPruning.summary.hidden = (ReplyPruning.total <= +Conf["Max Replies"])
# Maintain position in thread when posts are added/removed above # Maintain position in thread when posts are added/removed above

View File

@ -267,7 +267,7 @@ ThreadUpdater =
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[lastPost].info.date < 30 * $.SECOND
Build.spoilerRange[board] = OP.custom_spoiler g.SITE.Build.spoilerRange[board] = OP.custom_spoiler
thread.setStatus 'Archived', !!OP.archived thread.setStatus 'Archived', !!OP.archived
ThreadUpdater.updateThreadStatus 'Sticky', !!OP.sticky ThreadUpdater.updateThreadStatus 'Sticky', !!OP.sticky
ThreadUpdater.updateThreadStatus 'Closed', !!OP.closed ThreadUpdater.updateThreadStatus 'Closed', !!OP.closed
@ -295,7 +295,7 @@ ThreadUpdater =
continue continue
newPosts.push "#{board}.#{ID}" newPosts.push "#{board}.#{ID}"
node = Build.postFromObject postObject, board.ID node = g.SITE.Build.postFromObject postObject, board.ID
posts.push new Post node, thread, board posts.push new Post node, thread, board
# Fetching your own posts after posting # Fetching your own posts after posting
delete ThreadUpdater.postID if ThreadUpdater.postID is ID delete ThreadUpdater.postID if ThreadUpdater.postID is ID

View File

@ -325,6 +325,7 @@ ThreadWatcher =
ThreadWatcher.fetch url, {siteID, force}, [thread], ThreadWatcher.parseStatus ThreadWatcher.fetch url, {siteID, force}, [thread], ThreadWatcher.parseStatus
parseStatus: ({siteID, boardID, threadID, data}) -> parseStatus: ({siteID, boardID, threadID, data}) ->
site = g.sites[siteID]
if @status is 200 and @response if @status is 200 and @response
last = @response.posts[@response.posts.length-1].no last = @response.posts[@response.posts.length-1].no
replies = @response.posts.length-1 replies = @response.posts.length-1
@ -346,14 +347,14 @@ ThreadWatcher =
unread++ unread++
if !quotingYou and !Conf['Require OP Quote Link'] and youOP and not Filter.isHidden(Build.parseJSON postObj, boardID, siteID) if !quotingYou and !Conf['Require OP Quote Link'] and youOP and not Filter.isHidden(site.Build.parseJSON postObj, boardID, siteID)
quotingYou = true quotingYou = true
continue continue
continue unless !quotingYou and QuoteYou.db and postObj.com continue unless !quotingYou and QuoteYou.db and postObj.com
quotesYou = false quotesYou = false
regexp = g.sites[siteID].regexp.quotelinkHTML regexp = site.regexp.quotelinkHTML
regexp.lastIndex = 0 regexp.lastIndex = 0
while match = regexp.exec postObj.com while match = regexp.exec postObj.com
if QuoteYou.db.get { if QuoteYou.db.get {
@ -364,13 +365,13 @@ ThreadWatcher =
} }
quotesYou = true quotesYou = true
break break
if quotesYou and not Filter.isHidden(Build.parseJSON postObj, boardID, siteID) if quotesYou and not Filter.isHidden(site.Build.parseJSON postObj, boardID, siteID)
quotingYou = true quotingYou = true
ThreadWatcher.update siteID, boardID, threadID, {last, replies, isDead, unread, quotingYou} ThreadWatcher.update siteID, boardID, threadID, {last, replies, isDead, unread, quotingYou}
else if @status is 404 else if @status is 404
if g.sites[siteID].mayLackJSON and !data.last? if site.mayLackJSON and !data.last?
ThreadWatcher.update siteID, boardID, threadID, {last: -1} ThreadWatcher.update siteID, boardID, threadID, {last: -1}
else else
ThreadWatcher.update siteID, boardID, threadID, {isDead: true} ThreadWatcher.update siteID, boardID, threadID, {isDead: true}

View File

@ -29,7 +29,7 @@ QuoteBacklink =
return if @isClone or !@quotes.length or @isRebuilt return if @isClone or !@quotes.length or @isRebuilt
markYours = Conf['Mark Quotes of You'] and QuoteYou.isYou(@) markYours = Conf['Mark Quotes of You'] and QuoteYou.isYou(@)
a = $.el 'a', a = $.el 'a',
href: Build.postURL @board.ID, @thread.ID, @ID href: g.SITE.Build.postURL @board.ID, @thread.ID, @ID
className: if @isHidden then 'filtered backlink' else 'backlink' className: if @isHidden then 'filtered backlink' else 'backlink'
textContent: Conf['backlink'].replace(/%(?:id|%)/g, (x) => ({'%id': @ID, '%%': '%'})[x]) textContent: Conf['backlink'].replace(/%(?:id|%)/g, (x) => ({'%id': @ID, '%%': '%'})[x])
$.add a, QuoteYou.mark.cloneNode(true) if markYours $.add a, QuoteYou.mark.cloneNode(true) if markYours

View File

@ -59,13 +59,13 @@ Quotify =
# 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.
a = $.el 'a', a = $.el 'a',
href: Build.postURL boardID, post.thread.ID, postID href: g.SITE.Build.postURL boardID, post.thread.ID, postID
className: 'quotelink' className: 'quotelink'
textContent: quote textContent: quote
else else
# Replace the .deadlink span if we can redirect. # Replace the .deadlink span if we can redirect.
a = $.el 'a', a = $.el 'a',
href: Build.postURL boardID, post.thread.ID, postID href: g.SITE.Build.postURL boardID, post.thread.ID, postID
className: 'quotelink deadlink' className: 'quotelink deadlink'
textContent: quote textContent: quote
$.add a, Post.deadMark.cloneNode(true) $.add a, Post.deadMark.cloneNode(true)

View File

@ -7,7 +7,7 @@ class Fetcher
# 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["#{@boardID}.#{@threadID}"])
board = g.boards[@boardID] board = g.boards[@boardID]
post = new Post Build.postFromObject(post, @boardID), thread, board post = new Post g.SITE.Build.postFromObject(post, @boardID), thread, board
post.isFetchedQuote = true post.isFetchedQuote = true
Main.callbackNodes 'Post', [post] Main.callbackNodes 'Post', [post]
@insert post @insert post
@ -74,7 +74,7 @@ class Fetcher
return return
{posts} = req.response {posts} = req.response
Build.spoilerRange[@boardID] = posts[0].custom_spoiler g.SITE.Build.spoilerRange[@boardID] = posts[0].custom_spoiler
for post in posts for post in posts
break if post.no is @postID # we found it! break if post.no is @postID # we found it!
@ -99,7 +99,7 @@ class Fetcher
new Board @boardID new Board @boardID
thread = g.threads["#{@boardID}.#{@threadID}"] or thread = g.threads["#{@boardID}.#{@threadID}"] or
new Thread @threadID, board new Thread @threadID, board
post = new Post Build.postFromObject(post, @boardID), thread, board post = new Post g.SITE.Build.postFromObject(post, @boardID), thread, board
post.isFetchedQuote = true post.isFetchedQuote = true
Main.callbackNodes 'Post', [post] Main.callbackNodes 'Post', [post]
@insert post @insert post
@ -214,7 +214,7 @@ class Fetcher
new Board @boardID new Board @boardID
thread = g.threads["#{@boardID}.#{@threadID}"] or thread = g.threads["#{@boardID}.#{@threadID}"] or
new Thread @threadID, board new Thread @threadID, board
post = new Post Build.post(o), thread, board post = new Post g.SITE.Build.post(o), thread, board
post.kill() post.kill()
post.file.thumbURL = o.file.thumbURL if post.file post.file.thumbURL = o.file.thumbURL if post.file
post.isFetchedQuote = true post.isFetchedQuote = true

View File

@ -58,7 +58,7 @@ class Thread
$.rm $ ".#{typeLC}Icon", @catalogView.nodes.icons if @catalogView $.rm $ ".#{typeLC}Icon", @catalogView.nodes.icons if @catalogView
return return
icon = $.el 'img', icon = $.el 'img',
src: "#{Build.staticPath}#{typeLC}#{Build.gifIcon}" src: "#{g.SITE.Build.staticPath}#{typeLC}#{g.SITE.Build.gifIcon}"
alt: type alt: type
title: type title: type
className: "#{typeLC}Icon retina" className: "#{typeLC}Icon retina"

View File

@ -117,6 +117,15 @@ SW.tinyboard =
quotelinkHTML: quotelinkHTML:
/<a [^>]*\bhref="[^"]*\/([^\/]+)\/res\/(\d+)\.\w+#(\d+)"/g /<a [^>]*\bhref="[^"]*\/([^\/]+)\/res\/(\d+)\.\w+#(\d+)"/g
Build:
parseJSON: ->
SW.yotsuba.Build.parseJSON.apply SW.yotsuba.Build, arguments
parseComment: (html) ->
html = html
.replace(/<br\b[^<]*>/gi, '\n')
.replace(/<[^>]*>/g, '')
$.unescape html
bgColoredEl: -> bgColoredEl: ->
$.el 'div', className: 'post reply' $.el 'div', className: 'post reply'

View File

@ -256,3 +256,5 @@ Build =
link = Build.postURL thread.board.ID, thread.ID, data.no link = Build.postURL thread.board.ID, thread.ID, data.no
$.el 'div', {className: 'catalog-reply'}, $.el 'div', {className: 'catalog-reply'},
<%= readHTML('CatalogReply.html') %> <%= readHTML('CatalogReply.html') %>
SW.yotsuba.Build = Build