core-components: refactor AnsiProcessor to keep track of line numbers
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -20,37 +20,43 @@ describe('AnsiProcessor', () => {
|
||||
it('should process a single line', () => {
|
||||
const processor = new AnsiProcessor();
|
||||
expect(processor.process('foo\x1b[31mbar\x1b[39mbaz')).toEqual([
|
||||
[
|
||||
{
|
||||
text: 'foo',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
modifiers: { foreground: 'red' },
|
||||
},
|
||||
{
|
||||
text: 'baz',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
{
|
||||
chunks: [
|
||||
{
|
||||
text: 'foo',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
modifiers: { foreground: 'red' },
|
||||
},
|
||||
{
|
||||
text: 'baz',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
lineNumber: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(processor.process(`foo [32mbar[39m: baz`)).toEqual([
|
||||
[
|
||||
{
|
||||
text: 'foo ',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
modifiers: { foreground: 'green' },
|
||||
},
|
||||
{
|
||||
text: ': baz',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
{
|
||||
chunks: [
|
||||
{
|
||||
text: 'foo ',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'bar',
|
||||
modifiers: { foreground: 'green' },
|
||||
},
|
||||
{
|
||||
text: ': baz',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
lineNumber: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -62,36 +68,42 @@ a\x1b[34mb\x1b[39mc
|
||||
x\x1b[44my\x1b[49mz
|
||||
`),
|
||||
).toEqual([
|
||||
[{ text: '', modifiers: {} }],
|
||||
[
|
||||
{
|
||||
text: 'a',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'b',
|
||||
modifiers: { foreground: 'blue' },
|
||||
},
|
||||
{
|
||||
text: 'c',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
text: 'x',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'y',
|
||||
modifiers: { background: 'blue' },
|
||||
},
|
||||
{
|
||||
text: 'z',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
[{ text: '', modifiers: {} }],
|
||||
{ chunks: [{ text: '', modifiers: {} }], lineNumber: 1 },
|
||||
{
|
||||
chunks: [
|
||||
{
|
||||
text: 'a',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'b',
|
||||
modifiers: { foreground: 'blue' },
|
||||
},
|
||||
{
|
||||
text: 'c',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
lineNumber: 2,
|
||||
},
|
||||
{
|
||||
chunks: [
|
||||
{
|
||||
text: 'x',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'y',
|
||||
modifiers: { background: 'blue' },
|
||||
},
|
||||
{
|
||||
text: 'z',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
lineNumber: 3,
|
||||
},
|
||||
{ chunks: [{ text: '', modifiers: {} }], lineNumber: 4 },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -102,35 +114,41 @@ x\x1b[44my\x1b[49mz
|
||||
a\x1b[45mb\x1b[35mc
|
||||
x\x1b[39my\x1b[49mz`),
|
||||
).toEqual([
|
||||
[{ text: '', modifiers: {} }],
|
||||
[
|
||||
{
|
||||
text: 'a',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'b',
|
||||
modifiers: { background: 'magenta' },
|
||||
},
|
||||
{
|
||||
text: 'c',
|
||||
modifiers: { foreground: 'magenta', background: 'magenta' },
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
text: 'x',
|
||||
modifiers: { foreground: 'magenta', background: 'magenta' },
|
||||
},
|
||||
{
|
||||
text: 'y',
|
||||
modifiers: { background: 'magenta' },
|
||||
},
|
||||
{
|
||||
text: 'z',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
{ chunks: [{ text: '', modifiers: {} }], lineNumber: 1 },
|
||||
{
|
||||
chunks: [
|
||||
{
|
||||
text: 'a',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'b',
|
||||
modifiers: { background: 'magenta' },
|
||||
},
|
||||
{
|
||||
text: 'c',
|
||||
modifiers: { foreground: 'magenta', background: 'magenta' },
|
||||
},
|
||||
],
|
||||
lineNumber: 2,
|
||||
},
|
||||
{
|
||||
chunks: [
|
||||
{
|
||||
text: 'x',
|
||||
modifiers: { foreground: 'magenta', background: 'magenta' },
|
||||
},
|
||||
{
|
||||
text: 'y',
|
||||
modifiers: { background: 'magenta' },
|
||||
},
|
||||
{
|
||||
text: 'z',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
lineNumber: 3,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -139,56 +157,65 @@ x\x1b[39my\x1b[49mz`),
|
||||
const out1 = processor.process(`
|
||||
a\x1b[36mb\x1b[3mc`);
|
||||
expect(out1).toEqual([
|
||||
[{ text: '', modifiers: {} }],
|
||||
[
|
||||
{
|
||||
text: 'a',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'b',
|
||||
modifiers: { foreground: 'cyan' },
|
||||
},
|
||||
{
|
||||
text: 'c',
|
||||
modifiers: { foreground: 'cyan', italic: true },
|
||||
},
|
||||
],
|
||||
{ chunks: [{ text: '', modifiers: {} }], lineNumber: 1 },
|
||||
{
|
||||
chunks: [
|
||||
{
|
||||
text: 'a',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'b',
|
||||
modifiers: { foreground: 'cyan' },
|
||||
},
|
||||
{
|
||||
text: 'c',
|
||||
modifiers: { foreground: 'cyan', italic: true },
|
||||
},
|
||||
],
|
||||
lineNumber: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const out2 = processor.process(`
|
||||
a\x1b[36mb\x1b[3mc
|
||||
x\x1b[39my\x1b[23mz`);
|
||||
expect(out2).toEqual([
|
||||
[{ text: '', modifiers: {} }],
|
||||
[
|
||||
{
|
||||
text: 'a',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'b',
|
||||
modifiers: { foreground: 'cyan' },
|
||||
},
|
||||
{
|
||||
text: 'c',
|
||||
modifiers: { foreground: 'cyan', italic: true },
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
text: 'x',
|
||||
modifiers: { foreground: 'cyan', italic: true },
|
||||
},
|
||||
{
|
||||
text: 'y',
|
||||
modifiers: { italic: true },
|
||||
},
|
||||
{
|
||||
text: 'z',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
{ chunks: [{ text: '', modifiers: {} }], lineNumber: 1 },
|
||||
{
|
||||
chunks: [
|
||||
{
|
||||
text: 'a',
|
||||
modifiers: {},
|
||||
},
|
||||
{
|
||||
text: 'b',
|
||||
modifiers: { foreground: 'cyan' },
|
||||
},
|
||||
{
|
||||
text: 'c',
|
||||
modifiers: { foreground: 'cyan', italic: true },
|
||||
},
|
||||
],
|
||||
lineNumber: 2,
|
||||
},
|
||||
{
|
||||
chunks: [
|
||||
{
|
||||
text: 'x',
|
||||
modifiers: { foreground: 'cyan', italic: true },
|
||||
},
|
||||
{
|
||||
text: 'y',
|
||||
modifiers: { italic: true },
|
||||
},
|
||||
{
|
||||
text: 'z',
|
||||
modifiers: {},
|
||||
},
|
||||
],
|
||||
lineNumber: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
// Verifies that we appended rather than reprocessed
|
||||
|
||||
@@ -71,34 +71,56 @@ export interface ChunkModifiers {
|
||||
underline?: boolean;
|
||||
}
|
||||
|
||||
export interface Chunk {
|
||||
// export interface AnsiLine {
|
||||
// lineNumber: number;
|
||||
// chunks: AnsiChunk[];
|
||||
// }
|
||||
|
||||
export interface AnsiChunk {
|
||||
text: string;
|
||||
modifiers: ChunkModifiers;
|
||||
}
|
||||
|
||||
export class AnsiLine {
|
||||
constructor(
|
||||
readonly lineNumber: number = 1,
|
||||
readonly chunks: AnsiChunk[] = [],
|
||||
) {}
|
||||
|
||||
lastChunk(): AnsiChunk | undefined {
|
||||
return this.chunks[this.chunks.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
export class AnsiProcessor {
|
||||
private text: string = '';
|
||||
private lines: Chunk[][] = [];
|
||||
private lines: AnsiLine[] = [];
|
||||
|
||||
/**
|
||||
* Processes a chunk of text while keeping internal state that optimizes
|
||||
* subsequent processing that appends to the text.
|
||||
*/
|
||||
process(text: string): Chunk[][] {
|
||||
process(text: string): AnsiLine[] {
|
||||
if (this.text === text) {
|
||||
return this.lines;
|
||||
}
|
||||
|
||||
if (text.startsWith(this.text)) {
|
||||
const lastLineIndex = this.lines.length > 0 ? this.lines.length - 1 : 0;
|
||||
const lastLine = this.lines[lastLineIndex] ?? [];
|
||||
const lastChunk = lastLine[lastLine.length - 1] as Chunk | undefined;
|
||||
const lastLine = this.lines[lastLineIndex] ?? new AnsiLine();
|
||||
const lastChunk = lastLine.lastChunk();
|
||||
|
||||
const newLines = this.processLines(
|
||||
(lastChunk?.text ?? '') + text.slice(this.text.length),
|
||||
lastChunk?.modifiers,
|
||||
lastLine?.lineNumber,
|
||||
);
|
||||
this.text = text;
|
||||
lastLine.splice(lastLine.length - 1, 1, ...newLines[0]);
|
||||
lastLine.chunks.splice(
|
||||
lastLine.chunks.length - 1,
|
||||
1,
|
||||
...newLines[0]?.chunks,
|
||||
);
|
||||
this.lines[lastLineIndex] = lastLine;
|
||||
this.lines.push(...newLines.slice(1));
|
||||
} else {
|
||||
@@ -113,16 +135,23 @@ export class AnsiProcessor {
|
||||
private processLines = (
|
||||
text: string,
|
||||
modifiers: ChunkModifiers = {},
|
||||
): Chunk[][] => {
|
||||
const lines: Chunk[][] = [];
|
||||
startingLineNumber: number = 1,
|
||||
): AnsiLine[] => {
|
||||
const lines: AnsiLine[] = [];
|
||||
|
||||
let currentModifiers = modifiers;
|
||||
let currentLineNumber = startingLineNumber;
|
||||
|
||||
let prevIndex = 0;
|
||||
let currentModifiers = modifiers;
|
||||
newlineRegex.lastIndex = 0;
|
||||
for (;;) {
|
||||
const match = newlineRegex.exec(text);
|
||||
if (!match) {
|
||||
lines.push(this.processText(text.slice(prevIndex), currentModifiers));
|
||||
const chunks = this.processText(
|
||||
text.slice(prevIndex),
|
||||
currentModifiers,
|
||||
);
|
||||
lines.push(new AnsiLine(currentLineNumber, chunks));
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -130,11 +159,12 @@ export class AnsiProcessor {
|
||||
prevIndex = match.index + match[0].length;
|
||||
|
||||
const chunks = this.processText(line, currentModifiers);
|
||||
lines.push(chunks);
|
||||
lines.push(new AnsiLine(currentLineNumber, chunks));
|
||||
|
||||
// Modifiers that are active in the last chunk are carried over to the next line
|
||||
currentModifiers =
|
||||
chunks[chunks.length - 1].modifiers ?? currentModifiers;
|
||||
currentLineNumber += 1;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -142,11 +172,12 @@ export class AnsiProcessor {
|
||||
private processText = (
|
||||
fullText: string,
|
||||
modifiers: ChunkModifiers,
|
||||
): Chunk[] => {
|
||||
const chunks: Chunk[] = [];
|
||||
): AnsiChunk[] => {
|
||||
const chunks: AnsiChunk[] = [];
|
||||
|
||||
let currentModifiers = modifiers;
|
||||
|
||||
let prevIndex = 0;
|
||||
let currentModifiers = modifiers;
|
||||
ansiRegex.lastIndex = 0;
|
||||
for (;;) {
|
||||
const match = ansiRegex.exec(fullText);
|
||||
|
||||
@@ -179,7 +179,7 @@ export function LogViewer(props: LogViewerProps) {
|
||||
{!noLineNumbers && (
|
||||
<span className={classes.lineNumber}>{index + 1}</span>
|
||||
)}
|
||||
{data[index].map(({ text, modifiers }, i) => (
|
||||
{data[index].chunks.map(({ text, modifiers }, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={getModifierClasses(classes, modifiers)}
|
||||
|
||||
Reference in New Issue
Block a user