Line data Source code
1 : // Originally from linkify: https://github.com/Cretezy/linkify/blob/dfb3e43b0e56452bad584ddb0bf9b73d8db0589f/lib/src/url.dart
2 : //
3 : // Removed handling of `removeWWW` and `humanize`.
4 : // Removed auto-appending of `http(s)://` to the readable url
5 : //
6 : // MIT License
7 : //
8 : // Copyright (c) 2019 Charles-William Crete
9 : //
10 : // Permission is hereby granted, free of charge, to any person obtaining a copy
11 : // of this software and associated documentation files (the "Software"), to deal
12 : // in the Software without restriction, including without limitation the rights
13 : // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 : // copies of the Software, and to permit persons to whom the Software is
15 : // furnished to do so, subject to the following conditions:
16 : //
17 : // The above copyright notice and this permission notice shall be included in all
18 : // copies or substantial portions of the Software.
19 : //
20 : // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 : // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 : // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 : // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 : // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 : // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 : // SOFTWARE.
27 :
28 : import 'package:cwtch/config.dart';
29 :
30 : import 'linkify.dart';
31 :
32 3 : final _urlRegex = RegExp(r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)', caseSensitive: false, dotAll: true);
33 :
34 0 : final _looseUrlRegex = RegExp(r'^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,16}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*))', caseSensitive: false, dotAll: true);
35 :
36 : class Formatter {
37 : final RegExp expression;
38 : final LinkifyElement Function(String) element;
39 :
40 1 : Formatter(this.expression, this.element);
41 : }
42 :
43 : // regex to match **bold**
44 3 : final _boldRegex = RegExp(r'^(.*?)(\*\*([^*]+)\*\*)', caseSensitive: false, dotAll: true);
45 :
46 : // regex to match *italic*
47 3 : final _italicRegex = RegExp(r'^(.*?)(\*([^*]+)\*)', caseSensitive: false, dotAll: true);
48 :
49 : // regex to match ^superscript^
50 3 : final _superRegex = RegExp(r'^(.*?)(\^([^\^]*)\^)', caseSensitive: false, dotAll: true);
51 :
52 : // regex to match ^subscript^
53 0 : final _subRegex = RegExp(r'^(.*?)(\_([^\_]*)\_)', caseSensitive: false, dotAll: true);
54 :
55 : // regex to match ~~strikethrough~~
56 3 : final _strikeRegex = RegExp(r'^(.*?)(\~\~([^\~]*)\~\~)', caseSensitive: false, dotAll: true);
57 :
58 : // regex to match `code`
59 3 : final _codeRegex = RegExp(r'^(.*?)(\`([^\`]*)\`)', caseSensitive: false, dotAll: true);
60 :
61 : class UrlLinkifier extends Linkifier {
62 1 : const UrlLinkifier();
63 :
64 1 : List<LinkifyElement> replaceAndParse(tle, TextElement element, RegExpMatch match, List<LinkifyElement> list, options) {
65 3 : final text = element.text.replaceFirst(match.group(0)!, '');
66 :
67 3 : if (match.group(1)?.isNotEmpty == true) {
68 0 : list.addAll(parse([TextElement(match.group(1)!)], options));
69 : }
70 :
71 3 : if (match.group(2)?.isNotEmpty == true) {
72 3 : list.add(tle(match.group(2)!));
73 : }
74 :
75 1 : if (text.isNotEmpty) {
76 0 : list.addAll(parse([TextElement(text)], options));
77 : }
78 : return list;
79 : }
80 :
81 1 : List<LinkifyElement> parseFormatting(element, options) {
82 1 : var list = <LinkifyElement>[];
83 :
84 : // code -> bold -> italic -> super -> sub -> strike
85 : // note we don't currently allow combinations of these elements the first
86 : // one to match a given set will be the only style applied - this will be fixed
87 :
88 : // Per #836, URLs should be lower priority than code. Since URL parsing logic
89 : // is more complicated than a simple regex and is implemented in the recursing
90 : // function, code regex is injected before the URL logic one level up.
91 : // To preserve behaviour when the clickable links experiment is disabled,
92 : // the code regex is preserved here for the time being.
93 : // Eventually this might all be replaced by a more advanced formatting parser.
94 1 : final formattingPrecedence = [
95 2 : Formatter(_codeRegex, CodeElement.new),
96 2 : Formatter(_boldRegex, BoldElement.new),
97 2 : Formatter(_italicRegex, ItalicElement.new),
98 2 : Formatter(_superRegex, SuperElement.new),
99 : // Formatter(_subRegex, SubElement.new),
100 2 : Formatter(_strikeRegex, StrikeElement.new),
101 : ];
102 :
103 : // Loop through the formatters in with precedence and break when something is found...
104 2 : for (var formatter in formattingPrecedence) {
105 3 : var formattingMatch = formatter.expression.firstMatch(element.text);
106 : if (formattingMatch != null) {
107 2 : list = replaceAndParse(formatter.element, element, formattingMatch, list, options);
108 : break;
109 : }
110 : }
111 :
112 : // catch all case where we didn't match anything and so need to return back
113 : // the unformatted text
114 : // conceptually this is Formatter((.*), TextElement.new)
115 1 : if (list.isEmpty) {
116 1 : list.add(element);
117 : }
118 :
119 : return list;
120 : }
121 :
122 1 : @override
123 : List<LinkifyElement> parse(elements, options) {
124 1 : var list = <LinkifyElement>[];
125 :
126 2 : elements.forEach((element) {
127 1 : if (element is TextElement) {
128 4 : if (options.parseLinks == false && options.messageFormatting == false) {
129 1 : list.add(element);
130 2 : } else if (options.parseLinks == true) {
131 : // Per #836, code block formatting needs to take precedence over URLs.
132 : // Only in this combination of conditionals is this additional logic required.
133 3 : var codeBlockMatch = _codeRegex.firstMatch(element.text);
134 2 : if (codeBlockMatch != null && options.messageFormatting == true) {
135 3 : final text = element.text.replaceFirst(codeBlockMatch.group(0)!, '');
136 3 : if (codeBlockMatch.group(1)?.isNotEmpty == true) {
137 0 : list.addAll(parse([TextElement(codeBlockMatch.group(1)!)], options));
138 : }
139 3 : list.add(CodeElement(codeBlockMatch.group(2)!));
140 1 : if (text.isNotEmpty) {
141 0 : list.addAll(parse([TextElement(text)], options));
142 : }
143 : } else {
144 : // check if there is a link...
145 4 : var match = options.looseUrl ? _looseUrlRegex.firstMatch(element.text) : _urlRegex.firstMatch(element.text);
146 :
147 : // if not then we only have to consider formatting...
148 : if (match == null) {
149 : // only do formatting if message formatting is enabled
150 0 : if (options.messageFormatting == false) {
151 0 : list.add(element);
152 : } else {
153 : // add all the formatting elements contained in this text
154 0 : list.addAll(parseFormatting(element, options));
155 : }
156 : } else {
157 3 : final text = element.text.replaceFirst(match.group(0)!, '');
158 :
159 3 : if (match.group(1)?.isNotEmpty == true) {
160 : // we match links first and the feed everything before the link
161 : // back through the parser
162 0 : list.addAll(parse([TextElement(match.group(1)!)], options));
163 : }
164 :
165 3 : if (match.group(2)?.isNotEmpty == true) {
166 1 : var originalUrl = match.group(2)!;
167 : String? end;
168 :
169 5 : if ((options.excludeLastPeriod) && originalUrl[originalUrl.length - 1] == ".") {
170 : end = ".";
171 0 : originalUrl = originalUrl.substring(0, originalUrl.length - 1);
172 : }
173 :
174 : var url = originalUrl;
175 :
176 : // If protocol has not been specified then append a protocol
177 : // to the start of the URL so that it can be opened...
178 1 : if (!url.startsWith("https://") && !url.startsWith("http://")) {
179 0 : url = "https://" + url;
180 : }
181 :
182 2 : list.add(UrlElement(url, originalUrl));
183 :
184 : if (end != null) {
185 0 : list.add(TextElement(end));
186 : }
187 : }
188 :
189 1 : if (text.isNotEmpty) {
190 0 : list.addAll(parse([TextElement(text)], options));
191 : }
192 : }
193 : }
194 2 : } else if (options.messageFormatting == true) {
195 : // we can jump straight to message formatting...
196 2 : list.addAll(parseFormatting(element, options));
197 : } else {
198 : // unreachable - if we get here then there is something wrong in the above logic since every combination of
199 : // formatting options should have already been accounted for.
200 0 : EnvironmentConfig.debugLog("'unreachable' code path in formatting has been triggered. this is very likely a bug - please report $options");
201 : }
202 : }
203 : });
204 :
205 : return list;
206 : }
207 : }
208 :
209 : /// Represents an element containing a link
210 : class UrlElement extends LinkableElement {
211 2 : UrlElement(String url, [String? text]) : super(text, url);
212 :
213 0 : @override
214 : String toString() {
215 0 : return "LinkElement: '$url' ($text)";
216 : }
217 :
218 0 : @override
219 0 : bool operator ==(other) => equals(other);
220 :
221 0 : @override
222 0 : bool equals(other) => other is UrlElement && super.equals(other);
223 : }
|