From e24697e7a955bbca050cddce3e6c2afb654f96c4 Mon Sep 17 00:00:00 2001 From: Gowtham G Date: Tue, 17 Jun 2025 22:39:17 +0530 Subject: [PATCH] chore: initial work to prevent rerenders --- src/lib/Parser.tsx | 68 ++++++++++++++++--------- src/lib/Renderer.tsx | 119 ++++++++++++++++++++++++++++++++----------- src/lib/types.ts | 59 +++++++++++++++++---- src/utils/hash.ts | 16 ++++++ 4 files changed, 195 insertions(+), 67 deletions(-) create mode 100644 src/utils/hash.ts diff --git a/src/lib/Parser.tsx b/src/lib/Parser.tsx index 872642e1..794fcd8c 100644 --- a/src/lib/Parser.tsx +++ b/src/lib/Parser.tsx @@ -54,21 +54,25 @@ class Parser { this.styles.text, ); - return this.renderer.paragraph(children, this.styles.paragraph); + return this.renderer.paragraph(children, this.styles.paragraph, token); } case "blockquote": { const children = this.parse(token.tokens); - return this.renderer.blockquote(children, this.styles.blockquote); + return this.renderer.blockquote( + children, + this.styles.blockquote, + token, + ); } case "heading": { const styles = this.headingStylesMap[token.depth]; if (this.hasDuplicateTextChildToken(token)) { - return this.renderer.heading(token.text, styles, token.depth); + return this.renderer.heading(token.text, styles, token.depth, token); } const children = this._parse(token.tokens, styles); - return this.renderer.heading(children, styles, token.depth); + return this.renderer.heading(children, styles, token.depth, token); } case "code": { return this.renderer.code( @@ -76,6 +80,7 @@ class Parser { token.lang, this.styles.code, this.styles.em, + token, ); } case "hr": { @@ -104,7 +109,7 @@ class Parser { return this._parseToken(cItem); }); - return this.renderer.listItem(children, this.styles.li); + return this.renderer.listItem(children, this.styles.li, token); }); return this.renderer.list( @@ -139,17 +144,18 @@ class Parser { const href = getValidURL(this.baseUrl, token.href); if (this.hasDuplicateTextChildToken(token)) { - return this.renderer.link(token.text, href, linkStyle); + return this.renderer.link(token.text, href, linkStyle, token); } const children = this._parse(token.tokens, linkStyle); - return this.renderer.link(children, href, linkStyle); + return this.renderer.link(children, href, linkStyle, token); } case "image": { return this.renderer.image( token.href, token.text || token.title, this.styles.image, + token, ); } case "strong": { @@ -158,11 +164,11 @@ class Parser { ...styles, }; if (this.hasDuplicateTextChildToken(token)) { - return this.renderer.strong(token.text, boldStyle); + return this.renderer.strong(token.text, boldStyle, token); } const children = this._parse(token.tokens, boldStyle); - return this.renderer.strong(children, boldStyle); + return this.renderer.strong(children, boldStyle, token); } case "em": { const italicStyle = { @@ -170,17 +176,21 @@ class Parser { ...styles, }; if (this.hasDuplicateTextChildToken(token)) { - return this.renderer.em(token.text, italicStyle); + return this.renderer.em(token.text, italicStyle, token); } const children = this._parse(token.tokens, italicStyle); - return this.renderer.em(children, italicStyle); + return this.renderer.em(children, italicStyle, token); } case "codespan": { - return this.renderer.codespan(decode(token.text), { - ...this.styles.codespan, - ...styles, - }); + return this.renderer.codespan( + decode(token.text), + { + ...this.styles.codespan, + ...styles, + }, + token, + ); } case "br": { return this.renderer.br(); @@ -191,25 +201,33 @@ class Parser { ...styles, }; if (this.hasDuplicateTextChildToken(token)) { - return this.renderer.del(token.text, strikethroughStyle); + return this.renderer.del(token.text, strikethroughStyle, token); } const children = this._parse(token.tokens, strikethroughStyle); - return this.renderer.del(children, strikethroughStyle); + return this.renderer.del(children, strikethroughStyle, token); } case "text": - return this.renderer.text(token.raw, { - ...this.styles.text, - ...styles, - }); + return this.renderer.text( + token.raw, + { + ...this.styles.text, + ...styles, + }, + token, + ); case "html": { console.warn( "react-native-marked: rendering html from markdown is not supported", ); - return this.renderer.html(token.raw, { - ...this.styles.text, - ...styles, - }); + return this.renderer.html( + token.raw, + { + ...this.styles.text, + ...styles, + }, + token, + ); } case "table": { const header = (token as Tokens.Table).header.map((row, i) => diff --git a/src/lib/Renderer.tsx b/src/lib/Renderer.tsx index 47a39841..9b18dab3 100644 --- a/src/lib/Renderer.tsx +++ b/src/lib/Renderer.tsx @@ -19,6 +19,8 @@ import type { RendererInterface } from "./types"; import { getTableWidthArr } from "../utils/table"; import MDSvg from "./../components/MDSvg"; import MDTable from "./../components/MDTable"; +import type { Token } from "marked"; +import { superFastHash } from "../utils/hash"; class Renderer implements RendererInterface { private slugPrefix = "react-native-marked-ele"; @@ -30,16 +32,29 @@ class Renderer implements RendererInterface { this.windowWidth = width; } - paragraph(children: ReactNode[], styles?: ViewStyle): ReactNode { - return this.getViewNode(children, styles); + paragraph( + children: ReactNode[], + styles?: ViewStyle, + token?: Token, + ): ReactNode { + return this.getViewNode(children, styles, token); } - blockquote(children: ReactNode[], styles?: ViewStyle): ReactNode { - return this.getBlockquoteNode(children, styles); + blockquote( + children: ReactNode[], + styles?: ViewStyle, + token?: Token, + ): ReactNode { + return this.getBlockquoteNode(children, styles, token); } - heading(text: string | ReactNode[], styles?: TextStyle): ReactNode { - return this.getTextNode(text, styles); + heading( + text: string | ReactNode[], + styles?: TextStyle, + _depth?: number, + token?: Token, + ): ReactNode { + return this.getTextNode(text, styles, token); } code( @@ -47,11 +62,12 @@ class Renderer implements RendererInterface { _language?: string, containerStyle?: ViewStyle, textStyle?: TextStyle, + token?: Token, ): ReactNode { return ( {/* @@ -59,7 +75,7 @@ class Renderer implements RendererInterface { Error: Cannot add a child that doesn't have a YogaNode to a parent without a measure function! ref: https://github.com/facebook/react-native/issues/18773 */} - {this.getTextNode(text, textStyle)} + {this.getTextNode(text, textStyle, token)} ); } @@ -68,8 +84,12 @@ class Renderer implements RendererInterface { return this.getViewNode(null, styles); } - listItem(children: ReactNode[], styles?: ViewStyle): ReactNode { - return this.getViewNode(children, styles); + listItem( + children: ReactNode[], + styles?: ViewStyle, + token?: Token, + ): ReactNode { + return this.getViewNode(children, styles, token); } list( @@ -100,13 +120,14 @@ class Renderer implements RendererInterface { children: string | ReactNode[], href: string, styles?: TextStyle, + token?: Token, ): ReactNode { return ( @@ -115,40 +136,65 @@ class Renderer implements RendererInterface { ); } - image(uri: string, alt?: string, style?: ImageStyle): ReactNode { - const key = this.getKey(); + image( + uri: string, + alt?: string, + style?: ImageStyle, + token?: Token, + ): ReactNode { + const key = this.getKey(token?.type, token?.raw); if (uri.endsWith(".svg")) { return ; } return ; } - strong(children: string | ReactNode[], styles?: TextStyle): ReactNode { - return this.getTextNode(children, styles); + strong( + children: string | ReactNode[], + styles?: TextStyle, + token?: Token, + ): ReactNode { + return this.getTextNode(children, styles, token); } - em(children: string | ReactNode[], styles?: TextStyle): ReactNode { - return this.getTextNode(children, styles); + em( + children: string | ReactNode[], + styles?: TextStyle, + token?: Token, + ): ReactNode { + return this.getTextNode(children, styles, token); } - codespan(text: string, styles?: TextStyle): ReactNode { - return this.getTextNode(text, styles); + codespan(text: string, styles?: TextStyle, token?: Token): ReactNode { + return this.getTextNode(text, styles, token); } br(): ReactNode { return this.getTextNode("\n", {}); } - del(children: string | ReactNode[], styles?: TextStyle): ReactNode { - return this.getTextNode(children, styles); + del( + children: string | ReactNode[], + styles?: TextStyle, + token?: Token, + ): ReactNode { + return this.getTextNode(children, styles, token); } - text(text: string | ReactNode[], styles?: TextStyle): ReactNode { - return this.getTextNode(text, styles); + text( + text: string | ReactNode[], + styles?: TextStyle, + token?: Token, + ): ReactNode { + return this.getTextNode(text, styles, token); } - html(text: string | ReactNode[], styles?: TextStyle): ReactNode { - return this.getTextNode(text, styles); + html( + text: string | ReactNode[], + styles?: TextStyle, + token?: Token, + ): ReactNode { + return this.getTextNode(text, styles, token); } linkImage( @@ -156,6 +202,7 @@ class Renderer implements RendererInterface { imageUrl: string, alt?: string, style?: ImageStyle, + token?: Token, ): ReactNode { const imageNode = this.image(imageUrl, alt, style); return ( @@ -163,7 +210,7 @@ class Renderer implements RendererInterface { accessibilityRole="link" accessibilityHint="Opens in a new window" onPress={onLinkPress(href)} - key={this.getKey()} + key={this.getKey(token?.type, token?.raw)} > {imageNode} @@ -192,16 +239,24 @@ class Renderer implements RendererInterface { ); } - getKey(): string { - return this.slugger.slug(this.slugPrefix); + getKey(type = "", text = ""): string { + if (!type && !text) return this.slugger.slug(this.slugPrefix); + + const hash = superFastHash(type + text); + return String(hash); } private getTextNode( children: string | ReactNode[], styles?: TextStyle, + token?: Token, ): ReactNode { return ( - + {children} ); @@ -210,9 +265,10 @@ class Renderer implements RendererInterface { private getViewNode( children: ReactNode[] | null, styles?: ViewStyle, + token?: Token, ): ReactNode { return ( - + {children} ); @@ -221,9 +277,10 @@ class Renderer implements RendererInterface { private getBlockquoteNode( children: ReactNode[], styles?: ViewStyle, + token?: Token, ): ReactNode { return ( - + {children} ); diff --git a/src/lib/types.ts b/src/lib/types.ts index e2197013..33211636 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -6,7 +6,7 @@ import type { ImageStyle, } from "react-native"; import type { MarkedStyles, UserTheme } from "./../theme/types"; -import type { Tokenizer } from "marked"; +import type { Token, Tokenizer } from "marked"; export interface ParserOptions { styles?: MarkedStyles; @@ -27,21 +27,31 @@ export interface MarkdownProps extends Partial { export type TableColAlignment = "center" | "left" | "right" | null; export interface RendererInterface { - paragraph(children: ReactNode[], styles?: ViewStyle): ReactNode; - blockquote(children: ReactNode[], styles?: ViewStyle): ReactNode; + paragraph( + children: ReactNode[], + styles?: ViewStyle, + token?: Token, + ): ReactNode; + blockquote( + children: ReactNode[], + styles?: ViewStyle, + token?: Token, + ): ReactNode; heading( text: string | ReactNode[], styles?: TextStyle, depth?: number, + token?: Token, ): ReactNode; code( text: string, language?: string, containerStyle?: ViewStyle, textStyle?: TextStyle, + token?: Token, ): ReactNode; hr(styles?: ViewStyle): ReactNode; - listItem(children: ReactNode[], styles?: ViewStyle): ReactNode; + listItem(children: ReactNode[], styles?: ViewStyle, token?: Token): ReactNode; list( ordered: boolean, li: ReactNode[], @@ -54,20 +64,47 @@ export interface RendererInterface { children: string | ReactNode[], href: string, styles?: TextStyle, + token?: Token, + ): ReactNode; + image( + uri: string, + alt?: string, + style?: ImageStyle, + token?: Token, + ): ReactNode; + strong( + children: string | ReactNode[], + styles?: TextStyle, + token?: Token, + ): ReactNode; + em( + children: string | ReactNode[], + styles?: TextStyle, + token?: Token, ): ReactNode; - image(uri: string, alt?: string, style?: ImageStyle): ReactNode; - strong(children: string | ReactNode[], styles?: TextStyle): ReactNode; - em(children: string | ReactNode[], styles?: TextStyle): ReactNode; - codespan(text: string, styles?: TextStyle): ReactNode; + codespan(text: string, styles?: TextStyle, token?: Token): ReactNode; br(): ReactNode; - del(children: string | ReactNode[], styles?: TextStyle): ReactNode; - text(text: string | ReactNode[], styles?: TextStyle): ReactNode; - html(text: string | ReactNode[], styles?: TextStyle): ReactNode; + del( + children: string | ReactNode[], + styles?: TextStyle, + token?: Token, + ): ReactNode; + text( + text: string | ReactNode[], + styles?: TextStyle, + token?: Token, + ): ReactNode; + html( + text: string | ReactNode[], + styles?: TextStyle, + token?: Token, + ): ReactNode; linkImage( href: string, imageUrl: string, alt?: string, style?: ImageStyle, + token?: Token, ): ReactNode; table( header: ReactNode[][], diff --git a/src/utils/hash.ts b/src/utils/hash.ts new file mode 100644 index 00000000..1513b69f --- /dev/null +++ b/src/utils/hash.ts @@ -0,0 +1,16 @@ +// Paul Hsieh's SuperFastHash +// Ref: https://mojoauth.com/hashing/paul-hsiehs-superfasthash-in-javascript-in-browser/ +const superFastHash = (str: string): number => { + let hash = 0; + let i: number; + let chr: number; + if (str.length === 0) return hash; + for (i = 0; i < str.length; i++) { + chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr; // hash * 31 + chr + hash |= 0; // Convert to 32bit integer + } + return hash >>> 0; // Ensure a positive integer +}; + +export { superFastHash };