Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
10027d5
Add Indent Transformer
lytion Sep 22, 2025
e9aacd5
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
lytion Sep 22, 2025
c02ef17
Remove unused import
lytion Sep 22, 2025
e2a6712
Fix prettier error
lytion Sep 23, 2025
fe76540
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
lytion Sep 23, 2025
4c2a215
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
SimonBct Oct 6, 2025
f93caa9
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
SimonBct Oct 6, 2025
c93f030
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
lytion Oct 7, 2025
8cec7f1
Separate INDENT transformer from default one
SimonBct Nov 4, 2025
4eca17d
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
SimonBct Nov 4, 2025
81deb22
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
lytion Nov 7, 2025
6c6b36d
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
lytion Nov 12, 2025
b8a5dcd
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
lytion Nov 14, 2025
6bec51f
Refactor key comparison
lytion Nov 21, 2025
74fec50
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
lytion Nov 24, 2025
18faefc
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
lytion Nov 27, 2025
fdc8092
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
lytion Feb 10, 2026
3754f15
Merge branch 'main' into lytion/indet-lost-when-switching-markdown
lytion Feb 11, 2026
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
1 change: 1 addition & 0 deletions packages/lexical-markdown/flow/LexicalMarkdown.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ declare export var CHECK_LIST: ElementTransformer;
declare export var ORDERED_LIST: ElementTransformer;

declare export var LINK: TextMatchTransformer;
declare export var INDENT: TextMatchTransformer;

declare export var TRANSFORMERS: Array<Transformer>;
declare export var ELEMENT_TRANSFORMERS: Array<ElementTransformer>;
Expand Down
34 changes: 34 additions & 0 deletions packages/lexical-markdown/src/MarkdownTransformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
$createLineBreakNode,
$createTextNode,
$getState,
$isParagraphNode,
$isTextNode,
$setState,
createState,
ElementNode,
Expand Down Expand Up @@ -216,6 +218,7 @@ const TAG_START_REGEX = /^<[a-z_][\w-]*(?:\s[^<>]*)?\/?>/i;
const TAG_END_REGEX = /^<\/[a-z_][\w-]*\s*>/i;
const ENDS_WITH = (regex: RegExp) =>
new RegExp(`(?:${regex.source})$`, regex.flags);
const INDENT_REGEX = /^\t+/;

