Skip to content

Commit 393dd32

Browse files
committed
Add eachregion(::AnnotatedString) implementation to Base
1 parent 96b00c7 commit 393dd32

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed

base/strings/annotated.jl

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,113 @@ function annotated_chartransform(f::Function, str::AnnotatedString, state=nothin
459459
end
460460
AnnotatedString(String(take!(outstr)), annots)
461461
end
462+
463+
struct RegionIterator{S <: AbstractString}
464+
str::S
465+
regions::Vector{UnitRange{Int}}
466+
annotations::Vector{Vector{NamedTuple{(:label,:value),Tuple{Symbol,Any}}}}
467+
end
468+
469+
length(si::RegionIterator) = length(si.regions)
470+
471+
@propagate_inbounds function iterate(si::RegionIterator, i::Integer=1)
472+
if i <= length(si.regions)
473+
@inbounds ((SubString(si.str, si.regions[i]), si.annotations[i]), i+1)
474+
end
475+
end
476+
477+
eltype(::RegionIterator{S}) where { S <: AbstractString} =
478+
Tuple{SubString{S}, Vector{NamedTuple{(:label,:value),Tuple{Symbol,Any}}}}
479+
480+
"""
481+
eachregion(s::AnnotatedString{S})
482+
eachregion(s::SubString{AnnotatedString{S}})
483+
484+
Identify the contiguous substrings of `s` with a constant annotations, and return
485+
an iterator which provides each substring and the applicable annotations as a
486+
`Tuple{SubString{S}, Vector{NamedTuple{(:label,:value),Tuple{Symbol,Any}}}}`.
487+
488+
# Examples
489+
490+
```jldoctest
491+
julia> collect(Base.eachregion(AnnotatedString(
492+
"hey there", [(1:3, :face, :bold), (5:9, :face, :italic)])))
493+
3-element Vector{Tuple{SubString{String}, Vector{@NamedTuple{label::Symbol, value}}}}:
494+
("hey", [@NamedTuple{label::Symbol, value}((:face, :bold))])
495+
(" ", [])
496+
("there", [@NamedTuple{label::Symbol, value}((:face, :italic))])
497+
```
498+
"""
499+
function eachregion(s::AnnotatedString, subregion::UnitRange{Int}=firstindex(s):lastindex(s))
500+
isempty(s) || isempty(subregion) &&
501+
return RegionIterator(s.string, UnitRange{Int}[], Vector{NamedTuple{(:label,:value),Tuple{Symbol,Any}}}[])
502+
events = annotation_events(s, subregion)
503+
isempty(events) && return RegionIterator(s.string, [subregion], [NamedTuple{(:label,:value),Tuple{Symbol,Any}}[]])
504+
annotvals = NamedTuple{(:label,:value),Tuple{Symbol,Any}}[
505+
(; label, value) for (; label, value) in annotations(s)]
506+
regions = Vector{UnitRange{Int}}()
507+
annots = Vector{Vector{NamedTuple{(:label,:value),Tuple{Symbol,Any}}}}()
508+
pos = first(events).pos
509+
if pos > first(subregion)
510+
push!(regions, thisind(s, first(subregion)):prevind(s, pos))
511+
push!(annots, [])
512+
end
513+
activelist = Int[]
514+
for event in events
515+
if event.pos != pos
516+
push!(regions, pos:prevind(s, event.pos))
517+
push!(annots, annotvals[activelist])
518+
pos = event.pos
519+
end
520+
if event.active
521+
insert!(activelist, searchsortedfirst(activelist, event.index), event.index)
522+
else
523+
deleteat!(activelist, searchsortedfirst(activelist, event.index))
524+
end
525+
end
526+
if last(events).pos < nextind(s, last(subregion))
527+
push!(regions, last(events).pos:thisind(s, last(subregion)))
528+
push!(annots, [])
529+
end
530+
RegionIterator(s.string, regions, annots)
531+
end
532+
533+
function eachregion(s::SubString{<:AnnotatedString}, pos::UnitRange{Int}=firstindex(s):lastindex(s))
534+
if isempty(s)
535+
RegionIterator(
536+
s.string,
537+
Vector{UnitRange{Int}}(),
538+
Vector{Vector{NamedTuple{(:label,:value),Tuple{Symbol,Any}}}}(),
539+
)
540+
else
541+
eachregion(s.string, first(pos)+s.offset:last(pos)+s.offset)
542+
end
543+
end
544+
545+
"""
546+
annotation_events(string::AbstractString, annots::Vector{@NamedTuple{region::UnitRange{Int},label::Symbol,value::Any}}, subregion::UnitRange{Int})
547+
annotation_events(string::AnnotatedString, subregion::UnitRange{Int})
548+
549+
Find all annotation "change events" that occur within a `subregion` of `annots`,
550+
with respect to `string`. When `string` is styled, `annots` is inferred.
551+
552+
Each change event is given in the form of a `@NamedTuple{pos::Int, active::Bool,
553+
index::Int}` where `pos` is the position of the event, `active` is a boolean
554+
indicating whether the annotation is being activated or deactivated, and `index`
555+
is the index of the annotation in question.
556+
"""
557+
function annotation_events(s::AbstractString, annots::Vector{NamedTuple{(:region,:label,:value),Tuple{UnitRange{Int},Symbol,Any}}}, subregion::UnitRange{Int})
558+
events = Vector{NamedTuple{(:pos, :active, :index), Tuple{Int, Bool, Int}}}() # Position, Active?, Annotation index
559+
for (i, (; region)) in enumerate(annots)
560+
if !isempty(intersect(subregion, region))
561+
start, stop = max(first(subregion), first(region)), min(last(subregion), last(region))
562+
start <= stop || continue # Currently can't handle empty regions
563+
push!(events, (pos=thisind(s, start), active=true, index=i))
564+
push!(events, (pos=nextind(s, stop), active=false, index=i))
565+
end
566+
end
567+
sort(events, by=e -> e.pos)
568+
end
569+
570+
annotation_events(s::AnnotatedString, subregion::UnitRange{Int}) =
571+
annotation_events(s.string, annotations(s), subregion)

test/strings/annotated.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,51 @@ end
245245
@test read(seekstart(aio), Base.AnnotatedString) ==
246246
Base.AnnotatedString("heya", [(1:3, :x, 1), (4:4, :y, 2)])
247247
end
248+
249+
@testset "Eachregion" begin
250+
annregions(str::String, annots::Vector{<:Tuple{UnitRange{Int}, Symbol, <:Any}}) =
251+
[(s, Tuple.(a)) for (s, a) in Base.eachregion(AnnotatedString(str, annots))]
252+
# Regions that do/don't extend to the left/right edges
253+
@test annregions(" abc ", [(2:4, :face, :bold)]) ==
254+
[(" ", []),
255+
("abc", [(:face, :bold)]),
256+
(" ", [])]
257+
@test annregions(" x ", [(2:2, :face, :bold)]) ==
258+
[(" ", []),
259+
("x", [(:face, :bold)]),
260+
(" ", [])]
261+
@test annregions(" x", [(2:2, :face, :bold)]) ==
262+
[(" ", []),
263+
("x", [(:face, :bold)])]
264+
@test annregions("x ", [(1:1, :face, :bold)]) ==
265+
[("x", [(:face, :bold)]),
266+
(" ", [])]
267+
@test annregions("x", [(1:1, :face, :bold)]) ==
268+
[("x", [(:face, :bold)])]
269+
# Overlapping/nested regions
270+
@test annregions(" abc ", [(2:4, :face, :bold), (3:3, :face, :italic)]) ==
271+
[(" ", []),
272+
("a", [(:face, :bold)]),
273+
("b", [(:face, :bold), (:face, :italic)]),
274+
("c", [(:face, :bold)]),
275+
(" ", [])]
276+
@test annregions("abc-xyz", [(1:7, :face, :bold), (1:3, :face, :green), (4:4, :face, :yellow), (4:7, :face, :italic)]) ==
277+
[("abc", [(:face, :bold), (:face, :green)]),
278+
("-", [(:face, :bold), (:face, :yellow), (:face, :italic)]),
279+
("xyz", [(:face, :bold), (:face, :italic)])]
280+
# Preserving annotation order
281+
@test annregions("abcd", [(1:3, :face, :red), (2:2, :face, :yellow), (2:3, :face, :green), (2:4, :face, :blue)]) ==
282+
[("a", [(:face, :red)]),
283+
("b", [(:face, :red), (:face, :yellow), (:face, :green), (:face, :blue)]),
284+
("c", [(:face, :red), (:face, :green), (:face, :blue)]),
285+
("d", [(:face, :blue)])]
286+
@test annregions("abcd", [(2:4, :face, :blue), (1:3, :face, :red), (2:3, :face, :green), (2:2, :face, :yellow)]) ==
287+
[("a", [(:face, :red)]),
288+
("b", [(:face, :blue), (:face, :red), (:face, :green), (:face, :yellow)]),
289+
("c", [(:face, :blue), (:face, :red), (:face, :green)]),
290+
("d", [(:face, :blue)])]
291+
# Region starting after a character spanning multiple codepoints.
292+
@test annregions("𝟏x", [(1:4, :face, :red)]) ==
293+
[("𝟏", [(:face, :red)]),
294+
("x", [])]
295+
end

0 commit comments

Comments
 (0)