Skip to content
Open
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
730e078
perf(actions): skip `ansi_up` for log lines without escape sequences
silverwind Jul 24, 2026
b79b0c8
refactor: inline the single-use fast-path condition
silverwind Jul 24, 2026
41b5289
refactor: extract renderAnsiToHtml, check for URLs once
silverwind Jul 24, 2026
bff0488
expand comment
silverwind Jul 24, 2026
7dadd84
Apply suggestion from @silverwind
silverwind Jul 24, 2026
f04e432
fix ansi render instance & state
wxiaoguang Jul 25, 2026
757ecbd
feat(actions): replace ansi_up with a purpose-built ANSI renderer
silverwind Jul 25, 2026
bc47ff5
fix: restore the js-yaml version
silverwind Jul 25, 2026
f64f3b6
feat(actions): render OSC 8 hyperlinks
silverwind Jul 25, 2026
2c35c95
fix(actions): handle sgr colon sub-parameters and underline color
silverwind Jul 25, 2026
2015207
feat(actions): render inverse, conceal, strikethrough and overline
silverwind Jul 25, 2026
ee9bcce
refactor(actions): express ansi styles as classes
silverwind Jul 25, 2026
39c66c9
perf(actions): render blink, fix conceal, halve the colored line cost
silverwind Jul 25, 2026
66534ca
refactor(actions): fold the render function into AnsiLineRenderer
silverwind Jul 25, 2026
ed8aff7
refactor(actions): linkify while rendering, guard the initial style b…
silverwind Jul 25, 2026
e6f8ed0
refactor(actions): build hex colors with colord, trim the renderer
silverwind Jul 25, 2026
706c503
test: use the javascript scheme literally in the hyperlink case
silverwind Jul 25, 2026
acc65ea
refactor(actions): wrap the initial style check, drop a restating com…
silverwind Jul 25, 2026
8cfbbee
refactor(actions): decide class vs inline color in one place
silverwind Jul 25, 2026
9aaa635
refactor(actions): spell out the text-decoration-line combinations
silverwind Jul 25, 2026
38cb2c7
Apply suggestion from @silverwind
silverwind Jul 25, 2026
cfe71ed
Apply suggestion from @silverwind
silverwind Jul 25, 2026
e96ecda
Apply suggestion from @silverwind
silverwind Jul 26, 2026
ec70602
Merge branch 'main' into actions-log-ansi-fast-path
silverwind Jul 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 30 additions & 24 deletions web_src/js/render/ansi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,51 @@ const replacements: Array<[RegExp, string]> = [

// render ANSI to HTML
export function renderAnsiInto(el: HTMLElement, line: string) {
// create a fresh ansi_up instance because otherwise previous renders can influence
// the output of future renders, because ansi_up is stateful and remembers things like
// unclosed opening tags for colors.
const ansi_up = new AnsiUp();
ansi_up.use_classes = true;

if (line.endsWith('\r\n')) {
line = line.substring(0, line.length - 2);
} else if (line.endsWith('\n')) {
line = line.substring(0, line.length - 1);
}

// skip ansi_up for plain lines (no ANSI escapes) and no carriage-return ("\r") progress updates
el.textContent = line;
Comment thread
silverwind marked this conversation as resolved.
Outdated
} else {
el.innerHTML = renderAnsiToHtml(line);
}

// at the moment, only need to do post-process when there are potential URL links
if (line.includes('://')) renderAnsiPostProcessNode(el);
}

function renderAnsiToHtml(line: string): string {
// create a fresh ansi_up instance because otherwise previous renders can influence
// the output of future renders, because ansi_up is stateful and remembers things like
// unclosed opening tags for colors.
const ansi_up = new AnsiUp();
ansi_up.use_classes = true;

if (line.includes('\x1b')) {
for (const [regex, replacement] of replacements) {
line = line.replace(regex, replacement);
}
}

let result: string;
if (!line.includes('\r')) {
result = ansi_up.ansi_to_html(line);
} else {
// handle "\rReading...1%\rReading...5%\rReading...100%",
// convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%"
const lines: Array<string> = [];
for (const part of line.split('\r')) {
if (part === '') continue;
const partHtml = ansi_up.ansi_to_html(part);
if (partHtml !== '') {
lines.push(partHtml);
}
}
// the log message element is with "white-space: break-spaces;", so use "\n" to break lines
result = lines.join('\n');
return ansi_up.ansi_to_html(line);
}

el.innerHTML = result;
// at the moment, only need to do post-process when there are potential URL links
if (result.includes('://')) renderAnsiPostProcessNode(el);
// handle "\rReading...1%\rReading...5%\rReading...100%",
// convert it into a multiple-line string: "Reading...1%\nReading...5%\nReading...100%"
const lines: Array<string> = [];
for (const part of line.split('\r')) {
if (part === '') continue;
const partHtml = ansi_up.ansi_to_html(part);
if (partHtml !== '') {
lines.push(partHtml);
}
}
// the log message element is with "white-space: break-spaces;", so use "\n" to break lines
return lines.join('\n');
}

function renderAnsiProcessText(node: ChildNode): ChildNode {
Expand Down
Loading