Use Grunt for building from now on.

This commit is contained in:
Nicolas Stepien 2012-09-24 22:37:33 +02:00
parent 9037ad32da
commit b3d7f30f3a
18 changed files with 1399 additions and 1375 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
tmp/

21
4chan_x.meta.js Normal file
View File

@ -0,0 +1,21 @@
// ==UserScript==
// @name 4chan X Alpha
// @version 3.0.0
// @description Adds various features.
// @copyright 2009-2011 James Campos <james.r.campos@gmail.com>
// @copyright 2012 Nicolas Stepien <stepien.nicolas@gmail.com>
// @license MIT; http://en.wikipedia.org/wiki/Mit_license
// @match *://boards.4chan.org/*
// @match *://images.4chan.org/*
// @match *://sys.4chan.org/*
// @match *://api.4chan.org/*
// @match *://*.foolz.us/api/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_openInTab
// @run-at document-start
// @updateURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.meta.js
// @downloadURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js
// @icon data:image/gif;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAIALAAAAAAQABAAAAIxlI+pq+D9DAgUoFkPDlbs7lGiI2bSVnKglnJMOL6omczxVZK3dH/41AG6Lh7i6qUoAAA7
// ==/UserScript==

View File

@ -1,5 +1,5 @@
// ==UserScript==
// @name 4chan X alpha
// @name 4chan X Alpha
// @version 3.0.0
// @description Adds various features.
// @copyright 2009-2011 James Campos <james.r.campos@gmail.com>
@ -15,69 +15,33 @@
// @grant GM_deleteValue
// @grant GM_openInTab
// @run-at document-start
// @updateURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js
// @updateURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.meta.js
// @downloadURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js
// @icon data:image/gif;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAIALAAAAAAQABAAAAIxlI+pq+D9DAgUoFkPDlbs7lGiI2bSVnKglnJMOL6omczxVZK3dH/41AG6Lh7i6qUoAAA7
// @icon https://github.com/MayhemYDG/4chan-x/raw/stable/img/icon.gif
// ==/UserScript==
/* LICENSE
/* 4chan X Alpha - Version 3.0.0 - 2012-09-24
* http://mayhemydg.github.com/4chan-x/
*
* Copyright (c) 2009-2011 James Campos <james.r.campos@gmail.com>
* Copyright (c) 2012 Nicolas Stepien <stepien.nicolas@gmail.com>
* http://mayhemydg.github.com/4chan-x/
* 4chan X 3.0.0
* Licensed under the MIT license.
* https://github.com/MayhemYDG/4chan-x/blob/master/LICENSE
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* HACKING
*
* 4chan X is written in CoffeeScript[1], and developed on GitHub[2].
*
* [1]: http://coffeescript.org/
* [2]: https://github.com/MayhemYDG/4chan-x
*
* CONTRIBUTORS
*
* noface - unique ID fixes
* desuwa - Firefox filename upload fix
* seaweed - bottom padding for image hover
* Contributors:
* https://github.com/MayhemYDG/4chan-x/graphs/contributors
* ferongr, xat-, Ongpot, thisisanon and Anonymous - cooldown sanity check
* e000 - cooldown sanity check
* ahodesuka - scroll back when unexpanding images, file info formatting
* Shou- - pentadactyl fixes
* ferongr - new favicons
* xat- - new favicons
* Zixaphir - fix qr textarea - captcha-image gap
* Ongpot - sfw favicon
* thisisanon - nsfw + 404 favicons
* Anonymous - empty favicon
* Seiba - chrome quick reply focusing
* herpaderpderp - recaptcha fixes
* WakiMiko - recaptcha tab order http://userscripts.org/scripts/show/82657
* btmcsweeney - allow users to specify text for sauce links
*
* All the people who've taken the time to write bug reports.
*
* Thank you.
*/
// Generated by CoffeeScript 1.3.3
(function() {
var $, $$, AutoGIF, Board, Build, Clone, Conf, Config, FileInfo, Get, ImageHover, Main, Post, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Redirect, RevealSpoilers, Sauce, Thread, ThreadUpdater, Time, UI, d, g,
__hasProp = {}.hasOwnProperty,
@ -86,7 +50,7 @@
Config = {
main: {
Enhancing: {
'Disable 4chan\'s extension': [true, 'Avoid conflicts between 4chan X and 4chan\'s inline extension.'],
'Disable 4chan\'s extension': [true, 'Avoid conflicts between 4chan X Alpha and 4chan\'s inline extension.'],
'404 Redirect': [true, 'Redirect dead threads and images.'],
'Keybinds': [true, 'Bind actions to keyboard shortcuts.'],
'Time Formatting': [true, 'Localize and format timestamps arbitrarily.'],
@ -95,7 +59,7 @@
'Thread Expansion': [true, 'Can expand threads to view all replies.'],
'Index Navigation': [false, 'Navigate to previous / next thread.'],
'Reply Navigation': [false, 'Navigate to top / bottom of thread.'],
'Check for Updates': [true, 'Check for updated versions of 4chan X.']
'Check for Updates': [true, 'Check for updated versions of 4chan X Alpha.']
},
Filtering: {
'Anonymize': [false, 'Turn everyone Anonymous.'],
@ -206,22 +170,6 @@
imageFit: 'fit width'
};
if (!/^(boards|images|sys)\.4chan\.org$/.test(location.hostname)) {
return;
}
Conf = {};
d = document;
g = {
VERSION: '3.0.0',
NAMESPACE: '4chan_X.',
boards: {},
threads: {},
posts: {}
};
UI = (function() {
var dialog, drag, dragend, dragstart, hover, hoverend, hoverstart, touchend, touchmove;
dialog = function(id, position, html) {
@ -386,13 +334,6 @@
};
})();
/*
loosely follows the jquery api:
http://api.jquery.com/
not chainable
*/
$ = function(selector, root) {
if (root == null) {
root = d.body;
@ -715,6 +656,22 @@
}
});
if (!/^(boards|images|sys)\.4chan\.org$/.test(location.hostname)) {
return;
}
Conf = {};
d = document;
g = {
VERSION: '3.0.0',
NAMESPACE: "4chan_X_Alpha.",
boards: {},
threads: {},
posts: {}
};
Board = (function() {
Board.prototype.toString = function() {

45
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,45 @@
## Reporting bugs
1. Make sure your **browser** & **4chan X** are up to date.
2. Disable your other extensions & scripts.
3. If your issue persists:
1. Report precise steps to reproduce the problem.
2. Report console errors, if any.
3. Report browser and browser version.
Open your console with:
- `Ctrl + Shift + J` on Chrome & Firefox
- `Ctrl + Shift + O` on Opera.
***
## Development & Contribution
### Get started
- Clone 4chan X.
- `cd` into it.
- Install [node.js](http://nodejs.org/).
- Install [CoffeeScript](http://coffeescript.org/) with `npm install -g coffee-script`.
- Install [Grunt](http://gruntjs.com/) with `npm install -g grunt`.
- Install [grunt-exec](https://npmjs.org/package/grunt-exec) with `npm install grunt-exec`.
- Install [grunt-image-embed](https://npmjs.org/package/grunt-image-embed) with `npm install grunt-image-embed`.
### Build
- Build with `grunt`.
- For development (continuous builds), run `grunt watch`.
### Release
- To upgrade, edit the version in `grunt.js` and run `grunt upgrade`.
Note: this is only used to release new 4chan X versions, and is not needed or wanted in pull requests.
### Contribute
- Edit the CoffeeScript source.
- Build the JavaScript.
- If the edits affect regular users, edit the changelog.
- Fork the repo.
- Send a pull request.

120
Cakefile
View File

@ -1,120 +0,0 @@
{log} = console
{exec} = require 'child_process'
fs = require 'fs'
VERSION = '3.0.0'
CAKEFILE = 'Cakefile'
INFILE = 'script.coffee'
OUTFILE = '4chan_x.user.js'
CHANGELOG = 'changelog'
LATEST = 'latest.js'
HEADER = """
// ==UserScript==
// @name 4chan X alpha
// @version #{VERSION}
// @description Adds various features.
// @copyright 2009-2011 James Campos <james.r.campos@gmail.com>
// @copyright 2012 Nicolas Stepien <stepien.nicolas@gmail.com>
// @license MIT; http://en.wikipedia.org/wiki/Mit_license
// @match *://boards.4chan.org/*
// @match *://images.4chan.org/*
// @match *://sys.4chan.org/*
// @match *://api.4chan.org/*
// @match *://*.foolz.us/api/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_openInTab
// @run-at document-start
// @updateURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js
// @downloadURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js
// @icon data:image/gif;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAIALAAAAAAQABAAAAIxlI+pq+D9DAgUoFkPDlbs7lGiI2bSVnKglnJMOL6omczxVZK3dH/41AG6Lh7i6qUoAAA7
// ==/UserScript==
/* LICENSE
*
* Copyright (c) 2009-2011 James Campos <james.r.campos@gmail.com>
* Copyright (c) 2012 Nicolas Stepien <stepien.nicolas@gmail.com>
* http://mayhemydg.github.com/4chan-x/
* 4chan X #{VERSION}
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* HACKING
*
* 4chan X is written in CoffeeScript[1], and developed on GitHub[2].
*
* [1]: http://coffeescript.org/
* [2]: https://github.com/MayhemYDG/4chan-x
*
* CONTRIBUTORS
*
* noface - unique ID fixes
* desuwa - Firefox filename upload fix
* seaweed - bottom padding for image hover
* e000 - cooldown sanity check
* ahodesuka - scroll back when unexpanding images, file info formatting
* Shou- - pentadactyl fixes
* ferongr - new favicons
* xat- - new favicons
* Zixaphir - fix qr textarea - captcha-image gap
* Ongpot - sfw favicon
* thisisanon - nsfw + 404 favicons
* Anonymous - empty favicon
* Seiba - chrome quick reply focusing
* herpaderpderp - recaptcha fixes
* WakiMiko - recaptcha tab order http://userscripts.org/scripts/show/82657
* btmcsweeney - allow users to specify text for sauce links
*
* All the people who've taken the time to write bug reports.
*
* Thank you.
*/
"""
option '-v', '--version [version]', 'Upgrade version.'
task 'upgrade', (options) ->
{version} = options
unless version
console.warn 'Version argument not specified. Exiting.'
return
regexp = RegExp VERSION, 'g'
for file in [CAKEFILE, INFILE, OUTFILE, LATEST]
data = fs.readFileSync(file, 'utf8').replace regexp, version
fs.writeFileSync file, data
# data = fs.readFileSync CHANGELOG, 'utf8'
# fs.writeFileSync CHANGELOG, data.replace 'master', "master\n\n#{version}"
exec "git commit -am 'Release #{version}.' && git tag -a #{version} -m '#{version}' && git tag -af stable -m '#{version}'"
task 'build', ->
exec 'coffee --print script.coffee', (err, stdout, stderr) ->
throw err if err
fs.writeFile OUTFILE, HEADER + stdout, (err) ->
throw err if err
task 'dev', ->
invoke 'build'
fs.watchFile INFILE, interval: 250, (curr, prev) ->
if curr.mtime > prev.mtime
invoke 'build'

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2009-2011 James Campos <james.r.campos@gmail.com>
Copyright (c) 2012 Nicolas Stepien <stepien.nicolas@gmail.com>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

9
README.md Normal file
View File

@ -0,0 +1,9 @@
# Get 4chan X [HERE](http://mayhemydg.github.com/4chan-x/).
***
## [MIT License](/4chan-x/blob/master/LICENSE)
***
## [Contribute](/4chan-x/blob/master/LICENSE)

158
css/style.css Normal file
View File

@ -0,0 +1,158 @@
/* general */
.dialog.reply {
display: block;
border: 1px solid rgba(0, 0, 0, .25);
padding: 0;
}
.move {
cursor: move;
}
label {
cursor: pointer;
}
a[href="javascript:;"] {
text-decoration: none;
}
.warning {
color: red;
}
/* 4chan style fixes */
.opContainer, .op {
display: block !important;
}
.post {
overflow: visible !important;
}
/* fixed, z-index */
#qp, #ihover,
#updater, #stats,
#boardNavDesktop.reply,
#qr, #watcher {
position: fixed;
}
#qp, #ihover {
z-index: 100;
}
#updater, #stats {
z-index: 90;
}
#boardNavDesktop.reply:hover {
z-index: 80;
}
#qr {
z-index: 50;
}
#watcher {
z-index: 30;
}
#boardNavDesktop.reply {
z-index: 10;
}
/* header */
body.fourchan_x {
margin-top: 2.5em;
}
#boardNavDesktop.reply {
border-width: 0 0 1px;
padding: 4px;
top: 0;
right: 0;
left: 0;
transition: opacity .1s ease-in-out;
-o-transition: opacity .1s ease-in-out;
-moz-transition: opacity .1s ease-in-out;
-webkit-transition: opacity .1s ease-in-out;
}
#boardNavDesktop.reply:not(:hover) {
opacity: .4;
transition: opacity 1.5s .5s ease-in-out;
-o-transition: opacity 1.5s .5s ease-in-out;
-moz-transition: opacity 1.5s .5s ease-in-out;
-webkit-transition: opacity 1.5s .5s ease-in-out;
}
#boardNavDesktop.reply a {
margin: -1px;
}
#settings {
float: right;
}
/* thread updater */
#updater {
text-align: right;
}
#updater:not(:hover) {
background: none;
border: none;
}
#updater input[type=number] {
width: 4em;
}
#updater:not(:hover) > div:not(.move) {
display: none;
}
.new {
color: limegreen;
}
/* quote */
.quotelink.deadlink {
text-decoration: underline !important;
}
.deadlink:not(.quotelink) {
text-decoration: none !important;
}
.inlined {
opacity: .5;
}
#qp input, .forwarded {
display: none;
}
.quotelink.forwardlink,
.backlink.forwardlink {
text-decoration: none;
border-bottom: 1px dashed;
}
.inline {
border: 1px solid rgba(128, 128, 128, .5);
display: table;
margin: 2px 0;
}
.inline .post {
border: 0 !important;
display: table !important;
margin: 0 !important;
padding: 1px 2px !important;
}
#qp {
padding: 2px 2px 5px;
}
#qp .post {
border: none;
margin: 0;
padding: 0;
}
#qp img {
max-height: 300px;
max-width: 500px;
}
.qphl {
box-shadow: 0 0 0 2px rgba(216, 94, 49, .7);
}
/* file */
.fileText:hover .fntrunc,
.fileText:not(:hover) .fnfull {
display: none;
}
#ihover {
box-sizing: border-box;
-moz-box-sizing: border-box;
max-height: 100%;
max-width: 75%;
padding-bottom: 16px;
}

120
grunt.js Normal file
View File

@ -0,0 +1,120 @@
module.exports = function(grunt) {
// Some tasks do not support directives.
var meta = {
name: '4chan X Alpha',
version: '3.0.0',
};
// Project configuration.
grunt.initConfig({
meta: {
name: meta.name,
version: meta.version,
repo: 'https://github.com/MayhemYDG/4chan-x/',
banner: [
'/* <%= meta.name %> - Version <%= meta.version %> - <%= grunt.template.today("yyyy-mm-dd") %>',
' * http://mayhemydg.github.com/4chan-x/',
' *',
' * Copyright (c) 2009-2011 James Campos <james.r.campos@gmail.com>',
' * Copyright (c) <%= grunt.template.today("yyyy") %> Nicolas Stepien <stepien.nicolas@gmail.com>',
' * Licensed under the MIT license.',
' * <%= meta.repo %>blob/master/LICENSE',
' *',
' * Contributors:',
' * <%= meta.repo %>graphs/contributors',
' * ferongr, xat-, Ongpot, thisisanon and Anonymous - cooldown sanity check',
' * e000 - cooldown sanity check',
' * Seiba - chrome quick reply focusing',
' * herpaderpderp - recaptcha fixes',
' * WakiMiko - recaptcha tab order http://userscripts.org/scripts/show/82657',
' *',
' * All the people who\'ve taken the time to write bug reports.',
' *',
' * Thank you.',
' */'
].join('\n'),
metadataBlock: [
'// ==UserScript==',
'// @name <%= meta.name %>',
'// @version <%= meta.version %>',
'// @description Adds various features.',
'// @copyright 2009-2011 James Campos <james.r.campos@gmail.com>',
'// @copyright <%= grunt.template.today("yyyy") %> Nicolas Stepien <stepien.nicolas@gmail.com>',
'// @license MIT; http://en.wikipedia.org/wiki/Mit_license',
'// @match *://boards.4chan.org/*',
'// @match *://images.4chan.org/*',
'// @match *://sys.4chan.org/*',
'// @match *://api.4chan.org/*',
'// @match *://*.foolz.us/api/*',
'// @grant GM_getValue',
'// @grant GM_setValue',
'// @grant GM_deleteValue',
'// @grant GM_openInTab',
'// @run-at document-start',
'// @updateURL <%= meta.repo %>raw/stable/<%= meta.files.metajs %>',
'// @downloadURL <%= meta.repo %>raw/stable/<%= meta.files.userjs %>',
'// @icon <%= meta.repo %>raw/stable/img/icon.gif',
'// ==/UserScript=='
].join('\n'),
latest: 'document.dispatchEvent(new CustomEvent("<%= meta.name.replace(/ /g, "") %>Update",{detail:{v:"<%= meta.version %>"}}))',
files: {
metajs: '4chan_x.meta.js',
userjs: '4chan_x.user.js',
latestjs: 'latestv3.js'
},
},
concat: {
coffee: {
src: [
'<file_template:src/config.coffee>',
'<file_template:lib/ui.coffee>',
'<file_template:lib/$.coffee>',
'<file_template:src/globals.coffee>',
'<file_template:src/main.coffee>',
'<file_template:src/features.coffee>'
],
dest: 'tmp/script.coffee'
},
js: {
src: ['<banner:meta.metadataBlock>', '<banner:meta.banner>', 'tmp/script.js'],
dest: '<config:meta.files.userjs>'
},
meta: {
src: '<banner:meta.metadataBlock>',
dest: '<config:meta.files.metajs>'
},
latest: {
src: '<banner:meta.latest>',
dest: '<config:meta.files.latestjs>'
}
},
exec: {
coffee: {
command: 'coffee --compile tmp/script.coffee',
stdout: true
},
commit: {
command: [
'git commit -am "Release ' + meta.name + ' v' + meta.version + '."',
'git tag -a ' + meta.version + ' -m "' + meta.version + '"',
'git tag -af stable -m "' + meta.version + '"'
].join(' && '),
stdout: true
},
clean: {
command: 'rm -r tmp'
}
},
watch: {
files: ['grunt.js', 'lib/**/*.coffee', 'src/**/*.coffee', 'css/**/*.css', 'img/*'],
tasks: 'coffee concat:build'
}
});
grunt.loadNpmTasks('grunt-exec');
grunt.registerTask('default', 'concat:coffee exec:coffee concat:js exec:clean');
grunt.registerTask('upgrade', 'concat:meta concat:latest default exec:commit');
};

