Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lexical-markdown] Bug Fix: preserve the order of markdown tags for markdown combinations, and close the tags when the outmost tag is closed #5758

Merged
merged 4 commits into from
Dec 16, 2024
Merged
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
56 changes: 46 additions & 10 deletions packages/lexical-markdown/src/MarkdownExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ function exportChildren(
): string {
const output = [];
const children = node.getChildren();
// keep track of unclosed tags from the very beginning
const unclosedTags: {format: TextFormatType; tag: string}[] = [];

mainLoop: for (const child of children) {
for (const transformer of textMatchTransformers) {
Expand All @@ -124,7 +126,12 @@ function exportChildren(
textMatchTransformers,
),
(textNode, textContent) =>
exportTextFormat(textNode, textContent, textTransformersIndex),
exportTextFormat(
textNode,
textContent,
textTransformersIndex,
unclosedTags,
),
);

if (result != null) {
Expand All @@ -137,7 +144,12 @@ function exportChildren(
output.push('\n');
} else if ($isTextNode(child)) {
output.push(
exportTextFormat(child, child.getTextContent(), textTransformersIndex),
exportTextFormat(
child,
child.getTextContent(),
textTransformersIndex,
unclosedTags,
),
);
} else if ($isElementNode(child)) {
// empty paragraph returns ""
Expand All @@ -156,39 +168,63 @@ function exportTextFormat(
node: TextNode,
textContent: string,
textTransformers: Array<TextFormatTransformer>,
// unclosed tags include the markdown tags that haven't been closed yet, and their associated formats
unclosedTags: Array<{format: TextFormatType; tag: string}>,
): string {
// This function handles the case of a string looking like this: " foo "
// Where it would be invalid markdown to generate: "** foo **"
// We instead want to trim the whitespace out, apply formatting, and then
// bring the whitespace back. So our returned string looks like this: " **foo** "
const frozenString = textContent.trim();
let output = frozenString;
// the opening tags to be added to the result
let openingTags = '';
// the closing tags to be added to the result
let closingTags = '';

const prevNode = getTextSibling(node, true);
const nextNode = getTextSibling(node, false);

const applied = new Set();

for (const transformer of textTransformers) {
const format = transformer.format[0];
const tag = transformer.tag;

// dedup applied formats
if (hasFormat(node, format) && !applied.has(format)) {
// Multiple tags might be used for the same format (*, _)
applied.add(format);
// Prevent adding opening tag is already opened by the previous sibling
const previousNode = getTextSibling(node, true);

if (!hasFormat(previousNode, format)) {
output = tag + output;
// append the tag to openningTags, if it's not applied to the previous nodes,
// or the nodes before that (which would result in an unclosed tag)
if (
!hasFormat(prevNode, format) ||
!unclosedTags.find((element) => element.tag === tag)
) {
unclosedTags.push({format, tag});
openingTags += tag;
}
}
}

// Prevent adding closing tag if next sibling will do it
const nextNode = getTextSibling(node, false);
// close any tags in the same order they were applied, if necessary
for (let i = 0; i < unclosedTags.length; i++) {
// prevent adding closing tag if next sibling will do it
if (hasFormat(nextNode, unclosedTags[i].format)) {
continue;
}

if (!hasFormat(nextNode, format)) {
output += tag;
while (unclosedTags.length > i) {
const unclosedTag = unclosedTags.pop();
if (unclosedTag && typeof unclosedTag.tag === 'string') {
closingTags += unclosedTag.tag;
}
}
break;
}

output = openingTags + output + closingTags;
// Replace trimmed version of textContent ensuring surrounding whitespace is not modified
return textContent.replace(frozenString, () => output);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,15 @@ describe('Markdown', () => {
},
{
html: '<p><span style="white-space: pre-wrap;">Hello </span><s><i><b><strong style="white-space: pre-wrap;">world</strong></b></i></s><span style="white-space: pre-wrap;">!</span></p>',
md: 'Hello ~~***world***~~!',
md: 'Hello ***~~world~~***!',
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 changed the expected markdown here because I rearranged the order of tags in the markdownExport. The order change was to address edge cases, such as the one from L146-148, where the very first opening tag needs to be closed as the last one.

},
{
html: '<p><b><strong style="white-space: pre-wrap;">Hello </strong></b><s><b><strong style="white-space: pre-wrap;">world</strong></b></s><span style="white-space: pre-wrap;">!</span></p>',
md: '**Hello ~~world~~**!',
},
{
html: '<p><s><b><strong style="white-space: pre-wrap;">Hello </strong></b></s><s><i><b><strong style="white-space: pre-wrap;">world</strong></b></i></s><s><span style="white-space: pre-wrap;">!</span></s></p>',
md: '**~~Hello *world*~~**~~!~~',
},
{
html: '<p><i><em style="white-space: pre-wrap;">Hello </em></i><i><b><strong style="white-space: pre-wrap;">world</strong></b></i><i><em style="white-space: pre-wrap;">!</em></i></p>',
Expand Down
Loading