Skip to content

Commit

Permalink
Issues/23 clickable links (#26)
Browse files Browse the repository at this point in the history
* linkify urls

* update readme

* handle settings upgrade
  • Loading branch information
paolosimone committed Jun 9, 2022
1 parent 7073fdb commit 6f700ed
Show file tree
Hide file tree
Showing 28 changed files with 427 additions and 75 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ The extension has not been tested on other browsers, but should work on any chro
- [X] Preview nested item count for closed nodes
- [X] Color-encoded value types
- [X] Collapse/expand all nodes
- [X] Clickable URLs
- [X] Full text search
- [X] Highlight search results
- [X] Option to completely hide subtrees without any search match
Expand Down Expand Up @@ -202,6 +203,7 @@ Always `yarn format` before creating a commit.

|Tool |Usage |
|---------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|
|[anchorme](https://github.com/alexcorvi/anchorme.js) |Convert URLs to clickable HTML links |
|[cra-template-complex-browserext-typescript](https://github.com/hindmost/cra-template-complex-browserext-typescript) |Project scaffolding, huge help! |
|[customize-cra](https://github.com/arackaf/customize-cra) |Break webpack config, then fix it |
|[jq-wasm](https://github.com/paolosimone/jq-wasm) |JQ in the browser |
Expand Down
1 change: 1 addition & 0 deletions extension/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
rules: {
"react/react-in-jsx-scope": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
Expand Down
1 change: 1 addition & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.4",
"private": true,
"dependencies": {
"anchorme": "^2.1.2",
"json-stable-stringify": "^1.0.1",
"react": "^17.0.2",
"react-app-rewired": "^2.0",
Expand Down
3 changes: 2 additions & 1 deletion extension/src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ declare type EmptyObject = Record<string, never>;
declare type Color = string;

// Add some common React props fields to T
declare type Props<T> = T & {
declare type BaseProps = {
style?: React.CSSProperties;
className?: string;
children?: React.ReactNode;
};
declare type Props<T> = BaseProps & T;
11 changes: 10 additions & 1 deletion extension/src/options/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
TextSize,
} from "viewer/state";
import {
Checkbox,
LanguageSelect,
NumberInput,
TextSizeSelect,
Expand All @@ -32,7 +33,7 @@ export function App(): JSX.Element {
resolveTextSizeClass(settings.textSize)
)}
>
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-2 gap-3 items-center">
<label>{t.settings.labels.theme}</label>
<ThemeSelect theme={theme} setTheme={setTheme} />

Expand Down Expand Up @@ -64,6 +65,14 @@ export function App(): JSX.Element {
updateSettings({ searchDelay: newValue })
}
/>

<label>{t.settings.labels.linkifyUrls}</label>
<Checkbox
checked={settings.linkifyUrls}
setChecked={(checked: boolean) =>
updateSettings({ linkifyUrls: checked })
}
/>
</div>

<div className="flex items-center justify-center pt-6">
Expand Down
30 changes: 30 additions & 0 deletions extension/src/options/components/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import classNames from "classnames";
import { Dispatch, FormEvent } from "react";

export type CheckboxProps = Props<{
setChecked: Dispatch<boolean>;
checked?: boolean;
}>;

export function Checkbox({
setChecked,
checked,
className,
}: CheckboxProps): JSX.Element {
const setNewChecked = (e: FormEvent<HTMLInputElement>) => {
const newChecked = (e.target as HTMLInputElement).checked;
setChecked(newChecked);
};

return (
<input
type="checkbox"
className={classNames(
"pl-1 dark:bg-gray-500 focus:outline-none cursor-pointer",
className
)}
onChange={setNewChecked}
checked={checked}
/>
);
}
2 changes: 1 addition & 1 deletion extension/src/options/components/NumberInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function NumberInput({
<input
type="number"
className={classNames(
"pl-1 dark:bg-gray-500 focus:outline-none",
"pl-1 dark:bg-gray-500 focus:outline-none cursor-pointer",
className
)}
onChange={setNewValue}
Expand Down
5 changes: 4 additions & 1 deletion extension/src/options/components/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export function Select<T extends string>({

return (
<select
className={classNames("dark:bg-gray-500 focus:outline-none", className)}
className={classNames(
"dark:bg-gray-500 focus:outline-none cursor-pointer",
className
)}
onChange={setNewValue}
value={selected}
>
Expand Down
1 change: 1 addition & 0 deletions extension/src/options/components/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./Checkbox";
export * from "./LanguageSelect";
export * from "./NumberInput";
export * from "./Select";
Expand Down
29 changes: 0 additions & 29 deletions extension/src/viewer/components/HighlightedText.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions extension/src/viewer/components/RawViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "react";
import { EventType } from "viewer/commons/EventBus";
import * as Json from "viewer/commons/Json";
import { useEventBusListener, useHighlightedSearchResults } from "viewer/hooks";
import { useEventBusListener, useRenderedText } from "viewer/hooks";
import { Search, SettingsContext } from "viewer/state";

export type RawViewerProps = Props<{
Expand Down Expand Up @@ -39,7 +39,7 @@ export function RawViewer({
[json, space]
);

const highlightedText = useHighlightedSearchResults(raw, search);
const highlightedText = useRenderedText(raw, search);

const ref = useRef<HTMLDivElement>(null);
useSelectAllText(ref);
Expand Down
45 changes: 45 additions & 0 deletions extension/src/viewer/components/RenderedText/HighlightedText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ReactElement } from "react";
import { uid } from "uid";
import { Match } from "./Match";

export const SEARCH_TYPE = "search";

export interface SearchMatch extends Match<EmptyObject> {
type: typeof SEARCH_TYPE;
}

// Rendering

export function HighlightedText({
children,
}: BaseProps): ReactElement<HTMLElement> {
return <mark>{children}</mark>;
}

// Matching

export type SearchOptions = Props<{
searchText: string;
caseSensitive?: boolean;
}>;

export function matchSearch(
text: string,
{ searchText, caseSensitive }: SearchOptions
): SearchMatch[] {
const flags = "g" + (caseSensitive ? "" : "i");
const matches = text.matchAll(new RegExp(escapeRegExp(searchText), flags));

return Array.from(matches, (match) => ({
id: uid(),
start: match.index!,
end: match.index! + searchText.length,
type: SEARCH_TYPE,
metadata: {},
}));
}

// Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
68 changes: 68 additions & 0 deletions extension/src/viewer/components/RenderedText/LinkifiedText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import anchorme from "anchorme";
import type { ListingProps } from "anchorme/dist/node/types";
import { ReactElement } from "react";
import { uid } from "uid";
import { Match } from "./Match";

export const LINK_TYPE = "link";

// Rendering

export type LinkMetadata = {
linkType: "url" | "email";
href: string;
};

export interface LinkMatch extends Match<LinkMetadata> {
type: typeof LINK_TYPE;
}

export function LinkifiedText({
children,
href,
linkType,
}: Props<LinkMetadata>): ReactElement<HTMLElement> {
return (
<a
href={href}
target={linkType === "email" ? undefined : "_blank"}
rel="noreferrer"
className="underline"
>
{children}
</a>
);
}

// Matching

export function matchLinks(text: string): LinkMatch[] {
return anchorme
.list(text)
.filter((match) => getHref(match) !== null)
.map((match) => ({
id: uid(),
start: match.start,
end: match.end,
type: LINK_TYPE,
metadata: {
linkType: match.isEmail ? "email" : "url",
href: getHref(match)!,
},
}));
}

function getHref(match: ListingProps): Nullable<string> {
// already has the protocol
if (match.protocol) {
return match.string;
}

// set default protocol for emails
if (match.isEmail) {
return "mailto:" + match.string;
}

// ignore url matches without protocol to avoid false positives
return null;
}
7 changes: 7 additions & 0 deletions extension/src/viewer/components/RenderedText/Match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Match<Metadata> {
id: string;
start: number;
end: number;
type: string;
metadata: Metadata;
}
Loading

0 comments on commit 6f700ed

Please sign in to comment.