BIN
img/icon.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

1
latestv3.js Normal file
View File

@ -0,0 +1 @@
document.dispatchEvent(new CustomEvent("4chanXAlphaUpdate",{detail:{v:"3.0.0"}}))

230
lib/$.coffee Normal file
View File

@ -0,0 +1,230 @@
# loosely follows the jquery api:
# http://api.jquery.com/
# not chainable
$ = (selector, root=d.body) ->
root.querySelector selector
$$ = (selector, root=d.body) ->
Array::slice.call root.querySelectorAll selector
$.extend = (object, properties) ->
for key, val of properties
object[key] = val
return
$.extend $,
SECOND: 1000
MINUTE: 1000 * 60
HOUR : 1000 * 60 * 60
DAY : 1000 * 60 * 60 * 24
log: console.log.bind console
engine: /WebKit|Presto|Gecko/.exec(navigator.userAgent)[0].toLowerCase()
id: (id) ->
d.getElementById id
ready: (fc) ->
if /interactive|complete/.test d.readyState
$.queueTask fc
return
cb = ->
$.off d, 'DOMContentLoaded', cb
fc()
$.on d, 'DOMContentLoaded', cb
sync: (key, cb) ->
$.on window, 'storage', (e) ->
if e.key is "#{g.NAMESPACE}#{key}"
cb JSON.parse e.newValue
formData: (form) ->
if form instanceof HTMLFormElement
return new FormData form
fd = new FormData()
for key, val of form
fd.append key, val if val
fd
ajax: (url, callbacks, opts={}) ->
{type, headers, upCallbacks, form} = opts
r = new XMLHttpRequest()
type or= form and 'post' or 'get'
r.open type, url, true
for key, val of headers
r.setRequestHeader key, val
$.extend r, callbacks
$.extend r.upload, upCallbacks
r.withCredentials = type is 'post'
r.send form
r
cache: (->
reqs = {}
(url, cb) ->
if req = reqs[url]
if req.readyState is 4
cb.call req
else
req.callbacks.push cb
return
req = $.ajax url,
onload: ->
cb.call @ for cb in @callbacks
delete @callbacks
onabort: -> delete reqs[url]
onerror: -> delete reqs[url]
req.callbacks = [cb]
reqs[url] = req
)()
cb:
checked: ->
$.set @name, @checked
Conf[@name] = @checked
value: ->
$.set @name, @value.trim()
Conf[@name] = @value
addStyle: (css) ->
style = $.el 'style',
textContent: css
# That's terrible.
# XXX tmp fix for scriptish:
# https://github.com/scriptish/scriptish/issues/16
f = ->
# XXX Only Chrome has no d.head on document-start.
if root = d.head or d.documentElement
$.add root, style
else
setTimeout f, 20
f()
style
x: (path, root=d.body) ->
# XPathResult.ANY_UNORDERED_NODE_TYPE === 8
d.evaluate(path, root, null, 8, null).singleNodeValue
addClass: (el, className) ->
el.classList.add className
rmClass: (el, className) ->
el.classList.remove className
hasClass: (el, className) ->
el.classList.contains className
rm: (el) ->
el.parentNode.removeChild el
tn: (s) ->
d.createTextNode s
nodes: (nodes) ->
# In (at least) Chrome, elements created inside different
# scripts/window contexts inherit from unequal prototypes.
# window_context1.Node !== window_context2.Node
unless nodes instanceof Array
return nodes
frag = d.createDocumentFragment()
for node in nodes
frag.appendChild node
frag
add: (parent, el) ->
parent.appendChild $.nodes el
prepend: (parent, el) ->
parent.insertBefore $.nodes(el), parent.firstChild
after: (root, el) ->
root.parentNode.insertBefore $.nodes(el), root.nextSibling
before: (root, el) ->
root.parentNode.insertBefore $.nodes(el), root
replace: (root, el) ->
root.parentNode.replaceChild $.nodes(el), root
el: (tag, properties) ->
el = d.createElement tag
$.extend el, properties if properties
el
on: (el, events, handler) ->
for event in events.split ' '
el.addEventListener event, handler, false
return
off: (el, events, handler) ->
for event in events.split ' '
el.removeEventListener event, handler, false
return
open: (url) ->
(GM_openInTab or window.open) url, '_blank'
queueTask: (->
# inspired by https://www.w3.org/Bugs/Public/show_bug.cgi?id=15007
taskQueue = []
execTask = ->
task = taskQueue.shift()
func = task[0]
args = Array::slice.call task, 1
func.apply func, args
if window.MessageChannel
taskChannel = new MessageChannel()
taskChannel.port1.onmessage = execTask
->
taskQueue.push arguments
taskChannel.port2.postMessage null
else # XXX Firefox
->
taskQueue.push arguments
setTimeout execTask, 0
)()
globalEval: (code) ->
script = $.el 'script',
textContent: code
$.add d.head, script
$.rm script
# http://mths.be/unsafewindow
unsafeWindow:
if window.opera # Opera
window
else if unsafeWindow isnt window # Firefox
unsafeWindow
else # Chrome
(->
p = d.createElement 'p'
p.setAttribute 'onclick', 'return window'
p.onclick()
)()
bytesToString: (size) ->
unit = 0 # Bytes
while size >= 1024
size /= 1024
unit++
# Remove trailing 0s.
size =
if unit > 1
# Keep the size as a float if the size is greater than 2^20 B.
# Round to hundredth.
Math.round(size * 100) / 100
else
# Round to an integer otherwise.
Math.round size
"#{size} #{['B', 'KB', 'MB', 'GB'][unit]}"
$.extend $,
if GM_deleteValue?
delete: (name) ->
GM_deleteValue g.NAMESPACE + name
get: (name, defaultValue) ->
if value = GM_getValue g.NAMESPACE + name
JSON.parse value
else
defaultValue
set: (name, value) ->
name = g.NAMESPACE + name
value = JSON.stringify value
# for `storage` events
localStorage.setItem name, value
GM_setValue name, value
else if window.opera
delete: (name)->
delete opera.scriptStorage[g.NAMESPACE + name]
get: (name, defaultValue) ->
if value = opera.scriptStorage[g.NAMESPACE + name]
JSON.parse value
else
defaultValue
set: (name, value) ->
name = g.NAMESPACE + name
value = JSON.stringify value
# for `storage` events
localStorage.setItem name, value
opera.scriptStorage[name] = value
else
delete: (name) ->
localStorage.removeItem g.NAMESPACE + name
get: (name, defaultValue) ->
if value = localStorage.getItem g.NAMESPACE + name
JSON.parse value
else
defaultValue
set: (name, value) ->
localStorage.setItem g.NAMESPACE + name, JSON.stringify value

