mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-12-20 13:15:48 +01:00
refactor ANSII colors into it's own component
This commit is contained in:
parent
7d263e4223
commit
ad098e1f05
@ -16,6 +16,7 @@ import { BitFlumeModal } from "../../BitNode/ui/BitFlumeModal";
|
|||||||
import { CodingContractModal } from "../../ui/React/CodingContractModal";
|
import { CodingContractModal } from "../../ui/React/CodingContractModal";
|
||||||
|
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { ANSIITypography } from "../../ui/React/ANSIITypography";
|
||||||
|
|
||||||
interface IActionTimerProps {
|
interface IActionTimerProps {
|
||||||
terminal: ITerminal;
|
terminal: ITerminal;
|
||||||
@ -43,37 +44,6 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
padding: theme.spacing(0),
|
padding: theme.spacing(0),
|
||||||
height: "100%",
|
height: "100%",
|
||||||
},
|
},
|
||||||
|
|
||||||
success: {
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
margin: theme.spacing(0),
|
|
||||||
color: theme.colors.success,
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
margin: theme.spacing(0),
|
|
||||||
color: theme.palette.error.main,
|
|
||||||
},
|
|
||||||
primary: {
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
margin: theme.spacing(0),
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
},
|
|
||||||
info: {
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
margin: theme.spacing(0),
|
|
||||||
color: theme.palette.info.main,
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
margin: theme.spacing(0),
|
|
||||||
color: theme.palette.warning.main,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -133,194 +103,6 @@ export function TerminalRoot({ terminal, router, player }: IProps): React.ReactE
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function lineClass(s: string): string {
|
|
||||||
const lineClassMap: Record<string, string> = {
|
|
||||||
error: classes.error,
|
|
||||||
success: classes.success,
|
|
||||||
info: classes.info,
|
|
||||||
warn: classes.warning,
|
|
||||||
};
|
|
||||||
return lineClassMap[s] || classes.primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ansiCodeStyle(code: string | null): Record<string, any> {
|
|
||||||
// The ANSI colors actually have the dark color set as default and require extra work to get
|
|
||||||
// bright colors. But these are rarely used or, if they are, are often re-mapped by the
|
|
||||||
// terminal emulator to brighter colors. So for foreground colors we use the bright color set
|
|
||||||
// and for background colors we use the dark color set. Of course, all colors are available
|
|
||||||
// via the longer ESC[n8;5;c] sequence (n={3,4}, c=color). Ideally, these 8-bit maps could
|
|
||||||
// be managed in the user preferences/theme.
|
|
||||||
const COLOR_MAP_BRIGHT: Record<string | number, string> = {
|
|
||||||
0: "#404040",
|
|
||||||
1: "#ff0000",
|
|
||||||
2: "#00ff00",
|
|
||||||
3: "#ffff00",
|
|
||||||
4: "#0000ff",
|
|
||||||
5: "#ff00ff",
|
|
||||||
6: "#00ffff",
|
|
||||||
7: "#ffffff",
|
|
||||||
};
|
|
||||||
const COLOR_MAP_DARK: Record<string | number, string> = {
|
|
||||||
0: "#000000",
|
|
||||||
1: "#800000",
|
|
||||||
2: "#008000",
|
|
||||||
3: "#808000",
|
|
||||||
4: "#000080",
|
|
||||||
5: "#800080",
|
|
||||||
6: "#008080",
|
|
||||||
7: "#c0c0c0",
|
|
||||||
};
|
|
||||||
|
|
||||||
const ansi2rgb = (code: number): string => {
|
|
||||||
/* eslint-disable yoda */
|
|
||||||
if (0 <= code && code < 8) {
|
|
||||||
// x8 RGB
|
|
||||||
return COLOR_MAP_BRIGHT[code];
|
|
||||||
}
|
|
||||||
if (8 <= code && code < 16) {
|
|
||||||
// x8 RGB - "High Intensity" (but here, actually the dark set)
|
|
||||||
return COLOR_MAP_DARK[code];
|
|
||||||
}
|
|
||||||
if (16 <= code && code < 232) {
|
|
||||||
// x216 RGB
|
|
||||||
const base = code - 16;
|
|
||||||
const ir = Math.floor(base / 36);
|
|
||||||
const ig = Math.floor((base % 36) / 6);
|
|
||||||
const ib = Math.floor((base % 6) / 1);
|
|
||||||
const r = ir <= 0 ? 0 : 55 + ir * 40;
|
|
||||||
const g = ig <= 0 ? 0 : 55 + ig * 40;
|
|
||||||
const b = ib <= 0 ? 0 : 55 + ib * 40;
|
|
||||||
return `rgb(${r}, ${g}, ${b})`;
|
|
||||||
}
|
|
||||||
if (232 <= code && code < 256) {
|
|
||||||
// x32 greyscale
|
|
||||||
const base = code - 232;
|
|
||||||
const grey = base * 10 + 8;
|
|
||||||
return `rgb(${grey}, ${grey}, ${grey})`;
|
|
||||||
}
|
|
||||||
// shouldn't get here (under normal circumstances), but just in case
|
|
||||||
return "initial";
|
|
||||||
};
|
|
||||||
|
|
||||||
type styleKey = "fontWeight" | "textDecoration" | "color" | "backgroundColor" | "display";
|
|
||||||
const style: {
|
|
||||||
fontWeight?: string;
|
|
||||||
textDecoration?: string;
|
|
||||||
color?: string;
|
|
||||||
backgroundColor?: string;
|
|
||||||
display?: string;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
if (code === null || code === "0") {
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
const codeParts = code
|
|
||||||
.split(";")
|
|
||||||
.map((p) => parseInt(p))
|
|
||||||
.filter(
|
|
||||||
(p, i, arr) =>
|
|
||||||
// If the sequence is 38;5 (x256 foreground color) or 48;5 (x256 background color),
|
|
||||||
// filter out the 5 so the next codePart after {38,48} is the color code.
|
|
||||||
p != 5 || i == 0 || (arr[i - 1] != 38 && arr[i - 1] != 48),
|
|
||||||
);
|
|
||||||
|
|
||||||
let nextStyleKey: styleKey | null = null;
|
|
||||||
codeParts.forEach((codePart) => {
|
|
||||||
/* eslint-disable yoda */
|
|
||||||
if (nextStyleKey !== null) {
|
|
||||||
style[nextStyleKey] = ansi2rgb(codePart);
|
|
||||||
nextStyleKey = null;
|
|
||||||
}
|
|
||||||
// Decorations
|
|
||||||
else if (codePart == 1) {
|
|
||||||
style.fontWeight = "bold";
|
|
||||||
} else if (codePart == 4) {
|
|
||||||
style.textDecoration = "underline";
|
|
||||||
}
|
|
||||||
// Forground Color (x8)
|
|
||||||
else if (30 <= codePart && codePart < 38) {
|
|
||||||
if (COLOR_MAP_BRIGHT[codePart % 10]) {
|
|
||||||
style.color = COLOR_MAP_BRIGHT[codePart % 10];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Background Color (x8)
|
|
||||||
else if (40 <= codePart && codePart < 48) {
|
|
||||||
if (COLOR_MAP_DARK[codePart % 10]) {
|
|
||||||
style.backgroundColor = COLOR_MAP_DARK[codePart % 10];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Forground Color (x256)
|
|
||||||
else if (codePart == 38) {
|
|
||||||
nextStyleKey = "color";
|
|
||||||
}
|
|
||||||
// Background Color (x256)
|
|
||||||
else if (codePart == 48) {
|
|
||||||
nextStyleKey = "backgroundColor";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// If a background color is set, render this as an inline block element (instead of inline)
|
|
||||||
// so that the background between lines (at least those that don't wrap) is uninterrupted.
|
|
||||||
if (style.backgroundColor !== undefined) {
|
|
||||||
style.display = "inline-block";
|
|
||||||
}
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseOutputText(item: Output): React.ReactElement {
|
|
||||||
const parts = [];
|
|
||||||
// Some things, oddly, can cause item.text to not be a string (e.g. expr from the CLI), which
|
|
||||||
// causes .matchAll to throw. Ensure it's a string immediately
|
|
||||||
if (typeof item.text === "string") {
|
|
||||||
// eslint-disable-next-line no-control-regex
|
|
||||||
const ANSI_ESCAPE = new RegExp("\u{001b}\\[(?<code>.*?)m", "ug");
|
|
||||||
// Build a look-alike regex match to place at the front of the matches list
|
|
||||||
const INITIAL = {
|
|
||||||
0: "",
|
|
||||||
index: 0,
|
|
||||||
groups: { code: null },
|
|
||||||
};
|
|
||||||
const matches = [INITIAL, ...item.text.matchAll(ANSI_ESCAPE), null];
|
|
||||||
if (matches.length > 2) {
|
|
||||||
matches.slice(0, -1).forEach((m, i) => {
|
|
||||||
const n = matches[i + 1];
|
|
||||||
if (!m || m.index === undefined || m.groups === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const startIndex = m.index + m[0].length;
|
|
||||||
const stopIndex = n ? n.index : item.text.length;
|
|
||||||
const partText = item.text.slice(startIndex, stopIndex);
|
|
||||||
if (startIndex !== stopIndex) {
|
|
||||||
// Don't generate "empty" spans
|
|
||||||
parts.push({ code: m.groups.code, text: partText });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (parts.length === 0) {
|
|
||||||
// For example, if the string was empty or there were no escape sequence matches
|
|
||||||
parts.push({ code: null, text: item.text });
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Typography classes={{ root: lineClass(item.color) }} paragraph={false}>
|
|
||||||
{parts.map((part, index) => {
|
|
||||||
const spanStyle = ansiCodeStyle(part.code);
|
|
||||||
if (!_.isEmpty(spanStyle)) {
|
|
||||||
// Only wrap parts with spans if the calculated spanStyle is non-empty...
|
|
||||||
return (
|
|
||||||
<Typography key={index} paragraph={false} component="span" sx={spanStyle}>
|
|
||||||
{part.text}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// ... otherwise yield the unmodified, unwrapped part.
|
|
||||||
return part.text;
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -330,9 +112,7 @@ export function TerminalRoot({ terminal, router, player }: IProps): React.ReactE
|
|||||||
if (item instanceof Output)
|
if (item instanceof Output)
|
||||||
return (
|
return (
|
||||||
<ListItem key={i} classes={{ root: classes.nopadding }}>
|
<ListItem key={i} classes={{ root: classes.nopadding }}>
|
||||||
<Typography classes={{ root: lineClass(item.color) }} paragraph={false}>
|
<ANSIITypography text={item.text} color={item.color} />
|
||||||
{parseOutputText(item)}
|
|
||||||
</Typography>
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
if (item instanceof RawOutput)
|
if (item instanceof RawOutput)
|
||||||
|
235
src/ui/React/ANSIITypography.tsx
Normal file
235
src/ui/React/ANSIITypography.tsx
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import { Typography } from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import makeStyles from "@mui/styles/makeStyles";
|
||||||
|
import createStyles from "@mui/styles/createStyles";
|
||||||
|
import { Theme } from "@mui/material/styles";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
// This particular eslint-disable is correct.
|
||||||
|
// In this super specific weird case we in fact do want a regex on an ANSII character.
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const ANSI_ESCAPE = new RegExp("\u{001b}\\[(?<code>.*?)m", "ug");
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
success: {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
margin: theme.spacing(0),
|
||||||
|
color: theme.colors.success,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
margin: theme.spacing(0),
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
margin: theme.spacing(0),
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
margin: theme.spacing(0),
|
||||||
|
color: theme.palette.info.main,
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
overflowWrap: "anywhere",
|
||||||
|
margin: theme.spacing(0),
|
||||||
|
color: theme.palette.warning.main,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const lineClass = (classes: Record<string, string>, s: string): string => {
|
||||||
|
const lineClassMap: Record<string, string> = {
|
||||||
|
error: classes.error,
|
||||||
|
success: classes.success,
|
||||||
|
info: classes.info,
|
||||||
|
warn: classes.warning,
|
||||||
|
};
|
||||||
|
return lineClassMap[s] || classes.primary;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
text: string;
|
||||||
|
color: "primary" | "error" | "success" | "info" | "warn";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ANSIITypography = React.memo((props: IProps): React.ReactElement => {
|
||||||
|
const classes = useStyles();
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
// Build a look-alike regex match to place at the front of the matches list
|
||||||
|
const INITIAL = {
|
||||||
|
0: "",
|
||||||
|
index: 0,
|
||||||
|
groups: { code: null },
|
||||||
|
};
|
||||||
|
const matches = [INITIAL, ...props.text.matchAll(ANSI_ESCAPE), null];
|
||||||
|
if (matches.length > 2) {
|
||||||
|
matches.slice(0, -1).forEach((m, i) => {
|
||||||
|
const n = matches[i + 1];
|
||||||
|
if (!m || m.index === undefined || m.groups === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const startIndex = m.index + m[0].length;
|
||||||
|
const stopIndex = n ? n.index : props.text.length;
|
||||||
|
const partText = props.text.slice(startIndex, stopIndex);
|
||||||
|
if (startIndex !== stopIndex) {
|
||||||
|
// Don't generate "empty" spans
|
||||||
|
parts.push({ code: m.groups.code, text: partText });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (parts.length === 0) {
|
||||||
|
// For example, if the string was empty or there were no escape sequence matches
|
||||||
|
parts.push({ code: null, text: props.text });
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Typography classes={{ root: lineClass(classes, props.color) }} paragraph={false}>
|
||||||
|
{parts.map((part, index) => {
|
||||||
|
const spanStyle = ansiCodeStyle(part.code);
|
||||||
|
if (!_.isEmpty(spanStyle)) {
|
||||||
|
// Only wrap parts with spans if the calculated spanStyle is non-empty...
|
||||||
|
return (
|
||||||
|
<Typography key={index} paragraph={false} component="span" sx={spanStyle}>
|
||||||
|
{part.text}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// ... otherwise yield the unmodified, unwrapped part.
|
||||||
|
return part.text;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function ansiCodeStyle(code: string | null): Record<string, any> {
|
||||||
|
// The ANSI colors actually have the dark color set as default and require extra work to get
|
||||||
|
// bright colors. But these are rarely used or, if they are, are often re-mapped by the
|
||||||
|
// terminal emulator to brighter colors. So for foreground colors we use the bright color set
|
||||||
|
// and for background colors we use the dark color set. Of course, all colors are available
|
||||||
|
// via the longer ESC[n8;5;c] sequence (n={3,4}, c=color). Ideally, these 8-bit maps could
|
||||||
|
// be managed in the user preferences/theme.
|
||||||
|
const COLOR_MAP_BRIGHT: Record<number, string> = {
|
||||||
|
0: "#404040",
|
||||||
|
1: "#ff0000",
|
||||||
|
2: "#00ff00",
|
||||||
|
3: "#ffff00",
|
||||||
|
4: "#0000ff",
|
||||||
|
5: "#ff00ff",
|
||||||
|
6: "#00ffff",
|
||||||
|
7: "#ffffff",
|
||||||
|
};
|
||||||
|
const COLOR_MAP_DARK: Record<number, string> = {
|
||||||
|
0: "#000000",
|
||||||
|
1: "#800000",
|
||||||
|
2: "#008000",
|
||||||
|
3: "#808000",
|
||||||
|
4: "#000080",
|
||||||
|
5: "#800080",
|
||||||
|
6: "#008080",
|
||||||
|
7: "#c0c0c0",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ansi2rgb = (code: number): string => {
|
||||||
|
/* eslint-disable yoda */
|
||||||
|
if (0 <= code && code < 8) {
|
||||||
|
// x8 RGB
|
||||||
|
return COLOR_MAP_BRIGHT[code];
|
||||||
|
}
|
||||||
|
if (8 <= code && code < 16) {
|
||||||
|
// x8 RGB - "High Intensity" (but here, actually the dark set)
|
||||||
|
return COLOR_MAP_DARK[code];
|
||||||
|
}
|
||||||
|
if (16 <= code && code < 232) {
|
||||||
|
// x216 RGB
|
||||||
|
const base = code - 16;
|
||||||
|
const ir = Math.floor(base / 36);
|
||||||
|
const ig = Math.floor((base % 36) / 6);
|
||||||
|
const ib = Math.floor((base % 6) / 1);
|
||||||
|
const r = ir <= 0 ? 0 : 55 + ir * 40;
|
||||||
|
const g = ig <= 0 ? 0 : 55 + ig * 40;
|
||||||
|
const b = ib <= 0 ? 0 : 55 + ib * 40;
|
||||||
|
return `rgb(${r}, ${g}, ${b})`;
|
||||||
|
}
|
||||||
|
if (232 <= code && code < 256) {
|
||||||
|
// x32 greyscale
|
||||||
|
const base = code - 232;
|
||||||
|
const grey = base * 10 + 8;
|
||||||
|
return `rgb(${grey}, ${grey}, ${grey})`;
|
||||||
|
}
|
||||||
|
// shouldn't get here (under normal circumstances), but just in case
|
||||||
|
return "initial";
|
||||||
|
};
|
||||||
|
|
||||||
|
type styleKey = "fontWeight" | "textDecoration" | "color" | "backgroundColor" | "display";
|
||||||
|
const style: {
|
||||||
|
fontWeight?: string;
|
||||||
|
textDecoration?: string;
|
||||||
|
color?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
display?: string;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
if (code === null || code === "0") {
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeParts = code
|
||||||
|
.split(";")
|
||||||
|
.map((p) => parseInt(p))
|
||||||
|
.filter(
|
||||||
|
(p, i, arr) =>
|
||||||
|
// If the sequence is 38;5 (x256 foreground color) or 48;5 (x256 background color),
|
||||||
|
// filter out the 5 so the next codePart after {38,48} is the color code.
|
||||||
|
p != 5 || i == 0 || (arr[i - 1] != 38 && arr[i - 1] != 48),
|
||||||
|
);
|
||||||
|
|
||||||
|
let nextStyleKey: styleKey | null = null;
|
||||||
|
codeParts.forEach((codePart) => {
|
||||||
|
/* eslint-disable yoda */
|
||||||
|
if (nextStyleKey !== null) {
|
||||||
|
style[nextStyleKey] = ansi2rgb(codePart);
|
||||||
|
nextStyleKey = null;
|
||||||
|
}
|
||||||
|
// Decorations
|
||||||
|
else if (codePart == 1) {
|
||||||
|
style.fontWeight = "bold";
|
||||||
|
} else if (codePart == 4) {
|
||||||
|
style.textDecoration = "underline";
|
||||||
|
}
|
||||||
|
// Forground Color (x8)
|
||||||
|
else if (30 <= codePart && codePart < 38) {
|
||||||
|
if (COLOR_MAP_BRIGHT[codePart % 10]) {
|
||||||
|
style.color = COLOR_MAP_BRIGHT[codePart % 10];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Background Color (x8)
|
||||||
|
else if (40 <= codePart && codePart < 48) {
|
||||||
|
if (COLOR_MAP_DARK[codePart % 10]) {
|
||||||
|
style.backgroundColor = COLOR_MAP_DARK[codePart % 10];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Forground Color (x256)
|
||||||
|
else if (codePart == 38) {
|
||||||
|
nextStyleKey = "color";
|
||||||
|
}
|
||||||
|
// Background Color (x256)
|
||||||
|
else if (codePart == 48) {
|
||||||
|
nextStyleKey = "backgroundColor";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// If a background color is set, render this as an inline block element (instead of inline)
|
||||||
|
// so that the background between lines (at least those that don't wrap) is uninterrupted.
|
||||||
|
if (style.backgroundColor !== undefined) {
|
||||||
|
style.display = "inline-block";
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user