mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-07 16:39:03 +00:00
enhance(actions): replace ansi_up with first-party code (#38619)
Replaces the `ansi_up` dependency with first-party code and fixes a number of bugs in turn. - Faster rendering, around 7x for plain lines and 3x for colored ones. - Render many SGR features like hyperlinks, blink, inverse, conceal, strikethrough, overline, underline styles and underline color, including `:` sub-parameters, which no longer swallow the codes after them. - Drop OSC, DCS, SOS, PM and APC with their payload, ending them at BEL, `ESC \` or the 8-bit ST. A truncated sequence is dropped instead of corrupting a later line. - A backspace moves the cursor back a column, so what follows overwrites it, even across a style change. - A style inside an OSC 8 label renders instead of leaking, and a private CSI ending in `m` no longer resets the style. - Log lines render as DOM nodes, never as markup, and only an `http(s)` url becomes a link. - Named colors render as CSS classes, only 24-bit color stays inline. - Invisible text is now selectable, and the `z-index` workaround is gone. Signed-off-by: silverwind <me@silverwind.io> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import type {IntervalId} from '../types.ts';
|
||||
import {toggleFullScreen} from '../utils.ts';
|
||||
import {localUserSettings} from '../modules/user-settings.ts';
|
||||
import type {ActionsArtifact, ActionsJob, ActionsRun, ActionsStatus} from '../modules/gitea-actions.ts';
|
||||
import {AnsiLineRenderer} from '../render/ansi.ts';
|
||||
import {
|
||||
type ActionRunViewStore,
|
||||
createLogLineMessage,
|
||||
@@ -36,6 +37,9 @@ type JobStepState = {
|
||||
manuallyCollapsed: boolean, // whether the user manually collapsed the step, used to avoid auto-expanding it again
|
||||
}
|
||||
|
||||
// one ANSI renderer per step, so an unterminated color carries between that step's lines only
|
||||
const stepAnsiRenderers: AnsiLineRenderer[] = [];
|
||||
|
||||
type StepContainerElement = HTMLElement & {
|
||||
// To remember the last active logs container, for example: a batch of logs only starts a group but doesn't end it,
|
||||
// then the following batches of logs should still use the same group (active logs container).
|
||||
@@ -213,10 +217,11 @@ async function copyStepOutput(event: MouseEvent, stepIndex: number) {
|
||||
const data = await fetchJobData([{step: stepIndex, cursor: null, expanded: true}]);
|
||||
const stepLog = data.logs.stepsLog?.find((s) => s.step === stepIndex);
|
||||
const lines: string[] = [];
|
||||
const ansi = new AnsiLineRenderer();
|
||||
for (const line of stepLog?.lines ?? []) {
|
||||
const cmd = parseLogLineCommand(line);
|
||||
if (cmd?.name === 'hidden' || cmd?.name === 'endgroup') continue;
|
||||
const msg = createLogLineMessage(line, cmd).textContent ?? '';
|
||||
const msg = createLogLineMessage(ansi, line, cmd).textContent ?? '';
|
||||
lines.push(timeVisible.value['log-time-stamp'] ? `${formatDatetimeISO(line.timestamp)} ${msg}` : msg);
|
||||
}
|
||||
return lines.join('\n');
|
||||
@@ -240,7 +245,7 @@ function createLogLine(stepIndex: number, startTime: number, line: LogLine, cmd:
|
||||
const logTimeStamp = createElementFromAttrs('span', {class: 'log-time-stamp'},
|
||||
formatDatetime(line.timestamp * 1000), // for "Show timestamps"
|
||||
);
|
||||
const logMsg = createLogLineMessage(line, cmd);
|
||||
const logMsg = createLogLineMessage(stepAnsiRenderers[stepIndex] ??= new AnsiLineRenderer(), line, cmd);
|
||||
const seconds = Math.floor(line.timestamp - startTime);
|
||||
const logTimeSeconds = createElementFromAttrs('span', {class: 'log-time-seconds'},
|
||||
`${seconds}s`, // for "Show seconds"
|
||||
@@ -598,7 +603,6 @@ async function hashChangeListener() {
|
||||
.job-step-container {
|
||||
max-height: 100%;
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.job-step-container .job-step-summary {
|
||||
@@ -651,9 +655,6 @@ async function hashChangeListener() {
|
||||
background-color: var(--color-console-active-bg);
|
||||
position: sticky;
|
||||
top: 60px;
|
||||
/* workaround ansi_up issue related to faintStyle generating a CSS stacking context via `opacity`
|
||||
inline style which caused such elements to render above the .job-step-summary header. */
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {createLogLineMessage, parseLogLineCommand} from './ActionRunView.ts';
|
||||
import {AnsiLineRenderer} from '../render/ansi.ts';
|
||||
|
||||
test('LogLineMessage', () => {
|
||||
const cases = {
|
||||
@@ -31,10 +32,11 @@ test('LogLineMessage', () => {
|
||||
'::add-matcher::foo': '<span class="log-msg log-cmd-hidden">foo</span>',
|
||||
'::remove-matcher foo::': '<span class="log-msg log-cmd-hidden"> foo::</span>', // not correctly parsed, but we don't need it
|
||||
};
|
||||
const ansi = new AnsiLineRenderer();
|
||||
for (const [input, html] of Object.entries(cases)) {
|
||||
const line = {index: 0, timestamp: 0, message: input};
|
||||
const cmd = parseLogLineCommand(line);
|
||||
const el = createLogLineMessage(line, cmd);
|
||||
const el = createLogLineMessage(ansi, line, cmd);
|
||||
expect(el.outerHTML).toBe(html);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {createElementFromAttrs} from '../utils/dom.ts';
|
||||
import {renderAnsiInto} from '../render/ansi.ts';
|
||||
import type {AnsiLineRenderer} from '../render/ansi.ts';
|
||||
import {reactive} from 'vue';
|
||||
import type {ActionsArtifact, ActionsJob, ActionsRun, ActionsStatus} from '../modules/gitea-actions.ts';
|
||||
import type {IntervalId} from '../types.ts';
|
||||
@@ -76,11 +76,11 @@ function decodeLineMessage(line: LogLine, cmd: LogLineCommand | null): string {
|
||||
if (cmd.name === 'command') return msg; // "command" is only an output tag, do not parse or escape it
|
||||
// "##[cmd]" also escapes ";" and "]" which delimit its header, "::cmd::" does not
|
||||
if (!cmd.prefix.startsWith('::')) msg = msg.replace(/%3B/g, ';').replace(/%5D/g, ']');
|
||||
// renderAnsiInto breaks a line per "\r", so "%0D%0A" is one break. "%25" last keeps "%250A" literal
|
||||
// a line breaks per "\r" when rendered, so "%0D%0A" is one break. "%25" last keeps "%250A" literal
|
||||
return msg.replace(/(?:%0D)?%0A/g, '\n').replace(/%0D/g, '\r').replace(/%25/g, '%');
|
||||
}
|
||||
|
||||
export function createLogLineMessage(line: LogLine, cmd: LogLineCommand | null) {
|
||||
export function createLogLineMessage(ansi: AnsiLineRenderer, line: LogLine, cmd: LogLineCommand | null) {
|
||||
const logMsgAttrs = {class: 'log-msg'};
|
||||
if (cmd?.name) logMsgAttrs.class += ` log-cmd-${cmd.name}`; // make it easier to add styles to some commands like "error"
|
||||
|
||||
@@ -90,10 +90,10 @@ export function createLogLineMessage(line: LogLine, cmd: LogLineCommand | null)
|
||||
if (label) {
|
||||
logMsg.append(createElementFromAttrs('span', {class: 'log-msg-label'}, `${label}:`));
|
||||
const msgSpan = document.createElement('span');
|
||||
renderAnsiInto(msgSpan, ` ${msgContent.trimStart()}`);
|
||||
ansi.renderLine(msgSpan, ` ${msgContent.trimStart()}`);
|
||||
logMsg.append(msgSpan);
|
||||
} else {
|
||||
renderAnsiInto(logMsg, msgContent);
|
||||
ansi.renderLine(logMsg, msgContent);
|
||||
}
|
||||
return logMsg;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user