129
lib/ui.coffee Normal file
View File

@ -0,0 +1,129 @@
UI = (->
dialog = (id, position, html) ->
el = d.createElement 'div'
el.className = 'reply dialog'
el.innerHTML = html
el.id = id
el.style.cssText = localStorage.getItem("#{g.NAMESPACE}#{id}.position") or position
move = el.querySelector '.move'
move.addEventListener 'touchstart', dragstart, false
move.addEventListener 'mousedown', dragstart, false
el
dragstart = (e) ->
# prevent text selection
e.preventDefault()
el = @parentNode
if isTouching = e.type is 'touchstart'
e = e.changedTouches[e.changedTouches.length - 1]
# distance from pointer to el edge is constant; calculate it here.
rect = el.getBoundingClientRect()
screenHeight = d.documentElement.clientHeight
screenWidth = d.documentElement.clientWidth
o = {
id: el.id
style: el.style
dx: e.clientX - rect.left
dy: e.clientY - rect.top
height: screenHeight - rect.height
width: screenWidth - rect.width
screenHeight: screenHeight
screenWidth: screenWidth
isTouching: isTouching
}
if isTouching
o.identifier = e.identifier
o.move = touchmove.bind o
o.up = touchend.bind o
d.addEventListener 'touchmove', o.move, false
d.addEventListener 'touchend', o.up, false
d.addEventListener 'touchcancel', o.up, false
else # mousedown
o.move = drag.bind o
o.up = dragend.bind o
d.addEventListener 'mousemove', o.move, false
d.addEventListener 'mouseup', o.up, false
touchmove = (e) ->
for touch in e.changedTouches
if touch.identifier is @identifier
drag.call @, touch
return
drag = (e) ->
left = e.clientX - @dx
top = e.clientY - @dy
if left < 10 then left = 0
else if @width - left < 10 then left = null
if top < 10 then top = 0
else if @height - top < 10 then top = null
if left is null
@style.left = null
@style.right = '0%'
else
@style.left = left / @screenWidth * 100 + '%'
@style.right = null
if top is null
@style.top = null
@style.bottom = '0%'
else
@style.top = top / @screenHeight * 100 + '%'
@style.bottom = null
touchend = (e) ->
for touch in e.changedTouches
if touch.identifier is @identifier
dragend.call @
return
dragend = ->
if @isTouching
d.removeEventListener 'touchmove', @move, false
d.removeEventListener 'touchend', @up, false
d.removeEventListener 'touchcancel', @up, false
else # mouseup
d.removeEventListener 'mousemove', @move, false
d.removeEventListener 'mouseup', @up, false
localStorage.setItem "#{g.NAMESPACE}#{@id}.position", @style.cssText
hoverstart = (root, el, events, cb) ->
o = {
root: root
el: el
style: el.style
cb: cb
events: events.split ' '
clientHeight: d.documentElement.clientHeight
clientWidth: d.documentElement.clientWidth
}
o.hover = hover.bind o
o.hoverend = hoverend.bind o
for event in o.events
root.addEventListener event, o.hoverend, false
root.addEventListener 'mousemove', o.hover, false
hover = (e) ->
height = @el.offsetHeight
top = e.clientY - height / 2
@style.top =
if @clientHeight <= height or top <= 0
'0px'
else if top + height >= @clientHeight
@clientHeight - height + 'px'
else
top + 'px'
{clientX} = e
if clientX <= @clientWidth - 400
@style.left = clientX + 45 + 'px'
@style.right = null
else
@style.left = null
@style.right = @clientWidth - clientX + 45 + 'px'
hoverend = ->
@el.parentNode.removeChild @el
for event in @events
@root.removeEventListener event, @hoverend, false
@root.removeEventListener 'mousemove', @hover, false
@cb.call @ if @cb
return {
dialog: dialog
hover: hoverstart
}
)()

