import Foundation /// Strip org's `file:` link prefix. `[[file:diagram.png]]` targets a local path the same /// way `[[diagram.png]]` does; the scheme is org bookkeeping, not part of the URL. func normalizeOrgLinkTarget(_ target: String) -> String { target.hasPrefix("file:") ? String(target.dropFirst(5)) : target } private func isRenderableImageSource(_ source: String) -> Bool { let lowercased = source.lowercased() return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".heic"] .contains(where: { lowercased.hasSuffix($0) }) } /// If a whole line is just an image link with no description (`[[file:x.png]]`), return its /// path (with `file:` stripped). A link carrying a description is not a standalone image. func standaloneOrgImage(in line: String) -> String? { let trimmed = line.trimmingCharacters(in: .whitespaces) guard trimmed.hasPrefix("[[") && trimmed.hasSuffix("]]") else { return nil } let inner = String(trimmed.dropFirst(2).dropLast(2)) guard !inner.contains("][") else { return nil } let path = normalizeOrgLinkTarget(inner) guard isRenderableImageSource(path) else { return nil } return path } func resolveRepositoryLinkURL( _ source: String, host: String, owner: String, repositoryName: String, ref: String, readmePath: String?, pathSegment: String = "blob" ) -> String? { let trimmedSource = source.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedSource.isEmpty else { return nil } if trimmedSource.hasPrefix("http://") || trimmedSource.hasPrefix("https://") || trimmedSource.hasPrefix("mailto:") || trimmedSource.hasPrefix("#") { return trimmedSource } return resolveRepositoryAssetURL( trimmedSource, host: host, owner: owner, repositoryName: repositoryName, ref: ref, readmePath: readmePath, pathSegment: pathSegment ) } func resolveRepositoryAssetURL( _ source: String, host: String, owner: String, repositoryName: String, ref: String, readmePath: String?, pathSegment: String = "blob" ) -> String? { let trimmedSource = source.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedSource.isEmpty else { return nil } if trimmedSource.hasPrefix("http://") || trimmedSource.hasPrefix("https://") || trimmedSource.hasPrefix("data:") { return trimmedSource } let relativePath: String if trimmedSource.hasPrefix("/") { relativePath = String(trimmedSource.dropFirst()) } else { let readmeDirectory = (readmePath as NSString?)?.deletingLastPathComponent ?? "" relativePath = normalizeRepositoryPath( (readmeDirectory as NSString).appendingPathComponent(trimmedSource) ) } guard !relativePath.isEmpty else { return nil } var components = URLComponents() components.scheme = "https" components.host = host let encodedOwner = owner.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? owner let encodedRepository = repositoryName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? repositoryName let encodedRef = ref.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? ref let encodedRelativePath = relativePath .split(separator: "/", omittingEmptySubsequences: false) .map { segment in String(segment).addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? String(segment) } .joined(separator: "/") let encodedSegment = pathSegment.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? pathSegment components.percentEncodedPath = "/\(encodedOwner)/\(encodedRepository)/\(encodedSegment)/\(encodedRef)/\(encodedRelativePath)" return components.string } private func normalizeRepositoryPath(_ path: String) -> String { var components: [String] = [] for part in path.split(separator: "/") { switch part { case ".": continue case "..": if !components.isEmpty { components.removeLast() } default: components.append(String(part)) } } return components.joined(separator: "/") }