Skip to content

Commit 6f4a552

Browse files
添加查看题解md源码功能;使用洛谷同款markdown渲染器
1 parent 1bcd6d4 commit 6f4a552

File tree

23 files changed

+2526
-158
lines changed

23 files changed

+2526
-158
lines changed

.vscode/launch.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
{
22
"configurations": [
33
{
4-
"name": "Launch Edge",
4+
"name": "debug main",
55
"request": "launch",
66
"type": "msedge",
77
"url": "https://mr-python-in-china.github.io/lg-admin-extend/",
8-
"webRoot": "${workspaceFolder}",
98
"preLaunchTask": "debug"
9+
},
10+
{
11+
"name": "test articleViewer",
12+
"request": "launch",
13+
"type": "msedge",
14+
"url": "http://localhost:22552/",
15+
"preLaunchTask": "test:articleViewer"
1016
}
1117
]
1218
}

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"commentTranslate.hover.enabled": false
3+
}

.vscode/tasks.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727
}
2828
}
2929
]
30+
},
31+
{
32+
"label": "test:articleViewer",
33+
"isBackground": true,
34+
"type": "npm",
35+
"script": "test:articleViewer",
36+
"problemMatcher": "$ts-webpack-watch"
3037
}
3138
]
3239
}

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
# lg-admin-extend
2+
3+
# LICENSE
4+
5+
本项目主体使用 LGPL 协议。详见根目录下的 LISENSE 文件。
6+
7+
特别的,由于项目的特殊性,部分代码的实现参考了洛谷前端代码。此部分代码归洛谷所有,我不保留任何权利。这些文件的头部有类似 `Copyright © 2024 by Luogu` 的标识。还请使用时特别留意。

package.json

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,39 @@
1616
"build-with-bundle-analyzer": "webpack --mode production --env analyze",
1717
"prettier:check": "prettier -c .",
1818
"prettier:write": "prettier -w .",
19-
"lint:fix": "eslint --fix"
19+
"lint:fix": "eslint --fix",
20+
"test:articleViewer": "webpack serve -c test/articleViewer/webpack.config.js"
2021
},
2122
"dependencies": {
2223
"@fluentui/react-components": "^9.53.0",
2324
"@fluentui/react-icons": "^2.0.249",
2425
"@mr.python/axios-userscript-adapter": "^1.0.0",
25-
"@vscode/markdown-it-katex": "^1.1.0",
2626
"axios": "^1.7.2",
2727
"dayjs": "^1.11.11",
28+
"hast-util-to-jsx-runtime": "^2.3.0",
29+
"highlight.js": "^11.10.0",
30+
"katex": "^0.16.11",
2831
"lodash": "^4.17.21",
29-
"markdown-it": "^14.1.0",
30-
"markdown-it-highlightjs": "^4.1.0",
32+
"mdast-util-math": "^3.0.0",
33+
"micromark-factory-space": "^2.0.0",
34+
"micromark-util-character": "^2.1.0",
35+
"parse-path": "^7.0.0",
3136
"react": "^18.3.1",
32-
"react-dom": "^18.3.1"
37+
"react-dom": "^18.3.1",
38+
"rehype-highlight": "^7.0.0",
39+
"rehype-katex": "^7.0.0",
40+
"rehype-react": "^8.0.0",
41+
"rehype-stringify": "^10.0.0",
42+
"remark-gfm": "^4.0.0",
43+
"remark-parse": "^11.0.0",
44+
"remark-rehype": "^11.1.0",
45+
"unified": "^11.0.5"
3346
},
3447
"devDependencies": {
3548
"@eslint/js": "^9.4.0",
49+
"@types/katex": "^0",
3650
"@types/lodash": "^4",
37-
"@types/markdown-it": "^14",
51+
"@types/parse-path": "^7",
3852
"@types/react": "^18.3.3",
3953
"@types/react-dom": "^18",
4054
"@types/serviceworker": "^0.0.86",
@@ -44,12 +58,14 @@
4458
"css-minimizer-webpack-plugin": "^7.0.0",
4559
"eslint": "8.56.0",
4660
"eslint-plugin-react": "^7.34.2",
61+
"html-webpack-plugin": "^5.6.0",
4762
"http-server": "^14.1.1",
4863
"mini-css-extract-plugin": "^2.9.0",
4964
"postcss": "^8.4.38",
5065
"postcss-loader": "^8.1.1",
5166
"postcss-preset-env": "^9.5.14",
5267
"prettier": "^3.3.0",
68+
"remark-math": "^6.0.0",
5369
"terser-webpack-plugin": "^5.3.10",
5470
"ts-loader": "^9.5.1",
5571
"typescript": "^5.4.5",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.articleViewer img {
2+
max-width: 100%;
3+
}
4+
.articleViewer pre {
5+
font-size: 0.875em;
6+
background-color: #fafafa;
7+
border: 1px solid #e8e8e8;
8+
border-radius: 2px;
9+
}
10+
.articleViewer pre > code.hljs {
11+
background-color: unset;
12+
}
13+
.articleViewer blockquote {
14+
padding: 10px 20px;
15+
margin: 0 0 20px;
16+
border-left: 5px solid #eee;
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import getProcessor from './getProcessor';
2+
3+
import './articleViewer.css';
4+
import 'katex/dist/katex.css';
5+
import 'highlight.js/styles/base16/tomorrow.css';
6+
import { memo } from 'react';
7+
8+
const processor = getProcessor();
9+
10+
const ArticleViewer = memo(function ({
11+
children: markdown
12+
}: {
13+
children: string;
14+
}) {
15+
return processor.processSync(markdown).result;
16+
});
17+
18+
export default ArticleViewer;

src/app/features/article/articleViewer.tsx

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright © 2024 by Luogu
3+
*/
4+
5+
import remarkGfm from 'remark-gfm';
6+
import remarkParse from 'remark-parse';
7+
import remarkMath from './remarkMath.js';
8+
import remarkRehype from 'remark-rehype';
9+
import { unified } from 'unified';
10+
import rehypeKatex from 'rehype-katex';
11+
import rehypeHighlight from 'rehype-highlight';
12+
import rehypeReact from 'rehype-react';
13+
import { visit } from 'unist-util-visit';
14+
import parsePath from 'parse-path';
15+
import * as props from 'react/jsx-runtime';
16+
17+
const rehypeReactConfig: import('hast-util-to-jsx-runtime').Options = {
18+
Fragment: 'article',
19+
// @ts-expect-error
20+
jsx: props.jsx,
21+
// @ts-expect-error
22+
jsxs: props.jsxs
23+
};
24+
25+
export default function getProcessor() {
26+
return unified()
27+
.use(remarkParse)
28+
.use(remarkGfm)
29+
.use(remarkMath)
30+
.use(remarkRehype)
31+
.use(rehypeKatex)
32+
.use(hastBilibili)
33+
.use(rehypeHighlight)
34+
.use(rehypeReact, rehypeReactConfig)
35+
.freeze();
36+
}
37+
38+
function hastBilibili() {
39+
return (tree: import('hast').Root) =>
40+
visit(tree, 'element', function (element) {
41+
if (element.tagName !== 'img' || !element.properties) return;
42+
const src = element.properties.src;
43+
if (typeof src !== 'string') return;
44+
if (!src.startsWith('bilibili:')) return;
45+
const parsedUrl = parsePath(src);
46+
if ((parsedUrl.protocol as string) !== 'bilibili') return;
47+
const query = parsedUrl.query;
48+
const r: {
49+
aid?: string;
50+
bvid?: string;
51+
page?: string;
52+
danmaku: string;
53+
autoplay: string;
54+
playlist: string;
55+
high_quality: string;
56+
} = {
57+
danmaku: '0',
58+
autoplay: '0',
59+
playlist: '0',
60+
high_quality: '1'
61+
};
62+
const pathname = parsedUrl.pathname;
63+
const match = pathname.match(/^(av)?(\d+)$/);
64+
if (match) r.aid = match[2];
65+
else if (pathname.toLowerCase().startsWith('bv')) r.bvid = pathname;
66+
else r.bvid = 'bv' + pathname;
67+
68+
const page = Number(query.t || '');
69+
if (page) r.page = String(page);
70+
71+
element.tagName = 'div';
72+
element.properties.style = 'position: relative; padding-bottom: 62.5%';
73+
74+
element.children = [
75+
{
76+
type: 'element',
77+
tagName: 'iframe',
78+
properties: {
79+
src:
80+
'https://www.bilibili.com/blackboard/webplayer/embed-old.html?' +
81+
Object.entries(r)
82+
.filter(([_, value]) => value !== undefined)
83+
.map(
84+
([key, value]) =>
85+
`${encodeURIComponent(String(key))}=${encodeURIComponent(String(value))}`
86+
)
87+
.join('&'),
88+
scrolling: 'no',
89+
border: 0,
90+
frameborder: 'no',
91+
framespacing: 0,
92+
allowfullscreen: true,
93+
style:
94+
'position: absolute; top: 0; left: 0; width: 100%; height: 100%;'
95+
},
96+
children: []
97+
}
98+
];
99+
});
100+
}

src/app/features/article/index.tsx

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { SolutionAdminInfo } from '../../../interface';
22
import React, { useEffect, useRef, useState } from 'react';
3-
import { ArctleViewer } from './articleViewer';
3+
import ArticleViewer from './articleViewer';
44
import {
55
Button,
66
Field,
@@ -13,8 +13,7 @@ import {
1313
InfoLabel,
1414
Popover,
1515
PopoverTrigger,
16-
PopoverSurface,
17-
Label
16+
PopoverSurface
1817
} from '@fluentui/react-components';
1918
import { getArticle, submitArticleCheckResult } from '../../../fetch';
2019
import { ErrorDiv, InputDateTime, UserName } from '../../utils';
@@ -23,6 +22,7 @@ import emptyQueueImage from 'assets/emptyQueue.webp';
2322
import './style.css';
2423
import { useNotUndefinedContext } from '../../../notUndefinedContext';
2524
import { MyInfoContext } from '../../contexts';
25+
import { isCancel } from 'axios';
2626

2727
export default function Article() {
2828
const [status, setStatus] = useState<{
@@ -34,6 +34,7 @@ export default function Article() {
3434
const [skipBefore, setSkipBefore] = useState<number>(0);
3535
const [otherRefuseCommit, setOtherRefuseCommit] = useState('');
3636
const [showAdminInfo, setShowAdminInfo] = useState(true);
37+
const [viewSourceCode, setViewSourceCode] = useState(false);
3738
const myProfile = useNotUndefinedContext(MyInfoContext);
3839

3940
let refuseCommit = otherRefuseCommit;
@@ -42,18 +43,19 @@ export default function Article() {
4243
refuseCommit += `。审核管理员:${myProfile.name},对审核结果有疑问请私信交流`;
4344

4445
useEffect(() => {
45-
let ignore = false;
46-
getArticle(skipBefore / 1000)
46+
const cancel = new AbortController();
47+
getArticle(skipBefore / 1000, { signal: cancel.signal })
4748
.then(v => {
48-
if (!ignore)
49-
setStatus({ details: v }),
50-
v.article && setSkipBefore(v.article.promoteResult.updateAt * 1000);
49+
console.log(v);
50+
setStatus({ details: v }),
51+
v.article && setSkipBefore(v.article.promoteResult.updateAt * 1000);
5152
})
5253
.catch(e => {
53-
if (!ignore)
54-
console.error('Error in feature Article', e), setFetchError(e);
54+
console.log(e);
55+
if (isCancel(e)) return;
56+
console.error('Error in feature Article', e), setFetchError(e);
5557
});
56-
return () => void (ignore = true);
58+
return () => void cancel.abort();
5759
}, []);
5860
function updateArticle() {
5961
setStatus(null);
@@ -79,9 +81,20 @@ export default function Article() {
7981
<div className="articleFeature">
8082
{details ? (
8183
details.article ? (
82-
<ArctleViewer className="articleViewer">
83-
{details.article.content}
84-
</ArctleViewer>
84+
<>
85+
<pre
86+
className="articleViewer"
87+
style={{ display: viewSourceCode ? 'block' : 'none' }}
88+
>
89+
<code>{details.article.content}</code>
90+
</pre>
91+
<div
92+
className="articleViewer"
93+
style={{ display: viewSourceCode ? 'none' : 'block' }}
94+
>
95+
<ArticleViewer>{details.article.content}</ArticleViewer>
96+
</div>
97+
</>
8598
) : (
8699
<div className={'articleViewer articleQueueEmpty'}>
87100
<img src={emptyQueueImage} width={300} />
@@ -147,6 +160,11 @@ export default function Article() {
147160
) : undefined}
148161
</div>
149162
<div className="articleOperator">
163+
<Switch
164+
label="显示 MarkDown 源代码"
165+
checked={viewSourceCode}
166+
onChange={(e, x) => setViewSourceCode(x.checked)}
167+
/>
150168
<Field label="其他原因:">
151169
<Textarea
152170
className="otherReasons"
@@ -157,7 +175,7 @@ export default function Article() {
157175
<Switch
158176
label="显示审核员身份"
159177
checked={showAdminInfo}
160-
onChange={(e, x) => setShowAdminInfo(!!x.checked)}
178+
onChange={(e, x) => setShowAdminInfo(x.checked)}
161179
/>
162180
<Field label="预览:">
163181
<Text as="span" className="viewOperatorCommit">

0 commit comments

Comments
 (0)