Skip to content

Commit e4dcf69

Browse files
authored
Ignore exceptions caused by URL parsing errors (#34)
* fix markdown parsing * make prettier happy
1 parent 80a08a0 commit e4dcf69

File tree

6 files changed

+829
-288
lines changed

6 files changed

+829
-288
lines changed

packages/remark-lda-lfm/index.d.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @luogu-discussion-archive/remark-lda-lfm
3+
* Copyright (c) 2025 Luogu Discussion Archive Project
4+
*
5+
* Licensed under GNU Affero General Public License version 3 or later.
6+
* See the index.js file for details.
7+
*
8+
* @license AGPL-3.0-or-later
9+
*/
10+
11+
import "mdast";
12+
13+
declare module "mdast" {
14+
interface UserMention extends Parent {
15+
type: "userMention";
16+
uid: number;
17+
children: PhrasingContent[];
18+
}
19+
20+
interface BilibiliVideo extends Node {
21+
type: "bilibiliVideo";
22+
videoId: string;
23+
}
24+
25+
interface PhrasingContentMap {
26+
userMention: UserMention;
27+
bilibiliVideo: BilibiliVideo;
28+
}
29+
30+
interface RootContentMap {
31+
userMention: UserMention;
32+
bilibiliVideo: BilibiliVideo;
33+
}
34+
}
35+
36+
export default function remarkLuoguFlavor(): (
37+
tree: import("mdast").Root,
38+
) => void;

packages/remark-lda-lfm/index.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* @luogu-discussion-archive/remark-lda-lfm
3+
* Copyright (c) 2025 Luogu Discussion Archive Project
4+
* See AUTHORS.txt in the project root for the full list of contributors.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*
19+
* Please notice that 「洛谷」 (also known as "Luogu") is a registered trademark of
20+
* Shanghai Luogu Network Technology Co., Ltd (上海洛谷网络科技有限公司).
21+
*
22+
* @license AGPL-3.0-or-later
23+
*/
24+
25+
/// <reference types="remark-parse" />
26+
/// <reference types="remark-stringify" />
27+
/// <reference types="mdast" />
28+
/// <reference path="./index.d.ts" />
29+
30+
/**
31+
* @typedef {import('mdast').Root} Root
32+
* @typedef {import('vfile').VFile} VFile
33+
* @typedef {import('unified').Processor<Root>} Processor
34+
*/
35+
36+
/**
37+
* @typedef Options
38+
* Configuration.
39+
* @property {RegExp[] | null} [linkRootToLuoguWhiteList]
40+
* URL patterns in list will not point to https://www.luogu.com.cn/,
41+
* except /user/${uid} (userMention). (optional).
42+
* @property {boolean | null} [userLinkPointToLuogu]
43+
* /user/${uid} (userMention) point to luogu or not. Default true. (optional)
44+
*/
45+
46+
import { visit } from "unist-util-visit";
47+
48+
import { gfmFootnote } from "micromark-extension-gfm-footnote";
49+
import { gfmStrikethrough } from "micromark-extension-gfm-strikethrough";
50+
import { gfmTable } from "micromark-extension-gfm-table";
51+
import { gfmAutolinkLiteral } from "micromark-extension-gfm-autolink-literal";
52+
53+
import {
54+
gfmAutolinkLiteralFromMarkdown,
55+
gfmAutolinkLiteralToMarkdown,
56+
} from "mdast-util-gfm-autolink-literal";
57+
import { gfmTableFromMarkdown, gfmTableToMarkdown } from "mdast-util-gfm-table";
58+
import {
59+
gfmStrikethroughFromMarkdown,
60+
gfmStrikethroughToMarkdown,
61+
} from "mdast-util-gfm-strikethrough";
62+
import {
63+
gfmFootnoteFromMarkdown,
64+
gfmFootnoteToMarkdown,
65+
} from "mdast-util-gfm-footnote";
66+
67+
const mentionReg = /^\/user\/(\d+)$/;
68+
const legacyMentionReg = /^\/space\/show\?uid=(\d+)$/;
69+
70+
/** @type {Options} */
71+
const emptyOptions = {};
72+
73+
/**
74+
* remark-luogu-flavor plugin.
75+
*
76+
* @param {Options | null | undefined} [options]
77+
* Configuration (optional).
78+
* @this {Processor}
79+
*/
80+
export default function remarkLuoguFlavor(options) {
81+
const self = this;
82+
const settings = options || emptyOptions;
83+
const data = self.data();
84+
85+
const linkWhiteList = settings.linkRootToLuoguWhiteList ?? [];
86+
const userLinkPointToLuogu = settings.userLinkPointToLuogu ?? true;
87+
88+
const micromarkExtensions =
89+
data.micromarkExtensions || (data.micromarkExtensions = []);
90+
const fromMarkdownExtensions =
91+
data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []);
92+
const toMarkdownExtensions =
93+
data.toMarkdownExtensions || (data.toMarkdownExtensions = []);
94+
95+
micromarkExtensions.push(
96+
gfmFootnote(),
97+
gfmStrikethrough({ singleTilde: false, ...settings }),
98+
gfmTable(),
99+
gfmAutolinkLiteral(),
100+
);
101+
102+
fromMarkdownExtensions.push(
103+
gfmFootnoteFromMarkdown(),
104+
gfmStrikethroughFromMarkdown(),
105+
gfmTableFromMarkdown(),
106+
gfmAutolinkLiteralFromMarkdown(),
107+
);
108+
109+
toMarkdownExtensions.push(
110+
gfmFootnoteToMarkdown(),
111+
gfmTableToMarkdown(),
112+
gfmStrikethroughToMarkdown(),
113+
gfmAutolinkLiteralToMarkdown(),
114+
);
115+
116+
/**
117+
* Transform.
118+
*
119+
* @param {Root} tree
120+
* Tree.
121+
* @returns {undefined}
122+
* Nothing.
123+
*/
124+
return (tree) => {
125+
visit(tree, "paragraph", (node) => {
126+
const childNode = node.children;
127+
childNode.forEach((child, index) => {
128+
const lastNode = childNode[index - 1];
129+
if (
130+
child.type === "link" &&
131+
index >= 1 &&
132+
lastNode.type === "text" &&
133+
lastNode.value.endsWith("@")
134+
) {
135+
const match =
136+
mentionReg.exec(child.url) ?? legacyMentionReg.exec(child.url);
137+
if (!match) return;
138+
/** @type {import("mdast").UserMention} */
139+
const newNode = {
140+
type: "userMention",
141+
children: child.children,
142+
uid: parseInt(match[1]),
143+
data: {
144+
hName: "a",
145+
hProperties: {
146+
href: userLinkPointToLuogu
147+
? `https://www.luogu.com.cn/user/${match[1]}`
148+
: `/user/${match[1]}`,
149+
"data-uid": match[1],
150+
class: "lfm-user-mention",
151+
},
152+
},
153+
};
154+
childNode[index] = newNode;
155+
}
156+
if (child.type === "image" && child.url.startsWith("bilibili:")) {
157+
let videoId = child.url.replace("bilibili:", "");
158+
if (videoId.match(/^[0-9]/)) videoId = "av" + videoId;
159+
/** @type {import("mdast").BilibiliVideo} */
160+
const newNode = {
161+
type: "bilibiliVideo",
162+
videoId,
163+
data: {
164+
hName: "iframe",
165+
hProperties: {
166+
scrolling: "no",
167+
allowfullscreen: "true",
168+
class: "lfm-bilibili-video",
169+
src:
170+
"https://www.bilibili.com/blackboard/webplayer/embed-old.html?bvid=" +
171+
videoId.replace(/[\?&]/g, "&amp;"),
172+
},
173+
},
174+
};
175+
childNode[index] = newNode;
176+
}
177+
});
178+
});
179+
visit(tree, "link", (node) => {
180+
if (!linkWhiteList.some((reg) => reg.test(node.url))) {
181+
try {
182+
node.url = new URL(node.url, "https://www.luogu.com.cn/").href;
183+
} catch (_) {
184+
// ignore
185+
}
186+
}
187+
});
188+
};
189+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@luogu-discussion-archive/remark-lda-lfm",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"keywords": [],
10+
"author": "",
11+
"license": "AGPL-3.0-or-later",
12+
"type": "module",
13+
"devDependencies": {
14+
"@types/mdast": "^4.0.4",
15+
"@types/node": "^20.17.13",
16+
"mdast-util-from-markdown": "^2.0.2",
17+
"rehype-sanitize": "^6.0.0",
18+
"rehype-stringify": "^10.0.1",
19+
"remark": "^15.0.1",
20+
"remark-parse": "^11.0.0",
21+
"remark-rehype": "^11.1.1",
22+
"remark-stringify": "^11.0.0",
23+
"typescript": "^5.7.3",
24+
"unified": "^11.0.5",
25+
"vfile": "^6.0.3"
26+
},
27+
"dependencies": {
28+
"mdast-util-gfm-autolink-literal": "^2.0.1",
29+
"mdast-util-gfm-footnote": "^2.0.0",
30+
"mdast-util-gfm-strikethrough": "^2.0.0",
31+
"mdast-util-gfm-table": "^2.0.0",
32+
"micromark-extension-gfm-autolink-literal": "^2.1.0",
33+
"micromark-extension-gfm-footnote": "^2.1.0",
34+
"micromark-extension-gfm-strikethrough": "^2.1.0",
35+
"micromark-extension-gfm-table": "^2.1.0",
36+
"unist-util-visit": "^5.0.0"
37+
}
38+
}

