Skip to content

Commit 50ed53d

Browse files
authored
Fix cursor position problems on mWeb (#700)
1 parent 4d4bd82 commit 50ed53d

File tree

3 files changed

+237
-18
lines changed

3 files changed

+237
-18
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type {MarkdownRange} from '../commonTypes';
2+
import {normalizeLines} from '../web/utils/parserUtils';
3+
import type {Paragraph} from '../web/utils/parserUtils';
4+
5+
describe('normalizeLines', () => {
6+
it('should handle single line markdown with no multi-line ranges', () => {
7+
const lines: Paragraph[] = [
8+
{
9+
text: '*Hello, world!*',
10+
start: 0,
11+
length: 13,
12+
markdownRanges: [],
13+
},
14+
];
15+
16+
const ranges: MarkdownRange[] = [
17+
{
18+
type: 'syntax',
19+
start: 0,
20+
length: 1,
21+
},
22+
{
23+
type: 'bold',
24+
start: 1,
25+
length: 11,
26+
},
27+
{
28+
type: 'syntax',
29+
start: 12,
30+
length: 1,
31+
},
32+
];
33+
34+
const result = normalizeLines(lines, ranges);
35+
const paragraph = result[0] as Paragraph;
36+
expect(paragraph.markdownRanges).toEqual([
37+
{length: 1, start: 0, type: 'syntax'},
38+
{length: 11, start: 1, type: 'bold'},
39+
{length: 1, start: 12, type: 'syntax'},
40+
]);
41+
expect(paragraph.text).toEqual('*Hello, world!*');
42+
});
43+
44+
it('should handle multiline line markdown with no multi-line ranges', () => {
45+
const lines: Paragraph[] = [
46+
{
47+
text: '*Hello',
48+
start: 0,
49+
length: 6,
50+
markdownRanges: [],
51+
},
52+
{
53+
text: 'world!*',
54+
start: 7,
55+
length: 7,
56+
markdownRanges: [],
57+
},
58+
];
59+
60+
const ranges: MarkdownRange[] = [
61+
{
62+
type: 'syntax',
63+
start: 0,
64+
length: 1,
65+
},
66+
{
67+
type: 'bold',
68+
start: 0,
69+
length: 13,
70+
},
71+
{
72+
type: 'syntax',
73+
start: 13,
74+
length: 1,
75+
},
76+
];
77+
78+
const result = normalizeLines(lines, ranges);
79+
expect(result.length).toBe(2);
80+
const firstParagraph = result[0] as Paragraph;
81+
const secondParagraph = result[1] as Paragraph;
82+
expect(firstParagraph.text).toEqual('*Hello');
83+
expect(secondParagraph.text).toEqual('world!*');
84+
expect(firstParagraph.markdownRanges).toContainEqual({type: 'bold', start: 0, length: 6});
85+
expect(secondParagraph.markdownRanges).toContainEqual({type: 'bold', start: 7, length: 6});
86+
});
87+
88+
it('should merge lines when handling multi-line markdown ranges', () => {
89+
const lines: Paragraph[] = [
90+
{
91+
text: 'Here is some code:',
92+
start: 0,
93+
length: 18,
94+
markdownRanges: [],
95+
},
96+
{
97+
text: '```',
98+
start: 19,
99+
length: 3,
100+
markdownRanges: [],
101+
},
102+
{
103+
text: "const text = 'Hello, World!';",
104+
start: 23,
105+
length: 29,
106+
markdownRanges: [],
107+
},
108+
{
109+
text: 'console.log(text);',
110+
start: 53,
111+
length: 18,
112+
markdownRanges: [],
113+
},
114+
{
115+
text: '```',
116+
start: 72,
117+
length: 3,
118+
markdownRanges: [],
119+
},
120+
];
121+
122+
const ranges: MarkdownRange[] = [
123+
{
124+
type: 'syntax',
125+
start: 19,
126+
length: 3,
127+
},
128+
{
129+
type: 'pre',
130+
start: 22,
131+
length: 50, // This range spans across multiple lines
132+
},
133+
{
134+
type: 'syntax',
135+
start: 72,
136+
length: 3,
137+
},
138+
];
139+
140+
const result = normalizeLines(lines, ranges);
141+
142+
expect(result.length).toBe(2);
143+
const paragraph = result[1] as Paragraph;
144+
expect(paragraph.text).toEqual("```\nconst text = 'Hello, World!';\nconsole.log(text);\n```");
145+
expect(paragraph.markdownRanges.length).toEqual(3);
146+
expect((paragraph.markdownRanges[1] as MarkdownRange).type).toEqual('pre');
147+
});
148+
149+
it('should correctly handle multiple adjacent ranges', () => {
150+
const lines: Paragraph[] = [
151+
{
152+
text: 'Link: www.example.com',
153+
start: 0,
154+
length: 21,
155+
markdownRanges: [],
156+
},
157+
];
158+
159+
const ranges: MarkdownRange[] = [{type: 'link', start: 6, length: 15}];
160+
161+
const result = normalizeLines(lines, ranges);
162+
const paragraph = result[0] as Paragraph;
163+
expect(paragraph.markdownRanges).toContainEqual({type: 'link', start: 6, length: 15});
164+
});
165+
});

src/web/utils/blockUtils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,16 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty
9696

9797
const BLOCK_MARKDOWN_TYPES = ['inline-image'];
9898
const FULL_LINE_MARKDOWN_TYPES = ['blockquote'];
99+
const MULTILINE_MARKDOWN_TYPES = ['pre'];
99100

100101
function isBlockMarkdownType(type: NodeType) {
101102
return BLOCK_MARKDOWN_TYPES.includes(type);
102103
}
103104

105+
function isMultilineMarkdownType(type: NodeType) {
106+
return MULTILINE_MARKDOWN_TYPES.includes(type);
107+
}
108+
104109
function getFirstBlockMarkdownRange(ranges: MarkdownRange[]) {
105110
const blockMarkdownRange = ranges.find((r) => isBlockMarkdownType(r.type) || FULL_LINE_MARKDOWN_TYPES.includes(r.type));
106111
return FULL_LINE_MARKDOWN_TYPES.includes(blockMarkdownRange?.type || '') ? undefined : blockMarkdownRange;
@@ -125,4 +130,4 @@ function extendBlockStructure(
125130
return targetNode;
126131
}
127132

128-
export {addStyleToBlock, extendBlockStructure, isBlockMarkdownType, getFirstBlockMarkdownRange};
133+
export {addStyleToBlock, extendBlockStructure, isBlockMarkdownType, isMultilineMarkdownType, getFirstBlockMarkdownRange};

src/web/utils/parserUtils.ts

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {addNodeToTree, createRootTreeNode, updateTreeElementRefs} from './treeUt
33
import type {NodeType, TreeNode} from './treeUtils';
44
import type {PartialMarkdownStyle} from '../../styleUtils';
55
import {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition} from './cursorUtils';
6-
import {addStyleToBlock, extendBlockStructure, getFirstBlockMarkdownRange, isBlockMarkdownType} from './blockUtils';
6+
import {addStyleToBlock, extendBlockStructure, getFirstBlockMarkdownRange, isBlockMarkdownType, isMultilineMarkdownType} from './blockUtils';
77
import type {InlineImagesInputProps, MarkdownRange} from '../../commonTypes';
88
import {getAnimationCurrentTimes, updateAnimationsTime} from './animationUtils';
99
import {sortRanges, ungroupRanges} from '../../rangeUtils';
@@ -31,9 +31,62 @@ function splitTextIntoLines(text: string): Paragraph[] {
3131
return lines;
3232
}
3333

34-
/** Merges lines that contain multiline markdown tags into one line */
35-
function mergeLinesWithMultilineTags(lines: Paragraph[], ranges: MarkdownRange[]) {
36-
let mergedLines = [...lines];
34+
/**
35+
* Merges lines with multiline markdown tags (like `pre`) into a single line.
36+
* The main line will contain the text and all markdown ranges from the other lines.
37+
*/
38+
function mergeLinesWithMultilineTags(lines: Paragraph[], currentLine: Paragraph, range: MarkdownRange, correspondingLineIndexes: number[]) {
39+
const mainLine = currentLine;
40+
currentLine.markdownRanges.push(range);
41+
42+
correspondingLineIndexes.forEach((lineIndex) => {
43+
const otherLine = lines[lineIndex] as Paragraph;
44+
mainLine.text += `\n${otherLine.text}`;
45+
mainLine.length += otherLine.length + 1;
46+
mainLine.markdownRanges.push(...otherLine.markdownRanges);
47+
});
48+
49+
if (correspondingLineIndexes.length > 0 && correspondingLineIndexes[0] !== undefined) {
50+
lines.splice(correspondingLineIndexes[0], correspondingLineIndexes.length);
51+
}
52+
}
53+
54+
/**
55+
* Splits a markdown range that spans multiple lines into separate lines.
56+
*/
57+
function splitRangeIntoSeparateLines(lines: Paragraph[], currentLine: Paragraph, range: MarkdownRange, correspondingLineIndexes: number[]) {
58+
const mainLineRangeLength = currentLine.start + currentLine.length - range.start;
59+
currentLine.markdownRanges.push({
60+
...range,
61+
length: mainLineRangeLength,
62+
});
63+
64+
let rangeLength = range.length - mainLineRangeLength;
65+
correspondingLineIndexes.forEach((lineIndex) => {
66+
const otherLine = lines[lineIndex] as Paragraph;
67+
let currentLength = otherLine.length;
68+
if (rangeLength <= currentLength) {
69+
currentLength = rangeLength - 1;
70+
}
71+
72+
if (currentLength > 0) {
73+
lines[lineIndex]?.markdownRanges.push({
74+
...range,
75+
start: otherLine.start,
76+
length: currentLength,
77+
});
78+
}
79+
80+
rangeLength -= currentLength;
81+
});
82+
}
83+
84+
/**
85+
* For singleline markdown types, the function splits markdown ranges that spread beyond the line length into separate lines.
86+
* For multiline markdown types (like `pre`), it merges them and corresponding text into one line.
87+
*/
88+
function normalizeLines(lines: Paragraph[], ranges: MarkdownRange[]) {
89+
const mergedLines = [...lines];
3790
const lineIndexes = mergedLines.map((_line, index) => index);
3891

3992
ranges.forEach((range) => {
@@ -44,19 +97,14 @@ function mergeLinesWithMultilineTags(lines: Paragraph[], ranges: MarkdownRange[]
4497
if (correspondingLineIndexes.length > 0) {
4598
const mainLineIndex = correspondingLineIndexes[0] as number;
4699
const mainLine = mergedLines[mainLineIndex] as Paragraph;
47-
48-
mainLine.markdownRanges.push(range);
49-
50100
const otherLineIndexes = correspondingLineIndexes.slice(1);
51-
otherLineIndexes.forEach((lineIndex) => {
52-
const otherLine = mergedLines[lineIndex] as Paragraph;
53101

54-
mainLine.text += `\n${otherLine.text}`;
55-
mainLine.length += otherLine.length + 1;
56-
mainLine.markdownRanges.push(...otherLine.markdownRanges);
57-
});
58-
if (otherLineIndexes.length > 0) {
59-
mergedLines = mergedLines.filter((_line, index) => !otherLineIndexes.includes(index));
102+
if (isMultilineMarkdownType(range.type)) {
103+
mergeLinesWithMultilineTags(mergedLines, mainLine, range, otherLineIndexes);
104+
} else if (otherLineIndexes.length > 0) {
105+
splitRangeIntoSeparateLines(mergedLines, mainLine, range, otherLineIndexes);
106+
} else {
107+
mainLine.markdownRanges.push(range);
60108
}
61109
}
62110
});
@@ -163,7 +211,7 @@ function parseRangesToHTMLNodes(
163211
// Sort all ranges by start position, length, and by tag hierarchy so the styles and text are applied in correct order
164212
const sortedRanges = sortRanges(ranges);
165213
const markdownRanges = ungroupRanges(sortedRanges);
166-
lines = mergeLinesWithMultilineTags(lines, markdownRanges);
214+
lines = normalizeLines(lines, markdownRanges);
167215

168216
let lastRangeEndIndex = 0;
169217
while (lines.length > 0) {
@@ -316,4 +364,5 @@ function updateInputStructure(
316364
return {text, cursorPosition: cursorPosition || 0};
317365
}
318366

319-
export {updateInputStructure, parseRangesToHTMLNodes};
367+
export {updateInputStructure, parseRangesToHTMLNodes, normalizeLines};
368+
export type {Paragraph};

0 commit comments

Comments
 (0)