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