|  | 
|  | 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, '&' => "&", '<' => "<", '>' => ">") | 
|  | 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 | 
0 commit comments