Skip to content

Commit b33bc4f

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 f3bbc6e commit b33bc4f

File tree

4 files changed

+1315
-1026
lines changed

4 files changed

+1315
-1026
lines changed

__tests__/index.spec.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -320,5 +320,20 @@ describe("Ansi", () => {
320320
'<code><span>hello </span><span style="color:rgb(0, 187, 0);font-weight:bold">world</span><span>!</span></code>'
321321
);
322322
});
323+
324+
test("can linkify fuzzy links", () => {
325+
const el = shallow(
326+
React.createElement(
327+
Ansi,
328+
{ linkify: true, fuzzyLinks: true },
329+
"this is a fuzzy link: example.com"
330+
)
331+
);
332+
expect(el).not.toBeNull();
333+
expect(el.text()).toBe("this is a fuzzy link: example.com");
334+
expect(el.html()).toBe(
335+
'<code><span>this is a fuzzy link: <a href="http://example.com" target="_blank">example.com</a></span></code>'
336+
);
337+
});
323338
});
324339
});

package.json

+3-1
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

+86-3
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,8 +121,22 @@ 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[] = [];
123-
const linkRegex = /(\s+|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
138+
const linkRegex =
139+
/(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
124140

125141
let index = 0;
126142
let match: RegExpExecArray | null;
@@ -157,20 +173,87 @@ function convertBundleIntoReact(
157173
return React.createElement("span", { style, key, className }, content);
158174
}
159175

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

167245
export default function Ansi(props: Props): JSX.Element {
168-
const { className, useClasses, children, linkify } = props;
246+
const { className, useClasses, children, linkify, fuzzyLinks } = props;
169247
return React.createElement(
170248
"code",
171249
{ className },
172250
ansiToJSON(children ?? "", useClasses ?? false).map(
173-
convertBundleIntoReact.bind(null, linkify ?? false, useClasses ?? false)
251+
convertBundleIntoReact.bind(
252+
null,
253+
linkify ?? false,
254+
fuzzyLinks ?? false,
255+
useClasses ?? false
256+
)
174257
)
175258
);
176259
}

0 commit comments

Comments
 (0)