react-native-nitro-markdown 0.5.6 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,12 +39,20 @@ const resolveStreamText = ({
39
39
  }
40
40
 
41
41
  if (pendingFrom === previousText.length) {
42
- const appendedChunk = session.getTextRange(pendingFrom, pendingTo);
43
- return `${previousText}${appendedChunk}`;
42
+ try {
43
+ const appendedChunk = session.getTextRange(pendingFrom, pendingTo);
44
+ return `${previousText}${appendedChunk}`;
45
+ } catch {
46
+ return session.getAllText();
47
+ }
44
48
  }
45
49
 
46
50
  if (pendingFrom === 0) {
47
- return session.getTextRange(0, pendingTo);
51
+ try {
52
+ return session.getTextRange(0, pendingTo);
53
+ } catch {
54
+ return session.getAllText();
55
+ }
48
56
  }
49
57
 
50
58
  return session.getAllText();
@@ -56,7 +64,8 @@ export type MarkdownStreamProps = {
56
64
  */
57
65
  session: MarkdownSession;
58
66
  /**
59
- * Throttle UI updates to avoid re-rendering on every token.
67
+ * Throttle UI updates when updateStrategy is "interval".
68
+ * Ignored when updateStrategy is "raf".
60
69
  * Defaults to 50ms, which keeps UI responsive while streaming.
61
70
  */
62
71
  updateIntervalMs?: number;
package/src/markdown.tsx CHANGED
@@ -998,12 +998,16 @@ const getBaseStyles = (theme: MarkdownTheme): BaseStyles => {
998
998
  const createBaseStyles = (theme: MarkdownTheme) =>
999
999
  StyleSheet.create({
1000
1000
  container: {
1001
+ width: "100%",
1002
+ maxWidth: "100%",
1001
1003
  flexShrink: 1,
1002
1004
  },
1003
1005
  virtualizedList: {
1004
1006
  flex: 1,
1005
1007
  },
1006
1008
  document: {
1009
+ width: "100%",
1010
+ maxWidth: "100%",
1007
1011
  flexShrink: 1,
1008
1012
  },
1009
1013
  errorText: {
@@ -1,9 +1,12 @@
1
- import type { FC, ComponentType } from "react";
1
+ import type { FC, ComponentType, ReactNode } from "react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
2
3
  import {
3
4
  View,
4
5
  Text,
6
+ PanResponder,
5
7
  StyleSheet,
6
8
  Platform,
9
+ type LayoutChangeEvent,
7
10
  type StyleProp,
8
11
  type ViewStyle,
9
12
  } from "react-native";
@@ -16,13 +19,25 @@ let MathJaxComponent: ComponentType<{
16
19
  color?: string;
17
20
  fontCache?: boolean;
18
21
  style?: StyleProp<ViewStyle>;
22
+ width?: number;
23
+ height?: number;
19
24
  children?: string;
20
25
  }> | null = null;
26
+ let SvgFromXmlComponent: ComponentType<{
27
+ xml: string;
28
+ width?: number;
29
+ height?: number;
30
+ style?: StyleProp<ViewStyle>;
31
+ }> | null = null;
32
+ let texToSvg: ((textext: string, fontSize?: number) => string) | null = null;
21
33
 
22
34
  try {
23
35
  // eslint-disable-next-line @typescript-eslint/no-require-imports
24
36
  const mathJaxModule = require("react-native-mathjax-svg");
25
37
  MathJaxComponent = mathJaxModule.default || mathJaxModule;
38
+ texToSvg = mathJaxModule.texToSvg ?? null;
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ SvgFromXmlComponent = require("react-native-svg").SvgFromXml;
26
41
  } catch {
27
42
  if (__DEV__) {
28
43
  // eslint-disable-next-line no-console
@@ -40,6 +55,147 @@ type MathInlineProps = {
40
55
  type MathStyles = ReturnType<typeof createMathStyles>;
41
56
 
42
57
  const mathStylesCache = new WeakMap<MarkdownTheme, MathStyles>();
58
+ const SVG_WIDTH_PATTERN = /<svg[^>]*\bwidth="([\d.]+)(?:ex|px)"/i;
59
+ const SVG_HEIGHT_PATTERN = /<svg[^>]*\bheight="([\d.]+)(?:ex|px)"/i;
60
+
61
+ function colorizeSvg(svg: string, color: string | undefined) {
62
+ return color ? svg.replace(/currentColor/gim, color) : svg;
63
+ }
64
+
65
+ function createMathSvg(
66
+ content: string,
67
+ fontSize: number,
68
+ color: string | undefined,
69
+ ) {
70
+ if (!texToSvg) return null;
71
+
72
+ try {
73
+ const xml = colorizeSvg(texToSvg(content, fontSize / 2), color);
74
+ const widthMatch = SVG_WIDTH_PATTERN.exec(xml);
75
+ const heightMatch = SVG_HEIGHT_PATTERN.exec(xml);
76
+ if (!widthMatch || !heightMatch) return null;
77
+
78
+ const width = Number.parseFloat(widthMatch[1]);
79
+ const height = Number.parseFloat(heightMatch[1]);
80
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
81
+
82
+ return { xml, width, height };
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ type HorizontalMathViewportProps = {
89
+ children: ReactNode;
90
+ contentWidth?: number;
91
+ style: StyleProp<ViewStyle>;
92
+ contentStyle: StyleProp<ViewStyle>;
93
+ };
94
+
95
+ const HorizontalMathViewport: FC<HorizontalMathViewportProps> = ({
96
+ children,
97
+ contentWidth,
98
+ style,
99
+ contentStyle,
100
+ }) => {
101
+ const [viewportWidth, setViewportWidth] = useState(0);
102
+ const [measuredContentWidth, setMeasuredContentWidth] = useState(
103
+ contentWidth ?? 0,
104
+ );
105
+ const [offset, setOffset] = useState(0);
106
+ const viewportWidthRef = useRef(0);
107
+ const contentWidthRef = useRef(contentWidth ?? 0);
108
+ const offsetRef = useRef(0);
109
+ const gestureStartOffsetRef = useRef(0);
110
+ const [panHandlers, setPanHandlers] = useState<
111
+ ReturnType<typeof PanResponder.create>["panHandlers"] | null
112
+ >(null);
113
+
114
+ const setClampedOffset = useCallback((nextOffset: number) => {
115
+ const maxOffset = Math.max(
116
+ 0,
117
+ contentWidthRef.current - viewportWidthRef.current,
118
+ );
119
+ const clampedOffset = Math.min(0, Math.max(-maxOffset, nextOffset));
120
+ offsetRef.current = clampedOffset;
121
+ setOffset(clampedOffset);
122
+ }, []);
123
+
124
+ useEffect(() => {
125
+ if (typeof contentWidth !== "number") return;
126
+
127
+ contentWidthRef.current = contentWidth;
128
+ setMeasuredContentWidth(contentWidth);
129
+ setClampedOffset(offsetRef.current);
130
+ }, [contentWidth, setClampedOffset]);
131
+
132
+ const handleViewportLayout = useCallback(
133
+ (event: LayoutChangeEvent) => {
134
+ viewportWidthRef.current = event.nativeEvent.layout.width;
135
+ setViewportWidth(viewportWidthRef.current);
136
+ setClampedOffset(offsetRef.current);
137
+ },
138
+ [setClampedOffset],
139
+ );
140
+
141
+ const handleContentLayout = useCallback(
142
+ (event: LayoutChangeEvent) => {
143
+ if (typeof contentWidth === "number") return;
144
+
145
+ contentWidthRef.current = event.nativeEvent.layout.width;
146
+ setMeasuredContentWidth(contentWidthRef.current);
147
+ setClampedOffset(offsetRef.current);
148
+ },
149
+ [contentWidth, setClampedOffset],
150
+ );
151
+
152
+ useEffect(() => {
153
+ const responder = PanResponder.create({
154
+ onMoveShouldSetPanResponder: (_event, gestureState) => {
155
+ const hasOverflow =
156
+ contentWidthRef.current > viewportWidthRef.current + 1;
157
+ const isHorizontal =
158
+ Math.abs(gestureState.dx) > Math.abs(gestureState.dy) + 4;
159
+ return hasOverflow && isHorizontal;
160
+ },
161
+ onPanResponderGrant: () => {
162
+ gestureStartOffsetRef.current = offsetRef.current;
163
+ },
164
+ onPanResponderMove: (_event, gestureState) => {
165
+ setClampedOffset(gestureStartOffsetRef.current + gestureState.dx);
166
+ },
167
+ onPanResponderTerminationRequest: () => false,
168
+ });
169
+
170
+ setPanHandlers(responder.panHandlers);
171
+ }, [setClampedOffset]);
172
+
173
+ const centeredOffset =
174
+ measuredContentWidth > 0 && viewportWidth > measuredContentWidth
175
+ ? (viewportWidth - measuredContentWidth) / 2
176
+ : 0;
177
+
178
+ return (
179
+ <View
180
+ style={style}
181
+ onLayout={handleViewportLayout}
182
+ pointerEvents="box-only"
183
+ {...(panHandlers ?? {})}
184
+ >
185
+ <View
186
+ style={[
187
+ contentStyle,
188
+ typeof contentWidth === "number" && { width: contentWidth },
189
+ { transform: [{ translateX: centeredOffset + offset }] },
190
+ ]}
191
+ onLayout={handleContentLayout}
192
+ pointerEvents="none"
193
+ >
194
+ {children}
195
+ </View>
196
+ </View>
197
+ );
198
+ };
43
199
 
44
200
  const createMathStyles = (theme: MarkdownTheme) =>
45
201
  StyleSheet.create({
@@ -63,26 +219,49 @@ const createMathStyles = (theme: MarkdownTheme) =>
63
219
  ...(Platform.OS === "android" && { includeFontPadding: false }),
64
220
  },
65
221
  mathBlockContainer: {
222
+ width: "100%",
223
+ maxWidth: "100%",
224
+ alignSelf: "stretch",
66
225
  marginVertical: theme.spacing.m,
67
226
  paddingVertical: theme.spacing.l,
68
227
  paddingHorizontal: theme.spacing.l,
69
228
  backgroundColor: theme.colors.surface,
70
229
  borderRadius: theme.borderRadius.l,
71
- alignItems: "center",
72
230
  borderWidth: 1,
73
231
  borderColor: theme.colors.border,
74
232
  // Ensure we don't collapse if MathJax fails to report size immediately
75
233
  minHeight: 48,
234
+ overflow: "hidden",
235
+ },
236
+ mathBlockScroll: {
237
+ width: "100%",
238
+ alignSelf: "stretch",
239
+ maxWidth: "100%",
240
+ overflow: "hidden",
241
+ },
242
+ mathBlockScrollContent: {
243
+ alignSelf: "flex-start",
244
+ alignItems: "center",
245
+ justifyContent: "center",
246
+ },
247
+ mathBlockSvg: {
248
+ backgroundColor: "transparent",
249
+ },
250
+ mathBlockSvgFrame: {
251
+ flexShrink: 0,
76
252
  },
77
253
  mathBlockFallbackContainer: {
254
+ width: "100%",
255
+ maxWidth: "100%",
256
+ alignSelf: "stretch",
78
257
  marginVertical: theme.spacing.m,
79
258
  paddingVertical: theme.spacing.m,
80
259
  paddingHorizontal: theme.spacing.l,
81
260
  backgroundColor: theme.colors.codeBackground,
82
261
  borderRadius: theme.borderRadius.m,
83
- alignItems: "center",
84
262
  borderWidth: 1,
85
263
  borderColor: theme.colors.border,
264
+ overflow: "hidden",
86
265
  },
87
266
  mathBlockFallback: {
88
267
  fontFamily:
@@ -91,6 +270,7 @@ const createMathStyles = (theme: MarkdownTheme) =>
91
270
  fontSize: theme.fontSizes.m,
92
271
  color: theme.colors.code,
93
272
  textAlign: "center",
273
+ flexShrink: 0,
94
274
  ...(Platform.OS === "android" && { includeFontPadding: false }),
95
275
  },
96
276
  });
@@ -135,24 +315,65 @@ export const MathBlock: FC<MathBlockProps> = ({ content, style }) => {
135
315
 
136
316
  if (!content) return null;
137
317
 
318
+ const displayContent = `\\displaystyle ${content}`;
319
+ const blockSvg =
320
+ SvgFromXmlComponent &&
321
+ createMathSvg(displayContent, theme.fontSizes.l, theme.colors.text);
322
+
323
+ if (blockSvg && SvgFromXmlComponent) {
324
+ return (
325
+ <View style={[styles.mathBlockContainer, style]}>
326
+ <HorizontalMathViewport
327
+ style={styles.mathBlockScroll}
328
+ contentStyle={styles.mathBlockScrollContent}
329
+ contentWidth={blockSvg.width}
330
+ >
331
+ <View
332
+ style={[
333
+ styles.mathBlockSvgFrame,
334
+ { width: blockSvg.width, height: blockSvg.height },
335
+ ]}
336
+ >
337
+ <SvgFromXmlComponent
338
+ xml={blockSvg.xml}
339
+ width={blockSvg.width}
340
+ height={blockSvg.height}
341
+ style={styles.mathBlockSvg}
342
+ />
343
+ </View>
344
+ </HorizontalMathViewport>
345
+ </View>
346
+ );
347
+ }
348
+
138
349
  if (MathJaxComponent) {
139
350
  return (
140
351
  <View style={[styles.mathBlockContainer, style]}>
141
- <MathJaxComponent
142
- fontSize={theme.fontSizes.l}
143
- color={theme.colors.text}
144
- fontCache={false}
145
- style={{ backgroundColor: "transparent" }}
352
+ <HorizontalMathViewport
353
+ style={styles.mathBlockScroll}
354
+ contentStyle={styles.mathBlockScrollContent}
146
355
  >
147
- {`\\displaystyle ${content}`}
148
- </MathJaxComponent>
356
+ <MathJaxComponent
357
+ fontSize={theme.fontSizes.l}
358
+ color={theme.colors.text}
359
+ fontCache={false}
360
+ style={{ backgroundColor: "transparent" }}
361
+ >
362
+ {displayContent}
363
+ </MathJaxComponent>
364
+ </HorizontalMathViewport>
149
365
  </View>
150
366
  );
151
367
  }
152
368
 
153
369
  return (
154
370
  <View style={[styles.mathBlockFallbackContainer, style]}>
155
- <Text style={styles.mathBlockFallback}>{content}</Text>
371
+ <HorizontalMathViewport
372
+ style={styles.mathBlockScroll}
373
+ contentStyle={styles.mathBlockScrollContent}
374
+ >
375
+ <Text style={styles.mathBlockFallback}>{content}</Text>
376
+ </HorizontalMathViewport>
156
377
  </View>
157
378
  );
158
379
  };
@@ -38,8 +38,11 @@ const stylesCache = new WeakMap<MarkdownTheme, ParagraphStyles>();
38
38
  const createStyles = (theme: MarkdownTheme) =>
39
39
  StyleSheet.create({
40
40
  paragraph: {
41
+ width: "100%",
42
+ maxWidth: "100%",
41
43
  flexDirection: "row",
42
44
  flexWrap: "wrap",
45
+ flexShrink: 1,
43
46
  marginBottom: theme.spacing.l,
44
47
  gap: 0,
45
48
  },