Created
August 29, 2018 19:15
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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