Skip to content

Commit 09c40e9

Browse files
feat: add full support for fuzzy linking
this PR adds full support for fuzzy links. to keep backward compat, the old linkify is still kept.
1 parent 9e57430 commit 09c40e9

File tree

4 files changed

+2298
-2933
lines changed

4 files changed

+2298
-2933
lines changed

__tests__/index.spec.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,21 @@ describe("Ansi", () => {
230230
);
231231
});
232232

233+
test("can linkify fuzzy links", () => {
234+
const el = shallow(
235+
React.createElement(
236+
Ansi,
237+
{ linkify: "fuzzy" },
238+
"this is a fuzzy link: example.com"
239+
)
240+
);
241+
expect(el).not.toBeNull();
242+
expect(el.text()).toBe("this is a fuzzy link: example.com");
243+
expect(el.html()).toBe(
244+
'<code><span>this is a fuzzy link: <a href="http://example.com" target="_blank">example.com</a></span></code>'
245+
);
246+
});
247+
233248
describe("useClasses options", () => {
234249
test("can add the font color class", () => {
235250
const el = shallow(

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"license": "BSD-3-Clause",
2424
"dependencies": {
2525
"anser": "^1.4.1",
26-
"escape-carriage": "^1.3.0"
26+
"escape-carriage": "^1.3.0",
27+
"linkify-it": "^3.0.3"
2728
},
2829
"peerDependencies": {
2930
"react": "^16.3.2 || ^17.0.0",
@@ -33,6 +34,7 @@
3334
"@semantic-release/npm": "^7.0.8",
3435
"@types/enzyme": "^3.10.5",
3536
"@types/jest": "^25.1.4",
37+
"@types/linkify-it": "^3.0.2",
3638
"@types/react": "^16.9.23",
3739
"conventional-changelog-conventionalcommits": "^4.5.0",
3840
"enzyme": "^3.11.0",

src/index.ts

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Anser, { AnserJsonEntry } from "anser";
22
import { escapeCarriageReturn } from "escape-carriage";
3+
import linkifyit from "linkify-it";
34
import * as React from "react";
45

56
/**
@@ -101,9 +102,8 @@ function createStyle(bundle: AnserJsonEntry): React.CSSProperties {
101102
* @param bundle Anser output.
102103
* @param key
103104
*/
104-
105105
function convertBundleIntoReact(
106-
linkify: boolean,
106+
linkify: boolean | "fuzzy",
107107
useClasses: boolean,
108108
bundle: AnserJsonEntry,
109109
key: number
@@ -119,6 +119,19 @@ function convertBundleIntoReact(
119119
);
120120
}
121121

122+
if (linkify === "fuzzy") {
123+
return linkWithLinkify(bundle, key, style, className);
124+
}
125+
126+
return linkWithClassicMode(bundle, key, style, className);
127+
}
128+
129+
function linkWithClassicMode(
130+
bundle: AnserJsonEntry,
131+
key: number,
132+
style: React.CSSProperties | null,
133+
className: string | null
134+
) {
122135
const content: React.ReactNode[] = [];
123136
const linkRegex = /(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
124137

@@ -157,9 +170,70 @@ function convertBundleIntoReact(
157170
return React.createElement("span", { style, key, className }, content);
158171
}
159172

173+
function linkWithLinkify(
174+
bundle: AnserJsonEntry,
175+
key: number,
176+
style: React.CSSProperties | null,
177+
className: string | null
178+
): JSX.Element {
179+
const linker = linkifyit({ fuzzyEmail: false }).tlds(["io"], true);
180+
181+
if (!linker.pretest(bundle.content)) {
182+
return React.createElement(
183+
"span",
184+
{ style, key, className },
185+
bundle.content
186+
);
187+
}
188+
189+
const matches = linker.match(bundle.content);
190+
191+
if (!matches) {
192+
return React.createElement(
193+
"span",
194+
{ style, key, className },
195+
bundle.content
196+
);
197+
}
198+
199+
const content: React.ReactNode[] = [
200+
bundle.content.substring(0, matches[0]?.index),
201+
];
202+
203+
matches.forEach((match, i) => {
204+
content.push(
205+
React.createElement(
206+
"a",
207+
{
208+
href: match.url,
209+
target: "_blank",
210+
key: i,
211+
},
212+
bundle.content.substring(match.index, match.lastIndex)
213+
)
214+
);
215+
216+
if (matches[i + 1]) {
217+
content.push(
218+
bundle.content.substring(matches[i].lastIndex, matches[i + 1]?.index)
219+
);
220+
}
221+
});
222+
223+
if (matches[matches.length - 1].lastIndex !== bundle.content.length) {
224+
content.push(
225+
bundle.content.substring(
226+
matches[matches.length - 1].lastIndex,
227+
bundle.content.length
228+
)
229+
);
230+
}
231+
return React.createElement("span", { style, key, className }, content);
232+
}
233+
160234
declare interface Props {
161235
children?: string;
162-
linkify?: boolean;
236+
linkify?: boolean | "fuzzy";
163237
className?: string;
164238
useClasses?: boolean;
165239
}

0 commit comments

Comments
 (0)