Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@canisminor1990
Copy link
Member

@canisminor1990 canisminor1990 commented Aug 20, 2025

๐Ÿ’ป ๅ˜ๆ›ด็ฑปๅž‹ | Change Type

  • โœจ feat
  • ๐Ÿ› fix
  • โ™ป๏ธ refactor
  • ๐Ÿ’„ style
  • ๐Ÿ”จ chore
  • ๐Ÿ“ docs

๐Ÿ”€ ๅ˜ๆ›ด่ฏดๆ˜Ž | Description of Change

refs: https://streamdown.ai/

๐Ÿ“ ่กฅๅ……ไฟกๆฏ | Additional Information

Summary by Sourcery

Improve markdown rendering and syntax highlighting by splitting content into discrete blocks, auto-completing incomplete markdown tokens, and optimizing render performance through extensive memoization and useCallback stabilizations.

New Features:

  • Add marked library to parse markdown into discrete blocks for individual rendering.
  • Introduce parseIncompleteMarkdown utility to automatically complete or strip incomplete markdown tokens (links, emphasis, code spans, strikethrough, KaTeX).

Enhancements:

  • Memoize and add custom prop comparison to Line, Span, MarkdownRenderer, SyntaxMarkdown, CodeBlock, Markdown, SyntaxHighlighter, and SyntaxMermaid to prevent unnecessary re-renders.
  • Replace useMemo with useCallback for markdown component factories and include a

    fallback wrapper for stability.

  • Enhance MarkdownRenderer with Skeleton fallback, useMarkdownContent escaping, and pass an animated flag into preprocessContent.

@sourcery-ai
Copy link

sourcery-ai bot commented Aug 20, 2025

Reviewer's Guide

This PR refactors several memoized components with custom comparators for render optimization, overhauls the markdown rendering pipeline to split content into blocks with a skeleton fallback, introduces animated-content preprocessing via a new parseIncompleteMarkdown utility, refactors markdown component hooks from useMemo to useCallback for stability, and updates CodeBlock to a streamlined memoized version with simplified rendering logic and adds โ€œmarkedโ€ as a dependency.

Sequence diagram for Markdown block rendering with skeleton fallback

sequenceDiagram
  participant SyntaxMarkdown
  participant MarkdownRenderer
  participant MarkdownHooks
  participant Skeleton
  SyntaxMarkdown->>MarkdownRenderer: For each block, render MarkdownRenderer
  MarkdownRenderer->>MarkdownHooks: Render block content
  MarkdownHooks-->>MarkdownRenderer: If loading, show Skeleton
  MarkdownRenderer-->>SyntaxMarkdown: Rendered block or Skeleton
Loading

Class diagram for updated Markdown rendering components

classDiagram
  class SyntaxMarkdown {
    +parseMarkdownIntoBlocks(markdown: string): string[]
    +render(): JSX.Element[]
  }
  class MarkdownRenderer {
    +render(): JSX.Element
  }
  class CodeBlock {
    +render(): JSX.Element
  }
  SyntaxMarkdown --> MarkdownRenderer
  MarkdownRenderer --> CodeBlock
Loading

Class diagram for new parseIncompleteMarkdown utility

classDiagram
  class parseIncompleteMarkdown {
    +parseIncompleteMarkdown(text: string): string
    +handleIncompleteLinksAndImages(text: string): string
    +handleIncompleteBold(text: string): string
    +handleIncompleteDoubleUnderscoreItalic(text: string): string
    +handleIncompleteSingleAsteriskItalic(text: string): string
    +handleIncompleteSingleUnderscoreItalic(text: string): string
    +handleIncompleteInlineCode(text: string): string
    +handleIncompleteStrikethrough(text: string): string
    +handleIncompleteBlockKatex(text: string): string
    +handleIncompleteInlineKatex(text: string): string
    +handleIncompleteBoldItalic(text: string): string
  }
Loading

Class diagram for updated memoized Highlighter components

classDiagram
  class Line {
    +render(): JSX.Element
  }
  class Span {
    +render(): JSX.Element
  }
  Line --> Span
Loading

File-Level Changes

Change Details Files
Add custom equality checks to memoized components for render optimization
  • Appended comparator functions to memo calls comparing prevProps and nextProps to skip unnecessary updates
  • Wrapped CodeBlock, SyntaxHighlighter, Markdown, SyntaxMermaid, Line, and Span in memo with custom equality
  • Added comparator to MarkdownRenderer and SyntaxMarkdown to compare children and language props
