Skip to content

Commit

Permalink
Ignore exceptions caused by URL parsing errors (#34)
Browse files Browse the repository at this point in the history
* fix markdown parsing

* make prettier happy
  • Loading branch information
immccn123 authored Jan 16, 2025
1 parent 80a08a0 commit e4dcf69
Show file tree
Hide file tree
Showing 6 changed files with 829 additions and 288 deletions.
38 changes: 38 additions & 0 deletions packages/remark-lda-lfm/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @luogu-discussion-archive/remark-lda-lfm
* Copyright (c) 2025 Luogu Discussion Archive Project
*
* Licensed under GNU Affero General Public License version 3 or later.
* See the index.js file for details.
*
* @license AGPL-3.0-or-later
*/

import "mdast";

declare module "mdast" {
interface UserMention extends Parent {
type: "userMention";
uid: number;
children: PhrasingContent[];
}

interface BilibiliVideo extends Node {
type: "bilibiliVideo";
videoId: string;
}

interface PhrasingContentMap {
userMention: UserMention;
bilibiliVideo: BilibiliVideo;
}

interface RootContentMap {
userMention: UserMention;
bilibiliVideo: BilibiliVideo;
}
}

export default function remarkLuoguFlavor(): (
tree: import("mdast").Root,
) => void;
189 changes: 189 additions & 0 deletions packages/remark-lda-lfm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/**
* @luogu-discussion-archive/remark-lda-lfm
* Copyright (c) 2025 Luogu Discussion Archive Project
* See AUTHORS.txt in the project root for the full list of contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Please notice that 「洛谷」 (also known as "Luogu") is a registered trademark of
* Shanghai Luogu Network Technology Co., Ltd (上海洛谷网络科技有限公司).
*
* @license AGPL-3.0-or-later
*/

/// <reference types="remark-parse" />
/// <reference types="remark-stringify" />
/// <reference types="mdast" />
/// <reference path="./index.d.ts" />

/**
* @typedef {import('mdast').Root} Root
* @typedef {import('vfile').VFile} VFile
* @typedef {import('unified').Processor<Root>} Processor
*/

/**
* @typedef Options
* Configuration.
* @property {RegExp[] | null} [linkRootToLuoguWhiteList]
* URL patterns in list will not point to https://www.luogu.com.cn/,
* except /user/${uid} (userMention). (optional).
* @property {boolean | null} [userLinkPointToLuogu]
* /user/${uid} (userMention) point to luogu or not. Default true. (optional)
*/

import { visit } from "unist-util-visit";

import { gfmFootnote } from "micromark-extension-gfm-footnote";
import { gfmStrikethrough } from "micromark-extension-gfm-strikethrough";
import { gfmTable } from "micromark-extension-gfm-table";
import { gfmAutolinkLiteral } from "micromark-extension-gfm-autolink-literal";

import {
gfmAutolinkLiteralFromMarkdown,
gfmAutolinkLiteralToMarkdown,
} from "mdast-util-gfm-autolink-literal";
import { gfmTableFromMarkdown, gfmTableToMarkdown } from "mdast-util-gfm-table";
import {
gfmStrikethroughFromMarkdown,
gfmStrikethroughToMarkdown,
} from "mdast-util-gfm-strikethrough";
import {
gfmFootnoteFromMarkdown,
gfmFootnoteToMarkdown,
} from "mdast-util-gfm-footnote";

const mentionReg = /^\/user\/(\d+)$/;
const legacyMentionReg = /^\/space\/show\?uid=(\d+)$/;

/** @type {Options} */
const emptyOptions = {};

/**
* remark-luogu-flavor plugin.
*
* @param {Options | null | undefined} [options]
* Configuration (optional).
* @this {Processor}
*/
export default function remarkLuoguFlavor(options) {
const self = this;
const settings = options || emptyOptions;
const data = self.data();

const linkWhiteList = settings.linkRootToLuoguWhiteList ?? [];
const userLinkPointToLuogu = settings.userLinkPointToLuogu ?? true;

const micromarkExtensions =
data.micromarkExtensions || (data.micromarkExtensions = []);
const fromMarkdownExtensions =
data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []);
const toMarkdownExtensions =
data.toMarkdownExtensions || (data.toMarkdownExtensions = []);

micromarkExtensions.push(
gfmFootnote(),
gfmStrikethrough({ singleTilde: false, ...settings }),
gfmTable(),
gfmAutolinkLiteral(),
);

fromMarkdownExtensions.push(
gfmFootnoteFromMarkdown(),
gfmStrikethroughFromMarkdown(),
gfmTableFromMarkdown(),
gfmAutolinkLiteralFromMarkdown(),
);

toMarkdownExtensions.push(
gfmFootnoteToMarkdown(),
gfmTableToMarkdown(),
gfmStrikethroughToMarkdown(),
gfmAutolinkLiteralToMarkdown(),
);

/**
* Transform.
*
* @param {Root} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
return (tree) => {
visit(tree, "paragraph", (node) => {
const childNode = node.children;
childNode.forEach((child, index) => {
const lastNode = childNode[index - 1];
if (
child.type === "link" &&
index >= 1 &&
lastNode.type === "text" &&
lastNode.value.endsWith("@")
) {
const match =
mentionReg.exec(child.url) ?? legacyMentionReg.exec(child.url);
if (!match) return;
/** @type {import("mdast").UserMention} */
const newNode = {
type: "userMention",
children: child.children,
uid: parseInt(match[1]),
data: {
hName: "a",
hProperties: {
href: userLinkPointToLuogu
? `https://www.luogu.com.cn/user/${match[1]}`
: `/user/${match[1]}`,
"data-uid": match[1],
class: "lfm-user-mention",
},
},
};
childNode[index] = newNode;
}
if (child.type === "image" && child.url.startsWith("bilibili:")) {
let videoId = child.url.replace("bilibili:", "");
if (videoId.match(/^[0-9]/)) videoId = "av" + videoId;
/** @type {import("mdast").BilibiliVideo} */
const newNode = {
type: "bilibiliVideo",
videoId,
data: {
hName: "iframe",
hProperties: {
scrolling: "no",
allowfullscreen: "true",
class: "lfm-bilibili-video",
src:
"https://www.bilibili.com/blackboard/webplayer/embed-old.html?bvid=" +
videoId.replace(/[\?&]/g, "&amp;"),
},
},
};
childNode[index] = newNode;
}
});
});
visit(tree, "link", (node) => {
if (!linkWhiteList.some((reg) => reg.test(node.url))) {
try {
node.url = new URL(node.url, "https://www.luogu.com.cn/").href;
} catch (_) {
// ignore
}
}
});
};
}
38 changes: 38 additions & 0 deletions packages/remark-lda-lfm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@luogu-discussion-archive/remark-lda-lfm",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "AGPL-3.0-or-later",
"type": "module",
"devDependencies": {
"@types/mdast": "^4.0.4",
"@types/node": "^20.17.13",
"mdast-util-from-markdown": "^2.0.2",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"remark-stringify": "^11.0.0",
"typescript": "^5.7.3",
"unified": "^11.0.5",
"vfile": "^6.0.3"
},
"dependencies": {
"mdast-util-gfm-autolink-literal": "^2.0.1",
"mdast-util-gfm-footnote": "^2.0.0",
"mdast-util-gfm-strikethrough": "^2.0.0",
"mdast-util-gfm-table": "^2.0.0",
"micromark-extension-gfm-autolink-literal": "^2.1.0",
"micromark-extension-gfm-footnote": "^2.1.0",
"micromark-extension-gfm-strikethrough": "^2.1.0",
"micromark-extension-gfm-table": "^2.1.0",
"unist-util-visit": "^5.0.0"
}
}
4 changes: 3 additions & 1 deletion packages/viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,22 @@
},
"dependencies": {
"@floating-ui/dom": "^1.6.5",
"@luogu-discussion-archive/remark-lda-lfm": "workspace:*",
"@prisma/client": "^5.15.0",
"bootstrap": "^5.3.3",
"highlight.js": "^11.9.0",
"jsdom": "^22.1.0",
"katex": "^0.16.10",
"next": "^14.2.4",
"prismjs": "^1.29.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^4.12.0",
"react-infinite-scroll-component": "^6.1.0",
"react-markdown": "^9.0.1",
"rehype-highlight": "^6.0.0",
"rehype-katex": "^7.0.0",
"remark-luogu-flavor": "^1.0.0",
"rehype-prism-plus": "^2.0.0",
"remark-math": "^6.0.0",
"rsuite": "^5.68.1",
"socket.io-client": "^4.7.5",
Expand Down
12 changes: 7 additions & 5 deletions packages/viewer/src/components/replies/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import "katex/dist/katex.min.css";
import "highlight.js/styles/tokyo-night-dark.css";
import remarkLuoguFlavor from "remark-luogu-flavor";
// import rehypeHighlight from "rehype-highlight";
import "prismjs/themes/prism-okaidia.min.css";
import remarkLuoguFlavor from "@luogu-discussion-archive/remark-lda-lfm";
import rehypePrism from "rehype-prism-plus";

import { MutableRefObject, useEffect, useRef, useState } from "react";

import { computePosition, shift } from "@floating-ui/dom";

import UserInfo from "@/components/UserInfo";
import UserAvatar from "@/components/UserAvatar";
import useSWR from "swr";
Expand Down Expand Up @@ -40,7 +43,7 @@ function Tooltip({
<div className="bg-body rounded-4 shadow-bssb-sm px-3 py-2x mb-2">
<div className="d-flex me-auto">
<div>
<UserAvatar className="" user={{ id: uid }} decoratorShadow="sm" />
<UserAvatar user={{ id: uid }} decoratorShadow="sm" />
</div>
<div className="ms-2x mt-1x">
<div>
Expand Down Expand Up @@ -168,8 +171,7 @@ export default function Content({
ref={contentRef}
>
<Markdown
// TODO: upgrade the version of rehypeHighlight
rehypePlugins={[rehypeKatex /* , rehypeHighlight */]}
rehypePlugins={[rehypeKatex, rehypePrism]}
remarkPlugins={[
[remarkMath, {}],
[remarkLuoguFlavor, { userLinkPointToLuogu: false }],
Expand Down
Loading

0 comments on commit e4dcf69

Please sign in to comment.