View File

@ -1,22 +0,0 @@
# Get 4chan X [HERE](http://mayhemydg.github.com/4chan-x/).
# Building
- Install [node.js](http://nodejs.org/).
- Install [CoffeeScript](http://coffeescript.org/) with `npm install -g coffee-script`.
- Clone 4chan X.
- `cd` into it and build with `cake build`.
- For development (continuous builds), run `cake dev &`. Kill the process with `killall node`.
# Releasing
- Upgrade version with `cake -v VERSION upgrade`. Note that this is only used to
release new 4chan x versions, and is not needed or wanted in pull requests.
# Contributing
- Edit the CoffeeScript source
- Build the JavaScript
- If the edits affect regular users, edit the changelog
- Fork the repo
- Send a pull request

163
src/config.coffee Normal file
View File

@ -0,0 +1,163 @@
Config =
main:
Enhancing:
'Disable 4chan\'s extension': [true, 'Avoid conflicts between <%= meta.name %> and 4chan\'s inline extension.']
'404 Redirect': [true, 'Redirect dead threads and images.']
'Keybinds': [true, 'Bind actions to keyboard shortcuts.']
'Time Formatting': [true, 'Localize and format timestamps arbitrarily.']
'File Info Formatting': [true, 'Reformat the file information.']
'Comment Expansion': [true, 'Can expand too long comments.']
'Thread Expansion': [true, 'Can expand threads to view all replies.']
'Index Navigation': [false, 'Navigate to previous / next thread.']
'Reply Navigation': [false, 'Navigate to top / bottom of thread.']
'Check for Updates': [true, 'Check for updated versions of <%= meta.name %>.']
Filtering:
'Anonymize': [false, 'Turn everyone Anonymous.']
'Filter': [true, 'Self-moderation placebo.']
'Recursive Filtering': [true, 'Filter replies of filtered posts, recursively.']
'Reply Hiding': [true, 'Hide single replies.']
'Thread Hiding': [true, 'Hide entire threads.']
'Stubs': [true, 'Make stubs of hidden threads / replies.']
Imaging:
'Auto-GIF': [false, 'Animate GIF thumbnails.']
'Image Expansion': [true, 'Expand images.']
'Expand From Position': [true, 'Expand all images only from current position to thread end.']
'Image Hover': [false, 'Show full image on mouseover.']
'Sauce': [true, 'Add sauce links to images.']
'Reveal Spoilers': [false, 'Reveal spoiler thumbnails.']
Menu:
'Menu': [true, 'Add a drop-down menu in posts.']
'Report Link': [true, 'Add a report link to the menu.']
'Delete Link': [true, 'Add post and image deletion links to the menu.']
'Download Link': [true, 'Add a download with original filename link to the menu. Chrome-only currently.']
'Archive Link': [true, 'Add an archive link to the menu.']
Monitoring:
'Thread Updater': [true, 'Fetch and insert new replies. Has more options in its own dialog.']
'Unread Count': [true, 'Show the unread posts count in the tab title.']
'Unread Favicon': [true, 'Show a different favicon when there are unread posts.']
'Post in Title': [true, 'Show the thread\'s subject in the tab title.']
'Thread Stats': [true, 'Display reply and image count.']
'Thread Watcher': [true, 'Bookmark threads.']
'Auto Watch': [true, 'Automatically watch threads that you start.']
'Auto Watch Reply': [false, 'Automatically watch threads that you reply to.']
Posting:
'Quick Reply': [true, 'WMD.']
'Persistent QR': [false, 'The Quick reply won\'t disappear after posting.']
'Auto Hide QR': [false, 'Automatically hide the quick reply when posting.']
'Open Reply in New Tab': [false, 'Open replies posted from the board pages in a new tab.']
'Remember Subject': [false, 'Remember the subject field, instead of resetting after posting.']
'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.']
'Hide Original Post Form': [true, 'Replace the normal post form with a shortcut to open the QR.']
Quoting:
'Quote Backlinks': [true, 'Add quote backlinks.']
'OP Backlinks': [false, 'Add backlinks to the OP.']
'Quote Inline': [true, 'Inline quoted post on click.']
'Forward Hiding': [true, 'Hide original posts of inlined backlinks.']
'Quote Preview': [true, 'Show quoted post on hover.']
'Quote Highlighting': [true, 'Highlight the previewed post.']
'Resurrect Quotes': [true, 'Linkify dead quotes to archives.']
'Indicate OP Quotes': [true, 'Add \'(OP)\' to OP quotes.']
'Indicate Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes.']
filter:
name: [
'# Filter any namefags:'
'#/^(?!Anonymous$)/'
].join '\n'
uniqueid: [
'# Filter a specific ID:'
'#/Txhvk1Tl/'
].join '\n'
tripcode: [
'# Filter any tripfags'
'#/^!/'
].join '\n'
capcode: [
'# Set a custom class for mods:'
'#/Mod$/;highlight:mod;op:yes'
'# Set a custom class for moot:'
'#/Admin$/;highlight:moot;op:yes'
].join '\n'
email: [
'# Filter any e-mails that are not `sage` on /a/ and /jp/:'
'#/^(?!sage$)/;boards:a,jp'
].join '\n'
subject: [
'# Filter Generals on /v/:'
'#/general/i;boards:v;op:only'
].join '\n'
comment: [
'# Filter Stallman copypasta on /g/:'
'#/what you\'re refer+ing to as linux/i;boards:g'
].join '\n'
flag: [
''
].join '\n'
filename: [
''
].join '\n'
dimensions: [
'# Highlight potential wallpapers:'
'#/1920x1080/;op:yes;highlight;top:no;boards:w,wg'
].join '\n'
filesize: [
''
].join '\n'
md5: [
''
].join '\n'
sauces: [
'http://iqdb.org/?url=%turl'
'http://www.google.com/searchbyimage?image_url=%turl'
'#http://tineye.com/search?url=%turl'
'#http://saucenao.com/search.php?db=999&url=%turl'
'#http://3d.iqdb.org/?url=%turl'
'#http://regex.info/exif.cgi?imgurl=%url'
'# uploaders:'
'#http://imgur.com/upload?url=%url;text:Upload to imgur'
'#http://omploader.org/upload?url1=%url;text:Upload to omploader'
'# "View Same" in archives:'
'#//archive.foolz.us/_/search/image/%md5/;text:View same on foolz'
'#//archive.foolz.us/%board/search/image/%md5/;text:View same on foolz /%board/'
'#//archive.installgentoo.net/%board/image/%md5;text:View same on installgentoo /%board/'
].join '\n'
time: '%m/%d/%y(%a)%H:%M:%S'
backlink: '>>%id'
fileInfo: '%l (%p%s, %r)'
favicon: 'ferongr'
hotkeys:
# QR & Options
'open QR': ['q', 'Open QR with post number inserted.']
'open empty QR': ['Q', 'Open QR without post number inserted.']
'open options': ['alt+o', 'Open Options.']
'close': ['Esc', 'Close Options or QR.']
'spoiler tags': ['ctrl+s', 'Insert spoiler tags.']
'code tags': ['alt+c', 'Insert code tags.']
'submit QR': ['alt+s', 'Submit post.']
# Thread related
'watch': ['w', 'Watch thread.']
'update': ['u', 'Update the thread now.']
'read thread': ['r', 'Mark thread as read.']
# Images
'expand image': ['E', 'Expand selected image.']
'expand images': ['e', 'Expand all images.']
# Board Navigation
'front page': ['0', 'Jump to page 0.']
'next page': ['Right', 'Jump to the next page.']
'previous page': ['Left', 'Jump to the previous page.']
# Thread Navigation
'next thread': ['Down', 'See next thread.']
'previous thread': ['Up', 'See previous thread.']
'expand thread': ['ctrl+e', 'Expand thread.']
'open thread': ['o', 'Open thread in current tab.']
'open thread tab': ['O', 'Open thread in new tab.']
# Reply Navigation
'next reply': ['j', 'Select next reply.']
'previous reply': ['k', 'Select previous reply.']
'hide': ['x', 'Hide thread.']
updater:
checkbox:
'Auto Scroll': [false, 'Scroll updated posts into view. Only enabled at bottom of page.']
'Scroll BG': [false, 'Auto-scroll background tabs.']
'Auto Update': [true, 'Automatically fetch new posts.']
'Interval': 30
imageFit: 'fit width'

