diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..6715a5c
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @coldfgirl
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..22d0d82
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+vendor
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/instyle.iml b/.idea/instyle.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/instyle.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..5f4ee3f
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7a12277
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 robin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c6dbc86
--- /dev/null
+++ b/README.md
@@ -0,0 +1,144 @@
+# InStyle
+
+![demo.gif](demo.gif)
+
+InStyle is a small library for efficiently decorating strings with ANSI escape codes.
+
+## Syntax
+
+The tags follow the following format:
+
+```
+[!style]text to be styled[/]
+```
+
+Style can be a named style, or a raw value to be used in an ANSI escape code.
+For example, both of these will turn the text red:
+
+```
+[!red]this text will show up as red[/]
+[!31]this text will show up as red[/]
+```
+
+The ending sequence of `[/]` can be fully omitted for minor performance gains like so:
+
+```
+[!italic]ending tags need not be included
+```
+
+### Multiple Styles
+
+Multiple styles can be added by using the `+` character between each style desired.
+
+```
+[!magenta+bold]this text has two styles[/]
+```
+
+### Nesting & Sequential Tags
+
+Up to 5 tags can be nested.
+All unclosed tags are terminated at the end of a string.
+
+```
+[!cyan]i never said [!bold]you[/] did that[/]... [!italic]somebody else[/] did
+```
+
+### Named Styles
+
+
+
+Complete list of default styles.
+
+#### Text Styling
+
+- `plain`
+- `reset`
+- `bold`
+- `faint`
+- `italic`
+- `underline`
+- `blink`
+- `strike`
+
+#### Basic Colors
+
+- `black`
+- `red`
+- `green`
+- `yellow`
+- `blue`
+- `magenta`
+- `cyan`
+- `white`
+- `default`
+
+#### Basic Backgrounds
+
+- `bg-black`
+- `bg-red`
+- `bg-green`
+- `bg-yellow`
+- `bg-blue`
+- `bg-magenta`
+- `bg-cyan`
+- `bg-white`
+- `bg-default`
+
+#### Light Colors
+
+- `light-black`
+- `light-red`
+- `light-green`
+- `light-yellow`
+- `light-blue`
+- `light-magenta`
+- `light-cyan`
+- `light-white`
+
+#### Light Backgrounds
+
+- `bg-light-black`
+- `bg-light-red`
+- `bg-light-green`
+- `bg-light-yellow`
+- `bg-light-blue`
+- `bg-light-magenta`
+- `bg-light-cyan`
+- `bg-light-white`
+
+
+
+Aside from the named styles, additional styles can be added to a `Styler` instance by using the `Register` method.
+This can be used to associated more than one ANSI escape code to a name.
+
+```go
+s := instyle.NewStyler()
+s.Register("error", "1;31") // Bold and red
+```
+
+A style name can only be a maximum of 15 characters long.
+
+## Performance
+
+While applying a set of styles, this code runs ~2-3x slower than an unbuffered copy of an array of runes:
+
+```go
+// ideal performance goal:
+var dst []rune
+for _, r := range []rune("...") {
+ dst = append(dst, r)
+}
+```
+
+However, when compared a regex solution or using [Lip Gloss](https://github.com/charmbracelet/lipgloss) directly this will perform about 5-10x faster.
+
+Running on a M2 2022 MacBook Air, the truncated / formatted benchmark results look like:
+
+```
+BenchmarkBaseline/BestCase-8 15542974 70 ns/op
+BenchmarkBaseline/PerformanceGoal-8 4497812 266 ns/op
+BenchmarkApply/NoStyle-8 4112559 291 ns/op
+BenchmarkApply/WithStyle-8 1775058 677 ns/op
+BenchmarkApply/WithStyleToFromString-8 747465 1581 ns/op
+BenchmarkSimilarLipGloss-8 129320 9212 ns/op
+```
diff --git a/apply.go b/apply.go
new file mode 100644
index 0000000..e20fae4
--- /dev/null
+++ b/apply.go
@@ -0,0 +1,7 @@
+package instyle
+
+import "fmt"
+
+func Apply(format string, args ...any) string {
+ return fmt.Sprintf(string(NewStyler().Apply([]rune(format))), args...)
+}
diff --git a/apply_test.go b/apply_test.go
new file mode 100644
index 0000000..7432ec4
--- /dev/null
+++ b/apply_test.go
@@ -0,0 +1,18 @@
+package instyle_test
+
+import (
+ "instyle"
+ "testing"
+)
+
+func TestApply(t *testing.T) {
+ in := "[!bold]%s[/]"
+ inParam := "testing [!faint]string[/]"
+ out := "\033[0m\033[1mtesting [!faint]string[/]\033[0m"
+
+ if result := instyle.Apply(in, inParam); result != out {
+ t.Logf("Want: %+v", []rune(out))
+ t.Logf("Got: %+v", []rune(result))
+ t.FailNow()
+ }
+}
diff --git a/cmd/instyle/main.go b/cmd/instyle/main.go
new file mode 100644
index 0000000..8ba62c6
--- /dev/null
+++ b/cmd/instyle/main.go
@@ -0,0 +1,17 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/coldfgirl/instyle"
+)
+
+func main() {
+ if len(os.Args) > 1 {
+ fmt.Println(instyle.Apply(strings.Join(os.Args[1:], " ")))
+ } else {
+ _, _ = fmt.Fprintln(os.Stderr, instyle.Apply("[!bold+red]no command line arguments provided"))
+ }
+}
diff --git a/demo.gif b/demo.gif
new file mode 100644
index 0000000..7bf8122
Binary files /dev/null and b/demo.gif differ
diff --git a/demo.tape b/demo.tape
new file mode 100644
index 0000000..6f0d8bf
--- /dev/null
+++ b/demo.tape
@@ -0,0 +1,13 @@
+Output demo.gif
+
+Require echo
+
+Set Theme "Catppuccin Frappe"
+
+Set Shell "bash"
+Set FontSize 18
+Set Width 1200
+Set Height 200
+
+Type@45ms "go run ./cmd/... '[!italic]you can [!cyan]style[/] text with [!bold+magenta]InStyle[/]!!!'" Sleep 100ms Enter
+Sleep 10s
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..4a26287
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,16 @@
+module github.com/coldfgirl/instyle
+
+go 1.21.4
+
+require github.com/charmbracelet/lipgloss v0.13.0
+
+require (
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/charmbracelet/x/ansi v0.1.4 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/muesli/termenv v0.15.2 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ golang.org/x/sys v0.19.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..62605f0
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,20 @@
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
+github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
+github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
+github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
+github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/style_set.go b/style_set.go
new file mode 100644
index 0000000..c84b389
--- /dev/null
+++ b/style_set.go
@@ -0,0 +1,275 @@
+package instyle
+
+import (
+ "math"
+ "strings"
+
+ "github.com/charmbracelet/lipgloss"
+)
+
+const (
+ // keySizeMax is used to determine the maximum size of a named style.
+ // This is important for optimization in that it allows for a map to be used to find styles by name.
+ keySizeMax = 16
+
+ // styleDepthMax is used to determine how many levels of nesting are allowed.
+ styleDepthMax = 5
+)
+
+var (
+ openingBegin = []rune("[!")
+ openingClose = []rune("]")
+ closing = []rune("[/]")
+
+ reset = []rune{'\033', '[', '0', 'm'}
+)
+
+type Styler interface {
+ Apply(original []rune) (output []rune)
+ Register(key string, value string) (self Styler)
+ RegisterLipGlossStyle(key string, value lipgloss.Style) (self Styler)
+}
+
+type styleSet struct {
+ named map[[keySizeMax]rune][]rune
+}
+
+func NewStyler() Styler {
+ s := new(styleSet)
+ s.named = make(map[[keySizeMax]rune][]rune)
+
+ s.Register("plain", "22")
+
+ s.Register("reset", "0")
+ s.Register("bold", "1")
+ s.Register("faint", "2")
+ s.Register("italic", "3")
+ s.Register("underline", "4")
+ s.Register("blink", "5")
+ s.Register("strike", "6")
+
+ s.Register("black", "30")
+ s.Register("red", "31")
+ s.Register("green", "32")
+ s.Register("yellow", "33")
+ s.Register("blue", "34")
+ s.Register("magenta", "35")
+ s.Register("cyan", "36")
+ s.Register("white", "37")
+ s.Register("default", "39")
+
+ s.Register("bg-black", "40")
+ s.Register("bg-red", "41")
+ s.Register("bg-green", "42")
+ s.Register("bg-yellow", "43")
+ s.Register("bg-blue", "44")
+ s.Register("bg-magenta", "45")
+ s.Register("bg-cyan", "46")
+ s.Register("bg-white", "47")
+ s.Register("bg-default", "49")
+
+ s.Register("light-black", "90")
+ s.Register("light-red", "91")
+ s.Register("light-green", "92")
+ s.Register("light-yellow", "93")
+ s.Register("light-blue", "94")
+ s.Register("light-magenta", "95")
+ s.Register("light-cyan", "96")
+ s.Register("light-white", "97")
+
+ s.Register("bg-light-black", "100")
+ s.Register("bg-light-red", "101")
+ s.Register("bg-light-green", "102")
+ s.Register("bg-light-yellow", "103")
+ s.Register("bg-light-blue", "104")
+ s.Register("bg-light-magenta", "105")
+ s.Register("bg-light-cyan", "106")
+ s.Register("bg-light-white", "107")
+
+ return s
+}
+
+func (s *styleSet) Register(key string, value string) Styler {
+ parsed := [keySizeMax]rune{}
+ for k, v := range key[:int(math.Min(keySizeMax, float64(len(key))))] {
+ parsed[k] = v
+ }
+
+ s.named[parsed] = []rune(value)
+ return s
+}
+
+func (s *styleSet) RegisterLipGlossStyle(key string, value lipgloss.Style) Styler {
+ p := lipgloss.ColorProfile()
+
+ var sequence []string
+
+ if _, noColor := value.GetForeground().(lipgloss.NoColor); !noColor {
+ sequence = append(sequence, p.FromColor(value.GetForeground()).Sequence(false))
+ }
+
+ if _, noColor := value.GetBackground().(lipgloss.NoColor); !noColor {
+ sequence = append(sequence, p.FromColor(value.GetBackground()).Sequence(true))
+ }
+
+ if value.GetBold() {
+ sequence = append(sequence, "1")
+ }
+
+ if value.GetFaint() {
+ sequence = append(sequence, "2")
+ }
+
+ if value.GetItalic() {
+ sequence = append(sequence, "3")
+ }
+
+ if value.GetUnderline() {
+ sequence = append(sequence, "4")
+ }
+
+ if value.GetBlink() {
+ sequence = append(sequence, "5")
+ }
+
+ if value.GetStrikethrough() {
+ sequence = append(sequence, "6")
+ }
+
+ return s.Register(key, strings.Join(sequence, ";"))
+}
+
+func (s *styleSet) Apply(runes []rune) []rune {
+ var (
+ appliedStyleStack = [styleDepthMax][]rune{}
+ ok = false
+ output = make([]rune, 0, len(runes)*4/3+5) // Pre-allocate n * 1.33 + 5 the size of the passed runes.
+ )
+
+ output = append(output, reset...)
+
+ for i, nest := 0, 0; i < len(runes); i++ {
+ r := runes[i]
+
+ if r == openingBegin[0] && nest < styleDepthMax {
+ if appliedStyleStack[nest], i, ok = s.parseOpening(runes, i); ok {
+ if nest = nest + 1; nest > 0 {
+ output = append(output, appliedStyleStack[nest-1]...)
+ continue
+ }
+ }
+ }
+
+ if r == closing[0] && nest > 0 {
+ if i, ok = checkSequence(closing, runes, i); ok {
+ if nest = nest - 1; nest >= 0 {
+ output = append(output, reset...)
+ appliedStyleStack[nest] = nil
+
+ if i+1 == len(runes) {
+ appliedStyleStack[0] = nil
+ }
+
+ for i := 0; i < len(appliedStyleStack) && i < nest; i++ {
+ output = append(output, appliedStyleStack[i]...)
+ }
+
+ continue
+ }
+ }
+ }
+
+ output = append(output, r)
+ }
+
+ if appliedStyleStack[0] != nil {
+ output = append(output, reset...)
+ }
+
+ return output
+}
+
+// parseOpening operates similarly to checkSequence but specifically for the opening of a style tag.
+// When a valid style tag is found, the computed sequence of ANSI style runes is returned.
+func (s *styleSet) parseOpening(runes []rune, idx int) ([]rune, int, bool) {
+ after, ok := checkSequence(openingBegin, runes, idx)
+ if !ok {
+ return nil, idx, false
+ }
+
+ sequence := make([]rune, 0, 10)
+ sequence = append(sequence, '\033', '[')
+
+ first := true
+ numeric := true
+
+ key := [keySizeMax]rune{}
+
+ for i, count := after+1, 0; i < len(runes); i++ {
+ r := runes[i]
+
+ if isClose := r == openingClose[0]; isClose || r == '+' {
+ if count == 0 || count >= keySizeMax {
+ return nil, idx, false
+ }
+
+ if !first {
+ sequence = append(sequence, ';')
+ }
+
+ first = false
+
+ if found, ok := s.named[key]; ok {
+ sequence = append(sequence, found...)
+ } else if numeric {
+ sequence = append(sequence, key[:count]...)
+ } else {
+ return nil, idx, false
+ }
+
+ if isClose {
+ var ok bool
+ if after, ok = checkSequence(openingClose, runes, i); ok {
+ break
+ } else {
+ return nil, idx, false
+ }
+ }
+
+ count = 0
+ numeric = true
+ key = [keySizeMax]rune{}
+ continue
+ }
+
+ if r < '0' || r > '9' {
+ numeric = false
+ }
+
+ key[count] = r
+ count++
+ }
+
+ return append(sequence, 'm'), after, true
+}
+
+// checkSequence will attempt to find a sequence of runes at a given index.
+// If the sequence is found, the runes index at the end of the sequence is returned.
+func checkSequence(sequence []rune, runes []rune, idx int) (int, bool) {
+ lenRunes, lenSequence := len(runes), len(sequence)
+
+ // Determine if the sequence would be impossible given current lengths:
+ if lenRunes < lenSequence || lenRunes-idx < lenSequence {
+ return idx, false
+ }
+
+ // Attempt to find the sequence:
+ for i := 0; i < lenSequence; i++ {
+ if sequence[i] != runes[idx+i] {
+ return idx, false
+ }
+ }
+
+ // Return the index after the sequence:
+ return idx + lenSequence - 1, true
+}
diff --git a/style_set_test.go b/style_set_test.go
new file mode 100644
index 0000000..cd1a7fc
--- /dev/null
+++ b/style_set_test.go
@@ -0,0 +1,113 @@
+package instyle_test
+
+import (
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/x/ansi"
+ "instyle"
+ "testing"
+)
+
+const (
+ noStyle = "[tag] sending request 3.2 second ago... log id: 10298402358"
+ withStyle = "[!bold][tag][/] sending request [!faint]3.2 seconds ago...[/] [!bold][!cyan]log id:[/] [!magenta]10298402358[/][/]"
+)
+
+func BenchmarkBaseline(b *testing.B) {
+ b.Run("BestCase", func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ runes := make([]rune, 0, len(noStyle))
+ for _, r := range []rune(noStyle) {
+ runes = append(runes, r)
+ }
+ }
+ })
+
+ b.Run("PerformanceGoal", func(b *testing.B) { // Ideally it's as fast as an unoptimized copy from one rune away to another.
+ for i := 0; i < b.N; i++ {
+ var runes []rune
+ for _, r := range []rune(noStyle) {
+ runes = append(runes, r)
+ }
+ }
+ })
+}
+
+func BenchmarkApply(b *testing.B) {
+ b.Run("NoStyle", func(b *testing.B) {
+ m := instyle.NewStyler()
+ for i := 0; i < b.N; i++ {
+ _ = m.Apply([]rune(noStyle))
+ }
+ })
+
+ b.Run("WithStyle", func(b *testing.B) {
+ m := instyle.NewStyler()
+ withStyleRunes := []rune(withStyle)
+ for i := 0; i < b.N; i++ {
+ _ = m.Apply(withStyleRunes)
+ }
+ })
+
+ b.Run("WithStyleToFromString", func(b *testing.B) {
+ m := instyle.NewStyler()
+ for i := 0; i < b.N; i++ {
+ _ = string(m.Apply([]rune(withStyle)))
+ }
+ })
+}
+
+func BenchmarkSimilarLipGloss(b *testing.B) {
+ styleBold := lipgloss.NewStyle().Bold(true)
+ styleFaint := lipgloss.NewStyle().Faint(true)
+ styleBoldCyan := lipgloss.NewStyle().Foreground(lipgloss.Color(ansi.Cyan)).Bold(true)
+ styleBoldMagenta := lipgloss.NewStyle().Foreground(lipgloss.Color(ansi.Magenta)).Bold(true)
+
+ for i := 0; i < b.N; i++ {
+ _ = styleBold.Render("[tag]") +
+ " sending request " +
+ styleFaint.Render("3.2 seconds ago...") +
+ " " +
+ styleBoldCyan.Render("log id:") +
+ " " +
+ styleBoldMagenta.Render("10298402358")
+ }
+}
+
+func TestStyleSet_Apply(t *testing.T) {
+ tests := map[string]struct {
+ In string
+ Expected string
+ }{
+ "Simple": {
+ In: "[!bold]bolded text[/]",
+ Expected: "\033[0m\033[1mbolded text\033[0m",
+ },
+ "SequentialTags": {
+ In: "[!red]one[/] [!blue]two[/] [!black]three[/]",
+ Expected: "\033[0m\033[31mone\033[0m \033[34mtwo\033[0m \033[30mthree\033[0m",
+ },
+ "NestedTags": {
+ In: "[!italic]this text is [!bold]bold [!red]red[/]-ish[/] and italic[/]",
+ Expected: "\033[0m\033[3mthis text is \033[1mbold \033[31mred\033[0m\033[3m\033[1m-ish\033[0m\033[3m and italic\033[0m",
+ },
+ "UnclosedTags": {
+ In: "[!bold]bold and [!red]red also",
+ Expected: "\033[0m\033[1mbold and \033[31mred also\033[0m",
+ },
+ "DeDuplicateEndResetTags": {
+ In: "[!bold]nested and [!red]red[/]",
+ Expected: "\033[0m\033[1mnested and \033[31mred\033[0m",
+ },
+ }
+
+ for name, tc := range tests {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ if result := instyle.NewStyler().Apply([]rune(tc.In)); string(result) != tc.Expected {
+ t.Logf("Want: %+v", tc.Expected)
+ t.Logf("Got: %+v", string(result))
+ t.FailNow()
+ }
+ })
+ }
+}