Skip to content

Commit

Permalink
✨ Introduce AlertBlockSyntax (#570)
Browse files Browse the repository at this point in the history
* ✨ 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
  • Loading branch information
AlexV525 authored Dec 19, 2023
1 parent d2e7903 commit 5fab3a7
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 6 deletions.
2 changes: 1 addition & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ Daniel Schubert <daniel.schubert+github.com@gmail.com>
Jirka Daněk <dnk@mail.muni.cz>
Seth Westphal <westy92@gmail.com>
Tim Maffett <timmaffett@gmail.com>

Alex Li <alexv.525.li@gmail.com>
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 1 addition & 0 deletions lib/markdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
107 changes: 107 additions & 0 deletions lib/src/block_syntaxes/alert_block_syntax.dart
Original file line number Diff line number Diff line change
@@ -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<Line> parseChildLines(BlockParser parser) {
// Grab all of the lines that form the alert, stripping off the ">".
final childLines = <Line>[];
_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;
}
}
2 changes: 2 additions & 0 deletions lib/src/extension_set.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -60,6 +61,7 @@ class ExtensionSet {
const UnorderedListWithCheckboxSyntax(),
const OrderedListWithCheckboxSyntax(),
const FootnoteDefSyntax(),
const AlertBlockSyntax(),
],
),
List<InlineSyntax>.unmodifiable(
Expand Down
9 changes: 9 additions & 0 deletions lib/src/patterns.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
2 changes: 1 addition & 1 deletion lib/src/version.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
94 changes: 94 additions & 0 deletions test/extensions/alert_extension.unit
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
>>> type note
> [!NoTe]
> Test note alert.
<<<
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>Test note alert.</p>
</div>
>>> type tip
> [!TiP]
> Test tip alert.
<<<
<div class="markdown-alert markdown-alert-tip">
<p class="markdown-alert-title">Tip</p>
<p>Test tip alert.</p>
</div>
>>> type important
> [!ImpoRtanT]
> Test important alert.
<<<
<div class="markdown-alert markdown-alert-important">
<p class="markdown-alert-title">Important</p>
<p>Test important alert.</p>
</div>
>>> type warning
> [!WarNinG]
> Test warning alert.
<<<
<div class="markdown-alert markdown-alert-warning">
<p class="markdown-alert-title">Warning</p>
<p>Test warning alert.</p>
</div>
>>> type caution
> [!CauTioN]
> Test caution alert.
<<<
<div class="markdown-alert markdown-alert-caution">
<p class="markdown-alert-title">Caution</p>
<p>Test caution alert.</p>
</div>
>>> invalid type
> [!foo]
> Test foo alert.
<<<
<blockquote>
<p>[!foo]
Test foo alert.</p>
</blockquote>
>>> contents can both contain/not contain starting quote
> [!NOTE]
Test note alert.
>Test note alert x2.
<<<
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>Test note alert.
Test note alert x2.</p>
</div>
>>> spaces everywhere
> [!NOTE]
> Test note alert.
> Test note alert x2.
<<<
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>Test note alert.
Test note alert x2.</p>
</div>
>>> title has 3 more spaces then fallback to blockquote
> [!NOTE]
> Test blockquote.
<<<
<blockquote>
<p>[!NOTE]
Test blockquote.</p>
</blockquote>
>>>nested blockquote
> [!NOTE]
>> Test nested blockquote.
<<<
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<blockquote>
<p>Test nested blockquote.</p>
</blockquote>
</div>
>>>escape brackets
> \[!note\]
> Test escape brackets.
<<<
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>Test escape brackets.</p>
</div>
7 changes: 6 additions & 1 deletion test/markdown_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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()],
Expand Down

0 comments on commit 5fab3a7

Please sign in to comment.