/
style.jl
227 lines (197 loc) · 6.73 KB
/
style.jl
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
module Style
import Parameters: @with_kw
import Term:
unspace_commas,
NAMED_MODES,
has_markup,
OPEN_TAG_REGEX,
replace_text,
CODES,
ANSICode,
tview,
do_by_line,
ANSI_REGEX
import ..Colors:
AbstractColor, NamedColor, is_color, is_background, get_color, is_hex_color, hex2rgb
export apply_style
apply_style(text::String, style::String) =
if occursin('\n', text)
do_by_line(ln -> apply_style(ln, style), text)
else
apply_style("{" * style * "}" * text * "{/" * style * "}")
end
"""
Check if a string is a mode name
"""
is_mode(string) = string ∈ NAMED_MODES
# ---------------------------------------------------------------------------- #
# MarkupStyle #
# ---------------------------------------------------------------------------- #
"""
MarkupStyle
Holds information about the style specification set out by a `MarkupTag`.
"""
@with_kw mutable struct MarkupStyle
default::Bool = false
bold::Bool = false
dim::Bool = false
italic::Bool = false
underline::Bool = false
blink::Bool = false
inverse::Bool = false
hidden::Bool = false
striked::Bool = false
color::Union{Nothing,AbstractColor} = nothing
background::Union{Nothing,AbstractColor} = nothing
end
"""
MarkupStyle(tag::MarkupTag)
Builds a MarkupStyle definition from a MarkupTag.
"""
function MarkupStyle(markup)
style = MarkupStyle()
for code in split(unspace_commas(markup))
if is_mode(code)
setproperty!(style, Symbol(code), true)
elseif is_color(code)
style.color = get_color(code)
elseif is_background(code)
style.background = get_color(code; bg = true)
# elseif code != "nothing"
# @debug "Code type not recognized: $code"
end
end
return style
end
# -------------------------------- apply style ------------------------------- #
"""
get_style_codes(style::MarkupStyle)
Get `ANSICode`s corresponding to a `MarkupStyle`.
"""
function get_style_codes(style::MarkupStyle)
# start applying styles
style_init, style_finish = "", ""
for attr in fieldnames(MarkupStyle)
value = getfield(style, attr)
if attr ≡ :background
code = isnothing(value) ? nothing : ANSICode(value; bg = true)
elseif attr ≡ :color
if !isnothing(value)
try
code = ANSICode(value; bg = false)
catch
continue
end
else
code = nothing
end
elseif attr != :tag && value == true # MODES
code = CODES[attr]
else
# if value != false && attr != :tag
# @debug "Attr/value not recognized or not set" attr value
# end
continue
end
if !isnothing(code)
style_init *= code.open
style_finish *= code.close
style_finish *= (occursin(code.close, style_finish) ? "" : code.close)
end
end
return style_init, style_finish
end
"""
apply_style(text)
Apply style to a piece of text.
Extract markup style information and insert the
appropriate ANSI codes to style a string.
When multiple, nested color tags are present, like in"
"{red} abcd {green} asd {/green} eadsa {/red}"
extra care should be put to ensure that when `green` is closed
the text is rendered as red. To this end, this function
keeps track of the last color style information and where it occurred in the input
text. If the current markup tag is nested in the previous, it changes, for example
"{/green}"
to
"{/green}{red}".
The same in parallel has to be done for background colors.
By default, "orphaned" tags (i.e. open/close markup tags without the corresponding
tag) are removed from the string. Use `leave_orphan_tags` to change this behavior.
"""
function apply_style(text; leave_orphan_tags = false)::String
has_markup(text) || return text
previous_color = (0, length(text), MarkupStyle("default"))
previous_background = (0, length(text), MarkupStyle("default"))
while has_markup(text)
# get opening markup tag
open_match = match(OPEN_TAG_REGEX, text)
markup = open_match.match[2:(end - 1)]
# get style codes
ms = MarkupStyle(markup)
ansi_open, ansi_close = get_style_codes(ms)
# insert open tag
if ansi_open == "" && ansi_close == "" && leave_orphan_tags
# found an invalid tag (e.g. {string}). Leave it but edit it to avoid getting stuck in this lookup
# replace markup with ANSI codes
text = replace_text(
text,
max(open_match.offset - 1, 0),
open_match.offset + length(markup) + 1,
"{{" * markup * "}}",
)
else
# replace markup with ANSI codes
text = replace_text(
text,
max(open_match.offset - 1, 0),
open_match.offset + length(markup) + 1,
ansi_open,
)
end
# get closing tag (including [/] or missing close)
close_rx = r"(?<!\{)\{(?!\{)\/" * markup * r"\}"
if !occursin(close_rx, text)
text = text * "{/" * markup * "}"
end
close_match = match(close_rx, text)
# if previous style had color and we're nested, use color info
if open_match.offset > previous_color[1] &&
close_match.offset < previous_color[2] &&
!isnothing(previous_color[3].color)
col_prev_ansi_open, _ = get_style_codes(previous_color[3])
ansi_close = ansi_close * col_prev_ansi_open
end
# and for background
if open_match.offset > previous_background[1] &&
close_match.offset < previous_background[2] &&
!isnothing(previous_background[3].background)
bg_prev_ansi_open, _ = get_style_codes(previous_background[3])
ansi_close = ansi_close * bg_prev_ansi_open
end
# replace close tag
text = replace_text(
text,
close_match.offset - 1,
close_match.offset + length(markup) + 2,
ansi_close,
)
# store style info
isnothing(ms.color) || (
previous_color = (
max(open_match.offset - 1, 0),
close_match.offset + length(markup) + 2,
ms,
)
)
isnothing(ms.background) || (
previous_background = (
max(open_match.offset - 1, 0),
close_match.offset + length(markup) + 2,
ms,
)
)
end
return text
end
end