From 66f3bec43b0eee861944eb0a39295caa56b60d9a Mon Sep 17 00:00:00 2001 From: queue Date: Mon, 15 Dec 2014 20:29:33 -0700 Subject: [PATCH 01/12] Unfortunate: port first API happening.jpg --- src/c4.co | 36 ++++++++++++++++-------------------- src/unfortunate/index.co | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 src/unfortunate/index.co diff --git a/src/c4.co b/src/c4.co index 83ed9d8..985d020 100644 --- a/src/c4.co +++ b/src/c4.co @@ -9,29 +9,25 @@ console.timeStamp "c4-init" console.time "init" console.time "interactive" +unf = require \./unfortunate +here = unf.locate window.location + # `board` holds global state about the current page, like `document` for the # 4chan domain. -global.board = {} - # boards.4chan.org//'thread' or /, or catalog/ - [ , &name, page, &thread-no] = window.location.pathname / \/ - - &is-catalog = page is \catalog - &search-term = &thread-no if &is-catalog - - &is-thread = not &is-catalog and !!&thread-no - &is-board = not &is-catalog and not &is-thread - &page = parseInt page, 10 or 0 - - &url = "//boards.4chan.org/#{&name}/" - - &ready = false # flag similar to document.readyState - - &favicons = require \./favicon-data - +global.board = + # TODO just use `here` as a global instead of legacy names + name: here.board + thread-no: here.tno + is-catalog: here.type is \catalog + is-thread: here.type is \thread + is-board: here.type is \thread + page: here.page + url: "//boards.4chan.org/#{here.board}/" + favicons: require \./favicon-data # TODO moot's crazy spoiler number bullshit - &spoiler-url = "//s.4cdn.org/image/spoiler-#{&name}1.png" - &thumbs-base = "//t.4cdn.org/#{&name}/" - &images-base = "//i.4cdn.org/#{&name}/" + spoiler-url: "//s.4cdn.org/image/spoiler-#{here.board}1.png" + thumbs-base: "//t.4cdn.org/#{here.board}/" + images-base: "//i.4cdn.org/#{here.board}/" require \./archives require \./backlinks diff --git a/src/unfortunate/index.co b/src/unfortunate/index.co new file mode 100644 index 0000000..3f46f11 --- /dev/null +++ b/src/unfortunate/index.co @@ -0,0 +1,31 @@ +# unfortuate: the un4chan userscript toolkit +# i.e., the non-ui parts of c4, so others can userscript +# without being tied to either 4chan's UI choices or +# c4's UI choices (as much as possible, anyway) + +# data FLocation = Catalog {board :: string, +# searchTerm :: Maybe string} +# | Index {board :: string, +# page :: int} +# | Thread {board :: string, +# tno :: int} +# +# type is encoded as @type, since instanceof is annoying +# +# locate :: Location -> FLocation +export locate = (location) -> + # boards.4chan.org//'thread' or /, or 'catalog'/ + [ , board, type, qualifier] = location.pathname / \/ + if type is \catalog + type: \catalog + board: board + searchTerm: qualifier + else if type is \thread + type: \thread + board: board + tno: qualifier + else # board + type: \board + board: board + page: parseInt type, 10 or 1 + From 61e6fe1132ac544e3632d3b32d2b3f344dcefe1e Mon Sep 17 00:00:00 2001 From: queue Date: Mon, 15 Dec 2014 22:47:51 -0700 Subject: [PATCH 02/12] Unfortunate: port onready lots more ugliness both within c4 and the interface I just invented. It'll get better soon though --- src/c4.co | 2 +- src/onready.co | 103 +++++------------ src/parser.co | 231 ++------------------------------------ src/poster.co | 18 +-- src/unfortunate/index.co | 87 +++++++++++++- src/unfortunate/parser.co | 177 +++++++++++++++++++++++++++++ 6 files changed, 313 insertions(+), 305 deletions(-) create mode 100644 src/unfortunate/parser.co diff --git a/src/c4.co b/src/c4.co index 985d020..86cd57f 100644 --- a/src/c4.co +++ b/src/c4.co @@ -20,7 +20,7 @@ global.board = thread-no: here.tno is-catalog: here.type is \catalog is-thread: here.type is \thread - is-board: here.type is \thread + is-board: here.type is \board page: here.page url: "//boards.4chan.org/#{here.board}/" favicons: require \./favicon-data diff --git a/src/onready.co b/src/onready.co index 965422a..470226a 100644 --- a/src/onready.co +++ b/src/onready.co @@ -1,50 +1,23 @@ {L, $$, $} = require \./utils/dom -parser = require \./parser +{Thread} = require \./parser {onready} = require \./utils/features {truncate} = require \./utils/string {get, set, sset, sget} = require \./utils/storage board-template = require \templates/board - -# pre-create new DOM -html = L \html - &appendChild with head = L \head - &appendChild L \title - &appendChild with L \style - &id = \c4-style - &textContent = require \style/c4 - &appendChild with L \script - &src = \//www.google.com/recaptcha/api.js?render=explicit - -body = L \body - -# replace the original html with the new, but keep a reference to query and -# parse. This speeds up re-rendering consideraby, since the browser doesn't -# even have to attempt displaying the old content and style. -# -# in fact, it's so effective, even the original sript tags don't run, EVEN ON -# CHROME, negating the need to handle 'beforescriptexeute' or whatever. crazy. -d = document.replaceChild html, document.documentElement - catalog-template = require \templates/catalog +unf = require \./unfortunate -# init -<-! document.addEventListener \DOMContentLoaded +page <-! unf.lift {style: require \style/c4} .then console.time "initial render" -console.time "parse page" - -# get other useful information board - &title = d.querySelector \.boardTitle ?.textContent or '' - &subtitle = d.querySelector \.boardSubtitle ?.innerHTML or '' - &nav = d.querySelector \#boardNavDesktop .innerHTML - &banner = d.querySelector \#bannerCnt .dataset.src - &message = d.querySelector \.globalMessage ?.innerHTML - - # detect based on favicon href - &sfw = - d.querySelector 'link[rel="shortcut icon"]' .href.slice(-6) is \ws.ico + &title = page.title + &subtitle = page.subtitle + &nav = page.oldDoc.querySelector \#boardNavDesktop .innerHTML + &banner = page.banner + &message = page.message + &sfw = page.sfw &type = if &sfw then \sfw else \nsfw &favicon = board.favicons[&type] @@ -52,30 +25,13 @@ board # for post deletion &password = get \password or Math.random!toString!substr -8 -console.timeEnd "parse page" - -console.log board - -# XXX needs to go somewhere else -body +document.body &id = board.name &className = "#{board.type} \ #{if board.isThread then \threadpage else \boardpage}" if board.is-catalog - # find script tag that defines `catalog` - [catalog-text] = Array::filter.call d.querySelectorAll(\script), -> - /var catalog/.test it.textContent - throw new Error "what is happening" unless catalog-text - - # eval the script tag, which needs `new FC().applyCSS()` in scope... - class FC then applyCSS: -> - - catalog = eval catalog-text.text-content + "; catalog" - - board.catalog = catalog - - console.log catalog + catalog = board.catalog = page.data order = get \catalog-order or \date console.time "generate new HTML body" @@ -83,19 +39,18 @@ if board.is-catalog console.timeEnd "generate new HTML body" console.time "parse new body HTML" - body.innerHTML = body-html + document.body.innerHTML = body-html console.timeEnd "parse new body HTML" - # XXX need to think a lot harder about how to structure this code. hiding the - # two types of pages behind if-statements isn't that great of an architecture require \./catalog else - #console.profile! - - console.time "parse board" - board.threads = parser.dom d, board.name - console.timeEnd "parse board" - board.thread = board.threads.0 if board.isThread + # XXX this is awkward, change unf's api pls + if board.isThread + board.thread = new Thread page.data + board.threads = [board.thread] + else + board.threads = for t of page.data + new Thread t # global lookup post by hash # XXX cross-board still conflicts @@ -104,34 +59,32 @@ else for &posts board.posts[&no] = & - # XXX used in hide.co, really want IDB/minimongo/'real man's db' + # XXX used in features, really want IDB/minimongo/'real man's db' board.threads-by-id = {} for board.threads board.threads-by-id[&op.no] = & - console.log board console.time "generate new HTML body" body-html = board-template board console.timeEnd "generate new HTML body" console.time "parse new body HTML" - body.innerHTML = body-html + document.body.innerHTML = body-html console.timeEnd "parse new body HTML" +console.log board + console.time "prerender handlers" document.dispatchEvent new CustomEvent do \c4-prerender - detail: {body} + detail: {body: document.body} console.timeEnd "prerender handlers" -console.time "render new body" -html.appendChild body -console.timeEnd "render new body" - if board.isBoard console.time "highlight current page" - body.querySelector "\#pages a[href=\"#{board.page or board.url}\"]" + href = if board.page is 1 then board.url else board.page + document.body.querySelector "\#pages a[href=\"#href\"]" .id = \current console.timeEnd "highlight current page" @@ -171,8 +124,6 @@ if window.location.hash and not sget document.URL sset {+(document.URL)} window.removeEventListener \scroll registerPage -board.ready = true - # cache current thread hash (which is updated by updater) version = c4_COMPILATION_VERSION if board.is-thread @@ -184,5 +135,3 @@ document.dispatchEvent new CustomEvent do \c4-ready detail: {board.threads, el: $ \threads} console.timeEnd "onready handlers" - -#console.profileEnd! diff --git a/src/parser.co b/src/parser.co index 218f90c..24a4209 100644 --- a/src/parser.co +++ b/src/parser.co @@ -1,78 +1,19 @@ -# Parse 4chan's DOM into the API format, and the API format into -# a slightly more friendly struct. +# augments the raw api with the following: # -# Rough documentation (most of it is the same as the API): +# quotelinks: An array of post numbers this post quotes, as parsed by 4chan +# into anchors. Multiple quotelinks to the same post are +# deduplicated, i.e. this array is a set. # -# Thread = { -# op: the first Post of the thread. -# no : unique post number of the OP. +# backlinks: an array of post numbers whose posts *which are in the same +# thread is this post*, quote this post. In other words, only +# in-thread backlinks are listed, which is usually good enough, +# but you should query your own backlinks if you want +# cross-thread/board ones. # -# omitted_posts: number -# omitted_images: number -# -# sticky: boolean, whether thread is stuck on top of the board pages. -# closed: boolean, whether thread is open to replies -# -# replies: Array of reply posts. -# -# reply: Object, with references to each reply by `no` (for lookup) -# imageReplies: Array of replies with images. -# } -# -# Post: { -# no: unique post number within a board -# -# idx: post number within a thread, 0-based -# -# time: unix seconds (not millis) when the post was made -# -# tim: unix milliseconds when the image was created, off which -# the image thumb url is based -# -# sub: post subject (blue text) -# -# name: poster's enetered name, usually 'Anonymous' -# trip: tripcode or `undefined` -# -# capcode: 'Admin' or 'Mod' or 'Developer' are the only ones I think exist. -# -# id: the posterID string or `undefined` -# -# com: HTML string of post's text. -# -# filedeleted: boolean, whether post had an image but it was removed. -# -# quotelinks: An array of post numbers this post quotes, as parsed by 4chan -# into anchors. Multiple quotelinks to the same post are -# deduplicated, i.e. this array is a set. -# -# backlinks: an array of post numbers whose posts *which are in the same -# thread is this post*, quote this post. In other words, only -# in-thread backlinks are listed, which is usually good enough, -# but you should query your own backlinks if you want -# cross-thread/board ones. -# -# w,h: Numbers, image dimensions in pixels -# -# fsize: number, size of image in bytes -# -# filename: original filename, minus extension -# ext: file extension -# md5: hash of original image. -# -# spoiler: boolean, whether image is marked as a spoiler -# -# tn_w, tn_h: Numbers -# } -export dom = (document, board) -> - raw = parse-dom document, board - console.log \raw raw - new Thread t, board for t of raw - -export api = (api, board) -> - new Thread api, board +export api = (api) -> + new Thread api -class Thread then ({posts}, board) -> +export class Thread then ({posts}) -> [op, ...replies] = @posts = posts @op = op <<< { @@ -115,154 +56,6 @@ function quotelinks-of comment, thread-no Object.keys set -# `document` is a HTMLDocument or HTMLElement that contains the .threads div -# from 4chan's pages. -# -# `board` is a string for the name of the board, e.g. 'a'. -function parse-dom document, board - # pre-query always-existant post elements, which should be faster than - # individual querySelector calls - times = document.querySelectorAll \.dateTime - comments = document.querySelectorAll \.postMessage - names = document.querySelectorAll \.name - - # some elements exist on both .mobile info and .desktop info, because moot, - # so bicrement past .mobile versions - e-idx = 0 - b-idx = 1 - - for document.querySelectorAll \.thread - t = parse-thread do - & - times - comments - names - e-idx - b-idx - - e-idx += t.posts.length - b-idx += t.posts.length * 2 - - t - -function parse-thread el, times, comments, names, e-idx, b-idx - thread-no = el.id.substring 1 # t12345 - - op = new DOMPost do - thread-no - el.querySelector( \.op ) - 0 - times[b-idx] - comments[e-idx] - names[b-idx] - - op - &no = thread-no - if omitted = el.querySelector \.summary - &omitted_posts = - parseInt omitted.textContent.match(/\d+(?= replies?)/), 10 or 0 - &omitted_images = - parseInt omitted.textContent.match(/\d+(?= images??)/), 10 or 0 - &sticky = el.querySelector(\.stickyIcon)? - &closed = el.querySelector(\.closedIcon)? - &resto = 0 - - ++e-idx - b-idx += 2 - - idx = 1 + (op.omitted_posts || 0) - replies = for el.getElementsByClassName( \reply ) - p = new DOMPost do - thread-no - & - idx++ - times[b-idx] - comments[e-idx] - names[b-idx] - - ++e-idx - b-idx += 2 - - p - - return {posts: [op, ...replies]} - -dimension-regex = /(\d+)x(\d+)/ -size-regex = /([\d\.]+) ([KM]?B)/ - -class DOMPost then (thread-no, el, @idx, time-el, comment-el, name-el) -> - @resto = thread-no - @no = el.id.substring 1 - - @time = parseInt(time-el.dataset.utc, 10) - - @name = name-el.innerHTML - - # 4chan does this weird ellipsis wrapping thing: - # - # full t(...) - # - @sub = if el.querySelector \.subject - if title = that.firstElementChild?title - title - else - that.textContent - - @trip = el.querySelector \.postertrip ?.innerHTML - @capcode = el.querySelector \.capcode ?.innerHTML - - @com = comment-el.innerHTML - - @id = el.querySelector \.hand ?.textContent # hand? - - thumb-el = el.querySelector \.fileThumb - @filedeleted = thumb-el?firstElementChild.alt is "File deleted." - if thumb-el and not @filedeleted - info = thumb-el.parentNode.firstElementChild - - # the dimensions appear after the original filename, so we - # need to match after any (\d+)x(\d+) patterns in the original filename - # - # The .fileText element has 'File: ' as a TextNode before the - # link to the file, and a TextNode after with the size and dimensions, - # so use the last child's content. - dimensions = dimension-regex.exec info.lastChild.textContent - - thumb = thumb-el.firstElementChild - @tim = thumb-el.href.match /\/(\d+)\./ .1 - - # FIXME when image is spoiled, these are the spoiler image size. - # Since 4chan doesn't expose tn_{w|h} in the HTML, will probably need - # to clamp to 152wx151h (and 252x251 for OPs) manually if spoiled - @tn_w = parseInt thumb.style.width, 10 - @tn_h = parseInt thumb.style.height, 10 - - @w = parseInt dimensions.1, 10 - @h = parseInt dimensions.2, 10 - - [, num, unit] = size-regex.exec thumb.alt - mult = if unit is \KB - 1024 - else if unit is \MB - 1024 * 1024 - else - 1 - @fsize = parseFloat(num) * mult - - @spoiler = thumb-el.classList.contains \imgspoiler - - name-ext = if @spoiler - info.title - else - a = info.querySelector 'a' - a.title or a.text-content - - last-dot = name-ext.last-index-of \. - @filename = name-ext.substring 0 last-dot - @ext = name-ext.substring last-dot - - @md5 = thumb.dataset.md5 - # XXX the :prelude parsing in nephrite is totally fucked, stuff the functions # here for now export template-fns: diff --git a/src/poster.co b/src/poster.co index be7654d..72a836e 100644 --- a/src/poster.co +++ b/src/poster.co @@ -122,10 +122,14 @@ $ \name ?.value = get \name or '' # load captcha on demand # https://developers.google.com/recaptcha/docs/display#explicit_render listen $ \comment .once \input !-> - document.head.appendChild with L \script - &textContent = """ - grecaptcha.render( - 'captcha', - {'sitekey': '6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc', theme: 'light'} - ) - """ + document.headappendChild with L \script + &src = \//www.google.com/recaptcha/api.js?render=explicit + &onload = !-> + document.head.appendChild with L \script + &textContent = """ + grecaptcha.render( + 'captcha', + {'sitekey': '6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc', theme: 'light'} + ) + """ + diff --git a/src/unfortunate/index.co b/src/unfortunate/index.co index 3f46f11..f84c69e 100644 --- a/src/unfortunate/index.co +++ b/src/unfortunate/index.co @@ -13,7 +13,7 @@ # type is encoded as @type, since instanceof is annoying # # locate :: Location -> FLocation -export locate = (location) -> +export function locate location # boards.4chan.org//'thread' or /, or 'catalog'/ [ , board, type, qualifier] = location.pathname / \/ if type is \catalog @@ -29,3 +29,88 @@ export locate = (location) -> board: board page: parseInt type, 10 or 1 +# { +# title :: string # optional, new page title +# style :: string # css to apply to new html +# } -> HTMLElement, suitable for replacing the current element +export function create-page {title, style} + document.createElement \html + &appendChild with document.createElement \head + &appendChild document.createElement \title + &textContent = title if title? + &appendChild with document.createElement \style + &textContent = style + &appendChild with document.createElement \script + &src = \//www.google.com/recaptcha/api.js?render=explicit + &appendChild document.createElement \body + +export parse = require \./parser + +# HTMLElement -> FPage { +# title :: string # moot's current board title, e.g., "/a/ - Anime & Manga" +# subtitle :: HtmlString # board subtitle. I think only /b/ has this now... +# banner :: string # source of the random banner, if you want it +# message :: HtmlString # moot's broadcast, if present +# sfw :: boolean # true for blue boards, false otherwise, +# +# oldDoc :: HTMLElement # input document, for any extra info you want +# +# data: +# # if the page is a catalog, then +# # the light JSON structure that powers +# # the desuwa catalog. +# #_not_ equivalent to catalog.json +# # else if the page is a board, then [threads], +# # equivalent to /.json's `threads` +# # else if the page is a thread, then equivalent to /res/.json +# } +export function parse-page document + if document.querySelector \#threads # it's a catalog + # find script tag that defines `catalog` + [catalog-text] = Array::filter.call document.querySelectorAll(\script), -> + /var catalog/.test it.textContent + throw new Error "what is happening" unless catalog-text + + # eval the script tag, which needs `new FC().applyCSS()` in scope... + # we _could_ try to parse out the JSON part of the tag itself, but + # it's easier just to eval it + class FC then applyCSS: -> + + data = eval catalog-text.text-content + "; catalog" + else if document.querySelector \.pagelist # board + data = parse document + else # assume thread + data = parse document .0 + + oldDoc : document + title : document.querySelector \.boardTitle ?.textContent or '' + subtitle: document.querySelector \.boardSubtitle ?.innerHTML or '' + banner : document.querySelector \#bannerCnt .dataset.src + message : document.querySelector \.globalMessage ?.innerHTML + # detect based on favicon href + sfw : + document.querySelector 'link[rel="shortcut icon"]' .href.slice(-6) is \ws.ico + data: data + +# replace the original html with the new, with a reference to query and +# parse. This speeds up re-rendering consideraby, since the browser doesn't +# even have to attempt displaying the old content and style. +# +# in fact, it's so effective, even the original sript tags don't run, EVEN ON +# CHROME, negating the need to handle 'beforescriptexeute' or whatever. crazy. +# +# returns the old html element, which can be parsed by `parse-page` when +# DOMContentLoaded fires. +export function replace-page newHtml + document.replaceChild newHtml, document.documentElement + +# a convenient combination of the above functions. +# when called, replaces the current dom with the passed-in arguments, +# and returns a promise for the page once parseable. +# If you don't need finer-grained control over the process, use this +# TODO think of a better verb than `lift` +export function lift opts + old = replace-page create-page opts + new Promise (resolve, reject) -> + document.addEventListener \DOMContentLoaded !-> + resolve parse-page old diff --git a/src/unfortunate/parser.co b/src/unfortunate/parser.co new file mode 100644 index 0000000..839eeb1 --- /dev/null +++ b/src/unfortunate/parser.co @@ -0,0 +1,177 @@ +# parses 4chan dom back into an api response +# `document` is a HTMLDocument or HTMLElement that contains the .threads div +# from 4chan's pages. +module.exports = function parse-dom document + # pre-query always-existant post elements, which should be faster than + # individual querySelector calls + times = document.querySelectorAll \.dateTime + comments = document.querySelectorAll \.postMessage + names = document.querySelectorAll \.name + + # some elements exist on both .mobile info and .desktop info, because moot, + # so bicrement past .mobile versions + e-idx = 0 + b-idx = 1 + + for document.querySelectorAll \.thread + t = parse-thread do + & + times + comments + names + e-idx + b-idx + + e-idx += t.posts.length + b-idx += t.posts.length * 2 + + t + +function parse-thread el, times, comments, names, e-idx, b-idx + thread-no = el.id.substring 1 # t12345 + + op = new DOMPost do + thread-no + el.querySelector( \.op ) + 0 + times[b-idx] + comments[e-idx] + names[b-idx] + + op + &no = thread-no + if omitted = el.querySelector \.summary + &omitted_posts = + parseInt omitted.textContent.match(/\d+(?= replies?)/), 10 or 0 + &omitted_images = + parseInt omitted.textContent.match(/\d+(?= images??)/), 10 or 0 + &sticky = el.querySelector(\.stickyIcon)? + &closed = el.querySelector(\.closedIcon)? + &resto = 0 + + ++e-idx + b-idx += 2 + + idx = 1 + (op.omitted_posts || 0) + replies = for el.getElementsByClassName( \reply ) + p = new DOMPost do + thread-no + & + idx++ + times[b-idx] + comments[e-idx] + names[b-idx] + + ++e-idx + b-idx += 2 + + p + + return {posts: [op, ...replies]} + +dimension-regex = /(\d+)x(\d+)/ +size-regex = /([\d\.]+) ([KM]?B)/ + +class DOMPost then (thread-no, el, @idx, time-el, comment-el, name-el) -> + @resto = thread-no + @no = el.id.substring 1 + + @time = parseInt(time-el.dataset.utc, 10) + + @name = name-el.innerHTML + + # 4chan does this weird ellipsis wrapping thing: + # + # full t(...) + # + @sub = if el.querySelector \.subject + if title = that.firstElementChild?title + title + else + that.textContent + + @trip = el.querySelector \.postertrip ?.innerHTML + @capcode = el.querySelector \.capcode ?.innerHTML + + @com = comment-el.innerHTML + + @id = el.querySelector \.hand ?.textContent # hand? + + thumb-el = el.querySelector \.fileThumb + @filedeleted = thumb-el?firstElementChild.alt is "File deleted." + if thumb-el and not @filedeleted + info = thumb-el.parentNode.firstElementChild + + # the dimensions appear after the original filename, so we + # need to match after any (\d+)x(\d+) patterns in the original filename + # + # The .fileText element has 'File: ' as a TextNode before the + # link to the file, and a TextNode after with the size and dimensions, + # so use the last child's content. + dimensions = dimension-regex.exec info.lastChild.textContent + + thumb = thumb-el.firstElementChild + @tim = thumb-el.href.match /\/(\d+)\./ .1 + + # FIXME when image is spoiled, these are the spoiler image size. + # Since 4chan doesn't expose tn_{w|h} in the HTML, will probably need + # to clamp to 152wx151h (and 252x251 for OPs) manually if spoiled + @tn_w = parseInt thumb.style.width, 10 + @tn_h = parseInt thumb.style.height, 10 + + @w = parseInt dimensions.1, 10 + @h = parseInt dimensions.2, 10 + + [, num, unit] = size-regex.exec thumb.alt + mult = if unit is \KB + 1024 + else if unit is \MB + 1024 * 1024 + else + 1 + @fsize = parseFloat(num) * mult + + @spoiler = thumb-el.classList.contains \imgspoiler + + name-ext = if @spoiler + info.title + else + a = info.querySelector 'a' + a.title or a.text-content + + last-dot = name-ext.last-index-of \. + @filename = name-ext.substring 0 last-dot + @ext = name-ext.substring last-dot + + @md5 = thumb.dataset.md5 + +# XXX the :prelude parsing in nephrite is totally fucked, stuff the functions +# here for now +export template-fns: + classes: -> + c = 'post ' + c += 'imagepost ' if it.filename + c += 'tripcoded ' if it.trip + if it.capcode + c += if it.capcode is '## Admin' + 'admin ' + else 'mod ' + c += 'id ' if it.iid + c + + humanized: (bytes) -> + if bytes < 1024 + "#bytes B" + else if (kbytes = Math.round bytes / 1024) < 1024 + "#kbytes KB" + else + "#{(kbytes / 1024)toString!substring 0 3} MB" + + thumb-url: (post) -> + board.thumbs-base + post.tim + \s.jpg + + image-url: (post) -> + board.images-base + post.tim + post.ext + + permalink: (post) -> + "//boards.4chan.org/#{board.name}/thread/#{post.resto or post.no}\##{post.no}" From 93f42ed4ddcc226437a40b3896bb9ec225da0f42 Mon Sep 17 00:00:00 2001 From: queue Date: Tue, 16 Dec 2014 22:42:40 -0700 Subject: [PATCH 03/12] Unfortunate: port updater Couldn't think of a nice api, so I just wrapped the current api up in new naming conventions. Good enough for testing, I think. --- src/poster.co | 2 +- src/unfortunate/index.co | 2 + .../updater.co} | 142 ++++++++++++------ src/updater.co | 53 +++---- 4 files changed, 125 insertions(+), 74 deletions(-) rename src/{new-updater.co => unfortunate/updater.co} (71%) diff --git a/src/poster.co b/src/poster.co index 72a836e..93bc28c 100644 --- a/src/poster.co +++ b/src/poster.co @@ -122,7 +122,7 @@ $ \name ?.value = get \name or '' # load captcha on demand # https://developers.google.com/recaptcha/docs/display#explicit_render listen $ \comment .once \input !-> - document.headappendChild with L \script + document.head.appendChild with L \script &src = \//www.google.com/recaptcha/api.js?render=explicit &onload = !-> document.head.appendChild with L \script diff --git a/src/unfortunate/index.co b/src/unfortunate/index.co index f84c69e..c4664f5 100644 --- a/src/unfortunate/index.co +++ b/src/unfortunate/index.co @@ -114,3 +114,5 @@ export function lift opts new Promise (resolve, reject) -> document.addEventListener \DOMContentLoaded !-> resolve parse-page old + +export subscribe = require \./updater .subscribe diff --git a/src/new-updater.co b/src/unfortunate/updater.co similarity index 71% rename from src/new-updater.co rename to src/unfortunate/updater.co index fd10933..6457bc7 100644 --- a/src/new-updater.co +++ b/src/unfortunate/updater.co @@ -74,7 +74,7 @@ worker-code = !-> ports-to-last-seen.set port, Date.now! switch it.data.type case \register - {board-name, thread-no, last-modified} = it.data + {board-name, thread-no, last-modified ? new Date(0)} = it.data board-lm[board-name] = new Date 0 state = to-poll@[board-name][thread-no] ?= last-modified: last-modified @@ -91,7 +91,13 @@ worker-code = !-> ports-to-last-seen.delete port cleanup-port port log 'port went byebye' - # TODO deregister, when I start doing pushState page reloading + case \deregister + {board-name, thread-no} = it.data + state = to-poll@[board-name][thread-no] + if state? + state.ports.remove port + if state.ports.size is 0 + delete to-poll[board-name][thread-no] !function cleanup-port port for board-name, thread-states in to-poll @@ -161,7 +167,8 @@ worker-code = !-> for port of thread-states[it]ports port.postMessage do type: \dead - thread-no: it + board: board-name + tno: it delete thread-states[it] @@ -176,7 +183,6 @@ worker-code = !-> return if active.has key active.add key - url = "//a.4cdn.org/#{board-name}/res/#{thread-no}.json" ims = thread-states[thread-no]last-modified.toISOString! make-xhr url, ims, !(err, res) -> @@ -192,7 +198,8 @@ worker-code = !-> for port of thread-states[thread-no]ports port.postMessage do type: \dead - thread-no: thread-no + board: board-name + tno: thread-no delete thread-states[thread-no] else last-modified = res.last-modified @@ -208,47 +215,92 @@ worker-code = !-> log "broadcasting thread to a port" it.postMessage do type: \update + board: board-name + tno: thread-no thread: thread set-interval poll, INTERVAL / 2 - #set-timeout poll, 100 - -worker = new SharedWorker do - "data:application/javascript,#{encodeURIComponent "(#worker-code)()"}" - -worker.port.add-event-listener \message -> - switch it.data.type - case \ping - worker.port.postMessage type: \pong - case \xhr - {url, if-modified-since} = it.data - # make xhr for the worker - new XMLHttpRequest - &open \GET url - &set-request-header \If-Modified-Since if-modified-since - &onload = !-> - worker.port.postMessage do - type: \xhr - url: url - res: - body: @response - status: @status - last-modified: new Date @getResponseHeader \Last-Modified - &onerror = !-> - console.log it - worker.port.postMessage do - type: \xhr - url: url - err: @status - &send! - case \log - console.log.apply console, it.data.args - default - console.log 'from worker', it.data.type, it.data - -worker.port.start! - -window.add-event-listener \beforeunload !-> - worker.port.postMessage type: \bye -module.exports = worker.port +subscriptions = new Map # callback -> "#board#tno" + # reversed because there's never going to be + # enough keys to make linear traversal expensive +var worker + +!function initialize-worker + worker := new SharedWorker do + "data:application/javascript,#{encodeURIComponent "(#worker-code)()"}" + + worker.port.add-event-listener \message -> + switch it.data.type + case \ping + worker.port.postMessage type: \pong + case \xhr + {url, if-modified-since} = it.data + # make xhr for the worker + new XMLHttpRequest + &open \GET url + &set-request-header \If-Modified-Since if-modified-since + &onload = !-> + worker.port.postMessage do + type: \xhr + url: url + res: + body: @response + status: @status + last-modified: new Date @getResponseHeader \Last-Modified + &onerror = !-> + console.log it + worker.port.postMessage do + type: \xhr + url: url + err: @status + &send! + case \update \dead + key = it.data.board + it.data.tno + console.log "got #{it.data.type} #key" + console.log 'subscriptions', subscriptions + subscriptions.for-each !(k, sub) -> + console.log "sub #key" + if k is key + try + console.log "found sub for #{it.data.type} #key" + sub it.data + catch + console.error e + case \log + console.log.apply console, it.data.args + default + console.log 'from worker', it.data.type, it.data + + worker.port.start! + + window.add-event-listener \beforeunload !-> + worker.port.postMessage type: \bye + +!function cleanup-worker + worker.port.postMessage type: \bye + worker.port.close! + +# {board: string, tno: number, last-modified: maybe date} +# callback: ({type: \dead, tno: string, board: string} +# |{type: \update, tno: string, thread: Thead, board: string}) +export function subscribe {board, tno, last-modified}, callback + initialize-worker! + + worker.port.postMessage do + type: \register + board-name: board + thread-no: tno + last-modified: last-modified + + filter = board + tno + subscriptions.set callback, filter + + return function unsubscribe + worker.port.postMessage do + type: \deregister + board-name: board + thread-no: tno + subscriptions.remove callback + if subscriptions.length is 0 + cleanup-worker! diff --git a/src/updater.co b/src/updater.co index 578c20e..cca31c0 100644 --- a/src/updater.co +++ b/src/updater.co @@ -15,9 +15,7 @@ listen = require \./utils/listen post-template = require \templates/post parser = require \./parser draw-favicon = require \./utils/favicon -new-updater = require \./new-updater - -export updater = {} +{subscribe} = require \./unfortunate # state unread = 0 @@ -109,30 +107,29 @@ fade-when-visible = !-> onready !-> if board.isThread and not board.thread.op.closed - new-updater.add-event-listener \message !-> - switch it.data.type - case \dead - # TODO handle the archived/closed state somehow, since threads - # don't 404 immediately - document.title += '(dead)' - case \update - console.log 'updater get', it.data - old-thread = board.thread - - thread = parser.api it.data.thread, board.name - - new-posts = thread.replies.filter -> not old-thread.post[it.no] - deleted = old-thread.replies.filter -> not thread.post[it.no] - - console.log 'updates:' new-posts, deleted - - handle-new-posts thread, new-posts, deleted - - # swap out old thread state with new - board.thread = thread - new-updater.postMessage do - type: \register - board-name: board.name - thread-no: board.thread-no + subscribe do + board: board.name + tno: board.thread-no last-modified: new Date board.thread.posts[*-1].time * 1000 + !-> + console.log "got update" + switch it.type + case \dead + # TODO handle the archived/closed state somehow, since threads + # don't 404 immediately + document.title += '(dead)' + case \update + console.log 'updater get', it + old-thread = board.thread + + thread = parser.api it.thread, board.name + + new-posts = thread.replies.filter -> not old-thread.post[it.no] + deleted = old-thread.replies.filter -> not thread.post[it.no] + + console.log 'updates:' new-posts, deleted + + handle-new-posts thread, new-posts, deleted + # swap out old thread state with new + board.thread = thread From 5b5bb7baa5d3aa05920f967bb51b6aab9092f977 Mon Sep 17 00:00:00 2001 From: queue Date: Fri, 2 Jan 2015 15:10:02 -0700 Subject: [PATCH 04/12] Parser: fix subject truncation annoying, they must've changed the way that works. --- src/unfortunate/parser.co | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/unfortunate/parser.co b/src/unfortunate/parser.co index 839eeb1..1ee360f 100644 --- a/src/unfortunate/parser.co +++ b/src/unfortunate/parser.co @@ -80,15 +80,8 @@ class DOMPost then (thread-no, el, @idx, time-el, comment-el, name-el) -> @name = name-el.innerHTML - # 4chan does this weird ellipsis wrapping thing: - # - # full t(...) - # - @sub = if el.querySelector \.subject - if title = that.firstElementChild?title - title - else - that.textContent + @sub = if el.querySelector '.postInfo.desktop > .subject' + that.textContent @trip = el.querySelector \.postertrip ?.innerHTML @capcode = el.querySelector \.capcode ?.innerHTML From 2b7cd626d9e7292b302f57c364a482b3551d2b3d Mon Sep 17 00:00:00 2001 From: queue Date: Thu, 22 Sep 2016 23:04:17 -0600 Subject: [PATCH 05/12] catalog: fix to new catalog.json breaking my ancient crufty shit. The ordering no longer works, oh well. --- templates/catalog.ne | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/catalog.ne b/templates/catalog.ne index b0ecb99..81db646 100644 --- a/templates/catalog.ne +++ b/templates/catalog.ne @@ -16,5 +16,5 @@ input#date.order(type="radio", name="order", value="date") | Creation Date #catalog - for @order[@@order] - = catalog-thread @threads[&], no: & + for no, t in @threads + = catalog-thread t, {no} From a74dab59bdf4d7204a5940fc675ec6b0bcad12d4 Mon Sep 17 00:00:00 2001 From: queue Date: Thu, 22 Sep 2016 23:05:48 -0600 Subject: [PATCH 06/12] catalog: specify thumbnail size if you just wait 3 years, TODOs just get implemented, yo --- style/catalog.styl | 4 +--- templates/catalog-thread.ne | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/style/catalog.styl b/style/catalog.styl index 7e6c415..ad17318 100644 --- a/style/catalog.styl +++ b/style/catalog.styl @@ -44,9 +44,7 @@ cat-width = 150px overflow hidden .catalog-thumb - // TODO would prefer to specify width and height in html to prevent - // reflowing, but that would require object-fit CSS to make simple max-width - // scaling 'just werk'. + object-fit contain max-width cat-width max-height cat-width // prevent skinny images from being so big diff --git a/templates/catalog-thread.ne b/templates/catalog-thread.ne index 95943a1..7724368 100644 --- a/templates/catalog-thread.ne +++ b/templates/catalog-thread.ne @@ -2,7 +2,7 @@ a.catalog-link(href="//boards.4chan.org/#{board.name}/thread/#{@@no}", id="c#{@@no}") figure.catalog-thread if @imgurl - img.catalog-thumb(src="//t.4cdn.org/#{board.name}/#{@imgurl}s.jpg") + img.catalog-thumb(src="//t.4cdn.org/#{board.name}/#{@imgurl}s.jpg", width="#{@tn_w}", height="#{@tn_h}") else img.catalog-thumb.deleted-image(src="//s.4cdn.org/image/filedeleted.gif") figcaption.catalog-caption From 16347f230a348e8f136216401afed8abd7a1deb7 Mon Sep 17 00:00:00 2001 From: queue Date: Wed, 16 Nov 2016 19:06:17 -0700 Subject: [PATCH 07/12] Support /tg/ threads with pdfs Since I now browse that sad board. Holy shit is livescript sugary, I can see why I liked it. --- src/features/image-previews.co | 7 ++++++- src/unfortunate/parser.co | 7 +++++-- templates/post.ne | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/features/image-previews.co b/src/features/image-previews.co index 40153cc..6695aa6 100644 --- a/src/features/image-previews.co +++ b/src/features/image-previews.co @@ -6,9 +6,14 @@ {on-posts} = require \../utils/features lightbox = require \../utils/lightbox -on-posts \.thumb : mouseover: lightbox ({{dataset, href}: parentElement}) -> +boxy = lightbox ({{dataset, href}: parentElement}) -> {dataset.width, dataset.height, src: href} +on-posts \.thumb : mouseover: (e) -> + # XXX avoid trying to render pdfs (on /tg/) + if not /\.pdf$/.test e.target.parentElement.href + boxy.call(this, e) + # TODO alternative destructuring, I think there's an even shorter version # though # { ...it.parentElement{src: href}, ...it.parentElement.dataset{width, height} diff --git a/src/unfortunate/parser.co b/src/unfortunate/parser.co index 1ee360f..12efec5 100644 --- a/src/unfortunate/parser.co +++ b/src/unfortunate/parser.co @@ -95,6 +95,7 @@ class DOMPost then (thread-no, el, @idx, time-el, comment-el, name-el) -> if thumb-el and not @filedeleted info = thumb-el.parentNode.firstElementChild + # (for images only) # the dimensions appear after the original filename, so we # need to match after any (\d+)x(\d+) patterns in the original filename # @@ -112,8 +113,10 @@ class DOMPost then (thread-no, el, @idx, time-el, comment-el, name-el) -> @tn_w = parseInt thumb.style.width, 10 @tn_h = parseInt thumb.style.height, 10 - @w = parseInt dimensions.1, 10 - @h = parseInt dimensions.2, 10 + # dimensions is null if we're looking at a pdf or something + if dimensions + @w = parseInt dimensions.1, 10 + @h = parseInt dimensions.2, 10 [, num, unit] = size-regex.exec thumb.alt mult = if unit is \KB diff --git a/templates/post.ne b/templates/post.ne index 5cdcd0b..04f6771 100644 --- a/templates/post.ne +++ b/templates/post.ne @@ -21,7 +21,8 @@ if @filename .fileinfo span.filename= "#{@filename}#{@ext}" - span.dimensions= "#{@w}x#{@h}" + if @w and @h + span.dimensions= "#{@w}x#{@h}" span.size= humanized @fsize a.saucelink(href="http://iqdb.org/?url=http:#{image-url(locals)}", target="_blank") iqdb From 78614d56684410464c33985288d7954a681b7a8d Mon Sep 17 00:00:00 2001 From: queue Date: Tue, 27 Dec 2016 20:20:39 -0700 Subject: [PATCH 08/12] fix google reverse img search url --- templates/post.ne | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/post.ne b/templates/post.ne index 04f6771..733b361 100644 --- a/templates/post.ne +++ b/templates/post.ne @@ -26,7 +26,7 @@ span.size= humanized @fsize a.saucelink(href="http://iqdb.org/?url=http:#{image-url(locals)}", target="_blank") iqdb - a.saucelink(href="http://google.com/searchbyimage?image_url=http:#{image-url(locals)}", + a.saucelink(href="https://www.google.com/searchbyimage?image_url=http:#{image-url(locals)}", target="_blank") google a.saucelink(href="http://regex.info/exif.cgi/exif.cgi?imgurl=http:#{image-url(locals)}", target="_blank") exif From 792ef1922276fd11665d6fed415e76fb922c7699 Mon Sep 17 00:00:00 2001 From: queue Date: Tue, 27 Dec 2016 20:22:32 -0700 Subject: [PATCH 09/12] Show img byte size and dimensions by default People are always posting fuck huge images in /tg/ for some reason, like 6 MB phone pics. annoying. --- style/post.styl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/style/post.styl b/style/post.styl index d9b797b..fcd0074 100644 --- a/style/post.styl +++ b/style/post.styl @@ -69,9 +69,9 @@ font-size 8pt font-family sans-serif - // hide extended attributes until hovered + // hide sauce links until hovered &:not(:hover) - & > .saucelink, & > .dimensions, & > .size + & > .saucelink transition-delay .5s opacity 0 From 5a6d82d44303e7bdec2fc8ec8da15dfd8713564b Mon Sep 17 00:00:00 2001 From: queue Date: Fri, 17 Nov 2017 19:25:21 -0700 Subject: [PATCH 10/12] updater: fix dumb ff57 shit They finally did it. They broke my 2-year-old shit script. xhr no longer takes relative "//" urls anymore i guess, so https everywhere. I'm kind of surprised my sharedworker shit still works. I remember how terrible it all seemed when I wrote it. It probably still sux. javascript life. --- src/features/postpreviews.co | 2 +- src/unfortunate/updater.co | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/features/postpreviews.co b/src/features/postpreviews.co index 6b2141d..c6cb96d 100644 --- a/src/features/postpreviews.co +++ b/src/features/postpreviews.co @@ -20,7 +20,7 @@ fetch-new-post = !(no) -> # TODO guard against multiple requests in short time period xhr = new XMLHttpRequest - &open \GET "//api.4chan.org/#board-name/thread/#thread.json" + &open \GET "https://api.4chan.org/#board-name/thread/#thread.json" &onload = !-> # TODO handle 404 -> archive if @status is 200 diff --git a/src/unfortunate/updater.co b/src/unfortunate/updater.co index 6457bc7..8760a74 100644 --- a/src/unfortunate/updater.co +++ b/src/unfortunate/updater.co @@ -61,7 +61,7 @@ worker-code = !-> -> if active-requests.has url active-requests.delete url - cb "xhr timeout", void + cb "xhr timeout for url #url", void 5000 self.onconnect = !-> @@ -130,7 +130,7 @@ worker-code = !-> for let board-name, thread-states in to-poll return if throttled.has board-name throttled.add board-name - url = "//a.4cdn.org/#{board-name}/threads.json" + url = "https://a.4cdn.org/#{board-name}/threads.json" ims = board-lm[board-name].toISOString() make-xhr url, ims, !(err, res) -> log err if err @@ -183,7 +183,7 @@ worker-code = !-> return if active.has key active.add key - url = "//a.4cdn.org/#{board-name}/res/#{thread-no}.json" + url = "https://a.4cdn.org/#{board-name}/res/#{thread-no}.json" ims = thread-states[thread-no]last-modified.toISOString! make-xhr url, ims, !(err, res) -> log err if err @@ -227,8 +227,12 @@ subscriptions = new Map # callback -> "#board#tno" var worker !function initialize-worker - worker := new SharedWorker do - "data:application/javascript,#{encodeURIComponent "(#worker-code)()"}" + try + worker := new SharedWorker do + "data:application/javascript,#{encodeURIComponent "(#worker-code)()"}" + catch + console.error "couldn't make sharedworker" + console.error e worker.port.add-event-listener \message -> switch it.data.type @@ -264,7 +268,12 @@ var worker if k is key try console.log "found sub for #{it.data.type} #key" - sub it.data + console.log "sending data: ", it.data + # XXX need to cloneInto for some wack Xraywrapper reason + # or rather, need to somehow cleanse it.data of whatever + # cross-origin attachment it has. this works, apparently + cleaned-data = JSON.parse(JSON.stringify(it.data)) + sub cleaned-data catch console.error e case \log From 93f2f8c3f17eec12271e5ec7085a102d035344fb Mon Sep 17 00:00:00 2001 From: queue Date: Thu, 26 Apr 2018 10:16:10 -0600 Subject: [PATCH 11/12] fix thumbnails moving to i.4cdn.org hiro pls --- src/c4.co | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c4.co b/src/c4.co index 86cd57f..c6ff5f8 100644 --- a/src/c4.co +++ b/src/c4.co @@ -26,7 +26,7 @@ global.board = favicons: require \./favicon-data # TODO moot's crazy spoiler number bullshit spoiler-url: "//s.4cdn.org/image/spoiler-#{here.board}1.png" - thumbs-base: "//t.4cdn.org/#{here.board}/" + thumbs-base: "//i.4cdn.org/#{here.board}/" images-base: "//i.4cdn.org/#{here.board}/" require \./archives From 884d452f2344aeed5c99b2b9a87ca129025f1e0e Mon Sep 17 00:00:00 2001 From: queue Date: Thu, 26 Apr 2018 10:19:07 -0600 Subject: [PATCH 12/12] fix catalog-thread hardcoded t.4cdn sad. --- templates/catalog-thread.ne | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/catalog-thread.ne b/templates/catalog-thread.ne index 7724368..f5cac79 100644 --- a/templates/catalog-thread.ne +++ b/templates/catalog-thread.ne @@ -2,7 +2,7 @@ a.catalog-link(href="//boards.4chan.org/#{board.name}/thread/#{@@no}", id="c#{@@no}") figure.catalog-thread if @imgurl - img.catalog-thumb(src="//t.4cdn.org/#{board.name}/#{@imgurl}s.jpg", width="#{@tn_w}", height="#{@tn_h}") + img.catalog-thumb(src="//i.4cdn.org/#{board.name}/#{@imgurl}s.jpg", width="#{@tn_w}", height="#{@tn_h}") else img.catalog-thumb.deleted-image(src="//s.4cdn.org/image/filedeleted.gif") figcaption.catalog-caption