Skip to content

Commit 2100ae2

Browse files
committed
Additional work on supporting links.
1 parent 25c6ae9 commit 2100ae2

File tree

7 files changed

+235
-73
lines changed

7 files changed

+235
-73
lines changed

README.md

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<img src="Docs/logo.png" width="300" max-width="80%" alt="glide"/>
33
</p>
44

5-
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.
5+
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.
66

77
```
88
Text(
@@ -72,22 +72,17 @@ Text(
7272
)
7373
```
7474

75-
### 🔗 Links (not supported)
75+
### 🔗 Links
7676

77-
It is currently not supported in `SwiftUI` to combine other `View`(e.g. `Button`) elements with `Text` elements using `+` operator.
78-
79-
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`.
80-
81-
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.
82-
83-
If you have multiple links within the same sentence or paragraph, good luck with `NSAttributedString` and `UIViewRepresentable` of a `UITextView`. 🤷‍♂️
77+
You can add links inside your strings via:
78+
`<a href="http://www.example.com">This is a link</a>`
8479

8580
### 🎆 Images (not supported)
8681

87-
Similar to links, it is currently not supported in `SwiftUI` to combine `Image` elements with `Text` using `+` operator.
82+
It is currently not supported to include `Image` elements within `AttributedString`.
8883

8984
### Custom XML Attributes (not supported)
9085

9186
For example: `<italicStyle myAttribute="something"></italicStyle>`
9287

93-
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.
88+
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.

Sources/XMLText/Text.XMLString.swift

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import SwiftUI
99

1010
public extension Text {
11+
1112
/// Creates a Text with a given XML string and a style group.
1213
/// If unable to parse the XML, raw string value will be passed to
1314
/// resulting Text.
@@ -17,21 +18,13 @@ public extension Text {
1718
/// - styleGroup: Style group used for styling.
1819
init(xmlString: String, styleGroup: StyleGroup) {
1920
do {
20-
var text = AttributedString()
2121
let xmlParser = XMLTextBuilder(
2222
styleGroup: styleGroup,
23-
string: xmlString,
24-
didFindNewString: { string, styles in
25-
var currentText = AttributedString(string)
26-
if let style = styles.last {
27-
currentText = style.add(to: currentText)
28-
}
29-
text += currentText
30-
}
23+
string: xmlString
3124
)
3225
if let xmlParser = xmlParser {
3326
try xmlParser.parse()
34-
self = Text(text)
27+
self = Text(xmlParser.text)
3528
} else {
3629
self = Text(xmlString)
3730
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// Color.InitWithHex.swift
3+
// XMLText
4+
//
5+
// Created by cocoatoucher on 2021-12-30.
6+
//
7+
8+
import SwiftUI
9+
10+
extension Color {
11+
init(hex: String) {
12+
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
13+
var int: UInt64 = 0
14+
15+
Scanner(string: hex).scanHexInt64(&int)
16+
17+
let a, r, g, b: UInt64
18+
19+
switch hex.count {
20+
case 3: // RGB (12-bit)
21+
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
22+
case 6: // RGB (24-bit)
23+
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
24+
case 8: // ARGB (32-bit)
25+
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
26+
default:
27+
(a, r, g, b) = (1, 1, 1, 0)
28+
}
29+
30+
self.init(
31+
.sRGB,
32+
red: Double(r) / 255,
33+
green: Double(g) / 255,
34+
blue: Double(b) / 255,
35+
opacity: Double(a) / 255
36+
)
37+
}
38+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//
2+
// String.EscapeWithUnicodeEntities.swift
3+
// XMLText
4+
//
5+
// Implementation in this file is taken from
6+
// SwiftRichString repository on GitHub.
7+
// https://github.com/malcommac/SwiftRichString/
8+
//
9+
// SwiftRichString
10+
// Elegant Strings & Attributed Strings Toolkit for Swift
11+
//
12+
// Created by Daniele Margutti.
13+
// Copyright © 2018 Daniele Margutti. All rights reserved.
14+
//
15+
// Web: http://www.danielemargutti.com
16+
// Email: hello@danielemargutti.com
17+
// Twitter: @danielemargutti
18+
19+
import Foundation
20+
21+
extension String {
22+
23+
static let escapeAmpRegExp = try! NSRegularExpression(pattern: "&(?!(#[0-9]{2,4}|[A-z]{2,6});)", options: NSRegularExpression.Options(rawValue: 0))
24+
25+
func escapeWithUnicodeEntities() -> String {
26+
let range = NSRange(location: 0, length: self.count)
27+
return String.escapeAmpRegExp.stringByReplacingMatches(
28+
in: self,
29+
options: NSRegularExpression.MatchingOptions(rawValue: 0),
30+
range: range,
31+
withTemplate: "&amp;"
32+
)
33+
}
34+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// StandardXMLAttributesResolver.swift
3+
// XMLText
4+
//
5+
// Implementation in this file is taken from
6+
// SwiftRichString repository on GitHub.
7+
// https://github.com/malcommac/SwiftRichString/
8+
//
9+
// SwiftRichString
10+
// Elegant Strings & Attributed Strings Toolkit for Swift
11+
//
12+
// Created by Daniele Margutti.
13+
// Copyright © 2018 Daniele Margutti. All rights reserved.
14+
//
15+
// Web: http://www.danielemargutti.com
16+
// Email: hello@danielemargutti.com
17+
// Twitter: @danielemargutti
18+
19+
import Foundation
20+
import SwiftUI
21+
22+
class StandardXMLAttributesResolver {
23+
24+
func applyDynamicAttributes(
25+
to attributedString: inout AttributedString,
26+
xmlStyle: XMLDynamicStyle
27+
) {
28+
let finalStyleToApply = Style()
29+
xmlStyle.enumerateAttributes { key, value in
30+
switch key {
31+
case "color":
32+
finalStyleToApply.foregroundColor = Color(hex: value)
33+
default: break
34+
}
35+
}
36+
self.styleForUnknownXMLTag(
37+
xmlStyle.tag,
38+
to: &attributedString,
39+
attributes: xmlStyle.xmlAttributes
40+
)
41+
attributedString = finalStyleToApply.add(to: attributedString)
42+
}
43+
44+
func styleForUnknownXMLTag(
45+
_ tag: String,
46+
to attributedString: inout AttributedString,
47+
attributes: [String: String]?
48+
) {
49+
let finalStyleToApply = Style()
50+
switch tag {
51+
case "a": // href support
52+
if let href = attributes?["href"] {
53+
finalStyleToApply.link = URL(string: href)
54+
}
55+
default:
56+
break
57+
}
58+
attributedString = finalStyleToApply.add(to: attributedString)
59+
}
60+
61+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// XMLDynamicStyle.swift
3+
// XMLText
4+
//
5+
// Implementation in this file is taken from
6+
// SwiftRichString repository on GitHub.
7+
// https://github.com/malcommac/SwiftRichString/
8+
//
9+
// SwiftRichString
10+
// Elegant Strings & Attributed Strings Toolkit for Swift
11+
//
12+
// Created by Daniele Margutti.
13+
// Copyright © 2018 Daniele Margutti. All rights reserved.
14+
//
15+
// Web: http://www.danielemargutti.com
16+
// Email: hello@danielemargutti.com
17+
// Twitter: @danielemargutti
18+
19+
import Foundation
20+
21+
class XMLDynamicStyle {
22+
23+
// MARK: - Public Properties
24+
25+
/// Tag read for this style.
26+
let tag: String
27+
28+
/// Style found in receiver `TextStyleGroup` instance.
29+
let style: StyleProtocol?
30+
31+
/// Attributes found in the xml tag.
32+
let xmlAttributes: [String: String]?
33+
34+
// MARK: - Initialization
35+
36+
init(
37+
tag: String,
38+
style: StyleProtocol?,
39+
xmlAttributes: [String: String]?
40+
) {
41+
self.tag = tag
42+
self.style = style
43+
self.xmlAttributes = xmlAttributes
44+
}
45+
46+
func enumerateAttributes(_ handler: ((_ key: String, _ value: String) -> Void)) {
47+
guard let xmlAttributes = xmlAttributes else {
48+
return
49+
}
50+
51+
xmlAttributes.keys.forEach {
52+
handler($0, xmlAttributes[$0]!)
53+
}
54+
}
55+
56+
}

Sources/XMLText/XMLParser/XMLTextBuilder.swift

Lines changed: 37 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ class XMLTextBuilder: NSObject {
3333

3434
// MARK: Private Properties
3535

36-
private let didFindNewString: (String, [StyleProtocol]) -> Void
37-
3836
private static let topTag = "source"
3937

4038
/// Parser engine.
@@ -71,17 +69,18 @@ class XMLTextBuilder: NSObject {
7169

7270
init?(
7371
styleGroup: StyleGroup,
74-
string: String,
75-
didFindNewString: @escaping (String, [StyleProtocol]) -> Void
72+
string: String
7673
) {
7774
self.styleGroup = styleGroup
7875

79-
self.didFindNewString = didFindNewString
80-
81-
let xmlString = (styleGroup.xmlParsingOptions.contains(.escapeString) ? string.escapeWithUnicodeEntities() : string)
82-
let xml = (styleGroup.xmlParsingOptions.contains(.doNotWrapXML) ?
83-
xmlString :
84-
"<\(XMLTextBuilder.topTag)>\(xmlString)</\(XMLTextBuilder.topTag)>")
76+
let xmlString = (
77+
styleGroup.xmlParsingOptions.contains(.escapeString) ?
78+
string.escapeWithUnicodeEntities()
79+
: string
80+
)
81+
let xml = styleGroup.xmlParsingOptions.contains(.doNotWrapXML) ?
82+
xmlString :
83+
"<\(XMLTextBuilder.topTag)>\(xmlString)</\(XMLTextBuilder.topTag)>"
8584

8685
guard let data = xml.data(using: String.Encoding.utf8) else {
8786
return nil
@@ -91,7 +90,11 @@ class XMLTextBuilder: NSObject {
9190

9291
if let baseStyle = styleGroup.baseStyle {
9392
self.xmlStylers.append(
94-
XMLDynamicStyle(tag: XMLTextBuilder.topTag, style: baseStyle)
93+
XMLDynamicStyle(
94+
tag: XMLTextBuilder.topTag,
95+
style: baseStyle,
96+
xmlAttributes: nil
97+
)
9598
)
9699
}
97100

@@ -127,7 +130,8 @@ class XMLTextBuilder: NSObject {
127130
xmlStylers.append(
128131
XMLDynamicStyle(
129132
tag: elementName,
130-
style: styles[elementName]
133+
style: styles[elementName],
134+
xmlAttributes: attributes
131135
)
132136
)
133137
}
@@ -137,8 +141,28 @@ class XMLTextBuilder: NSObject {
137141
xmlStylers.removeLast()
138142
}
139143

144+
private(set) var text = AttributedString()
145+
private let xmlAttributesResolver = StandardXMLAttributesResolver()
146+
140147
private func foundNewString() {
141-
didFindNewString(currentString ?? "", xmlStylers.compactMap { $0.style })
148+
var currentText = AttributedString(currentString ?? "")
149+
150+
guard let xmlStyler = xmlStylers.last else {
151+
return
152+
}
153+
154+
if let style = xmlStyler.style {
155+
currentText = style.add(to: currentText)
156+
}
157+
158+
if xmlStyler.xmlAttributes != nil {
159+
xmlAttributesResolver.applyDynamicAttributes(
160+
to: &currentText,
161+
xmlStyle: xmlStyler
162+
)
163+
}
164+
text += currentText
165+
142166
currentString = nil
143167
}
144168

@@ -177,42 +201,3 @@ extension XMLTextBuilder: XMLParserDelegate {
177201
currentString = (currentString ?? "").appending(string)
178202
}
179203
}
180-
181-
private class XMLDynamicStyle {
182-
183-
// MARK: - Public Properties
184-
185-
/// Tag read for this style.
186-
let tag: String
187-
188-
/// Style found in receiver `TextStyleGroup` instance.
189-
let style: StyleProtocol?
190-
191-
// MARK: - Initialization
192-
193-
init(
194-
tag: String,
195-
style: StyleProtocol?
196-
) {
197-
self.tag = tag
198-
self.style = style
199-
}
200-
201-
}
202-
203-
private extension String {
204-
205-
static let escapeAmpRegExp = try! NSRegularExpression(pattern: "&(?!(#[0-9]{2,4}|[A-z]{2,6});)", options: NSRegularExpression.Options(rawValue: 0))
206-
207-
func escapeWithUnicodeEntities() -> String {
208-
let range = NSRange(location: 0, length: self.count)
209-
return String.escapeAmpRegExp.stringByReplacingMatches(
210-
in: self,
211-
options: NSRegularExpression.MatchingOptions(rawValue: 0),
212-
range: range,
213-
withTemplate: "&amp;"
214-
)
215-
}
216-
217-
}
218-

0 commit comments

Comments
 (0)