@pagopa/io-app-design-system 6.0.7 → 6.1.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 (103) 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/utils/pipe.js +29 -0
  30. package/lib/commonjs/utils/pipe.js.map +1 -0
  31. package/lib/module/components/badge/Badge.js +2 -2
  32. package/lib/module/components/badge/Badge.js.map +1 -1
  33. package/lib/module/components/badge/__test__/__snapshots__/badge.test.tsx.snap +2 -2
  34. package/lib/module/components/index.js +3 -2
  35. package/lib/module/components/index.js.map +1 -1
  36. package/lib/module/components/markdown/CodeBlock.js +31 -0
  37. package/lib/module/components/markdown/CodeBlock.js.map +1 -0
  38. package/lib/module/components/markdown/IOMarkdown.js +66 -0
  39. package/lib/module/components/markdown/IOMarkdown.js.map +1 -0
  40. package/lib/module/components/markdown/IOMarkdownLite.js +17 -0
  41. package/lib/module/components/markdown/IOMarkdownLite.js.map +1 -0
  42. package/lib/module/components/markdown/ImageRenderer.js +48 -0
  43. package/lib/module/components/markdown/ImageRenderer.js.map +1 -0
  44. package/lib/module/components/markdown/index.js +5 -0
  45. package/lib/module/components/markdown/index.js.map +1 -0
  46. package/lib/module/components/markdown/parser.js +246 -0
  47. package/lib/module/components/markdown/parser.js.map +1 -0
  48. package/lib/module/components/markdown/rules.js +319 -0
  49. package/lib/module/components/markdown/rules.js.map +1 -0
  50. package/lib/module/components/markdown/types.js +4 -0
  51. package/lib/module/components/markdown/types.js.map +1 -0
  52. package/lib/module/components/markdown/utils.js +103 -0
  53. package/lib/module/components/markdown/utils.js.map +1 -0
  54. package/lib/module/components/modules/__test__/__snapshots__/ModuleNavigationAlt.test.tsx.snap +2 -2
  55. package/lib/module/components/tag/Tag.js +2 -1
  56. package/lib/module/components/tag/Tag.js.map +1 -1
  57. package/lib/module/components/typography/BodySmall.js +5 -2
  58. package/lib/module/components/typography/BodySmall.js.map +1 -1
  59. package/lib/module/utils/pipe.js +25 -0
  60. package/lib/module/utils/pipe.js.map +1 -0
  61. package/lib/typescript/components/badge/Badge.d.ts.map +1 -1
  62. package/lib/typescript/components/index.d.ts +3 -2
  63. package/lib/typescript/components/index.d.ts.map +1 -1
  64. package/lib/typescript/components/markdown/CodeBlock.d.ts +10 -0
  65. package/lib/typescript/components/markdown/CodeBlock.d.ts.map +1 -0
  66. package/lib/typescript/components/markdown/IOMarkdown.d.ts +34 -0
  67. package/lib/typescript/components/markdown/IOMarkdown.d.ts.map +1 -0
  68. package/lib/typescript/components/markdown/IOMarkdownLite.d.ts +22 -0
  69. package/lib/typescript/components/markdown/IOMarkdownLite.d.ts.map +1 -0
  70. package/lib/typescript/components/markdown/ImageRenderer.d.ts +12 -0
  71. package/lib/typescript/components/markdown/ImageRenderer.d.ts.map +1 -0
  72. package/lib/typescript/components/markdown/index.d.ts +6 -0
  73. package/lib/typescript/components/markdown/index.d.ts.map +1 -0
  74. package/lib/typescript/components/markdown/parser.d.ts +17 -0
  75. package/lib/typescript/components/markdown/parser.d.ts.map +1 -0
  76. package/lib/typescript/components/markdown/rules.d.ts +6 -0
  77. package/lib/typescript/components/markdown/rules.d.ts.map +1 -0
  78. package/lib/typescript/components/markdown/types.d.ts +41 -0
  79. package/lib/typescript/components/markdown/types.d.ts.map +1 -0
  80. package/lib/typescript/components/markdown/utils.d.ts +27 -0
  81. package/lib/typescript/components/markdown/utils.d.ts.map +1 -0
  82. package/lib/typescript/components/tag/Tag.d.ts.map +1 -1
  83. package/lib/typescript/components/typography/BodySmall.d.ts +2 -0
  84. package/lib/typescript/components/typography/BodySmall.d.ts.map +1 -1
  85. package/lib/typescript/utils/pipe.d.ts +25 -0
  86. package/lib/typescript/utils/pipe.d.ts.map +1 -0
  87. package/package.json +3 -1
  88. package/src/components/badge/Badge.tsx +2 -2
  89. package/src/components/badge/__test__/__snapshots__/badge.test.tsx.snap +2 -2
  90. package/src/components/index.tsx +3 -2
  91. package/src/components/markdown/CodeBlock.tsx +32 -0
  92. package/src/components/markdown/IOMarkdown.tsx +110 -0
  93. package/src/components/markdown/IOMarkdownLite.tsx +27 -0
  94. package/src/components/markdown/ImageRenderer.tsx +52 -0
  95. package/src/components/markdown/index.ts +7 -0
  96. package/src/components/markdown/parser.ts +334 -0
  97. package/src/components/markdown/rules.tsx +366 -0
  98. package/src/components/markdown/types.ts +81 -0
  99. package/src/components/markdown/utils.ts +127 -0
  100. package/src/components/modules/__test__/__snapshots__/ModuleNavigationAlt.test.tsx.snap +2 -2
  101. package/src/components/tag/Tag.tsx +2 -1
  102. package/src/components/typography/BodySmall.tsx +5 -2
  103. package/src/utils/pipe.ts +55 -0
