Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

✨ Introduce AlertBlockSyntax #570

Merged
merged 15 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
Copy link
Collaborator

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
Collaborator

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