From 4ee2d830b5c917faf884cf9985124526a807f760 Mon Sep 17 00:00:00 2001 From: vnniz Date: Tue, 29 Aug 2023 18:17:13 +0700 Subject: [PATCH 01/10] Header alignments markdown serializer --- .../src/document_to_markdown_serializer.dart | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart index 984f0ec754..7336c810d4 100644 --- a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart +++ b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart @@ -25,6 +25,7 @@ String serializeDocumentToMarkdown( const HorizontalRuleNodeSerializer(), const ListItemNodeSerializer(), const TaskNodeSerializer(), + HeaderNodeSerializer(syntax), ParagraphNodeSerializer(syntax), ]; @@ -362,3 +363,76 @@ class AttributedTextMarkdownSerializer extends AttributionVisitor { return ""; } } + +/// [DocumentNodeMarkdownSerializer] for serializing [ParagraphNode]s as standard Markdown +/// paragraphs. +/// +/// Includes support for headers, blockquotes, and code blocks. +class HeaderNodeSerializer extends NodeTypedDocumentNodeMarkdownSerializer { + const HeaderNodeSerializer(this.markdownSyntax); + + final MarkdownSyntax markdownSyntax; + + @override + String? serialize(Document document, DocumentNode node) { + if (node is! ParagraphNode) { + return null; + } + + // Only serialize this node when this is a header node + final Attribution? blockType = node.getMetadataValue('blockType'); + if (blockType == header1Attribution || + blockType == header2Attribution || + blockType == header3Attribution || + blockType == header4Attribution || + blockType == header5Attribution || + blockType == header6Attribution) { + // this node is a header node + } else { + // this node is not a header node + return null; + } + + return doSerialization(document, node); + } + + @override + String doSerialization(Document document, ParagraphNode node) { + final buffer = StringBuffer(); + + final Attribution? blockType = node.getMetadataValue('blockType'); + final String? textAlign = node.getMetadataValue('textAlign'); + + // Left alignment is the default, so there is no need to add the alignment token. + if (markdownSyntax == MarkdownSyntax.superEditor && textAlign != null && textAlign != 'left') { + final alignmentToken = _convertAlignmentToMarkdown(textAlign); + if (alignmentToken != null) { + buffer.writeln(alignmentToken); + } + } + + if (blockType == header1Attribution) { + buffer.write('# ${node.text.toMarkdown()}'); + } else if (blockType == header2Attribution) { + buffer.write('## ${node.text.toMarkdown()}'); + } else if (blockType == header3Attribution) { + buffer.write('### ${node.text.toMarkdown()}'); + } else if (blockType == header4Attribution) { + buffer.write('#### ${node.text.toMarkdown()}'); + } else if (blockType == header5Attribution) { + buffer.write('##### ${node.text.toMarkdown()}'); + } else if (blockType == header6Attribution) { + buffer.write('###### ${node.text.toMarkdown()}'); + } + + // We're not at the end of the document yet. Add a blank line after the + // paragraph so that we can tell the difference between separate + // paragraphs vs. newlines within a single paragraph. + final nodeIndex = document.getNodeIndexById(node.id); + if (nodeIndex != document.nodes.length - 1) { + buffer.writeln(); + } + + return buffer.toString(); + } +} From c3a8370edb059009c198eb42096755736b627837 Mon Sep 17 00:00:00 2001 From: vnniz Date: Tue, 29 Aug 2023 18:17:34 +0700 Subject: [PATCH 02/10] Header alignments markdown parser --- .../lib/src/markdown_to_document_parsing.dart | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/super_editor_markdown/lib/src/markdown_to_document_parsing.dart b/super_editor_markdown/lib/src/markdown_to_document_parsing.dart index 8a9927d0cd..40cef006d8 100644 --- a/super_editor_markdown/lib/src/markdown_to_document_parsing.dart +++ b/super_editor_markdown/lib/src/markdown_to_document_parsing.dart @@ -25,8 +25,10 @@ MutableDocument deserializeMarkdownToDocument( final markdownDoc = md.Document( blockSyntaxes: [ ...customBlockSyntax, - if (syntax == MarkdownSyntax.superEditor) // + if (syntax == MarkdownSyntax.superEditor) ...[ + _HeaderWithAlignmentSyntax(), _ParagraphWithAlignmentSyntax(), + ], _EmptyLinePreservingParagraphSyntax(), _TaskSyntax(), ], @@ -197,12 +199,14 @@ class _MarkdownToDocument implements md.NodeVisitor { break; } + final textAlign = element.attributes['textAlign']; _content.add( ParagraphNode( id: Editor.createNodeId(), text: _parseInlineText(element), metadata: { 'blockType': headerAttribution, + 'textAlign': textAlign, }, ), ); @@ -694,6 +698,82 @@ class _TaskSyntax extends md.BlockSyntax { } } +/// Parses a header preceded by an alignment token. +class _HeaderWithAlignmentSyntax extends md.BlockSyntax { + /// This pattern matches the text aligment notation. + /// + /// Possible values are `:---`, `:---:` and `---:` + static final _alignmentNotationPattern = RegExp(r'^:-{3}|:-{3}:|-{3}:$'); + + /// Use internal HeaderSyntax + final _headerSyntax = const md.HeaderSyntax(); + + @override + RegExp get pattern => RegExp(''); + + @override + bool canEndBlock(md.BlockParser parser) => false; + + @override + bool canParse(md.BlockParser parser) { + // + if (!_alignmentNotationPattern.hasMatch(parser.current)) { + return false; + } + + final nextLine = parser.peek(1); + + // We found a match for a paragraph alignment token. However, the alignment token is the last + // line of content in the document. Therefore, it's not really a paragraph alignment token, and we + // should treat it as regular content. + if (nextLine == null) return false; + + // Only parse if the next line is header + if (!_headerSyntax.pattern.hasMatch(nextLine)) { + return false; + } + + return true; + } + + @override + md.Node? parse(md.BlockParser parser) { + final match = _alignmentNotationPattern.firstMatch(parser.current); + + // We've parsed the alignment token on the current line. We know a paragraph starts on the + // next line. Move the parser to the next line so that we can parse the paragraph. + parser.advance(); + + final headerNode = _headerSyntax.parse(parser); + + // Use markdown alignment converter from [_ParagraphWithAlignmentSyntax] + if (headerNode is md.Element) { + headerNode.attributes.addAll({'textAlign': _convertMarkdownAlignmentTokenToSuperEditorAlignment(match!.input)}); + } + + return headerNode; + } + + /// Converts a markdown alignment token to the textAlign metadata used to configure + /// the [ParagraphNode] alignment. + String _convertMarkdownAlignmentTokenToSuperEditorAlignment(String alignmentToken) { + switch (alignmentToken) { + case ':---': + return 'left'; + case ':---:': + return 'center'; + case '---:': + return 'right'; + case '-::-': + return 'justify'; + // As we already check that the input matches the notation, + // we shouldn't reach this point. + default: + return 'left'; + } + } +} + /// Matches empty lines or lines containing only whitespace. final _blankLinePattern = RegExp(r'^(?:[ \t]*)$'); From f0ddf9fee81582dd02d1534ea7772dff25cb8d86 Mon Sep 17 00:00:00 2001 From: vnniz Date: Tue, 29 Aug 2023 18:20:29 +0700 Subject: [PATCH 03/10] Header alignments markdown tests --- .../test/super_editor_markdown_test.dart | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/super_editor_markdown/test/super_editor_markdown_test.dart b/super_editor_markdown/test/super_editor_markdown_test.dart index 11c18a0327..72f435573a 100644 --- a/super_editor_markdown/test/super_editor_markdown_test.dart +++ b/super_editor_markdown/test/super_editor_markdown_test.dart @@ -1,5 +1,5 @@ -import 'package:super_editor/super_editor.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:super_editor/super_editor.dart'; import 'package:super_editor_markdown/super_editor_markdown.dart'; void main() { @@ -32,6 +32,64 @@ void main() { expect(serializeDocumentToMarkdown(doc), '###### My Header'); }); + test('header with left alignment', () { + final doc = MutableDocument(nodes: [ + ParagraphNode( + id: '1', + text: AttributedText('Header1'), + metadata: { + 'textAlign': 'left', + 'blockType': header1Attribution, + }, + ), + ]); + // Even when using superEditor markdown syntax, which has support + // for text alignment, we don't add an alignment token when + // the paragraph is left-aligned. + // Paragraphs are left-aligned by default, so it isn't necessary + // to serialize the alignment token. + expect(serializeDocumentToMarkdown(doc), '# Header1'); + }); + test('header with center alignment', () { + final doc = MutableDocument(nodes: [ + ParagraphNode( + id: '1', + text: AttributedText('Header1'), + metadata: { + 'textAlign': 'center', + 'blockType': header1Attribution, + }, + ), + ]); + expect(serializeDocumentToMarkdown(doc), ':---:\n# Header1'); + }); + test('header with right alignment', () { + final doc = MutableDocument(nodes: [ + ParagraphNode( + id: '1', + text: AttributedText('Header1'), + metadata: { + 'textAlign': 'right', + 'blockType': header1Attribution, + }, + ), + ]); + expect(serializeDocumentToMarkdown(doc), '---:\n# Header1'); + }); + test('header with justify alignment', () { + final doc = MutableDocument(nodes: [ + ParagraphNode( + id: '1', + text: AttributedText('Header1'), + metadata: { + 'textAlign': 'justify', + 'blockType': header1Attribution, + }, + ), + ]); + expect(serializeDocumentToMarkdown(doc), '-::-\n# Header1'); + }); + test('header with styles', () { final doc = MutableDocument(nodes: [ ParagraphNode( From b89ff9a3aa98c4297c3b1264a6e4da9a2c548662 Mon Sep 17 00:00:00 2001 From: vnniz Date: Wed, 30 Aug 2023 15:36:47 +0700 Subject: [PATCH 04/10] Add justify alignment to the _alignmentNotationPattern --- .../lib/src/markdown_to_document_parsing.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/super_editor_markdown/lib/src/markdown_to_document_parsing.dart b/super_editor_markdown/lib/src/markdown_to_document_parsing.dart index 40cef006d8..c9f85a5d39 100644 --- a/super_editor_markdown/lib/src/markdown_to_document_parsing.dart +++ b/super_editor_markdown/lib/src/markdown_to_document_parsing.dart @@ -700,10 +700,10 @@ class _TaskSyntax extends md.BlockSyntax { /// Parses a header preceded by an alignment token. class _HeaderWithAlignmentSyntax extends md.BlockSyntax { - /// This pattern matches the text aligment notation. + /// This pattern matches the text alignment notation. /// - /// Possible values are `:---`, `:---:` and `---:` - static final _alignmentNotationPattern = RegExp(r'^:-{3}|:-{3}:|-{3}:$'); + /// Possible values are `:---`, `:---:`, `---:` and `-::-`. + static final _alignmentNotationPattern = RegExp(r'^:-{3}|:-{3}:|-{3}:|-::-$'); /// Use internal HeaderSyntax final _headerSyntax = const md.HeaderSyntax(); From 7194a935473af9daea8883b9f3c76559b8e2cdeb Mon Sep 17 00:00:00 2001 From: vnniz Date: Sun, 10 Sep 2023 13:00:44 +0700 Subject: [PATCH 05/10] Update the HeaderNodeSerializer header comment --- .../lib/src/document_to_markdown_serializer.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart index 7336c810d4..f834d84ef6 100644 --- a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart +++ b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart @@ -364,10 +364,7 @@ class AttributedTextMarkdownSerializer extends AttributionVisitor { } } -/// [DocumentNodeMarkdownSerializer] for serializing [ParagraphNode]s as standard Markdown -/// paragraphs. -/// -/// Includes support for headers, blockquotes, and code blocks. +/// [DocumentNodeMarkdownSerializer] for serializing [ParagraphNode]s as headers class HeaderNodeSerializer extends NodeTypedDocumentNodeMarkdownSerializer { const HeaderNodeSerializer(this.markdownSyntax); From a86786dfd3ab096869d5f2376f48bd44c29c4a4e Mon Sep 17 00:00:00 2001 From: vnniz Date: Sun, 10 Sep 2023 13:10:03 +0700 Subject: [PATCH 06/10] Make the condition clearer --- .../lib/src/document_to_markdown_serializer.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart index f834d84ef6..212a881a2d 100644 --- a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart +++ b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart @@ -378,15 +378,14 @@ class HeaderNodeSerializer extends NodeTypedDocumentNodeMarkdownSerializer Date: Mon, 11 Sep 2023 20:10:04 +0700 Subject: [PATCH 07/10] Add tests for parsing a header with alignment, also updating the 'example doc' and 'example doc 1' --- .../test/super_editor_markdown_test.dart | 85 ++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/super_editor_markdown/test/super_editor_markdown_test.dart b/super_editor_markdown/test/super_editor_markdown_test.dart index 72f435573a..77acce10ed 100644 --- a/super_editor_markdown/test/super_editor_markdown_test.dart +++ b/super_editor_markdown/test/super_editor_markdown_test.dart @@ -616,6 +616,26 @@ with multiple lines text: AttributedText('Example Doc'), metadata: {'blockType': header1Attribution}, ), + ParagraphNode( + id: Editor.createNodeId(), + text: AttributedText('Example Doc With Left Alignment'), + metadata: {'blockType': header1Attribution, 'textAlign': 'left'}, + ), + ParagraphNode( + id: Editor.createNodeId(), + text: AttributedText('Example Doc With Center Alignment'), + metadata: {'blockType': header1Attribution, 'textAlign': 'center'}, + ), + ParagraphNode( + id: Editor.createNodeId(), + text: AttributedText('Example Doc With Right Alignment'), + metadata: {'blockType': header1Attribution, 'textAlign': 'right'}, + ), + ParagraphNode( + id: Editor.createNodeId(), + text: AttributedText('Example Doc With Justify Alignment'), + metadata: {'blockType': header1Attribution, 'textAlign': 'justify'}, + ), HorizontalRuleNode(id: Editor.createNodeId()), ParagraphNode( id: Editor.createNodeId(), @@ -729,6 +749,38 @@ with multiple lines expect((header6Doc.nodes.first as ParagraphNode).getMetadataValue('blockType'), header6Attribution); }); + test('header with left alignment', () { + final headerLeftAlignment1 = deserializeMarkdownToDocument(':---\n# Header 1'); + final header = headerLeftAlignment1.nodes.first as ParagraphNode; + expect(header.getMetadataValue('blockType'), header1Attribution); + expect(header.getMetadataValue('textAlign'), 'left'); + expect(header.text.text, 'Header 1'); + }); + + test('header with center alignment', () { + final headerLeftAlignment1 = deserializeMarkdownToDocument(':---:\n# Header 1'); + final header = headerLeftAlignment1.nodes.first as ParagraphNode; + expect(header.getMetadataValue('blockType'), header1Attribution); + expect(header.getMetadataValue('textAlign'), 'center'); + expect(header.text.text, 'Header 1'); + }); + + test('header with right alignment', () { + final headerLeftAlignment1 = deserializeMarkdownToDocument('---:\n# Header 1'); + final header = headerLeftAlignment1.nodes.first as ParagraphNode; + expect(header.getMetadataValue('blockType'), header1Attribution); + expect(header.getMetadataValue('textAlign'), 'right'); + expect(header.text.text, 'Header 1'); + }); + + test('header with justify alignment', () { + final headerLeftAlignment1 = deserializeMarkdownToDocument('-::-\n# Header 1'); + final header = headerLeftAlignment1.nodes.first as ParagraphNode; + expect(header.getMetadataValue('blockType'), header1Attribution); + expect(header.getMetadataValue('textAlign'), 'justify'); + expect(header.text.text, 'Header 1'); + }); + test('blockquote', () { final blockquoteDoc = deserializeMarkdownToDocument('> This is a blockquote'); @@ -943,7 +995,7 @@ with multiple lines test('example doc 1', () { final document = deserializeMarkdownToDocument(exampleMarkdownDoc1); - expect(document.nodes.length, 21); + expect(document.nodes.length, 26); expect(document.nodes[0], isA()); expect((document.nodes[0] as ParagraphNode).getMetadataValue('blockType'), header1Attribution); @@ -974,7 +1026,25 @@ with multiple lines expect(document.nodes[19], isA()); - expect(document.nodes[20], isA()); + expect(document.nodes[20], isA()); + + expect(document.nodes[21], isA()); + expect((document.nodes[21] as ParagraphNode).getMetadataValue('blockType'), header1Attribution); + expect((document.nodes[21] as ParagraphNode).getMetadataValue('textAlign'), 'left'); + + expect(document.nodes[22], isA()); + expect((document.nodes[22] as ParagraphNode).getMetadataValue('blockType'), header1Attribution); + expect((document.nodes[22] as ParagraphNode).getMetadataValue('textAlign'), 'center'); + + expect(document.nodes[23], isA()); + expect((document.nodes[23] as ParagraphNode).getMetadataValue('blockType'), header1Attribution); + expect((document.nodes[23] as ParagraphNode).getMetadataValue('textAlign'), 'right'); + + expect(document.nodes[24], isA()); + expect((document.nodes[24] as ParagraphNode).getMetadataValue('blockType'), header1Attribution); + expect((document.nodes[24] as ParagraphNode).getMetadataValue('textAlign'), 'justify'); + + expect(document.nodes[25], isA()); }); test('paragraph with strikethrough', () { @@ -1210,5 +1280,16 @@ Another paragraph - [x] Completed task +--- + +:--- +# Example 1 With Left Alignment +:---: +# Example 1 With Center Alignment +---: +# Example 1 With Right Alignment +-::- +# Example 1 With Justify Alignment + The end! '''; From b35e29c49dd2dbd340bafc984861cc978467f8eb Mon Sep 17 00:00:00 2001 From: vnniz Date: Tue, 12 Sep 2023 09:26:00 +0700 Subject: [PATCH 08/10] Format the code and improve the comments --- .../src/document_to_markdown_serializer.dart | 8 ++++++-- .../lib/src/markdown_to_document_parsing.dart | 18 +++++++++++------- .../test/super_editor_markdown_test.dart | 3 +++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart index 212a881a2d..aa3bd198db 100644 --- a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart +++ b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart @@ -364,7 +364,11 @@ class AttributedTextMarkdownSerializer extends AttributionVisitor { } } -/// [DocumentNodeMarkdownSerializer] for serializing [ParagraphNode]s as headers +/// [DocumentNodeMarkdownSerializer] for serializing [ParagraphNode]s as headers. +/// +/// We already had a header parser in [ParagraphNodeSerializer] but without the alignment, +/// so we added [HeaderNodeSerializer] to support the alignment for headers. Therefore, +/// the [HeaderNodeSerializer] **MUST** be put before the [ParagraphNodeSerializer]. class HeaderNodeSerializer extends NodeTypedDocumentNodeMarkdownSerializer { const HeaderNodeSerializer(this.markdownSyntax); @@ -376,7 +380,7 @@ class HeaderNodeSerializer extends NodeTypedDocumentNodeMarkdownSerializer Date: Thu, 21 Sep 2023 21:56:54 +0700 Subject: [PATCH 09/10] Update comments --- .../lib/src/document_to_markdown_serializer.dart | 10 ++++++---- .../lib/src/markdown_to_document_parsing.dart | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart index aa3bd198db..a40b953b9a 100644 --- a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart +++ b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart @@ -364,11 +364,13 @@ class AttributedTextMarkdownSerializer extends AttributionVisitor { } } -/// [DocumentNodeMarkdownSerializer] for serializing [ParagraphNode]s as headers. +/// [DocumentNodeMarkdownSerializer], which serializes Markdown headers to +/// [ParagraphNode]s with an appropriate header block type, and (optionally) a +/// block alignment. /// -/// We already had a header parser in [ParagraphNodeSerializer] but without the alignment, -/// so we added [HeaderNodeSerializer] to support the alignment for headers. Therefore, -/// the [HeaderNodeSerializer] **MUST** be put before the [ParagraphNodeSerializer]. +/// Headers are represented by `ParagraphNode`s and therefore this serializer must +/// run before a [ParagraphNodeSerializer], so that this serializer can process +/// header-specific details, such as header alignment. class HeaderNodeSerializer extends NodeTypedDocumentNodeMarkdownSerializer { const HeaderNodeSerializer(this.markdownSyntax); diff --git a/super_editor_markdown/lib/src/markdown_to_document_parsing.dart b/super_editor_markdown/lib/src/markdown_to_document_parsing.dart index 69956548eb..ad1c246a20 100644 --- a/super_editor_markdown/lib/src/markdown_to_document_parsing.dart +++ b/super_editor_markdown/lib/src/markdown_to_document_parsing.dart @@ -700,9 +700,9 @@ class _TaskSyntax extends md.BlockSyntax { /// Parses a header preceded by an alignment token. /// -/// We already had a header serializer in [_ParagraphWithAlignmentSyntax] but without the alignment, -/// so we added [_HeaderWithAlignmentSyntax] to support the alignment for headers. Therefore, -/// the [_HeaderWithAlignmentSyntax] **MUST** be put before the [_ParagraphWithAlignmentSyntax]. +/// Headers are represented by `_ParagraphWithAlignmentSyntax`s and therefore +/// this parser must run before a [_ParagraphWithAlignmentSyntax], so that this parser +/// can process header-specific details, such as header alignment. class _HeaderWithAlignmentSyntax extends md.BlockSyntax { /// This pattern matches the text alignment notation. /// From fca7d49b321a53a026791dcc5f8814f302109f72 Mon Sep 17 00:00:00 2001 From: vnniz Date: Fri, 29 Sep 2023 09:54:31 +0700 Subject: [PATCH 10/10] Update a comment --- .../lib/src/document_to_markdown_serializer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart index a40b953b9a..cd08f08c5b 100644 --- a/super_editor_markdown/lib/src/document_to_markdown_serializer.dart +++ b/super_editor_markdown/lib/src/document_to_markdown_serializer.dart @@ -405,7 +405,7 @@ class HeaderNodeSerializer extends NodeTypedDocumentNodeMarkdownSerializer