Skip to content

Commit 9fcae64

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 9fcae64

File tree

4 files changed

+2305
-2932
lines changed

4 files changed

+2305
-2932
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: true, fuzzyLinks: true },
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: 84 additions & 2 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
/**
@@ -104,6 +105,7 @@ function createStyle(bundle: AnserJsonEntry): React.CSSProperties {
104105

105106
function convertBundleIntoReact(
106107
linkify: boolean,
108+
fuzzyLinks: boolean,
107109
useClasses: boolean,
108110
bundle: AnserJsonEntry,
109111
key: number
@@ -119,6 +121,19 @@ function convertBundleIntoReact(
119121
);
120122
}
121123

124+
if (fuzzyLinks) {
125+
return linkWithLinkify(bundle, key, style, className);
126+
}
127+
128+
return linkWithClassicMode(bundle, key, style, className);
129+
}
130+
131+
function linkWithClassicMode(
132+
bundle: AnserJsonEntry,
133+
key: number,
134+
style: React.CSSProperties | null,
135+
className: string | null
136+
) {
122137
const content: React.ReactNode[] = [];
123138
const linkRegex = /(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
124139

@@ -157,20 +172,87 @@ function convertBundleIntoReact(
157172
return React.createElement("span", { style, key, className }, content);
158173
}
159174

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

167244
export default function Ansi(props: Props): JSX.Element {
168-
const { className, useClasses, children, linkify } = props;
245+
const { className, useClasses, children, linkify, fuzzyLinks } = props;
169246
return React.createElement(
170247
"code",
171248
{ className },
172249
ansiToJSON(children ?? "", useClasses ?? false).map(
173-
convertBundleIntoReact.bind(null, linkify ?? false, useClasses ?? false)
250+
convertBundleIntoReact.bind(
251+
null,
252+
linkify ?? false,
253+
fuzzyLinks ?? false,
254+
useClasses ?? false
255+
)
174256
)
175257
);
176258
}

0 commit comments

Comments
 (0)