krz/org-swift

A dependency-free Swift library that renders org-mode to sanitized HTML. html library org-mode swift

Commit 753f773650

753f7736507fbe15f790f3a14b233af0ccc0e5ea

parent: eb2beb19c9

Unsigned

cmc <hello@cleberg.net> · 2026-08-28 18:51 UTC

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.

Layout: unified · split

Sources/OrgSwift/Links.swift +22 −8
@@ -74,6 +74,19 @@ func normalizeOrgLinkTarget(_ target: String) -> String {
74 target.hasPrefix("file:") ? String(target.dropFirst(5)) : target 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.
80private 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
77private func renderOrgLink( 90private func renderOrgLink(
78 destination rawDestination: String, 91 destination rawDestination: String,
79 label: String?, 92 label: String?,
@@ -90,15 +103,16 @@ private func renderOrgLink(
90 return ##"<a href="#\##(escapeHTMLAttribute(id))">\##(text)</a>"## 103 return ##"<a href="#\##(escapeHTMLAttribute(id))">\##(text)</a>"##
91 } 104 }
92 let destination = normalizeOrgLinkTarget(rawDestination) 105 let destination = normalizeOrgLinkTarget(rawDestination)
93 if let label, label.hasPrefix("[["), label.hasSuffix("]]") { 106 // A description that is itself an image link (`[[url][file:badge.svg]]`, the common
94 let source = String(label.dropFirst(2).dropLast(2)) 107 // build-badge form, or the double-bracketed `[[url][[badge.svg]]]`) makes the image the
95 if let imageHTML = makeOrgImageHTML(source: source, alt: nil, imageURLResolver: imageURLResolver) { 108 // clickable content of the link. A plain-text description like `img.png` stays text.
96 let resolvedDestination = linkURLResolver?(destination) ?? destination 109 if let label, let source = descriptionImageSource(label),
97 guard let sanitizedURL = sanitizedReadmeLinkURLString(resolvedDestination) else { 110 let imageHTML = makeOrgImageHTML(source: source, alt: nil, imageURLResolver: imageURLResolver) {
98 return imageHTML 111 let resolvedDestination = linkURLResolver?(destination) ?? destination
99 } 112 guard let sanitizedURL = sanitizedReadmeLinkURLString(resolvedDestination) else {
100 return #"<a href="\#(sanitizedURL)">\#(imageHTML)</a>"# 113 return imageHTML
101 } 114 }
115 return #"<a href="\#(sanitizedURL)">\#(imageHTML)</a>"#
102 } 116 }
103 117
104 // A bare `[[image]]` with no description is an inline image. `[[image][text]]` is a 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 #expect(!html.contains("<img")) 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 @Test 344 @Test
329 func captionPromotesImageToFigure() { 345 func captionPromotesImageToFigure() {
330 let html = render(""" 346 let html = render("""