File diff suppressed because it is too large Load Diff

12
src/globals.coffee Normal file
View File

@ -0,0 +1,12 @@
# Opera doesn't support the @match metadata key,
# return 4chan X here if we're not on 4chan.
return unless /^(boards|images|sys)\.4chan\.org$/.test location.hostname
Conf = {}
d = document
g =
VERSION: '<%= meta.version %>'
NAMESPACE: "<%= meta.name.replace(/ /g, '_') %>."
boards: {}
threads: {}
posts: {}

458
src/main.coffee Normal file
View File

@ -0,0 +1,458 @@
class Board
toString: -> @ID
constructor: (@ID) ->
@threads = {}
@posts = {}
g.boards[@] = @
class Thread
callbacks: []
toString: -> @ID
constructor: (ID, @board) ->
@ID = +ID
@posts = {}
# XXX Can't check when parsing single posts
# move to Post constructor? unless @isReply
# postInfo = $ '.postInfo', root.firstElementChild
# @isClosed = !!$ 'img[title=Closed]', postInfo
# @isSticky = !!$ 'img[title=Sticky]', postInfo
g.threads["#{board}.#{@}"] = board.threads[@] = @
class Post
callbacks: []
toString: -> @ID
constructor: (root, @thread, @board, that={}) ->
@ID = +root.id[2..]
post = $ '.post', root
info = $ '.postInfo', post
@nodes =
root: root
post: post
info: info
comment: $ '.postMessage', post
quotelinks: []
backlinks: info.getElementsByClassName 'backlink'
@info = {}
if subject = $ '.subject', info
@nodes.subject = subject
@info.subject = subject.textContent
if name = $ '.name', info
@nodes.name = name
@info.name = name.textContent
if email = $ '.useremail', info
@nodes.email = email
@info.email = decodeURIComponent email.href[7..]
if tripcode = $ '.postertrip', info
@nodes.tripcode = tripcode
@info.tripcode = tripcode.textContent
if uniqueID = $ '.posteruid', info
@nodes.uniqueID = uniqueID
@info.uniqueID = uniqueID.textContent
if capcode = $ '.capcode', info
@nodes.capcode = capcode
@info.capcode = capcode.textContent
if flag = $ '.countryFlag', info
@nodes.flag = flag
@info.flag = flag.title
if date = $ '.dateTime', info
@nodes.date = date
@info.date = new Date date.dataset.utc * 1000
# Get the comment's text.
# <br> -> \n
# Remove:
# 'Comment too long'...
# Admin/Mod/Dev replies. (/q/)
# EXIF data. (/p/)
# Rolls. (/tg/)
# Preceding and following new lines.
# Trailing spaces.
bq = @nodes.comment.cloneNode true
for node in $$ '.abbr, .capcodeReplies, .exif, b', bq
$.rm node
text = []
# XPathResult.ORDERED_NODE_SNAPSHOT_TYPE === 7
nodes = d.evaluate './/br|.//text()', bq, null, 7, null
for i in [0...nodes.snapshotLength]
text.push if data = nodes.snapshotItem(i).data then data else '\n'
@info.comment = text.join('').replace /^\n+|\n+$| +(?=\n|$)/g, ''
quotes = {}
for quotelink in $$ '.quotelink', @nodes.comment
# Don't add board links. (>>>/b/)
# Don't add text-board quotelinks. (>>>/img/1234)
# Don't count capcode replies as quotes. (Admin/Mod/Dev Replies: ...)
# Only add quotes that link to posts on an imageboard.
if quotelink.hash
@nodes.quotelinks.push quotelink
continue if quotelink.parentNode.parentNode.className is 'capcodeReplies'
quotes["#{quotelink.pathname.split('/')[1]}.#{quotelink.hash[2..]}"] = true
@quotes = Object.keys quotes
if (file = $ '.file', post) and thumb = $ 'img[data-md5]', file
# Supports JPG/PNG/GIF/PDF.
# Flash files are not supported.
alt = thumb.alt
anchor = thumb.parentNode
fileInfo = file.firstElementChild
@file =
info: fileInfo
text: fileInfo.firstElementChild
thumb: thumb
URL: anchor.href
MD5: thumb.dataset.md5
isSpoiler: $.hasClass anchor, 'imgspoiler'
size = +alt.match(/\d+(\.\d+)?/)[0]
unit = ['B', 'KB', 'MB', 'GB'].indexOf alt.match(/\w+$/)[0]
while unit--
size *= 1024
@file.size = size
@file.thumbURL =
if that.isArchived
thumb.src
else
"#{location.protocol}//thumbs.4chan.org/#{board}/thumb/#{@file.URL.match(/(\d+)\./)[1]}s.jpg"
# replace %22 with quotes, see:
# crbug.com/81193
# webk.it/62107
# https://www.w3.org/Bugs/Public/show_bug.cgi?id=16909
# http://www.whatwg.org/specs/web-apps/current-work/#multipart-form-data
@file.name = $('span[title]', fileInfo).title.replace /%22/g, '"'
if @file.isImage = /(jpg|png|gif)$/i.test @file.name
@file.dimensions = @file.text.textContent.match(/\d+x\d+/)[0]
@isReply = $.hasClass post, 'reply'
@clones = []
g.posts["#{board}.#{@}"] = thread.posts[@] = board.posts[@] = @
@kill() if that.isArchived
kill: (img) ->
if @file and !@file.isDead
@file.isDead = true
return if img
@isDead = true
$.addClass @nodes.root, 'dead'
# XXX style dead posts.
# Get quote/backlinks to this post,
# and paint them (Dead).
# First:
# In every posts,
# if it did quote this post,
# get all their backlinks.
# Second:
# If we have quote backlinks,
# in all posts this post quoted,
# and their clones,
# get all of their backlinks.
# Third:
# In all collected links,
# apply (Dead) if relevant.
quotelinks = []
num = "#{@board}.#{@}"
for ID, post of g.posts
if -1 < post.quotes.indexOf num
for post in [post].concat post.clones
quotelinks.push.apply quotelinks, post.nodes.quotelinks
if Conf['Quote Backlinks']
for quote in @quotes
post = g.posts[quote]
for post in [post].concat post.clones
quotelinks.push.apply quotelinks, Array::slice.call post.nodes.backlinks
for quotelink in quotelinks
continue if $.hasClass quotelink, 'deadlink'
{board, postID} = Get.postDataFromLink quotelink
if board is @board.ID postID is @ID
$.add quotelink, $.tn '\u00A0(Dead)'
$.addClass quotelinks, 'deadlink'
return
addClone: (context) ->
new Clone @, context
rmClone: (index) ->
@clones.splice index, 1
for clone in @clones[index..]
clone.nodes.root.setAttribute 'data-clone', index++
return
class Clone extends Post
constructor: (@origin, @context) ->
for key in ['ID', 'board', 'thread', 'info', 'quotes', 'isReply']
# Copy or point to the origin's key value.
@[key] = origin[key]
{nodes} = origin
root = nodes.root.cloneNode true
post = $ '.post', root
info = $ '.postInfo', post
@nodes =
root: root
post: post
info: info
comment: $ '.postMessage', post
quotelinks: []
backlinks: info.getElementsByClassName 'backlink'
# Remove inlined posts inside of this post.
for inline in $$ '.inline', post
$.rm inline
for inlined in $$ '.inlined', post
$.rmClass inlined, 'inlined'
# root.hidden = false # post hiding
$.rmClass root, 'forwarded' # quote inlining
# $.rmClass post, 'highlight' # keybind navigation
if nodes.subject
@nodes.subject = $ '.subject', info
if nodes.name
@nodes.name = $ '.name', info
if nodes.email
@nodes.email = $ '.useremail', info
if nodes.tripcode
@nodes.tripcode = $ '.postertrip', info
if nodes.uniqueID
@nodes.uniqueID = $ '.posteruid', info
if nodes.capcode
@nodes.capcode = $ '.capcode', info
if nodes.flag
@nodes.flag = $ '.countryFlag', info
if nodes.date
@nodes.date = $ '.dateTime', info
for quotelink in $$ '.quotelink', @nodes.comment
# See comments in Post's constructor.
if quotelink.hash or $.hasClass quotelink, 'deadlink'
@nodes.quotelinks.push quotelink
if origin.file
# Copy values, point to relevant elements.
# See comments in Post's constructor.
@file = {}
for key, val of origin.file
@file[key] = val
file = $ '.file', post
@file.info = file.firstElementChild
@file.text = @file.info.firstElementChild
@file.thumb = $ 'img[data-md5]', file
@isDead = true if origin.isDead
@isClone = true
index = origin.clones.push(@) - 1
root.setAttribute 'data-clone', index
Main =
init: ->
# flatten Config into Conf
# and get saved or default values
flatten = (parent, obj) ->
if obj instanceof Array
Conf[parent] = obj[0]
else if typeof obj is 'object'
for key, val of obj
flatten key, val
else # string or number
Conf[parent] = obj
return
flatten null, Config
for key, val of Conf
Conf[key] = $.get key, val
pathname = location.pathname.split '/'
g.BOARD = new Board pathname[1]
if g.REPLY = pathname[2] is 'res'
g.THREAD = +pathname[3]
switch location.hostname
when 'boards.4chan.org'
Main.initHeader()
Main.initFeatures()
when 'sys.4chan.org'
return
when 'images.4chan.org'
$.ready ->
if Conf['404 Redirect'] and d.title is '4chan - 404 Not Found'
path = location.pathname.split '/'
url = Redirect.image path[1], path[3]
location.href = url if url
return
initHeader: ->
$.addStyle Main.css
Main.header = $.el 'div',
className: 'reply'
innerHTML: '<div class=extra></div>'
$.ready Main.initHeaderReady
initHeaderReady: ->
header = Main.header
$.prepend d.body, header
if nav = $.id 'boardNavDesktop'
header.id = nav.id
$.prepend header, nav
nav.id = nav.className = null
nav.lastElementChild.hidden = true
settings = $.el 'span',
id: 'settings'
innerHTML: '[<a href=javascript:;>Settings</a>]'
$.on settings.firstElementChild, 'click', Main.settings
$.add nav, settings
$("a[href$='/#{g.BOARD}/']", nav)?.className = 'current'
$.addClass d.body, $.engine
$.addClass d.body, 'fourchan_x'
# disable the mobile layout
$('link[href*=mobile]', d.head)?.disabled = true
$.id('boardNavDesktopFoot')?.hidden = true
initFeatures: ->
if Conf['Disable 4chan\'s extension']
settings = JSON.parse(localStorage.getItem '4chan-settings') or {}
settings.disableAll = true
localStorage.setItem '4chan-settings', JSON.stringify settings
if Conf['Resurrect Quotes']
try
Quotify.init()
catch err
# XXX handle error
$.log err, 'Resurrect Quotes'
if Conf['Quote Inline']
try
QuoteInline.init()
catch err
# XXX handle error
$.log err, 'Quote Inline'
if Conf['Quote Preview']
try
QuotePreview.init()
catch err
# XXX handle error
$.log err, 'Quote Preview'
if Conf['Quote Backlinks']
try
QuoteBacklink.init()
catch err
# XXX handle error
$.log err, 'Quote Backlinks'
if Conf['Indicate OP Quotes']
try
QuoteOP.init()
catch err
# XXX handle error
$.log err, 'Indicate OP Quotes'
if Conf['Indicate Cross-thread Quotes']
try
QuoteCT.init()
catch err
# XXX handle error
$.log err, 'Indicate Cross-thread Quotes'
if Conf['Time Formatting']
try
Time.init()
catch err
# XXX handle error
$.log err, 'Time Formatting'
if Conf['File Info Formatting']
try
FileInfo.init()
catch err
# XXX handle error
$.log err, 'File Info Formatting'
if Conf['Sauce']
try
Sauce.init()
catch err
# XXX handle error
$.log err, 'Sauce'
if Conf['Reveal Spoilers']
try
RevealSpoilers.init()
catch err
# XXX handle error
$.log err, 'Reveal Spoilers'
if Conf['Auto-GIF']
try
AutoGIF.init()
catch err
# XXX handle error
$.log err, 'Auto-GIF'
if Conf['Image Hover']
try
ImageHover.init()
catch err
# XXX handle error
$.log err, 'Image Hover'
if Conf['Thread Updater']
try
ThreadUpdater.init()
catch err
# XXX handle error
$.log err, 'Thread Updater'
$.ready Main.initFeaturesReady
initFeaturesReady: ->
if d.title is '4chan - 404 Not Found'
if Conf['404 Redirect'] and g.REPLY
location.href = Redirect.thread g.BOARD, g.THREAD, location.hash
return
return unless $.id 'navtopright'
threads = []
posts = []
for boardChild in $('.board').children
continue unless $.hasClass boardChild, 'thread'
thread = new Thread boardChild.id[1..], g.BOARD
threads.push thread
for threadChild in boardChild.children
continue unless $.hasClass threadChild, 'postContainer'
try
posts.push new Post threadChild, thread, g.BOARD
catch err
# Skip posts that we failed to parse.
# XXX handle error
# Post parser crashed for post No.#{threadChild.id[2..]}
$.log threadChild, err
Main.callbackNodes Thread, threads, true
Main.callbackNodes Post, posts, true
callbackNodes: (klass, nodes, notify) ->
# get the nodes' length only once
len = nodes.length
for callback in klass::callbacks
try
for i in [0...len]
callback.cb.call nodes[i]
catch err
# XXX handle error if notify
$.log callback.name, 'crashed. error:', err.message, nodes[i], err
return
settings: ->
alert 'Here be settings'
css: """<%= grunt.file.read('css/style.css') %>"""