Skip to content

Commit

Permalink
feat: add full support for fuzzy linking
Browse files Browse the repository at this point in the history
this PR adds full support for fuzzy links. to keep backward
compat, the old linkify is still kept.
  • Loading branch information
robertkowalski committed Mar 28, 2022
1 parent f3bbc6e commit b33bc4f
Show file tree
Hide file tree
Showing 4 changed files with 1,315 additions and 1,026 deletions.
15 changes: 15 additions & 0 deletions __tests__/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,5 +320,20 @@ describe("Ansi", () => {
'<code><span>hello </span><span style="color:rgb(0, 187, 0);font-weight:bold">world</span><span>!</span></code>'
);
});

test("can linkify fuzzy links", () => {
const el = shallow(
React.createElement(
Ansi,
{ linkify: true, fuzzyLinks: true },
"this is a fuzzy link: example.com"
)
);
expect(el).not.toBeNull();
expect(el.text()).toBe("this is a fuzzy link: example.com");
expect(el.html()).toBe(
'<code><span>this is a fuzzy link: <a href="http://example.com" target="_blank">example.com</a></span></code>'
);
});
});
});
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"license": "BSD-3-Clause",
"dependencies": {
"anser": "^1.4.1",
"escape-carriage": "^1.3.0"
"escape-carriage": "^1.3.0",
"linkify-it": "^3.0.3"
},
"peerDependencies": {
"react": "^16.3.2 || ^17.0.0",
Expand All @@ -33,6 +34,7 @@
"@semantic-release/npm": "^7.0.8",
"@types/enzyme": "^3.10.5",
"@types/jest": "^25.1.4",
"@types/linkify-it": "^3.0.2",
"@types/react": "^16.9.23",
"conventional-changelog-conventionalcommits": "^4.5.0",
"enzyme": "^3.11.0",
Expand Down
89 changes: 86 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Anser, { AnserJsonEntry } from "anser";
import { escapeCarriageReturn } from "escape-carriage";
import linkifyit from "linkify-it";
import * as React from "react";

/**
Expand Down Expand Up @@ -104,6 +105,7 @@ function createStyle(bundle: AnserJsonEntry): React.CSSProperties {

function convertBundleIntoReact(
linkify: boolean,
fuzzyLinks: boolean,
useClasses: boolean,
bundle: AnserJsonEntry,
key: number
Expand All @@ -119,8 +121,22 @@ function convertBundleIntoReact(
);
}

if (fuzzyLinks) {
return linkWithLinkify(bundle, key, style, className);
}

return linkWithClassicMode(bundle, key, style, className);
}

function linkWithClassicMode(
bundle: AnserJsonEntry,
key: number,
style: React.CSSProperties | null,
className: string | null
) {
const content: React.ReactNode[] = [];
const linkRegex = /(\s+|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;
const linkRegex =
/(\s|^)(https?:\/\/(?:www\.|(?!www))[^\s.]+\.[^\s]{2,}|www\.[^\s]+\.[^\s]{2,})/g;

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

function linkWithLinkify(
bundle: AnserJsonEntry,
key: number,
style: React.CSSProperties | null,
className: string | null
): JSX.Element {
const linker = linkifyit({ fuzzyEmail: false }).tlds(["io"], true);

if (!linker.pretest(bundle.content)) {
return React.createElement(
"span",
{ style, key, className },
bundle.content
);
}

const matches = linker.match(bundle.content);

if (!matches) {
return React.createElement(
"span",
{ style, key, className },
bundle.content
);
}

const content: React.ReactNode[] = [
bundle.content.substring(0, matches[0]?.index),
];

matches.forEach((match, i) => {
content.push(
React.createElement(
"a",
{
href: match.url,
target: "_blank",
key: i,
},
bundle.content.substring(match.index, match.lastIndex)
)
);

if (matches[i + 1]) {
content.push(
bundle.content.substring(matches[i].lastIndex, matches[i + 1]?.index)
);
}
});

if (matches[matches.length - 1].lastIndex !== bundle.content.length) {
content.push(
bundle.content.substring(
matches[matches.length - 1].lastIndex,
bundle.content.length
)
);
}
return React.createElement("span", { style, key, className }, content);
}

declare interface Props {
children?: string;
linkify?: boolean;
fuzzyLinks?: boolean;
className?: string;
useClasses?: boolean;
}

export default function Ansi(props: Props): JSX.Element {
const { className, useClasses, children, linkify } = props;
const { className, useClasses, children, linkify, fuzzyLinks } = props;
return React.createElement(
"code",
{ className },
ansiToJSON(children ?? "", useClasses ?? false).map(
convertBundleIntoReact.bind(null, linkify ?? false, useClasses ?? false)
convertBundleIntoReact.bind(
null,
linkify ?? false,
fuzzyLinks ?? false,
useClasses ?? false
)
)
);
}
Expand Down
Loading

0 comments on commit b33bc4f

Please sign in to comment.