diff --git a/Examples/Demo/Demo/ImagesView.swift b/Examples/Demo/Demo/ImagesView.swift index 3b8fab36..969ca300 100644 --- a/Examples/Demo/Demo/ImagesView.swift +++ b/Examples/Demo/Demo/ImagesView.swift @@ -5,14 +5,21 @@ struct ImagesView: View { private let content = """ You can display an image by adding `!` and wrapping the alt text in `[ ]`. Then wrap the link for the image in parentheses `()`. - + ``` - ![This is an image](https://picsum.photos/id/91/400/300) + ![This is a 50 px image](https://picsum.photos/id/91/400/300){width=50px} ``` - - ![This is an image](https://picsum.photos/id/91/400/300) - + + ![This is a 50px image](https://picsum.photos/id/91/400/300){width=50px} + ― Photo by Jennifer Trovato + + ``` + ![This is a 50% image](https://i.natgeofe.com/n/548467d8-c5f1-4551-9f58-6817a8d2c45e/NationalGeographic_2572187_3x2.jpg){width=50%} + ``` + + ![This is a 50% image](https://i.natgeofe.com/n/548467d8-c5f1-4551-9f58-6817a8d2c45e/NationalGeographic_2572187_3x2.jpg){width=50%} + """ private let inlineImageContent = """ @@ -45,6 +52,7 @@ struct ImagesView: View { } .markdownBlockStyle(\.image) { configuration in configuration.label + .scaledToFit() .clipShape(RoundedRectangle(cornerRadius: 8)) .shadow(radius: 8, y: 8) .markdownMargin(top: .em(1.6), bottom: .em(1.6)) diff --git a/Sources/MarkdownUI/Utility/InlineNode+RawImageData.swift b/Sources/MarkdownUI/Utility/InlineNode+RawImageData.swift index 11f77d3f..579859e1 100644 --- a/Sources/MarkdownUI/Utility/InlineNode+RawImageData.swift +++ b/Sources/MarkdownUI/Utility/InlineNode+RawImageData.swift @@ -20,3 +20,84 @@ extension InlineNode { } } } + +extension InlineNode { + var size: MarkdownImageSize? { + switch self { + case .text(let input): + // Trying first to found a fixed pattern match + let fixedPattern = "\\{(?:width\\s*=\\s*(\\d+)px\\s*)?(?:height\\s*=\\s*(\\d+)px\\s*)?(?:width\\s*=\\s*(\\d+)px\\s*)?(?:height\\s*=\\s*(\\d+)px\\s*)?\\}" + + if let (width, height) = extract(regexPattern: fixedPattern, from: input) { + return MarkdownImageSize(value: .fixed(width, height)) + } + + // Trying then to found a relative pattern match + let relativePattern = "\\{(?:width\\s*=\\s*(\\d+)%\\s*)?(?:height\\s*=\\s*(\\d+)%\\s*)?(?:width\\s*=\\s*(\\d+)%\\s*)?(?:height\\s*=\\s*(\\d+)%\\s*)?\\}" + + if let (wRatio, hRatio) = extract(regexPattern: relativePattern, from: input) { + return MarkdownImageSize(value: .relative((wRatio ?? 100)/100, (hRatio ?? 100)/100)) + } + + return nil + default: + return nil + } + } + + private func extract( + regexPattern pattern: String, + from input: String + ) -> (width: CGFloat?, height: CGFloat?)? { + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { + return nil + } + + let range = NSRange(input.startIndex.. some View { + self.modifier(ImageViewFrameModifier(size: size)) + } +} + +private struct ImageViewFrameModifier: ViewModifier { + let size: MarkdownImageSize? + + @State private var currentSize: CGSize = .zero + + func body(content: Content) -> some View { + if let size { + switch size.value { + case .fixed(let width, let height): + content + .frame(width: width, height: height) + case .relative(let wRatio, _): + ZStack(alignment: .leading) { + /// Track the full content width. + GeometryReader { metrics in + content + .preference(key: BoundsPreferenceKey.self, value: metrics.frame(in: .global).size) + } + .opacity(0.0) + + /// Draw the content applying relative width. Relative height is not handled. + content + .frame( + width: currentSize.width * wRatio + ) + } + .onPreferenceChange(BoundsPreferenceKey.self) { newValue in + /// Avoid recursive loop that could happens + /// https://developer.apple.com/videos/play/wwdc2022/10056/?time=1107 + if Int(currentSize.width) == Int(newValue.width), + Int(currentSize.height) == Int(newValue.height) { + return + } + + self.currentSize = newValue + } + } + } else { + content + } + } +} + +private struct BoundsPreferenceKey: PreferenceKey { + static var defaultValue: CGSize = .zero + + static func reduce(value: inout Value, nextValue: () -> Value) { + value = nextValue() + } +}