From f1e699f5b3ae2bf69e8e09bae3ef70be7218f70d Mon Sep 17 00:00:00 2001 From: Jan De Dobbeleer Date: Tue, 15 Nov 2022 21:11:29 +0100 Subject: [PATCH] fix(hyperlink): parse links correctly resolves #3082 --- src/color/ansi.go | 61 ++++++++++++++++++++++++++++++++++-------- src/color/ansi_test.go | 29 ++++++++++++++++---- 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/src/color/ansi.go b/src/color/ansi.go index 057cbad1..0b3e45d3 100644 --- a/src/color/ansi.go +++ b/src/color/ansi.go @@ -140,20 +140,59 @@ func (a *Ansi) InitPlain() { } func (a *Ansi) GenerateHyperlink(text string) string { - parts := strings.SplitAfter(text, ")") - var buffer strings.Builder - var part string - for i := range parts { - part += parts[i] - if strings.Contains(parts[i], "[") && !strings.Contains(parts[i], "]") { + const ( + LINK = "link" + TEXT = "text" + OTHER = "plain" + ) + + var result, hyperlink strings.Builder + var squareIndex, roundCount int + state := OTHER + + for i, s := range text { + if s == '[' && state == OTHER { + state = TEXT + hyperlink.WriteRune(s) continue } - buffer.WriteString(a.replaceHyperlink(part)) - part = "" + + if state == OTHER { + result.WriteRune(s) + continue + } + + hyperlink.WriteRune(s) + + switch s { + case ']': + // potential end of text part of hyperlink + squareIndex = i + case '(': + // split into link part + if squareIndex == i-1 { + state = LINK + } + if state == LINK { + roundCount++ + } + case ')': + if state != LINK { + continue + } + roundCount-- + if roundCount != 0 { + continue + } + // end of link part + result.WriteString(a.replaceHyperlink(hyperlink.String())) + hyperlink.Reset() + state = OTHER + } } - // when we did not process any parts, we return the original text - buffer.WriteString(part) - return buffer.String() + + result.WriteString(hyperlink.String()) + return result.String() } func (a *Ansi) replaceHyperlink(text string) string { diff --git a/src/color/ansi_test.go b/src/color/ansi_test.go index e060f927..50a31d9e 100644 --- a/src/color/ansi_test.go +++ b/src/color/ansi_test.go @@ -31,16 +31,16 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) { ShellName string Expected string }{ - { - Text: "in \x1b[1mpwsh \x1b[22m ", - ShellName: shell.PWSH, - Expected: "in \x1b[1mpwsh \x1b[22m ", - }, { Text: "[google](http://www.google.be) [maps (2/2)](http://maps.google.be)", ShellName: shell.FISH, Expected: "\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\ \x1b]8;;http://maps.google.be\x1b\\maps (2/2)\x1b]8;;\x1b\\", }, + { + Text: "in \x1b[1mpwsh \x1b[22m ", + ShellName: shell.PWSH, + Expected: "in \x1b[1mpwsh \x1b[22m ", + }, {Text: "[google](http://www.google.be)", ShellName: shell.ZSH, Expected: "%{\x1b]8;;http://www.google.be\x1b\\%}google%{\x1b]8;;\x1b\\%}"}, {Text: "[google](http://www.google.be)", ShellName: shell.PWSH, Expected: "\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\"}, {Text: "[google](http://www.google.be)", ShellName: shell.BASH, Expected: "\\[\x1b]8;;http://www.google.be\x1b\\\\\\]google\\[\x1b]8;;\x1b\\\\\\]"}, @@ -100,3 +100,22 @@ func TestFormatText(t *testing.T) { assert.Equal(t, tc.Expected, formattedText, tc.Case) } } + +func TestGenerateFileLink(t *testing.T) { + cases := []struct { + Text string + Expected string + }{ + { + Text: `[Posh](file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh)`, + Expected: "\x1b]8;;file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh\x1b\\Posh\x1b]8;;\x1b\\", + }, + {Text: `[Windows](file:C:/Windows)`, Expected: "\x1b]8;;file:C:/Windows\x1b\\Windows\x1b]8;;\x1b\\"}, + } + for _, tc := range cases { + a := Ansi{} + a.Init(shell.PWSH) + hyperlinkText := a.GenerateHyperlink(tc.Text) + assert.Equal(t, tc.Expected, hyperlinkText) + } +}