react-native-nitro-markdown 0.5.6 → 0.5.7

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.
@@ -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
  },