summaryrefslogtreecommitdiffstats
path: root/markup.go
blob: 69809a0b27d60c158ea6a33f1f284e91f734d44f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
// Copyright (c) 2013-2023 by Michael Dvorkin and contributors. All Rights Reserved.
// Use of this source code is governed by a MIT-style license that can
// be found in the LICENSE file.

package mop

import (
	"regexp"
	"strings"

	"github.com/nsf/termbox-go"
)

// Markup implements some minimalistic text formatting conventions that
// get translated to Termbox colors and attributes. To colorize a string
// wrap it in <color-name>...</> tags. Unlike HTML each tag sets a new
// color whereas the </> tag changes color back to default. For example:
//
// <green>Hello, <red>world!</>
//
// The color tags could be combined with the attributes: <b>...</b> for
// bold, <u>...</u> for underline, and <r>...</r> for reverse. Unlike
// colors the attributes require matching closing tag.
//
// The <right>...</right> tag is used to right align the enclosed string
// (ex. when displaying current time in the upper right corner).
type Markup struct {
	Foreground   termbox.Attribute            // Foreground color.
	Background   termbox.Attribute            // Background color (so far always termbox.ColorDefault).
	RightAligned bool                         // True when the string is right aligned.
	tags         map[string]termbox.Attribute // Tags to Termbox translation hash.
	regex        *regexp.Regexp               // Regex to identify the supported tag names.
}

// Creates markup to define tag to Termbox translation rules and store default
// colors and column alignments.
func NewMarkup(profile *Profile) *Markup {
	markup := &Markup{}

	markup.tags = make(map[string]termbox.Attribute)
	markup.tags[`/`] = termbox.ColorDefault
	markup.tags[`black`] = termbox.ColorBlack
	markup.tags[`red`] = termbox.ColorRed
	markup.tags[`green`] = termbox.ColorGreen
	markup.tags[`yellow`] = termbox.ColorYellow
	markup.tags[`blue`] = termbox.ColorBlue
	markup.tags[`magenta`] = termbox.ColorMagenta
	markup.tags[`cyan`] = termbox.ColorCyan
	markup.tags[`white`] = termbox.ColorWhite
	markup.tags[`darkgray`] = termbox.ColorDarkGray
	markup.tags[`lightred`] = termbox.ColorLightRed
	markup.tags[`lightgreen`] = termbox.ColorLightGreen
	markup.tags[`lightyellow`] = termbox.ColorLightYellow
	markup.tags[`lightblue`] = termbox.ColorLightBlue
	markup.tags[`lightmagenta`] = termbox.ColorLightMagenta
	markup.tags[`lightcyan`] = termbox.ColorLightCyan
	markup.tags[`lightgray`] = termbox.ColorLightGray

	markup.tags[`right`] = termbox.ColorDefault // Termbox can combine attributes and a single color using bitwise OR.
	markup.tags[`b`] = termbox.AttrBold         // Attribute = 1 << (iota + 4)
	markup.tags[`u`] = termbox.AttrUnderline
	markup.tags[`r`] = termbox.AttrReverse

	// Semantic markups
	markup.tags[`gain`] = markup.tags[profile.Colors.Gain]
	markup.tags[`loss`] = markup.tags[profile.Colors.Loss]
	markup.tags[`tag`] = markup.tags[profile.Colors.Tag]
	markup.tags[`header`] = markup.tags[profile.Colors.Header]
	markup.tags[`time`] = markup.tags[profile.Colors.Time]
	markup.tags[`default`] = markup.tags[profile.Colors.Default]

	markup.Foreground = markup.tags[profile.Colors.Default]

	markup.Background = termbox.ColorDefault
	markup.RightAligned = false

	markup.regex = markup.supportedTags() // Once we have the hash we could build the regex.

	return markup
}

// Tokenize works just like strings.Split() except the resulting array includes
// the delimiters. For example, the "<green>Hello, <red>world!</>" string when
// tokenized by tags produces the following:
//
//   [0] "<green>"
//   [1] "Hello, "
//   [2] "<red>"
//   [3] "world!"
//   [4] "</>"
//
func (markup *Markup) Tokenize(str string) []string {
	matches := markup.regex.FindAllStringIndex(str, -1)
	strings := make([]string, 0, len(matches))

	head, tail := 0, 0
	for _, match := range matches {
		tail = match[0]
		if match[1] != 0 {
			if head != 0 || tail != 0 {
				// Append the text between tags.
				strings = append(strings, str[head:tail])
			}
			// Append the tag itmarkup.
			strings = append(strings, str[match[0]:match[1]])
		}
		head = match[1]
	}

	if head != len(str) && tail != len(str) {
		strings = append(strings, str[head:])
	}

	return strings
}

// IsTag returns true when the given string looks like markup tag. When the
// tag name matches one of the markup-supported tags it gets translated to
// relevant Termbox attributes and colors.
func (markup *Markup) IsTag(str string) bool {
	tag, open := probeForTag(str)

	if tag == `` {
		return false
	}

	return markup.process(tag, open)
}

//-----------------------------------------------------------------------------
func (markup *Markup) process(tag string, open bool) bool {
	if attribute, ok := markup.tags[tag]; ok {
		switch tag {
		case `right`:
			markup.RightAligned = open // On for <right>, off for </right>.
		default:
			if open {
				if attribute >= termbox.AttrBold {
					markup.Foreground |= attribute // Set the Termbox attribute.
				} else {
					markup.Foreground = attribute // Set the Termbox color.
				}
			} else {
				if attribute >= termbox.AttrBold {
					markup.Foreground &= ^attribute // Clear the Termbox attribute.
				} else {
					markup.Foreground = markup.tags[`default`]
				}
			}
		}
	}

	return true
}

// supportedTags returns regular expression that matches all possible tags
// supported by the markup, i.e. </?black>|</?red>| ... |<?b>| ... |</?right>
func (markup *Markup) supportedTags() *regexp.Regexp {
	arr := []string{}

	for tag := range markup.tags {
		arr = append(arr, `</?`+tag+`>`)
	}

	return regexp.MustCompile(strings.Join(arr, `|`))
}

//-----------------------------------------------------------------------------
func probeForTag(str string) (string, bool) {
	if len(str) > 2 && str[0:1] == `<` && str[len(str)-1:] == `>` {
		return extractTagName(str), str[1:2] != `/`
	}

	return ``, false
}

// Extract tag name from the given tag, i.e. `<hello>` => `hello`.
func extractTagName(str string) string {
	if len(str) < 3 {
		return ``
	} else if str[1:2] != `/` {
		return str[1 : len(str)-1]
	} else if len(str) > 3 {
		return str[2 : len(str)-1]
	}

	return `/`
}