Replace SwiftSoup with HTMLStreamer

This commit is contained in:
Shadowfacts 2023-12-22 20:44:46 -05:00
parent e8576277e0
commit fd72390a22
30 changed files with 144 additions and 322 deletions

View File

@ -32,7 +32,7 @@
D608470F2A245D1F00C17380 /* ActiveInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D608470E2A245D1F00C17380 /* ActiveInstance.swift */; }; D608470F2A245D1F00C17380 /* ActiveInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D608470E2A245D1F00C17380 /* ActiveInstance.swift */; };
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; }; D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; }; D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; }; D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */ = {isa = PBXBuildFile; productRef = D60BB3932B30076F00DAEA65 /* HTMLStreamer */; };
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; }; D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; };
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; }; D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; };
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; }; D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; };
@ -303,7 +303,6 @@
D6D79F262A0C8D2700AB2315 /* FetchStatusSourceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */; }; D6D79F262A0C8D2700AB2315 /* FetchStatusSourceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */; };
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */; }; D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */; };
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */; }; D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */; };
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */; };
D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */; }; D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */; };
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */; }; D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */; };
D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */; }; D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */; };
@ -712,7 +711,6 @@
D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStatusSourceService.swift; sourceTree = "<group>"; }; D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStatusSourceService.swift; sourceTree = "<group>"; };
D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; }; D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditCollectionViewCell.swift; sourceTree = "<group>"; }; D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditCollectionViewCell.swift; sourceTree = "<group>"; };
D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditContentTextView.swift; sourceTree = "<group>"; };
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditPollView.swift; sourceTree = "<group>"; }; D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditPollView.swift; sourceTree = "<group>"; };
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableButton.swift; sourceTree = "<group>"; }; D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableButton.swift; sourceTree = "<group>"; };
D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; }; D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
@ -786,10 +784,10 @@
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */, D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
D659F35E2953A212002D944A /* TTTKit in Frameworks */, D659F35E2953A212002D944A /* TTTKit in Frameworks */,
D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */, D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */,
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */, D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */,
D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */, D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */,
D6552367289870790048A653 /* ScreenCorners in Frameworks */, D6552367289870790048A653 /* ScreenCorners in Frameworks */,
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */,
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */, D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */, D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */, D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */,
@ -1574,7 +1572,6 @@
children = ( children = (
D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */, D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */,
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */, D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */,
D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */,
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */, D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */,
); );
path = "Status Edit History"; path = "Status Edit History";
@ -1699,7 +1696,6 @@
); );
name = Tusker; name = Tusker;
packageProductDependencies = ( packageProductDependencies = (
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
D6676CA427A8D0020052936B /* WebURLFoundationExtras */, D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
D674A50827F9128D00BA03AC /* Pachyderm */, D674A50827F9128D00BA03AC /* Pachyderm */,
D6552366289870790048A653 /* ScreenCorners */, D6552366289870790048A653 /* ScreenCorners */,
@ -1711,6 +1707,7 @@
D635237029B78A7D009ED5E7 /* TuskerComponents */, D635237029B78A7D009ED5E7 /* TuskerComponents */,
D6BD395829B64426005FFD2B /* ComposeUI */, D6BD395829B64426005FFD2B /* ComposeUI */,
D6CA6ED129EF6091003EC5DF /* TuskerPreferences */, D6CA6ED129EF6091003EC5DF /* TuskerPreferences */,
D60BB3932B30076F00DAEA65 /* HTMLStreamer */,
); );
productName = Tusker; productName = Tusker;
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */; productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
@ -1821,10 +1818,10 @@
); );
mainGroup = D6D4DDC3212518A000E1C4BB; mainGroup = D6D4DDC3212518A000E1C4BB;
packageReferences = ( packageReferences = (
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */, D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */, D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */, D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */,
); );
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */; productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -2181,7 +2178,6 @@
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */, D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */, D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */, D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */,
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */, D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */, D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */,
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */, D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */,
@ -2956,12 +2952,12 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/scinfu/SwiftSoup.git"; repositoryURL = "https://git.shadowfacts.net/shadowfacts/HTMLStreamer.git";
requirement = { requirement = {
kind = upToNextMinorVersion; branch = main;
minimumVersion = 2.3.2; kind = branch;
}; };
}; };
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
@ -2991,10 +2987,10 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
D60CFFDA24A290BA00D00083 /* SwiftSoup */ = { D60BB3932B30076F00DAEA65 /* HTMLStreamer */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */; package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */;
productName = SwiftSoup; productName = HTMLStreamer;
}; };
D61ABEFB28F105DE00B29151 /* Pachyderm */ = { D61ABEFB28F105DE00B29151 /* Pachyderm */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
import LinkPresentation import LinkPresentation
import SwiftSoup import HTMLStreamer
class StatusActivityItemSource: NSObject, UIActivityItemSource { class StatusActivityItemSource: NSObject, UIActivityItemSource {
let status: StatusMO let status: StatusMO
@ -33,8 +33,8 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource {
let metadata = LPLinkMetadata() let metadata = LPLinkMetadata()
metadata.originalURL = status.url! metadata.originalURL = status.url!
metadata.url = status.url! metadata.url = status.url!
let doc = try! SwiftSoup.parse(status.content) var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self)
let content = try! doc.text() let content = converter.convert(html: status.content)
metadata.title = "\(status.account.displayName): \"\(content)\"" metadata.title = "\(status.account.displayName): \"\(content)\""
if let avatar = status.account.avatar, if let avatar = status.account.avatar,
let data = ImageCache.avatars.getData(avatar), let data = ImageCache.avatars.getData(avatar),

View File

@ -42,7 +42,7 @@ class Filterer {
var filtersChanged: ((Bool) -> Void)? var filtersChanged: ((Bool) -> Void)?
var htmlConverter = HTMLConverter() private var htmlConverter: HTMLConverter
private var hasSetup = false private var hasSetup = false
private var matchers = [(NSRegularExpression, Result)]() private var matchers = [(NSRegularExpression, Result)]()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
@ -55,9 +55,10 @@ class Filterer {
// are no longer valid, without needing to go through and update each of them // are no longer valid, without needing to go through and update each of them
private var generation = 0 private var generation = 0
init(mastodonController: MastodonController, context: FilterV1.Context) { init(mastodonController: MastodonController, context: FilterV1.Context, htmlConverter: HTMLConverter) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.context = context self.context = context
self.htmlConverter = htmlConverter
self.hideReblogsInTimelines = Preferences.shared.hideReblogsInTimelines self.hideReblogsInTimelines = Preferences.shared.hideReblogsInTimelines
self.hideRepliesInTimelines = Preferences.shared.hideRepliesInTimelines self.hideRepliesInTimelines = Preferences.shared.hideRepliesInTimelines

View File

@ -7,11 +7,11 @@
// //
import UIKit import UIKit
import SwiftSoup import HTMLStreamer
import WebURL import WebURL
import WebURLFoundationExtras import WebURLFoundationExtras
struct HTMLConverter { class HTMLConverter {
static let defaultFont = UIFont.systemFont(ofSize: 17) static let defaultFont = UIFont.systemFont(ofSize: 17)
static let defaultMonospaceFont = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular) static let defaultMonospaceFont = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular)
@ -23,150 +23,47 @@ struct HTMLConverter {
return style return style
}() }()
var font: UIFont = defaultFont private var converter: AttributedStringConverter<Callbacks>
var monospaceFont: UIFont = defaultMonospaceFont
var color: UIColor = defaultColor
var paragraphStyle: NSParagraphStyle = defaultParagraphStyle
init(font: UIFont, monospaceFont: UIFont, color: UIColor, paragraphStyle: NSParagraphStyle) {
let config = AttributedStringConverterConfiguration(font: font, monospaceFont: monospaceFont, color: color, paragraphStyle: paragraphStyle)
self.converter = AttributedStringConverter(configuration: config)
}
func convert(_ html: String) -> NSAttributedString { func convert(_ html: String) -> NSAttributedString {
let doc = try! SwiftSoup.parseBodyFragment(html) converter.convert(html: html)
let body = doc.body()!
if let attributedText = attributedTextForHTMLNode(body) {
let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
mutAttrString.collapseWhitespace()
// Wait until the end and then fill in the unset paragraph styles, to avoid clobbering the list style.
mutAttrString.enumerateAttribute(.paragraphStyle, in: mutAttrString.fullRange, options: .longestEffectiveRangeNotRequired) { value, range, stop in
if value == nil {
mutAttrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
}
}
return mutAttrString
} else {
return NSAttributedString()
}
} }
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:
if node.tagName() == "ol" || node.tagName() == "ul" {
return attributedTextForList(node, usePreformattedText: usePreformattedText)
}
let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color])
for child in node.getChildNodes() {
var appendEllipsis = false
if node.tagName() == "a",
let el = child as? Element {
if el.hasClass("invisible") {
continue
} else if el.hasClass("ellipsis") {
appendEllipsis = true
}
}
if let childText = attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre") {
attributed.append(childText)
}
if appendEllipsis {
attributed.append(NSAttributedString(""))
}
}
lazy var currentFont = if attributed.length == 0 {
font
} else {
attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? font
}
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":
attributed.addAttribute(.font, value: currentFont.withTraits(.traitItalic)!, range: attributed.fullRange)
case "strong", "b":
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: monospaceFont, range: attributed.fullRange)
case "pre":
attributed.append(NSAttributedString(string: "\n\n"))
attributed.addAttribute(.font, value: monospaceFont, range: attributed.fullRange)
case "blockquote":
let paragraphStyle = paragraphStyle.mutableCopy() as! NSMutableParagraphStyle
paragraphStyle.headIndent = 32
paragraphStyle.firstLineHeadIndent = 32
attributed.addAttributes([
.font: currentFont.withTraits(.traitItalic)!,
.paragraphStyle: paragraphStyle,
], range: attributed.fullRange)
default:
break
}
return attributed
default:
return nil
}
}
private func attributedTextForList(_ element: Element, usePreformattedText: Bool) -> NSAttributedString {
let list = element.tagName() == "ol" ? OrderedNumberTextList(markerFormat: .decimal, options: 0) : NSTextList(markerFormat: .disc, options: 0)
let paragraphStyle = paragraphStyle.mutableCopy() as! NSMutableParagraphStyle
// I don't like that I can't just use paragraphStyle.textLists, because it makes the list markers
// not use the monospace digit font (it seems to just use whatever font attribute is set for the whole thing),
// and it doesn't right align the list markers.
// Unfortunately, doing it manually means the list markers are incldued in the selectable text.
paragraphStyle.headIndent = 32
paragraphStyle.firstLineHeadIndent = 0
// Use 2 tab stops, one for the list marker, the second for the content.
paragraphStyle.tabStops = [NSTextTab(textAlignment: .right, location: 28), NSTextTab(textAlignment: .natural, location: 32)]
let str = NSMutableAttributedString(string: "")
var item = 1
for child in element.children() where child.tagName() == "li" {
if let childStr = attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText) {
str.append(NSAttributedString(string: "\t\(list.marker(forItemNumber: item))\t", attributes: [
.font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .monospacedDigitSystemFont(ofSize: 17, weight: .regular)),
]))
str.append(childStr)
str.append(NSAttributedString(string: "\n"))
item += 1
}
}
str.addAttribute(.paragraphStyle, value: paragraphStyle, range: str.fullRange)
return str
}
} }
private class OrderedNumberTextList: NSTextList { extension HTMLConverter {
override func marker(forItemNumber itemNumber: Int) -> String { struct Callbacks: HTMLConversionCallbacks {
"\(super.marker(forItemNumber: itemNumber))." static func makeURL(string: String) -> URL? {
// Converting WebURL to URL is a small but non-trivial expense (since it works by
// serializing the WebURL as a string and then having Foundation parse it again),
// so, if available, use the system parser which doesn't require another round trip.
if #available(iOS 16.0, macOS 13.0, *),
let url = try? URL.ParseStrategy().parse(string) {
url
} else if let web = WebURL(string),
let url = URL(web) {
url
} else {
URL(string: string)
}
}
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
guard name == "span" else {
return .default
}
let clazz = attributes.attributeValue(for: "class")
if clazz == "invisible" {
return .skip
} else if clazz == "ellipsis" {
return .replace("")
} else {
return .default
}
}
} }
} }

