Textual Documentation

repository·main·Indexed 21 days ago

https://github.com/gonzalezreal/textual

A SwiftUI text rendering engine that transforms markup into rich, attributed content. Textual supports Markdown and custom markup via the MarkupParser protocol, providing components like InlineText for formatted text and StructuredText for block-based documents. It features a five-stage rendering pipeline, customizable styling through protocols and presets, and support for syntax highlighting via Prism.js.

Tokens
3.4K
Snippets
11
Records
16
Agent score
74%

What's inside Textual

  1. Understand the Textual rendering pipeline

    main

    Textual transforms markup into rendered SwiftUI UI through a five-stage pipeline. Understanding this flow is essential for customizing how content is parsed, styled, or displayed:

    1. Parsing: Converts markup into an AttributedString using a MarkupParser. It uses PresentationIntent for blocks and Foundation attributes for inline formatting.
    2. Resolving attachments: Asynchronously loads references (like image URLs) and writes them back into the AttributedString as Textual.Attachment attributes.
    3. Styling: Applies inline styles (via WithInlineStyle) using TextEnvironmentValues and groups runs into block-specific views (via BlockContent).
    4. Building: Uses TextBuilder to construct SwiftUI Text objects. It creates invisible placeholders for attachments based on their size proposals.
    5. Overlaying: Uses resolved Text.Layout geometry to position attachment views and handle link interactions or text selection.
  2. Understand Textual coordinate systems

    main

    Textual uses three distinct coordinate systems to manage navigation, logic, and rendering:

    • Structural Navigation (TextPosition): Uses a hierarchical IndexPath structure ([layout, line, run, runSlice]) to navigate the layout and track selection state.
    • Text Operations: Uses character indices (integer offsets into attributed strings) for operations like word navigation.
    • Visual Geometry: Uses CGRect and CGPoint derived from Text.Layout to provide bounds and origins for overlay rendering (e.g., positioning an image attachment).
  3. Use font-relative measurements with .fontScaled()

    main

    Textual provides a .fontScaled() measurement system to ensure layouts scale harmoniously with text size and accessibility settings. You can use these values for padding, block spacing, frame sizes, and insets. A value of .fontScaled(0.5) creates a measurement equal to half of the current font size.

    .textual.padding(.fontScaled(1.0))
    .textual.blockSpacing(.fontScaled(top: 0.8, bottom: 1.2))
  4. How attachment resolution works

    main

    Attachments (like images via run.imageURL) are handled in two phases:

    1. Parsing Phase: The parser identifies the attachment reference and stores it as an attribute within the AttributedString.
    2. Resolution Phase: WithAttachments asynchronously resolves these references using environment-provided attachment loaders. Once loaded, the attribute is updated to a Textual.Attachment type, allowing the rest of the pipeline to treat it as a standard run.

    To implement custom attachment loading, ensure you provide the appropriate loaders via the SwiftUI environment.

  5. Customize block styling

    main

    Block styling is handled at the view level rather than the attribute level.

    1. Grouping: BlockContent groups runs by their PresentationIntent.
    2. View Creation: It creates specific views for different types, such as Paragraph, Heading, OrderedList, etc.
    3. Customization: You can customize these blocks by implementing style protocols that receive the content and associated metadata.
    4. Spacing: BlockVStack manages the reconciliation of spacing between adjacent blocks using SwiftUI preferences.
  6. Add a new syntax highlighting language to Textual

    main

    Textual uses Prism.js for syntax highlighting. To add support for a new language, you must update the Prism bundle used by the framework.

    Prerequisites:

    • Ensure the language is supported by Prism.js (check prismjs.com).
    • The language identifier you add must match Prism's internal language key (e.g., use cpp instead of c++).
    • Note that adding a language may introduce new token types that might fall back to base code styles if the highlighter theme does not explicitly map them.

    Steps to add a language:

    1. Open Scripts/bundle-prism.sh.
    2. Locate the LANGUAGES array and add the Prism language identifier to the appropriate category.
    3. Run the bundling script to download the definitions from the Prism CDN and generate the new bundle.
    4. Verify the language works by rendering a fenced code block with that language hint in Textual.
    5. Commit the updated Sources/Textual/Internal/Highlighter/Prism/prism-bundle.js file.
    ./Scripts/bundle-prism.sh
  7. Install Textual via Swift Package Manager

    main

    To use Textual in a SwiftPM project, add the package dependency to your Package.swift file and include the Textual product in your target's dependencies.

    dependencies: [
      .package(url: "https://github.com/gonzalezreal/textual", from: "0.1.0")
    ]
    
    // In your target definition:
    .product(name: "Textual", package: "textual")
  8. Customize InlineText styling

    main

    You can customize InlineText using standard SwiftUI modifiers (like .font() and .foregroundStyle()) or Textual's specific .textual.inlineStyle(_:) modifier to define styles for code, emphasis, and more.

    InlineText(
      markdown: "Use `git status` to check _uncommitted changes_"
    )
    .font(.custom("Avenir Next", size: 18))
    .textual.inlineStyle(
      InlineStyle()
        .code(
          .monospaced,
          .fontScale(0.85),
          .backgroundColor(.purple),
          .foregroundColor(.white)
        )
        .emphasis(.italic, .underlineStyle(.single))
    )
  9. Use StructuredText for block-based documents

    main

    Use StructuredText for documents containing structured elements like headings, paragraphs, lists, blockquotes, and tables. Each block type can be customized independently.

    StructuredText(
      markdown: """
        ## The Problem
    
        > After merging PR #347, users reported that tapping "Back" from the detail view would sometimes
        > navigate to a completely random screen.
    
        Here's what we knew going in:
    
        - The issue only appeared **after** the state restoration changes
        - It happened _inconsistently_—maybe 1 in 5 back navigations
        """
    )
  10. Use InlineText for inline-formatted text

    main

    Use InlineText for content that requires inline formatting (like bold, italics, or code), images, and links. It acts as a drop-in replacement for SwiftUI's Text but with attachment support and comprehensive styling. It flows naturally within its container.

    InlineText(
      markdown: """
        This is a *lighthearted* but **perfectly serious** paragraph where `inline code` lives \"happily alongside ~~a terrible idea~~ a better one, a [useful link](https://example.com), \"and a bit of _extra emphasis_ just for style. To keep things interesting without overdoing \"it, here’s a completely random image that adapts to the container width:\n\n    ![Random image](https://picsum.photos/seed/textual/400/250)\n    """
    )
  11. Extend Markdown with custom emoji and math

    main

    Textual supports syntax extensions via the syntaxExtensions parameter.

    • Emoji: Define custom emoji using Emoji objects with a shortcode and a URL.
    • Math: Enable math expression rendering by including .math in the extensions.
    // Custom Emoji
    let emoji: Set<Emoji> = [
      Emoji(shortcode: "rocket", url: URL(string: "https://example.com/rocket.png")!),
      Emoji(shortcode: "sparkles", url: URL(string: "https://example.com/sparkles.gif")!),
    ]
    
    InlineText(
      markdown: "Shipped the new feature :rocket: and it's working :sparkles:",
      syntaxExtensions: [.emoji(emoji)]
    )
    
    // Math
    StructuredText(
      markdown: "The area is $A = \\pi r^2$.",
      syntaxExtensions: [.math]
    )
  12. Implement a complete custom theme with StructuredText.Style

    main

    For full control over the entire document's appearance, implement the StructuredText.Style protocol. This allows you to define a cohesive theme by providing implementations for inlineStyle, headingStyle, paragraphStyle, and list markers.

    struct CompactStyle: StructuredText.Style {
      var inlineStyle: InlineStyle {
        InlineStyle()
          .code(.monospaced, .fontScale(0.9))
          .strong(.fontWeight(.semibold))
      }
    
      var headingStyle: some StructuredText.HeadingStyle { ... }
      var paragraphStyle: some StructuredText.ParagraphStyle { ... }
    
      var unorderedListMarker: StructuredText.UnorderedListMarker {
        .hierarchical(.disc, .circle, .square)
      }
    
      var orderedListMarker: StructuredText.OrderedListMarker {
        .decimal
      }
    }
    
    // Apply the custom style
    StructuredText(markdown: content)
      .textual.structuredTextStyle(CompactStyle())