diff --git a/border_test.go b/border_test.go new file mode 100644 index 00000000..3a887bef --- /dev/null +++ b/border_test.go @@ -0,0 +1,297 @@ +package lipgloss + +import ( + "strings" + "testing" +) + +var secretBorderFantasy = Border{ + Top: "._.:*:", + Bottom: "._.:*:", + Left: "|*", + Right: "|*", + TopLeft: "*", + TopRight: "*", + BottomLeft: "*", + BottomRight: "*", +} + +// NB: The superfluous TrimSpace calls and newlines here are for readability. + +func TestBorder(t *testing.T) { + for i, tc := range []struct { + name string + style Style + expected string + }{ + { + name: "default border via border style", + style: NewStyle(). + BorderStyle(NormalBorder()). + SetString("Hello"), + expected: strings.TrimSpace(` +┌─────┐ +│Hello│ +└─────┘`), + }, + + { + name: "default border via all-in-one, implicit", + style: NewStyle(). + Border(RoundedBorder()). + SetString("Hello"), + expected: strings.TrimSpace(` +╭─────╮ +│Hello│ +╰─────╯`), + }, + + { + name: "default border via all-in-one, explicit", + style: NewStyle(). + Border(RoundedBorder(), true, true, true, true). + SetString("Hello"), + expected: strings.TrimSpace(` +╭─────╮ +│Hello│ +╰─────╯`), + }, + + { + name: "rounded border via all-in-one, no bottom", + style: NewStyle(). + Border(RoundedBorder(), true, true, false, true). + SetString("Hello"), + expected: strings.TrimSpace(` +╭─────╮ +│Hello│`), + }, + + { + name: "rounded border method-by-method, no right", + style: NewStyle(). + BorderStyle(RoundedBorder()). + BorderTop(true). + BorderBottom(true). + BorderLeft(true). + SetString("Hello"), + expected: strings.TrimSpace(` +╭───── +│Hello +╰─────`), + }, + + { + name: "rounded border method-by-method, no left", + style: NewStyle(). + BorderStyle(RoundedBorder()). + BorderTop(true). + BorderBottom(true). + BorderLeft(false). + BorderRight(true). + SetString("Hello"), + expected: strings.TrimSpace(` +─────╮ +Hello│ +─────╯`), + }, + + { + name: "border via methods, no actual border set", + style: NewStyle(). + BorderTop(true). + BorderRight(true). + BorderBottom(true). + BorderLeft(true). + SetString("Hello"), + expected: "Hello", + }, + + { + name: "custom border", + style: NewStyle(). + BorderStyle(Border{ + Left: "|", + Right: "|", + Top: ">", + Bottom: "<", + TopLeft: "+", + TopRight: ">", + BottomLeft: "<", + BottomRight: "+", + }). + Padding(0, 1). + SetString("Hello"), + expected: strings.TrimSpace(` ++>>>>>>>> +| Hello | +<<<<<<<<+`), + }, + + { + name: "corners only", + style: NewStyle(). + BorderStyle(Border{ + TopLeft: "+", + TopRight: "+", + BottomLeft: "+", + BottomRight: "+", + }). + SetString("Hello"), + expected: strings.TrimSpace("\n" + + `+ +` + "\n" + + ` Hello ` + "\n" + + `+ +`), + }, + + { + name: "set top via method", + style: NewStyle().BorderTop(true).SetString("Hello"), + expected: "Hello", + }, + { + name: "set bottom via method", + style: NewStyle().BorderTop(true).SetString("Hello"), + expected: "Hello", + }, + + { + name: "inline, set right via method", + style: NewStyle().BorderRight(true).SetString("Hello"), + expected: `Hello`, + }, + + { + name: "set right via border style", + style: NewStyle(). + BorderStyle(Border{ + Right: "|", + }). + SetString("Hello"), + expected: "Hello|", + }, + + { + name: "left via border style only", + style: NewStyle(). + BorderStyle(Border{ + Left: "|", + }). + SetString("Hello"), + expected: "|Hello", + }, + + { + name: "left and right via border style only", + style: NewStyle(). + BorderStyle(Border{ + Left: "(", + Right: ")", + }). + SetString("你好"), + expected: `(你好)`, + }, + + { + name: "inline, left and right via border style only", + style: NewStyle(). + BorderStyle(Border{ + Left: "「", + Right: "」", + }). + Inline(true). + SetString("你好"), + expected: `「你好」`, + }, + + { + name: "left and right, two cells high", + style: NewStyle(). + BorderStyle(Border{ + Left: "(", + Right: ")", + }). + Padding(0, 1). + SetString("你\n好"), + expected: strings.TrimSpace(` +( 你 ) +( 好 )`), + }, + + { + name: "left and right with vertical padding", + style: NewStyle(). + BorderStyle(Border{ + Left: "(", + Right: ")", + }). + Padding(1). + SetString("你\n好"), + expected: strings.TrimSpace(` +( ) +( 你 ) +( 好 ) +( )`), + }, + + { + name: "right only by deduction, two cells high", + style: NewStyle(). + BorderStyle(Border{ + Left: "(", + Right: ")", + }). + BorderLeft(false). + BorderRight(true). + Padding(0, 1). + SetString("你\n好"), + expected: ` 你 ) + 好 )`, + }, + + { + name: "all but left via shorthand, two cells high", + style: NewStyle(). + Border(DoubleBorder(), true, true, true, false). + SetString("你\n好"), + expected: strings.TrimSpace(` +══╗ +你║ +好║ +══╝`), + }, + + { + name: "outrageous border", + style: NewStyle(). + BorderStyle(secretBorderFantasy). + Padding(1, 2). + SetString("Kitty\nCat"). + Align(Center), + expected: strings.TrimSpace(` +*._.:*:._.* +| | +* Kitty * +| Cat | +* * +*._.:*:._.*`), + }, + } { + res := tc.style.String() + if res != tc.expected { + t.Errorf( + "Test #%d (%s):\nExpected:\n%s\nGot: \n%s", + i+1, + tc.name, + showHiddenChars(tc.expected), + showHiddenChars(res), + ) + } + } +} + +func showHiddenChars(s string) string { + s = strings.ReplaceAll(s, " ", "•") + s = strings.ReplaceAll(s, "\t", "→") + return strings.ReplaceAll(s, "\n", "¶\n") +} diff --git a/borders.go b/borders.go index deb6b35a..8d071a5b 100644 --- a/borders.go +++ b/borders.go @@ -11,14 +11,17 @@ import ( // Border contains a series of values which comprise the various parts of a // border. type Border struct { - Top string - Bottom string - Left string - Right string - TopLeft string - TopRight string - BottomLeft string - BottomRight string + // Values for all borders. + Top string + Bottom string + Left string + Right string + TopLeft string + TopRight string + BottomLeft string + BottomRight string + + // Values for table borders. MiddleLeft string MiddleRight string Middle string @@ -233,7 +236,29 @@ func (s Style) applyBorder(str string) string { bottomSet = s.isSet(borderBottomKey) leftSet = s.isSet(borderLeftKey) - border = s.getBorderStyle() + border = s.getBorderStyle() + + hasBorderTopLeft = border.TopLeft != "" + hasBorderTop = border.Top != "" + hasBorderTopRight = border.TopRight != "" + hasBorderRight = border.Right != "" + hasBorderBottomRight = border.BottomRight != "" + hasBorderBottom = border.Bottom != "" + hasBorderBottomLeft = border.BottomLeft != "" + hasBorderLeft = border.Left != "" + + // Determine if a border was set. Borders also contain a middle section + // for tables, but those don't apply here, so we explicitly check for + // the relevant parts of borders only. + hasBorder = hasBorderTop || + hasBorderBottom || + hasBorderLeft || + hasBorderRight || + hasBorderTopLeft || + hasBorderTopRight || + hasBorderBottomLeft || + hasBorderBottomRight + hasTop = s.getAsBool(borderTopKey, false) hasRight = s.getAsBool(borderRightKey, false) hasBottom = s.getAsBool(borderBottomKey, false) @@ -252,7 +277,7 @@ func (s Style) applyBorder(str string) string { // If a border is set and no sides have been specifically turned on or off // render borders on all sides. - if border != noBorder && !(topSet || rightSet || bottomSet || leftSet) { + if hasBorder && !topSet && !rightSet && !bottomSet && !leftSet { hasTop = true hasRight = true hasBottom = true @@ -260,10 +285,43 @@ func (s Style) applyBorder(str string) string { } // If no border is set or all borders are been disabled, abort. - if border == noBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) { + if !hasBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) { return str } + // But should we really render the top border? + if !topSet && !hasBorderTop && !hasBorderTopLeft && !hasBorderTopRight { + hasTop = false + } + + // And should we render the bottom border? + if !bottomSet && !hasBorderBottom && !hasBorderBottomLeft && !hasBorderBottomRight { + hasBottom = false + } + + // Don't render horizontal borders if the top and bottom won't be rendered + // and the border edge isn't set. + // + // For example, we wouldn't render a left border in the following case + // because setting the right border only nullifies other borders: + // + // Style(). + // BorderStyle(NormalBorder()). + // BorderRight(true) + // + // We also wouldn't render the left border in the following case, where the + // top and bottom borders are missing and the left border doesn't have + // a value set: + // + // Style(). + // BorderStyle(Border{Right: "│"}) + if !hasBorderLeft && !hasTop && !hasBottom { + hasLeft = false + } + if !hasBorderRight && !hasTop && !hasBottom { + hasRight = false + } + lines, width := getLines(str) if hasLeft { @@ -277,8 +335,8 @@ func (s Style) applyBorder(str string) string { border.Right = " " } - // If corners should be rendered but are set with the empty string, fill them - // with a single space. + // If corners should be rendered but are set with the empty string, fill + // them with a single space. if hasTop && hasLeft && border.TopLeft == "" { border.TopLeft = " " } @@ -325,7 +383,7 @@ func (s Style) applyBorder(str string) string { var out strings.Builder - // Render top + // Render top. This includes the top left and right corners. if hasTop { top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width) top = s.styleBorder(top, topFG, topBG) @@ -339,7 +397,7 @@ func (s Style) applyBorder(str string) string { rightRunes := []rune(border.Right) rightIndex := 0 - // Render sides + // Render sides. This never includes any corners. for i, l := range lines { if hasLeft { r := string(leftRunes[leftIndex]) @@ -363,7 +421,7 @@ func (s Style) applyBorder(str string) string { } } - // Render bottom + // Render bottom. This includes the bottom left and right corners. if hasBottom { bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width) bottom = s.styleBorder(bottom, bottomFG, bottomBG) diff --git a/style.go b/style.go index 2aba56b0..b5edda93 100644 --- a/style.go +++ b/style.go @@ -438,11 +438,25 @@ func (s Style) Render(strs ...string) string { } } + // Don't apply margins in inline mode. if !inline { - str = s.applyBorder(str) str = s.applyMargins(str, inline) } + if inline { + // Obliterate top, bottom, and corder borders in inline mode, but leave + // the left and right as-is. + s.borderStyle.Top = "" + s.borderStyle.Bottom = "" + s.borderStyle.TopLeft = "" + s.borderStyle.TopRight = "" + s.borderStyle.BottomLeft = "" + s.borderStyle.BottomRight = "" + } + + // Apply borders. + str = s.applyBorder(str) + // Truncate according to MaxWidth if maxWidth > 0 { lines := strings.Split(str, "\n")