View File

@ -26,11 +26,9 @@ struct ComposeReplyContentView: UIViewRepresentable {
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.adjustsFontForContentSizeCategory = true view.adjustsFontForContentSizeCategory = true
view.defaultFont = TimelineStatusCollectionViewCell.contentFont
view.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
view.overrideMastodonController = mastodonController view.overrideMastodonController = mastodonController
view.setTextFrom(status: status) view.attributedText = TimelineStatusCollectionViewCell.htmlConverter.convert(status.content)
return view return view
} }

View File

@ -34,8 +34,6 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
displayNameLabel.adjustsFontForContentSizeCategory = true displayNameLabel.adjustsFontForContentSizeCategory = true
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
noteTextView.adjustsFontForContentSizeCategory = true noteTextView.adjustsFontForContentSizeCategory = true
noteTextView.textContainer.lineBreakMode = .byTruncatingTail noteTextView.textContainer.lineBreakMode = .byTruncatingTail
noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4) noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
@ -60,7 +58,7 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
displayNameLabel.updateForAccountDisplayName(account: account) displayNameLabel.updateForAccountDisplayName(account: account)
noteTextView.setTextFromHtml(account.note) noteTextView.setBodyTextFromHTML(account.note)
noteTextView.setEmojis(account.emojis, identifier: account.id) noteTextView.setEmojis(account.emojis, identifier: account.id)
avatarImageView.image = nil avatarImageView.image = nil

