update: fetch commits from upstream

Update from vfleaking/uoj upstream, commit aa8a85c - 9f1302c.
Because of this repo's modify, also with the adaption of community version.
This commit is contained in:
vfleaking 2018-07-09 10:40:30 +08:00 committed by Masco Skray
parent e9df8f54ab
commit 4d2b0735dc
55 changed files with 3540 additions and 1094 deletions

View File

@ -200,10 +200,17 @@ def update_problem_data(problem_id, problem_mtime):
copy_name = uoj_judger_path('/data/%d' % problem_id)
copy_zip_name = uoj_judger_path('/data/%d.zip' % problem_id)
if os.path.isdir(copy_name):
quoted_copy_name = pipes.quote(copy_name)
if os.path.getmtime(copy_name) >= problem_mtime:
execute('touch -a %s' % quoted_copy_name)
return
else:
execute('chmod 700 %s -R && rm -rf %s' % (pipes.quote(copy_name), pipes.quote(copy_name)))
execute('chmod 700 %s -R && rm -rf %s' % (quoted_copy_name, quoted_copy_name))
del_list = sorted(os.listdir(uoj_judger_path('/data')), key=lambda fname: os.path.getatime(uoj_judger_path('/data/%s' % fname)))[:-99]
for fname in del_list:
quoted_fname = pipes.quote(uoj_judger_path('/data/%s' % fname))
os.system('chmod 700 %s -R && rm -rf %s' % (quoted_fname, quoted_fname))
uoj_download('/problem/%d' % problem_id, copy_zip_name)
execute('cd %s && unzip -q %d.zip && rm %d.zip && chmod -w %d -R' % (uoj_judger_path('/data'), problem_id, problem_id, problem_id))
except Exception:

View File

@ -25,16 +25,19 @@ EXE_CHECKER = \
EXE = main_judger \
run/formatter \
run/run_program \
run/run_interaction \
builtin/judger/judger \
$(EXE_CHECKER)
all: $(EXE)
% : %.cpp
$(CXX) $(CXXFLAGS) $< -o $@
$(CXX) $(CXXFLAGS) $(EXTRA_CXXFLAGS) $< -o $@
run/run_program: include/uoj_env.h run/run_program_conf.h
run/formatter : include/testlib.h
run/run_interaction: run/run_interaction.cpp include/uoj_env.h
$(CXX) $(CXXFLAGS) --std=c++11 -pthread $< -o $@
builtin/judger/judger: include
main_judger: include

View File

