From 6670401079a0117565a621910f09a5022eaccb0e Mon Sep 17 00:00:00 2001 From: TEC Date: Sat, 28 Oct 2023 15:03:05 +0800 Subject: [PATCH] Make compatible with Julia 1.0 --- .github/workflows/CI.yml | 15 +- Project.toml | 9 +- src/StyledStrings.jl | 13 +- src/compat.jl | 133 ++++++++++ src/faces.jl | 85 ++++-- src/io.jl | 33 ++- src/strings/annotated.jl | 436 +++++++++++++++++++++++++++++++ src/strings/basic.jl | 5 + src/strings/io.jl | 49 ++++ src/strings/regex.jl | 45 ++++ src/strings/strings.jl | 17 ++ src/strings/util.jl | 29 +++ src/stylemacro.jl | 25 +- src/terminfo.jl | 296 +++++++++++++++++++++ src/terminfo_data.jl | 540 +++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 57 +++-- 16 files changed, 1698 insertions(+), 89 deletions(-) create mode 100644 src/compat.jl create mode 100644 src/strings/annotated.jl create mode 100644 src/strings/basic.jl create mode 100644 src/strings/io.jl create mode 100644 src/strings/regex.jl create mode 100644 src/strings/strings.jl create mode 100644 src/strings/util.jl create mode 100644 src/terminfo.jl create mode 100644 src/terminfo_data.jl diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c520dd6d..421a40f8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -3,6 +3,7 @@ on: push: branches: - main + - julia1-compat tags: ['*'] pull_request: concurrency: @@ -18,9 +19,17 @@ jobs: fail-fast: false matrix: version: - # - '1.0' - # - '1.6' - # - '1' + - '1.0' + - '1.1' + - '1.2' + - '1.3' + - '1.4' + - '1.5' + - '1.6' + - '1.7' + - '1.8' + - '1.9' + - '1.10' - 'nightly' os: - ubuntu-latest diff --git a/Project.toml b/Project.toml index 56789cb3..a8b77a91 100644 --- a/Project.toml +++ b/Project.toml @@ -1,10 +1,15 @@ name = "StyledStrings" uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" authors = ["TEC "] -version = "1.11.0" +version = "1.0.0" + +[deps] +Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" [compat] -julia = "1.11" +TOML = "1" +julia = "1" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/src/StyledStrings.jl b/src/StyledStrings.jl index f198aff4..7914c590 100644 --- a/src/StyledStrings.jl +++ b/src/StyledStrings.jl @@ -2,11 +2,16 @@ module StyledStrings -import Base: AnnotatedString, AnnotatedChar, annotations, annotate!, - annotatedstring, convert, merge, show, print, write +import Base: convert, merge, show, print, write export @styled_str -public Face, addface!, SimpleColor + +include("compat.jl") +include("terminfo.jl") +include("strings/strings.jl") + +import .AnnotatedStrings: AnnotatedString, AnnotatedChar, annotations, annotate!, + annotatedstring, annotatedstring_optimize! include("faces.jl") include("regioniterator.jl") @@ -20,7 +25,7 @@ function __init__() Legacy.load_env_colors!() end -if Base.generating_output() +if generating_output() include("precompile.jl") end diff --git a/src/compat.jl b/src/compat.jl new file mode 100644 index 00000000..2f8ffe47 --- /dev/null +++ b/src/compat.jl @@ -0,0 +1,133 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +@static if VERSION < v"1.1" + isnothing(x) = x === nothing +end + +@static if VERSION < v"1.2" + ncodeunits(s::AbstractString) = Base.ncodeunits(s) + ncodeunits(c::AbstractChar) = ncodeunits(string(c)) +else + const ncodeunits = Base.ncodeunits +end + +@static if VERSION < v"1.3" + function _str_sizehint(x) + if x isa Float64 + return 20 + elseif x isa Float32 + return 12 + elseif x isa String || x isa SubString{String} + return sizeof(x) + elseif x isa Char + return ncodeunits(x) + else + return 8 + end + end +else + import Base._str_sizehint +end + +@static if VERSION < v"1.3" + function unescape_string(io::IO, s::AbstractString, keep = ()) + a = Iterators.Stateful(s) + for c in a + if !isempty(a) && c == '\\' + c = popfirst!(a) + if c in keep + print(io, '\\', c) + elseif c == 'x' || c == 'u' || c == 'U' + n = k = 0 + m = c == 'x' ? 2 : + c == 'u' ? 4 : 8 + while (k += 1) <= m && !isempty(a) + nc = peek(a) + n = '0' <= nc <= '9' ? n<<4 + (nc-'0') : + 'a' <= nc <= 'f' ? n<<4 + (nc-'a'+10) : + 'A' <= nc <= 'F' ? n<<4 + (nc-'A'+10) : break + popfirst!(a) + end + if k == 1 || n > 0x10ffff + u = m == 4 ? 'u' : 'U' + throw(ArgumentError("invalid $(m == 2 ? "hex (\\x)" : + "unicode (\\$u)") escape sequence")) + end + if m == 2 # \x escape sequence + write(io, UInt8(n)) + else + print(io, Char(n)) + end + elseif '0' <= c <= '7' + k = 1 + n = c-'0' + while (k += 1) <= 3 && !isempty(a) + c = peek(a) + n = ('0' <= c <= '7') ? n<<3 + c-'0' : break + popfirst!(a) + end + if n > 255 + throw(ArgumentError("octal escape sequence out of range")) + end + write(io, UInt8(n)) + else + print(io, c == 'a' ? '\a' : + c == 'b' ? '\b' : + c == 't' ? '\t' : + c == 'n' ? '\n' : + c == 'v' ? '\v' : + c == 'f' ? '\f' : + c == 'r' ? '\r' : + c == 'e' ? '\e' : + (c == '\\' || c == '"') ? c : + throw(ArgumentError("invalid escape sequence \\$c"))) + end + else + print(io, c) + end + end + end + unescape_string(s::AbstractString, keep = ()) = + sprint(unescape_string, s, keep; sizehint=lastindex(s)) +end + +@static if VERSION < v"1.4" + function takewhile(f::Function, itr) + taken = Vector{eltype(itr)}() + next = iterate(itr) + while !isnothing(next) && ((item, state) = next) |> first |> f + push!(taken, item) + next = iterate(itr, state) + end + taken + end +else + const takewhile = Iterators.takewhile +end + +@static if VERSION < v"1.5" + function peek(s::Iterators.Stateful, sentinel=nothing) + ns = s.nextvalstate + return ns !== nothing ? ns[1] : sentinel + end +end + +@static if VERSION < v"1.6" + function parseatom(text::AbstractString, pos::Integer) + Meta.parse(text, pos, greedy = false) + end +else + const parseatom = Meta.parseatom +end + +@static if VERSION < v"1.11" + function generating_output(incremental::Union{Bool,Nothing}=nothing) + ccall(:jl_generating_output, Cint, ()) == 0 && return false + if incremental !== nothing + Base.JLOptions().incremental == incremental || return false + end + return true + end +else + const generating_output = Base.generating_output +end diff --git a/src/faces.jl b/src/faces.jl index 2fe4e4a9..8245aa28 100644 --- a/src/faces.jl +++ b/src/faces.jl @@ -1,5 +1,7 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license +using TOML + const RGBTuple = NamedTuple{(:r, :g, :b), NTuple{3, UInt8}} """ @@ -27,7 +29,7 @@ end SimpleColor(r::Integer, g::Integer, b::Integer) = SimpleColor((; r=UInt8(r), g=UInt8(g), b=UInt8(b))) SimpleColor(rgb::UInt32) = SimpleColor(reverse(reinterpret(UInt8, [rgb]))[2:end]...) -convert(::Type{SimpleColor}, (; r, g, b)::RGBTuple) = SimpleColor((; r, g, b)) +convert(::Type{SimpleColor}, rgb::RGBTuple) = SimpleColor(rgb) convert(::Type{SimpleColor}, namedcolor::Symbol) = SimpleColor(namedcolor) convert(::Type{SimpleColor}, rgb::UInt32) = SimpleColor(rgb) @@ -331,7 +333,7 @@ const FACES = let default = Dict{Symbol, Face}( :repl_prompt_pkg => Face(inherit=[:blue, :repl_prompt]), :repl_prompt_beep => Face(inherit=[:shadow, :repl_prompt]), ) - (; default, current=ScopedValue(copy(default)), lock=ReentrantLock()) + (; default=default, current=Ref(copy(default)), lock=ReentrantLock()) end ## Adding and resetting faces ## @@ -355,12 +357,14 @@ Face (sample) ``` """ function addface!((name, default)::Pair{Symbol, Face}) - @lock FACES.lock if !haskey(FACES.default, name) - FACES.default[name] = default - FACES.current[][name] = if haskey(FACES.current[], name) - merge(deepcopy(default), FACES.current[][name]) - else - deepcopy(default) + lock(FACES.lock) do + if !haskey(FACES.default, name) + FACES.default[name] = default + FACES.current[][name] = if haskey(FACES.current[], name) + merge(deepcopy(default), FACES.current[][name]) + else + deepcopy(default) + end end end end @@ -371,7 +375,7 @@ end Reset the current global face dictionary to the default value. """ function resetfaces!() - @lock FACES.lock begin + lock(FACES.lock) do current = FACES.current[] empty!(current) for (key, val) in FACES.default @@ -391,13 +395,15 @@ In the unlikely event that the face `name` does not have a default value, it is deleted, a warning message is printed, and `nothing` returned. """ function resetfaces!(name::Symbol) - @lock FACES.lock if !haskey(FACES.current[], name) - elseif haskey(FACES.default, name) - FACES.current[][name] = deepcopy(FACES.default[name]) - else # This shouldn't happen - delete!(FACES.current[], name) - @warn """The face $name was reset, but it had no default value, and so has been deleted instead!, - This should not have happened, perhaps the face was added without using `addface!`?""" + lock(FACES.lock) do + if !haskey(FACES.current[], name) + elseif haskey(FACES.default, name) + FACES.current[][name] = deepcopy(FACES.default[name]) + else # This shouldn't happen + delete!(FACES.current[], name) + @warn """The face $name was reset, but it had no default value, and so has been deleted instead!, + This should not have happened, perhaps the face was added without using `addface!`?""" + end end end @@ -410,6 +416,9 @@ Execute `f` with `FACES``.current` temporarily modified by zero or more temporarily unset an face (if if has been set). When `withfaces` returns, the original `FACES``.current` has been restored. + !!! warning + Changing faces is not thread-safe. + # Examples ```jldoctest; setup = :(import StyledStrings: Face, withfaces) @@ -420,17 +429,28 @@ red and blue mixed make purple ``` """ function withfaces(f, keyvals::Pair{Symbol, <:Union{Face, Symbol, Nothing}}...) - newfaces = copy(FACES.current[]) + old = Dict{Symbol, Union{Face, Nothing}}() for (name, face) in keyvals + old[name] = get(FACES.current[], name, nothing) if face isa Face - newfaces[name] = face + FACES.current[][name] = face elseif face isa Symbol - newfaces[name] =get(FACES.current[], face, Face()) - elseif haskey(newfaces, name) - delete!(newfaces, name) + FACES.current[][name] = + something(get(old, face, nothing), get(FACES.current[], face, Face())) + elseif haskey(FACES.current[], name) + delete!(FACES.current[], name) + end + end + try f() + finally + for (name, face) in old + if isnothing(face) + delete!(FACES.current[], name) + else + FACES.current[][name] = face + end end end - @with(FACES.current => newfaces, f()) end """ @@ -438,9 +458,16 @@ end Execute `f` with `FACES``.current` temporarily swapped out with `altfaces` When `withfaces` returns, the original `FACES``.current` has been restored. + + !!! warning + Changing faces is not thread-safe. """ function withfaces(f, altfaces::Dict{Symbol, Face}) - @with(FACES.current => altfaces, f()) + oldfaces, FACES.current[] = FACES.current[], altfaces + try f() + finally + FACES.current[] = oldfaces + end end withfaces(f) = f() @@ -570,10 +597,12 @@ Face (sample) ``` """ function loadface!((name, update)::Pair{Symbol, Face}) - @lock FACES.lock if haskey(FACES.current[], name) - FACES.current[][name] = merge(FACES.current[][name], update) - else - FACES.current[][name] = update + lock(FACES.lock) do + if haskey(FACES.current[], name) + FACES.current[][name] = merge(FACES.current[][name], update) + else + FACES.current[][name] = update + end end end @@ -609,7 +638,7 @@ end Load all faces declared in the Faces.toml file `tomlfile`. """ -loaduserfaces!(tomlfile::String) = loaduserfaces!(Base.parsed_toml(tomlfile)) +loaduserfaces!(tomlfile::String) = loaduserfaces!(open(TOML.parse, tomlfile)) function convert(::Type{Face}, spec::Dict) Face(if haskey(spec, "font") && spec["font"] isa String diff --git a/src/io.jl b/src/io.jl index 0ecc9763..bec29e82 100644 --- a/src/io.jl +++ b/src/io.jl @@ -53,7 +53,8 @@ end Print to `io` the best 8-bit SGR color code that sets the `category` color to be close to `color`. """ -function termcolor8bit(io::IO, (; r, g, b)::RGBTuple, category::Char) +function termcolor8bit(io::IO, rgb::RGBTuple, category::Char) + r, g, b = rgb.r, rgb.g, rgb.b # Magic numbers? Lots. cdistsq(r1, g1, b1) = (r1 - r)^2 + (g1 - g)^2 + (b1 - b)^2 to6cube(value) = if value < 48; 1 @@ -155,14 +156,14 @@ function termstyle(io::IO, face::Face, lastface::Face=getface()) termcolor(io, face.background, '4') face.weight == lastface.weight || print(io, if face.weight ∈ (:medium, :semibold, :bold, :extrabold, :black) - get(Base.current_terminfo, :bold, "\e[1m") + get(current_terminfo, :bold, "\e[1m") elseif face.weight ∈ (:semilight, :light, :extralight, :thin) - get(Base.current_terminfo, :dim, "") + get(current_terminfo, :dim, "") else # :normal ANSI_STYLE_CODES.normal_weight end) face.slant == lastface.slant || - if haskey(Base.current_terminfo, :enter_italics_mode) + if haskey(current_terminfo, :enter_italics_mode) print(io, ifelse(face.slant ∈ (:italic, :oblique), ANSI_STYLE_CODES.start_italics, ANSI_STYLE_CODES.end_italics)) @@ -174,7 +175,7 @@ function termstyle(io::IO, face::Face, lastface::Face=getface()) # Kitty fancy underlines, see # Supported in Kitty, VTE, iTerm2, Alacritty, and Wezterm. face.underline == lastface.underline || - if get(Base.current_terminfo, :Su, false) # Color/style capabilities + if get(current_terminfo, :Su, false) # Color/style capabilities if face.underline isa Tuple # Color and style color, style = face.underline print(io, "\e[4:", @@ -203,11 +204,11 @@ function termstyle(io::IO, face::Face, lastface::Face=getface()) ANSI_STYLE_CODES.start_underline, ANSI_STYLE_CODES.end_underline)) end - face.strikethrough == lastface.strikethrough || !haskey(Base.current_terminfo, :smxx) || + face.strikethrough == lastface.strikethrough || !haskey(current_terminfo, :smxx) || print(io, ifelse(face.strikethrough === true, ANSI_STYLE_CODES.start_strikethrough, ANSI_STYLE_CODES.end_strikethrough)) - face.inverse == lastface.inverse || !haskey(Base.current_terminfo, :enter_reverse_mode) || + face.inverse == lastface.inverse || !haskey(current_terminfo, :enter_reverse_mode) || print(io, ifelse(face.inverse === true, ANSI_STYLE_CODES.start_reverse, ANSI_STYLE_CODES.end_reverse)) @@ -246,9 +247,13 @@ write(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}) = print(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}) = (write(io, s); nothing) -escape_string(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}, - esc = ""; keep = ()) = - (_ansi_writer(io, s, (io, s) -> escape_string(io, s, esc; keep)); nothing) +@static if VERSION < v"1.3" + escape_string(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}, esc = "") = + (_ansi_writer(io, s, (io, s) -> escape_string(io, s, esc)); nothing) +else + escape_string(io::IO, s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}, esc = ""; keep = ()) = + (_ansi_writer(io, s, (io, s) -> escape_string(io, s, esc; keep=keep)); nothing) +end function write(io::IO, c::AnnotatedChar) if get(io, :color, false) == true @@ -306,7 +311,7 @@ function htmlcolor(io::IO, color::SimpleColor) htmlcolor(io, get(HTML_BASIC_COLORS, color.value, SimpleColor(:default))) end else - (; r, g, b) = color.value + r, g, b = color.value.r, color.value.g, color.value.b print(io, '#') r < 0x10 && print(io, '0') print(io, string(r, base=16)) @@ -401,7 +406,11 @@ function htmlstyle(io::IO, face::Face, lastface::Face=getface()) end function show(io::IO, ::MIME"text/html", s::Union{<:AnnotatedString, SubString{<:AnnotatedString}}; wrap::Symbol=:pre) - htmlescape(str) = replace(str, '&' => "&", '<' => "<", '>' => ">") + @static if VERSION >= v"1.7" + htmlescape(str) = replace(str, '&' => "&", '<' => "<", '>' => ">") + else + htmlescape(str) = replace(replace(replace(String(str), '&' => "&"), '<' => "<"), '>' => ">") + end buf = IOBuffer() # Avoid potential overhead in repeatadly printing a more complex IO wrap == :none || print(buf, '<', String(wrap), '>') diff --git a/src/strings/annotated.jl b/src/strings/annotated.jl new file mode 100644 index 00000000..7f5ef680 --- /dev/null +++ b/src/strings/annotated.jl @@ -0,0 +1,436 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +""" + AnnotatedString{S <: AbstractString} <: AbstractString + +A string with metadata, in the form of annotated regions. + +More specifically, this is a simple wrapper around any other +[`AbstractString`](@ref) that allows for regions of the wrapped string to be +annotated with labeled values. + +```text + C + ┌──────┸─────────┐ + "this is an example annotated string" + └──┰────────┼─────┘ │ + A └─────┰─────────┘ + B +``` + +The above diagram represents a `AnnotatedString` where three ranges have been +annotated (labeled `A`, `B`, and `C`). Each annotation holds a label (`Symbol`) +and a value (`Any`), paired together as a `Pair{Symbol, <:Any}`. + +Labels do not need to be unique, the same region can hold multiple annotations +with the same label. + +See also [`AnnotatedChar`](@ref), [`annotatedstring`](@ref), +[`annotations`](@ref), and [`annotate!`](@ref). + +!!! warning + While the constructors are part of the Base public API, the fields + of `AnnotatedString` are not. This is to allow for potential future + changes in the implementation of this type. Instead use the + [`annotations`](@ref), and [`annotate!`](@ref) getter/setter + functions. + +# Constructors + +```julia +AnnotatedString(s::S<:AbstractString) -> AnnotatedString{S} +AnnotatedString(s::S<:AbstractString, annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, <:Any}}}) +``` + +A AnnotatedString can also be created with [`annotatedstring`](@ref), which acts much +like [`string`](@ref) but preserves any annotations present in the arguments. + +# Example + +```julia-repl +julia> AnnotatedString("this is an example annotated string", + [(1:18, :A => 1), (12:28, :B => 2), (18:35, :C => 3)]) +"this is an example annotated string" +``` +""" +struct AnnotatedString{S <: AbstractString} <: AbstractString + string::S + annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}} +end + +""" + AnnotatedChar{S <: AbstractChar} <: AbstractChar + +A Char with annotations. + +More specifically, this is a simple wrapper around any other +[`AbstractChar`](@ref), which holds a list of arbitrary labeled annotations +(`Pair{Symbol, <:Any}`) with the wrapped character. + +See also: [`AnnotatedString`](@ref), [`annotatedstring`](@ref), `annotations`, +and `annotate!`. + +!!! warning + While the constructors are part of the Base public API, the fields + of `AnnotatedChar` are not. This it to allow for potential future + changes in the implementation of this type. Instead use the + [`annotations`](@ref), and [`annotate!`](@ref) getter/setter + functions. + +# Constructors + +```julia +AnnotatedChar(s::S) -> AnnotatedChar{S} +AnnotatedChar(s::S, annotations::Vector{Pair{Symbol, <:Any}}) +``` + +# Examples + +```julia-repl +julia> AnnotatedChar('j', :label => 1) +'j': ASCII/Unicode U+006A (category Ll: Letter, lowercase) +``` +""" +struct AnnotatedChar{C <: AbstractChar} <: AbstractChar + char::C + annotations::Vector{Pair{Symbol, Any}} +end + +## Constructors ## + +# When called with overly-specialised arguments + +AnnotatedString(s::AbstractString, annots::Vector{<:Tuple{UnitRange{Int}, <:Pair{Symbol, <:Any}}}) = + AnnotatedString(s, Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}(annots)) + +AnnotatedChar(c::AbstractChar, annots::Vector{<:Pair{Symbol, <:Any}}) = + AnnotatedChar(c, Vector{Pair{Symbol, Any}}(annots)) + +# Constructors to avoid recursive wrapping + +AnnotatedString(s::AnnotatedString, annots::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}) = + AnnotatedString(s.string, vcat(s.annotations, annots)) + +AnnotatedChar(c::AnnotatedChar, annots::Vector{Pair{Symbol, Any}}) = + AnnotatedChar(c.char, vcat(s.annotations, annots)) + +String(s::AnnotatedString{String}) = s.string # To avoid pointless overhead + +## Conversion/promotion ## + +convert(::Type{AnnotatedString}, s::AnnotatedString) = s +convert(::Type{AnnotatedString{S}}, s::S) where {S <: AbstractString} = + AnnotatedString(s, Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}()) +convert(::Type{AnnotatedString}, s::S) where {S <: AbstractString} = + convert(AnnotatedString{S}, s) +AnnotatedString(s::S) where {S <: AbstractString} = convert(AnnotatedString{S}, s) + +convert(::Type{AnnotatedChar}, c::AnnotatedChar) = c +convert(::Type{AnnotatedChar{C}}, c::C) where { C <: AbstractChar } = + AnnotatedChar{C}(c, Vector{Pair{Symbol, Any}}()) +convert(::Type{AnnotatedChar}, c::C) where { C <: AbstractChar } = + convert(AnnotatedChar{C}, c) + +AnnotatedChar(c::AbstractChar) = convert(AnnotatedChar, c) +AnnotatedChar(c::UInt32) = convert(AnnotatedChar, Char(c)) +AnnotatedChar{C}(c::UInt32) where {C <: AbstractChar} = convert(AnnotatedChar, C(c)) + +promote_rule(::Type{<:AnnotatedString}, ::Type{<:AbstractString}) = AnnotatedString + +## AbstractString interface ## + +ncodeunits(s::AnnotatedString) = ncodeunits(s.string) +codeunits(s::AnnotatedString) = codeunits(s.string) +codeunit(s::AnnotatedString) = codeunit(s.string) +codeunit(s::AnnotatedString, i::Integer) = codeunit(s.string, i) +isvalid(s::AnnotatedString, i::Integer) = isvalid(s.string, i) +@propagate_inbounds iterate(s::AnnotatedString, i::Integer=firstindex(s)) = + if i <= lastindex(s.string); (s[i], nextind(s, i)) end +eltype(::Type{<:AnnotatedString{S}}) where {S} = AnnotatedChar{eltype(S)} +firstindex(s::AnnotatedString) = firstindex(s.string) +lastindex(s::AnnotatedString) = lastindex(s.string) + +function getindex(s::AnnotatedString, i::Integer) + @boundscheck checkbounds(s, i) + @inbounds if isvalid(s, i) + AnnotatedChar(s.string[i], annotations(s, i)) + else + string_index_err(s, i) + end +end + +## AbstractChar interface ## + +ncodeunits(c::AnnotatedChar) = ncodeunits(c.char) +codepoint(c::AnnotatedChar) = codepoint(c.char) + +# Avoid the iteration fallback with comparison +cmp(a::AnnotatedString, b::AbstractString) = cmp(a.string, b) +cmp(a::AbstractString, b::AnnotatedString) = cmp(a, b.string) +# To avoid method ambiguity +cmp(a::AnnotatedString, b::AnnotatedString) = cmp(a.string, b.string) + +==(a::AnnotatedString, b::AnnotatedString) = + a.string == b.string && a.annotations == b.annotations + +==(a::AnnotatedString, b::AbstractString) = isempty(a.annotations) && a.string == b +==(a::AbstractString, b::AnnotatedString) = isempty(b.annotations) && a == b.string + +""" + annotatedstring(values...) + +Create a `AnnotatedString` from any number of `values` using their +[`print`](@ref)ed representation. + +This acts like [`string`](@ref), but takes care to preserve any annotations +present (in the form of [`AnnotatedString`](@ref) or [`AnnotatedChar`](@ref) values). + +See also [`AnnotatedString`](@ref) and [`AnnotatedChar`](@ref). + +## Examples + +```julia-repl +julia> annotatedstring("now a AnnotatedString") +"now a AnnotatedString" + +julia> annotatedstring(AnnotatedString("annotated", [(1:9, :label => 1)]), ", and unannotated") +"annotated, and unannotated" +``` +""" +function annotatedstring(xs...) + isempty(xs) && return AnnotatedString("") + size = mapreduce(_str_sizehint, +, xs) + s = IOContext(IOBuffer(sizehint=size), :color => true) + annotations = Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}() + for x in xs + if x isa AnnotatedString + for (region, annot) in x.annotations + push!(annotations, (s.io.size .+ (region), annot)) + end + print(s, x.string) + elseif x isa SubString{<:AnnotatedString} + for (region, annot) in x.string.annotations + start, stop = first(region), last(region) + if start <= x.offset + x.ncodeunits && stop > x.offset + rstart = s.io.size + max(0, start - x.offset - 1) + 1 + rstop = s.io.size + min(stop, x.offset + x.ncodeunits) - x.offset + push!(annotations, (rstart:rstop, annot)) + end + end + print(s, SubString(x.string.string, x.offset, x.ncodeunits, Val(:noshift))) + elseif x isa AnnotatedChar + for annot in x.annotations + push!(annotations, (1+s.io.size:1+s.io.size, annot)) + end + print(s, x.char) + else + print(s, x) + end + end + str = String(resize!(s.io.data, s.io.size)) + AnnotatedString(str, annotations) +end + +annotatedstring(s::AnnotatedString) = s +annotatedstring(c::AnnotatedChar) = + AnnotatedString(string(c.char), [(1:ncodeunits(c), annot) for annot in c.annotations]) + +AnnotatedString(s::SubString{<:AnnotatedString}) = annotatedstring(s) + +""" + annotatedstring_optimize!(str::AnnotatedString) + +Merge contiguous identical annotations in `str`. +""" +function annotatedstring_optimize!(s::AnnotatedString) + last_seen = Dict{Pair{Symbol, Any}, Int}() + i = 1 + while i <= length(s.annotations) + region, keyval = s.annotations[i] + prev = get(last_seen, keyval, 0) + if prev > 0 + lregion, _ = s.annotations[prev] + if last(lregion) + 1 == first(region) + s.annotations[prev] = + setindex(s.annotations[prev], + first(lregion):last(region), + 1) + deleteat!(s.annotations, i) + else + delete!(last_seen, keyval) + end + else + last_seen[keyval] = i + i += 1 + end + end + s +end + +function repeat(str::AnnotatedString, r::Integer) + r == 0 && return one(AnnotatedString) + r == 1 && return str + unannot = repeat(str.string, r) + annotations = Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}() + len = ncodeunits(str) + fullregion = firstindex(str):lastindex(str) + for (region, annot) in str.annotations + if region == fullregion + push!(annotations, (firstindex(unannot):lastindex(unannot), annot)) + end + end + for offset in 0:len:(r-1)*len + for (region, annot) in str.annotations + if region != fullregion + push!(annotations, (region .+ offset, annot)) + end + end + end + AnnotatedString(unannot, annotations) |> annotatedstring_optimize! +end + +repeat(str::SubString{<:AnnotatedString}, r::Integer) = + repeat(AnnotatedString(str), r) + +function repeat(c::AnnotatedChar, r::Integer) + str = repeat(c.char, r) + fullregion = firstindex(str):lastindex(str) + AnnotatedString(str, [(fullregion, annot) for annot in c.annotations]) +end + +function reverse(s::AnnotatedString) + lastind = lastindex(s) + AnnotatedString(reverse(s.string), + [(UnitRange(1 + lastind - last(region), + 1 + lastind - first(region)), + annot) + for (region, annot) in s.annotations]) +end + +# TODO optimise? +reverse(s::SubString{<:AnnotatedString}) = reverse(AnnotatedString(s)) + +# TODO implement `replace(::AnnotatedString, ...)` + +## End AbstractString interface ## + +""" + annotate!(str::AnnotatedString, [range::UnitRange{Int}], label::Symbol => value) + annotate!(str::SubString{AnnotatedString}, [range::UnitRange{Int}], label::Symbol => value) + +Annotate a `range` of `str` (or the entire string) with a labeled value (`label` => `value`). +To remove existing `label` annotations, use a value of `nothing`. +""" +function annotate!(s::AnnotatedString, range::UnitRange{Int}, @nospecialize(labelval::Pair{Symbol, <:Any})) + label, val = labelval + indices = searchsorted(s.annotations, (range,), by=first) + if val === nothing + labelindex = filter(i -> first(s.annotations[i][2]) === label, indices) + for index in Iterators.reverse(labelindex) + deleteat!(s.annotations, index) + end + else + splice!(s.annotations, indices, [(range, Pair{Symbol, Any}(label, val))]) + end + s +end + +annotate!(ss::AnnotatedString, @nospecialize(labelval::Pair{Symbol, <:Any})) = + annotate!(ss, firstindex(ss):lastindex(ss), labelval) + +annotate!(s::SubString{<:AnnotatedString}, range::UnitRange{Int}, @nospecialize(labelval::Pair{Symbol, <:Any})) = + (annotate!(s.string, s.offset .+ (range), labelval); s) + +annotate!(s::SubString{<:AnnotatedString}, @nospecialize(labelval::Pair{Symbol, <:Any})) = + (annotate!(s.string, s.offset .+ (1:s.ncodeunits), labelval); s) + +""" + annotate!(char::AnnotatedChar, label::Symbol => value) + +Annotate `char` with the pair `label => value`. +""" +annotate!(c::AnnotatedChar, @nospecialize(labelval::Pair{Symbol, <:Any})) = + (push!(c.annotations, labelval); c) + +""" + annotations(str::AnnotatedString, [position::Union{Integer, UnitRange}]) + annotations(str::SubString{AnnotatedString}, [position::Union{Integer, UnitRange}]) + +Get all annotations that apply to `str`. Should `position` be provided, only +annotations that overlap with `position` will be returned. + +See also: `annotate!`. +""" +annotations(s::AnnotatedString) = s.annotations + +annotations(s::SubString{<:AnnotatedString}) = + annotations(s, s.offset+1:s.offset+s.ncodeunits) + +function annotations(s::AnnotatedString, pos::UnitRange{<:Integer}) + # TODO optimise + annots = filter(label -> !isempty(intersect(pos, first(label))), + s.annotations) + last.(annots) +end + +annotations(s::AnnotatedString, pos::Integer) = annotations(s, pos:pos) + +annotations(s::SubString{<:AnnotatedString}, pos::Integer) = + annotations(s.string, s.offset + pos) +annotations(s::SubString{<:AnnotatedString}, pos::UnitRange{<:Integer}) = + annotations(s.string, first(pos)+s.offset:last(pos)+s.offset) + +""" + annotations(chr::AnnotatedChar) + +Get all annotations of `chr`. +""" +annotations(c::AnnotatedChar) = c.annotations + +## AnnotatedIOBuffer + +struct AnnotatedIOBuffer <: IO + io::IOBuffer + annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}} +end + +AnnotatedIOBuffer(io::IOBuffer) = AnnotatedIOBuffer(io, Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}()) +AnnotatedIOBuffer() = AnnotatedIOBuffer(IOBuffer()) + +function show(io::IO, annio::AnnotatedIOBuffer) + show(io, AnnotatedIOBuffer) + print(io, '(', annio.io.size, " bytes)") +end + +position(io::AnnotatedIOBuffer) = position(io.io) +lock(io::AnnotatedIOBuffer) = lock(io.io) +unlock(io::AnnotatedIOBuffer) = unlock(io.io) + +function write(io::AnnotatedIOBuffer, astr::Union{AnnotatedString, SubString{<:AnnotatedString}}) + astr = AnnotatedString(astr) + offset = position(io.io) + for (region, annot) in astr.annotations + start, stop = first(region), last(region) + push!(io.annotations, (start+offset:stop+offset, annot)) + end + write(io.io, astr) +end +write(io::AnnotatedIOBuffer, achr::AnnotatedChar) = write(io, AnnotatedString(achr)) +write(io::AnnotatedIOBuffer, x::AbstractString) = write(io.io, x) +write(io::AnnotatedIOBuffer, s::Union{SubString{String}, String}) = write(io.io, x) +write(io::AnnotatedIOBuffer, x::UInt8) = write(io.io, x) + +""" + read(io::AnnotatedIOBuffer, AnnotatedString) + +Read the entirety of `io`, as an `AnnotatedString`. This preserves the +annotations of any `AnnotatedString`s written to `io` and otherwise acts like +`read(io::IO, String)`. +""" +function read(io::AnnotatedIOBuffer, ::Type{AnnotatedString{>:String}}) + str = String(take!(io.io)) + annots = copy(io.annotations) + empty!(io.annotations) + seekstart(io.io) + AnnotatedString(str, annotss) +end diff --git a/src/strings/basic.jl b/src/strings/basic.jl new file mode 100644 index 00000000..73559197 --- /dev/null +++ b/src/strings/basic.jl @@ -0,0 +1,5 @@ +_isannotated(S::Type) = S != Union{} && (S <: AnnotatedString || S <: AnnotatedChar) +_isannotated(s) = _isannotated(typeof(s)) + +# actually from substring.jl +_isannotated(::SubString{T}) where {T} = _isannotated(T) diff --git a/src/strings/io.jl b/src/strings/io.jl new file mode 100644 index 00000000..5bfd843d --- /dev/null +++ b/src/strings/io.jl @@ -0,0 +1,49 @@ + +# TODO: If/when we have `AnnotatedIO`, we can revisit this and +# implement it more nicely. +function join_annotated(iterator, delim="", last=delim) + xs = zip(iterator, Iterators.repeated(delim)) |> Iterators.flatten |> collect + xs = xs[1:end-1] + if length(xs) > 1 + xs[end-1] = last + end + annotatedstring(xs...)::AnnotatedString{String} +end + +function _join_maybe_annotated(args...) + if any(function (arg) + t = eltype(arg) + !(t == Union{}) && (t <: AnnotatedString || t <: AnnotatedChar) + end, args) + join_annotated(args...) + else + sprint(join, args...) + end +end + +join(iterator) = _join_maybe_annotated(iterator) +join(iterator, delim) = _join_maybe_annotated(iterator, delim) +join(iterator, delim, last) = _join_maybe_annotated(iterator, delim, last) + +function AnnotatedString(chars::AbstractVector{C}) where {C<:AbstractChar} + str = if C <: AnnotatedChar + String(getfield.(chars, :char)) + else + sprint(sizehint=length(chars)) do io + for c in chars + print(io, c) + end + end + end + props = Tuple{UnitRange{Int}, Pair{Symbol, Any}}[] + point = 1 + for c in chars + if c isa AnnotatedChar + for prop in c.properties + push!(props, (point:point, prop)) + end + end + point += ncodeunits(c) + end + AnnotatedString(str, props) +end diff --git a/src/strings/regex.jl b/src/strings/regex.jl new file mode 100644 index 00000000..a48ef9be --- /dev/null +++ b/src/strings/regex.jl @@ -0,0 +1,45 @@ +@static if VERSION < v"1.6" + struct AnnotatedRegexMatch{S<:AbstractString} + match::SubString{S} + captures::Vector{Union{Nothing, SubString{S}}} + offset::Int64 + offsets::Vector{Int64} + regex::Regex + end +else + struct AnnotatedRegexMatch{S<:AbstractString} <: AbstractMatch + match::SubString{S} + captures::Vector{Union{Nothing, SubString{S}}} + offset::Int64 + offsets::Vector{Int64} + regex::Regex + end +end + +function _annotatedmatch(m::RegexMatch, str::AnnotatedString{S}) where {S<:AbstractString} + AnnotatedRegexMatch{AnnotatedString{S}}( + # It's surprisingly annoying to clone a substring, + # thanks to that pesky inner constructor. + eval(Expr(:new, SubString{AnnotatedString{S}}( + str, m.match.offset, m.match.ncodeunits))), + Union{Nothing,SubString{AnnotatedString{S}}}[ + if !isnothing(cap) + eval(Expr(:new, SubString{AnnotatedString{S}}( + str, cap.offset, cap.ncodeunits))) + end for cap in m.captures], + m.offset, m.offsets, m.regex) +end + +function match(re::Regex, str::AnnotatedString) + m = match(re, str.string) + if !isnothing(m) + _annotatedmatch(m, str) + end +end + +function match(re::Regex, str::AnnotatedString, idx::Integer, add_opts::UInt32=UInt32(0)) + m = match(re, str.string, idx, add_opts) + if !isnothing(m) + _annotatedmatch(m, str) + end +end diff --git a/src/strings/strings.jl b/src/strings/strings.jl new file mode 100644 index 00000000..372811db --- /dev/null +++ b/src/strings/strings.jl @@ -0,0 +1,17 @@ +module AnnotatedStrings + +import Base: @propagate_inbounds, TTY, String, cmp, codepoint, codeunit, + codeunits, convert, eltype, empty, firstindex, get, getindex, haskey, in, + isvalid, iterate, join, keys, keys, lastindex, length, merge, parse, print, + promote_rule, read, repeat, reverse, show, tryparse, write, *, == + +include(joinpath(dirname(@__DIR__), "compat.jl")) +include("annotated.jl") + +function __init__() + # Method overwriting + eval(:(include(joinpath($@__DIR__, "io.jl")))) + eval(:(include(joinpath($@__DIR__, "regex.jl")))) +end + +end diff --git a/src/strings/util.jl b/src/strings/util.jl new file mode 100644 index 00000000..ff80c23a --- /dev/null +++ b/src/strings/util.jl @@ -0,0 +1,29 @@ +function lpad( + s::Union{AbstractChar,AbstractString}, + n::Integer, + p::Union{AbstractChar,AbstractString}=' ', +) + stringfn = if any(isa.((s, p), Union{AnnotatedString, AnnotatedChar, SubString{<:AnnotatedString}})) + annotatedstring else string end + n = Int(n)::Int + m = signed(n) - Int(textwidth(s))::Int + m ≤ 0 && return stringfn(s) + l = textwidth(p) + q, r = divrem(m, l) + r == 0 ? stringfn(p^q, s) : stringfn(p^q, first(p, r), s) +end + +function rpad( + s::Union{AbstractChar,AbstractString}, + n::Integer, + p::Union{AbstractChar,AbstractString}=' ', +) + stringfn = if any(isa.((s, p), Union{AnnotatedString, AnnotatedChar, SubString{<:AnnotatedString}})) + annotatedstring else string end + n = Int(n)::Int + m = signed(n) - Int(textwidth(s))::Int + m ≤ 0 && return stringfn(s) + l = textwidth(p) + q, r = divrem(m, l) + r == 0 ? stringfn(s, p^q) : stringfn(s, p^q, first(p, r)) +end diff --git a/src/stylemacro.jl b/src/stylemacro.jl index fcd63cc8..5a3a1dbb 100644 --- a/src/stylemacro.jl +++ b/src/stylemacro.jl @@ -16,8 +16,7 @@ interpolated with `\$`. # Example ```julia -styled"The {bold:{italic:quick} {(foreground=#cd853f):brown} fox} jumped over \ -the {link={https://en.wikipedia.org/wiki/Laziness}:lazy} dog" +styled"The {bold:{italic:quick} {(foreground=#cd853f):brown} fox} jumped over the {link={https://en.wikipedia.org/wiki/Laziness}:lazy} dog" ``` # Extended help @@ -110,14 +109,14 @@ macro styled_str(raw_content::String) # Instead we'll just use a `NamedTuple` state = let content = unescape_string(raw_content, ('{', '}', ':', '$', '\n', '\r')) - (; content, bytes = Vector{UInt8}(content), + (; content=content, bytes = Vector{UInt8}(content), s = Iterators.Stateful(pairs(content)), parts = Any[], active_styles = Vector{Tuple{Int, Int, Union{Symbol, Expr, Pair{Symbol, Any}}}}[], pending_styles = Tuple{UnitRange{Int}, Union{Symbol, Expr, Pair{Symbol, Any}}}[], offset = Ref(0), point = Ref(1), escape = Ref(false), interpolated = Ref(false), - errors = Vector{NamedTuple{(:message, :position, :hint), - Tuple{AnnotatedString{String}, <:Union{Int, Nothing}, String}}}()) + errors = Vector{Union{NamedTuple{(:message, :position, :hint), Tuple{AnnotatedString{String}, Int, String}}, + NamedTuple{(:message, :position, :hint), Tuple{AnnotatedString{String}, Nothing, String}}}}()) end # Value restrictions we can check against @@ -141,11 +140,11 @@ macro styled_str(raw_content::String) end, -position) end - push!(state.errors, (; message=AnnotatedString(message), position, hint)) + push!(state.errors, (; message=AnnotatedString(message), position=position, hint=hint)) nothing end - hygienic_eval(expr) = Core.eval(__module__, Expr(:var"hygienic-scope", expr, @__MODULE__)) + hygienic_eval(expr) = Core.eval(__module__, Expr(Symbol("hygienic-scope"), expr, @__MODULE__)) function addpart!(state, stop::Int) if state.point[] > stop+state.offset[]+ncodeunits(state.content[stop])-1 @@ -244,7 +243,7 @@ macro styled_str(raw_content::String) -1, "right here") return "", pos end - expr, nextpos = Meta.parseatom(state.content, pos) + expr, nextpos = parseatom(state.content, pos) nchars = length(state.content[pos:prevind(state.content, nextpos)]) for _ in 1:nchars isempty(state.s) && break @@ -676,26 +675,28 @@ macro styled_str(raw_content::String) elseif state.interpolated[] :(annotatedstring($(state.parts...))) else - annotatedstring(map(hygienic_eval, state.parts)...) |> Base.annotatedstring_optimize! + annotatedstring(map(hygienic_eval, state.parts)...) |> annotatedstring_optimize! end end struct MalformedStylingMacro <: Exception raw::String - problems::Vector{NamedTuple{(:message, :position, :hint), Tuple{AnnotatedString{String}, <:Union{Int, Nothing}, String}}} + problems::Vector{Union{NamedTuple{(:message, :position, :hint), Tuple{AnnotatedString{String}, Int, String}}, + NamedTuple{(:message, :position, :hint), Tuple{AnnotatedString{String}, Nothing, String}}}} end function Base.showerror(io::IO, err::MalformedStylingMacro) # We would do "{error:┌ ERROR\:} MalformedStylingMacro" but this is already going # to be prefixed with "ERROR: LoadError", so it's better not to. println(io, "MalformedStylingMacro") - for (; message, position, hint) in err.problems + for prob in err.problems + message, position, hint = prob.message, prob.position, prob.hint posinfo = if isnothing(position) println(io, styled"{error:│} $message.") else infowidth = displaysize(stderr)[2] ÷ 3 j = clamp(12 * round(Int, position / 12), - firstindex(err.raw):lastindex(err.raw)) + firstindex(err.raw), lastindex(err.raw)) start = if j <= infowidth firstindex(err.raw) else max(prevind(err.raw, j, infowidth), firstindex(err.raw)) end diff --git a/src/terminfo.jl b/src/terminfo.jl new file mode 100644 index 00000000..3d867e0f --- /dev/null +++ b/src/terminfo.jl @@ -0,0 +1,296 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +include("terminfo_data.jl") + +""" + struct TermInfoRaw + +A structured representation of a terminfo file, without any knowledge of +particular capabilities, solely based on `term(5)`. + +!!! warning + This is not part of the public API, and thus subject to change without notice. + +# Fields + +- `names::Vector{String}`: The names this terminal is known by. +- `flags::BitVector`: A list of 0–$(length(TERM_FLAGS)) flag values. +- `numbers::Union{Vector{UInt16}, Vector{UInt32}}`: A list of 0–$(length(TERM_NUMBERS)) + number values. A value of `typemax(eltype(numbers))` is used to skip over + unspecified capabilities while ensuring value indices are correct. +- `strings::Vector{Union{String, Nothing}}`: A list of 0–$(length(TERM_STRINGS)) + string values. A value of `nothing` is used to skip over unspecified + capabilities while ensuring value indices are correct. +- `extended::Union{Nothing, Dict{Symbol, Union{Bool, Int, String}}}`: Should an + extended info section exist, this gives the entire extended info as a + dictionary. Otherwise `nothing`. + +See also: `TermInfo` and `TermCapability`. +""" +struct TermInfoRaw + names::Vector{String} + flags::BitVector + numbers::Union{Vector{UInt16}, Vector{UInt32}} + strings::Vector{Union{String, Nothing}} + extended::Union{Nothing, Dict{Symbol, Union{Bool, Int, String}}} +end + +""" + struct TermInfo + +A parsed terminfo paired with capability information. + +!!! warning + This is not part of the public API, and thus subject to change without notice. + +# Fields + +- `names::Vector{String}`: The names this terminal is known by. +- `flags::Int`: The number of flags specified. +- `numbers::BitVector`: A mask indicating which of `TERM_NUMBERS` have been + specified. +- `strings::BitVector`: A mask indicating which of `TERM_STRINGS` have been + specified. +- `extensions::Vector{Symbol}`: A list of extended capability variable names. +- `capabilities::Dict{Symbol, Union{Bool, Int, String}}`: The capability values + themselves. + +See also: `TermInfoRaw` and `TermCapability`. +""" +struct TermInfo + names::Vector{String} + flags::Int + numbers::BitVector + strings::BitVector + extensions::Vector{Symbol} + capabilities::Dict{Symbol, Union{Bool, Int, String}} +end + +TermInfo() = TermInfo([], 0, [], [], [], Dict()) + +function Base.read(data::IO, ::Type{TermInfoRaw}) + # Parse according to `term(5)` + # Header + magic = read(data, UInt16) |> ltoh + NumInt = if magic == 0o0432 + UInt16 + elseif magic == 0o01036 + UInt32 + else + throw(ArgumentError("Terminfo data did not start with the magic number 0o0432 or 0o01036")) + end + name_bytes = read(data, UInt16) |> ltoh + flag_bytes = read(data, UInt16) |> ltoh + numbers_count = read(data, UInt16) |> ltoh + string_count = read(data, UInt16) |> ltoh + table_bytes = read(data, UInt16) |> ltoh + # Terminal Names + term_names = split(String(read(data, name_bytes - 1)), '|') .|> String + 0x00 == read(data, UInt8) || + throw(ArgumentError("Terminfo data did not contain a null byte after the terminal names section")) + # Boolean Flags + flags = read(data, flag_bytes) .== 0x01 + if position(data) % 2 != 0 + 0x00 == read(data, UInt8) || + throw(ArgumentError("Terminfo did not contain a null byte after the flag section, expected to position the start of the numbers section on an even byte")) + end + # Numbers, Strings, Table + numbers = map(ltoh, reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt)))) + string_indices = map(ltoh, reinterpret(UInt16, read(data, string_count * sizeof(UInt16)))) + strings_table = read(data, table_bytes) + strings = map(string_indices) do idx + if idx ∉ (0xffff, 0xfffe) + len = findfirst(==(0x00), view(strings_table, 1+idx:length(strings_table))) + !isnothing(len) || + throw(ArgumentError("Terminfo string table entry does not terminate with a null byte")) + String(strings_table[1+idx:idx+len-1]) + end + end + TermInfoRaw(term_names, flags, numbers, strings, + if !eof(data) extendedterminfo(data, NumInt) end) +end + +""" + extendedterminfo(data::IO; NumInt::Union{Type{UInt16}, Type{UInt32}}) + +Read an extended terminfo section from `data`, with `NumInt` as the numbers type. + +This will accept any terminfo content that conforms with `term(5)`. + +See also: `read(::IO, ::Type{TermInfoRaw})` +""" +function extendedterminfo(data::IO, NumInt::Union{Type{UInt16}, Type{UInt32}}) + # Extended info + if position(data) % 2 != 0 + 0x00 == read(data, UInt8) || + throw(ArgumentError("Terminfo did not contain a null byte before the extended section, expected to position the start on an even byte")) + end + # Extended header + flag_bytes = read(data, UInt16) |> ltoh + numbers_count = read(data, UInt16) |> ltoh + string_count = read(data, UInt16) |> ltoh + table_count = read(data, UInt16) |> ltoh + table_bytes = read(data, UInt16) |> ltoh + # Extended flags/numbers/strings + flags = read(data, flag_bytes) .== 0x01 + if flag_bytes % 2 != 0 + 0x00 == read(data, UInt8) || + throw(ArgumentError("Terminfo did not contain a null byte after the extended flag section, expected to position the start of the numbers section on an even byte")) + end + numbers = map(n -> Int(ltoh(n)), reinterpret(NumInt, read(data, numbers_count * sizeof(NumInt)))) + table_indices = map(ltoh, reinterpret(UInt16, read(data, table_count * sizeof(UInt16)))) + table_strings = [String(readuntil(data, 0x00)) for _ in 1:length(table_indices)] + info = Dict{Symbol, Union{Bool, Int, String}}() + strings = table_strings[1:string_count] + labels = table_strings[string_count+1:end] + for (label, val) in zip(labels, vcat(flags, numbers, strings)) + info[Symbol(label)] = val + end + return info +end + +""" + TermInfo(raw::TermInfoRaw) + +Construct a `TermInfo` from `raw`, using known terminal capabilities (as of +NCurses 6.3, see `TERM_FLAGS`, `TERM_NUMBERS`, and `TERM_STRINGS`). +""" +function TermInfo(raw::TermInfoRaw) + capabilities = Dict{Symbol, Union{Bool, Int, String}}() + sizehint!(capabilities, 2 * (length(raw.flags) + length(raw.numbers) + length(raw.strings))) + for (flag, value) in zip(TERM_FLAGS, raw.flags) + capabilities[flag.short] = value + capabilities[flag.long] = value + end + for (num, value) in zip(TERM_NUMBERS, raw.numbers) + if value != typemax(eltype(raw.numbers)) + capabilities[num.short] = Int(value) + capabilities[num.long] = Int(value) + end + end + for (str, value) in zip(TERM_STRINGS, raw.strings) + if !isnothing(value) + capabilities[str.short] = value + capabilities[str.long] = value + end + end + extensions = if !isnothing(raw.extended) + capabilities = merge(capabilities, raw.extended) + keys(raw.extended) |> collect + else + Symbol[] + end + TermInfo(raw.names, length(raw.flags), + map(n-> n != typemax(typeof(n)), raw.numbers), + map(!isnothing, raw.strings), + extensions, capabilities) +end + +Base.getindex(ti::TermInfo, key::Symbol) = ti.capabilities[key] +Base.get(ti::TermInfo, key::Symbol, default::D) where D<:Union{Bool, Int, String} = + get(ti.capabilities, key, default)::D +Base.get(ti::TermInfo, key::Symbol, default) = get(ti.capabilities, key, default) +Base.keys(ti::TermInfo) = keys(ti.capabilities) +Base.haskey(ti::TermInfo, key::Symbol) = haskey(ti.capabilities, key) + +function Base.show(io::IO, ::MIME"text/plain", ti::TermInfo) + print(io, "TermInfo(", ti.names, "; ", ti.flags, " flags, ", + sum(ti.numbers), " numbers, ", sum(ti.strings), " strings") + !isempty(ti.extensions) > 0 && + print(io, ", ", length(ti.extensions), " extended capabilities") + print(io, ')') +end + +""" + find_terminfo_file(term::String) + +Locate the terminfo file for `term`, return `nothing` if none could be found. + +The lookup policy is described in `terminfo(5)` "Fetching Compiled +Descriptions". +""" +function find_terminfo_file(term::String) + isempty(term) && return + chr, chrcode = string(first(term)), string(Int(first(term)), base=16) + terminfo_dirs = if haskey(ENV, "TERMINFO") + [ENV["TERMINFO"]] + elseif isdir(joinpath(homedir(), ".terminfo")) + [joinpath(homedir(), ".terminfo")] + elseif haskey(ENV, "TERMINFO_DIRS") + split(ENV["TERMINFO_DIRS"], ':') + elseif Sys.isunix() + ["/usr/share/terminfo"] + else + String[] + end + for dir in terminfo_dirs + if isfile(joinpath(dir, chr, term)) + return joinpath(dir, chr, term) + elseif isfile(joinpath(dir, chrcode, term)) + return joinpath(dir, chrcode, term) + end + end +end + +""" + load_terminfo(term::String) + +Load the `TermInfo` for `term`, falling back on a blank `TermInfo`. +""" +function load_terminfo(term::String) + file = find_terminfo_file(term) + isnothing(file) && return TermInfo() + try + TermInfo(read(file, TermInfoRaw)) + catch err + if err isa ArgumentError || err isa IOError + TermInfo() + else + rethrow() + end + end +end + +@static if VERSION < v"1.8" + current_terminfo = TermInfo() +else + current_terminfo::TermInfo = TermInfo() +end + +# Legacy/Base.TTY methods and the `:color` parameter + +if Sys.iswindows() + ttyhascolor(term_type = nothing) = true +else + function ttyhascolor(term_type = get(ENV, "TERM", "")) + startswith(term_type, "xterm") || + haskey(current_terminfo, :setaf) + end +end + +""" + ttyhastruecolor() + +Return a boolean signifying whether the current terminal supports 24-bit colors. + +This uses the `COLORTERM` environment variable if possible, returning true if it +is set to either `"truecolor"` or `"24bit"`. + +As a fallback, first on unix systems the `colors` terminal capability is checked +— should more than 256 colors be reported, this is taken to signify 24-bit +support. +""" +function ttyhastruecolor() + get(ENV, "COLORTERM", "") ∈ ("truecolor", "24bit") || + @static if Sys.isunix() + get(current_terminfo, :colors, 0) > 256 + else + false + end +end + +function get_have_truecolor() + global have_truecolor + have_truecolor === nothing && (have_truecolor = ttyhastruecolor()) + return have_truecolor::Bool +end diff --git a/src/terminfo_data.jl b/src/terminfo_data.jl new file mode 100644 index 00000000..38c058f4 --- /dev/null +++ b/src/terminfo_data.jl @@ -0,0 +1,540 @@ +# This file is a part of Julia. License is MIT: https://julialang.org/license + +""" + struct TermCapability + +Specification of a single terminal capability. + +!!! warning + This is not part of the public API, and thus subject to change without notice. + +# Fields + +- `short::Symbol`: The *Cap-name* of the capability +- `long::Symbol`: The name of the terminfo capability variable +- `description::String`: A description of the purpose of the capability + +See also: `TermInfo`, `TERM_FLAGS`, `TERM_NUMBERS`, and `TERM_STRINGS`. +""" +struct TermCapability + short::Symbol + long::Symbol + description::String +end + +# Terminfo Capabilities as of NCurses 6.3 + +""" +Ordered list of known terminal capability flag fields, as of NCurses 6.3. +""" +const TERM_FLAGS = [ + TermCapability(:bw, :auto_left_margin, "cub1 wraps from column 0 to last column"), + TermCapability(:am, :auto_right_margin, "terminal has automatic margins"), + TermCapability(:xsb, :no_esc_ctlc, "beehive (f1=escape, f2=ctrl C)"), + TermCapability(:xhp, :ceol_standout_glitch, "standout not erased by overwriting (hp)"), + TermCapability(:xenl, :eat_newline_glitch, "newline ignored after 80 cols (concept)"), + TermCapability(:eo, :erase_overstrike, "can erase overstrikes with a blank"), + TermCapability(:gn, :generic_type, "generic line type"), + TermCapability(:hc, :hard_copy, "hardcopy terminal"), + TermCapability(:km, :has_meta_key, "Has a meta key (i.e., sets 8th-bit)"), + TermCapability(:hs, :has_status_line, "has extra status line"), + TermCapability(:in, :insert_null_glitch, "insert mode distinguishes nulls"), + TermCapability(:db, :memory_below, "display may be retained below the screen"), + TermCapability(:da, :memory_above, "display may be retained above the screen"), + TermCapability(:mir, :move_insert_mode, "safe to move while in insert mode"), + TermCapability(:msgr, :move_standout_mode, "safe to move while in standout mode"), + TermCapability(:os, :over_strike, "terminal can overstrike"), + TermCapability(:eslok, :status_line_esc_ok, "escape can be used on the status line"), + TermCapability(:xt, :dest_tabs_magic_smso, "tabs destructive, magic so char (t1061)"), + TermCapability(:hz, :tilde_glitch, "cannot print ~'s (Hazeltine)"), + TermCapability(:ul, :transparent_underline, "underline character overstrikes"), + TermCapability(:xon, :xon_xoff, "terminal uses xon/xoff handshaking"), + TermCapability(:nxon, :needs_xon_xoff, "padding will not work, xon/xoff required"), + TermCapability(:mc5i, :prtr_silent, "printer will not echo on screen"), + TermCapability(:chts, :hard_cursor, "cursor is hard to see"), + TermCapability(:nrrmc, :non_rev_rmcup, "smcup does not reverse rmcup"), + TermCapability(:npc, :no_pad_char, "pad character does not exist"), + TermCapability(:ndscr, :non_dest_scroll_region, "scrolling region is non-destructive"), + TermCapability(:ccc, :can_change, "terminal can re-define existing colors"), + TermCapability(:bce, :back_color_erase, "screen erased with background color"), + TermCapability(:hls, :hue_lightness_saturation, "terminal uses only HLS color notation (Tektronix)"), + TermCapability(:xhpa, :col_addr_glitch, "only positive motion for hpa/mhpa caps"), + TermCapability(:crxm, :cr_cancels_micro_mode, "using cr turns off micro mode"), + TermCapability(:daisy, :has_print_wheel, "printer needs operator to change character set"), + TermCapability(:xvpa, :row_addr_glitch, "only positive motion for vpa/mvpa caps"), + TermCapability(:sam, :semi_auto_right_margin, "printing in last column causes cr"), + TermCapability(:cpix, :cpi_changes_res, "changing character pitch changes resolution"), + TermCapability(:lpix, :lpi_changes_res, "changing line pitch changes resolution"), + TermCapability(:OTbs, :backspaces_with_bs, "uses ^H to move left"), + TermCapability(:OTns, :crt_no_scrolling, "crt cannot scroll"), + TermCapability(:OTnc, :no_correctly_working_cr, "no way to go to start of line"), + TermCapability(:OTMT, :gnu_has_meta_key, "has meta key"), + TermCapability(:OTNL, :linefeed_is_newline, "move down with \n"), + TermCapability(:OTpt, :has_hardware_tabs, "has 8-char tabs invoked with ^I"), + TermCapability(:OTxr, :return_does_clr_eol, "return clears the line"), +] + +""" +Ordered list of known terminal capability number fields, as of NCurses 6.3. +""" +const TERM_NUMBERS = [ + TermCapability(:cols, :columns, "number of columns in a line"), + TermCapability(:it, :init_tabs, "tabs initially every # spaces"), + TermCapability(:lines, :lines, "number of lines on screen or page"), + TermCapability(:lm, :lines_of_memory, "lines of memory if > line. 0 means varies"), + TermCapability(:xmc, :magic_cookie_glitch, "number of blank characters left by smso or rmso"), + TermCapability(:pb, :padding_baud_rate, "lowest baud rate where padding needed"), + TermCapability(:vt, :virtual_terminal, "virtual terminal number (CB/unix)"), + TermCapability(:wsl, :width_status_line, "number of columns in status line"), + TermCapability(:nlab, :num_labels, "number of labels on screen"), + TermCapability(:lh, :label_height, "rows in each label"), + TermCapability(:lw, :label_width, "columns in each label"), + TermCapability(:ma, :max_attributes, "maximum combined attributes terminal can handle"), + TermCapability(:wnum, :maximum_windows, "maximum number of definable windows"), + TermCapability(:colors, :max_colors, "maximum number of colors on screen"), + TermCapability(:pairs, :max_pairs, "maximum number of color-pairs on the screen"), + TermCapability(:ncv, :no_color_video, "video attributes that cannot be used with colors"), + TermCapability(:bufsz, :buffer_capacity, "numbers of bytes buffered before printing"), + TermCapability(:spinv, :dot_vert_spacing, "spacing of pins vertically in pins per inch"), + TermCapability(:spinh, :dot_horz_spacing, "spacing of dots horizontally in dots per inch"), + TermCapability(:maddr, :max_micro_address, "maximum value in micro_..._address"), + TermCapability(:mjump, :max_micro_jump, "maximum value in parm_..._micro"), + TermCapability(:mcs, :micro_col_size, "character step size when in micro mode"), + TermCapability(:mls, :micro_line_size, "line step size when in micro mode"), + TermCapability(:npins, :number_of_pins, "numbers of pins in print-head"), + TermCapability(:orc, :output_res_char, "horizontal resolution in units per line"), + TermCapability(:orl, :output_res_line, "vertical resolution in units per line"), + TermCapability(:orhi, :output_res_horz_inch, "horizontal resolution in units per inch"), + TermCapability(:orvi, :output_res_vert_inch, "vertical resolution in units per inch"), + TermCapability(:cps, :print_rate, "print rate in characters per second"), + TermCapability(:widcs, :wide_char_size, "character step size when in double wide mode"), + TermCapability(:btns, :buttons, "number of buttons on mouse"), + TermCapability(:bitwin, :bit_image_entwining, "number of passes for each bit-image row"), + TermCapability(:bitype, :bit_image_type, "type of bit-image device"), + TermCapability(:OTug, :magic_cookie_glitch_ul, "number of blanks left by ul"), + TermCapability(:OTdC, :carriage_return_delay, "pad needed for CR"), + TermCapability(:OTdN, :new_line_delay, "pad needed for LF"), + TermCapability(:OTdB, :backspace_delay, "padding required for ^H"), + TermCapability(:OTdT, :horizontal_tab_delay, "padding required for ^I"), + TermCapability(:OTkn, :number_of_function_keys, "count of function keys"), +] + +""" +Ordered list of known terminal capability string fields, as of NCurses 6.3. +""" +const TERM_STRINGS = [ + TermCapability(:cbt, :back_tab, "back tab (P)"), + TermCapability(:bel, :bell, "audible signal (bell) (P)"), + TermCapability(:cr, :carriage_return, "carriage return (P*) (P*)"), + TermCapability(:csr, :change_scroll_region, "change region to line #1 to line #2 (P)"), + TermCapability(:tbc, :clear_all_tabs, "clear all tab stops (P)"), + TermCapability(:clear, :clear_screen, "clear screen and home cursor (P*)"), + TermCapability(:el, :clr_eol, "clear to end of line (P)"), + TermCapability(:ed, :clr_eos, "clear to end of screen (P*)"), + TermCapability(:hpa, :column_address, "horizontal position #1, absolute (P)"), + TermCapability(:cmdch, :command_character, "terminal settable cmd character in prototype !?"), + TermCapability(:cup, :cursor_address, "move to row #1 columns #2"), + TermCapability(:cud1, :cursor_down, "down one line"), + TermCapability(:home, :cursor_home, "home cursor (if no cup)"), + TermCapability(:civis, :cursor_invisible, "make cursor invisible"), + TermCapability(:cub1, :cursor_left, "move left one space"), + TermCapability(:mrcup, :cursor_mem_address, "memory relative cursor addressing, move to row #1 columns #2"), + TermCapability(:cnorm, :cursor_normal, "make cursor appear normal (undo civis/cvvis)"), + TermCapability(:cuf1, :cursor_right, "non-destructive space (move right one space)"), + TermCapability(:ll, :cursor_to_ll, "last line, first column (if no cup)"), + TermCapability(:cuu1, :cursor_up, "up one line"), + TermCapability(:cvvis, :cursor_visible, "make cursor very visible"), + TermCapability(:dch1, :delete_character, "delete character (P*)"), + TermCapability(:dl1, :delete_line, "delete line (P*)"), + TermCapability(:dsl, :dis_status_line, "disable status line"), + TermCapability(:hd, :down_half_line, "half a line down"), + TermCapability(:smacs, :enter_alt_charset_mode, "start alternate character set (P)"), + TermCapability(:blink, :enter_blink_mode, "turn on blinking"), + TermCapability(:bold, :enter_bold_mode, "turn on bold (extra bright) mode"), + TermCapability(:smcup, :enter_ca_mode, "string to start programs using cup"), + TermCapability(:smdc, :enter_delete_mode, "enter delete mode"), + TermCapability(:dim, :enter_dim_mode, "turn on half-bright mode"), + TermCapability(:smir, :enter_insert_mode, "enter insert mode"), + TermCapability(:invis, :enter_secure_mode, "turn on blank mode (characters invisible)"), + TermCapability(:prot, :enter_protected_mode, "turn on protected mode"), + TermCapability(:rev, :enter_reverse_mode, "turn on reverse video mode"), + TermCapability(:smso, :enter_standout_mode, "begin standout mode"), + TermCapability(:smul, :enter_underline_mode, "begin underline mode"), + TermCapability(:ech, :erase_chars, "erase #1 characters (P)"), + TermCapability(:rmacs, :exit_alt_charset_mode, "end alternate character set (P)"), + TermCapability(:sgr0, :exit_attribute_mode, "turn off all attributes"), + TermCapability(:rmcup, :exit_ca_mode, "strings to end programs using cup"), + TermCapability(:rmdc, :exit_delete_mode, "end delete mode"), + TermCapability(:rmir, :exit_insert_mode, "exit insert mode"), + TermCapability(:rmso, :exit_standout_mode, "exit standout mode"), + TermCapability(:rmul, :exit_underline_mode, "exit underline mode"), + TermCapability(:flash, :flash_screen, "visible bell (may not move cursor)"), + TermCapability(:ff, :form_feed, "hardcopy terminal page eject (P*)"), + TermCapability(:fsl, :from_status_line, "return from status line"), + TermCapability(:is1, :init_1string, "initialization string"), + TermCapability(:is2, :init_2string, "initialization string"), + TermCapability(:is3, :init_3string, "initialization string"), + TermCapability(:if, :init_file, "name of initialization file"), + TermCapability(:ich1, :insert_character, "insert character (P)"), + TermCapability(:il1, :insert_line, "insert line (P*)"), + TermCapability(:ip, :insert_padding, "insert padding after inserted character"), + TermCapability(:kbs, :key_backspace, "backspace key"), + TermCapability(:ktbc, :key_catab, "clear-all-tabs key"), + TermCapability(:kclr, :key_clear, "clear-screen or erase key"), + TermCapability(:kctab, :key_ctab, "clear-tab key"), + TermCapability(:kdch1, :key_dc, "delete-character key"), + TermCapability(:kdl1, :key_dl, "delete-line key"), + TermCapability(:kcud1, :key_down, "down-arrow key"), + TermCapability(:krmir, :key_eic, "sent by rmir or smir in insert mode"), + TermCapability(:kel, :key_eol, "clear-to-end-of-line key"), + TermCapability(:ked, :key_eos, "clear-to-end-of-screen key"), + TermCapability(:kf0, :key_f0, "F0 function key"), + TermCapability(:kf1, :key_f1, "F1 function key"), + TermCapability(:kf10, :key_f10, "F10 function key"), + TermCapability(:kf2, :key_f2, "F2 function key"), + TermCapability(:kf3, :key_f3, "F3 function key"), + TermCapability(:kf4, :key_f4, "F4 function key"), + TermCapability(:kf5, :key_f5, "F5 function key"), + TermCapability(:kf6, :key_f6, "F6 function key"), + TermCapability(:kf7, :key_f7, "F7 function key"), + TermCapability(:kf8, :key_f8, "F8 function key"), + TermCapability(:kf9, :key_f9, "F9 function key"), + TermCapability(:khome, :key_home, "home key"), + TermCapability(:kich1, :key_ic, "insert-character key"), + TermCapability(:kil1, :key_il, "insert-line key"), + TermCapability(:kcub1, :key_left, "left-arrow key"), + TermCapability(:kll, :key_ll, "lower-left key (home down)"), + TermCapability(:knp, :key_npage, "next-page key"), + TermCapability(:kpp, :key_ppage, "previous-page key"), + TermCapability(:kcuf1, :key_right, "right-arrow key"), + TermCapability(:kind, :key_sf, "scroll-forward key"), + TermCapability(:kri, :key_sr, "scroll-backward key"), + TermCapability(:khts, :key_stab, "set-tab key"), + TermCapability(:kcuu1, :key_up, "up-arrow key"), + TermCapability(:rmkx, :keypad_local, "leave 'keyboard_transmit' mode"), + TermCapability(:smkx, :keypad_xmit, "enter 'keyboard_transmit' mode"), + TermCapability(:lf0, :lab_f0, "label on function key f0 if not f0"), + TermCapability(:lf1, :lab_f1, "label on function key f1 if not f1"), + TermCapability(:lf10, :lab_f10, "label on function key f10 if not f10"), + TermCapability(:lf2, :lab_f2, "label on function key f2 if not f2"), + TermCapability(:lf3, :lab_f3, "label on function key f3 if not f3"), + TermCapability(:lf4, :lab_f4, "label on function key f4 if not f4"), + TermCapability(:lf5, :lab_f5, "label on function key f5 if not f5"), + TermCapability(:lf6, :lab_f6, "label on function key f6 if not f6"), + TermCapability(:lf7, :lab_f7, "label on function key f7 if not f7"), + TermCapability(:lf8, :lab_f8, "label on function key f8 if not f8"), + TermCapability(:lf9, :lab_f9, "label on function key f9 if not f9"), + TermCapability(:rmm, :meta_off, "turn off meta mode"), + TermCapability(:smm, :meta_on, "turn on meta mode (8th-bit on)"), + TermCapability(:nel, :newline, "newline (behave like cr followed by lf)"), + TermCapability(:pad, :pad_char, "padding char (instead of null)"), + TermCapability(:dch, :parm_dch, "delete #1 characters (P*)"), + TermCapability(:dl, :parm_delete_line, "delete #1 lines (P*)"), + TermCapability(:cud, :parm_down_cursor, "down #1 lines (P*)"), + TermCapability(:ich, :parm_ich, "insert #1 characters (P*)"), + TermCapability(:indn, :parm_index, "scroll forward #1 lines (P)"), + TermCapability(:il, :parm_insert_line, "insert #1 lines (P*)"), + TermCapability(:cub, :parm_left_cursor, "move #1 characters to the left (P)"), + TermCapability(:cuf, :parm_right_cursor, "move #1 characters to the right (P*)"), + TermCapability(:rin, :parm_rindex, "scroll back #1 lines (P)"), + TermCapability(:cuu, :parm_up_cursor, "up #1 lines (P*)"), + TermCapability(:pfkey, :pkey_key, "program function key #1 to type string #2"), + TermCapability(:pfloc, :pkey_local, "program function key #1 to execute string #2"), + TermCapability(:pfx, :pkey_xmit, "program function key #1 to transmit string #2"), + TermCapability(:mc0, :print_screen, "print contents of screen"), + TermCapability(:mc4, :prtr_off, "turn off printer"), + TermCapability(:mc5, :prtr_on, "turn on printer"), + TermCapability(:rep, :repeat_char, "repeat char #1 #2 times (P*)"), + TermCapability(:rs1, :reset_1string, "reset string"), + TermCapability(:rs2, :reset_2string, "reset string"), + TermCapability(:rs3, :reset_3string, "reset string"), + TermCapability(:rf, :reset_file, "name of reset file"), + TermCapability(:rc, :restore_cursor, "restore cursor to position of last save_cursor"), + TermCapability(:vpa, :row_address, "vertical position #1 absolute (P)"), + TermCapability(:sc, :save_cursor, "save current cursor position (P)"), + TermCapability(:ind, :scroll_forward, "scroll text up (P)"), + TermCapability(:ri, :scroll_reverse, "scroll text down (P)"), + TermCapability(:sgr, :set_attributes, "define video attributes #1-#9 (PG9)"), + TermCapability(:hts, :set_tab, "set a tab in every row, current columns"), + TermCapability(:wind, :set_window, "current window is lines #1-#2 cols #3-#4"), + TermCapability(:ht, :tab, "tab to next 8-space hardware tab stop"), + TermCapability(:tsl, :to_status_line, "move to status line, column #1"), + TermCapability(:uc, :underline_char, "underline char and move past it"), + TermCapability(:hu, :up_half_line, "half a line up"), + TermCapability(:iprog, :init_prog, "path name of program for initialization"), + TermCapability(:ka1, :key_a1, "upper left of keypad"), + TermCapability(:ka3, :key_a3, "upper right of keypad"), + TermCapability(:kb2, :key_b2, "center of keypad"), + TermCapability(:kc1, :key_c1, "lower left of keypad"), + TermCapability(:kc3, :key_c3, "lower right of keypad"), + TermCapability(:mc5p, :prtr_non, "turn on printer for #1 bytes"), + TermCapability(:rmp, :char_padding, "like ip but when in insert mode"), + TermCapability(:acsc, :acs_chars, "graphics charset pairs, based on vt100"), + TermCapability(:pln, :plab_norm, "program label #1 to show string #2"), + TermCapability(:kcbt, :key_btab, "back-tab key"), + TermCapability(:smxon, :enter_xon_mode, "turn on xon/xoff handshaking"), + TermCapability(:rmxon, :exit_xon_mode, "turn off xon/xoff handshaking"), + TermCapability(:smam, :enter_am_mode, "turn on automatic margins"), + TermCapability(:rmam, :exit_am_mode, "turn off automatic margins"), + TermCapability(:xonc, :xon_character, "XON character"), + TermCapability(:xoffc, :xoff_character, "XOFF character"), + TermCapability(:enacs, :ena_acs, "enable alternate char set"), + TermCapability(:smln, :label_on, "turn on soft labels"), + TermCapability(:rmln, :label_off, "turn off soft labels"), + TermCapability(:kbeg, :key_beg, "begin key"), + TermCapability(:kcan, :key_cancel, "cancel key"), + TermCapability(:kclo, :key_close, "close key"), + TermCapability(:kcmd, :key_command, "command key"), + TermCapability(:kcpy, :key_copy, "copy key"), + TermCapability(:kcrt, :key_create, "create key"), + TermCapability(:kend, :key_end, "end key"), + TermCapability(:kent, :key_enter, "enter/send key"), + TermCapability(:kext, :key_exit, "exit key"), + TermCapability(:kfnd, :key_find, "find key"), + TermCapability(:khlp, :key_help, "help key"), + TermCapability(:kmrk, :key_mark, "mark key"), + TermCapability(:kmsg, :key_message, "message key"), + TermCapability(:kmov, :key_move, "move key"), + TermCapability(:knxt, :key_next, "next key"), + TermCapability(:kopn, :key_open, "open key"), + TermCapability(:kopt, :key_options, "options key"), + TermCapability(:kprv, :key_previous, "previous key"), + TermCapability(:kprt, :key_print, "print key"), + TermCapability(:krdo, :key_redo, "redo key"), + TermCapability(:kref, :key_reference, "reference key"), + TermCapability(:krfr, :key_refresh, "refresh key"), + TermCapability(:krpl, :key_replace, "replace key"), + TermCapability(:krst, :key_restart, "restart key"), + TermCapability(:kres, :key_resume, "resume key"), + TermCapability(:ksav, :key_save, "save key"), + TermCapability(:kspd, :key_suspend, "suspend key"), + TermCapability(:kund, :key_undo, "undo key"), + TermCapability(:kBEG, :key_sbeg, "shifted begin key"), + TermCapability(:kCAN, :key_scancel, "shifted cancel key"), + TermCapability(:kCMD, :key_scommand, "shifted command key"), + TermCapability(:kCPY, :key_scopy, "shifted copy key"), + TermCapability(:kCRT, :key_screate, "shifted create key"), + TermCapability(:kDC, :key_sdc, "shifted delete-character key"), + TermCapability(:kDL, :key_sdl, "shifted delete-line key"), + TermCapability(:kslt, :key_select, "select key"), + TermCapability(:kEND, :key_send, "shifted end key"), + TermCapability(:kEOL, :key_seol, "shifted clear-to-end-of-line key"), + TermCapability(:kEXT, :key_sexit, "shifted exit key"), + TermCapability(:kFND, :key_sfind, "shifted find key"), + TermCapability(:kHLP, :key_shelp, "shifted help key"), + TermCapability(:kHOM, :key_shome, "shifted home key"), + TermCapability(:kIC, :key_sic, "shifted insert-character key"), + TermCapability(:kLFT, :key_sleft, "shifted left-arrow key"), + TermCapability(:kMSG, :key_smessage, "shifted message key"), + TermCapability(:kMOV, :key_smove, "shifted move key"), + TermCapability(:kNXT, :key_snext, "shifted next key"), + TermCapability(:kOPT, :key_soptions, "shifted options key"), + TermCapability(:kPRV, :key_sprevious, "shifted previous key"), + TermCapability(:kPRT, :key_sprint, "shifted print key"), + TermCapability(:kRDO, :key_sredo, "shifted redo key"), + TermCapability(:kRPL, :key_sreplace, "shifted replace key"), + TermCapability(:kRIT, :key_sright, "shifted right-arrow key"), + TermCapability(:kRES, :key_srsume, "shifted resume key"), + TermCapability(:kSAV, :key_ssave, "shifted save key"), + TermCapability(:kSPD, :key_ssuspend, "shifted suspend key"), + TermCapability(:kUND, :key_sundo, "shifted undo key"), + TermCapability(:rfi, :req_for_input, "send next input char (for ptys)"), + TermCapability(:kf11, :key_f11, "F11 function key"), + TermCapability(:kf12, :key_f12, "F12 function key"), + TermCapability(:kf13, :key_f13, "F13 function key"), + TermCapability(:kf14, :key_f14, "F14 function key"), + TermCapability(:kf15, :key_f15, "F15 function key"), + TermCapability(:kf16, :key_f16, "F16 function key"), + TermCapability(:kf17, :key_f17, "F17 function key"), + TermCapability(:kf18, :key_f18, "F18 function key"), + TermCapability(:kf19, :key_f19, "F19 function key"), + TermCapability(:kf20, :key_f20, "F20 function key"), + TermCapability(:kf21, :key_f21, "F21 function key"), + TermCapability(:kf22, :key_f22, "F22 function key"), + TermCapability(:kf23, :key_f23, "F23 function key"), + TermCapability(:kf24, :key_f24, "F24 function key"), + TermCapability(:kf25, :key_f25, "F25 function key"), + TermCapability(:kf26, :key_f26, "F26 function key"), + TermCapability(:kf27, :key_f27, "F27 function key"), + TermCapability(:kf28, :key_f28, "F28 function key"), + TermCapability(:kf29, :key_f29, "F29 function key"), + TermCapability(:kf30, :key_f30, "F30 function key"), + TermCapability(:kf31, :key_f31, "F31 function key"), + TermCapability(:kf32, :key_f32, "F32 function key"), + TermCapability(:kf33, :key_f33, "F33 function key"), + TermCapability(:kf34, :key_f34, "F34 function key"), + TermCapability(:kf35, :key_f35, "F35 function key"), + TermCapability(:kf36, :key_f36, "F36 function key"), + TermCapability(:kf37, :key_f37, "F37 function key"), + TermCapability(:kf38, :key_f38, "F38 function key"), + TermCapability(:kf39, :key_f39, "F39 function key"), + TermCapability(:kf40, :key_f40, "F40 function key"), + TermCapability(:kf41, :key_f41, "F41 function key"), + TermCapability(:kf42, :key_f42, "F42 function key"), + TermCapability(:kf43, :key_f43, "F43 function key"), + TermCapability(:kf44, :key_f44, "F44 function key"), + TermCapability(:kf45, :key_f45, "F45 function key"), + TermCapability(:kf46, :key_f46, "F46 function key"), + TermCapability(:kf47, :key_f47, "F47 function key"), + TermCapability(:kf48, :key_f48, "F48 function key"), + TermCapability(:kf49, :key_f49, "F49 function key"), + TermCapability(:kf50, :key_f50, "F50 function key"), + TermCapability(:kf51, :key_f51, "F51 function key"), + TermCapability(:kf52, :key_f52, "F52 function key"), + TermCapability(:kf53, :key_f53, "F53 function key"), + TermCapability(:kf54, :key_f54, "F54 function key"), + TermCapability(:kf55, :key_f55, "F55 function key"), + TermCapability(:kf56, :key_f56, "F56 function key"), + TermCapability(:kf57, :key_f57, "F57 function key"), + TermCapability(:kf58, :key_f58, "F58 function key"), + TermCapability(:kf59, :key_f59, "F59 function key"), + TermCapability(:kf60, :key_f60, "F60 function key"), + TermCapability(:kf61, :key_f61, "F61 function key"), + TermCapability(:kf62, :key_f62, "F62 function key"), + TermCapability(:kf63, :key_f63, "F63 function key"), + TermCapability(:el1, :clr_bol, "Clear to beginning of line"), + TermCapability(:mgc, :clear_margins, "clear right and left soft margins"), + TermCapability(:smgl, :set_left_margin, "set left soft margin at current column. (ML is not in BSD termcap)."), + TermCapability(:smgr, :set_right_margin, "set right soft margin at current column"), + TermCapability(:fln, :label_format, "label format"), + TermCapability(:sclk, :set_clock, "set clock, #1 hrs #2 mins #3 secs"), + TermCapability(:dclk, :display_clock, "display clock"), + TermCapability(:rmclk, :remove_clock, "remove clock"), + TermCapability(:cwin, :create_window, "define a window #1 from #2, #3 to #4, #5"), + TermCapability(:wingo, :goto_window, "go to window #1"), + TermCapability(:hup, :hangup, "hang-up phone"), + TermCapability(:dial, :dial_phone, "dial number #1"), + TermCapability(:qdial, :quick_dial, "dial number #1 without checking"), + TermCapability(:tone, :tone, "select touch tone dialing"), + TermCapability(:pulse, :pulse, "select pulse dialing"), + TermCapability(:hook, :flash_hook, "flash switch hook"), + TermCapability(:pause, :fixed_pause, "pause for 2-3 seconds"), + TermCapability(:wait, :wait_tone, "wait for dial-tone"), + TermCapability(:u0, :user0, "User string #0"), + TermCapability(:u1, :user1, "User string #1"), + TermCapability(:u2, :user2, "User string #2"), + TermCapability(:u3, :user3, "User string #3"), + TermCapability(:u4, :user4, "User string #4"), + TermCapability(:u5, :user5, "User string #5"), + TermCapability(:u6, :user6, "User string #6"), + TermCapability(:u7, :user7, "User string #7"), + TermCapability(:u8, :user8, "User string #8"), + TermCapability(:u9, :user9, "User string #9"), + TermCapability(:op, :orig_pair, "Set default pair to its original value"), + TermCapability(:oc, :orig_colors, "Set all color pairs to the original ones"), + TermCapability(:initc, :initialize_color, "Initialize color #1 to (#2, #3, #4)"), + TermCapability(:initp, :initialize_pair, "Initialize color pair #1 to fg=(#2, #3, #4), bg=(#5,#6,#7)"), + TermCapability(:scp, :set_color_pair, "Set current color pair to #1"), + TermCapability(:setf, :set_foreground, "Set foreground color #1"), + TermCapability(:setb, :set_background, "Set background color #1"), + TermCapability(:cpi, :change_char_pitch, "Change number of characters per inch to #1"), + TermCapability(:lpi, :change_line_pitch, "Change number of lines per inch to #1"), + TermCapability(:chr, :change_res_horz, "Change horizontal resolution to #1"), + TermCapability(:cvr, :change_res_vert, "Change vertical resolution to #1"), + TermCapability(:defc, :define_char, "Define a character #1, #2 dots wide, descender #3"), + TermCapability(:swidm, :enter_doublewide_mode, "Enter double-wide mode"), + TermCapability(:sdrfq, :enter_draft_quality, "Enter draft-quality mode"), + TermCapability(:sitm, :enter_italics_mode, "Enter italic mode"), + TermCapability(:slm, :enter_leftward_mode, "Start leftward carriage motion"), + TermCapability(:smicm, :enter_micro_mode, "Start micro-motion mode"), + TermCapability(:snlq, :enter_near_letter_quality, "Enter NLQ mode"), + TermCapability(:snrmq, :enter_normal_quality, "Enter normal-quality mode"), + TermCapability(:sshm, :enter_shadow_mode, "Enter shadow-print mode"), + TermCapability(:ssubm, :enter_subscript_mode, "Enter subscript mode"), + TermCapability(:ssupm, :enter_superscript_mode, "Enter superscript mode"), + TermCapability(:sum, :enter_upward_mode, "Start upward carriage motion"), + TermCapability(:rwidm, :exit_doublewide_mode, "End double-wide mode"), + TermCapability(:ritm, :exit_italics_mode, "End italic mode"), + TermCapability(:rlm, :exit_leftward_mode, "End left-motion mode"), + TermCapability(:rmicm, :exit_micro_mode, "End micro-motion mode"), + TermCapability(:rshm, :exit_shadow_mode, "End shadow-print mode"), + TermCapability(:rsubm, :exit_subscript_mode, "End subscript mode"), + TermCapability(:rsupm, :exit_superscript_mode, "End superscript mode"), + TermCapability(:rum, :exit_upward_mode, "End reverse character motion"), + TermCapability(:mhpa, :micro_column_address, "Like column_address in micro mode"), + TermCapability(:mcud1, :micro_down, "Like cursor_down in micro mode"), + TermCapability(:mcub1, :micro_left, "Like cursor_left in micro mode"), + TermCapability(:mcuf1, :micro_right, "Like cursor_right in micro mode"), + TermCapability(:mvpa, :micro_row_address, "Like row_address #1 in micro mode"), + TermCapability(:mcuu1, :micro_up, "Like cursor_up in micro mode"), + TermCapability(:porder, :order_of_pins, "Match software bits to print-head pins"), + TermCapability(:mcud, :parm_down_micro, "Like parm_down_cursor in micro mode"), + TermCapability(:mcub, :parm_left_micro, "Like parm_left_cursor in micro mode"), + TermCapability(:mcuf, :parm_right_micro, "Like parm_right_cursor in micro mode"), + TermCapability(:mcuu, :parm_up_micro, "Like parm_up_cursor in micro mode"), + TermCapability(:scs, :select_char_set, "Select character set, #1"), + TermCapability(:smgb, :set_bottom_margin, "Set bottom margin at current line"), + TermCapability(:smgbp, :set_bottom_margin_parm, "Set bottom margin at line #1 or (if smgtp is not given) #2 lines from bottom"), + TermCapability(:smglp, :set_left_margin_parm, "Set left (right) margin at column #1"), + TermCapability(:smgrp, :set_right_margin_parm, "Set right margin at column #1"), + TermCapability(:smgt, :set_top_margin, "Set top margin at current line"), + TermCapability(:smgtp, :set_top_margin_parm, "Set top (bottom) margin at row #1"), + TermCapability(:sbim, :start_bit_image, "Start printing bit image graphics"), + TermCapability(:scsd, :start_char_set_def, "Start character set definition #1, with #2 characters in the set"), + TermCapability(:rbim, :stop_bit_image, "Stop printing bit image graphics"), + TermCapability(:rcsd, :stop_char_set_def, "End definition of character set #1"), + TermCapability(:subcs, :subscript_characters, "List of subscriptable characters"), + TermCapability(:supcs, :superscript_characters, "List of superscriptable characters"), + TermCapability(:docr, :these_cause_cr, "Printing any of these characters causes CR"), + TermCapability(:zerom, :zero_motion, "No motion for subsequent character"), + TermCapability(:csnm, :char_set_names, "Produce #1'th item from list of character set names"), + TermCapability(:kmous, :key_mouse, "Mouse event has occurred"), + TermCapability(:minfo, :mouse_info, "Mouse status information"), + TermCapability(:reqmp, :req_mouse_pos, "Request mouse position"), + TermCapability(:getm, :get_mouse, "Curses should get button events, parameter #1 not documented."), + TermCapability(:setaf, :set_a_foreground, "Set foreground color to #1, using ANSI escape"), + TermCapability(:setab, :set_a_background, "Set background color to #1, using ANSI escape"), + TermCapability(:pfxl, :pkey_plab, "Program function key #1 to type string #2 and show string #3"), + TermCapability(:devt, :device_type, "Indicate language/codeset support"), + TermCapability(:csin, :code_set_init, "Init sequence for multiple codesets"), + TermCapability(:s0ds, :set0_des_seq, "Shift to codeset 0 (EUC set 0, ASCII)"), + TermCapability(:s1ds, :set1_des_seq, "Shift to codeset 1"), + TermCapability(:s2ds, :set2_des_seq, "Shift to codeset 2"), + TermCapability(:s3ds, :set3_des_seq, "Shift to codeset 3"), + TermCapability(:smglr, :set_lr_margin, "Set both left and right margins to #1, #2. (ML is not in BSD termcap)."), + TermCapability(:smgtb, :set_tb_margin, "Sets both top and bottom margins to #1, #2"), + TermCapability(:birep, :bit_image_repeat, "Repeat bit image cell #1 #2 times"), + TermCapability(:binel, :bit_image_newline, "Move to next row of the bit image"), + TermCapability(:bicr, :bit_image_carriage_return, "Move to beginning of same row"), + TermCapability(:colornm, :color_names, "Give name for color #1"), + TermCapability(:defbi, :define_bit_image_region, "Define rectangular bit image region"), + TermCapability(:endbi, :end_bit_image_region, "End a bit-image region"), + TermCapability(:setcolor, :set_color_band, "Change to ribbon color #1"), + TermCapability(:slines, :set_page_length, "Set page length to #1 lines"), + TermCapability(:dispc, :display_pc_char, "Display PC character #1"), + TermCapability(:smpch, :enter_pc_charset_mode, "Enter PC character display mode"), + TermCapability(:rmpch, :exit_pc_charset_mode, "Exit PC character display mode"), + TermCapability(:smsc, :enter_scancode_mode, "Enter PC scancode mode"), + TermCapability(:rmsc, :exit_scancode_mode, "Exit PC scancode mode"), + TermCapability(:pctrm, :pc_term_options, "PC terminal options"), + TermCapability(:scesc, :scancode_escape, "Escape for scancode emulation"), + TermCapability(:scesa, :alt_scancode_esc, "Alternate escape for scancode emulation"), + TermCapability(:ehhlm, :enter_horizontal_hl_mode, "Enter horizontal highlight mode"), + TermCapability(:elhlm, :enter_left_hl_mode, "Enter left highlight mode"), + TermCapability(:elohlm, :enter_low_hl_mode, "Enter low highlight mode"), + TermCapability(:erhlm, :enter_right_hl_mode, "Enter right highlight mode"), + TermCapability(:ethlm, :enter_top_hl_mode, "Enter top highlight mode"), + TermCapability(:evhlm, :enter_vertical_hl_mode, "Enter vertical highlight mode"), + TermCapability(:sgr1, :set_a_attributes, "Define second set of video attributes #1-#6"), + TermCapability(:slength, :set_pglen_inch, "Set page length to #1 hundredth of an inch (some implementations use sL for termcap)."), + TermCapability(:OTi2, :termcap_init2, "secondary initialization string"), + TermCapability(:OTrs, :termcap_reset, "terminal reset string"), + TermCapability(:OTnl, :linefeed_if_not_lf, "use to move down"), + TermCapability(:OTbs, :backspaces_with_bs, "uses ^H to move left"), + TermCapability(:OTko, :other_non_function_keys, "list of self-mapped keycaps"), + TermCapability(:OTma, :arrow_key_map, "map motion-keys for vi version 2"), + TermCapability(:OTG2, :acs_ulcorner, "single upper left"), + TermCapability(:OTG3, :acs_llcorner, "single lower left"), + TermCapability(:OTG1, :acs_urcorner, "single upper right"), + TermCapability(:OTG4, :acs_lrcorner, "single lower right"), + TermCapability(:OTGR, :acs_ltee, "tee pointing right"), + TermCapability(:OTGL, :acs_rtee, "tee pointing left"), + TermCapability(:OTGU, :acs_btee, "tee pointing up"), + TermCapability(:OTGD, :acs_ttee, "tee pointing down"), + TermCapability(:OTGH, :acs_hline, "single horizontal line"), + TermCapability(:OTGV, :acs_vline, "single vertical line"), + TermCapability(:OTGC, :acs_plus, "single intersection"), + TermCapability(:meml, :memory_lock, "lock memory above cursor"), + TermCapability(:memu, :memory_unlock, "unlock memory"), + TermCapability(:box1, :box_chars_1, "box characters primary set"), +] diff --git a/test/runtests.jl b/test/runtests.jl index 97db4ede..369563ea 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,6 +3,7 @@ using Test, StyledStrings import StyledStrings: SimpleColor, Face +import StyledStrings: AnnotatedString @testset "SimpleColor" begin @test SimpleColor(:hey).value == :hey # no error @@ -151,34 +152,34 @@ end @testset "Styled string macro" begin # Preservation of an unstyled string - @test styled"some string" == Base.AnnotatedString("some string") + @test styled"some string" == AnnotatedString("some string") # Basic styled constructs - @test styled"{thing=val:some} string" == Base.AnnotatedString("some string", [(1:4, :thing => "val")]) - @test styled"some {thing=val:string}" == Base.AnnotatedString("some string", [(6:11, :thing => "val")]) - @test styled"some {a=1:s}trin{b=2:g}" == Base.AnnotatedString("some string", [(6:6, :a => "1"), (11:11, :b => "2")]) - @test styled"{thing=val with spaces:some} string" == Base.AnnotatedString("some string", [(1:4, :thing => "val with spaces")]) - @test styled"{aface:some} string" == Base.AnnotatedString("some string", [(1:4, :face => :aface)]) + @test styled"{thing=val:some} string" == AnnotatedString("some string", [(1:4, :thing => "val")]) + @test styled"some {thing=val:string}" == AnnotatedString("some string", [(6:11, :thing => "val")]) + @test styled"some {a=1:s}trin{b=2:g}" == AnnotatedString("some string", [(6:6, :a => "1"), (11:11, :b => "2")]) + @test styled"{thing=val with spaces:some} string" == AnnotatedString("some string", [(1:4, :thing => "val with spaces")]) + @test styled"{aface:some} string" == AnnotatedString("some string", [(1:4, :face => :aface)]) @test styled"{aface,bface:some} string" == - Base.AnnotatedString("some string", [(1:4, :face => :aface), (1:4, :face => :bface)]) + AnnotatedString("some string", [(1:4, :face => :aface), (1:4, :face => :bface)]) # Inline face attributes @test styled"{(slant=italic):some} string" == - Base.AnnotatedString("some string", [(1:4, :face => Face(slant=:italic))]) + AnnotatedString("some string", [(1:4, :face => Face(slant=:italic))]) @test styled"{(foreground=magenta,background=#555555):some} string" == - Base.AnnotatedString("some string", [(1:4, :face => Face(foreground=:magenta, background=0x555555))]) + AnnotatedString("some string", [(1:4, :face => Face(foreground=:magenta, background=0x555555))]) # Curly bracket escaping - @test styled"some \{string" == Base.AnnotatedString("some {string") - @test styled"some string\}" == Base.AnnotatedString("some string}") - @test styled"some \{string\}" == Base.AnnotatedString("some {string}") - @test styled"some \{str:ing\}" == Base.AnnotatedString("some {str:ing}") - @test styled"some \{{bold:string}\}" == Base.AnnotatedString("some {string}", [(7:12, :face => :bold)]) - @test styled"some {bold:string \{other\}}" == Base.AnnotatedString("some string {other}", [(6:19, :face => :bold)]) + @test styled"some \{string" == AnnotatedString("some {string") + @test styled"some string\}" == AnnotatedString("some string}") + @test styled"some \{string\}" == AnnotatedString("some {string}") + @test styled"some \{str:ing\}" == AnnotatedString("some {str:ing}") + @test styled"some \{{bold:string}\}" == AnnotatedString("some {string}", [(7:12, :face => :bold)]) + @test styled"some {bold:string \{other\}}" == AnnotatedString("some string {other}", [(6:19, :face => :bold)]) # Nesting @test styled"{bold:nest{italic:ed st{red:yling}}}" == - Base.AnnotatedString( + AnnotatedString( "nested styling", [(1:14, :face => :bold), (5:14, :face => :italic), (10:14, :face => :red)]) - # Production of a `(Base.AnnotatedString)` value instead of an expression when possible - @test Base.AnnotatedString("val") == @macroexpand styled"val" - @test Base.AnnotatedString("val", [(1:3, :face => :style)]) == @macroexpand styled"{style:val}" + # Production of a `(AnnotatedString)` value instead of an expression when possible + @test AnnotatedString("val") == @macroexpand styled"val" + @test AnnotatedString("val", [(1:3, :face => :style)]) == @macroexpand styled"{style:val}" # Interpolation let annotatedstring = GlobalRef(StyledStrings, :annotatedstring) AnnotatedString = GlobalRef(StyledStrings, :AnnotatedString) @@ -208,23 +209,23 @@ end end # newlines - normal = "abc\ - def" styled = styled"abc\ def" - @test normal == styled == "abcdef" + @test styled == "abcdef" normal = "abc\\ndef" styled = styled"abc\\ndef" @test normal == styled == "abc\\ndef" - normal = eval(Meta.parse("\"abc\\\n \tdef\"")) - styled = eval(Meta.parse("styled\"abc\\\n \tdef\"")) - @test normal == styled == "abcdef" + if VERSION >= v"1.7" + normal = eval(Meta.parse("\"abc\\\n \tdef\"")) + styled = eval(Meta.parse("styled\"abc\\\n \tdef\"")) + @test normal == styled == "abcdef" - normal = eval(Meta.parse("\"abc\\\r\n def\"")) - styled = eval(Meta.parse("styled\"abc\\\r\n def\"")) - @test normal == styled == "abcdef" + normal = eval(Meta.parse("\"abc\\\r\n def\"")) + styled = eval(Meta.parse("styled\"abc\\\r\n def\"")) + @test normal == styled == "abcdef" + end end @testset "Legacy" begin