View File

@ -54,8 +54,6 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
usernameLabel.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 15, weight: .light)) usernameLabel.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 15, weight: .light))
usernameLabel.adjustsFontForContentSizeCategory = true usernameLabel.adjustsFontForContentSizeCategory = true
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
noteTextView.adjustsFontForContentSizeCategory = true noteTextView.adjustsFontForContentSizeCategory = true
noteTextView.textContainer.lineBreakMode = .byTruncatingTail noteTextView.textContainer.lineBreakMode = .byTruncatingTail
@ -86,7 +84,7 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
avatarImageView.update(for: account.avatar) avatarImageView.update(for: account.avatar)
headerImageView.update(for: account.header) headerImageView.update(for: account.header)
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
noteTextView.setTextFromHtml(account.note) noteTextView.setBodyTextFromHTML(account.note)
var config = UIButton.Configuration.plain() var config = UIButton.Configuration.plain()
config.image = source.image config.image = source.image

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import WebURLFoundationExtras import WebURLFoundationExtras
import SwiftSoup import HTMLStreamer
class TrendingLinkCardCollectionViewCell: UICollectionViewCell { class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
@ -78,8 +78,8 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines) let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines)
providerLabel.text = provider providerLabel.text = provider
let description = try! SwiftSoup.parseBodyFragment(card.description).text() var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self)
descriptionLabel.text = description descriptionLabel.text = converter.convert(html: card.description)
descriptionLabel.isHidden = description.isEmpty descriptionLabel.isHidden = description.isEmpty
let sorted = card.history!.sorted(by: { $0.day < $1.day }) let sorted = card.history!.sorted(by: { $0.day < $1.day })

