// ==UserScript==
// @name 314n.org improver
// @name:ru 314n.org improver
// @namespace 314n.org
// @version 1.0.2
// @match https://314n.org/*
// @match https://314n.ru/*
// @description Improves 314n.org user-expiriense.
// @description:ru Улучшает взаимодействие с 314n.org.
// @icon https://314n.org/f1.png
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @run-at document-end
// @license GPLv3
// ==/UserScript==
/*jshint esnext: true */
const TEXT_COLOR_RGB = 'rgb(95, 191, 255)';
const TEXT_COLOR_HEX = '#5FBFFF';
const TEXT_HIGHLIGHTED_COLOR = '#eee';
const EVENT_SUBMIT_ACCOUNT = 'event-submit-account';
const CLASS_BOARD_LINK = 'board-link';
const CLASS_THREAD_LINK = 'thread-link';
const CONTEXT_EMPTY = '';
const CONTEXT_LOGGED = 'logged';
const CONTEXT_BOARDS = 'boards';
const CONTEXT_BOARD = 'board';
const CONTEXT_TOPIC = 'topic';
var scriptElements = [];
var loginForm;
var regForm;
var log = console.log;
var ce = document.createElement.bind(document);
var cn = document.createTextNode.bind(document);
var boardEreg = /\[(\d+)\]\s+(.+)\s+/;
var bracketsEreg = /\[(\d+)\]/;
let underliner = (e)=>e.target.style.setProperty('text-decoration','underline');
let unUnderliner = (e)=>e.target.style.removeProperty('text-decoration');
let pointer = (e)=>e.target.style.cursor = 'pointer';
let unPointer = (e)=>e.target.style.removeProperty('cursor');
let textHighlight = (e)=>e.target.style.color = TEXT_HIGHLIGHTED_COLOR;
let unTextHighlight = (e)=>e.target.style.removeProperty('color');
var currentContext = '';
let commandsMap = new Map(); /* String -> Command*/
let checkCommandEReg = null;
let lastEreg = /last/i;
let lastPageNum = 9999;
$(document).ready(init);
function init(){
initCommands();
regForm = regEl(createRegForm());
let reg = createPanelButton('reg', regForm.id);
reg.addEventListener('click', showAccountForm);
loginForm = regEl(createLoginForm());
let login = createPanelButton('log-in', loginForm.id);
login.addEventListener('click', showAccountForm);
// create menu buttons wrapper
let btnWrap = regEl(ce('div'));
btnWrap.style.display = 'flex';
btnWrap.style.setProperty('display','flex');
btnWrap.style.setProperty('flex-flow','row nowrap');
btnWrap.style.setProperty('justify-content','flex-start');
btnWrap.style.position = 'absolute';
btnWrap.style.bottom = '100%';
btnWrap.style.right = '0';
btnWrap.style.zIndex = '10';
btnWrap.appendChild($(createPanelButton('boards')).click(e=>simulateInput('BOARDS')).get(0));
btnWrap.appendChild(reg);
btnWrap.appendChild(login);
btnWrap.appendChild($(createPanelButton('log-out')).click(e => simulateInput('LOGOUT')).get(0));
btnWrap.appendChild($(createPanelButton(' ? ')).click(e => simulateInput('HELP')).get(0));
$('#board').append(btnWrap);
$('#board').click(globalClick);
reinitCmd();
setTimeout(focusCmd, 1);
currentContext = CONTEXT_EMPTY;
}
function initCommands(){
// no args
addCommand(new CommandHelp());
addCommand(new CommandBoards());
addCommand(new Command('LOGOUT'));
addCommand(new CommandNav('NEXT'));
addCommand(new CommandNav('PREV'));
addCommand(new CommandNav('FIRST'));
addCommand(new CommandNav('LAST'));
addCommand(new CommandNav('REFRESH'));
// one arg
addCommand(new CommandTimezone());
addCommand(new CommandBoard());
addCommand(new CommandRvt());
addCommand(new CommandReply());
addCommand(new CommandOneArg('DELETE','p'));
addCommand(new CommandOneArg('EDIT','p'));
addCommand(new CommandPage());
// two args
addCommand(new CommandAccount('LOGIN','u','p'));
addCommand(new CommandAccount('REGISTER','u','p'));
addCommand(new CommandTopic());
///NOTE No NEWTOPIC command - there is no simple way
// to separate title from content without keys
//construct ereg to match command name
rebuildCommandsEReg();
}
function addCommand(cmd,/*Bool*/rebuild){
commandsMap.set(cmd.name,cmd);
if(rebuild)
rebuildCommandsEReg();
}
function rebuildCommandsEReg(){
let cmdsStr = '';
commandsMap.forEach((val,key)=>{
cmdsStr+= (key+'|');
});
cmdsStr = cmdsStr.substr(0, cmdsStr.length-1); // remove last | char from string
checkCommandEReg = new RegExp(`^\\s*(${cmdsStr})`,'i');
log(checkCommandEReg);
}
function reinitCmd(){
let cmd = el('#cmd');
let newCmd = cmd.cloneNode();
newCmd.dataset.new = 'yes';
cmd.parentElement.appendChild(newCmd);
$(cmd).remove();
cmd = newCmd;
$(cmd).keydown((e)=>{
if (e.keyCode == 38) {
if (command_number > 0) {
if (command_number == commands.length)
commands.push(e.currentTarget.value);
command_number--;
e.currentTarget.value = commands[command_number];
}
return false;
}
if (e.keyCode == 40) {
if (command_number < commands.length - 1) {
command_number++;
e.currentTarget.value = commands[command_number];
}
return false;
}
if((e.keyCode == 13 || e.key=='Enter' || e.code=='Enter') && !e.shiftKey){
let command = getCommandObj(cmd.value);
logInput(cmd.value);
sendCommand(command.processInput(cmd.value), command.processOutput.bind(command));
return false;
}
});
return cmd;
}
function getCommandObj(input){
let match = checkCommandEReg.exec(input);
if(match!==null){
let cmd = commandsMap.get(match[1].toLowerCase());
if(cmd) return cmd;
}
return new Command(input);
}
function globalClick(e){
if(e.target.classList.contains(CLASS_BOARD_LINK)){
simulateInput(`BOARD -n ${e.target.dataset.boardNumber}`);
}
else if(e.target.classList.contains(CLASS_THREAD_LINK)){
simulateInput(`TOPIC -n ${e.target.dataset.threadNumber}`);
}
}
function createLoginForm(){
return createAccountForm('LOGIN');
}
function createRegForm(){
let form = createAccountForm('REGISTER');
form.addEventListener(EVENT_SUBMIT_ACCOUNT,(e)=>{
let login = id('login-form');
let reg = e.currentTarget;
if(login!==null){
let lname = el('[type=text]',login);
let lpass = el('[type=password]',login);
let rname = el('[type=text]',reg);
let rpass = el('[type=password]',reg);
lname.value = rname.value;
lname.innerText = rname.innerText;
lpass.value = rpass.value;
lpass.innerText = rpass.innerText;
}
});
return form;
}
function createAccountForm(commandText){
let form = ce('div');
function getInput(type, name){
var inp = ce('input');
inp.setAttribute('type',type);
inp.setAttribute('name',name);
sanitizeInputStyle(inp);
inp.style.color = TEXT_COLOR_HEX;
inp.style.paddingBottom = '2px';
inp.style.borderBottom = `2px solid ${TEXT_COLOR_HEX}`;
inp.style.width = '10em';
return inp;
}
let nameInput = getInput('text','name');
let nameLabel = ce('label');
nameLabel.appendChild(createReverse('name: '));
nameLabel.appendChild(nameInput);
let passInput = getInput('password','pass');
let passLabel = ce('label');
passLabel.appendChild(createReverse('password: '));
passLabel.appendChild(passInput);
let label = createReverse(`[ ${commandText.toUpperCase()} ]`);
label.style.outline='none';
let ok = ce('button');
ok.appendChild(label);
ok.style.margin = '0 auto';
ok.style.marginTop = '15px';
ok.style.color = TEXT_COLOR_HEX;
ok.style.display = 'block';
sanitizeInputStyle(ok);
$(ok).hover((e)=>{
e.currentTarget.querySelector('span').style.backgroundColor = '#eee';
ok.style.cursor = 'pointer';
}, (e)=>{
e.currentTarget.querySelector('span').style.removeProperty('background-color');
ok.style.removeProperty('cursor');
});
function submit(e){
let name = el('[type=text]',form);
let pass = el('[type=password]',form);
simulateInput(`${commandText.toLowerCase()} -u ${name.value} -p ${pass.value}`);
form.style.display = 'none';
form.dispatchEvent(new Event(EVENT_SUBMIT_ACCOUNT));
}
ok.addEventListener('click',submit);
passInput.addEventListener('keydown',(evt)=>{
if(evt.keyCode===13)
submit();
});
form.id = `${commandText.toLowerCase()}-form`;
form.classList.add('acc-form');
form.style.outline = '1px solid rgb(0,255,255)';
form.style.padding = '15px';
form.style.position = 'absolute';
form.style.top = '0';
form.style.right = '0';
form.style.textAlign = 'right';
form.style.backgroundColor = '#000';
form.style.display = 'none';
form.appendChild(nameLabel);
form.appendChild(ce('br'));
form.appendChild(passLabel);
form.appendChild(ce('br'));
form.appendChild(ok);
id('board').appendChild(form);
return form;
}
function sanitizeInputStyle(el){
el.style.outline = 'none';
el.style.border = 'none';
el.style.background = 'transparent';
return el;
}
function createPanelButton(text, forId){
let ret = createReverse(text);
$(ret).hover((e)=>{ret.style.cursor='pointer';
ret.style.backgroundColor='#eee';},
(e)=>{ret.style.cursor='initial';
ret.style.removeProperty('background-color')});
if(forId)
ret.dataset.forId = forId;
return ret;
}
function createReverse(text){
var ret = ce('span');
ret.classList.add('reverse');
ret.style.padding = '2px 4px';
ret.style.marginRight = '5px';
ret.innerText = text;
return ret;
}
function regEl(el){
scriptElements.push(el);
return el;
}
function removeRegistered(){
scriptElements.forEach(el=>{
$(el).remove();
});
scriptElements = [];
}
function simulateInput(command){
let cmd = el('#cmd');
cmd.value = command;
cmd.innerText = command;
cmd.dispatchEvent(new KeyboardEvent('keydown',{keyCode:13, shiftKey:false}));
}
function showAccountForm(e){
let form = id(e.currentTarget.dataset.forId);
if(form){
if(form.parentElement===null || form.style.display === 'none'){
let board = id('board');
els('.acc-form',board).forEach((elt)=>elt.style.display='none');
board.appendChild(form);
form.style.display = 'block';
}else{
form.style.display = 'none';
}
}
}
function logInput(/*String*/command){
commands.push(command);
command_number = commands.length;
}
function sendCommand(/*String*/command,/*Function*/callback){
el('#cmd').value = '';
el('#cmd').innerText = '';
$.ajax({
type:'POST',
url:'console.php',
dataType:'json',
data:{input: command},
success: callback
// success: (r)=>{callback(r);}
});
}
function focusCmd(){
el('#cmd').focus();
}
function removeEl(from,selector){
var els = from.querySelectorAll(selector);
els.forEach((v,i,l)=>{
if(v.parentElement!==null)
v.parentElement.removeChild(v);
})
}
function id(id){
return document.getElementById(id);
}
function el(/*String*/selector,/*Element*/el){
if(el) return el.querySelector(selector);
else return document.querySelector(selector);
}
function els(/*String*/selector,/*Element*/el){
if(el) return el.querySelectorAll(selector);
else return document.querySelectorAll(selector);
}
function loading(onComplete){
str = 'Loading...';
nextchar = str.charAt($('#loading').html().length);
if ($('#loading').html().length < 10) {
$('#loading').html($('#loading').html()+nextchar);
if(onComplete)
setTimeout(loading, 40, onComplete);
else
setTimeout(loading, 40);
} else {
$('.content').css('display', 'block');
if(onComplete)
onComplete();
else
nav_down();
}
}
function empty(){}
//---------------------------------------------------
// COMMANDS
//---------------------------------------------------
/*
NOTE How commands works.
There is two ways - WITH keys and WITHOUT keys.
If input has at least one key like '-k'
then it passes to server without any changes.
Otherwise there is an attempt to extract values
and rebuild input with concrete key-value data,
then new input passed to server.
Otherwise input passes to server without any changes.
*/
/** Base command */
function Command(name){
this.name = name?name:'';
this.name = this.name.toLowerCase();
this.argReg = /[^\\]-(.+?)(?= -|$)/i; // is need only test, so no GLOBAL flag
}
Command.prototype.processInput = function(input){return input;};
Command.prototype.processOutput = function(response){
if (response.edit) {
$('#path').html(response.path+' > ');
$('#cmd').val(response.edittext);
} else {
if (response.clear) $('#content').html('');
$('#content').append(response.message);
if (response.path) $('#path').html(response.path+' > ');
$('.content').css('display', 'block');
if (response.clear) loading();
else nav_down();
}
focusCmd();
};
Command.processOutputWithContext = function(response){
switch(currentContext){
case CONTEXT_BOARD:
// log('board context');
commandsMap.get('board').processOutput(response);
break;
case CONTEXT_TOPIC:
// log('topic context');
commandsMap.get('topic').processOutput(response);
break;
default:
// log('no ctx');
Object.getPrototypeOf(Object.getPrototypeOf(this)).processOutput(response);
}
};
Command.getLastPage = function(pageStr){
return lastEreg.test(pageStr)?lastPageNum:pageStr;
};
//---------------------------------------------------
// NO arg commands
//---------------------------------------------------
function CommandHelp(){
Command.call(this, 'help');
this.boardReg = /(BOARD +)(-n)/;
this.topicReg = /TOPIC -n <number> [-p <page>]/;
this.keysReg = /Before the parameter.+\./;
this.newKeysInfo = 'You can write commands* with** or without** keys (keys looks like "-k").<br><br>* with the exception of <span class=reverse style="padding:0 4px"> NEWTOPIC </span><br>** you cannot combine both ways - it is possible to use only one at a time';
}
CommandHelp.prototype = Object.create(Command.prototype);
CommandHelp.prototype.constructor = CommandHelp;
CommandHelp.prototype.processInput = function(input){return input;};
CommandHelp.prototype.processOutput = function(response){
if(response.clear) $('#content').html('');
let msg = response.message.replace(this.keysReg, this.newKeysInfo);
$('#content').append(msg);
if (response.path) $('#path').html(response.path+' > ');
if (response.clear) loading();
else nav_down();
};
function CommandBoards(){
Command.call(this,'boards');
}
CommandBoards.prototype = Object.create(Command.prototype);
CommandBoards.prototype.constructor = CommandBoards;
CommandBoards.prototype.processOutput = function(response){
if (response.clear) $('#content').html('');
let els = $.parseHTML(response.message);
els.forEach((el)=>{
let nodes = Array.prototype.slice.call(el.childNodes);
nodes = nodes.map((n)=>{
if(n.nodeType!==3)
return n;
else{
let matches = boardEreg.exec(n.textContent);
if(matches!==null){
let num = matches[1];
let name = matches[2];
let b = ce('span');
b.classList.add(CLASS_BOARD_LINK);
b.innerText = `[${num}] ${name} `;
b.dataset.boardNumber = num;
$(b).hover(underliner,unUnderliner);
$(b).hover(pointer,unPointer);
$(b).hover(textHighlight,unTextHighlight);
return b;
}
else return n;
}
});
while(el.firstChild!==null)
el.removeChild(el.firstChild);
nodes.forEach(n=>el.appendChild(n));
});
els.forEach((el)=>$('#content').append(el));
if (response.path) $('#path').html(response.path+' > ');
if (response.clear) loading();
else nav_down();
focusCmd();
currentContext = CONTEXT_BOARDS;
};
function CommandNav(name){
Command.call(this, name);
}
CommandNav.prototype = Object.create(Command.prototype);
CommandNav.prototype.constructor = CommandNav;
CommandNav.prototype.processOutput = function(response){
Command.processOutputWithContext.call(this,response);
};
//---------------------------------------------------
// ONE args commands
//---------------------------------------------------
function CommandOneArg(name,arg){
Command.call(this, name);
this.arg = arg;
this.ereg = new RegExp(`${this.name} +(.+)`,'i');
}
CommandOneArg.prototype = Object.create(Command.prototype);
CommandOneArg.prototype.constructor = CommandOneArg;
CommandOneArg.prototype.processInput = function(input){
let match = this.argReg.exec(input);
if(match){
return input;
}
match = this.ereg.exec(input);
if(match){
return `${this.name} -${this.arg} ${match[1]}`;
}
return input;
};
function CommandTimezone(){
CommandOneArg.call(this, 'timezone', 'u');
}
CommandTimezone.prototype = Object.create(CommandOneArg.prototype);
CommandTimezone.prototype.constructor = CommandTimezone;
function CommandBoard(){
CommandOneArg.call(this,'board', 'n');
}
CommandBoard.prototype = Object.create(CommandOneArg.prototype);
CommandBoard.prototype.constructor = CommandBoard;
CommandBoard.prototype.processOutput = function(response){
if (response.clear) $('#content').html('');
let wrap = ce('div');
let els = $.parseHTML(response.message);
els.forEach((elt)=>wrap.appendChild(elt));
let numSelector = 'tr > td.postsnumber';
let nameSelector = 'td > span.reverse';
let nums = wrap.querySelectorAll(numSelector);
let names = wrap.querySelectorAll(nameSelector);
nums.forEach((elt,i)=>{
let matches = bracketsEreg.exec(elt.innerText);
let threadNumber = matches[1];
let span = ce('span');
span.classList.add(CLASS_THREAD_LINK);
span.innerText = threadNumber;
span.dataset.threadNumber = threadNumber;
$(span).hover(underliner, unUnderliner);
$(span).hover(pointer, unPointer);
$(span).hover(textHighlight, unTextHighlight);
elt.innerText = '';
elt.appendChild(cn('['));
elt.appendChild(span);
elt.appendChild(cn(']'));
let nameElt = names[i];
nameElt.classList.add(CLASS_THREAD_LINK);
nameElt.dataset.threadNumber = threadNumber;
$(nameElt).hover(pointer, unPointer);
$(nameElt).hover((e)=>e.currentTarget.style.backgroundColor='#eee',
(e)=>e.currentTarget.style.removeProperty('background-color'));
});
els.forEach((elt)=>el('#content').appendChild(elt));
if (response.path) $('#path').html(response.path+' > ');
if (response.clear) loading(empty);
focusCmd();
currentContext = CONTEXT_BOARD;
};
function CommandRvt(){
CommandOneArg.call(this,'rvt','p');
this.ereg = new RegExp(`${this.name}( +(\\w+))?`,'i');
}
CommandRvt.prototype = Object.create(CommandOneArg.prototype);
CommandRvt.prototype.constructor = CommandRvt;
CommandRvt.prototype.processInput = function(input){
let match = this.argReg.exec(input);
if(match)
return input;
match = this.ereg.exec(input);
if(match){
let page = match[1];
let ret;
if(page){
page = Command.getLastPage(match[2]);
ret = `rvt -p ${page}`;
}else
ret = `rvt`;
return ret;
}
return ret;
};
CommandRvt.prototype.processOutput = CommandBoard.prototype.processOutput;
function CommandReply(){
CommandOneArg.call(this,'reply','m');
this.argReg = /[^\\]-(\w)(?= |$)/i;
}
CommandReply.prototype = Object.create(CommandOneArg.prototype);
CommandReply.prototype.constructor = CommandReply;
function CommandPage(){
CommandOneArg.call(this,'page','p');
}
CommandPage.prototype = Object.create(CommandOneArg.prototype);
CommandPage.prototype.constructor = CommandPage;
CommandPage.prototype.processOutput = function(r){
Command.processOutputWithContext.call(this, r);
};
//---------------------------------------------------
// TWO args commands
//---------------------------------------------------
function CommandTwoArgs(name,arg1,arg2){
CommandOneArg.call(this,name,arg1);
this.arg2 = arg2;
this.ereg = new RegExp(`${this.name} +(\\w+) +(.+)`,'i');
}
CommandTwoArgs.prototype = Object.create(CommandOneArg.prototype);
CommandTwoArgs.prototype.constructor = CommandTwoArgs;
CommandTwoArgs.prototype.processInput = function(input){
// check if input has key-value like '-u username'
// if so do nothing and return input as is
// otherwise check if input have no keys like 'login username password'
// if so - build command with appropriate keys and values
// else return input as is
let match = this.argReg.exec(input);
if(match){
// it is a key-value variant
return input;
}
match = this.ereg.exec(input);
if(match){
// no keys variant
return `${this.name} -${this.arg} ${match[1]} -${this.arg2} ${match[2]}`;
}
return input;
};
function CommandAccount(name, arg1, arg2){
CommandTwoArgs.call(this,name,arg1,arg2);
}
CommandAccount.prototype = Object.create(CommandTwoArgs.prototype);
CommandAccount.prototype.constructor = CommandAccount;
function CommandTopic(){
CommandTwoArgs.call(this,'topic','n','p');
this.ereg = new RegExp(`${this.name} +(\\w+)( +(\\w+))?`,'i');
}
CommandTopic.prototype = Object.create(CommandTwoArgs.prototype);
CommandTopic.prototype.constructor = CommandTopic;
CommandTopic.prototype.processInput = function(input){
let match = this.argReg.exec(input);
if(match)
return input;
match = this.ereg.exec(input);
if(match){
let num = match[1];
let page = match[2];
let ret;
if(page){
page = Command.getLastPage(match[3]);
ret = `${this.name} -n ${num} -p ${page}`;
}else{
ret = `${this.name} -n ${num}`;
}
return ret;
}
return input;
};
CommandTopic.prototype.processOutput = function(response){
Object.getPrototypeOf(CommandTopic.prototype).processOutput.call(this, response);
currentContext = CONTEXT_TOPIC;
};