Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/react-native-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,16 @@
"dependencies": {
"@khanacademy/simple-markdown": "^2.1.0",
"linkifyjs": "^4.3.2",
"lodash": "4.17.21"
"lodash": "4.17.21",
"react-native-syntax-highlighter": "^2.1.0",
"react-syntax-highlighter": "15.5.0"
},
"devDependencies": {
"@types/lodash": "4.17.20",
"@types/node": "^24",
"@types/react": "19.1.1",
"@types/react": "19.2.2",
"concurrently": "catalog:",
"react": "19.1.1",
"react": "19.2.0",
"react-native": "^0.82.1",
"react-native-builder-bob": "0.40.14",
"react-native-gesture-handler": "^2.29.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ReactNode, useRef } from 'react';
import { type ReactNode } from 'react';
import Animated, {
clamp,
scrollTo,
Expand Down
83 changes: 71 additions & 12 deletions packages/react-native-sdk/src/markdown/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,78 @@
import { Text } from 'react-native';
import { Pressable, type PressableProps, Text, View } from 'react-native';
import type { MarkdownComponentProps, RuleRenderFunction } from '../types.ts';
import { MarkdownReactiveScrollView } from '../../components';
// @ts-ignore
import SyntaxHighlighter from 'react-native-syntax-highlighter';
import { type PropsWithChildren, useCallback, useMemo } from 'react';

export const CodeBlock = ({
children,
styles,
state,
}: MarkdownComponentProps) => (
<MarkdownReactiveScrollView>
<Text style={styles.codeBlock}>{children}</Text>
</MarkdownReactiveScrollView>
export const CodeBlockCopyButton = ({
onPress,
}: {
onPress?: PressableProps['onPress'];
}) => (
<Pressable
style={({ pressed }) => ({ opacity: pressed ? 0.5 : 1 })}
onPress={onPress}
>
<Text style={{ fontSize: 15 }}>{'\u29C9'}</Text>
</Pressable>
);

export const CodeBlock = ({ styles, node }: MarkdownComponentProps) => {
const text = useMemo(() => node.content?.trim(), [node.content]);
const lineNumbers = useMemo(
() => Array.from({ length: text?.split('\n').length ?? 0 }, (_, i) => i),
[text],
);

const CodeTag = useCallback(
({ children }: PropsWithChildren) => (
<View style={styles.codeBlockContainer}>
<View style={styles.codeBlockLineNumberGutter}>
{lineNumbers.map((idx) => (
<Text style={styles.codeBlockLineNumberCell} key={idx}>
{`${idx + 1}.`}
</Text>
))}
</View>
<Text style={styles.codeBlock}>{children}</Text>
</View>
),
[styles, node.lang],
);

const CodeBlockHeader = useCallback(
() => (
<View style={styles.codeBlockHeaderContainer}>
<Text style={styles.codeBlockHeaderTitle}>{node.lang}</Text>
<CodeBlockCopyButton />
</View>
),
[styles, node.lang],
);

const CodeBlockWrapper = useCallback(
({ children }: PropsWithChildren) => (
<View style={styles.codeBlockWrapper}>
<CodeBlockHeader />
<MarkdownReactiveScrollView>{children}</MarkdownReactiveScrollView>
</View>
),
[styles],
);

return (
<SyntaxHighlighter
language={node.lang}
highlighter={'prism'}
CodeTag={CodeTag}
PreTag={CodeBlockWrapper}
>
{text}
</SyntaxHighlighter>
);
};

export const renderCodeBlock: RuleRenderFunction = ({
node,
output,
Expand All @@ -24,7 +85,5 @@ export const renderCodeBlock: RuleRenderFunction = ({
output={output}
state={state}
styles={styles}
>
{node.content?.trim()}
</CodeBlock>
/>
);
33 changes: 33 additions & 0 deletions packages/react-native-sdk/src/markdown/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,30 @@ export const getLocalRules = (
};
};

const parseFence: ParseFunction = (capture) => {
// capture[2] = language string (may be undefined/empty)
// capture[3] = code content
let lang = (capture[2] || '').trim() || undefined;
let content = capture[3] || '';

// If we've found no lang on the fence line (the opening fence) but the first code line
// is a lone token, treat it as lang and strip it from content. Helps with some specific
// use-cases such as "```<newline>sql<newline>...".
if (!lang) {
const nl = content.indexOf('\n');
const firstLine = (nl === -1 ? content : content.slice(0, nl)).trim();
if (/^[A-Za-z0-9_+.-]+$/.test(firstLine)) {
lang = firstLine;
content = nl === -1 ? '' : content.slice(nl + 1);
}
}

// Mirror your trimming behavior
content = content.replace(/\n+$/, '');

return { type: 'codeBlock', lang, content };
};

const enrichedRenderFunction =
(
render: RuleRenderFunction,
Expand Down Expand Up @@ -121,6 +145,15 @@ export const getLocalRules = (
codeBlock: {
react: enrichedRenderFunction(renderCodeBlock),
},
fence: {
match: SimpleMarkdown.blockRegex(
// 1: fence (``` or ~~~)
// 2: info string (lang etc.) - SAME CAPTURE INDEX AS YOURS
// 3: code content
/^ {0,3}(`{3,}|~{3,})[ \t]*(\S+)?[ \t]*\r?\n([\s\S]*?)\r?\n?(?: {0,3})\1[ \t]*(?:\r?\n+|$)/,
),
parse: parseFence,
},
del: {
react: enrichedRenderFunction(renderStrikethrough),
},
Expand Down
31 changes: 30 additions & 1 deletion packages/react-native-sdk/src/markdown/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,38 @@ export default StyleSheet.create({
backgroundColor: colors.code_block,
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'Monospace',
fontWeight: '500',
color: colors.black,
paddingLeft: 8,
lineHeight: 14,
},
codeBlockWrapper: {
backgroundColor: colors.code_block,
borderRadius: 8,
marginVertical: 8,
padding: 12,
},
codeBlockContainer: {
flexDirection: 'row',
},
codeBlockLineNumberGutter: {
flexDirection: 'column',
color: colors.black,
padding: 8,
},
codeBlockLineNumberCell: {
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'Monospace',
color: colors.grey,
paddingVertical: 1, // should be (codeBlock.lineHeight - this.fontSize) / 2
fontSize: 12,
},
codeBlockHeaderContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
},
codeBlockHeaderTitle: {
fontSize: 12,
color: colors.grey,
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'Monospace',
},
del: {
textDecorationLine: 'line-through',
Expand Down
6 changes: 6 additions & 0 deletions packages/react-native-sdk/src/markdown/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export type MarkdownStyle = Partial<{
blockQuoteText: TextStyle | ViewStyle;
br: TextStyle;
codeBlock: TextStyle;
codeBlockLineNumberGutter: ViewStyle;
codeBlockContainer: ViewStyle;
codeBlockWrapper: ViewStyle;
codeBlockLineNumberCell: TextStyle;
codeBlockHeaderContainer: ViewStyle;
codeBlockHeaderTitle: TextStyle;
del: TextStyle;
em: TextStyle;
heading: TextStyle;
Expand Down
Loading