Skip to content

Add GFM Alert Styling - #168

Open
Sam Clark (28pins) wants to merge 4 commits into
microsoft:mainfrom
28pins:28pins-add-alerts
Open

Sam Clark (28pins) wants to merge 4 commits into
microsoft:mainfrom
28pins:28pins-add-alerts

Conversation

@28pins

@28pins Sam Clark (28pins) commented Aug 15, 2026 •

Copy link
Copy Markdown

Summary

What changed: Added support for alerts, like [!NOTE]
Why: to give users and LLMs more flexibility and better UX

API change: BlockQuoteType now includes BlockQuoteAlertKind as an optional. See below:

Old:

text(String)
case nested([BlockQuoteType])

New:

case text(String, BlockQuoteAlertKind?)
case nested([BlockQuoteType], BlockQuoteAlertKind?)

Validation

Checked demos and stress-tested kitchen sink demo

OSS readiness

  • No secrets, internal URLs, private identifiers, or product-only service names were added.
  • Public docs, fixtures, or notices were updated if behavior or dependencies changed.
  • Third-party dependency changes (adds, removes, version bumps) are intentional and reviewed.
  • Streaming/incomplete markdown behavior remains covered by fixtures or tests.

@28pins
Sam Clark (28pins) requested review from a team and a lite review from Copilot August 15, 2026 22:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@28pins
Sam Clark (28pins) requested a lite review from Copilot August 15, 2026 22:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Suppressed comments (1)

Sources/MarkdownText/UI/BlockQuoteView.swift:104

  • These lines contain tab-indentation, which is likely to trip SwiftLint and makes the new alert view code inconsistent with the surrounding file’s spacing-based indentation.
			Image(systemName: style.imageName)
				.foregroundStyle(style.accentColor)
				.padding(.leading, 8.0)

Comment thread Sources/MarkdownText/Block/BlockQuote+.swift
Comment thread Sources/MarkdownText/UI/BlockQuoteView.swift Outdated
Comment thread Sources/MarkdownText/Models/MarkdownRenderConfig+Builders.swift Outdated
Comment thread Tests/MarkdownTextTests/BlockQuoteViewSnapshotTests.swift
Comment thread Sources/MarkdownText/Resources/Localizable.xcstrings Outdated
@28pins Sam Clark (28pins) changed the title 28pins add alerts Add GFM Alert Styling Sep 1, 2026
Sam Clark (28pins) and others added 2 commits September 1, 2026 18:38
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@28pins

Copy link
Copy Markdown
Author

Jun Yan (@junyan72) would you mind allowing the workflow runs? These are my first PRs on this repo, so let me know if I need to do anything else.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved parser, rendering, test-coverage, lint, accessibility, and documentation findings remain.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (5)