src/Highlighter/SyntaxHighlighter/Line.tsx
src/Highlighter/SyntaxHighlighter/Span.tsx
src/Highlighter/SyntaxHighlighter/index.tsx
src/Markdown/Markdown.tsx
src/Mermaid/SyntaxMermaid/index.tsx
src/Markdown/SyntaxMarkdown/index.tsx
src/Markdown/components/CodeBlock.tsx
Overhaul SyntaxMarkdown rendering pipeline with block parsing and fallback
  • Introduced parseMarkdownIntoBlocks using marked.lexer to split content into raw blocks
  • Updated SyntaxMarkdown to map each block into MarkdownRenderer instances with stable keys
  • Enhanced MarkdownRenderer to use MarkdownHooks, include a Skeleton fallback, and escape content via useMarkdownContent
src/Markdown/SyntaxMarkdown/index.tsx
package.json
Support animated markdown by preprocessing incomplete tokens
  • Extended preprocessContent to accept an animated flag and invoke parseIncompleteMarkdown
  • Added parseIncompleteMarkdown utility handling incomplete links, formatting marks, inline code, strikethrough, and KaTeX
src/hooks/useMarkdown/utils.ts
src/hooks/useMarkdown/parseIncompleteMarkdown.ts
Refactor markdown component hook implementations for stable callbacks
  • Replaced useMemo with useCallback for components (a, img, video, section, br, p, pre) to maintain stable references
  • Introduced a custom paragraph handler to bypass wrappers when children are images
src/hooks/useMarkdown/useMarkdownComponents.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@vercel
Copy link

vercel bot commented Aug 20, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
lobe-ui Ready Ready Preview Comment Aug 20, 2025 3:22pm

@gru-agent
Copy link
Contributor

gru-agent bot commented Aug 20, 2025

TestGru Assignment

Summary

Link CommitId Status Reason
Detail bb17209 โœ… Finished

History Assignment

Files

File Pull Request
src/Highlighter/SyntaxHighlighter/Line.tsx โŒ Failed (The test job failed due to an environmental issue related to ESM/CJS incompatibility in the Vite/Vitest setup, as indicated by the ERR_REQUIRE_ESM error. The test file itself passes lint and compile checks except for one unused '@ts-expect-error' directive, and covers all important scenarios. There is no evidence of a source code bug in Line.tsx or its tests.)
src/hooks/useMarkdown/utils.ts โŒ Failed (The test job failed due to a Vite/Vitest configuration issue, not a source code bug. The error message indicates that Vitest is trying to require an ES module (vite/dist/node/index.js) using require(), which is not supported. This is an environment/configuration problem with how Vitest and Vite are set up, likely related to ESM/CJS compatibility, and not related to the logic in src/hooks/useMarkdown/utils.ts or its tests.)
src/Highlighter/SyntaxHighlighter/Span.tsx โŒ Failed (Something is wrong with the environment: the test runner fails due to an ESM/CommonJS interop issue between Vite and Vitest. The error message indicates that an ES module is being required using CommonJS syntax, which is not supported. This is a configuration or environment problem and not related to the source code or the test code itself. The code passes lint and type checks, and there is no indication of a source code bug.)
src/hooks/useMarkdown/parseIncompleteMarkdown.ts โŒ Failed (Something wrong with the environment, specifically an ESM/CommonJS interop issue. The error message indicates that Vite (required by Vitest) is being loaded as an ES module, but Vitest is trying to require it as CommonJS. This is a tooling/environmental problem and not related to the source code or the test code, both of which pass lint and type checks.)
src/hooks/useMarkdown/useMarkdownComponents.tsx ๐Ÿ› Bug Detected (The source code attempts to export 'memoComponents' at the bottom with 'export { memoComponents };', but 'memoComponents' is defined inside the 'useMarkdownComponents' hook and is not accessible at the module scope. This causes a TypeScript error: 'Cannot find name 'memoComponents'. Did you mean 'Components'?'. This is a source code bug unrelated to the test logic or environment. Job Details)
src/Markdown/components/CodeBlock.tsx โŒ Failed (The test job failed due to environmental issues, specifically the use of jest-dom matchers (e.g., toHaveTextContent, toBeEmptyDOMElement) which are not supported in the current Vitest setup. Additionally, there is a startup error related to ESM/CJS module loading in Vitest and Vite. There is no bug in the source code or test logic itself; the failures are not caused by a code defect.)
src/Markdown/SyntaxMarkdown/index.tsx โŒ Failed (Something wrong with the environment, specifically an ESM/CJS compatibility issue between vite and vitest. The error message indicates that require() of an ES Module is not supported and suggests using dynamic import(). This is a tooling/environment problem and not related to the source code or the test code, which both lint and compile successfully.)
src/Highlighter/SyntaxHighlighter/index.tsx โŒ Failed (The test job failed due to environment/configuration issues, not a source code bug. The main error is a TypeScript error: 'Property 'toBeInTheDocument' does not exist on type 'Assertion''. This is because the '@testing-library/jest-dom' matchers (such as 'toBeInTheDocument') are not available in the test environment. Additionally, there is a startup error related to ESM/CJS interop in the Vite/Vitest configuration. The test code and source code are correct; the failure is due to missing test environment setup and configuration issues.)