View File

@ -23,10 +23,7 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.filterer = Filterer(mastodonController: mastodonController, context: .public) self.filterer = Filterer(mastodonController: mastodonController, context: .public, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import SwiftSoup import HTMLStreamer
class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell { class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
@ -161,9 +161,8 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
actionLabel.setEmojis(pairs: people.map { ($0.displayOrUserName, $0.emojis) }, identifier: group.id) actionLabel.setEmojis(pairs: people.map { ($0.displayOrUserName, $0.emojis) }, identifier: group.id)
// todo: use htmlconverter var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self)
let doc = try! SwiftSoup.parseBodyFragment(status.content) statusContentLabel.text = converter.convert(html: status.content)
statusContentLabel.text = try! doc.text()
} }
@objc private func updateUIForPreferences() { @objc private func updateUIForPreferences() {

View File

@ -34,10 +34,7 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
self.allowedTypes = allowedTypes self.allowedTypes = allowedTypes
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.filterer = Filterer(mastodonController: mastodonController, context: .notifications) self.filterer = Filterer(mastodonController: mastodonController, context: .notifications, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import SwiftSoup import HTMLStreamer
class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell { class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell {
@ -124,9 +124,8 @@ class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell {
updateTimestamp() updateTimestamp()
updateDisplayName(account: account) updateDisplayName(account: account)
// todo: use htmlconverter var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self)
let doc = try! SwiftSoup.parseBodyFragment(status.content) contentLabel.text = converter.convert(html: status.content)
contentLabel.text = try! doc.text()
pollView.mastodonController = mastodonController pollView.mastodonController = mastodonController
pollView.delegate = delegate pollView.delegate = delegate

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import SwiftSoup import HTMLStreamer
class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell { class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell {
@ -120,9 +120,8 @@ class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell {
updateTimestamp() updateTimestamp()
updateDisplayName(account: account) updateDisplayName(account: account)
// todo: use htmlconverter var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self)
let doc = try! SwiftSoup.parseBodyFragment(status.content) contentLabel.text = converter.convert(html: status.content)
contentLabel.text = try! doc.text()
} }
@objc private func updateUIForPreferences() { @objc private func updateUIForPreferences() {

View File

@ -60,31 +60,6 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
^[[SwiftSoup](https://github.com/scinfu/swiftsoup)](headingLevel: 2)
Copyright (c) 2016 Nabil Chatbi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Symbols
Symbol outline not available for this file
To inspect a symbol, try clicking on the symbol directly in the code view.
Code navigation supports a limited number of languages. See which languages are supported.
^[[swift-url](https://github.com/karwa/swift-url)](headingLevel: 2) ^[[swift-url](https://github.com/karwa/swift-url)](headingLevel: 2)
Apache License Apache License

View File

@ -41,10 +41,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
self.kind = kind self.kind = kind
self.owner = owner self.owner = owner
self.mastodonController = owner.mastodonController self.mastodonController = owner.mastodonController
self.filterer = Filterer(mastodonController: mastodonController, context: .account) self.filterer = Filterer(mastodonController: mastodonController, context: .account, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)

View File

@ -7,9 +7,13 @@
// //
import SwiftUI import SwiftUI
import SwiftSoup
private var converter = HTMLConverter() private var converter = HTMLConverter(
font: .preferredFont(forTextStyle: .body),
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
color: .label,
paragraphStyle: .default
)
struct ReportStatusView: View { struct ReportStatusView: View {
let status: StatusMO let status: StatusMO

View File

@ -70,15 +70,12 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
$0.setContentHuggingPriority(.defaultLow, for: .vertical) $0.setContentHuggingPriority(.defaultLow, for: .vertical)
} }
private let contentTextView = StatusEditContentTextView().configure { private let contentTextView = ContentTextView().configure {
$0.adjustsFontForContentSizeCategory = true $0.adjustsFontForContentSizeCategory = true
$0.isScrollEnabled = false $0.isScrollEnabled = false
$0.backgroundColor = nil $0.backgroundColor = nil
$0.isEditable = false $0.isEditable = false
$0.isSelectable = false $0.isSelectable = false
$0.defaultFont = TimelineStatusCollectionViewCell.contentFont
$0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
$0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
} }
private let cardView = StatusCardView().configure { private let cardView = StatusCardView().configure {
@ -191,7 +188,8 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt) timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt)
contentTextView.setTextFrom(edit: edit, index: index) contentTextView.attributedText = TimelineStatusCollectionViewCell.htmlConverter.convert(edit.content)
contentTextView.setEmojis(edit.emojis, identifier: index)
contentTextView.navigationDelegate = delegate contentTextView.navigationDelegate = delegate
attachmentsView.delegate = self attachmentsView.delegate = self
attachmentsView.updateUI(attachments: edit.attachments) attachmentsView.updateUI(attachments: edit.attachments)

View File

@ -1,22 +0,0 @@
//
// StatusEditContentTextView.swift
// Tusker
//
// Created by Shadowfacts on 5/11/23.
// Copyright © 2023 Shadowfacts. All rights reserved.
//
import UIKit
import Pachyderm
import WebURL
class StatusEditContentTextView: ContentTextView {
func setTextFrom(edit: StatusEdit, index: Int) {
setTextFromHtml(edit.content)
setEmojis(edit.emojis, identifier: index)
}
// mention links aren't included in the edit content, nothing else to do
}

View File

@ -30,7 +30,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
let timeline: Timeline let timeline: Timeline
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
let filterer: Filterer private let filterer: Filterer
var persistsState = false var persistsState = false
@ -59,10 +59,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
default: default:
filterContext = .public filterContext = .public
} }
self.filterer = Filterer(mastodonController: mastodonController, context: filterContext) self.filterer = Filterer(mastodonController: mastodonController, context: filterContext, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)

View File

@ -7,7 +7,7 @@
// //
import UIKit import UIKit
import SwiftSoup import HTMLStreamer
class AccountCollectionViewCell: UICollectionViewListCell { class AccountCollectionViewCell: UICollectionViewListCell {
@ -134,8 +134,8 @@ class AccountCollectionViewCell: UICollectionViewListCell {
displayNameLabel.setEmojis(account.emojis, identifier: account.id) displayNameLabel.setEmojis(account.emojis, identifier: account.id)
} }
let doc = try! SwiftSoup.parseBodyFragment(account.note) var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self)
noteLabel.text = try! doc.text() noteLabel.text = converter.convert(html: account.note)
noteLabel.setEmojis(account.emojis, identifier: account.id) noteLabel.setEmojis(account.emojis, identifier: account.id)
} }

View File

@ -10,6 +10,13 @@ import UIKit
class ConfirmReblogStatusPreviewView: UIView { class ConfirmReblogStatusPreviewView: UIView {
private static let htmlConverter = HTMLConverter(
font: .preferredFont(forTextStyle: .caption2),
monospaceFont: UIFontMetrics(forTextStyle: .caption2).scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
color: .label,
paragraphStyle: .default
)
private var avatarTask: Task<Void, Error>? private var avatarTask: Task<Void, Error>?
init(status: StatusMO) { init(status: StatusMO) {
@ -60,17 +67,13 @@ class ConfirmReblogStatusPreviewView: UIView {
vStack.addArrangedSubview(displayNameLabel) vStack.addArrangedSubview(displayNameLabel)
let contentView = StatusContentTextView() let contentView = StatusContentTextView()
contentView.defaultFont = .preferredFont(forTextStyle: .caption2)
contentView.monospaceFont = UIFontMetrics(forTextStyle: .caption2).scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
contentView.isUserInteractionEnabled = false contentView.isUserInteractionEnabled = false
contentView.isScrollEnabled = false contentView.isScrollEnabled = false
contentView.backgroundColor = nil contentView.backgroundColor = nil
contentView.textContainerInset = .zero contentView.textContainerInset = .zero
contentView.adjustsFontForContentSizeCategory = true contentView.adjustsFontForContentSizeCategory = true
// remove the extra line spacing applied by StatusContentTextView because, since we're using a smaller font, the regular 2pt looks big
contentView.paragraphStyle = .default
// TODO: line limit // TODO: line limit
contentView.setTextFrom(status: status) contentView.setTextFrom(status: status, content: ConfirmReblogStatusPreviewView.htmlConverter.convert(status.content))
contentView.translatesAutoresizingMaskIntoConstraints = false contentView.translatesAutoresizingMaskIntoConstraints = false
vStack.addArrangedSubview(contentView) vStack.addArrangedSubview(contentView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([

View File

@ -7,7 +7,6 @@
// //
import UIKit import UIKit
import SwiftSoup
import Pachyderm import Pachyderm
import SafariServices import SafariServices
import WebURL import WebURL
@ -22,31 +21,20 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
weak var navigationDelegate: TuskerNavigationDelegate? weak var navigationDelegate: TuskerNavigationDelegate?
weak var overrideMastodonController: MastodonController? weak var overrideMastodonController: MastodonController?
var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController } var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController }
private(set) var htmlConverter = HTMLConverter()
var defaultFont: UIFont {
_read { yield htmlConverter.font }
_modify { yield &htmlConverter.font }
}
var monospaceFont: UIFont {
_read { yield htmlConverter.monospaceFont }
_modify { yield &htmlConverter.monospaceFont }
}
var defaultColor: UIColor {
_read { yield htmlConverter.color }
_modify { yield &htmlConverter.color }
}
var paragraphStyle: NSParagraphStyle {
_read { yield htmlConverter.paragraphStyle }
_modify { yield &htmlConverter.paragraphStyle }
}
private static let defaultBodyHTMLConverter = HTMLConverter(
font: .preferredFont(forTextStyle: .body),
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
color: .label,
paragraphStyle: .default
)
private(set) var hasEmojis = false private(set) var hasEmojis = false
var emojiIdentifier: AnyHashable? var emojiIdentifier: AnyHashable?
var emojiRequests: [ImageCache.Request] = [] var emojiRequests: [ImageCache.Request] = []
var emojiFont: UIFont { defaultFont } var emojiFont: UIFont = .preferredFont(forTextStyle: .body)
var emojiTextColor: UIColor { defaultColor } var emojiTextColor: UIColor = .label
// The link range currently being previewed // The link range currently being previewed
private var currentPreviewedLinkRange: NSRange? private var currentPreviewedLinkRange: NSRange?
@ -120,8 +108,8 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
} }
// MARK: - HTML Parsing // MARK: - HTML Parsing
func setTextFromHtml(_ html: String) { func setBodyTextFromHTML(_ html: String) {
self.attributedText = htmlConverter.convert(html) self.attributedText = ContentTextView.defaultBodyHTMLConverter.convert(html)
} }
// MARK: - Interaction // MARK: - Interaction

View File

@ -34,8 +34,6 @@ class InstanceTableViewCell: UITableViewCell {
adultLabel.layer.masksToBounds = true adultLabel.layer.masksToBounds = true
adultLabel.layer.cornerRadius = 0.5 * adultLabel.bounds.height adultLabel.layer.cornerRadius = 0.5 * adultLabel.bounds.height
descriptionTextView.defaultFont = .preferredFont(forTextStyle: .body)
descriptionTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
descriptionTextView.adjustsFontForContentSizeCategory = true descriptionTextView.adjustsFontForContentSizeCategory = true
} }
@ -49,7 +47,7 @@ class InstanceTableViewCell: UITableViewCell {
domainLabel.text = instance.domain domainLabel.text = instance.domain
adultLabel.isHidden = instance.category != "adult" adultLabel.isHidden = instance.category != "adult"
descriptionTextView.setTextFromHtml(instance.description) descriptionTextView.setBodyTextFromHTML(instance.description)
updateThumbnail(url: instance.proxiedThumbnailURL) updateThumbnail(url: instance.proxiedThumbnailURL)
} }
@ -59,7 +57,7 @@ class InstanceTableViewCell: UITableViewCell {
domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri
adultLabel.isHidden = true adultLabel.isHidden = true
descriptionTextView.setTextFromHtml(instance.shortDescription ?? instance.description) descriptionTextView.setBodyTextFromHTML(instance.shortDescription ?? instance.description)
if let thumbnail = instance.thumbnail { if let thumbnail = instance.thumbnail {
updateThumbnail(url: thumbnail) updateThumbnail(url: thumbnail)

View File

@ -14,12 +14,12 @@ import SafariServices
class ProfileFieldValueView: UIView { class ProfileFieldValueView: UIView {
weak var navigationDelegate: TuskerNavigationDelegate? weak var navigationDelegate: TuskerNavigationDelegate?
private static let converter: HTMLConverter = { private static let converter = HTMLConverter(
var converter = HTMLConverter() font: .preferredFont(forTextStyle: .body),
converter.font = .preferredFont(forTextStyle: .body) monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
converter.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)) color: .label,
return converter paragraphStyle: .default
}() )
private let account: AccountMO private let account: AccountMO
private let field: Account.Field private let field: Account.Field

View File

@ -95,8 +95,6 @@ class ProfileHeaderView: UIView {
relationshipLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14)) relationshipLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14))
relationshipLabel.adjustsFontForContentSizeCategory = true relationshipLabel.adjustsFontForContentSizeCategory = true
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
noteTextView.adjustsFontForContentSizeCategory = true noteTextView.adjustsFontForContentSizeCategory = true
pagesSegmentedControl = ScrollingSegmentedControl(frame: .zero) pagesSegmentedControl = ScrollingSegmentedControl(frame: .zero)
@ -140,7 +138,7 @@ class ProfileHeaderView: UIView {
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForProfile(accountID: accountID, source: .view(moreButton), fetchRelationship: false) ?? []) moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForProfile(accountID: accountID, source: .view(moreButton), fetchRelationship: false) ?? [])
noteTextView.navigationDelegate = delegate noteTextView.navigationDelegate = delegate
noteTextView.setTextFromHtml(account.note) noteTextView.setBodyTextFromHTML(account.note)
noteTextView.setEmojis(account.emojis, identifier: account.id) noteTextView.setEmojis(account.emojis, identifier: account.id)
if accountID == mastodonController.account?.id { if accountID == mastodonController.account?.id {

View File

@ -17,6 +17,13 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle
static let metaFont = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 15)) static let metaFont = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 15))
private static let htmlConverter = HTMLConverter(
font: ConversationMainStatusCollectionViewCell.contentFont,
monospaceFont: ConversationMainStatusCollectionViewCell.monospaceFont,
color: .label,
paragraphStyle: ConversationMainStatusCollectionViewCell.contentParagraphStyle
)
static let dateFormatter: DateFormatter = { static let dateFormatter: DateFormatter = {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateStyle = .medium formatter.dateStyle = .medium
@ -132,9 +139,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
$0.backgroundColor = nil $0.backgroundColor = nil
$0.isEditable = false $0.isEditable = false
$0.isSelectable = true $0.isSelectable = true
$0.defaultFont = ConversationMainStatusCollectionViewCell.contentFont $0.emojiFont = ConversationMainStatusCollectionViewCell.contentFont
$0.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont
$0.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle
$0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber] $0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber]
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
$0.dataDetectorTypes.formUnion([.money, .physicalValue]) $0.dataDetectorTypes.formUnion([.money, .physicalValue])
@ -381,10 +386,9 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
self.statusID = statusID self.statusID = statusID
self.statusState = state self.statusState = state
let attributedTranslatedContent: NSAttributedString? = translation.map { let html = translation?.content ?? status.content
contentTextView.htmlConverter.convert($0.content) let attributedContent = ConversationMainStatusCollectionViewCell.htmlConverter.convert(html)
} doUpdateUI(status: status, content: attributedContent)
doUpdateUI(status: status, precomputedContent: attributedTranslatedContent)
if !status.spoilerText.isEmpty, if !status.spoilerText.isEmpty,
let translated = translation?.spoilerText { let translated = translation?.spoilerText {

View File

@ -10,7 +10,7 @@ import UIKit
import Pachyderm import Pachyderm
import SafariServices import SafariServices
import WebURLFoundationExtras import WebURLFoundationExtras
import SwiftSoup import HTMLStreamer
class StatusCardView: UIView { class StatusCardView: UIView {
@ -191,7 +191,8 @@ class StatusCardView: UIView {
titleLabel.text = title titleLabel.text = title
titleLabel.isHidden = title.isEmpty titleLabel.isHidden = title.isEmpty
let description = try! SwiftSoup.parseBodyFragment(card.description).text() var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self)
let description = converter.convert(html: card.description)
descriptionLabel.text = description descriptionLabel.text = description
descriptionLabel.isHidden = description.isEmpty descriptionLabel.isHidden = description.isEmpty

View File

@ -88,13 +88,13 @@ extension StatusCollectionViewCell {
.store(in: &cancellables) .store(in: &cancellables)
} }
func doUpdateUI(status: StatusMO, precomputedContent: NSAttributedString? = nil) { func doUpdateUI(status: StatusMO, content: NSAttributedString) {
statusID = status.id statusID = status.id
accountID = status.account.id accountID = status.account.id
updateAccountUI(account: status.account) updateAccountUI(account: status.account)
contentTextView.setTextFrom(status: status, precomputed: precomputedContent) contentTextView.setTextFrom(status: status, content: content)
contentTextView.navigationDelegate = delegate contentTextView.navigationDelegate = delegate
self.updateAttachmentsUI(status: status) self.updateAttachmentsUI(status: status)
pollView.isHidden = status.poll == nil pollView.isHidden = status.poll == nil

View File

@ -20,6 +20,13 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
static let monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 16, weight: .regular)) static let monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 16, weight: .regular))
static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle
static let htmlConverter = HTMLConverter(
font: TimelineStatusCollectionViewCell.contentFont,
monospaceFont: TimelineStatusCollectionViewCell.monospaceFont,
color: .label,
paragraphStyle: TimelineStatusCollectionViewCell.contentParagraphStyle
)
private static let timelineReasonIconSize: CGFloat = 25 private static let timelineReasonIconSize: CGFloat = 25
// MARK: Subviews // MARK: Subviews
@ -201,9 +208,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
$0.backgroundColor = nil $0.backgroundColor = nil
$0.isEditable = false $0.isEditable = false
$0.isSelectable = false $0.isSelectable = false
$0.defaultFont = TimelineStatusCollectionViewCell.contentFont $0.emojiFont = TimelineStatusCollectionViewCell.contentFont
$0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
$0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
} }
let cardView = StatusCardView().configure { let cardView = StatusCardView().configure {
@ -610,7 +615,8 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
mainContainerTopToReblogLabelConstraint.isActive = true mainContainerTopToReblogLabelConstraint.isActive = true
} }
doUpdateUI(status: status, precomputedContent: precomputedContent) let content = precomputedContent ?? TimelineStatusCollectionViewCell.htmlConverter.convert(status.content)
doUpdateUI(status: status, content: content)
doUpdateTimestamp(status: status) doUpdateTimestamp(status: status)
timestampLabel.isHidden = showPinned timestampLabel.isHidden = showPinned

View File

@ -14,13 +14,9 @@ class StatusContentTextView: ContentTextView {
private var statusID: String? private var statusID: String?
func setTextFrom(status: some StatusProtocol, precomputed attributedText: NSAttributedString? = nil) { func setTextFrom(status: some StatusProtocol, content attributedText: NSAttributedString) {
statusID = status.id statusID = status.id
if let attributedText { self.attributedText = attributedText
self.attributedText = attributedText
} else {
setTextFromHtml(status.content)
}
setEmojis(status.emojis, identifier: status.id) setEmojis(status.emojis, identifier: status.id)
} }