Skip to content

Commit f2f9d93

Browse files
Fix CRLF Line Ending Typesetting (#20)
1 parent cf4ee3b commit f2f9d93

File tree

2 files changed

+100
-3
lines changed

2 files changed

+100
-3
lines changed

Sources/CodeEditTextView/TextLine/Typesetter.swift

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,18 @@ final class Typesetter {
182182
startingOffset: Int,
183183
constrainingWidth: CGFloat
184184
) -> Int {
185-
let breakIndex = startingOffset + CTTypesetterSuggestClusterBreak(typesetter, startingOffset, constrainingWidth)
186-
if breakIndex >= string.length || (breakIndex - 1 > 0 && ensureCharacterCanBreakLine(at: breakIndex - 1)) {
185+
var breakIndex = startingOffset + CTTypesetterSuggestClusterBreak(typesetter, startingOffset, constrainingWidth)
186+
187+
let isBreakAtEndOfString = breakIndex >= string.length
188+
189+
let isNextCharacterCarriageReturn = checkIfLineBreakOnCRLF(breakIndex)
190+
if isNextCharacterCarriageReturn {
191+
breakIndex += 1
192+
}
193+
194+
let canLastCharacterBreak = (breakIndex - 1 > 0 && ensureCharacterCanBreakLine(at: breakIndex - 1))
195+
196+
if isBreakAtEndOfString || canLastCharacterBreak {
187197
// Breaking either at the end of the string, or on a whitespace.
188198
return breakIndex
189199
} else if breakIndex - 1 > 0 {
@@ -208,7 +218,20 @@ final class Typesetter {
208218
let set = CharacterSet(
209219
charactersIn: string.attributedSubstring(from: NSRange(location: index, length: 1)).string
210220
)
211-
return set.isSubset(of: .whitespaces) || set.isSubset(of: .punctuationCharacters)
221+
return set.isSubset(of: .whitespacesAndNewlines) || set.isSubset(of: .punctuationCharacters)
222+
}
223+
224+
/// Check if the break index is on a CRLF (`\r\n`) character, indicating a valid break position.
225+
/// - Parameter breakIndex: The index to check in the string.
226+
/// - Returns: True, if the break index lies after the `\n` character in a `\r\n` sequence.
227+
private func checkIfLineBreakOnCRLF(_ breakIndex: Int) -> Bool {
228+
guard breakIndex - 1 > 0 && breakIndex + 1 <= string.length else {
229+
return false
230+
}
231+
let substringRange = NSRange(location: breakIndex - 1, length: 2)
232+
let substring = string.attributedSubstring(from: substringRange).string
233+
234+
return substring == LineEnding.carriageReturnLineFeed.rawValue
212235
}
213236

214237
deinit {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import XCTest
2+
@testable import CodeEditTextView
3+
4+
// swiftlint:disable all
5+
6+
class TypesetterTests: XCTestCase {
7+
let limitedLineWidthDisplayData = TextLine.DisplayData(maxWidth: 150, lineHeightMultiplier: 1.0, estimatedLineHeight: 20.0)
8+
let unlimitedLineWidthDisplayData = TextLine.DisplayData(maxWidth: .infinity, lineHeightMultiplier: 1.0, estimatedLineHeight: 20.0)
9+
10+
func test_LineFeedBreak() {
11+
let typesetter = Typesetter()
12+
typesetter.typeset(
13+
NSAttributedString(string: "testline\n"),
14+
displayData: unlimitedLineWidthDisplayData,
15+
breakStrategy: .word,
16+
markedRanges: nil
17+
)
18+
19+
XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")
20+
21+
typesetter.typeset(
22+
NSAttributedString(string: "testline\n"),
23+
displayData: unlimitedLineWidthDisplayData,
24+
breakStrategy: .character,
25+
markedRanges: nil
26+
)
27+
28+
XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")
29+
}
30+
31+
func test_carriageReturnBreak() {
32+
let typesetter = Typesetter()
33+
typesetter.typeset(
34+
NSAttributedString(string: "testline\r"),
35+
displayData: unlimitedLineWidthDisplayData,
36+
breakStrategy: .word,
37+
markedRanges: nil
38+
)
39+
40+
XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")
41+
42+
typesetter.typeset(
43+
NSAttributedString(string: "testline\r"),
44+
displayData: unlimitedLineWidthDisplayData,
45+
breakStrategy: .character,
46+
markedRanges: nil
47+
)
48+
49+
XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")
50+
}
51+
52+
func test_carriageReturnLineFeedBreak() {
53+
let typesetter = Typesetter()
54+
typesetter.typeset(
55+
NSAttributedString(string: "testline\r\n"),
56+
displayData: unlimitedLineWidthDisplayData,
57+
breakStrategy: .word,
58+
markedRanges: nil
59+
)
60+
61+
XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")
62+
63+
typesetter.typeset(
64+
NSAttributedString(string: "testline\r\n"),
65+
displayData: unlimitedLineWidthDisplayData,
66+
breakStrategy: .character,
67+
markedRanges: nil
68+
)
69+
70+
XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")
71+
}
72+
}
73+
74+
// swiftlint:enable all

0 commit comments

Comments
 (0)