Browse Source

Collapse whitespace according to CSS spec after converting HTML to

attributed string

Fixes #27
master
Shadowfacts 1 month ago
parent
commit
e9db3fa0ac
Signed by: Shadowfacts <me@shadowfacts.net> GPG Key ID: 94A5AB95422746E5

+ 4
- 0
Tusker.xcodeproj/project.pbxproj View File

@@ -104,6 +104,7 @@
104 104
 		D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; };
105 105
 		D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
106 106
 		D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
107
+		D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */; };
107 108
 		D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Helpers.swift */; };
108 109
 		D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.swift */; };
109 110
 		D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; };
@@ -376,6 +377,7 @@
376 377
 		D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
377 378
 		D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
378 379
 		D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
380
+		D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringHelperTests.swift; sourceTree = "<group>"; };
379 381
 		D6333B362137838300CE884A /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = "<group>"; };
380 382
 		D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = "<group>"; };
381 383
 		D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = "<group>"; };
@@ -1206,6 +1208,7 @@
1206 1208
 			isa = PBXGroup;
1207 1209
 			children = (
1208 1210
 				D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */,
1211
+				D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */,
1209 1212
 				D6D4DDE6212518A200E1C4BB /* Info.plist */,
1210 1213
 			);
1211 1214
 			path = TuskerTests;
@@ -1743,6 +1746,7 @@
1743 1746
 			isa = PBXSourcesBuildPhase;
1744 1747
 			buildActionMask = 2147483647;
1745 1748
 			files = (
1749
+				D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */,
1746 1750
 				D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */,
1747 1751
 			);
1748 1752
 			runOnlyForDeploymentPostprocessing = 0;

+ 73
- 6
Tusker/Extensions/AttributedString+Helpers.swift View File

@@ -8,28 +8,95 @@
8 8
 
9 9
 import Foundation
10 10
 
11
-extension NSMutableAttributedString {
12
-    
11
+private let ASCII_NEWLINE: unichar = 10
12
+private let ASCII_SPACE: unichar = 32
13
+
14
+extension NSAttributedString {
15
+
13 16
     var fullRange: NSRange {
14 17
         return NSRange(location: 0, length: self.length)
15 18
     }
16
-    
19
+
20
+    /// Creates a new string with the whitespace collapsed according to the CSS Text Module Level 3 rules.
21
+    /// See https://www.w3.org/TR/css-text-3/#white-space-phase-1
22
+    func collapsingWhitespace() -> NSAttributedString {
23
+        let mut = NSMutableAttributedString(attributedString: self)
24
+        mut.collapseWhitespace()
25
+        return mut
26
+    }
27
+
28
+}
29
+
30
+extension NSMutableAttributedString {
31
+
17 32
     func trimLeadingCharactersInSet(_ charSet: CharacterSet) {
18 33
         var range = (string as NSString).rangeOfCharacter(from: charSet)
19
-        
34
+
20 35
         while range.length != 0 && range.location == 0 {
21 36
             replaceCharacters(in: range, with: "")
22 37
             range = (string as NSString).rangeOfCharacter(from: charSet)
23 38
         }
24 39
     }
25
-    
40
+
26 41
     func trimTrailingCharactersInSet(_ charSet: CharacterSet) {
27 42
         var range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
28
-        
43
+
29 44
         while range.length != 0 && range.length + range.location == length {
30 45
             replaceCharacters(in: range, with: "")
31 46
             range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
32 47
         }
33 48
     }
49
+
50
+    /// Collapses whitespace in this string according to the CSS Text Module Level 3 rules.
51
+    /// See https://www.w3.org/TR/css-text-3/#white-space-phase-1
52
+    func collapseWhitespace() {
53
+        let str = self.mutableString
54
+        
55
+        var i = 0
56
+        while i < str.length {
57
+            if str.character(at: i) == ASCII_NEWLINE {
58
+                var j: Int
59
+                if i > 0 {
60
+                    // scan backwards to find beginning of space characters preceeding newline
61
+                    j = i - 1
62
+                    while j >= 0 {
63
+                        if str.character(at: j) != ASCII_SPACE {
64
+                            break
65
+                        }
66
+                        j -= 1
67
+                    }
68
+                    // add one after loop completes because start of range is _inclusive_
69
+                    j += 1
70
+                } else {
71
+                    j = 0
72
+                }
73
+                
74
+                var k: Int
75
+                if i < str.length - 1 {
76
+                    // scan forwards to find end of space characters following newline
77
+                    k = i + 1
78
+                    while k < str.length {
79
+                        if str.character(at: k) != ASCII_SPACE {
80
+                            break
81
+                        }
82
+                        k += 1
83
+                    }
84
+                    // don't need to subtract one before breaking out of loop, because end of range is _exclusive_
85
+                } else {
86
+                    // range end is _exclusive_, so use whole string length that way last character is included
87
+                    k = str.length
88
+                }
89
+                
90
+                // if there's only one character to be replaced, that means we'd be replacing the newline with a newline, so don't bother
91
+                if k - j > 1 {
92
+                    str.replaceCharacters(in: NSRange(location: j, length: k - j), with: "\n")
93
+                    
94
+                    // continue scanning through the string starting after the newline we just inserted
95
+                    i = j
96
+                }
97
+            }
98
+            i += 1
99
+        }
100
+    }
34 101
     
35 102
 }

+ 2
- 1
Tusker/Views/ContentTextView.swift View File

@@ -109,11 +109,12 @@ class ContentTextView: LinkTextView {
109 109
         let attributedText = attributedTextForHTMLNode(body)
110 110
         let mutAttrString = NSMutableAttributedString(attributedString: attributedText)
111 111
         mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
112
+        mutAttrString.collapseWhitespace()
112 113
         
113 114
         self.attributedText = mutAttrString
114 115
     }
115 116
     
116
-    func attributedTextForHTMLNode(_ node: Node, usePreformattedText: Bool = false) -> NSAttributedString {
117
+    private func attributedTextForHTMLNode(_ node: Node, usePreformattedText: Bool = false) -> NSAttributedString {
117 118
         switch node {
118 119
         case let node as TextNode:
119 120
             let text: String

+ 46
- 0
TuskerTests/AttributedStringHelperTests.swift View File

@@ -0,0 +1,46 @@
1
+//
2
+//  AttributedStringHelperTests.swift
3
+//  TuskerTests
4
+//
5
+//  Created by Shadowfacts on 1/21/20.
6
+//  Copyright © 2020 Shadowfacts. All rights reserved.
7
+//
8
+
9
+import XCTest
10
+@testable import Tusker
11
+
12
+class AttributedStringHelperTests: XCTestCase {
13
+
14
+    override func setUp() {
15
+    }
16
+
17
+    override func tearDown() {
18
+    }
19
+
20
+    func testCollapsingWhitespace() {
21
+        var str = NSAttributedString(string: "test 1\n")
22
+        XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 1\n"))
23
+        
24
+        str = NSAttributedString(string: "test 2   \n")
25
+        XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 2\n"))
26
+        
27
+        str = NSAttributedString(string: "test 3\n    ")
28
+        XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 3\n"))
29
+        
30
+        str = NSAttributedString(string: "test 4     \n    ")
31
+        XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 4\n"))
32
+        
33
+        str = NSAttributedString(string: "test 5     \n    blah")
34
+        XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 5\nblah"))
35
+        
36
+        str = NSAttributedString(string: "\ntest 6")
37
+        XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "\ntest 6"))
38
+        
39
+        str = NSAttributedString(string: "   \ntest 7")
40
+        XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "\ntest 7"))
41
+        
42
+        str = NSAttributedString(string: "   \n    test 8")
43
+        XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "\ntest 8"))
44
+    }
45
+
46
+}

Loading…
Cancel
Save