From 5fab3a7bc880ddf979ccabfbcbad3fea091d7d0b Mon Sep 17 00:00:00 2001 From: Alex Li Date: Tue, 19 Dec 2023 11:43:25 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Introduce=20AlertBlockSyntax=20(#57?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Introduce `CalloutBlockSyntax` * ⚑️ More flex patterns * πŸš€ Produce correct structure * ⚑️ Case-insensitive types * ⚑️ Improve `canParse` * βœ… Fill test cases * Update AUTHORS * 🚚 Rename * πŸ“ Add CHANGELOG * πŸ”₯ Remove `zh` * Update CHANGELOG.md * Update pubspec.yaml * πŸ”– Fix version * πŸ§ͺ Add escape brackets case * ⚑️ const type text map --- AUTHORS | 2 +- CHANGELOG.md | 5 +- lib/markdown.dart | 1 + .../block_syntaxes/alert_block_syntax.dart | 107 ++++++++++++++++++ lib/src/extension_set.dart | 2 + lib/src/patterns.dart | 9 ++ lib/src/version.dart | 2 +- pubspec.yaml | 2 +- test/extensions/alert_extension.unit | 94 +++++++++++++++ test/markdown_test.dart | 7 +- 10 files changed, 225 insertions(+), 6 deletions(-) create mode 100644 lib/src/block_syntaxes/alert_block_syntax.dart create mode 100644 test/extensions/alert_extension.unit diff --git a/AUTHORS b/AUTHORS index 7f87fc43..ca5b46bf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,4 +10,4 @@ Daniel Schubert Jirka DanΔ›k Seth Westphal Tim Maffett - +Alex Li diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fcfd579..fe714142 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ -## 7.1.2-wip +## 7.2.0-wip * Require Dart `^3.1.0`. * Update all CommonMark specification links to 0.30. -* Fix beginning of line detection in `AutolinkExtensionSyntax`. +* Fix beginning of line detection in `AutolinkExtensionSyntax`. +* Add a new syntax `AlertBlockSyntax` to parse GitHub Alerts ## 7.1.1 diff --git a/lib/markdown.dart b/lib/markdown.dart index 409baaf6..a09e763a 100644 --- a/lib/markdown.dart +++ b/lib/markdown.dart @@ -42,6 +42,7 @@ import 'src/version.dart'; export 'src/ast.dart'; export 'src/block_parser.dart'; +export 'src/block_syntaxes/alert_block_syntax.dart'; export 'src/block_syntaxes/block_syntax.dart'; export 'src/block_syntaxes/blockquote_syntax.dart'; export 'src/block_syntaxes/code_block_syntax.dart'; diff --git a/lib/src/block_syntaxes/alert_block_syntax.dart b/lib/src/block_syntaxes/alert_block_syntax.dart new file mode 100644 index 00000000..be28ca93 --- /dev/null +++ b/lib/src/block_syntaxes/alert_block_syntax.dart @@ -0,0 +1,107 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../ast.dart'; +import '../block_parser.dart'; +import '../line.dart'; +import '../patterns.dart'; +import 'block_syntax.dart'; +import 'code_block_syntax.dart'; +import 'paragraph_syntax.dart'; + +/// Parses GitHub Alerts blocks. +/// +/// See also: https://docs.github.com/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts +class AlertBlockSyntax extends BlockSyntax { + const AlertBlockSyntax(); + + @override + RegExp get pattern => alertPattern; + + @override + bool canParse(BlockParser parser) { + return pattern.hasMatch(parser.current.content) && + parser.lines.any((line) => _contentLineRegExp.hasMatch(line.content)); + } + + /// Whether this alert ends with a lazy continuation line. + // The definition of lazy continuation lines: + // https://spec.commonmark.org/0.30/#lazy-continuation-line + static bool _lazyContinuation = false; + static final _contentLineRegExp = RegExp(r'>?\s?(.*)*'); + + @override + List parseChildLines(BlockParser parser) { + // Grab all of the lines that form the alert, stripping off the ">". + final childLines = []; + _lazyContinuation = false; + + while (!parser.isDone) { + final strippedContent = + parser.current.content.replaceFirst(RegExp(r'^\s*>?\s*'), ''); + final match = _contentLineRegExp.firstMatch(strippedContent); + if (match != null) { + childLines.add(Line(strippedContent)); + parser.advance(); + _lazyContinuation = false; + continue; + } + + final lastLine = childLines.last; + + // A paragraph continuation is OK. This is content that cannot be parsed + // as any other syntax except Paragraph, and it doesn't match the bar in + // a Setext header. + // Because indented code blocks cannot interrupt paragraphs, a line + // matched CodeBlockSyntax is also paragraph continuation text. + final otherMatched = + parser.blockSyntaxes.firstWhere((s) => s.canParse(parser)); + if ((otherMatched is ParagraphSyntax && + !lastLine.isBlankLine && + !codeFencePattern.hasMatch(lastLine.content)) || + (otherMatched is CodeBlockSyntax && + !indentPattern.hasMatch(lastLine.content))) { + childLines.add(parser.current); + _lazyContinuation = true; + parser.advance(); + } else { + break; + } + } + + return childLines; + } + + @override + Node parse(BlockParser parser) { + // Parse the alert type from the first line. + final type = + pattern.firstMatch(parser.current.content)!.group(1)!.toLowerCase(); + parser.advance(); + final childLines = parseChildLines(parser); + // Recursively parse the contents of the alert. + final children = BlockParser(childLines, parser.document).parseLines( + // The setext heading underline cannot be a lazy continuation line in a + // block quote. + // https://spec.commonmark.org/0.30/#example-93 + disabledSetextHeading: _lazyContinuation, + parentSyntax: this, + ); + + // Mapping the alert title text. + const typeTextMap = { + 'note': 'Note', + 'tip': 'Tip', + 'important': 'Important', + 'caution': 'Caution', + 'warning': 'Warning', + }; + final titleText = typeTextMap[type]!; + final titleElement = Element('p', [Text(titleText)]) + ..attributes['class'] = 'markdown-alert-title'; + final elementClass = 'markdown-alert markdown-alert-${type.toLowerCase()}'; + return Element('div', [titleElement, ...children]) + ..attributes['class'] = elementClass; + } +} diff --git a/lib/src/extension_set.dart b/lib/src/extension_set.dart index 660759ea..58a25d86 100644 --- a/lib/src/extension_set.dart +++ b/lib/src/extension_set.dart @@ -1,3 +1,4 @@ +import 'block_syntaxes/alert_block_syntax.dart'; import 'block_syntaxes/block_syntax.dart'; import 'block_syntaxes/fenced_code_block_syntax.dart'; import 'block_syntaxes/footnote_def_syntax.dart'; @@ -60,6 +61,7 @@ class ExtensionSet { const UnorderedListWithCheckboxSyntax(), const OrderedListWithCheckboxSyntax(), const FootnoteDefSyntax(), + const AlertBlockSyntax(), ], ), List.unmodifiable( diff --git a/lib/src/patterns.dart b/lib/src/patterns.dart index c75838ff..4899ea16 100644 --- a/lib/src/patterns.dart +++ b/lib/src/patterns.dart @@ -151,3 +151,12 @@ final htmlCharactersPattern = RegExp( /// A line starts with `[`. final linkReferenceDefinitionPattern = RegExp(r'^[ ]{0,3}\['); + +/// Alert type patterns. +/// A alert block is similar to a blockquote, +/// starts with `> [!TYPE]`, and only 5 types are supported +/// with case-insensitive. +final alertPattern = RegExp( + r'^\s{0,3}>\s{0,3}\\?\[!(note|tip|important|caution|warning)\\?\]\s*$', + caseSensitive: false, +); diff --git a/lib/src/version.dart b/lib/src/version.dart index c8a2b008..a70cac58 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '7.1.2-wip'; +const packageVersion = '7.2.0-wip'; diff --git a/pubspec.yaml b/pubspec.yaml index 6ddea8fa..2ef835f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: markdown -version: 7.1.2-wip +version: 7.2.0-wip description: >- A portable Markdown library written in Dart that can parse Markdown into HTML. diff --git a/test/extensions/alert_extension.unit b/test/extensions/alert_extension.unit new file mode 100644 index 00000000..c2a3da10 --- /dev/null +++ b/test/extensions/alert_extension.unit @@ -0,0 +1,94 @@ +>>> type note +> [!NoTe] +> Test note alert. +<<< +
+

Note

+

Test note alert.

+
+>>> type tip +> [!TiP] +> Test tip alert. +<<< +
+

Tip

+

Test tip alert.

+
+>>> type important +> [!ImpoRtanT] +> Test important alert. +<<< +
+

Important

+

Test important alert.

+
+>>> type warning +> [!WarNinG] +> Test warning alert. +<<< +
+

Warning

+

Test warning alert.

+
+>>> type caution +> [!CauTioN] +> Test caution alert. +<<< +
+

Caution

+

Test caution alert.

+
+>>> invalid type +> [!foo] +> Test foo alert. +<<< +
+

[!foo] +Test foo alert.

+
+>>> contents can both contain/not contain starting quote +> [!NOTE] +Test note alert. +>Test note alert x2. +<<< +
+

Note

+

Test note alert. +Test note alert x2.

+
+>>> spaces everywhere + > [!NOTE] +> Test note alert. + > Test note alert x2. +<<< +
+

Note

+

Test note alert. +Test note alert x2.

+
+>>> title has 3 more spaces then fallback to blockquote +> [!NOTE] +> Test blockquote. +<<< +
+

[!NOTE] +Test blockquote.

+
+>>>nested blockquote +> [!NOTE] +>> Test nested blockquote. +<<< +
+

Note

+
+

Test nested blockquote.

+
+
+>>>escape brackets +> \[!note\] +> Test escape brackets. +<<< +
+

Note

+

Test escape brackets.

+
diff --git a/test/markdown_test.dart b/test/markdown_test.dart index 4d21fe89..1fea6835 100644 --- a/test/markdown_test.dart +++ b/test/markdown_test.dart @@ -42,12 +42,17 @@ void main() async { 'extensions/unordered_list_with_checkboxes.unit', blockSyntaxes: [const UnorderedListWithCheckboxSyntax()], ); + testFile( + 'extensions/alert_extension.unit', + blockSyntaxes: [const AlertBlockSyntax()], + ); + + // Inline syntax extensions testFile( 'extensions/autolink_extension.unit', inlineSyntaxes: [AutolinkExtensionSyntax()], ); - // Inline syntax extensions testFile( 'extensions/emojis.unit', inlineSyntaxes: [EmojiSyntax()],