Skip to content

Commit 28665f1

Browse files
authored
Merge pull request openRin#519 from FreeL00P/main
fix: 优化 Markdown 渲染与文章摘要展示;文章列表支持md图片+iframe代码不显示至文章列表页+文章详情页&列表页换行无效的问题。
2 parents 6abad19 + b620227 commit 28665f1

7 files changed

Lines changed: 99 additions & 9 deletions

File tree

bun.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
"rehype-sanitize": "^6.0.0",
5656
"remark-gfm": "^4.0.0",
5757
"remark-math": "^6.0.0",
58-
"remark-rehype": "^11.1.0"
58+
"remark-rehype": "^11.1.0",
59+
"remark-breaks": "^4.0.0"
5960
},
6061
"devDependencies": {
6162
"@cloudflare/vite-plugin": "^0.1.1",

client/src/components/feed_card.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function FeedCardImage({ src, variant }: { src: string; variant: FeedCardVariant
3333
return (
3434
<div
3535
className={imageFrameClass}
36-
style={{ aspectRatio }}
36+
style={{ aspectRatio: aspectRatio || '16 / 9' }}
3737
>
3838
{blurhash && !loaded ? (
3939
<canvas
@@ -128,7 +128,7 @@ export function FeedCard({ id, title, avatar, draft, listed, top, summary, hasht
128128
{listed === 0 && <span>{t("unlisted")}</span>}
129129
{top === 1 && <span className="text-theme">{t('article.top.title')}</span>}
130130
</p>
131-
<p className={`${styles.summary} ${activeVariant === "editorial" ? "mt-4 max-w-3xl" : ""}`}>{summary}</p>
131+
<p className={`whitespace-pre-line ${styles.summary} ${activeVariant === "editorial" ? "mt-4 max-w-3xl" : ""}`}>{summary}</p>
132132
{safeHashtags.length > 0 &&
133133
<div className={`flex flex-row flex-wrap justify-start gap-2 ${activeVariant === "editorial" ? "mt-4" : "mt-2 gap-x-2"}`}>
134134
{safeHashtags.map(({ name }, index) => (

client/src/components/markdown.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import gfm from "remark-gfm";
1212
import remarkMermaid from "../remark/remarkMermaid";
1313
import { remarkAlert } from "remark-github-blockquote-alert";
1414
import remarkMath from "remark-math";
15+
import remarkBreaks from "remark-breaks";
1516
import Lightbox, { SlideImage } from "yet-another-react-lightbox";
1617
import Counter from "yet-another-react-lightbox/plugins/counter";
1718
import Download from "yet-another-react-lightbox/plugins/download";
@@ -126,7 +127,7 @@ export function Markdown({ content }: { content: string }) {
126127
const Content = useMemo(() => (
127128
<ReactMarkdown
128129
className="toc-content dark:text-neutral-300"
129-
remarkPlugins={[gfm, remarkMermaid, remarkMath, remarkAlert]}
130+
remarkPlugins={[gfm, remarkMermaid, remarkMath, remarkAlert, remarkBreaks]}
130131
children={content}
131132
rehypePlugins={[rehypeKatex, rehypeRaw]}
132133
components={{
@@ -408,6 +409,22 @@ export function Markdown({ content }: { content: string }) {
408409
});
409410
return <section {...props}>{modifiedChildren}</section>;
410411
},
412+
iframe({ node, src, title, ...props }) {
413+
return (
414+
<div className="my-4 w-full">
415+
<iframe
416+
{...props}
417+
src={src}
418+
title={title || "Embedded content"}
419+
className="w-full rounded-xl border border-black/10 dark:border-white/10"
420+
style={{ minHeight: "400px" }}
421+
loading="lazy"
422+
referrerPolicy="no-referrer"
423+
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
424+
/>
425+
</div>
426+
);
427+
},
411428
div({ children, node, ...props }) {
412429
return <div {...props}>{children}</div>;
413430
},

server/src/services/feed.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { profileAsync } from "../core/server-timing";
55
import { feeds, visits, visitStats } from "../db/schema";
66
import { HyperLogLog } from "../utils/hyperloglog";
77
import { extractImageWithMetadata } from "../utils/image";
8+
import { stripMarkdown } from "../utils/markdown";
89
import { syncFeedAISummaryQueueState } from "./feed-ai-summary";
910
import { bindTagToPost } from "./tag";
1011
import { clearFeedCache } from "./clear-feed-cache";
@@ -94,8 +95,9 @@ export function FeedService(): Hono<{
9495
limit: limit_num + 1,
9596
}))).map(({ content, hashtags, summary, ...other }: any) => {
9697
const avatar = extractImageWithMetadata(content);
98+
const plainText = stripMarkdown(content);
9799
return {
98-
summary: summary.length > 0 ? summary : content.length > 100 ? content.slice(0, 100) : content,
100+
summary: summary.length > 0 ? summary : plainText.length > 100 ? plainText.slice(0, 100) : plainText,
99101
hashtags: hashtags.map(({ hashtag }: any) => hashtag),
100102
avatar,
101103
...other
@@ -309,11 +311,10 @@ export function FeedService(): Hono<{
309311
function formatAndCacheData(feed: any, feedDirection: "previous_feed" | "next_feed") {
310312
if (feed) {
311313
const hashtags_flatten = feed.hashtags.map((f: any) => f.hashtag);
314+
const plainText = stripMarkdown(feed.content);
312315
const summary = feed.summary.length > 0
313316
? feed.summary
314-
: feed.content.length > 50
315-
? feed.content.slice(0, 50)
316-
: feed.content;
317+
: plainText.length > 50 ? plainText.slice(0, 50) : plainText;
317318
const cacheKey = `${feed.id}_${feedDirection}_${id_num}`;
318319
const cacheData = {
319320
id: feed.id,
@@ -531,8 +532,9 @@ export function SearchService(): Hono<{
531532
},
532533
orderBy: [desc(feeds.createdAt), desc(feeds.updatedAt)],
533534
})))).map(({ content, hashtags, summary, ...other }: any) => {
535+
const plainText = stripMarkdown(content);
534536
return {
535-
summary: summary.length > 0 ? summary : content.length > 100 ? content.slice(0, 100) : content,
537+
summary: summary.length > 0 ? summary : plainText.length > 100 ? plainText.slice(0, 100) : plainText,
536538
hashtags: hashtags.map(({ hashtag }: any) => hashtag),
537539
...other
538540
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, expect, it } from 'bun:test';
2+
import { stripMarkdown } from '../markdown';
3+
4+
describe('stripMarkdown', () => {
5+
it('removes media and code from summary text', () => {
6+
const result = stripMarkdown([
7+
'# Title',
8+
'<iframe src="https://example.com/embed"></iframe>',
9+
'```ts',
10+
'const hidden = true;',
11+
'```',
12+
'![cover](https://example.com/cover.png)',
13+
'[Read more](https://example.com)',
14+
].join('\n'));
15+
16+
expect(result).toContain('Title');
17+
expect(result).toContain('Read more');
18+
expect(result).not.toContain('iframe');
19+
expect(result).not.toContain('hidden');
20+
expect(result).not.toContain('cover.png');
21+
});
22+
23+
it('strips task markers before generic list markers', () => {
24+
expect(stripMarkdown('- [x] Done\n- [ ] Todo')).toBe('Done\nTodo');
25+
});
26+
});

server/src/utils/markdown.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export function stripMarkdown(content: string): string {
2+
let text = content;
3+
4+
text = text.replace(/<!--[\s\S]*?-->/g, "");
5+
text = text.replace(/<script[\s\S]*?<\/script>/gi, "");
6+
text = text.replace(/<style[\s\S]*?<\/style>/gi, "");
7+
text = text.replace(/<iframe[\s\S]*?<\/iframe>/gi, "");
8+
text = text.replace(/<pre[\s\S]*?<\/pre>/gi, "");
9+
10+
text = text.replace(/!\[.*?\]\(.*?\)/g, "");
11+
text = text.replace(/\[([^\]]*)\]\(.*?\)/g, "$1");
12+
text = text.replace(/^#{1,6}\s+/gm, "");
13+
text = text.replace(/^>\s+/gm, "");
14+
text = text.replace(/^(\s*)- \[[ x]\]\s+/gm, "$1");
15+
text = text.replace(/^[-*+]\s+/gm, "");
16+
text = text.replace(/^\d+\.\s+/gm, "");
17+
text = text.replace(/`{3}[\s\S]*?`{3}/g, "");
18+
text = text.replace(/`([^`]+)`/g, "$1");
19+
text = text.replace(/\*\*(.+?)\*\*/g, "$1");
20+
text = text.replace(/__(.+?)__/g, "$1");
21+
text = text.replace(/\*(.+?)\*/g, "$1");
22+
text = text.replace(/_(.+?)_/g, "$1");
23+
text = text.replace(/~~(.+?)~~/g, "$1");
24+
text = text.replace(/<[^>]+>/g, "");
25+
text = text.replace(/\[.*?\]\[.*?\]/g, "");
26+
text = text.replace(/^\[.*?\]:\s+.*$/gm, "");
27+
text = text.replace(/^---+$/gm, "");
28+
text = text.replace(/\|\s*[-:]+\s*\|/g, "");
29+
text = text.replace(/\|/g, " ");
30+
text = text.replace(/---+$/gm, "");
31+
text = text.replace(/\$\$[\s\S]*?\$\$/g, "");
32+
text = text.replace(/\$([^$]+)\$/g, "$1");
33+
text = text.replace(/:\w+:/g, "");
34+
text = text.replace(/^> \[!\w+\]/gm, "");
35+
36+
text = text.trim();
37+
38+
return text;
39+
}

0 commit comments

Comments
 (0)