export const listMarkerState = createState('mdListMarker', {
parse: (v) => (typeof v === 'string' && /^[-*+]$/.test(v) ? v : '-'),
Expand Down Expand Up @@ -707,6 +710,37 @@ export const LINK: TextMatchTransformer = {
type: 'text-match',
};

export const INDENT: TextMatchTransformer = {
dependencies: [],
export: (node, exportChildren, exportFormat) => {
const parentNode = node.getParent();
const textContent = node.getTextContent();
if (
!$isParagraphNode(parentNode) ||
!$isTextNode(node) ||
!node.is(parentNode.getFirstChild())
) {
return null;
}
const indent = parentNode.getIndent();
const textWithFormat = exportFormat(node, textContent);
return textWithFormat ? '\t'.repeat(indent) + textWithFormat : null;
},
importRegExp: INDENT_REGEX,
regExp: INDENT_REGEX,
replace: (textNode, match) => {
const [indents] = match;
const parentNode = textNode.getParent();
if (!parentNode || !$isParagraphNode(parentNode)) {
return;
}
parentNode.setIndent(indents.length);
textNode.setTextContent(textNode.getTextContent().replace(/^\t+/, ''));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like maybe there's a bug elsewhere because tabs are usually supposed to use TabNode

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that this PR was updated with main but this comment hasn't been addressed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I missed the comment.
I think you're right, when clicking on "left indent" button in normal mode, there's no INDENT_CONTENT_COMMAND which I believe should happen and therefore should add a TabNode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to make sure we're on the same page.
From what I see:
Markdown use \t for indent and tab key.
CodeHighlight convert \t into TabNode when converting to markdown.
Nodes use __indent and TabNode.
Here the transformer set the correct number of indent and remove all \t found from the start of the line.
If I understand correctly, what you're talking about is to convert all \t into TabNode even if CodeHighlight is disabled.
I can create a TAB transformer that does that, but maybe that's a separate issue

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally speaking there should never be '\t' or a '\n' in a TextNode, anything that creates a TextNode with those characters in the contents probably has a bug. The solution isn't a transformer, it's to fix the code that's creating those TextNodes. I don't think there is currently a helper to do this other than RangeSelection.insertRawText but one could be added to make this a little easier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a quick and dirty refactor but is this the direction you are thinking about ?

diff --git a/packages/lexical-markdown/src/MarkdownImport.ts b/packages/lexical-markdown/src/MarkdownImport.ts
index d64037d91..851707205 100644
--- a/packages/lexical-markdown/src/MarkdownImport.ts
+++ b/packages/lexical-markdown/src/MarkdownImport.ts

+// Copy of LexicalSelection insertRawText function
+function insertRawText(text: string): LexicalNode[] {
+  const parts = text.split(/(\r?\n|\t)/);
+  const nodes = [];
+  const length = parts.length;
+  for (let i = 0; i < length; i++) {
+  const part = parts[i];
+  if (part === '\n' || part === '\r\n') {
+    nodes.push($createLineBreakNode());
+  } else if (part === '\t') {
+    nodes.push($createTabNode());
+  } else {
+    nodes.push($createTextNode(part));
+  }
+}
+  return nodes;
+}
+
 function $importBlocks(
   lineText: string,
   rootNode: ElementNode,
@@ -227,27 +248,39 @@ function $importBlocks(
   textMatchTransformers: Array<TextMatchTransformer>,
   shouldPreserveNewLines: boolean,
 ) {
-  const textNode = $createTextNode(lineText);
+  const nodes = insertRawText(lineText);
   const elementNode = $createParagraphNode();
-  elementNode.append(textNode);
+  elementNode.append(...nodes);
   rootNode.append(elementNode);
 
   for (const {regExp, replace} of elementTransformers) {
-    const match = lineText.match(regExp);
-
-    if (match) {
-      textNode.setTextContent(lineText.slice(match[0].length));
-      if (replace(elementNode, [textNode], match, true) !== false) {
+    for (const node of nodes) {
+      if (!$isTextNode(node)) {
         break;
       }
+      const match = node.getTextContent().match(regExp);
+
+      if (match) {
+        node.setTextContent(lineText.slice(match[0].length));
+        if (replace(elementNode, [node], match, true) !== false) {
+          break;
+        }
+      }
     }
   }
 
-  importTextTransformers(
-    textNode,
-    textFormatTransformersIndex,
-    textMatchTransformers,
-  );
+  for (const node of nodes) {
+    if (!$isTextNode(node)) {
+      break;
+    }
+    // TODO: Do this for TabNode too (maybe Line
+    importTextTransformers(
+      node,
+      textFormatTransformersIndex,
+      textMatchTransformers,
+    );
+  }
+

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard to say without a thorough audit of that code but probably something like that. It might be the case that it’s better to fix it up afterwards in the markdown case depending on how that code works.

return textNode;
},
type: 'text-match',
};

export const ELEMENT_TRANSFORMERS: Array<ElementTransformer> = [
HEADING,
QUOTE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {describe, expect, it} from 'vitest';
import {
$convertFromMarkdownString,
$convertToMarkdownString,
INDENT,
LINK,
registerMarkdownShortcuts,
TextMatchTransformer,
Expand Down Expand Up @@ -716,6 +717,26 @@ describe('Markdown', () => {
md: '**text [link](https://lexical.dev)**',
mdAfterExport: '**text&#32;**[**link**](https://lexical.dev)',
},
{
customTransformers: [INDENT],
html: '<p style="padding-inline-start: 40px;"><span style="white-space: pre-wrap;">Hello Word</span></p>',
md: '\tHello Word',
},
{
customTransformers: [INDENT],
html: '<p style="padding-inline-start: 80px;"><span style="white-space: pre-wrap;">Hello Word</span></p>',
md: '\t\tHello Word',
},
{
customTransformers: [INDENT],
html: '<p><span style="white-space: pre-wrap;">Hello\t Word</span></p>',
md: 'Hello\t Word',
},
{
customTransformers: [INDENT],
html: '<p style="padding-inline-start: 40px;"><i><em style="white-space: pre-wrap;">Hello</em></i><span style="white-space: pre-wrap;"> Word</span></p>',
md: '\t*Hello* Word',
},
{
html: '<p><i><em style="white-space: pre-wrap;">text</em></i><i><b><strong style="white-space: pre-wrap;">text</strong></b></i></p>',
md: '*text**text***',
Expand Down
2 changes: 2 additions & 0 deletions packages/lexical-markdown/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
ELEMENT_TRANSFORMERS,
HEADING,
HIGHLIGHT,
INDENT,
INLINE_CODE,
ITALIC_STAR,
ITALIC_UNDERSCORE,
Expand Down Expand Up @@ -94,6 +95,7 @@ export {
type ElementTransformer,
HEADING,
HIGHLIGHT,
INDENT,
INLINE_CODE,
ITALIC_STAR,
ITALIC_UNDERSCORE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
CHECK_LIST,
ELEMENT_TRANSFORMERS,
ElementTransformer,
INDENT,
MULTILINE_ELEMENT_TRANSFORMERS,
TEXT_FORMAT_TRANSFORMERS,
TEXT_MATCH_TRANSFORMERS,
Expand Down Expand Up @@ -315,6 +316,7 @@ export const PLAYGROUND_TRANSFORMERS: Array<Transformer> = [
EQUATION,
TWEET,
CHECK_LIST,
INDENT,
...ELEMENT_TRANSFORMERS,
...MULTILINE_ELEMENT_TRANSFORMERS,
...TEXT_FORMAT_TRANSFORMERS,
Expand Down
Loading