From 4df2eabcce696bac262cac64dc10c29bbfa85b62 Mon Sep 17 00:00:00 2001 From: Kamil Tunkiewicz Date: Mon, 31 Aug 2020 20:25:18 +0200 Subject: [PATCH 1/2] adding support for multiple decorations per chunk --- lib/index.d.ts | 7 +++- lib/index.js | 99 ++++++++++++++++++++++++-------------------- test/ansi_up-test.js | 19 +++++++++ 3 files changed, 78 insertions(+), 47 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index ba57246..4c07981 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1,6 +1,8 @@ // Type definitions for Anser // Project: https://github.com/IonicaBizau/anser +type DecorationName = 'bold' | 'dim' | 'italic' | 'underline' | 'blink' | 'reverse' | 'hidden' | 'strikethrough'; + export interface AnserJsonEntry { /** The text. */ content: string; @@ -14,7 +16,10 @@ export interface AnserJsonEntry { bg_truecolor: string; /** `true` if a carriageReturn \r was fount at end of line. */ clearLine: boolean; - decoration: null | 'bold' | 'dim' | 'italic' | 'underline' | 'blink' | 'reverse' | 'hidden' | 'strikethrough'; + /** The decoration last declared before the text. */ + decoration: null | DecorationName; + /** All decorations that apply to the text. */ + decorations: Array; /** `true` if the colors were processed, `false` otherwise. */ was_processed: boolean; /** A function returning `true` if the content is empty, or `false` otherwise. */ diff --git a/lib/index.js b/lib/index.js index 40d15f8..7cc6b08 100644 --- a/lib/index.js +++ b/lib/index.js @@ -316,14 +316,15 @@ class Anser { let result = { content: text - , fg: null - , bg: null - , fg_truecolor: null - , bg_truecolor: null - , clearLine: options.clearLine - , decoration: null - , was_processed: false - , isEmpty: () => !result.content + , fg: null + , bg: null + , fg_truecolor: null + , bg_truecolor: null + , clearLine: options.clearLine + , decoration: null + , decorations: [] + , was_processed: false + , isEmpty: () => !result.content }; // Each "chunk" is the text after the CSI (ESC + "[") and before the next CSI/EOF. @@ -358,32 +359,33 @@ class Anser { let self = this; - self.decoration = null; + self.decorations = self.decorations || []; while (nums.length > 0) { let num_str = nums.shift(); let num = parseInt(num_str); if (isNaN(num) || num === 0) { - self.fg = self.bg = self.decoration = null; + self.fg = self.bg = null; + self.decorations = []; } else if (num === 1) { - self.decoration = "bold"; + self.decorations.push("bold"); } else if (num === 2) { - self.decoration = "dim"; + self.decorations.push("dim"); // Enable code 2 to get string } else if (num == 3) { - self.decoration = "italic"; + self.decorations.push("italic"); } else if (num == 4) { - self.decoration = "underline"; + self.decorations.push("underline"); } else if (num == 5) { - self.decoration = "blink"; + self.decorations.push("blink"); } else if (num === 7) { - self.decoration = "reverse"; + self.decorations.push("reverse"); } else if (num === 8) { - self.decoration = "hidden"; + self.decorations.push("hidden"); // Enable code 9 to get strikethrough } else if (num === 9) { - self.decoration = "strikethrough"; + self.decorations.push("strikethrough"); } else if (num == 39) { self.fg = null; } else if (num == 49) { @@ -454,7 +456,7 @@ class Anser { } } - if ((self.fg === null) && (self.bg === null) && (self.decoration === null)) { + if ((self.fg === null) && (self.bg === null) && (self.decorations.length === 0)) { return result; } else { let styles = []; @@ -465,7 +467,8 @@ class Anser { result.bg = self.bg; result.fg_truecolor = self.fg_truecolor; result.bg_truecolor = self.bg_truecolor; - result.decoration = self.decoration; + result.decorations = self.decorations; + result.decoration = self.decorations.slice(-1).pop() || null; result.was_processed = true; return result; @@ -488,8 +491,6 @@ class Anser { * @return {Object|String} The result (object if `json` is wanted back or string otherwise). */ processChunk (text, options, markup) { - - let self = this; options = options || {}; let jsonChunk = this.processChunkJson(text, options, markup); @@ -499,8 +500,10 @@ class Anser { let use_classes = options.use_classes; - let styles = []; - let classes = []; + let colors = []; + let decorations = []; + let textDecorations = []; + let data = {}; let render_data = data => { let fragments = []; @@ -515,53 +518,57 @@ class Anser { if (jsonChunk.fg) { if (use_classes) { - classes.push(jsonChunk.fg + "-fg"); + colors.push(jsonChunk.fg + "-fg"); if (jsonChunk.fg_truecolor !== null) { data["ansi-truecolor-fg"] = jsonChunk.fg_truecolor; jsonChunk.fg_truecolor = null; } } else { - styles.push("color:rgb(" + jsonChunk.fg + ")"); + colors.push("color:rgb(" + jsonChunk.fg + ")"); } } if (jsonChunk.bg) { if (use_classes) { - classes.push(jsonChunk.bg + "-bg"); + colors.push(jsonChunk.bg + "-bg"); if (jsonChunk.bg_truecolor !== null) { data["ansi-truecolor-bg"] = jsonChunk.bg_truecolor; jsonChunk.bg_truecolor = null; } } else { - styles.push("background-color:rgb(" + jsonChunk.bg + ")"); + colors.push("background-color:rgb(" + jsonChunk.bg + ")"); } } - if (jsonChunk.decoration) { + jsonChunk.decorations.forEach((decoration) => { if (use_classes) { - classes.push("ansi-" + jsonChunk.decoration); - } else if (jsonChunk.decoration === "bold") { - styles.push("font-weight:bold"); - } else if (jsonChunk.decoration === "dim") { - styles.push("opacity:0.5"); - } else if (jsonChunk.decoration === "italic") { - styles.push("font-style:italic"); - // underline and blink are treated bellow - } else if (jsonChunk.decoration === "reverse") { - styles.push("filter:invert(100%)"); - } else if (jsonChunk.decoration === "hidden") { - styles.push("visibility:hidden"); - } else if (jsonChunk.decoration === "strikethrough") { - styles.push("text-decoration:line-through"); + decorations.push("ansi-" + decoration); + } else if (decoration === "bold") { + decorations.push("font-weight:bold"); + } else if (decoration === "dim") { + decorations.push("opacity:0.5"); + } else if (decoration === "italic") { + decorations.push("font-style:italic"); + } else if (decoration === "reverse") { + decorations.push("filter:invert(100%)"); + } else if (decoration === "hidden") { + decorations.push("visibility:hidden"); + } else if (decoration === "strikethrough") { + textDecorations.push("line-through"); } else { - styles.push("text-decoration:" + jsonChunk.decoration); + // underline and blink are treated here + textDecorations.push(decoration); } + }); + + if (textDecorations.length) { + decorations.push("text-decoration:" + textDecorations.join(" ")); } if (use_classes) { - return "" + jsonChunk.content + ""; + return "" + jsonChunk.content + ""; } else { - return "" + jsonChunk.content + ""; + return "" + jsonChunk.content + ""; } } }; diff --git a/test/ansi_up-test.js b/test/ansi_up-test.js index 6564cec..63766dd 100644 --- a/test/ansi_up-test.js +++ b/test/ansi_up-test.js @@ -436,6 +436,25 @@ describe("Anser", () => { }); }); + describe("use multiple text styles", () => { + describe("default", () => { + it("underline, blinking, bold, blue text on red background", () => { + const start = "\x1B[4m" + "\x1B[5m" + "\x1B[1;34m" + "\x1B[41m" + "foo" + "\x1B[0m" + "bar"; + const expected = 'foobar'; + const l = Anser.ansiToHtml(start); + l.should.eql(expected); + }); + }); + describe("with classes", () => { + it("underline, blinking, bold, blue text on red background", () => { + const start = "\x1B[4m" + "\x1B[5m" + "\x1B[1;34m" + "\x1B[41m" + "foo" + "\x1B[0m" + "bar"; + const expected = 'foobar'; + const l = Anser.ansiToHtml(start, {use_classes: true}); + l.should.eql(expected); + }); + }); + }); + describe("ignore unsupported CSI", () => { it("should correctly convert a string similar to CSI", () => { // https://github.com/drudru/Anser/pull/15 From b71f99ca54465b885159c1d78a8bcb0ae55c9cef Mon Sep 17 00:00:00 2001 From: Kamil Tunkiewicz Date: Mon, 31 Aug 2020 20:30:27 +0200 Subject: [PATCH 2/2] adding support for reversing fb and bf colors --- lib/index.js | 41 +++++++++++++++++++++++++++++++++++------ test/ansi_up-test.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/lib/index.js b/lib/index.js index 7cc6b08..1c3dfab 100644 --- a/lib/index.js +++ b/lib/index.js @@ -320,6 +320,7 @@ class Anser { , bg: null , fg_truecolor: null , bg_truecolor: null + , isInverted: false , clearLine: options.clearLine , decoration: null , decorations: [] @@ -493,18 +494,40 @@ class Anser { processChunk (text, options, markup) { options = options || {}; let jsonChunk = this.processChunkJson(text, options, markup); + let use_classes = options.use_classes; + + // "reverse" decoration reverses foreground and background colors + jsonChunk.decorations = jsonChunk.decorations + .filter((decoration) => { + if (decoration === "reverse") { + // when reversing, missing colors are defaulted to black (bg) and white (fg) + if (!jsonChunk.fg) { + jsonChunk.fg = ANSI_COLORS[0][7][use_classes ? "class" : "color"]; + } + if (!jsonChunk.bg) { + jsonChunk.bg = ANSI_COLORS[0][0][use_classes ? "class" : "color"]; + } + let tmpFg = jsonChunk.fg; + jsonChunk.fg = jsonChunk.bg; + jsonChunk.bg = tmpFg; + let tmpFgTrue = jsonChunk.fg_truecolor; + jsonChunk.fg_truecolor = jsonChunk.bg_truecolor; + jsonChunk.bg_truecolor = tmpFgTrue; + jsonChunk.isInverted = true; + return false; + } + return true; + }); if (options.json) { return jsonChunk; } if (jsonChunk.isEmpty()) { return ""; } if (!jsonChunk.was_processed) { return jsonChunk.content; } - let use_classes = options.use_classes; - let colors = []; let decorations = []; let textDecorations = []; - let data = {}; + let render_data = data => { let fragments = []; let key; @@ -516,6 +539,10 @@ class Anser { return fragments.length > 0 ? " " + fragments.join(" ") : ""; }; + if (jsonChunk.isInverted) { + data["ansi-is-inverted"] = "true"; + } + if (jsonChunk.fg) { if (use_classes) { colors.push(jsonChunk.fg + "-fg"); @@ -541,16 +568,18 @@ class Anser { } jsonChunk.decorations.forEach((decoration) => { + // use classes if (use_classes) { decorations.push("ansi-" + decoration); - } else if (decoration === "bold") { + return; + } + // use styles + if (decoration === "bold") { decorations.push("font-weight:bold"); } else if (decoration === "dim") { decorations.push("opacity:0.5"); } else if (decoration === "italic") { decorations.push("font-style:italic"); - } else if (decoration === "reverse") { - decorations.push("filter:invert(100%)"); } else if (decoration === "hidden") { decorations.push("visibility:hidden"); } else if (decoration === "strikethrough") { diff --git a/test/ansi_up-test.js b/test/ansi_up-test.js index 63766dd..d350844 100644 --- a/test/ansi_up-test.js +++ b/test/ansi_up-test.js @@ -455,6 +455,49 @@ describe("Anser", () => { }); }); + describe("reverse/inverse colors", () => { + describe("default", () => { + it("blue text on red background, to red text on blue background", () => { + const start = "\x1B[7m" + "\x1B[34m" + "\x1B[41m" + "foo" + "\x1B[0m"; + const expected = 'foo'; + const l = Anser.ansiToHtml(start); + l.should.eql(expected); + }); + it("blue text inversed to be blue background", () => { + const start = "\x1B[7m" + "\x1B[34m" + "foo" + "\x1B[0m"; + const expected = 'foo'; + const l = Anser.ansiToHtml(start); + l.should.eql(expected); + }); + it("red background inversed to be red text", () => { + const start = "\x1B[7m" + "\x1B[41m" + "foo" + "\x1B[0m"; + const expected = 'foo'; + const l = Anser.ansiToHtml(start); + l.should.eql(expected); + }); + }); + describe("with classes", () => { + it("blue text on red background, to red text on blue background", () => { + const start = "\x1B[7m" + "\x1B[34m" + "\x1B[41m" + "foo" + "\x1B[0m"; + const expected = 'foo'; + const l = Anser.ansiToHtml(start, {use_classes: true}); + l.should.eql(expected); + }); + it("blue text inversed to be blue background", () => { + const start = "\x1B[7m" + "\x1B[34m" + "foo" + "\x1B[0m"; + const expected = 'foo'; + const l = Anser.ansiToHtml(start, {use_classes: true}); + l.should.eql(expected); + }); + it("red background inversed to be red text", () => { + const start = "\x1B[7m" + "\x1B[41m" + "foo" + "\x1B[0m"; + const expected = 'foo'; + const l = Anser.ansiToHtml(start, {use_classes: true}); + l.should.eql(expected); + }); + }); + }); + describe("ignore unsupported CSI", () => { it("should correctly convert a string similar to CSI", () => { // https://github.com/drudru/Anser/pull/15