Skip to content

Commit

Permalink
Render images (#147)
Browse files Browse the repository at this point in the history
* Enable code coverage

* Implement image loading

* Render single-image paragraphs

* Update workflows

* Render paragraphs with multiple images and no text

* Run swift format

* Test image links in multi-image paragraphs
  • Loading branch information
Guille Gonzalez authored Oct 24, 2022
1 parent 8073737 commit 6396fc7
Show file tree
Hide file tree
Showing 41 changed files with 1,146 additions and 12 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: macos-12
steps:
- uses: actions/checkout@v2
- name: Select Xcode 14.0
run: sudo xcode-select -s /Applications/Xcode_14.0.app
- name: Select Xcode 14.1
run: sudo xcode-select -s /Applications/Xcode_14.1.app
- name: Run tests
run: make test
6 changes: 3 additions & 3 deletions .github/workflows/format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ jobs:
runs-on: macos-12
steps:
- uses: actions/checkout@v2
- name: Select Xcode 13.4.1
run: sudo xcode-select -s /Applications/Xcode_13.4.1.app
- name: Select Xcode 14.1
run: sudo xcode-select -s /Applications/Xcode_14.1.app
- name: Tap
run: brew tap pointfreeco/formulae
- name: Install
run: brew install Formulae/swift-format@5.6
run: brew install Formulae/swift-format@5.7
- name: Format
run: make format
- uses: stefanzweifel/git-auto-commit-action@v4
Expand Down
3 changes: 2 additions & 1 deletion .swiftpm/xcode/xcshareddata/xcschemes/MarkdownUI.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ test-macos:
test-ios:
xcodebuild test \
-scheme MarkdownUI \
-destination platform="iOS Simulator,name=iPhone 8"
-destination platform="iOS Simulator,name=iPhone SE (3rd generation)"

test-tvos:
xcodebuild test \
Expand All @@ -18,7 +18,7 @@ readme-images:
xcodebuild test \
"OTHER_SWIFT_FLAGS=${inherited} -D README_IMAGES" \
-scheme MarkdownUI \
-destination platform="iOS Simulator,name=iPhone 8" \
-destination platform="iOS Simulator,name=iPhone SE (3rd generation)" \
-only-testing "MarkdownUITests/ReadMeImagesTests" || true
mv Tests/MarkdownUITests/__Snapshots__/ReadMeImagesTests Images
sips -Z 400 Images/*.png
Expand Down
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ let package = Package(
.target(name: "cmark-gfm"),
.target(
name: "MarkdownUI",
dependencies: ["cmark-gfm"]
dependencies: [
"cmark-gfm",
.product(name: "CombineSchedulers", package: "combine-schedulers"),
]
),
.testTarget(
name: "MarkdownUITests",
Expand Down
49 changes: 49 additions & 0 deletions Sources/MarkdownUI/Document/Inlines/AnyInline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,53 @@ extension AnyInline {
return nil
}
}

var text: String {
switch self {
case .text(let content):
return content
case .softBreak:
return " "
case .lineBreak:
return "\n"
case .code(let content):
return content
case .html(let content):
return content
case .emphasis(let children):
return children.text
case .strong(let children):
return children.text
case .strikethrough(let children):
return children.text
case .link(_, let children):
return children.text
case .image(_, _, let children):
return children.text
}
}
}

extension Array where Element == AnyInline {
var text: String {
map(\.text).joined()
}
}

extension AnyInline {
var image: (source: String?, alt: String)? {
guard case let .image(source, _, children) = self else {
return nil
}
return (source, children.text)
}

var imageLink: (source: String?, alt: String, destination: String?)? {
guard case let .link(destination, children) = self, children.count == 1,
let (source, alt) = children.first?.image
else {
return nil
}
return (source, alt, destination)
}
}
38 changes: 38 additions & 0 deletions Sources/MarkdownUI/Theme/Styles/ImageStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import SwiftUI

public struct ImageStyle {
public struct Configuration {
public struct Content: View {
init<C: View>(_ content: C) {
self.body = AnyView(content)
}

public let body: AnyView
}

public let content: Content
}

let makeBody: (Configuration) -> AnyView

public init<Body>(@ViewBuilder makeBody: @escaping (Configuration) -> Body) where Body: View {
self.makeBody = { configuration in
AnyView(makeBody(configuration))
}
}
}

extension ImageStyle {
public static var `default`: Self {
.init { $0.content }
}

public static func alignment(_ alignment: HorizontalAlignment) -> Self {
.init { configuration in
ZStack {
configuration.content
}
.frame(maxWidth: .infinity, alignment: .init(horizontal: alignment, vertical: .center))
}
}
}
16 changes: 14 additions & 2 deletions Sources/MarkdownUI/Theme/Theme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ public struct Theme {
// MARK: - Metrics

public var paragraphSpacing: CGFloat
public var horizontalImageSpacing: CGFloat
public var verticalImageSpacing: CGFloat

// MARK: - Inline styles

Expand All @@ -14,29 +16,38 @@ public struct Theme {
public var strong: InlineStyle
public var strikethrough: InlineStyle
public var link: InlineStyle
public var image: ImageStyle
}

extension Theme {
private enum Defaults {
static let paragraphSpacing = Font.TextStyle.body.pointSize
static let horizontalImageSpacing = floor(Font.TextStyle.body.pointSize / 4)
static let verticalImageSpacing = floor(Font.TextStyle.body.pointSize / 4)
}

public init(
paragraphSpacing: CGFloat? = nil,
horizontalImageSpacing: CGFloat? = nil,
verticalImageSpacing: CGFloat? = nil,
baseFont: Font = .body,
inlineCode: InlineStyle,
emphasis: InlineStyle,
strong: InlineStyle,
strikethrough: InlineStyle,
link: InlineStyle
link: InlineStyle,
image: ImageStyle
) {
self.paragraphSpacing = paragraphSpacing ?? Defaults.paragraphSpacing
self.horizontalImageSpacing = horizontalImageSpacing ?? Defaults.horizontalImageSpacing
self.verticalImageSpacing = verticalImageSpacing ?? Defaults.verticalImageSpacing
self.baseFont = baseFont
self.inlineCode = inlineCode
self.emphasis = emphasis
self.strong = strong
self.strikethrough = strikethrough
self.link = link
self.image = image
}
}

Expand All @@ -47,7 +58,8 @@ extension Theme {
emphasis: .italic,
strong: .bold,
strikethrough: .strikethrough,
link: .default
link: .default,
image: .default
)
}
}
10 changes: 9 additions & 1 deletion Sources/MarkdownUI/Views/Blocks/AnyBlock+View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ extension AnyBlock: View {
public var body: some View {
switch self {
case .paragraph(let inlines):
ParagraphView(inlines)
if let singleImageParagraph = SingleImageParagraphView(inlines) {
singleImageParagraph
} else if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *),
let imageParagraph = ImageParagraphView(inlines)
{
imageParagraph
} else {
ParagraphView(inlines)
}
}
}
}
58 changes: 58 additions & 0 deletions Sources/MarkdownUI/Views/Blocks/ImageParagraphView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import SwiftUI

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
struct ImageParagraphView: View {
private enum Item: Hashable {
case image(source: String?, alt: String, destination: String? = nil)
case lineBreak
}

@Environment(\.theme.paragraphSpacing) private var paragraphSpacing
@Environment(\.theme.horizontalImageSpacing) private var horizontalSpacing
@Environment(\.theme.verticalImageSpacing) private var verticalSpacing

private let items: [Identified<Int, Item>]

var body: some View {
Flow(horizontalSpacing: self.horizontalSpacing, verticalSpacing: self.verticalSpacing) {
ForEach(self.items) { item in
switch item.value {
case let .image(source, alt, destination):
ImageView(source: source, alt: alt, destination: destination)
case .lineBreak:
Spacer()
}
}
}
.preference(key: BlockSpacingPreference.self, value: paragraphSpacing)
}
}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
extension ImageParagraphView {
init?(_ inlines: [AnyInline]) {
var items: [Item] = []

for inline in inlines {
switch inline {
case let .text(text) where text.isEmpty:
continue
case .softBreak:
continue
case .lineBreak:
items.append(.lineBreak)
case let .image(source, _, children):
items.append(.image(source: source, alt: children.text))
case let .link(destination, children) where children.count == 1:
guard let (source, alt) = children.first?.image else {
return nil
}
items.append(.image(source: source, alt: alt, destination: destination))
default:
return nil
}
}

self.items = items.identified()
}
}
25 changes: 25 additions & 0 deletions Sources/MarkdownUI/Views/Blocks/SingleImageParagraphView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SwiftUI

struct SingleImageParagraphView: View {
@Environment(\.theme.paragraphSpacing) private var paragraphSpacing

private let content: ImageView

private init(content: ImageView) {
self.content = content
}

var body: some View {
self.content
.preference(key: BlockSpacingPreference.self, value: paragraphSpacing)
}
}

extension SingleImageParagraphView {
init?(_ inlines: [AnyInline]) {
guard let content = ImageView(inlines) else {
return nil
}
self.init(content: content)
}
}
Loading

0 comments on commit 6396fc7

Please sign in to comment.