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

html library org-mode swift

Sources/OrgSwift/Escaping.swift

82 lines · 2738 bytes

 1import Foundation
 2
 3// MARK: - HTML Escaping
 4
 5func escapeHTML(_ text: String) -> String {
 6    text.replacingOccurrences(of: "&", with: "&")
 7        .replacingOccurrences(of: "<", with: "&lt;")
 8        .replacingOccurrences(of: ">", with: "&gt;")
 9        .replacingOccurrences(of: "\"", with: "&quot;")
10}
11
12func escapeHTMLAttribute(_ text: String) -> String {
13    escapeHTML(text).replacingOccurrences(of: "'", with: "&#39;")
14}
15
16// MARK: - URL Sanitization
17
18func sanitizedReadmeLinkURLString(_ rawURL: String) -> String? {
19    sanitizeReadmeURLString(
20        rawURL,
21        allowedSchemes: ["http", "https", "mailto"],
22        allowsFragmentOnly: true
23    )
24}
25
26func sanitizedReadmeImageURLString(_ rawURL: String) -> String? {
27    sanitizeReadmeURLString(
28        rawURL,
29        allowedSchemes: ["http", "https"],
30        allowsFragmentOnly: false
31    )
32}
33
34private func sanitizeReadmeURLString(
35    _ rawURL: String,
36    allowedSchemes: Set<String>,
37    allowsFragmentOnly: Bool
38) -> String? {
39    let trimmedURL = rawURL.trimmingCharacters(in: .whitespacesAndNewlines)
40    guard !trimmedURL.isEmpty else { return nil }
41
42    if allowsFragmentOnly, trimmedURL.hasPrefix("#"), trimmedURL.count > 1 {
43        return escapeHTMLAttribute(trimmedURL)
44    }
45
46    if let scheme = URLComponents(string: trimmedURL)?.scheme?.lowercased() {
47        guard allowedSchemes.contains(scheme),
48              let sanitizedURL = URLComponents(string: trimmedURL)?.url?.absoluteString else {
49            return nil
50        }
51        return escapeHTMLAttribute(sanitizedURL)
52    }
53
54    // No scheme  a relative reference (`diagram.png`, `docs/x`). It cannot carry a
55    // `javascript:`/`data:` payload, so it is safe to keep as-is; apps that supply a
56    // resolver turn these into absolute URLs before they reach here. Reject only the
57    // protocol-relative `//host` form, which points off-origin.
58    guard !trimmedURL.hasPrefix("//") else { return nil }
59    return escapeHTMLAttribute(trimmedURL)
60}
61
62// MARK: - Regex Helpers
63
64func orgKeywordDirective(in line: String) -> (keyword: String, value: String)? {
65    guard let match = line.firstMatch(of: /^#\+([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/) else {
66        return nil
67    }
68    return (
69        keyword: String(match.1).lowercased(),
70        value: String(match.2).trimmingCharacters(in: .whitespaces)
71    )
72}
73
74func isOrgHorizontalRule(_ line: String) -> Bool {
75    matchesRegex(line, pattern: #"^\s*-{5,}\s*$"#)
76}
77
78func matchesRegex(_ text: String, pattern: String) -> Bool {
79    guard let regex = try? NSRegularExpression(pattern: pattern) else { return false }
80    let range = NSRange(location: 0, length: (text as NSString).length)
81    return regex.firstMatch(in: text, range: range) != nil
82}