From 056e843b2d428bb9735b03af0cff97e738ac7e14 Mon Sep 17 00:00:00 2001 From: TEC Date: Thu, 12 Sep 2024 01:47:01 +0800 Subject: [PATCH] Adjust to change of annotations type in Base By popular demand, the type of an annotation is changing from a Tuple{UnitRange{Int}, Pair{Symbol, Any}} to a NamedTuple{(:region, :label, :value), Tuple{UnitRange{Int}, Symbol, Any}}. This necessitates some adjustments in the StyledStrings codebase. --- docs/src/index.md | 6 +- src/faces.jl | 18 ++--- src/io.jl | 5 +- src/regioniterator.jl | 31 ++++---- src/styledmarkup.jl | 86 +++++++++++----------- test/runtests.jl | 161 +++++++++++++++++++++--------------------- 6 files changed, 156 insertions(+), 151 deletions(-) diff --git a/docs/src/index.md b/docs/src/index.md index 04f6c62..984582d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -68,7 +68,7 @@ When concatenating a [`AnnotatedString`](@ref Base.AnnotatedString), take care t to keep the string annotations. ```jldoctest -julia> str = AnnotatedString("hello there", [(1:5, :word => :greeting), (7:11, :label => 1)]) +julia> str = AnnotatedString("hello there", [(1:5, :word, :greeting), (7:11, :label, 1)]) "hello there" julia> length(str) @@ -80,7 +80,7 @@ julia> lpad(str, 14) julia> typeof(lpad(str, 7)) AnnotatedString{String} -julia> str2 = AnnotatedString(" julia", [(2:6, :face => :magenta)]) +julia> str2 = AnnotatedString(" julia", [(2:6, :face, :magenta)]) " julia" julia> annotatedstring(str, str2) @@ -206,7 +206,7 @@ them to the properties list afterwards, or use the convenient [Styled String literals](@ref stdlib-styledstring-literals). ```@repl demo -str1 = AnnotatedString("blue text", [(1:9, :face => :blue)]) +str1 = AnnotatedString("blue text", [(1:9, :face, :blue)]) str2 = styled"{blue:blue text}" str1 == str2 sprint(print, str1, context = :color => true) diff --git a/src/faces.jl b/src/faces.jl index 6dcbd09..eedc95c 100644 --- a/src/faces.jl +++ b/src/faces.jl @@ -187,7 +187,7 @@ function Base.show(io::IO, ::MIME"text/plain", color::SimpleColor) skiptype || show(io, SimpleColor) skiptype || print(io, '(') if get(io, :color, false)::Bool - print(io, AnnotatedString("■", [(1:1, :face => Face(foreground=color))]), ' ') + print(io, AnnotatedString("■", [(region=1:1, label=:face, value=Face(foreground=color))]), ' ') end if color.value isa RGBTuple (; r, g, b) = color.value @@ -222,7 +222,7 @@ function Base.show(io::IO, ::MIME"text/plain", face::Face) show(io, Face) if get(io, :color, false)::Bool # Could do styled"({$face:sample})", but S_str isn't defined yet - print(io, AnnotatedString("(sample)", [(2:7, :face => face)])) + print(io, AnnotatedString("(sample)", [(region=2:7, label=:face, value=face)])) # elseif get(io, :limit, false)::Bool # print(io, "(…)") else @@ -244,7 +244,7 @@ function Base.show(io::IO, ::MIME"text/plain", face::Face) end else show(io, Face) - print(io, AnnotatedString(" (sample)", [(3:8, :face => face)])) + print(io, AnnotatedString(" (sample)", [(region=3:8, label=:face, value=face)])) showcolor(io, color) = show(IOContext(io, :typeinfo => SimpleColor), MIME("text/plain"), color) setfields = Pair{Symbol, Any}[] @@ -286,7 +286,7 @@ function Base.show(io::IO, ::MIME"text/plain", face::Face) isfirst = true for iface in face.inherit if isfirst; isfirst = false else print(io, ", ") end - print(io, iface, '(', AnnotatedString("*", [(1:1, :face => iface)]), ')') + print(io, iface, '(', AnnotatedString("*", [(region=1:1, label=:face, value=iface)]), ')') end end end @@ -555,12 +555,12 @@ function getface(faces) end """ - getface(annotations::Vector{Pair{Symbol, Any}}) + getface(annotations::Vector{@NamedTuple{label::Symbol, value::Any}}) Combine all of the `:face` annotations with `getfaces`. """ -function getface(annotations::Vector{Pair{Symbol, Any}}) - faces = (last(annot) for annot in annotations if first(annot) === :face) +function getface(annotations::Vector{@NamedTuple{label::Symbol, value::Any}}) + faces = (ann.value for ann in annotations if ann.label === :face) getface(faces) end @@ -599,11 +599,11 @@ Apply `face` to `str`, along `range` if specified or the whole of `str`. """ face!(s::Union{<:AnnotatedString, <:SubString{<:AnnotatedString}}, range::UnitRange{Int}, face::Union{Symbol, Face, <:Vector{<:Union{Symbol, Face}}}) = - annotate!(s, range, :face => face) + annotate!(s, range, :face, face) face!(s::Union{<:AnnotatedString, <:SubString{<:AnnotatedString}}, face::Union{Symbol, Face, <:Vector{<:Union{Symbol, Face}}}) = - annotate!(s, firstindex(s):lastindex(s), :face => face) + annotate!(s, firstindex(s):lastindex(s), :face, face) ## Reading face definitions from a dictionary ## diff --git a/src/io.jl b/src/io.jl index 2f32670..a88c312 100644 --- a/src/io.jl +++ b/src/io.jl @@ -287,7 +287,10 @@ function Base.show(io::IO, c::AnnotatedChar) if get(io, :color, false) == true out = IOBuffer() show(out, c.char) - print(io, ''', AnnotatedString(String(take!(out)[2:end-1]), map(a -> (1:ncodeunits(c), a), c.annotations)), ''') + cstr = AnnotatedString( + String(take!(out)[2:end-1]), + [(1:ncodeunits(c), a...) for a in c.annotations]) + print(io, ''', cstr, ''') else show(io, c.char) end diff --git a/src/regioniterator.jl b/src/regioniterator.jl index 4cbeccf..6a35264 100644 --- a/src/regioniterator.jl +++ b/src/regioniterator.jl @@ -3,7 +3,7 @@ struct RegionIterator{S <: AbstractString} str::S regions::Vector{UnitRange{Int}} - annotations::Vector{Vector{Pair{Symbol, Any}}} + annotations::Vector{Vector{@NamedTuple{label::Symbol, value::Any}}} end Base.length(si::RegionIterator) = length(si.regions) @@ -15,7 +15,7 @@ Base.@propagate_inbounds function Base.iterate(si::RegionIterator, i::Integer=1) end Base.eltype(::RegionIterator{S}) where { S <: AbstractString} = - Tuple{SubString{S}, Vector{Pair{Symbol, Any}}} + Tuple{SubString{S}, Vector{@NamedTuple{label::Symbol, value::Any}}} """ eachregion(s::AnnotatedString{S}) @@ -23,27 +23,28 @@ Base.eltype(::RegionIterator{S}) where { S <: AbstractString} = Identify the contiguous substrings of `s` with a constant annotations, and return an iterator which provides each substring and the applicable annotations as a -`Tuple{SubString{S}, Vector{Pair{Symbol, Any}}}`. +`Tuple{SubString{S}, Vector{@NamedTuple{label::Symbol, value::Any}}}`. # Examples ```jldoctest julia> collect(StyledStrings.eachregion(AnnotatedString( - "hey there", [(1:3, :face => :bold), (5:9, :face => :italic)]))) -3-element Vector{Tuple{SubString{String}, Vector{Pair{Symbol, Any}}}}: - ("hey", [:face => :bold]) + "hey there", [(1:3, :face, :bold), (5:9, :face, :italic)]))) +3-element Vector{Tuple{SubString{String}, Vector{@NamedTuple{label::Symbol, value}}}}: + ("hey", [@NamedTuple{label::Symbol, value}((:face, :bold))]) (" ", []) - ("there", [:face => :italic]) + ("there", [@NamedTuple{label::Symbol, value}((:face, :italic))]) ``` """ function eachregion(s::AnnotatedString, subregion::UnitRange{Int}=firstindex(s):lastindex(s)) isempty(s) || isempty(subregion) && - return RegionIterator(s.string, UnitRange{Int}[], Vector{Pair{Symbol, Any}}[]) + return RegionIterator(s.string, UnitRange{Int}[], Vector{@NamedTuple{label::Symbol, value::Any}}[]) events = annotation_events(s, subregion) - isempty(events) && return RegionIterator(s.string, [subregion], [Pair{Symbol, Any}[]]) - annotvals = last.(annotations(s)) + isempty(events) && return RegionIterator(s.string, [subregion], [@NamedTuple{label::Symbol, value::Any}[]]) + annotvals = @NamedTuple{label::Symbol, value::Any}[ + (; label, value) for (; label, value) in annotations(s)] regions = Vector{UnitRange{Int}}() - annots = Vector{Vector{Pair{Symbol, Any}}}() + annots = Vector{Vector{@NamedTuple{label::Symbol, value::Any}}}() pos = first(events).pos if pos > first(subregion) push!(regions, thisind(s, first(subregion)):prevind(s, pos)) @@ -71,14 +72,14 @@ end function eachregion(s::SubString{<:AnnotatedString}, pos::UnitRange{Int}=firstindex(s):lastindex(s)) if isempty(s) - RegionIterator(s.string, Vector{UnitRange{Int}}(), Vector{Vector{Pair{Symbol, Any}}}()) + RegionIterator(s.string, Vector{UnitRange{Int}}(), Vector{Vector{@NamedTuple{label::Symbol, value::Any}}}()) else eachregion(s.string, first(pos)+s.offset:last(pos)+s.offset) end end """ - annotation_events(string::AbstractString, annots::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}, subregion::UnitRange{Int}) + annotation_events(string::AbstractString, annots::Vector{@NamedTuple{region::UnitRange{Int}, label::Symbol, value::Any}}, subregion::UnitRange{Int}) annotation_events(string::AnnotatedString, subregion::UnitRange{Int}) Find all annotation "change events" that occur within a `subregion` of `annots`, @@ -89,9 +90,9 @@ index::Int}` where `pos` is the position of the event, `active` is a boolean indicating whether the annotation is being activated or deactivated, and `index` is the index of the annotation in question. """ -function annotation_events(s::AbstractString, annots::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}, subregion::UnitRange{Int}) +function annotation_events(s::AbstractString, annots::Vector{@NamedTuple{region::UnitRange{Int}, label::Symbol, value::Any}}, subregion::UnitRange{Int}) events = Vector{NamedTuple{(:pos, :active, :index), Tuple{Int, Bool, Int}}}() # Position, Active?, Annotation index - for (i, (region, _)) in enumerate(annots) + for (i, (; region)) in enumerate(annots) if !isempty(intersect(subregion, region)) start, stop = max(first(subregion), first(region)), min(last(subregion), last(region)) start <= stop || continue # Currently can't handle empty regions diff --git a/src/styledmarkup.jl b/src/styledmarkup.jl index 12c8435..e1edcd4 100644 --- a/src/styledmarkup.jl +++ b/src/styledmarkup.jl @@ -69,12 +69,12 @@ Its fields are as follows: - `parts::Vector{Any}`, the result of the parsing, a list of elements that when passed to `annotatedstring` produce the styled markup string. The types of its values are highly diverse, hence the `Any` element type. -- `active_styles::Vector{Vector{Tuple{Int, Int, Union{Symbol, Expr, Pair{Symbol, Any}}}}}}`, +- `active_styles::Vector{Vector{Tuple{Int, Int, Union{Symbol, Expr, Tuple{Symbol, Any}}}}}}`, A list of batches of styles that have yet to be applied to any content. Entries of a batch consist of `(source_position, start_position, style)` tuples, where `style` may be just - a symbol (referring to a face), a `Pair{Symbol, Any}` annotation, or an `Expr` that evaluates + a symbol (referring to a face), a `Tuple{Symbol, Any}` annotation, or an `Expr` that evaluates to a valid annotation (when `mod` is set). -- `pending_styles::Vector{Tuple{UnitRange{Int}, Union{Symbol, Expr, Pair{Symbol, Any}}}}`, +- `pending_styles::Vector{Tuple{UnitRange{Int}, Union{Symbol, Expr, Tuple{Symbol, Any}}}}`, A list of styles that have been terminated, and so are known to occur over a certain range, but have yet to be applied. - `offset::Int`, a record of the between the `content` index and the index in the resulting @@ -95,8 +95,8 @@ mutable struct State const s::Iterators.Stateful const mod::Union{Module, Nothing} const parts::Vector{Any} - const active_styles::Vector{Vector{Tuple{Int, Int, Union{Symbol, Expr, Pair{Symbol, Any}}}}} - const pending_styles::Vector{Tuple{UnitRange{Int}, Union{Symbol, Expr, Pair{Symbol, Any}}}} + const active_styles::Vector{Vector{Tuple{Int, Int, Union{Symbol, Expr, Tuple{Symbol, Any}}}}} + const pending_styles::Vector{Tuple{UnitRange{Int}, Union{Symbol, Expr, Tuple{Symbol, Any}}}} offset::Int point::Int escape::Bool @@ -108,8 +108,8 @@ function State(content::AbstractString, mod::Union{Module, Nothing}=nothing) State(content, Vector{UInt8}(content), # content, bytes Iterators.Stateful(pairs(content)), mod, # s, eval Any[], # parts - Vector{Tuple{Int, Int, Union{Symbol, Expr, Pair{Symbol, Any}}}}[], # active_styles - Tuple{UnitRange{Int}, Union{Symbol, Expr, Pair{Symbol, Any}}}[], # pending_styles + Vector{Tuple{Int, Int, Union{Symbol, Expr, Tuple{Symbol, Any}}}}[], # active_styles + Tuple{UnitRange{Int}, Union{Symbol, Expr, Tuple{Symbol, Any}}}[], # pending_styles 0, 1, # offset, point false, 0, # escape, interpolations NamedTuple{(:message, :position, :hint), # errors @@ -198,9 +198,9 @@ function addpart!(state::State, stop::Int) str = String(state.bytes[ state.point:stop+state.offset+ncodeunits(state.content[stop])-1]) sty_type, tupl = if ismacro(state) - Expr, (a, b) -> Expr(:tuple, a, b) + Expr, (r, lv) -> :(merge((; region=$r), NamedTuple{(:label, :value), Tuple{Symbol, Any}}($lv))) else - Tuple{UnitRange{Int}, Pair{Symbol, Any}}, (a, b) -> (a, b) + @NamedTuple{region::UnitRange{Int}, label::Symbol, value::Any}, (r, (l, v)) -> (; region=r, label=l, value=v) end push!(state.parts, if isempty(state.pending_styles) && isempty(state.active_styles) @@ -248,7 +248,8 @@ function addpart!(state::State, start::Int, expr, stop::Int) str = gensym("str") len = gensym("len") annots = Expr(:vect, [ - Expr(:tuple, Expr(:call, UnitRange, 1, len), annot) + :(NamedTuple{(:region, :label, :value), Tuple{UnitRange{Int}, Symbol, Any}}( + (1:$len, $annot...))) for annot in map(last, (Iterators.flatten( @@ -324,7 +325,7 @@ function readexpr!(state::State, pos::Int = first(popfirst!(state.s)) + 1) if isempty(state.s) styerr!(state, AnnotatedString("Identifier or parenthesised expression expected after \$ in string", - [(55:55, :face => :warning)]), + [(55:55, :face, :warning)]), -1, "right here") return "", pos end @@ -400,7 +401,7 @@ and register it in the active styles list. """ function begin_style!(state::State, i::Int, char::Char) hasvalue = false - newstyles = Vector{Tuple{Int, Int, Union{Symbol, Expr, Pair{Symbol, Any}}}}() + newstyles = Vector{Tuple{Int, Int, Union{Symbol, Expr, Tuple{Symbol, Any}}}}() while read_annotation!(state, i, char, newstyles) end push!(state.active_styles, reverse!(newstyles)) # Adjust bytes/offset based on how much the index @@ -534,9 +535,9 @@ function read_inlineface!(state::State, i::Int, char::Char, newstyles) valid_options = join(VALID_UNDERLINE_STYLES, ", ", ", or ") styerr!(state, AnnotatedString("Invalid underline style '$ustyle_word' (should be $valid_options)", - [(26:25+ncodeunits(ustyle_word), :face => :warning) - (28+ncodeunits(ustyle_word):39+ncodeunits(ustyle_word)+ncodeunits(valid_options), - :face => :light)]), + [(26:25+ncodeunits(ustyle_word), :face, :warning) + (28+ncodeunits(ustyle_word):39+ncodeunits(ustyle_word)+ncodeunits(valid_options), + :face, :light)]), -length(ustyle_word) - 3) end ustyle = Symbol(ustyle_word) @@ -657,7 +658,7 @@ function read_inlineface!(state::State, i::Int, char::Char, newstyles) else invalid, lastchar = readsymbol!(state, lastchar) styerr!(state, AnnotatedString("Invalid height '$invalid', should be a natural number or positive float", - [(17:16+ncodeunits(string(invalid)), :face => :warning)]), + [(17:16+ncodeunits(string(invalid)), :face, :warning)]), -3) end elseif key ∈ (:weight, :slant) @@ -665,16 +666,16 @@ function read_inlineface!(state::State, i::Int, char::Char, newstyles) if key == :weight && v ∉ VALID_WEIGHTS valid_options = join(VALID_WEIGHTS, ", ", ", or ") styerr!(state, AnnotatedString("Invalid weight '$v' (should be $valid_options)", - [(17:16+ncodeunits(v), :face => :warning), - (19+ncodeunits(v):30+ncodeunits(v)+ncodeunits(valid_options), - :face => :light)]), + [(17:16+ncodeunits(v), :face, :warning), + (19+ncodeunits(v):30+ncodeunits(v)+ncodeunits(valid_options), + :face, :light)]), -3) elseif key == :slant && v ∉ VALID_SLANTS valid_options = join(VALID_SLANTS, ", ", ", or ") styerr!(state, AnnotatedString("Invalid slant '$v' (should be $valid_options)", - [(16:15+ncodeunits(v), :face => :warning), - (18+ncodeunits(v):29+ncodeunits(v)+ncodeunits(valid_options), - :face => :light)]), + [(16:15+ncodeunits(v), :face, :warning), + (18+ncodeunits(v):29+ncodeunits(v)+ncodeunits(valid_options), + :face, :light)]), -3) end Symbol(v) |> if ismacro(state) QuoteNode else identity end @@ -701,7 +702,7 @@ function read_inlineface!(state::State, i::Int, char::Char, newstyles) else styerr!(state, AnnotatedString( "Uses unrecognised face key '$key'. Recognised keys are: $(join(VALID_FACE_ATTRS, ", ", ", and "))", - [(29:28+ncodeunits(String(key)), :face => :warning)]), + [(29:28+ncodeunits(String(key)), :face, :warning)]), -length(str_key) - 2) end if ismacro(state) && !any(k -> first(k.args) == key, kwargs) @@ -710,7 +711,7 @@ function read_inlineface!(state::State, i::Int, char::Char, newstyles) push!(kwargs, key => val) else styerr!(state, AnnotatedString("Contains repeated face key '$key'", - [(29:28+ncodeunits(String(key)), :face => :warning)]), + [(29:28+ncodeunits(String(key)), :face, :warning)]), -length(str_key) - 2) end isempty(state.s) && styerr!(state, "Incomplete inline face declaration", -1) @@ -726,11 +727,11 @@ function read_inlineface!(state::State, i::Int, char::Char, newstyles) push!(newstyles, (i, i + state.offset + 1, if !ismacro(state) - Pair{Symbol, Any}(:face, Face(; NamedTuple(kwargs)...)) + :face, Face(; NamedTuple(kwargs)...) elseif needseval - :(Pair{Symbol, Any}(:face, $face)) + :((:face, $face)) else - Pair{Symbol, Any}(:face, hygienic_eval(state, face)) + :face, hygienic_eval(state, face) end)) end @@ -802,20 +803,19 @@ function read_face_or_keyval!(state::State, i::Int, char::Char, newstyles) push!(newstyles, (i, i + state.offset + ncodeunits('{'), if key isa String && !(value isa Symbol || value isa Expr) - Pair{Symbol, Any}(Symbol(key), value) + Symbol(key), value elseif key isa Expr || key isa Symbol - :(Pair{Symbol, Any}($key, $value)) + :(($key, $value)) else - :(Pair{Symbol, Any}( - $(QuoteNode(Symbol(key))), $value)) + :(($(QuoteNode(Symbol(key))), $value)) end)) elseif key !== "" push!(newstyles, (i, i + state.offset + ncodeunits('{'), if key isa Symbol || key isa Expr - :(Pair{Symbol, Any}(:face, $key)) + :((:face, $key)) else # Face symbol - Pair{Symbol, Any}(:face, Symbol(key)) + :face, Symbol(key) end)) end if isempty(state.s) || last(peek(state.s)) ∉ (' ', '\t', '\n', '\r', ',', ':') @@ -857,7 +857,7 @@ function run_state_machine!(state::State) end for incomplete in Iterators.flatten(state.active_styles) styerr!(state, AnnotatedString("Unterminated annotation (missing closing '}')", - [(43:43, :face => :warning)]), + [(43:43, :face, :warning)]), prevind(state.content, first(incomplete)), "starts here") end end @@ -869,24 +869,22 @@ Merge contiguous identical annotations in `str`. """ function annotatedstring_optimize!(s::AnnotatedString) length(s.annotations) <= 1 && return s - last_seen = Dict{Pair{Symbol, Any}, Int}() + last_seen = Dict{Tuple{Symbol, Any}, Int}() i = 1 while i <= length(s.annotations) - region, keyval = s.annotations[i] - prev = get(last_seen, keyval, 0) + ann = s.annotations[i] + prev = get(last_seen, (ann.label, ann.value), 0) if prev > 0 - lregion, _ = s.annotations[prev] - if last(lregion) + 1 == first(region) + lregion = s.annotations[prev].region + if last(lregion) + 1 == first(ann.region) s.annotations[prev] = - Base.setindex(s.annotations[prev], - first(lregion):last(region), - 1) + merge(s.annotations[prev], (; region=first(lregion):last(ann.region))) deleteat!(s.annotations, i) else - delete!(last_seen, keyval) + delete!(last_seen, (ann.label, ann.value)) end else - last_seen[keyval] = i + last_seen[(ann.label, ann.value)] = i i += 1 end end diff --git a/test/runtests.jl b/test/runtests.jl index c2baad5..006556e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -36,50 +36,50 @@ end choppkg(s::String) = chopprefix(s, "StyledStrings.") @testset "Eachregion" begin - annregions(str::String, annots::Vector{<:Tuple{UnitRange{Int}, <:Pair{Symbol, <:Any}}}) = - collect(eachregion(AnnotatedString(str, annots))) + annregions(str::String, annots::Vector{<:Tuple{UnitRange{Int}, Symbol, <:Any}}) = + [(s, Tuple.(a)) for (s, a) in eachregion(AnnotatedString(str, annots))] # Regions that do/don't extend to the left/right edges - @test annregions(" abc ", [(2:4, :face => :bold)]) == + @test annregions(" abc ", [(2:4, :face, :bold)]) == [(" ", []), - ("abc", [:face => :bold]), + ("abc", [(:face, :bold)]), (" ", [])] - @test annregions(" x ", [(2:2, :face => :bold)]) == + @test annregions(" x ", [(2:2, :face, :bold)]) == [(" ", []), - ("x", [:face => :bold]), + ("x", [(:face, :bold)]), (" ", [])] - @test annregions(" x", [(2:2, :face => :bold)]) == + @test annregions(" x", [(2:2, :face, :bold)]) == [(" ", []), - ("x", [:face => :bold])] - @test annregions("x ", [(1:1, :face => :bold)]) == - [("x", [:face => :bold]), + ("x", [(:face, :bold)])] + @test annregions("x ", [(1:1, :face, :bold)]) == + [("x", [(:face, :bold)]), (" ", [])] - @test annregions("x", [(1:1, :face => :bold)]) == - [("x", [:face => :bold])] + @test annregions("x", [(1:1, :face, :bold)]) == + [("x", [(:face, :bold)])] # Overlapping/nested regions - @test annregions(" abc ", [(2:4, :face => :bold), (3:3, :face => :italic)]) == + @test annregions(" abc ", [(2:4, :face, :bold), (3:3, :face, :italic)]) == [(" ", []), - ("a", [:face => :bold]), - ("b", [:face => :bold, :face => :italic]), - ("c", [:face => :bold]), + ("a", [(:face, :bold)]), + ("b", [(:face, :bold), (:face, :italic)]), + ("c", [(:face, :bold)]), (" ", [])] - @test annregions("abc-xyz", [(1:7, :face => :bold), (1:3, :face => :green), (4:4, :face => :yellow), (4:7, :face => :italic)]) == - [("abc", [:face => :bold, :face => :green]), - ("-", [:face => :bold, :face => :yellow, :face => :italic]), - ("xyz", [:face => :bold, :face => :italic])] + @test annregions("abc-xyz", [(1:7, :face, :bold), (1:3, :face, :green), (4:4, :face, :yellow), (4:7, :face, :italic)]) == + [("abc", [(:face, :bold), (:face, :green)]), + ("-", [(:face, :bold), (:face, :yellow), (:face, :italic)]), + ("xyz", [(:face, :bold), (:face, :italic)])] # Preserving annotation order - @test annregions("abcd", [(1:3, :face => :red), (2:2, :face => :yellow), (2:3, :face => :green), (2:4, :face => :blue)]) == - [("a", [:face => :red]), - ("b", [:face => :red, :face => :yellow, :face => :green, :face => :blue]), - ("c", [:face => :red, :face => :green, :face => :blue]), - ("d", [:face => :blue])] - @test annregions("abcd", [(2:4, :face => :blue), (1:3, :face => :red), (2:3, :face => :green), (2:2, :face => :yellow)]) == - [("a", [:face => :red]), - ("b", [:face => :blue, :face => :red, :face => :green, :face => :yellow]), - ("c", [:face => :blue, :face => :red, :face => :green]), - ("d", [:face => :blue])] + @test annregions("abcd", [(1:3, :face, :red), (2:2, :face, :yellow), (2:3, :face, :green), (2:4, :face, :blue)]) == + [("a", [(:face, :red)]), + ("b", [(:face, :red), (:face, :yellow), (:face, :green), (:face, :blue)]), + ("c", [(:face, :red), (:face, :green), (:face, :blue)]), + ("d", [(:face, :blue)])] + @test annregions("abcd", [(2:4, :face, :blue), (1:3, :face, :red), (2:3, :face, :green), (2:2, :face, :yellow)]) == + [("a", [(:face, :red)]), + ("b", [(:face, :blue), (:face, :red), (:face, :green), (:face, :yellow)]), + ("c", [(:face, :blue), (:face, :red), (:face, :green)]), + ("d", [(:face, :blue)])] # Region starting after a character spanning multiple codepoints. - @test annregions("𝟏x", [(1:4, :face => :red)]) == - [("𝟏", [:face => :red]), + @test annregions("𝟏x", [(1:4, :face, :red)]) == + [("𝟏", [(:face, :red)]), ("x", [])] end @@ -338,97 +338,100 @@ end # Preservation of an unstyled string @test styled"some string" == AnnotatedString("some string") # Basic styled constructs - @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"{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)]) # Annotation prioritisation @test styled"{aface,bface:some} string" == - AnnotatedString("some string", [(1:4, :face => :aface), (1:4, :face => :bface)]) + AnnotatedString("some string", [(1:4, :face, :aface), (1:4, :face, :bface)]) @test styled"{aface:{bface:some}} string" == - AnnotatedString("some string", [(1:4, :face => :aface), (1:4, :face => :bface)]) + AnnotatedString("some string", [(1:4, :face, :aface), (1:4, :face, :bface)]) @test styled"{aface,bface:$(1)} string" == - AnnotatedString("1 string", [(1:1, :face => :aface), (1:1, :face => :bface)]) + AnnotatedString("1 string", [(1:1, :face, :aface), (1:1, :face, :bface)]) @test styled"{aface:{bface:$(1)}} string" == - AnnotatedString("1 string", [(1:1, :face => :aface), (1:1, :face => :bface)]) + AnnotatedString("1 string", [(1:1, :face, :aface), (1:1, :face, :bface)]) # Inline face attributes @test styled"{(slant=italic):some} string" == - 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" == - AnnotatedString("some string", [(1:4, :face => Face(foreground=:magenta, background=0x555555))]) + AnnotatedString("some string", [(1:4, :face, Face(foreground=:magenta, background=0x555555))]) # Inline face attributes: empty attribute lists are legal @test styled"{():}" == styled"{( ):}" == AnnotatedString("") # Inline face attributes: leading/trailing whitespace - @test styled"{ ( fg=red , ) :a}" == AnnotatedString("a", [(1:1, :face => Face(foreground=:red))]) + @test styled"{ ( fg=red , ) :a}" == AnnotatedString("a", [(1:1, :face, Face(foreground=:red))]) # Inline face attributes: each recognised key - @test styled"{(font=serif):a}" == AnnotatedString("a", [(1:1, :face => Face(font="serif"))]) - @test styled"{(font=some serif):a}" == AnnotatedString("a", [(1:1, :face => Face(font="some serif"))]) - @test styled"{(font=\"some serif\"):a}" == AnnotatedString("a", [(1:1, :face => Face(font="some serif"))]) - @test styled"{(font=\"{},):\"):a}" == AnnotatedString("a", [(1:1, :face => Face(font="{},):"))]) - @test styled"{(height=120):a}" == AnnotatedString("a", [(1:1, :face => Face(height=120))]) - @test styled"{(height=1.2):a}" == AnnotatedString("a", [(1:1, :face => Face(height=1.2))]) - @test styled"{(weight=normal):a}" == AnnotatedString("a", [(1:1, :face => Face(weight=:normal))]) - @test styled"{(weight=bold):a}" == AnnotatedString("a", [(1:1, :face => Face(weight=:bold))]) - @test styled"{(slant=italic):a}" == AnnotatedString("a", [(1:1, :face => Face(slant=:italic))]) - @test styled"{(fg=red):a}" == AnnotatedString("a", [(1:1, :face => Face(foreground=:red))]) - @test styled"{(foreground=red):a}" == AnnotatedString("a", [(1:1, :face => Face(foreground=:red))]) - @test styled"{(bg=red):a}" == AnnotatedString("a", [(1:1, :face => Face(background=:red))]) - @test styled"{(background=red):a}" == AnnotatedString("a", [(1:1, :face => Face(background=:red))]) - @test styled"{(underline=true):a}" == AnnotatedString("a", [(1:1, :face => Face(underline=true))]) - @test styled"{(underline=cyan):a}" == AnnotatedString("a", [(1:1, :face => Face(underline=:cyan))]) - @test styled"{(underline=(cyan,curly)):a}" == AnnotatedString("a", [(1:1, :face => Face(underline=(:cyan, :curly)))]) - @test styled"{(strikethrough=true):a}" == AnnotatedString("a", [(1:1, :face => Face(strikethrough=true))]) - @test styled"{(inverse=true):a}" == AnnotatedString("a", [(1:1, :face => Face(inverse=true))]) - @test styled"{(inherit=b):a}" == AnnotatedString("a", [(1:1, :face => Face(inherit=:b))]) - @test styled"{(inherit=[b,c]):a}" == AnnotatedString("a", [(1:1, :face => Face(inherit=[:b, :c]))]) + @test styled"{(font=serif):a}" == AnnotatedString("a", [(1:1, :face, Face(font="serif"))]) + @test styled"{(font=some serif):a}" == AnnotatedString("a", [(1:1, :face, Face(font="some serif"))]) + @test styled"{(font=\"some serif\"):a}" == AnnotatedString("a", [(1:1, :face, Face(font="some serif"))]) + @test styled"{(font=\"{},):\"):a}" == AnnotatedString("a", [(1:1, :face, Face(font="{},):"))]) + @test styled"{(height=120):a}" == AnnotatedString("a", [(1:1, :face, Face(height=120))]) + @test styled"{(height=1.2):a}" == AnnotatedString("a", [(1:1, :face, Face(height=1.2))]) + @test styled"{(weight=normal):a}" == AnnotatedString("a", [(1:1, :face, Face(weight=:normal))]) + @test styled"{(weight=bold):a}" == AnnotatedString("a", [(1:1, :face, Face(weight=:bold))]) + @test styled"{(slant=italic):a}" == AnnotatedString("a", [(1:1, :face, Face(slant=:italic))]) + @test styled"{(fg=red):a}" == AnnotatedString("a", [(1:1, :face, Face(foreground=:red))]) + @test styled"{(foreground=red):a}" == AnnotatedString("a", [(1:1, :face, Face(foreground=:red))]) + @test styled"{(bg=red):a}" == AnnotatedString("a", [(1:1, :face, Face(background=:red))]) + @test styled"{(background=red):a}" == AnnotatedString("a", [(1:1, :face, Face(background=:red))]) + @test styled"{(underline=true):a}" == AnnotatedString("a", [(1:1, :face, Face(underline=true))]) + @test styled"{(underline=cyan):a}" == AnnotatedString("a", [(1:1, :face, Face(underline=:cyan))]) + @test styled"{(underline=(cyan,curly)):a}" == AnnotatedString("a", [(1:1, :face, Face(underline=(:cyan, :curly)))]) + @test styled"{(strikethrough=true):a}" == AnnotatedString("a", [(1:1, :face, Face(strikethrough=true))]) + @test styled"{(inverse=true):a}" == AnnotatedString("a", [(1:1, :face, Face(inverse=true))]) + @test styled"{(inherit=b):a}" == AnnotatedString("a", [(1:1, :face, Face(inherit=:b))]) + @test styled"{(inherit=[b,c]):a}" == AnnotatedString("a", [(1:1, :face, Face(inherit=[:b, :c]))]) # Curly bracket escaping @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)]) + @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}}}" == AnnotatedString( - "nested styling", [(1:14, :face => :bold), (5:14, :face => :italic), (10:14, :face => :red)]) + "nested styling", [(1:14, :face, :bold), (5:14, :face, :italic), (10:14, :face, :red)]) # 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}" + @test AnnotatedString("val", [(1:3, :face, :style)]) == @macroexpand styled"{style:val}" # Interpolation let annotatedstring = GlobalRef(StyledMarkup, :annotatedstring) AnnotatedString = GlobalRef(StyledMarkup, :AnnotatedString) annotatedstring_optimize! = GlobalRef(StyledMarkup, :annotatedstring_optimize!) chain = GlobalRef(StyledMarkup, :|>) - Pair = GlobalRef(StyledMarkup, :Pair) - Symbol = GlobalRef(StyledMarkup, :Symbol) - Any = GlobalRef(StyledMarkup, :Any) + merge = GlobalRef(StyledMarkup, :merge) + Tuple = GlobalRef(StyledMarkup, :Tuple) + NamedTuple = GlobalRef(StyledMarkup, :NamedTuple) + Symbol = GlobalRef(StyledMarkup, :Symbol) + Any = GlobalRef(StyledMarkup, :Any) + NamedTupleLV = :($NamedTuple{(:label, :value), $Tuple{$Symbol, $Any}}) @test :($annotatedstring(val)) == @macroexpand styled"$val" @test :($chain($annotatedstring("a", val), $annotatedstring_optimize!)) == @macroexpand styled"a$val" @test :($chain($annotatedstring("a", val, "b"), $annotatedstring_optimize!)) == @macroexpand styled"a$(val)b" # @test :($annotatedstring(StyledStrings.AnnotatedString(string(val), $(Pair{Symbol, Any}(:face, :style))))) == # @macroexpand styled"{style:$val}" @test :($annotatedstring($AnnotatedString( - "val", [($(1:3), $Pair{$Symbol, $Any}(:face, face))]))) == + "val", [$merge((; region=$(1:3)), $NamedTupleLV((:face, face)))]))) == @macroexpand styled"{$face:val}" @test :($chain($annotatedstring($AnnotatedString( - "v1v2", [($(1:2), $Pair{$Symbol, $Any}(:face, f1)), - ($(3:4), $Pair{$Symbol, $Any}(:face, f2))])), + "v1v2", [$merge((; region=$(1:2)), $NamedTupleLV((:face, f1))), + $merge((; region=$(3:4)), $NamedTupleLV((:face, f2)))])), $annotatedstring_optimize!)) == @macroexpand styled"{$f1:v1}{$f2:v2}" @test :($annotatedstring($AnnotatedString( - "val", [($(1:3), $Pair{$Symbol, $Any}(key, "val"))]))) == + "val", [$merge((; region=$(1:3)), $NamedTupleLV((key, "val")))]))) == @macroexpand styled"{$key=val:val}" @test :($chain($annotatedstring($AnnotatedString( - "val", [($(1:3), $Pair{$Symbol, $Any}(key, val))])), + "val", [$merge((; region=$(1:3)), $NamedTupleLV((key, val)))])), $annotatedstring_optimize!)) == @macroexpand styled"{$key=$val:val}" # @test :($annotatedstring($AnnotatedString( # string(val), $Pair{$Symbol, $Any}(key, val)))) == # @macroexpand styled"{$key=$val:$val}" @test :($annotatedstring($AnnotatedString( - "val", [($(1:3), $Pair{$Symbol, $Any}(:face, $(Face)(foreground = color)))]))) == + "val", [$merge((; region=$(1:3)), $NamedTupleLV((:face, $(Face)(foreground = color))))]))) == @macroexpand styled"{(foreground=$color):val}" end @@ -567,9 +570,9 @@ end end # AnnotatedChar @test sprint(print, AnnotatedChar('a')) == "a" - @test sprint(print, AnnotatedChar('a', [:face => :red]), context = :color => true) == "\e[31ma\e[39m" + @test sprint(print, AnnotatedChar('a', [(:face, :red)]), context = :color => true) == "\e[31ma\e[39m" @test sprint(show, AnnotatedChar('a')) == "'a'" - @test sprint(show, AnnotatedChar('a', [:face => :red]), context = :color => true) == "'\e[31ma\e[39m'" + @test sprint(show, AnnotatedChar('a', [(:face, :red)]), context = :color => true) == "'\e[31ma\e[39m'" # Might as well put everything together for a final test fancy_string = styled"The {magenta:`{green:StyledStrings}`} package {italic:builds}\ {bold: on top} of the {magenta:`{green:AnnotatedString}`} {link={https://en.wikipedia.org/wiki/Type_system}:type} \