| // 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 '../charcode.dart'; |
| import '../inline_parser.dart'; |
| import '../util.dart'; |
| import 'inline_syntax.dart'; |
| |
| /// Matches autolinks like `http://foo.com` and `foo@bar.com`. |
| class AutolinkExtensionSyntax extends InlineSyntax { |
| static const _linkPattern = |
| // Autolinks can only come at the beginning of a line, after whitespace, |
| // or any of the delimiting characters *, _, ~, and (. |
| r'(?<=^|[\s*_~(>])' |
| |
| // An extended url autolink will be recognised when one of the schemes |
| // http://, or https://, followed by a valid domain. See |
| // https://github.github.com/gfm/#extended-url-autolink. |
| r'(?:(?:https?|ftp):\/\/|www\.)' |
| |
| // A valid domain consists of segments of alphanumeric characters, |
| // underscores (_) and hyphens (-) separated by periods (.). There must |
| // be at least one period, and no underscores may be present in the last |
| // two segments of the domain. See |
| // https://github.github.com/gfm/#valid-domain. |
| r'(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)' |
| |
| // After a valid domain, zero or more non-space non-< characters may |
| // follow. |
| r'[^\s<]*' |
| |
| // Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will |
| // not be considered part of the autolink, though they may be included in |
| // the interior of the link. See |
| // https://github.github.com/gfm/#extended-autolink-path-validation. |
| '(?<![?!.,:*_~])'; |
| |
| // An extended email autolink, see |
| // https://github.github.com/gfm/#extended-email-autolink. |
| static const _emailPattern = |
| r'[-_.+a-z0-9]+@(?:[-_a-z0-9]+\.)+[-_a-z0-9]*[a-z0-9](?![-_])'; |
| |
| AutolinkExtensionSyntax() |
| : super( |
| '($_linkPattern)|($_emailPattern)', |
| caseSensitive: false, |
| ); |
| |
| @override |
| bool tryMatch(InlineParser parser, [int? startMatchPos]) { |
| startMatchPos ??= parser.pos; |
| final startMatch = pattern.matchAsPrefix(parser.source, startMatchPos); |
| if (startMatch == null) { |
| return false; |
| } |
| parser.writeText(); |
| return onMatch(parser, startMatch); |
| } |
| |
| @override |
| bool onMatch(InlineParser parser, Match match) { |
| int consumeLength; |
| |
| final isEmailLink = match[2] != null; |
| if (isEmailLink) { |
| consumeLength = match.match.length; |
| } else { |
| consumeLength = _getConsumeLength(match.match); |
| } |
| |
| var text = match.match.substring(0, consumeLength); |
| text = parser.encodeHtml ? escapeHtml(text) : text; |
| |
| var destination = text; |
| if (isEmailLink) { |
| destination = 'mailto:$destination'; |
| } else if (destination[0] == 'w') { |
| // When there is no scheme specified, insert the scheme `http`. |
| destination = 'http://$destination'; |
| } |
| |
| final anchor = Element.text('a', text) |
| ..attributes['href'] = Uri.encodeFull(destination); |
| |
| parser |
| ..addNode(anchor) |
| ..consume(consumeLength); |
| |
| return true; |
| } |
| |
| int _getConsumeLength(String text) { |
| var excludedLength = 0; |
| |
| // When an autolink ends in `)`, see |
| // https://github.github.com/gfm/#example-625. |
| if (text.endsWith(')')) { |
| final match = RegExp(r'(\(.*)?(\)+)$').firstMatch(text)!; |
| |
| if (match[1] == null) { |
| excludedLength = match[2]!.length; |
| } else { |
| var parenCount = 0; |
| for (var i = 0; i < text.length; i++) { |
| final char = text.codeUnitAt(i); |
| if (char == $lparen) { |
| parenCount++; |
| } else if (char == $rparen) { |
| parenCount--; |
| } |
| } |
| if (parenCount < 0) { |
| excludedLength = parenCount.abs(); |
| } |
| } |
| } |
| // If an autolink ends in a semicolon `;`, see |
| // https://github.github.com/gfm/#example-627 |
| else if (text.endsWith(';')) { |
| final match = RegExp(r'&[0-9a-z]+;$').firstMatch(text); |
| if (match != null) { |
| excludedLength = match.match.length; |
| } |
| } |
| |
| return text.length - excludedLength; |
| } |
| } |