packages/viewer/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,22 @@
1111
},
1212
"dependencies": {
1313
"@floating-ui/dom": "^1.6.5",
14+
"@luogu-discussion-archive/remark-lda-lfm": "workspace:*",
1415
"@prisma/client": "^5.15.0",
1516
"bootstrap": "^5.3.3",
1617
"highlight.js": "^11.9.0",
1718
"jsdom": "^22.1.0",
1819
"katex": "^0.16.10",
1920
"next": "^14.2.4",
21+
"prismjs": "^1.29.0",
2022
"react": "^18.3.1",
2123
"react-dom": "^18.3.1",
2224
"react-icons": "^4.12.0",
2325
"react-infinite-scroll-component": "^6.1.0",
2426
"react-markdown": "^9.0.1",
2527
"rehype-highlight": "^6.0.0",
2628
"rehype-katex": "^7.0.0",
27-
"remark-luogu-flavor": "^1.0.0",
29+
"rehype-prism-plus": "^2.0.0",
2830
"remark-math": "^6.0.0",
2931
"rsuite": "^5.68.1",
3032
"socket.io-client": "^4.7.5",

packages/viewer/src/components/replies/Content.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import "katex/dist/katex.min.css";
44
import "highlight.js/styles/tokyo-night-dark.css";
5-
import remarkLuoguFlavor from "remark-luogu-flavor";
6-
// import rehypeHighlight from "rehype-highlight";
5+
import "prismjs/themes/prism-okaidia.min.css";
6+
import remarkLuoguFlavor from "@luogu-discussion-archive/remark-lda-lfm";
7+
import rehypePrism from "rehype-prism-plus";
8+
79
import { MutableRefObject, useEffect, useRef, useState } from "react";
810

911
import { computePosition, shift } from "@floating-ui/dom";
12+
1013
import UserInfo from "@/components/UserInfo";
1114
import UserAvatar from "@/components/UserAvatar";
1215
import useSWR from "swr";
@@ -40,7 +43,7 @@ function Tooltip({
4043
<div className="bg-body rounded-4 shadow-bssb-sm px-3 py-2x mb-2">
4144
<div className="d-flex me-auto">
4245
<div>
43-
<UserAvatar className="" user={{ id: uid }} decoratorShadow="sm" />
46+
<UserAvatar user={{ id: uid }} decoratorShadow="sm" />
4447
</div>
4548
<div className="ms-2x mt-1x">
4649
<div>
@@ -168,8 +171,7 @@ export default function Content({
168171
ref={contentRef}
169172
>
170173
<Markdown
171-
// TODO: upgrade the version of rehypeHighlight
172-
rehypePlugins={[rehypeKatex /* , rehypeHighlight */]}
174+
rehypePlugins={[rehypeKatex, rehypePrism]}
173175
remarkPlugins={[
174176
[remarkMath, {}],
175177
[remarkLuoguFlavor, { userLinkPointToLuogu: false }],

0 commit comments

Comments
 (0)