1
0
Fork 1
mirror of https://github.com/thatmattlove/hyperglass.git synced 2026-01-17 00:38:06 +00:00

Closes #173: Implement customizable highlighting of text output

This commit is contained in:
thatmattlove 2021-12-14 01:31:45 -07:00
parent 5bf69f7923
commit bd0eb65ffc
6 changed files with 110 additions and 24 deletions

View file

@ -61,7 +61,7 @@ class Menu(HyperglassModel):
order: StrictInt = 0
@validator("content")
def validate_content(cls, value):
def validate_content(cls: "Menu", value: str) -> str:
"""Read content from file if a path is provided."""
if len(value) < 260:
@ -135,14 +135,14 @@ class Text(HyperglassModel):
ip_button: StrictStr = "My IP"
@validator("title_mode")
def validate_title_mode(cls, value):
def validate_title_mode(cls: "Text", value: str) -> str:
"""Set legacy logo_title to logo_subtitle."""
if value == "logo_title":
value = "logo_subtitle"
return value
@validator("cache_prefix")
def validate_cache_prefix(cls, value):
def validate_cache_prefix(cls: "Text", value: str) -> str:
"""Ensure trailing whitespace."""
return " ".join(value.split()) + " "
@ -172,21 +172,16 @@ class ThemeColors(HyperglassModel):
danger: t.Optional[Color]
@validator(*FUNC_COLOR_MAP.keys(), pre=True, always=True)
def validate_colors(cls, value, values, field):
"""Set default functional color mapping.
Arguments:
value {str|None} -- Functional color
values {str} -- Already-validated colors
Returns:
{str} -- Mapped color.
"""
def validate_colors(
cls: "ThemeColors", value: str, values: t.Dict[str, t.Optional[str]], field
) -> str:
"""Set default functional color mapping."""
if value is None:
default_color = FUNC_COLOR_MAP[field.name]
value = str(values[default_color])
return value
def dict(self, *args, **kwargs):
def dict(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, str]:
"""Return dict for colors only."""
return {k: v.as_hex() for k, v in self.__dict__.items()}
@ -213,20 +208,32 @@ class DnsOverHttps(HyperglassModel):
url: StrictStr = ""
@root_validator
def validate_dns(cls, values):
"""Assign url field to model based on selected provider.
Arguments:
values {dict} -- Dict of selected provider
Returns:
{dict} -- Dict with url attribute
"""
def validate_dns(cls: "DnsOverHttps", values: t.Dict[str, str]) -> t.Dict[str, str]:
"""Assign url field to model based on selected provider."""
provider = values["name"]
values["url"] = DNS_OVER_HTTPS[provider]
return values
class HighlightPattern(HyperglassModel):
"""Validation model for highlight pattern configuration."""
pattern: StrictStr
label: t.Optional[StrictStr] = None
color: StrictStr = "primary"
@validator("color")
def validate_color(cls: "HighlightPattern", value: str) -> str:
"""Ensure highlight color is a valid theme color."""
colors = list(ThemeColors.__fields__.keys())
color_list = "\n - ".join(("", *colors))
if value not in colors:
raise ValueError(
"{!r} is not a supported color. Must be one of:{!s}".format(value, color_list)
)
return value
class Web(HyperglassModel):
"""Validation model for all web/browser-related configuration."""
@ -247,6 +254,7 @@ class Web(HyperglassModel):
location_display_mode: LocationDisplayMode = "auto"
custom_javascript: t.Optional[FilePath]
custom_html: t.Optional[FilePath]
highlight: t.List[HighlightPattern] = []
class WebPublic(Web):

View file

