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 0 : final _urlRegex = RegExp(
33 : r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[^\s]*)',
34 : caseSensitive: false,
35 : dotAll: true,
36 : );
37 :
38 0 : final _looseUrlRegex = RegExp(
39 : r'^(.*?)((https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,16}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*))',
40 : caseSensitive: false,
41 : dotAll: true,
42 : );
43 :
44 : class Formatter {
45 : final RegExp expression;
46 : final LinkifyElement Function(String) element;
47 :
48 0 : Formatter(this.expression, this.element);
49 : }
50 :
51 : // regex to match **bold**
52 0 : final _boldRegex = RegExp(
53 : r'^(.*?)(\*\*([^*]+)\*\*)',
54 : caseSensitive: false,
55 : dotAll: true,
56 : );
57 :
58 : // regex to match *italic*
59 0 : final _italicRegex = RegExp(
60 : r'^(.*?)(\*([^*]+)\*)',
61 : caseSensitive: false,
62 : dotAll: true,
63 : );
64 :
65 : // regex to match ^superscript^
66 0 : final _superRegex = RegExp(
67 : r'^(.*?)(\^([^\^]*)\^)',
68 : caseSensitive: false,
69 : dotAll: true,
70 : );
71 :
72 : // regex to match ^subscript^
73 0 : final _subRegex = RegExp(
74 : r'^(.*?)(\_([^\_]*)\_)',
75 : caseSensitive: false,
76 : dotAll: true,
77 : );
78 :
79 : // regex to match ~~strikethrough~~
80 0 : final _strikeRegex = RegExp(
81 : r'^(.*?)(\~\~([^\~]*)\~\~)',
82 : caseSensitive: false,
83 : dotAll: true,
84 : );
85 :
86 : // regex to match `code`
87 0 : final _codeRegex = RegExp(
88 : r'^(.*?)(\`([^\`]*)\`)',
89 : caseSensitive: false,
90 : dotAll: true,
91 : );
92 :
93 : class UrlLinkifier extends Linkifier {
94 4 : const UrlLinkifier();
95 :
96 0 : List<LinkifyElement> replaceAndParse(tle, TextElement element, RegExpMatch match, List<LinkifyElement> list, options) {
97 0 : final text = element.text.replaceFirst(match.group(0)!, '');
98 :
99 0 : if (match.group(1)?.isNotEmpty == true) {
100 0 : list.addAll(parse([TextElement(match.group(1)!)], options));
101 : }
102 :
103 0 : if (match.group(2)?.isNotEmpty == true) {
104 0 : list.add(tle(match.group(2)!));
105 : }
106 :
107 0 : if (text.isNotEmpty) {
108 0 : list.addAll(parse([TextElement(text)], options));
109 : }
110 : return list;
111 : }
112 :
113 0 : List<LinkifyElement> parseFormatting(element, options) {
114 0 : var list = <LinkifyElement>[];
115 :
116 : // code -> bold -> italic -> super -> sub -> strike
117 : // note we don't currently allow combinations of these elements the first
118 : // one to match a given set will be the only style applied - this will be fixed
119 :
120 : // Per #836, URLs should be lower priority than code. Since URL parsing logic
121 : // is more complicated than a simple regex and is implemented in the recursing
122 : // function, code regex is injected before the URL logic one level up.
123 : // To preserve behaviour when the clickable links experiment is disabled,
124 : // the code regex is preserved here for the time being.
125 : // Eventually this might all be replaced by a more advanced formatting parser.
126 0 : final formattingPrecedence = [
127 0 : Formatter(_codeRegex, CodeElement.new),
128 0 : Formatter(_boldRegex, BoldElement.new),
129 0 : Formatter(_italicRegex, ItalicElement.new),
130 0 : Formatter(_superRegex, SuperElement.new),
131 : // Formatter(_subRegex, SubElement.new),
132 0 : Formatter(_strikeRegex, StrikeElement.new)
133 : ];
134 :
135 : // Loop through the formatters in with precedence and break when something is found...
136 0 : for (var formatter in formattingPrecedence) {
137 0 : var formattingMatch = formatter.expression.firstMatch(element.text);
138 : if (formattingMatch != null) {
139 0 : list = replaceAndParse(formatter.element, element, formattingMatch, list, options);
140 : break;
141 : }
142 : }
143 :
144 : // catch all case where we didn't match anything and so need to return back
145 : // the unformatted text
146 : // conceptually this is Formatter((.*), TextElement.new)
147 0 : if (list.isEmpty) {
148 0 : list.add(element);
149 : }
150 :
151 : return list;
152 : }
153 :
154 0 : @override
155 : List<LinkifyElement> parse(elements, options) {
156 0 : var list = <LinkifyElement>[];
157 :
158 0 : elements.forEach((element) {
159 0 : if (element is TextElement) {
160 0 : if (options.parseLinks == false && options.messageFormatting == false) {
161 0 : list.add(element);
162 0 : } else if (options.parseLinks == true) {
163 : // Per #836, code block formatting needs to take precedence over URLs.
164 : // Only in this combination of conditionals is this additional logic required.
165 0 : var codeBlockMatch = _codeRegex.firstMatch(element.text);
166 0 : if (codeBlockMatch != null && options.messageFormatting == true) {
167 0 : final text = element.text.replaceFirst(codeBlockMatch.group(0)!, '');
168 0 : if (codeBlockMatch.group(1)?.isNotEmpty == true) {
169 0 : list.addAll(parse([TextElement(codeBlockMatch.group(1)!)], options));
170 : }
171 0 : list.add(CodeElement(codeBlockMatch.group(2)!));
172 0 : if (text.isNotEmpty) {
173 0 : list.addAll(parse([TextElement(text)], options));
174 : }
175 : } else {
176 : // check if there is a link...
177 0 : var match = options.looseUrl ? _looseUrlRegex.firstMatch(element.text) : _urlRegex.firstMatch(element.text);
178 :
179 : // if not then we only have to consider formatting...
180 : if (match == null) {
181 : // only do formatting if message formatting is enabled
182 0 : if (options.messageFormatting == false) {
183 0 : list.add(element);
184 : } else {
185 : // add all the formatting elements contained in this text
186 0 : list.addAll(parseFormatting(element, options));
187 : }
188 : } else {
189 0 : final text = element.text.replaceFirst(match.group(0)!, '');
190 :
191 0 : if (match.group(1)?.isNotEmpty == true) {
192 : // we match links first and the feed everything before the link
193 : // back through the parser
194 0 : list.addAll(parse([TextElement(match.group(1)!)], options));
195 : }
196 :
197 0 : if (match.group(2)?.isNotEmpty == true) {
198 0 : var originalUrl = match.group(2)!;
199 : String? end;
200 :
201 0 : if ((options.excludeLastPeriod) && originalUrl[originalUrl.length - 1] == ".") {
202 : end = ".";
203 0 : originalUrl = originalUrl.substring(0, originalUrl.length - 1);
204 : }
205 :
206 : var url = originalUrl;
207 :
208 : // If protocol has not been specified then append a protocol
209 : // to the start of the URL so that it can be opened...
210 0 : if (!url.startsWith("https://") && !url.startsWith("http://")) {
211 0 : url = "https://" + url;
212 : }
213 :
214 0 : list.add(UrlElement(url, originalUrl));
215 :
216 : if (end != null) {
217 0 : list.add(TextElement(end));
218 : }
219 : }
220 :
221 0 : if (text.isNotEmpty) {
222 0 : list.addAll(parse([TextElement(text)], options));
223 : }
224 : }
225 : }
226 0 : } else if (options.messageFormatting == true) {
227 : // we can jump straight to message formatting...
228 0 : list.addAll(parseFormatting(element, options));
229 : } else {
230 : // unreachable - if we get here then there is something wrong in the above logic since every combination of
231 : // formatting options should have already been accounted for.
232 0 : EnvironmentConfig.debugLog("'unreachable' code path in formatting has been triggered. this is very likely a bug - please report $options");
233 : }
234 : }
235 : });
236 :
237 : return list;
238 : }
239 : }
240 :
241 : /// Represents an element containing a link
242 : class UrlElement extends LinkableElement {
243 0 : UrlElement(String url, [String? text]) : super(text, url);
244 :
245 0 : @override
246 : String toString() {
247 0 : return "LinkElement: '$url' ($text)";
248 : }
249 :
250 0 : @override
251 0 : bool operator ==(other) => equals(other);
252 :
253 0 : @override
254 0 : bool equals(other) => other is UrlElement && super.equals(other);
255 : }
|