diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index 864c6d90..f5e77a99 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -355,7 +355,7 @@ const MarkdownTextInput = React.forwardRef { + 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(''); + expect(result.dom.innerHTML).not.toContain('
'); + }); + + 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('

{ + 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(''); + expect(result.dom.innerHTML).not.toContain('
'); + 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(''); + } + }); + + 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(''); + }); + }); + + 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(''); + expect(result.dom.innerHTML).not.toContain('
'); + + // 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(''); + expect(result.dom.innerHTML).not.toContain('
'); + }); + }); +}); diff --git a/src/web/utils/inputUtils.ts b/src/web/utils/inputUtils.ts index 8bc59817..dba864d7 100644 --- a/src/web/utils/inputUtils.ts +++ b/src/web/utils/inputUtils.ts @@ -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; @@ -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; @@ -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 { diff --git a/src/web/utils/parserUtils.ts b/src/web/utils/parserUtils.ts index 58a12639..ce9d8428 100644 --- a/src/web/utils/parserUtils.ts +++ b/src/web/utils/parserUtils.ts @@ -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); } }); @@ -228,7 +229,7 @@ function parseRangesToHTMLNodes( } if (line.markdownRanges.length === 0) { - addTextToElement(currentParentNode, line.text); + addTextToElement(currentParentNode, line.text, isMultiline); } let wasBlockGenerated = false; @@ -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 @@ -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') {