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
|
order: StrictInt = 0
|
||||||
|
|
||||||
@validator("content")
|
@validator("content")
|
||||||
def validate_content(cls, value):
|
def validate_content(cls: "Menu", value: str) -> str:
|
||||||
"""Read content from file if a path is provided."""
|
"""Read content from file if a path is provided."""
|
||||||
|
|
||||||
if len(value) < 260:
|
if len(value) < 260:
|
||||||
|
|
@ -135,14 +135,14 @@ class Text(HyperglassModel):
|
||||||
ip_button: StrictStr = "My IP"
|
ip_button: StrictStr = "My IP"
|
||||||
|
|
||||||
@validator("title_mode")
|
@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."""
|
"""Set legacy logo_title to logo_subtitle."""
|
||||||
if value == "logo_title":
|
if value == "logo_title":
|
||||||
value = "logo_subtitle"
|
value = "logo_subtitle"
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@validator("cache_prefix")
|
@validator("cache_prefix")
|
||||||
def validate_cache_prefix(cls, value):
|
def validate_cache_prefix(cls: "Text", value: str) -> str:
|
||||||
"""Ensure trailing whitespace."""
|
"""Ensure trailing whitespace."""
|
||||||
return " ".join(value.split()) + " "
|
return " ".join(value.split()) + " "
|
||||||
|
|
||||||
|
|
@ -172,21 +172,16 @@ class ThemeColors(HyperglassModel):
|
||||||
danger: t.Optional[Color]
|
danger: t.Optional[Color]
|
||||||
|
|
||||||
@validator(*FUNC_COLOR_MAP.keys(), pre=True, always=True)
|
@validator(*FUNC_COLOR_MAP.keys(), pre=True, always=True)
|
||||||
def validate_colors(cls, value, values, field):
|
def validate_colors(
|
||||||
"""Set default functional color mapping.
|
cls: "ThemeColors", value: str, values: t.Dict[str, t.Optional[str]], field
|
||||||
|
) -> str:
|
||||||
Arguments:
|
"""Set default functional color mapping."""
|
||||||
value {str|None} -- Functional color
|
|
||||||
values {str} -- Already-validated colors
|
|
||||||
Returns:
|
|
||||||
{str} -- Mapped color.
|
|
||||||
"""
|
|
||||||
if value is None:
|
if value is None:
|
||||||
default_color = FUNC_COLOR_MAP[field.name]
|
default_color = FUNC_COLOR_MAP[field.name]
|
||||||
value = str(values[default_color])
|
value = str(values[default_color])
|
||||||
return value
|
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 dict for colors only."""
|
||||||
return {k: v.as_hex() for k, v in self.__dict__.items()}
|
return {k: v.as_hex() for k, v in self.__dict__.items()}
|
||||||
|
|
||||||
|
|
@ -213,20 +208,32 @@ class DnsOverHttps(HyperglassModel):
|
||||||
url: StrictStr = ""
|
url: StrictStr = ""
|
||||||
|
|
||||||
@root_validator
|
@root_validator
|
||||||
def validate_dns(cls, values):
|
def validate_dns(cls: "DnsOverHttps", values: t.Dict[str, str]) -> t.Dict[str, str]:
|
||||||
"""Assign url field to model based on selected provider.
|
"""Assign url field to model based on selected provider."""
|
||||||
|
|
||||||
Arguments:
|
|
||||||
values {dict} -- Dict of selected provider
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
{dict} -- Dict with url attribute
|
|
||||||
"""
|
|
||||||
provider = values["name"]
|
provider = values["name"]
|
||||||
values["url"] = DNS_OVER_HTTPS[provider]
|
values["url"] = DNS_OVER_HTTPS[provider]
|
||||||
return values
|
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):
|
class Web(HyperglassModel):
|
||||||
"""Validation model for all web/browser-related configuration."""
|
"""Validation model for all web/browser-related configuration."""
|
||||||
|
|
||||||
|
|
@ -247,6 +254,7 @@ class Web(HyperglassModel):
|
||||||
location_display_mode: LocationDisplayMode = "auto"
|
location_display_mode: LocationDisplayMode = "auto"
|
||||||
custom_javascript: t.Optional[FilePath]
|
custom_javascript: t.Optional[FilePath]
|
||||||
custom_html: t.Optional[FilePath]
|
custom_html: t.Optional[FilePath]
|
||||||
|
highlight: t.List[HighlightPattern] = []
|
||||||
|
|
||||||
|
|
||||||
class WebPublic(Web):
|
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 { Box } from '@chakra-ui/react';
|
||||||
import { useColorValue } from '~/context';
|
import { useColorValue, useConfig } from '~/context';
|
||||||
|
import { Highlighted } from './highlighted';
|
||||||
|
|
||||||
import type { TTextOutput } from './types';
|
import type { TTextOutput } from './types';
|
||||||
|
|
||||||
|
|
@ -11,6 +12,10 @@ export const TextOutput: React.FC<TTextOutput> = (props: TTextOutput) => {
|
||||||
const selectionBg = useColorValue('black', 'white');
|
const selectionBg = useColorValue('black', 'white');
|
||||||
const selectionColor = useColorValue('white', 'black');
|
const selectionColor = useColorValue('white', 'black');
|
||||||
|
|
||||||
|
const {
|
||||||
|
web: { highlight },
|
||||||
|
} = useConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
p={3}
|
p={3}
|
||||||
|
|
@ -33,7 +38,9 @@ export const TextOutput: React.FC<TTextOutput> = (props: TTextOutput) => {
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...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>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
1
hyperglass/ui/package.json
vendored
1
hyperglass/ui/package.json
vendored
|
|
@ -42,6 +42,7 @@
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
"react-query": "^3.16.0",
|
"react-query": "^3.16.0",
|
||||||
"react-select": "^5.2.1",
|
"react-select": "^5.2.1",
|
||||||
|
"react-string-replace": "^v0.5.0",
|
||||||
"react-table": "^7.7.0",
|
"react-table": "^7.7.0",
|
||||||
"remark-gfm": "^1.0.0",
|
"remark-gfm": "^1.0.0",
|
||||||
"string-format": "^2.0.0",
|
"string-format": "^2.0.0",
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,12 @@ interface _Credit {
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface _Highlight {
|
||||||
|
pattern: string;
|
||||||
|
label: string | null;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface _Web {
|
interface _Web {
|
||||||
credit: _Credit;
|
credit: _Credit;
|
||||||
dns_provider: { name: string; url: string };
|
dns_provider: { name: string; url: string };
|
||||||
|
|
@ -97,6 +103,7 @@ interface _Web {
|
||||||
text: _Text;
|
text: _Text;
|
||||||
theme: _ThemeConfig;
|
theme: _ThemeConfig;
|
||||||
location_display_mode: 'auto' | 'gallery' | 'dropdown';
|
location_display_mode: 'auto' | 'gallery' | 'dropdown';
|
||||||
|
highlight: _Highlight[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type _DirectiveBase = {
|
type _DirectiveBase = {
|
||||||
|
|
@ -201,3 +208,4 @@ export type Greeting = CamelCasedProperties<_Greeting>;
|
||||||
export type Logo = CamelCasedProperties<_Logo>;
|
export type Logo = CamelCasedProperties<_Logo>;
|
||||||
export type Link = CamelCasedProperties<_Link>;
|
export type Link = CamelCasedProperties<_Link>;
|
||||||
export type Menu = CamelCasedProperties<_Menu>;
|
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"
|
resolved "https://registry.yarnpkg.com/react-simple-animate/-/react-simple-animate-3.3.12.tgz#ddea0f230feb3c1f069fbdb0a26e735e0b233265"
|
||||||
integrity sha512-lFXjxD6ficcpOMsHfcDs1jqdkCve6jNlJnubOCzVOLswFDRANsaLN4KwpezDuliEFz8Q1zyj4J7Tmj3KMRnPcg==
|
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:
|
react-style-singleton@^2.1.0:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.1.1.tgz#ce7f90b67618be2b6b94902a30aaea152ce52e66"
|
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.1.1.tgz#ce7f90b67618be2b6b94902a30aaea152ce52e66"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue