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 93ca146 commit 1340cb0
Show file tree
Hide file tree
Showing 4 changed files with 2,330 additions and 2,955 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
134 changes: 109 additions & 25 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 @@ -67,29 +68,29 @@ function createStyle(bundle: AnserJsonEntry): React.CSSProperties {
style.color = `rgb(${bundle.fg})`;
}
switch (bundle.decoration) {
case 'bold':
style.fontWeight = 'bold';
break;
case 'dim':
style.opacity = '0.5';
break;
case 'italic':
style.fontStyle = 'italic';
break;
case 'hidden':
style.visibility = 'hidden';
break;
case 'strikethrough':
style.textDecoration = 'line-through';
break;
case 'underline':
style.textDecoration = 'underline';
break;
case 'blink':
style.textDecoration = 'blink';
break;
case "bold":
style.fontWeight = "bold";
break;
case "dim":
style.opacity = "0.5";
break;
case "italic":
style.fontStyle = "italic";
break;
case "hidden":
style.visibility = "hidden";
break;
case "strikethrough":
style.textDecoration = "line-through";
break;
case "underline":
style.textDecoration = "underline";
break;
case "blink":
style.textDecoration = "blink";
break;
default:
break;
break;
}
return style;
}
Expand All @@ -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,23 @@ 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 +174,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 1340cb0

Please sign in to comment.