forked from mirrors/thatmattlove-hyperglass
Closes #173: Implement customizable highlighting of text output
This commit is contained in:
parent
5bf69f7923
commit
bd0eb65ffc
6 changed files with 110 additions and 24 deletions
|
|
@ -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):
|
||||
|
|
|
|||
57
hyperglass/ui/components/output/highlighted.tsx
Normal file
57
hyperglass/ui/components/output/highlighted.tsx
Normal 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);
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
1
hyperglass/ui/package.json
vendored
1
hyperglass/ui/package.json
vendored
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
5
hyperglass/ui/yarn.lock
vendored
5
hyperglass/ui/yarn.lock
vendored
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue