Skip to content

Spans and Paragraphs

Arranger separates document formatting into two orthogonal scopes: Span attributes (inline character-level styling) and Paragraph attributes (block-level paragraph styling).

Advanced Formatting Sample


Two Fundamental Scopes

1. Span Attributes (Inline Styling)

Styling applied locally across arbitrary character ranges (IntRange), targeting specific words, phrases, or clauses.

  • Examples: Bold (BoldKey), Italic (ItalicKey), Underline (UnderlineKey), Text Color (TextColorKey), Hyperlinks (LinkKey).
  • Characteristics: Multiple distinct span attributes can overlap arbitrarily over the same character slice (e.g. bold + underlined + red text).
  • Base Interface: SpanAttributeKey<T>.

2. Paragraph Attributes (Block Styling)

Block-level styling applied across entire newline-delimited paragraphs (\n).

  • Examples: Headings (HeadingKey), Bullet Lists (BulletListKey), Blockquotes (BlockquoteKey), Text Alignment (TextAlignmentKey).
  • Characteristics: Even if applied to a partial substring within a paragraph, the engine automatically expands the formatting range to encompass the entire paragraph from boundary to boundary (paragraph snapping).
  • Base Interface: ParagraphAttributeKey<T>.

Type-Safe Attribute Key Hierarchy

In Arranger, attribute categorization and behaviors are guaranteed at compile time via Kotlin's type hierarchy.

flowchart TD
    AttributeKey["AttributeKey (Root Interface)"]
    SpanKey["SpanAttributeKey<br>(Character-level: Bold, Color, Link)"]
    ParagraphKey["ParagraphAttributeKey<br>(Block-level: Heading, List, Alignment)"]
    BlockTypeKey["BlockTypeAttributeKey<br>(Mutually exclusive block types)"]
    AlignmentKey["AlignmentAttributeKey<br>(Mutually exclusive alignments)"]

    AttributeKey --> SpanKey
    AttributeKey --> ParagraphKey
    ParagraphKey --> BlockTypeKey
    ParagraphKey --> AlignmentKey

Paragraph Snapping

When a user selects only a few words within a paragraph and applies a heading or blockquote, Arranger automatically snaps the target range across the entire paragraph boundary to maintain document integrity.

state.edit {
    // Even when targeting a single word within a paragraph...
    val wordRange = 10..15 

    // Heading H2 is automatically applied across the entire encompassing paragraph
    setParagraphAttribute(HeadingKey, HeadingLevel.H2, range = wordRange)
}

Developers never need to write manual arithmetic to locate line start and end offsets.


Automatic Mutual Exclusion

Certain paragraph block attributes are conceptually mutually exclusive. Arranger detects these collisions and handles substitution safely.

1. Block Type Exclusion (BlockTypeAttributeKey)

Headings (H1–H6), bullet lists, ordered lists, and blockquotes implement BlockTypeAttributeKey.

Assigning a new block type to a paragraph automatically clears any existing block-type attribute on that paragraph.

state.edit {
    // 1. First apply a blockquote
    blockquote()

    // 2. Then apply Heading H1 to the same paragraph
    headingLevel(HeadingLevel.H1)

    // => BlockquoteKey is automatically removed, leaving only HeadingKey active
}

2. Horizontal Alignment Exclusion (AlignmentAttributeKey)

Left, Center, Right, and Justify alignment implement AlignmentAttributeKey.

Applying a new alignment attribute automatically supersedes any previous alignment attribute on that paragraph.


Practical Styling Example

@Composable
fun StylingExample() {
    val text = "Chapter 1 Introduction\nThis paragraph is centered.\nA quoted callout block."

    val state = remember {
        RichTextState(
            initialText = RichString(text).edit {
                // Paragraph attribute: Heading H1
                editAttributes(range = text.rangeOf("Chapter 1 Introduction")) {
                    headingLevel(HeadingLevel.H1)
                }

                // Paragraph attribute: Centered alignment
                editAttributes(range = text.rangeOf("This paragraph is centered.")) {
                    textAlignment(TextAlignment.Center)
                }

                // Paragraph attribute: Blockquote
                editAttributes(range = text.rangeOf("A quoted callout block.")) {
                    blockquote()
                }

                // Span attribute: Keyword styling
                editAttributes(range = text.rangeOf("centered")) {
                    bold()
                    underline()
                    textColor(Color(0xFF1976D2))
                }
            }
        )
    }

    RichTextEditor(state = state, modifier = Modifier.fillMaxWidth())
}