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

✨ Introduce AlertBlockSyntax #570

Merged
merged 15 commits into from
Dec 19, 2023
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

AlexV525 marked this conversation as resolved.
Show resolved Hide resolved
* 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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests look great! Can you see about this text: \[!note] or [\!note] and see what GitHub does and ensure we do the same. Other than that, test cases look great!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

foobar

Ah it shows an alert. I'll add this case.

> 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\]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha! Normally I would call that a bug. But our job is to match what GitHub does, and they likely will not change this behavior without making a big fuss, so 🤷 lgtm 😁

> 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