Sources/MarkdownText/Block/BlockQuote+.swift:24

  • The added parser test keeps [!NOTE] and its body in one paragraph, so it does not exercise a multi-paragraph alert where a later child must inherit alertKind. Add a focused case with a quoted blank line and a second paragraph, and verify that both paragraphs remain inside the alert.
        if alertKind == nil, let (kind, rest) = Self.parseAlertTag(text) {
          alertKind = kind
          if !rest.isEmpty {
            finalQuoteTypes.append(.text(rest, alertKind))
          }
        } else {
          finalQuoteTypes.append(.text(text, alertKind))

Sources/MarkdownText/Models/MarkdownRenderConfig.swift:110

  • This public initializer also requires an imageName, but its documentation only describes the two colors. Document the SF Symbol parameter so callers know what controls the icon shown by the alert.
    /// Create an alert style with the supplied accent and background colors.
		public init(accentColor: Color, backgroundColor: Color, imageName: String) {

Sources/MarkdownText/UI/BlockQuoteView.swift:50

  • Nested quote items are rendered directly through InternalBlockQuoteView, so a nested .nested(..., .warning) never passes through BlockQuoteView's alert wrapper. A nested GFM alert is consequently rendered as an ordinary quote; recurse through BlockQuoteView (or apply the same alert wrapper) for each child.
        case .nested(let subItems, _):
          ForEach(subItems.indices, id: \.self) { index in
            InternalBlockQuoteView(item: subItems[index])

Sources/MarkdownText/UI/BlockQuoteView.swift:104

  • After [!NOTE] is stripped, the icon is the only indication of the alert kind, but this Image has no accessibility label. VoiceOver users can receive the body without knowing that it is a note/warning, and custom imageName values have no reliable semantic label; expose the kind as the icon's label.
			Image(systemName: style.imageName)
				.foregroundStyle(style.accentColor)
				.padding(.leading, 8.0)

Tests/MarkdownTextTests/MarkdownParserTests.swift:74

  • This test only uses [!NOTE] and its body on the same quoted line, while the canonical GFM form in the fixture puts the marker on its own line and supports subsequent paragraphs. Add coverage for the marker-only first paragraph and multiline content; otherwise a regression in the actual syntax advertised by the README could pass the tests.
    let markdown = """
    > [!NOTE] This is a note.
    """
  • Files reviewed: 11/11 changed files
  • Comments generated: 3
  • Review effort level: Lite

imageConfig: value
)
}

Comment on lines +18 to +20
if alertKind == nil, let (kind, rest) = Self.parseAlertTag(text) {
alertKind = kind
if !rest.isEmpty {
Comment on lines +17 to +20
if let kind = item.alertKind {
AlertBlockQuoteView(kind: kind) {
InternalBlockQuoteView(item: item, isForAlertQuote: true)
}
@vincent-peng

Copy link
Copy Markdown
Contributor

Reviewed in depth against github.com alert behavior. The design is sound — detection at convert-time keeps the parse pipeline clean, and config-styled BlockQuoteView is the right seam. Findings before merge:

Blocking

  1. CI lint fails. ~16 whitespace-only lines and literal tabs (BlockQuoteView.swift ~L29,102-104; MarkdownRenderConfig.swift ~L106-113) → swiftlint --strict fails. Separately, MarkdownRenderConfig+Builders.swift was wholesale reindented 2→4 spaces — that guarantees conflicts with every other PR touching the file. Suggest reverting to a minimal diff.

Correctness

  1. Marker accepted at the start of any paragraph, not just the first: > intro\n>\n> [!NOTE] becomes an alert whose content is "intro". GitHub requires [!NOTE] alone on the blockquote's first line.
  2. Flattened-text detection causes false positives: [!NOTE] as link text, inside a code span, or as image alt all trigger alerts — > [!NOTE](https://…) renders an empty alert and eats the link. Check the first inline child is a Text node rather than flattening.
  3. Nested >> [!NOTE] strips the marker but renders plain — quoteTypes recursion detects the kind while InternalBlockQuoteView ignores it on nested items. Either render nested chrome or don't strip.
  4. rest strips only a leading " ", not "\n" — > [!NOTE]␣␣ (hard break) + > body leaves a stray leading newline inside the alert.
  5. Same-line tail semantics. > [!NOTE] Tail is an alert here; GitHub renders it as a plain quote (GitLab/Obsidian treat it as a titled alert). The README claims "GitHub alerts" — worth picking a side and documenting; the GitLab superset is a defensible choice either way.
  6. Block children inside the quote are still dropped — lists/code/tables inside > flatten to text. Pre-existing gap, but the [!WARNING] kitchen-sink fixture now demonstrates the loss inside styled chrome.
  7. No snapshot coverage for the alert UI itself — the only visual feature in the PR has 2 parser tests. Image(systemName:) also needs Image(decorative:) or an accessibility label for VoiceOver.

The imageConfig-forwarding fix included here deserves its own PR — it's a real bug and would land faster unbundled. (Filed separately as #179.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants