Skip to content

Commit

Permalink
Additional work on supporting links.
Browse files Browse the repository at this point in the history
  • Loading branch information
cocoatoucher committed Dec 30, 2021
1 parent 25c6ae9 commit 2100ae2
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 73 deletions.
17 changes: 6 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="Docs/logo.png" width="300" max-width="80%" alt="glide"/>
</p>

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(
Expand Down Expand Up @@ -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:
`<a href="http://www.example.com">This is a link</a>`

### 🎆 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: `<italicStyle myAttribute="something"></italicStyle>`

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.
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.
13 changes: 3 additions & 10 deletions Sources/XMLText/Text.XMLString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}
Expand Down
38 changes: 38 additions & 0 deletions Sources/XMLText/XMLParser/Extensions/Color.InitWithHex.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -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: "&amp;"
)
}
}
61 changes: 61 additions & 0 deletions Sources/XMLText/XMLParser/StandardXMLAttributesResolver.swift
Original file line number Diff line number Diff line change
@@ -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)
}

}
56 changes: 56 additions & 0 deletions Sources/XMLText/XMLParser/XMLDynamicStyle.swift
Original file line number Diff line number Diff line change
@@ -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]!)
}
}

}
89 changes: 37 additions & 52 deletions Sources/XMLText/XMLParser/XMLTextBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ class XMLTextBuilder: NSObject {

// MARK: Private Properties

private let didFindNewString: (String, [StyleProtocol]) -> Void

private static let topTag = "source"

/// Parser engine.
Expand Down Expand Up @@ -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)</\(XMLTextBuilder.topTag)>")
let xmlString = (
styleGroup.xmlParsingOptions.contains(.escapeString) ?
string.escapeWithUnicodeEntities()
: string
)
let xml = styleGroup.xmlParsingOptions.contains(.doNotWrapXML) ?
xmlString :
"<\(XMLTextBuilder.topTag)>\(xmlString)</\(XMLTextBuilder.topTag)>"

guard let data = xml.data(using: String.Encoding.utf8) else {
return nil
Expand All @@ -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
)
)
}

Expand Down Expand Up @@ -127,7 +130,8 @@ class XMLTextBuilder: NSObject {
xmlStylers.append(
XMLDynamicStyle(
tag: elementName,
style: styles[elementName]
style: styles[elementName],
xmlAttributes: attributes
)
)
}
Expand All @@ -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: &currentText,
xmlStyle: xmlStyler
)
}
text += currentText

currentString = nil
}

Expand Down Expand Up @@ -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: "&amp;"
)
}

}

0 comments on commit 2100ae2

Please sign in to comment.