Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Container directives from Markdown-it and GitHub-style alerts are extension synt
:::

> [!NOTE]
> GitHub alert syntax is included as another block quote extension. Unsupported renderers should display it as quoted text.
> GitHub alert syntax is included as another block quote extension. This should display with a colored bar and icon to the left.

> [!WARNING]
> Alerts with multiple paragraphs should still wrap and stream without corrupting following content.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,13 @@ The renderer targets the subset of CommonMark + GitHub-flavored Markdown that LL
- [x] Inline LaTeX math via `\( … \)`
- [x] Display LaTeX math via `$$ … $$`
- [x] Inline citation pills
- [x] GitHub alerts (`> [!NOTE]`) — rendered as block quotes with icon and color

### Not yet supported

- [ ] Footnotes (`[^1]`)
- [ ] Highlight (`==text==`), superscript (`^x^`), subscript (`~x~`)
- [ ] Raw HTML (`<details>`, `<kbd>`, `<aside>`, …) — kept inline as text
- [ ] GitHub alerts (`> [!NOTE]`) — rendered as plain block quotes
- [ ] Container directives (`::: warning … :::`) and admonitions (`!!! note`)
- [ ] Mermaid / PlantUML diagrams — rendered as fenced code

Expand Down
31 changes: 27 additions & 4 deletions Sources/MarkdownText/Block/BlockQuote+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,41 @@ import SwiftUI
extension BlockQuote: BlockConvertible {
var quoteTypes: BlockQuoteType {
var finalQuoteTypes = [BlockQuoteType]()
var alertKind: BlockQuoteAlertKind?

for child in children {
if let inlineContainer = child as? InlineContainer {
// Use our custom extractPlainText method instead of the built-in plainText property
// to properly handle attachment citations
finalQuoteTypes.append(.text(inlineContainer.extractPlainText(removeHeading: false)))
let text = inlineContainer.extractPlainText(removeHeading: false)
if alertKind == nil, let (kind, rest) = Self.parseAlertTag(text) {
alertKind = kind
finalQuoteTypes.append(.text(rest, alertKind))
} else {
finalQuoteTypes.append(.text(text, alertKind))
}
} else if let blockQuoteContainer = child as? BlockQuote {
finalQuoteTypes.append(blockQuoteContainer.quoteTypes)
}
}

return .nested(finalQuoteTypes)
return .nested(finalQuoteTypes, alertKind)
}

/// Recognizes a leading GitHub-style `[!KIND]` alert marker, returning the
/// parsed kind and the remaining text with the marker stripped.
static func parseAlertTag(_ text: String) -> (kind: BlockQuoteAlertKind, rest: String)? {
guard text.hasPrefix("[!"),
let close = text.firstIndex(of: "]"),
close > text.index(text.startIndex, offsetBy: 2),
let kind = BlockQuoteAlertKind(rawValue: String(text[text.index(text.startIndex, offsetBy: 2)..<close]).lowercased())
else {
return nil
}

var rest = String(text[text.index(after: close)...])
if rest.hasPrefix(" ") {
rest.removeFirst()
}
return (kind, rest)
}

func convert(attributeContainer: NSAttributeContainer, config: MarkdownRenderConfig) -> MarkdownRenderable {
Expand Down
21 changes: 21 additions & 0 deletions Sources/MarkdownText/Models/MarkdownRenderConfig+Builders.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,25 @@ extension MarkdownRenderConfig {
imageConfig: value
)
}

/// Returns a copy with `blockQuoteAlertStyle` replaced.
public func withBlockQuoteAlertStyle(_ value: [BlockQuoteAlertKind: MarkdownBlockQuoteAlertStyle]) -> MarkdownRenderConfig {
MarkdownRenderConfig(
shouldAnimateText: shouldAnimateText,
blockQuoteStyle: blockQuoteStyle,
headingStyle: headingStyle,
orderedListStyle: orderedListStyle,
paragraphStyle: paragraphStyle,
tableStyle: tableStyle,
inlineStyle: inlineStyle,
textContextMenu: textContextMenu,
citationConfig: citationConfig,
codeBlockConfig: codeBlockConfig,
blockSpacing: blockSpacing,
textSelectionConfig: textSelectionConfig,
thematicBreakColor: thematicBreakColor,
imageConfig: imageConfig,
blockQuoteAlertStyle: value
)
}
}
39 changes: 38 additions & 1 deletion Sources/MarkdownText/Models/MarkdownRenderConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public struct MarkdownRenderConfig: Hashable, Sendable {
/// - Important: Image support is **experimental**. The behavior, API, and
/// rendering output may change in future releases. Defaults to `.disabled`.
public let imageConfig: ImageConfig
/// Per-kind styling applied to block-quote alerts (`> [!NOTE]`).
public let blockQuoteAlertStyle: [BlockQuoteAlertKind: MarkdownBlockQuoteAlertStyle]

/// Font and color style for a uniformly-styled run of markdown text.
public struct MarkdownTextStyle: Hashable, Sendable {
Expand Down Expand Up @@ -95,6 +97,30 @@ public struct MarkdownRenderConfig: Hashable, Sendable {
}
}

/// Styling applied to a block-quote alert of a given `BlockQuoteAlertKind`.
public struct MarkdownBlockQuoteAlertStyle: Hashable, Sendable {
/// Color of the leading accent bar.
public let accentColor: Color
/// Fill color behind the alert content.
public let backgroundColor: Color
/// SF Symbols icon name
public let imageName: String

/// Create an alert style with the supplied accent and background colors.
public init(accentColor: Color, backgroundColor: Color, imageName: String) {
self.accentColor = accentColor
self.backgroundColor = backgroundColor
self.imageName = imageName
}

/// A neutral fallback used when a kind is missing from the style map.
public static let `default` = MarkdownBlockQuoteAlertStyle(
accentColor: .blue,
backgroundColor: .blue.opacity(0.08),
imageName: "info.circle"
)
}

/// Per-level fonts and shared foreground color for markdown headings.
public struct MarkdownHeadingTextStyle: Hashable, Sendable {
/// Font set for level-1 headings.
Expand Down Expand Up @@ -222,6 +248,15 @@ public struct MarkdownRenderConfig: Hashable, Sendable {
textColor: Color.Theme.Foreground.Primary.Primary750
)

/// Default per-kind styling for block-quote alerts.
public static let defaultBlockQuoteAlertStyle: [BlockQuoteAlertKind: MarkdownBlockQuoteAlertStyle] = [
.note: MarkdownBlockQuoteAlertStyle(accentColor: .blue, backgroundColor: .blue.opacity(0.08), imageName: "note.text"),
.tip: MarkdownBlockQuoteAlertStyle(accentColor: .green, backgroundColor: .green.opacity(0.08), imageName: "lightbulb.circle"),
.important: MarkdownBlockQuoteAlertStyle(accentColor: .purple, backgroundColor: .purple.opacity(0.08), imageName: "info.circle"),
.warning: MarkdownBlockQuoteAlertStyle(accentColor: .yellow, backgroundColor: .yellow.opacity(0.08), imageName: "exclamationmark.triangle"),
.caution: MarkdownBlockQuoteAlertStyle(accentColor: .red, backgroundColor: .red.opacity(0.08), imageName: "exclamationmark.octagon")
]

/// Default styling for `headingStyle`.
public static let defaultHeadingStyle = MarkdownHeadingTextStyle(
h1Font: Typography.extraLargeTextFonts,
Expand Down Expand Up @@ -283,7 +318,8 @@ public struct MarkdownRenderConfig: Hashable, Sendable {
blockSpacing: CGFloat = MarkdownRenderConfig.defaultBlockSpacing,
textSelectionConfig: TextSelectionConfig = .default,
thematicBreakColor: Color = MarkdownRenderConfig.defaultThematicBreakColor,
imageConfig: ImageConfig = .disabled
imageConfig: ImageConfig = .disabled,
blockQuoteAlertStyle: [BlockQuoteAlertKind: MarkdownBlockQuoteAlertStyle] = MarkdownRenderConfig.defaultBlockQuoteAlertStyle
) {
self.shouldAnimateText = shouldAnimateText
self.blockQuoteStyle = blockQuoteStyle
Expand All @@ -299,6 +335,7 @@ public struct MarkdownRenderConfig: Hashable, Sendable {
self.textSelectionConfig = textSelectionConfig
self.thematicBreakColor = thematicBreakColor
self.imageConfig = imageConfig
self.blockQuoteAlertStyle = blockQuoteAlertStyle
}

/// The default render config, equivalent to calling `init()` with no
Expand Down
4 changes: 2 additions & 2 deletions Sources/MarkdownText/Models/RenderableDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ private extension Array where Element == MarkdownListItem {
private extension BlockQuoteType {
var plainText: String {
switch self {
case .text(let text):
case .text(let text, _):
return text
case .nested(let items):
case .nested(let items, _):
return items.map { $0.plainText }.joined(separator: "\n")
}
}
Expand Down
Loading