@@ -25,6 +25,17 @@ and a value (`Any`), paired together as a `Pair{Symbol, <:Any}`.
2525Labels do not need to be unique, the same region can hold multiple annotations
2626with the same label.
2727
28+ Code written for `AnnotatedString`s in general should conserve the following
29+ properties:
30+ - Which characters an annotation is applied to
31+ - The order in which annotations are applied to each character
32+
33+ Additional semantics may be introduced by specific uses of `AnnotatedString`s.
34+
35+ A corollary of these rules is that adjacent, consecutively placed, annotations
36+ with identical labels and values are equivalent to a single annotation spanning
37+ the combined range.
38+
2839See also [`AnnotatedChar`](@ref), [`annotatedstring`](@ref),
2940[`annotations`](@ref), and [`annotate!`](@ref).
3041
@@ -255,56 +266,26 @@ annotatedstring(c::AnnotatedChar) =
255266
256267AnnotatedString (s:: SubString{<:AnnotatedString} ) = annotatedstring (s)
257268
258- """
259- annotatedstring_optimize!(str::AnnotatedString)
260-
261- Merge contiguous identical annotations in `str`.
262- """
263- function annotatedstring_optimize! (s:: AnnotatedString )
264- last_seen = Dict {Pair{Symbol, Any}, Int} ()
265- i = 1
266- while i <= length (s. annotations)
267- region, keyval = s. annotations[i]
268- prev = get (last_seen, keyval, 0 )
269- if prev > 0
270- lregion, _ = s. annotations[prev]
271- if last (lregion) + 1 == first (region)
272- s. annotations[prev] =
273- setindex (s. annotations[prev],
274- first (lregion): last (region),
275- 1 )
276- deleteat! (s. annotations, i)
277- else
278- delete! (last_seen, keyval)
279- end
280- else
281- last_seen[keyval] = i
282- i += 1
283- end
284- end
285- s
286- end
287-
288269function repeat (str:: AnnotatedString , r:: Integer )
289270 r == 0 && return one (AnnotatedString)
290271 r == 1 && return str
291272 unannot = repeat (str. string, r)
292273 annotations = Vector {Tuple{UnitRange{Int}, Pair{Symbol, Any}}} ()
293274 len = ncodeunits (str)
294275 fullregion = firstindex (str): lastindex (str)
295- for (region, annot) in str. annotations
296- if region == fullregion
297- push! (annotations, (firstindex (unannot): lastindex (unannot), annot))
276+ if allequal (first, str. annotations) && first (first (str. annotations)) == fullregion
277+ newfullregion = firstindex (unannot): lastindex (unannot)
278+ for (_, annot) in str. annotations
279+ push! (annotations, (newfullregion, annot))
298280 end
299- end
300- for offset in 0 : len: (r- 1 )* len
301- for (region, annot) in str. annotations
302- if region != fullregion
281+ else
282+ for offset in 0 : len: (r- 1 )* len
283+ for (region, annot) in str. annotations
303284 push! (annotations, (region .+ offset, annot))
304285 end
305286 end
306287 end
307- AnnotatedString (unannot, annotations) |> annotatedstring_optimize!
288+ AnnotatedString (unannot, annotations)
308289end
309290
310291repeat (str:: SubString{<:AnnotatedString} , r:: Integer ) =
@@ -335,14 +316,9 @@ reverse(s::SubString{<:AnnotatedString}) = reverse(AnnotatedString(s))
335316function _annotate! (annlist:: Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}} , range:: UnitRange{Int} , @nospecialize (labelval:: Pair{Symbol, <:Any} ))
336317 label, val = labelval
337318 if val === nothing
338- indices = searchsorted (annlist, (range,), by= first)
339- labelindex = filter (i -> first (annlist[i][2 ]) === label, indices)
340- for index in Iterators. reverse (labelindex)
341- deleteat! (annlist, index)
342- end
319+ deleteat! (annlist, findall (ann -> ann[1 ] == range && first (ann[2 ]) === label, annlist))
343320 else
344- sortedindex = searchsortedlast (annlist, (range,), by= first) + 1
345- insert! (annlist, sortedindex, (range, Pair {Symbol, Any} (label, val)))
321+ push! (annlist, (range, Pair {Symbol, Any} (label, val)))
346322 end
347323end
348324
352328
353329Annotate a `range` of `str` (or the entire string) with a labeled value (`label` => `value`).
354330To remove existing `label` annotations, use a value of `nothing`.
331+
332+ The order in which annotations are applied to `str` is semantically meaningful,
333+ as described in [`AnnotatedString`](@ref).
355334"""
356335annotate! (s:: AnnotatedString , range:: UnitRange{Int} , @nospecialize (labelval:: Pair{Symbol, <:Any} )) =
357336 (_annotate! (s. annotations, range, labelval); s)
@@ -384,6 +363,9 @@ annotations that overlap with `position` will be returned.
384363Annotations are provided together with the regions they apply to, in the form of
385364a vector of region–annotation tuples.
386365
366+ In accordance with the semantics documented in [`AnnotatedString`](@ref), the
367+ order of annotations returned matches the order in which they were applied.
368+
387369See also: `annotate!`.
388370"""
389371annotations (s:: AnnotatedString ) = s. annotations
@@ -518,10 +500,19 @@ function write(dest::AnnotatedIOBuffer, src::AnnotatedIOBuffer)
518500 nb
519501end
520502
503+ """
504+ _clear_annotations_in_region!(annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}, span::UnitRange{Int})
505+
506+ Erase the presence of `annotations` within a certain `span`.
507+
508+ This operates by removing all elements of `annotations` that are entirely
509+ contained in `span`, truncating ranges that partially overlap, and splitting
510+ annotations that subsume `span` to just exist either side of `span`.
511+ """
521512function _clear_annotations_in_region! (annotations:: Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}} , span:: UnitRange{Int} )
522513 # Clear out any overlapping pre-existing annotations.
523514 filter! (((region, _),) -> first (region) < first (span) || last (region) > last (span), annotations)
524- extras = Tuple{UnitRange{Int}, Pair{Symbol, Any}}[]
515+ extras = Tuple{Int, Tuple{ UnitRange{Int}, Pair{Symbol, Any} }}[]
525516 for i in eachindex (annotations)
526517 region, annot = annotations[i]
527518 # Test for partial overlap
@@ -532,31 +523,68 @@ function _clear_annotations_in_region!(annotations::Vector{Tuple{UnitRange{Int},
532523 # If `span` fits exactly within `region`, then we've only copied over
533524 # the beginning overhang, but also need to conserve the end overhang.
534525 if first (region) < first (span) && last (span) < last (region)
535- push! (extras, (last (span)+ 1 : last (region), annot))
526+ push! (extras, (i, ( last (span)+ 1 : last (region), annot) ))
536527 end
537528 end
538- # Insert any extra entries in the appropriate position
539- for entry in extras
540- sortedindex = searchsortedlast (annotations, (first (entry),), by= first) + 1
541- insert! (annotations, sortedindex, entry)
542- end
529+ end
530+ # Insert any extra entries in the appropriate position
531+ for (offset, (i, entry)) in enumerate (extras)
532+ insert! (annotations, i + offset, entry)
543533 end
544534 annotations
545535end
546536
537+ """
538+ _insert_annotations!(io::AnnotatedIOBuffer, annotations::Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}}, offset::Int = position(io))
539+
540+ Register new `annotations` in `io`, applying an `offset` to their regions.
541+
542+ The largely consists of simply shifting the regions of `annotations` by `offset`
543+ and pushing them onto `io`'s annotations. However, when it is possible to merge
544+ the new annotations with recent annotations in accordance with the semantics
545+ outlined in [`AnnotatedString`](@ref), we do so. More specifically, when there
546+ is a run of the most recent annotations that are also present as the first
547+ `annotations`, with the same value and adjacent regions, the new annotations are
548+ merged into the existing recent annotations by simply extending their range.
549+
550+ This is implemented so that one can say write an `AnnotatedString` to an
551+ `AnnotatedIOBuffer` one character at a time without needlessly producing a
552+ new annotation for each character.
553+ """
547554function _insert_annotations! (io:: AnnotatedIOBuffer , annotations:: Vector{Tuple{UnitRange{Int}, Pair{Symbol, Any}}} , offset:: Int = position (io))
548- if ! eof (io)
549- for (region, annot) in annotations
550- region = first (region)+ offset: last (region)+ offset
551- sortedindex = searchsortedlast (io. annotations, (region,), by= first) + 1
552- insert! (io. annotations, sortedindex, (region, annot))
553- end
554- else
555- for (region, annot) in annotations
556- region = first (region)+ offset: last (region)+ offset
557- push! (io. annotations, (region, annot))
555+ run = 0
556+ if ! isempty (io. annotations) && last (first (last (io. annotations))) == offset
557+ for i in reverse (axes (annotations, 1 ))
558+ annot = annotations[i]
559+ first (first (annot)) == 1 || continue
560+ if last (annot) == last (last (io. annotations))
561+ valid_run = true
562+ for runlen in 1 : i
563+ new_range, new_annot = annotations[begin + runlen- 1 ]
564+ old_range, old_annot = io. annotations[end - i+ runlen]
565+ if last (old_range) != offset || first (new_range) != 1 || old_annot != new_annot
566+ valid_run = false
567+ break
568+ end
569+ end
570+ if valid_run
571+ run = i
572+ break
573+ end
574+ end
558575 end
559576 end
577+ for runindex in 0 : run- 1
578+ old_index = lastindex (io. annotations) - run + 1 + runindex
579+ old_region, annot = io. annotations[old_index]
580+ new_region, _ = annotations[begin + runindex]
581+ io. annotations[old_index] = (first (old_region): last (new_region)+ offset, annot)
582+ end
583+ for index in run+ 1 : lastindex (annotations)
584+ region, annot = annotations[index]
585+ start, stop = first (region), last (region)
586+ push! (io. annotations, (start+ offset: stop+ offset, annot))
587+ end
560588end
561589
562590function read (io:: AnnotatedIOBuffer , :: Type{AnnotatedString{T}} ) where {T <: AbstractString }
0 commit comments