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.
- 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.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.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 +7 -2
- package/src/markdown-stream.tsx +2 -1
- package/src/markdown.tsx +4 -0
- package/src/renderers/math.tsx +232 -11
- package/src/renderers/paragraph.tsx +3 -0
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
|
},
|