@@ -0,0 +1,366 @@
1
+ import React, { Fragment } from "react";
2
+ import { View } from "react-native";
3
+ import { Banner } from "../banner";
4
+ import { Divider, HSpacer, VSpacer } from "../layout";
5
+ import { Body } from "../typography/Body";
6
+ import { BodyMonospace } from "../typography/BodyMonospace";
7
+ import { H1 } from "../typography/H1";
8
+ import { H2 } from "../typography/H2";
9
+ import { H3 } from "../typography/H3";
10
+ import { H4 } from "../typography/H4";
11
+ import { H5 } from "../typography/H5";
12
+ import { H6 } from "../typography/H6";
13
+ import { IOText } from "../typography/IOText";
14
+ import { CodeBlock } from "./CodeBlock";
15
+ import { ImageRenderer } from "./ImageRenderer";
16
+ import type {
17
+ MarkdownNode,
18
+ MarkdownNodeType,
19
+ RenderContext,
20
+ RenderRule
21
+ } from "./types";
22
+ import {
23
+ collectRawText,
24
+ extractPictogramName,
25
+ getOrderedListMarker,
26
+ getUnorderedListBullet,
27
+ isBrTag,
28
+ stripPictogramPrefix
29
+ } from "./utils";
30
+
31
+ /* ─── Inline flattening (shared between heading and paragraph rendering) ─── */
32
+
33
+ type InlineStyle = {
34
+ bold: boolean;
35
+ italic: boolean;
36
+ link?: string;
37
+ };
38
+
39
+ type StyledSegment = {
40
+ key: string;
41
+ text: string;
42
+ style: InlineStyle;
43
+ };
44
+
45
+ /**
46
+ * Recursively walks inline AST nodes and produces flat styled segments
47
+ * with accumulated bold/italic/link state.
48
+ */
49
+ const flattenInlineNodes = (
50
+ nodes: ReadonlyArray<MarkdownNode>,
51
+ inherited: InlineStyle
52
+ ): Array<StyledSegment> =>
53
+ nodes.reduce<Array<StyledSegment>>((acc, node) => {
54
+ switch (node.type) {
55
+ case "text":
56
+ case "code_inline":
57
+ return [
58
+ ...acc,
59
+ { key: node.key, text: node.content ?? "", style: inherited }
60
+ ];
61
+
62
+ case "softbreak":
63
+ case "hardbreak":
64
+ return [...acc, { key: node.key, text: "\n", style: inherited }];
65
+
66
+ case "strong":
67
+ return [
68
+ ...acc,
69
+ ...flattenInlineNodes(node.children, {
70
+ ...inherited,
71
+ bold: true
72
+ })
73
+ ];
74
+
75
+ case "em":
76
+ return [
77
+ ...acc,
78
+ ...flattenInlineNodes(node.children, {
79
+ ...inherited,
80
+ italic: true
81
+ })
82
+ ];
83
+
84
+ case "link": {
85
+ const href = node.attributes?.href;
86
+ return [
87
+ ...acc,
88
+ ...flattenInlineNodes(node.children, {
89
+ ...inherited,
90
+ link: href
91
+ })
92
+ ];
93
+ }
94
+
95
+ default:
96
+ return acc;
97
+ }
98
+ }, []);
99
+
100
+ /**
101
+ * Renders a single styled segment as either a raw string
102
+ * or an IOText element with the appropriate props.
103
+ */
104
+ const renderSegment = (
105
+ segment: StyledSegment,
106
+ context: RenderContext,
107
+ isCode?: boolean
108
+ ): React.ReactNode => {
109
+ if (isCode) {
110
+ return <BodyMonospace key={segment.key}>{segment.text}</BodyMonospace>;
111
+ }
112
+
113
+ const { bold, italic, link } = segment.style;
114
+
115
+ return (
116
+ <IOText
117
+ key={segment.key}
118
+ {...(bold ? { weight: "Semibold" } : {})}
119
+ {...(italic ? { fontStyle: "italic" } : {})}
120
+ {...(link
121
+ ? {
122
+ color: context.linkColor,
123
+ onPress: () => context.onLinkPress?.(link),
124
+ accessibilityRole: "link" as const,
125
+ textStyle: { textDecorationLine: "underline" as const }
126
+ }
127
+ : {})}
128
+ size={context.fontSize}
129
+ lineHeight={context.lineHeight}
130
+ >
131
+ {segment.text}
132
+ </IOText>
133
+ );
134
+ };
135
+
136
+ /* ─── Heading component map ─── */
137
+
138
+ type HeadingComponent = typeof H1;
139
+
140
+ const headingComponentMap: Record<string, HeadingComponent> = {
141
+ heading1: H1,
142
+ heading2: H2,
143
+ heading3: H3,
144
+ heading4: H4,
145
+ heading5: H5,
146
+ heading6: H6
147
+ };
148
+
149
+ /* ─── Block rendering helpers ─── */
150
+
151
+ /**
152
+ * Renders a paragraph block by flattening inline children
153
+ * into styled segments.
154
+ */
155
+ const renderParagraph = (
156
+ node: MarkdownNode,
157
+ context: RenderContext
158
+ ): React.ReactNode => {
159
+ const segments = flattenInlineNodes(node.children, {
160
+ bold: false,
161
+ italic: false
162
+ });
163
+
164
+ return (
165
+ <Body key={node.key} style={{ textAlign: context.textAlign }}>
166
+ {segments.map(seg => {
167
+ const matchingNode = node.children.find(c => c.key === seg.key);
168
+ const isCode = matchingNode?.type === "code_inline";
169
+ return renderSegment(seg, context, isCode);
170
+ })}
171
+ </Body>
172
+ );
173
+ };
174
+
175
+ /**
176
+ * Creates a render rule for a heading level using the corresponding
177
+ * DS heading component (H1-H6). This ensures headings inherit
178
+ * dynamicTypeRamp, uppercase/letterSpacing (H5), legacy typeface (H6),
179
+ * and theme colors automatically.
180
+ */
181
+ const makeHeadingRule =
182
+ (Heading: HeadingComponent): RenderRule =>
183
+ (node, _renderChildren, context) => {
184
+ const segments = flattenInlineNodes(node.children, {
185
+ bold: false,
186
+ italic: false
187
+ });
188
+
189
+ return (
190
+ <View key={node.key} accessibilityRole="header">
191
+ <Heading style={{ textAlign: context.textAlign }}>
192
+ {segments.map(seg => seg.text)}
193
+ </Heading>
194
+ </View>
195
+ );
196
+ };
197
+
198
+ /* ─── Default render rules ─── */
199
+
200
+ const paragraphRule: RenderRule = (node, _renderChildren, context) =>
201
+ renderParagraph(node, context);
202
+
203
+ const textRule: RenderRule = node => (
204
+ <Fragment key={node.key}>{node.content ?? ""}</Fragment>
205
+ );
206
+
207
+ const strongRule: RenderRule = (node, renderChildren) => (
208
+ <IOText key={node.key} weight="Semibold">
209
+ {renderChildren(node.children)}
210
+ </IOText>
211
+ );
212
+
213
+ const emRule: RenderRule = (node, renderChildren) => (
214
+ <IOText key={node.key} fontStyle="italic">
215
+ {renderChildren(node.children)}
216
+ </IOText>
217
+ );
218
+
219
+ const linkRule: RenderRule = (node, renderChildren, context) => {
220
+ const href = node.attributes?.href;
221
+ return (
222
+ <IOText
223
+ key={node.key}
224
+ color={context.linkColor}
225
+ onPress={href ? () => context.onLinkPress?.(href) : undefined}
226
+ accessibilityRole="link"
227
+ textStyle={{ textDecorationLine: "underline" }}
228
+ >
229
+ {renderChildren(node.children)}
230
+ </IOText>
231
+ );
232
+ };
233
+
234
+ const softbreakRule: RenderRule = node => (
235
+ <Fragment key={node.key}>{"\n"}</Fragment>
236
+ );
237
+
238
+ const hardbreakRule: RenderRule = node => (
239
+ <Fragment key={node.key}>{"\n"}</Fragment>
240
+ );
241
+
242
+ const bulletListRule: RenderRule = (node, renderChildren) => (
243
+ <View
244
+ key={node.key}
245
+ accessible={true}
246
+ accessibilityRole="list"
247
+ style={{ paddingLeft: 12 }}
248
+ >
249
+ <VSpacer size={8} />
250
+ {node.children.map(child => (
251
+ <View key={child.key} style={{ flexDirection: "row" }}>
252
+ <Body>{getUnorderedListBullet(node.listDepth ?? 0)}</Body>
253
+ <HSpacer size={8} />
254
+ <View style={{ flex: 1, flexShrink: 1 }}>
255
+ {renderChildren(child.children)}
256
+ </View>
257
+ </View>
258
+ ))}
259
+ <VSpacer size={8} />
260
+ </View>
261
+ );
262
+
263
+ const orderedListRule: RenderRule = (node, renderChildren) => (
264
+ <View
265
+ key={node.key}
266
+ accessible={true}
267
+ accessibilityRole="list"
268
+ style={{ paddingLeft: 12 }}
269
+ >
270
+ <VSpacer size={8} />
271
+ {node.children.map((child, i) => (
272
+ <View key={child.key} style={{ flexDirection: "row" }}>
273
+ <Body>{getOrderedListMarker(i + 1, node.listDepth ?? 0)}</Body>
274
+ <HSpacer size={8} />
275
+ <View style={{ flex: 1, flexShrink: 1 }}>
276
+ {renderChildren(child.children)}
277
+ </View>
278
+ </View>
279
+ ))}
280
+ <VSpacer size={8} />
281
+ </View>
282
+ );
283
+
284
+ const listItemRule: RenderRule = (node, renderChildren) => (
285
+ <View key={node.key} style={{ flex: 1, flexShrink: 1 }}>
286
+ {renderChildren(node.children)}
287
+ </View>
288
+ );
289
+
290
+ const blockquoteRule: RenderRule = node => {
291
+ const allText = collectRawText(node);
292
+ const pictogramName = extractPictogramName(allText);
293
+
294
+ // Find the first heading child for the banner title
295
+ const headingNode = node.children.find(c => c.type.startsWith("heading"));
296
+ const title = headingNode ? collectRawText(headingNode).trim() : undefined;
297
+
298
+ // Collect content from paragraph children, stripping the pictogram pattern
299
+ const content = node.children
300
+ .filter(c => c.type === "paragraph")
301
+ .map(c => stripPictogramPrefix(collectRawText(c)).trim())
302
+ .filter(Boolean)
303
+ .join("\n");
304
+
305
+ return (
306
+ <Banner
307
+ key={node.key}
308
+ pictogramName={pictogramName}
309
+ color="neutral"
310
+ title={title}
311
+ content={content || undefined}
312
+ />
313
+ );
314
+ };
315
+
316
+ const imageRule: RenderRule = node => (
317
+ <View key={node.key} style={{ marginVertical: 16 }}>
318
+ <ImageRenderer node={node} />
319
+ </View>
320
+ );
321
+
322
+ const codeInlineRule: RenderRule = node => (
323
+ <BodyMonospace key={node.key}>{node.content ?? ""}</BodyMonospace>
324
+ );
325
+
326
+ const fenceRule: RenderRule = node => (
327
+ <CodeBlock key={node.key} content={(node.content ?? "").trimEnd()} />
328
+ );
329
+
330
+ const hrRule: RenderRule = node => <Divider key={node.key} />;
331
+
332
+ const htmlRule: RenderRule = node => {
333
+ if (node.content && isBrTag(node.content)) {
334
+ return <Fragment key={node.key}>{"\n"}</Fragment>;
335
+ }
336
+ return null;
337
+ };
338
+
339
+ /**
340
+ * The complete set of default render rules for all supported node types.
341
+ */
342
+ export const DEFAULT_RULES: Record<MarkdownNodeType, RenderRule> = {
343
+ heading1: makeHeadingRule(headingComponentMap.heading1),
344
+ heading2: makeHeadingRule(headingComponentMap.heading2),
345
+ heading3: makeHeadingRule(headingComponentMap.heading3),
346
+ heading4: makeHeadingRule(headingComponentMap.heading4),
347
+ heading5: makeHeadingRule(headingComponentMap.heading5),
348
+ heading6: makeHeadingRule(headingComponentMap.heading6),
349
+ paragraph: paragraphRule,
350
+ text: textRule,
351
+ strong: strongRule,
352
+ em: emRule,
353
+ link: linkRule,
354
+ softbreak: softbreakRule,
355
+ hardbreak: hardbreakRule,
356
+ bullet_list: bulletListRule,
357
+ ordered_list: orderedListRule,
358
+ list_item: listItemRule,
359
+ blockquote: blockquoteRule,
360
+ image: imageRule,
361
+ code_inline: codeInlineRule,
362
+ fence: fenceRule,
363
+ hr: hrRule,
364
+ html_block: htmlRule,
365
+ html_inline: htmlRule
366
+ };
@@ -0,0 +1,81 @@
1
+ import type React from "react";
2
+ import type { TextStyle } from "react-native";
3
+ import type { IOColors } from "../../core";
4
+
5
+ /**
6
+ * All supported markdown node types.
7
+ */
8
+ export type MarkdownNodeType =
9
+ /* Headings */
10
+ | "heading1"
11
+ | "heading2"
12
+ | "heading3"
13
+ | "heading4"
14
+ | "heading5"
15
+ | "heading6"
16
+ /* Inline */
17
+ | "paragraph"
18
+ | "text"
19
+ | "strong"
20
+ | "em"
21
+ | "link"
22
+ | "softbreak"
23
+ | "hardbreak"
24
+ /* Lists */
25
+ | "bullet_list"
26
+ | "ordered_list"
27
+ | "list_item"
28
+ /* Block-level */
29
+ | "blockquote"
30
+ | "image"
31
+ | "code_inline"
32
+ | "fence"
33
+ | "hr"
34
+ | "html_block"
35
+ | "html_inline";
36
+
37
+ /**
38
+ * A node in the markdown AST.
39
+ */
40
+ export type MarkdownNode = {
41
+ type: MarkdownNodeType;
42
+ key: string;
43
+ content?: string;
44
+ attributes?: Record<string, string>;
45
+ children: ReadonlyArray<MarkdownNode>;
46
+ /** List metadata: whether the list is ordered */
47
+ ordered?: boolean;
48
+ /** Number of ancestor lists wrapping this node */
49
+ listDepth?: number;
50
+ };
51
+
52
+ export type RenderContext = {
53
+ onLinkPress?: (url: string) => void;
54
+ linkColor: IOColors;
55
+ /** Applied to paragraph */
56
+ textAlign: TextStyle["textAlign"];
57
+ /** Applied to paragraph segments */
58
+ fontSize: TextStyle["fontSize"];
59
+ lineHeight: TextStyle["lineHeight"];
60
+ };
61
+
62
+ export type RenderChildrenFn = (
63
+ nodes: ReadonlyArray<MarkdownNode>
64
+ ) => ReadonlyArray<React.ReactNode>;
65
+
66
+ /**
67
+ * A render rule receives a node, a function to recursively render children,
68
+ * and the current render context.
69
+ */
70
+ export type RenderRule = (
71
+ node: MarkdownNode,
72
+ renderChildren: RenderChildrenFn,
73
+ context: RenderContext
74
+ ) => React.ReactNode;
75
+
76
+ /**
77
+ * Partial record of render rules. Only the provided keys override the defaults.
78
+ */
79
+ export type IOMarkdownRenderRules = Partial<
80
+ Record<MarkdownNodeType, RenderRule>
81
+ >;
@@ -0,0 +1,127 @@
1
+ import { IOPictogramsBleed } from "../pictograms";
2
+ import type { MarkdownNode } from "./types";
3
+
4
+ const BULLET_FULL = "\u2022";
5
+ const BULLET_HOLLOW = "\u25E6";
6
+ const BULLET_SQUARE = "\u25B8";
7
+
8
+ const PICTOGRAM_REGEXP = /^\s*\[!(.*?)\]/;
9
+
10
+ const ROMAN_NUMERALS: ReadonlyArray<readonly [number, string]> = [
11
+ [1000, "m"],
12
+ [900, "cm"],
13
+ [500, "d"],
14
+ [400, "cd"],
15
+ [100, "c"],
16
+ [90, "xc"],
17
+ [50, "l"],
18
+ [40, "xl"],
19
+ [10, "x"],
20
+ [9, "ix"],
21
+ [5, "v"],
22
+ [4, "iv"],
23
+ [1, "i"]
24
+ ];
25
+
26
+ /**
27
+ * Returns the bullet glyph used for unordered lists at a given nesting depth.
28
+ */
29
+ export const getUnorderedListBullet = (listDepth: number): string => {
30
+ switch (listDepth % 3) {
31
+ case 0:
32
+ return BULLET_FULL;
33
+ case 1:
34
+ return BULLET_HOLLOW;
35
+ default:
36
+ return BULLET_SQUARE;
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Converts a positive integer to its lowercase Roman numeral representation.
42
+ */
43
+ const toRomanNumeral = (value: number, index = 0): string => {
44
+ const numeral = ROMAN_NUMERALS[index];
45
+
46
+ if (value <= 0 || numeral === undefined) {
47
+ return "";
48
+ }
49
+
50
+ const [arabic, roman] = numeral;
51
+
52
+ if (value >= arabic) {
53
+ // Consume the current numeral and keep using it until the remainder is smaller.
54
+ return `${roman}${toRomanNumeral(value - arabic, index)}`;
55
+ }
56
+
57
+ // Try the next Roman numeral when the current one no longer fits.
58
+ return toRomanNumeral(value, index + 1);
59
+ };
60
+
61
+ /**
62
+ * Converts a positive integer to a lowercase alphabetic sequence (`a`, `b`, `aa`).
63
+ */
64
+ const toAlphabeticMarker = (value: number): string => {
65
+ if (value <= 0) {
66
+ return "";
67
+ }
68
+
69
+ // Convert to a zero-based base-26 index so 1 -> a, 26 -> z, 27 -> aa.
70
+ const normalizedValue = value - 1;
71
+ const prefix = toAlphabeticMarker(Math.floor(normalizedValue / 26));
72
+ const suffix = String.fromCodePoint(97 + (normalizedValue % 26));
73
+
74
+ return `${prefix}${suffix}`;
75
+ };
76
+
77
+ /**
78
+ * Returns the ordered-list marker for a given item index and nesting depth.
79
+ */
80
+ export const getOrderedListMarker = (
81
+ value: number,
82
+ listDepth: number
83
+ ): string => {
84
+ switch (listDepth % 3) {
85
+ case 0:
86
+ return `${value}.`;
87
+ case 1:
88
+ return `${toRomanNumeral(value)}.`;
89
+ default:
90
+ return `${toAlphabeticMarker(value)}.`;
91
+ }
92
+ };
93
+
94
+ /**
95
+ * Extracts a banner pictogram name from a `[!pictogramName]` prefix.
96
+ */
97
+ export const extractPictogramName = (text: string): IOPictogramsBleed => {
98
+ const match = PICTOGRAM_REGEXP.exec(text);
99
+ const value = match?.[1];
100
+ const isValid = value != null && value in IOPictogramsBleed;
101
+ return isValid ? (value as IOPictogramsBleed) : "notification";
102
+ };
103
+
104
+ /**
105
+ * Removes the leading pictogram directive from blockquote content.
106
+ */
107
+ export const stripPictogramPrefix = (text: string): string =>
108
+ text.replace(PICTOGRAM_REGEXP, "");
109
+
110
+ /**
111
+ * Recursively collects plain text content from a Markdown AST node.
112
+ */
113
+ export const collectRawText = (node: MarkdownNode): string => {
114
+ if (node.content) {
115
+ return node.content;
116
+ }
117
+
118
+ return node.children.map(collectRawText).join("");
119
+ };
120
+
121
+ /**
122
+ * Returns true when a raw HTML fragment is a `<br>` tag.
123
+ */
124
+ export const isBrTag = (content: string): boolean => {
125
+ const match = new RegExp(/<([^\s/>]+)\s*\/?>/).exec(content);
126
+ return match?.[1] === "br";
127
+ };
@@ -1095,6 +1095,7 @@ exports[`ModuleNavigationAlt - Snapshot (Experimental Enabled) With Badge (badge
1095
1095
  "borderCurve": "continuous",
1096
1096
  "flexDirection": "row",
1097
1097
  "justifyContent": "center",
1098
+ "overflow": "hidden",
1098
1099
  },
1099
1100
  {
1100
1101
  "borderRadius": 24,
@@ -1125,7 +1126,6 @@ exports[`ModuleNavigationAlt - Snapshot (Experimental Enabled) With Badge (badge
1125
1126
  },
1126
1127
  {
1127
1128
  "alignSelf": "center",
1128
- "flexShrink": 1,
1129
1129
  "letterSpacing": 0.5,
1130
1130
  "textTransform": "uppercase",
1131
1131
  },
@@ -2571,6 +2571,7 @@ exports[`ModuleNavigationAlt - Snapshot With Badge (badge + chevron) 1`] = `
2571
2571
  "borderCurve": "continuous",
2572
2572
  "flexDirection": "row",
2573
2573
  "justifyContent": "center",
2574
+ "overflow": "hidden",
2574
2575
  },
2575
2576
  {
2576
2577
  "borderRadius": 24,
@@ -2601,7 +2602,6 @@ exports[`ModuleNavigationAlt - Snapshot With Badge (badge + chevron) 1`] = `
2601
2602
  },
2602
2603
  {
2603
2604
  "alignSelf": "center",
2604
- "flexShrink": 1,
2605
2605
  "letterSpacing": 0.5,
2606
2606
  "textTransform": "uppercase",
2607
2607
  },
@@ -77,6 +77,7 @@ const styles = StyleSheet.create({
77
77
  textAlignVertical: "center"
78
78
  }
79
79
  }),
80
+ overflow: "hidden",
80
81
  borderWidth: 1,
81
82
  borderCurve: "continuous"
82
83
  },
