From 5c745fff820776ff6f827ba081c21a1a2eb792bd Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 12 May 2021 13:25:45 +0800 Subject: [PATCH] ui: allow using html in markdown --- .../ui-default/backendlib/markdown-it-xss.js | 90 +++++++++++++++++++ packages/ui-default/backendlib/markdown.js | 9 +- packages/ui-default/backendlib/template.js | 70 +-------------- packages/ui-default/build/config/webpack.js | 13 ++- .../ui-default/components/editor/index.js | 2 +- .../ui-default/components/monaco/index.js | 32 +++++++ .../notification/notification.page.js | 9 +- packages/ui-default/package.json | 3 +- 8 files changed, 151 insertions(+), 77 deletions(-) create mode 100644 packages/ui-default/backendlib/markdown-it-xss.js create mode 100644 packages/ui-default/components/monaco/index.js diff --git a/packages/ui-default/backendlib/markdown-it-xss.js b/packages/ui-default/backendlib/markdown-it-xss.js new file mode 100644 index 000000000..ddf109e5c --- /dev/null +++ b/packages/ui-default/backendlib/markdown-it-xss.js @@ -0,0 +1,90 @@ +const Xss = require('xss'); + +const xss = new Xss.FilterXSS({ + whiteList: { + a: ['target', 'href', 'title'], + abbr: ['title'], + address: [], + area: ['shape', 'coords', 'href', 'alt'], + article: [], + aside: [], + audio: ['autoplay', 'controls', 'loop', 'preload', 'src'], + b: [], + bdi: ['dir'], + bdo: ['dir'], + big: [], + blockquote: ['cite'], + br: [], + caption: [], + center: [], + cite: [], + code: [], + col: ['align', 'valign', 'span', 'width'], + colgroup: ['align', 'valign', 'span', 'width'], + dd: [], + del: ['datetime'], + details: ['open'], + div: [], + dl: [], + dt: [], + em: [], + font: ['color', 'size', 'face'], + h1: [], + h2: [], + h3: [], + h4: [], + h5: [], + h6: [], + header: [], + hr: [], + i: [], + img: ['src', 'alt', 'title', 'width', 'height'], + ins: ['datetime'], + li: [], + mark: [], + ol: [], + p: [], + pre: [], + s: [], + section: [], + small: [], + span: ['class'], + sub: [], + sup: [], + strong: [], + table: ['width', 'border', 'align', 'valign'], + tbody: ['align', 'valign'], + td: ['width', 'rowspan', 'colspan', 'align', 'valign'], + tfoot: ['align', 'valign'], + th: ['width', 'rowspan', 'colspan', 'align', 'valign'], + thead: ['align', 'valign'], + tr: ['rowspan', 'align', 'valign'], + tt: [], + u: [], + ul: [], + video: ['autoplay', 'controls', 'loop', 'preload', 'src', 'height', 'width'], + }, +}); + +function xssProtector(md) { + function protector(state) { + for (let i = 0; i < state.tokens.length; i++) { + const cur = state.tokens[i]; + if (cur.type === 'html_block') { + cur.content = xss.process(cur.content); + } + if (cur.type === 'inline') { + const inlineTokens = cur.children; + for (let ii = 0; ii < inlineTokens.length; ii++) { + if (inlineTokens[ii].type === 'html_inline') { + inlineTokens[ii].content = xss.process(inlineTokens[ii].content); + } + } + } + } + } + + md.core.ruler.after('linkify', 'xss', protector); +} + +module.exports = { xss, xssProtector }; diff --git a/packages/ui-default/backendlib/markdown.js b/packages/ui-default/backendlib/markdown.js index 15e26fa3d..3f6f7adc1 100644 --- a/packages/ui-default/backendlib/markdown.js +++ b/packages/ui-default/backendlib/markdown.js @@ -23,11 +23,13 @@ const Anchor = require('markdown-it-anchor'); // @[pdf](https://foo.com/bar.pdf) const Media = require('./markdown-it-media'); const Katex = require('./markdown-it-katex'); +const { xssProtector } = require('./markdown-it-xss'); class Markdown extends MarkdownIt { constructor() { super({ linkify: true, + html: true, }); this.linkify.tlds('.py', false); this.use(Media); @@ -37,6 +39,7 @@ class Markdown extends MarkdownIt { this.use(Anchor); this.use(TOC); this.use(Katex); + this.use(xssProtector); } } @@ -46,13 +49,11 @@ function plugin(func, ...args) { md.use(func, ...args); } -function render(text, html = false) { - md.set({ html: !!html }); +function render(text) { return md.render(text); } -function renderInline(text, html = false) { - md.set({ html: !!html }); +function renderInline(text) { return md.renderInline(text); } diff --git a/packages/ui-default/backendlib/template.js b/packages/ui-default/backendlib/template.js index 1ac3c8367..3ebb103f5 100644 --- a/packages/ui-default/backendlib/template.js +++ b/packages/ui-default/backendlib/template.js @@ -5,79 +5,12 @@ const serialize = require('serialize-javascript'); const nunjucks = require('nunjucks'); const { filter } = require('lodash'); const { argv } = require('yargs'); -const Xss = require('xss'); const { findFileSync } = require('@hydrooj/utils/lib/utils'); const status = require('@hydrooj/utils/lib/status'); const markdown = require('./markdown'); const { misc, buildContent, avatar } = global.Hydro.lib; -const xss = new Xss.FilterXSS({ - whiteList: { - a: ['target', 'href', 'title'], - abbr: ['title'], - address: [], - area: ['shape', 'coords', 'href', 'alt'], - article: [], - aside: [], - audio: ['autoplay', 'controls', 'loop', 'preload', 'src'], - b: [], - bdi: ['dir'], - bdo: ['dir'], - big: [], - blockquote: ['cite'], - br: [], - caption: [], - center: [], - cite: [], - code: [], - col: ['align', 'valign', 'span', 'width'], - colgroup: ['align', 'valign', 'span', 'width'], - dd: [], - del: ['datetime'], - details: ['open'], - div: [], - dl: [], - dt: [], - em: [], - font: ['color', 'size', 'face'], - h1: [], - h2: [], - h3: [], - h4: [], - h5: [], - h6: [], - header: [], - hr: [], - i: [], - img: ['src', 'alt', 'title', 'width', 'height'], - ins: ['datetime'], - li: [], - mark: [], - ol: [], - p: [], - pre: [], - s: [], - section: [], - small: [], - span: ['class'], - sub: [], - sup: [], - strong: [], - table: ['width', 'border', 'align', 'valign'], - tbody: ['align', 'valign'], - td: ['width', 'rowspan', 'colspan', 'align', 'valign'], - tfoot: ['align', 'valign'], - th: ['width', 'rowspan', 'colspan', 'align', 'valign'], - thead: ['align', 'valign'], - tr: ['rowspan', 'align', 'valign'], - tt: [], - u: [], - ul: [], - video: ['autoplay', 'controls', 'loop', 'preload', 'src', 'height', 'width'], - }, -}); - if (argv.template && argv.template !== 'string') argv.template = findFileSync('@hydrooj/ui-default/templates'); else if (argv.template) argv.template = findFileSync(argv.template); @@ -125,7 +58,6 @@ class Nunjucks extends nunjucks.Environment { this.addFilter('json', (self) => JSON.stringify(self, replacer)); this.addFilter('parseYaml', (self) => yaml.load(self)); this.addFilter('dumpYaml', (self) => yaml.dump(self)); - this.addFilter('xss', (self) => xss.process(self)); this.addFilter('serialize', (self, ignoreFunction = true) => serialize(self, { ignoreFunction })); this.addFilter('assign', (self, data) => Object.assign(self, data)); this.addFilter('markdown', (self, html = false) => markdown.render(self, html)); @@ -150,7 +82,7 @@ class Nunjucks extends nunjucks.Environment { else s = s[langs[0]]; } if (s instanceof Array) s = buildContent(s, html ? 'html' : 'markdown', (str) => str.translate(language)); - return html ? xss.process(s) : markdown.render(s); + return markdown.render(s); }); this.addFilter('log', (self) => { console.log(self); diff --git a/packages/ui-default/build/config/webpack.js b/packages/ui-default/build/config/webpack.js index 5f91f9e61..150db1c98 100644 --- a/packages/ui-default/build/config/webpack.js +++ b/packages/ui-default/build/config/webpack.js @@ -2,7 +2,7 @@ import { dirname } from 'path'; import webpack from 'webpack'; import ExtractCssPlugin from 'mini-css-extract-plugin'; -import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'; +// import { languagesArr } from 'monaco-editor-webpack-plugin/out/languages'; import FriendlyErrorsPlugin from 'friendly-errors-webpack-plugin'; import OptimizeCssAssetsPlugin from 'optimize-css-assets-webpack-plugin'; import CopyWebpackPlugin from 'copy-webpack-plugin'; @@ -13,6 +13,17 @@ import mapWebpackUrlPrefix from '../utils/mapWebpackUrlPrefix'; import StaticManifestPlugin from '../plugins/manifest.webpack'; import root from '../utils/root'; +// const yamlLang = languagesArr.find((t) => t.label === 'yaml'); +// yamlLang.entry = [ +// yamlLang.entry, +// require.resolve('@undefined-moe/monaco-yaml/lib/esm/monaco.contribution'), +// ]; +// yamlLang.worker = { +// id: 'vs/language/yaml/yamlWorker', +// entry: require.resolve('@undefined-moe/monaco-yaml/lib/esm/yaml.worker.js'), +// }; +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); + const beautifyOutputUrl = mapWebpackUrlPrefix([ { prefix: 'node_modules/katex/dist/', replace: './katex/' }, { prefix: 'misc/.iconfont', replace: './ui/iconfont' }, diff --git a/packages/ui-default/components/editor/index.js b/packages/ui-default/components/editor/index.js index 0a4e7abe6..4e6d6c3df 100644 --- a/packages/ui-default/components/editor/index.js +++ b/packages/ui-default/components/editor/index.js @@ -59,7 +59,6 @@ export default class Editor extends DOMAttachedObject { lightbulb: { enabled: true }, model: this.model, automaticLayout: true, - wordWarp: 'bounded', minimap: { enabled: false }, hideCursorInOverviewRuler: true, overviewRulerLanes: 0, @@ -71,6 +70,7 @@ export default class Editor extends DOMAttachedObject { horizontal: 'hidden', handleMouseWheel: false, }; + cfg.wordWrap = 'bounded'; cfg.scrollBeyondLastLine = false; } this.editor = monaco.editor.create(ele, cfg); diff --git a/packages/ui-default/components/monaco/index.js b/packages/ui-default/components/monaco/index.js new file mode 100644 index 000000000..bfcdb055c --- /dev/null +++ b/packages/ui-default/components/monaco/index.js @@ -0,0 +1,32 @@ +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; + +monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: false, + noSyntaxValidation: false, +}); +monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + target: monaco.languages.typescript.ScriptTarget.ES6, + allowNonTsExtensions: true, +}); +const libSource = [ + 'declare function readline(): string;', + 'declare function print(content: string): void;', +].join('\n'); +const libUri = 'ts:filename/basic.d.ts'; +monaco.languages.typescript.javascriptDefaults.addExtraLib(libSource, libUri); +monaco.editor.createModel(libSource, 'typescript', monaco.Uri.parse(libUri)); +monaco.languages.yaml.yamlDefaults.setDiagnosticsOptions({ + validate: true, + enableSchemaRequest: true, + hover: true, + completion: true, + schemas: [ + { + uri: '/manage/system-schema.json', + fileMatch: ['hydro://system/config.yaml'], + }, + ...window.Context.schemas, + ], +}); + +export default monaco; diff --git a/packages/ui-default/components/notification/notification.page.js b/packages/ui-default/components/notification/notification.page.js index eb4f72153..516a503d2 100644 --- a/packages/ui-default/components/notification/notification.page.js +++ b/packages/ui-default/components/notification/notification.page.js @@ -1,7 +1,14 @@ import { AutoloadPage } from 'vj/misc/Page'; import Notification from 'vj/components/notification/index'; +import i18n from 'vj/utils/i18n'; -export default new AutoloadPage('notificationPage', () => { +export default new AutoloadPage('notificationPage', (pagename) => { + const message = i18n(`Hint::Page::${pagename}`); + const item = localStorage.getItem(`hint.${message}`); + if (message !== `Hint::Page::${pagename}` && !item) { + Notification.info(message, message.length * 500); + localStorage.setItem(`hint.${message}`, true); + } const text = new URL(window.location.href).searchParams.get('notification'); if (text) Notification.success(text); }); diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index a7fa43f7e..fbfc81de7 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.8.9", + "version": "4.8.10", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", @@ -23,6 +23,7 @@ "@babel/register": "^7.13.14", "@babel/runtime-corejs3": "^7.14.0", "@hydrooj/utils": "^1.0.15", + "@undefined-moe/monaco-yaml": "^2.5.0", "ansi_up": "^5.0.1", "autoprefixer": "^9.8.6", "babel-loader": "^8.2.2",