core-components: refactor AnsiProcessor to keep track of line numbers

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-12-04 17:12:38 +01:00
committed by blam
parent cef1f124f6
commit 8af6baa2d1
3 changed files with 204 additions and 146 deletions
@@ -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 bar: 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)}