diff --git a/README.md b/README.md index 6f945b8..8fdc755 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ glide

-XMLText is a mini library that can generate SwiftUI `Text` from a given XML string with tags. It uses `+` operator of `Text` to compose the final output. +XMLText is a mini library that can generate SwiftUI `Text` from a given XML string with tags. It uses `AttributedString` to compose the final text output. ``` Text( @@ -72,22 +72,17 @@ Text( ) ``` -### 🔗 Links (not supported) +### 🔗 Links -It is currently not supported in `SwiftUI` to combine other `View`(e.g. `Button`) elements with `Text` elements using `+` operator. - -Adding tap gesture recognizer to individual parts of `Text` while using `+` operator is also not supported, as gesture recognizer modifiers return an opaque type of `View`, which means it is not `Text` anymore, then it can't be added to other `Text`. - -If you have only one link within a given paragraph or sentence, consider getting away with adding a tap gesture recognizer to the whole `Text` of paragraph or sentence which is generated via `XMLText` library. - -If you have multiple links within the same sentence or paragraph, good luck with `NSAttributedString` and `UIViewRepresentable` of a `UITextView`. 🤷‍♂️ +You can add links inside your strings via: +`This is a link` ### 🎆 Images (not supported) -Similar to links, it is currently not supported in `SwiftUI` to combine `Image` elements with `Text` using `+` operator. +It is currently not supported to include `Image` elements within `AttributedString`. ### Custom XML Attributes (not supported) For example: `` -This is currently not supported for sake of simplicity and given the fact that the library doesn't have so many capabilities for that to make sense. If there would be some use cases regarding this, a similar approach to `XMLDynamicAttributesResolver` of `SwiftRichString` library could be considered in the future. \ No newline at end of file +This is currently not supported for sake of simplicity and given the fact that the library doesn't have so many capabilities for that to make sense. If there would be some use cases regarding this, a similar approach to `XMLDynamicAttributesResolver` of `SwiftRichString` library could be considered in the future. diff --git a/Sources/XMLText/Text.XMLString.swift b/Sources/XMLText/Text.XMLString.swift index ed02475..22e07ad 100644 --- a/Sources/XMLText/Text.XMLString.swift +++ b/Sources/XMLText/Text.XMLString.swift @@ -8,6 +8,7 @@ import SwiftUI public extension Text { + /// Creates a Text with a given XML string and a style group. /// If unable to parse the XML, raw string value will be passed to /// resulting Text. @@ -17,21 +18,13 @@ public extension Text { /// - styleGroup: Style group used for styling. init(xmlString: String, styleGroup: StyleGroup) { do { - var text = AttributedString() let xmlParser = XMLTextBuilder( styleGroup: styleGroup, - string: xmlString, - didFindNewString: { string, styles in - var currentText = AttributedString(string) - if let style = styles.last { - currentText = style.add(to: currentText) - } - text += currentText - } + string: xmlString ) if let xmlParser = xmlParser { try xmlParser.parse() - self = Text(text) + self = Text(xmlParser.text) } else { self = Text(xmlString) } diff --git a/Sources/XMLText/XMLParser/Extensions/Color.InitWithHex.swift b/Sources/XMLText/XMLParser/Extensions/Color.InitWithHex.swift new file mode 100644 index 0000000..423792c --- /dev/null +++ b/Sources/XMLText/XMLParser/Extensions/Color.InitWithHex.swift @@ -0,0 +1,38 @@ +// +// Color.InitWithHex.swift +// XMLText +// +// Created by cocoatoucher on 2021-12-30. +// + +import SwiftUI + +extension Color { + init(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + + Scanner(string: hex).scanHexInt64(&int) + + let a, r, g, b: UInt64 + + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (1, 1, 1, 0) + } + + self.init( + .sRGB, + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255, + opacity: Double(a) / 255 + ) + } +} diff --git a/Sources/XMLText/XMLParser/Extensions/String.EscapeWithUnicodeEntities.swift b/Sources/XMLText/XMLParser/Extensions/String.EscapeWithUnicodeEntities.swift new file mode 100644 index 0000000..05e0402 --- /dev/null +++ b/Sources/XMLText/XMLParser/Extensions/String.EscapeWithUnicodeEntities.swift @@ -0,0 +1,34 @@ +// +// String.EscapeWithUnicodeEntities.swift +// XMLText +// +// Implementation in this file is taken from +// SwiftRichString repository on GitHub. +// https://github.com/malcommac/SwiftRichString/ +// +// SwiftRichString +// Elegant Strings & Attributed Strings Toolkit for Swift +// +// Created by Daniele Margutti. +// Copyright © 2018 Daniele Margutti. All rights reserved. +// +// Web: http://www.danielemargutti.com +// Email: hello@danielemargutti.com +// Twitter: @danielemargutti + +import Foundation + +extension String { + + static let escapeAmpRegExp = try! NSRegularExpression(pattern: "&(?!(#[0-9]{2,4}|[A-z]{2,6});)", options: NSRegularExpression.Options(rawValue: 0)) + + func escapeWithUnicodeEntities() -> String { + let range = NSRange(location: 0, length: self.count) + return String.escapeAmpRegExp.stringByReplacingMatches( + in: self, + options: NSRegularExpression.MatchingOptions(rawValue: 0), + range: range, + withTemplate: "&" + ) + } +} diff --git a/Sources/XMLText/XMLParser/StandardXMLAttributesResolver.swift b/Sources/XMLText/XMLParser/StandardXMLAttributesResolver.swift new file mode 100644 index 0000000..4546028 --- /dev/null +++ b/Sources/XMLText/XMLParser/StandardXMLAttributesResolver.swift @@ -0,0 +1,61 @@ +// +// StandardXMLAttributesResolver.swift +// XMLText +// +// Implementation in this file is taken from +// SwiftRichString repository on GitHub. +// https://github.com/malcommac/SwiftRichString/ +// +// SwiftRichString +// Elegant Strings & Attributed Strings Toolkit for Swift +// +// Created by Daniele Margutti. +// Copyright © 2018 Daniele Margutti. All rights reserved. +// +// Web: http://www.danielemargutti.com +// Email: hello@danielemargutti.com +// Twitter: @danielemargutti + +import Foundation +import SwiftUI + +class StandardXMLAttributesResolver { + + func applyDynamicAttributes( + to attributedString: inout AttributedString, + xmlStyle: XMLDynamicStyle + ) { + let finalStyleToApply = Style() + xmlStyle.enumerateAttributes { key, value in + switch key { + case "color": + finalStyleToApply.foregroundColor = Color(hex: value) + default: break + } + } + self.styleForUnknownXMLTag( + xmlStyle.tag, + to: &attributedString, + attributes: xmlStyle.xmlAttributes + ) + attributedString = finalStyleToApply.add(to: attributedString) + } + + func styleForUnknownXMLTag( + _ tag: String, + to attributedString: inout AttributedString, + attributes: [String: String]? + ) { + let finalStyleToApply = Style() + switch tag { + case "a": // href support + if let href = attributes?["href"] { + finalStyleToApply.link = URL(string: href) + } + default: + break + } + attributedString = finalStyleToApply.add(to: attributedString) + } + +} diff --git a/Sources/XMLText/XMLParser/XMLDynamicStyle.swift b/Sources/XMLText/XMLParser/XMLDynamicStyle.swift new file mode 100644 index 0000000..0ff9b0c --- /dev/null +++ b/Sources/XMLText/XMLParser/XMLDynamicStyle.swift @@ -0,0 +1,56 @@ +// +// XMLDynamicStyle.swift +// XMLText +// +// Implementation in this file is taken from +// SwiftRichString repository on GitHub. +// https://github.com/malcommac/SwiftRichString/ +// +// SwiftRichString +// Elegant Strings & Attributed Strings Toolkit for Swift +// +// Created by Daniele Margutti. +// Copyright © 2018 Daniele Margutti. All rights reserved. +// +// Web: http://www.danielemargutti.com +// Email: hello@danielemargutti.com +// Twitter: @danielemargutti + +import Foundation + +class XMLDynamicStyle { + + // MARK: - Public Properties + + /// Tag read for this style. + let tag: String + + /// Style found in receiver `TextStyleGroup` instance. + let style: StyleProtocol? + + /// Attributes found in the xml tag. + let xmlAttributes: [String: String]? + + // MARK: - Initialization + + init( + tag: String, + style: StyleProtocol?, + xmlAttributes: [String: String]? + ) { + self.tag = tag + self.style = style + self.xmlAttributes = xmlAttributes + } + + func enumerateAttributes(_ handler: ((_ key: String, _ value: String) -> Void)) { + guard let xmlAttributes = xmlAttributes else { + return + } + + xmlAttributes.keys.forEach { + handler($0, xmlAttributes[$0]!) + } + } + +} diff --git a/Sources/XMLText/XMLParser/XMLTextBuilder.swift b/Sources/XMLText/XMLParser/XMLTextBuilder.swift index 3ecd57b..2806bb4 100644 --- a/Sources/XMLText/XMLParser/XMLTextBuilder.swift +++ b/Sources/XMLText/XMLParser/XMLTextBuilder.swift @@ -33,8 +33,6 @@ class XMLTextBuilder: NSObject { // MARK: Private Properties - private let didFindNewString: (String, [StyleProtocol]) -> Void - private static let topTag = "source" /// Parser engine. @@ -71,17 +69,18 @@ class XMLTextBuilder: NSObject { init?( styleGroup: StyleGroup, - string: String, - didFindNewString: @escaping (String, [StyleProtocol]) -> Void + string: String ) { self.styleGroup = styleGroup - self.didFindNewString = didFindNewString - - let xmlString = (styleGroup.xmlParsingOptions.contains(.escapeString) ? string.escapeWithUnicodeEntities() : string) - let xml = (styleGroup.xmlParsingOptions.contains(.doNotWrapXML) ? - xmlString : - "<\(XMLTextBuilder.topTag)>\(xmlString)") + let xmlString = ( + styleGroup.xmlParsingOptions.contains(.escapeString) ? + string.escapeWithUnicodeEntities() + : string + ) + let xml = styleGroup.xmlParsingOptions.contains(.doNotWrapXML) ? + xmlString : + "<\(XMLTextBuilder.topTag)>\(xmlString)" guard let data = xml.data(using: String.Encoding.utf8) else { return nil @@ -91,7 +90,11 @@ class XMLTextBuilder: NSObject { if let baseStyle = styleGroup.baseStyle { self.xmlStylers.append( - XMLDynamicStyle(tag: XMLTextBuilder.topTag, style: baseStyle) + XMLDynamicStyle( + tag: XMLTextBuilder.topTag, + style: baseStyle, + xmlAttributes: nil + ) ) } @@ -127,7 +130,8 @@ class XMLTextBuilder: NSObject { xmlStylers.append( XMLDynamicStyle( tag: elementName, - style: styles[elementName] + style: styles[elementName], + xmlAttributes: attributes ) ) } @@ -137,8 +141,28 @@ class XMLTextBuilder: NSObject { xmlStylers.removeLast() } + private(set) var text = AttributedString() + private let xmlAttributesResolver = StandardXMLAttributesResolver() + private func foundNewString() { - didFindNewString(currentString ?? "", xmlStylers.compactMap { $0.style }) + var currentText = AttributedString(currentString ?? "") + + guard let xmlStyler = xmlStylers.last else { + return + } + + if let style = xmlStyler.style { + currentText = style.add(to: currentText) + } + + if xmlStyler.xmlAttributes != nil { + xmlAttributesResolver.applyDynamicAttributes( + to: ¤tText, + xmlStyle: xmlStyler + ) + } + text += currentText + currentString = nil } @@ -177,42 +201,3 @@ extension XMLTextBuilder: XMLParserDelegate { currentString = (currentString ?? "").appending(string) } } - -private class XMLDynamicStyle { - - // MARK: - Public Properties - - /// Tag read for this style. - let tag: String - - /// Style found in receiver `TextStyleGroup` instance. - let style: StyleProtocol? - - // MARK: - Initialization - - init( - tag: String, - style: StyleProtocol? - ) { - self.tag = tag - self.style = style - } - -} - -private extension String { - - static let escapeAmpRegExp = try! NSRegularExpression(pattern: "&(?!(#[0-9]{2,4}|[A-z]{2,6});)", options: NSRegularExpression.Options(rawValue: 0)) - - func escapeWithUnicodeEntities() -> String { - let range = NSRange(location: 0, length: self.count) - return String.escapeAmpRegExp.stringByReplacingMatches( - in: self, - options: NSRegularExpression.MatchingOptions(rawValue: 0), - range: range, - withTemplate: "&" - ) - } - -} -