1 /*
<lambda>null2  * Copyright 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.text
18 
19 import androidx.collection.mutableIntListOf
20 import androidx.compose.runtime.Immutable
21 import androidx.compose.runtime.Stable
22 import androidx.compose.runtime.saveable.Saver
23 import androidx.compose.ui.text.AnnotatedString.Annotation
24 import androidx.compose.ui.text.AnnotatedString.Builder
25 import androidx.compose.ui.text.AnnotatedString.Range
26 import androidx.compose.ui.text.internal.checkPrecondition
27 import androidx.compose.ui.text.internal.requirePrecondition
28 import androidx.compose.ui.text.intl.LocaleList
29 import androidx.compose.ui.text.style.TextIndent
30 import androidx.compose.ui.unit.TextUnit
31 import androidx.compose.ui.unit.TextUnitType.Companion.Em
32 import androidx.compose.ui.unit.TextUnitType.Companion.Sp
33 import androidx.compose.ui.unit.em
34 import androidx.compose.ui.unit.sp
35 import androidx.compose.ui.util.fastAny
36 import androidx.compose.ui.util.fastCoerceIn
37 import androidx.compose.ui.util.fastFilter
38 import androidx.compose.ui.util.fastFilteredMap
39 import androidx.compose.ui.util.fastFlatMap
40 import androidx.compose.ui.util.fastForEach
41 import androidx.compose.ui.util.fastMap
42 import kotlin.jvm.JvmName
43 
44 /**
45  * The basic data structure of text with multiple styles. To construct an [AnnotatedString] you can
46  * use [Builder].
47  */
48 @Immutable
49 class AnnotatedString
50 internal constructor(internal val annotations: List<Range<out Annotation>>?, val text: String) :
51     CharSequence {
52 
53     internal val spanStylesOrNull: List<Range<SpanStyle>>?
54     /** All [SpanStyle] that have been applied to a range of this String */
55     val spanStyles: List<Range<SpanStyle>>
56         get() = spanStylesOrNull ?: listOf()
57 
58     internal val paragraphStylesOrNull: List<Range<ParagraphStyle>>?
59     /** All [ParagraphStyle] that have been applied to a range of this String */
60     val paragraphStyles: List<Range<ParagraphStyle>>
61         get() = paragraphStylesOrNull ?: listOf()
62 
63     /**
64      * The basic data structure of text with multiple styles. To construct an [AnnotatedString] you
65      * can use [Builder].
66      *
67      * If you need to provide other types of [Annotation]s, use an alternative constructor.
68      *
69      * @param text the text to be displayed.
70      * @param spanStyles a list of [Range]s that specifies [SpanStyle]s on certain portion of the
71      *   text. These styles will be applied in the order of the list. And the [SpanStyle]s applied
72      *   later can override the former styles. Notice that [SpanStyle] attributes which are null or
73      *   unspecified won't change the current ones.
74      * @param paragraphStyles a list of [Range]s that specifies [ParagraphStyle]s on certain portion
75      *   of the text. Each [ParagraphStyle] with a [Range] defines a paragraph of text. It's
76      *   required that [Range]s of paragraphs don't overlap with each other. If there are gaps
77      *   between specified paragraph [Range]s, a default paragraph will be created in between.
78      * @throws IllegalArgumentException if [paragraphStyles] contains any two overlapping [Range]s.
79      * @sample androidx.compose.ui.text.samples.AnnotatedStringConstructorSample
80      * @see SpanStyle
81      * @see ParagraphStyle
82      */
83     constructor(
84         text: String,
85         spanStyles: List<Range<SpanStyle>> = listOf(),
86         paragraphStyles: List<Range<ParagraphStyle>> = listOf()
87     ) : this(constructAnnotationsFromSpansAndParagraphs(spanStyles, paragraphStyles), text)
88 
89     /**
90      * The basic data structure of text with multiple styles and other annotations. To construct an
91      * [AnnotatedString] you may use a [Builder].
92      *
93      * @param text the text to be displayed.
94      * @param annotations a list of [Range]s that specifies [Annotation]s on certain portion of the
95      *   text. These annotations will be applied in the order of the list. There're a few properties
96      *   that these annotations have:
97      * - [Annotation]s applied later can override the former annotations. For example, the
98      *   attributes of the last applied [SpanStyle] will override similar attributes of the
99      *   previously applied [SpanStyle]s.
100      * - [SpanStyle] attributes which are null or Unspecified won't change the styling.
101      * - If there are gaps between specified paragraph [Range]s, a default paragraph will be created
102      *   in between.
103      * - The paragraph [Range]s can't partially overlap. They must either not overlap at all, be
104      *   nested (when inner paragraph's range is fully within the range of the outer paragraph) or
105      *   fully overlap (when ranges of two paragraph are the same). For more details check the
106      *   [AnnotatedString.Builder.addStyle] documentation.
107      *
108      * @throws IllegalArgumentException if [ParagraphStyle]s contains any two overlapping [Range]s.
109      * @sample androidx.compose.ui.text.samples.AnnotatedStringMainConstructorSample
110      * @see Annotation
111      */
112     constructor(
113         text: String,
114         annotations: List<Range<out Annotation>> = listOf()
115     ) : this(annotations.ifEmpty { null }, text)
116 
117     init {
118         var spanStyles: MutableList<Range<SpanStyle>>? = null
119         var paragraphStyles: MutableList<Range<ParagraphStyle>>? = null
120         @Suppress("UNCHECKED_CAST")
121         annotations?.fastForEach { annotation ->
122             if (annotation.item is SpanStyle) {
123                 if (spanStyles == null) {
124                     spanStyles = mutableListOf()
125                 }
126                 spanStyles!!.add(annotation as Range<SpanStyle>)
127             } else if (annotation.item is ParagraphStyle) {
128                 if (paragraphStyles == null) {
129                     paragraphStyles = mutableListOf()
130                 }
131                 paragraphStyles!!.add(annotation as Range<ParagraphStyle>)
132             }
133         }
134         spanStylesOrNull = spanStyles
135         paragraphStylesOrNull = paragraphStyles
136 
137         @Suppress("ListIterator") val sorted = paragraphStylesOrNull?.sortedBy { it.start }
138         if (!sorted.isNullOrEmpty()) {
139             val previousEnds = mutableIntListOf(sorted.first().end)
140             for (i in 1 until sorted.size) {
141                 val current = sorted[i]
142                 // [*************************************].....
143                 // ..[******]..................................
144                 // ................[***************]...........
145                 // ..................[******]..................
146                 // current can only be one of these relatively to previous (start/end inclusive)
147                 // ................... [**]...[**]...[**]..[**]
148                 while (previousEnds.isNotEmpty()) {
149                     val previousEnd = previousEnds.last()
150                     if (current.start >= previousEnd) {
151                         previousEnds.removeAt(previousEnds.lastIndex)
152                     } else {
153                         requirePrecondition(current.end <= previousEnd) {
154                             "Paragraph overlap not allowed, end ${current.end} should be less than or equal to $previousEnd"
155                         }
156                         break
157                     }
158                 }
159                 previousEnds.add(current.end)
160             }
161         }
162     }
163 
164     override val length: Int
165         get() = text.length
166 
167     override operator fun get(index: Int): Char = text[index]
168 
169     /**
170      * Return a substring for the AnnotatedString and include the styles in the range of
171      * [startIndex] (inclusive) and [endIndex] (exclusive).
172      *
173      * @param startIndex the inclusive start offset of the range
174      * @param endIndex the exclusive end offset of the range
175      */
176     override fun subSequence(startIndex: Int, endIndex: Int): AnnotatedString {
177         requirePrecondition(startIndex <= endIndex) {
178             "start ($startIndex) should be less or equal to end ($endIndex)"
179         }
180         if (startIndex == 0 && endIndex == text.length) return this
181         val text = text.substring(startIndex, endIndex)
182         return AnnotatedString(
183             text = text,
184             annotations = filterRanges(annotations, startIndex, endIndex)
185         )
186     }
187 
188     /**
189      * Return a substring for the AnnotatedString and include the styles in the given [range].
190      *
191      * @param range the text range
192      * @see subSequence(start: Int, end: Int)
193      */
194     fun subSequence(range: TextRange): AnnotatedString {
195         return subSequence(range.min, range.max)
196     }
197 
198     @Stable
199     operator fun plus(other: AnnotatedString): AnnotatedString {
200         return with(Builder(this)) {
201             append(other)
202             toAnnotatedString()
203         }
204     }
205 
206     /**
207      * Query the string annotations attached on this AnnotatedString. Annotations are metadata
208      * attached on the AnnotatedString, for example, a URL is a string metadata attached on the a
209      * certain range. Annotations are also store with [Range] like the styles.
210      *
211      * @param tag the tag of the annotations that is being queried. It's used to distinguish the
212      *   annotations for different purposes.
213      * @param start the start of the query range, inclusive.
214      * @param end the end of the query range, exclusive.
215      * @return a list of annotations stored in [Range]. Notice that All annotations that intersect
216      *   with the range [start, end) will be returned. When [start] is bigger than [end], an empty
217      *   list will be returned.
218      */
219     @Suppress("UNCHECKED_CAST", "KotlinRedundantDiagnosticSuppress")
220     fun getStringAnnotations(tag: String, start: Int, end: Int): List<Range<String>> =
221         (annotations?.fastFilteredMap({
222             it.item is StringAnnotation && tag == it.tag && intersect(start, end, it.start, it.end)
223         }) {
224             it.unbox()
225         } ?: listOf())
226 
227     /**
228      * Returns true if [getStringAnnotations] with the same parameters would return a non-empty list
229      */
230     fun hasStringAnnotations(tag: String, start: Int, end: Int): Boolean =
231         annotations?.fastAny {
232             it.item is StringAnnotation && tag == it.tag && intersect(start, end, it.start, it.end)
233         } ?: false
234 
235     /**
236      * Query all of the string annotations attached on this AnnotatedString.
237      *
238      * @param start the start of the query range, inclusive.
239      * @param end the end of the query range, exclusive.
240      * @return a list of annotations stored in [Range]. Notice that All annotations that intersect
241      *   with the range [start, end) will be returned. When [start] is bigger than [end], an empty
242      *   list will be returned.
243      */
244     @Suppress("UNCHECKED_CAST", "KotlinRedundantDiagnosticSuppress")
245     fun getStringAnnotations(start: Int, end: Int): List<Range<String>> =
246         annotations?.fastFilteredMap({
247             it.item is StringAnnotation && intersect(start, end, it.start, it.end)
248         }) {
249             it.unbox()
250         } ?: listOf()
251 
252     /**
253      * Query all of the [TtsAnnotation]s attached on this [AnnotatedString].
254      *
255      * @param start the start of the query range, inclusive.
256      * @param end the end of the query range, exclusive.
257      * @return a list of annotations stored in [Range]. Notice that All annotations that intersect
258      *   with the range [start, end) will be returned. When [start] is bigger than [end], an empty
259      *   list will be returned.
260      */
261     @Suppress("UNCHECKED_CAST")
262     fun getTtsAnnotations(start: Int, end: Int): List<Range<TtsAnnotation>> =
263         ((annotations?.fastFilter {
264             it.item is TtsAnnotation && intersect(start, end, it.start, it.end)
265         } ?: listOf())
266             as List<Range<TtsAnnotation>>)
267 
268     /**
269      * Query all of the [UrlAnnotation]s attached on this [AnnotatedString].
270      *
271      * @param start the start of the query range, inclusive.
272      * @param end the end of the query range, exclusive.
273      * @return a list of annotations stored in [Range]. Notice that All annotations that intersect
274      *   with the range [start, end) will be returned. When [start] is bigger than [end], an empty
275      *   list will be returned.
276      */
277     @ExperimentalTextApi
278     @Suppress("UNCHECKED_CAST", "Deprecation")
279     @Deprecated("Use LinkAnnotation API instead", ReplaceWith("getLinkAnnotations(start, end)"))
280     fun getUrlAnnotations(start: Int, end: Int): List<Range<UrlAnnotation>> =
281         ((annotations?.fastFilter {
282             it.item is UrlAnnotation && intersect(start, end, it.start, it.end)
283         } ?: listOf())
284             as List<Range<UrlAnnotation>>)
285 
286     /**
287      * Query all of the [LinkAnnotation]s attached on this [AnnotatedString].
288      *
289      * @param start the start of the query range, inclusive.
290      * @param end the end of the query range, exclusive.
291      * @return a list of annotations stored in [Range]. Notice that All annotations that intersect
292      *   with the range [start, end) will be returned. When [start] is bigger than [end], an empty
293      *   list will be returned.
294      */
295     @Suppress("UNCHECKED_CAST")
296     fun getLinkAnnotations(start: Int, end: Int): List<Range<LinkAnnotation>> =
297         ((annotations?.fastFilter {
298             it.item is LinkAnnotation && intersect(start, end, it.start, it.end)
299         } ?: listOf())
300             as List<Range<LinkAnnotation>>)
301 
302     /**
303      * Returns true if [getLinkAnnotations] with the same parameters would return a non-empty list
304      */
305     fun hasLinkAnnotations(start: Int, end: Int): Boolean =
306         annotations?.fastAny {
307             it.item is LinkAnnotation && intersect(start, end, it.start, it.end)
308         } ?: false
309 
310     override fun equals(other: Any?): Boolean {
311         if (this === other) return true
312         if (other !is AnnotatedString) return false
313         if (text != other.text) return false
314         if (annotations != other.annotations) return false
315         return true
316     }
317 
318     override fun hashCode(): Int {
319         var result = text.hashCode()
320         result = 31 * result + (annotations?.hashCode() ?: 0)
321         return result
322     }
323 
324     override fun toString(): String {
325         // AnnotatedString.toString has special value, it converts it into regular String
326         // rather than debug string.
327         return text
328     }
329 
330     /**
331      * Compare the annotations between this and another AnnotatedString.
332      *
333      * This may be used for fast partial equality checks.
334      *
335      * Note that this checks all annotations including [spanStyles] and [paragraphStyles], but
336      * [equals] still may be false if [text] is different.
337      *
338      * @param other to compare annotations with
339      * @return true if and only if this compares equal on annotations with other
340      */
341     fun hasEqualAnnotations(other: AnnotatedString): Boolean = this.annotations == other.annotations
342 
343     /**
344      * Returns a new [AnnotatedString] where a list of annotations contains the results of applying
345      * the given [transform] function to each element in the original annotations list.
346      *
347      * @sample androidx.compose.ui.text.samples.AnnotatedStringMapAnnotationsSamples
348      */
349     fun mapAnnotations(
350         transform: (Range<out Annotation>) -> Range<out Annotation>
351     ): AnnotatedString {
352         val builder = Builder(this)
353         builder.mapAnnotations(transform)
354         return builder.toAnnotatedString()
355     }
356 
357     /**
358      * Returns a new [AnnotatedString] where a list of annotations contains all elements yielded
359      * from results [transform] function being invoked on each element of original annotations list.
360      *
361      * @see mapAnnotations
362      */
363     fun flatMapAnnotations(
364         transform: (Range<out Annotation>) -> List<Range<out Annotation>>
365     ): AnnotatedString {
366         val builder = Builder(this)
367         builder.flatMapAnnotations(transform)
368         return builder.toAnnotatedString()
369     }
370 
371     /**
372      * The information attached on the text such as a [SpanStyle].
373      *
374      * @param item The object attached to [AnnotatedString]s.
375      * @param start The start of the range where [item] takes effect. It's inclusive
376      * @param end The end of the range where [item] takes effect. It's exclusive
377      * @param tag The tag used to distinguish the different ranges. It is useful to store custom
378      *   data. And [Range]s with same tag can be queried with functions such as
379      *   [getStringAnnotations].
380      */
381     @Immutable
382     @Suppress("DataClassDefinition")
383     data class Range<T>(val item: T, val start: Int, val end: Int, val tag: String) {
384         constructor(item: T, start: Int, end: Int) : this(item, start, end, "")
385 
386         init {
387             requirePrecondition(start <= end) { "Reversed range is not supported" }
388         }
389     }
390 
391     /**
392      * Builder class for AnnotatedString. Enables construction of an [AnnotatedString] using methods
393      * such as [append] and [addStyle].
394      *
395      * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderSample
396      *
397      * This class implements [Appendable] and can be used with other APIs that don't know about
398      * [AnnotatedString]s:
399      *
400      * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderAppendableSample
401      * @param capacity initial capacity for the internal char buffer
402      */
403     class Builder(capacity: Int = 16) : Appendable {
404 
405         private data class MutableRange<T>(
406             val item: T,
407             val start: Int,
408             var end: Int = Int.MIN_VALUE,
409             val tag: String = ""
410         ) {
411             /**
412              * Create an immutable [Range] object.
413              *
414              * @param defaultEnd if the end is not set yet, it will be set to this value.
415              */
416             fun toRange(defaultEnd: Int = Int.MIN_VALUE): Range<T> {
417                 val end = if (end == Int.MIN_VALUE) defaultEnd else end
418                 checkPrecondition(end != Int.MIN_VALUE) { "Item.end should be set first" }
419                 return Range(item = item, start = start, end = end, tag = tag)
420             }
421 
422             /**
423              * Create an immutable [Range] object.
424              *
425              * @param defaultEnd if the end is not set yet, it will be set to this value.
426              */
427             fun <R> toRange(transform: (T) -> R, defaultEnd: Int = Int.MIN_VALUE): Range<R> {
428                 val end = if (end == Int.MIN_VALUE) defaultEnd else end
429                 checkPrecondition(end != Int.MIN_VALUE) { "Item.end should be set first" }
430                 return Range(item = transform(item), start = start, end = end, tag = tag)
431             }
432 
433             companion object {
434                 fun <T> fromRange(range: Range<T>) =
435                     MutableRange(range.item, range.start, range.end, range.tag)
436             }
437         }
438 
439         private val text: StringBuilder = StringBuilder(capacity)
440         private val styleStack: MutableList<MutableRange<out Any>> = mutableListOf()
441         /**
442          * Holds all objects of type [AnnotatedString.Annotation] including [SpanStyle]s and
443          * [ParagraphStyle]s in order.
444          */
445         private val annotations = mutableListOf<MutableRange<out Annotation>>()
446 
447         /** Create an [Builder] instance using the given [String]. */
448         constructor(text: String) : this() {
449             append(text)
450         }
451 
452         /** Create an [Builder] instance using the given [AnnotatedString]. */
453         constructor(text: AnnotatedString) : this() {
454             append(text)
455         }
456 
457         /** Returns the length of the [String]. */
458         val length: Int
459             get() = text.length
460 
461         /**
462          * Appends the given [String] to this [Builder].
463          *
464          * @param text the text to append
465          */
466         fun append(text: String) {
467             this.text.append(text)
468         }
469 
470         @Deprecated(
471             message =
472                 "Replaced by the append(Char) method that returns an Appendable. " +
473                     "This method must be kept around for binary compatibility.",
474             level = DeprecationLevel.HIDDEN
475         )
476         @Suppress("FunctionName", "unused")
477         // Set the JvmName to preserve compatibility with bytecode that expects a void return type.
478         @JvmName("append")
479         fun deprecated_append_returning_void(char: Char) {
480             append(char)
481         }
482 
483         /**
484          * Appends [text] to this [Builder] if non-null, and returns this [Builder].
485          *
486          * If [text] is an [AnnotatedString], all spans and annotations will be copied over as well.
487          * No other subtypes of [CharSequence] will be treated specially. For example, any
488          * platform-specific types, such as `SpannedString` on Android, will only have their text
489          * copied and any other information held in the sequence, such as Android `Span`s, will be
490          * dropped.
491          */
492         @Suppress("BuilderSetStyle", "PARAMETER_NAME_CHANGED_ON_OVERRIDE")
493         override fun append(text: CharSequence?): Builder {
494             if (text is AnnotatedString) {
495                 append(text)
496             } else {
497                 this.text.append(text)
498             }
499             return this
500         }
501 
502         /**
503          * Appends the range of [text] between [start] (inclusive) and [end] (exclusive) to this
504          * [Builder] if non-null, and returns this [Builder].
505          *
506          * If [text] is an [AnnotatedString], all spans and annotations from [text] between [start]
507          * and [end] will be copied over as well. No other subtypes of [CharSequence] will be
508          * treated specially. For example, any platform-specific types, such as `SpannedString` on
509          * Android, will only have their text copied and any other information held in the sequence,
510          * such as Android `Span`s, will be dropped.
511          *
512          * @param start The index of the first character in [text] to copy over (inclusive).
513          * @param end The index after the last character in [text] to copy over (exclusive).
514          */
515         @Suppress("BuilderSetStyle", "PARAMETER_NAME_CHANGED_ON_OVERRIDE")
516         override fun append(text: CharSequence?, start: Int, end: Int): Builder {
517             if (text is AnnotatedString) {
518                 append(text, start, end)
519             } else {
520                 this.text.append(text, start, end)
521             }
522             return this
523         }
524 
525         // Kdoc comes from interface method.
526         @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
527         override fun append(char: Char): Builder {
528             this.text.append(char)
529             return this
530         }
531 
532         /**
533          * Appends the given [AnnotatedString] to this [Builder].
534          *
535          * @param text the text to append
536          */
537         fun append(text: AnnotatedString) {
538             val start = this.text.length
539             this.text.append(text.text)
540             // offset every annotation with start and add to the builder
541             text.annotations?.fastForEach {
542                 annotations.add(MutableRange(it.item, start + it.start, start + it.end, it.tag))
543             }
544         }
545 
546         /**
547          * Appends the range of [text] between [start] (inclusive) and [end] (exclusive) to this
548          * [Builder]. All spans and annotations from [text] between [start] and [end] will be copied
549          * over as well.
550          *
551          * @param start The index of the first character in [text] to copy over (inclusive).
552          * @param end The index after the last character in [text] to copy over (exclusive).
553          */
554         @Suppress("BuilderSetStyle")
555         fun append(text: AnnotatedString, start: Int, end: Int) {
556             val insertionStart = this.text.length
557             this.text.append(text.text, start, end)
558             // offset every annotation with insertionStart and add to the builder
559             text.getLocalAnnotations(start, end)?.fastForEach {
560                 annotations.add(
561                     MutableRange(
562                         it.item,
563                         insertionStart + it.start,
564                         insertionStart + it.end,
565                         it.tag
566                     )
567                 )
568             }
569         }
570 
571         /**
572          * Set a [SpanStyle] for the given range defined by [start] and [end].
573          *
574          * @param style [SpanStyle] to be applied
575          * @param start the inclusive starting offset of the range
576          * @param end the exclusive end offset of the range
577          */
578         fun addStyle(style: SpanStyle, start: Int, end: Int) {
579             annotations.add(MutableRange(item = style, start = start, end = end))
580         }
581 
582         /**
583          * Set a [ParagraphStyle] for the given range defined by [start] and [end]. When a
584          * [ParagraphStyle] is applied to the [AnnotatedString], it will be rendered as a separate
585          * paragraph.
586          *
587          * **Paragraphs arrangement**
588          *
589          * AnnotatedString only supports a few ways that arrangements can be arranged.
590          *
591          * The () and {} below represent different [ParagraphStyle]s passed in that particular order
592          * to the AnnotatedString.
593          * * **Non-overlapping:** paragraphs don't affect each other. Example: (abc){def} or
594          *   abc(def)ghi{jkl}.
595          * * **Nested:** one paragraph is completely inside the other. Example: (abc{def}ghi) or
596          *   ({abc}def) or (abd{def}). Note that because () is passed before {} to the
597          *   AnnotatedString, these are considered nested.
598          * * **Fully overlapping:** two paragraphs cover the exact same range of text. Example:
599          *   ({abc}).
600          * * **Overlapping:** one paragraph partially overlaps the other. Note that this is invalid!
601          *   Example: (abc{de)f}.
602          *
603          * The order in which you apply `ParagraphStyle` can affect how the paragraphs are arranged.
604          * For example, when you first add () at range 0..4 and then {} at range 0..2, this
605          * paragraphs arrangement is considered nested. But if you first add a () paragraph at range
606          * 0..2 and then {} at range 0..4, this arrangement is considered overlapping and is
607          * invalid.
608          *
609          * **Styling**
610          *
611          * If you don't pass a paragraph style for any part of the text, a paragraph will be created
612          * anyway with a default style. In case of nested paragraphs, the outer paragraph will be
613          * split on the bounds of inner paragraph when the paragraphs are passed to be measured and
614          * rendered. For example, (abc{def}ghi) will be split into (abc)({def})(ghi). The inner
615          * paragraph, similarly to fully overlapping paragraphs, will have a style that is a
616          * combination of two created using a [ParagraphStyle.merge] method.
617          *
618          * @param style [ParagraphStyle] to be applied
619          * @param start the inclusive starting offset of the range
620          * @param end the exclusive end offset of the range
621          */
622         fun addStyle(style: ParagraphStyle, start: Int, end: Int) {
623             annotations.add(MutableRange(item = style, start = start, end = end))
624         }
625 
626         /**
627          * Set an Annotation for the given range defined by [start] and [end].
628          *
629          * @param tag the tag used to distinguish annotations
630          * @param annotation the string annotation that is attached
631          * @param start the inclusive starting offset of the range
632          * @param end the exclusive end offset of the range
633          * @sample androidx.compose.ui.text.samples.AnnotatedStringAddStringAnnotationSample
634          * @see getStringAnnotations
635          */
636         fun addStringAnnotation(tag: String, annotation: String, start: Int, end: Int) {
637             annotations.add(
638                 MutableRange(
639                     item = StringAnnotation(annotation),
640                     start = start,
641                     end = end,
642                     tag = tag
643                 )
644             )
645         }
646 
647         /**
648          * Set a [TtsAnnotation] for the given range defined by [start] and [end].
649          *
650          * @param ttsAnnotation an object that stores text to speech metadata that intended for the
651          *   TTS engine.
652          * @param start the inclusive starting offset of the range
653          * @param end the exclusive end offset of the range
654          * @sample androidx.compose.ui.text.samples.AnnotatedStringAddStringAnnotationSample
655          * @see getStringAnnotations
656          */
657         @Suppress("SetterReturnsThis")
658         fun addTtsAnnotation(ttsAnnotation: TtsAnnotation, start: Int, end: Int) {
659             annotations.add(MutableRange(ttsAnnotation, start, end))
660         }
661 
662         /**
663          * Set a [UrlAnnotation] for the given range defined by [start] and [end]. URLs may be
664          * treated specially by screen readers, including being identified while reading text with
665          * an audio icon or being summarized in a links menu.
666          *
667          * @param urlAnnotation A [UrlAnnotation] object that stores the URL being linked to.
668          * @param start the inclusive starting offset of the range
669          * @param end the exclusive end offset of the range
670          * @sample androidx.compose.ui.text.samples.AnnotatedStringAddStringAnnotationSample
671          * @see getStringAnnotations
672          */
673         @ExperimentalTextApi
674         @Suppress("SetterReturnsThis", "Deprecation")
675         @Deprecated(
676             "Use LinkAnnotation API for links instead",
677             ReplaceWith("addLink(, start, end)")
678         )
679         fun addUrlAnnotation(urlAnnotation: UrlAnnotation, start: Int, end: Int) {
680             annotations.add(MutableRange(urlAnnotation, start, end))
681         }
682 
683         /**
684          * Set a [LinkAnnotation.Url] for the given range defined by [start] and [end].
685          *
686          * When clicking on the text in range, the corresponding URL from the [url] annotation will
687          * be opened using [androidx.compose.ui.platform.UriHandler].
688          *
689          * URLs may be treated specially by screen readers, including being identified while reading
690          * text with an audio icon or being summarized in a links menu.
691          *
692          * @param url A [LinkAnnotation.Url] object that stores the URL being linked to.
693          * @param start the inclusive starting offset of the range
694          * @param end the exclusive end offset of the range
695          * @see getStringAnnotations
696          */
697         @Suppress("SetterReturnsThis")
698         fun addLink(url: LinkAnnotation.Url, start: Int, end: Int) {
699             annotations.add(MutableRange(url, start, end))
700         }
701 
702         /**
703          * Set a [LinkAnnotation.Clickable] for the given range defined by [start] and [end].
704          *
705          * When clicking on the text in range, a [LinkInteractionListener] will be triggered with
706          * the [clickable] object.
707          *
708          * Clickable link may be treated specially by screen readers, including being identified
709          * while reading text with an audio icon or being summarized in a links menu.
710          *
711          * @param clickable A [LinkAnnotation.Clickable] object that stores the tag being linked to.
712          * @param start the inclusive starting offset of the range
713          * @param end the exclusive end offset of the range
714          * @see getStringAnnotations
715          */
716         @Suppress("SetterReturnsThis")
717         fun addLink(clickable: LinkAnnotation.Clickable, start: Int, end: Int) {
718             annotations.add(MutableRange(clickable, start, end))
719         }
720 
721         /**
722          * Adds an annotation to draw a bullet. Unlike another overload, this one doesn't add a
723          * separate [ParagraphStyle]. As so for bullet to be rendered, make sure it starts on a
724          * separate line by adding a newline before or wrapping with a [ParagraphStyle].
725          *
726          * For a convenient API to create a bullet list check [withBulletList].
727          *
728          * @param bullet a bullet to draw before the text
729          * @param start the inclusive starting offset of the range
730          * @param end the exclusive end offset of the range
731          * @see withBulletList
732          */
733         internal fun addBullet(bullet: Bullet, start: Int, end: Int) {
734             annotations.add(MutableRange(item = bullet, start = start, end = end))
735         }
736 
737         /**
738          * Adds an annotation to draw a [bullet] together with a paragraph that adds an
739          * [indentation].
740          *
741          * @param bullet a bullet to draw before the text
742          * @param indentation indentation that is added to the paragraph. Note that this indentation
743          *   should be large enough to fit a bullet and a padding between the bullet and beginning
744          *   of the paragraph
745          * @param start the inclusive starting offset of the range
746          * @param end the exclusive end offset of the range
747          * @see withBulletList
748          */
749         internal fun addBullet(bullet: Bullet, indentation: TextUnit, start: Int, end: Int) {
750             val bulletParStyle = ParagraphStyle(textIndent = TextIndent(indentation, indentation))
751             annotations.add(MutableRange(item = bulletParStyle, start = start, end = end))
752             annotations.add(MutableRange(item = bullet, start = start, end = end))
753         }
754 
755         /**
756          * Applies the given [SpanStyle] to any appended text until a corresponding [pop] is called.
757          *
758          * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderPushSample
759          * @param style SpanStyle to be applied
760          */
761         fun pushStyle(style: SpanStyle): Int {
762             MutableRange(item = style, start = text.length).also {
763                 styleStack.add(it)
764                 annotations.add(it)
765             }
766             return styleStack.size - 1
767         }
768 
769         /**
770          * Applies the given [ParagraphStyle] to any appended text until a corresponding [pop] is
771          * called.
772          *
773          * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderPushParagraphStyleSample
774          * @param style ParagraphStyle to be applied
775          */
776         fun pushStyle(style: ParagraphStyle): Int {
777             MutableRange(item = style, start = text.length).also {
778                 styleStack.add(it)
779                 annotations.add(it)
780             }
781             return styleStack.size - 1
782         }
783 
784         /**
785          * Applies the given [bullet] annotation to any appended text until a corresponding [pop] is
786          * called. For bullet to be rendered, make sure it starts on a separate line by either
787          * adding a newline before or by wrapping with a [ParagraphStyle].
788          *
789          * For a convenient API to create a bullet list check [withBulletList].
790          *
791          * @see withBulletList
792          */
793         internal fun pushBullet(bullet: Bullet): Int {
794             MutableRange(item = bullet, start = text.length).also {
795                 styleStack.add(it)
796                 annotations.add(it)
797             }
798             return styleStack.size - 1
799         }
800 
801         /** Scope for a bullet list */
802         internal class BulletScope internal constructor(internal val builder: Builder) {
803             internal val bulletListSettingStack = mutableListOf<Pair<TextUnit, Bullet>>()
804         }
805 
806         private val bulletScope = BulletScope(this)
807 
808         /**
809          * Creates a bullet list which allows to define a common [indentation] and a [bullet] for
810          * evey bullet list item created inside the list.
811          *
812          * Note that when nesting the [withBulletList] calls, the indentation inside the nested list
813          * will be a combination of all indentations in the nested chain. For example,
814          *
815          * withBulletList(10.sp) { withBulletList(15.sp) { // items indentation 25.sp } }
816          */
817         internal fun <R : Any> withBulletList(
818             indentation: TextUnit = DefaultBulletIndentation,
819             bullet: Bullet = DefaultBullet,
820             block: BulletScope.() -> R
821         ): R {
822             val adjustedIndentation =
823                 bulletScope.bulletListSettingStack.lastOrNull()?.first?.let {
824                     checkPrecondition(it.type == indentation.type) {
825                         "Indentation unit types of nested bullet lists must match. Current $it and previous is $indentation"
826                     }
827                     when (indentation.type) {
828                         Sp -> (indentation.value + it.value).sp
829                         Em -> (indentation.value + it.value).em
830                         else -> indentation
831                     }
832                 } ?: indentation
833 
834             val parIndex =
835                 pushStyle(
836                     ParagraphStyle(
837                         textIndent = TextIndent(adjustedIndentation, adjustedIndentation)
838                     )
839                 )
840             bulletScope.bulletListSettingStack.add(Pair(adjustedIndentation, bullet))
841             return try {
842                 block(bulletScope)
843             } finally {
844                 if (bulletScope.bulletListSettingStack.isNotEmpty()) {
845                     bulletScope.bulletListSettingStack.removeAt(
846                         bulletScope.bulletListSettingStack.lastIndex
847                     )
848                 }
849                 pop(parIndex)
850             }
851         }
852 
853         /**
854          * Attach the given [annotation] to any appended text until a corresponding [pop] is called.
855          *
856          * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderPushStringAnnotationSample
857          * @param tag the tag used to distinguish annotations
858          * @param annotation the string annotation attached on this AnnotatedString
859          * @see getStringAnnotations
860          * @see Range
861          */
862         fun pushStringAnnotation(tag: String, annotation: String): Int {
863             MutableRange(item = StringAnnotation(annotation), start = text.length, tag = tag).also {
864                 styleStack.add(it)
865                 annotations.add(it)
866             }
867             return styleStack.size - 1
868         }
869 
870         /**
871          * Attach the given [ttsAnnotation] to any appended text until a corresponding [pop] is
872          * called.
873          *
874          * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderPushStringAnnotationSample
875          * @param ttsAnnotation an object that stores text to speech metadata that intended for the
876          *   TTS engine.
877          * @see getStringAnnotations
878          * @see Range
879          */
880         fun pushTtsAnnotation(ttsAnnotation: TtsAnnotation): Int {
881             MutableRange(item = ttsAnnotation, start = text.length).also {
882                 styleStack.add(it)
883                 annotations.add(it)
884             }
885             return styleStack.size - 1
886         }
887 
888         /**
889          * Attach the given [UrlAnnotation] to any appended text until a corresponding [pop] is
890          * called.
891          *
892          * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderPushStringAnnotationSample
893          * @param urlAnnotation A [UrlAnnotation] object that stores the URL being linked to.
894          * @see getStringAnnotations
895          * @see Range
896          */
897         @ExperimentalTextApi
898         @Suppress("BuilderSetStyle", "Deprecation")
899         @Deprecated(
900             "Use LinkAnnotation API for links instead",
901             ReplaceWith("pushLink(, start, end)")
902         )
903         fun pushUrlAnnotation(urlAnnotation: UrlAnnotation): Int {
904             MutableRange(item = urlAnnotation, start = text.length).also {
905                 styleStack.add(it)
906                 annotations.add(it)
907             }
908             return styleStack.size - 1
909         }
910 
911         /**
912          * Attach the given [LinkAnnotation] to any appended text until a corresponding [pop] is
913          * called.
914          *
915          * @param link A [LinkAnnotation] object that stores the URL or clickable tag being linked
916          *   to.
917          * @see getStringAnnotations
918          * @see Range
919          */
920         @Suppress("BuilderSetStyle")
921         fun pushLink(link: LinkAnnotation): Int {
922             MutableRange(item = link, start = text.length).also {
923                 styleStack.add(it)
924                 annotations.add(it)
925             }
926             return styleStack.size - 1
927         }
928 
929         /**
930          * Ends the style or annotation that was added via a push operation before.
931          *
932          * @see pushStyle
933          * @see pushStringAnnotation
934          */
935         fun pop() {
936             checkPrecondition(styleStack.isNotEmpty()) { "Nothing to pop." }
937             // pop the last element
938             val item = styleStack.removeAt(styleStack.size - 1)
939             item.end = text.length
940         }
941 
942         /**
943          * Ends the styles or annotation up to and `including` the [pushStyle] or
944          * [pushStringAnnotation] that returned the given index.
945          *
946          * @param index the result of the a previous [pushStyle] or [pushStringAnnotation] in order
947          *   to pop to
948          * @see pop
949          * @see pushStyle
950          * @see pushStringAnnotation
951          */
952         fun pop(index: Int) {
953             checkPrecondition(index < styleStack.size) {
954                 "$index should be less than ${styleStack.size}"
955             }
956             while ((styleStack.size - 1) >= index) {
957                 pop()
958             }
959         }
960 
961         /** Constructs an [AnnotatedString] based on the configurations applied to the [Builder]. */
962         fun toAnnotatedString(): AnnotatedString {
963             return AnnotatedString(
964                 text = text.toString(),
965                 annotations = annotations.fastMap { it.toRange(text.length) }
966             )
967         }
968 
969         /** @see AnnotatedString.mapAnnotations */
970         internal fun mapAnnotations(transform: (Range<out Annotation>) -> Range<out Annotation>) {
971             for (i in annotations.indices) {
972                 val newAnnotation = transform(annotations[i].toRange())
973                 annotations[i] = MutableRange.fromRange(newAnnotation)
974             }
975         }
976 
977         /** @see AnnotatedString.flatMapAnnotations */
978         internal fun flatMapAnnotations(
979             transform: (Range<out Annotation>) -> List<Range<out Annotation>>
980         ) {
981             val replacedAnnotations =
982                 annotations.fastFlatMap { annotation ->
983                     transform(annotation.toRange()).fastMap { MutableRange.fromRange(it) }
984                 }
985             annotations.clear()
986             annotations.addAll(replacedAnnotations)
987         }
988     }
989 
990     /**
991      * Defines annotations that specify additional information to apply to ranges of text within the
992      * given AnnotatedString.
993      *
994      * The AnnotatedString supports annotations that provide different kind of information, such as
995      * * [SpanStyle] specifies character level styling such as color, font, letter spacing etc.
996      * * [ParagraphStyle] for configuring styling on a paragraph level such as line heights, text
997      *   aligning, text direction etc.
998      * * [LinkAnnotation] to mark links in the text.
999      * * [TtsAnnotation] provides information to assistive technologies such as screen readers.
1000      * * Custom annotations using the [StringAnnotation].
1001      */
1002     sealed interface Annotation
1003 
1004     // Unused private subclass of the marker interface to avoid exhaustive "when" statement
1005     @Suppress("unused") private class ExhaustiveAnnotation : Annotation
1006 
1007     companion object {
1008         /**
1009          * The default [Saver] implementation for [AnnotatedString].
1010          *
1011          * Note this Saver doesn't preserve the [LinkInteractionListener] of the links. You should
1012          * handle this case manually if required (check
1013          * https://issuetracker.google.com/issues/332901550 for an example).
1014          */
1015         val Saver: Saver<AnnotatedString, *> = AnnotatedStringSaver
1016     }
1017 }
1018 
constructAnnotationsFromSpansAndParagraphsnull1019 private fun constructAnnotationsFromSpansAndParagraphs(
1020     spanStyles: List<Range<SpanStyle>>,
1021     paragraphStyles: List<Range<ParagraphStyle>>
1022 ): List<Range<out Annotation>>? {
1023     return if (spanStyles.isEmpty() && paragraphStyles.isEmpty()) {
1024         null
1025     } else if (paragraphStyles.isEmpty()) {
1026         spanStyles
1027     } else if (spanStyles.isEmpty()) {
1028         paragraphStyles
1029     } else {
1030         ArrayList<Range<out Annotation>>(spanStyles.size + paragraphStyles.size).also { array ->
1031             spanStyles.fastForEach { array.add(it) }
1032             paragraphStyles.fastForEach { array.add(it) }
1033         }
1034     }
1035 }
1036 
1037 /**
1038  * A helper function used to determine the paragraph boundaries in [MultiParagraph].
1039  *
1040  * It reads paragraph information from [AnnotatedString.paragraphStyles] where only some parts of
1041  * text has [ParagraphStyle] specified, and unspecified parts(gaps between specified paragraphs) are
1042  * considered as default paragraph with default [ParagraphStyle]. For example, the following string
1043  * "(Hello World)Hi!" with a specified paragraph denoted by () will result in paragraphs "Hello
1044  * World" and "Hi!".
1045  *
1046  * **Algorithm implementation**
1047  * * Keep a stack of paragraphs that to be *fully* processed yet and a pointer to the end of last
1048  *   paragraph already added to the result.
1049  * * Iterate through each paragraph.
1050  * * Check if there's a gap between last added paragraph and start of current paragraph. If yes, we
1051  *   need to add text covered by it to the result, making sure to check the existing state of the
1052  *   stack to merge the styles correctly.
1053  * * Add a paragraph to the stack. Depending on its range, we might need to merge its style with the
1054  *   latest one in the stack.
1055  * * Along the way handle special cases like fully overlapped or zero-length paragraphs.
1056  * * After the last iteration, clear the stack by adding additional paragraphs to the result. Also
1057  *   move the pointer to the end of the text.
1058  *
1059  * @param defaultParagraphStyle The default [ParagraphStyle]. It's used for both unspecified default
1060  *   paragraphs and specified paragraph. When a specified paragraph's [ParagraphStyle] has a null
1061  *   attribute, the default one will be used instead.
1062  */
normalizedParagraphStylesnull1063 internal fun AnnotatedString.normalizedParagraphStyles(
1064     defaultParagraphStyle: ParagraphStyle
1065 ): List<Range<ParagraphStyle>> {
1066     @Suppress("ListIterator")
1067     val sortedParagraphs = paragraphStylesOrNull?.sortedBy { it.start } ?: listOf()
1068     val result = mutableListOf<Range<ParagraphStyle>>()
1069 
1070     // a pointer to the last character added to the result list, takes values from 0 to text.length
1071     var lastAdded = 0
1072     val stack = ArrayDeque<Range<ParagraphStyle>>()
1073 
1074     sortedParagraphs.fastForEach {
1075         val current = it.copy(defaultParagraphStyle.merge(it.item))
1076         while (lastAdded < current.start && stack.isNotEmpty()) {
1077             val lastInStack = stack.last()
1078             // ..withStyle(A) { <-- last in stack....
1079             // ....append............................
1080             // ....withStyle(B) { <-- current........
1081             // ......append..........................
1082             // ....}.................................
1083             // ..}...................................
1084             if (current.start < lastInStack.end) {
1085                 result.add(Range(lastInStack.item, lastAdded, current.start))
1086                 lastAdded = current.start
1087             } else {
1088                 // ..withStyle(A) {......................
1089                 // ....append............................
1090                 // ....withStyle(B) { <-- last in stack..
1091                 // ......append..........................
1092                 // ....}.................................
1093                 // ..}...................................
1094                 // withStyle(C) <-- current
1095                 result.add(Range(lastInStack.item, lastAdded, lastInStack.end))
1096                 lastAdded = lastInStack.end
1097                 // We now need to remove it from the stack but also make sure that we remove other
1098                 // stack
1099                 // entrances that have the same ends as the lastAdded
1100                 while (stack.isNotEmpty() && lastAdded == stack.last().end) {
1101                     stack.removeLast()
1102                 }
1103             }
1104         }
1105 
1106         if (lastAdded < current.start) {
1107             result.add(Range(defaultParagraphStyle, lastAdded, current.start))
1108             lastAdded = current.start
1109         }
1110 
1111         val lastInStack = stack.lastOrNull()
1112         if (lastInStack != null) {
1113             if (lastInStack.start == current.start && lastInStack.end == current.end) {
1114                 // fully overlapped, we'll merge current with the previous one and remove the
1115                 // previous one from the stack
1116                 stack.removeLast()
1117                 stack.add(Range(lastInStack.item.merge(current.item), current.start, current.end))
1118             } else if (lastInStack.start == lastInStack.end) {
1119                 // this is a zero-length paragraph
1120                 result.add(Range(lastInStack.item, lastInStack.start, lastInStack.end))
1121                 stack.removeLast()
1122                 stack.add(Range(current.item, current.start, current.end))
1123             } else if (lastInStack.end < current.end) {
1124                 // This is already handled in the init require checks
1125                 throw IllegalArgumentException()
1126             } else {
1127                 stack.add(Range(lastInStack.item.merge(current.item), current.start, current.end))
1128             }
1129         } else {
1130             stack.add(Range(current.item, current.start, current.end))
1131         }
1132     }
1133 
1134     // The paragraph styles finished so we need to empty the stack to add the remaining to the
1135     // result
1136     while (lastAdded <= text.length && stack.isNotEmpty()) {
1137         // ..withStyle(A) {......................
1138         // ....append............................
1139         // ....withStyle(B) { <-- last in stack..
1140         // ......append..........................
1141         // ....}.................................
1142         // ..}...................................
1143         // ....End of AnnotatedString builder....
1144         val lastInStack = stack.last()
1145         result.add(Range(lastInStack.item, lastAdded, lastInStack.end))
1146         lastAdded = lastInStack.end
1147         // We now need to remove it from the stack but also make sure that we remove other stack
1148         // entrances that have the same ends as the lastAdded
1149         while (stack.isNotEmpty() && lastAdded == stack.last().end) {
1150             stack.removeLast()
1151         }
1152     }
1153 
1154     // There might be a text left at the end that isn't covered with a paragraph so using a default
1155     if (lastAdded < text.length) {
1156         result.add(Range(defaultParagraphStyle, lastAdded, text.length))
1157     }
1158 
1159     // This is a corner case where annotatedString is an empty string without any ParagraphStyle.
1160     // In this case, an empty ParagraphStyle is created.
1161     if (result.isEmpty()) {
1162         result.add(Range(defaultParagraphStyle, 0, 0))
1163     }
1164     return result
1165 }
1166 
1167 /**
1168  * Helper function used to find the [ParagraphStyle]s in the given range and also convert the range
1169  * of those styles to the local range.
1170  *
1171  * @param start The start index of the range, inclusive
1172  * @param end The end index of the range, exclusive
1173  */
AnnotatedStringnull1174 private fun AnnotatedString.getLocalParagraphStyles(
1175     start: Int,
1176     end: Int
1177 ): List<Range<ParagraphStyle>>? {
1178     if (start == end) return null
1179     val paragraphStyles = paragraphStylesOrNull ?: return null
1180     // If the given range covers the whole AnnotatedString, return SpanStyles without conversion.
1181     if (start == 0 && end >= this.text.length) {
1182         return paragraphStyles
1183     }
1184     return paragraphStyles.fastFilteredMap({ intersect(start, end, it.start, it.end) }) {
1185         Range(
1186             it.item,
1187             it.start.fastCoerceIn(start, end) - start,
1188             it.end.fastCoerceIn(start, end) - start
1189         )
1190     }
1191 }
1192 
1193 /**
1194  * Helper function used to find the annotations in the given range that match the [predicate], and
1195  * also convert the range of those annotations to the local range. Null [predicate] means is similar
1196  * to passing true.
1197  */
AnnotatedStringnull1198 private fun AnnotatedString.getLocalAnnotations(
1199     start: Int,
1200     end: Int,
1201     predicate: ((Annotation) -> Boolean)? = null
1202 ): List<Range<out AnnotatedString.Annotation>>? {
1203     if (start == end) return null
1204     val annotations = annotations ?: return null
1205     // If the given range covers the whole AnnotatedString, return it without conversion.
1206     if (start == 0 && end >= this.text.length) {
1207         return if (predicate == null) {
1208             annotations
1209         } else {
1210             annotations.fastFilter { predicate(it.item) }
1211         }
1212     }
1213     return annotations.fastFilteredMap({
1214         (predicate?.invoke(it.item) ?: true) && intersect(start, end, it.start, it.end)
1215     }) {
1216         Range(
1217             tag = it.tag,
1218             item = it.item,
1219             start = it.start.coerceIn(start, end) - start,
1220             end = it.end.coerceIn(start, end) - start
1221         )
1222     }
1223 }
1224 
1225 /**
1226  * Helper function used to return another AnnotatedString that is a substring from [start] to [end].
1227  * This will ignore the [ParagraphStyle]s and the resulting [AnnotatedString] will have no
1228  * [ParagraphStyle]s.
1229  *
1230  * @param start The start index of the paragraph range, inclusive
1231  * @param end The end index of the paragraph range, exclusive
1232  * @return The list of converted [SpanStyle]s in the given paragraph range
1233  */
AnnotatedStringnull1234 private fun AnnotatedString.substringWithoutParagraphStyles(start: Int, end: Int): AnnotatedString {
1235     return AnnotatedString(
1236         text = if (start != end) text.substring(start, end) else "",
1237         annotations = getLocalAnnotations(start, end) { it !is ParagraphStyle } ?: listOf()
1238     )
1239 }
1240 
mapEachParagraphStylenull1241 internal inline fun <T> AnnotatedString.mapEachParagraphStyle(
1242     defaultParagraphStyle: ParagraphStyle,
1243     crossinline block:
1244         (annotatedString: AnnotatedString, paragraphStyle: Range<ParagraphStyle>) -> T
1245 ): List<T> {
1246     return normalizedParagraphStyles(defaultParagraphStyle).fastMap { paragraphStyleRange ->
1247         val annotatedString =
1248             substringWithoutParagraphStyles(paragraphStyleRange.start, paragraphStyleRange.end)
1249         block(annotatedString, paragraphStyleRange)
1250     }
1251 }
1252 
1253 /**
1254  * Create upper case transformed [AnnotatedString]
1255  *
1256  * The uppercase sometimes maps different number of characters. This function adjusts the text style
1257  * and paragraph style ranges to transformed offset.
1258  *
1259  * Note, if the style's offset is middle of the uppercase mapping context, this function won't
1260  * transform the character, e.g. style starts from between base alphabet character and accent
1261  * character.
1262  *
1263  * @param localeList A locale list used for upper case mapping. Only the first locale is effective.
1264  *   If empty locale list is passed, use the current locale instead.
1265  * @return A uppercase transformed string.
1266  */
AnnotatedStringnull1267 fun AnnotatedString.toUpperCase(localeList: LocaleList = LocaleList.current): AnnotatedString {
1268     return transform { str, start, end -> str.substring(start, end).toUpperCase(localeList) }
1269 }
1270 
1271 /**
1272  * Create lower case transformed [AnnotatedString]
1273  *
1274  * The lowercase sometimes maps different number of characters. This function adjusts the text style
1275  * and paragraph style ranges to transformed offset.
1276  *
1277  * Note, if the style's offset is middle of the lowercase mapping context, this function won't
1278  * transform the character, e.g. style starts from between base alphabet character and accent
1279  * character.
1280  *
1281  * @param localeList A locale list used for lower case mapping. Only the first locale is effective.
1282  *   If empty locale list is passed, use the current locale instead.
1283  * @return A lowercase transformed string.
1284  */
AnnotatedStringnull1285 fun AnnotatedString.toLowerCase(localeList: LocaleList = LocaleList.current): AnnotatedString {
1286     return transform { str, start, end -> str.substring(start, end).toLowerCase(localeList) }
1287 }
1288 
1289 /**
1290  * Create capitalized [AnnotatedString]
1291  *
1292  * The capitalization sometimes maps different number of characters. This function adjusts the text
1293  * style and paragraph style ranges to transformed offset.
1294  *
1295  * Note, if the style's offset is middle of the capitalization context, this function won't
1296  * transform the character, e.g. style starts from between base alphabet character and accent
1297  * character.
1298  *
1299  * @param localeList A locale list used for capitalize mapping. Only the first locale is effective.
1300  *   If empty locale list is passed, use the current locale instead. Note that, this locale is
1301  *   currently ignored since underlying Kotlin method is experimental.
1302  * @return A capitalized string.
1303  */
AnnotatedStringnull1304 fun AnnotatedString.capitalize(localeList: LocaleList = LocaleList.current): AnnotatedString {
1305     return transform { str, start, end ->
1306         if (start == 0) {
1307             str.substring(start, end).capitalize(localeList)
1308         } else {
1309             str.substring(start, end)
1310         }
1311     }
1312 }
1313 
1314 /**
1315  * Create capitalized [AnnotatedString]
1316  *
1317  * The decapitalization sometimes maps different number of characters. This function adjusts the
1318  * text style and paragraph style ranges to transformed offset.
1319  *
1320  * Note, if the style's offset is middle of the decapitalization context, this function won't
1321  * transform the character, e.g. style starts from between base alphabet character and accent
1322  * character.
1323  *
1324  * @param localeList A locale list used for decapitalize mapping. Only the first locale is
1325  *   effective. If empty locale list is passed, use the current locale instead. Note that, this
1326  *   locale is currently ignored since underlying Kotlin method is experimental.
1327  * @return A decapitalized string.
1328  */
AnnotatedStringnull1329 fun AnnotatedString.decapitalize(localeList: LocaleList = LocaleList.current): AnnotatedString {
1330     return transform { str, start, end ->
1331         if (start == 0) {
1332             str.substring(start, end).decapitalize(localeList)
1333         } else {
1334             str.substring(start, end)
1335         }
1336     }
1337 }
1338 
1339 /**
1340  * The core function of [AnnotatedString] transformation.
1341  *
1342  * @param transform the transformation method
1343  * @return newly allocated transformed AnnotatedString
1344  */
transformnull1345 internal expect fun AnnotatedString.transform(
1346     transform: (String, Int, Int) -> String
1347 ): AnnotatedString
1348 
1349 /**
1350  * Pushes [style] to the [AnnotatedString.Builder], executes [block] and then pops the [style].
1351  *
1352  * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderWithStyleSample
1353  * @param style [SpanStyle] to be applied
1354  * @param block function to be executed
1355  * @return result of the [block]
1356  * @see AnnotatedString.Builder.pushStyle
1357  * @see AnnotatedString.Builder.pop
1358  */
1359 inline fun <R : Any> Builder.withStyle(style: SpanStyle, block: Builder.() -> R): R {
1360     val index = pushStyle(style)
1361     return try {
1362         block(this)
1363     } finally {
1364         pop(index)
1365     }
1366 }
1367 
1368 /**
1369  * Pushes [style] to the [AnnotatedString.Builder], executes [block] and then pops the [style].
1370  *
1371  * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderWithStyleSample
1372  * @param style [SpanStyle] to be applied
1373  * @param block function to be executed
1374  * @return result of the [block]
1375  * @see AnnotatedString.Builder.pushStyle
1376  * @see AnnotatedString.Builder.pop
1377  */
withStylenull1378 inline fun <R : Any> Builder.withStyle(
1379     style: ParagraphStyle,
1380     crossinline block: Builder.() -> R
1381 ): R {
1382     val index = pushStyle(style)
1383     return try {
1384         block(this)
1385     } finally {
1386         pop(index)
1387     }
1388 }
1389 
1390 /**
1391  * Pushes an annotation to the [AnnotatedString.Builder], executes [block] and then pops the
1392  * annotation.
1393  *
1394  * @param tag the tag used to distinguish annotations
1395  * @param annotation the string annotation attached on this AnnotatedString
1396  * @param block function to be executed
1397  * @return result of the [block]
1398  * @see AnnotatedString.Builder.pushStringAnnotation
1399  * @see AnnotatedString.Builder.pop
1400  */
withAnnotationnull1401 inline fun <R : Any> Builder.withAnnotation(
1402     tag: String,
1403     annotation: String,
1404     crossinline block: Builder.() -> R
1405 ): R {
1406     val index = pushStringAnnotation(tag, annotation)
1407     return try {
1408         block(this)
1409     } finally {
1410         pop(index)
1411     }
1412 }
1413 
1414 /**
1415  * Pushes an [TtsAnnotation] to the [AnnotatedString.Builder], executes [block] and then pops the
1416  * annotation.
1417  *
1418  * @param ttsAnnotation an object that stores text to speech metadata that intended for the TTS
1419  *   engine.
1420  * @param block function to be executed
1421  * @return result of the [block]
1422  * @see AnnotatedString.Builder.pushStringAnnotation
1423  * @see AnnotatedString.Builder.pop
1424  */
withAnnotationnull1425 inline fun <R : Any> Builder.withAnnotation(
1426     ttsAnnotation: TtsAnnotation,
1427     crossinline block: Builder.() -> R
1428 ): R {
1429     val index = pushTtsAnnotation(ttsAnnotation)
1430     return try {
1431         block(this)
1432     } finally {
1433         pop(index)
1434     }
1435 }
1436 
1437 /**
1438  * Pushes an [UrlAnnotation] to the [AnnotatedString.Builder], executes [block] and then pops the
1439  * annotation.
1440  *
1441  * @param urlAnnotation A [UrlAnnotation] object that stores the URL being linked to.
1442  * @param block function to be executed
1443  * @return result of the [block]
1444  * @see AnnotatedString.Builder.pushStringAnnotation
1445  * @see AnnotatedString.Builder.pop
1446  */
1447 @ExperimentalTextApi
1448 @Deprecated("Use LinkAnnotation API for links instead", ReplaceWith("withLink(, block)"))
1449 @Suppress("Deprecation")
withAnnotationnull1450 inline fun <R : Any> Builder.withAnnotation(
1451     urlAnnotation: UrlAnnotation,
1452     crossinline block: Builder.() -> R
1453 ): R {
1454     val index = pushUrlAnnotation(urlAnnotation)
1455     return try {
1456         block(this)
1457     } finally {
1458         pop(index)
1459     }
1460 }
1461 
1462 /**
1463  * Pushes a [LinkAnnotation] to the [AnnotatedString.Builder], executes [block] and then pops the
1464  * annotation.
1465  *
1466  * @param link A [LinkAnnotation] object representing a clickable part of the text
1467  * @param block function to be executed
1468  * @return result of the [block]
1469  * @sample androidx.compose.ui.text.samples.AnnotatedStringWithLinkSample
1470  * @sample androidx.compose.ui.text.samples.AnnotatedStringWithHoveredLinkStylingSample
1471  * @sample androidx.compose.ui.text.samples.AnnotatedStringWithListenerSample
1472  */
withLinknull1473 inline fun <R : Any> Builder.withLink(link: LinkAnnotation, block: Builder.() -> R): R {
1474     val index = pushLink(link)
1475     return try {
1476         block(this)
1477     } finally {
1478         pop(index)
1479     }
1480 }
1481 
1482 /**
1483  * Creates a bullet list item around the content produced by the [block]. The list item creates a
1484  * separate paragraph with the indentation to the bullet defined by the preceding
1485  * [Builder.withBulletList] calls.
1486  *
1487  * @param bullet defines the bullet to be drawn
1488  * @param block function to be executed
1489  */
withBulletListItemnull1490 internal fun <R : Any> Builder.BulletScope.withBulletListItem(
1491     bullet: Bullet? = null,
1492     block: Builder.() -> R
1493 ): R {
1494     val lastItemInStack = bulletListSettingStack.lastOrNull()
1495     val itemIndentation = lastItemInStack?.first ?: DefaultBulletIndentation
1496     val itemBullet = bullet ?: (lastItemInStack?.second ?: DefaultBullet)
1497     val parIndex =
1498         builder.pushStyle(ParagraphStyle(textIndent = TextIndent(itemIndentation, itemIndentation)))
1499     val bulletIndex = builder.pushBullet(itemBullet)
1500     return try {
1501         block(builder)
1502     } finally {
1503         builder.pop(bulletIndex)
1504         builder.pop(parIndex)
1505     }
1506 }
1507 
1508 /**
1509  * Filter the range list based on [Range.start] and [Range.end] to include ranges only in the range
1510  * of [start] (inclusive) and [end] (exclusive).
1511  *
1512  * @param start the inclusive start offset of the text range
1513  * @param end the exclusive end offset of the text range
1514  */
filterRangesnull1515 private fun <T> filterRanges(ranges: List<Range<out T>>?, start: Int, end: Int): List<Range<T>>? {
1516     requirePrecondition(start <= end) {
1517         "start ($start) should be less than or equal to end ($end)"
1518     }
1519     val nonNullRange = ranges ?: return null
1520 
1521     return nonNullRange
1522         .fastFilteredMap({ intersect(start, end, it.start, it.end) }) {
1523             Range(
1524                 item = it.item,
1525                 start = maxOf(start, it.start) - start,
1526                 end = minOf(end, it.end) - start,
1527                 tag = it.tag
1528             )
1529         }
1530         .ifEmpty { null }
1531 }
1532 
1533 /**
1534  * Create an AnnotatedString with a [spanStyle] that will apply to the whole text.
1535  *
1536  * @param spanStyle [SpanStyle] to be applied to whole text
1537  * @param paragraphStyle [ParagraphStyle] to be applied to whole text
1538  */
AnnotatedStringnull1539 fun AnnotatedString(
1540     text: String,
1541     spanStyle: SpanStyle,
1542     paragraphStyle: ParagraphStyle? = null
1543 ): AnnotatedString =
1544     AnnotatedString(
1545         text,
1546         listOf(Range(spanStyle, 0, text.length)),
1547         if (paragraphStyle == null) listOf() else listOf(Range(paragraphStyle, 0, text.length))
1548     )
1549 
1550 /**
1551  * Create an AnnotatedString with a [paragraphStyle] that will apply to the whole text.
1552  *
1553  * @param paragraphStyle [ParagraphStyle] to be applied to whole text
1554  */
1555 fun AnnotatedString(text: String, paragraphStyle: ParagraphStyle): AnnotatedString =
1556     AnnotatedString(text, listOf(), listOf(Range(paragraphStyle, 0, text.length)))
1557 
1558 /**
1559  * Build a new AnnotatedString by populating newly created [AnnotatedString.Builder] provided by
1560  * [builder].
1561  *
1562  * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderLambdaSample
1563  * @param builder lambda to modify [AnnotatedString.Builder]
1564  */
1565 inline fun buildAnnotatedString(builder: (Builder).() -> Unit): AnnotatedString =
1566     Builder().apply(builder).toAnnotatedString()
1567 
1568 /**
1569  * Helper function that checks if the range [baseStart, baseEnd) contains the range [targetStart,
1570  * targetEnd).
1571  *
1572  * @return true if
1573  *   [baseStart, baseEnd) contains [targetStart, targetEnd), vice versa. When [baseStart]==[baseEnd]
1574  *   it return true iff [targetStart]==[targetEnd]==[baseStart].
1575  */
1576 internal fun contains(baseStart: Int, baseEnd: Int, targetStart: Int, targetEnd: Int) =
1577     (baseStart <= targetStart && targetEnd <= baseEnd) &&
1578         (baseEnd != targetEnd || (targetStart == targetEnd) == (baseStart == baseEnd))
1579 
1580 /**
1581  * Helper function that checks if the range [lStart, lEnd) intersects with the range [rStart, rEnd).
1582  *
1583  * @return [lStart, lEnd) intersects with range [rStart, rEnd), vice versa.
1584  */
1585 internal fun intersect(lStart: Int, lEnd: Int, rStart: Int, rEnd: Int): Boolean {
1586     // We can check if two ranges intersect just by performing the following operation:
1587     //
1588     //     lStart < rEnd && rStart < lEnd
1589     //
1590     // This operation handles all cases, including when one of the ranges is fully included in the
1591     // other ranges. This is however not enough in this particular case because our ranges are open
1592     // at the end, but closed at the start.
1593     //
1594     // This means the test above would fail cases like: [1, 4) intersect [1, 1)
1595     // To address this we check if either one of the ranges is a "point" (empty selection). If
1596     // that's the case and both ranges share the same start point, then they intersect.
1597     //
1598     // In addition, we use bitwise operators (or, and) instead of boolean operators (||, &&) to
1599     // generate branchless code.
1600     return ((lStart == lEnd) or (rStart == rEnd) and (lStart == rStart)) or
1601         ((lStart < rEnd) and (rStart < lEnd))
1602 }
1603 
1604 private val EmptyAnnotatedString: AnnotatedString = AnnotatedString("")
1605 
1606 /** Returns an AnnotatedString with empty text and no annotations. */
emptyAnnotatedStringnull1607 internal fun emptyAnnotatedString() = EmptyAnnotatedString
1608