@ -1,231 +1,14 @@
#include "uoj_judger.h"
/*
void hack_test() {
put_status("Judging Hack ...");
in = work_path "/hack_input.txt";
out = work_path "/pro_output.txt";
ans = work_path "/std_output.txt";
struct SubtaskInfo {
bool passed;
int score;
run_result rn;
if (maker) {
if (compile("maker") == false) {
compile_info_update();
put_info(1, 0, -1, -1, "Hack Failed : Maker Compile Error", "", "", read_file(result_path "/compile_result.txt", ""));
end_info(0);
}
rn = run_maker(mtL, mmL, moL, info);
if (rn.type != 0) {
put_info(1, 0, -1, -1, "Hack Failed : " + info, "", "", "");
end_info(0);
}
SubtaskInfo() {
}
if (getValid(in, info));
else put_info(1, 0, -1, -1, "Hack Failed : Illegal Input", read_file(in, "", 100), "", info), end_info(0);
rn = run_program(stp, in, ans, tL, mL, oL, info);
if (rn.type != 0) put_info(1, 0, -1, -1, "Hack Failed : std " + info, read_file(in, "", 100), "", ""), end_info(0);
rn = run_program(pro, in, out, tL, mL, oL, info);
int ust = rn.ust, usm = rn.usm;
if (rn.type != 0) put_info(1, 1, -1, -1, "Hack Successfully : " + info, read_file(in, "", 100), "", ""), end_info(0);
rn = run_judge(stL, smL, soL, info);
if (rn.type != 0) put_info(1, 1, -1, -1, "Hack Successfully : Checker " + info, read_file(in, "", 100), read_file(out, "", 100), ""), end_info(0);
else if (abs(SCORE() - 1) < 1e-5) put_info(1, 0, ust, usm, "Hack failed : Answer is Correct", read_file(in, "", 100), read_file(out, "", 100), info), end_info(0);
else put_info(1, 1, ust, usm, "Hack Successfully : Wrong Answer", read_file(in, "", 100), read_file(out, "", 100), read_file(res, "", 100)), end_info(0);
exit(0);
}
void sample_test() {
n = conf_int("n_sample_tests", n);
for (int i = 1; i <= n; ++i) {
put_status("Judging Sample Test ... " + toStr(i));
in = mdata_path + "/" + input(-i);
ans = mdata_path + "/" + output(-i);
out = work_path "/" + output(-i);
int S = conf_int("sample_point_score", i, 100 / n);
if (valid) {
if (getValid(in, info));
else {
put_info(i, 0, -1, -1, "Illegal Input", read_file(in, "", 100), "", info);
continue;
}
}
run_result rn;
if (submit) {
if (file_exist(out)) put_info(i, 0, -1, -1, "File not exists", "", "", "");
else {
rn = run_judge(stL, smL, soL, info);
if (rn.type != 0) put_info(i, 0, -1, -1, "Checker " + info, read_file(in, "", 100), read_file(out, "", 100), "");
else {
string ninfo;
if (abs(SCORE() - 1) < 1e-5) ninfo = "Accepted";
else if (abs(SCORE()) < 1e-5) ninfo = "Wrong Answer";
else ninfo = "Acceptable Output";
put_info(i, SCORE() * S, -1, -1, ninfo, read_file(in, "", 100), read_file(out, "", 100), read_file(res, ""));
}
}
continue;
}
rn = run_program(pro, in, out, tL, mL, oL, info);
int ust = rn.ust, usm = rn.usm;
if (rn.type != 0) {
put_info(i, 0, -1, -1, info, read_file(in, "", 100), "", "");
continue;
}
rn = run_judge(stL, smL, soL, info);
if (rn.type != 0) put_info(i, 0, -1, -1, "Checker " + info, read_file(in, "", 100), read_file(out, "", 100), "");
else {
string ninfo;
if (abs(SCORE() - 1) < 1e-5) ninfo = "Accepted", ++cnt;
else if (abs(SCORE()) < 1e-5) ninfo = "Wrong Answer";
else ninfo = "Acceptable Output";
put_info(i, SCORE() * S, ust, usm, ninfo, read_file(in, "", 100), read_file(out, "", 100), read_file(res, ""));
}
}
if (cnt == n) totScore = 100;
end_info(0);
}
void normal_test() {
n = conf_int("n_tests", 10);
m = conf_int("n_ex_tests", 0);
for (int i = 1; i <= n; ++i) {
put_status("Judging Test ... " + toStr(i));
in = mdata_path + "/" + input(i);
ans = mdata_path + "/" + output(i);
out = work_path "/" + output(i);
int ntL = conf_int("time_limit", i, tL),
nmL = conf_int("memory_limit", i, mL),
noL = conf_int("output_limit", i, oL),
nstL = conf_int("checker_time_limit", i, stL),
nsmL = conf_int("checker_memory_limit", i, smL),
nsoL = conf_int("checker_output_limit", i, soL);
int S = conf_int("point_score", i, 100 / n);
if (valid) {
if (getValid(in, info));
else {
put_info(i, 0, -1, -1, "Illegal Input", read_file(in, "", 100), "", info);
continue;
}
}
run_result rn;
if (submit) {
if (file_exist(out)) put_info(i, 0, -1, -1, "File not exists", "", "", "");
else {
rn = run_judge(nstL, nsmL, nsoL, info);
if (rn.type != 0) put_info(i, 0, -1, -1, "Checker " + info, read_file(in, "", 100), read_file(out, "", 100), "");
else {
string ninfo;
if (abs(SCORE() - 1) < 1e-5) ninfo = "Accepted";
else if (abs(SCORE()) < 1e-5) ninfo = "Wrong Answer";
else ninfo = "Acceptable Output";
put_info(i, SCORE() * S, -1, -1, ninfo, read_file(in, "", 100), read_file(out, "", 100), read_file(res, ""));
}
}
continue;
}
rn = run_program(pro, in, out, ntL, nmL, noL, info);
int ust = rn.ust, usm = rn.usm;
if (rn.type != 0) {
put_info(i, 0, -1, -1, info, read_file(in, "", 100), "", "");
continue;
}
rn = run_judge(nstL, nsmL, nsoL, info);
if (rn.type != 0) put_info(i, 0, -1, -1, "Checker " + info, read_file(in, "", 100), read_file(out, "", 100), "");
else {
string ninfo;
if (abs(SCORE() - 1) < 1e-5) ninfo = "Accepted", ++cnt;
else if (abs(SCORE()) < 1e-5) ninfo = "Wrong Answer";
else ninfo = "Acceptable Output";
put_info(i, SCORE() * S, ust, usm, ninfo, read_file(in, "", 100), read_file(out, "", 100), read_file(res, ""));
}
}
if (cnt != n) end_info(0);
totScore = 100;
bool pass = true;
for (int i = 1; i <= m; ++i) {
put_status("Judging Extra Test ... " + toStr(i));
in = mdata_path + "/" + input(-i);
ans = mdata_path + "/" + output(-i);
out = work_path "/" + output(-i);
run_result rn;
if (valid) {
if (getValid(in, info));
else {
put_info(-1, -3, -1, -1, "Extra Test Failed : Illegal Input on " + toStr(i), read_file(in, "", 100), "", info);
pass = false; break;
}
}
rn = run_program(pro, in, out, tL, mL, oL, info);
int ust = rn.ust, usm = rn.usm;
if (rn.type != 0) {
put_info(-1, -3, -1, -1, "Extra Test Failed : " + info + " on " + toStr(i), read_file(in, "", 100), "", "");
pass = false; break;
}
rn = run_judge(stL, smL, soL, info);
if (rn.type != 0) {
put_info(-1, -3, -1, -1, "Extra Test Failed : Checker " + info + " on " + toStr(i), read_file(in, "", 100), read_file(out, "", 100), "");
pass = false; break;
}
else if (abs(SCORE() - 1) < 1e-5);
else {
put_info(-1, -3, ust, usm, "Extra Test Failed : Wrong Answer on " + toStr(i), read_file(in, "", 100), read_file(out, "", 100), read_file(res, "", 100));
pass = false; break;
}
}
if (pass && m) put_info(-1, 0, -1, -1, "Pass Extra Test", "", "", "");
end_info(0);
}
void new_ex_test() {
bool pass = true;
put_status("Judging New Extra Test ... ");
int R = conf_int("n_ex_tests", 0);
for (int i = R; i <= R; ++i) {
in = mdata_path + "/" + input(-i);
ans = mdata_path + "/" + output(-i);
if (file_exist(in)) in = mdata_path + "/" + input(i);
if (file_exist(ans)) ans = mdata_path + "/" + input(i);
out = work_path "/" + output(-i);
run_result rn;
if (valid) {
if (getValid(in, info));
else {
put_info(1, 0, -1, -1, "Extra Test Failed : Illegal Input on " + toStr(i), read_file(in, "", 100), "", info);
pass = false; break;
}
}
rn = run_program(pro, in, out, tL, mL, oL, info);
int ust = rn.ust, usm = rn.usm;
if (rn.type != 0) {
put_info(1, 0, -1, -1, "Extra Test Failed : " + info + " on " + toStr(i), read_file(in, "", 100), "", "");
pass = false; break;
}
rn = run_judge(stL, smL, soL, info);
if (rn.type != 0) {
put_info(1, 0, -1, -1, "Extra Test Failed : Checker " + info + " on " + toStr(i), read_file(in, "", 100), read_file(out, "", 100), "");
pass = false; break;
}
else if (abs(SCORE() - 1) < 1e-5);
else {
put_info(1, 0, ust, usm, "Extra Test Failed : Wrong Answer on " + toStr(i), read_file(in, "", 100), read_file(out, "", 100), read_file(res, "", 100));
pass = false; break;
}
}
if (pass) put_info(1, 1, -1, -1, "Pass Extra Test", "", "", "");
end_info(0);
}
*/
SubtaskInfo(const bool &_p, const int &_s)
: passed(_p), score(_s){}
};
void ordinary_test() {
int n = conf_int("n_tests", 10);
@ -266,12 +49,15 @@ void ordinary_test() {
}
}
} else {
set<int> passedSubtasks;
map<int, SubtaskInfo> subtasks;
map<int,int> minScore;
for (int t = 1; t <= nT; t++) {
string subtaskType = conf_str("subtask_type", t, "packed");
int startI = conf_int("subtask_end", t - 1, 0) + 1;
int endI = conf_int("subtask_end", t, 0);
vector<PointInfo> points;
minScore[t] = 100;
vector<int> dependences;
if (conf_str("subtask_dependence", t, "none") == "many") {
@ -286,9 +72,13 @@ void ordinary_test() {
}
bool skipped = false;
for (vector<int>::iterator it = dependences.begin(); it != dependences.end(); it++) {
if (!passedSubtasks.count(*it)) {
skipped = true;
break;
if (subtaskType == "packed") {
if (!subtasks[*it].passed) {
skipped = true;
break;
}
} else if (subtaskType == "min") {
minScore[t] = min(minScore[t], minScore[*it]);
}
}
if (skipped) {
@ -297,28 +87,41 @@ void ordinary_test() {
}
int tfull = conf_int("subtask_score", t, 100 / nT);
int tscore = tfull;
int tscore = scale_score(minScore[t], tfull);
string info = "Accepted";
for (int i = startI; i <= endI; i++) {
report_judge_status_f("Judging Test #%d of Subtask #%d", i, t);
PointInfo po = test_point("answer", i);
if (po.scr != 100) {
passed = false;
po.scr = i == startI ? 0 : -tfull;
tscore = 0;
points.push_back(po);
info = po.info;
break;
} else {
po.scr = i == startI ? tfull : 0;
tscore = tfull;
points.push_back(po);
if (subtaskType == "packed") {
if (po.scr != 100) {
passed = false;
po.scr = i == startI ? 0 : -tfull;
tscore = 0;
points.push_back(po);
info = po.info;
break;
} else {
po.scr = i == startI ? tfull : 0;
tscore = tfull;
points.push_back(po);
}
} else if (subtaskType == "min") {
minScore[t] = min(minScore[t], po.scr);
if (po.scr != 100) {
passed = false;
}
po.scr = scale_score(po.scr, tfull);
if (po.scr <= tscore) {
tscore = po.scr;
points.push_back(po);
info = po.info;
} else {
points.push_back(po);
}
}
}
if (info == "Accepted") {
passedSubtasks.insert(t);
}
subtasks[t] = SubtaskInfo(info == "Accepted", tscore);
add_subtask_info(t, tscore, info, points);
}
@ -359,7 +162,6 @@ void hack_test() {
tpc.output_file_name = work_path + "/pro_output.txt";
tpc.answer_file_name = work_path + "/std_output.txt";
prepare_run_standard_program();
PointInfo po = test_hack_point("answer", tpc);
add_point_info(po);
end_judge_ok();
@ -371,14 +173,34 @@ void sample_test() {
int n = conf_int("n_tests", 10);
for (int i = 1; i <= n; i++) {
report_judge_status_f("Judging Test #%d", i);
PointInfo po = test_point("answer", i);
if (po.scr != 0) {
po.info = "Accepted";
po.scr = 100;
if (conf_is("check_existence_only_in_sample_test", "on")) {
TestPointConfig tpc = TestPointConfig();
tpc.auto_complete(i);
string usrout = file_preview(tpc.output_file_name);
if (usrout == "") {
add_point_info(PointInfo(i, 0, -1, -1,
"default",
file_preview(tpc.input_file_name), usrout,
"wrong answer empty file\n"));
} else {
PointInfo po = PointInfo(i, 100, -1, -1,
"default",
file_preview(tpc.input_file_name), usrout,
"ok nonempty file\n");
po.scr = scale_score(po.scr, conf_int("point_score", i, 100 / n));
add_point_info(po);
}
} else {
PointInfo po = test_point("answer", i);
if (po.scr != 0) {
po.info = "Accepted";
po.scr = 100;
}
po.scr = scale_score(po.scr, conf_int("point_score", i, 100 / n));
po.res = "no comment";
add_point_info(po);
}
po.scr = scale_score(po.scr, conf_int("point_score", i, 100 / n));
po.res = "no comment";
add_point_info(po);
}
end_judge_ok();
} else {
@ -436,45 +258,4 @@ int main(int argc, char **argv) {
} else {
ordinary_test();
}
/*
submit = conf_is("submit_answer", "on");
hack = conf_is("test_new_hack_only", "on");
sample = conf_is("test_sample_only", "on");
maker = conf_is("make_hack_test", "on");
valid = conf_is("validate_input_before_test", "on");
newt = conf_is("test_new_extra", "on");
if (submit == 0) {
if (compile("answer") == false) end_info(-1);
}
jud = config["use_checker"];
pro = work_path "/answer";
mak = work_path "/maker";
stp = mdata_path + "/std";
chk = mdata_path + "/val";
vre = result_path"/valid_result.txt";
sco = result_path"/checker_score.txt";
res = result_path"/checker_result.txt";
tL = conf_int("time_limit", 1);
mL = conf_int("memory_limit", 256);
oL = conf_int("output_limit", 64);
stL = conf_int("checker_time_limit", 10);
smL = conf_int("checker_memory_limit", 256);
soL = conf_int("checker_output_limit", 64);
vtL = conf_int("validator_time_limit", 10);
vmL = conf_int("validator_memory_limit", 256);
voL = conf_int("validator_output_limit", 64);
mtL = conf_int("maker_time_limit", 1);
mmL = conf_int("maker_memory_limit", 256);
moL = conf_int("maker_output_limit", 64);
if (hack) hack_test();
if (sample) sample_test();
if (newt) new_ex_test();
normal_test();
*/
}

View File

@ -2147,8 +2147,17 @@ void InStream::reset()
void InStream::init(std::string fileName, TMode mode)
{
opened = false;
name = fileName;
stdfile = false;
if (fileName == "/dev/stdin")
{
name = "stdin";
this->file = stdin;
stdfile = true;
}
else
{
name = fileName;
stdfile = false;
}
this->mode = mode;
reset();
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,263 @@
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <system_error>
#include <thread>
#include <unistd.h>
#include <asm/unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <sys/user.h>
#include <fcntl.h>
#include <cstring>
#include <string>
#include <vector>
#include <set>
#include <argp.h>
#include "uoj_env.h"
using namespace std;
struct RunResult {
int result;
int ust;
int usm;
int exit_code;
RunResult(int _result, int _ust = -1, int _usm = -1, int _exit_code = -1)
: result(_result), ust(_ust), usm(_usm), exit_code(_exit_code) {
if (result != RS_AC) {
ust = -1, usm = -1;
}
}
};
struct PipeConfig {
int from, to;
int from_fd, to_fd;
string saving_file_name; // empty for none
PipeConfig() {
}
PipeConfig(string str) {
if (sscanf(str.c_str(), "%d:%d-%d:%d", &from, &from_fd, &to, &to_fd) != 4) {
throw invalid_argument("bad init str for PipeConfig");
}
from -= 1;
to -= 1;
}
};
struct RunInteractionConfig {
vector<string> cmds;
vector<PipeConfig> pipes;
};
struct RunCmdData {
string cmd;
pid_t pid;
vector<int> ipipes, opipes;
};
struct PipeData {
PipeConfig config;
int ipipefd[2], opipefd[2];
thread io_thread;
exception_ptr eptr;
};
class RunInteraction {
private:
vector<RunCmdData> cmds;
vector<PipeData> pipes;
void prepare_fd() { // me
for (int i = 0; i < (int)pipes.size(); i++) {
close(pipes[i].ipipefd[1]);
close(pipes[i].opipefd[0]);
}
}
void prepare_fd_for_cmd(int id) {
freopen("/dev/null", "r", stdin);
freopen("/dev/null", "w", stdout);
freopen("/dev/null", "w", stderr);
for (int i = 0; i < (int)pipes.size(); i++) {
if (pipes[i].config.from == id) {
dup2(pipes[i].ipipefd[1], 128 + pipes[i].ipipefd[1]);
}
if (pipes[i].config.to == id) {
dup2(pipes[i].opipefd[0], 128 + pipes[i].opipefd[0]);
}
close(pipes[i].ipipefd[0]);
close(pipes[i].ipipefd[1]);
close(pipes[i].opipefd[0]);
close(pipes[i].opipefd[1]);
}
for (int i = 0; i < (int)pipes.size(); i++) {
if (pipes[i].config.from == id) {
dup2(128 + pipes[i].ipipefd[1], pipes[i].config.from_fd);
close(128 + pipes[i].ipipefd[1]);
}
if (pipes[i].config.to == id) {
dup2(128 + pipes[i].opipefd[0], pipes[i].config.to_fd);
close(128 + pipes[i].opipefd[0]);
}
}
}
void wait_pipe_io(int pipe_id) {
FILE *sf = NULL;
if (!pipes[pipe_id].config.saving_file_name.empty())
sf = fopen(pipes[pipe_id].config.saving_file_name.c_str(), "w");
int ifd = pipes[pipe_id].ipipefd[0];
int ofd = pipes[pipe_id].opipefd[1];
FILE *inf = fdopen(ifd, "r");
FILE *ouf = fdopen(ofd, "w");
try {
pipes[pipe_id].eptr = nullptr;
const int L = 4096;
char buf[L];
while (true) {
int c = fgetc(inf);
if (c == EOF) {
if (errno) {
throw system_error(errno, system_category());
}
break;
}
if (fputc(c, ouf) == EOF) {
throw system_error(errno, system_category());
}
fflush(ouf);
if (fputc(c, sf) == EOF) {
throw system_error(errno, system_category());
}
}
} catch (exception &e) {
cerr << e.what() << endl;
pipes[pipe_id].eptr = current_exception();
}
fclose(sf);
fclose(inf);
fclose(ouf);
}
public:
RunInteraction(const RunInteractionConfig &config) {
cmds.resize(config.cmds.size());
for (int i = 0; i < (int)config.cmds.size(); i++) {
cmds[i].cmd = config.cmds[i];
}
pipes.resize(config.pipes.size());
for (int i = 0; i < (int)config.pipes.size(); i++) {
pipes[i].config = config.pipes[i];
cmds[pipes[i].config.from].opipes.push_back(i);
cmds[pipes[i].config.to].ipipes.push_back(i);
}
for (int i = 0; i < (int)pipes.size(); i++) {
if (pipe(pipes[i].ipipefd) == -1 || pipe(pipes[i].opipefd) == -1) {
throw system_error(errno, system_category());
}
}
for (int i = 0; i < (int)cmds.size(); i++) {
cmds[i].pid = fork();
if (cmds[i].pid == 0) {
prepare_fd_for_cmd(i);
system(cmds[i].cmd.c_str());
exit(0);
} else if (cmds[i].pid == -1) {
throw system_error(errno, system_category());
}
}
prepare_fd();
for (int i = 0; i < (int)pipes.size(); i++) {
pipes[i].io_thread = thread(&RunInteraction::wait_pipe_io, this, i);
}
}
void join() {
int status;
while (wait(&status) > 0);
for (int i = 0; i < (int)pipes.size(); i++) {
pipes[i].io_thread.join();
}
}
};
error_t run_interaction_argp_parse_opt (int key, char *arg, struct argp_state *state) {
RunInteractionConfig *config = (RunInteractionConfig*)state->input;
try {
switch (key) {
case 'p':
config->pipes.push_back(PipeConfig(arg));
break;
case 's':
if (config->pipes.empty()) {
argp_usage(state);
}
config->pipes.back().saving_file_name = arg;
break;
case ARGP_KEY_ARG:
config->cmds.push_back(arg);
break;
case ARGP_KEY_END:
if (state->arg_num == 0) {
argp_usage(state);
}
break;
default:
return ARGP_ERR_UNKNOWN;
}
} catch (exception &e) {
argp_usage(state);
}
return 0;
}
RunInteractionConfig parse_args(int argc, char **argv) {
argp_option run_interaction_argp_options[] = {
{"add-pipe" , 'p', "PIPE" , 0, "Add a pipe <from>:<fd>-<to>:<fd> (fd < 128)" , 1},
{"save-pipe" , 's', "FILE" , 0, "Set last pipe saving file" , 2},
{0}
};
char run_interaction_argp_args_doc[] = "cmd1 cmd2 ...";
char run_interaction_argp_doc[] = "run_interaction: a tool to run multiple commands with interaction";
argp run_interaction_argp = {
run_interaction_argp_options,
run_interaction_argp_parse_opt,
run_interaction_argp_args_doc,
run_interaction_argp_doc
};
RunInteractionConfig config;
argp_parse(&run_interaction_argp, argc, argv, ARGP_NO_ARGS | ARGP_IN_ORDER, 0, &config);
return config;
}
int main(int argc, char **argv) {
signal(SIGPIPE, SIG_IGN);
RunInteraction ri(parse_args(argc, argv));
ri.join();
return 0;
}

View File

@ -30,27 +30,21 @@ struct RunResult {
}
};
int put_result(RunResult res) {
printf("%d %d %d %d\n", res.result, res.ust, res.usm, res.exit_code);
if (res.result == RS_JGF) {
return 1;
} else {
return 0;
}
}
struct RunProgramConfig
{
int time_limit;
int real_time_limit;
int memory_limit;
int output_limit;
int stack_limit;
string input_file_name;
string output_file_name;
string error_file_name;
string result_file_name;
string work_path;
string type;
vector<string> extra_readable_files;
vector<string> extra_writable_files;
bool allow_proc;
bool safe_mode;
bool need_show_trace_details;
@ -58,8 +52,29 @@ struct RunProgramConfig
string program_name;
string program_basename;
vector<string> argv;
};
int put_result(string result_file_name, RunResult res) {
FILE *f;
if (result_file_name == "stdout") {
f = stdout;
} else if (result_file_name == "stderr") {
f = stderr;
} else {
f = fopen(result_file_name.c_str(), "w");
}
fprintf(f, "%d %d %d %d\n", res.result, res.ust, res.usm, res.exit_code);
if (f != stdout && f != stderr) {
fclose(f);
}
if (res.result == RS_JGF) {
return 1;
} else {
return 0;
}
}
char self_path[PATH_MAX + 1] = {};
#include "run_program_conf.h"
@ -67,19 +82,23 @@ char self_path[PATH_MAX + 1] = {};
argp_option run_program_argp_options[] =
{
{"tl" , 'T', "TIME_LIMIT" , 0, "Set time limit (in second)" , 1},
{"ml" , 'M', "MEMORY_LIMIT", 0, "Set memory limit (in mb)" , 2},
{"ol" , 'O', "OUTPUT_LIMIT", 0, "Set output limit (in mb)" , 3},
{"sl" , 'S', "STACK_LIMIT" , 0, "Set stack limit (in mb)" , 4},
{"in" , 'i', "IN" , 0, "Set input file name" , 5},
{"out" , 'o', "OUT" , 0, "Set output file name" , 6},
{"err" , 'e', "ERR" , 0, "Set error file name" , 7},
{"work-path" , 'w', "WORK_PATH" , 0, "Set the work path of the program" , 8},
{"type" , 't', "TYPE" , 0, "Set the program type (for some program such as python)", 9},
{"add-readable" , 500, "FILE" , 0, "Add a readable file" , 10},
{"unsafe" , 501, 0 , 0, "Don't check dangerous syscalls" , 11},
{"show-trace-details" , 502, 0 , 0, "Show trace details" , 12},
{"allow-proc" , 503, 0 , 0, "Allow fork, exec... etc." , 13},
{"add-readable-raw" , 504, "FOLDER" , 0, "Add a readable (don't transform to its real path)" , 14},
{"rtl" , 'R', "TIME_LIMIT" , 0, "Set real time limit (in second)" , 2},
{"ml" , 'M', "MEMORY_LIMIT", 0, "Set memory limit (in mb)" , 3},
{"ol" , 'O', "OUTPUT_LIMIT", 0, "Set output limit (in mb)" , 4},
{"sl" , 'S', "STACK_LIMIT" , 0, "Set stack limit (in mb)" , 5},
{"in" , 'i', "IN" , 0, "Set input file name" , 6},
{"out" , 'o', "OUT" , 0, "Set output file name" , 7},
{"err" , 'e', "ERR" , 0, "Set error file name" , 8},
{"work-path" , 'w', "WORK_PATH" , 0, "Set the work path of the program" , 9},
{"type" , 't', "TYPE" , 0, "Set the program type (for some program such as python)", 10},
{"res" , 'r', "RESULT_FILE" , 0, "Set the file name for outputing the result ", 10},
{"add-readable" , 500, "FILE" , 0, "Add a readable file" , 11},
{"add-writable" , 505, "FILE" , 0, "Add a writable file" , 11},
{"unsafe" , 501, 0 , 0, "Don't check dangerous syscalls" , 12},
{"show-trace-details" , 502, 0 , 0, "Show trace details" , 13},
{"allow-proc" , 503, 0 , 0, "Allow fork, exec... etc." , 14},
{"add-readable-raw" , 504, "FILE" , 0, "Add a readable (don't transform to its real path)" , 15},
{"add-writable-raw" , 506, "FILE" , 0, "Add a writable (don't transform to its real path)" , 15},
{0}
};
error_t run_program_argp_parse_opt (int key, char *arg, struct argp_state *state)
@ -91,6 +110,9 @@ error_t run_program_argp_parse_opt (int key, char *arg, struct argp_state *state
case 'T':
config->time_limit = atoi(arg);
break;
case 'R':
config->real_time_limit = atoi(arg);
break;
case 'M':
config->memory_limit = atoi(arg);
break;
@ -115,6 +137,9 @@ error_t run_program_argp_parse_opt (int key, char *arg, struct argp_state *state
argp_usage(state);
}
break;
case 'r':
config->result_file_name = arg;
break;
case 't':
config->type = arg;
break;
@ -133,6 +158,12 @@ error_t run_program_argp_parse_opt (int key, char *arg, struct argp_state *state
case 504:
config->extra_readable_files.push_back(arg);
break;
case 505:
config->extra_writable_files.push_back(realpath(arg));
break;
case 506:
config->extra_writable_files.push_back(arg);
break;
case ARGP_KEY_ARG:
config->argv.push_back(arg);
for (int i = state->next; i < state->argc; i++) {
@ -164,6 +195,7 @@ RunProgramConfig run_program_config;
void parse_args(int argc, char **argv) {
run_program_config.time_limit = 1;
run_program_config.real_time_limit = -1;
run_program_config.memory_limit = 256;
run_program_config.output_limit = 64;
run_program_config.stack_limit = 1024;
@ -171,6 +203,7 @@ void parse_args(int argc, char **argv) {
run_program_config.output_file_name = "stdout";
run_program_config.error_file_name = "stderr";
run_program_config.work_path = "";
run_program_config.result_file_name = "stdout";
run_program_config.type = "default";
run_program_config.safe_mode = true;
run_program_config.need_show_trace_details = false;
@ -178,11 +211,13 @@ void parse_args(int argc, char **argv) {
argp_parse(&run_program_argp, argc, argv, ARGP_NO_ARGS | ARGP_IN_ORDER, 0, &run_program_config);
if (run_program_config.real_time_limit == -1)
run_program_config.real_time_limit = run_program_config.time_limit + 2;
run_program_config.stack_limit = min(run_program_config.stack_limit, run_program_config.memory_limit);
if (!run_program_config.work_path.empty()) {
if (chdir(run_program_config.work_path.c_str()) == -1) {
exit(put_result(RS_JGF));
exit(put_result(run_program_config.result_file_name, RS_JGF));
}
}
@ -197,7 +232,7 @@ void parse_args(int argc, char **argv) {
run_program_config.argv[0] = "./" + run_program_config.program_basename;
if (chdir(run_program_config.work_path.c_str()) == -1) {
exit(put_result(RS_JGF));
exit(put_result(run_program_config.result_file_name, RS_JGF));
}
}
@ -230,7 +265,7 @@ void set_limit(int r, int rcur, int rmax = -1) {
}
}
void run_child() {
set_limit(RLIMIT_CPU, run_program_config.time_limit, run_program_config.time_limit + 2);
set_limit(RLIMIT_CPU, run_program_config.time_limit, run_program_config.real_time_limit);
set_limit(RLIMIT_FSIZE, run_program_config.output_limit << 20);
set_limit(RLIMIT_STACK, run_program_config.stack_limit << 20);
@ -352,7 +387,7 @@ RunResult trace_children() {
return RunResult(RS_JGF);
} else if (rp_timer_pid == 0) {
struct timespec ts;
ts.tv_sec = run_program_config.time_limit + 2;
ts.tv_sec = run_program_config.real_time_limit;
ts.tv_nsec = 0;
nanosleep(&ts, NULL);
exit(0);
@ -536,11 +571,11 @@ int main(int argc, char **argv) {
pid_t pid = fork();
if (pid == -1) {
return put_result(RS_JGF);
return put_result(run_program_config.result_file_name, RS_JGF);
} else if (pid == 0) {
run_child();
} else {
return put_result(run_parent(pid));
return put_result(run_program_config.result_file_name, run_parent(pid));
}
return put_result(RS_JGF);
return put_result(run_program_config.result_file_name, RS_JGF);
}

View File

@ -311,6 +311,9 @@ void init_conf(const RunProgramConfig &config) {
for (vector<string>::const_iterator it = config.extra_readable_files.begin(); it != config.extra_readable_files.end(); it++) {
add_file_permission(*it, 'r');
}
for (vector<string>::const_iterator it = config.extra_writable_files.begin(); it != config.extra_writable_files.end(); it++) {
add_file_permission(*it, 'w');
}
writable_file_name_set.insert("/dev/null");

View File

@ -40,8 +40,8 @@ return [
'username' => 'noreply@local_uoj.ac',
'password' => '_mail_noreply_password_',
'host' => 'smtp.local_uoj.ac',
'secure' => '',
'port' => 25
'secure' => 'tls',
'port' => 587
]
],
'judger' => [

View File

@ -6,129 +6,75 @@
}
genMoreContestInfo($contest);
if (!hasContestPermission($myUser, $contest)) {
if (!hasContestPermission(Auth::user(), $contest)) {
if ($contest['cur_progress'] == CONTEST_NOT_STARTED) {
header("Location: /contest/{$contest['id']}/register");
die();
} elseif ($contest['cur_progress'] == CONTEST_IN_PROGRESS) {
if ($myUser == null || !hasRegistered($myUser, $contest)) {
if ($myUser == null || !hasRegistered(Auth::user(), $contest)) {
becomeMsgPage("<h1>比赛正在进行中</h1><p>很遗憾,您尚未报名。比赛结束后再来看吧~</p>");
}
}
}
if (isset($_POST['check_notice'])) {
$result = DB::query("select * from contests_notice where contest_id = '${contest['id']}' order by time desc limit 1");
try {
while ($row = DB::fetch($result)) {
if (new DateTime($row['time']) > new DateTime($_POST['last_time'])) {
die(json_encode(array('msg' => $row['title'] . ' : ' . $row['content'], 'time' => UOJTime::$time_now_str)));
}
}
} catch (Exception $e) {
}
die(json_encode(array('time' => UOJTime::$time_now_str)));
}
if (isset($_GET['tab'])) {
$cur_tab = $_GET['tab'];
} else {
$cur_tab = 'dashboard';
}
// problems: pos => id
// data : id, submit_time, submitter, problem_pos, score
// people : username, user_rating
function queryContestData() {
global $contest;
$problems = array();
$prob_pos = array();
$n_problems = 0;
$result = DB::query("select problem_id from contests_problems where contest_id = ${contest['id']} order by problem_id");
while ($row = DB::fetch($result, MYSQLI_NUM)) {
$prob_pos[$problems[] = (int)$row[0]] = $n_problems++;
}
$data = array();
if ($contest['cur_progress'] < CONTEST_FINISHED) {
$result = DB::query("select id, submit_time, submitter, problem_id, score from submissions where contest_id = {$contest['id']} and score is not null order by id");
} else {
$result = DB::query("select submission_id, date_add('{$contest['start_time_str']}', interval penalty second), submitter, problem_id, score from contests_submissions where contest_id = {$contest['id']}");
}
while ($row = DB::fetch($result, MYSQLI_NUM)) {
$row[0] = (int)$row[0];
$row[3] = $prob_pos[$row[3]];
$row[4] = (int)$row[4];
$data[] = $row;
}
$people = array();
$result = DB::query("select username, user_rating from contests_registrants where contest_id = {$contest['id']} and has_participated = 1");
while ($row = DB::fetch($result, MYSQLI_NUM)) {
$row[1] = (int)$row[1];
$people[] = $row;
}
return array('problems' => $problems, 'data' => $data, 'people' => $people);
$tabs_info = array(
'dashboard' => array(
'name' => UOJLocale::get('contests::contest dashboard'),
'url' => "/contest/{$contest['id']}"
),
'submissions' => array(
'name' => UOJLocale::get('contests::contest submissions'),
'url' => "/contest/{$contest['id']}/submissions"
),
'standings' => array(
'name' => UOJLocale::get('contests::contest standings'),
'url' => "/contest/{$contest['id']}/standings"
)
);
if (hasContestPermission(Auth::user(), $contest)) {
$tabs_info['backstage'] = array(
'name' => UOJLocale::get('contests::contest backstage'),
'url' => "/contest/{$contest['id']}/backstage"
);
}
function calcStandings($contest_data, &$score, &$standings, $update_contests_submissions = false) {
global $contest;
// score: username, problem_pos => score, penalty, id
$score = array();
$n_people = count($contest_data['people']);
$n_problems = count($contest_data['problems']);
foreach ($contest_data['people'] as $person) {
$score[$person[0]] = array();
}
foreach ($contest_data['data'] as $submission) {
$penalty = (new DateTime($submission[1]))->getTimestamp() - $contest['start_time']->getTimestamp();
if ($contest['extra_config']['standings_version'] >= 2) {
if ($submission[4] == 0) {
$penalty = 0;
if (!isset($tabs_info[$cur_tab])) {
become404Page();
}
if (isset($_POST['check_notice'])) {
$result = DB::query("select * from contests_notice where contest_id = '${contest['id']}' order by time desc limit 10");
$ch = array();
$flag = false;
try {
while ($row = DB::fetch($result)) {
if (new DateTime($row['time']) > new DateTime($_POST['last_time'])) {
$ch[] = $row['title'].': '.$row['content'];
}
}
$score[$submission[2]][$submission[3]] = array($submission[4], $penalty, $submission[0]);
} catch (Exception $e) {
}
// standings: rank => score, penalty, [username, user_rating], virtual_rank
$standings = array();
foreach ($contest_data['people'] as $person) {
$cur = array(0, 0, $person);
for ($i = 0; $i < $n_problems; $i++) {
if (isset($score[$person[0]][$i])) {
$cur_row = $score[$person[0]][$i];
$cur[0] += $cur_row[0];
$cur[1] += $cur_row[1];
if ($update_contests_submissions) {
DB::insert("insert into contests_submissions (contest_id, submitter, problem_id, submission_id, score, penalty) values ({$contest['id']}, '{$person[0]}', {$contest_data['problems'][$i]}, {$cur_row[2]}, {$cur_row[0]}, {$cur_row[1]})");
}
global $myUser;
$result=DB::query("select * from contests_asks where contest_id='${contest['id']}' and username='${myUser['username']}' order by reply_time desc limit 10");
try {
while ($row = DB::fetch($result)) {
if (new DateTime($row['reply_time']) > new DateTime($_POST['last_time'])) {
$ch[] = $row['question'].': '.$row['answer'];
}
}
$standings[] = $cur;
} catch (Exception $e) {
}
usort($standings, function($lhs, $rhs) {
if ($lhs[0] != $rhs[0]) {
return $rhs[0] - $lhs[0];
} else if ($lhs[1] != $rhs[1]) {
return $lhs[1] - $rhs[1];
} else {
return strcmp($lhs[2][0], $rhs[2][0]);
}
});
$is_same_rank = function($lhs, $rhs) {
return $lhs[0] == $rhs[0] && $lhs[1] == $rhs[1];
};
for ($i = 0; $i < $n_people; $i++) {
if ($i == 0 || !$is_same_rank($standings[$i - 1], $standings[$i])) {
$standings[$i][] = $i + 1;
} else {
$standings[$i][] = $standings[$i - 1][3];
}
if ($ch) {
die(json_encode(array('msg' => $ch, 'time' => UOJTime::$time_now_str)));
} else {
die(json_encode(array('time' => UOJTime::$time_now_str)));
}
}
@ -172,8 +118,8 @@
ignore_user_abort(true);
global $contest;
$contest_data = queryContestData();
calcStandings($contest_data, $score, $standings, true);
$contest_data = queryContestData($contest);
calcStandings($contest, $contest_data, $score, $standings, true);
if (!isset($contest['extra_config']['unrated'])) {
$rating_k = isset($contest['extra_config']['rating_k']) ? $contest['extra_config']['rating_k'] : 400;
$ratings = calcRating($standings, $rating_k);
@ -215,71 +161,206 @@ EOD;
}
}
function echoDashboard() {
global $myUser, $contest, $post_notice;
echo '<div class="table-responsive">';
echo '<table class="table table-bordered table-hover table-striped table-text-center">';
echo '<thead>';
echo '<th style="width:5em">#</th>';
echo '<th>', UOJLocale::get('problems::problem'), '</th>';
echo '</thead>';
echo '<tbody>';
$contest_problems = DB::selectAll("select contests_problems.problem_id, best_ac_submissions.submission_id from contests_problems left join best_ac_submissions on contests_problems.problem_id = best_ac_submissions.problem_id and submitter = '{$myUser['username']}' where contest_id = {$contest['id']} order by contests_problems.problem_id asc");
for ($i = 0; $i < count($contest_problems); $i++) {
$problem = queryProblemBrief($contest_problems[$i]['problem_id']);
echo '<tr>';
if ($contest_problems[$i]['submission_id']) {
echo '<td class="success">';
} else {
echo '<td>';
}
echo chr(ord('A') + $i), '</td>';
echo '<td>', getContestProblemLink($problem, $contest['id']), '</td>';
echo '</tr>';
if ($cur_tab == 'dashboard') {
if ($contest['cur_progress'] <= CONTEST_IN_PROGRESS) {
$post_question = new UOJForm('post_question');
$post_question->addVTextArea('qcontent', '问题', '',
function($content) {
if (!Auth::check()) {
return '您尚未登录';
}
if (!$content || strlen($content) == 0) {
return '问题不能为空';
}
if (strlen($content) > 140 * 4) {
return '问题太长';
}
return '';
},
null
);
$post_question->handle = function() {
global $contest;
$content = DB::escape($_POST['qcontent']);
$username = Auth::id();
DB::query("insert into contests_asks (contest_id, question, username, post_time, is_hidden) values ('{$contest['id']}', '$content', '$username', now(), 1)");
};
$post_question->runAtServer();
} else {
$post_question = null;
}
echo '</tbody>';
echo '</table>';
echo '</div>';
echo '<h3>', UOJLocale::get('contests::contest notice'), '</h3>';
$header = '';
$header .= '<tr>';
$header .= '<th style="width:10em">'.UOJLocale::get('title').'</th>';
$header .= '<th>'.UOJLocale::get('content').'</th>';
$header .= '<th style="width:12em">'.UOJLocale::get('time').'</th>';
$header .= '</tr>';
echoLongTable(array('*'), 'contests_notice', "contest_id = '{$contest['id']}'", "order by time desc", $header,
function($notice) {
echo '<tr>';
echo '<td>', HTML::escape($notice['title']), '</td>';
echo '<td style="white-space:pre-wrap; text-align: left">', $notice['content'], '</td>';
echo '<td>', $notice['time'], '</td>';
echo '</tr>';
},
array(
'table_classes' => array('table', 'table-bordered', 'table-hover', 'table-striped', 'table-vertical-middle', 'table-text-center'),
'echo_full' => true
)
);
} elseif ($cur_tab == 'backstage') {
if (isSuperUser(Auth::user())) {
echo '<div class="text-center">';
echo '<button id="button-display-post-notice" type="button" class="btn btn-danger btn-xs">发布比赛公告</button>';
echo '</div>';
echo '<div id="div-form-post-notice" style="display:none" class="bot-buffer-md">';
$post_notice->printHTML();
echo '</div>';
echo <<<EOD
<script type="text/javascript">
$(document).ready(function() {
$('#button-display-post-notice').click(function() {
$('#div-form-post-notice').toggle('fast');
});
});
</script>
EOD;
$post_notice = new UOJForm('post_notice');
$post_notice->addInput('title', 'text', '标题', '',
function($title) {
if (!$title) {
return '标题不能为空';
}
return '';
},
null
);
$post_notice->addTextArea('content', '正文', '',
function($content) {
if (!$content) {
return '公告不能为空';
}
return '';
},
null
);
$post_notice->handle = function() {
global $contest;
$title = DB::escape($_POST['title']);
$content = DB::escape($_POST['content']);
DB::insert("insert into contests_notice (contest_id, title, content, time) values ('{$contest['id']}', '$title', '$content', now())");
};
$post_notice->runAtServer();
} else {
$post_notice = null;
}
if (hasContestPermission(Auth::user(), $contest)) {
$reply_question = new UOJForm('reply_question');
$reply_question->addHidden('rid', '0',
function($id) {
global $contest;
if (!validateUInt($id)) {
return '无效ID';
}
$q = DB::selectFirst("select * from contests_asks where id = $id");
if ($q['contest_id'] != $contest['id']) {
return '无效ID';
}
return '';
},
null
);
$reply_question->addVSelect('rtype', [
'public' => '公开',
'private' => '非公开',
'statement' => '请仔细阅读题面(非公开)',
'no_comment' => '无可奉告(非公开)',
'no_play' => '请认真比赛(非公开)',
], '回复类型', 'private');
$reply_question->addVTextArea('rcontent', '回复', '',
function($content) {
if (!Auth::check()) {
return '您尚未登录';
}
switch ($_POST['rtype']) {
case 'public':
case 'private':
if (strlen($content) == 0) {
return '回复不能为空';
}
break;
}
return '';
},
null
);
$reply_question->handle = function() {
global $contest;
$content = DB::escape($_POST['rcontent']);
$is_hidden = 1;
switch ($_POST['rtype']) {
case 'statement':
$content = '请仔细阅读题面';
break;
case 'no_comment':
$content = '无可奉告 ╮(╯▽╰)╭ ';
break;
case 'no_play':
$content = '请认真比赛 ( ̄口 ̄)!!';
break;
case 'public':
$is_hidden = 0;
break;
default:
break;
}
DB::update("update contests_asks set answer = '$content', reply_time = now(), is_hidden = {$is_hidden} where id = {$_POST['rid']}");
};
$reply_question->runAtServer();
} else {
$reply_question = null;
}
}
function echoDashboard() {
global $contest, $post_notice, $post_question, $reply_question;
$myname = Auth::id();
$contest_problems = DB::selectAll("select contests_problems.problem_id, best_ac_submissions.submission_id from contests_problems left join best_ac_submissions on contests_problems.problem_id = best_ac_submissions.problem_id and submitter = '{$myname}' where contest_id = {$contest['id']} order by contests_problems.problem_id asc");
for ($i = 0; $i < count($contest_problems); $i++) {
$contest_problems[$i]['problem'] = queryProblemBrief($contest_problems[$i]['problem_id']);
}
$contest_notice = DB::selectAll("select * from contests_notice where contest_id = {$contest['id']} order by time desc");
if (Auth::check()) {
$my_questions = DB::selectAll("select * from contests_asks where contest_id = {$contest['id']} and username = '{$myname}' order by post_time desc");
$my_questions_pag = new Paginator([
'data' => $my_questions
]);
} else {
$my_questions_pag = null;
}
$others_questions_pag = new Paginator([
'col_names' => array('*'),
'table_name' => 'contests_asks',
'cond' => "contest_id = {$contest['id']} and username != '{$myname}' and is_hidden = 0",
'tail' => 'order by reply_time desc',
'page_len' => 10
]);
uojIncludeView('contest-dashboard', [
'contest' => $contest,
'contest_notice' => $contest_notice,
'contest_problems' => $contest_problems,
'post_question' => $post_question,
'my_questions_pag' => $my_questions_pag,
'others_questions_pag' => $others_questions_pag
]);
}
function echoBackstage() {
global $contest, $post_notice, $reply_question;
$questions_pag = new Paginator([
'col_names' => array('*'),
'table_name' => 'contests_asks',
'cond' => "contest_id = {$contest['id']}",
'tail' => 'order by post_time desc',
'page_len' => 50
]);
if ($contest['cur_progress'] < CONTEST_TESTING) {
$contest_data = queryContestData($contest, ['pre_final' => true]);
calcStandings($contest, $contest_data, $score, $standings);
$standings_data = [
'contest' => $contest,
'standings' => $standings,
'score' => $score,
'contest_data' => $contest_data
];
} else {
$standings_data = null;
}
uojIncludeView('contest-backstage', [
'contest' => $contest,
'post_notice' => $post_notice,
'reply_question' => $reply_question,
'questions_pag' => $questions_pag,
'standings_data' => $standings_data
]);
}
function echoMySubmissions() {
@ -312,27 +393,15 @@ EOD;
function echoStandings() {
global $contest;
$contest_data = queryContestData();
calcStandings($contest_data, $score, $standings);
echo '<div id="standings">';
echo '</div>';
/*
echo '<div class="table-responsive">';
echo '<table id="standings-table" class="table table-bordered table-striped table-text-center table-vertical-middle">';
echo '</table>';
echo '</div>';
*/
echo '<script type="text/javascript">';
echo 'standings_version=', $contest['extra_config']['standings_version'], ';';
echo 'contest_id=', $contest['id'], ';';
echo 'standings=', json_encode($standings), ';';
echo 'score=', json_encode($score), ';';
echo 'problems=', json_encode($contest_data['problems']), ';';
echo '$(document).ready(showStandings());';
echo '</script>';
$contest_data = queryContestData($contest);
calcStandings($contest, $contest_data, $score, $standings);
uojIncludeView('contest-standings', [
'contest' => $contest,
'standings' => $standings,
'score' => $score,
'contest_data' => $contest_data
]);
}
function echoContestCountdown() {
@ -389,52 +458,6 @@ EOD;
EOD;
}
$post_notice = new UOJForm('post_notice');
$post_notice->addInput('title', 'text', '标题', '',
function($title) {
if (!$title) {
return '标题不能为空';
}
return '';
},
null
);
$post_notice->addTextArea('content', '正文', '',
function($content) {
if (!$content) {
return '公告不能为空';
}
return '';
},
null
);
$post_notice->handle = function() {
global $contest;
$title = DB::escape($_POST['title']);
$content = DB::escape($_POST['content']);
DB::query("insert into contests_notice (contest_id, title, content, time) values ('{$contest['id']}', '$title', '$content', now())");
};
$post_notice->runAtServer();
$tabs_info = array(
'dashboard' => array(
'name' => UOJLocale::get('contests::contest dashboard'),
'url' => "/contest/{$contest['id']}"
),
'submissions' => array(
'name' => UOJLocale::get('contests::contest submissions'),
'url' => "/contest/{$contest['id']}/submissions"
),
'standings' => array(
'name' => UOJLocale::get('contests::contest standings'),
'url' => "/contest/{$contest['id']}/standings"
)
);
if (!isset($tabs_info[$cur_tab])) {
become404Page();
}
$page_header = HTML::stripTags($contest['name']) . ' - ';
?>
<?php echoUOJPageHeader(HTML::stripTags($contest['name']) . ' - ' . $tabs_info[$cur_tab]['name'] . ' - ' . UOJLocale::get('contests::contest')) ?>
@ -457,6 +480,8 @@ EOD;
echoMySubmissions();
} elseif ($cur_tab == 'standings') {
echoStandings();
} elseif ($cur_tab == 'backstage') {
echoBackstage();
}
?>
</div>

View File

@ -198,7 +198,7 @@
$submission['id'] = (int)$submission['id'];
$submission['problem_id'] = (int)$submission['problem_id'];
$submission['problem_mtime'] = filemtime("/var/uoj_data/{$submission['problem_id']}.zip");
$submission['problem_mtime'] = filemtime("/var/uoj_data/{$submission['problem_id']}");
$submission['content'] = json_decode($submission['content']);
if ($hack) {

View File

@ -34,6 +34,8 @@ EOD;
$mailer->msgHTML($html);
if ($mailer->send()) {
echo 'good';
} else {
error_log('PHPMailer: '.$mailer->ErrorInfo);
}
die();
}
@ -147,7 +149,6 @@ EOD;
$info_form = new UOJForm('info');
$http_host = HTML::escape(UOJContext::httpHost());
//$download_url = HTML::escape(HTML::url("/download.php?type=problem&id={$problem['id']}"));
$download_url = HTML::url("/download.php?type=problem&id={$problem['id']}");
$info_form->appendHTML(<<<EOD
<div class="form-group">
@ -487,17 +488,22 @@ EOD
);
}
if (isset($problem_conf['use_builtin_checker'])) {
$data_disp->addDisplayer('checker', function($self) {
echo '<h4>use builtin checker : ', $self->problem_conf['use_builtin_checker']['val'], '</h4>';
});
} else {
$data_disp->addDisplayer('checker', $getDisplaySrcFunc('chk'));
if (!isset($problem_conf['interaction_mode'])) {
if (isset($problem_conf['use_builtin_checker'])) {
$data_disp->addDisplayer('checker', function($self) {
echo '<h4>use builtin checker : ', $self->problem_conf['use_builtin_checker']['val'], '</h4>';
});
} else {
$data_disp->addDisplayer('checker', $getDisplaySrcFunc('chk'));
}
}
if ($problem['hackable']) {
$data_disp->addDisplayer('standard', $getDisplaySrcFunc('std'));
$data_disp->addDisplayer('validator', $getDisplaySrcFunc('val'));
}
if (isset($problem_conf['interaction_mode'])) {
$data_disp->addDisplayer('interactor', $getDisplaySrcFunc('interactor'));
}
return $data_disp;
} else {
return (new DataDisplayer($problem_conf))->setProblemConfRowStatus('use_builtin_judger', 'danger');
@ -535,6 +541,7 @@ EOD
$data_form = new UOJForm('data');
$data_form->handle = function() {
global $problem, $myUser;
set_time_limit(60 * 5);
$ret = svnSyncProblemData($problem, $myUser);
if ($ret) {
becomeMsgPage('<div>' . $ret . '</div><a href="/problem/'.$problem['id'].'/manage/data">返回</a>');
@ -563,6 +570,16 @@ EOD
$rejudge_form->submit_button_config['text'] = '重测该题';
$rejudge_form->submit_button_config['smart_confirm'] = '';
$rejudgege97_form = new UOJForm('rejudgege97');
$rejudgege97_form->handle = function() {
global $problem;
rejudgeProblemGe97($problem);
};
$rejudgege97_form->succ_href = "/submissions?problem_id={$problem['id']}";
$rejudgege97_form->submit_button_config['class_str'] = 'btn btn-danger btn-block';
$rejudgege97_form->submit_button_config['text'] = '重测 >=97 的程序';
$rejudgege97_form->submit_button_config['smart_confirm'] = '';
$view_type_form = new UOJForm('view_type');
$view_type_form->addVSelect('view_content_type',
array('NONE' => '禁止',
@ -660,6 +677,7 @@ EOD
$data_form->runAtServer();
$clear_data_form->runAtServer();
$rejudge_form->runAtServer();
$rejudgege97_form->runAtServer();
$info_form->runAtServer();
?>
<?php
@ -739,8 +757,10 @@ EOD
<div class="top-buffer-md">
<?php $rejudge_form->printHTML(); ?>
</div>
<div class="top-buffer-md">
<?php $rejudgege97_form->printHTML(); ?>
</div>
<div class="top-buffer-md">
<button type="button" class="btn btn-block btn-primary" data-toggle="modal" data-target="#UploadDataModal">上传数据</button>
</div>

View File

@ -29,7 +29,11 @@
echo '<td>';
}
echo '#', $problem['id'], '</td>';
echo '<td class="text-left">', '<a href="/problem/', $problem['id'], '">', $problem['title'], '</a>';
echo '<td class="text-left">';
if ($problem['is_hidden']) {
echo ' <span class="text-danger">[隐藏]</span> ';
}
echo '<a href="/problem/', $problem['id'], '">', $problem['title'], '</a>';
if (isset($_COOKIE['show_tags_mode'])) {
foreach (queryProblemTags($problem['id']) as $tag) {
echo '<a class="uoj-problem-tag">', '<span class="badge">', HTML::escape($tag), '</span>', '</a>';

View File

@ -125,3 +125,117 @@ function genMoreContestInfo(&$contest) {
function updateContestPlayerNum($contest) {
DB::update("update contests set player_num = (select count(*) from contests_registrants where contest_id = {$contest['id']}) where id = {$contest['id']}");
}
// problems: pos => id
// data : id, submit_time, submitter, problem_pos, score
// people : username, user_rating
function queryContestData($contest, $config = array()) {
mergeConfig($config, [
'pre_final' => false
]);
$problems = [];
$prob_pos = [];
$n_problems = 0;
$result = DB::query("select problem_id from contests_problems where contest_id = {$contest['id']} order by problem_id");
while ($row = DB::fetch($result, MYSQLI_NUM)) {
$prob_pos[$problems[] = (int)$row[0]] = $n_problems++;
}
$data = [];
if ($config['pre_final']) {
$result = DB::query("select id, submit_time, submitter, problem_id, result from submissions"
." where contest_id = {$contest['id']} and score is not null order by id");
while ($row = DB::fetch($result, MYSQLI_NUM)) {
$r = json_decode($row[4], true);
if (!isset($r['final_result'])) {
continue;
}
$row[0] = (int)$row[0];
$row[3] = $prob_pos[$row[3]];
$row[4] = (int)($r['final_result']['score']);
$data[] = $row;
}
} else {
if ($contest['cur_progress'] < CONTEST_FINISHED) {
$result = DB::query("select id, submit_time, submitter, problem_id, score from submissions"
." where contest_id = {$contest['id']} and score is not null order by id");
} else {
$result = DB::query("select submission_id, date_add('{$contest['start_time_str']}', interval penalty second),"
." submitter, problem_id, score from contests_submissions where contest_id = {$contest['id']}");
}
while ($row = DB::fetch($result, MYSQLI_NUM)) {
$row[0] = (int)$row[0];
$row[3] = $prob_pos[$row[3]];
$row[4] = (int)$row[4];
$data[] = $row;
}
}
$people = [];
$result = DB::query("select username, user_rating from contests_registrants where contest_id = {$contest['id']} and has_participated = 1");
while ($row = DB::fetch($result, MYSQLI_NUM)) {
$row[1] = (int)$row[1];
$people[] = $row;
}
return ['problems' => $problems, 'data' => $data, 'people' => $people];
}
function calcStandings($contest, $contest_data, &$score, &$standings, $update_contests_submissions = false) {
// score: username, problem_pos => score, penalty, id
$score = array();
$n_people = count($contest_data['people']);
$n_problems = count($contest_data['problems']);
foreach ($contest_data['people'] as $person) {
$score[$person[0]] = array();
}
foreach ($contest_data['data'] as $submission) {
$penalty = (new DateTime($submission[1]))->getTimestamp() - $contest['start_time']->getTimestamp();
if ($contest['extra_config']['standings_version'] >= 2) {
if ($submission[4] == 0) {
$penalty = 0;
}
}
$score[$submission[2]][$submission[3]] = array($submission[4], $penalty, $submission[0]);
}
// standings: rank => score, penalty, [username, user_rating], virtual_rank
$standings = array();
foreach ($contest_data['people'] as $person) {
$cur = array(0, 0, $person);
for ($i = 0; $i < $n_problems; $i++) {
if (isset($score[$person[0]][$i])) {
$cur_row = $score[$person[0]][$i];
$cur[0] += $cur_row[0];
$cur[1] += $cur_row[1];
if ($update_contests_submissions) {
DB::insert("insert into contests_submissions (contest_id, submitter, problem_id, submission_id, score, penalty) values ({$contest['id']}, '{$person[0]}', {$contest_data['problems'][$i]}, {$cur_row[2]}, {$cur_row[0]}, {$cur_row[1]})");
}
}
}
$standings[] = $cur;
}
usort($standings, function($lhs, $rhs) {
if ($lhs[0] != $rhs[0]) {
return $rhs[0] - $lhs[0];
} else if ($lhs[1] != $rhs[1]) {
return $lhs[1] - $rhs[1];
} else {
return strcmp($lhs[2][0], $rhs[2][0]);
}
});
$is_same_rank = function($lhs, $rhs) {
return $lhs[0] == $rhs[0] && $lhs[1] == $rhs[1];
};
for ($i = 0; $i < $n_people; $i++) {
if ($i == 0 || !$is_same_rank($standings[$i - 1], $standings[$i])) {
$standings[$i][] = $i + 1;
} else {
$standings[$i][] = $standings[$i - 1][3];
}
}
}

View File

@ -12,6 +12,7 @@
private $data = array();
private $vdata = array();
private $main_html = '';
public $max_post_size = 15728640; // 15M
public $handle;
@ -34,6 +35,16 @@
becomeMsgPage('The form is incomplete.');
}
}
if (UOJContext::requestMethod() == 'POST') {
$len = UOJContext::contentLength();
if ($len === null) {
become403Page();
} elseif ($len > $this->max_post_size) {
becomeMsgPage('The form is too large.');
}
}
crsf_defend();
$errors = $this->validateAtServer();
if ($errors) {
@ -130,7 +141,7 @@ EOD;
function($opt) use ($options) {
return isset($options[$opt]) ? '' : "无效选项";
},
'always_ok'
null
);
}
@ -143,7 +154,7 @@ EOD;
$html = <<<EOD
<div id="div-$name">
<label for="input-$name" class="control-label">$label_text</label>
<select class="form-control" id="input-content" name="$name">
<select class="form-control" id="input-{$name}" name="$name">
EOD;
foreach ($options as $opt_name => $opt_label) {
@ -167,7 +178,7 @@ EOD;
function($opt) use ($options) {
return isset($options[$opt]) ? '' : "无效选项";
},
'always_ok'
null
);
}

View File

@ -164,6 +164,9 @@
function rejudgeProblemAC($problem) {
DB::query("update submissions set judge_time = NULL , result = '' , score = NULL , status = 'Waiting Rejudge' where problem_id = ${problem['id']} and score = 100");
}
function rejudgeProblemGe97($problem) {
DB::query("update submissions set judge_time = NULL , result = '' , score = NULL , status = 'Waiting Rejudge' where problem_id = ${problem['id']} and score >= 97");
}
function rejudgeSubmission($submission) {
DB::query("update submissions set judge_time = NULL , result = '' , score = NULL , status = 'Waiting Rejudge' where id = ${submission['id']}");
}

View File

@ -53,6 +53,10 @@
$this->user = $user;
}
private function check_conf_on($name) {
return isset($this->problem_conf[$name]) && $this->problem_conf[$name] == 'on';
}
private function copy_to_prepare($file_name) {
global $uojMainJudgerWorkPath;
if (!isset($this->allow_files[$file_name])) {
@ -194,7 +198,7 @@
$this->copy_to_prepare('require');
}
if (isset($this->problem_conf['use_builtin_judger']) && $this->problem_conf['use_builtin_judger'] == 'on') {
if ($this->check_conf_on('use_builtin_judger')) {
$n_tests = getUOJConfVal($this->problem_conf, 'n_tests', 10);
if (!validateUInt($n_tests) || $n_tests <= 0) {
throw new UOJProblemConfException("n_tests must be a positive integer");
@ -207,16 +211,18 @@
$this->copy_file_to_prepare($output_file_name);
}
if (isset($this->problem_conf['use_builtin_checker'])) {
if (!preg_match('/^[a-zA-Z0-9_]{1,20}$/', $this->problem_conf['use_builtin_checker'])) {
throw new Exception("<strong>" . htmlspecialchars($this->problem_conf['use_builtin_checker']) . "</strong> is not a valid checker");
if (!$this->check_conf_on('interaction_mode')) {
if (isset($this->problem_conf['use_builtin_checker'])) {
if (!preg_match('/^[a-zA-Z0-9_]{1,20}$/', $this->problem_conf['use_builtin_checker'])) {
throw new Exception("<strong>" . htmlspecialchars($this->problem_conf['use_builtin_checker']) . "</strong> is not a valid checker");
}
} else {
$this->copy_file_to_prepare('chk.cpp');
$this->compile_at_prepare('chk', array('need_include_header' => true));
}
} else {
$this->copy_file_to_prepare('chk.cpp');
$this->compile_at_prepare('chk', array('need_include_header' => true));
}
if (isset($this->problem_conf['submit_answer']) && $this->problem_conf['submit_answer'] == 'on') {
if ($this->check_conf_on('submit_answer')) {
if ($this->problem['hackable']) {
throw new UOJProblemConfException("the problem can't be hackable if submit_answer is on");
}
@ -260,6 +266,11 @@
$this->copy_file_to_prepare('val.cpp');
$this->compile_at_prepare('val', array('need_include_header' => true));
}
if ($this->check_conf_on('interaction_mode')) {
$this->copy_file_to_prepare('interactor.cpp');
$this->compile_at_prepare('interactor', array('need_include_header' => true));
}
$n_sample_tests = getUOJConfVal($this->problem_conf, 'n_sample_tests', $n_tests);
if (!validateUInt($n_sample_tests) || $n_sample_tests < 0) {
@ -269,19 +280,21 @@
throw new UOJProblemConfException("n_sample_tests can't be greater than n_ex_tests");
}
for ($num = 1; $num <= $n_sample_tests; $num++) {
$input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num);
$output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num);
$zip_file->addFile("{$this->prepare_dir}/{$input_file_name}", "$input_file_name");
if (!isset($this->problem_extra_config['dont_download_sample_output'])) {
$zip_file->addFile("{$this->prepare_dir}/{$output_file_name}", "$output_file_name");
if (!isset($this->problem_extra_config['dont_download_sample'])) {
for ($num = 1; $num <= $n_sample_tests; $num++) {
$input_file_name = getUOJProblemExtraInputFileName($this->problem_conf, $num);
$output_file_name = getUOJProblemExtraOutputFileName($this->problem_conf, $num);
$zip_file->addFile("{$this->prepare_dir}/{$input_file_name}", "$input_file_name");
if (!isset($this->problem_extra_config['dont_download_sample_output'])) {
$zip_file->addFile("{$this->prepare_dir}/{$output_file_name}", "$output_file_name");
}
}
}
$this->requirement[] = array('name' => 'answer', 'type' => 'source code', 'file_name' => 'answer.code');
}
} else {
if (isSuperUser($user)) {
if (!isSuperUser($this->user)) {
throw new UOJProblemConfException("use_builtin_judger must be on.");
} else {
foreach ($this->allow_files as $file_name => $file_num) {

View File

@ -18,6 +18,7 @@ return [
'contest dashboard' => 'Dashboard',
'contest submissions' => 'Submissions',
'contest standings' => 'Standings',
'contest backstage' => 'Backstage',
'contest notice' => 'Notice',
'show all submissions' => 'Show all submissions',
'contest ends in' => 'Contest ends in',

View File

@ -18,6 +18,7 @@ return [
'contest dashboard' => '比赛主页',
'contest submissions' => '提交记录',
'contest standings' => '排行榜',
'contest backstage' => '大后台',
'contest notice' => '比赛通知',
'show all submissions' => '显示所有提交',
'contest ends in' => '距离比赛结束',

View File

@ -10,7 +10,12 @@ class Paginator {
public $table;
public function __construct($config) {
if (!isset($config['echo_full'])) {
if (isset($config['data'])) {
$this->n_pages = 1;
$this->cur_page = 1;
$this->cur_start = 0;
$this->table = $config['data'];
} elseif (!isset($config['echo_full'])) {
$this->n_rows = DB::selectCount("select count(*) from {$config['table_name']} where {$config['cond']}");
$this->page_len = isset($config['page_len']) ? $config['page_len'] : 10;

View File

@ -21,6 +21,13 @@ class UOJContext {
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
}
public static function contentLength() {
if (!isset($_SERVER['CONTENT_LENGTH'])) {
return null;
}
return (int)$_SERVER['CONTENT_LENGTH'];
}
public static function documentRoot() {
return $_SERVER['DOCUMENT_ROOT'];
}

View File

@ -8,7 +8,7 @@ Route::pattern('rand_str_id', '[0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL
Route::pattern('upgrade_name', '[a-zA-Z0-9_]{1,50}');
Route::group([
'domain' => UOJConfig::$data['web']['main']['host']."|127.0.0.1"
'domain' => '('.UOJConfig::$data['web']['main']['host'].'|127.0.0.1'.')'
], function() {
Route::any('/', '/index.php');
Route::any('/problems', '/problem_set.php');
@ -27,6 +27,7 @@ Route::group([
Route::any('/contest/{id}/manage', '/contest_manage.php');
Route::any('/contest/{id}/submissions', '/contest_inside.php?tab=submissions');
Route::any('/contest/{id}/standings', '/contest_inside.php?tab=standings');
Route::any('/contest/{id}/backstage', '/contest_inside.php?tab=backstage');
Route::any('/contest/{contest_id}/problem/{id}', '/problem.php');
Route::any('/contest/{contest_id}/problem/{id}/statistics', '/problem_statistics.php');

View File

@ -0,0 +1 @@
alter table contests_asks drop column is_hidden;

View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS `contests_asks` (
`id` int(11) NOT NULL primary key AUTO_INCREMENT,
`contest_id` int(11) NOT NULL,
`username` varchar(20) NOT NULL,
`question` text NOT NULL,
`answer` text NOT NULL,
`post_time` datetime NOT NULL,
`reply_time` datetime NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

View File

@ -0,0 +1 @@
drop table `contests_asks`;

View File

@ -0,0 +1 @@
alter table contests_asks add is_hidden boolean default false;

View File

@ -0,0 +1,28 @@
<ul class="nav nav-tabs" role="tablist">
<li class="active"><a href="#tab-question" role="tab" data-toggle="tab">提问</a></li>
<?php if ($post_notice): ?>
<li><a href="#tab-notice" role="tab" data-toggle="tab">公告</a></li>
<?php endif ?>
<?php if ($standings_data): ?>
<li><a href="#tab-standings" role="tab" data-toggle="tab">终榜</a></li>
<?php endif ?>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="tab-question">
<h3>提问</h3>
<?php uojIncludeView('contest-question-table', ['pag' => $questions_pag, 'can_reply' => true, 'reply_question' => $reply_question]) ?>
</div>
<?php if ($post_notice): ?>
<div class="tab-pane" id="tab-notice">
<h3>发布比赛公告</h3>
<?php $post_notice->printHTML() ?>
</div>
<?php endif ?>
<?php if ($standings_data): ?>
<div class="tab-pane" id="tab-standings">
<h3>终榜</h3>
<?php uojIncludeView('contest-standings', $standings_data) ?>
</div>
<?php endif ?>
</div>

View File

@ -0,0 +1,97 @@
<div class="table-responsive">
<table class="table table-bordered table-hover table-striped table-text-center">
<thead>
<tr>
<th style="width:5em">#</th>
<th><?= UOJLocale::get('problems::problem') ?></th>
</tr>
</thead>
<tbody>
<?php for ($i = 0; $i < count($contest_problems); $i++): ?>
<tr>
<?php
echo $contest_problems[$i]['submission_id'] ? '<td class="success">' : '<td>';
echo chr(ord('A') + $i);
echo '</td>';
?>
<td><?= getContestProblemLink($contest_problems[$i]['problem'], $contest['id']) ?></td>
</tr>
<?php endfor ?>
</tbody>
</table>
</div>
<h3><?= UOJLocale::get('contests::contest notice') ?></h3>
<div class="table-responsive">
<table class="table table-bordered table-hover table-vertical-middle table-text-center">
<thead>
<tr>
<th style="width:10em"><?= UOJLocale::get('title') ?></th>
<th><?= UOJLocale::get('content') ?></th>
<th style="width:12em"><?= UOJLocale::get('time') ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($contest_notice)): ?>
<tr><td colspan="233"><?= UOJLocale::get('none') ?></td></tr>
<?php else: foreach ($contest_notice as $notice): ?>
<tr>
<td><?= HTML::escape($notice['title']) ?></td>
<td style="white-space:pre-wrap; text-align: left"><?= $notice['content'] ?></td>
<td><?= $notice['time'] ?></td>
</tr>
<?php endforeach; endif ?>
</tbody>
</table>
</div>
<?php if ($post_notice): ?>
<div class="text-center">
<button id="button-display-post-notice" type="button" class="btn btn-danger btn-xs">发布比赛公告</button>
</div>
<div id="div-form-post-notice" style="display:none" class="bot-buffer-md">
<?php $post_notice->printHTML() ?>
</div>
<script type="text/javascript">
$(document).ready(function() {
$('#button-display-post-notice').click(function() {
$('#div-form-post-notice').toggle('fast');
});
});
</script>
<?php endif ?>
<h3>提问</h3>
<?php if ($my_questions_pag != null): ?>
<div>
<?php if ($post_question): ?>
<div class="pull-right">
<button id="button-display-post-question" type="button" class="btn btn-primary btn-xs">提问题</button>
</div>
<?php endif ?>
<h4>我的提问</h4>
<?php if ($post_question): ?>
<div id="div-form-post-question" style="display:none" class="bot-buffer-md">
<?php $post_question->printHTML() ?>
</div>
<script type="text/javascript">
$(document).ready(function() {
$('#button-display-post-question').click(function() {
$('#div-form-post-question').toggle('fast');
});
});
</script>
<?php endif ?>
<?php uojIncludeView('contest-question-table', ['pag' => $my_questions_pag]) ?>
</div>
<?php endif ?>
<div>
<?php if ($my_questions_pag != null): ?>
<h4>其他人的提问</h4>
<?php else: ?>
<h4>所有人的提问</h4>
<?php endif ?>
<?php uojIncludeView('contest-question-table', ['pag' => $others_questions_pag]) ?>
</div>

View File

@ -0,0 +1,96 @@
<?php
if (!isset($can_reply)) {
$can_reply = false;
$reply_question = null;
}
?>
<div class="table-responsive">
<table class="table table-bordered table-hover table-vertical-middle table-text-center">
<thead>
<tr>
<th style="width:10em">提问者</th>
<th style="width:7em">提问时间</th>
<th>问题</th>
</tr>
</thead>
<tbody>
<?php if ($pag->isEmpty()): ?>
<tr><td colspan="233"><?= UOJLocale::get('none') ?></td></tr>
<?php else: foreach ($pag->get() as $question): ?>
<tr>
<td><?= getUserLink($question['username']) ?></td>
<td class="small"><?= $question['post_time'] ?></td>
<td style="text-align: left" class="question" data-qid="<?=$question['id']?>">
<div class="question-content uoj-readmore"><?= HTML::escape($question['question']) ?></div>
<?php if ($can_reply): ?>
<div class="text-right">
<button type="button" class="btn btn-primary btn-xs question-reply-button">编辑回复</button>
</div>
<?php endif ?>
<hr class="top-buffer-sm bot-buffer-md" />
<?php if ($question['answer'] === ''): ?>
<div class="question-content"><strong class="text-muted">暂无回复</strong></div>
<?php else: ?>
<div class="question-content uoj-readmore"><strong class="text-danger">回复:</strong><span class="text-warning"><?= HTML::escape($question['answer']) ?></span></div>
<?php endif ?>
</td>
</tr>
<?php endforeach; endif ?>
</tbody>
</table>
</div>
<?= $pag->pagination() ?>
<?php if ($reply_question): ?>
<div id="div-reply" style="display:none" class="top-buffer-md bot-buffer-md">
<?php $reply_question->printHTML() ?>
</div>
<script type="text/javascript">
var onclick = function(p) {
return function() {
qid = parseInt($(p).data('qid'))
q = '#div-reply';
t = '#input-rid';
r = '#input-rcontent';
if ($(q).data('qid') != qid) {
$(q).data('qid', qid);
$(q).hide('fast', function() {
$(this).appendTo(p).show('fast', function() {
$(t).val(qid);
$(r).val('').focus();
});
});
} else if ($(q).css('display') != 'none') {
$(q).appendTo(p).hide('fast');
} else {
$(q).appendTo(p).show('fast', function() {
$(t).val(qid);
$(r).val('').focus();
});
}
};
};
$('.question').each(function() {
$(this).find('.question-reply-button').click(onclick(this));
});
var onselectchange = function() {
switch ($('#input-rtype').val()) {
case 'private':
case 'public':
$('#input-rcontent').prop('readonly', false);
break;
default:
$('#input-rcontent').val('').prop('readonly', true);
break;
}
};
$(document).ready(onselectchange);
$('#input-rtype').on('change', onselectchange);
</script>
<?php endif ?>

View File

@ -0,0 +1,14 @@
<div id="standings"></div>
<div class="table-responsive">
<table id="standings-table" class="table table-bordered table-striped table-text-center table-vertical-middle"></table>
</div>
<script type="text/javascript">
standings_version=<?=$contest['extra_config']['standings_version']?>;
contest_id=<?=$contest['id']?>;
standings=<?=json_encode($standings)?>;
score=<?=json_encode($score)?>;
problems=<?=json_encode($contest_data['problems'])?>;
$(document).ready(showStandings());
</script>

View File

@ -47,7 +47,7 @@
<?= HTML::css_link('/css/bootstrap-theme.min.css?v=2015.5.31') ?>
<!-- Custom styles for this template -->
<?= HTML::css_link('/css/uoj-theme.css?v=2.33') ?>
<?= HTML::css_link('/css/uoj-theme.css?v=2.3333') ?>
<!-- jQuery (necessary for Bootstrap\'s JavaScript plugins) -->
<?= HTML::js_src('/js/jquery.min.js') ?>
@ -66,8 +66,8 @@
<!-- jQuery modal -->
<?= HTML::js_src('/js/jquery.modal.js') ?>
<!-- jQuery tag canvas -->
<?php if (isset($REQUIRE_LIB['tagcanvas'])): ?>
<!-- jQuery tag canvas -->
<?= HTML::js_src('/js/jquery.tagcanvas.min.js') ?>
<?php endif ?>
@ -78,7 +78,10 @@
<?= HTML::js_src('/js/color-converter.min.js') ?>
<!-- uoj -->
<?= HTML::js_src('/js/uoj.js?v=2016.8.15') ?>
<?= HTML::js_src('/js/uoj.js?v=2017.01.01') ?>
<!-- readmore -->
<?= HTML::js_src('/js/readmore/readmore.min.js') ?>
<!-- LAB -->
<?= HTML::js_src('/js/LAB.min.js') ?>
@ -92,7 +95,7 @@
<?php $REQUIRE_LIB['switch'] = '' ?>
<?= HTML::css_link('/js/codemirror/lib/codemirror.css') ?>
<?= HTML::css_link('/css/blog-editor.css') ?>
<?= HTML::js_src('/js/marked.js') ?>
<?= HTML::js_src('/js/marked.js?v=2016.10.19') ?>
<?= HTML::js_src('/js/blog-editor/blog-editor.js?v=2015.7.9') ?>
<?= HTML::js_src('/js/codemirror/lib/codemirror.js') ?>
<?= HTML::js_src('/js/codemirror/addon/mode/overlay.js') ?>

View File

@ -62,7 +62,7 @@
transition: 'slide',
math: {
mathjax: '//cdnjscn.b0.upaiyun.com/libs/mathjax/2.4.0/MathJax.js',
mathjax: '//cdn.bootcss.com/mathjax/2.6.0/MathJax.js',
config: 'TeX-AMS_HTML-full'
},

View File

@ -14,7 +14,7 @@
// Include theme-specific fonts
@import url(/fonts/league-gothic/league-gothic.css);
@import url(//fonts.useso.com/css?family=Lato:400,700,400italic,700italic);
@import url(//fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);
// Override theme settings (see ../template/settings.scss)

View File

@ -17,7 +17,7 @@
// Include theme-specific fonts
@import url(//fonts.useso.com/css?family=Ubuntu:300,700,300italic,700italic);
@import url(//fonts.googleapis.com/css?family=Ubuntu:300,700,300italic,700italic);
// Colors used in the theme
$blood: #a23;

View File

@ -16,7 +16,7 @@
// Include theme-specific fonts
@import url(/fonts/league-gothic/league-gothic.css);
@import url(//fonts.useso.com/css?family=Lato:400,700,400italic,700italic);
@import url(//fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);
// Override theme settings (see ../template/settings.scss)
$headingTextShadow: 0px 0px 6px rgba(0,0,0,0.2);

View File

@ -13,7 +13,7 @@
// Include theme-specific fonts
@import url(/fonts/league-gothic/league-gothic.css);
@import url(//fonts.useso.com/css?family=Lato:400,700,400italic,700italic);
@import url(//fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);
/**
* Solarized colors by Ethan Schoonover

View File

@ -12,8 +12,8 @@
// Include theme-specific fonts
@import url(//fonts.useso.com/css?family=Montserrat:700);
@import url(//fonts.useso.com/css?family=Open+Sans:400,700,400italic,700italic);
@import url(//fonts.googleapis.com/css?family=Montserrat:700);
@import url(//fonts.googleapis.com/css?family=Open+Sans:400,700,400italic,700italic);
// Override theme settings (see ../template/settings.scss)

View File

@ -15,8 +15,8 @@
// Include theme-specific fonts
@import url(//fonts.useso.com/css?family=News+Cycle:400,700);
@import url(//fonts.useso.com/css?family=Lato:400,700,400italic,700italic);
@import url(//fonts.googleapis.com/css?family=News+Cycle:400,700);
@import url(//fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);
// Override theme settings (see ../template/settings.scss)

View File

@ -13,8 +13,8 @@
// Include theme-specific fonts
@import url(//fonts.useso.com/css?family=Quicksand:400,700,400italic,700italic);
@import url(//fonts.useso.com/css?family=Open+Sans:400italic,700italic,400,700);
@import url(//fonts.googleapis.com/css?family=Quicksand:400,700,400italic,700italic);
@import url(//fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700);
// Override theme settings (see ../template/settings.scss)

View File

@ -13,7 +13,7 @@
// Include theme-specific fonts
@import url(/fonts/league-gothic/league-gothic.css);
@import url(//fonts.useso.com/css?family=Lato:400,700,400italic,700italic);
@import url(//fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);
/**

View File

@ -270,6 +270,59 @@ pre {
white-space: pre-wrap;
word-break: break-all;
}
.comtbox7 {
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #fafafa 100%);
background-image: -o-linear-gradient(top, #f5f5f5 0%, #fafafa 100%);
background-image: linear-gradient(to bottom, #f5f5f5 0%, #fafafa 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fffafafa', GradientType=0);
border-color: #dcdcdc;
-webkit-box-shadow: inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);
border-radius: 3px;
margin-left: 50px;
margin-top: 10px;
margin-bottom: 2px;
padding-left: 10px;
padding-right: 10px;
padding-top:10px;
white-space: nowrap;
padding-bottom: 10px;
border: 1px solid #e3e3e3;
text-align: left;
}
.comtbox8 {
background-image: -webkit-linear-gradient(top, #ffffff 100%, #fafafa 0%);
background-image: -o-linear-gradient(top, #ffffff 100%, #fafafa 0%);
background-image: linear-gradient(to bottom, #ffffff 100%, #fafafa 0%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fffafafa', GradientType=0);
border-color: #dcdcdc;
-webkit-box-shadow: inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);
border-radius: 3px;
margin-left: 50px;
margin-top: 10px;
margin-bottom: 2px;
padding-left: 10px;
padding-right: 10px;
padding-top:10px;
white-space: nowrap;
padding-bottom: 10px;
border: 1px solid #e3e3e3;
text-align: left;
}
.question-content {
max-height: 100px;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
}
.uoj-click-zan-block {
display: inline-block;

View File

@ -152,13 +152,13 @@ function blog_editor_init(name, editor_config) {
}
var post_data = {};
post_data["save-" + name] = '';
$($(this_form).serializeArray()).each(function() {
post_data[this["name"]] = this["value"];
});
if (config.need_preview) {
post_data['need_preview'] = 'on';
}
post_data["save-" + name] = '';
$.ajax({
type : 'POST',

View File

@ -952,9 +952,10 @@ Parser.prototype.tok = function() {
+ '</li>\n';
}
case 'html': {
return !this.token.pre && !this.options.pedantic
return this.token.text;
/*return !this.token.pre && !this.options.pedantic
? this.inline.output(this.token.text)
: this.token.text;
: this.token.text;*/
}
case 'paragraph': {
return '<p' + tok_class + '>'

View File

@ -0,0 +1,36 @@
# 2.0.0
## New features
- Install with Bower: `bower install readmore`
- Blocks can now be toggled programmatically: `$('article:nth-of-type(3)').readmore('toggle')`
- ARIA semantics describe expanded state and relationship between blocks and their toggles
- Blocks are now assigned an ID if they don't already have one
- Install development dependencies with NPM
- Gulp task to minifiy with UglifyJS
## Improvements
- Height calculations on window resize are "debounced", resulting in more efficient rendering
- Height calculation in general has been improved
- The value of the `expanded` argument passed to the `beforeToggle` callback now correctly reflects the _pre-toggle_ state
- Multiple instances are now fully supported: e.g. `$('article').readmore({speed: 200})` and `$('fieldset').readmore({speed: 900})` will work on the same page
- Fully responsive, plugin now prefers max-heights set in CSS, even inside media queries
## Potentially breaking changes
- `maxHeight` option is now `collapsedHeight`
- `sectionCSS` option is now `blockCSS`
- `toggleSlider()` method is now just `toggle()`
- Animation is now performed with CSS3 transitions, rather than `jQuery.animate()`
- IE 8 and 9 are no longer supported, because those browsers hate kittens
- `init()` is now called within a `window.onload` event handler, which can briefly delay collapsing content
- `setBoxHeight()` is now a "private" method called `setBoxHeights()`
- `resizeBoxes()` is also now private
- Readmore.js now uses attribute selectors, rather than classes
- The `.readmore-js-section` and `.readmore-js-toggle` classes are gone
- The `expandedClass` and `collapsedClass` options are also gone
- Every Readmore.js block needs an ID, if one is not already present, one will be generated

22
uoj/1/js/readmore/LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Jed Foster
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.

177
uoj/1/js/readmore/README.md Normal file
View File

@ -0,0 +1,177 @@
# Readmore.js
A smooth, responsive jQuery plugin for collapsing and expanding long blocks of text with "Read more" and "Close" links.
The markup Readmore.js requires is so simple, you can probably use it with your existing HTML—there's no need for complicated sets of `div`'s or hardcoded classes, just call `.readmore()` on the element containing your block of text and Readmore.js takes care of the rest. Readmore.js plays well in a responsive environment, too.
Readmore.js is tested with—and supported on—all versions of jQuery greater than 1.9.1. All the "good" browsers are supported, as well as IE10+; IE8 & 9 _should_ work, but are not supported and the experience will not be ideal.
## Install
Install Readmore.js with npm:
```
$ npm install readmore-js
```
Then include it in your HTML:
```html
<script src="/node_modules/readmore-js/readmore.min.js"></script>
```
Or, using Webpack or Browserify:
```javascript
require('readmore-js');
```
## Use
```javascript
$('article').readmore();
```
It's that simple. You can change the speed of the animation, the height of the collapsed block, and the open and close elements.
```javascript
$('article').readmore({
speed: 75,
lessLink: '<a href="#">Read less</a>'
});
```
### The options:
* `speed: 100` in milliseconds
* `collapsedHeight: 200` in pixels
* `heightMargin: 16` in pixels, avoids collapsing blocks that are only slightly larger than `collapsedHeight`
* `moreLink: '<a href="#">Read more</a>'`
* `lessLink: '<a href="#">Close</a>'`
* `embedCSS: true` insert required CSS dynamically, set this to `false` if you include the necessary CSS in a stylesheet
* `blockCSS: 'display: block; width: 100%;'` sets the styling of the blocks, ignored if `embedCSS` is `false`
* `startOpen: false` do not immediately truncate, start in the fully opened position
* `beforeToggle: function() {}` called after a more or less link is clicked, but *before* the block is collapsed or expanded
* `afterToggle: function() {}` called *after* the block is collapsed or expanded
If the element has a `max-height` CSS property, Readmore.js will use that value rather than the value of the `collapsedHeight` option.
### The callbacks:
The callback functions, `beforeToggle` and `afterToggle`, both receive the same arguments: `trigger`, `element`, and `expanded`.
* `trigger`: the "Read more" or "Close" element that was clicked
* `element`: the block that is being collapsed or expanded
* `expanded`: Boolean; `true` means the block is expanded
#### Callback example:
Here's an example of how you could use the `afterToggle` callback to scroll back to the top of a block when the "Close" link is clicked.
```javascript
$('article').readmore({
afterToggle: function(trigger, element, expanded) {
if(! expanded) { // The "Close" link was clicked
$('html, body').animate( { scrollTop: element.offset().top }, {duration: 100 } );
}
}
});
```
### Removing Readmore:
You can remove the Readmore.js functionality like so:
```javascript
$('article').readmore('destroy');
```
Or, you can be more surgical by specifying a particular element:
```javascript
$('article:first').readmore('destroy');
```
### Toggling blocks programmatically:
You can toggle a block from code:
```javascript
$('article:nth-of-type(3)').readmore('toggle');
```
## CSS:
Readmore.js is designed to use CSS for as much functionality as possible: collapsed height can be set in CSS with the `max-height` property; "collapsing" is achieved by setting `overflow: hidden` on the containing block and changing the `height` property; and, finally, the expanding/collapsing animation is done with CSS3 transitions.
By default, Readmore.js inserts the following CSS, in addition to some transition-related rules:
```css
selector + [data-readmore-toggle], selector[data-readmore] {
display: block;
width: 100%;
}
```
_`selector` would be the element you invoked `readmore()` on, e.g.: `$('selector').readmore()`_
You can override the base rules when you set up Readmore.js like so:
```javascript
$('article').readmore({blockCSS: 'display: inline-block; width: 50%;'});
```
If you want to include the necessary styling in your site's stylesheet, you can disable the dynamic embedding by setting `embedCSS` to `false`:
```javascript
$('article').readmore({embedCSS: false});
```
### Media queries and other CSS tricks:
If you wanted to set a `maxHeight` based on lines, you could do so in CSS with something like:
```css
body {
font: 16px/1.5 sans-serif;
}
/* Show only 4 lines in smaller screens */
article {
max-height: 6em; /* (4 * 1.5 = 6) */
}
```
Then, with a media query you could change the number of lines shown, like so:
```css
/* Show 8 lines on larger screens */
@media screen and (min-width: 640px) {
article {
max-height: 12em;
}
}
```
## Contributing
Pull requests are always welcome, but not all suggested features will get merged. Feel free to contact me if you have an idea for a feature.
Pull requests should include the minified script and this readme and the demo HTML should be updated with descriptions of your new feature.
You'll need NPM:
```
$ npm install
```
Which will install the necessary development dependencies. Then, to build the minified script:
```
$ gulp compress
```

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,25 @@
{
"name": "Readmore.js",
"main": "readmore.min.js",
"version": "2.1.0",
"homepage": "http://jedfoster.com/Readmore.js/",
"authors": [
"Jed Foster <jed@jedfoster.com>"
],
"description": "A lightweight jQuery plugin for collapsing and expanding long blocks of text with \"Read more\" and \"Close\" links.",
"keywords": [
"css",
"jquery",
"readmore",
"expand",
"collapse"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
}

235
uoj/1/js/readmore/demo.html Normal file
View File

@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Readmore.js: jQuery plugin for long blocks of text</title>
<meta name="description" content="A smooth, lightweight jQuery plugin for collapsing and expanding long blocks of text with &#8220;Read more&#8221; and &#8220;Close&#8221; links.">
<meta name="author" content="Jed Foster">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7/html5shiv.min.js"></script>
<![endif]-->
<style media="screen">
body { font: 16px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; color: #444; }
code { color: #777; font-family: "Source Code Pro", "Menlo", "Courier New", monospace;}
a { color: #178DB1; }
.container { margin: 0 auto; max-width: 960px; }
#info + .readmore-js-toggle { padding-bottom: 1.5em; border-bottom: 1px solid #999; font-weight: bold;}
#demo { padding: 0 10%; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>Readmore.js</h1>
<p>A smooth, responsive jQuery plugin for collapsing and expanding long blocks of text with &#8220;Read more&#8221; and &#8220;Close&#8221; links.</p>
</header>
<section id="info">
<p>The markup Readmore.js requires is so simple, you can probably use it with your existing HTML—there&#8217;s no need for complicated sets of <code>div</code>&#8217;s or hardcoded classes, just call <code>.readmore()</code> on the element containing your block of text and Readmore.js takes care of the rest. Readmore.js plays well in a responsive environment, too.</p>
<p>Readmore.js is tested with—and supported on—all versions of jQuery greater than 1.9.1. All the &#8220;good&#8221; browsers are supported, as well as IE10+; IE8 &amp; 9 <em>should</em> work, but are not supported and the experience will not be ideal.</p>
<h2 id="install">Install</h2>
<p>Install Readmore.js with npm:</p>
<pre><code>$ npm install readmore-js</code></pre>
<p>Then include it in your HTML:</p>
<pre><code class="html">&lt;script src=&quot;/node_modules/readmore-js/readmore.min.js&quot;&gt;&lt;/script&gt;</code></pre>
<p>Or, using Webpack or Browserify:</p>
<pre><code class="javascript">require('readmore-js');</code></pre>
<h2 id="use">Use</h2>
<pre><code class="javascript">$(&apos;article&apos;).readmore();</code></pre>
<p>It&#8217;s that simple. You can change the speed of the animation, the height of the collapsed block, and the open and close elements.</p>
<pre><code class="javascript">$(&apos;article&apos;).readmore({
speed: 75,
lessLink: &apos;&lt;a href=&quot;#&quot;&gt;Read less&lt;/a&gt;&apos;
});</code></pre>
<h3 id="theoptions">The options:</h3>
<ul>
<li><code>speed: 100</code> in milliseconds</li>
<li><code>collapsedHeight: 200</code> in pixels</li>
<li><code>heightMargin: 16</code> in pixels, avoids collapsing blocks that are only slightly larger than <code>collapsedHeight</code></li>
<li><code>moreLink: '&lt;a href=&quot;#&quot;&gt;Read more&lt;/a&gt;'</code></li>
<li><code>lessLink: '&lt;a href=&quot;#&quot;&gt;Close&lt;/a&gt;'</code></li>
<li><code>embedCSS: true</code> insert required CSS dynamically, set this to <code>false</code> if you include the necessary CSS in a stylesheet</li>
<li><code>blockCSS: 'display: block; width: 100%;'</code> sets the styling of the blocks, ignored if <code>embedCSS</code> is <code>false</code></li>
<li><code>startOpen: false</code> do not immediately truncate, start in the fully opened position</li>
<li><code>beforeToggle: function() {}</code> called after a more or less link is clicked, but <em>before</em> the block is collapsed or expanded</li>
<li><code>afterToggle: function() {}</code> called <em>after</em> the block is collapsed or expanded</li>
</ul>
<p>If the element has a <code>max-height</code> CSS property, Readmore.js will use that value rather than the value of the <code>collapsedHeight</code> option.</p>
<h3 id="thecallbacks">The callbacks:</h3>
<p>The callback functions, <code>beforeToggle</code> and <code>afterToggle</code>, both receive the same arguments: <code>trigger</code>, <code>element</code>, and <code>expanded</code>.</p>
<ul>
<li><code>trigger</code>: the &#8220;Read more&#8221; or &#8220;Close&#8221; element that was clicked</li>
<li><code>element</code>: the block that is being collapsed or expanded</li>
<li><code>expanded</code>: Boolean; <code>true</code> means the block is expanded</li>
</ul>
<h4 id="callbackexample">Callback example:</h4>
<p>Here&#8217;s an example of how you could use the <code>afterToggle</code> callback to scroll back to the top of a block when the &#8220;Close&#8221; link is clicked.</p>
<pre><code class="javascript">$(&apos;article&apos;).readmore({
afterToggle: function(trigger, element, expanded) {
if(! expanded) { // The &quot;Close&quot; link was clicked
$(&apos;html, body&apos;).animate( { scrollTop: element.offset().top }, {duration: 100 } );
}
}
});</code></pre>
<h3 id="removingreadmore">Removing Readmore:</h3>
<p>You can remove the Readmore.js functionality like so:</p>
<pre><code class="javascript">$(&apos;article&apos;).readmore(&apos;destroy&apos;);</code></pre>
<p>Or, you can be more surgical by specifying a particular element:</p>
<pre><code class="javascript">$(&apos;article:first&apos;).readmore(&apos;destroy&apos;);</code></pre>
<h3 id="togglingblocksprogrammatically">Toggling blocks programmatically:</h3>
<p>You can toggle a block from code:</p>
<pre><code class="javascript">$(&apos;article:nth-of-type(3)&apos;).readmore(&apos;toggle&apos;);</code></pre>
<h2 id="css">CSS:</h2>
<p>Readmore.js is designed to use CSS for as much functionality as possible: collapsed height can be set in CSS with the <code>max-height</code> property; &#8220;collapsing&#8221; is achieved by setting <code>overflow: hidden</code> on the containing block and changing the <code>height</code> property; and, finally, the expanding/collapsing animation is done with CSS3 transitions.</p>
<p>By default, Readmore.js inserts the following CSS, in addition to some transition-related rules:</p>
<pre><code class="css">selector + [data-readmore-toggle], selector[data-readmore] {
display: block;
width: 100%;
}</code></pre>
<p><em><code>selector</code> would be the element you invoked <code>readmore()</code> on, e.g.: <code>$('selector').readmore()</code></em></p>
<p>You can override the base rules when you set up Readmore.js like so:</p>
<pre><code class="javascript">$(&apos;article&apos;).readmore({blockCSS: &apos;display: inline-block; width: 50%;&apos;});</code></pre>
<p>If you want to include the necessary styling in your site&#8217;s stylesheet, you can disable the dynamic embedding by setting <code>embedCSS</code> to <code>false</code>:</p>
<pre><code class="javascript">$(&apos;article&apos;).readmore({embedCSS: false});</code></pre>
<h3 id="mediaqueriesandothercsstricks">Media queries and other CSS tricks:</h3>
<p>If you wanted to set a <code>maxHeight</code> based on lines, you could do so in CSS with something like:</p>
<pre><code class="css">body {
font: 16px/1.5 sans-serif;
}
/* Show only 4 lines in smaller screens */
article {
max-height: 6em; /* (4 * 1.5 = 6) */
}</code></pre>
<p>Then, with a media query you could change the number of lines shown, like so:</p>
<pre><code class="css">/* Show 8 lines on larger screens */
@media screen and (min-width: 640px) {
article {
max-height: 12em;
}
}</code></pre>
<h2 id="contributing">Contributing</h2>
<p>Pull requests are always welcome, but not all suggested features will get merged. Feel free to contact me if you have an idea for a feature.</p>
<p>Pull requests should include the minified script and this readme and the demo HTML should be updated with descriptions of your new feature. </p>
<p>You&#8217;ll need NPM:</p>
<pre><code>$ npm install</code></pre>
<p>Which will install the necessary development dependencies. Then, to build the minified script:</p>
<pre><code>$ gulp compress</code></pre>
</section>
<h1>Demo</h1>
<section id="demo">
<article>
<h2>Artisanal Narwahls</h2>
<p>From this distant vantage point, the Earth might not seem of any particular interest. But for us, it's different. Consider again that dot. That's here. That's home. That's us. On it everyone you love, everyone you know, everyone you ever heard of, every human being who ever was, lived out their lives. The aggregate of our joy and suffering, thousands of confident religions, ideologies, and economic doctrines, every hunter and forager, every hero and coward, every creator and destroyer of civilization, every king and peasant, every young couple in love, every mother and father, hopeful child, inventor and explorer, every teacher of morals, every corrupt politician, every "superstar," every "supreme leader," every saint and sinner in the history of our species lived there  on a mote of dust suspended in a sunbeam.</p>
<p>Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!</p>
<p>Here's how it is: Earth got used up, so we terraformed a whole new galaxy of Earths, some rich and flush with the new technologies, some not so much. Central Planets, them was formed the Alliance, waged war to bring everyone under their rule; a few idiots tried to fight it, among them myself. I'm Malcolm Reynolds, captain of Serenity. Got a good crew: fighters, pilot, mechanic. We even picked up a preacher, and a bona fide companion. There's a doctor, too, took his genius sister out of some Alliance camp, so they're keeping a low profile. You got a job, we can do it, don't much care what it is.</p>
<p>Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!</p>
</article>
<article>
<h2>Portland Leggings</h2>
<p>Here's how it is: Earth got used up, so we terraformed a whole new galaxy of Earths, some rich and flush with the new technologies, some not so much. Central Planets, them was formed the Alliance, waged war to bring everyone under their rule; a few idiots tried to fight it, among them myself. I'm Malcolm Reynolds, captain of Serenity. Got a good crew: fighters, pilot, mechanic. We even picked up a preacher, and a bona fide companion. There's a doctor, too, took his genius sister out of some Alliance camp, so they're keeping a low profile. You got a job, we can do it, don't much care what it is.</p>
<p>I am Duncan Macleod, born 400 years ago in the Highlands of Scotland. I am Immortal, and I am not alone. For centuries, we have waited for the time of the Gathering when the stroke of a sword and the fall of a head will release the power of the Quickening. In the end, there can be only one.</p>
<p>From this distant vantage point, the Earth might not seem of any particular interest. But for us, it's different. Consider again that dot. That's here. That's home. That's us. On it everyone you love, everyone you know, everyone you ever heard of, every human being who ever was, lived out their lives. The aggregate of our joy and suffering, thousands of confident religions, ideologies, and economic doctrines, every hunter and forager, every hero and coward, every creator and destroyer of civilization, every king and peasant, every young couple in love, every mother and father, hopeful child, inventor and explorer, every teacher of morals, every corrupt politician, every "superstar," every "supreme leader," every saint and sinner in the history of our species lived there  on a mote of dust suspended in a sunbeam.</p>
<p>Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!</p>
</article>
<article>
<h2>This section is shorter than the Readmore minimum</h2>
<p>Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!</p>
</article>
</section>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="readmore.js"></script>
<script>
$('#info').readmore({
moreLink: '<a href="#">Usage, examples, and options</a>',
collapsedHeight: 384,
afterToggle: function(trigger, element, expanded) {
if(! expanded) { // The "Close" link was clicked
$('html, body').animate({scrollTop: element.offset().top}, {duration: 100});
}
}
});
$('article').readmore({speed: 500});
</script>
</body>
</html>

View File

@ -0,0 +1,14 @@
var gulp = require('gulp'),
uglify = require('gulp-uglify'),
rename = require('gulp-rename');
gulp.task('compress', function() {
gulp.src('readmore.js')
.pipe(uglify({
mangle: true,
compress: true,
preserveComments: 'some'
}))
.pipe(rename('readmore.min.js'))
.pipe(gulp.dest('./'));
});

View File

@ -0,0 +1,692 @@
/*!
* MockJax - jQuery Plugin to Mock Ajax requests
*
* Version: 1.6.1
* Released:
* Home: https://github.com/jakerella/jquery-mockjax
* Author: Jonathan Sharp (http://jdsharp.com)
* License: MIT,GPL
*
* Copyright (c) 2014 appendTo, Jordan Kasper
* NOTE: This repository was taken over by Jordan Kasper (@jakerella) October, 2014
*
* Dual licensed under the MIT or GPL licenses.
* http://opensource.org/licenses/MIT OR http://www.gnu.org/licenses/gpl-2.0.html
*/
(function($) {
var _ajax = $.ajax,
mockHandlers = [],
mockedAjaxCalls = [],
unmockedAjaxCalls = [],
CALLBACK_REGEX = /=\?(&|$)/,
jsc = (new Date()).getTime();
// Parse the given XML string.
function parseXML(xml) {
if ( window.DOMParser == undefined && window.ActiveXObject ) {
DOMParser = function() { };
DOMParser.prototype.parseFromString = function( xmlString ) {
var doc = new ActiveXObject('Microsoft.XMLDOM');
doc.async = 'false';
doc.loadXML( xmlString );
return doc;
};
}
try {
var xmlDoc = ( new DOMParser() ).parseFromString( xml, 'text/xml' );
if ( $.isXMLDoc( xmlDoc ) ) {
var err = $('parsererror', xmlDoc);
if ( err.length == 1 ) {
throw new Error('Error: ' + $(xmlDoc).text() );
}
} else {
throw new Error('Unable to parse XML');
}
return xmlDoc;
} catch( e ) {
var msg = ( e.name == undefined ? e : e.name + ': ' + e.message );
$(document).trigger('xmlParseError', [ msg ]);
return undefined;
}
}
// Check if the data field on the mock handler and the request match. This
// can be used to restrict a mock handler to being used only when a certain
// set of data is passed to it.
function isMockDataEqual( mock, live ) {
var identical = true;
// Test for situations where the data is a querystring (not an object)
if (typeof live === 'string') {
// Querystring may be a regex
return $.isFunction( mock.test ) ? mock.test(live) : mock == live;
}
$.each(mock, function(k) {
if ( live[k] === undefined ) {
identical = false;
return identical;
} else {
if ( typeof live[k] === 'object' && live[k] !== null ) {
if ( identical && $.isArray( live[k] ) ) {
identical = $.isArray( mock[k] ) && live[k].length === mock[k].length;
}
identical = identical && isMockDataEqual(mock[k], live[k]);
} else {
if ( mock[k] && $.isFunction( mock[k].test ) ) {
identical = identical && mock[k].test(live[k]);
} else {
identical = identical && ( mock[k] == live[k] );
}
}
}
});
return identical;
}
// See if a mock handler property matches the default settings
function isDefaultSetting(handler, property) {
return handler[property] === $.mockjaxSettings[property];
}
// Check the given handler should mock the given request
function getMockForRequest( handler, requestSettings ) {
// If the mock was registered with a function, let the function decide if we
// want to mock this request
if ( $.isFunction(handler) ) {
return handler( requestSettings );
}
// Inspect the URL of the request and check if the mock handler's url
// matches the url for this ajax request
if ( $.isFunction(handler.url.test) ) {
// The user provided a regex for the url, test it
if ( !handler.url.test( requestSettings.url ) ) {
return null;
}
} else {
// Look for a simple wildcard '*' or a direct URL match
var star = handler.url.indexOf('*');
if (handler.url !== requestSettings.url && star === -1 ||
!new RegExp(handler.url.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&").replace(/\*/g, '.+')).test(requestSettings.url)) {
return null;
}
}
// Inspect the data submitted in the request (either POST body or GET query string)
if ( handler.data ) {
if ( ! requestSettings.data || !isMockDataEqual(handler.data, requestSettings.data) ) {
// They're not identical, do not mock this request
return null;
}
}
// Inspect the request type
if ( handler && handler.type &&
handler.type.toLowerCase() != requestSettings.type.toLowerCase() ) {
// The request type doesn't match (GET vs. POST)
return null;
}
return handler;
}
function parseResponseTimeOpt(responseTime) {
if ($.isArray(responseTime)) {
var min = responseTime[0];
var max = responseTime[1];
return (typeof min === 'number' && typeof max === 'number') ? Math.floor(Math.random() * (max - min)) + min : null;
} else {
return (typeof responseTime === 'number') ? responseTime: null;
}
}
// Process the xhr objects send operation
function _xhrSend(mockHandler, requestSettings, origSettings) {
// This is a substitute for < 1.4 which lacks $.proxy
var process = (function(that) {
return function() {
return (function() {
// The request has returned
this.status = mockHandler.status;
this.statusText = mockHandler.statusText;
this.readyState = 1;
var finishRequest = function () {
this.readyState = 4;
var onReady;
// Copy over our mock to our xhr object before passing control back to
// jQuery's onreadystatechange callback
if ( requestSettings.dataType == 'json' && ( typeof mockHandler.responseText == 'object' ) ) {
this.responseText = JSON.stringify(mockHandler.responseText);
} else if ( requestSettings.dataType == 'xml' ) {
if ( typeof mockHandler.responseXML == 'string' ) {
this.responseXML = parseXML(mockHandler.responseXML);
//in jQuery 1.9.1+, responseXML is processed differently and relies on responseText
this.responseText = mockHandler.responseXML;
} else {
this.responseXML = mockHandler.responseXML;
}
} else if (typeof mockHandler.responseText === 'object' && mockHandler.responseText !== null) {
// since jQuery 1.9 responseText type has to match contentType
mockHandler.contentType = 'application/json';
this.responseText = JSON.stringify(mockHandler.responseText);
} else {
this.responseText = mockHandler.responseText;
}
if( typeof mockHandler.status == 'number' || typeof mockHandler.status == 'string' ) {
this.status = mockHandler.status;
}
if( typeof mockHandler.statusText === "string") {
this.statusText = mockHandler.statusText;
}
// jQuery 2.0 renamed onreadystatechange to onload
onReady = this.onreadystatechange || this.onload;
// jQuery < 1.4 doesn't have onreadystate change for xhr
if ( $.isFunction( onReady ) ) {
if( mockHandler.isTimeout) {
this.status = -1;
}
onReady.call( this, mockHandler.isTimeout ? 'timeout' : undefined );
} else if ( mockHandler.isTimeout ) {
// Fix for 1.3.2 timeout to keep success from firing.
this.status = -1;
}
};
// We have an executable function, call it to give
// the mock handler a chance to update it's data
if ( $.isFunction(mockHandler.response) ) {
// Wait for it to finish
if ( mockHandler.response.length === 2 ) {
mockHandler.response(origSettings, function () {
finishRequest.call(that);
});
return;
} else {
mockHandler.response(origSettings);
}
}
finishRequest.call(that);
}).apply(that);
};
})(this);
if ( mockHandler.proxy ) {
// We're proxying this request and loading in an external file instead
_ajax({
global: false,
url: mockHandler.proxy,
type: mockHandler.proxyType,
data: mockHandler.data,
dataType: requestSettings.dataType === "script" ? "text/plain" : requestSettings.dataType,
complete: function(xhr) {
mockHandler.responseXML = xhr.responseXML;
mockHandler.responseText = xhr.responseText;
// Don't override the handler status/statusText if it's specified by the config
if (isDefaultSetting(mockHandler, 'status')) {
mockHandler.status = xhr.status;
}
if (isDefaultSetting(mockHandler, 'statusText')) {
mockHandler.statusText = xhr.statusText;
}
this.responseTimer = setTimeout(process, parseResponseTimeOpt(mockHandler.responseTime) || 0);
}
});
} else {
// type == 'POST' || 'GET' || 'DELETE'
if ( requestSettings.async === false ) {
// TODO: Blocking delay
process();
} else {
this.responseTimer = setTimeout(process, parseResponseTimeOpt(mockHandler.responseTime) || 50);
}
}
}
// Construct a mocked XHR Object
function xhr(mockHandler, requestSettings, origSettings, origHandler) {
// Extend with our default mockjax settings
mockHandler = $.extend(true, {}, $.mockjaxSettings, mockHandler);
if (typeof mockHandler.headers === 'undefined') {
mockHandler.headers = {};
}
if (typeof requestSettings.headers === 'undefined') {
requestSettings.headers = {};
}
if ( mockHandler.contentType ) {
mockHandler.headers['content-type'] = mockHandler.contentType;
}
return {
status: mockHandler.status,
statusText: mockHandler.statusText,
readyState: 1,
open: function() { },
send: function() {
origHandler.fired = true;
_xhrSend.call(this, mockHandler, requestSettings, origSettings);
},
abort: function() {
clearTimeout(this.responseTimer);
},
setRequestHeader: function(header, value) {
requestSettings.headers[header] = value;
},
getResponseHeader: function(header) {
// 'Last-modified', 'Etag', 'content-type' are all checked by jQuery
if ( mockHandler.headers && mockHandler.headers[header] ) {
// Return arbitrary headers
return mockHandler.headers[header];
} else if ( header.toLowerCase() == 'last-modified' ) {
return mockHandler.lastModified || (new Date()).toString();
} else if ( header.toLowerCase() == 'etag' ) {
return mockHandler.etag || '';
} else if ( header.toLowerCase() == 'content-type' ) {
return mockHandler.contentType || 'text/plain';
}
},
getAllResponseHeaders: function() {
var headers = '';
// since jQuery 1.9 responseText type has to match contentType
if (mockHandler.contentType) {
mockHandler.headers['Content-Type'] = mockHandler.contentType;
}
$.each(mockHandler.headers, function(k, v) {
headers += k + ': ' + v + "\n";
});
return headers;
}
};
}
// Process a JSONP mock request.
function processJsonpMock( requestSettings, mockHandler, origSettings ) {
// Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here
// because there isn't an easy hook for the cross domain script tag of jsonp
processJsonpUrl( requestSettings );
requestSettings.dataType = "json";
if(requestSettings.data && CALLBACK_REGEX.test(requestSettings.data) || CALLBACK_REGEX.test(requestSettings.url)) {
createJsonpCallback(requestSettings, mockHandler, origSettings);
// We need to make sure
// that a JSONP style response is executed properly
var rurl = /^(\w+:)?\/\/([^\/?#]+)/,
parts = rurl.exec( requestSettings.url ),
remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
requestSettings.dataType = "script";
if(requestSettings.type.toUpperCase() === "GET" && remote ) {
var newMockReturn = processJsonpRequest( requestSettings, mockHandler, origSettings );
// Check if we are supposed to return a Deferred back to the mock call, or just
// signal success
if(newMockReturn) {
return newMockReturn;
} else {
return true;
}
}
}
return null;
}
// Append the required callback parameter to the end of the request URL, for a JSONP request
function processJsonpUrl( requestSettings ) {
if ( requestSettings.type.toUpperCase() === "GET" ) {
if ( !CALLBACK_REGEX.test( requestSettings.url ) ) {
requestSettings.url += (/\?/.test( requestSettings.url ) ? "&" : "?") +
(requestSettings.jsonp || "callback") + "=?";
}
} else if ( !requestSettings.data || !CALLBACK_REGEX.test(requestSettings.data) ) {
requestSettings.data = (requestSettings.data ? requestSettings.data + "&" : "") + (requestSettings.jsonp || "callback") + "=?";
}
}
// Process a JSONP request by evaluating the mocked response text
function processJsonpRequest( requestSettings, mockHandler, origSettings ) {
// Synthesize the mock request for adding a script tag
var callbackContext = origSettings && origSettings.context || requestSettings,
newMock = null;
// If the response handler on the moock is a function, call it
if ( mockHandler.response && $.isFunction(mockHandler.response) ) {
mockHandler.response(origSettings);
} else {
// Evaluate the responseText javascript in a global context
if( typeof mockHandler.responseText === 'object' ) {
$.globalEval( '(' + JSON.stringify( mockHandler.responseText ) + ')');
} else {
$.globalEval( '(' + mockHandler.responseText + ')');
}
}
// Successful response
setTimeout(function() {
jsonpSuccess( requestSettings, callbackContext, mockHandler );
jsonpComplete( requestSettings, callbackContext, mockHandler );
}, parseResponseTimeOpt(mockHandler.responseTime) || 0);
// If we are running under jQuery 1.5+, return a deferred object
if($.Deferred){
newMock = new $.Deferred();
if(typeof mockHandler.responseText == "object"){
newMock.resolveWith( callbackContext, [mockHandler.responseText] );
}
else{
newMock.resolveWith( callbackContext, [$.parseJSON( mockHandler.responseText )] );
}
}
return newMock;
}
// Create the required JSONP callback function for the request
function createJsonpCallback( requestSettings, mockHandler, origSettings ) {
var callbackContext = origSettings && origSettings.context || requestSettings;
var jsonp = requestSettings.jsonpCallback || ("jsonp" + jsc++);
// Replace the =? sequence both in the query string and the data
if ( requestSettings.data ) {
requestSettings.data = (requestSettings.data + "").replace(CALLBACK_REGEX, "=" + jsonp + "$1");
}
requestSettings.url = requestSettings.url.replace(CALLBACK_REGEX, "=" + jsonp + "$1");
// Handle JSONP-style loading
window[ jsonp ] = window[ jsonp ] || function( tmp ) {
data = tmp;
jsonpSuccess( requestSettings, callbackContext, mockHandler );
jsonpComplete( requestSettings, callbackContext, mockHandler );
// Garbage collect
window[ jsonp ] = undefined;
try {
delete window[ jsonp ];
} catch(e) {}
if ( head ) {
head.removeChild( script );
}
};
}
// The JSONP request was successful
function jsonpSuccess(requestSettings, callbackContext, mockHandler) {
// If a local callback was specified, fire it and pass it the data
if ( requestSettings.success ) {
requestSettings.success.call( callbackContext, mockHandler.responseText || "", status, {} );
}
// Fire the global callback
if ( requestSettings.global ) {
(requestSettings.context ? $(requestSettings.context) : $.event).trigger("ajaxSuccess", [{}, requestSettings]);
}
}
// The JSONP request was completed
function jsonpComplete(requestSettings, callbackContext) {
// Process result
if ( requestSettings.complete ) {
requestSettings.complete.call( callbackContext, {} , status );
}
// The request was completed
if ( requestSettings.global ) {
(requestSettings.context ? $(requestSettings.context) : $.event).trigger("ajaxComplete", [{}, requestSettings]);
}
// Handle the global AJAX counter
if ( requestSettings.global && ! --$.active ) {
$.event.trigger( "ajaxStop" );
}
}
// The core $.ajax replacement.
function handleAjax( url, origSettings ) {
var mockRequest, requestSettings, mockHandler, overrideCallback;
// If url is an object, simulate pre-1.5 signature
if ( typeof url === "object" ) {
origSettings = url;
url = undefined;
} else {
// work around to support 1.5 signature
origSettings = origSettings || {};
origSettings.url = url;
}
// Extend the original settings for the request
requestSettings = $.extend(true, {}, $.ajaxSettings, origSettings);
// Generic function to override callback methods for use with
// callback options (onAfterSuccess, onAfterError, onAfterComplete)
overrideCallback = function(action, mockHandler) {
var origHandler = origSettings[action.toLowerCase()];
return function() {
if ( $.isFunction(origHandler) ) {
origHandler.apply(this, [].slice.call(arguments));
}
mockHandler['onAfter' + action]();
};
};
// Iterate over our mock handlers (in registration order) until we find
// one that is willing to intercept the request
for(var k = 0; k < mockHandlers.length; k++) {
if ( !mockHandlers[k] ) {
continue;
}
mockHandler = getMockForRequest( mockHandlers[k], requestSettings );
if(!mockHandler) {
// No valid mock found for this request
continue;
}
mockedAjaxCalls.push(requestSettings);
// If logging is enabled, log the mock to the console
$.mockjaxSettings.log( mockHandler, requestSettings );
if ( requestSettings.dataType && requestSettings.dataType.toUpperCase() === 'JSONP' ) {
if ((mockRequest = processJsonpMock( requestSettings, mockHandler, origSettings ))) {
// This mock will handle the JSONP request
return mockRequest;
}
}
// Removed to fix #54 - keep the mocking data object intact
//mockHandler.data = requestSettings.data;
mockHandler.cache = requestSettings.cache;
mockHandler.timeout = requestSettings.timeout;
mockHandler.global = requestSettings.global;
// In the case of a timeout, we just need to ensure
// an actual jQuery timeout (That is, our reponse won't)
// return faster than the timeout setting.
if ( mockHandler.isTimeout ) {
if ( mockHandler.responseTime > 1 ) {
origSettings.timeout = mockHandler.responseTime - 1;
} else {
mockHandler.responseTime = 2;
origSettings.timeout = 1;
}
mockHandler.isTimeout = false;
}
// Set up onAfter[X] callback functions
if ( $.isFunction( mockHandler.onAfterSuccess ) ) {
origSettings.success = overrideCallback('Success', mockHandler);
}
if ( $.isFunction( mockHandler.onAfterError ) ) {
origSettings.error = overrideCallback('Error', mockHandler);
}
if ( $.isFunction( mockHandler.onAfterComplete ) ) {
origSettings.complete = overrideCallback('Complete', mockHandler);
}
copyUrlParameters(mockHandler, origSettings);
(function(mockHandler, requestSettings, origSettings, origHandler) {
mockRequest = _ajax.call($, $.extend(true, {}, origSettings, {
// Mock the XHR object
xhr: function() { return xhr( mockHandler, requestSettings, origSettings, origHandler ); }
}));
})(mockHandler, requestSettings, origSettings, mockHandlers[k]);
return mockRequest;
}
// We don't have a mock request
unmockedAjaxCalls.push(origSettings);
if($.mockjaxSettings.throwUnmocked === true) {
throw new Error('AJAX not mocked: ' + origSettings.url);
}
else { // trigger a normal request
return _ajax.apply($, [origSettings]);
}
}
/**
* Copies URL parameter values if they were captured by a regular expression
* @param {Object} mockHandler
* @param {Object} origSettings
*/
function copyUrlParameters(mockHandler, origSettings) {
//parameters aren't captured if the URL isn't a RegExp
if (!(mockHandler.url instanceof RegExp)) {
return;
}
//if no URL params were defined on the handler, don't attempt a capture
if (!mockHandler.hasOwnProperty('urlParams')) {
return;
}
var captures = mockHandler.url.exec(origSettings.url);
//the whole RegExp match is always the first value in the capture results
if (captures.length === 1) {
return;
}
captures.shift();
//use handler params as keys and capture resuts as values
var i = 0,
capturesLength = captures.length,
paramsLength = mockHandler.urlParams.length,
//in case the number of params specified is less than actual captures
maxIterations = Math.min(capturesLength, paramsLength),
paramValues = {};
for (i; i < maxIterations; i++) {
var key = mockHandler.urlParams[i];
paramValues[key] = captures[i];
}
origSettings.urlParams = paramValues;
}
// Public
$.extend({
ajax: handleAjax
});
$.mockjaxSettings = {
//url: null,
//type: 'GET',
log: function( mockHandler, requestSettings ) {
if ( mockHandler.logging === false ||
( typeof mockHandler.logging === 'undefined' && $.mockjaxSettings.logging === false ) ) {
return;
}
if ( window.console && console.log ) {
var message = 'MOCK ' + requestSettings.type.toUpperCase() + ': ' + requestSettings.url;
var request = $.extend({}, requestSettings);
if (typeof console.log === 'function') {
console.log(message, request);
} else {
try {
console.log( message + ' ' + JSON.stringify(request) );
} catch (e) {
console.log(message);
}
}
}
},
logging: true,
status: 200,
statusText: "OK",
responseTime: 500,
isTimeout: false,
throwUnmocked: false,
contentType: 'text/plain',
response: '',
responseText: '',
responseXML: '',
proxy: '',
proxyType: 'GET',
lastModified: null,
etag: '',
headers: {
etag: 'IJF@H#@923uf8023hFO@I#H#',
'content-type' : 'text/plain'
}
};
$.mockjax = function(settings) {
var i = mockHandlers.length;
mockHandlers[i] = settings;
return i;
};
$.mockjax.clear = function(i) {
if ( arguments.length == 1 ) {
mockHandlers[i] = null;
} else {
mockHandlers = [];
}
mockedAjaxCalls = [];
unmockedAjaxCalls = [];
};
// support older, deprecated version
$.mockjaxClear = function(i) {
window.console && window.console.warn && window.console.warn( 'DEPRECATED: The $.mockjaxClear() method has been deprecated in 1.6.0. Please use $.mockjax.clear() as the older function will be removed soon!' );
$.mockjax.clear();
};
$.mockjax.handler = function(i) {
if ( arguments.length == 1 ) {
return mockHandlers[i];
}
};
$.mockjax.mockedAjaxCalls = function() {
return mockedAjaxCalls;
};
$.mockjax.unfiredHandlers = function() {
var results = [];
for (var i=0, len=mockHandlers.length; i<len; i++) {
var handler = mockHandlers[i];
if (handler !== null && !handler.fired) {
results.push(handler);
}
}
return results;
};
$.mockjax.unmockedAjaxCalls = function() {
return unmockedAjaxCalls;
};
})(jQuery);

View File

@ -0,0 +1,34 @@
{
"name": "readmore-js",
"version": "2.1.0",
"description": "A lightweight jQuery plugin for collapsing and expanding long blocks of text with \"Read more\" and \"Close\" links.",
"main": "readmore.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/jedfoster/Readmore.js.git"
},
"keywords": [
"css",
"jquery",
"readmore",
"expand",
"collapse"
],
"author": "Jed Foster <jed@jedfoster.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/jedfoster/Readmore.js/issues"
},
"homepage": "https://github.com/jedfoster/Readmore.js",
"dependencies": {
"jquery": "~2.1.4"
},
"devDependencies": {
"gulp": "^3.9.0",
"gulp-rename": "^1.2.0",
"gulp-uglify": "^1.0.2"
}
}

View File

@ -0,0 +1,330 @@
/*!
* @preserve
*
* Readmore.js jQuery plugin
* Author: @jed_foster
* Project home: http://jedfoster.github.io/Readmore.js
* Licensed under the MIT license
*
* Debounce function from http://davidwalsh.name/javascript-debounce-function
*/
/* global jQuery */
(function(factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('jquery'));
} else {
// Browser globals
factory(jQuery);
}
}(function($) {
'use strict';
var readmore = 'readmore',
defaults = {
speed: 100,
collapsedHeight: 200,
heightMargin: 16,
moreLink: '<a href="#">Read More</a>',
lessLink: '<a href="#">Close</a>',
embedCSS: true,
blockCSS: 'display: block; width: 100%;',
startOpen: false,
// callbacks
beforeToggle: function(){},
afterToggle: function(){}
},
cssEmbedded = {},
uniqueIdCounter = 0;
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (! immediate) {
func.apply(context, args);
}
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
function uniqueId(prefix) {
var id = ++uniqueIdCounter;
return String(prefix == null ? 'rmjs-' : prefix) + id;
}
function setBoxHeights(element) {
var el = element.clone().css({
height: 'auto',
width: element.width(),
maxHeight: 'none',
overflow: 'hidden'
}).insertAfter(element),
expandedHeight = el.outerHeight(),
cssMaxHeight = parseInt(el.css({maxHeight: ''}).css('max-height').replace(/[^-\d\.]/g, ''), 10),
defaultHeight = element.data('defaultHeight');
el.remove();
var collapsedHeight = cssMaxHeight || element.data('collapsedHeight') || defaultHeight;
// Store our measurements.
element.data({
expandedHeight: expandedHeight,
maxHeight: cssMaxHeight,
collapsedHeight: collapsedHeight
})
// and disable any `max-height` property set in CSS
.css({
maxHeight: 'none'
});
}
var resizeBoxes = debounce(function() {
$('[data-readmore]').each(function() {
var current = $(this),
isExpanded = (current.attr('aria-expanded') === 'true');
setBoxHeights(current);
current.css({
height: current.data( (isExpanded ? 'expandedHeight' : 'collapsedHeight') )
});
});
}, 100);
function embedCSS(options) {
if (! cssEmbedded[options.selector]) {
var styles = ' ';
if (options.embedCSS && options.blockCSS !== '') {
styles += options.selector + ' + [data-readmore-toggle], ' +
options.selector + '[data-readmore]{' +
options.blockCSS +
'}';
}
// Include the transition CSS even if embedCSS is false
styles += options.selector + '[data-readmore]{' +
'transition: height ' + options.speed + 'ms;' +
'overflow: hidden;' +
'}';
(function(d, u) {
var css = d.createElement('style');
css.type = 'text/css';
if (css.styleSheet) {
css.styleSheet.cssText = u;
}
else {
css.appendChild(d.createTextNode(u));
}
d.getElementsByTagName('head')[0].appendChild(css);
}(document, styles));
cssEmbedded[options.selector] = true;
}
}
function Readmore(element, options) {
this.element = element;
this.options = $.extend({}, defaults, options);
embedCSS(this.options);
this._defaults = defaults;
this._name = readmore;
this.init();
// IE8 chokes on `window.addEventListener`, so need to test for support.
if (window.addEventListener) {
// Need to resize boxes when the page has fully loaded.
window.addEventListener('load', resizeBoxes);
window.addEventListener('resize', resizeBoxes);
}
else {
window.attachEvent('load', resizeBoxes);
window.attachEvent('resize', resizeBoxes);
}
}
Readmore.prototype = {
init: function() {
var current = $(this.element);
current.data({
defaultHeight: this.options.collapsedHeight,
heightMargin: this.options.heightMargin
});
setBoxHeights(current);
var collapsedHeight = current.data('collapsedHeight'),
heightMargin = current.data('heightMargin');
if (current.outerHeight(true) <= collapsedHeight + heightMargin) {
// The block is shorter than the limit, so there's no need to truncate it.
return true;
}
else {
var id = current.attr('id') || uniqueId(),
useLink = this.options.startOpen ? this.options.lessLink : this.options.moreLink;
current.attr({
'data-readmore': '',
'aria-expanded': this.options.startOpen,
'id': id
});
current.after($(useLink)
.on('click', (function(_this) {
return function(event) {
_this.toggle(this, current[0], event);
};
})(this))
.attr({
'data-readmore-toggle': '',
'aria-controls': id
}));
if (! this.options.startOpen) {
current.css({
height: collapsedHeight
});
}
}
},
toggle: function(trigger, element, event) {
if (event) {
event.preventDefault();
}
if (! trigger) {
trigger = $('[aria-controls="' + _this.element.id + '"]')[0];
}
if (! element) {
element = _this.element;
}
var $element = $(element),
newHeight = '',
newLink = '',
expanded = false,
collapsedHeight = $element.data('collapsedHeight');
if ($element.height() <= collapsedHeight) {
newHeight = $element.data('expandedHeight') + 'px';
newLink = 'lessLink';
expanded = true;
}
else {
newHeight = collapsedHeight;
newLink = 'moreLink';
}
// Fire beforeToggle callback
// Since we determined the new "expanded" state above we're now out of sync
// with our true current state, so we need to flip the value of `expanded`
this.options.beforeToggle(trigger, $element, ! expanded);
$element.css({'height': newHeight});
// Fire afterToggle callback
$element.on('transitionend', (function(_this) {
return function() {
_this.options.afterToggle(trigger, $element, expanded);
$(this).attr({
'aria-expanded': expanded
}).off('transitionend');
}
})(this));
$(trigger).replaceWith($(this.options[newLink])
.on('click', (function(_this) {
return function(event) {
_this.toggle(this, element, event);
};
})(this))
.attr({
'data-readmore-toggle': '',
'aria-controls': $element.attr('id')
}));
},
destroy: function() {
$(this.element).each(function() {
var current = $(this);
current.attr({
'data-readmore': null,
'aria-expanded': null
})
.css({
maxHeight: '',
height: ''
})
.next('[data-readmore-toggle]')
.remove();
current.removeData();
});
}
};
$.fn.readmore = function(options) {
var args = arguments,
selector = this.selector;
options = options || {};
if (typeof options === 'object') {
return this.each(function() {
if ($.data(this, 'plugin_' + readmore)) {
var instance = $.data(this, 'plugin_' + readmore);
instance.destroy.apply(instance);
}
options.selector = selector;
$.data(this, 'plugin_' + readmore, new Readmore(this, options));
});
}
else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') {
return this.each(function () {
var instance = $.data(this, 'plugin_' + readmore);
if (instance instanceof Readmore && typeof instance[options] === 'function') {
instance[options].apply(instance, Array.prototype.slice.call(args, 1));
}
});
}
};
}));

11
uoj/1/js/readmore/readmore.min.js vendored Normal file
View File

@ -0,0 +1,11 @@
/*!
* @preserve
*
* Readmore.js jQuery plugin
* Author: @jed_foster
* Project home: http://jedfoster.github.io/Readmore.js
* Licensed under the MIT license
*
* Debounce function from http://davidwalsh.name/javascript-debounce-function
*/
!function(t){"function"==typeof define&&define.amd?define(["jquery"],t):"object"==typeof exports?module.exports=t(require("jquery")):t(jQuery)}(function(t){"use strict";function e(t,e,i){var a;return function(){var n=this,o=arguments,r=function(){a=null,i||t.apply(n,o)},s=i&&!a;clearTimeout(a),a=setTimeout(r,e),s&&t.apply(n,o)}}function i(t){var e=++h;return String(null==t?"rmjs-":t)+e}function a(t){var e=t.clone().css({height:"auto",width:t.width(),maxHeight:"none",overflow:"hidden"}).insertAfter(t),i=e.outerHeight(),a=parseInt(e.css({maxHeight:""}).css("max-height").replace(/[^-\d\.]/g,""),10),n=t.data("defaultHeight");e.remove();var o=a||t.data("collapsedHeight")||n;t.data({expandedHeight:i,maxHeight:a,collapsedHeight:o}).css({maxHeight:"none"})}function n(t){if(!d[t.selector]){var e=" ";t.embedCSS&&""!==t.blockCSS&&(e+=t.selector+" + [data-readmore-toggle], "+t.selector+"[data-readmore]{"+t.blockCSS+"}"),e+=t.selector+"[data-readmore]{transition: height "+t.speed+"ms;overflow: hidden;}",function(t,e){var i=t.createElement("style");i.type="text/css",i.styleSheet?i.styleSheet.cssText=e:i.appendChild(t.createTextNode(e)),t.getElementsByTagName("head")[0].appendChild(i)}(document,e),d[t.selector]=!0}}function o(e,i){this.element=e,this.options=t.extend({},s,i),n(this.options),this._defaults=s,this._name=r,this.init(),window.addEventListener?(window.addEventListener("load",l),window.addEventListener("resize",l)):(window.attachEvent("load",l),window.attachEvent("resize",l))}var r="readmore",s={speed:100,collapsedHeight:200,heightMargin:16,moreLink:'<a href="#">Read More</a>',lessLink:'<a href="#">Close</a>',embedCSS:!0,blockCSS:"display: block; width: 100%;",startOpen:!1,beforeToggle:function(){},afterToggle:function(){}},d={},h=0,l=e(function(){t("[data-readmore]").each(function(){var e=t(this),i="true"===e.attr("aria-expanded");a(e),e.css({height:e.data(i?"expandedHeight":"collapsedHeight")})})},100);o.prototype={init:function(){var e=t(this.element);e.data({defaultHeight:this.options.collapsedHeight,heightMargin:this.options.heightMargin}),a(e);var n=e.data("collapsedHeight"),o=e.data("heightMargin");if(e.outerHeight(!0)<=n+o)return!0;var r=e.attr("id")||i(),s=this.options.startOpen?this.options.lessLink:this.options.moreLink;e.attr({"data-readmore":"","aria-expanded":this.options.startOpen,id:r}),e.after(t(s).on("click",function(t){return function(i){t.toggle(this,e[0],i)}}(this)).attr({"data-readmore-toggle":"","aria-controls":r})),this.options.startOpen||e.css({height:n})},toggle:function(e,i,a){a&&a.preventDefault(),e||(e=t('[aria-controls="'+_this.element.id+'"]')[0]),i||(i=_this.element);var n=t(i),o="",r="",s=!1,d=n.data("collapsedHeight");n.height()<=d?(o=n.data("expandedHeight")+"px",r="lessLink",s=!0):(o=d,r="moreLink"),this.options.beforeToggle(e,n,!s),n.css({height:o}),n.on("transitionend",function(i){return function(){i.options.afterToggle(e,n,s),t(this).attr({"aria-expanded":s}).off("transitionend")}}(this)),t(e).replaceWith(t(this.options[r]).on("click",function(t){return function(e){t.toggle(this,i,e)}}(this)).attr({"data-readmore-toggle":"","aria-controls":n.attr("id")}))},destroy:function(){t(this.element).each(function(){var e=t(this);e.attr({"data-readmore":null,"aria-expanded":null}).css({maxHeight:"",height:""}).next("[data-readmore-toggle]").remove(),e.removeData()})}},t.fn.readmore=function(e){var i=arguments,a=this.selector;return e=e||{},"object"==typeof e?this.each(function(){if(t.data(this,"plugin_"+r)){var i=t.data(this,"plugin_"+r);i.destroy.apply(i)}e.selector=a,t.data(this,"plugin_"+r,new o(this,e))}):"string"==typeof e&&"_"!==e[0]&&"init"!==e?this.each(function(){var a=t.data(this,"plugin_"+r);a instanceof o&&"function"==typeof a[e]&&a[e].apply(a,Array.prototype.slice.call(i,1))}):void 0}});

View File

@ -451,6 +451,10 @@ $.fn.uoj_highlight = function() {
$(this).find(".uoj-blog-tag").uoj_blog_tag();
$(this).find(".uoj-click-zan-block").click_zan_block();
$(this).find(".countdown").countdown();
$(this).find(".uoj-readmore").readmore({
moreLink: '<a href="#" class="text-right">more...</a>',
lessLink: '<a href="#" class="text-right">close</a>',
});
});
};
@ -469,7 +473,8 @@ function checkContestNotice(id, lastTime) {
checkContestNotice(id, data.time);
}, 60000);
if (data.msg != undefined) {
alert(data.msg);
var len=data.msg.length;
for (var i=0;i<len;i++) alert(data.msg[i]);
}
},
'json'