Commit 1620a0f1e7
1620a0f1e742ba53f162de2e613ce25dc6fe8145
parent: e0ca3558d1
Verified · cmc
cmc <hello@cleberg.net> · 2026-08-28T18:51:52Z
OrgSwift: image-as-link descriptions render as clickable images
An org link whose description is itself an image — [[url][file:badge.svg]] (the
common build-badge form) or [[url][[badge.svg]]] — makes the image the anchor's
content instead of showing the raw description text. A plain-text description
that merely looks path-like stays text.
Sources/OrgSwift/Links.swift
+22 −8
| @@ -74,6 +74,19 @@ func normalizeOrgLinkTarget(_ target: String) -> String { |
| 74 | 74 | target.hasPrefix("file:") ? String(target.dropFirst(5)) : target |
| 75 | 75 | } |
| 76 | 76 | |
| 77 | /// If a link *description* is itself an image reference — `[[img]]`, a `file:` image, or an |
| 78 | /// image URL — return its source, so the image becomes the link's content. A bare relative |
| 79 | /// string (`img.png`) is a plain text description, not an image. |
| 80 | private func descriptionImageSource(_ label: String) -> String? { |
| 81 | if label.hasPrefix("[["), label.hasSuffix("]]") { |
| 82 | return String(label.dropFirst(2).dropLast(2)) |
| 83 | } |
| 84 | if label.hasPrefix("file:") || label.hasPrefix("http://") || label.hasPrefix("https://") { |
| 85 | return label |
| 86 | } |
| 87 | return nil |
| 88 | } |
| 89 | |
| 77 | 90 | private func renderOrgLink( |
| 78 | 91 | destination rawDestination: String, |
| 79 | 92 | label: String?, |
| @@ -90,15 +103,16 @@ private func renderOrgLink( |
| 90 | 103 | return ##"<a href="#\##(escapeHTMLAttribute(id))">\##(text)</a>"## |
| 91 | 104 | } |
| 92 | 105 | let destination = normalizeOrgLinkTarget(rawDestination) |
| 93 | | if let label, label.hasPrefix("[["), label.hasSuffix("]]") { |
| 94 | | let source = String(label.dropFirst(2).dropLast(2)) |
| 95 | | if let imageHTML = makeOrgImageHTML(source: source, alt: nil, imageURLResolver: imageURLResolver) { |
| 96 | | let resolvedDestination = linkURLResolver?(destination) ?? destination |
| 97 | | guard let sanitizedURL = sanitizedReadmeLinkURLString(resolvedDestination) else { |
| 98 | | return imageHTML |
| 99 | | } |
| 100 | | return #"<a href="\#(sanitizedURL)">\#(imageHTML)</a>"# |
| 106 | // A description that is itself an image link (`[[url][file:badge.svg]]`, the common |
| 107 | // build-badge form, or the double-bracketed `[[url][[badge.svg]]]`) makes the image the |
| 108 | // clickable content of the link. A plain-text description like `img.png` stays text. |
| 109 | if let label, let source = descriptionImageSource(label), |
| 110 | let imageHTML = makeOrgImageHTML(source: source, alt: nil, imageURLResolver: imageURLResolver) { |
| 111 | let resolvedDestination = linkURLResolver?(destination) ?? destination |
| 112 | guard let sanitizedURL = sanitizedReadmeLinkURLString(resolvedDestination) else { |
| 113 | return imageHTML |
| 101 | 114 | } |
| 115 | return #"<a href="\#(sanitizedURL)">\#(imageHTML)</a>"# |
| 102 | 116 | } |
| 103 | 117 | |
| 104 | 118 | // A bare `[[image]]` with no description is an inline image. `[[image][text]]` is a |
Tests/OrgSwiftTests/OrgRendererTests.swift
+16
| @@ -325,6 +325,22 @@ struct OrgRendererTests { |
| 325 | 325 | #expect(!html.contains("<img")) |
| 326 | 326 | } |
| 327 | 327 | |
| 328 | @Test |
| 329 | func imageDescriptionBecomesClickableImage() { |
| 330 | // A build-badge: the description is itself an image, so it is the link's content. |
| 331 | let badge = render("[[https://example.com/builds][file:https://example.com/badge.svg]]") |
| 332 | #expect(badge.contains(#"<a href="https://example.com/builds"><img src="https://example.com/badge.svg" alt=""></a>"#)) |
| 333 | |
| 334 | // The double-bracketed form works the same. |
| 335 | let bracketed = render("[[https://example.com/builds][[https://example.com/badge.svg]]]") |
| 336 | #expect(bracketed.contains(#"<a href="https://example.com/builds"><img"#)) |
| 337 | |
| 338 | // A plain-text description that merely looks path-like stays text. |
| 339 | let plain = render("[[https://example.com][see diagram.png]]") |
| 340 | #expect(plain.contains(#"<a href="https://example.com">see diagram.png</a>"#)) |
| 341 | #expect(!plain.contains("<img")) |
| 342 | } |
| 343 | |
| 328 | 344 | @Test |
| 329 | 345 | func captionPromotesImageToFigure() { |
| 330 | 346 | let html = render(""" |