Compare commits

...

7 Commits

34 changed files with 210 additions and 333 deletions

View File

@ -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

View File

@ -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;

View File

@ -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),

View File

@ -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: "")
}
}

View File

@ -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

View File

@ -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()
}
converter.convert(html: html)
}
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))."
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
}
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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 })

View File

@ -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)

View File

@ -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() {

View File

@ -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)

View File

@ -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

View File

@ -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() {

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
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

View File

@ -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)

View File

@ -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

View File

@ -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)

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
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)

View File

@ -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)
}

View File

@ -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([

View File

@ -7,7 +7,6 @@
//
import UIKit
import SwiftSoup
import Pachyderm
import SafariServices
import WebURL
@ -22,31 +21,20 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
weak var navigationDelegate: TuskerNavigationDelegate?
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

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -16,6 +16,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")

View File

@ -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