|
@@ -0,0 +1,180 @@
|
|
|
|
|
+import type {
|
|
|
|
|
+ FC
|
|
|
|
|
+} from "react";
|
|
|
|
|
+import {
|
|
|
|
|
+ Image,
|
|
|
|
|
+ Text as NativeText,
|
|
|
|
|
+ View
|
|
|
|
|
+} from "react-native";
|
|
|
|
|
+import type IMarkdownViewerProps from "./type";
|
|
|
|
|
+import stylesheet from "./stylesheet";
|
|
|
|
|
+import Text from "../text";
|
|
|
|
|
+
|
|
|
|
|
+type MarkdownKeys = "# " |
|
|
|
|
|
+ "## " |
|
|
|
|
|
+ "### " |
|
|
|
|
|
+ "#### " |
|
|
|
|
|
+ "##### " |
|
|
|
|
|
+ "###### " |
|
|
|
|
|
+ "* " |
|
|
|
|
|
+ "- " |
|
|
|
|
|
+ "p";
|
|
|
|
|
+
|
|
|
|
|
+const DEFAULT_VARIANTS: Record<MarkdownKeys, keyof NCoreUIKit.Typography> = {
|
|
|
|
|
+ "# ": "displayLargeSize",
|
|
|
|
|
+ "## ": "displayMediumSize",
|
|
|
|
|
+ "### ": "displaySmallSize",
|
|
|
|
|
+ "#### ": "headlineLargeSize",
|
|
|
|
|
+ "##### ": "headlineMediumSize",
|
|
|
|
|
+ "###### ": "headlineSmallSize",
|
|
|
|
|
+ "* ": "bodyLargeSize",
|
|
|
|
|
+ "- ": "bodyLargeSize",
|
|
|
|
|
+ "p": "bodyMediumSize"
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const parseMarkdown = (rawText: string): Array<{
|
|
|
|
|
+ variant?: keyof NCoreUIKit.Typography;
|
|
|
|
|
+ nativeType: MarkdownKeys | "image";
|
|
|
|
|
+ type: "text" | "bullet" | "image";
|
|
|
|
|
+ content: string;
|
|
|
|
|
+ url?: string;
|
|
|
|
|
+ key: number;
|
|
|
|
|
+}> => {
|
|
|
|
|
+ const lines = rawText.split("\n");
|
|
|
|
|
+
|
|
|
|
|
+ return lines.map((line, index) => {
|
|
|
|
|
+ if (line.startsWith("# ")) return {
|
|
|
|
|
+ content: line.replace("# ", ""),
|
|
|
|
|
+ variant: DEFAULT_VARIANTS["# "],
|
|
|
|
|
+ nativeType: "# ",
|
|
|
|
|
+ type: "text",
|
|
|
|
|
+ key: index
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (line.startsWith("## ")) return {
|
|
|
|
|
+ content: line.replace("## ", ""),
|
|
|
|
|
+ variant: DEFAULT_VARIANTS["## "],
|
|
|
|
|
+ nativeType: "## ",
|
|
|
|
|
+ type: "text",
|
|
|
|
|
+ key: index
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const isStartWithImage = line.startsWith("[");
|
|
|
|
|
+
|
|
|
|
|
+ if(isStartWithImage || line.startsWith("[")) {
|
|
|
|
|
+ const contentMatch = line.match(/\[(.*?)\]/);
|
|
|
|
|
+
|
|
|
|
|
+ const imageContent = (contentMatch && contentMatch[1]) ? contentMatch[1] : "";
|
|
|
|
|
+
|
|
|
|
|
+ const tmpLine = line.replace(/\[.*?\]/g, "");
|
|
|
|
|
+
|
|
|
|
|
+ const urlMatch = tmpLine.match(/\((.*?)\)/);
|
|
|
|
|
+
|
|
|
|
|
+ const imageURL = (urlMatch && urlMatch[1]) ? urlMatch[1] : "";
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: imageContent,
|
|
|
|
|
+ nativeType: "image",
|
|
|
|
|
+ type: "image",
|
|
|
|
|
+ url: imageURL,
|
|
|
|
|
+ key: index
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const isStarList = line.startsWith("* ");
|
|
|
|
|
+
|
|
|
|
|
+ if (isStarList || line.startsWith("- ")) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ variant: DEFAULT_VARIANTS[isStarList ? "* " : "- "],
|
|
|
|
|
+ nativeType: isStarList ? "* " : "- ",
|
|
|
|
|
+ content: line.substring(2),
|
|
|
|
|
+ type: "bullet",
|
|
|
|
|
+ key: index
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ variant: DEFAULT_VARIANTS["p"],
|
|
|
|
|
+ nativeType: "p",
|
|
|
|
|
+ content: line,
|
|
|
|
|
+ type: "text",
|
|
|
|
|
+ key: index
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const MarkdownViewer: FC<IMarkdownViewerProps> = ({
|
|
|
|
|
+ content
|
|
|
|
|
+}) => {
|
|
|
|
|
+ const nodes = parseMarkdown(content);
|
|
|
|
|
+
|
|
|
|
|
+ const renderInlineStyles = (_content: string) => {
|
|
|
|
|
+ const parts = _content.split(/(\*\*.*?\*\*)/g);
|
|
|
|
|
+
|
|
|
|
|
+ return parts.map((part, i) => {
|
|
|
|
|
+ if (part.startsWith("**") && part.endsWith("**")) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <NativeText
|
|
|
|
|
+ key={i}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ fontWeight: "bold"
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {part.replace(/\*\*/g, "")}
|
|
|
|
|
+ </NativeText>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return part;
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return <View
|
|
|
|
|
+ style={[
|
|
|
|
|
+ stylesheet.container
|
|
|
|
|
+ ]}
|
|
|
|
|
+ >
|
|
|
|
|
+ {nodes.map((node) => {
|
|
|
|
|
+ if (!node.content.trim() && node.nativeType === "p") return null;
|
|
|
|
|
+
|
|
|
|
|
+ switch (node.type) {
|
|
|
|
|
+ case "text":
|
|
|
|
|
+ return <Text
|
|
|
|
|
+ variant={node.variant}
|
|
|
|
|
+ key={node.key}
|
|
|
|
|
+ >{renderInlineStyles(node.content)}</Text>;
|
|
|
|
|
+ case "image":
|
|
|
|
|
+ return <Image
|
|
|
|
|
+ alt={node.content}
|
|
|
|
|
+ key={node.key}
|
|
|
|
|
+ source={{
|
|
|
|
|
+ uri: node.url
|
|
|
|
|
+ }}
|
|
|
|
|
+ resizeMode="contain"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: 300,
|
|
|
|
|
+ height: 300
|
|
|
|
|
+ }}
|
|
|
|
|
+ />;
|
|
|
|
|
+ case "bullet":
|
|
|
|
|
+ return (
|
|
|
|
|
+ <View
|
|
|
|
|
+ key={node.key}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ flexDirection: "row",
|
|
|
|
|
+ alignItems: "center"
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Text variant={node.variant}>• </Text><Text variant={node.variant}>{renderInlineStyles(node.content)}</Text>
|
|
|
|
|
+ </View>
|
|
|
|
|
+ );
|
|
|
|
|
+ default:
|
|
|
|
|
+ return <Text
|
|
|
|
|
+ variant={node.variant}
|
|
|
|
|
+ key={node.key}
|
|
|
|
|
+ >{renderInlineStyles(node.content)}</Text>;
|
|
|
|
|
+ }
|
|
|
|
|
+ })}
|
|
|
|
|
+ </View>;
|
|
|
|
|
+};
|
|
|
|
|
+export default MarkdownViewer;
|