Repository navigation
Expand file tree
/
Copy pathHighlightedCodeBlock.tsx
More file actions
137 lines (133 loc) · 4.39 KB
/
Copy pathHighlightedCodeBlock.tsx
File metadata and controls
137 lines (133 loc) · 4.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { useState } from "react";
import { Pressable, ScrollView, Text } from "react-native";
import type { MarkdownStyle } from "react-native-enriched-markdown";
import { Menu } from "../components/ui/Menu";
import { useMarkdownCodeHighlight } from "../features/threads/markdownCodeHighlightState";
import { copyTextWithHaptic } from "../lib/copyTextWithHaptic";
import type { HighlightScheme } from "../lib/highlights";
import { CodeBlockFrame, CodeBlockHeaderButton } from "./CodeBlockFrame";
import { fencedCodeMarkdown } from "./codeBlockMarkdown";
import type { NativeMarkdownTextStyle } from "./SelectableMarkdownText.types";
// Header names from enriched-markdown's CodeBlockLanguages.cpp that are not the fence capitalized.
const FENCE_DISPLAY_NAMES: Readonly<Record<string, string>> = {
cc: "C++",
cpp: "C++",
cxx: "C++",
cs: "C#",
csharp: "C#",
graphql: "GraphQL",
objc: "Objective-C",
objectivec: "Objective-C",
php: "PHP",
rb: "Ruby",
scss: "SCSS",
sql: "SQL",
toml: "TOML",
xml: "XML",
};
// The native block's long-press menu, with its labels and iOS symbols.
const CODE_MENU_ACTIONS = [
{ id: "copy-code", title: "Copy", image: "doc.on.doc" },
{ id: "copy-markdown", title: "Copy as Markdown", image: "doc.text" },
];
/** A fence in a language enriched-markdown cannot colour, drawn like its native block. */
export function HighlightedCodeBlock(props: {
readonly code: string;
readonly fence: string;
readonly language: string;
/** False only while the library withholds an unclosed fence (`streamingAnimation`, which T3 never enables, so always true today); highlighting waits for it. */
readonly complete: boolean;
readonly scheme: HighlightScheme;
readonly textStyle: NativeMarkdownTextStyle;
readonly codeStyle: NonNullable<MarkdownStyle["codeBlock"]>;
}) {
const { code, codeStyle, textStyle } = props;
const [wrapped, setWrapped] = useState(false);
const lines = useMarkdownCodeHighlight({
code,
language: props.language,
theme: props.scheme,
enabled: props.complete,
});
const fence = props.fence.toLowerCase();
let offset = 0;
const text = (
// Not selectable: as in the native block, a long-press opens the copy menu instead.
<Text
style={{
color: codeStyle.color,
fontFamily: codeStyle.fontFamily,
fontSize: codeStyle.fontSize,
lineHeight: codeStyle.lineHeight,
}}
>
{lines?.flatMap((line, lineIndex) => {
const separator = lineIndex > 0 ? "\n" : "";
offset += separator.length;
return [
separator,
...line.map((token) => {
const start = offset;
offset += token.content.length;
return (
<Text key={start} style={token.color ? { color: token.color } : undefined}>
{token.content}
</Text>
);
}),
];
}) ?? code}
</Text>
);
// Inside the scroller: on iOS an ancestor JS responder disables a ScrollView's panning.
const content = (
<Menu
shouldOpenOnLongPress
actions={CODE_MENU_ACTIONS}
style={{ flexGrow: 1 }}
onPressAction={({ nativeEvent }) => {
copyTextWithHaptic(
nativeEvent.event === "copy-markdown" ? fencedCodeMarkdown(code, props.fence) : code,
);
}}
>
<Pressable style={{ padding: codeStyle.padding }}>{text}</Pressable>
</Menu>
);
return (
<CodeBlockFrame
label={FENCE_DISPLAY_NAMES[fence] ?? fence.charAt(0).toUpperCase() + fence.slice(1)}
textStyle={textStyle}
actions={
<>
<CodeBlockHeaderButton
icon="text.alignleft"
label={wrapped ? "Disable line wrap" : "Wrap lines"}
textStyle={textStyle}
active={wrapped}
onPress={() => setWrapped((value) => !value)}
/>
<CodeBlockHeaderButton
icon="doc.on.doc"
label="Copy code"
textStyle={textStyle}
onPress={() => copyTextWithHaptic(code)}
/>
</>
}
>
{wrapped ? (
content
) : (
<ScrollView
horizontal
// As in the thread feed: with the keyboard open, the first long-press reaches the copy menu.
keyboardShouldPersistTaps="always"
contentContainerStyle={{ flexGrow: 1 }}
>
{content}
</ScrollView>
)}
</CodeBlockFrame>
);
}