index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. import {
  2. useEffect,
  3. useState,
  4. type FC
  5. } from "react";
  6. import {
  7. Text as NativeText,
  8. TouchableOpacity,
  9. Linking,
  10. Image,
  11. View
  12. } from "react-native";
  13. import type IMarkdownViewerProps from "./type";
  14. import type {
  15. MarkdownObject,
  16. TextMarkdownTypes
  17. } from "./type";
  18. import stylesheet, {
  19. useStyles
  20. } from "./stylesheet";
  21. import {
  22. parseMarkdown
  23. } from "./util";
  24. import {
  25. NCoreUIKitTheme
  26. } from "../../core/hooks";
  27. import type ITextProps from "../text/type";
  28. import {
  29. Quote as QuoteIcon
  30. } from "lucide-react-native";
  31. import Text from "../text";
  32. const MarkdownViewer: FC<IMarkdownViewerProps> = ({
  33. blockquoteIconSize = "titleLargeSize",
  34. codeTextColor = "emphasized",
  35. blockquoteIconColor = "mid",
  36. blockquoteTextColor = "mid",
  37. blockquoteContainerProps,
  38. centerContainerProps,
  39. blockquoteTextStyle,
  40. rightContainerProps,
  41. blockquoteIconStyle,
  42. centerMarkdownProps,
  43. leftContainerProps,
  44. rightMarkdownProps,
  45. listContainerProps,
  46. linkContainerProps,
  47. codeContainerProps,
  48. leftMarkdownProps,
  49. linkMarkdownProps,
  50. customTextStyles,
  51. customTextProps,
  52. italicTextProps,
  53. enterTextProps,
  54. boldTextProps,
  55. listIconProps,
  56. codeTextProps,
  57. listTextProps,
  58. italicStyle,
  59. imageProps,
  60. boldStyle,
  61. content,
  62. style,
  63. ...props
  64. }) => {
  65. const {
  66. typography,
  67. radiuses,
  68. colors,
  69. spaces
  70. } = NCoreUIKitTheme.useContext();
  71. const [
  72. containerWidth,
  73. setContainerWidth
  74. ] = useState<null | number>(null);
  75. const {
  76. blockquoteContainer: blockquoteContainerDynamicStyle,
  77. blockquoteIcon: blockquoteIconDynamicStyle,
  78. codeContainer: codeContainerDynamicStyle
  79. } = useStyles({
  80. radiuses,
  81. colors,
  82. spaces
  83. });
  84. const nodes = parseMarkdown(content);
  85. const renderBold = ({
  86. part,
  87. i
  88. }: {
  89. part: string;
  90. i: number;
  91. }) => {
  92. return <NativeText
  93. {...boldTextProps}
  94. key={`bold-${i}`}
  95. style={{
  96. ...stylesheet.bold,
  97. ...boldStyle
  98. }}
  99. >
  100. {part.replace(/\*\*/g, "")}
  101. </NativeText>;
  102. };
  103. const renderItalic = ({
  104. part,
  105. i
  106. }: {
  107. part: string;
  108. i: number;
  109. }) => {
  110. return <NativeText
  111. {...italicTextProps}
  112. key={`italic-${i}`}
  113. style={{
  114. ...stylesheet.italic,
  115. ...italicStyle
  116. }}
  117. >
  118. {part.replace(/__/g, "")}
  119. </NativeText>;
  120. };
  121. const renderInlineStyles = (_content: string) => {
  122. const parts = _content.split(/(\*\*.*?\*\*|__.*?__)/g);
  123. return parts.map((part, i) => {
  124. if (part.startsWith("**") && part.endsWith("**")) return renderBold({
  125. part,
  126. i
  127. });
  128. if (part.startsWith("__") && part.endsWith("__")) return renderItalic({
  129. part,
  130. i
  131. });
  132. return part;
  133. });
  134. };
  135. const renderText = ({
  136. node
  137. }: {
  138. node: MarkdownObject
  139. }) => {
  140. let textProps: ITextProps = {
  141. variant: node.variant
  142. };
  143. if(customTextStyles && customTextStyles[node.nativeType as TextMarkdownTypes]) {
  144. const currentTextStyle = customTextStyles[node.nativeType as TextMarkdownTypes];
  145. textProps.style = currentTextStyle;
  146. }
  147. if(customTextProps && customTextProps[node.nativeType as TextMarkdownTypes]) {
  148. const currentTextProps = customTextProps[node.nativeType as TextMarkdownTypes];
  149. textProps = {
  150. ...textProps,
  151. ...currentTextProps
  152. };
  153. }
  154. return <Text
  155. {...textProps}
  156. key={node.key}
  157. >{renderInlineStyles(node.content)}</Text>;
  158. };
  159. const renderEnter = ({
  160. node
  161. }: {
  162. node: MarkdownObject
  163. }) => {
  164. return <View
  165. {...enterTextProps}
  166. key={node.key}
  167. style={[
  168. stylesheet.enter,
  169. {
  170. height: typography[node.variant as keyof NCoreUIKit.Typography].fontSize
  171. },
  172. enterTextProps?.style
  173. ]}
  174. />;
  175. };
  176. const renderBlockquote = ({
  177. node
  178. }: {
  179. node: MarkdownObject
  180. }) => {
  181. return <View
  182. {...blockquoteContainerProps}
  183. key={node.key}
  184. style={[
  185. blockquoteContainerDynamicStyle,
  186. blockquoteContainerProps?.style
  187. ]}
  188. >
  189. <QuoteIcon
  190. color={colors.content.icon[blockquoteIconColor]}
  191. size={typography[blockquoteIconSize].fontSize}
  192. style={{
  193. ...blockquoteIconDynamicStyle,
  194. ...blockquoteIconStyle
  195. }}
  196. />
  197. <Text
  198. color={blockquoteTextColor}
  199. variant={node.variant}
  200. style={{
  201. ...stylesheet.blockquoteText,
  202. ...blockquoteTextStyle
  203. }}
  204. >
  205. {renderInlineStyles(node.content)}
  206. </Text>
  207. </View>;
  208. };
  209. const renderImage = ({
  210. node
  211. }: {
  212. node: MarkdownObject
  213. }) => {
  214. const [
  215. imgSize,
  216. setImgSize
  217. ] = useState<{
  218. height: number | string;
  219. width: number | string;
  220. isSetted: boolean;
  221. }>({
  222. isSetted: false,
  223. width: "100%",
  224. height: 0
  225. });
  226. useEffect(() => {
  227. if(!imgSize.isSetted && node.size) {
  228. setImgSize({
  229. height: node.size[1] as number | string,
  230. width: node.size[0] as number | string,
  231. isSetted: true
  232. });
  233. }
  234. }, [imgSize]);
  235. const currentWidth = typeof imgSize.width === "number" || imgSize.width.indexOf("%") === -1 || !containerWidth
  236. ? imgSize.width
  237. : (containerWidth * Number(imgSize.width.split("%")[0])) / 100;
  238. const currentHeight = typeof imgSize.height === "number" || imgSize.height.indexOf("%") === -1 || !containerWidth
  239. ? imgSize.height
  240. : (containerWidth * Number(imgSize.height.split("%")[0])) / 100;
  241. return <Image
  242. {...imageProps}
  243. alt={node.content}
  244. key={node.key}
  245. source={{
  246. uri: node.url
  247. }}
  248. resizeMode={node.resizeMode ? node.resizeMode : "contain"}
  249. onLoad={(event) => {
  250. if(!node.size) {
  251. const {
  252. height,
  253. width
  254. } = event.nativeEvent.source;
  255. setImgSize({
  256. isSetted: true,
  257. height,
  258. width
  259. });
  260. }
  261. }}
  262. style={{
  263. ...stylesheet.image,
  264. height: currentHeight,
  265. width: currentWidth,
  266. ...imageProps?.style
  267. }}
  268. />;
  269. };
  270. const renderList = ({
  271. node
  272. }: {
  273. node: MarkdownObject
  274. }) => {
  275. return <View
  276. {...listContainerProps}
  277. key={node.key}
  278. style={[
  279. stylesheet.listContainer,
  280. listContainerProps?.style
  281. ]}
  282. >
  283. <Text
  284. {...listIconProps}
  285. variant={node.variant}
  286. >
  287. •{" "}
  288. </Text>
  289. <Text
  290. {...listTextProps}
  291. variant={node.variant}
  292. >
  293. {renderInlineStyles(node.content)}
  294. </Text>
  295. </View>;
  296. };
  297. const renderCode = ({
  298. node
  299. }: {
  300. node: MarkdownObject
  301. }) => {
  302. return <View
  303. {...codeContainerProps}
  304. key={node.key}
  305. style={[
  306. codeContainerDynamicStyle,
  307. codeContainerProps?.style
  308. ]}
  309. >
  310. <Text
  311. {...codeTextProps}
  312. variant={node.variant}
  313. color={codeTextColor}
  314. >
  315. {node.content}
  316. </Text>
  317. </View>;
  318. };
  319. const renderLink = ({
  320. node
  321. }: {
  322. node: MarkdownObject
  323. }) => {
  324. return <TouchableOpacity
  325. {...linkContainerProps}
  326. key={node.key}
  327. onPress={() => {
  328. Linking.canOpenURL(node.url as string);
  329. }}
  330. >
  331. <MarkdownViewer
  332. {...linkMarkdownProps}
  333. content={node.content}
  334. />
  335. </TouchableOpacity>;
  336. };
  337. const renderCenter = ({
  338. node
  339. }: {
  340. node: MarkdownObject
  341. }) => {
  342. return <View
  343. {...centerContainerProps}
  344. key={node.key}
  345. style={[
  346. stylesheet.centerContainer,
  347. centerContainerProps?.style
  348. ]}
  349. >
  350. <MarkdownViewer
  351. {...centerMarkdownProps}
  352. content={node.content}
  353. />
  354. </View>;
  355. };
  356. const renderLeft = ({
  357. node
  358. }: {
  359. node: MarkdownObject
  360. }) => {
  361. return <View
  362. {...leftContainerProps}
  363. key={node.key}
  364. style={[
  365. stylesheet.leftContainer,
  366. leftContainerProps?.style
  367. ]}
  368. >
  369. <MarkdownViewer
  370. {...leftMarkdownProps}
  371. content={node.content}
  372. />
  373. </View>;
  374. };
  375. const renderRight = ({
  376. node
  377. }: {
  378. node: MarkdownObject
  379. }) => {
  380. return <View
  381. {...rightContainerProps}
  382. key={node.key}
  383. style={[
  384. stylesheet.rightContainer,
  385. rightContainerProps?.style
  386. ]}
  387. >
  388. <MarkdownViewer
  389. {...rightMarkdownProps}
  390. content={node.content}
  391. />
  392. </View>;
  393. };
  394. return <View
  395. {...props}
  396. onLayout={({
  397. nativeEvent
  398. }) => {
  399. setContainerWidth(nativeEvent.layout.width);
  400. }}
  401. style={[
  402. stylesheet.container,
  403. style
  404. ]}
  405. >
  406. {nodes.map((node) => {
  407. if (!node.content.trim() && node.nativeType === "<p>") return null;
  408. switch (node.type) {
  409. case undefined:
  410. return null;
  411. case "text": {
  412. return renderText({
  413. node
  414. });
  415. }
  416. case "space":
  417. return renderEnter({
  418. node
  419. });
  420. case "blockquote": {
  421. return renderBlockquote({
  422. node
  423. });
  424. }
  425. case "right": {
  426. return renderRight({
  427. node
  428. });
  429. }
  430. case "left": {
  431. return renderLeft({
  432. node
  433. });
  434. }
  435. case "center": {
  436. return renderCenter({
  437. node
  438. });
  439. }
  440. case "link": {
  441. return renderLink({
  442. node
  443. });
  444. }
  445. case "code": {
  446. return renderCode({
  447. node
  448. });
  449. }
  450. case "image": {
  451. return renderImage({
  452. node
  453. });
  454. }
  455. case "list":
  456. return renderList({
  457. node
  458. });
  459. default:
  460. return renderText({
  461. node
  462. });
  463. }
  464. })}
  465. </View>;
  466. };
  467. export default MarkdownViewer;