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
2 changes: 1 addition & 1 deletion src/MarkdownTextInput.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputP
updateTextColor(divRef.current, e.target.textContent ?? '');
const previousText = divRef.current.value;
let parsedText = normalizeValue(
inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement, contentSelection.current.start, inputType),
inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement, contentSelection.current.start, inputType, multiline),
);

if (pasteContent.current) {
Expand Down
117 changes: 117 additions & 0 deletions src/__tests__/singleLineInputFix.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {expect} from '@jest/globals';
import {parseRangesToHTMLNodes} from '../web/utils/parserUtils';
import parseExpensiMark from '../parseExpensiMark';

/**
* Focused tests for the single-line input fix
* These tests validate the specific fix for unwanted space insertion
*/

describe('Single-line input fix validation', () => {
describe('parseRangesToHTMLNodes with isMultiline parameter', () => {
it('should not generate BR elements for single-line text (isMultiline=false)', () => {
const text = 'simple text';
const ranges = parseExpensiMark(text);

const result = parseRangesToHTMLNodes(text, ranges, false, {}, true);

// Should not contain BR elements for single-line input
expect(result.dom.innerHTML).not.toContain('<span data-type="br">');
expect(result.dom.innerHTML).not.toContain('<br>');
});

it('should generate proper structure for multiline text (isMultiline=true)', () => {
const text = 'line 1\nline 2';
const ranges = parseExpensiMark(text);

const result = parseRangesToHTMLNodes(text, ranges, true, {}, true);

// Should contain proper paragraph structure for multiline
expect(result.dom.innerHTML).toContain('<p data-type="line"');
// Note: The library may not always use BR elements depending on the implementation
// The key is that it handles multiline properly
expect(result.dom.innerHTML.split('<p data-type="line"').length - 1).toBeGreaterThan(1);
});

it('should not generate BR elements for single-line markdown (isMultiline=false)', () => {
const text = 'hello *world* test';
const ranges = parseExpensiMark(text);

const result = parseRangesToHTMLNodes(text, ranges, false, {}, true);

// Should not contain BR elements but should have markdown formatting
expect(result.dom.innerHTML).not.toContain('<span data-type="br">');
expect(result.dom.innerHTML).not.toContain('<br>');
expect(result.dom.innerHTML).toContain('data-type="bold"');
});

it('should handle empty single-line input without BR elements (isMultiline=false)', () => {
const text = '';
const ranges = parseExpensiMark(text);

const result = parseRangesToHTMLNodes(text, ranges, false, {}, true);

// For empty input, should either be empty or have minimal structure without BR
const html = result.dom.innerHTML;
if (html !== '') {
// If there's structure, it shouldn't have BR elements for single-line
expect(html).not.toContain('<span data-type="br">');
}
});

it('should preserve existing multiline behavior when isMultiline=true', () => {
const singleLineText = 'test text';
const ranges = parseExpensiMark(singleLineText);

// Compare single-line vs multiline behavior
const singleLineResult = parseRangesToHTMLNodes(singleLineText, ranges, false, {}, true);
const multilineResult = parseRangesToHTMLNodes(singleLineText, ranges, true, {}, true);

// Single-line should be more compact than multiline
expect(singleLineResult.dom.innerHTML.length).toBeLessThanOrEqual(multilineResult.dom.innerHTML.length);

// Single-line should not have BR elements
expect(singleLineResult.dom.innerHTML).not.toContain('<span data-type="br">');
});
});

describe('Integration test for the original bug scenario', () => {
it('should demonstrate the fix for typing after selecting all text', () => {
// This test validates the core fix: when user types "t" after selecting all,
// the generated DOM should not contain elements that would cause space insertion

const userTypedText = 't';
const ranges = parseExpensiMark(userTypedText);

// Generate DOM with single-line setting (the fix)
const result = parseRangesToHTMLNodes(userTypedText, ranges, false, {}, true);

// Critical assertions for the fix:
// 1. Should not contain BR elements that add newlines
expect(result.dom.innerHTML).not.toContain('<span data-type="br">');
expect(result.dom.innerHTML).not.toContain('<br>');

// 2. Should contain the text properly structured
expect(result.dom.innerHTML).toContain('t');

// 3. The DOM should be as minimal as possible for single character
expect(result.dom.innerHTML.length).toBeLessThan(200); // Reasonable upper bound
});

it('should work correctly with markdown in single-line context', () => {
// Test that our fix doesn't break markdown functionality
const markdownText = '*bold* normal `code`';
const ranges = parseExpensiMark(markdownText);

const result = parseRangesToHTMLNodes(markdownText, ranges, false, {}, true);

// Should have proper markdown elements
expect(result.dom.innerHTML).toContain('data-type="bold"');
expect(result.dom.innerHTML).toContain('data-type="code"');

// But no BR elements
expect(result.dom.innerHTML).not.toContain('<span data-type="br">');
expect(result.dom.innerHTML).not.toContain('<br>');
});
});
});
8 changes: 4 additions & 4 deletions src/web/utils/inputUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function normalizeValue(value: string) {
}

// Parses the HTML structure of a MarkdownTextInputElement to a plain text string. Used for getting the correct value of the input element.
function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition: number, inputType?: string): string {
function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition: number, inputType?: string, isMultiline = true): string {
// Returns the parent of a given node that is higher in the hierarchy and is of a different type than 'text', 'br' or 'line'
function getTopParentNode(node: ChildNode) {
let currentParentNode = node.parentNode;
Expand Down Expand Up @@ -67,7 +67,7 @@ function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition:
if (isTopComponent) {
// When inputType is undefined, the first part of the replaced text is added as a text node.
// Because of it, we need to prevent adding new lines in this case
if (!inputType && node.nodeType === Node.TEXT_NODE) {
if (!isMultiline || (!inputType && node.nodeType === Node.TEXT_NODE)) {
shouldAddNewline = false;
} else {
const firstChild = node.firstChild as HTMLElement;
Expand All @@ -85,9 +85,9 @@ function parseInnerHTMLToText(target: MarkdownTextInputElement, cursorPosition:
text += node.textContent;
} else if (node.nodeName === 'BR') {
const parentNode = getTopParentNode(node);
if (parentNode && parentNode.parentElement?.contentEditable !== 'true' && !!(node as HTMLElement).getAttribute('data-id')) {
if (isMultiline && parentNode && parentNode.parentElement?.contentEditable !== 'true' && !!(node as HTMLElement).getAttribute('data-id')) {
// Parse br elements into newlines only if their parent is not a child of the MarkdownTextInputElement (a paragraph when writing or a div when pasting).
// It prevents adding extra newlines when entering text
// It prevents adding extra newlines when entering text - and now only for multiline inputs
text += '\n';
}
} else {
Expand Down
9 changes: 5 additions & 4 deletions src/web/utils/parserUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ function addTextToElement(node: TreeNode, text: string, isMultiline = true) {
}
}

if (index < lines.length - 1 || (index === 0 && line === '')) {
// Only add BR elements for multiline inputs or when there are actual line breaks
if (isMultiline && (index < lines.length - 1 || (index === 0 && line === ''))) {
addBrElement(node);
}
});
Expand Down Expand Up @@ -228,7 +229,7 @@ function parseRangesToHTMLNodes(
}

if (line.markdownRanges.length === 0) {
addTextToElement(currentParentNode, line.text);
addTextToElement(currentParentNode, line.text, isMultiline);
}

let wasBlockGenerated = false;
Expand All @@ -254,7 +255,7 @@ function parseRangesToHTMLNodes(
// add text before the markdown range
const textBeforeRange = line.text.substring(lastRangeEndIndex - line.start, range.start - line.start);
if (textBeforeRange) {
addTextToElement(currentParentNode, textBeforeRange);
addTextToElement(currentParentNode, textBeforeRange, isMultiline);
}

// create markdown span element
Expand Down Expand Up @@ -284,7 +285,7 @@ function parseRangesToHTMLNodes(
while (currentParentNode.parentNode !== null && nextRangeStartIndex >= currentParentNode.start + currentParentNode.length) {
const textAfterRange = line.text.substring(lastRangeEndIndex - line.start, currentParentNode.start - line.start + currentParentNode.length);
if (textAfterRange) {
addTextToElement(currentParentNode, textAfterRange);
addTextToElement(currentParentNode, textAfterRange, isMultiline);
}
lastRangeEndIndex = currentParentNode.start + currentParentNode.length;
if (currentParentNode.parentNode.type !== 'root') {
Expand Down