markdown语法增强,错误修复

This commit is contained in:
undefined committed 2020-06-16 12:10:55 +08:00
1 parent 4b3c10b4eb
commit a29b801e72
22 files changed
+311 -200

No files matched your search

+12 -10
View File
@@ -147,25 +147,27 @@ class ContestEditHandler extends ContestDetailHandler {
}
async post({
domainId, beginAtDate, beginAtTime, duration, title, content, rule, pids,
domainId, beginAtDate, beginAtTime, duration, title, content, rule, pids, rated = false,
}) {
let beginAt;
try {
beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.uset.timeZone);
beginAt = moment.tz(`${beginAtDate} ${beginAtTime}`, this.user.timeZone);
} catch (e) {
throw new ValidationError('beginAtDate', 'beginAtTime');
}
const endAt = beginAt.add(duration, 'hours').toDate();
const endAt = beginAt.clone().add(duration, 'hours').toDate();
if (beginAt.isSameOrAfter(endAt)) throw new ValidationError('duration');
beginAt = beginAt.toDate();
pids = await this.verifyProblems(domainId, pids);
await contest.edit(domainId, this.tdoc.docId, title, content, rule, beginAt, endAt, pids);
await contest.edit(domainId, this.tdoc.docId, {
title, content, rule, beginAt, endAt, pids, rated,
});
if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt
|| Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule !== rule) {
await contest.recalcStatus(this.tdoc.docId);
}
if (this.preferJson) this.response.body = { tid: this.tdoc.docId };
else this.response.redirect = `/c/${this.tdoc.docId}`;
this.response.body = { tid: this.tdoc.docId };
this.response.redirect = this.url('contest_detail', { tid: this.tdoc.docId });
}
}
@@ -251,10 +253,10 @@ class ContestProblemSubmitHandler extends ContestProblemHandler {
await contest.updateStatus(domainId, this.tdoc.docId, this.user._id, rid, this.pdoc.docId);
if (!this.canShowRecord(this.tdoc)) {
this.response.body = { tid };
this.response.redirect = `/c/${tid}`;
this.response.redirect = this.url('contest_detail', { tid });
} else {
this.response.body = { rid };
this.response.redirect = `/r/${rid}`;
this.response.redirect = this.url('record_detail', { rid });
}
}
}
@@ -292,14 +294,14 @@ class ContestCreateHandler extends ContestHandler {
} catch (e) {
throw new ValidationError('beginAtDate', 'beginAtTime');
}
const endAt = beginAt.add(duration, 'hours');
const endAt = beginAt.clone().add(duration, 'hours');
pids = await this.verifyProblems(domainId, pids);
const tid = await contest.add(
domainId, title, content,
this.user._id, rule, beginAt, endAt, pids, rated,
);
this.response.body = { tid };
this.response.redirect = `/c/${tid}`;
this.response.redirect = this.url('contest_detail', { tid });
}
}
+3 -3
View File
@@ -176,7 +176,7 @@ class HomeworkDetailProblemSubmitHandler extends HomeworkDetailProblemHandler {
class HomeworkCreateHandler extends HomeworkHandler {
async get() {
const beginAt = moment().add(1, 'day');
const penaltySince = beginAt.add(7, 'days');
const penaltySince = beginAt.clone().add(7, 'days');
const path = [
['Hydro', 'homepage'],
['homework_main', 'homework_main'],
@@ -212,7 +212,7 @@ class HomeworkCreateHandler extends HomeworkHandler {
} catch (e) {
throw new ValidationError('endAtDate', 'endAtTime');
}
const endAt = penaltySince.add(extensionDays, 'days');
const endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
if (penaltySince.isAfter(endAt)) throw new ValidationError('extensionDays');
penaltySince = penaltySince.toDate();
@@ -273,7 +273,7 @@ class HomeworkEditHandler extends HomeworkHandler {
} catch (e) {
throw new ValidationError('endAtDate', 'endAtTime');
}
let endAt = penaltySince.add(extensionDays, 'days');
let endAt = penaltySince.clone().add(extensionDays, 'days');
if (beginAt.isSameOrAfter(penaltySince)) throw new ValidationError('endAtDate', 'endAtTime');
await this.verifyProblems(domainId, pids);
beginAt = beginAt.toDate();
+15 -10
View File
@@ -38,14 +38,17 @@ async function next(body) {
const $set = {};
const $push = {};
if (body.case) {
body.case.memory = body.case.memory_kb;
body.case.time = body.case.time_ms;
rdoc.testCases.push(body.case);
$push.testCases = body.case;
const c = {};
c.memory = body.case.memory_kb || body.case.memory;
c.time = body.case.time_ms || body.case.time;
c.judgeText = body.case.judge_text || body.case.judgeText || body.case.message;
c.status = body.case.status;
rdoc.testCases.push(c);
$push.testCases = c;
}
if (body.judge_text) {
rdoc.judgeTexts.push(body.judge_text);
$push.judgeTexts = body.judge_text;
if (body.judge_text || body.message) {
rdoc.judgeTexts.push(body.judge_text || body.message);
$push.judgeTexts = body.judge_text || body.message;
}
if (body.compiler_text) {
rdoc.compilerTexts.push(body.compiler_text);
@@ -53,8 +56,9 @@ async function next(body) {
}
if (body.status) $set.status = body.status;
if (body.score) $set.score = body.score;
if (body.time_ms) $set.time = body.time_ms;
if (body.memory_kb) $set.memory = body.memory_kb;
if (body.time_ms) $set.time = body.time_ms || body.time;
if (body.memory_kb) $set.memory = body.memory_kb || body.memory;
if (body.progress) $set.progress = body.progress;
rdoc = await record.update(body.domainId, body.rid, $set, $push);
bus.publish('record_change', rdoc);
}
@@ -63,6 +67,7 @@ async function end(body) {
let rdoc = await record.get(body.domainId, body.rid);
const $set = {};
const $push = {};
const $unset = { progress: '' };
if (body.judge_text) {
rdoc.judgeTexts.push(body.judge_text);
$push.judgeTexts = body.judge_text;
@@ -77,7 +82,7 @@ async function end(body) {
if (body.memory_kb) $set.memory = body.memory_kb;
$set.judgeAt = new Date();
$set.judger = body.judger;
rdoc = await record.update(body.domainId, body.rid, $set, $push);
rdoc = await record.update(body.domainId, body.rid, $set, $push, $unset);
await _postJudge(rdoc);
rdoc = await record.update(body.domainId, body.rid, $set, $push);
}
+44 -3
View File
@@ -1,5 +1,8 @@
const setting = require('../model/setting');
const system = require('../model/system');
const { STATUS } = require('../model/builtin');
const record = require('../model/record');
const judge = require('./judge');
const { Route, Handler } = require('../service/server');
const hpm = require('../lib/hpm');
const loader = require('../loader');
@@ -56,11 +59,49 @@ class SystemScriptHandler extends SystemHandler {
this.response.body.path.push(['manage_script', null]);
}
async post({ id, args = '{}' }) {
async post({ domainId, id, args = '{}' }) {
args = JSON.parse(args);
// TODO Do not use console.log
await global.Hydro.script[id].run(args, console.log);
this.back();
const rid = await record.add(domainId, {
pid: id,
uid: this.user._id,
lang: null,
code: null,
status: STATUS.STATUS_JUDGING,
}, false);
async function report(data) {
judge.next({ domainId, rid, ...data });
}
setTimeout(() => {
const start = new Date().getTime();
global.Hydro.script[id].run(args, report)
.then((ret) => {
const time = new Date().getTime() - start;
judge.end({
domainId,
rid,
status: STATUS.STATUS_ACCEPTED,
judge_text: ret,
judger: 1,
time_ms: time,
memory_kb: 0,
});
})
.catch((err) => {
const time = new Date().getTime() - start;
judge.end({
domainId,
rid,
status: STATUS.STATUS_SYSTEM_ERROR,
judge_text: `${err}\n${err.stack}`,
judger: 1,
time_ms: time,
memory_kb: 0,
});
});
}, 500);
this.response.body = { rid };
this.response.redirect = this.url('record_main', { rid });
}
}
+14 -9
View File
@@ -5,6 +5,7 @@ const record = require('../model/record');
const contest = require('../model/contest');
const system = require('../model/system');
const user = require('../model/user');
const paginate = require('../lib/paginate');
const bus = require('../service/bus');
const {
Route, Handler, Connection, ConnectionHandler,
@@ -25,7 +26,11 @@ class RecordListHandler extends RecordHandler {
];
}
if (pid) q.pid = pid;
const rdocs = await record.getMany(domainId, q, { _id: -1 }, page, await system.get('RECORD_PER_PAGE'));
const [rdocs] = await paginate(
record.getMulti(domainId, q).sort('_id', -1),
page,
await system.get('RECORD_PER_PAGE'),
);
const [udict, pdict] = await Promise.all([
user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)),
problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), false),
@@ -59,7 +64,7 @@ class RecordDetailHandler extends RecordHandler {
rdoc.code = null;
}
const [pdoc, udoc] = await Promise.all([
problem.get(domainId, rdoc.pid),
problem.get(domainId, rdoc.pid, null, false),
user.getById(domainId, rdoc.uid),
]);
this.response.body = {
@@ -101,13 +106,13 @@ class RecordMainConnectionHandler extends RecordConnectionHandler {
}
async message(msg) {
const rdocs = await record.getMany(
this.domainId,
{ _id: { $in: msg.rids } },
{ _id: -1 }, 1, 100,
);
for (const rdoc of rdocs) {
this.onRecordChange({ value: rdoc });
if (msg.rids instanceof Array) {
const rdocs = await record.getMulti(
this.domainId, { _id: { $in: msg.rids } },
).toArray();
for (const rdoc of rdocs) {
this.onRecordChange({ value: rdoc });
}
}
}
+56 -1
View File
@@ -1,6 +1,29 @@
const MarkdownIt = require('markdown-it');
const katex = require('markdown-it-katex');
const md = MarkdownIt();
const Prism = require('prismjs');
// For math: $a\times b\eq 10$
const katex = require('markdown-it-katex');
// Specific image size: ![image](image.png =100x100)
const Imsize = require('markdown-it-imsize');
/* Footnote support.
Here is a footnote reference,[^1] and another.[^longnote]
[^1]: Here is the footnote.
[^longnote]: Here's one with multiple blocks.
Subsequent paragraphs are indented to show that they
belong to the previous footnote.
*/
const Footnote = require('markdown-it-footnote');
// ==Highlight==
const Mark = require('markdown-it-mark');
// :::warn This page requires javascript. :::
// :::record-pass Accepted :::
const Container = require('markdown-it-container');
require('prismjs/components/index');
class Markdown extends MarkdownIt {
@@ -19,6 +42,38 @@ class Markdown extends MarkdownIt {
});
this.linkify.tlds('.py', false);
this.use(katex);
this.use(Imsize);
this.use(Footnote);
this.use(Mark);
const RE_CONTAINER = /^(note|warn|record-pending|record-progress|record-fail|record-pass|record-ignored)\s+(.*)$/;
const CONTAINER_MAP = {
note: ['<blockquote class="note">', '</blockquote>'],
warn: ['<blockquote class="warn">', '</blockquote>'],
'record-pending': ['<span class="record-status--text pending">', '</span>'],
'record-progress': ['<span class="record-status--text progress">', '</span>'],
'record-fail': ['<span class="record-status--text fail">', '</span>'],
'record-pass': ['<span class="record-status--text pass">', '</span>'],
'record-ignored': ['<span class="record-status--text ignored">', '</span>'],
};
this.use(Container, 'blockquote', {
validate(params) {
return params.trim().match(RE_CONTAINER);
},
render(tokens, idx) {
const m = tokens[idx].info.trim().match(RE_CONTAINER);
if (!m) return '';
if (tokens[idx].nesting === 1) {
if (CONTAINER_MAP[m[1]]) {
return `${CONTAINER_MAP[m[1]][0] + md.utils.escapeHtml(m[2])}\n`;
}
return `[${m[1]}] ${md.utils.escapeHtml(m[2])}\n`;
}
if (CONTAINER_MAP[m[1]]) {
return `${CONTAINER_MAP[m[1]][1]}\n`;
}
return `[/${m[1]}]`;
},
});
}
}
+6
View File
@@ -1,5 +1,11 @@
const { ValidationError } = require('../error');
/**
* @param {import('mongodb').Cursor} cursor
* @param {number} page
* @param {number} pageSize
* @returns {Promise<Array[]>} pageDocs numPages count
*/
async function paginate(cursor, page, pageSize) {
if (page <= 0) throw new ValidationError('page');
const [count, pageDocs] = await Promise.all([
+2 -1
View File
@@ -348,7 +348,8 @@ const ContestHandlerMixin = (c) => class extends c {
}
async verifyProblems(domainId, pids) { // eslint-disable-line class-methods-use-this
return await problem.getList(domainId, pids, true);
await problem.getList(domainId, pids, true);
return pids;
}
};
+5 -1
View File
@@ -25,4 +25,8 @@ async function inc(op, ident, periodSecs, maxOperations) {
}
}
global.Hydro.model.opcount = module.exports = { inc };
function ensureIndexes() {
return coll.createIndex('expireAt', { expireAfterSeconds: 0 });
}
global.Hydro.model.opcount = module.exports = { inc, ensureIndexes };
+9 -6
View File
@@ -44,13 +44,16 @@ async function add(domainId, title, content, owner, {
* @param {string} domainId
* @param {string|number} pid
* @param {number} uid
* @returns {Pdoc}
* @returns {Promise<Pdoc>}
*/
async function get(domainId, pid, uid = null) {
async function get(domainId, pid, uid = null, doThrow = true) {
const pdoc = Number.isInteger(pid)
? await document.get(domainId, document.TYPE_PROBLEM, pid)
: (await document.getMulti(domainId, document.TYPE_PROBLEM, { pid }).toArray())[0];
if (!pdoc) throw new ProblemNotFoundError(domainId, pid);
if (!pdoc) {
if (doThrow) throw new ProblemNotFoundError(domainId, pid);
return null;
}
if (uid) {
pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid);
}
@@ -63,7 +66,7 @@ async function get(domainId, pid, uid = null) {
* @param {object} sort
* @param {number} page
* @param {number} limit
* @returns {Pdoc[]}
* @returns {Promise<Pdoc[]>}
*/
function getMany(domainId, query, sort, page, limit) {
return document.getMulti(domainId, query).sort(sort).skip((page - 1) * limit).limit(limit)
@@ -116,7 +119,7 @@ async function random(domainId, query) {
}
async function getList(domainId, pids, doThrow = true) {
pids = new Set(pids);
pids = Array.from(new Set(pids));
const r = {};
const pdocs = await document.getMulti(
domainId, document.TYPE_PROBLEM,
@@ -125,7 +128,7 @@ async function getList(domainId, pids, doThrow = true) {
for (const pdoc of pdocs) {
r[pdoc.docId] = r[pdoc.pid] = pdoc;
}
if (pdocs.length !== pids.size) {
if (pdocs.length !== pids.length) {
if (doThrow) {
for (const pid of pids) {
if (!r[pid]) throw new ProblemNotFoundError(domainId, pid);
+18 -16
View File
@@ -12,7 +12,7 @@ const coll = db.collection('record');
* @param {string} domainId
* @param {import('../interface').Record} data
*/
async function add(domainId, data) {
async function add(domainId, data, addTask = true) {
_.defaults(data, {
status: STATUS_WAITING,
domainId,
@@ -27,18 +27,20 @@ async function add(domainId, data) {
judgeAt: null,
});
const [pdoc, res] = await Promise.all([
problem.get(domainId, data.pid),
problem.get(domainId, data.pid, null, false),
coll.insertOne(data),
]);
await task.add({
type: 'judge',
rid: res.insertedId,
domainId,
pid: data.pid,
data: pdoc.data,
lang: data.lang,
code: data.code,
});
if (addTask) {
await task.add({
type: 'judge',
rid: res.insertedId,
domainId,
pid: data.pid,
data: (pdoc || {}).data,
lang: data.lang,
code: data.code,
});
}
return res.insertedId;
}
@@ -54,16 +56,16 @@ async function get(domainId, rid) {
return rdoc;
}
function getMany(domainId, query, sort, page, limit) {
return coll.find({ ...query, domainId }).sort(sort).skip((page - 1) * limit).limit(limit)
.toArray();
function getMulti(domainId, query) {
return coll.find({ ...query, domainId });
}
async function update(domainId, rid, $set, $push) {
async function update(domainId, rid, $set, $push, $unset) {
const _id = new ObjectID(rid);
const upd = {};
if ($set && Object.keys($set).length) upd.$set = $set;
if ($push && Object.keys($push).length) upd.$push = $push;
if ($unset && Object.keys($unset).length) upd.$unset = $unset;
await coll.findOneAndUpdate({ domainId, _id }, upd);
const rdoc = await coll.findOne({ _id });
if (!rdoc) throw new RecordNotFoundError(rid);
@@ -136,7 +138,7 @@ async function rejudge(domainId, rid) {
global.Hydro.model.record = module.exports = {
add,
get,
getMany,
getMulti,
update,
count,
reset,
+28 -5
View File
@@ -3,21 +3,34 @@ const domain = require('../model/domain');
const contest = require('../model/contest');
const user = require('../model/user');
const rating = require('../lib/rating');
const { STATUS } = require('../model/builtin');
async function run({ domainId, isSub = false }, report) {
if (!domainId) {
const domains = await domain.getMulti().toArray();
await report({ total: domains.length });
await report({ message: `Found ${domains.length} domains` });
for (const i in domains) {
const start = new Date().getTime();
await run({ domainId: domains[i]._id, isSub: true }, report);
await report({ current: parseInt(i) + 1 });
await report({
case: {
status: STATUS.STATUS_ACCEPTED,
judgeText: `Domain ${i} finished`,
time_ms: new Date().getTime() - start,
memory_kb: 0,
score: 0,
},
progress: Math.floor(((parseInt(i) + 1) / domains.length) * 100),
});
}
return true;
}
await user.setMultiInDomain(domainId, {}, { rating: 1500 });
const contests = await contest.getMulti(domainId, { rated: true }).sort('endAt', -1).toArray();
await report({ total: contests.length });
await report({ message: `Found ${contests.length} contests in ${domainId}` });
const udict = {};
for (const i in contests) {
const start = new Date().getTime();
const tdoc = contests[i];
const tsdocs = await contest.getMultiStatus(domainId, { docId: tdoc.docId })
.sort(contest.RULES[tdoc.rule].statusSort).toArray();
@@ -30,8 +43,18 @@ async function run({ domainId, isSub = false }, report) {
for (const udoc of rated) {
udict[udoc.uid] = udoc.new;
}
if (isSub) await report({ subProgress: (i + 1) / contests.length });
else await report({ current: parseInt(i) + 1 });
if (!isSub) {
await report({ progress: Math.floor(((parseInt(i) + 1) / contests.length) * 100) });
}
await report({
case: {
status: STATUS.STATUS_ACCEPTED,
judgeText: `Contest ${tdoc.title} finished`,
time_ms: new Date().getTime() - start,
memory_kb: 0,
score: 0,
},
});
}
const tasks = [];
for (const uid in udict) {
+36 -15
View File
@@ -31,6 +31,18 @@ const _validateObjectId = (id, key) => {
if (ObjectID.isValid(id)) return new ObjectID(id);
throw new ValidationError(key);
};
const _bool = (val) => !!val;
const _splitAndTrim = (val) => val.split(',').map((i) => i.trim());
const _date = (val) => {
const d = val.split('-');
assert(d.length === 3);
return `${d[0]}-${d[1].length === 1 ? '0' : ''}${d[1]}-${d[2].length === 1 ? '0' : ''}${d[2]}`;
};
const _time = (val) => {
const t = val.split(':');
assert(t.length === 2);
return `${(t[0].length === 1 ? '0' : '') + t[0]}:${t[1].length === 1 ? '0' : ''}${t[1]}`;
};
const validate = {
tid: _validateObjectId,
@@ -42,6 +54,12 @@ const validate = {
psrid: _validateObjectId,
docId: _validateObjectId,
mongoId: _validateObjectId,
hidden: _bool,
rated: _bool,
category: _splitAndTrim,
tag: _splitAndTrim,
beginAtDate: _date,
beginAtTime: _time,
pid: (pid) => (Number.isSafeInteger(parseInt(pid)) ? parseInt(pid) : pid),
content: validator.checkContent,
title: validator.checkTitle,
@@ -54,9 +72,6 @@ const validate = {
if (page <= 0) throw new ValidationError('page');
return page;
},
category: (category) => category.split(',').map((i) => i.trim()),
tag: (tag) => tag.split(',').map((i) => i.trim()),
hidden: (hidden) => !!hidden,
duration: (duration) => {
if (!Number.isNaN(parseFloat(duration))) duration = parseFloat(duration);
if (duration <= 0) throw new ValidationError('duration');
@@ -192,18 +207,22 @@ class Handler {
}
url(name, kwargs = {}) { // eslint-disable-line class-methods-use-this
delete kwargs.__keywords;
if (this.args.domainId !== 'system') {
name += '_with_domainId';
kwargs.domainId = kwargs.domainId || this.args.domainId;
let res = '#';
try {
delete kwargs.__keywords;
if (this.args.domainId !== 'system') {
name += '_with_domainId';
kwargs.domainId = kwargs.domainId || this.args.domainId;
}
const { anchor, query } = kwargs;
delete kwargs.anchor;
delete kwargs.query;
if (query) res = router.url(name, kwargs, { query });
else res = router.url(name, kwargs);
if (anchor) return `${res}#${anchor}`;
} catch (e) {
console.error(e.message);
}
let res;
const { anchor, query } = kwargs;
delete kwargs.anchor;
delete kwargs.query;
if (query) res = router.url(name, kwargs, { query });
else res = router.url(name, kwargs);
if (anchor) return `${res}#${anchor}`;
return res;
}
@@ -331,15 +350,17 @@ async function handle(ctx, HandlerClass, permission) {
if (h.___prepare) await h.___prepare(args);
if (permission) h.checkPerm(permission);
let checking = '';
try {
for (const key in validate) {
checking = key;
if (args[key]) {
args[key] = validate[key](args[key], key);
}
}
} catch (e) {
if (e instanceof ValidationError) throw e;
throw new ValidationError('Argument check failed');
throw new ValidationError(`Argument ${checking} check failed`);
}
h.args = args;
if (h.__prepare) await h.__prepare(args);
+11 -4
View File
@@ -1,3 +1,5 @@
const { hasIn } = require("lodash");
const dict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
String.random = function random(digit) {
@@ -19,14 +21,19 @@ Array.isDiff = function isDiff(a, b) {
* @returns {string} Formatted date string
*/
Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') {
let h = this.getHours();
if (h < 10) h = `0${h}`;
let m = this.getMinutes();
if (m < 10) m = `0${m}`;
let s = this.getSeconds();
if (s < 10) s = `0${s}`;
return fmt
.replace('%Y', this.getFullYear())
.replace('%m', this.getMonth() + 1)
.replace('%D', this.getDate())
.replace('%d', this.getDate())
.replace('%H', this.getHours())
.replace('%M', this.getMinutes())
.replace('%S', this.getSeconds());
.replace('%H', h)
.replace('%M', m)
.replace('%S', s);
};
/**