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 : // not 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 0 : final formattingPrecedence = [
120 0 : Formatter(_codeRegex, CodeElement.new),
121 0 : Formatter(_boldRegex, BoldElement.new),
122 0 : Formatter(_italicRegex, ItalicElement.new),
123 0 : Formatter(_superRegex, SuperElement.new),
124 : // Formatter(_subRegex, SubElement.new),
125 0 : Formatter(_strikeRegex, StrikeElement.new)
126 : ];
127 :
128 : // Loop through the formatters in with precedence and break when something is found...
129 0 : for (var formatter in formattingPrecedence) {
130 0 : var formattingMatch = formatter.expression.firstMatch(element.text);
131 : if (formattingMatch != null) {
132 0 : list = replaceAndParse(formatter.element, element, formattingMatch, list, options);
133 : break;
134 : }
135 : }
136 :
137 : // catch all case where we didn't match anything and so need to return back
138 : // the unformatted text
139 : // conceptually this is Formatter((.*), TextElement.new)
140 0 : if (list.isEmpty) {
141 0 : list.add(element);
142 : }
143 :
144 : return list;
145 : }
146 :
147 0 : @override
148 : List<LinkifyElement> parse(elements, options) {
149 0 : var list = <LinkifyElement>[];
150 :
151 0 : elements.forEach((element) {
152 0 : if (element is TextElement) {
153 0 : if (options.parseLinks == false && options.messageFormatting == false) {
154 0 : list.add(element);
155 0 : } else if (options.parseLinks == true) {
156 : // check if there is a link...
157 0 : var match = options.looseUrl ? _looseUrlRegex.firstMatch(element.text) : _urlRegex.firstMatch(element.text);
158 :
159 : // if not then we only have to consider formatting...
160 : if (match == null) {
161 : // only do formatting if message formatting is enabled
162 0 : if (options.messageFormatting == false) {
163 0 : list.add(element);
164 : } else {
165 : // add all the formatting elements contained in this text
166 0 : list.addAll(parseFormatting(element, options));
167 : }
168 : } else {
169 0 : final text = element.text.replaceFirst(match.group(0)!, '');
170 :
171 0 : if (match.group(1)?.isNotEmpty == true) {
172 : // we match links first and the feed everything before the link
173 : // back through the parser
174 0 : list.addAll(parse([TextElement(match.group(1)!)], options));
175 : }
176 :
177 0 : if (match.group(2)?.isNotEmpty == true) {
178 0 : var originalUrl = match.group(2)!;
179 : String? end;
180 :
181 0 : if ((options.excludeLastPeriod) && originalUrl[originalUrl.length - 1] == ".") {
182 : end = ".";
183 0 : originalUrl = originalUrl.substring(0, originalUrl.length - 1);
184 : }
185 :
186 : var url = originalUrl;
187 :
188 : // If protocol has not been specified then append a protocol
189 : // to the start of the URL so that it can be opened...
190 0 : if (!url.startsWith("https://") && !url.startsWith("http://")) {
191 0 : url = "https://" + url;
192 : }
193 :
194 0 : list.add(UrlElement(url, originalUrl));
195 :
196 : if (end != null) {
197 0 : list.add(TextElement(end));
198 : }
199 : }
200 :
201 0 : if (text.isNotEmpty) {
202 0 : list.addAll(parse([TextElement(text)], options));
203 : }
204 : }
205 0 : } else if (options.messageFormatting == true) {
206 : // we can jump straight to message formatting...
207 0 : list.addAll(parseFormatting(element, options));
208 : } else {
209 : // unreachable - if we get here then there is something wrong in the above logic since every combination of
210 : // formatting options should have already been accounted for.
211 0 : EnvironmentConfig.debugLog("'unreachable' code path in formatting has been triggered. this is very likely a bug - please report $options");
212 : }
213 : }
214 : });
215 :
216 : return list;
217 : }
218 : }
219 :
220 : /// Represents an element containing a link
221 : class UrlElement extends LinkableElement {
222 0 : UrlElement(String url, [String? text]) : super(text, url);
223 :
224 0 : @override
225 : String toString() {
226 0 : return "LinkElement: '$url' ($text)";
227 : }
228 :
229 0 : @override
230 0 : bool operator ==(other) => equals(other);
231 :
232 0 : @override
233 0 : bool equals(other) => other is UrlElement && super.equals(other);
234 : }
|