johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
import SwiftUI
import WebKit
/// A picture Markdown names, at its own size unless that's wider than the
/// page. One that won't load says what it was of.
struct MarkdownImage: View {
let urls: [URL]
let alt: String
private enum Loaded {
case bitmap(UIImage)
case svg(Data, CGSize)
}
@State private var loaded: Loaded?
@State private var failed = false
var body: some View {
Group {
switch loaded {
case .bitmap(let image):
Image(uiImage: image).resizable().sized(image.size)
case .svg(let data, let size):
SVGView(data: data).sized(size)
case nil:
if failed {
if !alt.isEmpty { Text(alt).foregroundStyle(.secondary) }
} else {
RoundedRectangle(cornerRadius: Radius.small).fill(Color.raised).frame(width: 120, height: 24)
}
}
}
.accessibilityLabel(alt)
.task(id: urls) { await load() }
}
private func load() async {
for url in urls {
guard let data = try? await Client.shared.picture(url) else { continue }
if let image = UIImage(data: data), image.size.width > 0 {
loaded = .bitmap(image)
return
}
if let size = SVGSize.of(data) {
loaded = .svg(data, size)
return
}
}
failed = true
}
}
private extension View {
func sized(_ size: CGSize) -> some View {
aspectRatio(size.width / size.height, contentMode: .fit).frame(maxWidth: size.width)
}
}
/// What an SVG says its size is, from its width and height, or its viewBox.
enum SVGSize {
static func of(_ data: Data) -> CGSize? {
guard let text = String(data: data.prefix(4096), encoding: .utf8),
let open = text.firstMatch(of: #/<svg\b[^>]*>/#)?.output else { return nil }
let tag = String(open)
func number(_ name: String) -> CGFloat? {
guard let found = try? Regex("\\s\(name)=[\"']([0-9.]+)(px)?[\"']").firstMatch(in: tag),
let value = found[1].substring else { return nil }
return Double(value).map { CGFloat($0) }
}
if let w = number("width"), let h = number("height"), w > 0, h > 0 { return CGSize(width: w, height: h) }
if let box = tag.firstMatch(of: #/viewBox=["'][-0-9.]+[\s,]+[-0-9.]+[\s,]+([0-9.]+)[\s,]+([0-9.]+)["']/#),
let w = Double(box.1), let h = Double(box.2), w > 0, h > 0 {
return CGSize(width: w, height: h)
}
return CGSize(width: 240, height: 240)
}
}
/// An SVG drawn by WebKit as an image, in which it can run no script and fetch nothing.
private struct SVGView: UIViewRepresentable {
let data: Data
func makeUIView(context: Context) -> WKWebView {
let view = WKWebView(frame: .zero)
view.isOpaque = false
view.backgroundColor = .clear
view.scrollView.isScrollEnabled = false
view.isUserInteractionEnabled = false
view.loadHTMLString(
"<!doctype html><meta name=viewport content='width=device-width,initial-scale=1'><style>html,body{margin:0;background:transparent}img{display:block;width:100%;height:auto}</style><img src='data:image/svg+xml;base64,\(data.base64EncodedString())'>",
baseURL: nil)
return view
}
func updateUIView(_ view: WKWebView, context: Context) {}
}
/// Pictures side by side, wrapping as words do, as a README's row of badges.
struct FlowRow: Layout {
var spacing: CGFloat = 6
private func rows(_ width: CGFloat, _ subviews: Subviews) -> (CGSize, [(Int, CGPoint, CGSize)]) {
var placed: [(Int, CGPoint, CGSize)] = []
var x: CGFloat = 0, y: CGFloat = 0, tallest: CGFloat = 0, widest: CGFloat = 0
for (i, view) in subviews.enumerated() {
let size = view.sizeThatFits(ProposedViewSize(width: width, height: nil))
if x > 0, x + size.width > width {
x = 0
y += tallest + spacing
tallest = 0
}
placed.append((i, CGPoint(x: x, y: y), size))
x += size.width + spacing
tallest = max(tallest, size.height)
widest = max(widest, x - spacing)
}
return (CGSize(width: widest, height: y + tallest), placed)
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
rows(proposal.width ?? .infinity, subviews).0
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
for (i, at, size) in rows(bounds.width, subviews).1 {
subviews[i].place(at: CGPoint(x: bounds.minX + at.x, y: bounds.minY + at.y), proposal: ProposedViewSize(size))
}
}
}