diff --git a/docs/addon.md b/docs/addon.md index bc272d896..4571a50f0 100644 --- a/docs/addon.md +++ b/docs/addon.md @@ -35,7 +35,7 @@ Hydro的推荐架构如下: - handler.js: 用于处理路由 - model.js: 数据库模型 - lib.js: 不依赖于数据库等的库(如md5函数) -- script.js: 可能会被用户多次使用到的脚本(如重新计算rating) +- script.js: 可能会被用户多次使用到的脚本(如重新计算rp) - locale/: 翻译文件 - template/: 页面模板 - setting.yaml: 模块所用到的设置,格式在下方说明 diff --git a/hydro/entry/worker.ts b/hydro/entry/worker.ts index 67303e777..4e745b53c 100644 --- a/hydro/entry/worker.ts +++ b/hydro/entry/worker.ts @@ -31,6 +31,7 @@ export async function load() { for (const i of builtinLib) require(`../lib/${i}`); await lib(pending, fail); require('../service/gridfs'); + require('../service/monitor'); const server = require('../service/server'); await server.prepare(); await service(pending, fail); diff --git a/hydro/handler/domain.ts b/hydro/handler/domain.ts index ebccb87d5..01ef32788 100644 --- a/hydro/handler/domain.ts +++ b/hydro/handler/domain.ts @@ -13,7 +13,7 @@ class DomainRankHandler extends Handler { @param('page', Types.UnsignedInt, true) async get(domainId: string, page = 1) { const [dudocs, upcount, ucount] = await paginate( - domain.getMultiInDomain(domainId).sort({ rating: -1 }), + domain.getMultiInDomain(domainId).sort({ rp: -1 }), page, 100, ); diff --git a/hydro/handler/manage.ts b/hydro/handler/manage.ts index a50a9486a..159baf1cf 100644 --- a/hydro/handler/manage.ts +++ b/hydro/handler/manage.ts @@ -95,7 +95,7 @@ class SystemScriptHandler extends SystemHandler { validate(global.Hydro.script[id].validate, args); const rid = await record.add(domainId, -1, this.user._id, '-', id, false, { input: 'raw' }); const report = (data) => judge.next({ domainId, rid, ...data }); - report({ message: `Running script: ${id}` }); + report({ message: `Running script: ${id}`, status: STATUS.STATUS_JUDGING }); const start = new Date().getTime(); // Maybe async? global.Hydro.script[id].run(args, report) diff --git a/hydro/handler/record.ts b/hydro/handler/record.ts index ed773d942..4d925b81d 100644 --- a/hydro/handler/record.ts +++ b/hydro/handler/record.ts @@ -1,5 +1,5 @@ import { ObjectID } from 'mongodb'; -import { PermissionError } from '../error'; +import { PermissionError, RecordNotFoundError } from '../error'; import { PERM } from '../model/builtin'; import * as problem from '../model/problem'; import * as record from '../model/record'; @@ -63,6 +63,7 @@ class RecordDetailHandler extends RecordHandler { async get(domainId: string, rid: ObjectID) { this.response.template = 'record_detail.html'; const rdoc = await record.get(domainId, rid); + if (!rdoc) throw new RecordNotFoundError(rid); if (rdoc.contest) { const tdoc = await contest.get(domainId, rdoc.contest.tid, rdoc.contest.type); if (!this.canShowRecord(tdoc, true)) throw new PermissionError(rid); @@ -71,7 +72,7 @@ class RecordDetailHandler extends RecordHandler { rdoc.code = null; } const [pdoc, udoc] = await Promise.all([ - problem.get(domainId, rdoc.pid, null), + problem.get(domainId, rdoc.pid), user.getById(domainId, rdoc.uid), ]); this.response.body = { diff --git a/hydro/interface.ts b/hydro/interface.ts index 45e6587ac..ae60af992 100644 --- a/hydro/interface.ts +++ b/hydro/interface.ts @@ -135,6 +135,7 @@ export interface Pdoc { nAccept: number, tag: string[], category: string[], + difficulty: number, data?: ProblemData, hidden: boolean, config: ProblemConfig, diff --git a/hydro/lib/rank.ts b/hydro/lib/rank.ts index 53a8e450b..d1903344d 100644 --- a/hydro/lib/rank.ts +++ b/hydro/lib/rank.ts @@ -1,4 +1,4 @@ -function* ranked(diter, equ = (a, b) => a === b) { +function* ranked(diter: T[], equ = (a: T, b: T) => a === b): Generator<[number, T], void> { let last = null; let r = 0; let count = 0; diff --git a/hydro/lib/rating.ts b/hydro/lib/rating.ts index f56e12171..40c389df8 100644 --- a/hydro/lib/rating.ts +++ b/hydro/lib/rating.ts @@ -1,15 +1,17 @@ +// Based on https://github.com/QAQrz/Codeforces-Rating-System/blob/master/rating.py + class User { + uid: number; + rank: number; old: number; - seed: number; - - uid: number; + seed?: number; delta?: number; - new: number; + new?: number; constructor(rank: number, old: number, uid = 0) { this.rank = rank; @@ -22,7 +24,7 @@ class User { class RatingCalculator { users: User[]; - constructor(users) { + constructor(users: User[]) { this.users = []; for (const user of users) { this.users.push(new User(user.rank, user.old, user.uid)); @@ -106,8 +108,20 @@ interface RatingOutputUser { } export default function calculate(users: RatingInputUser[]): RatingOutputUser[] { + let last_idx = 0; + let last_rank = 1; + for (let i = 1; i < users.length; i++) { + if (users[i].rank > last_rank) { + for (let j = last_idx; j < i; j++) users[j].rank = i; + last_idx = i; + last_rank = users[i].rank; + } + } + for (let i = last_idx; i < users.length; i++) { + users[i].rank = users.length; + } const calculator = new RatingCalculator(users); - return calculator.calculate(); + return calculator.calculate() as RatingOutputUser[]; } global.Hydro.lib.rating = calculate; diff --git a/hydro/lib/template.ts b/hydro/lib/template.ts index bb19f663f..19a8359b1 100644 --- a/hydro/lib/template.ts +++ b/hydro/lib/template.ts @@ -2,6 +2,7 @@ import fs from 'fs'; import path from 'path'; import serialize from 'serialize-javascript'; import nunjucks from 'nunjucks'; +import html2text from 'html-to-text'; import { argv } from 'yargs'; import * as markdown from './markdown'; import * as misc from './misc'; @@ -37,6 +38,7 @@ class Nunjucks extends nunjucks.Environment { this.addFilter('assign', (self, data) => Object.assign(self, data)); this.addFilter('markdown', (self, html = false) => markdown.render(self, html)); this.addFilter('ansi', (self) => misc.ansiToHtml(self)); + this.addFilter('inline', (self) => html2text.fromString(self).replace(/\n/gmi, ' ')); this.addFilter('base64_encode', (s) => Buffer.from(s).toString('base64')); this.addFilter('bitand', (self, val) => self & val); } diff --git a/hydro/model/problem.ts b/hydro/model/problem.ts index f964fc2a1..66a05e2d0 100644 --- a/hydro/model/problem.ts +++ b/hydro/model/problem.ts @@ -18,6 +18,7 @@ export const pdocHidden: Pdoc = { content: '', nSubmit: 0, nAccept: 0, + difficulty: 0, tag: [], category: [], data: null, diff --git a/hydro/model/record.ts b/hydro/model/record.ts index fa16a9710..1548cb094 100644 --- a/hydro/model/record.ts +++ b/hydro/model/record.ts @@ -109,14 +109,20 @@ export function getMulti(domainId: string, query: any) { export async function update( domainId: string, _id: ObjectID, $set: any = {}, $push: any = {}, $unset: any = {}, -): Promise { +): Promise { const $update: any = {}; if ($set && Object.keys($set).length) $update.$set = $set; if ($push && Object.keys($push).length) $update.$push = $push; if ($unset && Object.keys($unset).length) $update.$unset = $unset; - const res = await coll.findOneAndUpdate({ domainId, _id }, $update, { returnOriginal: false }); - if (!res.value) throw new RecordNotFoundError(_id); - return res.value; + if (Object.keys($update).length) { + const res = await coll.findOneAndUpdate( + { _id, domainId }, + $update, + { returnOriginal: false }, + ); + return res.value; + } + return await get(domainId, _id); } export function reset(domainId: string, rid: ObjectID, isRejudge: boolean) { diff --git a/hydro/model/setting.ts b/hydro/model/setting.ts index 226a6b215..6345dee4f 100644 --- a/hydro/model/setting.ts +++ b/hydro/model/setting.ts @@ -107,7 +107,7 @@ DomainUserSetting( Setting('setting_storage', 'nAccept', null, 0, 'number', 'nAccept', null, FLAG_HIDDEN | FLAG_DISABLED), Setting('setting_storage', 'nSubmit', null, 0, 'number', 'nSubmit', null, FLAG_HIDDEN | FLAG_DISABLED), Setting('setting_storage', 'nLike', null, 0, 'number', 'nLike', null, FLAG_HIDDEN | FLAG_DISABLED), - Setting('setting_storage', 'rating', null, 1500, 'number', 'rating', null, FLAG_HIDDEN | FLAG_DISABLED), + Setting('setting_storage', 'rp', null, 1500, 'number', 'RP', null, FLAG_HIDDEN | FLAG_DISABLED), ); SystemSetting( diff --git a/hydro/script/rating.ts b/hydro/script/rating.ts index de81f363c..26884c6c8 100644 --- a/hydro/script/rating.ts +++ b/hydro/script/rating.ts @@ -1,47 +1,50 @@ /* eslint-disable no-await-in-loop */ -import { NumericDictionary } from 'lodash'; +import { NumericDictionary, filter } from 'lodash'; import { ObjectID } from 'mongodb'; +import { Tdoc, Pdoc } from '../interface'; import * as domain from '../model/domain'; import * as contest from '../model/contest'; import * as problem from '../model/problem'; -import rating from '../lib/rating'; +import * as record from '../model/record'; import { STATUS } from '../model/builtin'; -import { Tdoc, Pdoc } from '../interface'; +import rating from '../lib/rating'; -export const description = 'Calculate rating of a domain, or all domains'; +export const description = 'Calculate rp of a domain, or all domains'; type ND = NumericDictionary function calc(udict: ND, rankedDocs: [number, number][]) { const users = []; - for (const [rank, uid] of rankedDocs) { - users.push({ uid, rank, old: udict[uid] || 1500 }); + for (const [rk, uid] of rankedDocs) { + users.push({ uid, rank: rk, old: udict[uid] || 1500 }); } + // FIXME sum(rating.new) always less than sum(rating.old) const rated = rating(users); for (const udoc of rated) { udict[udoc.uid] = udoc.new; } } -async function runProblem(pdoc: Pdoc, udict: ND, uids: number[]): Promise -async function runProblem(domainId: string, pid: number, udict: ND, uids: number[]): Promise +async function runProblem(pdoc: Pdoc, udict: ND): Promise +async function runProblem(domainId: string, pid: number, udict: ND): Promise async function runProblem(...arg: any[]) { const pdoc: Pdoc = (typeof arg[0] === 'string') - ? await contest.get(arg[0], arg[1], -1) + ? await problem.get(arg[0], arg[1], -1) : arg[0]; const udict: ND = (typeof arg[0] === 'string') ? arg[2] : arg[1]; - const uids = (typeof arg[0] === 'string') ? arg[3] : arg[2]; const psdocs = await problem.getMultiStatus( - pdoc.domainId, { docId: pdoc.docId }, - ).sort({ score: -1, _id: 1 }).toArray(); - const ranked = []; - const u = new Set(Array.from(uids)); - for (const index in psdocs) { - ranked.push([index + 1, psdocs[index].uid]); - u.delete(psdocs[index].uid); + pdoc.domainId, { docId: pdoc.docId, rid: { $ne: null } }, + ).toArray(); + if (!psdocs.length) return; + const rdict = await record.getList(pdoc.domainId, psdocs.map((psdoc) => psdoc.rid), true); + const nAccept = filter(psdocs, (psdoc) => psdoc.status === STATUS.STATUS_ACCEPTED).length; + const p = (pdoc.difficulty || 5) / (Math.sqrt(Math.sqrt(nAccept)) + 1); + for (const psdoc of psdocs) { + if (rdict[psdoc.rid]) { + const rp = rdict[psdoc.rid].score * p; + udict[psdoc.uid] = (udict[psdoc.uid] || 1500) + rp; + } } - u.forEach((uid) => ranked.push(psdocs.length, uid)); - calc(udict, ranked); } async function runContest(tdoc: Tdoc, udict: ND, report: Function): Promise @@ -57,6 +60,7 @@ async function runContest(...arg: any[]) { const report = (typeof arg[0] === 'string') ? arg[3] : arg[2]; const tsdocs = await contest.getMultiStatus(tdoc.domainId, tdoc.docId, tdoc.docType) .sort(contest.RULES[tdoc.rule].statusSort).toArray(); + if (!tsdocs.length) return; const rankedTsdocs = contest.RULES[tdoc.rule].rank(tsdocs); const ranked = []; for (const result of rankedTsdocs) { @@ -74,40 +78,9 @@ async function runContest(...arg: any[]) { }); } -async function getRelatedUsers(domainId: string) { - const uids: Set = new Set(); - const problems = await problem.getMulti(domainId, { hidden: false }).toArray(); - for (const pdoc of problems) { - const psdocs = await problem.getMultiStatus( - pdoc.domainId, { docId: pdoc.docId, status: STATUS.STATUS_ACCEPTED }, - ).sort('rid', 1).toArray(); - for (const psdoc of psdocs) uids.add(psdoc.uid); - } - const contests = await contest.getMulti(domainId, { rated: true }, -1).sort('endAt', -1).toArray(); - for (const tdoc of contests) { - const tsdocs = await contest.getMultiStatus(tdoc.domainId, tdoc.docId, tdoc.docType) - .sort(contest.RULES[tdoc.rule].statusSort).toArray(); - for (const tsdoc of tsdocs) uids.add(tsdoc.uid); - } - return Array.from(uids); -} - async function runInDomain(domainId: string, isSub: boolean, report: Function) { - await domain.setMultiUserInDomain(domainId, {}, { rating: 1500 }); - const uids = await getRelatedUsers(domainId); + await domain.setMultiUserInDomain(domainId, {}, { rp: 1500 }); const udict: ND = {}; - // TODO pagination - const problems = await problem.getMulti(domainId, { hidden: false }).toArray(); - await report({ message: `Found ${problems.length} problems in ${domainId}` }); - for (const i in problems) { - const pdoc = problems[i]; - await runProblem(pdoc, udict, uids); - if (!isSub) { - await report({ - progress: Math.floor(((parseInt(i, 10) + 1) / problems.length) * 100), - }); - } - } const contests = await contest.getMulti(domainId, { rated: true }, -1).sort('endAt', -1).toArray(); await report({ message: `Found ${contests.length} contests in ${domainId}` }); for (const i in contests) { @@ -119,9 +92,21 @@ async function runInDomain(domainId: string, isSub: boolean, report: Function) { }); } } + // TODO pagination + const problems = await problem.getMulti(domainId, { hidden: false }).toArray(); + await report({ message: `Found ${problems.length} problems in ${domainId}` }); + for (const i in problems) { + const pdoc = problems[i]; + await runProblem(pdoc, udict); + if (!isSub) { + await report({ + progress: Math.floor(((parseInt(i, 10) + 1) / problems.length) * 100), + }); + } + } const tasks = []; for (const uid in udict) { - tasks.push(domain.setUserInDomain(domainId, parseInt(uid, 10), { rating: udict[uid] })); + tasks.push(domain.setUserInDomain(domainId, parseInt(uid, 10), { rp: udict[uid] })); } await Promise.all(tasks); } @@ -144,7 +129,7 @@ export async function run({ domainId }, report: Function) { progress: Math.floor(((parseInt(i, 10) + 1) / domains.length) * 100), }); } - } else runInDomain(domainId, false, report); + } else await runInDomain(domainId, false, report); return true; } @@ -155,4 +140,4 @@ export const validate = { ], }; -global.Hydro.script.rating = { run, description, validate }; +global.Hydro.script.rp = { run, description, validate }; diff --git a/hydro/service/bus.ts b/hydro/service/bus.ts index 6aa531e1d..1c2553631 100644 --- a/hydro/service/bus.ts +++ b/hydro/service/bus.ts @@ -29,7 +29,10 @@ export function publish(event: string, payload: any, isMaster = true) { } else { if (!bus[event]) bus[event] = {}; const funcs = Object.keys(bus[event]); - Promise.all(funcs.map((func) => bus[event][func]())); + Promise.all(funcs.map((func) => bus[event][func]({ + event, + value: payload, + }))); } } diff --git a/hydro/service/monitor.ts b/hydro/service/monitor.ts index 7d9d9fa84..0ad87eec0 100644 --- a/hydro/service/monitor.ts +++ b/hydro/service/monitor.ts @@ -1,3 +1,4 @@ +import cluster from 'cluster'; import * as db from './db'; import * as sysinfo from '../lib/sysinfo'; @@ -22,16 +23,18 @@ export function updateJudger(args) { ); } -global.Hydro.postInit.push( - async () => { - const info = await sysinfo.get(); - await coll.updateOne( - { mid: info.mid, type: 'server' }, - { $set: { ...info, updateAt: new Date(), type: 'server' } }, - { upsert: true }, - ); - setInterval(update, 60 * 1000); - }, -); +if (cluster.isMaster) { + global.Hydro.postInit.push( + async () => { + const info = await sysinfo.get(); + await coll.updateOne( + { mid: info.mid, type: 'server' }, + { $set: { ...info, updateAt: new Date(), type: 'server' } }, + { upsert: true }, + ); + setInterval(update, 60 * 1000); + }, + ); +} global.Hydro.service.monitor = { update, updateJudger }; diff --git a/package.json b/package.json index 0d526a4c3..5ac87a50d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "2.10.13", + "version": "2.11.1", "main": "dist/loader.js", "bin": "bin/hydrooj.js", "repository": "https://github.com/hydro-dev/Hydro.git", @@ -14,6 +14,7 @@ "ansi_up": "^4.0.4", "detect-browser": "^5.1.1", "fs-extra": "^9.0.1", + "html-to-text": "^5.1.1", "js-yaml": "^3.13.1", "koa": "^2.13.0", "koa-body": "^4.2.0", @@ -44,6 +45,7 @@ "devDependencies": { "@types/adm-zip": "^0.4.33", "@types/fs-extra": "^9.0.1", + "@types/html-to-text": "^5.1.1", "@types/js-yaml": "^3.12.5", "@types/katex": "^0.11.0", "@types/koa": "^2.11.3", diff --git a/yarn.lock b/yarn.lock index 2a21b0415..460494372 100644 --- a/yarn.lock +++ b/yarn.lock @@ -128,6 +128,11 @@ dependencies: "@types/node" "*" +"@types/html-to-text@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-5.1.1.tgz#11d402dcd734ed9f8faa6ad96ce27e58e32ba80f" + integrity sha512-V4eDOSqT3+Md4MBJvV3zF1u9ImXFQJp62qceBZlI9LTkH5lj8m6QWVWKmlQNWVB0XDKLezL0CBi318JF69S5SQ== + "@types/http-assert@*": version "1.5.1" resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b" @@ -895,6 +900,39 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +domelementtype@1, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" + integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== + +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -922,7 +960,12 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" -entities@~2.0.0: +entities@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +entities@^2.0.0, entities@~2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== @@ -1388,11 +1431,38 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + hosted-git-info@^2.1.4: version "2.8.8" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== +html-to-text@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-5.1.1.tgz#2d89db7bf34bc7bcb7d546b1b228991a16926e87" + integrity sha512-Bci6bD/JIfZSvG4s0gW/9mMKwBRoe/1RWLxUME/d6WUSZCdY7T60bssf/jFf7EYXRyqU4P5xdClVqiYU0/ypdA== + dependencies: + he "^1.2.0" + htmlparser2 "^3.10.1" + lodash "^4.17.11" + minimist "^1.2.0" + +htmlparser2@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + http-assert@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878" @@ -1482,7 +1552,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1762,7 +1832,7 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: +lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: version "4.17.19" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== @@ -2322,7 +2392,7 @@ readable-stream@2, readable-stream@^2.3.5: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==