Skip to content

Commit 1340cb0

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 93ca146 commit 1340cb0

File tree

4 files changed

+2330
-2955
lines changed

4 files changed

+2330
-2955
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

+109-25
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
/**
@@ -67,29 +68,29 @@ function createStyle(bundle: AnserJsonEntry): React.CSSProperties {
6768
style.color = `rgb(${bundle.fg})`;
6869
}
6970
switch (bundle.decoration) {
70-
case 'bold':
71-
style.fontWeight = 'bold';
72-
break;
73-
case 'dim':
74-
style.opacity = '0.5';
75-
break;
76-
case 'italic':
77-
style.fontStyle = 'italic';
78-
break;
79-
case 'hidden':
80-
style.visibility = 'hidden';
81-
break;
82-
case 'strikethrough':
83-
style.textDecoration = 'line-through';
84-
break;
85-
case 'underline':
86-
style.textDecoration = 'underline';
87-
break;
88-
case 'blink':
89-
style.textDecoration = 'blink';
90-
break;
71+
case "bold":
72+
style.fontWeight = "bold";
73+
break;
74+
case "dim":
75+
style.opacity = "0.5";
76+
break;
77+
case "italic":
78+
style.fontStyle = "italic";
79+
break;
80+
case "hidden":
81+
style.visibility = "hidden";
82+
break;
83+
case "strikethrough":
84+
style.textDecoration = "line-through";
85+
break;
86+
case "underline":
87+
style.textDecoration = "underline";
88+
break;
89+
case "blink":
90+
style.textDecoration = "blink";
91+
break;
9192
default:
92-
break;
93+
break;
9394
}
9495
return style;
9596
}
@@ -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,23 @@ 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+
139+
const linkRegex =
140+
/(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
124141

125142
let index = 0;
126143
let match: RegExpExecArray | null;
@@ -157,20 +174,87 @@ function convertBundleIntoReact(
157174
return React.createElement("span", { style, key, className }, content);
158175
}
159176

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

167246
export default function Ansi(props: Props): JSX.Element {
168-
const { className, useClasses, children, linkify } = props;
247+
const { className, useClasses, children, linkify, fuzzyLinks } = props;
169248
return React.createElement(
170249
"code",
171250
{ className },
172251
ansiToJSON(children ?? "", useClasses ?? false).map(
173-
convertBundleIntoReact.bind(null, linkify ?? false, useClasses ?? false)
252+
convertBundleIntoReact.bind(
253+
null,
254+
linkify ?? false,
255+
fuzzyLinks ?? false,
256+
useClasses ?? false
257+
)
174258
)
175259
);
176260
}

0 commit comments

Comments
 (0)