From d0ec533da67b3212cea5b919e845cee2031fd301 Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Sat, 1 Aug 2026 15:24:01 +0200 Subject: [PATCH] Render descriptions with shared Markdown preview Use one Markdown presentation for free-form content. - issue and pull-request descriptions and comments - milestone and commit descriptions --- TESTING.md | 11 ++++++++++- ios/Sources/ContentScreens.swift | 19 ++++++++++-------- ios/Sources/DetailScreens.swift | 34 ++++++++++---------------------- ios/Sources/Support.swift | 34 +++++++++++++++++++++----------- 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/TESTING.md b/TESTING.md index e8d964b..ce443d9 100644 --- a/TESTING.md +++ b/TESTING.md @@ -192,6 +192,10 @@ xcrun simctl launch booted de.rfc1437.gotcha Multiple labels wrap without clipping at large Dynamic Type sizes and VoiceOver announces each label. Links and selectable text use normal iOS interaction. +- [ ] Open issue #50 and verify its body renders separate paragraphs and a + two-item unordered list. Add or edit a comment containing two paragraphs, + an unordered list, emphasis, and a link; verify every Markdown block and + inline style renders correctly after saving and after pull-to-refresh. - [ ] Tap the **Add comment** icon on an issue. Enter headings, emphasis, a list, link, and fenced code block; **Write** syntax-highlights the Markdown, **Preview** renders it, and switching modes preserves the exact @@ -220,7 +224,9 @@ xcrun simctl launch booted de.rfc1437.gotcha - [ ] Open a commit and verify its header shows the full title and description, full hash, author, committer, date, known branch/ref, and signature status above the Changed Files paths and statuses. Unsigned commits say - **Unsigned**. Check that long values wrap and remain readable at the + **Unsigned**. Verify paragraphs, lists, links, and inline styles in a + multi-line commit description use the repository file Preview Markdown + presentation. Check that long values wrap and remain readable at the largest Dynamic Type accessibility size. - [ ] Open a changed file and verify the diff title, old/new line numbers, monospaced text, addition/removal/hunk colors, vertical scrolling, and @@ -300,6 +306,9 @@ xcrun simctl launch booted de.rfc1437.gotcha - [ ] Each milestone shows its title, description, state, due date, issue counts, and a green/amber closed/open progress bar. Lists and details show a green open or purple closed icon and VoiceOver announces the state. +- [ ] A milestone description containing multiple paragraphs, a list, emphasis, + and a link uses the repository file Preview Markdown presentation in both + the milestone list and detail. - [ ] Open a milestone and verify every assigned issue and pull request is listed; tapping either opens its normal detail. - [ ] Tap the add button. **New Milestone** has native Cancel and Save controls, diff --git a/ios/Sources/ContentScreens.swift b/ios/Sources/ContentScreens.swift index 4ac1d0e..c63d3e1 100644 --- a/ios/Sources/ContentScreens.swift +++ b/ios/Sources/ContentScreens.swift @@ -733,7 +733,8 @@ final class MilestoneViewController: RefreshingTableViewController { final class MilestoneCell: UITableViewCell { private let stateIcon = UIImageView() private let titleLabel = UILabel() - private let descriptionLabel = UILabel() + private let stack = UIStackView() + private var descriptionView: UIView? private let metaLabel = UILabel() private let progress = UIProgressView(progressViewStyle: .bar) @@ -744,16 +745,13 @@ final class MilestoneCell: UITableViewCell { let titleStack = UIStackView(arrangedSubviews: [stateIcon, titleLabel]) titleStack.alignment = .firstBaseline titleStack.spacing = 8 - descriptionLabel.font = .preferredFont(forTextStyle: .subheadline) - descriptionLabel.textColor = .secondaryLabel - descriptionLabel.numberOfLines = 2 metaLabel.font = .preferredFont(forTextStyle: .caption1) metaLabel.textColor = .tertiaryLabel metaLabel.numberOfLines = 2 progress.progressTintColor = .systemGreen - let stack = UIStackView( - arrangedSubviews: [titleStack, descriptionLabel, progress, metaLabel] - ) + stack.addArrangedSubview(titleStack) + stack.addArrangedSubview(progress) + stack.addArrangedSubview(metaLabel) stack.axis = .vertical stack.spacing = 7 stack.translatesAutoresizingMaskIntoConstraints = false @@ -778,7 +776,12 @@ final class MilestoneCell: UITableViewCell { textStyle: .headline ) titleLabel.text = row.title - descriptionLabel.text = row.description + descriptionView?.removeFromSuperview() + if !row.description.isEmpty { + let view = markdownView(row.description) + stack.insertArrangedSubview(view, at: 1) + descriptionView = view + } metaLabel.text = row.meta progress.progress = Float(row.progress) progress.trackTintColor = row.hasIssues ? .systemOrange : .systemGray5 diff --git a/ios/Sources/DetailScreens.swift b/ios/Sources/DetailScreens.swift index 885988e..5361b4e 100644 --- a/ios/Sources/DetailScreens.swift +++ b/ios/Sources/DetailScreens.swift @@ -1,5 +1,4 @@ import Highlighter -import MarkdownUI import QuickLook import SwiftUI import UIKit @@ -402,7 +401,7 @@ private final class FileListView: UITableView, UITableViewDataSource, UITableVie private final class CommitHeaderView: UIView { private let stack = UIStackView() private let titleLabel = UILabel() - private let descriptionLabel = UILabel() + private var descriptionView: UIView? private let metadataStack = UIStackView() override init(frame: CGRect) { @@ -414,9 +413,6 @@ private final class CommitHeaderView: UIView { titleLabel.adjustsFontForContentSizeCategory = true titleLabel.numberOfLines = 0 titleLabel.accessibilityTraits = .header - descriptionLabel.font = .preferredFont(forTextStyle: .body) - descriptionLabel.adjustsFontForContentSizeCategory = true - descriptionLabel.numberOfLines = 0 metadataStack.axis = .vertical metadataStack.spacing = 10 @@ -424,7 +420,6 @@ private final class CommitHeaderView: UIView { stack.spacing = 12 stack.translatesAutoresizingMaskIntoConstraints = false stack.addArrangedSubview(titleLabel) - stack.addArrangedSubview(descriptionLabel) stack.addArrangedSubview(metadataStack) addSubview(stack) NSLayoutConstraint.activate([ @@ -440,8 +435,12 @@ private final class CommitHeaderView: UIView { func configure(_ page: CommitDetailsPage) { titleLabel.text = page.title - descriptionLabel.text = page.description - descriptionLabel.isHidden = page.description.isEmpty + descriptionView?.removeFromSuperview() + if !page.description.isEmpty { + let view = markdownView(page.description) + stack.insertArrangedSubview(view, at: 1) + descriptionView = view + } metadataStack.arrangedSubviews.forEach { $0.removeFromSuperview() } for metadata in page.metadata { let isCommit = metadata.label == "Commit" @@ -1132,9 +1131,7 @@ struct RepositoryMarkdownPreview: View { var body: some View { ScrollView { - Markdown(source) - .markdownTheme(.gitHub) - .frame(maxWidth: .infinity, alignment: .leading) + MarkdownContent(source: source) .padding() } .background(Color(uiColor: .systemBackground)) @@ -1235,6 +1232,7 @@ final class DiffViewController: UIViewController { } } +@MainActor private func detailViews( title: String, state: String? = nil, @@ -1285,6 +1283,7 @@ private func stateTitle(_ titleLabel: UILabel, state: String, subject: String) - return stack } +@MainActor private func commentViews( _ comments: [CommentRow], editComment: ((CommentRow) -> Void)? = nil @@ -1323,19 +1322,6 @@ private func commentViews( return views } -private func markdownView(_ source: String) -> UITextView { - let view = UITextView() - view.attributedText = markdown(source) - view.isEditable = false - view.isSelectable = true - view.isScrollEnabled = false - view.backgroundColor = .clear - view.textContainerInset = .zero - view.textContainer.lineFragmentPadding = 0 - view.adjustsFontForContentSizeCategory = true - return view -} - private func sectionLabel(_ text: String) -> UILabel { let label = UILabel() label.text = text diff --git a/ios/Sources/Support.swift b/ios/Sources/Support.swift index 11ceba0..f08d137 100644 --- a/ios/Sources/Support.swift +++ b/ios/Sources/Support.swift @@ -1,5 +1,28 @@ +import MarkdownUI +import SwiftUI import UIKit +struct MarkdownContent: View { + let source: String + + var body: some View { + Markdown(source) + .markdownTheme(.gitHub) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +@MainActor +func markdownView(_ source: String) -> UIView { + UIHostingConfiguration { + MarkdownContent(source: source) + .fixedSize(horizontal: false, vertical: true) + } + .margins(.all, 0) + .makeContentView() +} + func localDate(forUTCTimestamp timestamp: Int64) -> Date { var utc = Calendar(identifier: .gregorian) utc.timeZone = TimeZone(secondsFromGMT: 0)! @@ -179,17 +202,6 @@ extension UIColor { } } -func markdown(_ source: String, textStyle: UIFont.TextStyle = .body) -> NSAttributedString { - let attributed = (try? AttributedString( - markdown: source, - options: .init(interpretedSyntax: .full) - )) ?? AttributedString(source) - var mutable = attributed - mutable.font = .preferredFont(forTextStyle: textStyle) - mutable.foregroundColor = UIColor.label - return NSAttributedString(mutable) -} - func configureTextCell(_ cell: UITableViewCell, title: String, detail: String, image: UIImage? = nil) { var content = cell.defaultContentConfiguration() content.text = title