diff --git a/cmd/terminal-to-html/terminal-to-html.go b/cmd/terminal-to-html/terminal-to-html.go
index 1ba3a1d..94f3d73 100644
--- a/cmd/terminal-to-html/terminal-to-html.go
+++ b/cmd/terminal-to-html/terminal-to-html.go
@@ -9,6 +9,7 @@ import (
"net/http"
"os"
"runtime"
+ "slices"
"strconv"
"time"
@@ -76,7 +77,7 @@ func writePreviewEnd(w io.Writer) error {
return err
}
-func webservice(listen string, preview bool, maxLines int) {
+func webservice(listen string, preview bool, maxLines int, format, timeFmt string) {
http.HandleFunc("/terminal", func(w http.ResponseWriter, r *http.Request) {
// Process the request body, but write to a buffer before serving it.
// Consuming the body before any writes is necessary because of HTTP
@@ -86,13 +87,20 @@ func webservice(listen string, preview bool, maxLines int) {
// > Request.Body.
// However, it lets us provide Content-Length in all cases.
b := bytes.NewBuffer(nil)
- if _, _, _, err := process(b, r.Body, preview, maxLines); err != nil {
+ if _, _, _, err := process(b, r.Body, preview, maxLines, format, timeFmt); err != nil {
log.Printf("error starting preview: %v", err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Error creating preview.")
}
- w.Header().Set("Content-Type", "text/html")
+ switch format {
+ case "html":
+ w.Header().Set("Content-Type", "text/html")
+ case "plain":
+ w.Header().Set("Content-Type", "text/plain")
+ case "json", "json-plain":
+ w.Header().Set("Content-Type", "application/json")
+ }
w.Header().Set("Content-Length", strconv.Itoa(b.Len()))
if _, err := w.Write(b.Bytes()); err != nil {
log.Printf("error writing response: %v", err)
@@ -172,7 +180,7 @@ func (wc *writeCounter) Write(b []byte) (int, error) {
// process streams the src through a terminal renderer to the dst. If preview is
// true, the preview wrapper is added.
-func process(dst io.Writer, src io.Reader, preview bool, maxLines int) (in, out int, screen *terminal.Screen, err error) {
+func process(dst io.Writer, src io.Reader, preview bool, maxLines int, format, timeFmt string) (in, out int, screen *terminal.Screen, err error) {
// Wrap dst in writeCounter to count bytes written
wc := &writeCounter{out: dst}
@@ -182,9 +190,33 @@ func process(dst io.Writer, src io.Reader, preview bool, maxLines int) (in, out
}
}
+ var scrollOutFunc func(*terminal.ScreenLine)
+ switch format {
+ case "html":
+ scrollOutFunc = func(line *terminal.ScreenLine) { fmt.Fprintln(wc, line.AsHTML(true)) }
+ case "plain":
+ scrollOutFunc = func(line *terminal.ScreenLine) { fmt.Fprintln(wc, line.AsPlain(timeFmt)) }
+ case "json":
+ enc := json.NewEncoder(wc)
+ scrollOutFunc = func(line *terminal.ScreenLine) {
+ _ = enc.Encode(map[string]any{
+ "metadata": line.Metadata,
+ "content": line.AsHTML(false), // don't include timestamp in content, it's metadata
+ })
+ }
+ case "json-plain":
+ enc := json.NewEncoder(wc)
+ scrollOutFunc = func(line *terminal.ScreenLine) {
+ _ = enc.Encode(map[string]any{
+ "metadata": line.Metadata,
+ "content": line.AsPlain(""), // don't include timestamp in content, it's metadata
+ })
+ }
+ }
+
screen = &terminal.Screen{
MaxLines: maxLines,
- ScrollOutFunc: func(line string) { fmt.Fprintln(wc, line) },
+ ScrollOutFunc: scrollOutFunc,
}
inBytes, err := io.Copy(screen, src)
if err != nil {
@@ -193,7 +225,16 @@ func process(dst io.Writer, src io.Reader, preview bool, maxLines int) (in, out
// Write what remains in the screen buffer (everything that didn't scroll
// out of the top).
- fmt.Fprint(wc, screen.AsHTML())
+ switch format {
+ case "html":
+ fmt.Fprint(wc, screen.AsHTML(true))
+ case "plain":
+ fmt.Fprint(wc, screen.AsPlainText(timeFmt))
+ case "json", "json-plain":
+ for _, line := range screen.Screen {
+ scrollOutFunc(&line)
+ }
+ }
if preview {
if err := writePreviewEnd(wc); err != nil {
@@ -203,6 +244,8 @@ func process(dst io.Writer, src io.Reader, preview bool, maxLines int) (in, out
return int(inBytes), wc.counter, screen, nil
}
+var allowedFormats = []string{"html", "plain", "json", "json-plain"}
+
func main() {
cli.AppHelpTemplate = appHelpTemplate
@@ -230,11 +273,42 @@ func main() {
Value: 300,
Usage: "Sets a limit on the number of lines to hold in the screen buffer, allowing the renderer to operate in a streaming fashion and enabling the processing of large inputs. Setting to 0 disables the limit, causing the renderer to buffer the entire screen before producing any output",
},
+ &cli.StringFlag{
+ Name: "format",
+ Value: "html",
+ Usage: fmt.Sprintf("Configures output format. Must be one of %v", allowedFormats),
+ },
+ &cli.StringFlag{
+ Name: "timestamp-format",
+ Value: "rfc3339milli",
+ Usage: "Changes how timestamps are formatted (in plain format). Either 'none' (no timestamps), 'raw' (milliseconds since Unix epoch), 'rfc3339', 'rfc3339milli', or a custom Go time format string, used to format line timestamps for plain output (see https://pkg.go.dev/time#pkg-constants)",
+ },
}
app.Action = func(c *cli.Context) error {
+
+ format := c.String("format")
+ if !slices.Contains(allowedFormats, format) {
+ return fmt.Errorf("invalid format %q - must be one of %v", format, allowedFormats)
+ }
+
+ // The preview HTML should only be added if the output format is HTML
+ // All other formats do not support being surrounded by extra HTML
+ preview := c.Bool("preview") && format == "html"
+
+ // Timestamp format only applies to plain output.
+ timeFmt := c.String("timestamp-format")
+ switch timeFmt {
+ case "none":
+ timeFmt = ""
+ case "rfc3339":
+ timeFmt = time.RFC3339
+ case "rfc3339milli":
+ timeFmt = "2006-01-02T15:04:05.999Z07:00"
+ }
+
// Run a web server?
if addr := c.String("http"); addr != "" {
- webservice(addr, c.Bool("preview"), c.Int("buffer-max-lines"))
+ webservice(addr, preview, c.Int("buffer-max-lines"), format, timeFmt)
return nil
}
@@ -251,7 +325,14 @@ func main() {
input = f
}
- in, out, screen, err := process(os.Stdout, input, c.Bool("preview"), c.Int("buffer-max-lines"))
+ in, out, screen, err := process(
+ os.Stdout,
+ input,
+ preview,
+ c.Int("buffer-max-lines"),
+ format,
+ timeFmt,
+ )
if err != nil {
return err
}
diff --git a/output.go b/output.go
index e7d1059..e240c89 100644
--- a/output.go
+++ b/output.go
@@ -4,7 +4,9 @@ import (
"fmt"
"html"
"sort"
+ "strconv"
"strings"
+ "time"
)
type outputBuffer struct {
@@ -66,12 +68,12 @@ func (b *outputBuffer) appendChar(char rune) {
}
}
-// asHTML returns the line with HTML formatting.
-func (l *screenLine) asHTML() string {
+// AsHTML returns the line with HTML formatting.
+func (l *ScreenLine) AsHTML(withMetadata bool) string {
var spanOpen bool
var lineBuf outputBuffer
- if data, ok := l.metadata[bkNamespace]; ok {
+ if data, ok := l.Metadata[bkNamespace]; ok && withMetadata {
lineBuf.appendMeta(bkNamespace, data)
}
@@ -111,15 +113,59 @@ func (l *screenLine) asHTML() string {
return line
}
-// asPlain returns the line contents without any added HTML.
-func (l *screenLine) asPlain() string {
+// AsPlain returns the line contents without any added HTML.
+func (l *ScreenLine) AsPlain(timestampFormat string) string {
var buf strings.Builder
+ if timestampFormat != "" {
+ buf.WriteString(bkTimestamp(l.Metadata["bk"]["t"], timestampFormat))
+ buf.WriteRune('\t')
+ }
+
for _, node := range l.nodes {
- if !node.style.element() {
+ if node.style.element() {
+ element := l.elements[node.blob]
+ switch element.elementType {
+ case ELEMENT_LINK:
+ content := element.content
+ if content == "" {
+ content = element.url
+ }
+ buf.WriteString(content)
+
+ case ELEMENT_IMAGE:
+ content := element.alt
+ if content == "" {
+ content = element.url
+ }
+ buf.WriteString(content)
+
+ case ELEMENT_ITERM_IMAGE:
+
+ }
+
+ } else {
buf.WriteRune(node.blob)
}
}
return strings.TrimRight(buf.String(), " \t")
}
+
+func bkTimestamp(ts, timeFmt string) string {
+ if timeFmt == "raw" {
+ return ts
+ }
+
+ if ts == "" {
+ return "(no timestamp)"
+ }
+
+ tsint, err := strconv.ParseInt(ts, 10, 64)
+ if err != nil {
+ return err.Error()
+ }
+
+ tstime := time.UnixMilli(tsint)
+ return tstime.Format(timeFmt)
+}
diff --git a/parser_test.go b/parser_test.go
index 0b06236..0e9af6a 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -90,7 +90,7 @@ func assertXY(t *testing.T, s *Screen, x, y int) error {
}
func assertText(t *testing.T, s *Screen, expected string) error {
- if actual := s.AsPlainText(); actual != expected {
+ if actual := s.AsPlainText(""); actual != expected {
return fmt.Errorf("expected text %q, got %q", expected, actual)
}
return nil
diff --git a/screen.go b/screen.go
index b8dcb0e..b6d04b1 100644
--- a/screen.go
+++ b/screen.go
@@ -12,7 +12,7 @@ type Screen struct {
x, y int
// Screen contents
- screen []screenLine
+ Screen []ScreenLine
// Current style
style style
@@ -25,8 +25,10 @@ type Screen struct {
MaxLines int
// Optional callback. If not nil, as each line is scrolled out of the top of
- // the buffer, this func is called with the HTML.
- ScrollOutFunc func(lineHTML string)
+ // the buffer, this func is called with the line. The screen recycles
+ // storage used for old lines when creating new ones, so the callback should
+ // avoid using the line after returning.
+ ScrollOutFunc func(line *ScreenLine)
// Processing statistics
LinesScrolledOut int // count of lines that scrolled off the top
@@ -34,12 +36,12 @@ type Screen struct {
CursorBackOOB int // count of times ESC [D tried to move x < 0
}
-type screenLine struct {
+type ScreenLine struct {
nodes []node
- // metadata is { namespace => { key => value, ... }, ... }
+ // Metadata is { namespace => { key => value, ... }, ... }
// e.g. { "bk" => { "t" => "1234" } }
- metadata map[string]map[string]string
+ Metadata map[string]map[string]string
// element nodes refer to elements in this slice by index
// (if node.style.element(), then elements[node.blob] is the element)
@@ -54,7 +56,7 @@ const (
// Clear part (or all) of a line on the screen. The range to clear is inclusive
// of xStart and xEnd.
func (s *Screen) clear(y, xStart, xEnd int) {
- if y < 0 || y >= len(s.screen) {
+ if y < 0 || y >= len(s.Screen) {
return
}
@@ -66,7 +68,7 @@ func (s *Screen) clear(y, xStart, xEnd int) {
return
}
- line := &s.screen[y]
+ line := &s.Screen[y]
if xStart >= len(line.nodes) {
// Clearing part of a line starting after the end of the current line...
@@ -121,16 +123,16 @@ func (s *Screen) backward(i string) {
}
}
-func (s *Screen) getCurrentLineForWriting() *screenLine {
+func (s *Screen) getCurrentLineForWriting() *ScreenLine {
// Ensure there are enough lines on screen for the cursor's Y position.
- for s.y >= len(s.screen) {
+ for s.y >= len(s.Screen) {
// If MaxLines is not in use, or adding a new line would not make it
// larger than MaxLines, then just allocate a new line.
- if s.MaxLines <= 0 || len(s.screen)+1 <= s.MaxLines {
+ if s.MaxLines <= 0 || len(s.Screen)+1 <= s.MaxLines {
// nodes is preallocated with space for 80 columns, which is
// arbitrary, but also the traditional terminal width.
- newLine := screenLine{nodes: make([]node, 0, 80)}
- s.screen = append(s.screen, newLine)
+ newLine := ScreenLine{nodes: make([]node, 0, 80)}
+ s.Screen = append(s.Screen, newLine)
continue
}
@@ -138,18 +140,18 @@ func (s *Screen) getCurrentLineForWriting() *screenLine {
// larger than MaxLines.
// Pass the line being scrolled out to ScrollOutFunc if it exists.
if s.ScrollOutFunc != nil {
- s.ScrollOutFunc(s.screen[0].asHTML())
+ s.ScrollOutFunc(&s.Screen[0])
}
s.LinesScrolledOut++
// Trim the first line off the top of the screen.
// Recycle its nodes slice to make a new line on the bottom.
- newLine := screenLine{nodes: s.screen[0].nodes[:0]}
- s.screen = append(s.screen[1:], newLine)
+ newLine := ScreenLine{nodes: s.Screen[0].nodes[:0]}
+ s.Screen = append(s.Screen[1:], newLine)
s.y--
}
- line := &s.screen[s.y]
+ line := &s.Screen[s.y]
// Add columns if currently shorter than the cursor's x position
for i := len(line.nodes); i <= s.x; i++ {
@@ -191,17 +193,17 @@ func (s *Screen) appendElement(i *element) {
// metadata for the current line, overwriting data when keys collide.
func (s *Screen) setLineMetadata(namespace string, data map[string]string) {
line := s.getCurrentLineForWriting()
- if line.metadata == nil {
- line.metadata = map[string]map[string]string{
+ if line.Metadata == nil {
+ line.Metadata = map[string]map[string]string{
namespace: data,
}
return
}
- ns := line.metadata[namespace]
+ ns := line.Metadata[namespace]
if ns == nil {
// namespace did not exist, set all data
- line.metadata[namespace] = data
+ line.Metadata[namespace] = data
return
}
@@ -256,23 +258,23 @@ func (s *Screen) applyEscape(code rune, instructions []string) {
// This line should be equivalent to K0
s.clear(s.y, s.x, screenEndOfLine)
// Truncate the screen below the current line
- if len(s.screen) > s.y {
- s.screen = s.screen[:s.y+1]
+ if len(s.Screen) > s.y {
+ s.Screen = s.Screen[:s.y+1]
}
// "erase from beginning to current position (inclusive)"
case "1":
// This line should be equivalent to K1
s.clear(s.y, screenStartOfLine, s.x)
// Truncate the screen above the current line
- if len(s.screen) > s.y {
- s.screen = s.screen[s.y+1:]
+ if len(s.Screen) > s.y {
+ s.Screen = s.Screen[s.y+1:]
}
// Adjust the cursor position to compensate
s.y = 0
// 2: "erase entire display", 3: "erase whole display including scroll-back buffer"
// Given we don't have a scrollback of our own, we treat these as equivalent
case "2", "3":
- s.screen = nil
+ s.Screen = nil
s.x = 0
s.y = 0
}
@@ -305,22 +307,25 @@ func (s *Screen) Write(input []byte) (int, error) {
}
// AsHTML returns the contents of the current screen buffer as HTML.
-func (s *Screen) AsHTML() string {
- lines := make([]string, 0, len(s.screen))
+func (s *Screen) AsHTML(withMetadata bool) string {
+ lines := make([]string, 0, len(s.Screen))
- for _, line := range s.screen {
- lines = append(lines, line.asHTML())
+ for _, line := range s.Screen {
+ lines = append(lines, line.AsHTML(withMetadata))
}
return strings.Join(lines, "\n")
}
// AsPlainText renders the screen without any ANSI style etc.
-func (s *Screen) AsPlainText() string {
- lines := make([]string, 0, len(s.screen))
-
- for _, line := range s.screen {
- lines = append(lines, line.asPlain())
+// If timestampFormat is empty, then timestamps are not included. Otherwise,
+// it is used to format the bk.t metadata for the line as a timestamp at the
+// start of the line.
+func (s *Screen) AsPlainText(timestampFormat string) string {
+ lines := make([]string, 0, len(s.Screen))
+
+ for _, line := range s.Screen {
+ lines = append(lines, line.AsPlain(timestampFormat))
}
return strings.Join(lines, "\n")
diff --git a/terminal.go b/terminal.go
index 4d45fc8..e4554b8 100644
--- a/terminal.go
+++ b/terminal.go
@@ -14,5 +14,5 @@ package terminal
func Render(input []byte) string {
screen := Screen{}
screen.Write(input)
- return screen.AsHTML()
+ return screen.AsHTML(true)
}
diff --git a/terminal_test.go b/terminal_test.go
index b92d9c1..3697d99 100644
--- a/terminal_test.go
+++ b/terminal_test.go
@@ -37,60 +37,85 @@ func base64Encode(stringToEncode string) string {
}
var rendererTestCases = []struct {
- name string
- input string
- expected string
+ name string
+ input string
+ wantHTML string
+ wantPlain string
}{
{
- `input that ends in a newline will not include that newline`,
- "hello\n",
- "hello",
- }, {
- `closes colors that get opened`,
- "he\033[32mllo",
- "hello",
- }, {
- `treats multi-byte unicode characters as individual runes`,
- "€€€€€€\b\b\baaa",
- "€€€aaa",
- }, {
- `skips over colors when backspacing`,
- "he\x1b[32m\x1b[33m\bllo",
- "hllo",
- }, {
- `handles \x1b[m (no parameter) as a reset`,
- "\x1b[36mthis has a color\x1b[mthis is normal now\r\n",
- "this has a colorthis is normal now",
- }, {
- `treats \x1b[39m as a reset`,
- "\x1b[36mthis has a color\x1b[39mthis is normal now\r\n",
- "this has a colorthis is normal now",
- }, {
- `starts overwriting characters when you \r midway through something`,
- "hello\rb",
- "bello",
- }, {
- `colors across multiple lines`,
- "\x1b[32mhello\n\nfriend\x1b[0m",
- "hello\n \nfriend",
- }, {
- `allows you to control the cursor forwards`,
- "this is\x1b[4Cpoop and stuff",
- "this is poop and stuff",
- }, {
- `allows you to jump down further than the bottom of the buffer`,
- "this is great \x1b[1Bhello",
- "this is great\n hello",
- }, {
- `allows you to control the cursor backwards`,
- "this is good\x1b[4Dpoop and stuff",
- "this is poop and stuff",
- }, {
- `allows you to control the cursor upwards`,
- "1234\n56\x1b[1A78\x1b[B",
- "1278\n56",
- }, {
- `allows you to control the cursor downwards`,
+ name: `input that ends in a newline will not include that newline`,
+ input: "hello\n",
+ wantHTML: "hello",
+ wantPlain: "hello",
+ },
+ {
+ name: `closes colors that get opened`,
+ input: "he\033[32mllo",
+ wantHTML: "hello",
+ wantPlain: "hello",
+ },
+ {
+ name: `treats multi-byte unicode characters as individual runes`,
+ input: "€€€€€€\b\b\baaa",
+ wantHTML: "€€€aaa",
+ wantPlain: "€€€aaa",
+ },
+ {
+ name: `skips over colors when backspacing`,
+ input: "he\x1b[32m\x1b[33m\bllo",
+ wantHTML: "hllo",
+ wantPlain: "hllo",
+ },
+ {
+ name: `handles \x1b[m (no parameter) as a reset`,
+ input: "\x1b[36mthis has a color\x1b[mthis is normal now\r\n",
+ wantHTML: "this has a colorthis is normal now",
+ wantPlain: "this has a colorthis is normal now",
+ },
+ {
+ name: `treats \x1b[39m as a reset`,
+ input: "\x1b[36mthis has a color\x1b[39mthis is normal now\r\n",
+ wantHTML: "this has a colorthis is normal now",
+ wantPlain: "this has a colorthis is normal now",
+ },
+ {
+ name: `starts overwriting characters when you \r midway through something`,
+ input: "hello\rb",
+ wantHTML: "bello",
+ wantPlain: "bello",
+ },
+ {
+ name: `colors across multiple lines`,
+ input: "\x1b[32mhello\n\nfriend\x1b[0m",
+ wantHTML: "hello\n \nfriend",
+ wantPlain: "hello\n\nfriend",
+ },
+ {
+ name: `allows you to control the cursor forwards`,
+ input: "this is\x1b[4Cpoop and stuff",
+ wantHTML: "this is poop and stuff",
+ wantPlain: "this is poop and stuff",
+ },
+ {
+ name: `allows you to jump down further than the bottom of the buffer`,
+ input: "this is great \x1b[1Bhello",
+ wantHTML: "this is great\n hello",
+ wantPlain: "this is great\n hello",
+ },
+ {
+ name: `allows you to control the cursor backwards`,
+ input: "this is good\x1b[4Dpoop and stuff",
+ wantHTML: "this is poop and stuff",
+ wantPlain: "this is poop and stuff",
+ },
+ {
+ name: `allows you to control the cursor upwards`,
+ input: "1234\n56\x1b[1A78\x1b[B",
+ wantHTML: "1278\n56",
+ wantPlain: "1278\n56",
+ },
+ {
+ name: `allows you to control the cursor downwards`,
// creates a grid of:
// aaaa
// bbbb
@@ -98,240 +123,351 @@ var rendererTestCases = []struct {
// Then goes up 2 rows, down 1 row, jumps to the begining
// of the line, rewrites it to 1234, then jumps back down
// to the end of the grid.
- "aaaa\nbbbb\ncccc\x1b[2A\x1b[1B\r1234\x1b[1B",
- "aaaa\n1234\ncccc",
- }, {
- `doesn't blow up if you go back too many characters`,
- "this is good\x1b[100Dpoop and stuff",
- "poop and stuff",
- }, {
- `doesn't blow up if you backspace too many characters`,
- "hi\b\b\b\b\b\b\b\bbye",
- "bye",
- }, {
- `\x1b[1K clears everything before it`,
- "hello\x1b[1Kfriend!",
- " friend!",
- }, {
- `clears everything after the \x1b[0K`,
- "hello\nfriend!\x1b[A\r\x1b[0K",
- " \nfriend!",
- }, {
- `handles \x1b[0G ghetto style`,
- "hello friend\x1b[Ggoodbye buddy!",
- "goodbye buddy!",
- }, {
- `preserves characters already written in a certain color`,
- " \x1b[90m․\x1b[0m\x1b[90m․\x1b[0m\x1b[0G\x1b[90m․\x1b[0m\x1b[90m․\x1b[0m",
- "․․․․",
- }, {
- `replaces empty lines with non-breaking spaces`,
- "hello\n\nfriend",
- "hello\n \nfriend",
- }, {
- `preserves opening colors when using \x1b[0G`,
- "\x1b[33mhello\x1b[0m\x1b[33m\x1b[44m\x1b[0Ggoodbye",
- "goodbye",
- }, {
- `allows clearing lines below the current line`,
- "foo\nbar\x1b[A\x1b[Jbaz",
- "foobaz",
- }, {
- `doesn't freak out about clearing lines below when there aren't any`,
- "foobar\x1b[0J",
- "foobar",
- }, {
- `allows clearing lines above the current line`,
- "foo\nbar\x1b[A\x1b[1Jbaz",
- "barbaz",
- }, {
- `doesn't freak out about clearing lines above when there aren't any`,
- "\x1b[1Jfoobar",
- "foobar",
- }, {
- `allows clearing the entire scrollback buffer with escape 2J`,
- "this is a big long bit of terminal output\nplease pay it no mind, we will clear it soon\nokay, get ready for a disappearing act...\nand...and...\n\n\x1b[2Jhey presto",
- "hey presto",
- }, {
- `allows clearing the entire scrollback buffer with escape 3J also`,
- "this is a big long bit of terminal output\nplease pay it no mind, we will clear it soon\nokay, get ready for a disappearing act...\nand...and...\n\n\x1b[2Jhey presto",
- "hey presto",
- }, {
- `allows erasing the current line up to a point`,
- "hello friend\x1b[1K!",
- " !",
- }, {
- `allows clearing of the current line`,
- "hello friend\x1b[2K!",
- " !",
- }, {
- `doesn't close spans if no colors have been opened`,
- "hello \x1b[0mfriend",
- "hello friend",
- }, {
- `\x1b[K correctly clears all previous parts of the string`,
- "remote: Compressing objects: 0% (1/3342)\x1b[K\rremote: Compressing objects: 1% (34/3342)",
- "remote: Compressing objects: 1% (34/3342)",
- }, {
- `handles reverse linefeed`,
- "meow\npurr\nnyan\x1bMrawr",
- "meow\npurrrawr\nnyan",
- }, {
- `collapses many spans of the same color into 1`,
- "\x1b[90m․\x1b[90m․\x1b[90m․\x1b[90m․\n\x1b[90m․\x1b[90m․\x1b[90m․\x1b[90m․",
- "․․․․\n․․․․",
- }, {
- `escapes HTML`,
- "hello friend",
- "hello <strong>friend</strong>",
- }, {
- `escapes HTML in color codes`,
- "hello \x1b[\"hellomfriend",
- "hello ["hellomfriend",
- }, {
- `handles background colors`,
- "\x1b[30;42m\x1b[2KOK (244 tests, 558 assertions)",
- "OK (244 tests, 558 assertions)",
- }, {
- `does not attempt to incorrectly nest CSS in HTML (https://github.com/buildkite/terminal-to-html/issues/36)`,
- "Some plain text\x1b[0;30;42m yay a green background \x1b[0m\x1b[0;33;49mnow this has no background but is yellow \x1b[0m",
- "Some plain text yay a green background now this has no background but is yellow ",
- }, {
- `handles xterm colors`,
- "\x1b[38;5;169;48;5;50mhello\x1b[0m \x1b[38;5;179mgoodbye",
- "hello goodbye",
- }, {
- `handles non-xterm codes on the same line as xterm colors`,
- "\x1b[38;5;228;5;1mblinking and bold\x1b",
- `blinking and bold`,
- }, {
- `ignores broken escape characters, stripping the escape rune itself`,
- "hi amazing \x1b[12 nom nom nom friends",
- "hi amazing [12 nom nom nom friends",
- }, {
- `handles colors with 3 attributes`,
- "\x1b[0;10;4m\x1b[1m\x1b[34mgood news\x1b[0;10m\n\neveryone",
- "good news\n \neveryone",
- }, {
- `ends underlining with \x1b[24`,
- "\x1b[4mbegin\x1b[24m\r\nend",
- "begin\nend",
- }, {
- `ends bold with \x1b[21`,
- "\x1b[1mbegin\x1b[21m\r\nend",
- "begin\nend",
- }, {
- `ends bold with \x1b[22`,
- "\x1b[1mbegin\x1b[22m\r\nend",
- "begin\nend",
- }, {
- `ends crossed out with \x1b[29`,
- "\x1b[9mbegin\x1b[29m\r\nend",
- "begin\nend",
- }, {
- `ends italic out with \x1b[23`,
- "\x1b[3mbegin\x1b[23m\r\nend",
- "begin\nend",
- }, {
- `ends decreased intensity with \x1b[22`,
- "\x1b[2mbegin\x1b[22m\r\nend",
- "begin\nend",
- }, {
- `ignores cursor show/hide`,
- "\x1b[?25ldoing a thing without a cursor\x1b[?25h",
- "doing a thing without a cursor",
- }, {
- `renders simple images on their own line`, // http://iterm2.com/images.html
- "hi\x1b]1337;File=name=MS5naWY=;inline=1:AA==\ahello",
- "hi\n" + `` + "\nhello",
- }, {
- `does not start a new line for iterm images if we're already at the start of a line`,
- "\x1b]1337;File=name=MS5naWY=;inline=1:AA==\a",
- ``,
- }, {
- `silently ignores unsupported ANSI escape sequences`,
- "abc\x1b]9999\aghi",
- "abcghi",
- }, {
- `correctly handles images that we decide not to render`,
- "hi\x1b]1337;File=name=MS5naWY=;inline=0:AA==\ahello",
- "hihello",
- }, {
- `renders external images`,
- "\x1b]1338;url=http://foo.com/foobar.gif;alt=foo bar\a",
- ``,
- }, {
- `disallows non-allow-listed schemes for images`,
- "before\x1b]1338;url=javascript:alert(1);alt=hello\x07after",
- "before\n \nafter", // don't really care about the middle, as long as it's white-spacey
- }, {
- `renders links, and renders them inline on other content`,
- "a link to \x1b]1339;url=http://google.com;content=google\a.",
- `a link to google.`,
- }, {
- `uses URL as link content if missing`,
- "\x1b]1339;url=http://google.com\a",
- `http://google.com`,
- }, {
- `protects inline images against XSS by escaping HTML during rendering`,
- "hi\x1b]1337;File=name=" + base64Encode("