fix(markdown): linked inline code keeps inline code size

This commit is contained in:
Mohamed Boudra
2026-10-16 08:39:19 +02:00
parent 89d42e4419
commit 313d284c3f
6 changed files with 115 additions and 9 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ Math and video are disabled. Task lists and admonitions are enabled; task checkb
Mermaid fences use the existing isolated diagram runtime and fullscreen viewer. Web dispatches them through the library’s code-block override. Native has no block-renderer hook, so the renderer extracts only Mermaid fences and places the existing WebView host between library-rendered segments. This is the only block exception; see [native selection limitations](native-text-selection.md) for diagram grouping and leading inline images. Streaming keeps the last valid diagram visible while the next revision renders.
Native code blocks use the library's copy control and compiled grammars. Web code blocks use `HighlightedCodeBlock` through the library's `renderers.codeBlock` override. The patch also preserves web list numbering and link titles, marks pending parses with `aria-busy` so Find waits for rendered text, and carries link syntax through native and web tap events. The package patch is tracked by [upstream PR #997](https://github.com/software-mansion/enriched-markdown/pull/997); remove the patch when the pinned release includes it.
Native code blocks use the library's copy control and compiled grammars. Web code blocks use `HighlightedCodeBlock` through the library's `renderers.codeBlock` override. The patch also preserves web list numbering and link titles, marks pending parses with `aria-busy` so Find waits for rendered text, and carries link syntax through native and web tap events. Web inline code inside a link inherits only the link colour; its code font, size and background stay intact. Keep link variants colour-only: native pills replace text runs and prevent paths from wrapping; Android pills also use the surrounding block’s font size. The package patch is tracked by [upstream PR #997](https://github.com/software-mansion/enriched-markdown/pull/997); remove the patch when the pinned release includes it.
The library determines how incomplete Markdown appears during streaming. Completed text uses the same component and engine as history. Find and scroll-to-message address source message ids; selection-copy understands semantic HTML and the web code component's copy metadata.
@@ -118,3 +118,73 @@ async function openSharedMarkdownFile(page: Page, name: string) {
await returnToSharedMarkdownChat(page);
await page.getByTestId("assistant-message").getByRole("link", { name, exact: true }).click();
}
test("linked inline code keeps code typography and wraps long paths", async ({
page,
}, testInfo) => {
const longPath =
"packages/app/src/components/markdown/renderer/very-long-directory-name/another-long-directory-name/file.ts:12";
const session = await seedMockAgentWorkspace({
repoPrefix: "inline-code-style-",
title: "Inline code style",
featureValues: {
mockAssistantResponse:
"Compare `ordinary_token`, `file.ts:12`, `https://example.com`. At the end of this line open `" +
longPath +
"`.",
},
initialPrompt: "Show inline code",
});
try {
await session.client.waitForFinish(session.agentId, 30_000);
await openAgentRoute(page, session);
const message = page.getByTestId("assistant-message");
await expect(message.getByRole("link", { name: longPath, exact: true })).toBeVisible();
const measurements = await measureInlineCode(message);
await testInfo.attach("inline-code-measurements", {
body: JSON.stringify(measurements, null, 2),
contentType: "application/json",
});
expect(measurements.codes).toHaveLength(4);
const [plain, file, url, long] = measurements.codes;
for (const linked of [file, url, long]) {
expect(linked.fontSize).toBe(plain.fontSize);
expect(linked.fontFamily).toBe(plain.fontFamily);
expect(linked.backgroundColor).toBe(plain.backgroundColor);
expect(linked.color).toBe(linked.anchorColor);
expect(linked.color).not.toBe(plain.color);
}
expect(long.lines).toBeGreaterThan(1);
expect(long.right).toBeLessThanOrEqual(measurements.right + 1);
} finally {
await session.cleanup();
}
});
async function measureInlineCode(message: Locator) {
return message.evaluate((element) => {
// Constrain the paragraph to exercise an inline path at a line boundary.
const paragraph = element.querySelector("p")!;
paragraph.style.width = "300px";
const codes = Array.from(paragraph.querySelectorAll("code"), (code) => {
const style = getComputedStyle(code);
const range = document.createRange();
range.selectNodeContents(code);
const rects = Array.from(range.getClientRects());
return {
text: code.textContent,
fontSize: style.fontSize,
fontFamily: style.fontFamily,
backgroundColor: style.backgroundColor,
color: style.color,
anchorColor: code.closest("a") ? getComputedStyle(code.closest("a")!).color : null,
whiteSpace: style.whiteSpace,
overflowWrap: style.overflowWrap,
rects: rects.map((rect) => ({ left: rect.left, right: rect.right, top: rect.top })),
lines: new Set(rects.map((rect) => rect.top)).size,
right: Math.max(...rects.map((rect) => rect.right)),
};
});
return { codes, right: paragraph.getBoundingClientRect().right };
});
}
@@ -200,7 +200,9 @@ it.each([
borderColor: "transparent",
});
expect(style.link?.color).toBe(theme.colors.accentBright);
expect(style.linkVariants?.["^(?!https?://).+"]?.color).toBe(theme.colors.accentBright);
expect(style.linkVariants?.["^(?!https?://).+"]).toEqual({
color: theme.colors.accentBright,
});
expect(style.thematicBreak?.color).toBe(theme.colors.border);
},
);
@@ -1,5 +1,5 @@
import { Text } from "react-native";
import { forwardRef, type ComponentProps } from "react";
import { forwardRef, useMemo, type ComponentProps } from "react";
import type {
CodeBlockRendererProps,
LinkRendererProps,
@@ -29,7 +29,15 @@ function WebCodeBlock({ code, language }: CodeBlockRendererProps) {
function WebLink({ link, anchorProps, children }: LinkRendererProps) {
const { tooltipPath, onHoverIn } = useFileLink(markdownLinkSource(link.url, link.sourceType));
const anchor = <a {...anchorProps}>{children}</a>;
const anchorStyle = useMemo(
() => ({ ...anchorProps.style, overflowWrap: "anywhere" as const }),
[anchorProps.style],
);
const anchor = (
<a {...anchorProps} style={anchorStyle}>
{children}
</a>
);
return (
<Tooltip delayDuration={400}>
<TooltipTrigger asChild onHoverIn={onHoverIn}>
@@ -60,7 +60,6 @@ export function createMarkdownStyles(
linkVariants: {
"^(?!https?://).+": {
color: theme.colors.accentBright,
pill: { borderRadius: 4, paddingHorizontal: 3 },
},
},
code: {
@@ -2283,9 +2283,22 @@ diff --git a/node_modules/react-native-enriched-markdown/lib/typescript/src/inde
export type { EnrichedMarkdownTextProps } from './types/MarkdownTextProps.web';
export type { MarkdownStyle, Md4cFlags } from './types/MarkdownStyle';
diff --git a/node_modules/react-native-enriched-markdown/src/web/renderers/InlineRenderers.tsx b/node_modules/react-native-enriched-markdown/src/web/renderers/InlineRenderers.tsx
index 091eb19..bbee3ce 100644
--- a/node_modules/react-native-enriched-markdown/src/web/renderers/InlineRenderers.tsx
+++ b/node_modules/react-native-enriched-markdown/src/web/renderers/InlineRenderers.tsx
@@ -58,16 +58,25 @@
@@ -48,9 +48,9 @@ function HighlightRenderer({ node, styles, renderChildren }: RendererProps) {
return <mark style={styles.highlight}>{renderChildren(node)}</mark>;
}
-function CodeRenderer({ node, styles, renderChildren }: RendererProps) {
+function CodeRenderer({ node, styles, parentType, renderChildren }: RendererProps) {
return (
- <code style={styles.code}>{node.content ?? renderChildren(node)}</code>
+ <code style={parentType === 'Link' ? { ...styles.code, color: 'inherit' } : styles.code}>{node.content ?? renderChildren(node)}</code>
);
}
@@ -58,16 +58,25 @@ function LinkRenderer({
node,
style,
callbacks,
@@ -2312,7 +2325,7 @@ diff --git a/node_modules/react-native-enriched-markdown/src/web/renderers/Inlin
}
};
@@ -78,18 +87,27 @@
@@ -78,18 +87,27 @@ function LinkRenderer({
}
};
@@ -2353,9 +2366,23 @@ diff --git a/node_modules/react-native-enriched-markdown/src/web/renderers/Inlin
function LatexMathInlineRenderer({
diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js
index c8311d4..d1e698c 100644
--- a/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js
+++ b/node_modules/react-native-enriched-markdown/lib/module/web/renderers/InlineRenderers.js
@@ -103,18 +103,24 @@
@@ -92,10 +92,11 @@ function HighlightRenderer({
function CodeRenderer({
node,
styles,
+ parentType,
renderChildren
}) {
return /*#__PURE__*/_jsx("code", {
- style: styles.code,
+ style: parentType === 'Link' ? { ...styles.code, color: 'inherit' } : styles.code,
children: node.content ?? renderChildren(node)
});
}
@@ -103,18 +104,24 @@ function LinkRenderer({
node,
style,
callbacks,
@@ -2383,7 +2410,7 @@ diff --git a/node_modules/react-native-enriched-markdown/lib/module/web/renderer
}
};
const handleContextMenu = event => {
@@ -125,15 +131,24 @@
@@ -125,15 +132,24 @@ function LinkRenderer({
});
}
};