@@ -88,7 +89,7 @@ const styles = StyleSheet.create({
88
89
  columnGap: IOTagIconMargin
89
90
  },
90
91
  iconWrapper: {
91
- flexShrink: 1
92
+ flexShrink: 0
92
93
  }
93
94
  });
94
95
 
@@ -13,6 +13,9 @@ type BodySmallProps = TypographicStyleProps & {
13
13
  weight?: Extract<IOFontWeight, "Regular" | "Semibold">;
14
14
  } & TypographicStyleAsLinkProps;
15
15
 
16
+ export const bodySmallFontSize = 14;
17
+ export const bodySmallLineHeight = 21;
18
+
16
19
  /**
17
20
  * `BodySmall` typographic style
18
21
  */
@@ -40,8 +43,8 @@ export const BodySmall = forwardRef<View, BodySmallProps>(
40
43
  ...props,
41
44
  dynamicTypeRamp: "footnote" /* iOS only */,
42
45
  weight: customWeight ?? "Regular",
43
- size: 14,
44
- lineHeight: 21,
46
+ size: bodySmallFontSize,
47
+ lineHeight: bodySmallLineHeight,
45
48
  color: customColor ?? defaultColor,
46
49
  ...(asLink
47
50
  ? {
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Chains functions together, passing the result of each function as input to the next.
3
+ * The first argument is the initial value, followed by any number of transformation functions.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * const addOne = (x: number) => x + 1;
8
+ * const double = (x: number) => x * 2;
9
+ * const stringify = (x: number) => `Result: ${x}`;
10
+ *
11
+ * const result = pipe(
12
+ * 10,
13
+ * addOne, // 11
14
+ * double, // 22
15
+ * stringify // "Result: 22"
16
+ * );
17
+ * ```
18
+ */
19
+ export function pipe<T>(value: T): T;
20
+ export function pipe<T, R1>(value: T, fn1: (arg: T) => R1): R1;
21
+ export function pipe<T, R1, R2>(
22
+ value: T,
23
+ fn1: (arg: T) => R1,
24
+ fn2: (arg: R1) => R2
25
+ ): R2;
26
+ export function pipe<T, R1, R2, R3>(
27
+ value: T,
28
+ fn1: (arg: T) => R1,
29
+ fn2: (arg: R1) => R2,
30
+ fn3: (arg: R2) => R3
31
+ ): R3;
32
+ export function pipe<T, R1, R2, R3, R4>(
33
+ value: T,
34
+ fn1: (arg: T) => R1,
35
+ fn2: (arg: R1) => R2,
36
+ fn3: (arg: R2) => R3,
37
+ fn4: (arg: R3) => R4
38
+ ): R4;
39
+ export function pipe<T, R1, R2, R3, R4, R5>(
40
+ value: T,
41
+ fn1: (arg: T) => R1,
42
+ fn2: (arg: R1) => R2,
43
+ fn3: (arg: R2) => R3,
44
+ fn4: (arg: R3) => R4,
45
+ fn5: (arg: R4) => R5
46
+ ): R5;
47
+ export function pipe(
48
+ initialValue: any,
49
+ ...transformers: Array<(arg: any) => any>
50
+ ): any {
51
+ return transformers.reduce(
52
+ (currentValue, transformer) => transformer(currentValue),
53
+ initialValue
54
+ );
55
+ }