Skip to content

Instantly share code, notes, and snippets.

@mflint
Created August 29, 2018 19:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mflint/72460f8404a10a3f7b833e81998b0e63 to your computer and use it in GitHub Desktop.
Save mflint/72460f8404a10a3f7b833e81998b0e63 to your computer and use it in GitHub Desktop.
A hacky String extension to convert html into NSAttributedString. Very Mastodon-specific. Very ugly.
import Foundation
extension String {
class HtmlComponent {
var parent: HtmlComponent?
var children = [HtmlComponent]()
init(parent: HtmlComponent? = nil) {
self.parent = parent
}
func attributedString() -> NSMutableAttributedString {
preconditionFailure()
}
func add(child: HtmlComponent) {
children.append(child)
}
}
class UnstyledComponent: HtmlComponent {
private let tagContent: String
init(parent: HtmlComponent?, tagContent: String) {
self.tagContent = tagContent
super.init(parent: parent)
}
override func attributedString() -> NSMutableAttributedString {
let result = NSMutableAttributedString()
if !tagContent.contains("\"invisible\"") {
for child in children {
let childResult = child.attributedString()
result.append(childResult)
}
if tagContent.contains("\"ellipsis\"") {
result.append(NSAttributedString(string: "..."))
}
}
return result
}
}
class ParagraphComponent: HtmlComponent {
override func attributedString() -> NSMutableAttributedString {
let result = NSMutableAttributedString()
for child in children {
let childResult = child.attributedString()
result.append(childResult)
}
result.append(NSAttributedString(string: "\n\n"))
return result
}
}
class LineBreakComponent: HtmlComponent {
override func attributedString() -> NSMutableAttributedString {
let result = NSMutableAttributedString(string: "\n")
return result
}
}
class AnchorComponent: HtmlComponent {
private let url: String
init(parent: HtmlComponent?, url: String) {
self.url = url
super.init(parent: parent)
}
override func attributedString() -> NSMutableAttributedString {
let result = NSMutableAttributedString()
for child in children {
let childResult = child.attributedString()
result.append(childResult)
}
result.addAttribute(.link, value: url, range: NSMakeRange(0, result.string.count))
return result
}
}
class TextComponent: HtmlComponent {
let text: String
init(text: String) {
self.text = text
}
override func attributedString() -> NSMutableAttributedString {
return NSMutableAttributedString(string: text)
}
}
// lol 🤪
func attributedStringFromHtml() -> NSAttributedString {
let body = UnstyledComponent(parent: nil, tagContent: "")
var parent = body as HtmlComponent?
var index = startIndex
repeat {
if let thisParent = parent {
(index, parent) = nextComponent(from: index, parent: thisParent)
}
} while parent != nil
let result = body.attributedString()
return result
}
private func nextComponent(from index: Index, parent: HtmlComponent) -> (Index, HtmlComponent?) {
let remainder = self[index...]
if remainder.count == 0 {
return (endIndex, nil)
}
// check for a close tag
if remainder.starts(with: "</") {
if let closeTagIndex = remainder.index(of: ">") {
let nextIndex = remainder.index(closeTagIndex, offsetBy: 1)
return (nextIndex, parent.parent)
}
}
// check for an open tag
if remainder.starts(with: "<"), let closeTagIndex = remainder.index(of: ">") {
let startTagIndex = remainder.index(remainder.startIndex, offsetBy: 1)
let tagContent = String(remainder[startTagIndex..<closeTagIndex]).trimmingCharacters(in: .whitespacesAndNewlines)
let newParent: HtmlComponent
if tagContent == "p" {
let paragraph = ParagraphComponent(parent: parent)
parent.add(child: paragraph)
newParent = paragraph
} else if tagContent == "br" || tagContent == "br/" || tagContent == "br /" {
parent.add(child: LineBreakComponent(parent: parent))
newParent = parent
} else if tagContent.starts(with: "a ") {
if let urlStartIndex = tagContent.range(of: "href=\"")?.upperBound {
let intermediateString = tagContent[urlStartIndex...]
if let urlEndIndex = intermediateString.index(of: "\"") {
let url = intermediateString[..<urlEndIndex]
let anchor = AnchorComponent(parent: parent, url: String(url))
parent.add(child: anchor)
newParent = anchor
} else {
// TODO: something wrong with this anchor tag
let unstyledComponent = UnstyledComponent(parent: parent, tagContent: tagContent)
parent.add(child: unstyledComponent)
newParent = unstyledComponent
}
} else {
// TODO: something wrong with this anchor tag
let unstyledComponent = UnstyledComponent(parent: parent, tagContent: tagContent)
parent.add(child: unstyledComponent)
newParent = unstyledComponent
}
} else {
// TODO: record these unexpected tags? ('tagContent')
let unstyledComponent = UnstyledComponent(parent: parent, tagContent: tagContent)
parent.add(child: unstyledComponent)
newParent = unstyledComponent
}
let nextIndex = remainder.index(closeTagIndex, offsetBy: 1)
return (nextIndex, newParent)
}
// this isn't an open tag, so grab the text until the next open tag (or the end of the string, if there are no more tags)
let textEnd = remainder.index(of: "<") ?? self.endIndex
let textContent = String(remainder[..<textEnd])
parent.add(child: TextComponent(text: textContent))
return (textEnd, parent)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment