Txt markdown
Render and parse Markdown in SwiftUI Text and AttributedString — inline syntax that works, block-level syntax that doesn't, PresentationIntent interpretation, and custom `^[text](key:value)` attributes via MarkdownDecodableAttributedStringKey. Use when Markdown isn't rendering as expected, headings/lists are silently dropped, a `String` variable shows literal asterisks, you're choosing between native parsing and a third-party renderer, or you need block-level formatting in UITextView. Do NOT use for the AttributedString-vs-NSAttributedString decision in general — see txt-attributed-string. Do NOT use for parser/regex mechanics on non-Markdown text — see txt-regex.From its SKILL.md
npx -y skills add sitapix/apple-text --skill txt-markdownAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
15.0 KB, ~3.1k tokens by cl100k_base, as published. Nobody here has run it
Markdown in Apple Text
Authored against iOS 26.x / Swift 6.x / Xcode 26.x.
This skill covers what Markdown actually does inside Apple's text APIs — which syntax SwiftUI Text renders, which it silently ignores, how AttributedString(markdown:) parses block structure into presentationIntent, and what it takes to render that block structure in TextKit. The patterns here describe how Markdown rendering usually fails; before assuming a feature is missing, check the actual interpretedSyntax option on the call site and verify whether the destination view is SwiftUI Text or a TextKit-backed view, since those have very different rendering surfaces.
The rule that catches every team: SwiftUI Text renders inline Markdown (bold, italic, code, links, strikethrough) and silently drops everything else. There is no error, no warning, no fallback rendering — headings render as plain text, lists render as plain lines with literal - characters, code blocks render as inline-code spans without block formatting. Block-level Markdown requires either a TextKit view interpreting presentationIntent or a third-party SwiftUI renderer.
Contents
- Inline Markdown in SwiftUI Text
- LocalizedStringKey vs String
- AttributedString markdown parsing
- PresentationIntent and block structure
- Rendering block formatting in UITextView
- Custom Markdown attributes
- Native vs third-party renderers
- Common Mistakes
- References
Inline Markdown in SwiftUI Text
A string literal passed to Text(_:) is a LocalizedStringKey, and LocalizedStringKey parses inline Markdown automatically:
Text("**Bold** and *italic* and `code`")
Text("~~Strikethrough~~ and [Link](https://apple.com)")
Text("***Bold italic*** together")
The inline syntax that renders:
| Syntax | Renders as |
|---|---|
**bold**, __bold__ | Bold |
*italic*, _italic_ | Italic |
***bold italic*** | Bold + italic |
`code` | Monospaced inline span |
~~strikethrough~~ | Strikethrough |
[text](url) | Tappable link in accent color |
The block-level syntax that does not render in SwiftUI Text: headings (# Heading), unordered lists (- item), ordered lists (1. item), block quotes (> quote), fenced code blocks, images (), tables, horizontal rules, task lists (- [ ]). They are silently dropped — heading characters are stripped, list bullets become literal - characters, code fences appear as inline-code spans.
LocalizedStringKey vs String
The most common "Markdown stopped working" bug is feeding a String variable to Text instead of a literal:
// String literal — Text(_:) takes LocalizedStringKey, Markdown renders
Text("**bold** text")
// String variable — Text(_:) takes a String overload, Markdown does NOT render
let text: String = "**bold** text"
Text(text) // displays literal asterisks
// Force Markdown on a String variable
Text(LocalizedStringKey(text))
// Disable Markdown on a literal
Text(verbatim: "**not bold**")
For attributed text, parse to an AttributedString first and pass that:
let str = try AttributedString(
markdown: userMessage,
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
)
Text(str)
Text(_: AttributedString) is the third overload and renders all attributes the SwiftUI compatibility scope understands.
AttributedString markdown parsing
AttributedString(markdown:) runs Apple's parser and returns an attributed string with attributes set per the matching syntax. The interpretedSyntax option controls how aggressive the parser is:
| Option | Parses | Whitespace | Best for |
|---|---|---|---|
.inlineOnly | Inline only — bold, italic, code, links, strikethrough | Collapsed (Markdown rules) | Simple formatted strings |
.inlineOnlyPreservingWhitespace | Inline only | Preserved verbatim | Chat messages, multi-line user input |
.full | Full Markdown — inline plus block-level | Markdown rules | Documents, articles, reading content |
.inlineOnlyPreservingWhitespace is usually the right choice for short user-generated content because it doesn't fold runs of whitespace or strip newlines. .full enables block parsing but requires the destination view to interpret presentationIntent — passing a .full-parsed AttributedString to SwiftUI Text gives the same inline-only rendering as .inlineOnly, just with extra metadata stored that nothing reads.
let inline = try AttributedString(
markdown: "Visit [Apple](https://apple.com) for **details**",
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
)
let document = try AttributedString(
markdown: rawDocumentMarkdown,
options: .init(interpretedSyntax: .full)
)
PresentationIntent and block structure
Block-level structure parsed by .full is stored in the presentationIntent attribute on each run. It is structural metadata, not visual instructions — presentationIntent describes "this run is part of a level-2 heading inside a block quote," and rendering is left to the destination.
for run in document.runs {
guard let intent = run.presentationIntent else { continue }
for component in intent.components {
switch component.kind {
case .header(let level): break // level 1-6
case .unorderedList: break
case .orderedList: break
case .listItem(let ordinal): break
case .blockQuote: break
case .codeBlock(let lang): break // language hint or nil
case .paragraph: break
case .table: break
case .tableHeaderRow: break
case .tableRow(let index): break
case .tableCell(let column): break
case .thematicBreak: break // horizontal rule
@unknown default: break
}
}
}
A run can carry multiple intents — a paragraph inside a list item inside a block quote produces three components on each run in that paragraph. The components are ordered outermost-first.
Rendering block formatting in UITextView
To render block-level Markdown in a TextKit view, walk the runs, read presentationIntent, and translate each kind into the corresponding NSAttributedString attributes (paragraph style for indentation/spacing, font for headings, background color for code blocks). The translation isn't built in — Apple parses but doesn't render block structure outside SwiftUI's own document-rendering surfaces.
func applyBlockFormatting(to attrStr: AttributedString) -> NSAttributedString {
let mutable = NSMutableAttributedString(attrStr)
for run in attrStr.runs {
guard let intent = run.presentationIntent else { continue }
let nsRange = NSRange(run.range, in: attrStr)
let style = NSMutableParagraphStyle()
for component in intent.components {
switch component.kind {
case .header(let level):
let sizes: [Int: CGFloat] = [1: 28, 2: 24, 3: 20, 4: 18, 5: 16, 6: 14]
let size = sizes[level] ?? 16
mutable.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: size), range: nsRange)
style.paragraphSpacingBefore = 12
style.paragraphSpacing = 8
case .unorderedList, .orderedList:
style.headIndent = 24
style.firstLineHeadIndent = 8
case .blockQuote:
style.headIndent = 16
style.firstLineHeadIndent = 16
mutable.addAttribute(.foregroundColor, value: UIColor.secondaryLabel, range: nsRange)
case .codeBlock:
mutable.addAttribute(.font,
value: UIFont.monospacedSystemFont(ofSize: 14, weight: .regular),
range: nsRange)
mutable.addAttribute(.backgroundColor,
value: UIColor.secondarySystemBackground,
range: nsRange)
default: break
}
}
mutable.addAttribute(.paragraphStyle, value: style, range: nsRange)
}
return mutable
}
This is the minimal version. Lists with bullets need text-list configuration; nested lists need indent multiplied by depth; code blocks need padding via line layout — all of which is the work the third-party renderers do for you.
Custom Markdown attributes
Apple's Markdown parser supports a custom inline syntax: ^[text](key1: value1, key2: value2). Custom attribute keys that conform to MarkdownDecodableAttributedStringKey are populated automatically when the parser encounters their name.
enum HighlightAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
typealias Value = Bool
static let name = "highlight"
}
enum ColorNameAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
typealias Value = String
static let name = "color"
}
extension AttributeScopes {
struct MyMarkdownAttributes: AttributeScope {
let highlight: HighlightAttribute
let color: ColorNameAttribute
let foundation: FoundationAttributes
let swiftUI: SwiftUIAttributes
}
var myMarkdown: MyMarkdownAttributes.Type { MyMarkdownAttributes.self }
}
extension AttributeDynamicLookup {
subscript<T: AttributedStringKey>(
dynamicMember keyPath: KeyPath<AttributeScopes.MyMarkdownAttributes, T>
) -> T { self[T.self] }
}
let str = try AttributedString(
markdown: "Read ^[this](highlight: true, color: 'blue') carefully",
including: \.myMarkdown
)
Without including: \.myMarkdown, the parser ignores ^[...](...) syntax and the inline text renders without the custom attributes. The scope is required at parse time, not just at render time.
The general protocol — defining a custom AttributedStringKey, building a scope, extending AttributeDynamicLookup — is covered in txt-attributed-string. The Markdown-specific bit is MarkdownDecodableAttributedStringKey conformance, which gives the parser permission to populate the key from ^[...](key: value) syntax.
Native vs third-party renderers
Native parsing (AttributedString(markdown:) plus Text or a TextKit translator) is dependency-free, type-safe, Codable-friendly, and localization-aware. Its weakness is rendering: SwiftUI Text doesn't render block structure, and writing the TextKit translator is real work for every project.
Third-party libraries solve different problems:
- MarkdownUI (gonzalezreal/swift-markdown-ui): renders full Markdown — headings, lists, fenced code blocks with syntax highlighting, images, tables, block quotes — directly in SwiftUI. Themeable. Trades native simplicity for rendering completeness.
- swift-markdown (Apple): a CommonMark parser with full AST access. Useful when you need to manipulate the document tree before rendering, or build a non-Markdown rendering pipeline (DocC uses this internally). It does not include views.
A practical decision rule: simple bold/italic/links in SwiftUI is native Text territory; a full reading view with images and code blocks is MarkdownUI territory; an editor with live syntax highlighting is a TextKit view with parsed presentationIntent plus custom attribute application.
Common Mistakes
-
Expecting SwiftUI Text to render headings or lists. Inline Markdown only. The fix is either an inline-only data model (no headings expected) or switch to MarkdownUI / a TextKit-rendered view. There is no Text-only escape hatch.
-
Markdown not rendering on a String variable.
Text(_:)has overloads for bothLocalizedStringKeyandString; Swift picksStringfor a variable, which doesn't parse Markdown.// WRONG — picks String overload, displays literal asterisks let body: String = userInput Text(body) // CORRECT — force LocalizedStringKey Text(LocalizedStringKey(body)) // CORRECT — parse to AttributedString first Text(try AttributedString(markdown: body, options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace))) -
Parsing with
.fulland expecting block rendering..fullpopulatespresentationIntentbut doesn't render block structure visually. Without an interpreter, the result in SwiftUI Text is identical to.inlineOnly. The visual difference appears only when something walks the runs and applies paragraph styles. -
Forgetting
including: \.myMarkdownfor custom attributes. The parser doesn't know about your scope unless you tell it at parse time. Custom^[text](...)syntax without the matching scope is silently ignored — no error, no warning. -
Treating
presentationIntentas a per-paragraph attribute. It is per-run. A single paragraph with a bold word in the middle is three runs, all carrying the paragraph's intent. When applying paragraph styles, take the union of intents across the paragraph or apply on a per-paragraph basis derived from intent boundaries. -
Whitespace folding eating user content.
.inlineOnlycollapses runs of whitespace per Markdown rules, which is wrong for chat-style content with intentional line breaks. Use.inlineOnlyPreservingWhitespacefor any user-typed string. -
Unhandled
@unknown defaulton intent kind.PresentationIntent.Kindis a non-frozen enum; future Foundation releases can add cases. A switch over the kinds needs@unknown defaultto compile cleanly under-Wexhaustive-switch.
References
txt-attributed-string— choosing between AttributedString and NSAttributedString, custom attribute keys and scopestxt-attribute-keys— full catalog of NSAttributedString.Key values applied during PresentationIntent renderingtxt-swiftui-interop— what survives the SwiftUI/TextKit boundary- AttributedString.MarkdownParsingOptions
- MarkdownDecodableAttributedStringKey
- PresentationIntent
- SwiftUI Text
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.