blob: 0f4f5b11a94f81179c86c774bb4872f23f9fc6b4 [file] [log] [blame]
// 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 alertPattern.hasMatch(parser.current.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 lineContent = parser.current.content.trimLeft();
final strippedContent = lineContent.replaceFirst(RegExp(r'^>?\s*'), '');
final match = strippedContent.isEmpty && !lineContent.startsWith('>')
? null
: _contentLineRegExp.firstMatch(strippedContent);
if (match != null) {
childLines.add(Line(strippedContent));
parser.advance();
_lazyContinuation = false;
continue;
}
final lastLine = childLines.isEmpty ? Line('') : 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';
return Element('div', [titleElement, ...children])
..attributes['class'] = elementClass;
}
}