Merge branch 'master' into monaco

This commit is contained in:
undefined committed 2024-07-10 11:07:48 +08:00
commit 0951017539
93 files changed
+1414 -736

No files matched your search

+5 -4
View File
@@ -1,7 +1,7 @@
import esbuild from 'esbuild';
import {
Context, fs, Handler, Logger, NotFoundError, param, SettingModel, sha1,
size, Types, UiContextBase,
size, SystemModel, Types, UiContextBase,
} from 'hydrooj';
import { debounce } from 'lodash';
import { tmpdir } from 'os';
@@ -46,6 +46,7 @@ const build = async (contents: string) => {
format: 'iife' as 'iife',
bundle: true,
outdir: tmp,
sourcemap: SystemModel.get('ui-default.nosourcemap') ? false : 'external',
splitting: false,
write: false,
target: ['chrome65'],
@@ -103,11 +104,11 @@ export async function buildUI() {
}
const entry = await build([
`window.lazyloadMetadata = ${JSON.stringify(hashes)};`,
`window.LANGS=${JSON.stringify(SettingModel.langs)};`,
...entryPoints.map((i) => `import '${relative(tmp, i).replace(/\\/g, '\\\\')}';`),
].join('\n'));
const pages = entry.outputFiles.map((i) => i.text);
const str = `window.LANGS=${JSON.stringify(SettingModel.langs)};${pages.join('\n')}`;
addFile('entry.js', str);
const pages = entry.outputFiles.filter((i) => i.path.endsWith('.js')).map((i) => i.text);
addFile('entry.js', `window._hydroLoad=()=>{ ${pages.join('\n')} };`);
UiContextBase.constantVersion = hashes['entry.js'];
for (const key in vfs) {
if (newFiles.includes(key)) continue;
@@ -0,0 +1,235 @@
// Process ![test]( x =100x200)
// ^^^^^^^^ this size specification
function parseNextNumber(str, pos, max) {
const start = pos;
const result = {
ok: false,
pos,
value: '',
};
let code = str.charCodeAt(pos);
while (pos < max && (code >= 0x30 /* 0 */ && code <= 0x39 /* 9 */) || code === 0x25 /* % */) {
code = str.charCodeAt(++pos);
}
result.ok = true;
result.pos = pos;
result.value = str.slice(start, pos);
return result;
}
function parseImageSize(str, pos, max) {
const result = {
ok: false,
pos: 0,
width: '',
height: '',
};
if (pos >= max) { return result; }
let code = str.charCodeAt(pos);
if (code !== 0x3d /* = */) { return result; }
pos++;
code = str.charCodeAt(pos);
if (code !== 0x78 /* x */ && (code < 0x30 || code > 0x39) /* [0-9] */) {
return result;
}
// parse width
const resultW = parseNextNumber(str, pos, max);
pos = resultW.pos;
// next charactor must be 'x'
code = str.charCodeAt(pos);
if (code !== 0x78 /* x */) { return result; }
pos++;
// parse height
const resultH = parseNextNumber(str, pos, max);
pos = resultH.pos;
result.width = resultW.value;
result.height = resultH.value;
result.pos = pos;
result.ok = true;
return result;
}
export default function plugin(md) {
md.inline.ruler.before('emphasis', 'image', (state, silent) => {
let attrs;
let code;
let label;
let pos;
let ref;
let res;
let title;
let width = '';
let height = '';
let token;
let tokens;
let start;
let href = '';
const oldPos = state.pos;
const max = state.posMax;
if (state.src.charCodeAt(state.pos) !== 0x21/* ! */) { return false; }
if (state.src.charCodeAt(state.pos + 1) !== 0x5B/* [ */) { return false; }
const labelStart = state.pos + 2;
const labelEnd = md.helpers.parseLinkLabel(state, state.pos + 1, false);
// parser failed to find ']', so it's not a valid link
if (labelEnd < 0) { return false; }
pos = labelEnd + 1;
if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) {
//
// Inline link
//
// [link]( <href> "title" )
// ^^ skipping these spaces
pos++;
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0A) { break; }
}
if (pos >= max) { return false; }
// [link]( <href> "title" )
// ^^^^^^ parsing link destination
start = pos;
res = md.helpers.parseLinkDestination(state.src, pos, state.posMax);
if (res.ok) {
href = state.md.normalizeLink(res.str);
if (state.md.validateLink(href)) {
pos = res.pos;
} else {
href = '';
}
}
// [link]( <href> "title" )
// ^^ skipping these spaces
start = pos;
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0A) { break; }
}
// [link]( <href> "title" )
// ^^^^^^^ parsing link title
res = md.helpers.parseLinkTitle(state.src, pos, state.posMax);
if (pos < max && start !== pos && res.ok) {
title = res.str;
pos = res.pos;
// [link]( <href> "title" )
// ^^ skipping these spaces
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0A) { break; }
}
} else {
title = '';
}
// [link]( <href> "title" =WxH )
// ^^^^ parsing image size
if (pos - 1 >= 0) {
code = state.src.charCodeAt(pos - 1);
// there must be at least one white spaces
// between previous field and the size
if (code === 0x20) {
res = parseImageSize(state.src, pos, state.posMax);
if (res.ok) {
width = res.width;
height = res.height;
pos = res.pos;
// [link]( <href> "title" =WxH )
// ^^ skipping these spaces
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0A) { break; }
}
}
}
}
if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) {
state.pos = oldPos;
return false;
}
pos++;
} else {
//
// Link reference
//
if (typeof state.env.references === 'undefined') { return false; }
// [foo] [bar]
// ^^ optional whitespace (can include newlines)
for (; pos < max; pos++) {
code = state.src.charCodeAt(pos);
if (code !== 0x20 && code !== 0x0A) { break; }
}
if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) {
start = pos + 1;
pos = md.helpers.parseLinkLabel(state, pos);
if (pos >= 0) {
label = state.src.slice(start, pos++);
} else {
pos = labelEnd + 1;
}
} else {
pos = labelEnd + 1;
}
// covers label === '' and label === undefined
// (collapsed reference link and shortcut reference link respectively)
label ||= state.src.slice(labelStart, labelEnd);
ref = state.env.references[md.utils.normalizeReference(label)];
if (!ref) {
state.pos = oldPos;
return false;
}
href = ref.href;
title = ref.title;
}
//
// We found the end of the link, and know for a fact it's a valid link;
// so all that's left to do is to call tokenizer.
//
if (!silent) {
state.pos = labelStart;
state.posMax = labelEnd;
const newState = new state.md.inline.State(
state.src.slice(labelStart, labelEnd),
state.md,
state.env,
tokens = [],
);
newState.md.inline.tokenize(newState);
token = state.push('image', 'img', 0);
token.attrs = attrs = [['src', href],
['alt', '']];
token.children = tokens;
if (title) {
attrs.push(['title', title]);
}
if (width !== '') {
attrs.push(['width', width]);
}
if (height !== '') {
attrs.push(['height', height]);
}
}
state.pos = pos;
state.posMax = max;
return true;
});
};
@@ -1,7 +1,8 @@
// https://github.com/waylonflinn/markdown-it-katex/blob/master/index.js
const katex = require('katex');
import { Logger } from 'hydrooj';
import katex from 'katex';
const logger = new global.Hydro.Logger('katex');
const logger = new Logger('katex');
function isValidDelim(state, pos) {
const max = state.posMax;
@@ -109,8 +110,8 @@ function block(state, start, end, silent) {
return true;
}
module.exports = function plugin(md) {
const options = { throwOnError: false, strict: 'ignore' };
export default function plugin(md) {
const options = { throwOnError: false, strict: 'ignore', displayMode: false };
const render = (latex: string, displayMode = false) => {
options.displayMode = displayMode;
try {
@@ -136,4 +137,4 @@ module.exports = function plugin(md) {
});
md.renderer.rules.math_inline = inlineRenderer;
md.renderer.rules.math_block = blockRenderer;
};
}
@@ -1,8 +1,8 @@
/* eslint-disable max-len */
/* eslint-disable prefer-destructuring */
import { randomUUID } from 'crypto';
import MarkdownIt from 'markdown-it';
import type MarkdownIt from 'markdown-it';
import { v4 as uuid } from 'uuid';
const allowFullScreen = ' webkitallowfullscreen mozallowfullscreen allowfullscreen';
@@ -110,7 +110,7 @@ export function Media(md: MarkdownIt) {
if (service === 'pdf') {
if (src.startsWith('file://') || src.startsWith('./')) src += src.includes('?') ? '&noDisposition=1' : '?noDisposition=1';
return `\
<object classid="clsid:${randomUUID().toUpperCase()}">
<object classid="clsid:${uuid().toUpperCase()}">
<param name="SRC" value="${src}" >
<embed width="100%" style="min-height: 100vh;border: none;" fullscreen="yes" src="${src}">
<noembed></noembed>
+1 -1
View File
@@ -23,7 +23,7 @@ const MergeCells = require('markdown-it-merge-cells');
// @[youtube](https://youtube.com/watch?v=xxx)
// @[pdf](https://foo.com/bar.pdf)
const { Media } = require('./markdown-it-media');
const Katex = require('./markdown-it-katex');
const { default: Katex } = require('./markdown-it-katex');
const { xssProtector } = require('./markdown-it-xss');
class Markdown extends MarkdownIt {
+43 -6
View File
@@ -2,17 +2,42 @@
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import { version as coreJsVersion } from 'core-js/package.json';
import compat from 'core-js-compat';
import { EsbuildPlugin } from 'esbuild-loader';
import fs from 'fs';
import { DuplicatesPlugin } from 'inspectpack/plugin';
import ExtractCssPlugin from 'mini-css-extract-plugin';
import packageJson from 'package-json';
import { dirname } from 'path';
import { gt } from 'semver';
import webpack from 'webpack';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
import WebpackBar from 'webpackbar';
import { version } from '../../package.json';
import root from '../utils/root';
export default function (env: { watch?: boolean, production?: boolean, measure?: boolean } = {}) {
const {
list,
targets,
} = compat({
targets: '> 1%, chrome 70, firefox 60, safari 16, ios_saf 16, not ie 11, not op_mini all',
modules: [
'core-js/stable',
],
exclude: [],
version: coreJsVersion,
inverse: false,
});
fs.writeFileSync(root('__core-js.js'), `${list.map((i) => `import 'core-js/modules/${i}';`).join('\n')}\n`);
export default async function (env: { watch?: boolean, production?: boolean, measure?: boolean } = {}) {
if (env.production) console.log(targets);
let isNew = false;
const { version: latest } = await packageJson('@hydrooj/ui-default', { version: 'latest' });
if (typeof version === 'string' && gt(version, latest)) isNew = true;
function cssLoader() {
return {
loader: 'css-loader',
@@ -61,11 +86,9 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
// sentry requires source-map while keep it simple in dev mode
devtool: env.production ? 'source-map' : false,
entry: {
hydro: './entry.js',
polyfill: './polyfill.ts',
[`hydro-${version}`]: './entry.js',
'default.theme': './theme/default.js',
'service-worker': './service-worker.ts',
'messages-shared-worker': './components/message/worker.ts',
},
cache: {
type: 'filesystem',
@@ -78,6 +101,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
output: {
path: root('public'),
publicPath: '/', // overwrite in entry.js
workerPublicPath: '/',
hashFunction: 'sha1',
hashDigest: 'hex',
hashDigestLength: 10,
@@ -233,18 +257,30 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
filename: '[name].css?[fullhash:6]',
}),
new WebpackManifestPlugin({}),
new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$|mathjax|abcjs|vditor.+\.d\.ts)/ }),
new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$)/ }),
new CopyWebpackPlugin({
patterns: [
{ from: root('static') },
{ from: root('components/navigation/nav-logo-small_dark.png'), to: 'components/navigation/nav-logo-small_dark.png' },
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/mitm.html`), to: 'streamsaver/mitm.html' },
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/sw.js`), to: 'streamsaver/sw.js' },
{ from: root(`${dirname(require.resolve('vditor/package.json'))}/dist`), to: 'vditor/dist' },
{ from: root(`${dirname(require.resolve('graphiql/package.json'))}/graphiql.min.css`), to: 'graphiql.min.css' },
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
],
}),
sentryWebpackPlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: 'hydro-dev',
project: 'hydro-web',
url: 'https://sentry.hydro.ac',
sourcemaps: {
rewriteSources: (source) => source.replace('@hydrooj/ui-default/../../node_modules/', ''),
},
release: (process.env.CI && isNew) ? {
name: `hydro-web@${version}`,
uploadLegacySourcemaps: root('public'),
} : {},
}),
new webpack.DefinePlugin({
'process.env.VERSION': JSON.stringify(require('@hydrooj/ui-default/package.json').version),
}),
@@ -265,6 +301,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
url: 'https://sentry.hydro.ac',
}),
] : []),
new webpack.NormalModuleReplacementPlugin(/core-js\/stable/, root('__core-js.js')),
...env.measure ? [
new BundleAnalyzerPlugin({ analyzerPort: 'auto' }),
new DuplicatesPlugin(),
+5 -15
View File
@@ -19,7 +19,7 @@ const argv = cac().parse();
async function runWebpack({
watch, production, measure, dev, https,
}) {
const compiler = webpack(webpackConfig({ watch, production, measure }));
const compiler = webpack(await webpackConfig({ watch, production, measure }));
if (dev) {
const server = new WebpackDevServer({
port: https ? 8001 : 8000,
@@ -49,7 +49,10 @@ async function runWebpack({
return;
}
if (argv.options.detail) console.log(stats.toString());
if (!watch && (!stats || stats.hasErrors())) process.exitCode = 1;
if (!watch && (!stats || stats.hasErrors())) {
if (!argv.options.detail) console.log(stats.toString());
process.exitCode = 1;
}
resolve(stats);
}
if (watch) compiler.watch({}, compilerCallback);
@@ -121,22 +124,9 @@ async function main() {
if (argv.options.gulp) await runGulp();
else {
await runWebpack(argv.options as any);
if (fs.existsSync('public/hydro.js')) {
fs.copyFileSync('public/hydro.js', `public/hydro-${pkg.version}.js`);
}
if (fs.existsSync('public/polyfill.js')) {
fs.copyFileSync('public/polyfill.js', `public/polyfill-${pkg.version}.js`);
}
if (fs.existsSync('public/theme.css')) {
fs.copyFileSync('public/theme.css', `public/theme-${pkg.version}.css`);
}
if (argv.options.production) {
for (const f of ['echarts', 'graphviz', 'mermaid', 'mathjax']) {
fs.removeSync(`public/vditor/dist/js/${f}`);
}
const files = fs.readdirSync('public');
files.filter((i) => /(^[in]\..+|worker)\.js\.map$/.test(i)).forEach((i) => fs.removeSync(`public/${i}`));
}
await Promise.all(globbySync('public/**/*.map').map((i) => fs.remove(i)));
}
process.chdir(dir);
@@ -24,9 +24,8 @@ const Component = React.forwardRef<any, any>((props, ref) => {
);
});
export default class CustomSelectAutoComplete<Multi extends boolean> extends AutoComplete {
export default class CustomSelectAutoComplete<Multi extends boolean> extends AutoComplete<CustomSelectOptions> {
static DOMAttachKey = 'ucwCustomSelectAutoCompleteInstance';
options: CustomSelectOptions & AutoCompleteOptions;
constructor($dom, options: CustomSelectOptions & AutoCompleteOptions<Multi>) {
super($dom, {
@@ -27,9 +27,8 @@ const Component = React.forwardRef<any, any>((props, ref) => {
);
});
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete {
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete<FileSelectOptions> {
static DOMAttachKey = 'ucwFileSelectAutoCompleteInstance';
options: FileSelectOptions & AutoCompleteOptions;
constructor($dom, options: FileSelectOptions & AutoCompleteOptions<Multi>) {
super($dom, {
@@ -11,10 +11,7 @@ const CustomSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
<AutoComplete<DefaultProps>
ref={ref as any}
fetchItems={(keys) => props.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
queryItems={(query) => props.data.filter((i) => (i.name
? i.name.toLowerCase().match(query.toLowerCase())
: i.toString().toLowerCase().match(query.toLowerCase())
))}
queryItems={(query) => props.data.filter((i) => (i.name || i).toString().toLowerCase().includes(query.toLowerCase()))}
itemText={(item) => `${item.name || item}`}
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
renderItem={(item) => `${item.name || item}`}
@@ -8,6 +8,7 @@ export interface AutoCompleteOptions<Multi extends boolean = boolean> {
defaultItems?: string;
width?: string;
height?: string;
classes?: string;
listStyle?: any;
allowEmptyQuery?: boolean;
freeSolo?: boolean;
@@ -18,17 +19,17 @@ export interface AutoCompleteOptions<Multi extends boolean = boolean> {
text?: () => string;
}
export default class AutoComplete extends DOMAttachedObject {
export default class AutoComplete<Options extends Record<string, any> = {}> extends DOMAttachedObject {
static DOMAttachKey = 'ucwAutoCompleteInstance';
ref = null;
container = document.createElement('div');
options: AutoCompleteOptions;
options: AutoCompleteOptions & Options;
component = ReactDOM.createRoot(this.container);
changeListener = [
(val) => this.$dom.val(val),
];
constructor($dom, options = {}) {
constructor($dom, options = {} as Options) {
super($dom);
this.options = {
items: async () => [],
@@ -6,9 +6,13 @@
.drop.nav__dropdown
max-height: calc(100% - 60px)
overflow-x: hidden
overflow-y: auto
box-shadow: 0rem 0.125rem 0.4375rem rgba(0 0 0 , 30%);
.drop.nav__dropdown.drop-open
overflow-x: auto
.dropdown
.menu
min-width: rem(150px)
@@ -1,12 +1,3 @@
.vditor
margin-bottom: rem(10px)
.vditor-preview__action
display: none
.vditor ol
list-style: decimal
.decoration-hide
color: grey !important
background-color: grey !important
@@ -1,36 +1,11 @@
import $ from 'jquery';
import type { editor } from 'monaco-editor';
import { nanoid } from 'nanoid';
import React from 'react';
import ReactDOM from 'react-dom/client';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import Notification from 'vj/components/notification';
import { i18n, request } from 'vj/utils';
export const config = {
toolbar: [
'emoji', 'headings', 'bold', 'italic', 'strike', 'link', '|',
'list', 'ordered-list', 'check', 'outdent', 'indent', '|',
'quote', 'line', 'code', 'inline-code', 'table', '|',
'upload', 'edit-mode', 'fullscreen', 'export',
],
mode: UserContext.preferredEditorType || 'ir',
toolbarConfig: {
pin: true,
},
cdn: `${UiContext.cdn_prefix}vditor`,
counter: {
enable: true,
max: 65536,
},
preview: {
math: {
inlineDigit: true,
},
theme: {
path: `${UiContext.cdn_prefix}vditor/dist/css/content-theme`,
current: 'light',
},
},
};
import { i18n } from 'vj/utils';
import uploadFiles from '../upload';
interface MonacoOptions {
language?: string;
@@ -43,23 +18,23 @@ interface MonacoOptions {
hide?: string[];
lineNumbers?: 'on' | 'off' | 'relative' | 'interval';
}
interface VditorOptions {
theme?: 'classic' | 'dark'
}
type Options = MonacoOptions & VditorOptions;
type Options = MonacoOptions;
export default class Editor extends DOMAttachedObject {
static DOMAttachKey = 'vjEditorInstance';
model: editor.IModel;
editor: editor.IStandaloneCodeEditor;
vditor: import('vditor').default;
markdownEditor: any;
valueCache?: string;
setMarkdownEditorValue?: (v: string) => void;
reactRoot?: ReactDOM.Root;
isValid: boolean;
constructor($dom, public options: Options = {}) {
super($dom);
if (UserContext.preferredEditorType === 'monaco') this.initMonaco();
else if (options.language && options.language !== 'markdown') this.initMonaco();
else this.initVditor();
else this.initMarkdownEditor();
}
async initMonaco() {
@@ -169,87 +144,102 @@ export default class Editor extends DOMAttachedObject {
window.editor = this.editor;
}
async initVditor() {
async initMarkdownEditor() {
const pagename = document.documentElement.getAttribute('data-page');
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
const isProblemEdit = pagename === 'problem_edit';
const { default: Vditor } = await import('vditor');
const that = this;
const { $dom } = this;
const hasFocus = $dom.is(':focus') || $dom.hasClass('autofocus');
const origin = $dom.get(0);
const ele = document.createElement('div');
const value = $dom.val();
const { onChange } = this.options;
await new Promise((resolve) => {
this.vditor = new Vditor(ele, {
...config,
upload: {
multiple: false,
handler: (files) => {
let wrapper = ['', ''];
let ext: string;
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
if (matches) {
wrapper = ['![image](', ')'];
[, ext] = matches;
} else if (files[0].type === 'application/x-zip-compressed') {
wrapper = ['[file](', ')'];
ext = 'zip';
}
if (!ext) return i18n('No Supported file type.');
const filename = `${nanoid()}.${ext}`;
const data = new FormData();
data.append('filename', filename);
data.append('file', files[0]);
data.append('operation', 'upload_file');
if (isProblemEdit) data.append('type', 'additional_file');
let progress = 0;
request.postFile(isProblemEdit ? './files' : '/file', data, {
xhr: () => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('loadstart', () => this.vditor.vditor.tip.show(i18n('Uploading...'), 0));
xhr.upload.addEventListener('progress', (e) => {
if (!e.lengthComputable) return;
const percentComplete = Math.round((e.loaded / e.total) * 100);
if (percentComplete === progress) return;
progress = percentComplete;
this.vditor.vditor.tip.show(`${i18n('Uploading...')} ${percentComplete}%`, 0);
}, false);
return xhr;
},
})
.then(() => {
this.vditor.insertValue(`${wrapper.join(`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`)} `);
this.vditor.vditor.tip.hide();
})
.catch((e: { message: string; }) => {
console.error(e);
return `${i18n('Upload Failed')}: ${e.message}`;
});
return null;
},
},
...this.options,
after: () => {
const pos = $(ele).find('button[data-mode="sv"]').get();
const button = $('<button data-mode="monaco">Monaco Editor</button>');
button.on('click', (e) => {
Notification.info('You can select this in PreferenceSettings');
e.preventDefault();
});
button.insertAfter(pos);
resolve(null);
},
input(v) {
const { MdEditor } = await import('./mdeditor');
const renderCallback = (ref) => {
this.markdownEditor = ref;
};
function EditorComponent() {
const [val, setVal] = React.useState(value);
that.setMarkdownEditorValue = setVal;
return <MdEditor
className='textbox'
autoFocus={hasFocus}
codeTheme='github'
codeStyleReverse={false}
ref={renderCallback}
modelValue={val}
noMermaid
noPrettier
autoDetectCode
toolbarsExclude={[
// 'bold',
// 'underline',
// 'italic',
// '-',
// 'strikeThrough',
// 'title',
// 'sub',
// 'sup',
// 'quote',
// 'unorderedList',
// 'orderedList',
// 'task',
// '-',
// 'codeRow',
// 'code',
// 'link',
// 'image',
// 'table',
'mermaid',
// 'katex',
// '-',
// 'revoke',
// 'next',
'save',
// '=',
'pageFullscreen',
'fullscreen',
// 'preview',
'previewOnly',
'htmlPreview',
// 'catalog',
'github',
]}
onChange={(v) => {
that.valueCache = v;
setVal(v);
$dom.val(v);
$dom.text(v);
if (onChange) onChange(v);
},
value,
cache: { id: Math.random().toString() },
});
});
$(ele).addClass('textbox');
onChange?.(v);
}}
onUploadImg={async (files, callback) => {
let ext: string;
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
if (matches) {
[, ext] = matches;
} else if (files[0].type === 'application/x-zip-compressed') {
ext = 'zip';
}
if (!ext) return i18n('No Supported file type.');
const filename = `${nanoid()}.${ext}`;
await uploadFiles(isProblemEdit ? './files' : '/file', [files[0]], {
type: isProblemEdit ? 'additional_file' : undefined,
filenameCallback: () => filename,
}).then(() => {
callback([`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`]);
}).catch(() => {
callback([]);
});
return null;
}}
/>;
}
this.reactRoot = ReactDOM.createRoot(ele);
this.reactRoot.render(<EditorComponent />);
$dom.hide();
origin.parentElement.appendChild(ele);
this.isValid = true;
@@ -258,7 +248,7 @@ export default class Editor extends DOMAttachedObject {
destroy() {
this.detach();
if (this.vditor?.destroy) this.vditor.destroy();
if (this.reactRoot) this.reactRoot.unmount();
else if (this.editor?.dispose) this.editor.dispose();
}
@@ -268,13 +258,17 @@ export default class Editor extends DOMAttachedObject {
value(val?: string) {
this.ensureValid();
if (typeof val === 'string') return (this.editor || this.vditor).setValue(val);
return (this.editor || this.vditor).getValue({ lineEnding: '\n', preserveBOM: false });
if (typeof val === 'string') {
if (this.editor) return this.editor.setValue(val);
this.setMarkdownEditorValue?.(val);
this.markdownEditor?.resetHistory?.();
}
if (this.editor) return this.editor.getValue({ lineEnding: '\n', preserveBOM: false });
return this.valueCache;
}
focus() {
this.ensureValid();
this.vditor?.focus();
if (!this.editor || !this.model) return;
this.editor.focus();
const range = this.model.getFullModelRange();
@@ -0,0 +1,39 @@
import 'md-editor-rt/lib/style.css';
import Anchor from 'markdown-it-anchor';
import Footnote from 'markdown-it-footnote';
import Mark from 'markdown-it-mark';
import MergeCells from 'markdown-it-merge-cells';
import TOC from 'markdown-it-table-of-contents';
import { config, MdEditor } from 'md-editor-rt';
import Imsize from '../../backendlib/markdown-it-imsize';
import { Media } from '../../backendlib/markdown-it-media';
import { xssProtector } from '../../backendlib/markdown-it-xss';
const pagename = document.documentElement.getAttribute('data-page');
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
config({
markdownItConfig(mdit) {
mdit.options.html = true;
mdit.options.linkify = true;
mdit.linkify.tlds('.py', false);
mdit.linkify.tlds('.zip', false);
mdit.linkify.tlds('.mov', false);
mdit.use(Media);
mdit.use(Footnote);
mdit.use(Mark);
mdit.use(Imsize);
mdit.use(Anchor);
mdit.use(TOC);
mdit.use(MergeCells);
mdit.use(xssProtector);
if (isProblemPage) {
mdit.core.ruler.before('normalize', 'xss', (state) => {
state.src = state.src.replace(/file:\/\//g, pagename === 'problem_create' ? `/file/${UserContext._id}/` : './file/');
});
}
},
});
export { MdEditor };
@@ -1,5 +1,4 @@
import { nanoid } from 'nanoid';
import ReconnectingWebsocket from 'reconnecting-websocket';
import { InfoDialog } from 'vj/components/dialog';
import VjNotification from 'vj/components/notification/index';
import {
@@ -7,6 +6,7 @@ import {
} from 'vj/constant/message';
import { AutoloadPage } from 'vj/misc/Page';
import { i18n, tpl } from 'vj/utils';
import Sock from '../socket';
let previous: VjNotification;
const onmessage = (msg) => {
@@ -61,7 +61,7 @@ const onmessage = (msg) => {
const initWorkerMode = (endpoint) => {
console.log('Messages: using SharedWorker');
const worker = new SharedWorker('/messages-shared-worker.js', { name: 'HydroMessagesWorker' });
const worker = new SharedWorker(new URL('./worker.ts', import.meta.url), { name: 'HydroMessagesWorker' });
worker.port.start();
window.addEventListener('beforeunload', () => {
worker.port.postMessage({ type: 'unload' });
@@ -128,12 +128,8 @@ const messagePage = new AutoloadPage('messagePage', (pagename) => {
isMaster = true;
localStorage.setItem('page.master', selfId);
const masterChannel = new BroadcastChannel('hydro-messages');
const sock = new ReconnectingWebsocket(endpoint);
sock.onopen = () => console.log('Connected');
sock.onerror = console.error;
sock.onclose = (...args) => console.log('Closed', ...args);
const sock = new Sock(endpoint);
sock.onmessage = async (message) => {
if (process.env.NODE_ENV !== 'production') console.log('onmessage: ', message);
const payload = JSON.parse(message.data);
masterChannel.postMessage({ type: 'message', payload });
};
@@ -31,12 +31,17 @@ function broadcastMsg(message: any) {
function initConn(path: string, port: MessagePort, cookie: any) {
if (cookie !== lcookie) conn?.close();
else if (conn && conn.readyState === conn.OPEN) return;
const url = new URL(path, location.origin);
if (cookie) url.searchParams.set('sid', cookie.split('sid=')[1].split(';')[0]);
lcookie = cookie;
console.log('Init connection for', path);
conn?.close();
conn = new ReconnectingWebsocket(path);
conn = new ReconnectingWebsocket(url.toString());
ports.push(port);
conn.onopen = () => conn.send(cookie);
conn.onopen = () => {
console.log('Connected to', path);
broadcastMsg({ type: 'open' });
};
conn.onerror = () => broadcastMsg({ type: 'error' });
conn.onclose = (ev) => broadcastMsg({ type: 'close', error: ev.reason });
conn.onmessage = (message) => {
@@ -46,14 +51,9 @@ function initConn(path: string, port: MessagePort, cookie: any) {
return;
}
const payload = JSON.parse(message.data);
if (payload.event === 'auth') {
if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
broadcastMsg({ type: 'close', error: payload.error });
conn.close();
} else {
console.log('Connected to', path);
broadcastMsg({ type: 'open' });
}
if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
broadcastMsg({ type: 'close', error: payload.error });
conn.close();
} else {
broadcastMsg({ type: 'message', payload });
let acked = false;
@@ -87,8 +87,8 @@ export function configYamlFormat(config: ProblemConfigFile) {
export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor extends React.PureComponent<Props> {
disposable = [];
containerElement: HTMLElement;
private __preventUpdate = false;
private __preventFormat = false;
__preventUpdate = false;
__preventFormat = false;
editor: editor.IStandaloneCodeEditor;
model: editor.ITextModel;
@@ -1,6 +1,7 @@
let cacheKey = `${UserContext._id}/${UiContext.pdoc.domainId}/${UiContext.pdoc.docId}`;
if (UiContext.tdoc?._id && UiContext.tdoc.rule !== 'homework') cacheKey += `@${UiContext.tdoc._id}`;
// TODO switch to indexeddb
export default function reducer(state = {
lang: localStorage.getItem(`${cacheKey}#lang`) || UiContext.codeLang,
code: localStorage.getItem(cacheKey) || UiContext.codeTemplate,
@@ -54,6 +54,10 @@ export default class Sock {
onclose: (code: number, reason: string) => void;
onopen: (sock: ReconnectingWebSocket) => void;
on(event: 'message' | 'close' | 'open', callback: (...args: any[]) => void) {
this[`on${event}`] = callback;
}
send(data) {
this.sock.send(data);
}
@@ -1,5 +1,4 @@
import $ from 'jquery';
import _ from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
const TAB_TRANSITION_DURATION = 300;
@@ -144,5 +143,3 @@ export default class Tab extends DOMAttachedObject {
return true;
}
}
_.assign(Tab, DOMAttachedObject);
@@ -73,6 +73,26 @@
background: $secondary-color
content: ''
.section__tab-container:not(.immersive)
.section__tab-header
padding-top: 0 !important
.section__tab-header-item
border-top: 3px solid transparent;
transition: border-color 0.2s, background 0.2s, color 0.2s;
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
margin-right: 0
&:hover
border-color: #5f9fd6;
background: #f8f8f8;
&.tab--active
border-color: #ed5f82;
&.selected
border-color: #ed5f82;
.section__tab-content
position: relative
+2 -1
View File
@@ -13,6 +13,7 @@ interface UploadOptions {
pjax?: boolean;
sidebar?: boolean;
singleFileUploadCallback?: (file: File) => any;
filenameCallback?: (file: File) => string;
}
export default async function uploadFiles(endpoint = '', files: File[] | FileList = [], options: UploadOptions = {}) {
const dialog = new Dialog({
@@ -38,7 +39,7 @@ export default async function uploadFiles(endpoint = '', files: File[] | FileLis
if (Number.isNaN(+i)) continue;
const file = files[i];
const data = new FormData();
data.append('filename', file.name);
data.append('filename', options.filenameCallback?.(file) || file.name);
data.append('file', file);
if (options.type) data.append('type', options.type);
data.append('operation', 'upload_file');
+16 -14
View File
@@ -1,3 +1,5 @@
import './polyfill';
import * as Sentry from '@sentry/browser';
import $ from 'jquery';
@@ -29,17 +31,23 @@ console.log(
window.UiContext = JSON.parse(window.UiContext);
window.UserContext = JSON.parse(window.UserContext);
if (process.env.NODE_ENV === 'production' && !UiContext.sentry_disable) {
window.captureException = (e) => Sentry.captureException(e);
window.captureException = (e) => {
if (!e.isUserFacingError) Sentry.captureException(e);
};
Sentry.init({
dsn: UiContext.sentry_dsn || 'https://04d1d77b095231d55e1202bdc912ff16@sentry.hydro.ac/3',
release: process.env.VERSION,
dsn: UiContext.sentry_dsn || 'https://2f95d53751e08c74c1af1c4b93ccaff7@sentry.hydro.ac/2',
release: `hydro-web@${process.env.VERSION}`,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
Sentry.browserApiErrorsIntegration(),
Sentry.replayIntegration({
networkRequestHeaders: ['Content-Type'],
networkResponseHeaders: ['Content-Type', 'Location'],
}),
],
tracesSampleRate: 0.1,
tracePropagationTargets: ['localhost', /^\//, window.location.host],
replaysSessionSampleRate: 0.1,
replaysSessionSampleRate: 0.03,
replaysOnErrorSampleRate: 1.0,
});
}
@@ -57,16 +65,10 @@ const PageLoader = '<div class="page-loader nojs--hide" style="display:none;"><d
$('body').prepend(PageLoader);
$('.page-loader').fadeIn(500);
const prefetch = Promise.all([
fetch(`/constant/${UiContext.constantVersion}.js`).then((r) => r.text()),
import('./api'),
]);
document.addEventListener('DOMContentLoaded', async () => {
Object.assign(window.UiContext, JSON.parse(window.UiContextNew));
Object.assign(window.UserContext, JSON.parse(window.UserContextNew));
const [data, HydroExports] = await prefetch;
Object.assign(window, { HydroExports });
eval(data); // eslint-disable-line no-eval
await HydroExports.initPageLoader();
window.HydroExports = await import('./api');
await window._hydroLoad();
await window.HydroExports.initPageLoader();
}, false);
-7
View File
@@ -10,13 +10,6 @@ class WikiHelpHandler extends Handler {
noCheckPermView = true;
async get() {
const LANGS = SettingModel.langs;
const languages = {};
for (const key in LANGS) {
if (LANGS[key].hidden) continue;
languages[`${LANGS[key].display}(${key})`] = LANGS[key].compile || LANGS[key].execute;
}
this.response.body = { languages };
this.response.template = 'wiki_help.html';
}
}
+19 -13
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.50.2",
"version": "4.51.4",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "index.ts",
@@ -14,13 +14,13 @@
"cli": false
},
"devDependencies": {
"@blueprintjs/core": "^5.10.3",
"@blueprintjs/core": "^5.10.5",
"@codingame/monaco-editor-wrapper": "^6.6.10",
"@codingame/monaco-vscode-language-pack-ko": "5.3.0",
"@codingame/monaco-vscode-language-pack-zh-hans": "5.3.0",
"@codingame/monaco-vscode-language-pack-zh-hant": "5.3.0",
"@codingame/monaco-vscode-typescript-language-features-default-extension": "5.3.0",
"@cordisjs/core": "^3.16.2",
"@cordisjs/core": "3.17.3",
"@fontsource/dm-mono": "^5.0.20",
"@fontsource/fira-code": "^5.0.18",
"@fontsource/inconsolata": "^5.0.18",
@@ -30,12 +30,13 @@
"@fontsource/source-code-pro": "^5.0.18",
"@fontsource/ubuntu-mono": "^5.0.20",
"@hydrooj/utils": "workspace:^",
"@sentry/browser": "^8.8.0",
"@sentry/webpack-plugin": "^2.18.0",
"@sentry/browser": "^8.15.0",
"@sentry/webpack-plugin": "^2.20.1",
"@simplewebauthn/browser": "7.4.0",
"@svgr/webpack": "^8.1.0",
"@types/gulp-if": "^0.0.34",
"@types/jquery": "^3.5.30",
"@types/jquery.transit": "^0.9.33",
"@types/json-schema": "^7.0.15",
"@types/katex": "^0.16.7",
"@types/markdown-it": "^14.1.1",
@@ -45,25 +46,28 @@
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/redux-logger": "^3.0.13",
"@types/uuid": "^10.0.0",
"@types/vscode": "^1.89.0",
"@types/webpack-env": "^1.18.5",
"@vscode/codicons": "^0.0.36",
"ajv": "^8.16.0",
"allotment": "^1.20.2",
"autoprefixer": "^10.4.19",
"browser-update": "^3.3.51",
"browser-update": "^3.3.53",
"chalk": "^5.3.0",
"classnames": "^2.5.1",
"clean-webpack-plugin": "^4.0.0",
"clipboard": "^2.0.11",
"copy-webpack-plugin": "^12.0.2",
"core-js": "^3.37.1",
"core-js-compat": "^3.37.1",
"css-loader": "^6.11.0",
"diff": "^5.2.0",
"diff-dom": "^5.1.3",
"echarts": "^5.5.0",
"echarts": "^5.5.1",
"emojis-keywords": "2.0.0",
"emojis-list": "2.1.0",
"esbuild-loader": "^4.1.0",
"esbuild-loader": "^4.2.0",
"fancy-log": "^2.0.0",
"flatpickr": "^4.6.13",
"graphiql": "^2.4.7",
@@ -72,17 +76,19 @@
"gulp-if": "^3.0.0",
"gulp-plumber": "^1.2.1",
"gulp-svgmin": "^4.1.0",
"idb": "^8.0.0",
"jquery": "^3.7.1",
"jquery-scroll-lock": "^3.1.3",
"jquery.easing": "^1.4.1",
"jquery.transit": "^0.9.12",
"matchmedia-polyfill": "^0.3.2",
"md-editor-rt": "^4.17.3",
"mini-css-extract-plugin": "^2.9.0",
"moment": "^2.30.1",
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@5.3.0",
"monaco-languageclient": "^8.4.0",
"monaco-themes": "^0.4.4",
"monaco-yaml": "^5.1.1",
"monaco-yaml": "5.1.1",
"nanoid": "^5.0.7",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
@@ -95,7 +101,6 @@
"prop-types": "^15.8.1",
"qface": "^1.4.1",
"qrcode": "^1.5.3",
"queue-microtask": "^1.2.3",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-dnd": "^16.0.1",
@@ -129,7 +134,7 @@
"vscode": "npm:@codingame/monaco-vscode-api@^5.3.0",
"vscode-languageclient": "^9.0.1",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.91.0",
"webpack": "^5.92.1",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "^5.0.4",
"webpack-manifest-plugin": "^5.0.0",
@@ -142,10 +147,10 @@
"fs-extra": "^11.2.0",
"js-yaml": "^4.1.0",
"jsesc": "^3.0.2",
"katex": "^0.16.10",
"katex": "^0.16.11",
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^8.6.7",
"markdown-it-anchor": "^9.0.1",
"markdown-it-attrs": "^4.1.6",
"markdown-it-footnote": "^4.0.0",
"markdown-it-imsize": "^2.0.1",
@@ -155,6 +160,7 @@
"nunjucks": "^3.2.4",
"schemastery-jsonschema": "^1.1.0",
"streamsaver": "^2.0.6",
"uuid": "^10.0.0",
"xss": "^1.0.15"
}
}
@@ -1,17 +1,19 @@
import $ from 'jquery';
import { NamedPage } from 'vj/misc/Page';
import { pjax } from 'vj/utils';
import { openDB } from 'vj/utils/db';
const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], () => {
const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], async () => {
const db = await openDB;
const { tdoc } = UiContext;
const key = `scoreboard-star/${tdoc.domainId}/${tdoc.docId}`;
const read = () => JSON.parse(localStorage.getItem(key) || '[]');
const write = (data) => localStorage.setItem(key, JSON.stringify(data));
const id = `${tdoc.domainId}/${tdoc.docId}`;
const read = async () => (await db.get('scoreboard-star', id))?.value || [];
const write = (data) => db.put('scoreboard-star', { id, data });
$(`.star.user--${UserContext._id}`).closest('tr').addClass('star-highlight');
read().forEach((uid) => $(`.star.user--${uid}`).addClass('activated').closest('tr').addClass('star-highlight'));
$('.star').on('click', (e) => {
const star = read();
(await read()).forEach((uid) => $(`.star.user--${uid}`).addClass('activated').closest('tr').addClass('star-highlight'));
$('.star').on('click', async (e) => {
const star = await read();
const $target = $(e.currentTarget);
const uid = $target.data('uid');
if (star.includes(uid)) {
@@ -24,14 +26,14 @@ const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], () =>
write(star);
});
$('.select.filter').on('change', (e) => {
const val = $(e.target).val();
async function update() {
const val = $($('.select.filter')).val();
if (val === 'all') {
$('.data-table tbody tr').show();
read().forEach((uid) => $(`.star.user--${uid}`).closest('tr').addClass('star-highlight'));
(await read()).forEach((uid) => $(`.star.user--${uid}`).closest('tr').addClass('star-highlight'));
} else if (val === 'star') {
$('.data-table tbody tr').hide();
read().forEach((uid) => $(`.star.user--${uid}`).closest('tr').show().removeClass('star-highlight'));
(await read()).forEach((uid) => $(`.star.user--${uid}`).closest('tr').show().removeClass('star-highlight'));
} else if (val === 'rank') {
$('.data-table tbody tr').show();
$('.rank--unrank').closest('tr').hide();
@@ -41,13 +43,17 @@ const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], () =>
if (!uids?.length) return;
uids.forEach((uid) => $(`.user--${uid}`).closest('tr').show());
}
});
}
$('.select.filter').on('change', update);
const beginAt = new Date(UiContext.tdoc.beginAt).getTime();
const endAt = new Date(UiContext.tdoc.endAt).getTime();
function updateScoreboard() {
async function updateScoreboard() {
const now = Date.now();
if (beginAt <= now && now <= endAt) pjax.request({ url: UiContext.scoreboardUrl || '', push: false });
if (beginAt <= now && now <= endAt) {
await pjax.request({ url: UiContext.scoreboardUrl || '', push: false });
await update();
}
}
setInterval(updateScoreboard, 180000);
@@ -9,15 +9,14 @@ import { NamedPage } from 'vj/misc/Page';
import {
delay, i18n, loadReactRedux, pjax, request, tpl,
} from 'vj/utils';
import { openDB } from 'vj/utils/db';
class ProblemPageExtender {
constructor() {
this.isExtended = false;
this.inProgress = false;
this.$content = $('.problem-content-container');
this.$contentBound = this.$content.closest('.section');
this.$scratchpadContainer = $('.scratchpad-container');
}
isExtended = false;
inProgress = false;
$content = $('.problem-content-container');
$contentBound = this.$content.closest('.section');
$scratchpadContainer = $('.scratchpad-container');
async extend() {
if (this.inProgress) return;
@@ -28,6 +27,7 @@ class ProblemPageExtender {
.get(0)
.getBoundingClientRect();
// @ts-ignore
this.$content.transition({ opacity: 0 }, { duration: 100 });
await delay(100);
@@ -41,6 +41,7 @@ class ProblemPageExtender {
})
.show()
.transition({
// @ts-ignore
left: 0,
top: 0,
width: '100%',
@@ -77,6 +78,7 @@ class ProblemPageExtender {
await this.$scratchpadContainer
.transition({
// @ts-ignore
left: bound.left,
top: bound.top,
width: bound.width,
@@ -88,6 +90,7 @@ class ProblemPageExtender {
.promise();
this.$scratchpadContainer.hide();
// @ts-ignore
this.$content.transition({ opacity: 1 }, { duration: 100 });
window.document.body.style.overflow = 'scroll';
@@ -112,6 +115,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
}
async function scratchpadFadeIn() {
// @ts-ignore
await $('#scratchpad')
.transition(
{ opacity: 1 },
@@ -121,6 +125,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
}
async function scratchpadFadeOut() {
// @ts-ignore
await $('#scratchpad')
.transition(
{ opacity: 0 },
@@ -138,6 +143,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
const { default: ScratchpadReducer } = await import('../components/scratchpad/reducers');
const { Provider, store } = await loadReactRedux(ScratchpadReducer);
// @ts-ignore
window.store = store;
const sock = new WebSocket(UiContext.ws_prefix + UiContext.pretestConnUrl);
sock.onmessage = (message) => {
@@ -237,22 +243,25 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
[, setUpdate] = React.useState(0);
return <div className="contest-problems" style={{ margin: '1em' }}>
{pids.map((i) => <a href={`#p${i}`} className={ans[i] ? 'pass ' : ''}>
<span class="id">{i}</span>
{ans[i] && <span class="icon icon-check"></span>}
<span className="id">{i}</span>
{ans[i] && <span className="icon icon-check"></span>}
</a>)}
</div>;
}
function saveAns() {
localStorage.setItem(`${cacheKey}#objective`, JSON.stringify(ans));
setUpdate?.((i) => i + 1);
const db = await openDB;
async function saveAns() {
await db.put('solutions', {
id: `${cacheKey}#objective`,
value: JSON.stringify(ans),
});
}
function clearAns() {
localStorage.removeItem(`${cacheKey}#objective`);
async function clearAns() {
await db.delete('solutions', `${cacheKey}#objective`);
window.location.reload();
}
function loadAns() {
const saved = localStorage.getItem(`${cacheKey}#objective`);
async function loadAns() {
const saved = await db.get('solutions', `${cacheKey}#objective`);
if (!saved) return;
const isValidOption = (v) => v.length === 1 && v.charCodeAt(0) >= 65 && v.charCodeAt(0) <= 90;
Object.assign(ans, JSON.parse(saved));
@@ -261,27 +270,28 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
for (const v of val) {
if (isValidOption(v)) $(`.objective_${id} input[value="${v}"]`).prop('checked', true);
}
} else {
$(`.objective_${id} input[type=text], .objective_${id} textarea`).val(val);
} else if (val) {
$(`.objective_${id} input[type=text], .objective_${id} textarea`).val(val.toString());
if (isValidOption(val)) $(`.objective_${id}.radiobox [value="${val}"]`).prop('checked', true);
}
}
setUpdate((v) => v + 1);
}
if (cnt) {
loadAns();
await loadAns();
$('.problem-content .typo').append(document.getElementsByClassName('nav__item--round').length
? `<input type="submit" disabled class="button rounded primary disabled" value="${i18n('Login to Submit')}" />`
: `<input type="submit" class="button rounded primary" value="${i18n('Submit')}" />`);
$('.objective-input[type!=checkbox]').on('input', (e) => {
$('.objective-input[type!=checkbox]').on('input', (e: JQuery.TriggeredEvent<HTMLInputElement>) => {
ans[e.target.name] = e.target.value;
saveAns();
});
$('input.objective-input[type=checkbox]').on('input', (e) => {
$('input.objective-input[type=checkbox]').on('input', (e: JQuery.TriggeredEvent<HTMLInputElement>) => {
if (e.target.checked) {
ans[e.target.name] ||= [];
ans[e.target.name].push(e.target.value);
ans[e.target.name] = [...new Set(ans[e.target.name])].sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0));
ans[e.target.name] = [...new Set(ans[e.target.name])].sort((a: string, b: string) => a.charCodeAt(0) - b.charCodeAt(0));
} else {
ans[e.target.name] = ans[e.target.name].filter((v) => v !== e.target.value);
}
@@ -310,7 +320,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
const result = await new ConfirmDialog({
$body: tpl.typoMsg('All changes will be lost. Are you sure to clear all answers?'),
}).open();
if (result === 'yes') clearAns();
if (result === 'yes') await clearAns();
});
}
const ele = document.createElement('div');
@@ -154,7 +154,7 @@ async function handleSection(ev, sidebar, type) {
$section.removeClass('animating');
}
export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
export default new NamedPage(['problem_create', 'problem_edit'], () => {
let confirmed = false;
$(document).on('click', '[name="operation"]', (ev) => {
ev.preventDefault();
@@ -240,14 +240,6 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
}
setInterval(() => {
$('img').each(function () {
if (this.src.startsWith('file://')) {
$(this).attr('src', $(this).attr('src').replace('file://', (pagename === 'problem_create' ? `/file/${UserContext._id}/` : './file/')));
}
});
}, 500);
const $main = $('textarea[data-editor]');
const $field = $('textarea[data-markdown-upload]');
let content = $field.val();
@@ -1,17 +1,4 @@
.page--problem_edit,.page--problem_create
.section__tab-header-item
border-top: 3px solid transparent;
transition: border-color 0.2s, background 0.2s, color 0.2s;
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
margin-right: 0
&:hover
border-color: #5f9fd6;
background: #f8f8f8;
&.tab--active
border-color: #ed5f82;
.problem-sidebar-additional_file
height: 270px
overflow-y: auto
+14 -3
View File
@@ -1,9 +1,8 @@
import 'core-js/stable';
import 'matchmedia-polyfill';
import browserUpdate from 'browser-update';
import queueMicrotask from 'queue-microtask';
window.queueMicrotask = queueMicrotask;
browserUpdate({
required: {
e: -10, f: -10, o: -3, s: -1, c: -10,
@@ -13,11 +12,23 @@ browserUpdate({
});
// monaco-editor requires this polyfill
if (window.MediaQueryList) {
if (window.MediaQueryList && !MediaQueryList.prototype.addEventListener) {
MediaQueryList.prototype.addEventListener = (k, listener) => {
MediaQueryList.prototype.addListener(listener);
};
}
if (typeof window['WeakRef'] === 'undefined') {
// @ts-ignore
window.WeakRef = (function (wm) {
function WeakRef(target) {
wm.set(this, target);
}
WeakRef.prototype.deref = function () {
return wm.get(this);
};
return WeakRef;
}(new WeakMap()));
}
if (!(window.matchMedia('all').addListener || window.matchMedia('all').addEventListener)) {
const localMatchMedia = window.matchMedia;
const hasMediaQueries = localMatchMedia('only all').matches;
+1 -1
View File
@@ -89,7 +89,7 @@ self.addEventListener('notificationclick', (event) => {
});
const PRECACHE = `precache-${process.env.VERSION}`;
const DO_NOT_PRECACHE = ['vditor', '.worker.js', 'fonts', 'i.monaco'];
const DO_NOT_PRECACHE = ['.worker.js', 'fonts', 'i.monaco'];
function shouldCachePath(path: string) {
if (!path.split('?')[0].split('/').pop()) return false;
-2
View File
@@ -43,8 +43,6 @@ nav_logo_dark:
default: /components/navigation/nav-logo-small_dark.png
preferredEditorType:
type:
wysiwyg: WYSIWYG
ir: Instant Rendering
sv: Split View
monaco: Monaco Editor
family: markdown
@@ -46,32 +46,18 @@
<script>
var _htmlNode = document.documentElement;
_htmlNode.className = _htmlNode.className.replace(' nojs', ' hasjs');
try {
import('data:text/javascript,');Object.fromEntries([]);window.matchMedia('all').addEventListener('change',()=>{});
} catch(e) {
const s = document.createElement('script');
s.async = false;
s.src = "{{ '/polyfill.js' if process.env.DEV else (UiContext.cdn_prefix + 'polyfill-' + global.Hydro.version['ui-default'] + '.js') }}";
document.head.appendChild(s);
}
</script>
{% endif %}
</head>
<body>
{% if not isIE(handler.request.headers['user-agent']) and not handler.session.legacy %}
<script>
var UiContext = '{{ UiContext|json|jsesc|safe }}';
var UserContext = '{{ UserContext|json|jsesc|safe }}';
</script>
{% set UiContext = Object.create(UiContext) %}
{% set UserContext = Object.create(UserContext) %}
{% if process.env.DEV %}
<script type="text/javascript" src="/resource/hash/lang-{{ _('__id') }}.js?version={{ global.Hydro.version['ui-default'] }}"></script>
<script type="text/javascript" src="/hydro.js?version={{ global.Hydro.version['ui-default'] }}"></script>
{% else %}
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}resource/{{ global.Hydro.version['ui-default'] }}/lang-{{ _('__id') }}.js"></script>
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}hydro-{{ global.Hydro.version['ui-default'] }}.js"></script>
{% endif %}
{% endif %}
{% set UiContext = Object.create(UiContext) %}
{% set UserContext = Object.create(UserContext) %}
</head>
<body>
{% if not isIE(handler.request.headers['user-agent']) and not handler.session.legacy %}
<script type="text/javascript" src="/constant/{{ UiContext.constantVersion }}.js"></script>
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}resource/{{ global.Hydro.version['ui-default'] }}/lang-{{ _('__id') }}.js"></script>
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}hydro-{{ global.Hydro.version['ui-default'] }}.js"></script>
{% endif %}
{% block body %}{% endblock %}
{% block script %}{% endblock %}
+65 -35
View File
@@ -3,41 +3,71 @@
<div class="row">
<div class="medium-12 columns">
<div class="section">
<div class="section__body no-padding">
<div data-fragment-id="status">
<table class="data-table">
<colgroup>
<col class="col--id">
<col class="col--status">
<col class="col--os">
<col class="col--cpu">
<col class="col--memory">
<col class="col--request">
</colgroup>
<thead>
<tr>
<th class="col--id">{{ _('ID') }}</th>
<th class="col--status">{{ _('Current Status') }}</th>
<th class="col--os">{{ _('OS Info') }}</th>
<th class="col--cpu">{{ _('CPU Info') }}</th>
<th class="col--memory">{{ _('Memory Info') }}</th>
<th class="col--request">{{ _('Handled requests') }}</th>
</tr>
</thead>
<tbody>
{%- for doc in stats -%}
<tr>
<td class="col--id">{{ doc._id.toHexString()|truncate(8,True,'') }}</td>
<td class="col--status">{% if doc.isOnline %}{{ doc.status }}{% else %}{{ datetimeSpan(doc.updateAt)|safe }}{% endif %}</td>
<td class="col--os" data-tooltip="{{ doc.osinfo.kernel }}">{{ doc.osinfo.distro }} {{ doc.osinfo.release }} {{ doc.osinfo.codename }} {{ doc.osinfo.arch }}</td>
<td class="col--cpu" data-tooltip="{{ doc.cpu.flags }}">{{ doc.cpu.manufacturer }} {{ doc.cpu.brand }}@{{ doc.cpu.speed }}GHz</td>
<td class="col--memory"{% if doc.stack %} data-tooltip="Stack size: {{ doc.stack }}MB"{% endif %}>{{ size(doc.memory.used) }}/{{ size(doc.memory.total) }}</td>
<td class="col--request">{{ doc.reqCount }}</td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
<div class="section__tab-container">
<ul class="section__tabs">
<li class="section__tab-item">
<h1 class="section__tab-title">{{ _('Servers') }}</h1>
<div class="section__tab-main">
<table class="data-table">
<colgroup>
<col class="col--id">
<col class="col--status">
<col class="col--os">
<col class="col--cpu">
<col class="col--memory">
<col class="col--request">
</colgroup>
<thead>
<tr>
<th class="col--id">{{ _('ID') }}</th>
<th class="col--status">{{ _('Current Status') }}</th>
<th class="col--os">{{ _('OS Info') }}</th>
<th class="col--cpu">{{ _('CPU Info') }}</th>
<th class="col--memory">{{ _('Memory Info') }}</th>
<th class="col--request">{{ _('Handled requests') }}</th>
</tr>
</thead>
<tbody>
{%- for doc in stats -%}
<tr>
<td class="col--id">{{ doc._id.toHexString()|truncate(8,True,'') }}</td>
<td class="col--status">{% if doc.isOnline %}{{ doc.status }}{% else %}{{ datetimeSpan(doc.updateAt)|safe }}{% endif %}</td>
<td class="col--os" data-tooltip="{{ doc.osinfo.kernel }}">{{ doc.osinfo.distro }} {{ doc.osinfo.release }} {{ doc.osinfo.codename }} {{ doc.osinfo.arch }}</td>
<td class="col--cpu" data-tooltip="{{ doc.cpu.flags }}">{{ doc.cpu.manufacturer }} {{ doc.cpu.brand }}@{{ doc.cpu.speed }}GHz</td>
<td class="col--memory"{% if doc.stack %} data-tooltip="Stack size: {{ doc.stack }}MB"{% endif %}>{{ size(doc.memory.used) }}/{{ size(doc.memory.total) }}</td>
<td class="col--request">{{ doc.reqCount }}</td>
</tr>
{%- endfor -%}
</tbody>
</table>
</div>
</li>
{% if compilers|length %}
<li class="section__tab-item">
<h1 class="section__tab-title">{{ _('Compiler Versions') }}</h1>
<div class="section__tab-main">
<div class="section__body typo">
{% for line in compilers %}
<p>{{ line.key|join(', ') }}</p>
<pre><code>{{ line.message }}</code></pre>
{% endfor %}
</div>
</div>
</li>
{% endif %}
<li class="section__tab-item">
<h1 class="section__tab-title">{{ _('Compile Commands') }}</h1>
<div class="section__tab-main">
<div class="section__body typo">
<ul>
{%- for k, v in languages -%}
<li>{{ k }} <pre>{{ v }}</pre></li>
{%- endfor -%}
</ul>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
+1 -8
View File
@@ -17,14 +17,7 @@
<h1 class="section__title" id="compiler" data-heading>{{ _("Compilers' Version and Parameters") }}</h1>
</div>
<div class="section__body typo">
<p>{{ name }} 使用 <a href="https://github.com/hydro-dev/Hydro/tree/master/packages/hydrojudge">HydroJudge</a> 进行评测,编译参数如下:</p>
{% if languages %}
<blockquote><ul>
{%- for k, v in languages -%}
<li>{{ k }} <pre>{{ v }}</pre></li>
{%- endfor -%}
</ul></blockquote>
{% endif %}
<p>{{ name }} 使用 <a href="https://github.com/hydro-dev/Hydro/tree/master/packages/hydrojudge">HydroJudge</a> 进行评测,编译参数和当前所用编译器版本见状态页。</p>
</div>
</div>
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
+1 -1
View File
@@ -196,7 +196,7 @@ $hover-background-color = #424242
background-color: #171717
color: #999
pre:not(.vditor-sv), code
pre, code
background-color: #171717
.scratchpad__toolbar
-1
View File
@@ -1,5 +1,4 @@
import 'normalize.css/normalize.css';
import 'vditor/dist/index.css';
import 'allotment/dist/style.css';
import 'pickadate/lib/themes/classic.css';
import 'pickadate/lib/themes/classic.date.css';
+12 -5
View File
@@ -34,8 +34,8 @@ export function secureRandomString(digit = 32, dict = defaultDict) {
type Substitution = string | number | { templateRaw: true, html: string };
export function tpl(node: React.ReactNode, reactive?: boolean);
export function tpl(pieces: TemplateStringsArray, ...substitutions: Substitution[]);
export function tpl<T extends boolean = false>(node: React.ReactNode, reactive?: T): T extends true ? HTMLDivElement : string;
export function tpl(pieces: TemplateStringsArray, ...substitutions: Substitution[]): string;
export function tpl(pieces: TemplateStringsArray | React.ReactNode, ...substitutions: Substitution[] | boolean[]) {
if (React.isValidElement(pieces)) {
if (substitutions[0]) {
@@ -49,7 +49,7 @@ export function tpl(pieces: TemplateStringsArray | React.ReactNode, ...substitut
for (let i = 0; i < substitutions.length; ++i) {
const subst = substitutions[i];
let substHtml: string;
if (typeof subst === 'object' && subst.templateRaw) {
if (subst && typeof subst === 'object' && subst.templateRaw) {
substHtml = subst.html;
} else substHtml = _.escape(String(subst));
result += substHtml + pieces[i + 1];
@@ -82,7 +82,8 @@ export const zIndexManager = {
};
export const request = {
async ajax(options: Record<string, any>) {
ajax(options: Record<string, any>) {
const stack = new Error().stack;
return new Promise<any>((resolve, reject) => {
$
.ajax({
@@ -96,9 +97,12 @@ export const request = {
if (textStatus === 'abort') {
const err = new Error(i18n('Aborted')) as any;
err.aborted = true;
err.isUserFacingError = true;
reject(err);
} else if (jqXHR.readyState === 0) {
reject(new Error(i18n('Network error')));
const err = new Error(i18n('Network error')) as any;
err.isUserFacingError = true;
reject(err);
} else if (typeof jqXHR.responseJSON === 'object' && jqXHR.responseJSON.error) {
const { error } = jqXHR.responseJSON;
if (error.params) {
@@ -117,6 +121,9 @@ export const request = {
}
})
.done(resolve);
}).catch((e: Error) => {
e.stack = stack;
throw e;
});
},
+37
View File
@@ -0,0 +1,37 @@
import { openDB as _open } from 'idb';
import { InfoDialog } from 'vj/components/dialog';
import { tpl } from 'vj/utils';
export const openDB = _open('hydro', 1, {
upgrade(db, oldVersion) {
if (oldVersion < 1) {
const solutionStore = db.createObjectStore('solutions', { keyPath: 'id' });
db.createObjectStore('scoreboard-star', { keyPath: 'id' });
const keys = Object.keys(localStorage);
for (const key of keys) {
if (key.endsWith('#objective')) {
const value = localStorage.getItem(key);
if (value) solutionStore.put({ id: key, value: JSON.parse(value) });
localStorage.removeItem(key);
}
if (key.startsWith('scoreboard-star/')) localStorage.removeItem(key);
if (/^0\.\d+$/.test(key)) localStorage.removeItem(key);
}
}
},
blocked(currentVersion, blockedVersion) {
console.error('IDB Blocked by version', blockedVersion, 'want', currentVersion);
new InfoDialog({
$body: tpl.typoMsg('Some other opened tabs locked the database. Please close them.'),
}).open();
},
blocking(currentVersion, blockedVersion) {
console.error('IDB Blocking version', blockedVersion);
new InfoDialog({
$body: tpl.typoMsg('Please close or refresh this tab to perform the upgrade.'),
}).open();
},
terminated() {
console.error('IDB Terminated');
},
});