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
This commit is contained in:
11
TESTING.md
11
TESTING.md
@@ -192,6 +192,10 @@ xcrun simctl launch booted de.rfc1437.gotcha
|
|||||||
Multiple labels wrap without clipping at large Dynamic Type sizes and
|
Multiple labels wrap without clipping at large Dynamic Type sizes and
|
||||||
VoiceOver announces each label. Links and selectable text use normal iOS
|
VoiceOver announces each label. Links and selectable text use normal iOS
|
||||||
interaction.
|
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
|
- [ ] Tap the **Add comment** icon on an issue. Enter headings, emphasis, a
|
||||||
list, link, and fenced code block; **Write** syntax-highlights the
|
list, link, and fenced code block; **Write** syntax-highlights the
|
||||||
Markdown, **Preview** renders it, and switching modes preserves the exact
|
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,
|
- [ ] Open a commit and verify its header shows the full title and description,
|
||||||
full hash, author, committer, date, known branch/ref, and signature status
|
full hash, author, committer, date, known branch/ref, and signature status
|
||||||
above the Changed Files paths and statuses. Unsigned commits say
|
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.
|
largest Dynamic Type accessibility size.
|
||||||
- [ ] Open a changed file and verify the diff title, old/new line numbers,
|
- [ ] Open a changed file and verify the diff title, old/new line numbers,
|
||||||
monospaced text, addition/removal/hunk colors, vertical scrolling, and
|
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
|
- [ ] Each milestone shows its title, description, state, due date, issue
|
||||||
counts, and a green/amber closed/open progress bar. Lists and details show
|
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 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
|
- [ ] Open a milestone and verify every assigned issue and pull request is
|
||||||
listed; tapping either opens its normal detail.
|
listed; tapping either opens its normal detail.
|
||||||
- [ ] Tap the add button. **New Milestone** has native Cancel and Save controls,
|
- [ ] Tap the add button. **New Milestone** has native Cancel and Save controls,
|
||||||
|
|||||||
@@ -733,7 +733,8 @@ final class MilestoneViewController: RefreshingTableViewController {
|
|||||||
final class MilestoneCell: UITableViewCell {
|
final class MilestoneCell: UITableViewCell {
|
||||||
private let stateIcon = UIImageView()
|
private let stateIcon = UIImageView()
|
||||||
private let titleLabel = UILabel()
|
private let titleLabel = UILabel()
|
||||||
private let descriptionLabel = UILabel()
|
private let stack = UIStackView()
|
||||||
|
private var descriptionView: UIView?
|
||||||
private let metaLabel = UILabel()
|
private let metaLabel = UILabel()
|
||||||
private let progress = UIProgressView(progressViewStyle: .bar)
|
private let progress = UIProgressView(progressViewStyle: .bar)
|
||||||
|
|
||||||
@@ -744,16 +745,13 @@ final class MilestoneCell: UITableViewCell {
|
|||||||
let titleStack = UIStackView(arrangedSubviews: [stateIcon, titleLabel])
|
let titleStack = UIStackView(arrangedSubviews: [stateIcon, titleLabel])
|
||||||
titleStack.alignment = .firstBaseline
|
titleStack.alignment = .firstBaseline
|
||||||
titleStack.spacing = 8
|
titleStack.spacing = 8
|
||||||
descriptionLabel.font = .preferredFont(forTextStyle: .subheadline)
|
|
||||||
descriptionLabel.textColor = .secondaryLabel
|
|
||||||
descriptionLabel.numberOfLines = 2
|
|
||||||
metaLabel.font = .preferredFont(forTextStyle: .caption1)
|
metaLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||||
metaLabel.textColor = .tertiaryLabel
|
metaLabel.textColor = .tertiaryLabel
|
||||||
metaLabel.numberOfLines = 2
|
metaLabel.numberOfLines = 2
|
||||||
progress.progressTintColor = .systemGreen
|
progress.progressTintColor = .systemGreen
|
||||||
let stack = UIStackView(
|
stack.addArrangedSubview(titleStack)
|
||||||
arrangedSubviews: [titleStack, descriptionLabel, progress, metaLabel]
|
stack.addArrangedSubview(progress)
|
||||||
)
|
stack.addArrangedSubview(metaLabel)
|
||||||
stack.axis = .vertical
|
stack.axis = .vertical
|
||||||
stack.spacing = 7
|
stack.spacing = 7
|
||||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@@ -778,7 +776,12 @@ final class MilestoneCell: UITableViewCell {
|
|||||||
textStyle: .headline
|
textStyle: .headline
|
||||||
)
|
)
|
||||||
titleLabel.text = row.title
|
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
|
metaLabel.text = row.meta
|
||||||
progress.progress = Float(row.progress)
|
progress.progress = Float(row.progress)
|
||||||
progress.trackTintColor = row.hasIssues ? .systemOrange : .systemGray5
|
progress.trackTintColor = row.hasIssues ? .systemOrange : .systemGray5
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Highlighter
|
import Highlighter
|
||||||
import MarkdownUI
|
|
||||||
import QuickLook
|
import QuickLook
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
@@ -402,7 +401,7 @@ private final class FileListView: UITableView, UITableViewDataSource, UITableVie
|
|||||||
private final class CommitHeaderView: UIView {
|
private final class CommitHeaderView: UIView {
|
||||||
private let stack = UIStackView()
|
private let stack = UIStackView()
|
||||||
private let titleLabel = UILabel()
|
private let titleLabel = UILabel()
|
||||||
private let descriptionLabel = UILabel()
|
private var descriptionView: UIView?
|
||||||
private let metadataStack = UIStackView()
|
private let metadataStack = UIStackView()
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
@@ -414,9 +413,6 @@ private final class CommitHeaderView: UIView {
|
|||||||
titleLabel.adjustsFontForContentSizeCategory = true
|
titleLabel.adjustsFontForContentSizeCategory = true
|
||||||
titleLabel.numberOfLines = 0
|
titleLabel.numberOfLines = 0
|
||||||
titleLabel.accessibilityTraits = .header
|
titleLabel.accessibilityTraits = .header
|
||||||
descriptionLabel.font = .preferredFont(forTextStyle: .body)
|
|
||||||
descriptionLabel.adjustsFontForContentSizeCategory = true
|
|
||||||
descriptionLabel.numberOfLines = 0
|
|
||||||
|
|
||||||
metadataStack.axis = .vertical
|
metadataStack.axis = .vertical
|
||||||
metadataStack.spacing = 10
|
metadataStack.spacing = 10
|
||||||
@@ -424,7 +420,6 @@ private final class CommitHeaderView: UIView {
|
|||||||
stack.spacing = 12
|
stack.spacing = 12
|
||||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
stack.addArrangedSubview(titleLabel)
|
stack.addArrangedSubview(titleLabel)
|
||||||
stack.addArrangedSubview(descriptionLabel)
|
|
||||||
stack.addArrangedSubview(metadataStack)
|
stack.addArrangedSubview(metadataStack)
|
||||||
addSubview(stack)
|
addSubview(stack)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
@@ -440,8 +435,12 @@ private final class CommitHeaderView: UIView {
|
|||||||
|
|
||||||
func configure(_ page: CommitDetailsPage) {
|
func configure(_ page: CommitDetailsPage) {
|
||||||
titleLabel.text = page.title
|
titleLabel.text = page.title
|
||||||
descriptionLabel.text = page.description
|
descriptionView?.removeFromSuperview()
|
||||||
descriptionLabel.isHidden = page.description.isEmpty
|
if !page.description.isEmpty {
|
||||||
|
let view = markdownView(page.description)
|
||||||
|
stack.insertArrangedSubview(view, at: 1)
|
||||||
|
descriptionView = view
|
||||||
|
}
|
||||||
metadataStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
metadataStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
for metadata in page.metadata {
|
for metadata in page.metadata {
|
||||||
let isCommit = metadata.label == "Commit"
|
let isCommit = metadata.label == "Commit"
|
||||||
@@ -1132,9 +1131,7 @@ struct RepositoryMarkdownPreview: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
Markdown(source)
|
MarkdownContent(source: source)
|
||||||
.markdownTheme(.gitHub)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
.background(Color(uiColor: .systemBackground))
|
.background(Color(uiColor: .systemBackground))
|
||||||
@@ -1235,6 +1232,7 @@ final class DiffViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private func detailViews(
|
private func detailViews(
|
||||||
title: String,
|
title: String,
|
||||||
state: String? = nil,
|
state: String? = nil,
|
||||||
@@ -1285,6 +1283,7 @@ private func stateTitle(_ titleLabel: UILabel, state: String, subject: String) -
|
|||||||
return stack
|
return stack
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private func commentViews(
|
private func commentViews(
|
||||||
_ comments: [CommentRow],
|
_ comments: [CommentRow],
|
||||||
editComment: ((CommentRow) -> Void)? = nil
|
editComment: ((CommentRow) -> Void)? = nil
|
||||||
@@ -1323,19 +1322,6 @@ private func commentViews(
|
|||||||
return views
|
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 {
|
private func sectionLabel(_ text: String) -> UILabel {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = text
|
label.text = text
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
|
import MarkdownUI
|
||||||
|
import SwiftUI
|
||||||
import UIKit
|
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 {
|
func localDate(forUTCTimestamp timestamp: Int64) -> Date {
|
||||||
var utc = Calendar(identifier: .gregorian)
|
var utc = Calendar(identifier: .gregorian)
|
||||||
utc.timeZone = TimeZone(secondsFromGMT: 0)!
|
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) {
|
func configureTextCell(_ cell: UITableViewCell, title: String, detail: String, image: UIImage? = nil) {
|
||||||
var content = cell.defaultContentConfiguration()
|
var content = cell.defaultContentConfiguration()
|
||||||
content.text = title
|
content.text = title
|
||||||
|
|||||||
Reference in New Issue
Block a user