Compare commits

...

4 Commits

Author SHA1 Message Date
Bryan Morgan 9bdc87e333 Merge branch 'main' into fix/latex-arrows 2026-02-08 17:31:31 -05:00
Bryan Morgan 361750b987 fix(cli): improve link measurement and refactor LaTeX arrow mapping 2026-02-08 17:25:18 -05:00
Bryan Morgan 0d4ffc6f72 Merge branch 'main' into fix/latex-arrows 2026-02-08 16:34:34 -05:00
Bryan Morgan 3a2a72786c fix(cli): support LaTeX-style arrows in markdown renderer 2026-02-08 14:49:40 -05:00
3 changed files with 78 additions and 28 deletions
@@ -1,25 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { getPlainTextLength } from './InlineMarkdownRenderer.js';
import { describe, it, expect } from 'vitest';
describe('getPlainTextLength', () => {
it.each([
['**Primary Go', 12],
['*Primary Go', 11],
['**Primary Go**', 10],
['*Primary Go*', 10],
['**', 2],
['*', 1],
['compile-time**', 14],
])(
'should measure markdown text length correctly for "%s"',
(input, expected) => {
expect(getPlainTextLength(input)).toBe(expected);
},
);
});
@@ -0,0 +1,55 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { RenderInline, getPlainTextLength } from './InlineMarkdownRenderer.js';
import { renderWithProviders } from '../../test-utils/render.js';
import { Text } from 'ink';
describe('InlineMarkdownRenderer', () => {
describe('getPlainTextLength', () => {
it.each([
['**Primary Go', 12],
['*Primary Go', 11],
['**Primary Go**', 10],
['*Primary Go*', 10],
['**', 2],
['*', 1],
['compile-time**', 14],
['$\\rightarrow$', 1],
['Sign Out $\\rightarrow$ Sign In', 18],
['[Google](https://google.com)', 27],
['Preceding [Link](url)', 20],
])(
'should measure markdown text length correctly for "%s"',
(input, expected) => {
expect(getPlainTextLength(input)).toBe(expected);
},
);
});
describe('<RenderInline />', () => {
it('renders LaTeX rightarrow correctly', () => {
const text = 'Sign Out $\\rightarrow$ Sign In';
const { lastFrame } = renderWithProviders(
<Text>
<RenderInline text={text} />
</Text>,
);
expect(lastFrame()).toContain('Sign Out → Sign In');
});
it('renders other LaTeX arrows correctly', () => {
const text = '$\\leftarrow$ $\\uparrow$ $\\downarrow$';
const { lastFrame } = renderWithProviders(
<Text>
<RenderInline text={text} />
</Text>,
);
expect(lastFrame()).toContain('← ↑ ↓');
});
});
});
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -18,6 +18,13 @@ const INLINE_CODE_MARKER_LENGTH = 1; // For "`"
const UNDERLINE_TAG_START_LENGTH = 3; // For "<u>"
const UNDERLINE_TAG_END_LENGTH = 4; // For "</u>"
const LATEX_MAP: Record<string, string> = {
'$\\rightarrow$': '→',
'$\\leftarrow$': '←',
'$\\uparrow$': '↑',
'$\\downarrow$': '↓',
};
interface RenderInlineProps {
text: string;
defaultColor?: string;
@@ -36,7 +43,7 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({
const nodes: React.ReactNode[] = [];
let lastIndex = 0;
const inlineRegex =
/(\*\*.*?\*\*|\*.*?\*|_.*?_|~~.*?~~|\[.*?\]\(.*?\)|`+.+?`+|<u>.*?<\/u>|https?:\/\/\S+)/g;
/(\*\*.*?\*\*|\*.*?\*|_.*?_|~~.*?~~|\[.*?\]\(.*?\)|`+.+?`+|<u>.*?<\/u>|https?:\/\/\S+|\$\\[a-z]+\$)/g;
let match;
while ((match = inlineRegex.exec(text)) !== null) {
@@ -143,6 +150,15 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({
{fullMatch}
</Text>
);
} else if (fullMatch.startsWith('$') && fullMatch.endsWith('$')) {
const replacement = LATEX_MAP[fullMatch];
if (replacement) {
renderedNode = (
<Text key={key} color={baseColor}>
{replacement}
</Text>
);
}
}
} catch (e) {
debugLogger.warn('Error parsing inline markdown part:', fullMatch, e);
@@ -184,6 +200,10 @@ export const getPlainTextLength = (text: string): number => {
.replace(/~~(.*?)~~/g, '$1')
.replace(/`(.*?)`/g, '$1')
.replace(/<u>(.*?)<\/u>/g, '$1')
.replace(/.*\[(.*?)\]\(.*\)/g, '$1');
.replace(/\[(.*?)\]\((.*?)\)/g, '$1 ($2)')
.replace(
/\$\\(rightarrow|leftarrow|uparrow|downarrow)\$/g,
(match) => LATEX_MAP[match] ?? match,
);
return stringWidth(cleanText);
};