Skip to content

Commit 93cd579

Browse files
authored
Switch to ansi_up for ansi rendering in actions (#25401)
Fixes: #24777
1 parent 656d3cc commit 93cd579

File tree

6 files changed

+77
-100
lines changed

6 files changed

+77
-100
lines changed

package-lock.json

Lines changed: 9 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@primer/octicons": "19.3.0",
1717
"@webcomponents/custom-elements": "1.6.0",
1818
"add-asset-webpack-plugin": "2.0.1",
19-
"ansi-to-html": "0.7.2",
19+
"ansi_up": "5.2.1",
2020
"asciinema-player": "3.4.0",
2121
"clippie": "4.0.1",
2222
"css-loader": "6.8.1",

web_src/js/components/RepoActionView.test.js

Lines changed: 0 additions & 30 deletions
This file was deleted.

web_src/js/components/RepoActionView.vue

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,12 @@
119119
import {SvgIcon} from '../svg.js';
120120
import ActionRunStatus from './ActionRunStatus.vue';
121121
import {createApp} from 'vue';
122-
import AnsiToHTML from 'ansi-to-html';
123122
import {toggleElem} from '../utils/dom.js';
124123
import {getCurrentLocale} from '../utils.js';
124+
import {renderAnsi} from '../render/ansi.js';
125125
126126
const {csrfToken} = window.config;
127127
128-
const ansiLogRender = new AnsiToHTML({escapeXML: true});
129-
130128
const sfc = {
131129
name: 'RepoActionView',
132130
components: {
@@ -304,7 +302,7 @@ const sfc = {
304302
305303
const logMessage = document.createElement('span');
306304
logMessage.className = 'log-msg';
307-
logMessage.innerHTML = ansiLogToHTML(line.message);
305+
logMessage.innerHTML = renderAnsi(line.message);
308306
div.append(logTimeStamp);
309307
div.append(logMessage);
310308
div.append(logTimeSeconds);
@@ -470,48 +468,6 @@ export function initRepositoryActionView() {
470468
view.mount(el);
471469
}
472470
473-
// some unhandled control sequences by AnsiToHTML
474-
// https://man7.org/linux/man-pages/man4/console_codes.4.html
475-
const ansiRegexpRemove = /\x1b\[\d+[A-H]/g; // Move cursor, treat them as no-op.
476-
const ansiRegexpNewLine = /\x1b\[\d?[JK]/g; // Erase display/line, treat them as a Carriage Return
477-
478-
function ansiCleanControlSequences(line) {
479-
if (line.includes('\x1b')) {
480-
line = line.replace(ansiRegexpRemove, '');
481-
line = line.replace(ansiRegexpNewLine, '\r');
482-
}
483-
return line;
484-
}
485-
486-
export function ansiLogToHTML(line) {
487-
if (line.endsWith('\r\n')) {
488-
line = line.substring(0, line.length - 2);
489-
} else if (line.endsWith('\n')) {
490-
line = line.substring(0, line.length - 1);
491-
}
492-
493-
// usually we do not need to process control chars like "\033[", let AnsiToHTML do it
494-
// but AnsiToHTML has bugs, so we need to clean some control sequences first
495-
line = ansiCleanControlSequences(line);
496-
497-
if (!line.includes('\r')) {
498-
return ansiLogRender.toHtml(line);
499-
}
500-
501-
// handle "\rReading...1%\rReading...5%\rReading...100%",
502-
// convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%"
503-
const lines = [];
504-
for (const part of line.split('\r')) {
505-
if (part === '') continue;
506-
const partHtml = ansiLogRender.toHtml(part);
507-
if (partHtml !== '') {
508-
lines.push(partHtml);
509-
}
510-
}
511-
// the log message element is with "white-space: break-spaces;", so use "\n" to break lines
512-
return lines.join('\n');
513-
}
514-
515471
</script>
516472
517473
<style scoped>

web_src/js/render/ansi.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import AnsiUp from 'ansi_up';
2+
3+
const replacements = [
4+
[/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op
5+
[/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return
6+
];
7+
8+
// render ANSI to HTML
9+
export function renderAnsi(line) {
10+
// create a fresh ansi_up instance because otherwise previous renders can influence
11+
// the output of future renders, because ansi_up is stateful and remembers things like
12+
// unclosed opening tags for colors.
13+
const ansi_up = new (AnsiUp.default || AnsiUp)();
14+
15+
if (line.endsWith('\r\n')) {
16+
line = line.substring(0, line.length - 2);
17+
} else if (line.endsWith('\n')) {
18+
line = line.substring(0, line.length - 1);
19+
}
20+
21+
if (line.includes('\x1b')) {
22+
for (const [regex, replacement] of replacements) {
23+
line = line.replace(regex, replacement);
24+
}
25+
}
26+
27+
if (!line.includes('\r')) {
28+
return ansi_up.ansi_to_html(line);
29+
}
30+
31+
// handle "\rReading...1%\rReading...5%\rReading...100%",
32+
// convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%"
33+
const lines = [];
34+
for (const part of line.split('\r')) {
35+
if (part === '') continue;
36+
const partHtml = ansi_up.ansi_to_html(part);
37+
if (partHtml !== '') {
38+
lines.push(partHtml);
39+
}
40+
}
41+
42+
// the log message element is with "white-space: break-spaces;", so use "\n" to break lines
43+
return lines.join('\n');
44+
}

web_src/js/render/ansi.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {expect, test} from 'vitest';
2+
import {renderAnsi} from './ansi.js';
3+
4+
test('renderAnsi', () => {
5+
expect(renderAnsi('abc')).toEqual('abc');
6+
expect(renderAnsi('abc\n')).toEqual('abc');
7+
expect(renderAnsi('abc\r\n')).toEqual('abc');
8+
expect(renderAnsi('\r')).toEqual('');
9+
expect(renderAnsi('\rx\rabc')).toEqual('x\nabc');
10+
expect(renderAnsi('\rabc\rx\r')).toEqual('abc\nx');
11+
expect(renderAnsi('\x1b[30mblack\x1b[37mwhite')).toEqual('<span style="color:rgb(0,0,0)">black</span><span style="color:rgb(255,255,255)">white</span>'); // unclosed
12+
expect(renderAnsi('<script>')).toEqual('&lt;script&gt;');
13+
expect(renderAnsi('\x1b[1A\x1b[2Ktest\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
14+
expect(renderAnsi('\x1b[1A\x1b[2K\rtest\r\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
15+
expect(renderAnsi('\x1b[1A\x1b[2Ktest\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
16+
expect(renderAnsi('\x1b[1A\x1b[2K\rtest\r\x1b[1B\x1b[1A\x1b[2K')).toEqual('test');
17+
18+
// treat "\033[0K" and "\033[0J" (Erase display/line) as "\r", then it will be covered to "\n" finally.
19+
expect(renderAnsi('a\x1b[Kb\x1b[2Jc')).toEqual('a\nb\nc');
20+
expect(renderAnsi('\x1b[48;5;88ma\x1b[38;208;48;5;159mb\x1b[m')).toEqual(`<span style="background-color:rgb(135,0,0)">a</span><span style="background-color:rgb(175,255,255)">b</span>`);
21+
});

0 commit comments

Comments
 (0)