Skip to content

modernize: CDMarkdownKit v3.0.0#51

Merged
chrisdhaan merged 100 commits into
masterfrom
modernize/v3.0-implementation
May 10, 2026
Merged

modernize: CDMarkdownKit v3.0.0#51
chrisdhaan merged 100 commits into
masterfrom
modernize/v3.0-implementation

Conversation

@chrisdhaan
Copy link
Copy Markdown
Owner

Summary

This PR delivers the full CDMarkdownKit v3.0.0 modernization across 86 commits, touching every layer of the library: build infrastructure, Swift 6 concurrency, bug fixes, a complete test suite, CI hardening, Example app repairs, and comprehensive documentation.


Build & Package

  • swift-tools-version 6.0 with swiftLanguageModes: [.v5] — compiles in Swift 5 language mode on a Swift 6 toolchain
  • Removed versioned Package@swift-X.Y.swift files — unified single Package.swift
  • Added dynamicLibrary product and declared PrivacyInfo.xcprivacy as a resource
  • Updated SPM deployment targets: iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0
  • Added .ruby-version and .bundle/ to .gitignore; added Gemfile / Gemfile.lock for CocoaPods + Jazzy Ruby deps
  • CocoaPods: bumped version to 3.0.0, aligned deployment targets, added PrivacyInfo.xcprivacy resource bundle, documentation_url, and cocoapods_version >= 1.13.0 constraint
  • Updated copyright year to 2026 across all source file headers

Swift 6 Concurrency

  • @MainActor on CDMarkdownParser, CDMarkdownLabel, CDMarkdownTextView, and CDMarkdownLayoutManager
  • Sendable conformance added to CDMarkdownElement, CDMarkdownStyle, and all concrete element types
  • CDMarkdownStyle protocol marked Sendable
  • @preconcurrency on NSLayoutManagerDelegate conformance in CDMarkdownLabel to silence false positives
  • Enabled ExistentialAny upcoming feature; updated API call sites accordingly

New Features

  • Async parse pipeline: CDMarkdownParser.parse(_:) is now async; added a second overload that resolves remote images via URLSession after the synchronous parse completes
  • Both async overloads are open to allow subclass extensibility
  • Added two custom NSAttributedString.Key values — .cdMarkdownRoundedBackground and .cdMarkdownImageURL — used internally for attribute-based corner rounding and deferred image resolution

Bug Fixes

  • CDMarkdownLayoutManager: replaced RGBA color comparison with .cdMarkdownRoundedBackground attribute check for rounded-corner rendering — fixes breakage when callers customise the background colors
  • CDMarkdownSyntax: strip language hint from fenced code blocks (e.g. ```swift)
  • CDMarkdownLink: use negative lookbehind in regex to prevent false matches adjacent to ! (image syntax)
  • CDMarkdownImage: fix character class bug in regex that prevented some image URLs from matching
  • CDMarkdownAutomaticLink: guard NSDataDetector initialization behind #if !os(watchOS) to prevent crash on watchOS
  • CDMarkdownTextView: correct TextKit 1 layout manager wiring so the custom CDMarkdownLayoutManager is actually used; fix super.attributedText call to enable UITextView link interactions; clear urlRanges before each new parse
  • CDMarkdownLabel: fix CodeLabel multi-line layout; restore CDMarkdownTextView rendering pipeline
  • CDFont+CDMarkdownKit: handle missing font traits (e.g. bold/italic unavailable) gracefully instead of crashing
  • Back-deployed async URLSession data task helper; moved all image loading off the main thread

Refactor

  • Moved strikethroughColor and strikethroughStyle from CDMarkdownStrikethrough into the CDMarkdownStyle protocol, making them available uniformly across all elements

Test Suite (new — 109 tests / 19 suites)

Added a full SPM test target under Tests/:

Suite Coverage
CDMarkdownParserTests isBold, isItalic, multi-element documents, async parse
CDMarkdownParserInitTests Default/custom element registration, automaticLink toggle
CDMarkdownParserStyleTests Font, color, background, paragraph style propagation
CDMarkdownSquashNewlinesTests squashNewlines on/off behaviour
CDMarkdownCustomElementTests Custom element registration and application
CDMarkdownAutomaticLinkTests URL detection, disabled state, scheme variations
CDMarkdownBoldTests ** and __ syntax, multi-word
CDMarkdownCodeTests Inline backtick, font attributes
CDMarkdownHeaderTests H1–H6 ordering and font scaling
CDMarkdownImageTests Async image loading
CDMarkdownItalicTests * and _ syntax
CDMarkdownLinkTests URL value, text rendering, link enumeration
CDMarkdownListTests Bullet replacement, indentation
CDMarkdownQuoteTests > prefix handling
CDMarkdownStrikethroughTests Style and color attributes
CDMarkdownSyntaxTests Fenced block, font attributes
CDMarkdownEscapingTests Code protection round-trip
StringTests escapeUTF16, unescapeUTF16, range(from:), characterCount
CDMarkdownNSAttributedStringExtensionTests enumerateLinkAttribute overload

CI / GitHub Actions

  • Updated actions/checkout → v4, actions/cache → v4
  • Replaced xcpretty with xcbeautify --renderer github-actions across all jobs
  • Added per-platform simulator/OS version matrix (iOS 26.0 / 18.6, tvOS 26.0 / 18.6, watchOS 26.0 / 11.5, macOS 26 / 15) on matching runners (macos-26 / macos-15)
  • Added SwiftLint enforcement job (--strict)
  • Added CodeQL security scanning job
  • SPM job now runs swift test (tests exist)
  • Added Tests/** to push/PR path filters
  • pod lib lint uses bundle exec

Example App

  • Adopted UIScene lifecycle (SceneDelegate.swift)
  • Fixed NSLayoutConstraint activation crash in CodeLabelViewController
  • Fixed roundAllCorners API usage
  • Fixed async image loading in the Example app views
  • Updated all view controllers to compile cleanly under the new concurrency model

Documentation

  • CLAUDE.md: new AI-agent guide documenting architecture, source map, CI, build commands, and known issues
  • Documentation/ARCHITECTURE.md: detailed three-phase parse pipeline diagram, protocol hierarchy, TextKit 1 wiring notes
  • Documentation/Usage.md: comprehensive API usage guide covering all elements, styling, async parse, and custom elements
  • Documentation/CDMarkdownKit 3.0 Migration Guide.md: step-by-step migration from v2.x
  • README.md: restructured as a navigation hub pointing to the above docs
  • CHANGELOG.md: reformatted to semantic versioning standard with full v3.0.0 entry
  • .jazzy.yaml: Jazzy configuration added; Jazzy HTML docs regenerated and committed to docs/
  • Public API documentation comments added to all source files
  • Fixed GitHub username in Jazzy-generated URLs (christopherdehaanchrisdhaan) and in FUNDING.yml

Test Plan

  • swift test passes (109 tests / 19 suites)
  • xcodebuild Debug + Release for iOS, macOS, tvOS, watchOS all succeed
  • pod lib lint passes
  • swiftlint --strict passes with zero violations
  • CodeQL scan completes with no findings
  • Example app launches; all tabs render markdown correctly; async image loading works

🤖 Generated with Claude Code

chrisdhaan and others added 30 commits May 3, 2026 22:44
- Replace versioned release groupings with flat Table of Contents
- Remove checkboxes and subtitles from release entries
- Standardize to three categories: Added, Updated, Fixed
- Use YYYY-MM-DD date format throughout
- Flatten nested bullet points to concise descriptions
- Update GitHub repository URLs to use correct username

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Delete monolithic .github/ISSUE_TEMPLATE.md
- Create .github/ISSUE_TEMPLATE/ directory with config.yml
- Add bug_report.md template with structured format
- Add feature_request.md template for enhancements
- Disable blank issues and provide contact links for support

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Simplify section headers by removing emoji
- Replace comment instructions with blockquote guidance
- Standardize to four sections: Issue, Goals, Implementation Details, Testing Details
- Make instructions more concise and actionable

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Create .github/FUNDING.yml with sponsorship link
- Enable GitHub Sponsors button on repository

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add Gemfile specifying cocoapods and jazzy dependencies
- Generate Gemfile.lock via bundle lock for reproducible builds
- Enables future documentation generation and dependency management via Bundler

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Replace all instances of actions/checkout@v3 with @v4 across all CI jobs
- Provides latest security updates and improvements from GitHub Actions
- Part of modernization to Section 2.2 of IMPLEMENTATION.md

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Replace all macOS runner versions (12, 11, 10.15) with macOS-15
- Update all Xcode versions to Xcode_16.2.app across all CI jobs
- Add 'Select Xcode' step before build steps in all jobs that use Xcode
- Provides latest runner images and Xcode toolchain for CI pipelines
- Part of modernization to Section 2.3 of IMPLEMENTATION.md

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add 'Install xcbeautify' step before build steps in all xcodebuild jobs
- Replace all xcpretty piping with xcbeautify --renderer github-actions
- Add 2>&1 to capture stderr in all xcodebuild commands
- Improves CI log formatting and integration with GitHub Actions
- Part of modernization to Section 2.5 of IMPLEMENTATION.md

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add 'Tests/**' to push paths filter
- Add 'Tests/**' to pull_request paths filter
- Ensures CI runs when test files are modified
- Part of modernization to Section 2.6 of IMPLEMENTATION.md

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Update swift build command to include set -o pipefail
- Pipe output through xcbeautify --renderer github-actions
- Add 2>&1 to capture stderr
- Improves SPM job log formatting in GitHub Actions
- Part of modernization to Section 2.7 of IMPLEMENTATION.md

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add new 'swiftlint' job that runs on macos-15
- Install SwiftLint via Homebrew
- Run 'swiftlint lint --strict' for strict linting
- Ensures code style compliance in CI pipeline
- Part of modernization to Section 2.8 of IMPLEMENTATION.md

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add new 'codeql' job that runs on macos-15
- Initialize CodeQL analysis for Swift code
- Build CDMarkdownKit iOS scheme for analysis
- Perform CodeQL security analysis
- Write security events for GitHub Security tab
- Enables automated security vulnerability detection
- Part of modernization to Section 2.9 of IMPLEMENTATION.md

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Replace gem install cocoapods with bundle install
- Use 'bundle exec pod lib lint' instead of direct pod command
- Uses pinned CocoaPods version from Gemfile
- Ensures consistent dependency versions across environments
- Removed --use-libraries flag as it's not needed
- Part of modernization to Section 2.10 of IMPLEMENTATION.md

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Replaces swiftLanguageVersions with swiftLanguageModes: [.v5] in Package.swift
to enable Swift 5 language mode support (Step 3.1).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Adds CDMarkdownKitDynamic as a dynamic library product alongside the default
static library product (Step 3.2).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Creates Source/PrivacyInfo.xcprivacy to declare privacy practices. CDMarkdownKit
does not collect data, does not track users, and does not use any APIs that
require a reason string (Step 3.3).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Adds resources parameter to the CDMarkdownKit target to include the privacy
manifest file in the package (Step 3.4).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Updates deployment targets in Package.swift and all versioned Package files:
- iOS: 11 → 15
- macOS: 10.13 → 12
- tvOS: 11 → 15
- watchOS: 4 → 8

These targets enable native Swift concurrency APIs (async/await) and are the
recommended floor for v3.0 release (Step 3.5).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Consolidates Package manifest to a single Package.swift with swift-tools-version 5.7.
The versioned files are now redundant as the main manifest supports the modernized
v3.0 feature set with proper support for all platforms (Step 3.6).

The single Package.swift with Swift 5.7 is sufficient for modern SPM usage.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Updates swift-tools-version from 5.8 to 6.0 to support swiftLanguageModes
parameter which requires PackageDescription 6. Build verified successfully
with no errors (Step 3.7).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…S 12, tvOS 15, watchOS 8)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…rivacy resource bundling

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
CDMarkdownLabel.parseTextAndExtractURLRanges(_:) was appending to the
urlRanges array without clearing it first. Setting attributedText multiple
times would accumulate URL ranges from all previous assignments, producing
incorrect tap targets. Now the method clears urlRanges before populating it.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
CDMarkdownLayoutManager was comparing background colors by RGBA value to
decide whether to draw rounded corners. This approach breaks when colors
are customized or dynamic. Instead, add a cdMarkdownRoundedBackground
attribute to CDMarkdownCode and CDMarkdownSyntax during parsing, and read
that attribute in the layout manager.

Remove the now-redundant roundCodeCorners and roundSyntaxCorners properties
from CDMarkdownLayoutManager and CDMarkdownLabel. Color customization now
works regardless of the actual color values.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Add strikethroughColor and strikethroughStyle as requirements to the
CDMarkdownStyle protocol with default nil implementations in the protocol
extension. Update the attributes computed property to include strikethrough
attributes when non-nil. This makes strikethrough styling consistent with
other style properties.

Remove the addAttributes(_:range:) override from CDMarkdownStrikethrough
since the base implementation in CDMarkdownCommonElement now correctly
applies strikethrough attributes via the inherited CDMarkdownStyle protocol.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
NSDataDetector with link type can throw Objective-C exceptions on older
watchOS versions, which aren't caught by Swift's do-catch. Additionally,
automatic link detection is not useful on watchOS since WKInterfaceLabel
does not support link taps.

Use a no-op regex pattern on watchOS that never matches, preventing the
crash and avoiding unnecessary work on the platform.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Replace the broken character class [^!{1}] (which incorrectly excludes
characters !, {, 1, }) with a proper negative lookbehind (?<![!]). This
fixes two issues:

1. Character class did not support {1} quantifier; the braces were literal
2. The leading character requirement prevented matching links at position 0

Update match(_:attributedString:) offsets to account for the lookbehind
starting at [ instead of at the preceding character:
- Delete [ at match.range.location instead of location + 1
- Calculate format range using location and length - 2 instead of location + 1
  and length - 3

Links at the start of a string and after digits now parse correctly.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Replace force unwrap in CDFont+CDMarkdownKit.swift withTraits(_:) method
with a guard let fallback. When a font does not support the requested
trait (e.g., a custom font with no bold or italic variant), return the
original font unchanged instead of crashing.

This allows custom fonts without bold/italic variants to be used with
CDMarkdownKit without crashes when parsing bold or italic text.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
chrisdhaan and others added 14 commits May 9, 2026 20:43
Update OS versions to match actual runtimes available on each runner
(sourced from actions/runner-images): iOS 26.4, tvOS 26.4, watchOS 26.4
on macos-26; tvOS 18.5 (not 18.6) on macos-15. Fix Apple Watch model
from Series 10 to Series 11 on macos-26.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Raise minimum OS floors to the lowest versions SPM can express without
deprecation warnings: iOS 12.0, macOS 10.13, tvOS 12.0, watchOS 4.0.
Updated in Package.swift, podspec, Xcode project (all targets including
Example), and README requirements table.

Also fix the SwiftLint Xcode build phase across all four schemes:
- Export /opt/homebrew/bin so `which swiftlint` resolves correctly
- cd to $SRCROOT so SwiftLint finds lintable files
- Disable ENABLE_USER_SCRIPT_SANDBOXING (required for recursive linting)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove machine-specific Local Swift version line. Update platform
support table to reflect new deployment target floors. Fix stale CI
table (tvOS 18.6 -> 18.5).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each platform job (iOS, macOS, tvOS, watchOS) now tests against all five
Xcode 26.x releases on macos-26 (26.0.1, 26.1.1, 26.2, 26.3, 26.4.1)
and all five Xcode 16.x releases on macos-15 (16.0–16.4). Each entry
uses the simulator runtime bundled with that Xcode version; Xcode 26.3
shares the 26.2 runtime as no 26.3 runtime exists.

Also fixes the Xcode app path from the non-existent Xcode_26.0.app to
the correct Xcode_26.0.1.app on macos-26.

Update CLAUDE.md CI table and IMPLEMENTATION.md visionOS CI snippet to
reflect the expanded matrix and corrected Xcode paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- iOS 26 (Xcode 26.4.1): fall back to OS=26.2 (iOS 26.4 runtime has
  issues on GitHub-hosted macos-26 runners)
- iOS/tvOS/watchOS 26 (Xcode 26.0.1): correct iOS to OS=26.0.1 (the
  bundled runtime version), tvOS to name=Apple TV (26.0 bundled runtime
  lacks the 4K 1080p device), watchOS to OS=26.1 (use separately
  installed runtime to ensure device availability)
- iOS/tvOS/watchOS 18 (Xcode 16.1/16.2/16.3): switch from OS=18.5/11.5
  to each Xcode's bundled runtime (16.1→18.1/18.1/11.1,
  16.2→18.2/18.2/11.2, 16.3→18.3.1/18.3/11.3); older Xcodes cannot
  use runtimes that are 3+ minor versions ahead
- All Xcode 16.0 entries: fix path from Xcode.app (which resolves to
  Xcode 16.4, the default) to Xcode_16.0.app with bundled runtimes
  (iOS 18.0, tvOS 18.0, watchOS 11.0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sted runners

macos-26 only has iOS/tvOS/watchOS 26.1, 26.2, 26.4 runtimes (no 26.0.x).
macos-15 only has iOS 18.5/18.6, tvOS 18.5, watchOS 11.5 runtimes (no older 18.x).

- iOS/tvOS 26 (Xcode 26.0.1): OS=26.0.x → OS=26.1
- watchOS 26 (Xcode 26.0.1): use generic/platform=watchOS Simulator (26.1 incompatible with Xcode 26.0.1)
- iOS 18 (Xcode 16.0-16.3): OS=18.0-18.3.1 → OS=18.5 (only available runtime)
- tvOS 18 (Xcode 16.0-16.3): OS=18.0-18.3 → OS=18.5
- watchOS 11 (Xcode 16.0-16.3): OS=11.0-11.3 → OS=11.5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Xcode 26.0.1 shipped before the iOS 26.1 simulator runtime so it cannot
resolve a destination specifier with OS=26.1. generic/platform bypasses
the runtime version check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Xcode 16.3 shipped with iOS 18.3 SDK; the macos-15 runner only has iOS
18.5+ runtimes, so OS=18.5 cannot be resolved. generic/platform bypasses
the runtime version check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove old Xcode versions (26.0.1, 16.0–16.3) that cannot resolve
simulator destinations on available GH runners. Keep only versions
where iOS/tvOS/watchOS SDKs match available runtime versions.

Removed:
- iOS/tvOS: Xcode 26.0.1, 16.0, 16.1, 16.2, 16.3
- watchOS: Xcode 26.0.1, 16.0, 16.1, 16.2, 16.3

Kept: Xcode 26.1.1+ (macos-26), 16.4 (macos-15)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The undocumented.json file generated by Jazzy contains absolute file
system paths and should not be tracked. Add docs/undocumented.json to
.gitignore so it's regenerated locally but not exposed in public docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update all documentation to reflect the removal of unsupported Xcode versions
(26.0.1, 16.0–16.3) from the CI matrix:

- CLAUDE.md: iOS/tvOS/watchOS now run 4 matrix entries (Xcode 26.1.1–26.4.1 + 16.4)
- CHANGELOG.md: Update v3.0.0 CI/CD pipeline description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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