From 705639b2f53a8609b0883caa59e83f3ce3d8fda8 Mon Sep 17 00:00:00 2001 From: Daniel Arantes Loverde Date: Sat, 29 Aug 2026 19:22:12 -0300 Subject: [PATCH 01/14] [docs] Scaffold reference guides + README Documentation section - Documentation/{Extensions,SwiftUI,UIKit}.md shells with category headings + TOC - README: Documentation table linking all 4 guides - footer: drop the second github badge, keep the resume link --- Documentation/Extensions.md | 52 +++++++++++++++++++++++++++++++++++++ Documentation/SwiftUI.md | 21 +++++++++++++++ Documentation/UIKit.md | 40 ++++++++++++++++++++++++++++ README.md | 13 ++++++++-- 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 Documentation/Extensions.md create mode 100644 Documentation/SwiftUI.md create mode 100644 Documentation/UIKit.md diff --git a/Documentation/Extensions.md b/Documentation/Extensions.md new file mode 100644 index 0000000..38b23c5 --- /dev/null +++ b/Documentation/Extensions.md @@ -0,0 +1,52 @@ +# LCEssentials — Extensions + +Foundation, value-type, string, collection, numeric, date, and crypto helpers, plus +the `LCEssentials` namespace itself. UIKit extensions live in +[UIKit.md](UIKit.md); SwiftUI helpers in [SwiftUI.md](SwiftUI.md). + +Every section is a collapsible block — click a heading to expand it. + +## Contents + +- [API & Networking](#api--networking) +- [Strings & Text](#strings--text) +- [Collections & Sequences](#collections--sequences) +- [Numbers & Geometry](#numbers--geometry) +- [Date, Data & Files](#date-data--files) +- [Encoding & Errors](#encoding--errors) +- [Crypto](#crypto) +- [Core — the `LCEssentials` namespace](#core--the-lcessentials-namespace) + +--- + +## API & Networking + + + +## Strings & Text + + + +## Collections & Sequences + + + +## Numbers & Geometry + + + +## Date, Data & Files + + + +## Encoding & Errors + + + +## Crypto + + + +## Core — the `LCEssentials` namespace + + diff --git a/Documentation/SwiftUI.md b/Documentation/SwiftUI.md new file mode 100644 index 0000000..996fe6b --- /dev/null +++ b/Documentation/SwiftUI.md @@ -0,0 +1,21 @@ +# LCEssentials — SwiftUI + +SwiftUI components and `View` helpers. Foundation/value-type helpers are in +[Extensions.md](Extensions.md); UIKit-era helpers in [UIKit.md](UIKit.md). + +Every section is a collapsible block — click a heading to expand it. + +## Contents + +- [Navigation](#navigation) +- [View helpers](#view-helpers) + +--- + +## Navigation + + + +## View helpers + + diff --git a/Documentation/UIKit.md b/Documentation/UIKit.md new file mode 100644 index 0000000..2f8e1e6 --- /dev/null +++ b/Documentation/UIKit.md @@ -0,0 +1,40 @@ +# LCEssentials — UIKit + +UIKit-era helpers: programmatic layout & constraints, view/control extensions, +navigation, table/collection helpers, and drop-in components (image picker, image +zoom, snackbar, GIF loading). + +Foundation/value-type helpers are in [Extensions.md](Extensions.md); SwiftUI +helpers in [SwiftUI.md](SwiftUI.md). + +Every section is a collapsible block — click a heading to expand it. + +## Contents + +- [Layout & Constraints](#layout--constraints) +- [Views & Controls](#views--controls) +- [Navigation & Controllers](#navigation--controllers) +- [Collections & Tables](#collections--tables) +- [Media & Components](#media--components) + +--- + +## Layout & Constraints + + + +## Views & Controls + + + +## Navigation & Controllers + + + +## Collections & Tables + + + +## Media & Components + + diff --git a/README.md b/README.md index f252ffe..ae18fd9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,17 @@ This is a repository of essential scripts written in Swift for Loverde Co. used ## Features - [x] Many usefull scripts extensions -- [x] `API` — typed async networking + multipart uploads. See **[Documentation/API.md](Documentation/API.md)** +- [x] `API` — typed async networking + multipart uploads +- [x] SwiftUI components (`LCENavigationView`) and UIKit-era helpers + +## Documentation + +| Guide | Covers | +| --- | --- | +| **[API.md](Documentation/API.md)** | `API` networking — requests, multipart uploads, client certificates, error handling, and why it beats a hand-rolled `URLSession` | +| **[Extensions.md](Documentation/Extensions.md)** | Foundation / value-type / string / collection / numeric / date / crypto extensions and the `LCEssentials` namespace | +| **[SwiftUI.md](Documentation/SwiftUI.md)** | SwiftUI components and `View` helpers | +| **[UIKit.md](Documentation/UIKit.md)** | Programmatic layout & constraints, view/control extensions, navigation, tables, and drop-in components | Installation @@ -80,6 +90,5 @@ Any question or doubts, please send thru email Daniel Arantes Loverde - [![Alt text](https://loverde.com.br/_signature/loverde_github_mail.gif "My Resume")](https://github.com/loverde-co/resume/) -[![Alt text](https://loverde.com.br/_signature/loverde_github_mail.gif "Loverde Co. Github")](https://github.com/loverde-co) Autor: Daniel Arantes Loverde From 9f1db7c121aed1fb1a0ae47583476ee9fcba4f79 Mon Sep 17 00:00:00 2001 From: Daniel Arantes Loverde Date: Sat, 29 Aug 2026 19:23:23 -0300 Subject: [PATCH 02/14] [docs] Extensions.md: API overview + Crypto section --- Documentation/Extensions.md | 197 +++++++++++++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 2 deletions(-) diff --git a/Documentation/Extensions.md b/Documentation/Extensions.md index 38b23c5..ff75537 100644 --- a/Documentation/Extensions.md +++ b/Documentation/Extensions.md @@ -21,7 +21,101 @@ Every section is a collapsible block — click a heading to expand it. ## API & Networking - +`API` is an `actor` wrapping `URLSession` for typed JSON requests and multipart +uploads. This is a summary — the **[full guide is in API.md](API.md)** (all +parameters, error model, client certificates, custom body types, and the +rationale vs. a hand-rolled `URLSession`). + +
+API — typed async requests & uploads + +### `API.shared` + +The shared `actor` instance. Every call is `await`; nothing runs on the main +thread unless you hop there yourself. + +```swift +struct User: Decodable, Sendable { let id: Int; let name: String } + +let user: User = try await API.shared.request( + url: "https://api.example.com/users/{id}", + method: .get, + pathParams: ["id": "42"] +) +``` + +### `request(url:method:body:pathParams:headers:debug:timeoutInterval:networkServiceType:persistConnection:)` + +Sends a request and decodes the JSON response into `T: Decodable & Sendable`. +Non-2xx responses throw an `NSError` whose `code` is the HTTP status and whose +`localizedFailureReason` is the response body. When `T == String` the raw body +is returned without JSON decoding. + +```swift +struct CreateUser: Encodable, Sendable { let name: String } + +// JSON body +let created: User = try await API.shared.request( + url: "https://api.example.com/users", + method: .post, + body: jsonBody(CreateUser(name: "Ana")) +) + +// form-url-encoded body +let token: Token = try await API.shared.request( + url: "https://api.example.com/oauth/token", + method: .post, + body: .form(["grant_type": "password", "username": "ana"]) +) +``` + +### `upload(url:method:form:pathParams:headers:debug:timeoutInterval:networkServiceType:)` and its `onProgress:` overload + +Uploads a `multipart/form-data` body serialised to a temp file (removed +afterwards) and streamed from disk, so large files never fully load into memory. + +```swift +var form = MultipartForm() +form.field("caption", "Sunset") +form.file("photo", data: jpegData, filename: "p.jpg") +form.file("video", url: localVideoURL) // streamed from disk + +let result: UploadResult = try await API.shared.upload( + url: "https://api.example.com/media", + form: form, + onProgress: { fraction in + Task { @MainActor in progressView.progress = Float(fraction) } + } +) +``` + +### Body types — `HTTPBody` + +`jsonBody(_:)` wraps any `Encodable & Sendable`; `FormURLEncodedBody` (a.k.a. +`.form([:])`) percent-escapes every value and never drops one; `RawBody` lets you +supply the bytes and `Content-Type` yourself. Conform your own type to `HTTPBody` +and `API` accepts it with no change. + +```swift +struct CSVBody: HTTPBody { + let rows: [[String]] + func encoded() throws -> (data: Data, contentType: String) { + let text = rows.map { $0.joined(separator: ",") }.joined(separator: "\n") + return (Data(text.utf8), "text/csv; charset=UTF-8") + } +} +``` + +### `await API.shared.setupCertification(certData:password:)` + +Registers a client certificate (`.p12`) for mutual-TLS on subsequent requests. + +```swift +let p12 = try Data(contentsOf: certURL) +await API.shared.setupCertification(certData: p12, password: "cert-pw") +``` + +
## Strings & Text @@ -45,7 +139,106 @@ Every section is a collapsible block — click a heading to expand it. ## Crypto - +
+RIPEMD_160 — RIPEMD-160 digest + +### `static func hash(_ message: Data) -> Data` + +Returns the 20-byte RIPEMD-160 digest of `message`. Pure Swift, no system +dependency. Mainly useful for Bitcoin-style address hashing (`RIPEMD160(SHA256(x))`). + +```swift +let digest = RIPEMD_160.hash(Data("abc".utf8)) +digest.count // 20 +digest.map { String(format: "%02x", $0) }.joined() +// "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc" +``` + +
+ +
+LCECryptoKitManager — OTP / peppered-login bridge (needs the LCECryptoKit binary) + +A thin facade over the optional `LCECryptoKit` binary product (enabled by the +`LCE_ENABLE_CRYPTO_BINARY` build flag). **When the binary is not linked every +method is a no-op** returning `nil` / `""` / `false`, so calling code still +compiles and runs. + +### `init()` / `init(privateKey:)` + +Create a manager. The `privateKey` (a.k.a. "hash key") is only needed by the +`*WithKey` methods. + +```swift +let crypto = LCECryptoKitManager() +let keyed = LCECryptoKitManager(privateKey: serverHashKey) +``` + +### `static func generateKey() -> String` + +Generates a random AES key string. + +```swift +let key = LCECryptoKitManager.generateKey() +``` + +### `func encodeTP(email:password:) -> String?` / `func decodeOTP(_:) -> String?` + +Encode an email+password pair into an OTP seed hash, and decode it back. + +```swift +let hash = crypto.encodeTP(email: "ana@x.com", password: "s3cr3t") +let back = crypto.decodeOTP(hash ?? "") +``` + +### `func encodeOTPWithKey(email:password:) -> String?` / `func decodeOTPWithKey(_:) -> Bool` + +Same as above but bound to the instance's `privateKey`; `decodeOTPWithKey` +returns whether the hash validates against that key rather than the decoded value. + +```swift +let keyed = LCECryptoKitManager(privateKey: serverHashKey) +let hash = keyed.encodeOTPWithKey(email: "ana@x.com", password: "s3cr3t") +let ok = keyed.decodeOTPWithKey(hash ?? "") // Bool +``` + +### `static func generateSalt() -> String` + +Random salt for the salted/iterated/peppered login flow. + +```swift +let salt = LCECryptoKitManager.generateSalt() +``` + +### `static func computeClientHash(email:password:salt:) -> String` + +Client-side hash of the credentials with the given salt — sent to the server +instead of the raw password. + +```swift +let clientHash = LCECryptoKitManager.computeClientHash( + email: "ana@x.com", password: "s3cr3t", salt: salt +) +``` + +### `static func computeLoginBearerToken(userId:clientHash:) -> String?` + +Derives the login bearer token from a user id and the client hash. + +```swift +let token = LCECryptoKitManager.computeLoginBearerToken(userId: "42", clientHash: clientHash) +``` + +### `static func otpEncode(_:) -> String?` / `static func otpDecode(_:) -> String?` + +One-time-pad encode/decode of an arbitrary string. + +```swift +let enc = LCECryptoKitManager.otpEncode("secret-value") +let dec = LCECryptoKitManager.otpDecode(enc ?? "") // "secret-value" +``` + +
## Core — the `LCEssentials` namespace From 1d2c595c78b80ea93b34514eccf0d8bd47db99a7 Mon Sep 17 00:00:00 2001 From: Daniel Arantes Loverde Date: Sat, 29 Aug 2026 19:25:15 -0300 Subject: [PATCH 03/14] [docs] Extensions.md: Strings & Text section --- Documentation/Extensions.md | 555 +++++++++++++++++++++++++++++++++++- 1 file changed, 554 insertions(+), 1 deletion(-) diff --git a/Documentation/Extensions.md b/Documentation/Extensions.md index ff75537..ad19611 100644 --- a/Documentation/Extensions.md +++ b/Documentation/Extensions.md @@ -119,7 +119,560 @@ await API.shared.setupCertification(certData: p12, password: "cert-pw") ## Strings & Text - +
+String — validation, parsing, formatting, masks, HTML, dates + +### URLs + +#### `var isValidUrl: Bool` / `var isValidHttpsUrl: Bool` / `var isValidHttpUrl: Bool` +`isValidUrl` is true when `URL(string:)` succeeds; the other two additionally +require the `https` / `http` scheme. + +```swift +"https://google.com".isValidUrl // true +"https://google.com".isValidHttpsUrl // true +"http://google.com".isValidHttpsUrl // false +``` + +#### `var urlEncoded: String` / `var urlDecoded: String` +Percent-encode (host-allowed set) / decode. `urlDecoded` returns the original +string when it is not encoded. + +```swift +"it's easy".urlEncoded // "it's%20easy" +"it's%20easy".urlDecoded // "it's easy" +``` + +#### `mutating func urlEncode() -> String` / `mutating func urlDecode() -> String` +In-place variants; also return the new value (`@discardableResult`). + +```swift +var s = "a b"; s.urlEncode() // s == "a%20b" +``` + +#### `func stringByAddingPercentEncodingForRFC3986() -> String` +Percent-encodes for use as a query key/value, escaping `;/?:@&=+$,` and space. + +```swift +"a&b=c".stringByAddingPercentEncodingForRFC3986() // "a%26b%3Dc" +``` + +#### `var url: String?` +First URL detected **inside** the string (via `NSDataDetector`), or `nil`. + +```swift +"visit www.site.com.br now".url // "www.site.com.br" +``` + +#### `var toURL: NSURL?` +`NSURL(string:)` wrapper. + +```swift +"https://x.com".toURL // NSURL +``` + +### Validation + +#### `var isEmail: Bool` +Regex check for a syntactically valid email (TLD 2–20 chars). + +```swift +"user@example.com".isEmail // true +"nope".isEmail // false +``` + +#### `var isCPF: Bool` +Validates a Brazilian CPF including both check digits. Strips formatting first +(`onlyNumbers`), requires 11 digits. + +```swift +"123.456.789-09".isCPF // true when the check digits match +``` + +#### `var isValidCNPJ: Bool` +Validates a Brazilian CNPJ (14 digits, both check digits, rejects all-same-digit). + +```swift +"11.222.333/0001-81".isValidCNPJ // true when valid +``` + +#### `var isAlphabetic: Bool` +Letters only, no digits. + +```swift +"abc".isAlphabetic // true +"123abc".isAlphabetic // false +``` + +#### `var isAlphaNumeric: Bool` +Contains at least one letter **and** one digit and nothing else — handy for +password rules. + +```swift +"123abc".isAlphaNumeric // true +"abc".isAlphaNumeric // false +``` + +#### `var isHTML: Bool` +True if the string contains an HTML tag. + +```swift +"hi".isHTML // true +``` + +#### `func validateBolean(comparingBoolean: Bool = true) -> Bool` +Loose truthy/falsy check against a large set of EN/PT words. With +`comparingBoolean: true` returns whether the string means "true" +(`YES`, `ON`, `SIM`, `ATIVO`, `1`, `T`, …); with `false`, whether it means "false". + +```swift +"SIM".validateBolean() // true +"nao".validateBolean(comparingBoolean: false) // true +``` + +### Conversion + +#### `var bool: Bool?` +`"true"/"yes"/"1"` → `true`, `"false"/"no"/"0"` → `false`, else `nil` (trimmed, case-insensitive). + +```swift +" YES ".bool // true +"maybe".bool // nil +``` + +#### `var int: Int?` / `var float: Float?` / `var double: Double?` +Plain `Int(self)` / `Float(self)` / `Double(self)`. + +```swift +"101".int // 101 +"1.5".double // 1.5 +"x".int // nil +``` + +#### `func float(locale: Locale = .current) -> Float?` / `func double(locale:) -> Double?` +Locale-aware parsing via `NumberFormatter` (accepts grouping separators). + +```swift +"1,5".double(locale: Locale(identifier: "pt_BR")) // 1.5 +``` + +#### `var currencyStringToDouble: Double` +Parses a `pt_BR` currency string to `Double`, `0.0` on failure. + +```swift +"R$ 1.234,56".currencyStringToDouble // 1234.56 +``` + +#### `var btcToSats: Int` / `var bitcoinToSatoshis: Int` +Multiplies a BTC amount string by 100,000,000. `bitcoinToSatoshis` is an alias. + +```swift +"0.0001".btcToSats // 10000 +``` + +#### `var data: Data` +UTF-8 bytes. + +```swift +"hi".data // 2 bytes +``` + +#### `var nsString: NSString` / `var fullNSRange: NSRange` +Bridge to `NSString`; `NSRange` spanning the whole string (UTF-16 aware). + +```swift +"café".fullNSRange // {0, 4} +``` + +#### `func nsRange(from range: Range) -> NSRange?` +Convert a Swift `Range` to an `NSRange` in the UTF-16 view. + +#### `var base64Encode: String?` / `var base64Decode: String?` +Base64 encode the UTF-8 bytes / decode a Base64 string back to text. + +```swift +"hi".base64Encode // "aGk=" +"aGk=".base64Decode // "hi" +``` + +#### `func date(withCurrFormatt:localeIdentifier:timeZone:) -> Date?` +Parse the string to `Date` using the given input format (default +`"yyyy-MM-dd HH:mm:ss"`, locale `pt-BR`, current time zone). A `" 0000"` suffix +is normalised to `" +0000"`. + +```swift +"2026-08-29 14:30:00".date() // Date +``` + +#### `func date(withCurrFormatt:newFormatt:localeIdentifier:timeZone:) -> Date?` +Parse with one format and round-trip through another (normalises the value). + +#### `var currentTimeZone: String` +Current time-zone offset string, e.g. `"-0300"`. + +### Cleaning & filtering + +#### `var withoutSpacesAndNewLines: String` +Removes every space and `\n`. + +```swift +" a \n b ".withoutSpacesAndNewLines // "ab" +``` + +#### `var onlyNumbers: String` / `var numbers: String` +Digits only. `onlyNumbers` uses a `\D` regex; `numbers` uses `decimalDigits`. + +```swift +"(11) 98765-4321".onlyNumbers // "11987654321" +``` + +#### `var letters: String` / `var lettersWithWhiteSpace: String` +Keep only letters (optionally keeping spaces). + +```swift +"a1 b2".letters // "ab" +"a1 b2".lettersWithWhiteSpace // "a b" +``` + +#### `var alphanumeric: String` / `var alphanumericWithWhiteSpace: String` +Keep only alphanumerics (optionally keeping spaces). + +```swift +"a-b_c 1".alphanumeric // "abc1" +``` + +#### `var removeSpecialChars: String` +Keeps `[A-Za-z0-9 -]` only. + +```swift +"a@b#c".removeSpecialChars // "abc" +``` + +#### `var removeHTMLTags: String` / `var removeEmoji: String` +Strip HTML tags / strip emoji (`CharacterSet.symbols`). + +```swift +"