Tip

You can @gru-agent and leave your feedback. TestGru will make adjustments based on your input

@lobehubbot
Copy link
Member

๐Ÿ‘ @canisminor1990


Thank you for raising your pull request and contributing to our Community
Please make sure you have followed our contributing guidelines. We will review it as soon as possible.
If you encounter any problems, please feel free to connect with us.
้žๅธธๆ„Ÿ่ฐขๆ‚จๆๅ‡บๆ‹‰ๅ–่ฏทๆฑ‚ๅนถไธบๆˆ‘ไปฌ็š„็คพๅŒบๅšๅ‡บ่ดก็Œฎ๏ผŒ่ฏท็กฎไฟๆ‚จๅทฒ็ป้ตๅพชไบ†ๆˆ‘ไปฌ็š„่ดก็ŒฎๆŒ‡ๅ—๏ผŒๆˆ‘ไปฌไผšๅฐฝๅฟซๅฎกๆŸฅๅฎƒใ€‚
ๅฆ‚ๆžœๆ‚จ้‡ๅˆฐไปปไฝ•้—ฎ้ข˜๏ผŒ่ฏท้šๆ—ถไธŽๆˆ‘ไปฌ่”็ณปใ€‚

@canisminor1990 canisminor1990 changed the title โœจ feat: Update Markdown link Streamdown โœจ feat: Update Markdown as Streamdown Aug 20, 2025
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • Many of the custom memo comparators only check โ€œchildrenโ€ and ignore other props, which can lead to stale renders when those props changeโ€”please update the comparators to include all relevant props or remove them if unnecessary.
  • Splitting the markdown into separate blocks via marked.lexer may break nested or cross-block constructs (like footnotes or multi-line formatting) โ€“ verify that context is preserved or adjust the parsing to handle edge cases.
  • The new parseIncompleteMarkdown utility is very regex-heavy and could impact performance on larger inputsโ€”consider refactoring or memoizing its logic to reduce overhead.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Many of the custom memo comparators only check โ€œchildrenโ€ and ignore other props, which can lead to stale renders when those props changeโ€”please update the comparators to include all relevant props or remove them if unnecessary.
- Splitting the markdown into separate blocks via marked.lexer may break nested or cross-block constructs (like footnotes or multi-line formatting) โ€“ verify that context is preserved or adjust the parsing to handle edge cases.
- The new parseIncompleteMarkdown utility is very regex-heavy and could impact performance on larger inputsโ€”consider refactoring or memoizing its logic to reduce overhead.

## Individual Comments

### Comment 1
<location> `src/hooks/useMarkdown/useMarkdownComponents.tsx:46` </location>
<code_context>
-  const memoSection = useMemo(() => {
-    return (props: any) => <Section showFootnotes={showFootnotes} {...props} />;
-  }, [showFootnotes]);
+  const memeP = useCallback(({ children, ...rest }: any) => {
+    const hasImage = typeof children === 'object';
+    return hasImage ? children : <p {...rest}>{children}</p>;
+  }, []);

</code_context>

<issue_to_address>
The logic for detecting images in memeP may be too broad.

The current check will match arrays and React elements, not just images. Please update the logic to specifically identify image elements to avoid incorrect handling of other content types.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
  const memeP = useCallback(({ children, ...rest }: any) => {
    const hasImage = typeof children === 'object';
    return hasImage ? children : <p {...rest}>{children}</p>;
  }, []);
=======
  const memeP = useCallback(({ children, ...rest }: any) => {
    // Helper to check if a React element is an image
    const isImageElement = (el: any) =>
      el && el.type === 'img';

    let hasImage = false;
    if (Array.isArray(children)) {
      hasImage = children.every(isImageElement);
    } else {
      hasImage = isImageElement(children);
    }

    return hasImage ? children : <p {...rest}>{children}</p>;
  }, []);
>>>>>>> REPLACE

</suggested_fix>

