diff --git a/docs/src/internals.md b/docs/src/internals.md index 5495d83..55c55d5 100644 --- a/docs/src/internals.md +++ b/docs/src/internals.md @@ -16,6 +16,7 @@ StyledStrings.Legacy.legacy_color StyledStrings.Legacy.load_env_colors! StyledStrings.ansi_4bit_color_code StyledStrings.eachregion +StyledStrings.annotation_events StyledStrings.face! StyledStrings.getface StyledStrings.loadface! diff --git a/src/regioniterator.jl b/src/regioniterator.jl index 99df7f2..c5ccca1 100644 --- a/src/regioniterator.jl +++ b/src/regioniterator.jl @@ -36,37 +36,71 @@ julia> collect(StyledStrings.eachregion(Base.AnnotatedString( ("there", [:face => :italic]) ``` """ -function eachregion(s::AnnotatedString, region::UnitRange{Int}=firstindex(s):lastindex(s)) - isempty(s) || isempty(region) && - return RegionIterator(s, Vector{UnitRange{Int}}(), Vector{Vector{Pair{Symbol, Any}}}()) +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}}[]) + events = annotation_events(s, subregion) + isempty(events) && return RegionIterator(s.string, [subregion], [Pair{Symbol, Any}[]]) + annotvals = last.(annotations(s)) regions = Vector{UnitRange{Int}}() annots = Vector{Vector{Pair{Symbol, Any}}}() - changepoints = filter(c -> c in region, - Iterators.flatten((first(region), nextind(s, last(region))) - for region in first.(s.annotations)) |> - unique |> sort) - isempty(changepoints) && - return RegionIterator(s.string, UnitRange{Int}[region], Vector{Pair{Symbol, Any}}[map(last, annotations(s, first(region)))]) - function registerchange!(start, stop) - push!(regions, start:stop) - push!(annots, map(last, annotations(s, start))) + pos = first(events).pos + if pos > first(subregion) + push!(regions, first(subregion):pos-1) + push!(annots, []) end - if first(region) < first(changepoints) - registerchange!(first(region), prevind(s, first(changepoints))) + activelist = Int[] + for event in events + if event.pos != pos + push!(regions, pos:prevind(s, event.pos)) + push!(annots, annotvals[activelist]) + pos = event.pos + end + if event.active + insert!(activelist, searchsortedfirst(activelist, event.index), event.index) + else + deleteat!(activelist, searchsortedfirst(activelist, event.index)) + end end - for (start, stop) in zip(changepoints, changepoints[2:end]) - registerchange!(start, prevind(s, stop)) - end - if last(changepoints) <= last(region) - registerchange!(last(changepoints), last(region)) + if last(events).pos < nextind(s, last(subregion)) + push!(regions, last(events).pos:last(subregion)) + push!(annots, []) end RegionIterator(s.string, regions, annots) end -function eachregion(s::SubString{<:AnnotatedString}, region::UnitRange{Int}=firstindex(s):lastindex(s)) +function eachregion(s::SubString{<:AnnotatedString}, pos::UnitRange{Int}=firstindex(s):lastindex(s)) if isempty(s) - RegionIterator(s, Vector{UnitRange{Int}}(), Vector{Vector{Pair{Symbol, Any}}}()) + RegionIterator(s.string, Vector{UnitRange{Int}}(), Vector{Vector{Pair{Symbol, Any}}}()) else - eachregion(s.string, first(region)+s.offset:last(region)+s.offset) + eachregion(s.string, first(pos)+s.offset:last(pos)+s.offset) end end + +""" + annotation_events(string::AbstractString, annots::Vector{Tuple{UnitRange{Int64}, Pair{Symbol, Any}}}, subregion::UnitRange{Int}) + annotation_events(string::AnnotatedString, subregion::UnitRange{Int}) + +Find all annotation "change events" that occur within a `subregion` of `annots`, +with respect to `string`. When `string` is styled, `annots` is inferred. + +Each change event is given in the form of a `@NamedTuple{pos::Int, active::Bool, +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{Int64}, Pair{Symbol, Any}}}, subregion::UnitRange{Int}) + events = Vector{NamedTuple{(:pos, :active, :index), Tuple{Int, Bool, Int}}}() # Position, Active?, Annotation index + 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 + push!(events, (pos=start, active=true, index=i)) + push!(events, (pos=nextind(s, stop), active=false, index=i)) + end + end + sort(events, by=e -> e.pos) +end + +annotation_events(s::AnnotatedString, subregion::UnitRange{Int}) = + annotation_events(s.string, annotations(s), subregion) diff --git a/test/runtests.jl b/test/runtests.jl index 82cd940..bbb2f65 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,7 +3,7 @@ using Test using StyledStrings: StyledStrings, Legacy, SimpleColor, FACES, Face, - @styled_str, styled, StyledMarkup, getface, addface!, loadface!, resetfaces! + @styled_str, styled, StyledMarkup, eachregion, getface, addface!, loadface!, resetfaces! using .StyledMarkup: MalformedStylingMacro using Base: AnnotatedString, AnnotatedChar, AnnotatedIOBuffer, annotations @@ -35,6 +35,50 @@ end # When tested as part of the stdlib, the package prefix can start appearing in show methods. 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))) + # Regions that do/don't extend to the left/right edges + @test annregions(" abc ", [(2:4, :face => :bold)]) == + [(" ", []), + ("abc", [:face => :bold]), + (" ", [])] + @test annregions(" x ", [(2:2, :face => :bold)]) == + [(" ", []), + ("x", [:face => :bold]), + (" ", [])] + @test annregions(" x", [(2:2, :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)]) == + [(" ", []), + ("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])] + # 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])] +end + @testset "SimpleColor" begin @test SimpleColor(:hey).value == :hey # no error @test SimpleColor(0x01, 0x02, 0x03).value == (r=0x01, g=0x02, b=0x03)