diff --git a/textRender.go b/textRender.go index 04ef135..93b2e21 100644 --- a/textRender.go +++ b/textRender.go @@ -8,7 +8,7 @@ import ( // TextRender adds common methods for rendering a text on screeen. type TextRender interface { NormalizedText(text string) string - RenderSequence(text string, lastColor, background Attribute) RenderedSequence + RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence } // MarkdownRegex is used by MarkdownTextRenderer to determine how to format the @@ -22,12 +22,30 @@ var markdownPattern = regexp.MustCompile(MarkdownRegex) // MarkdownTextRenderer is used for rendering the text with colors using // markdown-like syntax. // See: https://github.com/gizak/termui/issues/4#issuecomment-87270635 -type MarkdownTextRenderer struct{} +type MarkdownTextRenderer struct { + Text string +} // NormalizedText returns the text the user will see (without colors). // It strips out all formatting option and only preserves plain text. -func (r MarkdownTextRenderer) NormalizedText(text string) string { - return r.RenderSequence(text, 0, 0).NormalizedText +func (r MarkdownTextRenderer) NormalizedText() string { + return r.normalizeText(r.Text) +} + +func (r MarkdownTextRenderer) normalizeText(text string) string { + lText := strings.ToLower(text) + indexes := markdownPattern.FindAllStringSubmatchIndex(lText, -1) + + // Interate through indexes in reverse order. + for i := len(indexes) - 1; i >= 0; i-- { + theIndex := indexes[i] + start, end := theIndex[0], theIndex[1] + contentStart, contentEnd := theIndex[2], theIndex[3] + + text = text[:start] + text[contentStart:contentEnd] + text[end:] + } + + return text } /* @@ -42,14 +60,21 @@ For all available combinations, colors, and attribute, see: `StringToAttribute`. This method returns a RenderedSequence */ -func (r MarkdownTextRenderer) RenderSequence(text string, lastColor, background Attribute) RenderedSequence { +func (r MarkdownTextRenderer) RenderSequence(start, end int, lastColor, background Attribute) RenderedSequence { + text := r.Text + if end == -1 { + end = len(r.NormalizedText()) + } + getMatch := func(s string) []int { return markdownPattern.FindStringSubmatchIndex(strings.ToLower(s)) } var sequences []ColorSubsequence for match := getMatch(text); match != nil; match = getMatch(text) { - start, end := match[0], match[1] + // Check if match is in the start/end range. + + matchStart, matchEnd := match[0], match[1] colorStart, colorEnd := match[4], match[5] contentStart, contentEnd := match[2], match[3] @@ -57,11 +82,30 @@ func (r MarkdownTextRenderer) RenderSequence(text string, lastColor, background content := text[contentStart:contentEnd] theSequence := ColorSubsequence{color, contentStart - 1, contentEnd - 1} - sequences = append(sequences, theSequence) - text = text[:start] + content + text[end:] + if start < theSequence.End && end > theSequence.Start { + // Make the sequence relative and append. + theSequence.Start -= start + if theSequence.Start < 0 { + theSequence.Start = 0 + } + + theSequence.End -= start + if theSequence.End < 0 { + theSequence.End = 0 + } else if theSequence.End > end-start { + theSequence.End = end - start + } + + sequences = append(sequences, theSequence) + } + + text = text[:matchStart] + content + text[matchEnd:] } - return RenderedSequence{text, lastColor, background, sequences} + if end == -1 { + end = len(text) + } + return RenderedSequence{text[start:end], lastColor, background, sequences} } // RenderedSequence is a string sequence that is capable of returning the @@ -80,8 +124,35 @@ type ColorSubsequence struct { End int } +// ColorSubsequencesToMap creates a map with all colors that from the +// subsequences. +func ColorSubsequencesToMap(sequences []ColorSubsequence) map[int]Attribute { + result := make(map[int]Attribute) + for _, theSequence := range sequences { + for i := theSequence.Start; i < theSequence.End; i++ { + result[i] = theSequence.Color + } + } + + return result +} + // Buffer returns the colorful formatted buffer and the last color that was // used. func (s *RenderedSequence) Buffer(x, y int) ([]Point, Attribute) { - return nil, s.LastColor + buffer := make([]Point, 0, len(s.NormalizedText)) // This is just an assumtion + + colors := ColorSubsequencesToMap(s.Sequences) + for i, r := range []rune(s.NormalizedText) { + color, ok := colors[i] + if !ok { + color = s.LastColor + } + + p := Point{r, s.BackgroundColor, color, x, y} + buffer = append(buffer, p) + x += charWidth(r) + } + + return buffer, s.LastColor } diff --git a/textRender_test.go b/textRender_test.go index 3eb75f0..219363b 100644 --- a/textRender_test.go +++ b/textRender_test.go @@ -1,36 +1,41 @@ package termui import ( + "fmt" "testing" + "github.com/davecgh/go-spew/spew" "github.com/stretchr/testify/assert" ) -func getMDRenderer() MarkdownTextRenderer { - return MarkdownTextRenderer{} -} +func TestMarkdownTextRenderer_normalizeText(t *testing.T) { + renderer := MarkdownTextRenderer{} -func TestMarkdownTextRenderer_NormalizedText(t *testing.T) { - renderer := getMDRenderer() - - got := renderer.NormalizedText("[ERROR](red,bold) Something went wrong") + got := renderer.normalizeText("[ERROR](red,bold) Something went wrong") assert.Equal(t, got, "ERROR Something went wrong") - got = renderer.NormalizedText("[foo](red) hello [bar](green) world") + got = renderer.normalizeText("[foo](red) hello [bar](green) world") assert.Equal(t, got, "foo hello bar world") - got = renderer.NormalizedText("[foo](g) hello [bar]green (world)") + got = renderer.normalizeText("[foo](g) hello [bar]green (world)") assert.Equal(t, got, "foo hello [bar]green (world)") // FIXME: [[ERROR]](red,bold) test should normalize to: // [ERROR] test + // FIXME: Support unicode inside the error message. } -func assertRenderSequence(t *testing.T, sequence RenderedSequence, last, background Attribute, text string, lenSequences int) { - assert.Equal(t, last, sequence.LastColor) - assert.Equal(t, background, sequence.BackgroundColor) - assert.Equal(t, text, sequence.NormalizedText) - assert.Equal(t, lenSequences, len(sequence.Sequences)) +func TestMarkdownTextRenderer_NormalizedText(t *testing.T) { + renderer := MarkdownTextRenderer{"[ERROR](red,bold) Something went wrong"} + assert.Equal(t, renderer.NormalizedText(), "ERROR Something went wrong") +} + +func assertRenderSequence(t *testing.T, sequence RenderedSequence, last, background Attribute, text string, lenSequences int) bool { + msg := fmt.Sprintf("seq: %v", spew.Sdump(sequence)) + assert.Equal(t, last, sequence.LastColor, msg) + assert.Equal(t, background, sequence.BackgroundColor, msg) + assert.Equal(t, text, sequence.NormalizedText, msg) + return assert.Equal(t, lenSequences, len(sequence.Sequences), msg) } func assertColorSubsequence(t *testing.T, s ColorSubsequence, color string, start, end int) { @@ -38,15 +43,102 @@ func assertColorSubsequence(t *testing.T, s ColorSubsequence, color string, star } func TestMarkdownTextRenderer_RenderSequence(t *testing.T) { - renderer := getMDRenderer() + // Simple test. + renderer := MarkdownTextRenderer{"[ERROR](red,bold) something went wrong"} + got := renderer.RenderSequence(0, -1, 3, 5) + if assertRenderSequence(t, got, 3, 5, "ERROR something went wrong", 1) { + assertColorSubsequence(t, got.Sequences[0], "RED,BOLD", 0, 5) + } - got := renderer.RenderSequence("[ERROR](red,bold) something went wrong", 3, 5) - assertRenderSequence(t, got, 3, 5, "ERROR something went wrong", 1) - assertColorSubsequence(t, got.Sequences[0], "RED,BOLD", 0, 5) + got = renderer.RenderSequence(3, 8, 3, 5) + if assertRenderSequence(t, got, 3, 5, "OR so", 1) { + assertColorSubsequence(t, got.Sequences[0], "RED,BOLD", 0, 2) + } - got = renderer.RenderSequence("[foo](red) hello [bar](green) world", 7, 2) - assertRenderSequence(t, got, 3, 2, "foo hello bar world", 2) + // Test for mutiple colors. + renderer = MarkdownTextRenderer{"[foo](red) hello [bar](blue) world"} + got = renderer.RenderSequence(0, -1, 7, 2) + if assertRenderSequence(t, got, 7, 2, "foo hello bar world", 2) { + assertColorSubsequence(t, got.Sequences[0], "RED", 0, 3) + assertColorSubsequence(t, got.Sequences[1], "BLUE", 10, 13) + } - assertColorSubsequence(t, got.Sequences[0], "RED", 0, 3) - assertColorSubsequence(t, got.Sequences[1], "GREEN", 10, 13) + // Test that out-of-bound color sequences are not added. + got = renderer.RenderSequence(4, 6, 8, 1) + assertRenderSequence(t, got, 8, 1, "he", 0) + + // Test Half-rendered text + got = renderer.RenderSequence(1, 12, 0, 0) + if assertRenderSequence(t, got, 0, 0, "oo hello ba", 2) { + assertColorSubsequence(t, got.Sequences[0], "RED", 0, 2) + assertColorSubsequence(t, got.Sequences[1], "BLUE", 9, 11) + } + + // Test Half-rendered text (edges) + got = renderer.RenderSequence(2, 11, 0, 0) + if assertRenderSequence(t, got, 0, 0, "o hello b", 2) { + assertColorSubsequence(t, got.Sequences[0], "RED", 0, 1) + assertColorSubsequence(t, got.Sequences[1], "BLUE", 8, 9) + } + + // Test half-rendered text (unicode) + // FIXME: Add + + // Test inside + renderer = MarkdownTextRenderer{"foo [foobar](red) bar"} + got = renderer.RenderSequence(4, 10, 0, 0) + if assertRenderSequence(t, got, 0, 0, "foobar", 1) { + assertColorSubsequence(t, got.Sequences[0], "RED", 0, 6) + } +} + +func TestColorSubsequencesToMap(t *testing.T) { + colorSubsequences := []ColorSubsequence{ + {ColorRed, 1, 4}, + {ColorBlue | AttrBold, 9, 10}, + } + + expected := make(map[int]Attribute) + expected[1] = ColorRed + expected[2] = ColorRed + expected[3] = ColorRed + expected[9] = ColorBlue | AttrBold + + assert.Equal(t, expected, ColorSubsequencesToMap(colorSubsequences)) +} + +func TestRenderedSequence_Buffer(t *testing.T) { + cs := []ColorSubsequence{ + {ColorRed, 3, 5}, + {ColorBlue | AttrBold, 9, 10}, + } + sequence := RenderedSequence{"Hello world", ColorWhite, ColorBlack, cs} + newPoint := func(char string, x, y int, colorA ...Attribute) Point { + var color Attribute + if colorA != nil && len(colorA) == 1 { + color = colorA[0] + } else { + color = ColorWhite + } + + return Point{[]rune(char)[0], ColorBlack, color, x, y} + } + + expected := []Point{ + newPoint("H", 5, 7), + newPoint("e", 6, 7), + newPoint("l", 7, 7), + newPoint("l", 7, 7, ColorRed), + newPoint("o", 8, 7, ColorRed), + newPoint(" ", 9, 7), + newPoint("w", 10, 7), + newPoint("o", 11, 7), + newPoint("r", 12, 7), + newPoint("l", 13, 7, ColorBlue|AttrBold), + newPoint("d", 14, 7), + } + buffer, lastColor := sequence.Buffer(5, 7) + + assert.Equal(t, expected[:3], buffer[:3]) + assert.Equal(t, ColorWhite, lastColor) }