### Comment 2
<location> `src/hooks/useMarkdown/parseIncompleteMarkdown.ts:285` </location>
<code_context>
+
+// Parses markdown text and removes incomplete tokens to prevent partial rendering
+export const parseIncompleteMarkdown = (text: string): string => {
+  if (!text || typeof text !== 'string') {
+    return text;
+  }
+
</code_context>

<issue_to_address>
Returning non-string values from parseIncompleteMarkdown may cause downstream issues.

Downstream consumers may receive non-string values if invalid input is passed. Recommend returning an empty string for invalid input to ensure consistent output type.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
  if (!text || typeof text !== 'string') {
    return text;
  }
=======
  if (!text || typeof text !== 'string') {
    return '';
  }
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `src/Markdown/components/CodeBlock.tsx:43` </location>
<code_context>
+export const CodeBlock = memo<CodeBlockProps>(
</code_context>

<issue_to_address>
Memoization only on children may miss prop changes affecting rendering.

Include all props that impact rendering in the memoization comparator to prevent outdated renders when those props change.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them โœจ
Help me be more useful! Please click ๐Ÿ‘ or ๐Ÿ‘Ž on each comment and I'll use the feedback to improve your reviews.

Comment on lines 46 to 49
const memeP = useCallback(({ children, ...rest }: any) => {
const hasImage = typeof children === 'object';
return hasImage ? children : <p {...rest}>{children}</p>;
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The logic for detecting images in memeP may be too broad.

The current check will match arrays and React elements, not just images. Please update the logic to specifically identify image elements to avoid incorrect handling of other content types.

Suggested change
const memeP = useCallback(({ children, ...rest }: any) => {
const hasImage = typeof children === 'object';
return hasImage ? children : <p {...rest}>{children}</p>;
}, []);
const memeP = useCallback(({ children, ...rest }: any) => {
// Helper to check if a React element is an image
const isImageElement = (el: any) =>
el && el.type === 'img';
let hasImage = false;
if (Array.isArray(children)) {
hasImage = children.every(isImageElement);
} else {
hasImage = isImageElement(children);
}
return hasImage ? children : <p {...rest}>{children}</p>;
}, []);

Comment on lines 285 to 287
if (!text || typeof text !== 'string') {
return text;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Returning non-string values from parseIncompleteMarkdown may cause downstream issues.

Downstream consumers may receive non-string values if invalid input is passed. Recommend returning an empty string for invalid input to ensure consistent output type.

Suggested change
if (!text || typeof text !== 'string') {
return text;
}
if (!text || typeof text !== 'string') {
return '';
}

Comment on lines +43 to +52
export const CodeBlock = memo<CodeBlockProps>(
({ fullFeatured, enableMermaid, highlight, mermaid, children, animated, ...rest }) => {
const code = useCode(children);

if (!code) return;
if (!code) return;

if (enableMermaid && code.lang === 'mermaid')
return (
<PreMermaid fullFeatured={fullFeatured} {...mermaid} {...rest}>
{code.content}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Memoization only on children may miss prop changes affecting rendering.

Include all props that impact rendering in the memoization comparator to prevent outdated renders when those props change.

@canisminor1990 canisminor1990 changed the title โœจ feat: Update Markdown as Streamdown โœจ feat: Update Markdown aslike Streamdown Aug 20, 2025
@pkg-pr-new
Copy link

pkg-pr-new bot commented Aug 20, 2025

Open in StackBlitz

npm i https://pkg.pr.new/lobehub/lobe-ui/@lobehub/ui@388

commit: 054c785

@canisminor1990 canisminor1990 merged commit 3865fa0 into master Aug 20, 2025
6 checks passed
@canisminor1990 canisminor1990 deleted the feat/streamdown branch August 20, 2025 15:22
@lobehubbot
Copy link
Member

โค๏ธ Great PR @canisminor1990 โค๏ธ


The growth of project is inseparable from user feedback and contribution, thanks for your contribution!
้กน็›ฎ็š„ๆˆ้•ฟ็ฆปไธๅผ€็”จๆˆทๅ้ฆˆๅ’Œ่ดก็Œฎ๏ผŒๆ„Ÿ่ฐขๆ‚จ็š„่ดก็Œฎ!

github-actions bot pushed a commit that referenced this pull request Aug 20, 2025
## [Version&nbsp;2.10.0](v2.9.5...v2.10.0)
<sup>Released on **2025-08-20**</sup>

#### โœจ Features

- **misc**: Update Markdown aslike Streamdown.

#### ๐Ÿ› Bug Fixes

- **misc**: Fix markdown emphasis spacing (fixed.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's improved

* **misc**: Update Markdown aslike Streamdown, closes [#388](#388) ([3865fa0](3865fa0))

#### What's fixed

* **misc**: Fix markdown emphasis spacing (fixed, closes [#387](#387) ([bbf5940](bbf5940))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
@lobehubbot
Copy link
Member

๐ŸŽ‰ This PR is included in version 2.10.0 ๐ŸŽ‰

The release is available on:

Your semantic-release bot ๐Ÿ“ฆ๐Ÿš€

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants