@pagopa/io-app-design-system 6.0.7 → 7.0.0

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.
Files changed (109) hide show
  1. package/lib/commonjs/components/badge/Badge.js +2 -2
  2. package/lib/commonjs/components/badge/Badge.js.map +1 -1
  3. package/lib/commonjs/components/badge/__test__/__snapshots__/badge.test.tsx.snap +2 -2
  4. package/lib/commonjs/components/index.js +30 -19
  5. package/lib/commonjs/components/index.js.map +1 -1
  6. package/lib/commonjs/components/markdown/CodeBlock.js +36 -0
  7. package/lib/commonjs/components/markdown/CodeBlock.js.map +1 -0
  8. package/lib/commonjs/components/markdown/IOMarkdown.js +71 -0
  9. package/lib/commonjs/components/markdown/IOMarkdown.js.map +1 -0
  10. package/lib/commonjs/components/markdown/IOMarkdownLite.js +22 -0
  11. package/lib/commonjs/components/markdown/IOMarkdownLite.js.map +1 -0
  12. package/lib/commonjs/components/markdown/ImageRenderer.js +53 -0
  13. package/lib/commonjs/components/markdown/ImageRenderer.js.map +1 -0
  14. package/lib/commonjs/components/markdown/index.js +20 -0
  15. package/lib/commonjs/components/markdown/index.js.map +1 -0
  16. package/lib/commonjs/components/markdown/parser.js +253 -0
  17. package/lib/commonjs/components/markdown/parser.js.map +1 -0
  18. package/lib/commonjs/components/markdown/rules.js +324 -0
  19. package/lib/commonjs/components/markdown/rules.js.map +1 -0
  20. package/lib/commonjs/components/markdown/types.js +6 -0
  21. package/lib/commonjs/components/markdown/types.js.map +1 -0
  22. package/lib/commonjs/components/markdown/utils.js +113 -0
  23. package/lib/commonjs/components/markdown/utils.js.map +1 -0
  24. package/lib/commonjs/components/modules/__test__/__snapshots__/ModuleNavigationAlt.test.tsx.snap +2 -2
  25. package/lib/commonjs/components/tag/Tag.js +2 -1
  26. package/lib/commonjs/components/tag/Tag.js.map +1 -1
  27. package/lib/commonjs/components/typography/BodySmall.js +6 -3
  28. package/lib/commonjs/components/typography/BodySmall.js.map +1 -1
  29. package/lib/commonjs/context/IOThemeContextProvider.js +4 -3
  30. package/lib/commonjs/context/IOThemeContextProvider.js.map +1 -1
  31. package/lib/commonjs/utils/pipe.js +29 -0
  32. package/lib/commonjs/utils/pipe.js.map +1 -0
  33. package/lib/module/components/badge/Badge.js +2 -2
  34. package/lib/module/components/badge/Badge.js.map +1 -1
  35. package/lib/module/components/badge/__test__/__snapshots__/badge.test.tsx.snap +2 -2
  36. package/lib/module/components/index.js +3 -2
  37. package/lib/module/components/index.js.map +1 -1
  38. package/lib/module/components/markdown/CodeBlock.js +31 -0
  39. package/lib/module/components/markdown/CodeBlock.js.map +1 -0
  40. package/lib/module/components/markdown/IOMarkdown.js +66 -0
  41. package/lib/module/components/markdown/IOMarkdown.js.map +1 -0
  42. package/lib/module/components/markdown/IOMarkdownLite.js +17 -0
  43. package/lib/module/components/markdown/IOMarkdownLite.js.map +1 -0
  44. package/lib/module/components/markdown/ImageRenderer.js +48 -0
  45. package/lib/module/components/markdown/ImageRenderer.js.map +1 -0
  46. package/lib/module/components/markdown/index.js +5 -0
  47. package/lib/module/components/markdown/index.js.map +1 -0
  48. package/lib/module/components/markdown/parser.js +246 -0
  49. package/lib/module/components/markdown/parser.js.map +1 -0
  50. package/lib/module/components/markdown/rules.js +319 -0
  51. package/lib/module/components/markdown/rules.js.map +1 -0
  52. package/lib/module/components/markdown/types.js +4 -0
  53. package/lib/module/components/markdown/types.js.map +1 -0
  54. package/lib/module/components/markdown/utils.js +103 -0
  55. package/lib/module/components/markdown/utils.js.map +1 -0
  56. package/lib/module/components/modules/__test__/__snapshots__/ModuleNavigationAlt.test.tsx.snap +2 -2
  57. package/lib/module/components/tag/Tag.js +2 -1
  58. package/lib/module/components/tag/Tag.js.map +1 -1
  59. package/lib/module/components/typography/BodySmall.js +5 -2
  60. package/lib/module/components/typography/BodySmall.js.map +1 -1
  61. package/lib/module/context/IOThemeContextProvider.js +4 -3
  62. package/lib/module/context/IOThemeContextProvider.js.map +1 -1
  63. package/lib/module/utils/pipe.js +25 -0
  64. package/lib/module/utils/pipe.js.map +1 -0
  65. package/lib/typescript/components/badge/Badge.d.ts.map +1 -1
  66. package/lib/typescript/components/index.d.ts +3 -2
  67. package/lib/typescript/components/index.d.ts.map +1 -1
  68. package/lib/typescript/components/markdown/CodeBlock.d.ts +10 -0
  69. package/lib/typescript/components/markdown/CodeBlock.d.ts.map +1 -0
  70. package/lib/typescript/components/markdown/IOMarkdown.d.ts +34 -0
  71. package/lib/typescript/components/markdown/IOMarkdown.d.ts.map +1 -0
  72. package/lib/typescript/components/markdown/IOMarkdownLite.d.ts +22 -0
  73. package/lib/typescript/components/markdown/IOMarkdownLite.d.ts.map +1 -0
  74. package/lib/typescript/components/markdown/ImageRenderer.d.ts +12 -0
  75. package/lib/typescript/components/markdown/ImageRenderer.d.ts.map +1 -0
  76. package/lib/typescript/components/markdown/index.d.ts +6 -0
  77. package/lib/typescript/components/markdown/index.d.ts.map +1 -0
  78. package/lib/typescript/components/markdown/parser.d.ts +17 -0
  79. package/lib/typescript/components/markdown/parser.d.ts.map +1 -0
  80. package/lib/typescript/components/markdown/rules.d.ts +6 -0
  81. package/lib/typescript/components/markdown/rules.d.ts.map +1 -0
  82. package/lib/typescript/components/markdown/types.d.ts +41 -0
  83. package/lib/typescript/components/markdown/types.d.ts.map +1 -0
  84. package/lib/typescript/components/markdown/utils.d.ts +27 -0
  85. package/lib/typescript/components/markdown/utils.d.ts.map +1 -0
  86. package/lib/typescript/components/tag/Tag.d.ts.map +1 -1
  87. package/lib/typescript/components/typography/BodySmall.d.ts +2 -0
  88. package/lib/typescript/components/typography/BodySmall.d.ts.map +1 -1
  89. package/lib/typescript/context/IOThemeContextProvider.d.ts.map +1 -1
  90. package/lib/typescript/utils/pipe.d.ts +25 -0
  91. package/lib/typescript/utils/pipe.d.ts.map +1 -0
  92. package/package.json +8 -5
  93. package/src/components/badge/Badge.tsx +2 -2
  94. package/src/components/badge/__test__/__snapshots__/badge.test.tsx.snap +2 -2
  95. package/src/components/index.tsx +3 -2
  96. package/src/components/markdown/CodeBlock.tsx +32 -0
  97. package/src/components/markdown/IOMarkdown.tsx +110 -0
  98. package/src/components/markdown/IOMarkdownLite.tsx +27 -0
  99. package/src/components/markdown/ImageRenderer.tsx +52 -0
  100. package/src/components/markdown/index.ts +7 -0
  101. package/src/components/markdown/parser.ts +334 -0
  102. package/src/components/markdown/rules.tsx +366 -0
  103. package/src/components/markdown/types.ts +81 -0
  104. package/src/components/markdown/utils.ts +127 -0
  105. package/src/components/modules/__test__/__snapshots__/ModuleNavigationAlt.test.tsx.snap +2 -2
  106. package/src/components/tag/Tag.tsx +2 -1
  107. package/src/components/typography/BodySmall.tsx +5 -2
  108. package/src/context/IOThemeContextProvider.tsx +12 -4
  109. package/src/utils/pipe.ts +55 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipe.d.ts","sourceRoot":"","sources":["../../../src/utils/pipe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;AACrC,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC;AAC/D,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAC5B,KAAK,EAAE,CAAC,EACR,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EACnB,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,GACnB,EAAE,CAAC;AACN,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAChC,KAAK,EAAE,CAAC,EACR,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EACnB,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EACpB,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,GACnB,EAAE,CAAC;AACN,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EACpC,KAAK,EAAE,CAAC,EACR,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EACnB,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EACpB,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EACpB,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,GACnB,EAAE,CAAC;AACN,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EACxC,KAAK,EAAE,CAAC,EACR,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,EACnB,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EACpB,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EACpB,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EACpB,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,GACnB,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagopa/io-app-design-system",
3
- "version": "6.0.7",
3
+ "version": "7.0.0",
4
4
  "description": "The library defining the core components of the design system of @pagopa/io-app",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -63,12 +63,14 @@
63
63
  "@react-native-community/eslint-config": "^3.0.2",
64
64
  "@react-native/babel-preset": "^0.82.1",
65
65
  "@release-it/conventional-changelog": "^5.0.0",
66
+ "@stylistic/eslint-plugin": "^2.12.1",
66
67
  "@svgr/core": "^8.1.0",
67
68
  "@svgr/plugin-jsx": "^8.1.0",
68
69
  "@svgr/plugin-svgo": "^8.1.0",
69
70
  "@testing-library/react-native": "^13.2.0",
70
71
  "@types/jest": "^29.5.14",
71
- "@types/react": "^19.1.0",
72
+ "@types/markdown-it": "^14.1.2",
73
+ "@types/react": "^19.2.0",
72
74
  "@types/react-test-renderer": "19.1.0",
73
75
  "babel-jest": "^29.7.0",
74
76
  "babel-plugin-module-resolver": "^5.0.2",
@@ -85,10 +87,10 @@
85
87
  "jest": "^29.6.3",
86
88
  "pod-install": "^0.1.0",
87
89
  "prettier": "^2.0.5",
88
- "react": "19.1.0",
89
- "react-native": "0.81.5",
90
+ "react": "19.2.0",
91
+ "react-native": "0.83.4",
90
92
  "react-native-builder-bob": "^0.38.0",
91
- "react-test-renderer": "19.1.0",
93
+ "react-test-renderer": "19.2.0",
92
94
  "release-it": "^15.0.0",
93
95
  "svgo": "^3.0.2",
94
96
  "typescript": "^5.8.3"
@@ -164,6 +166,7 @@
164
166
  },
165
167
  "dependencies": {
166
168
  "auto-changelog": "^2.4.0",
169
+ "markdown-it": "^14.1.0",
167
170
  "react-native-easing-gradient": "^1.1.1",
168
171
  "react-native-gesture-handler": "^2.25.0",
169
172
  "react-native-haptic-feedback": "^2.3.3",
@@ -40,6 +40,7 @@ const styles = StyleSheet.create({
40
40
  flexDirection: "row",
41
41
  alignItems: "center",
42
42
  justifyContent: "center",
43
+ overflow: "hidden",
43
44
  borderCurve: "continuous",
44
45
  ...Platform.select({
45
46
  android: {
@@ -219,8 +220,7 @@ export const Badge = ({
219
220
  style={{
220
221
  alignSelf: "center",
221
222
  textTransform: "uppercase",
222
- letterSpacing: 0.5,
223
- flexShrink: 1
223
+ letterSpacing: 0.5
224
224
  }}
225
225
  >
226
226
  {text}
@@ -10,6 +10,7 @@ exports[`Test Badge Components - Experimental Enabled Badge Snapshot 1`] = `
10
10
  "borderCurve": "continuous",
11
11
  "flexDirection": "row",
12
12
  "justifyContent": "center",
13
+ "overflow": "hidden",
13
14
  },
14
15
  {
15
16
  "borderRadius": 24,
@@ -40,7 +41,6 @@ exports[`Test Badge Components - Experimental Enabled Badge Snapshot 1`] = `
40
41
  },
41
42
  {
42
43
  "alignSelf": "center",
43
- "flexShrink": 1,
44
44
  "letterSpacing": 0.5,
45
45
  "textTransform": "uppercase",
46
46
  },
@@ -62,6 +62,7 @@ exports[`Test Badge Components Badge Snapshot 1`] = `
62
62
  "borderCurve": "continuous",
63
63
  "flexDirection": "row",
64
64
  "justifyContent": "center",
65
+ "overflow": "hidden",
65
66
  },
66
67
  {
67
68
  "borderRadius": 24,
@@ -92,7 +93,6 @@ exports[`Test Badge Components Badge Snapshot 1`] = `
92
93
  },
93
94
  {
94
95
  "alignSelf": "center",
95
- "flexShrink": 1,
96
96
  "letterSpacing": 0.5,
97
97
  "textTransform": "uppercase",
98
98
  },
@@ -8,13 +8,14 @@ export * from "./checkbox";
8
8
  export * from "./claimsSelector";
9
9
  export * from "./codeInput";
10
10
  export * from "./featureInfo";
11
+ export * from "./headers";
11
12
  export * from "./icons";
12
13
  export * from "./image";
13
14
  export * from "./layout";
14
- export * from "./headers";
15
15
  export * from "./listitems";
16
16
  export * from "./loadingSpinner";
17
17
  export * from "./logos";
18
+ export * from "./markdown";
18
19
  export * from "./modules";
19
20
  export * from "./numberpad";
20
21
  export * from "./otpInput";
@@ -27,8 +28,8 @@ export * from "./stepper";
27
28
  export * from "./switch";
28
29
  export * from "./tabs";
29
30
  export * from "./tag";
30
- export * from "./textInput";
31
31
  export * from "./templates";
32
+ export * from "./textInput";
32
33
  export * from "./toast";
33
34
  export * from "./tooltip";
34
35
  export * from "./typography";
@@ -0,0 +1,32 @@
1
+ import { View } from "react-native";
2
+ import { useIOTheme } from "../../context";
3
+ import { IOColors } from "../../core";
4
+ import { BodyMonospace } from "../typography/BodyMonospace";
5
+
6
+ type CodeBlockProps = {
7
+ content: string;
8
+ };
9
+
10
+ /**
11
+ * Theme-aware code block with a subtle background and border,
12
+ * similar to GitHub's fenced code block styling.
13
+ */
14
+ export const CodeBlock = ({ content }: CodeBlockProps) => {
15
+ const theme = useIOTheme();
16
+
17
+ return (
18
+ <View
19
+ style={{
20
+ backgroundColor: IOColors[theme["appBackground-secondary"]],
21
+ borderColor: IOColors[theme["cardBorder-default"]],
22
+ borderWidth: 1,
23
+ borderRadius: 4,
24
+ borderCurve: "continuous",
25
+ paddingHorizontal: 16,
26
+ paddingVertical: 12
27
+ }}
28
+ >
29
+ <BodyMonospace>{content}</BodyMonospace>
30
+ </View>
31
+ );
32
+ };
@@ -0,0 +1,110 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { Linking, type TextStyle, View } from "react-native";
3
+ import { useIOTheme } from "../../context";
4
+ import {
5
+ bodyFontSize,
6
+ bodyLineHeight,
7
+ bodySmallFontSize,
8
+ bodySmallLineHeight
9
+ } from "../typography";
10
+ import { parse } from "./parser";
11
+ import { DEFAULT_RULES } from "./rules";
12
+ import type {
13
+ IOMarkdownRenderRules,
14
+ MarkdownNode,
15
+ MarkdownNodeType,
16
+ RenderContext,
17
+ RenderRule
18
+ } from "./types";
19
+
20
+ export type IOMarkdownProps = {
21
+ /** The markdown string to render */
22
+ content: string;
23
+ /** Override default link press behavior. Default: Linking.openURL(url) */
24
+ onLinkPress?: (url: string) => void;
25
+ /** Paragraph alignment. Default: "auto" */
26
+ textAlign?: TextStyle["textAlign"];
27
+ /** Override default text size */
28
+ small?: boolean;
29
+ /** Test ID for the container View */
30
+ testID?: string;
31
+ /** Node types to disable (parser will skip them entirely) */
32
+ disabledRules?: ReadonlyArray<MarkdownNodeType>;
33
+ /** Override individual render rules */
34
+ rules?: IOMarkdownRenderRules;
35
+ };
36
+
37
+ /**
38
+ * Full-featured markdown component that renders markdown content
39
+ * using design system primitives.
40
+ *
41
+ * @remarks
42
+ * This component is still experimental. Check that it is correctly
43
+ * formatting your text before proceeding to use it.
44
+ *
45
+ * Supports headings, paragraphs, bold, italic, links, lists,
46
+ * blockquotes (as Banner), images, code, horizontal rules, and HTML breaks.
47
+ *
48
+ * Individual node types can be disabled via `disabledRules`, and
49
+ * render rules can be overridden via the `rules` prop.
50
+ */
51
+ export const IOMarkdown = ({
52
+ content,
53
+ onLinkPress,
54
+ textAlign,
55
+ small,
56
+ testID,
57
+ disabledRules,
58
+ rules = {}
59
+ }: IOMarkdownProps) => {
60
+ const theme = useIOTheme();
61
+
62
+ const ast = useMemo(
63
+ () => parse(content, disabledRules),
64
+ [content, disabledRules]
65
+ );
66
+
67
+ const handleLinkPress = useCallback(
68
+ (url: string) => {
69
+ if (onLinkPress) {
70
+ onLinkPress(url);
71
+ } else {
72
+ Linking.openURL(url).catch(() => null);
73
+ }
74
+ },
75
+ [onLinkPress]
76
+ );
77
+
78
+ const context = useMemo<RenderContext>(
79
+ () => ({
80
+ onLinkPress: handleLinkPress,
81
+ linkColor: theme["interactiveElem-default"],
82
+ textAlign: textAlign ?? "auto",
83
+ fontSize: small ? bodySmallFontSize : bodyFontSize,
84
+ lineHeight: small ? bodySmallLineHeight : bodyLineHeight
85
+ }),
86
+ [handleLinkPress, textAlign, small, theme]
87
+ );
88
+
89
+ const mergedRules = useMemo<Record<MarkdownNodeType, RenderRule>>(
90
+ () => ({ ...DEFAULT_RULES, ...rules }),
91
+ [rules]
92
+ );
93
+
94
+ const renderChildren = useCallback(
95
+ (nodes: ReadonlyArray<MarkdownNode>) =>
96
+ nodes.map(node => {
97
+ const rule = mergedRules[node.type];
98
+ return rule ? rule(node, renderChildren, context) : null;
99
+ }),
100
+ [mergedRules, context]
101
+ );
102
+
103
+ const rendered = renderChildren(ast);
104
+
105
+ return (
106
+ <View style={{ gap: 8 }} testID={testID}>
107
+ {rendered}
108
+ </View>
109
+ );
110
+ };
@@ -0,0 +1,27 @@
1
+ import type { TextStyle } from "react-native";
2
+ import { IOMarkdown } from "./IOMarkdown";
3
+ import { LITE_DISABLED_TYPES } from "./parser";
4
+
5
+ export type IOMarkdownLiteProps = {
6
+ /** The markdown string to render */
7
+ content: string;
8
+ /** Override default link press behavior. Default: Linking.openURL(url) */
9
+ onLinkPress?: (url: string) => void;
10
+ /** Paragraph alignment. Default: "auto" */
11
+ textAlign?: TextStyle["textAlign"];
12
+ /** Override default text size */
13
+ small?: boolean;
14
+ /** Test ID for the container View */
15
+ testID?: string;
16
+ };
17
+
18
+ /**
19
+ * Lightweight markdown component supporting only paragraphs, bold, italic,
20
+ * links, and line breaks.
21
+ *
22
+ * This is a thin wrapper around `IOMarkdown` with extra node types (headings,
23
+ * lists, blockquotes, images, code, horizontal rules, and HTML) disabled.
24
+ */
25
+ export const IOMarkdownLite = (props: IOMarkdownLiteProps) => (
26
+ <IOMarkdown {...props} disabledRules={LITE_DISABLED_TYPES} />
27
+ );
@@ -0,0 +1,52 @@
1
+ import { useLayoutEffect, useState } from "react";
2
+ import { Dimensions, Image } from "react-native";
3
+ import { IOVisualCostants } from "../../core";
4
+ import type { MarkdownNode } from "./types";
5
+
6
+ type ImageRendererProps = {
7
+ node: MarkdownNode;
8
+ };
9
+
10
+ /**
11
+ * Stateful component that renders a remote image with auto-sizing.
12
+ * Uses `Image.getSize()` to determine intrinsic dimensions, then
13
+ * constrains width to the available screen width.
14
+ */
15
+ export const ImageRenderer = ({ node }: ImageRendererProps) => {
16
+ const src = node.attributes?.src ?? "";
17
+ const alt = node.attributes?.alt ?? "";
18
+
19
+ const [imageSize, setImageSize] = useState({
20
+ width: 0,
21
+ aspectRatio: 1
22
+ });
23
+
24
+ const screenWidth =
25
+ Dimensions.get("window").width - IOVisualCostants.appMarginDefault * 2;
26
+
27
+ useLayoutEffect(() => {
28
+ if (!src) {
29
+ return;
30
+ }
31
+ Image.getSize(src, (width, height) => {
32
+ const aspectRatio = width / height;
33
+ const constrainedWidth = Math.min(width, screenWidth);
34
+ setImageSize({ width: constrainedWidth, aspectRatio });
35
+ });
36
+ }, [screenWidth, src]);
37
+
38
+ if (!src) {
39
+ return null;
40
+ }
41
+
42
+ return (
43
+ <Image
44
+ key={node.key}
45
+ accessibilityIgnoresInvertColors
46
+ style={imageSize}
47
+ resizeMode="contain"
48
+ accessibilityLabel={alt}
49
+ source={{ uri: src }}
50
+ />
51
+ );
52
+ };
@@ -0,0 +1,7 @@
1
+ export type { IOMarkdownRenderRules, MarkdownNodeType } from "./types";
2
+
3
+ export { IOMarkdown } from "./IOMarkdown";
4
+ export type { IOMarkdownProps } from "./IOMarkdown";
5
+
6
+ export { IOMarkdownLite } from "./IOMarkdownLite";
7
+ export type { IOMarkdownLiteProps } from "./IOMarkdownLite";
@@ -0,0 +1,334 @@
1
+ import MarkdownIt, { Token } from "markdown-it";
2
+ import { pipe } from "../../utils/pipe";
3
+ import { MarkdownNode, MarkdownNodeType } from "./types";
4
+
5
+ /* Two markdown-it instances: lite (no HTML) and full (HTML enabled) */
6
+ const mdLite = MarkdownIt({ html: false, typographer: false, linkify: false });
7
+ const mdFull = MarkdownIt({ html: true, typographer: false, linkify: false });
8
+
9
+ /**
10
+ * Creates a zero-dependency key generator for the markdown AST.
11
+ *
12
+ * These keys are only used as local React render keys, so they do not need
13
+ * cryptographic randomness or an external package: a per-parse incrementing
14
+ * counter is sufficient for our needs.
15
+ */
16
+ const createKeyFactory = () => {
17
+ // eslint-disable-next-line functional/no-let
18
+ let keyCounter = 0;
19
+
20
+ return (prefix: string) => `md_${prefix}_${keyCounter++}`;
21
+ };
22
+
23
+ /**
24
+ * Complete set of all supported node types.
25
+ */
26
+ const ALL_TYPES = new Set<string>([
27
+ /* lite types */
28
+ "heading1",
29
+ "heading2",
30
+ "heading3",
31
+ "heading4",
32
+ "heading5",
33
+ "heading6",
34
+ "paragraph",
35
+ "text",
36
+ "strong",
37
+ "em",
38
+ "link",
39
+ "softbreak",
40
+ "hardbreak",
41
+ /* full types */
42
+ "bullet_list",
43
+ "ordered_list",
44
+ "list_item",
45
+ "blockquote",
46
+ "image",
47
+ "code_inline",
48
+ "fence",
49
+ "hr",
50
+ "html_block",
51
+ "html_inline"
52
+ ]);
53
+
54
+ /**
55
+ * The types disabled when using IOMarkdownLite.
56
+ */
57
+ export const LITE_DISABLED_TYPES: ReadonlyArray<MarkdownNodeType> = [
58
+ "heading1",
59
+ "heading2",
60
+ "heading3",
61
+ "heading4",
62
+ "heading5",
63
+ "heading6",
64
+ "bullet_list",
65
+ "ordered_list",
66
+ "list_item",
67
+ "blockquote",
68
+ "image",
69
+ "code_inline",
70
+ "fence",
71
+ "hr",
72
+ "html_block",
73
+ "html_inline"
74
+ ];
75
+
76
+ /**
77
+ * Maps a markdown-it token type to a MarkdownNodeType.
78
+ * Normalizes `*_open` / `*_close` suffixes and heading tags.
79
+ * Returns undefined for unsupported or disabled types.
80
+ */
81
+ const getNodeType = (
82
+ token: Token,
83
+ enabledTypes: Set<string>
84
+ ): MarkdownNodeType | undefined => {
85
+ const cleanedType = token.type.replace(/_open|_close/g, "");
86
+
87
+ const type =
88
+ cleanedType === "heading"
89
+ ? `${cleanedType}${token.tag.slice(1)}`
90
+ : cleanedType;
91
+
92
+ return enabledTypes.has(type) ? (type as MarkdownNodeType) : undefined;
93
+ };
94
+
95
+ /**
96
+ * Flattens nested inline tokens into the parent token stream.
97
+ * markdown-it wraps inline content in `inline` tokens with children.
98
+ */
99
+ const flattenInline = (tokens: ReadonlyArray<Token>): ReadonlyArray<Token> =>
100
+ tokens.reduce<ReadonlyArray<Token>>((acc, token) => {
101
+ if (
102
+ token.type === "inline" &&
103
+ token.children &&
104
+ token.children.length > 0
105
+ ) {
106
+ return [...acc, ...flattenInline(token.children)];
107
+ }
108
+ return [...acc, token];
109
+ }, []);
110
+
111
+ /**
112
+ * Converts a flat array of tokens into a hierarchical AST,
113
+ * skipping disabled/unsupported token types entirely.
114
+ */
115
+ const tokensToAST = (
116
+ tokens: ReadonlyArray<Token>,
117
+ enabledTypes: Set<string>,
118
+ getKey: (prefix: string) => string
119
+ ): Array<MarkdownNode> => {
120
+ if (!tokens || tokens.length === 0) {
121
+ return [];
122
+ }
123
+
124
+ const parseFrom = (index: number): [Array<MarkdownNode>, number] => {
125
+ if (index >= tokens.length) {
126
+ return [[], index];
127
+ }
128
+
129
+ const token = tokens[index];
130
+ const nodeType = getNodeType(token, enabledTypes);
131
+
132
+ // Closing token — stop and return to caller
133
+ if (token.nesting === -1) {
134
+ return [[], index + 1];
135
+ }
136
+
137
+ // Unsupported / disabled type — skip it
138
+ if (nodeType === undefined) {
139
+ if (token.nesting === 1) {
140
+ // Opening token: skip ahead to matching close
141
+ const findMatchingClose = (pos: number, depth: number): number =>
142
+ pos >= tokens.length || depth === 0
143
+ ? pos
144
+ : findMatchingClose(pos + 1, depth + tokens[pos].nesting);
145
+ return parseFrom(findMatchingClose(index + 1, 1));
146
+ }
147
+ // Self-closing / inline token: skip single token
148
+ return parseFrom(index + 1);
149
+ }
150
+
151
+ // Skip empty text nodes
152
+ if (nodeType === "text" && token.content === "") {
153
+ return parseFrom(index + 1);
154
+ }
155
+
156
+ const attributes = token.attrs?.reduce<Record<string, string>>(
157
+ (prev, [name, value]) => ({ ...prev, [name]: value }),
158
+ {}
159
+ );
160
+
161
+ const node: MarkdownNode = {
162
+ type: nodeType,
163
+ key: getKey(nodeType),
164
+ content: token.content || undefined,
165
+ attributes: attributes || undefined,
166
+ children: [],
167
+ // Preserve ordered flag for lists
168
+ ...(nodeType === "ordered_list" ? { ordered: true } : {}),
169
+ ...(nodeType === "bullet_list" ? { ordered: false } : {}),
170
+ // Preserve image src and alt via attributes
171
+ ...(nodeType === "image"
172
+ ? {
173
+ attributes: {
174
+ ...attributes,
175
+ src: token.attrGet?.("src") ?? attributes?.src ?? "",
176
+ alt: token.content ?? ""
177
+ }
178
+ }
179
+ : {})
180
+ };
181
+
182
+ if (token.nesting === 1) {
183
+ // Opening token — parse children
184
+ const [childNodes, nextIndex] = parseFrom(index + 1);
185
+
186
+ const nodeWithChildren: MarkdownNode = {
187
+ ...node,
188
+ children: childNodes
189
+ };
190
+ const [restNodes, finalIndex] = parseFrom(nextIndex);
191
+ return [[nodeWithChildren, ...restNodes], finalIndex];
192
+ }
193
+
194
+ // Self-closing / inline token (nesting === 0)
195
+ const [restNodes, finalIndex] = parseFrom(index + 1);
196
+ return [[node, ...restNodes], finalIndex];
197
+ };
198
+
199
+ const [nodes] = parseFrom(0);
200
+ return nodes;
201
+ };
202
+
203
+ /**
204
+ * Lifts image nodes out of paragraph containers so they become
205
+ * top-level siblings. markdown-it always wraps images inside
206
+ * paragraphs; this post-processing step ensures the existing
207
+ * `imageRule` is actually invoked during rendering.
208
+ *
209
+ * - Paragraph with **only** image children → replaced by the images.
210
+ * - Paragraph with a **mix** of text and images → split into
211
+ * alternating paragraph (text run) and standalone image nodes.
212
+ * - Paragraphs without images → unchanged.
213
+ */
214
+ const liftImages = (
215
+ nodes: ReadonlyArray<MarkdownNode>,
216
+ getKey: (prefix: string) => string
217
+ ): Array<MarkdownNode> =>
218
+ nodes.flatMap(node => {
219
+ if (node.type !== "paragraph") {
220
+ return [node];
221
+ }
222
+
223
+ const hasImage = node.children.some(c => c.type === "image");
224
+ if (!hasImage) {
225
+ return [node];
226
+ }
227
+
228
+ // Every child is an image → lift them all out
229
+ const allImages = node.children.every(c => c.type === "image");
230
+ if (allImages) {
231
+ // Return images as top-level nodes (they keep their own keys)
232
+ return [...node.children];
233
+ }
234
+
235
+ // Mixed content: split children into text runs and standalone images.
236
+ // A single reduce accumulates finished nodes and the current text run;
237
+ // a trailing text run is flushed after the fold.
238
+ type Acc = {
239
+ readonly result: ReadonlyArray<MarkdownNode>;
240
+ readonly textRun: ReadonlyArray<MarkdownNode>;
241
+ };
242
+
243
+ const wrapTextRun = (run: ReadonlyArray<MarkdownNode>): MarkdownNode => ({
244
+ ...node,
245
+ key: getKey("paragraph"),
246
+ children: run
247
+ });
248
+
249
+ const { result, textRun } = node.children.reduce<Acc>(
250
+ (acc, child) =>
251
+ child.type === "image"
252
+ ? {
253
+ result: [
254
+ ...acc.result,
255
+ ...(acc.textRun.length > 0 ? [wrapTextRun(acc.textRun)] : []),
256
+ child
257
+ ],
258
+ textRun: []
259
+ }
260
+ : { ...acc, textRun: [...acc.textRun, child] },
261
+ { result: [], textRun: [] }
262
+ );
263
+
264
+ return textRun.length > 0 ? [...result, wrapTextRun(textRun)] : [...result];
265
+ });
266
+
267
+ const annotateListDepth = (
268
+ nodes: ReadonlyArray<MarkdownNode>,
269
+ parentListDepth = 0
270
+ ): Array<MarkdownNode> =>
271
+ nodes.map(node => {
272
+ const listDepth = parentListDepth;
273
+ const childListDepth =
274
+ node.type === "bullet_list" || node.type === "ordered_list"
275
+ ? parentListDepth + 1
276
+ : parentListDepth;
277
+
278
+ return {
279
+ ...node,
280
+ listDepth,
281
+ children: annotateListDepth(node.children, childListDepth)
282
+ };
283
+ });
284
+
285
+ /**
286
+ * Computes the enabled types set from the full set minus disabled types.
287
+ */
288
+ const getEnabledTypes = (
289
+ disabledTypes?: ReadonlyArray<string>
290
+ ): Set<string> => {
291
+ if (!disabledTypes || disabledTypes.length === 0) {
292
+ return ALL_TYPES;
293
+ }
294
+ const disabled = new Set(disabledTypes);
295
+ return new Set([...ALL_TYPES].filter(t => !disabled.has(t)));
296
+ };
297
+
298
+ /**
299
+ * Parses a markdown source string into an AST.
300
+ * @param source The markdown string.
301
+ * @param disabledTypes Node types to exclude from parsing.
302
+ * @returns Array of MarkdownNode.
303
+ */
304
+ export const parse = (
305
+ source: string,
306
+ disabledTypes?: ReadonlyArray<MarkdownNodeType>
307
+ ): Array<MarkdownNode> => {
308
+ const enabledTypes = getEnabledTypes(disabledTypes);
309
+ const needsHtml =
310
+ enabledTypes.has("html_block") || enabledTypes.has("html_inline");
311
+ const md = needsHtml ? mdFull : mdLite;
312
+ const getKey = createKeyFactory();
313
+
314
+ return pipe(
315
+ // 1. Tokenize the markdown source using markdown-it
316
+ md.parse(source, {}),
317
+ // 2. Unwrap nested inline tokens into a flat token stream
318
+ flattenInline,
319
+ // 3. Convert the flat token stream into a hierarchical AST
320
+ tokens => tokensToAST(tokens, enabledTypes, getKey),
321
+ // 4. Hoist image nodes out of paragraph wrappers so imageRule is invoked
322
+ nodes => liftImages(nodes, getKey),
323
+ // 5. Drop empty paragraphs left behind by disabled/lifted node types
324
+ nodes => nodes.filter(n => n.type !== "paragraph" || n.children.length > 0),
325
+ // 6. Annotate nodes with their list nesting depth for rendering
326
+ annotateListDepth
327
+ );
328
+ };
329
+
330
+ /**
331
+ * Parses markdown with the lite subset of rules only.
332
+ */
333
+ export const parseLite = (source: string): Array<MarkdownNode> =>
334
+ parse(source, LITE_DISABLED_TYPES);