[lexical-markdown] Feature: Add $convertSelectionToMarkdownString API#8395
[lexical-markdown] Feature: Add $convertSelectionToMarkdownString API#8395etrepum merged 3 commits intofacebook:mainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Not fully sure this is the right approach — feedback on the direction would be appreciated before going further. |
|
I won't have time to read through the code tonight but if it is following the same approach that HTML and JSON export takes then it's probably the right direction. If it's some other way then maybe not. |
|
Thanks — checked how HTML and JSON export handle this, they both use |
488907f to
e7a5a8a
Compare
e7a5a8a to
3771975
Compare
3771975 to
2c3e359
Compare
etrepum
left a comment
There was a problem hiding this comment.
I think there is probably some additional work that should be done to support extractWithChild to support situations like a partial LinkNode selection. I think taking the 'html' path makes the most sense for markdown (not 'clone').
Note that main has changed its prettier config so it might be easiest to copy over the new config, run prettier, commit, and then merge to sync.
|
For Thinking the safest route is building the recursive structure separately for |
|
I think building something separate is probably best, and then we can worry about refactoring or deprecating the current implementation later. Eventually I think the markdown module will be changing a lot for correctness and flexibility reasons, so I don’t think we should care all that much about the existing code other than to keep it backwards compatible. |
2c3e359 to
c34bd2a
Compare
|
Added Went through the transformer implementations looking for edge cases — found one with the list transformer. It iterates children itself and adds To fix the list issue, the transformer callback would need to support skipping nodes, which means changing the Added a test showing the current list behavior so you can see the edge case directly. |
|
Yeah, to that point, maybe it would be better to not add this API until we have better markdown infrastructure to support it. I think in an ideal world we would have separate markdown shortcuts (that operate as you type) and markdown import/export (full document or selection import/export) infrastructure. They are very similar in theory but in practice trying to write code that does the right thing in both scenarios doesn't work very well. It would also be much easier to wire up a fully featured markdown implementation like remark or micromark to the import/export operations. |
|
Makes sense. Would it be useful to keep this as a draft until then, or better to close it entirely? The basic cases (text, formatting, links) work well — just wanted to check if there's value in having it available even with the list limitation. |
|
Up to you whether you keep it draft or close it. I don't think it can work correctly without refactoring other parts as you've noted. Maybe there's a possible refactoring of the existing transformer interface that allows this to work (adding an argument wouldn't necessarily break existing transformers or their types), if you want to look into that it would be a good interim solution for anyone looking for this functionality. |
c34bd2a to
2d67545
Compare
|
Thanks for the pointer on the transformer interface — added an optional The list edge case from earlier is now fixed. |
etrepum
left a comment
There was a problem hiding this comment.
Overall this looks great, a welcome addition to the current API. All of the comments are really just syntax nits and simplifications
| ): void; | ||
|
|
||
| declare export function $convertSelectionToMarkdownString( | ||
| transformers?: Array<Transformer>, |
There was a problem hiding this comment.
Current flow syntax and conventions are much closer to Typescript
| transformers?: Array<Transformer>, | |
| transformers?: Transformer[], |
| normal.select(0, 6); | ||
| const selection = $getSelection(); | ||
| if ($isRangeSelection(selection)) { | ||
| selection.focus.set(bold.getKey(), 4, 'text'); | ||
| } |
There was a problem hiding this comment.
| normal.select(0, 6); | |
| const selection = $getSelection(); | |
| if ($isRangeSelection(selection)) { | |
| selection.focus.set(bold.getKey(), 4, 'text'); | |
| } | |
| const selection = normal.select(0, 6); | |
| selection.focus.set(bold.getKey(), 4, 'text'); |
There was a problem hiding this comment.
or alternatively you could use the NodeCaret APIs:
$setSelectionFromCaretRange(
$getCaretRange(
$getTextPointCaret(normal, 'next', 0),
$getTextPointCaret(bold, 'next', 4),
)
);There was a problem hiding this comment.
Switched to NodeCaret API.
There was a problem hiding this comment.
Went with this approach, cleaner to read.
| p2.append(t2); | ||
| root.append(p1, p2); | ||
| // Select from "First" to "Second" | ||
| t1.select(0, 15); |
There was a problem hiding this comment.
see above for simpler ways to create this RangeSelection
| item2.append(t2); | ||
| list.append(item1, item2); | ||
| root.append(list); | ||
| t1.select(0, 6); |
There was a problem hiding this comment.
see above for simpler ways to create this RangeSelection
| list.append(item1, item2, item3); | ||
| root.append(list); | ||
| // Select only "Item 2" and partial "Item 3" | ||
| t2.select(0, 6); |
There was a problem hiding this comment.
see above for simpler ways to create this RangeSelection
| if (!selection) { | ||
| return ''; | ||
| } | ||
| if ($isRangeSelection(selection) && selection.isCollapsed()) { | ||
| return ''; | ||
| } |
There was a problem hiding this comment.
These can be fused to save a few bytes
| if (!selection) { | |
| return ''; | |
| } | |
| if ($isRangeSelection(selection) && selection.isCollapsed()) { | |
| return ''; | |
| } | |
| if (!selection || $isRangeSelection(selection) && selection.isCollapsed()) { | |
| return ''; | |
| } |
| * inline elements like links. | ||
| */ | ||
| export function createSelectionMarkdownExport( | ||
| transformers: Array<Transformer>, |
There was a problem hiding this comment.
| transformers: Array<Transformer>, | |
| transformers: Transformer[], |
| elementTransformers: Array<ElementTransformer | MultilineElementTransformer>, | ||
| textFormatTransformers: Array<TextFormatTransformer>, | ||
| textMatchTransformers: Array<TextMatchTransformer>, |
There was a problem hiding this comment.
| elementTransformers: Array<ElementTransformer | MultilineElementTransformer>, | |
| textFormatTransformers: Array<TextFormatTransformer>, | |
| textMatchTransformers: Array<TextMatchTransformer>, | |
| elementTransformers: (ElementTransformer | MultilineElementTransformer)[], | |
| textFormatTransformers: TextFormatTransformer[], | |
| textMatchTransformers: TextMatchTransformer[], |
| _node => | ||
| $exportChildrenForSelection( | ||
| _node, |
There was a problem hiding this comment.
underscore prefix is for unused variables, shouldn't really be used when just avoiding shadowing
| _node => | |
| $exportChildrenForSelection( | |
| _node, | |
| node_ => | |
| $exportChildrenForSelection( | |
| node_, |
There was a problem hiding this comment.
Makes sense, fixed.
| textFormatTransformers: Array<TextFormatTransformer>, | ||
| textMatchTransformers: Array<TextMatchTransformer>, | ||
| shouldPreserveNewLines: boolean, | ||
| unclosedTags?: Array<{format: TextFormatType; tag: string}>, | ||
| unclosableTags?: Array<{format: TextFormatType; tag: string}>, |
There was a problem hiding this comment.
| textFormatTransformers: Array<TextFormatTransformer>, | |
| textMatchTransformers: Array<TextMatchTransformer>, | |
| shouldPreserveNewLines: boolean, | |
| unclosedTags?: Array<{format: TextFormatType; tag: string}>, | |
| unclosableTags?: Array<{format: TextFormatType; tag: string}>, | |
| textFormatTransformers: TextFormatTransformer[], | |
| textMatchTransformers: TextMatchTransformer[], | |
| shouldPreserveNewLines: boolean, | |
| unclosedTags?: {format: TextFormatType; tag: string}[], | |
| unclosableTags?: {format: TextFormatType; tag: string}[], |
2d67545 to
eae7cff
Compare
|
All addressed, force-pushed. Thanks for the review! |
Add a new function that converts only the selected content to a markdown string, reusing existing export logic with selection filtering. Handles partial text selection by slicing at anchor/focus offsets and returns empty string for null or collapsed selections. Fixes facebook#6503
eae7cff to
3c1fe6e
Compare
Description
Fixes #6503
Adds a new
$convertSelectionToMarkdownStringfunction that converts only the selected content to a markdown string. Previously,$convertToMarkdownStringcould only convert the entire editor content.What changed
$convertSelectionToMarkdownString(transformers, selection, shouldPreserveNewLines)to@lexical/markdownexportTopLevelElements,exportChildren,exportTextFormat) with selection filteringeditor.read()— no node creation neededUsage
Implementation approach
Rather than creating a separate export pipeline, this extends the existing
createMarkdownExportwith an optionalselectionparameter. When provided, nodes are filtered viaisSelected()and text content is sliced at selection boundaries. This provides a cleaner API for the use case described by @etrepum in #6503, without requiringeditor.update()or manual node reconstruction.Open to alternative approaches if a different direction is preferred.
Test Plan