fix: prevent documentToMarkdown from mutating source document (#1184)

* fix: prevent documentToMarkdown from mutating source document

convertNodes() was calling pageNode(children: nodes) which triggered
Node's constructor to unlink() each child from its original parent,
orphaning nested list items in the source document.

Fix by deep-copying nodes before passing them to pageNode().

Fixes https://github.com/AppFlowy-IO/AppFlowy/issues/8486

* chore: add regression test for issue #8486

Verifies that documentToMarkdown() does not orphan children of nested
list items in the source document after conversion.
This commit is contained in:
Lucas 2026-02-23 16:01:58 +08:00 • committed by GitHub
commit 301d0fa579
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 35 additions and 1 deletions

View file

@ -34,7 +34,9 @@ class DocumentMarkdownEncoder extends Converter<Document, String> {
List<Node> nodes, {
bool withIndent = false,
}) {
final result = convert(Document(root: pageNode(children: nodes)));
final result = convert(
Document(root: pageNode(children: nodes.map((n) => n.deepCopy()))),
);
if (result.isNotEmpty && withIndent) {
return result
.split('\n')

View file

@ -57,6 +57,38 @@ void main() {
expect(nodes[0].delta?.toPlainText(), 'This is the first line');
expect(nodes[1].attributes['url'], 'https://example.com/image.png');
});
// Regression test for https://github.com/AppFlowy-IO/AppFlowy/issues/8486
// documentToMarkdown() must not mutate the source document when processing
// nested (indented) list items.
test('documentToMarkdown does not orphan children of nested list items',
() {
final childNode = bulletedListNode(text: 'Child item');
final parentNode = bulletedListNode(
text: 'Parent item',
children: [childNode],
);
final document = Document(root: pageNode(children: [parentNode]));
// Verify initial structure: parent has 1 child.
expect(document.root.children.length, 1);
expect(document.root.children.first.children.length, 1);
final markdown = documentToMarkdown(document);
// The markdown output should be correct.
expect(markdown, '* Parent item\n\t* Child item\n');
// After conversion, the source document must be unchanged.
// Before the fix, convertNodes() called pageNode(children: nodes) which
// invoked unlink() on each child, removing them from their original parent.
expect(
document.root.children.first.children.length,
1,
reason:
'documentToMarkdown() must not remove children from the source document',
);
});
});
}