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.
- package/README.md +24 -5
- package/android/src/main/AndroidManifest.xml +1 -4
- package/android/src/main/java/com/nitromarkdown/NitroMarkdownPackage.kt +2 -0
- package/lib/commonjs/markdown-stream.js +11 -3
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +4 -0
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/math.js +175 -15
- package/lib/commonjs/renderers/math.js.map +1 -1
- package/lib/commonjs/renderers/paragraph.js +3 -0
- package/lib/commonjs/renderers/paragraph.js.map +1 -1
- package/lib/module/markdown-stream.js +11 -3
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +4 -0
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/math.js +176 -16
- package/lib/module/renderers/math.js.map +1 -1
- package/lib/module/renderers/paragraph.js +3 -0
- package/lib/module/renderers/paragraph.js.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts +2 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts +2 -1
- package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/renderers/math.d.ts.map +1 -1
- package/package.json +8 -3
- package/src/markdown-stream.tsx +13 -4
- package/src/markdown.tsx +4 -0
- package/src/renderers/math.tsx +232 -11
- package/src/renderers/paragraph.tsx +3 -0
package/src/markdown-stream.tsx
CHANGED
|
@@ -39,12 +39,20 @@ const resolveStreamText = ({
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
if (pendingFrom === previousText.length) {
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|
package/src/renderers/math.tsx
CHANGED
|
@@ -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
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
fontCache={false}
|
|
145
|
-
style={{ backgroundColor: "transparent" }}
|
|
352
|
+
<HorizontalMathViewport
|
|
353
|
+
style={styles.mathBlockScroll}
|
|
354
|
+
contentStyle={styles.mathBlockScrollContent}
|
|
146
355
|
>
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
<
|
|
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
|
},
|