Skip to content

Added support for attributed text runs and inline Markdown - #584

Open
tifroz wants to merge 1 commit into
skiptools:mainfrom
tifroz:tifroz/attributed-text
Open

tifroz wants to merge 1 commit into
skiptools:mainfrom
tifroz:tifroz/attributed-text

Conversation

@tifroz

@tifroz tifroz commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Thank you for contributing to the Skip project! Please review the contribution guide at https://skip.dev/docs/contributing/ for advice and guidance on making high-quality PRs.

Use this space to describe your change and add any labels (bug, enhancement, documentation, etc.) to help categorize your contribution.

Skip Pull Request Checklist:

  • REQUIRED: I have signed the Contributor Agreement
  • REQUIRED: I have tested my change locally with swift test
  • OPTIONAL: I have tested my change on an iOS simulator or device
  • OPTIONAL: I have tested my change on an Android emulator or device
  • REQUIRED: I have checked whether this change requires a corresponding update in the Skip Fuse UI repository (link related PR if applicable)
  • OPTIONAL: I have added an example of any UI changes to the Showcase sample app

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.

Codex generated the code, validated by testing manually


Summary

This PR will be accompanied by a SkipFuseUI companion PR (depends on this PR, will be submitted later), and a ShowCase update (same comment), but adding an attributed string preview here:

image

Render attributed segments supplied by native Swift

Add Text(attributedRuns:) so callers can pass literal segments with their own fonts, colors, inline styles, links, and baseline offsets. The renderer uses these attributes directly without reparsing the segment text. Run-based text now applies text case and placeholder colors, and shares link handling with Markdown text.

Source files:

  • Sources/SkipUI/SkipUI/Text/AttributedText.swift
  • Sources/SkipUI/SkipUI/Text/Text.swift
  • Sources/SkipUI/Skip/AttributedTextBaseline.kt

Tests:

  • Existing coverage in Tests/SkipUITests/TextTests.swift: testTextConcatenationLaysOutAsSingleString checks layout against a joined literal; testTextConcatenationAppliesPerSegmentFont checks that segment font sizes survive concatenation.
  • No direct automated coverage for attributed-run link activation, redaction, or baseline positioning.

Parse inline Markdown while preserving whitespace and literal content

Add AttributedTextRun.markdownRuns(_:), using the existing CommonMark dependency to produce segments for nested emphasis, code, strikethrough, and links. Preserve inline HTML as literal text and normalize null characters and CR/CRLF line endings before parsing, keeping source-span lookups aligned with the normalized input.

Source files:

  • Sources/SkipUI/SkipUI/Text/AttributedText.swift
  • Sources/SkipUI/Skip/InlineMarkdown.kt

Tests:

  • Created Tests/SkipUITests/AttributedTextTests.swift with nine tests:
    • testNestedMarkdownLinks and testCombinedInlineStyles: nested styles and link destinations.
    • testWhitespaceAndUnicode and testInlineParsingPreservesWhitespaceWithinEmphasis: whitespace, Unicode, multiline emphasis, and inline code.
    • testEscapesAndEntities and testLiteralPercentAndPlainText: escaped markup, entities, literal percent signs, plain and empty input.
    • testInlineHTMLIsPreservedAsLiteralText, testNullCharactersDoNotTruncateText, and testLineEndingsAreNormalizedWithoutLosingWhitespace: literal HTML, text after null characters, normalized line endings, and source-span alignment.

Usage Example

import SkipUI

Text(attributedRuns: [
    AttributedTextRun(text: "Read "),
    AttributedTextRun(
        text: "the guide",
        bold: true,
        link: "https://example.com/guide"
    )
])

On Android, inline Markdown can produce the same run representation:

Text(attributedRuns: AttributedTextRun.markdownRuns(
    "Read [**the guide**](https://example.com/guide)"
))

API Shape

public struct AttributedTextRun: Equatable {
    // Immutable properties correspond to these initializer parameters.
    public init(
        text: String,
        bold: Bool = false,
        italic: Bool = false,
        code: Bool = false,
        strikethrough: Bool = false,
        underline: Bool = false,
        link: String? = nil,
        font: Font? = nil,
        color: Color? = nil,
        baselineOffset: Double = 0.0
    )
    public static func markdownRuns(_ markdown: String) -> [AttributedTextRun]
}

extension Text {
    public init(attributedRuns: [AttributedTextRun])
}

Limitations

  • The parser handles inline Markdown, not block structure. Its non-SKIP fallback returns one literal segment.
  • Device interaction, attributed-run link activation, redaction, and baseline positioning have not been validated in this preparation.
  • Native AttributedString conversion belongs to the calling layer; this change supplies the bridge-facing run API.

@cla-bot cla-bot Bot added the cla-signed label Oct 22, 2026
@tifroz

tifroz commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@fhasse95 this is the follow-up AttributedTextString PR, following up on our discussion #571 in case you want to weigh in

(SkipFuseUI and ShowCase PRs to follow)

@fhasse95

Copy link
Copy Markdown
Member

Thanks for the quick implementation! I'll be happy to try it out in the Showcase app and play around with it a bit once it's available! 👍🏻

@tifroz

tifroz commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

@fhasse95 Use my showcase branch (pinned to the patched SkipUI and SkipFuseUI branches in my repo).

https://github.com/tifroz/skipapp-showcase

@tifroz
tifroz force-pushed the tifroz/attributed-text branch from f3f7c27 to 53f03aa Compare October 23, 2026 03:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants