forked from shadowfacts/Tusker
Extract HTML to attributed string converter to separate helper
This commit is contained in:
parent
cde3109203
commit
7e5d8675c2
|
@ -64,6 +64,7 @@
|
||||||
D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B0293BD85300C0B37F /* CreateFilterService.swift */; };
|
D61F75B1293BD85300C0B37F /* CreateFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B0293BD85300C0B37F /* CreateFilterService.swift */; };
|
||||||
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B2293BD89C00C0B37F /* UpdateFilterService.swift */; };
|
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B2293BD89C00C0B37F /* UpdateFilterService.swift */; };
|
||||||
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B4293BD97400C0B37F /* DeleteFilterService.swift */; };
|
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B4293BD97400C0B37F /* DeleteFilterService.swift */; };
|
||||||
|
D61F75BB293C183100C0B37F /* HTMLConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75BA293C183100C0B37F /* HTMLConverter.swift */; };
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
||||||
|
@ -444,6 +445,7 @@
|
||||||
D61F75B0293BD85300C0B37F /* CreateFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFilterService.swift; sourceTree = "<group>"; };
|
D61F75B0293BD85300C0B37F /* CreateFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFilterService.swift; sourceTree = "<group>"; };
|
||||||
D61F75B2293BD89C00C0B37F /* UpdateFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateFilterService.swift; sourceTree = "<group>"; };
|
D61F75B2293BD89C00C0B37F /* UpdateFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateFilterService.swift; sourceTree = "<group>"; };
|
||||||
D61F75B4293BD97400C0B37F /* DeleteFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteFilterService.swift; sourceTree = "<group>"; };
|
D61F75B4293BD97400C0B37F /* DeleteFilterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteFilterService.swift; sourceTree = "<group>"; };
|
||||||
|
D61F75BA293C183100C0B37F /* HTMLConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLConverter.swift; sourceTree = "<group>"; };
|
||||||
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1453,6 +1455,7 @@
|
||||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
||||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
||||||
|
D61F75BA293C183100C0B37F /* HTMLConverter.swift */,
|
||||||
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
|
||||||
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
|
||||||
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
|
||||||
|
@ -1856,6 +1859,7 @@
|
||||||
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */,
|
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */,
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */,
|
||||||
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */,
|
||||||
|
D61F75BB293C183100C0B37F /* HTMLConverter.swift in Sources */,
|
||||||
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */,
|
||||||
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */,
|
D6AEBB412321642700E5038B /* SendMesasgeActivity.swift in Sources */,
|
||||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
//
|
||||||
|
// HTMLConverter.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/3/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SwiftSoup
|
||||||
|
import WebURL
|
||||||
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
|
struct HTMLConverter {
|
||||||
|
|
||||||
|
static let defaultFont = UIFont.systemFont(ofSize: 17)
|
||||||
|
static let defaultColor = UIColor.label
|
||||||
|
static let defaultParagraphStyle: NSParagraphStyle = {
|
||||||
|
let style = NSMutableParagraphStyle()
|
||||||
|
// 2 points is enough that it doesn't make things look weirdly spaced out, but leaves a slight gap when there are multiple lines of emojis
|
||||||
|
style.lineSpacing = 2
|
||||||
|
return style
|
||||||
|
}()
|
||||||
|
|
||||||
|
var font: UIFont = defaultFont
|
||||||
|
var color: UIColor = defaultColor
|
||||||
|
var paragraphStyle: NSParagraphStyle = defaultParagraphStyle
|
||||||
|
|
||||||
|
func convert(_ html: String) -> NSAttributedString {
|
||||||
|
let doc = try! SwiftSoup.parseBodyFragment(html)
|
||||||
|
let body = doc.body()!
|
||||||
|
|
||||||
|
let attributedText = attributedTextForHTMLNode(body)
|
||||||
|
let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
|
||||||
|
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
||||||
|
mutAttrString.collapseWhitespace()
|
||||||
|
|
||||||
|
mutAttrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: mutAttrString.fullRange)
|
||||||
|
|
||||||
|
return mutAttrString
|
||||||
|
}
|
||||||
|
|
||||||
|
private func attributedTextForHTMLNode(_ node: Node, usePreformattedText: Bool = false) -> NSAttributedString {
|
||||||
|
switch node {
|
||||||
|
case let node as TextNode:
|
||||||
|
let text: String
|
||||||
|
if usePreformattedText {
|
||||||
|
text = node.getWholeText()
|
||||||
|
} else {
|
||||||
|
text = node.text()
|
||||||
|
}
|
||||||
|
return NSAttributedString(string: text, attributes: [.font: font, .foregroundColor: color])
|
||||||
|
case let node as Element:
|
||||||
|
let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color])
|
||||||
|
for child in node.getChildNodes() {
|
||||||
|
attributed.append(attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre"))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch node.tagName() {
|
||||||
|
case "br":
|
||||||
|
// need to specify defaultFont here b/c otherwise it uses the default 12pt Helvetica which
|
||||||
|
// screws up its determination of the line height making multiple lines of emojis squash together
|
||||||
|
attributed.append(NSAttributedString(string: "\n", attributes: [.font: font]))
|
||||||
|
case "a":
|
||||||
|
let href = try! node.attr("href")
|
||||||
|
if let webURL = WebURL(href),
|
||||||
|
let url = URL(webURL) {
|
||||||
|
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
|
||||||
|
} else if let url = URL(string: href) {
|
||||||
|
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
|
||||||
|
}
|
||||||
|
case "p":
|
||||||
|
attributed.append(NSAttributedString(string: "\n\n", attributes: [.font: font]))
|
||||||
|
case "em", "i":
|
||||||
|
let currentFont: UIFont
|
||||||
|
if attributed.length == 0 {
|
||||||
|
currentFont = font
|
||||||
|
} else {
|
||||||
|
currentFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? font
|
||||||
|
}
|
||||||
|
attributed.addAttribute(.font, value: currentFont.withTraits(.traitItalic)!, range: attributed.fullRange)
|
||||||
|
case "strong", "b":
|
||||||
|
let currentFont: UIFont
|
||||||
|
if attributed.length == 0 {
|
||||||
|
currentFont = font
|
||||||
|
} else {
|
||||||
|
currentFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? font
|
||||||
|
}
|
||||||
|
attributed.addAttribute(.font, value: currentFont.withTraits(.traitBold)!, range: attributed.fullRange)
|
||||||
|
case "del":
|
||||||
|
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
||||||
|
case "code":
|
||||||
|
// TODO: this probably breaks with dynamic type
|
||||||
|
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: font.pointSize, weight: .regular), range: attributed.fullRange)
|
||||||
|
case "pre":
|
||||||
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
|
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: font.pointSize, weight: .regular), range: attributed.fullRange)
|
||||||
|
case "ol", "ul":
|
||||||
|
attributed.append(NSAttributedString(string: "\n\n"))
|
||||||
|
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
||||||
|
case "li":
|
||||||
|
let parentEl = node.parent()!
|
||||||
|
let parentTag = parentEl.tagName()
|
||||||
|
let bullet: NSAttributedString
|
||||||
|
if parentTag == "ol" {
|
||||||
|
let index = (try? node.elementSiblingIndex()) ?? 0
|
||||||
|
// we use the monospace digit font so that the periods of all the list items line up
|
||||||
|
// TODO: this probably breaks with dynamic type
|
||||||
|
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: font.pointSize, weight: .regular)])
|
||||||
|
} else if parentTag == "ul" {
|
||||||
|
bullet = NSAttributedString(string: "\u{2022}\t", attributes: [.font: font])
|
||||||
|
} else {
|
||||||
|
bullet = NSAttributedString()
|
||||||
|
}
|
||||||
|
attributed.insert(bullet, at: 0)
|
||||||
|
attributed.append(NSAttributedString(string: "\n", attributes: [.font: font]))
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributed
|
||||||
|
default:
|
||||||
|
fatalError("Unexpected node type \(type(of: node))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -22,14 +22,19 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
weak var overrideMastodonController: MastodonController?
|
weak var overrideMastodonController: MastodonController?
|
||||||
var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController }
|
var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController }
|
||||||
|
|
||||||
var defaultFont: UIFont = .systemFont(ofSize: 17)
|
private var htmlConverter = HTMLConverter()
|
||||||
var defaultColor: UIColor = .label
|
var defaultFont: UIFont {
|
||||||
var paragraphStyle: NSParagraphStyle = {
|
_read { yield htmlConverter.font }
|
||||||
let style = NSMutableParagraphStyle()
|
_modify { yield &htmlConverter.font }
|
||||||
// 2 points is enough that it doesn't make things look weirdly spaced out, but leaves a slight gap when there are multiple lines of emojis
|
}
|
||||||
style.lineSpacing = 2
|
var defaultColor: UIColor {
|
||||||
return style
|
_read { yield htmlConverter.color }
|
||||||
}()
|
_modify { yield &htmlConverter.color }
|
||||||
|
}
|
||||||
|
var paragraphStyle: NSParagraphStyle {
|
||||||
|
_read { yield htmlConverter.paragraphStyle }
|
||||||
|
_modify { yield &htmlConverter.paragraphStyle }
|
||||||
|
}
|
||||||
|
|
||||||
private(set) var hasEmojis = false
|
private(set) var hasEmojis = false
|
||||||
|
|
||||||
|
@ -85,99 +90,7 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
|
|
||||||
// MARK: - HTML Parsing
|
// MARK: - HTML Parsing
|
||||||
func setTextFromHtml(_ html: String) {
|
func setTextFromHtml(_ html: String) {
|
||||||
let doc = try! SwiftSoup.parse(html)
|
self.attributedText = htmlConverter.convert(html)
|
||||||
let body = doc.body()!
|
|
||||||
|
|
||||||
let attributedText = attributedTextForHTMLNode(body)
|
|
||||||
let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
|
|
||||||
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
|
||||||
mutAttrString.collapseWhitespace()
|
|
||||||
|
|
||||||
mutAttrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: mutAttrString.fullRange)
|
|
||||||
|
|
||||||
self.attributedText = mutAttrString
|
|
||||||
}
|
|
||||||
|
|
||||||
private func attributedTextForHTMLNode(_ node: Node, usePreformattedText: Bool = false) -> NSAttributedString {
|
|
||||||
switch node {
|
|
||||||
case let node as TextNode:
|
|
||||||
let text: String
|
|
||||||
if usePreformattedText {
|
|
||||||
text = node.getWholeText()
|
|
||||||
} else {
|
|
||||||
text = node.text()
|
|
||||||
}
|
|
||||||
return NSAttributedString(string: text, attributes: [.font: defaultFont, .foregroundColor: defaultColor])
|
|
||||||
case let node as Element:
|
|
||||||
let attributed = NSMutableAttributedString(string: "", attributes: [.font: defaultFont, .foregroundColor: defaultColor])
|
|
||||||
for child in node.getChildNodes() {
|
|
||||||
attributed.append(attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre"))
|
|
||||||
}
|
|
||||||
|
|
||||||
switch node.tagName() {
|
|
||||||
case "br":
|
|
||||||
// need to specify defaultFont here b/c otherwise it uses the default 12pt Helvetica which
|
|
||||||
// screws up its determination of the line height making multiple lines of emojis squash together
|
|
||||||
attributed.append(NSAttributedString(string: "\n", attributes: [.font: defaultFont]))
|
|
||||||
case "a":
|
|
||||||
let href = try! node.attr("href")
|
|
||||||
if let webURL = WebURL(href),
|
|
||||||
let url = URL(webURL) {
|
|
||||||
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
|
|
||||||
} else if let url = URL(string: href) {
|
|
||||||
attributed.addAttribute(.link, value: url, range: attributed.fullRange)
|
|
||||||
}
|
|
||||||
case "p":
|
|
||||||
attributed.append(NSAttributedString(string: "\n\n", attributes: [.font: defaultFont]))
|
|
||||||
case "em", "i":
|
|
||||||
let currentFont: UIFont
|
|
||||||
if attributed.length == 0 {
|
|
||||||
currentFont = defaultFont
|
|
||||||
} else {
|
|
||||||
currentFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? defaultFont
|
|
||||||
}
|
|
||||||
attributed.addAttribute(.font, value: currentFont.withTraits(.traitItalic)!, range: attributed.fullRange)
|
|
||||||
case "strong", "b":
|
|
||||||
let currentFont: UIFont
|
|
||||||
if attributed.length == 0 {
|
|
||||||
currentFont = defaultFont
|
|
||||||
} else {
|
|
||||||
currentFont = attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? defaultFont
|
|
||||||
}
|
|
||||||
attributed.addAttribute(.font, value: currentFont.withTraits(.traitBold)!, range: attributed.fullRange)
|
|
||||||
case "del":
|
|
||||||
attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange)
|
|
||||||
case "code":
|
|
||||||
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
|
|
||||||
case "pre":
|
|
||||||
attributed.append(NSAttributedString(string: "\n\n"))
|
|
||||||
attributed.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: defaultFont.pointSize, weight: .regular), range: attributed.fullRange)
|
|
||||||
case "ol", "ul":
|
|
||||||
attributed.append(NSAttributedString(string: "\n\n"))
|
|
||||||
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
|
||||||
case "li":
|
|
||||||
let parentEl = node.parent()!
|
|
||||||
let parentTag = parentEl.tagName()
|
|
||||||
let bullet: NSAttributedString
|
|
||||||
if parentTag == "ol" {
|
|
||||||
let index = (try? node.elementSiblingIndex()) ?? 0
|
|
||||||
// we use the monospace digit font so that the periods of all the list items line up
|
|
||||||
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: UIFont.monospacedDigitSystemFont(ofSize: defaultFont.pointSize, weight: .regular)])
|
|
||||||
} else if parentTag == "ul" {
|
|
||||||
bullet = NSAttributedString(string: "\u{2022}\t", attributes: [.font: defaultFont])
|
|
||||||
} else {
|
|
||||||
bullet = NSAttributedString()
|
|
||||||
}
|
|
||||||
attributed.insert(bullet, at: 0)
|
|
||||||
attributed.append(NSAttributedString(string: "\n", attributes: [.font: defaultFont]))
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributed
|
|
||||||
default:
|
|
||||||
fatalError("Unexpected node type \(type(of: node))")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
Loading…
Reference in New Issue