fix(hyperlink): use unique format to avoid conflicts

BREAKING CHANGE: this can cause existing manual hyperlinks
(`[text](link)`) to stop working. To fix, change those to
the following syntax: `«text»(link)`

resolves #3353
This commit is contained in:
Jan De Dobbeleer 2023-01-12 09:39:15 +01:00 committed by Jan De Dobbeleer
parent 7ce487d6b1
commit 4f83452e84
9 changed files with 38 additions and 33 deletions

View file

@ -110,10 +110,10 @@ type Writer struct {
osc51 string
// hyperlink
hasHyperlink bool
hyperlinkBuilder strings.Builder
squareIndex, roundCount int
hyperlinkState string
hasHyperlink bool
hyperlinkBuilder strings.Builder
bracketIndex, roundCount int
hyperlinkState string
}
func (w *Writer) Init(shellName string) {
@ -302,7 +302,7 @@ func (w *Writer) Write(background, foreground, text string) {
w.runes = []rune(text)
// only run hyperlink logic when we have to
w.hasHyperlink = strings.Count(text, "[")+strings.Count(text, "]")+strings.Count(text, "(")+strings.Count(text, ")") >= 4
w.hasHyperlink = strings.Count(text, "«")+strings.Count(text, "»")+strings.Count(text, "(")+strings.Count(text, ")") >= 4
for i := 0; i < len(w.runes); i++ {
s := w.runes[i]

View file

@ -20,7 +20,7 @@ func (w *Writer) write(i int, s rune) {
return
}
if s == '[' && w.hyperlinkState == OTHER {
if s == '«' && w.hyperlinkState == OTHER {
w.hyperlinkState = TEXT
w.hyperlinkBuilder.WriteRune(s)
return
@ -35,12 +35,12 @@ func (w *Writer) write(i int, s rune) {
w.hyperlinkBuilder.WriteRune(s)
switch s {
case ']':
case '»':
// potential end of text part of hyperlink
w.squareIndex = i
w.bracketIndex = i
case '(':
// split into link part
if w.squareIndex == i-1 {
if w.bracketIndex == i-1 {
w.hyperlinkState = LINK
}
if w.hyperlinkState == LINK {
@ -63,7 +63,7 @@ func (w *Writer) write(i int, s rune) {
func (w *Writer) replaceHyperlink(text string) string {
// hyperlink matching
results := regex.FindNamedRegexMatch("(?P<ALL>(?:\\[(?P<TEXT>.+)\\])(?:\\((?P<URL>.*)\\)))", text)
results := regex.FindNamedRegexMatch("(?P<ALL>(?:«(?P<TEXT>.+)»)(?:\\((?P<URL>.*)\\)))", text)
if len(results) != 3 {
return text
}

View file

@ -37,7 +37,7 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
Expected string
}{
{
Text: "[google](http://www.google.be) [maps (2/2)](http://maps.google.be)",
Text: "«google»(http://www.google.be) «maps (2/2)»(http://maps.google.be)",
ShellName: shell.FISH,
Expected: "\x1b[47m\x1b[30m\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\ \x1b]8;;http://maps.google.be\x1b\\maps (2/2)\x1b]8;;\x1b\\\x1b[0m",
},
@ -46,18 +46,23 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
ShellName: shell.PWSH,
Expected: "\x1b[47m\x1b[30min \x1b[0m\x1b[30m\x1b[1mpwsh \x1b[22m\x1b[47m \x1b[0m",
},
{Text: "[google](http://www.google.be)", ShellName: shell.ZSH, Expected: "%{\x1b[47m%}%{\x1b[30m%}%{\x1b]8;;http://www.google.be\x1b\\%}google%{\x1b]8;;\x1b\\%}%{\x1b[0m%}"},
{Text: "[google](http://www.google.be)", ShellName: shell.PWSH, Expected: "\x1b[47m\x1b[30m\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\\x1b[0m"},
{Text: "«google»(http://www.google.be)", ShellName: shell.ZSH, Expected: "%{\x1b[47m%}%{\x1b[30m%}%{\x1b]8;;http://www.google.be\x1b\\%}google%{\x1b]8;;\x1b\\%}%{\x1b[0m%}"},
{Text: "«google»(http://www.google.be)", ShellName: shell.PWSH, Expected: "\x1b[47m\x1b[30m\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\\x1b[0m"},
{
Text: "[google](http://www.google.be)",
Text: "«google»(http://www.google.be)",
ShellName: shell.BASH,
Expected: "\\[\x1b[47m\\]\\[\x1b[30m\\]\\[\x1b]8;;http://www.google.be\x1b\\\\\\]google\\[\x1b]8;;\x1b\\\\\\]\\[\x1b[0m\\]",
},
{
Text: "[google](http://www.google.be) [maps](http://maps.google.be)",
Text: "«google»(http://www.google.be) «maps»(http://maps.google.be)",
ShellName: shell.FISH,
Expected: "\x1b[47m\x1b[30m\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\ \x1b]8;;http://maps.google.be\x1b\\maps\x1b]8;;\x1b\\\x1b[0m",
},
{
Text: "[]«google»(http://www.google.be)[]",
ShellName: shell.FISH,
Expected: "\x1b[47m\x1b[30m[]\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\[]\x1b[0m",
},
}
for _, tc := range cases {
a := Writer{
@ -76,9 +81,9 @@ func TestGenerateHyperlinkWithUrlNoName(t *testing.T) {
ShellName string
Expected string
}{
{Text: "[](http://www.google.be)", ShellName: shell.ZSH, Expected: "%{\x1b[47m%}%{\x1b[30m%}[](http://www.google.be)%{\x1b[0m%}"},
{Text: "[](http://www.google.be)", ShellName: shell.PWSH, Expected: "\x1b[47m\x1b[30m[](http://www.google.be)\x1b[0m"},
{Text: "[](http://www.google.be)", ShellName: shell.BASH, Expected: "\\[\x1b[47m\\]\\[\x1b[30m\\][](http://www.google.be)\\[\x1b[0m\\]"},
{Text: "«»(http://www.google.be)", ShellName: shell.ZSH, Expected: "%{\x1b[47m%}%{\x1b[30m%}«»(http://www.google.be)%{\x1b[0m%}"},
{Text: "«»(http://www.google.be)", ShellName: shell.PWSH, Expected: "\x1b[47m\x1b[30m«»(http://www.google.be)\x1b[0m"},
{Text: "«»(http://www.google.be)", ShellName: shell.BASH, Expected: "\\[\x1b[47m\\]\\[\x1b[30m\\]«»(http://www.google.be)\\[\x1b[0m\\]"},
}
for _, tc := range cases {
a := Writer{
@ -97,10 +102,10 @@ func TestGenerateFileLink(t *testing.T) {
Expected string
}{
{
Text: `[Posh](file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh)`,
Text: `«Posh»(file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh)`,
Expected: "\x1b[47m\x1b[30m\x1b]8;;file:C:/Program Files (x86)/Common Files/Microsoft Shared/Posh\x1b\\Posh\x1b]8;;\x1b\\\x1b[0m",
},
{Text: `[Windows](file:C:/Windows)`, Expected: "\x1b[47m\x1b[30m\x1b]8;;file:C:/Windows\x1b\\Windows\x1b]8;;\x1b\\\x1b[0m"},
{Text: `«Windows»(file:C:/Windows)`, Expected: "\x1b[47m\x1b[30m\x1b]8;;file:C:/Windows\x1b\\Windows\x1b]8;;\x1b\\\x1b[0m"},
}
for _, tc := range cases {
a := Writer{

View file

@ -226,13 +226,13 @@ func TestWriteLength(t *testing.T) {
},
{
Case: "Bold with color override and link",
Input: "<b><#ffffff>test</></b> [url](https://example.com)",
Input: "<b><#ffffff>test</></b> «url»(https://example.com)",
Expected: 8,
Colors: &Colors{Foreground: "black", Background: ParentBackground},
},
{
Case: "Bold with color override and link and leading/trailing spaces",
Input: " <b><#ffffff>test</></b> [url](https://example.com) ",
Input: " <b><#ffffff>test</></b> «url»(https://example.com) ",
Expected: 10,
Colors: &Colors{Foreground: "black", Background: ParentBackground},
},

View file

@ -29,7 +29,7 @@ type Brewfather struct {
DaysBottled uint
DaysBottledOrFermented *uint // help avoid chronic template logic - code will point this to one of above or be nil depending on status
URL string // URL of batch page to open if hyperlink enabled on the segment and URL formatting used in template: [name](link)
URL string // URL of batch page to open if hyperlink enabled on the segment and URL formatting used in template: «text»(link)
}
const (

View file

@ -207,7 +207,7 @@ func TestOWMSegmentIcons(t *testing.T) {
env := &mock.MockedEnvironment{}
response := fmt.Sprintf(`{"weather":[{"icon":"%s"}],"main":{"temp":20.3}}`, tc.IconID)
expectedString := fmt.Sprintf("[%s (20°C)](http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key)", tc.ExpectedIconString)
expectedString := fmt.Sprintf("«%s (20°C)»(http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key)", tc.ExpectedIconString)
env.On("HTTPRequest", OWMAPIURL).Return([]byte(response), nil)
@ -222,7 +222,7 @@ func TestOWMSegmentIcons(t *testing.T) {
}
assert.Nil(t, o.setStatus())
assert.Equal(t, expectedString, renderTemplate(env, "[{{.Weather}} ({{.Temperature}}{{.UnitIcon}})]({{.URL}})", o), tc.Case)
assert.Equal(t, expectedString, renderTemplate(env, "«{{.Weather}} ({{.Temperature}}{{.UnitIcon}})»({{.URL}})", o), tc.Case)
}
}
func TestOWMSegmentFromCache(t *testing.T) {
@ -250,7 +250,7 @@ func TestOWMSegmentFromCache(t *testing.T) {
func TestOWMSegmentFromCacheWithHyperlink(t *testing.T) {
response := fmt.Sprintf(`{"weather":[{"icon":"%s"}],"main":{"temp":20}}`, "01d")
expectedString := fmt.Sprintf("[%s (20°C)](http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key)", "\ufa98")
expectedString := fmt.Sprintf("«%s (20°C)»(http://api.openweathermap.org/data/2.5/weather?q=AMSTERDAM,NL&units=metric&appid=key)", "\ufa98")
env := &mock.MockedEnvironment{}
cache := &mock.MockedCache{}
@ -269,5 +269,5 @@ func TestOWMSegmentFromCacheWithHyperlink(t *testing.T) {
env.On("Cache").Return(cache)
assert.Nil(t, o.setStatus())
assert.Equal(t, expectedString, renderTemplate(env, "[{{.Weather}} ({{.Temperature}}{{.UnitIcon}})]({{.URL}})", o))
assert.Equal(t, expectedString, renderTemplate(env, "«{{.Weather}} ({{.Temperature}}{{.UnitIcon}})»({{.URL}})", o))
}

View file

@ -14,9 +14,9 @@ func url(text, url string) (string, error) {
if err != nil {
return "", err
}
return fmt.Sprintf("[%s](%s)", text, url), nil
return fmt.Sprintf("«%s»(%s)", text, url), nil
}
func path(text, path string) (string, error) {
return fmt.Sprintf("[%s](file:%s)", text, path), nil
return fmt.Sprintf("«%s»(file:%s)", text, path), nil
}

View file

@ -17,7 +17,7 @@ func TestUrl(t *testing.T) {
Template string
ShouldError bool
}{
{Case: "valid url", Expected: "[link](https://ohmyposh.dev)", Template: `{{ url "link" "https://ohmyposh.dev" }}`},
{Case: "valid url", Expected: "«link»(https://ohmyposh.dev)", Template: `{{ url "link" "https://ohmyposh.dev" }}`},
{Case: "invalid url", Expected: "", Template: `{{ url "link" "Foo" }}`, ShouldError: true},
}
@ -48,7 +48,7 @@ func TestPath(t *testing.T) {
Expected string
Template string
}{
{Case: "valid path", Expected: "[link](file:/test/test)", Template: `{{ path "link" "/test/test" }}`},
{Case: "valid path", Expected: "«link»(file:/test/test)", Template: `{{ path "link" "/test/test" }}`},
}
env := &mock.MockedEnvironment{}

View file

@ -137,8 +137,8 @@ Hyperlink formatting example
```json
{
// General format: [Text](Url)
"template": "[{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d{{end}} {{.Recipe.Name}}]({{.URL}})"
// General format: «text»(link)
"template": "«{{.StatusIcon}} {{if .DaysBottledOrFermented}}{{.DaysBottledOrFermented}}d{{end}} {{.Recipe.Name}}»({{.URL}})"
}
```