-
-
Notifications
You must be signed in to change notification settings - Fork 86
Allow escape sequences to span multiple lines and support default fg/bg colors #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
e6b8ff5
cfa7144
a95ea81
2c19b61
a2a74eb
7d8b58d
c1d7aab
50893ab
ce424f3
be7df32
31bb937
200bf2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,8 +31,11 @@ | |
| import hudson.model.Queue; | ||
| import hudson.model.Run; | ||
| import java.io.IOException; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
| import javax.annotation.Nonnull; | ||
| import jenkins.model.Jenkins; | ||
| import org.apache.commons.io.output.CountingOutputStream; | ||
| import org.apache.commons.io.output.NullOutputStream; | ||
|
|
@@ -51,17 +54,24 @@ final class ColorConsoleAnnotator extends ConsoleAnnotator<Object> { | |
|
|
||
| private final String colorMapName; | ||
| private final String charset; | ||
| private final @Nonnull List<AnsiAttributeElement> openTags; | ||
|
|
||
| ColorConsoleAnnotator(String colorMapName, String charset) { | ||
| ColorConsoleAnnotator(String colorMapName, String charset, List<AnsiAttributeElement> openTags) { | ||
| this.colorMapName = colorMapName; | ||
| this.charset = charset; | ||
| LOGGER.fine("creating annotator with colorMapName=" + colorMapName + " charset=" + charset); | ||
| this.openTags = openTags; | ||
| LOGGER.log(Level.FINE, "creating annotator with colorMapName={0} charset={1}, openTags={2}", new Object[] { colorMapName, charset, openTags }); | ||
| } | ||
|
|
||
| ColorConsoleAnnotator(String colorMapName, String charset) { | ||
| this(colorMapName, charset, Collections.emptyList()); | ||
| } | ||
|
|
||
| @Override | ||
| public ConsoleAnnotator<Object> annotate(Object context, MarkupText text) { | ||
| String s = text.getText(); | ||
| if (s.indexOf('\u001B') != -1) { | ||
| List<AnsiAttributeElement> nextOpenTags = openTags; | ||
| if (s.indexOf('\u001B') != -1 || !openTags.isEmpty()) { | ||
dwnusbaum marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| AnsiColorMap colorMap = Jenkins.get().getDescriptorByType(AnsiColorBuildWrapper.DescriptorImpl.class).getColorMap(colorMapName); | ||
| CountingOutputStream outgoing = new CountingOutputStream(new NullOutputStream()); | ||
| class EmitterImpl implements AnsiAttributeElement.Emitter { | ||
|
|
@@ -70,40 +80,67 @@ class EmitterImpl implements AnsiAttributeElement.Emitter { | |
| int lastPoint = -1; // multiple HTML tags may be emitted for one control sequence | ||
| @Override | ||
| public void emitHtml(String html) { | ||
| LOGGER.finest("emitting " + html + " @" + incoming.getCount()); | ||
| LOGGER.log(Level.FINEST, "emitting {0} @{1}", new Object[] { html, incoming.getCount() }); | ||
| text.addMarkup(incoming.getCount(), html); | ||
| if (incoming.getCount() != lastPoint) { | ||
| lastPoint = incoming.getCount(); | ||
| int hide = incoming.getCount() - outgoing.getCount() - adjustment; | ||
| LOGGER.finest("hiding " + hide + " @" + (outgoing.getCount() + adjustment)); | ||
| text.addMarkup(outgoing.getCount() + adjustment, outgoing.getCount() + adjustment + hide, "<span style=\"display: none\">", "</span>"); | ||
| adjustment += hide; | ||
| // If openTags is not empty, but there are no escape sequences directly on this line, or if we | ||
| // are just closing tags at the end of the line, there is nothing to hide. | ||
| if (hide != 0) { | ||
| LOGGER.log(Level.FINEST, "hiding {0} @{1}", new Object[] { hide, outgoing.getCount() + adjustment }); | ||
| text.addMarkup(outgoing.getCount() + adjustment, outgoing.getCount() + adjustment + hide, "<span style=\"display: none\">", "</span>"); | ||
| adjustment += hide; | ||
| } | ||
| } | ||
| } | ||
| public void emitHtmlDirect(String html) { | ||
| text.addMarkup(incoming.getCount(), html); | ||
| } | ||
| } | ||
| EmitterImpl emitter = new EmitterImpl(); | ||
| CountingOutputStream incoming = new CountingOutputStream(new AnsiHtmlOutputStream(outgoing, colorMap, emitter)); | ||
| AnsiHtmlOutputStream ansiOs = new AnsiHtmlOutputStream(outgoing, colorMap, emitter); | ||
| CountingOutputStream incoming = new CountingOutputStream(ansiOs); | ||
| emitter.incoming = incoming; | ||
| try { | ||
| for (AnsiAttributeElement element : openTags) { | ||
| // We need to reopen tags that were still open at the end of the previous line in the | ||
| // so the stream's state is correct in case those tags are closed mid-line. | ||
| ansiOs.openTag(element); | ||
| } | ||
| byte[] data = s.getBytes(charset); | ||
| for (int i = 0; i < data.length; i++) { | ||
| // Do not use write(byte[]) as offsets in incoming would not be accurate. | ||
| incoming.write(data[i]); | ||
| } | ||
| nextOpenTags = ansiOs.getOpenTags(); | ||
| // Close any tags that are still open at the end of the line. | ||
| ansiOs.closeOpenTags(null); | ||
| } catch (IOException x) { | ||
| LOGGER.log(Level.WARNING, null, x); | ||
| } | ||
| LOGGER.finer(() -> "\"" + StringEscapeUtils.escapeJava(s) + "\" → \"" + StringEscapeUtils.escapeJava(text.toString(true)) + "\""); | ||
| } | ||
| return this; | ||
| return openTags == nextOpenTags | ||
| ? this | ||
| : new ColorConsoleAnnotator(colorMapName, charset, nextOpenTags); | ||
|
||
| } | ||
|
|
||
| private Object readResolve() { | ||
dwnusbaum marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // Compatibility for instances serialized before the openTags field was added. | ||
| if (openTags == null) { | ||
| return new ColorConsoleAnnotator(colorMapName, charset); | ||
| } else { | ||
| return this; | ||
| } | ||
| } | ||
|
|
||
| @Extension | ||
| public static final class Factory extends ConsoleAnnotatorFactory<Object> { | ||
|
|
||
| @Override | ||
| public ConsoleAnnotator<Object> newInstance(Object context) { | ||
| LOGGER.fine("context=" + context); | ||
| LOGGER.log(Level.FINE, "context={0}", context); | ||
| if (context instanceof Run) { | ||
| ColorizedAction action = ((Run) context).getAction(ColorizedAction.class); | ||
| if (action != null) { | ||
|
|
@@ -122,7 +159,7 @@ public ConsoleAnnotator<Object> newInstance(Object context) { | |
| if (exec instanceof Run) { | ||
| ColorizedAction action = ((Run) exec).getAction(ColorizedAction.class); | ||
| if (action != null) { | ||
| return new ColorConsoleAnnotator(action.colorMapName, /* JEp-206 */ "UTF-8"); | ||
| return new ColorConsoleAnnotator(action.colorMapName, /* JEP-206 */ "UTF-8"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,6 +88,33 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen | |
| }); | ||
| } | ||
|
|
||
| @Test | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should create a JIRA issue for these and add |
||
| public void testMultilineEscapeSequence() throws Exception { | ||
| story.then(r -> { | ||
| FreeStyleProject p = r.createFreeStyleProject(); | ||
| p.getBuildWrappersList().add(new AnsiColorBuildWrapper(null)); | ||
| p.getBuildersList().add(new TestBuilder() { | ||
| @Override | ||
| public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { | ||
| listener.getLogger().println("\u001B[1;34mThis text should be bold and blue"); | ||
| listener.getLogger().println("Still bold and blue"); | ||
| listener.getLogger().println("\u001B[mThis text should be normal"); | ||
| return true; | ||
| } | ||
| }); | ||
| FreeStyleBuild b = r.buildAndAssertSuccess(p); | ||
| StringWriter writer = new StringWriter(); | ||
| b.getLogText().writeHtmlTo(0L, writer); | ||
| String html = writer.toString(); | ||
| System.out.print(html); | ||
| assertThat(html.replaceAll("<span style=\"display: none\">.+?</span>", ""), | ||
| allOf( | ||
| containsString("<b><span style=\"color: #1E90FF;\">This text should be bold and blue\n</span></b>"), | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The literal |
||
| containsString("<b><span style=\"color: #1E90FF;\">Still bold and blue\n</span></b>"), | ||
| not(containsString("\u001B[m")))); | ||
| }); | ||
| } | ||
|
|
||
| @Issue("JENKINS-54133") | ||
| @Test | ||
| public void testWorkflowWrap() throws Exception { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.