// ==UserScript==
// @name CafeCoder Enhancer
// @namespace iilj
// @version 2020.01.05.5
// @description CafeCoder のUIを改善し,コンテストを快適にします(たぶん)
// @author iilj
// @supportURL https://github.com/iilj/CafeCodeEnhancer/issues
// @match https://www.cafecoder.top/*
// @require https://cdnjs.cloudflare.com/ajax/libs/noty/3.1.4/noty.js
// @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.js
// @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/mode/clike/clike.js
// @require https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/mode/python/python.js
// @require https://cdnjs.cloudflare.com/ajax/libs/list.js/1.5.0/list.js
// @resource css_noty https://cdnjs.cloudflare.com/ajax/libs/noty/3.1.4/noty.css
// @resource css_cm https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.css
// @grant GM_addStyle
// @grant GM_getResourceText
// ==/UserScript==
/* globals CodeMirror, Noty, List */
(function () {
'use strict';
GM_addStyle(GM_getResourceText('css_noty'));
GM_addStyle(GM_getResourceText('css_cm'));
GM_addStyle(`
/* h4 まわりの UI 改善 */
h4 {
margin-top: 1rem;
border-bottom: 2px solid lightblue;
border-left: 10px solid lightblue;
padding-left: 0.5rem;
}
/* ページ最下部のコンテンツが見やすいように調整する */
div.card {
marginBottom: 30px;
}
/* コンテストページ上部のメニューを使いやすくする */
div.card-body a.nav-item.nav-link {
border: 1px solid #bbbbbb;
margin: 0.3rem;
border-radius: 0.3rem;
color: #007bff;
}
div.card-body a.nav-item.nav-link.cce-active {
background-color: #ffffff;
color: rgba(0,0,0,.5);
}
div.card-body a.nav-item.nav-link:hover{
background-color: #dddddd;
}
/* 入出力サンプルのUI */
.cce-myprenode {
display: block;
margin: 0.4rem;
padding: 0.4rem;
background-color: #efefef !important;
border: 1px solid #bbbbbb;
border-radius: 0.4rem;
font-family: Menlo,Monaco,Consolas,"Courier New",monospace;
}
.CodeMirror {
border-top: 1px solid black;
border-bottom: 1px solid black;
}
/* sortable table */
table.table th {
padding: 6px;
vertical-align: middle;
}
table.table tbody th {
font-weight: normal; /* hotfix for unformal use of th tag */
position: relative;
}
table thead th[data-sort] {
cursor: pointer;
color: #007bff;
}
table thead th[data-sort]:hover {
background-color: #dddddd;
}
table thead th[data-sort].sort.desc:after {
content: " ▲";
color: #888;
}
table thead th[data-sort].sort.asc:after {
content: " ▼";
color: #888;
}
/* result icon */
span.result, th.result>span {
display: inline;
padding: .2em .6em .3em;
font-size: 75%;
font-weight: bold;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: .25em;
border: none;
-webkit-text-stroke: unset;
text-shadow: none;
margin: 0;
cursor: default;
}
.AC {
background-color: #5cb85c;
}
.WA, .TLE {
background-color: #f0ad4e;
}
.WJ {
background-color: #777;
}
/* ranking page */
table.table.cce-ranking-table th {
padding: 10px;
}
div.point {
height: auto;
width: auto;
position: absolute;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
-webkit-transform: translateY(-50%) translateX(-50%);
}
div.point a {
color: #00AA3E;
font-weight: bold;
}
div.point span.submit_time {
margin: 0 0 3px;
color: #888;
font-size: 90%;
font-weight: normal;
}
table.table th.cce-ranking-username {
font-weight: bold;
width: auto;
height: auto;
}
.cce-ranking-point div.point {
color: blue;
font-weight: bold;
}
/* icon */
@font-face {
font-family: 'Glyphicons Halflings';
src: url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot');
src: url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2') format('woff2'),
url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff') format('woff'),
url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
url('https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')
}
.glyphicon {
position: relative;
top: 1px;
display: inline-block;
font-family: 'Glyphicons Halflings';
font-style: normal;
font-weight: normal;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin-right: 0.2rem;
}
.glyphicon-home:before {
content: "\\e021"
}
.glyphicon-tasks:before {
content: "\\e137"
}
.glyphicon-sort-by-attributes-alt:before {
content: "\\e156"
}
.glyphicon-user:before {
content: "\\e008"
}
.glyphicon-list:before {
content: "\\e056"
}
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box
}
*:before,
*:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box
}
`);
const msg = (type, text) => {
new Noty({
type: type,
layout: 'top',
timeout: 3000,
text: text
}).show();
};
const dqs = (selectors) => document.querySelector(selectors);
const dqsa = (selectors) => document.querySelectorAll(selectors);
// add title tag when there exists no title tag
let result;
if (!dqs("title")) {
const title = document.createElement("title");
let stitle = "";
if (result = location.href.match(/www\.cafecoder\.top\/([^\/]+)\/(index\.(php|html?))?$/)) {
stitle += `${result[1]} (${dqs("h1").innerText.trim()})`;
} else if (result = location.href.match(/www\.cafecoder\.top\/([^\/]+)\/problem_list\.(php|html?)$/)) {
stitle += `${result[1]} 問題一覧`;
} else if (result = location.href.match(/www\.cafecoder\.top\/([^\/]+)\/Problems\/([^\/]+)\.(php|html?)$/)) {
stitle += `${result[1]}-${result[2]}`;
const h3 = dqs("h3");
if (h3) {
stitle += " " + h3.innerText.trim();
}
}
stitle += (stitle == "" ? "" : " : ") + "CafeCoder";
title.innerText = stitle;
dqs("head").insertAdjacentElement('afterbegin', title);
}
// fix invalid/broken uri
dqsa("a[href*='kakecoder.com']").forEach((lnk) => {
lnk.href = lnk.href.replace('kakecoder.com', 'cafecoder.top');
});
dqsa("a[href*='.html']").forEach((lnk) => {
lnk.href = lnk.href.replace('.html', '.php');
});
dqsa("a[href^='//'][href$='.php']").forEach((lnk) => {
const href = lnk.getAttribute('href');
if (result = href.match(/^\/\/([^\/]+)\.(php|html?)$/)) {
lnk.setAttribute('href', href.replace('//', location.href.indexOf("/Problems/") != -1 ? '../' : './'));
}
});
// add icon
dqsa('div.card-body a.nav-item.nav-link').forEach((lnk) => {
const href = lnk.getAttribute('href');
let type = 'home';
if (result = href.match(/([^\/]+).(php|html?)(\?[^/]+)?$/)) {
const nm = result[1];
switch (nm) {
case 'index':
type = 'home';
break;
case 'problem_list':
type = 'tasks';
break;
case 'ranking':
type = 'sort-by-attributes-alt';
break;
case 'my_submit':
type = 'user';
break;
case 'all_submit':
type = 'list';
break;
}
}
const icon = document.createElement("span");
icon.classList.add('glyphicon', `glyphicon-${type}`);
icon.setAttribute('aria-hidden', 'true');
lnk.insertAdjacentElement('afterbegin', icon);
if (lnk.href == location.href) {
lnk.classList.add('cce-active');
}
});
// when problem page
if (location.href.indexOf("/Problems/") != -1 && dqs("h3") != null) {
// improve UI/UX of I/O sample, and add sample copy button feature
dqsa("span[style]:not([class]), pre[style]:not([class]), div[style]:not([class]), .sample").forEach((node, idx, _nodelist) => {
if (!node.classList.contains('sample') && !node.style.backgroundColor && node.getAttribute("style").indexOf("background-color") == -1) {
return;
}
node.classList.add('cce-myprenode');
node.id = `cce-myprenode-${idx}`;
if (node.firstChild.nodeName == "#text") {
node.firstChild.data = node.firstChild.data.trim();
}
if (node.lastChild.nodeName == "#text") {
node.lastChild.data = node.lastChild.data.trim();
}
let btn = document.createElement("button");
btn.innerText = "テキストをコピー!";
btn.classList.add('btn', 'btn-primary', 'copy-sample-input');
btn.style.display = "block";
btn.addEventListener("click", () => {
const elem = document.getElementById(node.id);
document.getSelection().selectAllChildren(elem);
if (document.execCommand("copy")) {
msg('success', 'テキストをコピーしました!');
document.getSelection().removeAllRanges();
} else {
msg('error', 'コピーに失敗してしまったようです.');
}
}, false);
node.insertAdjacentElement('beforebegin', btn);
});
// CodeMirror init
const textarea = dqs('form[name=submit_form] textarea[name=sourcecode]');
const editor = CodeMirror.fromTextArea(textarea, {
mode: "text/x-c++src",
lineNumbers: true,
});
// CodeMirror lang selection changed event handler
const selectlang = dqs('form[name=submit_form] select[name=language]');
selectlang.addEventListener('change', (event) => {
const modelist = [
'text/x-csrc', 'text/x-c++src', 'text/x-java', 'python', 'text/x-csharp'
];
editor.setOption("mode", modelist[event.target.selectedIndex]);
});
// select lang C++17 as a default
selectlang.selectedIndex = 1;
selectlang.classList.add('form-control');
// CodeMirror submit preprocess, remove default broken event
const submitbtn = dqs('form[name=submit_form] input[type=submit]');
submitbtn.removeAttribute("onclick");
submitbtn.classList.add('btn-primary');
document.submit_form.addEventListener('submit', (event) => {
editor.save();
if (textarea.value == '') {
msg('warning', 'ソースコードが入力されていません');
event.preventDefault();
}
});
} else if (location.href.indexOf("/all_submit.php?") != -1 || location.href.indexOf("/my_submit.php?") != -1) {
// on submit list page
const parent = dqs('div.card-body');
parent.id = 'cce-list-parent';
const table = parent.querySelector('table.table');
table.classList.add('table-striped', 'small');
const tbody = table.querySelector('tbody');
tbody.classList.add('list');
tbody.querySelectorAll('tr').forEach((tr) => {
tr.querySelectorAll('th').forEach((td, idx, nodelist) => { /* unformal html (th here should be td) */
if (idx == nodelist.length - 1) {
return;
}
td.classList.add(`cce-list-sort-${idx}`);
});
});
parent.querySelector('thead').querySelectorAll('tr th').forEach((th, idx, nodelist) => {
if (idx == nodelist.length - 1) {
return;
} else if (idx == 1) {
th.classList.add('desc');
}
th.classList.add('sort');
th.setAttribute('data-sort', `cce-list-sort-${idx}`);
});
const userList = new List(parent.id, {
valueNames: ['cce-list-sort-0', 'cce-list-sort-1', 'cce-list-sort-2', 'cce-list-sort-3']
});
} else if (location.href.indexOf("/problem_list.") != -1) {
// on contest problem list page
const table = dqs('table.table');
const thead = table.querySelector('thead tr');
const th0 = document.createElement("th");
th0.innerText = '#';
thead.insertAdjacentElement('afterbegin', th0);
table.querySelectorAll('tbody tr').forEach((tr) => {
const tr0 = document.createElement("th");
console.log(tr.querySelector('a[href]').href);
const a0 = tr.querySelector('a[href]');
if (result = a0.href.match(/\/([^\/])\.(php|html?)?$/)) {
const pid = result[1];
const a1 = document.createElement("a");
a1.href = a0.href;
a1.innerText = pid;
tr0.insertAdjacentElement('afterbegin', a1);
} else {
tr0.innerText = '?';
}
tr.insertAdjacentElement('afterbegin', tr0);
});
} else if (location.href.indexOf("/ranking.php?") != -1) {
// on contest ranking page
const parent = dqs('div.card-body');
parent.id = 'cce-list-parent';
const table = parent.querySelector('table.table');
table.classList.add('table-striped', 'small', 'cce-ranking-table');
const tbody = table.querySelector('tbody');
tbody.classList.add('list');
tbody.querySelectorAll('tr').forEach((tr, tridx) => {
let endtime = '00:00:00';
const submit_time = document.createElement("span");
submit_time.classList.add('submit_time');
tr.querySelectorAll('th').forEach((td, idx, nodelist) => { /* unformal html (th here should be td) */
if (idx == 1) {
td.classList.add('cce-ranking-username');
} else if (idx == 2) {
td.classList.add('cce-ranking-point');
td.setAttribute('data-cce-list-sort-point', `${tridx}`);
const divpoint = td.firstElementChild;
divpoint.insertAdjacentHTML('beforeend', '<br>');
divpoint.insertAdjacentElement('beforeend', submit_time);
} else if (idx >= 3) {
const timespan = td.querySelector('span.submit_time');
if (timespan) {
td.setAttribute('data-cce-list-sort-timespan', timespan.innerText);
if (timespan.innerText > endtime) {
endtime = timespan.innerText;
}
} else {
td.setAttribute('data-cce-list-sort-timespan', '99:99:99');
}
}
td.classList.add(`cce-list-sort-${idx}`);
});
submit_time.innerText = endtime;
});
parent.querySelector('thead').querySelectorAll('tr th').forEach((th, idx, nodelist) => {
if (idx == 0) {
th.classList.add('asc');
}
th.classList.add('sort');
th.setAttribute('data-sort', `cce-list-sort-${idx}`);
});
const userList = new List(parent.id, {
valueNames: ['cce-list-sort-0', 'cce-list-sort-1',
{ name: 'cce-list-sort-2', attr: 'data-cce-list-sort-point' },
{ name: 'cce-list-sort-3', attr: 'data-cce-list-sort-timespan' },
{ name: 'cce-list-sort-4', attr: 'data-cce-list-sort-timespan' },
{ name: 'cce-list-sort-5', attr: 'data-cce-list-sort-timespan' },
{ name: 'cce-list-sort-6', attr: 'data-cce-list-sort-timespan' },
{ name: 'cce-list-sort-7', attr: 'data-cce-list-sort-timespan' },
{ name: 'cce-list-sort-8', attr: 'data-cce-list-sort-timespan' }]
});
}
})();