panelui-native 0.28.1 → 0.32.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.
- package/README.md +22 -3
- package/lib/module/components/calendar/index.js +450 -132
- package/lib/module/components/calendar/index.js.map +1 -1
- package/lib/module/components/code-block/highlight.js +363 -0
- package/lib/module/components/code-block/highlight.js.map +1 -0
- package/lib/module/components/code-block/index.js +313 -0
- package/lib/module/components/code-block/index.js.map +1 -0
- package/lib/module/components/date-picker/index.js +8 -1
- package/lib/module/components/date-picker/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +9 -8
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +50 -4
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/menu/index.js +714 -0
- package/lib/module/components/menu/index.js.map +1 -0
- package/lib/module/components/plan/index.js +582 -0
- package/lib/module/components/plan/index.js.map +1 -0
- package/lib/module/components/popover/index.js +35 -4
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/post/index.js +992 -0
- package/lib/module/components/post/index.js.map +1 -0
- package/lib/module/components/radio-group/index.js +13 -8
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/reasoning/index.js +244 -0
- package/lib/module/components/reasoning/index.js.map +1 -0
- package/lib/module/components/response/index.js +296 -0
- package/lib/module/components/response/index.js.map +1 -0
- package/lib/module/components/response/markdown.js +463 -0
- package/lib/module/components/response/markdown.js.map +1 -0
- package/lib/module/components/select/index.js +97 -15
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/sources/index.js +221 -0
- package/lib/module/components/sources/index.js.map +1 -0
- package/lib/module/components/table/index.js +632 -0
- package/lib/module/components/table/index.js.map +1 -0
- package/lib/module/components/task/index.js +300 -0
- package/lib/module/components/task/index.js.map +1 -0
- package/lib/module/components/textarea/index.js +295 -0
- package/lib/module/components/textarea/index.js.map +1 -0
- package/lib/module/components/tooltip/index.js +179 -23
- package/lib/module/components/tooltip/index.js.map +1 -1
- package/lib/module/icons/index.js +339 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +12 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/collapse.js +112 -0
- package/lib/module/primitives/collapse.js.map +1 -0
- package/lib/module/utils/date.js +129 -22
- package/lib/module/utils/date.js.map +1 -1
- package/lib/typescript/src/components/calendar/index.d.ts +79 -5
- package/lib/typescript/src/components/calendar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/code-block/highlight.d.ts +39 -0
- package/lib/typescript/src/components/code-block/highlight.d.ts.map +1 -0
- package/lib/typescript/src/components/code-block/index.d.ts +113 -0
- package/lib/typescript/src/components/code-block/index.d.ts.map +1 -0
- package/lib/typescript/src/components/date-picker/index.d.ts +7 -1
- package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +8 -1
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/menu/index.d.ts +261 -0
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -0
- package/lib/typescript/src/components/plan/index.d.ts +238 -0
- package/lib/typescript/src/components/plan/index.d.ts.map +1 -0
- package/lib/typescript/src/components/popover/index.d.ts +35 -1
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/post/index.d.ts +358 -0
- package/lib/typescript/src/components/post/index.d.ts.map +1 -0
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/reasoning/index.d.ts +97 -0
- package/lib/typescript/src/components/reasoning/index.d.ts.map +1 -0
- package/lib/typescript/src/components/response/index.d.ts +91 -0
- package/lib/typescript/src/components/response/index.d.ts.map +1 -0
- package/lib/typescript/src/components/response/markdown.d.ts +88 -0
- package/lib/typescript/src/components/response/markdown.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts +27 -2
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sources/index.d.ts +85 -0
- package/lib/typescript/src/components/sources/index.d.ts.map +1 -0
- package/lib/typescript/src/components/table/index.d.ts +274 -0
- package/lib/typescript/src/components/table/index.d.ts.map +1 -0
- package/lib/typescript/src/components/task/index.d.ts +103 -0
- package/lib/typescript/src/components/task/index.d.ts.map +1 -0
- package/lib/typescript/src/components/textarea/index.d.ts +175 -0
- package/lib/typescript/src/components/textarea/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tooltip/index.d.ts +63 -8
- package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +54 -1
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +14 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/collapse.d.ts +42 -0
- package/lib/typescript/src/primitives/collapse.d.ts.map +1 -0
- package/lib/typescript/src/utils/date.d.ts +49 -5
- package/lib/typescript/src/utils/date.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/calendar/index.tsx +519 -139
- package/src/components/code-block/highlight.ts +278 -0
- package/src/components/code-block/index.tsx +341 -0
- package/src/components/date-picker/index.tsx +19 -1
- package/src/components/heatmap-chart/index.tsx +9 -9
- package/src/components/map/index.tsx +53 -4
- package/src/components/menu/index.tsx +812 -0
- package/src/components/plan/index.tsx +648 -0
- package/src/components/popover/index.tsx +69 -2
- package/src/components/post/index.tsx +1116 -0
- package/src/components/radio-group/index.tsx +11 -2
- package/src/components/reasoning/index.tsx +297 -0
- package/src/components/response/index.tsx +390 -0
- package/src/components/response/markdown.ts +489 -0
- package/src/components/select/index.tsx +132 -12
- package/src/components/sources/index.tsx +278 -0
- package/src/components/table/index.tsx +747 -0
- package/src/components/task/index.tsx +329 -0
- package/src/components/textarea/index.tsx +381 -0
- package/src/components/tooltip/index.tsx +203 -20
- package/src/icons/index.tsx +264 -1
- package/src/index.ts +131 -0
- package/src/primitives/collapse.tsx +128 -0
- package/src/utils/date.ts +146 -22
- package/theme.css +112 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response — a model's answer, rendered as it arrives.
|
|
3
|
+
*
|
|
4
|
+
* An answer is markdown. It has headings and lists and fenced code in it,
|
|
5
|
+
* because that is what a model writes, and rendering it as one run of plain
|
|
6
|
+
* text throws away the structure the model went to the trouble of producing.
|
|
7
|
+
*
|
|
8
|
+
* So this reads the markdown and renders it through the library's own parts —
|
|
9
|
+
* `Typography` for prose, `CodeBlock` for fences, `Table` for tables. Nothing
|
|
10
|
+
* here draws its own type or its own colours: an answer inside a message bubble
|
|
11
|
+
* should look like the app it is in, not like a document viewer someone
|
|
12
|
+
* embedded.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <Response isStreaming={status === 'streaming'}>{text}</Response>
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* ## Why it is a whole component and not a `<Text>`
|
|
19
|
+
*
|
|
20
|
+
* Because the text is still arriving, and that changes everything about how it
|
|
21
|
+
* has to be read. A token stream hands you every prefix of the final answer, so
|
|
22
|
+
* a renderer sees `**bo`, then `**bol`, then `**bold**` — three documents, two
|
|
23
|
+
* of which have literal asterisks in them. Render each faithfully and the
|
|
24
|
+
* answer flickers between styles on nearly every frame, which is worse than no
|
|
25
|
+
* formatting at all: the eye tracks the flicker instead of the words.
|
|
26
|
+
*
|
|
27
|
+
* `isStreaming` tells the reader to finish an unterminated construct at the end
|
|
28
|
+
* of the input rather than escaping it — an open fence is a code block that is
|
|
29
|
+
* still filling, an open `**` is bold text still being written. The rule it
|
|
30
|
+
* works to is that no word already on screen may disappear when the next token
|
|
31
|
+
* arrives; delimiters may vanish as they are recognised, words never do.
|
|
32
|
+
*
|
|
33
|
+
* ## Where the props come from
|
|
34
|
+
*
|
|
35
|
+
* With the AI SDK, `children` is the text of the assistant message's text parts
|
|
36
|
+
* joined together, and `isStreaming` is `status === 'streaming'`. Join the
|
|
37
|
+
* parts — one `Response` per part renders a heading in one component and the
|
|
38
|
+
* paragraph under it in another, and neither knows about the other.
|
|
39
|
+
*/
|
|
40
|
+
import { memo, useMemo, type ComponentType, type ReactNode } from 'react';
|
|
41
|
+
import { Linking, View, type ViewProps } from 'react-native';
|
|
42
|
+
import { Text } from '../../primitives/text';
|
|
43
|
+
import { cn } from '../../utils/cn';
|
|
44
|
+
import { CodeBlock } from '../code-block';
|
|
45
|
+
import { Table } from '../table';
|
|
46
|
+
import { Typography } from '../typography';
|
|
47
|
+
import {
|
|
48
|
+
parseMarkdown,
|
|
49
|
+
type Align,
|
|
50
|
+
type Block,
|
|
51
|
+
type InlineToken,
|
|
52
|
+
} from './markdown';
|
|
53
|
+
|
|
54
|
+
export type { Block as ResponseBlock, InlineToken as ResponseInline } from './markdown';
|
|
55
|
+
|
|
56
|
+
/** Heading levels below h4 are h4's size; a chat answer has no use for six. */
|
|
57
|
+
const HEADING_TYPE = ['h2', 'h3', 'h4', 'h4', 'h4', 'h4'] as const;
|
|
58
|
+
|
|
59
|
+
/** Schemes a link may open with. Anything else is rendered but not pressable. */
|
|
60
|
+
const DEFAULT_LINK_PREFIXES = ['https://', 'http://', 'mailto:', 'tel:'];
|
|
61
|
+
|
|
62
|
+
export interface ResponseComponents {
|
|
63
|
+
/** Replaces the whole code block — for a runnable snippet, or a diff viewer. */
|
|
64
|
+
code?: ComponentType<{ code: string; language?: string; streaming: boolean }>;
|
|
65
|
+
/** Replaces an image. Nothing is rendered for one by default. */
|
|
66
|
+
image?: ComponentType<{ src: string; alt: string }>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ResponseProps extends Omit<ViewProps, 'children'> {
|
|
70
|
+
className?: string;
|
|
71
|
+
/** The markdown. */
|
|
72
|
+
children?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Whether more is still coming. Finishes an unterminated construct at the end
|
|
75
|
+
* of the text instead of escaping it, so the answer does not flicker between
|
|
76
|
+
* styles as its delimiters arrive.
|
|
77
|
+
*/
|
|
78
|
+
isStreaming?: boolean;
|
|
79
|
+
/** Turns off speculative completion entirely, even while streaming. */
|
|
80
|
+
parseIncompleteMarkdown?: boolean;
|
|
81
|
+
/** What a link does. Opens it with the system handler by default. */
|
|
82
|
+
onLinkPress?: (href: string) => void;
|
|
83
|
+
/**
|
|
84
|
+
* Schemes a link is allowed to open. A model can write any URL it likes, and
|
|
85
|
+
* an answer is not a trusted document — so the default list is the four that
|
|
86
|
+
* cannot do anything but navigate.
|
|
87
|
+
*/
|
|
88
|
+
allowedLinkPrefixes?: string[];
|
|
89
|
+
/** Swap out how a block is drawn. */
|
|
90
|
+
components?: ResponseComponents;
|
|
91
|
+
/** Line numbers in fenced code. */
|
|
92
|
+
showLineNumbers?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ResponseRoot({
|
|
96
|
+
className,
|
|
97
|
+
children = '',
|
|
98
|
+
isStreaming = false,
|
|
99
|
+
parseIncompleteMarkdown = true,
|
|
100
|
+
onLinkPress,
|
|
101
|
+
allowedLinkPrefixes = DEFAULT_LINK_PREFIXES,
|
|
102
|
+
components,
|
|
103
|
+
showLineNumbers = false,
|
|
104
|
+
...props
|
|
105
|
+
}: ResponseProps) {
|
|
106
|
+
const speculate = isStreaming && parseIncompleteMarkdown;
|
|
107
|
+
const blocks = useMemo(
|
|
108
|
+
() => parseMarkdown(children, speculate),
|
|
109
|
+
[children, speculate]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const context: RenderContext = {
|
|
113
|
+
streaming: isStreaming,
|
|
114
|
+
onLinkPress,
|
|
115
|
+
allowedLinkPrefixes,
|
|
116
|
+
components,
|
|
117
|
+
showLineNumbers,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<View className={cn('w-full gap-3', className)} {...props}>
|
|
122
|
+
{blocks.map((block, index) => (
|
|
123
|
+
<BlockView key={index} block={block} context={context} />
|
|
124
|
+
))}
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Re-renders only when the text changes.
|
|
131
|
+
*
|
|
132
|
+
* A stream re-renders its parent on every token, and everything else the parent
|
|
133
|
+
* hands down — the callbacks, the overrides — is usually a fresh object each
|
|
134
|
+
* time. Comparing the one prop that actually decides the output keeps a long
|
|
135
|
+
* answer from re-parsing because a sibling moved.
|
|
136
|
+
*/
|
|
137
|
+
export const Response = memo(
|
|
138
|
+
ResponseRoot,
|
|
139
|
+
(previous, next) =>
|
|
140
|
+
previous.children === next.children &&
|
|
141
|
+
previous.isStreaming === next.isStreaming &&
|
|
142
|
+
previous.className === next.className
|
|
143
|
+
);
|
|
144
|
+
Response.displayName = 'Response';
|
|
145
|
+
|
|
146
|
+
/* -------------------------------------------------------------------------- */
|
|
147
|
+
/* Blocks */
|
|
148
|
+
/* -------------------------------------------------------------------------- */
|
|
149
|
+
|
|
150
|
+
interface RenderContext {
|
|
151
|
+
streaming: boolean;
|
|
152
|
+
onLinkPress?: (href: string) => void;
|
|
153
|
+
allowedLinkPrefixes: string[];
|
|
154
|
+
components?: ResponseComponents;
|
|
155
|
+
showLineNumbers: boolean;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function BlockView({ block, context }: { block: Block; context: RenderContext }) {
|
|
159
|
+
switch (block.type) {
|
|
160
|
+
case 'heading':
|
|
161
|
+
return (
|
|
162
|
+
<Typography.Heading type={HEADING_TYPE[block.level - 1]}>
|
|
163
|
+
{spans(block.inline, context)}
|
|
164
|
+
</Typography.Heading>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
case 'paragraph':
|
|
168
|
+
return (
|
|
169
|
+
<Typography.Paragraph>{spans(block.inline, context)}</Typography.Paragraph>
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
case 'code': {
|
|
173
|
+
const Custom = context.components?.code;
|
|
174
|
+
if (Custom) {
|
|
175
|
+
return (
|
|
176
|
+
<Custom
|
|
177
|
+
code={block.code}
|
|
178
|
+
language={block.language}
|
|
179
|
+
streaming={block.open && context.streaming}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return (
|
|
184
|
+
<CodeBlock
|
|
185
|
+
code={block.code}
|
|
186
|
+
language={block.language}
|
|
187
|
+
showLineNumbers={context.showLineNumbers}
|
|
188
|
+
>
|
|
189
|
+
{block.language ? (
|
|
190
|
+
<CodeBlock.Header>
|
|
191
|
+
<CodeBlock.Language>{block.language}</CodeBlock.Language>
|
|
192
|
+
<CodeBlock.Actions>
|
|
193
|
+
{/*
|
|
194
|
+
No copy button while the fence is still open: copying half a
|
|
195
|
+
snippet is worse than not offering to, because it succeeds.
|
|
196
|
+
*/}
|
|
197
|
+
{block.open && context.streaming ? null : <CodeBlock.CopyButton />}
|
|
198
|
+
</CodeBlock.Actions>
|
|
199
|
+
</CodeBlock.Header>
|
|
200
|
+
) : null}
|
|
201
|
+
</CodeBlock>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
case 'quote':
|
|
206
|
+
// The rule is drawn here rather than by `Typography.Blockquote`, which
|
|
207
|
+
// puts its children inside a `Text` — right for a quoted sentence, wrong
|
|
208
|
+
// for a quote that turns out to contain a list and a snippet. `border-s`
|
|
209
|
+
// so the rule moves to the other side under a right-to-left `Direction`.
|
|
210
|
+
return (
|
|
211
|
+
<View className="w-full gap-2 border-s-2 border-border ps-4">
|
|
212
|
+
{block.blocks.map((child, index) => (
|
|
213
|
+
<BlockView key={index} block={child} context={context} />
|
|
214
|
+
))}
|
|
215
|
+
</View>
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
case 'list':
|
|
219
|
+
return (
|
|
220
|
+
<Typography.List ordered={block.ordered}>
|
|
221
|
+
{block.items.map((item, index) => (
|
|
222
|
+
<View key={index} className="gap-2">
|
|
223
|
+
{item.map((child, position) => (
|
|
224
|
+
<BlockView key={position} block={child} context={context} />
|
|
225
|
+
))}
|
|
226
|
+
</View>
|
|
227
|
+
))}
|
|
228
|
+
</Typography.List>
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
case 'table':
|
|
232
|
+
return <TableBlock block={block} context={context} />;
|
|
233
|
+
|
|
234
|
+
case 'rule':
|
|
235
|
+
return <View className="h-px w-full bg-border" />;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const ALIGNMENT: Record<Align, 'start' | 'center' | 'end'> = {
|
|
240
|
+
left: 'start',
|
|
241
|
+
center: 'center',
|
|
242
|
+
right: 'end',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* A GFM table.
|
|
247
|
+
*
|
|
248
|
+
* Every column gets the same share of the width. A markdown table carries no
|
|
249
|
+
* widths, and guessing them from the longest cell makes the columns jump about
|
|
250
|
+
* as rows stream in — which is exactly the movement everything else here is
|
|
251
|
+
* built to avoid.
|
|
252
|
+
*/
|
|
253
|
+
function TableBlock({
|
|
254
|
+
block,
|
|
255
|
+
context,
|
|
256
|
+
}: {
|
|
257
|
+
block: Extract<Block, { type: 'table' }>;
|
|
258
|
+
context: RenderContext;
|
|
259
|
+
}) {
|
|
260
|
+
const align = (index: number) => ALIGNMENT[block.align[index] ?? 'left'];
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<Table variant="outline" size="sm">
|
|
264
|
+
<Table.Header>
|
|
265
|
+
<Table.Row>
|
|
266
|
+
{block.head.map((cell, index) => (
|
|
267
|
+
<Table.Head key={index} align={align(index)}>
|
|
268
|
+
<InlineRun
|
|
269
|
+
tokens={cell}
|
|
270
|
+
context={context}
|
|
271
|
+
className="text-[11px] font-medium text-muted-foreground"
|
|
272
|
+
/>
|
|
273
|
+
</Table.Head>
|
|
274
|
+
))}
|
|
275
|
+
</Table.Row>
|
|
276
|
+
</Table.Header>
|
|
277
|
+
<Table.Body>
|
|
278
|
+
{block.rows.map((row, rowIndex) => (
|
|
279
|
+
<Table.Row key={rowIndex}>
|
|
280
|
+
{block.head.map((_unused, index) => (
|
|
281
|
+
<Table.Cell key={index} align={align(index)}>
|
|
282
|
+
<InlineRun
|
|
283
|
+
tokens={row[index] ?? []}
|
|
284
|
+
context={context}
|
|
285
|
+
className="text-xs text-foreground"
|
|
286
|
+
/>
|
|
287
|
+
</Table.Cell>
|
|
288
|
+
))}
|
|
289
|
+
</Table.Row>
|
|
290
|
+
))}
|
|
291
|
+
</Table.Body>
|
|
292
|
+
</Table>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* -------------------------------------------------------------------------- */
|
|
297
|
+
/* Inline */
|
|
298
|
+
/* -------------------------------------------------------------------------- */
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* A run of styled spans, inside a `Text` of its own.
|
|
302
|
+
*
|
|
303
|
+
* Nested `Text` rather than a row of views, because only nested text wraps as
|
|
304
|
+
* one paragraph — a bolded word in a view would break the line before it and
|
|
305
|
+
* leave a gap where the sentence should have continued.
|
|
306
|
+
*
|
|
307
|
+
* The wrapper is not optional. Half these spans are bare strings, and a bare
|
|
308
|
+
* string is only legal inside a `Text`: dropped into a `Table.Cell` — which is
|
|
309
|
+
* a view — React Native refuses to render it at all.
|
|
310
|
+
*/
|
|
311
|
+
function InlineRun({
|
|
312
|
+
tokens,
|
|
313
|
+
context,
|
|
314
|
+
className,
|
|
315
|
+
}: {
|
|
316
|
+
tokens: InlineToken[];
|
|
317
|
+
context: RenderContext;
|
|
318
|
+
className?: string;
|
|
319
|
+
}) {
|
|
320
|
+
return <Text className={className}>{spans(tokens, context)}</Text>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function spans(tokens: InlineToken[], context: RenderContext): ReactNode {
|
|
324
|
+
return tokens.map((token, index) => {
|
|
325
|
+
if (token.kind === 'image') {
|
|
326
|
+
const Custom = context.components?.image;
|
|
327
|
+
// Nothing by default. An image in an answer is a URL the model wrote, and
|
|
328
|
+
// fetching it unasked is a request to a host nobody chose.
|
|
329
|
+
return Custom ? <Custom key={index} src={token.href ?? ''} alt={token.value} /> : null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (token.kind === 'code') {
|
|
333
|
+
// A `Text`, not `Typography.Code` — that one is a view around a text, and
|
|
334
|
+
// a view inside a paragraph breaks the line before it and leaves a gap
|
|
335
|
+
// where the sentence should have carried on. A background and a
|
|
336
|
+
// monospace face are what make it read as code; they survive nesting,
|
|
337
|
+
// and padding does not.
|
|
338
|
+
//
|
|
339
|
+
// No relative font size either. There is no `em` here to be relative to:
|
|
340
|
+
// a value in one sends the style resolver looking for a parent size that
|
|
341
|
+
// is itself expressed in `em`, and it does not come back. The inherited
|
|
342
|
+
// size is the right one anyway — inline code is part of its sentence.
|
|
343
|
+
return (
|
|
344
|
+
<Text key={index} className={cn('bg-muted font-mono', marks(token))}>
|
|
345
|
+
{token.value}
|
|
346
|
+
</Text>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (token.kind === 'link') {
|
|
351
|
+
const href = token.href ?? '';
|
|
352
|
+
const openable = context.allowedLinkPrefixes.some((prefix) => href.startsWith(prefix));
|
|
353
|
+
return (
|
|
354
|
+
<Text
|
|
355
|
+
key={index}
|
|
356
|
+
className={cn('text-primary underline', marks(token))}
|
|
357
|
+
onPress={
|
|
358
|
+
openable
|
|
359
|
+
? () => (context.onLinkPress ?? defaultOpen)(href)
|
|
360
|
+
: undefined
|
|
361
|
+
}
|
|
362
|
+
>
|
|
363
|
+
{token.value}
|
|
364
|
+
</Text>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!token.bold && !token.italic && !token.strike) return token.value;
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<Text key={index} className={marks(token)}>
|
|
372
|
+
{token.value}
|
|
373
|
+
</Text>
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function marks(token: InlineToken): string {
|
|
379
|
+
return cn(
|
|
380
|
+
token.bold && 'font-semibold',
|
|
381
|
+
token.italic && 'italic',
|
|
382
|
+
token.strike && 'line-through'
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function defaultOpen(href: string) {
|
|
387
|
+
// Fire and forget: a link the platform declines to open is not worth an
|
|
388
|
+
// unhandled rejection, and there is nothing useful to do about one.
|
|
389
|
+
void Linking.openURL(href).catch(() => {});
|
|
390
|
+
}
|