314n.org improver

Improves 314n.org user-expiriense.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==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+'&nbsp;>&nbsp;');
      $('#cmd').val(response.edittext);
    } else {
      if (response.clear) $('#content').html('');                                
      $('#content').append(response.message);
      if (response.path) $('#path').html(response.path+'&nbsp;>&nbsp;');
      
      $('.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+'&nbsp;>&nbsp;');
  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+'&nbsp;>&nbsp;');
  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+'&nbsp;>&nbsp;');
  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;
};