Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues/23 clickable links #26

Merged
merged 5 commits into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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