Compare commits
7 Commits
5a4323067a
...
fb5581ae67
Author | SHA1 | Date |
---|---|---|
Shadowfacts | fb5581ae67 | |
Shadowfacts | cd01d2f8c3 | |
Shadowfacts | 65c3c8026d | |
Shadowfacts | 534f83e716 | |
Shadowfacts | 93c859a3c4 | |
Shadowfacts | 4d183fe0b2 | |
Shadowfacts | fd72390a22 |
|
@ -1,5 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
## 2024.1 (111)
|
||||
This build contains a complete rewrite of the HTML parsing pipeline for displaying posts. If you notice any issues with how post text appears—especially when it differs from on the web—please report it!
|
||||
|
||||
## 2023.8 (110)
|
||||
Bugfixes:
|
||||
- Fix potential crash after deleting List on Explore screen
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
D608470F2A245D1F00C17380 /* ActiveInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D608470E2A245D1F00C17380 /* ActiveInstance.swift */; };
|
||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.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 */; };
|
||||
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.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 */; };
|
||||
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.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 */; };
|
||||
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -786,10 +784,10 @@
|
|||
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
|
||||
D659F35E2953A212002D944A /* TTTKit in Frameworks */,
|
||||
D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */,
|
||||
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
|
||||
D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */,
|
||||
D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */,
|
||||
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
|
||||
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */,
|
||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
||||
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
|
||||
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */,
|
||||
|
@ -1574,7 +1572,6 @@
|
|||
children = (
|
||||
D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */,
|
||||
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */,
|
||||
D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */,
|
||||
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */,
|
||||
);
|
||||
path = "Status Edit History";
|
||||
|
@ -1699,7 +1696,6 @@
|
|||
);
|
||||
name = Tusker;
|
||||
packageProductDependencies = (
|
||||
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
|
||||
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
|
||||
D674A50827F9128D00BA03AC /* Pachyderm */,
|
||||
D6552366289870790048A653 /* ScreenCorners */,
|
||||
|
@ -1711,6 +1707,7 @@
|
|||
D635237029B78A7D009ED5E7 /* TuskerComponents */,
|
||||
D6BD395829B64426005FFD2B /* ComposeUI */,
|
||||
D6CA6ED129EF6091003EC5DF /* TuskerPreferences */,
|
||||
D60BB3932B30076F00DAEA65 /* HTMLStreamer */,
|
||||
);
|
||||
productName = Tusker;
|
||||
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
||||
|
@ -1821,10 +1818,10 @@
|
|||
);
|
||||
mainGroup = D6D4DDC3212518A000E1C4BB;
|
||||
packageReferences = (
|
||||
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
|
||||
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
|
||||
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
||||
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */,
|
||||
);
|
||||
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -2181,7 +2178,6 @@
|
|||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
||||
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
|
||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
||||
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */,
|
||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
||||
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */,
|
||||
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */,
|
||||
|
@ -2455,6 +2451,7 @@
|
|||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2808,6 +2805,7 @@
|
|||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2827,6 +2825,7 @@
|
|||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -2956,12 +2955,12 @@
|
|||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
|
||||
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/scinfu/SwiftSoup.git";
|
||||
repositoryURL = "https://git.shadowfacts.net/shadowfacts/HTMLStreamer.git";
|
||||
requirement = {
|
||||
kind = upToNextMinorVersion;
|
||||
minimumVersion = 2.3.2;
|
||||
minimumVersion = 0.1.0;
|
||||
};
|
||||
};
|
||||
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
|
||||
|
@ -2991,10 +2990,10 @@
|
|||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
D60CFFDA24A290BA00D00083 /* SwiftSoup */ = {
|
||||
D60BB3932B30076F00DAEA65 /* HTMLStreamer */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||
productName = SwiftSoup;
|
||||
package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */;
|
||||
productName = HTMLStreamer;
|
||||
};
|
||||
D61ABEFB28F105DE00B29151 /* Pachyderm */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
import LinkPresentation
|
||||
import SwiftSoup
|
||||
import HTMLStreamer
|
||||
|
||||
class StatusActivityItemSource: NSObject, UIActivityItemSource {
|
||||
let status: StatusMO
|
||||
|
@ -33,8 +33,8 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource {
|
|||
let metadata = LPLinkMetadata()
|
||||
metadata.originalURL = status.url!
|
||||
metadata.url = status.url!
|
||||
let doc = try! SwiftSoup.parse(status.content)
|
||||
let content = try! doc.text()
|
||||
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||
let content = converter.convert(html: status.content)
|
||||
metadata.title = "\(status.account.displayName): \"\(content)\""
|
||||
if let avatar = status.account.avatar,
|
||||
let data = ImageCache.avatars.getData(avatar),
|
||||
|
|
|
@ -30,20 +30,31 @@ extension NSAttributedString {
|
|||
extension NSMutableAttributedString {
|
||||
|
||||
func trimLeadingCharactersInSet(_ charSet: CharacterSet) {
|
||||
var range = (string as NSString).rangeOfCharacter(from: charSet)
|
||||
|
||||
while range.length != 0 && range.location == 0 {
|
||||
replaceCharacters(in: range, with: "")
|
||||
range = (string as NSString).rangeOfCharacter(from: charSet)
|
||||
var end = string.startIndex
|
||||
while end < string.endIndex && charSet.contains(string.unicodeScalars[end]) {
|
||||
end = string.unicodeScalars.index(after: end)
|
||||
}
|
||||
if end > string.startIndex {
|
||||
let length = string.utf16.distance(from: string.startIndex, to: end)
|
||||
replaceCharacters(in: NSRange(location: 0, length: length), with: "")
|
||||
}
|
||||
}
|
||||
|
||||
func trimTrailingCharactersInSet(_ charSet: CharacterSet) {
|
||||
var range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
|
||||
|
||||
while range.length != 0 && range.length + range.location == length {
|
||||
replaceCharacters(in: range, with: "")
|
||||
range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
|
||||
if string.isEmpty {
|
||||
return
|
||||
}
|
||||
var start = string.index(before: string.endIndex)
|
||||
while start > string.startIndex && charSet.contains(string.unicodeScalars[start]) {
|
||||
start = string.unicodeScalars.index(before: start)
|
||||
}
|
||||
if start < string.endIndex {
|
||||
if start != string.startIndex || !charSet.contains(string.unicodeScalars[start]) {
|
||||
start = string.unicodeScalars.index(after: start)
|
||||
}
|
||||
let location = string.utf16.distance(from: string.startIndex, to: start)
|
||||
let length = string.utf16.distance(from: start, to: string.endIndex)
|
||||
replaceCharacters(in: NSRange(location: location, length: length), with: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ class Filterer {
|
|||
|
||||
var filtersChanged: ((Bool) -> Void)?
|
||||
|
||||
var htmlConverter = HTMLConverter()
|
||||
private var htmlConverter: HTMLConverter
|
||||
private var hasSetup = false
|
||||
private var matchers = [(NSRegularExpression, Result)]()
|
||||
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
|
||||
private var generation = 0
|
||||
|
||||
init(mastodonController: MastodonController, context: FilterV1.Context) {
|
||||
init(mastodonController: MastodonController, context: FilterV1.Context, htmlConverter: HTMLConverter) {
|
||||
self.mastodonController = mastodonController
|
||||
self.context = context
|
||||
self.htmlConverter = htmlConverter
|
||||
self.hideReblogsInTimelines = Preferences.shared.hideReblogsInTimelines
|
||||
self.hideRepliesInTimelines = Preferences.shared.hideRepliesInTimelines
|
||||
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftSoup
|
||||
import HTMLStreamer
|
||||
import WebURL
|
||||
import WebURLFoundationExtras
|
||||
|
||||
struct HTMLConverter {
|
||||
class HTMLConverter {
|
||||
|
||||
static let defaultFont = UIFont.systemFont(ofSize: 17)
|
||||
static let defaultMonospaceFont = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular)
|
||||
|
@ -23,150 +23,47 @@ struct HTMLConverter {
|
|||
return style
|
||||
}()
|
||||
|
||||
var font: UIFont = defaultFont
|
||||
var monospaceFont: UIFont = defaultMonospaceFont
|
||||
var color: UIColor = defaultColor
|
||||
var paragraphStyle: NSParagraphStyle = defaultParagraphStyle
|
||||
private var converter: AttributedStringConverter<Callbacks>
|
||||
|
||||
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 {
|
||||
let doc = try! SwiftSoup.parseBodyFragment(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 {
|
||||
override func marker(forItemNumber itemNumber: Int) -> String {
|
||||
"\(super.marker(forItemNumber: itemNumber))."
|
||||
converter.convert(html: html)
|
||||
}
|
||||
}
|
||||
|
||||
extension HTMLConverter {
|
||||
struct Callbacks: HTMLConversionCallbacks {
|
||||
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 .append("…")
|
||||
} else {
|
||||
return .default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,11 +26,9 @@ struct ComposeReplyContentView: UIViewRepresentable {
|
|||
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
view.adjustsFontForContentSizeCategory = true
|
||||
view.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
||||
view.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||
|
||||
view.overrideMastodonController = mastodonController
|
||||
view.setTextFrom(status: status)
|
||||
view.attributedText = TimelineStatusCollectionViewCell.htmlConverter.convert(status.content)
|
||||
|
||||
return view
|
||||
}
|
||||
|
|
|
@ -34,8 +34,6 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
|||
displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
|
||||
displayNameLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
|
||||
noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
||||
noteTextView.adjustsFontForContentSizeCategory = true
|
||||
noteTextView.textContainer.lineBreakMode = .byTruncatingTail
|
||||
noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
|
||||
|
@ -60,7 +58,7 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
|||
|
||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||
|
||||
noteTextView.setTextFromHtml(account.note)
|
||||
noteTextView.setBodyTextFromHTML(account.note)
|
||||
noteTextView.setEmojis(account.emojis, identifier: account.id)
|
||||
|
||||
avatarImageView.image = nil
|
||||
|
|
|
@ -54,8 +54,6 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
|
|||
usernameLabel.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 15, weight: .light))
|
||||
usernameLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
|
||||
noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
||||
noteTextView.adjustsFontForContentSizeCategory = true
|
||||
noteTextView.textContainer.lineBreakMode = .byTruncatingTail
|
||||
|
||||
|
@ -86,7 +84,7 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
|
|||
avatarImageView.update(for: account.avatar)
|
||||
headerImageView.update(for: account.header)
|
||||
usernameLabel.text = "@\(account.acct)"
|
||||
noteTextView.setTextFromHtml(account.note)
|
||||
noteTextView.setBodyTextFromHTML(account.note)
|
||||
|
||||
var config = UIButton.Configuration.plain()
|
||||
config.image = source.image
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
import WebURLFoundationExtras
|
||||
import SwiftSoup
|
||||
import HTMLStreamer
|
||||
|
||||
class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
||||
|
||||
|
@ -78,8 +78,8 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
|
|||
let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
providerLabel.text = provider
|
||||
|
||||
let description = try! SwiftSoup.parseBodyFragment(card.description).text()
|
||||
descriptionLabel.text = description
|
||||
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||
descriptionLabel.text = converter.convert(html: card.description)
|
||||
descriptionLabel.isHidden = description.isEmpty
|
||||
|
||||
let sorted = card.history!.sorted(by: { $0.day < $1.day })
|
||||
|
|
|
@ -23,10 +23,7 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
|
|||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
self.filterer = Filterer(mastodonController: mastodonController, context: .public)
|
||||
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
|
||||
self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||
self.filterer = Filterer(mastodonController: mastodonController, context: .public, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import SwiftSoup
|
||||
import HTMLStreamer
|
||||
|
||||
class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
|
||||
|
||||
|
@ -161,9 +161,8 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
|
|||
|
||||
actionLabel.setEmojis(pairs: people.map { ($0.displayOrUserName, $0.emojis) }, identifier: group.id)
|
||||
|
||||
// todo: use htmlconverter
|
||||
let doc = try! SwiftSoup.parseBodyFragment(status.content)
|
||||
statusContentLabel.text = try! doc.text()
|
||||
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||
statusContentLabel.text = converter.convert(html: status.content)
|
||||
}
|
||||
|
||||
@objc private func updateUIForPreferences() {
|
||||
|
|
|
@ -34,10 +34,7 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
|||
self.allowedTypes = allowedTypes
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
self.filterer = Filterer(mastodonController: mastodonController, context: .notifications)
|
||||
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
|
||||
self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||
self.filterer = Filterer(mastodonController: mastodonController, context: .notifications, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import SwiftSoup
|
||||
import HTMLStreamer
|
||||
|
||||
class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell {
|
||||
|
||||
|
@ -124,9 +124,8 @@ class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell {
|
|||
updateTimestamp()
|
||||
updateDisplayName(account: account)
|
||||
|
||||
// todo: use htmlconverter
|
||||
let doc = try! SwiftSoup.parseBodyFragment(status.content)
|
||||
contentLabel.text = try! doc.text()
|
||||
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||
contentLabel.text = converter.convert(html: status.content)
|
||||
|
||||
pollView.mastodonController = mastodonController
|
||||
pollView.delegate = delegate
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import SwiftSoup
|
||||
import HTMLStreamer
|
||||
|
||||
class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell {
|
||||
|
||||
|
@ -120,9 +120,8 @@ class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell {
|
|||
updateTimestamp()
|
||||
updateDisplayName(account: account)
|
||||
|
||||
// todo: use htmlconverter
|
||||
let doc = try! SwiftSoup.parseBodyFragment(status.content)
|
||||
contentLabel.text = try! doc.text()
|
||||
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||
contentLabel.text = converter.convert(html: status.content)
|
||||
}
|
||||
|
||||
@objc private func updateUIForPreferences() {
|
||||
|
|
|
@ -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
|
||||
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)
|
||||
|
||||
Apache License
|
||||
|
|
|
@ -41,10 +41,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
|||
self.kind = kind
|
||||
self.owner = owner
|
||||
self.mastodonController = owner.mastodonController
|
||||
self.filterer = Filterer(mastodonController: mastodonController, context: .account)
|
||||
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
|
||||
self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||
self.filterer = Filterer(mastodonController: mastodonController, context: .account, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
|
|
|
@ -7,9 +7,13 @@
|
|||
//
|
||||
|
||||
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 {
|
||||
let status: StatusMO
|
||||
|
|
|
@ -70,15 +70,12 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
|||
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
}
|
||||
|
||||
private let contentTextView = StatusEditContentTextView().configure {
|
||||
private let contentTextView = ContentTextView().configure {
|
||||
$0.adjustsFontForContentSizeCategory = true
|
||||
$0.isScrollEnabled = false
|
||||
$0.backgroundColor = nil
|
||||
$0.isEditable = false
|
||||
$0.isSelectable = false
|
||||
$0.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
||||
$0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||
$0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||
}
|
||||
|
||||
private let cardView = StatusCardView().configure {
|
||||
|
@ -191,7 +188,8 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
|||
|
||||
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
|
||||
attachmentsView.delegate = self
|
||||
attachmentsView.updateUI(attachments: edit.attachments)
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -30,7 +30,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
|
||||
let timeline: Timeline
|
||||
weak var mastodonController: MastodonController!
|
||||
let filterer: Filterer
|
||||
private let filterer: Filterer
|
||||
|
||||
var persistsState = false
|
||||
|
||||
|
@ -59,10 +59,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
|||
default:
|
||||
filterContext = .public
|
||||
}
|
||||
self.filterer = Filterer(mastodonController: mastodonController, context: filterContext)
|
||||
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
|
||||
self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||
self.filterer = Filterer(mastodonController: mastodonController, context: filterContext, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftSoup
|
||||
import HTMLStreamer
|
||||
|
||||
class AccountCollectionViewCell: UICollectionViewListCell {
|
||||
|
||||
|
@ -134,8 +134,8 @@ class AccountCollectionViewCell: UICollectionViewListCell {
|
|||
displayNameLabel.setEmojis(account.emojis, identifier: account.id)
|
||||
}
|
||||
|
||||
let doc = try! SwiftSoup.parseBodyFragment(account.note)
|
||||
noteLabel.text = try! doc.text()
|
||||
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||
noteLabel.text = converter.convert(html: account.note)
|
||||
noteLabel.setEmojis(account.emojis, identifier: account.id)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,13 @@ import UIKit
|
|||
|
||||
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>?
|
||||
|
||||
init(status: StatusMO) {
|
||||
|
@ -60,17 +67,13 @@ class ConfirmReblogStatusPreviewView: UIView {
|
|||
vStack.addArrangedSubview(displayNameLabel)
|
||||
|
||||
let contentView = StatusContentTextView()
|
||||
contentView.defaultFont = .preferredFont(forTextStyle: .caption2)
|
||||
contentView.monospaceFont = UIFontMetrics(forTextStyle: .caption2).scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
||||
contentView.isUserInteractionEnabled = false
|
||||
contentView.isScrollEnabled = false
|
||||
contentView.backgroundColor = nil
|
||||
contentView.textContainerInset = .zero
|
||||
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
|
||||
contentView.setTextFrom(status: status)
|
||||
contentView.setTextFrom(status: status, content: ConfirmReblogStatusPreviewView.htmlConverter.convert(status.content))
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
vStack.addArrangedSubview(contentView)
|
||||
NSLayoutConstraint.activate([
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftSoup
|
||||
import Pachyderm
|
||||
import SafariServices
|
||||
import WebURL
|
||||
|
@ -23,30 +22,19 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
|||
weak var overrideMastodonController: MastodonController?
|
||||
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
|
||||
|
||||
var emojiIdentifier: AnyHashable?
|
||||
var emojiRequests: [ImageCache.Request] = []
|
||||
var emojiFont: UIFont { defaultFont }
|
||||
var emojiTextColor: UIColor { defaultColor }
|
||||
var emojiFont: UIFont = .preferredFont(forTextStyle: .body)
|
||||
var emojiTextColor: UIColor = .label
|
||||
|
||||
// The link range currently being previewed
|
||||
private var currentPreviewedLinkRange: NSRange?
|
||||
|
@ -120,8 +108,8 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
|||
}
|
||||
|
||||
// MARK: - HTML Parsing
|
||||
func setTextFromHtml(_ html: String) {
|
||||
self.attributedText = htmlConverter.convert(html)
|
||||
func setBodyTextFromHTML(_ html: String) {
|
||||
self.attributedText = ContentTextView.defaultBodyHTMLConverter.convert(html)
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
|
|
@ -34,8 +34,6 @@ class InstanceTableViewCell: UITableViewCell {
|
|||
adultLabel.layer.masksToBounds = true
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -49,7 +47,7 @@ class InstanceTableViewCell: UITableViewCell {
|
|||
|
||||
domainLabel.text = instance.domain
|
||||
adultLabel.isHidden = instance.category != "adult"
|
||||
descriptionTextView.setTextFromHtml(instance.description)
|
||||
descriptionTextView.setBodyTextFromHTML(instance.description)
|
||||
updateThumbnail(url: instance.proxiedThumbnailURL)
|
||||
}
|
||||
|
||||
|
@ -59,7 +57,7 @@ class InstanceTableViewCell: UITableViewCell {
|
|||
|
||||
domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri
|
||||
adultLabel.isHidden = true
|
||||
descriptionTextView.setTextFromHtml(instance.shortDescription ?? instance.description)
|
||||
descriptionTextView.setBodyTextFromHTML(instance.shortDescription ?? instance.description)
|
||||
|
||||
if let thumbnail = instance.thumbnail {
|
||||
updateThumbnail(url: thumbnail)
|
||||
|
|
|
@ -14,12 +14,12 @@ import SafariServices
|
|||
class ProfileFieldValueView: UIView {
|
||||
weak var navigationDelegate: TuskerNavigationDelegate?
|
||||
|
||||
private static let converter: HTMLConverter = {
|
||||
var converter = HTMLConverter()
|
||||
converter.font = .preferredFont(forTextStyle: .body)
|
||||
converter.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
||||
return converter
|
||||
}()
|
||||
private static let converter = HTMLConverter(
|
||||
font: .preferredFont(forTextStyle: .body),
|
||||
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
||||
color: .label,
|
||||
paragraphStyle: .default
|
||||
)
|
||||
|
||||
private let account: AccountMO
|
||||
private let field: Account.Field
|
||||
|
|
|
@ -95,8 +95,6 @@ class ProfileHeaderView: UIView {
|
|||
relationshipLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14))
|
||||
relationshipLabel.adjustsFontForContentSizeCategory = true
|
||||
|
||||
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
|
||||
noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
||||
noteTextView.adjustsFontForContentSizeCategory = true
|
||||
|
||||
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) ?? [])
|
||||
|
||||
noteTextView.navigationDelegate = delegate
|
||||
noteTextView.setTextFromHtml(account.note)
|
||||
noteTextView.setBodyTextFromHTML(account.note)
|
||||
noteTextView.setEmojis(account.emojis, identifier: account.id)
|
||||
|
||||
if accountID == mastodonController.account?.id {
|
||||
|
|
|
@ -17,6 +17,13 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle
|
||||
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 = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
|
@ -132,9 +139,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
$0.backgroundColor = nil
|
||||
$0.isEditable = false
|
||||
$0.isSelectable = true
|
||||
$0.defaultFont = ConversationMainStatusCollectionViewCell.contentFont
|
||||
$0.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont
|
||||
$0.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle
|
||||
$0.emojiFont = ConversationMainStatusCollectionViewCell.contentFont
|
||||
$0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber]
|
||||
if #available(iOS 16.0, *) {
|
||||
$0.dataDetectorTypes.formUnion([.money, .physicalValue])
|
||||
|
@ -381,10 +386,13 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
|||
self.statusID = statusID
|
||||
self.statusState = state
|
||||
|
||||
let attributedTranslatedContent: NSAttributedString? = translation.map {
|
||||
contentTextView.htmlConverter.convert($0.content)
|
||||
}
|
||||
doUpdateUI(status: status, precomputedContent: attributedTranslatedContent)
|
||||
let html = translation?.content ?? status.content
|
||||
let attributedContent = ConversationMainStatusCollectionViewCell.htmlConverter.convert(html)
|
||||
let collapsedContent = NSMutableAttributedString(attributedString: attributedContent)
|
||||
collapsedContent.collapseWhitespace()
|
||||
collapsedContent.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
||||
collapsedContent.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
||||
doUpdateUI(status: status, content: collapsedContent)
|
||||
|
||||
if !status.spoilerText.isEmpty,
|
||||
let translated = translation?.spoilerText {
|
||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
|||
import Pachyderm
|
||||
import SafariServices
|
||||
import WebURLFoundationExtras
|
||||
import SwiftSoup
|
||||
import HTMLStreamer
|
||||
|
||||
class StatusCardView: UIView {
|
||||
|
||||
|
@ -191,7 +191,8 @@ class StatusCardView: UIView {
|
|||
titleLabel.text = title
|
||||
titleLabel.isHidden = title.isEmpty
|
||||
|
||||
let description = try! SwiftSoup.parseBodyFragment(card.description).text()
|
||||
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||
let description = converter.convert(html: card.description)
|
||||
descriptionLabel.text = description
|
||||
descriptionLabel.isHidden = description.isEmpty
|
||||
|
||||
|
|
|
@ -88,13 +88,13 @@ extension StatusCollectionViewCell {
|
|||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func doUpdateUI(status: StatusMO, precomputedContent: NSAttributedString? = nil) {
|
||||
func doUpdateUI(status: StatusMO, content: NSAttributedString) {
|
||||
statusID = status.id
|
||||
accountID = status.account.id
|
||||
|
||||
updateAccountUI(account: status.account)
|
||||
|
||||
contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
|
||||
contentTextView.setTextFrom(status: status, content: content)
|
||||
contentTextView.navigationDelegate = delegate
|
||||
self.updateAttachmentsUI(status: status)
|
||||
pollView.isHidden = status.poll == nil
|
||||
|
|
|
@ -20,6 +20,13 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
static let monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 16, weight: .regular))
|
||||
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
|
||||
|
||||
// MARK: Subviews
|
||||
|
@ -201,9 +208,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
$0.backgroundColor = nil
|
||||
$0.isEditable = false
|
||||
$0.isSelectable = false
|
||||
$0.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
||||
$0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
||||
$0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
||||
$0.emojiFont = TimelineStatusCollectionViewCell.contentFont
|
||||
}
|
||||
|
||||
let cardView = StatusCardView().configure {
|
||||
|
@ -610,7 +615,12 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
|||
mainContainerTopToReblogLabelConstraint.isActive = true
|
||||
}
|
||||
|
||||
doUpdateUI(status: status, precomputedContent: precomputedContent)
|
||||
let content = precomputedContent ?? TimelineStatusCollectionViewCell.htmlConverter.convert(status.content)
|
||||
let collapsedContent = NSMutableAttributedString(attributedString: content)
|
||||
collapsedContent.collapseWhitespace()
|
||||
collapsedContent.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
||||
collapsedContent.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
||||
doUpdateUI(status: status, content: collapsedContent)
|
||||
|
||||
doUpdateTimestamp(status: status)
|
||||
timestampLabel.isHidden = showPinned
|
||||
|
|
|
@ -14,13 +14,9 @@ class StatusContentTextView: ContentTextView {
|
|||
|
||||
private var statusID: String?
|
||||
|
||||
func setTextFrom(status: some StatusProtocol, precomputed attributedText: NSAttributedString? = nil) {
|
||||
func setTextFrom(status: some StatusProtocol, content attributedText: NSAttributedString) {
|
||||
statusID = status.id
|
||||
if let attributedText {
|
||||
self.attributedText = attributedText
|
||||
} else {
|
||||
setTextFromHtml(status.content)
|
||||
}
|
||||
self.attributedText = attributedText
|
||||
setEmojis(status.emojis, identifier: status.id)
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,36 @@ class AttributedStringHelperTests: XCTestCase {
|
|||
override func tearDown() {
|
||||
}
|
||||
|
||||
func testTrimLeading() {
|
||||
let a = NSMutableAttributedString(string: " a ")
|
||||
a.trimLeadingCharactersInSet(.whitespaces)
|
||||
XCTAssertEqual(a, NSAttributedString(string: "a "))
|
||||
let b = NSMutableAttributedString(string: " ")
|
||||
b.trimLeadingCharactersInSet(.whitespaces)
|
||||
XCTAssertEqual(b, NSAttributedString(string: ""))
|
||||
let c = NSMutableAttributedString(string: "")
|
||||
c.trimLeadingCharactersInSet(.whitespaces)
|
||||
XCTAssertEqual(c, NSAttributedString(string: ""))
|
||||
let d = NSMutableAttributedString(string: "abc")
|
||||
d.trimLeadingCharactersInSet(.whitespaces)
|
||||
XCTAssertEqual(d, NSAttributedString(string: "abc"))
|
||||
}
|
||||
|
||||
func testTrimTrailing() {
|
||||
let a = NSMutableAttributedString(string: " a ")
|
||||
a.trimTrailingCharactersInSet(.whitespaces)
|
||||
XCTAssertEqual(a, NSAttributedString(string: " a"))
|
||||
let b = NSMutableAttributedString(string: " ")
|
||||
b.trimTrailingCharactersInSet(.whitespaces)
|
||||
XCTAssertEqual(b, NSAttributedString(string: ""))
|
||||
let c = NSMutableAttributedString(string: "")
|
||||
c.trimTrailingCharactersInSet(.whitespaces)
|
||||
XCTAssertEqual(c, NSAttributedString(string: ""))
|
||||
let d = NSMutableAttributedString(string: "abc")
|
||||
d.trimTrailingCharactersInSet(.whitespaces)
|
||||
XCTAssertEqual(d, NSAttributedString(string: "abc"))
|
||||
}
|
||||
|
||||
func testCollapsingWhitespace() {
|
||||
var str = NSAttributedString(string: "test 1\n")
|
||||
XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 1\n"))
|
||||
|
|
|
@ -9,8 +9,8 @@
|
|||
// Configuration settings file format documentation can be found at:
|
||||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
MARKETING_VERSION = 2023.8
|
||||
CURRENT_PROJECT_VERSION = 110
|
||||
MARKETING_VERSION = 2024.1
|
||||
CURRENT_PROJECT_VERSION = 111
|
||||
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
||||
|
||||
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
||||
|
|
Loading…
Reference in New Issue