@ -0,0 +1,57 @@
import React, { memo } from 'react';
import { Badge, Tooltip, useStyleConfig } from '@chakra-ui/react';
import isEqual from 'react-fast-compare';
import replace from 'react-string-replace';
import type { TooltipProps } from '@chakra-ui/react';
import type { Highlight as HighlightConfig } from '~/types';
interface HighlightedProps {
patterns: HighlightConfig[];
children: string;
}
interface HighlightProps {
label: string | null;
colorScheme: string;
children: React.ReactNode;
}
const Highlight = (props: HighlightProps): JSX.Element => {
const { colorScheme, label, children } = props;
const { bg, color } = useStyleConfig('Button', { colorScheme }) as TooltipProps;
return (
<Tooltip label={label} bg={bg} color={color} hasArrow>
<Badge colorScheme={colorScheme}>{children}</Badge>
</Tooltip>
);
};
const _Highlighted = (props: HighlightedProps): JSX.Element => {
const { patterns, children } = props;
let result: React.ReactNodeArray = [];
let times: number = 0;
for (const config of patterns) {
let toReplace: string | React.ReactNodeArray = children;
if (times !== 0) {
toReplace = result;
}
result = replace(toReplace, new RegExp(`(${config.pattern})`, 'gm'), (m, i) => (
<Highlight key={`${m + i}`} label={config.label} colorScheme={config.color}>
{m}
</Highlight>
));
times++;
}
return (
<>
{result.map(r => (
<>{r}</>
))}
</>
);
};
export const Highlighted = memo(_Highlighted, isEqual);

View file

@ -1,5 +1,6 @@
import { Box } from '@chakra-ui/react';
import { useColorValue } from '~/context';
import { useColorValue, useConfig } from '~/context';
import { Highlighted } from './highlighted';
import type { TTextOutput } from './types';
@ -11,6 +12,10 @@ export const TextOutput: React.FC<TTextOutput> = (props: TTextOutput) => {
const selectionBg = useColorValue('black', 'white');
const selectionColor = useColorValue('white', 'black');
const {
web: { highlight },
} = useConfig();
return (
<Box
p={3}
@ -33,7 +38,9 @@ export const TextOutput: React.FC<TTextOutput> = (props: TTextOutput) => {
}}
{...rest}
>
{children.split('\\n').join('\n').replace(/\n\n/g, '\n')}
<Highlighted patterns={highlight}>
{children.split('\\n').join('\n').replace(/\n\n/g, '\n')}
</Highlighted>
</Box>
);
};

View file

@ -42,6 +42,7 @@
"react-markdown": "^5.0.3",
"react-query": "^3.16.0",
"react-select": "^5.2.1",
"react-string-replace": "^v0.5.0",
"react-table": "^7.7.0",
"remark-gfm": "^1.0.0",
"string-format": "^2.0.0",

View file

@ -85,6 +85,12 @@ interface _Credit {
enable: boolean;
}
interface _Highlight {
pattern: string;
label: string | null;
color: string;
}
interface _Web {
credit: _Credit;
dns_provider: { name: string; url: string };
@ -97,6 +103,7 @@ interface _Web {
text: _Text;
theme: _ThemeConfig;
location_display_mode: 'auto' | 'gallery' | 'dropdown';
highlight: _Highlight[];
}
type _DirectiveBase = {
@ -201,3 +208,4 @@ export type Greeting = CamelCasedProperties<_Greeting>;
export type Logo = CamelCasedProperties<_Logo>;
export type Link = CamelCasedProperties<_Link>;
export type Menu = CamelCasedProperties<_Menu>;
export type Highlight = CamelCasedProperties<_Highlight>;

View file

@ -7058,6 +7058,11 @@ react-simple-animate@^3.3.12:
resolved "https://registry.yarnpkg.com/react-simple-animate/-/react-simple-animate-3.3.12.tgz#ddea0f230feb3c1f069fbdb0a26e735e0b233265"
integrity sha512-lFXjxD6ficcpOMsHfcDs1jqdkCve6jNlJnubOCzVOLswFDRANsaLN4KwpezDuliEFz8Q1zyj4J7Tmj3KMRnPcg==
react-string-replace@^v0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/react-string-replace/-/react-string-replace-0.5.0.tgz#d563ea608f98c449eea5eb3c7511fe806dba7025"
integrity sha512-xtfotmm+Gby5LjfYg3s5+eT6bnwgOIdZRAdHTouLY/7SicDtX4JXjG7CAGaGDcS0ax4nsaaEVNRxArSa8BgQKw==
react-style-singleton@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.1.1.tgz#ce7f90b67618be2b6b94902a30aaea152ce52e66"