Skip to content

Commit 9bb8ffd

Browse files
committed
Align the semantics and handling of default fg/bg
The intended semantics of the default face is that it represents the typesetting context that the AnnotatedString can expect to be placed in. As such, when the value of a Face's fg/bg is the same as the default value, we should take this to mean that the fg/bg can be applied by restoring the default style (using the appropriate ANSI codes or CSS declaration). To model this appropriately, we can't make do with setting the default foreground and background to ":default". Adding a :foreground and :background face won't quite do the trick either. Instead, we dedicate our basic color expectations in a new FACES attribute: basecolors. I expect to make use of this new design soon to help implement colour blending. For now, the HTML colour codes are put there, along with two special colors: foreground, and background. These are then used in the default face, and let us disambiguate usage.
1 parent 3c2f4f5 commit 9bb8ffd

File tree

4 files changed

+97
-74
lines changed

4 files changed

+97
-74
lines changed

docs/src/internals.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ opening a pull request or issue to discuss making them part of the public API.
88
```@docs
99
StyledStrings.ANSI_4BIT_COLORS
1010
StyledStrings.FACES
11-
StyledStrings.HTML_BASIC_COLORS
1211
StyledStrings.Legacy.ANSI_256_COLORS
1312
StyledStrings.Legacy.NAMED_COLORS
1413
StyledStrings.Legacy.RENAMED_COLORS
1514
StyledStrings.Legacy.legacy_color
1615
StyledStrings.Legacy.load_env_colors!
17-
StyledStrings.ansi_4bit_color_code
16+
StyledStrings.ansi_4bit
1817
StyledStrings.face!
1918
StyledStrings.getface
2019
StyledStrings.loadface!

src/faces.jl

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,8 @@ const FACES = let default = Dict{Symbol, Face}(
318318
:default => Face(
319319
"monospace", 120, # font, height
320320
:normal, :normal, # weight, slant
321-
SimpleColor(:default), # foreground
322-
SimpleColor(:default), # background
321+
SimpleColor(:foreground), # foreground
322+
SimpleColor(:background), # background
323323
false, false, false, # underline, strikethrough, overline
324324
Symbol[]), # inherit
325325
# Property faces
@@ -382,7 +382,30 @@ const FACES = let default = Dict{Symbol, Face}(
382382
:repl_prompt_pkg => Face(inherit=[:blue, :repl_prompt]),
383383
:repl_prompt_beep => Face(inherit=[:shadow, :repl_prompt]),
384384
)
385-
(; default, current=ScopedValue(copy(default)), lock=ReentrantLock())
385+
basecolors = Dict{Symbol, RGBTuple}(
386+
:background => (r = 0xff, g = 0xff, b = 0xff),
387+
:foreground => (r = 0x00, g = 0x00, b = 0x00),
388+
:black => (r = 0x1c, g = 0x1a, b = 0x23),
389+
:red => (r = 0xa5, g = 0x1c, b = 0x2c),
390+
:green => (r = 0x25, g = 0xa2, b = 0x68),
391+
:yellow => (r = 0xe5, g = 0xa5, b = 0x09),
392+
:blue => (r = 0x19, g = 0x5e, b = 0xb3),
393+
:magenta => (r = 0x80, g = 0x3d, b = 0x9b),
394+
:cyan => (r = 0x00, g = 0x97, b = 0xa7),
395+
:white => (r = 0xdd, g = 0xdc, b = 0xd9),
396+
:bright_black => (r = 0x76, g = 0x75, b = 0x7a),
397+
:grey => (r = 0x76, g = 0x75, b = 0x7a),
398+
:gray => (r = 0x76, g = 0x75, b = 0x7a),
399+
:bright_red => (r = 0xed, g = 0x33, b = 0x3b),
400+
:bright_green => (r = 0x33, g = 0xd0, b = 0x79),
401+
:bright_yellow => (r = 0xf6, g = 0xd2, b = 0x2c),
402+
:bright_blue => (r = 0x35, g = 0x83, b = 0xe4),
403+
:bright_magenta => (r = 0xbf, g = 0x60, b = 0xca),
404+
:bright_cyan => (r = 0x26, g = 0xc6, b = 0xda),
405+
:bright_white => (r = 0xf6, g = 0xf5, b = 0xf4))
406+
(; default, basecolors,
407+
current = ScopedValue(copy(default)),
408+
lock = ReentrantLock())
386409
end
387410

388411
## Adding and resetting faces ##

src/io.jl

Lines changed: 57 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,17 @@ const ANSI_4BIT_COLORS = Dict{Symbol, Int}(
3030
:bright_white => 15)
3131

3232
"""
33-
ansi_4bit_color_code(color::Symbol, background::Bool=false)
33+
ansi_4bit(color::Integer, background::Bool=false)
34+
35+
Provide the color code (30-37, 40-47, 90-97, 100-107) for `color` (0–15).
3436
35-
Provide the color code (30-37, 40-47, 90-97, 100-107) for `color`, as an integer.
3637
When `background` is set the background variant will be provided, otherwise
3738
the provided code is for setting the foreground color.
3839
"""
39-
function ansi_4bit_color_code(color::Symbol, background::Bool=false)
40-
code = get(ANSI_4BIT_COLORS, color, nothing)
41-
if code !== nothing
42-
code >= 8 && (code += 52)
43-
background && (code += 10)
44-
code + 30
45-
else
46-
ifelse(background, 49, 39)
47-
end
40+
function ansi_4bit(code::Integer, background::Bool=false)
41+
code >= 8 && (code += 52)
42+
background && (code += 10)
43+
code + 30
4844
end
4945

5046
"""
@@ -96,6 +92,8 @@ function termcolor24bit(io::IO, color::RGBTuple, category::Char)
9692
string(color.b), 'm')
9793
end
9894

95+
const MAX_COLOR_FORWARDS = 12
96+
9997
"""
10098
termcolor(io::IO, color::SimpleColor, category::Char)
10199
@@ -110,31 +108,45 @@ If `color` is a `SimpleColor{Symbol}`, the value should be a a member of
110108
111109
If `color` is a `SimpleColor{RGBTuple}` and `get_have_truecolor()` returns true,
112110
24-bit color is used. Otherwise, an 8-bit approximation of `color` is used.
111+
112+
If `color` is unknown, no output is produced.
113113
"""
114114
function termcolor(io::IO, color::SimpleColor, category::Char)
115+
if category == '4'
116+
if color.value (:background, FACES.basecolors[:background])
117+
return print(io, "\e[", category, "9m")
118+
elseif color.value == :foreground
119+
return print(io, "\e[47m") # Technically not quite, but close enough
120+
end
121+
elseif color.value (:foreground, FACES.basecolors[:foreground])
122+
return print(io, "\e[", category, "9m")
123+
elseif category == '3' && color.value == :background
124+
return print(io, "\e[30m") # Technically not quite, but close enough
125+
end
126+
for _ in 1:MAX_COLOR_FORWARDS
127+
color.value isa RGBTuple && break
128+
fg = get(FACES.current[], color.value, Face()).foreground
129+
isnothing(fg) && return
130+
color == fg && break
131+
color = fg
132+
end
115133
if color.value isa RGBTuple
116134
if Base.get_have_truecolor()
117135
termcolor24bit(io, color.value, category)
118136
else
119137
termcolor8bit(io, color.value, category)
120138
end
121-
elseif color.value === :default
122-
print(io, "\e[", category, "9m")
123-
elseif (fg = get(FACES.current[], color.value, getface()).foreground) != SimpleColor(color.value)
124-
termcolor(io, fg, category)
125-
else
126-
print(io, "\e[")
127-
if category == '3' || category == '4'
128-
print(io, ansi_4bit_color_code(color.value, category == '4'))
129-
elseif category == '5'
130-
if haskey(ANSI_4BIT_COLORS, color.value)
131-
print(io, "58;5;", ANSI_4BIT_COLORS[color.value])
132-
else
133-
print(io, "59")
134-
end
135-
end
136-
print(io, "m")
139+
return
140+
end
141+
ansi = get(ANSI_4BIT_COLORS, color.value, nothing)
142+
isnothing(ansi) && return
143+
print(io, "\e[")
144+
if category == '3' || category == '4'
145+
print(io, ansi_4bit(ansi, category == '4'))
146+
elseif category == '5'
147+
print(io, "58;5;", ansi)
137148
end
149+
print(io, 'm')
138150
end
139151

140152
"""
@@ -208,7 +220,7 @@ function termstyle(io::IO, face::Face, lastface::Face=getface())
208220
termcolor(io, face.underline, '5')
209221
else
210222
if lastface.underline isa SimpleColor || lastface.underline isa Tuple && first(lastface.underline) isa SimpleColor
211-
termcolor(io, SimpleColor(:none), '5')
223+
termcolor(io, SimpleColor(:foreground), '5')
212224
end
213225
print(io, ifelse(face.underline == true,
214226
ANSI_STYLE_CODES.start_underline,
@@ -233,9 +245,10 @@ function _ansi_writer(string_writer::F, io::IO, s::Union{<:AnnotatedString, SubS
233245
# We need to make sure that the customisations are loaded
234246
# before we start outputting any styled content.
235247
load_customisations!()
248+
default = FACES.default[:default]
236249
if get(io, :color, false)::Bool
237250
buf = IOBuffer() # Avoid the overhead in repeatedly printing to `stdout`
238-
lastface::Face = FACES.default[:default]
251+
lastface::Face = default
239252
for (str, styles) in eachregion(s)
240253
face = getface(styles)
241254
link = let idx=findfirst(==(:link) first, styles)
@@ -248,7 +261,7 @@ function _ansi_writer(string_writer::F, io::IO, s::Union{<:AnnotatedString, SubS
248261
!isnothing(link) && write(buf, "\e]8;;\e\\")
249262
lastface = face
250263
end
251-
termstyle(buf, FACES.default[:default], lastface)
264+
termstyle(buf, default, lastface)
252265
write(io, seekstart(buf))
253266
elseif s isa AnnotatedString
254267
string_writer(io, s.string)
@@ -296,39 +309,24 @@ Base.AnnotatedDisplay.show_annot(io::IO, ::MIME"text/html", s::Union{<:Annotated
296309
# End AnnotatedDisplay hooks
297310
# ------------
298311

299-
"""
300-
A mapping between ANSI named colors and 8-bit colors for use in HTML
301-
representations.
302-
"""
303-
const HTML_BASIC_COLORS = Dict{Symbol, SimpleColor}(
304-
:black => SimpleColor(0x1c, 0x1a, 0x23),
305-
:red => SimpleColor(0xa5, 0x1c, 0x2c),
306-
:green => SimpleColor(0x25, 0xa2, 0x68),
307-
:yellow => SimpleColor(0xe5, 0xa5, 0x09),
308-
:blue => SimpleColor(0x19, 0x5e, 0xb3),
309-
:magenta => SimpleColor(0x80, 0x3d, 0x9b),
310-
:cyan => SimpleColor(0x00, 0x97, 0xa7),
311-
:white => SimpleColor(0xdd, 0xdc, 0xd9),
312-
:bright_black => SimpleColor(0x76, 0x75, 0x7a),
313-
:grey => SimpleColor(0x76, 0x75, 0x7a),
314-
:gray => SimpleColor(0x76, 0x75, 0x7a),
315-
:bright_red => SimpleColor(0xed, 0x33, 0x3b),
316-
:bright_green => SimpleColor(0x33, 0xd0, 0x79),
317-
:bright_yellow => SimpleColor(0xf6, 0xd2, 0x2c),
318-
:bright_blue => SimpleColor(0x35, 0x83, 0xe4),
319-
:bright_magenta => SimpleColor(0xbf, 0x60, 0xca),
320-
:bright_cyan => SimpleColor(0x26, 0xc6, 0xda),
321-
:bright_white => SimpleColor(0xf6, 0xf5, 0xf4))
322-
323-
function htmlcolor(io::IO, color::SimpleColor)
312+
function htmlcolor(io::IO, color::SimpleColor, background::Bool = false)
313+
default = getface()
324314
if color.value isa Symbol
325-
if color.value === :default
315+
if background && color.value == :background
316+
print(io, "initial")
317+
elseif !background && color.value == :foreground
326318
print(io, "initial")
327-
elseif (fg = get(FACES.current[], color.value, getface()).foreground) != SimpleColor(color.value)
319+
elseif (fg = get(FACES.current[], color.value, default).foreground) != SimpleColor(color.value)
328320
htmlcolor(io, fg)
321+
elseif haskey(FACES.basecolors, color.value)
322+
htmlcolor(io, SimpleColor(FACES.basecolors[color.value]))
329323
else
330-
htmlcolor(io, get(HTML_BASIC_COLORS, color.value, SimpleColor(:default)))
324+
print(io, "inherit")
331325
end
326+
elseif background && color.value == default.background
327+
htmlcolor(io, SimpleColor(:background), true)
328+
elseif !background && color.value ==default.foreground
329+
htmlcolor(io, SimpleColor(:foreground))
332330
else
333331
(; r, g, b) = color.value
334332
print(io, '#')
@@ -387,7 +385,7 @@ function cssattrs(io::IO, face::Face, lastface::Face=getface(), escapequotes::Bo
387385
end
388386
if background != lastbackground
389387
printattr(io, "background-color")
390-
htmlcolor(io, background)
388+
htmlcolor(io, background, true)
391389
end
392390
face.underline == lastface.underline ||
393391
if face.underline isa Tuple # Color and style

test/runtests.jl

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,8 @@ end
235235
height: 120
236236
weight: normal
237237
slant: normal
238-
foreground: default
239-
background: default
238+
foreground: foreground
239+
background: background
240240
underline: false
241241
strikethrough: false
242242
inverse: false\
@@ -248,8 +248,8 @@ end
248248
height: 120
249249
weight: normal
250250
slant: normal
251-
foreground: ■ default
252-
background: ■ default
251+
foreground: ■ foreground
252+
background: \e[30m■\e[39m background
253253
underline: false
254254
strikethrough: false
255255
inverse: false\
@@ -475,11 +475,14 @@ end
475475

476476
@testset "ANSI encoding" begin
477477
# 4-bit color
478-
@test StyledStrings.ansi_4bit_color_code(:cyan, false) == 36
479-
@test StyledStrings.ansi_4bit_color_code(:cyan, true) == 46
480-
@test StyledStrings.ansi_4bit_color_code(:bright_cyan, false) == 96
481-
@test StyledStrings.ansi_4bit_color_code(:bright_cyan, true) == 106
482-
@test StyledStrings.ansi_4bit_color_code(:nonexistant) == 39
478+
@test StyledStrings.ansi_4bit(
479+
StyledStrings.ANSI_4BIT_COLORS[:cyan], false) == 36
480+
@test StyledStrings.ansi_4bit(
481+
StyledStrings.ANSI_4BIT_COLORS[:cyan], true) == 46
482+
@test StyledStrings.ansi_4bit(
483+
StyledStrings.ANSI_4BIT_COLORS[:bright_cyan], false) == 96
484+
@test StyledStrings.ansi_4bit(
485+
StyledStrings.ANSI_4BIT_COLORS[:bright_cyan], true) == 106
483486
# 8-bit color
484487
@test sprint(StyledStrings.termcolor8bit, (r=0x40, g=0x63, b=0xd8), '3') == "\e[38;5;26m"
485488
@test sprint(StyledStrings.termcolor8bit, (r=0x38, g=0x98, b=0x26), '3') == "\e[38;5;28m"
@@ -606,7 +609,7 @@ end
606609
<span style=\"color: #803d9b\">`</span><span style=\"color: #25a268\">AnnotatedString</span><span style=\"color: #803d9b\">`</span> \
607610
<a href=\"https://en.wikipedia.org/wiki/Type_system\">type</a> to provide a <span style=\"text-decoration: #a51c2c wavy underline\">\
608611
full-fledged</span> textual <span style=\"font-weight: 700; color: #adbdf8; background-color: #4063d8; text-decoration: line-through\">\
609-
styling</span> system, suitable for <span style=\"\">terminal</span> and graphical displays."
612+
styling</span> system, suitable for <span style=\"color: initial; background-color: #000000\">terminal</span> and graphical displays."
610613
end
611614

612615
@testset "Legacy" begin

0 commit comments

Comments
 (0)