mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-19 10:57:19 +00:00

* Update ts-eslint * Remove comments * Remove unused ts-ignore * Add undefined generic type instead of unknown * Remove unused undefined type * Fix type issues * Use undefined instead of void for subscribed return type
389 lines
9.7 KiB
TypeScript
389 lines
9.7 KiB
TypeScript
import {
|
|
css,
|
|
type CSSResultGroup,
|
|
html,
|
|
LitElement,
|
|
type PropertyValues,
|
|
type TemplateResult,
|
|
} from "lit";
|
|
import {
|
|
customElement,
|
|
property,
|
|
query,
|
|
state as litState,
|
|
} from "lit/decorators";
|
|
import { classMap } from "lit/directives/class-map";
|
|
|
|
interface State {
|
|
bold: boolean;
|
|
italic: boolean;
|
|
underline: boolean;
|
|
strikethrough: boolean;
|
|
foregroundColor: null | string;
|
|
backgroundColor: null | string;
|
|
}
|
|
|
|
@customElement("ha-ansi-to-html")
|
|
export class HaAnsiToHtml extends LitElement {
|
|
@property() public content!: string;
|
|
|
|
@property({ type: Boolean, attribute: "wrap-disabled" }) public wrapDisabled =
|
|
false;
|
|
|
|
@query("pre") private _pre?: HTMLPreElement;
|
|
|
|
@litState() private _filter = "";
|
|
|
|
protected render(): TemplateResult {
|
|
return html`<pre class=${classMap({ wrap: !this.wrapDisabled })}></pre>`;
|
|
}
|
|
|
|
protected firstUpdated(_changedProperties: PropertyValues): void {
|
|
super.firstUpdated(_changedProperties);
|
|
|
|
// handle initial content
|
|
if (this.content) {
|
|
this.parseTextToColoredPre(this.content);
|
|
}
|
|
}
|
|
|
|
static get styles(): CSSResultGroup {
|
|
return css`
|
|
pre {
|
|
overflow-x: auto;
|
|
margin: 0;
|
|
}
|
|
pre.wrap {
|
|
white-space: pre-wrap;
|
|
overflow-wrap: break-word;
|
|
}
|
|
.bold {
|
|
font-weight: bold;
|
|
}
|
|
.italic {
|
|
font-style: italic;
|
|
}
|
|
.underline {
|
|
text-decoration: underline;
|
|
}
|
|
.strikethrough {
|
|
text-decoration: line-through;
|
|
}
|
|
.underline.strikethrough {
|
|
text-decoration: underline line-through;
|
|
}
|
|
.fg-red {
|
|
color: var(--error-color);
|
|
}
|
|
.fg-green {
|
|
color: var(--success-color);
|
|
}
|
|
.fg-yellow {
|
|
color: var(--warning-color);
|
|
}
|
|
.fg-blue {
|
|
color: var(--info-color);
|
|
}
|
|
.fg-magenta {
|
|
color: rgb(118, 38, 113);
|
|
}
|
|
.fg-cyan {
|
|
color: rgb(44, 181, 233);
|
|
}
|
|
.fg-white {
|
|
color: rgb(204, 204, 204);
|
|
}
|
|
.bg-black {
|
|
background-color: rgb(0, 0, 0);
|
|
}
|
|
.bg-red {
|
|
background-color: var(--error-color);
|
|
}
|
|
.bg-green {
|
|
background-color: var(--success-color);
|
|
}
|
|
.bg-yellow {
|
|
background-color: var(--warning-color);
|
|
}
|
|
.bg-blue {
|
|
background-color: var(--info-color);
|
|
}
|
|
.bg-magenta {
|
|
background-color: rgb(118, 38, 113);
|
|
}
|
|
.bg-cyan {
|
|
background-color: rgb(44, 181, 233);
|
|
}
|
|
.bg-white {
|
|
background-color: rgb(204, 204, 204);
|
|
}
|
|
|
|
::highlight(search-results) {
|
|
background-color: var(--primary-color);
|
|
color: var(--text-primary-color);
|
|
}
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* add new lines to the log
|
|
* @param lines log lines
|
|
* @param top should the new lines be added to the top of the log
|
|
*/
|
|
public parseLinesToColoredPre(lines: string[], top = false) {
|
|
for (const line of lines) {
|
|
this.parseLineToColoredPre(line, top);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a single line to the log
|
|
* @param line log line
|
|
* @param top should the new line be added to the top of the log
|
|
*/
|
|
public parseLineToColoredPre(line, top = false) {
|
|
const lineDiv = document.createElement("div");
|
|
|
|
// eslint-disable-next-line no-control-regex
|
|
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
|
|
let i = 0;
|
|
|
|
const state: State = {
|
|
bold: false,
|
|
italic: false,
|
|
underline: false,
|
|
strikethrough: false,
|
|
foregroundColor: null,
|
|
backgroundColor: null,
|
|
};
|
|
|
|
const addPart = (content) => {
|
|
const span = document.createElement("span");
|
|
if (state.bold) {
|
|
span.classList.add("bold");
|
|
}
|
|
if (state.italic) {
|
|
span.classList.add("italic");
|
|
}
|
|
if (state.underline) {
|
|
span.classList.add("underline");
|
|
}
|
|
if (state.strikethrough) {
|
|
span.classList.add("strikethrough");
|
|
}
|
|
if (state.foregroundColor !== null) {
|
|
span.classList.add(`fg-${state.foregroundColor}`);
|
|
}
|
|
if (state.backgroundColor !== null) {
|
|
span.classList.add(`bg-${state.backgroundColor}`);
|
|
}
|
|
span.appendChild(document.createTextNode(content));
|
|
lineDiv.appendChild(span);
|
|
};
|
|
|
|
/* eslint-disable no-cond-assign */
|
|
let match;
|
|
|
|
while ((match = re.exec(line)) !== null) {
|
|
const j = match!.index;
|
|
const substring = line.substring(i, j);
|
|
if (substring) {
|
|
addPart(substring);
|
|
}
|
|
i = j + match[0].length;
|
|
|
|
if (match[1] === undefined) {
|
|
continue;
|
|
}
|
|
|
|
match[1].split(";").forEach((colorCode: string) => {
|
|
switch (parseInt(colorCode, 10)) {
|
|
case 0:
|
|
// reset
|
|
state.bold = false;
|
|
state.italic = false;
|
|
state.underline = false;
|
|
state.strikethrough = false;
|
|
state.foregroundColor = null;
|
|
state.backgroundColor = null;
|
|
break;
|
|
case 1:
|
|
state.bold = true;
|
|
break;
|
|
case 3:
|
|
state.italic = true;
|
|
break;
|
|
case 4:
|
|
state.underline = true;
|
|
break;
|
|
case 9:
|
|
state.strikethrough = true;
|
|
break;
|
|
case 22:
|
|
state.bold = false;
|
|
break;
|
|
case 23:
|
|
state.italic = false;
|
|
break;
|
|
case 24:
|
|
state.underline = false;
|
|
break;
|
|
case 29:
|
|
state.strikethrough = false;
|
|
break;
|
|
case 30:
|
|
// foreground black
|
|
state.foregroundColor = null;
|
|
break;
|
|
case 31:
|
|
state.foregroundColor = "red";
|
|
break;
|
|
case 32:
|
|
state.foregroundColor = "green";
|
|
break;
|
|
case 33:
|
|
state.foregroundColor = "yellow";
|
|
break;
|
|
case 34:
|
|
state.foregroundColor = "blue";
|
|
break;
|
|
case 35:
|
|
state.foregroundColor = "magenta";
|
|
break;
|
|
case 36:
|
|
state.foregroundColor = "cyan";
|
|
break;
|
|
case 37:
|
|
state.foregroundColor = "white";
|
|
break;
|
|
case 39:
|
|
// foreground reset
|
|
state.foregroundColor = null;
|
|
break;
|
|
case 40:
|
|
state.backgroundColor = "black";
|
|
break;
|
|
case 41:
|
|
state.backgroundColor = "red";
|
|
break;
|
|
case 42:
|
|
state.backgroundColor = "green";
|
|
break;
|
|
case 43:
|
|
state.backgroundColor = "yellow";
|
|
break;
|
|
case 44:
|
|
state.backgroundColor = "blue";
|
|
break;
|
|
case 45:
|
|
state.backgroundColor = "magenta";
|
|
break;
|
|
case 46:
|
|
state.backgroundColor = "cyan";
|
|
break;
|
|
case 47:
|
|
state.backgroundColor = "white";
|
|
break;
|
|
case 49:
|
|
// background reset
|
|
state.backgroundColor = null;
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
const substring = line.substring(i);
|
|
if (substring) {
|
|
addPart(substring);
|
|
}
|
|
|
|
if (top) {
|
|
this._pre?.prepend(lineDiv);
|
|
lineDiv.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500 });
|
|
} else {
|
|
this._pre?.appendChild(lineDiv);
|
|
}
|
|
|
|
// filter new lines if a filter is set
|
|
if (this._filter) {
|
|
this.filterLines(this._filter);
|
|
}
|
|
}
|
|
|
|
public parseTextToColoredPre(text) {
|
|
const lines = text.split("\n");
|
|
|
|
for (const line of lines) {
|
|
this.parseLineToColoredPre(line);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filter lines based on a search string, lines and search string will be converted to lowercase
|
|
* @param filter the search string
|
|
* @returns true if there are lines to display
|
|
*/
|
|
filterLines(filter: string): boolean {
|
|
this._filter = filter;
|
|
const lines = this.shadowRoot?.querySelectorAll("div") || [];
|
|
let numberOfFoundLines = 0;
|
|
if (!filter) {
|
|
lines.forEach((line) => {
|
|
line.style.display = "";
|
|
});
|
|
numberOfFoundLines = lines.length;
|
|
if (CSS.highlights) {
|
|
CSS.highlights.delete("search-results");
|
|
}
|
|
} else {
|
|
const highlightRanges: Range[] = [];
|
|
lines.forEach((line) => {
|
|
if (!line.textContent?.toLowerCase().includes(filter.toLowerCase())) {
|
|
line.style.display = "none";
|
|
} else {
|
|
line.style.display = "";
|
|
numberOfFoundLines++;
|
|
if (CSS.highlights && line.firstChild !== null && line.textContent) {
|
|
const spansOfLine = line.querySelectorAll("span");
|
|
spansOfLine.forEach((span) => {
|
|
const text = span.textContent.toLowerCase();
|
|
const indices: number[] = [];
|
|
let startPos = 0;
|
|
while (startPos < text.length) {
|
|
const index = text.indexOf(filter.toLowerCase(), startPos);
|
|
if (index === -1) break;
|
|
indices.push(index);
|
|
startPos = index + filter.length;
|
|
}
|
|
|
|
indices.forEach((index) => {
|
|
const range = new Range();
|
|
range.setStart(span.firstChild!, index);
|
|
range.setEnd(span.firstChild!, index + filter.length);
|
|
highlightRanges.push(range);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
});
|
|
if (CSS.highlights) {
|
|
CSS.highlights.set("search-results", new Highlight(...highlightRanges));
|
|
}
|
|
}
|
|
|
|
return !!numberOfFoundLines;
|
|
}
|
|
|
|
public clear() {
|
|
if (this._pre) {
|
|
this._pre.innerHTML = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"ha-ansi-to-html": HaAnsiToHtml;
|
|
}
|
|
}
|