Skip to content

Commit 5615845

Browse files
committed
Implement "display protocol" for AnnotatedString
This is required for downstream libraries (esp. StyledStrings) to be able to influence the display of an AnnotatedString without type-piracy. The idea here is to provide an `iterate`-like display protocol that passes style information into the implementing library, which takes that information (and a "state" from the last display update), and decides how it needs to update the output to render the string. Since AnnotatedStrings don't have a distinguished "owner" or "type" (only their annotations have types), this leads to corner cases where the "laststate" you receive might be from another library. In that case, it's the responsibility of the downstream renderer to discard the laststate information and "reset" the display state as needed.
1 parent f211a77 commit 5615845

File tree

2 files changed

+186
-0
lines changed

2 files changed

+186
-0
lines changed

base/strings/annotated_render.jl

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
module AnnotatedDisplay
2+
3+
using ..Base: IO, SubString, IOBuffer, AnnotatedString, AnnotatedChar, AnnotatedIOBuffer
4+
using ..Base: eachregion, invoke_in_world, tls_world_age
5+
6+
import ..Base: write, print, show, escape_string # implemented methods
7+
8+
# This is the "display interface" for printing an AnnotatedString
9+
termreset(io::IO, ::Nothing) = nothing # no-op
10+
termstyle(io::IO, ::Nothing, laststyle::Any) = termreset(io, laststyle)
11+
# termstyle(io::IO, face::Symbol, laststyle::Any) = error("Unresolved `:face` annotation encountered: $face")
12+
13+
htmlreset(io::IO, ::Nothing) = nothing # no-op
14+
htmlstyle(io::IO, ::Nothing, laststyle::Any) = htmlreset(io, laststyle)
15+
# htmlstyle(io::IO, face::Symbol, laststyle::Any) = error("Unresolved `:face` annotation encountered: $face")
16+
17+
mergestyle(::Nothing, @nospecialize(style::Any)) = (style, true)
18+
# mergestyle(style::Symbol, @nospecialize(::Any)) = (style, false)
19+
20+
# call mergestyle(...) w/ invalidation barrier
21+
mergestyle_(@nospecialize(merged::Any), @nospecialize(style::Any)) =
22+
invoke_in_world(tls_world_age(), mergestyle, merged, style)
23+
# call termreset(...) w/ invalidation barrier
24+
termreset_(io::IO, @nospecialize(laststyle::Any)) =
25+
invoke_in_world(tls_world_age(), termreset, io, laststyle)
26+
# call termstyle(...) w/ invalidation barrier
27+
termstyle_(io::IO, @nospecialize(style::Any), @nospecialize(laststyle::Any)) =
28+
invoke_in_world(tls_world_age(), termstyle, io, style, laststyle)
29+
# call htmlreset(...) w/ invalidation barrier
30+
htmlreset_(io::IO, @nospecialize(laststyle::Any)) =
31+
invoke_in_world(tls_world_age(), htmlreset, io, laststyle)
32+
# call htmlstyle(...) w/ invalidation barrier
33+
htmlstyle_(io::IO, @nospecialize(style::Any), @nospecialize(laststyle::Any)) =
34+
invoke_in_world(tls_world_age(), htmlstyle, io, style, laststyle)
35+
36+
function _ansi_writer(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}},
37+
string_writer::F) where {F <: Function}
38+
# We need to make sure that the customisations are loaded
39+
# before we start outputting any styled content.
40+
if get(io, :color, false)::Bool
41+
buf = IOBuffer() # Avoid the overhead in repeatedly printing to `stdout`
42+
active_style::Any = nothing # Represents the currently-applied style
43+
for (str, styles) in eachregion(s)
44+
link = nothing
45+
merged_style = nothing
46+
for (key, style) in reverse(styles)
47+
if key === :link
48+
link = style::String
49+
end
50+
key !== :face && continue
51+
# Merge as many of these as we can into a single style before
52+
# we print to the terminal
53+
(merged_style, successful) = mergestyle_(merged_style, style)
54+
if !successful
55+
active_style = termstyle_(buf, merged_style, active_style)
56+
merged_style = style
57+
end
58+
end
59+
60+
active_style = termstyle_(buf, merged_style, active_style)
61+
!isnothing(link) && write(buf, "\e]8;;", link, "\e\\")
62+
string_writer(buf, str)
63+
!isnothing(link) && write(buf, "\e]8;;\e\\")
64+
end
65+
# Reset the terminal state (whoever last wrote has the responsibility)
66+
termreset_(buf, active_style)
67+
write(io, take!(buf))
68+
elseif s isa AnnotatedString
69+
string_writer(io, s.string)
70+
elseif s isa SubString
71+
string_writer(
72+
io, SubString(s.string.string, s.offset, s.ncodeunits, Val(:noshift)))
73+
end
74+
end
75+
76+
write(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}) =
77+
_ansi_writer(io, s, write)::Int
78+
79+
print(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}) =
80+
(_ansi_writer(io, s, print); nothing)
81+
82+
# We need to make sure that printing to an `AnnotatedIOBuffer` calls `write` not `print`
83+
# so we get the specialised handling that `_ansi_writer` doesn't provide.
84+
print(io::AnnotatedIOBuffer, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}) =
85+
(write(io, s); nothing)
86+
87+
escape_string(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}},
88+
esc = ""; keep = ()) =
89+
(_ansi_writer(io, s, (io, s) -> escape_string(io, s, esc; keep)); nothing)
90+
91+
function write(io::IO, c::AnnotatedChar)
92+
if get(io, :color, false) == true
93+
active_style::Any = nothing # Represents the currently-applied style
94+
95+
# TODO: re-factor into separate (shared) function
96+
link = nothing
97+
merged_style = nothing
98+
for (key, style) in reverse(c.annotations)
99+
if key === :link
100+
link = style::String
101+
end
102+
key !== :face && continue
103+
# Merge as many of these as we can into a single style before
104+
# we print to the terminal
105+
(merged_style, successful) = mergestyle_(merged_style, style)
106+
if !successful
107+
active_style = termstyle_(buf, merged_style, active_style)
108+
merged_style = style
109+
end
110+
end
111+
112+
active_style = termstyle_(io, merged_style, active_style)
113+
bytes = write(io, c.char)
114+
termreset_(io, active_style)
115+
bytes
116+
else
117+
write(io, c.char)
118+
end
119+
end
120+
121+
print(io::IO, c::AnnotatedChar) = (write(io, c); nothing)
122+
123+
function show(io::IO, c::AnnotatedChar)
124+
if get(io, :color, false) == true
125+
out = IOBuffer()
126+
show(out, c.char)
127+
cstr = AnnotatedString(
128+
String(take!(out)[2:end-1]),
129+
[(1:ncodeunits(c), a...) for a in c.annotations])
130+
print(io, ''', cstr, ''')
131+
else
132+
show(io, c.char)
133+
end
134+
end
135+
136+
function write(io::IO, aio::AnnotatedIOBuffer)
137+
if get(io, :color, false) == true
138+
# This does introduce an overhead that technically
139+
# could be avoided, but I'm not sure that it's currently
140+
# worth the effort to implement an efficient version of
141+
# writing from a AnnotatedIOBuffer with style.
142+
# In the meantime, by converting to an `AnnotatedString` we can just
143+
# reuse all the work done to make that work.
144+
write(io, read(aio, AnnotatedString))
145+
else
146+
write(io, aio.io)
147+
end
148+
end
149+
150+
function show(io::IO, ::MIME"text/html", s::Union{<:AnnotatedString, SubString{<:AnnotatedString}})
151+
htmlescape(str) = replace(str, '&' => "&amp;", '<' => "&lt;", '>' => "&gt;")
152+
153+
buf = IOBuffer() # Avoid potential overhead in repeatedly printing a more complex IO
154+
active_style::Any = nothing # Represents the currently-applied style
155+
for (str, styles) in eachregion(s)
156+
link = nothing
157+
merged_style = nothing
158+
for (key, style) in reverse(styles)
159+
if key === :link
160+
link = style::String
161+
end
162+
key !== :face && continue
163+
164+
# Merge as many of these as we can into a single style before
165+
# we print to the terminal
166+
(merged_style, successful) = mergestyle_(merged_style, style)
167+
if !successful
168+
active_style = htmlstyle_(buf, merged_style, active_style)
169+
merged_style = style
170+
end
171+
end
172+
173+
active_style = htmlstyle_(buf, merged_style, active_style)
174+
!isnothing(link) && print(buf, "<a href=\"", link, "\">")
175+
print(buf, htmlescape(str))
176+
!isnothing(link) && print(buf, "</a>")
177+
end
178+
179+
# Reset the terminal state (whoever last wrote has the responsibility)
180+
htmlreset_(buf, active_style)
181+
write(io, take!(buf))
182+
nothing
183+
end
184+
185+
end

base/strings/strings.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ import .Iterators: PartitionIterator
1212
include("strings/util.jl")
1313
include("strings/io.jl")
1414
include("strings/annotated_io.jl")
15+
include("strings/annotated_render.jl")

0 commit comments

Comments
 (0)