| // 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; |
| } |