Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 3 additions & 11 deletions apps/mobile/modules/t3-markdown-text/src/MarkdownTextPrimitive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,8 @@ function MarkdownTextPrimitiveChild({
() => [true, flattenedStyle],
[flattenedStyle],
);
let childPosition = 0;
const nativeChildren = React.Children.toArray(children).map((child) => {
const position = childPosition;
childPosition += 1;

// Children.map keeps each text child's position key while its text grows.
const nativeChildren = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return child;
}
Expand All @@ -75,12 +72,7 @@ function MarkdownTextPrimitiveChild({
const text = child.toString();
return (
// @ts-expect-error The generated run props do not include inherited Text props.
<T3MarkdownTextRunNativeComponent
key={`text-${position}-${text.length}-${text}`}
style={flattenedStyle}
text={text}
{...rest}
/>
<T3MarkdownTextRunNativeComponent style={flattenedStyle} text={text} {...rest} />
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import type { MarkdownNode } from "react-native-nitro-markdown/headless";

import { CopyTextButton } from "./CopyTextButton";
import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";
import { nativeMarkdownDocumentRuns, nativeMarkdownListItemBlocks } from "./nativeMarkdownText";
import {
nativeMarkdownDocumentRuns,
nativeMarkdownListItemBlocks,
nativeMarkdownNodeKey,
} from "./nativeMarkdownText";
import { NativeMarkdownSelectableText } from "./NativeMarkdownSelectableText.ios";
import type {
MarkdownCodeHighlighter,
Expand All @@ -28,10 +32,6 @@ const MONO_FONT_FAMILY = Platform.select({
default: "monospace",
});

function nodeKey(node: MarkdownNode, index: number): string {
return `${node.type}:${node.beg ?? index}:${node.end ?? index}`;
}

/** Code inside markdown scales with the base text size (12pt at the default 15pt body). */
function codeBlockFontSize(textStyle: NativeMarkdownTextStyle): number {
return Math.max(10, Math.round(textStyle.fontSize * 0.8));
Expand Down Expand Up @@ -193,14 +193,14 @@ function HighlightedCodeText(props: {
const tokens = line.map((token) => {
const start = sourceOffset;
sourceOffset += token.content.length;
const signature = `${start}:${token.content}:${token.color ?? ""}:${token.fontStyle ?? ""}`;
const signature = `${start}:${token.color ?? ""}:${token.fontStyle ?? ""}`;
const occurrence = keyOccurrences.get(signature) ?? 0;
keyOccurrences.set(signature, occurrence + 1);
return { key: `${signature}:${occurrence}`, token };
});
sourceOffset += 1;
return {
key: `line:${lineStart}:${line.map((token) => token.content).join("")}`,
key: `line:${lineStart}`,
tokens,
};
});
Expand Down Expand Up @@ -353,7 +353,7 @@ function NativeTable(props: {
>
{rows.map((row, rowIndex) => (
<View
key={nodeKey(row, rowIndex)}
key={nativeMarkdownNodeKey(row, rowIndex)}
style={{
flexDirection: "row",
backgroundColor: rowIndex === 0 ? props.textStyle.codeBackgroundColor : "transparent",
Expand All @@ -363,7 +363,7 @@ function NativeTable(props: {
>
{(row.children ?? []).map((cell, cellIndex) => (
<View
key={nodeKey(cell, cellIndex)}
key={nativeMarkdownNodeKey(cell, cellIndex)}
style={{
width: 160,
borderLeftColor: props.textStyle.dividerColor,
Expand Down Expand Up @@ -482,15 +482,15 @@ function NativeMixedParagraph(props: {
{inlineGroups(props.node.children ?? []).map((child, index) =>
child.type === "image" ? (
<NativeMarkdownImage
key={nodeKey(child, index)}
key={nativeMarkdownNodeKey(child, index)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
onLinkPress={props.onLinkPress}
/>
) : (
<SelectableNode
key={nodeKey(child, index)}
key={nativeMarkdownNodeKey(child, index)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
Expand Down Expand Up @@ -536,7 +536,7 @@ function NativeList(props: {
const markerOffset = taskMarker ? 3 : ordered ? 0 : 2;
return (
<View
key={nodeKey(item, index)}
key={nativeMarkdownNodeKey(item, index)}
style={{ alignItems: "flex-start", flexDirection: "row" }}
>
<View
Expand Down Expand Up @@ -564,7 +564,7 @@ function NativeList(props: {
<View style={{ flex: 1, minWidth: 0, gap: 2 }}>
{nativeMarkdownListItemBlocks(item).map((child, childIndex) => (
<NativeMarkdownBlock
key={nodeKey(child, childIndex)}
key={nativeMarkdownNodeKey(child, childIndex)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
Expand Down Expand Up @@ -598,7 +598,7 @@ export function NativeMarkdownBlock(props: {
<View style={{ gap: 8 }}>
{(props.node.children ?? []).map((child, index) => (
<NativeMarkdownBlock
key={nodeKey(child, index)}
key={nativeMarkdownNodeKey(child, index)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
Expand Down Expand Up @@ -659,7 +659,7 @@ export function NativeMarkdownBlock(props: {
>
{(props.node.children ?? []).map((child, index) => (
<NativeMarkdownBlock
key={nodeKey(child, index)}
key={nativeMarkdownNodeKey(child, index)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
Expand Down Expand Up @@ -730,7 +730,7 @@ export function NativeMarkdownBlock(props: {
<View style={{ gap: 4 }}>
{(props.node.children ?? []).map((child, index) => (
<NativeMarkdownBlock
key={nodeKey(child, index)}
key={nativeMarkdownNodeKey(child, index)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {

import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";
import { markdownFileIconSource } from "./markdownFileIcons";
import type { NativeMarkdownTextRun } from "./nativeMarkdownText";
import { nativeMarkdownTextRunKey, type NativeMarkdownTextRun } from "./nativeMarkdownText";
import type {
MarkdownFileContextMenu,
NativeMarkdownTextStyle,
Expand Down Expand Up @@ -47,28 +47,6 @@ const styles = StyleSheet.create({
},
});

function runKeySignature(run: NativeMarkdownTextRun): string {
return [
run.text,
run.bold,
run.italic,
run.strikethrough,
run.code,
run.href,
run.externalHost,
run.fileIcon,
run.skillName,
run.skillLabel,
run.role,
run.headingLevel,
run.depth,
run.spacing,
run.firstLineHeadIndent,
run.headIndent,
run.paragraphSpacing,
].join(":");
}

const DEFAULT_BODY_FONT_SIZE = 15;
const DEFAULT_HEADING_FONT_SIZES = [22, 19, 17, 16, 15, 15] as const;

Expand Down Expand Up @@ -190,13 +168,8 @@ export function NativeMarkdownSelectableText(props: {
},
[containsInlineFileIcon],
);
const occurrences = new Map<string, number>();
const prefixedExternalLinks = new Set<string>();
const keyedRuns = props.runs.map((run) => {
const signature = runKeySignature(run);
const occurrence = occurrences.get(signature) ?? 0;
occurrences.set(signature, occurrence + 1);

const keyedRuns = props.runs.map((run, index) => {
let text = run.text;
if (run.fileIcon && Platform.OS === "ios") {
text = `${INLINE_ATTACHMENT_PREFIX}${text}`;
Expand All @@ -210,7 +183,7 @@ export function NativeMarkdownSelectableText(props: {
text = `${EXTERNAL_LINK_PREFIX}${text}`;
}

return { key: `${signature}:${occurrence}`, run, text };
return { key: nativeMarkdownTextRunKey(run, index), run, text };
});
// T3MarkdownText only rebuilds its attributed string during native layout. A
// color-only child update can otherwise leave the previous appearance cached.
Expand Down
41 changes: 36 additions & 5 deletions apps/mobile/modules/t3-markdown-text/src/nativeMarkdownText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,34 @@ export interface NativeMarkdownTextRun {
readonly paragraphSpacing?: number;
}

/** Keep text updates in the same run. Attribute changes still replace the run. */
export function nativeMarkdownTextRunKey(run: NativeMarkdownTextRun, index: number): string {
return [
index,
run.bold,
run.italic,
run.strikethrough,
run.code,
run.href,
run.externalHost,
run.fileIcon,
run.skillName,
run.skillLabel,
run.role,
run.headingLevel,
run.depth,
run.spacing,
run.firstLineHeadIndent,
run.headIndent,
run.paragraphSpacing,
].join(":");
}

/** The block's end moves during streaming. Its type and start identify the block. */
export function nativeMarkdownNodeKey(node: MarkdownNode, index: number): string {
return `${node.type}:${node.beg ?? `index:${index}`}`;
}

export type NativeMarkdownDocumentChunk =
| {
readonly kind: "selectable";
Expand Down Expand Up @@ -698,16 +726,16 @@ export function nativeMarkdownDocumentChunks(
): ReadonlyArray<NativeMarkdownDocumentChunk> {
const chunks: NativeMarkdownDocumentChunk[] = [];
let selectableNodes: MarkdownNode[] = [];
let selectableStartIndex = 0;

const flushSelectable = () => {
if (selectableNodes.length === 0) {
const first = selectableNodes[0];
if (!first) {
return;
}
const first = selectableNodes[0];
const last = selectableNodes.at(-1);
chunks.push({
kind: "selectable",
key: `selectable:${first?.beg ?? "start"}:${last?.end ?? "end"}`,
key: `selectable:${nativeMarkdownNodeKey(first, selectableStartIndex)}`,
node: {
type: "document",
children: selectableNodes,
Expand All @@ -718,14 +746,17 @@ export function nativeMarkdownDocumentChunks(

for (const [index, child] of (document.children ?? []).entries()) {
if (!containsRichBlock(child)) {
if (selectableNodes.length === 0) {
selectableStartIndex = index;
}
selectableNodes.push(child);
continue;
}

flushSelectable();
chunks.push({
kind: "rich",
key: `rich:${child.type}:${child.beg ?? index}:${child.end ?? index}`,
key: `rich:${nativeMarkdownNodeKey(child, index)}`,
node: child,
});
}
Expand Down
Loading
Loading