Skip to content

Commit 2a355ee

Browse files
committed
#104 Added escapeString options in XMLParsingOptions of StyleXML. Applied by default.
1 parent b432258 commit 2a355ee

File tree

3 files changed

+58
-2
lines changed

3 files changed

+58
-2
lines changed

Sources/SwiftRichString/Style/StyleGroup.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ public class StyleXML: StyleProtocol {
5252
public var baseStyle: StyleProtocol?
5353

5454
/// XML Parsing options.
55-
public var xmlParsingOptions: XMLParsingOptions = []
55+
/// By default `escapeString` is applied.
56+
public var xmlParsingOptions: XMLParsingOptions = [.escapeString]
5657

5758
/// Image provider is called to provide custom image when `StyleXML` encounter a `img` tag image.
5859
/// If not implemented the image is automatically searched inside any bundled `xcassets`.

Sources/SwiftRichString/Support/Extensions.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,54 @@ import AppKit
3535
import UIKit
3636
#endif
3737

38+
extension String {
39+
40+
// Current implementation by @alexaubry in
41+
// https://github.com/alexaubry/HTMLString
42+
public func escapeWithUnicodeEntities() -> String {
43+
var copy = self
44+
copy.addUnicodeEntities()
45+
return copy
46+
}
47+
48+
internal mutating func addUnicodeEntities() {
49+
var position: String.Index? = startIndex
50+
let requiredEscapes: Set<Character> = ["!", "\"", "$", "%", "&", "'", "+", ",", "<", "=", ">", "@", "[", "]", "`", "{", "}"]
51+
52+
while let cursorPosition = position {
53+
guard cursorPosition != endIndex else { break }
54+
let character = self[cursorPosition]
55+
56+
if requiredEscapes.contains(character) {
57+
// One of the required escapes for security reasons
58+
let escape = "&#\(character.asciiValue!);" // required escapes can can only be ASCII
59+
position = positionAfterReplacingCharacter(at: cursorPosition, with: escape)
60+
} else {
61+
// Not a required escape, no need to replace the character
62+
position = index(cursorPosition, offsetBy: 1, limitedBy: endIndex)
63+
}
64+
}
65+
}
66+
67+
/// Replaces the character at the given position with the escape and returns the new position.
68+
fileprivate mutating func positionAfterReplacingCharacter(at position: String.Index, with escape: String) -> String.Index? {
69+
let nextIndex = index(position, offsetBy: 1)
70+
71+
if let fittingPosition = index(position, offsetBy: escape.count, limitedBy: endIndex) {
72+
// Check if we can fit the whole escape in the receiver
73+
replaceSubrange(position ..< nextIndex, with: escape)
74+
return fittingPosition
75+
} else {
76+
// If we can't, remove the character and insert the escape to make it fit.
77+
remove(at: position)
78+
insert(contentsOf: escape, at: position)
79+
return index(position, offsetBy: escape.count, limitedBy: endIndex)
80+
}
81+
}
82+
83+
84+
}
85+
3886
extension NSNumber {
3987

4088
internal static func from(float: Float?) -> NSNumber? {

Sources/SwiftRichString/Support/XMLStringBuilder.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ public struct XMLParsingOptions: OptionSet {
5252
/// recommended that you include a root node yourself and pass this option.
5353
public static let doNotWrapXML = XMLParsingOptions(rawValue: 1)
5454

55+
/// Perform string escaping to replace all characters which is not supported by NSXMLParser
56+
/// into the specified encoding with decimal entity.
57+
/// For example if your string contains '&' character parser will break the style.
58+
/// This option is active by default.
59+
public static let escapeString = XMLParsingOptions(rawValue: 2)
60+
5561
}
5662

5763
// MARK: - XMLStringBuilder
@@ -104,7 +110,8 @@ public class XMLStringBuilder: NSObject, XMLParserDelegate {
104110
public init(styleXML: StyleXML, string: String) {
105111
self.styleXML = styleXML
106112

107-
let xml = (styleXML.xmlParsingOptions.contains(.doNotWrapXML) ? string : "<\(XMLStringBuilder.topTag)>\(string)</\(XMLStringBuilder.topTag)>")
113+
let xmlString = (styleXML.xmlParsingOptions.contains(.escapeString) ? string.escapeWithUnicodeEntities() : string)
114+
let xml = (styleXML.xmlParsingOptions.contains(.doNotWrapXML) ? xmlString : "<\(XMLStringBuilder.topTag)>\(xmlString)</\(XMLStringBuilder.topTag)>")
108115
guard let data = xml.data(using: String.Encoding.utf8) else {
109116
fatalError("Unable to convert to UTF8")
110117
}

0 commit comments

Comments
 (0)