hi

".removeHTMLTags // "hi" +"hi 😀".removeEmoji // "hi " +``` + +### Slicing & padding + +#### `var first: String` / `var last: String` +First / last character **as a String** (`""` when empty). + +#### `var uppercaseFirst: String` +Capitalises the first character only. + +```swift +"hello".uppercaseFirst // "Hello" +``` + +#### `var firstCharacterAsString: String?` / `var lastCharacterAsString: String?` +Optional variants — `nil` when empty. + +#### `func paddingStart(_ length: Int, with: String = " ") -> String` / `func paddingEnd(...)` +Pad to `length` with a repeating pad string at the start / end. No-op if already long enough. + +```swift +"hue".paddingStart(10) // " hue" +"hue".paddingEnd(10, with: "br") // "huebrbrbrb" +``` + +#### `func truncated(toLength: Int, trailing: String? = "...") -> String` +Non-mutating truncation. + +```swift +"This is long".truncated(toLength: 7) // "This is..." +``` + +#### `mutating func truncate(toLength: Int, trailing: String? = "...") -> String` +In-place truncation (`@discardableResult`). + +#### `mutating func trim() -> String` +Trim leading/trailing whitespace and newlines, in place (`@discardableResult`). + +```swift +var s = " hi \n"; s.trim() // s == "hi" +``` + +#### `mutating func reverse() -> String` +Reverse in place (`@discardableResult`). + +#### `mutating func insertAtIndexEnd(string:ind:)` / `insertAtIndexStart(string:ind:)` +Insert `string` at an offset measured from `endIndex` (negative `ind` moves left). + +```swift +var s = "abcd"; s.insertAtIndexEnd(string: "-", ind: -1) // "abc-d" +``` + +### Replacing + +#### `func replace(from:to:)` / `func findAndReplace(from:to:)` +Simple substring replacement (`findAndReplace` is generic over `StringProtocol`). + +```swift +"a.b.c".replace(from: ".", to: "-") // "a-b-c" +``` + +#### `func replacing(range: CountableClosedRange, with: String) -> String` +Replace by integer character range. + +```swift +"abcdef".replacing(range: 1...3, with: "X") // "aXef" +``` + +#### `func replacingLastOccurrenceOfString(_:with:caseInsensitive: Bool = true) -> String` +Replace only the last match. + +```swift +"a-b-c".replacingLastOccurrenceOfString("-", with: "+") // "a-b+c" +``` + +#### `func replaceAll(of pattern: String, with: String, options: = []) -> String` +Regex replace-all; returns the original on a bad pattern. + +```swift +"a1b2c3".replaceAll(of: "[0-9]", with: "#") // "a#b#c#" +``` + +#### `@discardableResult func replaceURL(_ withDict: [String: Any]) -> String` +Substitute `{key}` placeholders — used by `API.request(pathParams:)`. + +```swift +"/users/{id}/posts/{p}".replaceURL(["id": 7, "p": "x"]) // "/users/7/posts/x" +``` + +### Words & search + +#### `func words() -> [String]` / `func wordCount() -> Int` +Split on whitespace + punctuation, dropping empties. + +```swift +"Swift is amazing".words() // ["Swift", "is", "amazing"] +"Swift is amazing".wordCount() // 3 +``` + +#### `func contains(_:caseSensitive: Bool = true) -> Bool` +Substring check with optional case-insensitivity. + +```swift +"Hello".contains("ell") // true +"Hello".contains("HELLO", caseSensitive: false) // true +``` + +### Formatting helpers + +#### `func applyMask(toText: String, mask: String) -> String` +Apply a `#`-placeholder mask; literal characters in the mask are inserted. + +```swift +"11987654321".applyMask(toText: "11987654321", mask: "(##) #####-####") +// "(11) 98765-4321" +``` + +#### `func exponentize(str: String) -> String` +Turn `^`-prefixed digits into Unicode superscripts. + +```swift +"x^2 + y^3".exponentize(str: "x^2 + y^3") // "x² + y³" +``` + +#### `func stringFromTimeInterval(_ interval: TimeInterval) -> NSString` +Format a `TimeInterval` as `HH:MM:SS.mmm`. + +```swift +"".stringFromTimeInterval(3661.5) // "01:01:01.500" +``` + +#### `func toSlug() -> String` +Lowercase, de-accent, spaces → `-`, strip other punctuation. + +```swift +"Olá Mundo!".toSlug() // "ola-mundo" +``` + +#### `func localized(comment: String = "") -> String` +`NSLocalizedString(self, comment:)`. + +```swift +"welcome_title".localized() +``` + +### Generators & misc + +#### `static func loremIpsum(ofLength length: Int = 445) -> String` +Lorem-ipsum text truncated to `length` (max 445). + +```swift +String.loremIpsum(ofLength: 20) // "Lorem ipsum dolor si" +``` + +#### `func randomString(length: Int) -> String` +Random `[A-Za-z0-9]` string. (Instance method — the receiver is ignored.) + +```swift +"".randomString(length: 8) // e.g. "a9Fk2Lp0" +``` + +#### `var JSONStringToDictionary: [String: Any]?` +Parse a JSON object string to a dictionary (`nil` + logs on failure). + +```swift +#"{"a":1}"#.JSONStringToDictionary // ["a": 1] +``` + +#### `var convertToHTML: NSAttributedString?` +Render an HTML string to `NSAttributedString` (UIKit path uses the CSS converter below). + +#### `func convertHtmlToAttributedStringWithCSS(font:csscolor:lineheight:csstextalign:customCSS:) -> NSAttributedString?` — *UIKit only* +HTML → `NSAttributedString` with an injected `