blob: 931fb88fe65cd2144adf5cd836d1e9f69e88a8c8 [file] [log] [blame]
// Copyright (c) 2022, 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 '../patterns.dart';
import '../util.dart';
import 'block_syntax.dart';
/// Parses preformatted code blocks between two ~~~ or ``` sequences.
///
/// See the CommonMark spec: https://spec.commonmark.org/0.29/#fenced-code-blocks
class FencedCodeBlockSyntax extends BlockSyntax {
@override
RegExp get pattern => codeFencePattern;
const FencedCodeBlockSyntax();
@override
Node parse(BlockParser parser) {
final openingFence = _FenceMatch.fromMatch(pattern.firstMatch(
escapePunctuation(parser.current),
)!);
var text = parseChildLines(
parser,
openingFence.marker,
openingFence.indent,
).join('\n');
if (parser.document.encodeHtml) {
text = escapeHtml(text);
}
if (text.isNotEmpty) {
text = '$text\n';
}
final code = Element.text('code', text);
if (openingFence.hasLanguage) {
var language = decodeHtmlCharacters(openingFence.language);
if (parser.document.encodeHtml) {
language = escapeHtmlAttribute(language);
}
code.attributes['class'] = 'language-$language';
}
return Element('pre', [code]);
}
String _removeIndentation(String content, int length) {
final text = content.replaceFirst(RegExp('^\\s{0,$length}'), '');
return content.substring(content.length - text.length);
}
@override
List<String> parseChildLines(
BlockParser parser, [
String openingMarker = '',
int indent = 0,
]) {
final childLines = <String>[];
parser.advance();
_FenceMatch? closingFence;
while (!parser.isDone) {
final match = pattern.firstMatch(parser.current);
closingFence = match == null ? null : _FenceMatch.fromMatch(match);
// Closing code fences cannot have info strings:
// https://spec.commonmark.org/0.30/#example-147
if (closingFence == null ||
!closingFence.marker.startsWith(openingMarker) ||
closingFence.hasInfo) {
childLines.add(_removeIndentation(parser.current, indent));
parser.advance();
} else {
parser.advance();
break;
}
}
// https://spec.commonmark.org/0.30/#example-127
// https://spec.commonmark.org/0.30/#example-128
if (closingFence == null &&
childLines.isNotEmpty &&
childLines.last.trim().isEmpty) {
childLines.removeLast();
}
return childLines;
}
}
class _FenceMatch {
_FenceMatch._({
required this.indent,
required this.marker,
required this.info,
});
factory _FenceMatch.fromMatch(RegExpMatch match) {
String marker;
String info;
if (match.namedGroup('backtick') != null) {
marker = match.namedGroup('backtick')!;
info = match.namedGroup('backtickInfo')!;
} else {
marker = match.namedGroup('tilde')!;
info = match.namedGroup('tildeInfo')!;
}
return _FenceMatch._(
indent: match[1]!.length,
marker: marker,
info: info.trim(),
);
}
final int indent;
final String marker;
// The info-string should be trimmed,
// https://spec.commonmark.org/0.30/#info-string.
final String info;
// The first word of the info string is typically used to specify the language
// of the code sample,
// https://spec.commonmark.org/0.30/#example-143.
String get language => info.split(' ').first;
bool get hasInfo => info.isNotEmpty;
bool get hasLanguage => language.isNotEmpty;
}