Skip to content

Instantly share code, notes, and snippets.

@brentdax brentdax/HTML.swift Secret
Last active Jul 16, 2019

Embed
What would you like to do?
Some non-`String`, non-`AttributedString` examples of types which might use `ExpressibleByStringInterpolation`. These designs assume we use an interpolation design which allows `appendInterpolatedSegment(_:)` to be overloaded, but an unconstrained generic design would work only a little bit less gracefully.
import Foundation
/// Contains an HTML-escaped string.
///
/// An `HTML` instance works like a string, except that it is marked as containing HTML code.
/// When the contents of an ordinary `String` are inserted or interpolated into an `HTML`
/// instance, they are escaped before insertion. Use of `HTML` can therefore help prevent
/// cross-site scripting attacks.
///
/// Types can customize the way they're added to `HTML` instances by adopting the `HTMLConvertible`
/// protocol.
///
/// Example:
///
/// func makeRow(for account: Account, origin: String, accountInfo: HTML) throws -> HTML {
/// return try """
/// <tr id="account_\(account.id)" class="account">
/// <td class="account_name">
/// <img src="\(account.avatarURL)" class="avatar">
/// <a href="/account/\(account.id)?origin=\(url: origin, withAllowedCharacters: .urlQueryAllowed)">
/// \(account.name)
/// </a>
/// </td>
/// <td class="account_info">
/// \(accountInfo)
/// </td>
/// <td class="account_actions">
/// <button class="flag" data-ajax-target-account="\(js: account)">
/// Flag Account
/// </button>
/// </td>
/// </tr>
/// """
/// }
struct HTML {
var raw: String
init(raw: String = "") { self.raw = raw }
mutating func append(raw: String) { self.raw += raw }
mutating func append(_ html: HTML) { append(raw: html.raw) }
mutating func append(_ text: String) {
append(raw: text.applyingTransform(.toXMLHex, reverse: false))
}
}
extension HTML: ExpressibleByStringInterpolation {
init(stringInterpolation: StringInterpolation) {
self = stringInterpolation.instance
}
struct StringInterpolation: StringInterpolationProtocol {
var instance = HTML()
init(literalCapacity: Int, interpolationCount: Int) {
let estimatedSize = literalCapacity + interpolationCount * 8
instance.raw.reserveCapacity(estimatedSize)
}
mutating func appendLiteral(_ literal: String) {
instance.append(raw: literal)
}
// Allow interpolation of other HTML instances.
mutating func appendInterpolation(_ html: HTML) {
instance.append(html)
}
// Allow interpolation of auto-escaped strings (and other types converted
// to strings).
mutating func appendInterpolation<T>(_ text: T) {
instance.append(String(describing: text))
}
// Allow interpolation of unescaped strings (and other types converted
// to strings) with the raw: label.
mutating func appendInterpolation<T>(raw: T) {
instance.append(raw: String(describing: raw))
}
// Allow interpolation of URLs.
mutating func appendInterpolation(_ url: URL) {
instance.append(url.absoluteString)
}
// Allow interpolation of URL-escaped strings with the url: label.
mutating func appendInterpolation(url string: String,
withAllowedCharacters allowedCharacters: CharacterSet = .urlPathAllowed) {
instance.append(raw: string.addingPercentEncoding(withAllowedCharacters: allowedCharacters))
}
// Allow interpolation of structured data as JSON, optionally specifying
// the encoder to use (for its strategy).
mutating func appendInterpolation<T: Encodable>
(js value: T, using encoder: JSONEncoder = JSONEncoder()) throws {
let jsonData = try encoder.encode(value)
let json = String(decoding: jsonData, as: UTF8.self)
instance.append(json)
}
}
}
/// Represents a string whose exact phrasing is locale-dependent.
///
/// `LocalizableString` contains a `key` and a series of `arguments`. When the `LocalizableString` is
/// passed to the `String(localized:)` initializer, its `key` is used to find a format string in a
/// `Bundle`'s localized string tables, and the format string is passed with the `arguments` to
/// `String(format:arguments:)` to create the final value.
///
/// `LocalizableString` can be instantiated directly to use it with any key and set of arguments, or
/// it can be instantiated with a string literal, with or without interpolations, to automatically
/// calculate a format string and use it as the key.
///
/// Example:
///
/// let alert = NSAlert()
/// alert.messageText = String(localized: "\(appName) could not add “\(name)” because a person with that name already exists.")
/// alert.addButton(withTitle: String(localized: "Cancel"))
/// alert.addButton(withTitle: String(localized: "Replace"))
public struct LocalizableString {
public var key: String
public var arguments: [CVarArg]
public init(key: String, arguments: [CVarArg]) {
self.key = key
self.arguments = arguments
}
public func format(inTable table: String? = nil, from bundle: Bundle = .main) -> String {
return bundle.localizedString(forKey: key, value: key, table: table)
}
}
extension String {
/// Converts a localizable string into a concrete, fully localized `String`.
public init(localized str: LocalizableString, inTable table: String? = nil, from bundle: Bundle = .main) -> String {
self.init(format: str.format(inTable: table, from: bundle), arguments: str.arguments)
}
}
public protocol LocalizableStringInterpolatable {
var localizableKeyFormat: String { get }
var localizableValue: CVarArg { get }
}
extension Int: LocalizableStringInterpolatable {
public var localizableKeyFormat: String { return "%lld" }
public var localizableValue: CVarArg { return Int64(self) }
}
// Other conformances omitted
extension LocalizableString: ExpressibleByStringInterpolation {
public init(stringInterpolation: StringInterpolation) {
self.init(key: stringInterpolation.key, arguments: stringInterpolation.arguments)
}
public struct StringInterpolation: StringInterpolationProtocol {
var key: String = ""
var arguments: [CVarArg] = []
init(literalCapacity: Int, interpolationCount: Int) {
let assumedSizeOfFormatSpecifier = 2
key.reserveCapacity(literalCapacity + interpolationCount * assumedSizeOfFormatSpecifier)
arguments.reserveCapacity(interpolationCount)
}
public mutating func appendLiteral(_ literal: String) {
// Escape any % characters in the literal.
key.append(contentsOf: literal.lazy.flatMap { $0 == "%" ? "%%" : String($0) })
}
public mutating func appendInterpolation(_ value: CVarArg, format: String) {
key += format
arguments.append(value)
}
public mutating func appendInterpolation<T: LocalizableStringInterpolatable>(_ value: T) {
appendInterpolation(value.localizableValue, format: value.localizableKeyFormat)
}
}
}
import sqlite3
/// Represents a statement and its parameters, ready to be executed with a database.
///
/// A `SQLStatement` can be created by either passing a SQL query string and an array of parameters,
/// or by using a string literal and interpolating any values you want to include as parameters.
/// Once you've created a statement, you can execute it with `SQLiteDatabase.execute(_:)`.
///
/// Example:
///
/// let resultSet = try db.execute("""
/// SELECT id, title, slug FROM posts WHERE user_id = \(user.id)
/// ORDER BY title \(raw: reversed ? "DESC" : "ASC")
/// """)
/// for row in resultSet {
/// print(row[0, as: Int.self], row[2, as: String.self], row[1, as: String.self])
/// }
public struct SQLiteStatement {
public var sql: String
public var parameters: [Parameter]
public init(sql: String, parameters: [Parameter]) {
self.sql = sql
self.parameters = parameters
}
public enum Parameter {
case blob(Data)
case int64(Int64)
case double(Double)
case text(String)
case null
private func bind(to resultSet: SQLiteResultSet, at i: Int) throws {
switch self {
case .blob(let data):
try data.withUnsafeBytes { buffer in
try resultSet.bindBlob(buffer, count: data.count, to: i, destructor: .transient)
}
// ...other cases omitted...
}
}
}
}
extension SQLiteDatabase {
public func execute(_ statement: SQLiteStatement) throws -> SQLiteResultSet {
var preparedStatement: UnsafeRawPointer?
try statement.sql.withCString { ptr in
try SQLiteError.check(sqlite3_prepare_v3(db, ptr, strlen(ptr), 0, &preparedStatement, nil))
}
let resultSet = SQLiteResultSet(preparedStatement: preparedStatement!)
for (i, parameter) in zip(1..., statement.parameters) {
try parameter.bind(to: resultSet, at: i)
}
return resultSet
}
}
public protocol SQLiteStatementConvertible {
var sqliteStatement: SQLiteStatement { get }
}
extension Data: SQLiteStatementConvertible {
public var sqliteStatement: SQLiteStatement {
return SQLStatement(sql: "?", parameters: [.blob(self)])
}
}
// Other conformances omitted
extension SQLiteStatement: SQLiteStatementConvertible {
public var sqliteStatement: SQLiteStatement {
return self
}
}
extension SQLiteStatement: ExpressibleByStringInterpolation {
public init(stringInterpolation: StringInterpolation) throws -> Void) rethrows {
self = stringInterpolation.statement
}
public struct StringInterpolation: StringInterpolationProtocol {
var statement: SQLiteStatement
init(literalCapacity: Int, interpolationCount: Int) {
statement.sql.reserveCapacity(literalCapacity + interpolationCount)
statement.parameters.reserveCapacity(interpolationCount)
}
public mutating func appendLiteral(_ literal: String) {
statement.sql += literal
}
public mutating func appendInterpolation<T: SQLiteStatementConvertible>(_ value: T?) {
statement.sql += value?.statement.sql ?? "?"
statement.parameters += value?.statement.parameters ?? [.null]
}
public mutating func appendInterpolation(raw sql: SQLiteStatement) {
appendInterpolation(sql)
}
}
}
@AliSoftware

This comment has been minimized.

Copy link

commented Sep 28, 2018

On lines 97 & 98 of your SQLiteStatement.swift example, value conforms to SQLiteStatementConvertible, not a SQLiteStatement.StringInterpolation; so the property you're accessing should be sqliteStatement, not statement there, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.