From 081c98b64fc93bf78119d8926f1a8242b79e5e60 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:30:23 +0100 Subject: [PATCH 01/61] Remove comments. --- Package.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Package.swift b/Package.swift index a8c51d6..aaa9d79 100644 --- a/Package.swift +++ b/Package.swift @@ -6,18 +6,18 @@ import PackageDescription let package = Package( name: "PersianJustify", products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "PersianJustify", - targets: ["PersianJustify"]), + targets: ["PersianJustify"] + ), ], targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. .target( - name: "PersianJustify"), + name: "PersianJustify" + ), .testTarget( name: "PersianJustifyTests", - dependencies: ["PersianJustify"]), + dependencies: ["PersianJustify"] + ), ] ) From 24b2e1c57c07263d0511ce07520478e522057c7f Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:30:43 +0100 Subject: [PATCH 02/61] Remove extra white spaces. --- Tests/PersianJustifyTests/MainFunctionalityTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/PersianJustifyTests/MainFunctionalityTests.swift b/Tests/PersianJustifyTests/MainFunctionalityTests.swift index 65c0389..8d691e5 100644 --- a/Tests/PersianJustifyTests/MainFunctionalityTests.swift +++ b/Tests/PersianJustifyTests/MainFunctionalityTests.swift @@ -10,7 +10,6 @@ import XCTest #if canImport(UIKit) final class MainFunctionalityTests: XCTestCase { - func testMainFunctionIsGettingOutput() { let text1 = "" let text2 = "blah blah" @@ -62,7 +61,6 @@ final class MainFunctionalityTests: XCTestCase { #endif private extension String { - func getNextLineCount()-> Int { return filter({$0 == "\n"}).count } From 3b80603655c038c4f90c4b2a581864e35602851c Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:31:19 +0100 Subject: [PATCH 03/61] Revisit file in order to improve readability. --- Sources/Extensions/View.getFont.swift | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Sources/Extensions/View.getFont.swift b/Sources/Extensions/View.getFont.swift index 028e5d4..11d404d 100644 --- a/Sources/Extensions/View.getFont.swift +++ b/Sources/Extensions/View.getFont.swift @@ -1,17 +1,22 @@ #if canImport(UIKit) import UIKit -public typealias Font = UIFont -public typealias View = UIView #elseif canImport(AppKit) import AppKit -public typealias Font = NSFont -public typealias View = NSView #endif -internal extension View { - func getFont() -> Font? { - let key = "font" - guard responds(to: Selector(key)) else { return nil } +private let key = "font" + +extension View { + func getFont() -> Font? { + // Opaque pointer to font in the given view + let fontSelector = Selector(key) + + // Arguably this defensive check can be omitted, + // Since if a view has a font property, we can fetch font from + guard responds(to: fontSelector) else { + return nil + } + return value(forKey: key) as? Font } } From 120136871230c671a20a523fc0b67457c8912080 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:35:42 +0100 Subject: [PATCH 04/61] Introduce `ArabicStringEvaluator`. Small object that can determine if an string/character is in Arabic. --- Sources/ArabicCharacterEvaluator.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Sources/ArabicCharacterEvaluator.swift diff --git a/Sources/ArabicCharacterEvaluator.swift b/Sources/ArabicCharacterEvaluator.swift new file mode 100644 index 0000000..8cc5047 --- /dev/null +++ b/Sources/ArabicCharacterEvaluator.swift @@ -0,0 +1,17 @@ +import class Foundation.NSPredicate + +/// Regex that will be used against an arbitrary string, +/// to identify if it's in Arabic. +private let arabicRegex = "(?s).*\\p{Arabic}.*" + +/// Wrapper around a predicate that will encapsulate the logic of identifying if a given string is in Arabic. +struct ArabicCharacterEvaluator { + private static let _predicate = NSPredicate(format: "SELF MATCHES %@", arabicRegex) + + func evaluate(with character: Character) -> Bool { + // Character must be converted to string before being fed to predicate. + let stringRepresentation = String(character) + + return Self._predicate.evaluate(with: stringRepresentation) + } +} From 19ec1d6715ec48b8eca0f43b038cd805f4bf306a Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:35:52 +0100 Subject: [PATCH 05/61] Introduce `ExtenderCharacter`. --- Sources/Characters/ExtenderCharacter.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Sources/Characters/ExtenderCharacter.swift diff --git a/Sources/Characters/ExtenderCharacter.swift b/Sources/Characters/ExtenderCharacter.swift new file mode 100644 index 0000000..9137ec2 --- /dev/null +++ b/Sources/Characters/ExtenderCharacter.swift @@ -0,0 +1,14 @@ +import Foundation + +/// Wrapper around a character which is used to extend some characters in `Farsi`. +struct ExtenderCharacter { + private static let _character: Character = "ـ" // Persian underline + + static var stringRepresentation: String { + String(Self._character) + } + + static var wordRepresentation: Word { + Word(stringRepresentation) + } +} From a907f6ff5f15f152b0eca3d9c95dae29facab3fa Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:36:02 +0100 Subject: [PATCH 06/61] Introduce `MiniSpaceCharacter `. --- Sources/Characters/MiniSpaceCharacter.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Sources/Characters/MiniSpaceCharacter.swift diff --git a/Sources/Characters/MiniSpaceCharacter.swift b/Sources/Characters/MiniSpaceCharacter.swift new file mode 100644 index 0000000..33afdfa --- /dev/null +++ b/Sources/Characters/MiniSpaceCharacter.swift @@ -0,0 +1,18 @@ +import Foundation + +/// Wrapper around a character which is used to add a small spacer between characters in `Farsi`. +struct MiniSpaceCharacter { + private static let _character: Character = "‌" + + private static var stringRepresentation: String { + String(Self._character) + } + + static func + (lhs: MiniSpaceCharacter, rhs: MiniSpaceCharacter) -> String { + stringRepresentation + stringRepresentation + } + + static func + (lhs: String, rhs: MiniSpaceCharacter) -> String { + lhs + stringRepresentation + } +} From 6b1ec6c029424e9e3dac6e680f9c5b45fbd3ba89 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:36:56 +0100 Subject: [PATCH 07/61] Introduce `NewLine `. --- Sources/Characters/NewLineCharacter.swift | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Sources/Characters/NewLineCharacter.swift diff --git a/Sources/Characters/NewLineCharacter.swift b/Sources/Characters/NewLineCharacter.swift new file mode 100644 index 0000000..2ca6ff9 --- /dev/null +++ b/Sources/Characters/NewLineCharacter.swift @@ -0,0 +1,28 @@ +import Foundation + +/// Wrapper around a character which is used to append a new line. +struct NewLine { + /*private*/ static let _character: Character = "\n" + + static var stringRepresentation: String { + String(Self._character) + } + + static var attributedStringRepresentation: NSAttributedString { + NSAttributedString(string: stringRepresentation) + } + + static func + (lhs: NewLine, rhs: NewLine) -> String { + stringRepresentation + stringRepresentation + } + + static func + (lhs: String, rhs: NewLine) -> String { + lhs + stringRepresentation + } +} + +extension String { + func replacingOccurrences(of target: String, with replacement: NewLine) -> String { + replacingOccurrences(of: target, with: NewLine.stringRepresentation) + } +} From 63fd8cb0504fb7de5996ce0cbbdbb10a65d08eda Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:37:15 +0100 Subject: [PATCH 08/61] Introduce `SpaceCharacter `. --- Sources/Characters/SpaceCharacter.swift | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Sources/Characters/SpaceCharacter.swift diff --git a/Sources/Characters/SpaceCharacter.swift b/Sources/Characters/SpaceCharacter.swift new file mode 100644 index 0000000..1cc1a80 --- /dev/null +++ b/Sources/Characters/SpaceCharacter.swift @@ -0,0 +1,24 @@ +import Foundation + +/// Wrapper around a character which is used to append a space. +struct SpaceCharacter { + /*private*/ static let character: Character = " " + + static var stringRepresentation: String { + String(Self.character) + } + + static var attributedStringRepresentation: NSAttributedString { + NSAttributedString(string: stringRepresentation) + } + + static func + (lhs: String, rhs: SpaceCharacter) -> String { + lhs + stringRepresentation + } +} + +extension NSMutableAttributedString { + func appendSpaceCharacter() { + append(SpaceCharacter.attributedStringRepresentation) + } +} From a5e16ec10599f86160719dfb1b7f07d1e9b68516 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:44:06 +0100 Subject: [PATCH 09/61] Introduce `Line`. --- Sources/CoreTextExtensible/Line.swift | 116 ++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 Sources/CoreTextExtensible/Line.swift diff --git a/Sources/CoreTextExtensible/Line.swift b/Sources/CoreTextExtensible/Line.swift new file mode 100644 index 0000000..9db0171 --- /dev/null +++ b/Sources/CoreTextExtensible/Line.swift @@ -0,0 +1,116 @@ +import Foundation + +#if canImport(UIKit) +import class UIKit.NSMutableParagraphStyle +import enum UIKit.NSTextAlignment +#elseif canImport(AppKit) +import class AppKit.NSMutableParagraphStyle +import enum AppKit.NSTextAlignment +#endif + +/// Wrapper around a line. +/// Object will extend line capabilities and encapsulate logics related to justifying a line. +struct Line { + private let _line: String.SubSequence + + init(_ line: String.SubSequence) { + self._line = line + } + + init(_ line: String) { + self._line = String.SubSequence(line) + } + + var stringRepresentation: String { + String(_line) + } + + /// Get every word in a line. + func getWords() -> [Word] { + splitLineIntoWords() + .map { Word($0) } + } + + /// Re-aligns a line based on proposed width and given font. + func justify( + in proposedWidth: CGFloat, + isLastLineInParagraph: Bool, + font: Font + ) -> NSMutableAttributedString { + let words = getWords() + + lazy var emptySpace: CGFloat = { + let totalWordsWidth = words.getRequiredWidth(with: font) + + return proposedWidth - totalWordsWidth + }() + + lazy var requiredExtender: CGFloat = { + let singleExtenderWidth = ExtenderCharacter + .wordRepresentation + .getWordWidth(font: font, isRequiredSpace: false) + + let extractedExpr = emptySpace / singleExtenderWidth + return Swift.max(extractedExpr, 0) + }() + + let supportedExtenderWords = words.filter { $0.canSupportExtender() } + + if isLastLineInParagraph { + // May not required justify. + return NSMutableAttributedString(string: stringRepresentation) + } else { + lazy var isManyExtendersRequired = CGFloat(supportedExtenderWords.count) < requiredExtender + + let requiredExtend: CGFloat + + if isManyExtendersRequired { + requiredExtend = emptySpace / CGFloat(supportedExtenderWords.count) + } else if requiredExtender > 0 && supportedExtenderWords.count > 0 { + requiredExtend = max(requiredExtender * 0.1, 0) + } else { + return NSMutableAttributedString(string: stringRepresentation) + } + + return getExtendedWords( + words: supportedExtenderWords, + requiredExtend: requiredExtend * 0.2, + font: font + ) + } + } + + private func splitLineIntoWords() -> [Substring] { + _line.split(separator: SpaceCharacter.character) + } + + private func getExtendedWords( + words: [Word], + requiredExtend: CGFloat, + font: Font + ) -> NSMutableAttributedString { + let style: NSMutableParagraphStyle = { + let style = NSMutableParagraphStyle() + style.alignment = NSTextAlignment.justified + style.baseWritingDirection = .rightToLeft + return style + }() + + let totalRange = NSRange(location: 0, length: _line.utf16.count) + + let attributedText = NSMutableAttributedString(string: stringRepresentation) + attributedText.setAttributes([NSAttributedString.Key.font: font], range: totalRange) + + for word in words { + let range = getRange(of: word) + attributedText.addAttribute(NSAttributedString.Key.kern, value: requiredExtend, range: range) + attributedText.addAttributes([NSAttributedString.Key.paragraphStyle: style], range: range) + } + + return attributedText + } + + private func getRange(of word: Word) -> NSRange { + (_line as NSString).range(of: word.stringRepresentation, options: .widthInsensitive) + } +} From 165a0429469b49d4579f20c5a06dff990a8098ad Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:44:16 +0100 Subject: [PATCH 10/61] Introduce `Word`. --- Sources/CoreTextExtensible/Word.swift | 102 ++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Sources/CoreTextExtensible/Word.swift diff --git a/Sources/CoreTextExtensible/Word.swift b/Sources/CoreTextExtensible/Word.swift new file mode 100644 index 0000000..db91900 --- /dev/null +++ b/Sources/CoreTextExtensible/Word.swift @@ -0,0 +1,102 @@ +import Foundation +import func CoreText.CTLineCreateWithAttributedString +import func CoreText.CTLineGetTypographicBounds + +/// Character that should not be extended in any circumstances in `Farsi`. +private let forbiddenExtendableCharacters = ["ا", "د", "ذ", "ر", "ز", "و", "آ", "ژ"] + +/// Wrapper around a word. +struct Word { + fileprivate let _word: Substring + + init(_ word: Substring) { + self._word = word + } + + init(_ word: String) { + self._word = Substring(word) + } + + var stringRepresentation: String { + String(_word) + } + + /// Flag that determines if a word can support extending. + /// It's common in `Farsi` for a word to be extended in an arbitrary font. + var canSupportExtender: Bool { + let count = _word.count + + let isWordEmpty = !_word.lazy.dropFirst().isEmpty + guard isWordEmpty else { + return false + } + + let characters = Array(_word) + + // @Sajad double check where condition + for i in stride(from: count-1, to: 0, by: -1) where (i > 0) && i < count { + let char = characters[i] + let rightChar = characters[i-1] + + let isNotForbiddenCharacter = !forbiddenExtendableCharacters.contains(String(rightChar)) + let isNextCharArabic = rightChar.isArabic + let isCharArabic = char.isArabic + + if isNotForbiddenCharacter && isNextCharArabic && isCharArabic { + return true + } + } + + return false + } + + /// Calculate word width based on font. + func getWordWidth(font: Font, isRequiredSpace: Bool = true) -> CGFloat { + let wordStringRepresentation = stringRepresentation + let text = isRequiredSpace ? (wordStringRepresentation + SpaceCharacter()) : wordStringRepresentation + + let attributedString = NSAttributedString(string: text, attributes: [.font: font]) + + let line = CTLineCreateWithAttributedString(attributedString) + var ascent: CGFloat = 0 + var descent: CGFloat = 0 + var leading: CGFloat = 0 + + let width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading) + return CGFloat(width) + } +} + +extension String.Element { + fileprivate var isArabic: Bool { + let evaluator = ArabicCharacterEvaluator() + return evaluator.evaluate(with: self) + } +} + +extension [Word] { + /// Re-create a line from words. + func createLineFromWords() -> Line { + let joinedWords = map(\._word) + .joined(separator: SpaceCharacter.stringRepresentation) + + return Line(joinedWords) + } +} + +extension [Word] { + /// Method that will determine if a given word can fit inside the line based on proposed width. + func hasRoomForNextWord(nextWord: Word, proposedWidth: CGFloat, font: Font) -> Bool { + let requiredWidth = nextWord.getWordWidth(font: font) + let currentWidth = getRequiredWidth(with: font) + return (currentWidth + requiredWidth) <= proposedWidth + } +} + +extension [Word] { + /// Method that will calculate required width for words. + func getRequiredWidth(with font: Font) -> CGFloat { + map { $0.getWordWidth(font: font) } + .reduce(0, +) + } +} From f93975f8e71fae36b7498d0373177ef779774e2a Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:45:05 +0100 Subject: [PATCH 11/61] Introduce `HeaderTypes`. --- Sources/HeaderTypes.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 Sources/HeaderTypes.swift diff --git a/Sources/HeaderTypes.swift b/Sources/HeaderTypes.swift new file mode 100644 index 0000000..71dcdbc --- /dev/null +++ b/Sources/HeaderTypes.swift @@ -0,0 +1,15 @@ +import Foundation + +/// File will contain only headers. + +#if canImport(UIKit) +import UIKit + +public typealias Font = UIFont +public typealias View = UIView +#elseif canImport(AppKit) +import AppKit + +public typealias Font = NSFont +public typealias View = NSView +#endif From 96ce0c4953313308d1f78972a59a78021dd6c3fa Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:45:37 +0100 Subject: [PATCH 12/61] Remove unneeded variables. --- Sources/PersianJustify.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 422707b..c01f03a 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -11,17 +11,6 @@ import UIKit import AppKit #endif -import CoreText - -// MARK: - Variables -fileprivate let nextLineCharacter: Character = "\n" -fileprivate let spaceCharacter: Character = " " -fileprivate let miniSpaceCharacter: Character = "‌" -fileprivate let extenderCharacter: Character = "ـ" // Persian underline -fileprivate let attributedSpace = NSAttributedString(string: spaceCharacter.description) -fileprivate let attributedNextLine = NSMutableAttributedString(string: nextLineCharacter.description) -fileprivate let forbiddenExtendableCharacters = ["ا", "د", "ذ", "ر", "ز", "و", "آ", "ژ"] - // MARK: - Usage using toPJString function extension String { From 24ef91c97bde7ed5835bdd0e753031cfb5eecbfe Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:46:03 +0100 Subject: [PATCH 13/61] Introduce `splitStringToLines` method. --- Sources/PersianJustify.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index c01f03a..20051fe 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -20,6 +20,11 @@ extension String { if isEmpty { return defaultAttributedTest } let defaultFont = Font() let font = view.getFont() ?? defaultFont + private func splitStringToLines() -> [Line] { + replaceDoubleEmptyLines() + .split(separator: NewLine._character) + .map { Line($0) } + } let final = NSMutableAttributedString(string: "") let doubleNextLine = nextLineCharacter.description + nextLineCharacter.description let allLines = replacingOccurrences(of:doubleNextLine, with: nextLineCharacter.description).getWords(separator: nextLineCharacter) From 92e2724c8617a42c06f0ddf833d9b51d85ef30ec Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:46:20 +0100 Subject: [PATCH 14/61] Introduce `replaceDoubleEmptyLines` method. --- Sources/PersianJustify.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 20051fe..4d3356a 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -25,6 +25,16 @@ extension String { .split(separator: NewLine._character) .map { Line($0) } } + + private func replaceDoubleEmptyLines() -> String { + let doubleNextLine = NewLine() + NewLine() + + // Replacing double empty lines with one empty line + return replacingOccurrences( + of: doubleNextLine, + with: NewLine() + ) + } let final = NSMutableAttributedString(string: "") let doubleNextLine = nextLineCharacter.description + nextLineCharacter.description let allLines = replacingOccurrences(of:doubleNextLine, with: nextLineCharacter.description).getWords(separator: nextLineCharacter) From 9152d2bb77aaa4af6cbd96aaf5ee633558a210c9 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:46:39 +0100 Subject: [PATCH 15/61] Introduce `justify` method. --- Sources/PersianJustify.swift | 71 +++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 4d3356a..d04e182 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -35,33 +35,60 @@ extension String { with: NewLine() ) } + + private func justify( + _ lines: [Line], + in proposedWidth: CGFloat, + with font: Font + ) -> NSAttributedString { let final = NSMutableAttributedString(string: "") - let doubleNextLine = nextLineCharacter.description + nextLineCharacter.description - let allLines = replacingOccurrences(of:doubleNextLine, with: nextLineCharacter.description).getWords(separator: nextLineCharacter) - let parentWidth = getTotalWidth(in: view) - for i in 0.. Date: Mon, 18 Mar 2024 10:47:00 +0100 Subject: [PATCH 16/61] Remove `getJustifiedLine` method. --- Sources/PersianJustify.swift | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index d04e182..47012e0 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -90,37 +90,8 @@ extension String { isLastLineInParagraph: true ) -// MARK: - Private Functions -private extension String { - - func getJustifiedLine(in parentWidth: CGFloat, isLastLineInParagraph: Bool, font: Font) -> NSMutableAttributedString { - let words = getWords(separator: spaceCharacter) - let totalWordsWidth = words.compactMap({$0.getWordWidth(font: font)}).reduce(0, +) - let emptySpace = parentWidth - totalWordsWidth - let singleExtenderWidth = extenderCharacter.description.getWordWidth(font: font, isRequiredSpace: false) - let requiredExtender = Swift.max((emptySpace / singleExtenderWidth), 0) - let supportedExtenderWords = words.filter({$0.isSupportExtender()}) - print("words: ", self) - print("parent width: \(parentWidth)") - print("totalWordsWidth \(totalWordsWidth)") - print("emptySpace: \(emptySpace)") - if isLastLineInParagraph { // MARK: May not required justify. - return .init(attributedString: NSAttributedString(string: self)) - } else { - let isManyExtendersRequired = CGFloat(supportedExtenderWords.count) < requiredExtender - if isManyExtendersRequired { - print("many extenders required") - let requiredExtend = emptySpace / CGFloat(supportedExtenderWords.count) - return getExtendedWords(words: supportedExtenderWords, requiredExtend: requiredExtend * 0.2, font: font) - } else if requiredExtender > 0 && supportedExtenderWords.count > 0 { - print("little extenders required") - return getExtendedWords(words: supportedExtenderWords, requiredExtend: max(requiredExtender * 0.1, 0), font: font) - } else { - print("no extender added") - return NSMutableAttributedString(attributedString: NSAttributedString(string: self)) + final.append(extracted) } - } - } func getExtendedWords(words: [String], requiredExtend: CGFloat, font: Font) -> NSMutableAttributedString { print("------------------------------------------") From e126f60f4415bbeda83dad307201fabaa06fd1d7 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:47:13 +0100 Subject: [PATCH 17/61] Remove `getExtendedWords ` method. --- Sources/PersianJustify.swift | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 47012e0..501d361 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -93,24 +93,6 @@ extension String { final.append(extracted) } - func getExtendedWords(words: [String], requiredExtend: CGFloat, font: Font) -> NSMutableAttributedString { - print("------------------------------------------") - let style = NSMutableParagraphStyle() - style.alignment = NSTextAlignment.justified - style.baseWritingDirection = .rightToLeft - let totalRange = NSRange(location: 0, length: self.utf16.count) - let attributedText = NSMutableAttributedString(string: self) - attributedText.setAttributes([NSAttributedString.Key.font: font], range: totalRange) - for word in words { - let range = getRange(of: word) - attributedText.addAttribute(NSAttributedString.Key.kern, value: requiredExtend, range: range) - attributedText.addAttributes([NSAttributedString.Key.paragraphStyle: style], range: range) - print("applying extend | \(requiredExtend) to \(word)") - } - print("------------------------------------------") - return attributedText - } - var isArabic: Bool { let predicate = NSPredicate(format: "SELF MATCHES %@", "(?s).*\\p{Arabic}.*") return predicate.evaluate(with: self) From 57130d4df9a3be40bea6256b7585b2d390ed7cf6 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:47:28 +0100 Subject: [PATCH 18/61] Remove `isArabic` computed property. --- Sources/PersianJustify.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 501d361..fd89610 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -93,11 +93,6 @@ extension String { final.append(extracted) } - var isArabic: Bool { - let predicate = NSPredicate(format: "SELF MATCHES %@", "(?s).*\\p{Arabic}.*") - return predicate.evaluate(with: self) - } - func getWords(separator: Character) -> [String] { return split(separator: separator).compactMap({$0.description}) } From d78d1611c016c0c0470b08dc2c25131c87229c77 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:47:35 +0100 Subject: [PATCH 19/61] Remove `getWords ` method. --- Sources/PersianJustify.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index fd89610..663bde7 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -92,10 +92,6 @@ extension String { final.append(extracted) } - - func getWords(separator: Character) -> [String] { - return split(separator: separator).compactMap({$0.description}) - } func getTotalWidth(in view: View) -> CGFloat { return view.frame.width From 8819450183845b5f70712d229596ab7774a9e7e7 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:47:44 +0100 Subject: [PATCH 20/61] Remove `getTotalWidth ` method. --- Sources/PersianJustify.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 663bde7..856d02c 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -93,9 +93,6 @@ extension String { final.append(extracted) } - func getTotalWidth(in view: View) -> CGFloat { - return view.frame.width - } func getWordWidth(font: Font, isRequiredSpace: Bool = true) -> CGFloat { let text = isRequiredSpace ? (self + spaceCharacter.description) : self From d542b74781c4b2b0672c9740841aba70d07ef439 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:47:56 +0100 Subject: [PATCH 21/61] Remove `getWordWidth` method. --- Sources/PersianJustify.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 856d02c..07c7855 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -94,17 +94,6 @@ extension String { } - func getWordWidth(font: Font, isRequiredSpace: Bool = true) -> CGFloat { - let text = isRequiredSpace ? (self + spaceCharacter.description) : self - let attributedString = NSAttributedString(string: text, attributes: [.font: font]) - let line = CTLineCreateWithAttributedString(attributedString) - var ascent: CGFloat = 0 - var descent: CGFloat = 0 - var leading: CGFloat = 0 - let width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading) - return CGFloat(width) - } - func getRange(of word: String) -> NSRange { return (self as NSString).range(of: word, options: .widthInsensitive) } From eaecc588d06a8e6117ee93a6a9ff4ba75e5420cc Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:48:06 +0100 Subject: [PATCH 22/61] Remove `getRange ` method. --- Sources/PersianJustify.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 07c7855..35813b1 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -94,10 +94,6 @@ extension String { } - func getRange(of word: String) -> NSRange { - return (self as NSString).range(of: word, options: .widthInsensitive) - } - func isSupportExtender() -> Bool { guard count > 1 else { return false } let array = Array(self) From 6fcb92f6e4ce0b4515044225dae2a3dc36174e8a Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:48:23 +0100 Subject: [PATCH 23/61] Remove `isSupportExtender` method. --- Sources/PersianJustify.swift | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 35813b1..c3a01f8 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -93,20 +93,11 @@ extension String { final.append(extracted) } - - func isSupportExtender() -> Bool { - guard count > 1 else { return false } - let array = Array(self) - for i in stride(from: count-1, to: 0, by: -1) where (i > 0) && i < count { - let char = array[i].description - let rightChar = array[i-1].description - if !forbiddenExtendableCharacters.contains(rightChar) && rightChar.isArabic && char.isArabic { - return true + // To avoid add extra next line at the end of text + if index < lines.count-1 { + final.append(NewLine.attributedStringRepresentation) } } - return false - } -} private extension [String] { From bcf117608a9bd4c6d53ca938da44573c84e1224a Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:48:42 +0100 Subject: [PATCH 24/61] Remove `hasRoomForNextWord ` method. --- Sources/PersianJustify.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index c3a01f8..2dcff6d 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -99,12 +99,7 @@ extension String { } } -private extension [String] { - - func hasRoomForNextWord(nextWord: String, parentWidth: CGFloat, font: Font) -> Bool { - let requiredWidth = nextWord.getWordWidth(font: font) - let currentWidth = compactMap({$0.getWordWidth(font: font)}).reduce(0, +) - return (currentWidth + requiredWidth) <= parentWidth + return final } func joinWithSpace() -> String { From ce47538fde75b1094a49c0c36588c10b737c7bf7 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:48:49 +0100 Subject: [PATCH 25/61] Remove `joinWithSpace ` method. --- Sources/PersianJustify.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 2dcff6d..9e8112f 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -101,9 +101,6 @@ extension String { return final } - - func joinWithSpace() -> String { - return joined(separator: spaceCharacter.description) } } From b328b5f19df4c5dee0e129bc422aad5c64b42caf Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:49:02 +0100 Subject: [PATCH 26/61] Introduce `justifyLine ` method. --- Sources/PersianJustify.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 9e8112f..b0a66c9 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -101,6 +101,19 @@ extension String { return final } + + private func justifyLine( + from words: [Word], + in proposedWidth: CGFloat, + with font: Font, + isLastLineInParagraph: Bool + ) -> NSMutableAttributedString { + words + .createLineFromWords() + .justify( + in: proposedWidth, + isLastLineInParagraph: isLastLineInParagraph, + font: font + ) } } - From 871ac88a82d831c1fcd1db88d234d017e0598872 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 10:53:37 +0100 Subject: [PATCH 27/61] Refactor `toPJString` method. --- Sources/PersianJustify.swift | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index b0a66c9..a99c335 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -13,13 +13,26 @@ import AppKit // MARK: - Usage using toPJString function extension String { - + /// Method that will layout words in a `Farsi` calligraphy friendly way. + /// - Parameter view: Ancestor view that string will be displayed in. + /// - Warning: This is a computed heavy operation. public func toPJString(in view: View) -> NSAttributedString { - let defaultAttributedTest = NSAttributedString(string: self) - // return defaultAttributedTest // MARK: Uncomment to see the unjustified text - if isEmpty { return defaultAttributedTest } - let defaultFont = Font() - let font = view.getFont() ?? defaultFont + guard !isEmpty else { + return NSAttributedString() + } + + let lines = splitStringToLines() + + let viewWidth = view.frame.width + + let font: Font = { + lazy var defaultFont = Font() + return view.getFont() ?? defaultFont + }() + + return justify(lines, in: viewWidth, with: font) + } + private func splitStringToLines() -> [Line] { replaceDoubleEmptyLines() .split(separator: NewLine._character) From c996e364df0513d37691169843d17feadbb06860 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 11:00:51 +0100 Subject: [PATCH 28/61] Add `MeasurementTests` file. --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- Sources/CoreTextExtensible/Line.swift | 2 +- Sources/PersianJustify.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Example/perian_justify_example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/perian_justify_example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8c5a2de..cd3cf99 100644 --- a/Example/perian_justify_example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/perian_justify_example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "a3510e27242cfd4d5e1fce3ce0f52d9f9523f6630c83c36dbfd5fde858554755", + "originHash" : "fbd9878878133309570b047b11f57a737e995fb2bc7eba51a820ab14bf03a929", "pins" : [ { "identity" : "persianjustify", diff --git a/Sources/CoreTextExtensible/Line.swift b/Sources/CoreTextExtensible/Line.swift index 9db0171..78c511c 100644 --- a/Sources/CoreTextExtensible/Line.swift +++ b/Sources/CoreTextExtensible/Line.swift @@ -54,7 +54,7 @@ struct Line { return Swift.max(extractedExpr, 0) }() - let supportedExtenderWords = words.filter { $0.canSupportExtender() } + let supportedExtenderWords = words.filter { $0.canSupportExtender } if isLastLineInParagraph { // May not required justify. diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index a99c335..412901f 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -65,7 +65,7 @@ extension String { let canAddNewWord: Bool = { let lineHasRoomForNextWord = currentLineWords.hasRoomForNextWord( nextWord: word, - parentWidth: proposedWidth, + proposedWidth: proposedWidth, font: font ) From 35e0daa2fcc8deb0ea8efcc4f8580897a03704bd Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 11:01:15 +0100 Subject: [PATCH 29/61] Introduce `MeasurementTests ` `UIKit` test case. --- .../PersianJustifyTests/MesurementTests.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 Tests/PersianJustifyTests/MesurementTests.swift diff --git a/Tests/PersianJustifyTests/MesurementTests.swift b/Tests/PersianJustifyTests/MesurementTests.swift new file mode 100644 index 0000000..ce8c078 --- /dev/null +++ b/Tests/PersianJustifyTests/MesurementTests.swift @@ -0,0 +1,20 @@ +import XCTest + +#if canImport(UIKit) +final class MeasurementTests: XCTestCase { + func testPerformance() throws { + self.measure(metrics: [XCTCPUMetric(), XCTMemoryMetric()]) { + let text1 = "" + let text2 = "blah blah" + let text3 = "السلام اللعیکم\nو رحمت الله و برکاتو" + + let sut = UILabel() + sut.attributedText = text1.toPJString(in: sut) + + sut.attributedText = text2.toPJString(in: sut) + + sut.attributedText = text3.toPJString(in: sut) + } + } +} +#endif From cc4c4bd0110b677bcd70631e9b8297466d1aee95 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 11:01:28 +0100 Subject: [PATCH 30/61] Introduce `MeasurementTests ` `AppKit ` test case. --- .../PersianJustifyTests/MesurementTests.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Tests/PersianJustifyTests/MesurementTests.swift b/Tests/PersianJustifyTests/MesurementTests.swift index ce8c078..0c87b8b 100644 --- a/Tests/PersianJustifyTests/MesurementTests.swift +++ b/Tests/PersianJustifyTests/MesurementTests.swift @@ -18,3 +18,22 @@ final class MeasurementTests: XCTestCase { } } #endif + +#if canImport(AppKit) +final class MeasurementTests: XCTestCase { + func testPerformance() throws { + self.measure(metrics: [XCTCPUMetric(), XCTMemoryMetric()]) { + let text1 = "" + let text2 = "blah blah" + let text3 = "السلام اللعیکم\nو رحمت الله و برکاتو" + + let sut = NSTextView() + sut.textStorage?.append(text1.toPJString(in: sut)) + + sut.textStorage?.append(text2.toPJString(in: sut)) + + sut.textStorage?.append(text3.toPJString(in: sut)) + } + } +} +#endif From 69226c64327d856ad8fd42df1bfa37904295cccb Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 11:02:09 +0100 Subject: [PATCH 31/61] Set base line for `AppKit.MeasurementTests`. --- ...599589E8-9A65-4A13-A7CA-EA286CFECAE6.plist | 22 +++++++++++++ .../PersianJustifyTests.xcbaseline/Info.plist | 33 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 .swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/599589E8-9A65-4A13-A7CA-EA286CFECAE6.plist create mode 100644 .swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/Info.plist diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/599589E8-9A65-4A13-A7CA-EA286CFECAE6.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/599589E8-9A65-4A13-A7CA-EA286CFECAE6.plist new file mode 100644 index 0000000..0e904cb --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/599589E8-9A65-4A13-A7CA-EA286CFECAE6.plist @@ -0,0 +1,22 @@ + + + + + classNames + + MeasurementTests + + testPerformance() + + com.apple.dt.XCTMetric_Memory.physical + + baselineAverage + 13.107200 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/Info.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/Info.plist new file mode 100644 index 0000000..f5a71a0 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/Info.plist @@ -0,0 +1,33 @@ + + + + + runDestinationsByUUID + + 599589E8-9A65-4A13-A7CA-EA286CFECAE6 + + localComputer + + busSpeedInMHz + 0 + cpuCount + 1 + cpuKind + Apple M1 Pro + cpuSpeedInMHz + 0 + logicalCPUCoresPerPackage + 10 + modelCode + MacBookPro18,1 + physicalCPUCoresPerPackage + 10 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + arm64e + + + + From aea4b0442e9e5dd95503ed659b0b232da0701c56 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 11:02:58 +0100 Subject: [PATCH 32/61] Set base line for `UIKit.MeasurementTests`. --- ...99F2CE76-E54F-4D94-A4E3-79239E11F46F.plist | 22 +++++++++++++ .../PersianJustifyTests.xcbaseline/Info.plist | 31 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 .swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/99F2CE76-E54F-4D94-A4E3-79239E11F46F.plist diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/99F2CE76-E54F-4D94-A4E3-79239E11F46F.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/99F2CE76-E54F-4D94-A4E3-79239E11F46F.plist new file mode 100644 index 0000000..4db60e6 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/99F2CE76-E54F-4D94-A4E3-79239E11F46F.plist @@ -0,0 +1,22 @@ + + + + + classNames + + MeasurementTests + + testPerformance() + + com.apple.dt.XCTMetric_CPU.cycles + + baselineAverage + 3585.980600 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/Info.plist b/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/Info.plist index f5a71a0..e6657ee 100644 --- a/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/Info.plist +++ b/.swiftpm/xcode/xcshareddata/xcbaselines/PersianJustifyTests.xcbaseline/Info.plist @@ -28,6 +28,37 @@ targetArchitecture arm64e + 99F2CE76-E54F-4D94-A4E3-79239E11F46F + + localComputer + + busSpeedInMHz + 0 + cpuCount + 1 + cpuKind + Apple M1 Pro + cpuSpeedInMHz + 0 + logicalCPUCoresPerPackage + 10 + modelCode + MacBookPro18,1 + physicalCPUCoresPerPackage + 10 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + arm64 + targetDevice + + modelCode + iPhone16,1 + platformIdentifier + com.apple.platform.iphonesimulator + + From d96853a68ccd89e822107844a18860de304a55a5 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 13:24:13 +0100 Subject: [PATCH 33/61] Revisit [Word] extension. --- Sources/Characters/MiniSpaceCharacter.swift | 2 +- Sources/CoreTextExtensible/Line.swift | 15 +++++++++++++++ Sources/CoreTextExtensible/Word.swift | 17 ----------------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Sources/Characters/MiniSpaceCharacter.swift b/Sources/Characters/MiniSpaceCharacter.swift index 33afdfa..56d786c 100644 --- a/Sources/Characters/MiniSpaceCharacter.swift +++ b/Sources/Characters/MiniSpaceCharacter.swift @@ -4,7 +4,7 @@ import Foundation struct MiniSpaceCharacter { private static let _character: Character = "‌" - private static var stringRepresentation: String { + static var stringRepresentation: String { String(Self._character) } diff --git a/Sources/CoreTextExtensible/Line.swift b/Sources/CoreTextExtensible/Line.swift index 78c511c..6b6bdc1 100644 --- a/Sources/CoreTextExtensible/Line.swift +++ b/Sources/CoreTextExtensible/Line.swift @@ -114,3 +114,18 @@ struct Line { (_line as NSString).range(of: word.stringRepresentation, options: .widthInsensitive) } } + +extension [Word] { + /// Method that will determine if a given word can fit inside the line based on proposed width. + func hasRoomForNextWord(nextWord: Word, proposedWidth: CGFloat, font: Font) -> Bool { + let requiredWidth = nextWord.getWordWidth(font: font) + let currentWidth = getRequiredWidth(with: font) + return (currentWidth + requiredWidth) <= proposedWidth + } + + /// Method that will calculate required width for words. + fileprivate func getRequiredWidth(with font: Font) -> CGFloat { + map { $0.getWordWidth(font: font) } + .reduce(0, +) + } +} diff --git a/Sources/CoreTextExtensible/Word.swift b/Sources/CoreTextExtensible/Word.swift index db91900..c3ab022 100644 --- a/Sources/CoreTextExtensible/Word.swift +++ b/Sources/CoreTextExtensible/Word.swift @@ -83,20 +83,3 @@ extension [Word] { return Line(joinedWords) } } - -extension [Word] { - /// Method that will determine if a given word can fit inside the line based on proposed width. - func hasRoomForNextWord(nextWord: Word, proposedWidth: CGFloat, font: Font) -> Bool { - let requiredWidth = nextWord.getWordWidth(font: font) - let currentWidth = getRequiredWidth(with: font) - return (currentWidth + requiredWidth) <= proposedWidth - } -} - -extension [Word] { - /// Method that will calculate required width for words. - func getRequiredWidth(with font: Font) -> CGFloat { - map { $0.getWordWidth(font: font) } - .reduce(0, +) - } -} From c52ee61325ed7858a61c4f9603d1b1117550f6fb Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 13:30:42 +0100 Subject: [PATCH 34/61] Revisit `SpaceCharacter`. --- Sources/Characters/SpaceCharacter.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/Characters/SpaceCharacter.swift b/Sources/Characters/SpaceCharacter.swift index 1cc1a80..3639667 100644 --- a/Sources/Characters/SpaceCharacter.swift +++ b/Sources/Characters/SpaceCharacter.swift @@ -2,10 +2,10 @@ import Foundation /// Wrapper around a character which is used to append a space. struct SpaceCharacter { - /*private*/ static let character: Character = " " + fileprivate static let _character: Character = " " static var stringRepresentation: String { - String(Self.character) + String(Self._character) } static var attributedStringRepresentation: NSAttributedString { @@ -22,3 +22,9 @@ extension NSMutableAttributedString { append(SpaceCharacter.attributedStringRepresentation) } } + +extension String.SubSequence { + func splitWithSpaceSeparator() -> [Substring] { + split(separator: SpaceCharacter._character) + } +} From 37c75624c21c148b77b555890735936bb7bd1cc4 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 13:30:48 +0100 Subject: [PATCH 35/61] Update call site. --- Sources/CoreTextExtensible/Line.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/CoreTextExtensible/Line.swift b/Sources/CoreTextExtensible/Line.swift index 6b6bdc1..8c55071 100644 --- a/Sources/CoreTextExtensible/Line.swift +++ b/Sources/CoreTextExtensible/Line.swift @@ -27,7 +27,7 @@ struct Line { /// Get every word in a line. func getWords() -> [Word] { - splitLineIntoWords() + _line.splitWithSpaceSeparator() .map { Word($0) } } @@ -80,10 +80,6 @@ struct Line { } } - private func splitLineIntoWords() -> [Substring] { - _line.split(separator: SpaceCharacter.character) - } - private func getExtendedWords( words: [Word], requiredExtend: CGFloat, From cb653a10e199e7e964a50c66a68e489f41ba46fe Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 13:34:29 +0100 Subject: [PATCH 36/61] Revisit `NewLine`. Rename it to `LineBreakCharacter`. Introduce `splitWithLineSeparator` method. --- Sources/Characters/LineBreakCharacter.swift | 32 +++++++++++++++++++++ Sources/Characters/NewLineCharacter.swift | 28 ------------------ 2 files changed, 32 insertions(+), 28 deletions(-) create mode 100644 Sources/Characters/LineBreakCharacter.swift delete mode 100644 Sources/Characters/NewLineCharacter.swift diff --git a/Sources/Characters/LineBreakCharacter.swift b/Sources/Characters/LineBreakCharacter.swift new file mode 100644 index 0000000..8057b96 --- /dev/null +++ b/Sources/Characters/LineBreakCharacter.swift @@ -0,0 +1,32 @@ +import Foundation + +/// Wrapper around a character which is used to break a line. +struct LineBreakCharacter { + fileprivate static let _character: Character = "\n" + + static var stringRepresentation: String { + String(LineBreakCharacter._character) + } + + static var attributedStringRepresentation: NSAttributedString { + NSAttributedString(string: stringRepresentation) + } + + static func + (lhs: LineBreakCharacter, rhs: LineBreakCharacter) -> String { + stringRepresentation + stringRepresentation + } + + static func + (lhs: String, rhs: LineBreakCharacter) -> String { + lhs + stringRepresentation + } +} + +extension String { + func replacingOccurrences(of target: String, with replacement: LineBreakCharacter) -> String { + replacingOccurrences(of: target, with: LineBreakCharacter.stringRepresentation) + } + + func splitWithLineSeparator() -> [Substring] { + split(separator: LineBreakCharacter._character) + } +} diff --git a/Sources/Characters/NewLineCharacter.swift b/Sources/Characters/NewLineCharacter.swift deleted file mode 100644 index 2ca6ff9..0000000 --- a/Sources/Characters/NewLineCharacter.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - -/// Wrapper around a character which is used to append a new line. -struct NewLine { - /*private*/ static let _character: Character = "\n" - - static var stringRepresentation: String { - String(Self._character) - } - - static var attributedStringRepresentation: NSAttributedString { - NSAttributedString(string: stringRepresentation) - } - - static func + (lhs: NewLine, rhs: NewLine) -> String { - stringRepresentation + stringRepresentation - } - - static func + (lhs: String, rhs: NewLine) -> String { - lhs + stringRepresentation - } -} - -extension String { - func replacingOccurrences(of target: String, with replacement: NewLine) -> String { - replacingOccurrences(of: target, with: NewLine.stringRepresentation) - } -} From c3d52f2885de90145e749ee47ceabbf514a4d55e Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Mon, 18 Mar 2024 13:34:35 +0100 Subject: [PATCH 37/61] Update call site. --- Sources/PersianJustify.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 412901f..dfdc444 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -35,17 +35,17 @@ extension String { private func splitStringToLines() -> [Line] { replaceDoubleEmptyLines() - .split(separator: NewLine._character) + .splitWithLineSeparator() .map { Line($0) } } private func replaceDoubleEmptyLines() -> String { - let doubleNextLine = NewLine() + NewLine() + let doubleNextLine = LineBreakCharacter() + LineBreakCharacter() // Replacing double empty lines with one empty line return replacingOccurrences( of: doubleNextLine, - with: NewLine() + with: LineBreakCharacter() ) } @@ -108,7 +108,7 @@ extension String { // To avoid add extra next line at the end of text if index < lines.count-1 { - final.append(NewLine.attributedStringRepresentation) + final.append(LineBreakCharacter.attributedStringRepresentation) } } From cd7cceb5b924902f2a7deec2f40491dcb60cd617 Mon Sep 17 00:00:00 2001 From: Ahmadreza Date: Tue, 19 Mar 2024 12:30:05 +0330 Subject: [PATCH 38/61] feat: Implemented "Create swift.yml" to add automated tests. Implemented automation for unit testing to prevent broken commits into the project. --- .github/workflows/swift.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/swift.yml diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 0000000..cda61a2 --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,22 @@ +# This workflow will build a Swift project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift + +name: Swift + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: macos-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: swift build -v + - name: Run tests + run: swift test -v From 07857b91447a7f670b7921d7a5a074bdc44bc224 Mon Sep 17 00:00:00 2001 From: Ahmadreza Date: Tue, 19 Mar 2024 12:45:14 +0330 Subject: [PATCH 39/61] feat: Updated the Swift tools version to "5.10.0" on iml file to help the GitHub automation do it's things and test the project. --- PersianJustify.iml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 PersianJustify.iml diff --git a/PersianJustify.iml b/PersianJustify.iml new file mode 100644 index 0000000..273cbbc --- /dev/null +++ b/PersianJustify.iml @@ -0,0 +1,16 @@ + + + + + + + + + + ... + + + + + + \ No newline at end of file From a6dc74b2db72fcfca84f13cf72f2418128bd616e Mon Sep 17 00:00:00 2001 From: Ahmadreza Date: Tue, 19 Mar 2024 12:58:24 +0330 Subject: [PATCH 40/61] GitHub: Downgraded the Swift tool version to fix the GitHub automations errors for uneven Swift tool version. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index a8c51d6..8fb2bf4 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.10 +// swift-tools-version: 5.7.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription From fe453921b01a75ac2a77254857ffbfa20beca7b9 Mon Sep 17 00:00:00 2001 From: Ahmadreza Date: Tue, 19 Mar 2024 13:04:50 +0330 Subject: [PATCH 41/61] revert: Restored the Swift tool version to the 5.10 due to ruining the package functionality on my own machine. --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 8fb2bf4..a8c51d6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7.1 +// swift-tools-version: 5.10 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription From 27e0a28691c0dfb1d3af4095e5d12ffe749646f9 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Tue, 19 Mar 2024 10:57:42 +0100 Subject: [PATCH 42/61] Adopt new method signature. Co-Authored-By: Seyed Mojtaba Hosseini Zeidabadi --- Sources/PersianJustify.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index dfdc444..ec8271d 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -16,21 +16,14 @@ extension String { /// Method that will layout words in a `Farsi` calligraphy friendly way. /// - Parameter view: Ancestor view that string will be displayed in. /// - Warning: This is a computed heavy operation. - public func toPJString(in view: View) -> NSAttributedString { + public func toPJString(fittingWidth proposedWidth: CGFloat, font: Font = Font()) -> NSAttributedString { guard !isEmpty else { return NSAttributedString() } let lines = splitStringToLines() - let viewWidth = view.frame.width - - let font: Font = { - lazy var defaultFont = Font() - return view.getFont() ?? defaultFont - }() - - return justify(lines, in: viewWidth, with: font) + return justify(lines, in: proposedWidth, with: font) } private func splitStringToLines() -> [Line] { From 3039c711023a27a0e5307a2cfd079ab318629b11 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Tue, 19 Mar 2024 10:59:15 +0100 Subject: [PATCH 43/61] Removed unused files. --- .../Extensions/String.range(of,options).swift | 9 --------- Sources/Extensions/String.toPJString(in).swift | 17 ----------------- 2 files changed, 26 deletions(-) delete mode 100644 Sources/Extensions/String.range(of,options).swift delete mode 100644 Sources/Extensions/String.toPJString(in).swift diff --git a/Sources/Extensions/String.range(of,options).swift b/Sources/Extensions/String.range(of,options).swift deleted file mode 100644 index c003fb7..0000000 --- a/Sources/Extensions/String.range(of,options).swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -extension String { - - /// Finds and returns the range of the first occurrence of a given string within the string, subject to given options. - func range(of searchString: String, options mask: NSString.CompareOptions = []) -> NSRange { - (self as NSString).range(of: searchString, options: mask) - } -} diff --git a/Sources/Extensions/String.toPJString(in).swift b/Sources/Extensions/String.toPJString(in).swift deleted file mode 100644 index 5fcf71e..0000000 --- a/Sources/Extensions/String.toPJString(in).swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -extension String { - - @available( - *, deprecated, - renamed: "toPJString(fittingWidth:font:)", - message: "This method requires too much information and will not be available from v1.0" - ) - public func toPJString(in view: View) -> NSAttributedString { - let defaultFont = Font() - let font = view.getFont() ?? defaultFont - let parentWidth = view.frame.width - - return toPJString(fittingWidth: parentWidth, font: font) - } -} From 937b3c51471d7abb969ac682f64b67a3d7772ab8 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Tue, 19 Mar 2024 11:02:30 +0100 Subject: [PATCH 44/61] Introduce new method `linesProcessing`. Mohammad's original commit: r - extract linesProcessing Co-Authored-By: Mohamad Rahmani --- Sources/PersianJustify.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index ec8271d..a55135c 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -108,6 +108,18 @@ extension String { return final } + private func linesProcessing(lines: [Line]) -> (index: Int, line: [Word]) { + var line = [Word]() + var index = 0 + + lines.enumerated().lazy.forEach { + line = $1.getWords() + index = $0 + } + + return (index, line) + } + private func justifyLine( from words: [Word], in proposedWidth: CGFloat, From 60ac543126e6593baff1fbfc8c752d8bd05aca69 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Tue, 19 Mar 2024 11:03:18 +0100 Subject: [PATCH 45/61] Update call site, remove foreach and use new method. Mohammad's original coomit: r - use CharacterSet newlines instead of hardcoding newline + use components String's method and return an actual Array of String instead of a Substring (since you'll need to convert that again). Co-Authored-By: Mohamad Rahmani --- Sources/PersianJustify.swift | 90 ++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index a55135c..490a7de 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -49,62 +49,60 @@ extension String { ) -> NSAttributedString { let final = NSMutableAttributedString(string: "") - lines.enumerated().forEach { index, line in - let words = line.getWords() - - var currentLineWords: [Word] = [] - - words.forEach { word in - let canAddNewWord: Bool = { - let lineHasRoomForNextWord = currentLineWords.hasRoomForNextWord( - nextWord: word, - proposedWidth: proposedWidth, - font: font - ) - - lazy var isLineEmpty = currentLineWords.isEmpty - - return lineHasRoomForNextWord || isLineEmpty - }() - - if canAddNewWord { - currentLineWords.append(word) - } - // Line is filled and is ready to justify - else { - let justifiedLine = justifyLine( - from: currentLineWords, - in: proposedWidth, - with: font, - isLastLineInParagraph: false - ) - - // Appending space at the end - justifiedLine.appendSpaceCharacter() - - final.append(justifiedLine) - - currentLineWords = [word] - } - } + let (index, words) = linesProcessing(lines: lines) + + var currentLineWords: [Word] = [] + + words.forEach { word in + let canAddNewWord: Bool = { + let lineHasRoomForNextWord = currentLineWords.hasRoomForNextWord( + nextWord: word, + proposedWidth: proposedWidth, + font: font + ) - if !currentLineWords.isEmpty { - let extracted = justifyLine( + lazy var isLineEmpty = currentLineWords.isEmpty + + return lineHasRoomForNextWord || isLineEmpty + }() + + if canAddNewWord { + currentLineWords.append(word) + } + // Line is filled and is ready to justify + else { + let justifiedLine = justifyLine( from: currentLineWords, in: proposedWidth, with: font, - isLastLineInParagraph: true + isLastLineInParagraph: false ) - final.append(extracted) - } + // Appending space at the end + justifiedLine.appendSpaceCharacter() - // To avoid add extra next line at the end of text - if index < lines.count-1 { - final.append(LineBreakCharacter.attributedStringRepresentation) + final.append(justifiedLine) + + currentLineWords = [word] } } + if !currentLineWords.isEmpty { + let extracted = justifyLine( + from: currentLineWords, + in: proposedWidth, + with: font, + isLastLineInParagraph: true + ) + + final.append(extracted) + } + + // To avoid add extra next line at the end of text + if index < lines.count-1 { + final.append(LineBreakCharacter.attributedStringRepresentation) + } + return final } From ccf208072a5f5aed93deef8e633777df7a924716 Mon Sep 17 00:00:00 2001 From: Seyed Mojtaba Hosseini Zeidabadi Date: Thu, 21 Mar 2024 13:20:53 +0330 Subject: [PATCH 46/61] build: use release configurations for the tests --- .github/workflows/swift.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 76851ba..1841e75 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -18,5 +18,5 @@ jobs: - uses: actions/checkout@v3 - name: Build run: swift build -v - - name: Run tests - run: swift test -v + - name: Run tests with release configurations + run: swift test -v -c release From 26adb6385c1cebe16150205c0d4428774ebe7c36 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 12:16:44 +0100 Subject: [PATCH 47/61] Revert "Introduce new method `linesProcessing`." This reverts commit 937b3c51471d7abb969ac682f64b67a3d7772ab8. --- Sources/PersianJustify.swift | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 490a7de..0b23837 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -106,18 +106,6 @@ extension String { return final } - private func linesProcessing(lines: [Line]) -> (index: Int, line: [Word]) { - var line = [Word]() - var index = 0 - - lines.enumerated().lazy.forEach { - line = $1.getWords() - index = $0 - } - - return (index, line) - } - private func justifyLine( from words: [Word], in proposedWidth: CGFloat, From e79436734c8e30f85924a1568ee763ec3d6f2f94 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 12:16:49 +0100 Subject: [PATCH 48/61] Revert "Update call site, remove foreach and use new method." This reverts commit 60ac543126e6593baff1fbfc8c752d8bd05aca69. --- Sources/PersianJustify.swift | 90 ++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/Sources/PersianJustify.swift b/Sources/PersianJustify.swift index 0b23837..ec8271d 100644 --- a/Sources/PersianJustify.swift +++ b/Sources/PersianJustify.swift @@ -49,58 +49,60 @@ extension String { ) -> NSAttributedString { let final = NSMutableAttributedString(string: "") - let (index, words) = linesProcessing(lines: lines) - - var currentLineWords: [Word] = [] - - words.forEach { word in - let canAddNewWord: Bool = { - let lineHasRoomForNextWord = currentLineWords.hasRoomForNextWord( - nextWord: word, - proposedWidth: proposedWidth, - font: font - ) - - lazy var isLineEmpty = currentLineWords.isEmpty - - return lineHasRoomForNextWord || isLineEmpty - }() - - if canAddNewWord { - currentLineWords.append(word) + lines.enumerated().forEach { index, line in + let words = line.getWords() + + var currentLineWords: [Word] = [] + + words.forEach { word in + let canAddNewWord: Bool = { + let lineHasRoomForNextWord = currentLineWords.hasRoomForNextWord( + nextWord: word, + proposedWidth: proposedWidth, + font: font + ) + + lazy var isLineEmpty = currentLineWords.isEmpty + + return lineHasRoomForNextWord || isLineEmpty + }() + + if canAddNewWord { + currentLineWords.append(word) + } + // Line is filled and is ready to justify + else { + let justifiedLine = justifyLine( + from: currentLineWords, + in: proposedWidth, + with: font, + isLastLineInParagraph: false + ) + + // Appending space at the end + justifiedLine.appendSpaceCharacter() + + final.append(justifiedLine) + + currentLineWords = [word] + } } - // Line is filled and is ready to justify - else { - let justifiedLine = justifyLine( + + if !currentLineWords.isEmpty { + let extracted = justifyLine( from: currentLineWords, in: proposedWidth, with: font, - isLastLineInParagraph: false + isLastLineInParagraph: true ) - // Appending space at the end - justifiedLine.appendSpaceCharacter() - - final.append(justifiedLine) - - currentLineWords = [word] + final.append(extracted) } - } - - if !currentLineWords.isEmpty { - let extracted = justifyLine( - from: currentLineWords, - in: proposedWidth, - with: font, - isLastLineInParagraph: true - ) - - final.append(extracted) - } - // To avoid add extra next line at the end of text - if index < lines.count-1 { - final.append(LineBreakCharacter.attributedStringRepresentation) + // To avoid add extra next line at the end of text + if index < lines.count-1 { + final.append(LineBreakCharacter.attributedStringRepresentation) + } } return final From e565ab4384979d70ee9d27385efeb31a855ad1ab Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 12:43:52 +0100 Subject: [PATCH 49/61] Move `Typealias` file. --- Sources/PersianJustify/Extensions/Typealias.swift | 9 --------- Sources/PersianJustify/PersianJustify.swift | 1 - Sources/{HeaderTypes.swift => Typealias.swift} | 0 3 files changed, 10 deletions(-) delete mode 100644 Sources/PersianJustify/Extensions/Typealias.swift rename Sources/{HeaderTypes.swift => Typealias.swift} (100%) diff --git a/Sources/PersianJustify/Extensions/Typealias.swift b/Sources/PersianJustify/Extensions/Typealias.swift deleted file mode 100644 index 6bb0d35..0000000 --- a/Sources/PersianJustify/Extensions/Typealias.swift +++ /dev/null @@ -1,9 +0,0 @@ -#if canImport(UIKit) -import UIKit -public typealias Font = UIFont -public typealias View = UIView -#elseif canImport(AppKit) -import AppKit -public typealias Font = NSFont -public typealias View = NSView -#endif diff --git a/Sources/PersianJustify/PersianJustify.swift b/Sources/PersianJustify/PersianJustify.swift index ec8271d..6bde9d4 100644 --- a/Sources/PersianJustify/PersianJustify.swift +++ b/Sources/PersianJustify/PersianJustify.swift @@ -11,7 +11,6 @@ import UIKit import AppKit #endif -// MARK: - Usage using toPJString function extension String { /// Method that will layout words in a `Farsi` calligraphy friendly way. /// - Parameter view: Ancestor view that string will be displayed in. diff --git a/Sources/HeaderTypes.swift b/Sources/Typealias.swift similarity index 100% rename from Sources/HeaderTypes.swift rename to Sources/Typealias.swift From 15a76218723564717851de7800511ccb7ad88b64 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 12:44:09 +0100 Subject: [PATCH 50/61] Specify target path. --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index a0d87d0..00d280e 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,8 @@ let package = Package( ], targets: [ .target( - name: "PersianJustify" + name: "PersianJustify", + path: "Sources" ), .testTarget( name: "PersianJustifyTests", From 9660b7e2ac89a8cbe494d7dbe2f963535633376f Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 12:54:31 +0100 Subject: [PATCH 51/61] Update MesurementTests.swift --- .../PersianJustifyTests/MesurementTests.swift | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/Tests/PersianJustifyTests/MesurementTests.swift b/Tests/PersianJustifyTests/MesurementTests.swift index 0c87b8b..095ef8c 100644 --- a/Tests/PersianJustifyTests/MesurementTests.swift +++ b/Tests/PersianJustifyTests/MesurementTests.swift @@ -4,16 +4,18 @@ import XCTest final class MeasurementTests: XCTestCase { func testPerformance() throws { self.measure(metrics: [XCTCPUMetric(), XCTMemoryMetric()]) { - let text1 = "" - let text2 = "blah blah" - let text3 = "السلام اللعیکم\nو رحمت الله و برکاتو" + let texts = [ + "", + "blah blah", + "السلام اللعیکم\nو رحمت الله و برکاتو" + ] let sut = UILabel() - sut.attributedText = text1.toPJString(in: sut) + let fittingWidth = sut.frame.width - sut.attributedText = text2.toPJString(in: sut) - - sut.attributedText = text3.toPJString(in: sut) + texts.forEach { text in + sut.attributedText = text.toPJString(fittingWidth: fittingWidth) + } } } } @@ -23,16 +25,18 @@ final class MeasurementTests: XCTestCase { final class MeasurementTests: XCTestCase { func testPerformance() throws { self.measure(metrics: [XCTCPUMetric(), XCTMemoryMetric()]) { - let text1 = "" - let text2 = "blah blah" - let text3 = "السلام اللعیکم\nو رحمت الله و برکاتو" + let texts = [ + "", + "blah blah", + "السلام اللعیکم\nو رحمت الله و برکاتو" + ] let sut = NSTextView() - sut.textStorage?.append(text1.toPJString(in: sut)) - - sut.textStorage?.append(text2.toPJString(in: sut)) + let fittingWidth = sut.frame.width - sut.textStorage?.append(text3.toPJString(in: sut)) + texts.forEach { text in + sut.textStorage?.append(text.toPJString(fittingWidth: fittingWidth, font: sut.font!)) + } } } } From bfee432562f32911bf251de68293cd04ff5444b5 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 13:33:10 +0100 Subject: [PATCH 52/61] Update MesurementTests.swift --- Tests/PersianJustifyTests/MesurementTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/PersianJustifyTests/MesurementTests.swift b/Tests/PersianJustifyTests/MesurementTests.swift index 095ef8c..3073044 100644 --- a/Tests/PersianJustifyTests/MesurementTests.swift +++ b/Tests/PersianJustifyTests/MesurementTests.swift @@ -14,7 +14,7 @@ final class MeasurementTests: XCTestCase { let fittingWidth = sut.frame.width texts.forEach { text in - sut.attributedText = text.toPJString(fittingWidth: fittingWidth) + sut.attributedText = text.toPJString(fittingWidth: fittingWidth, font: sut.font) } } } From 5358f931cff321e15a1f5769c9c5b47031a899e4 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 13:33:30 +0100 Subject: [PATCH 53/61] Add a new typealias named `Label`. --- Sources/Typealias.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Typealias.swift b/Sources/Typealias.swift index 71dcdbc..249cd21 100644 --- a/Sources/Typealias.swift +++ b/Sources/Typealias.swift @@ -7,9 +7,11 @@ import UIKit public typealias Font = UIFont public typealias View = UIView +public typealias Label = UILabel #elseif canImport(AppKit) import AppKit public typealias Font = NSFont public typealias View = NSView +public typealias Label = NSTextView #endif From 3f6f92add224013a2064014c6bcf5c96456481a7 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 13:34:07 +0100 Subject: [PATCH 54/61] Update method documentation. --- Sources/PersianJustify/PersianJustify.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/PersianJustify/PersianJustify.swift b/Sources/PersianJustify/PersianJustify.swift index 6bde9d4..11e1bdb 100644 --- a/Sources/PersianJustify/PersianJustify.swift +++ b/Sources/PersianJustify/PersianJustify.swift @@ -12,8 +12,7 @@ import AppKit #endif extension String { - /// Method that will layout words in a `Farsi` calligraphy friendly way. - /// - Parameter view: Ancestor view that string will be displayed in. + /// Method that will layouts string in a `Farsi` calligraphy friendly way. /// - Warning: This is a computed heavy operation. public func toPJString(fittingWidth proposedWidth: CGFloat, font: Font = Font()) -> NSAttributedString { guard !isEmpty else { From e5db0c65059d44d403b1d78fba7fbd592debc0d3 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 13:34:34 +0100 Subject: [PATCH 55/61] Remove default font argument value. --- Sources/PersianJustify/PersianJustify.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/PersianJustify/PersianJustify.swift b/Sources/PersianJustify/PersianJustify.swift index 11e1bdb..f2edb23 100644 --- a/Sources/PersianJustify/PersianJustify.swift +++ b/Sources/PersianJustify/PersianJustify.swift @@ -14,7 +14,7 @@ import AppKit extension String { /// Method that will layouts string in a `Farsi` calligraphy friendly way. /// - Warning: This is a computed heavy operation. - public func toPJString(fittingWidth proposedWidth: CGFloat, font: Font = Font()) -> NSAttributedString { + public func toPJString(fittingWidth proposedWidth: CGFloat, font: Font) -> NSAttributedString { guard !isEmpty else { return NSAttributedString() } From 2cdb47b80f0c6b6fff39d6aef29407f5c689caf0 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 13:35:09 +0100 Subject: [PATCH 56/61] Introduce new error `PersianJustifyFailure`. --- Sources/PersianJustify/PersianJustify.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Sources/PersianJustify/PersianJustify.swift b/Sources/PersianJustify/PersianJustify.swift index f2edb23..81713ba 100644 --- a/Sources/PersianJustify/PersianJustify.swift +++ b/Sources/PersianJustify/PersianJustify.swift @@ -121,3 +121,20 @@ extension String { ) } } +public enum PersianJustifyFailure: LocalizedError { + /// Failure to get font from the given view. + case getFont(View) + + /// Failure to get text from the given view. + case getText(View) + + public var errorDescription: String? { + switch self { + case let .getFont(view): + return "Failure to get font from \(view)" + + case let .getText(view): + return "Failure to get text from the \(view)" + } + } +} From 39f66d80ef42fc869e8b1eb39dae3608aa103a1d Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 13:36:14 +0100 Subject: [PATCH 57/61] Add new method on `Label` named `toPJString`. --- Sources/PersianJustify/PersianJustify.swift | 41 +++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Sources/PersianJustify/PersianJustify.swift b/Sources/PersianJustify/PersianJustify.swift index 81713ba..e0b9304 100644 --- a/Sources/PersianJustify/PersianJustify.swift +++ b/Sources/PersianJustify/PersianJustify.swift @@ -121,6 +121,47 @@ extension String { ) } } + +#if canImport(UIKit) +extension Label { + /// Method that will layouts text in a `Farsi` calligraphy friendly way. + /// - Warning: This is a computed heavy operation. + public func toPJString(idealWidth: CGFloat? = nil) throws { + guard let text else { + throw PersianJustifyFailure.getText(self) + } + + guard let font else { + throw PersianJustifyFailure.getFont(self) + } + + let proposedWidth = idealWidth ?? frame.width + + let realignedText = text.toPJString(fittingWidth: proposedWidth, font: font) + + attributedText = realignedText + } +} +#elseif canImport(AppKit) +extension Label { + /// Method that will layouts text in a `Farsi` calligraphy friendly way. + /// - Warning: This is a computed heavy operation. + public func toPJString(idealWidth: CGFloat? = nil) throws { + let text = string + + guard let font else { + throw PersianJustifyFailure.getFont + } + + let proposedWidth = idealWidth ?? frame.width + + let realignedText = text.toPJString(fittingWidth: proposedWidth, font: font) + + textStorage?.append(realignedText) + } +} +#endif + public enum PersianJustifyFailure: LocalizedError { /// Failure to get font from the given view. case getFont(View) From 1b85866b40b767da763173cd8b4622f73fe6f863 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 13:38:30 +0100 Subject: [PATCH 58/61] Decouple public interfaces from privates. --- Sources/PersianJustify/PersianJustify.swift | 82 +++++++++++---------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/Sources/PersianJustify/PersianJustify.swift b/Sources/PersianJustify/PersianJustify.swift index e0b9304..8117f99 100644 --- a/Sources/PersianJustify/PersianJustify.swift +++ b/Sources/PersianJustify/PersianJustify.swift @@ -23,7 +23,49 @@ extension String { return justify(lines, in: proposedWidth, with: font) } +} + +#if canImport(UIKit) +extension Label { + /// Method that will layouts text in a `Farsi` calligraphy friendly way. + /// - Warning: This is a computed heavy operation. + public func toPJString(idealWidth: CGFloat? = nil) throws { + guard let text else { + throw PersianJustifyFailure.getText(self) + } + + guard let font else { + throw PersianJustifyFailure.getFont(self) + } + + let proposedWidth = idealWidth ?? frame.width + + let realignedText = text.toPJString(fittingWidth: proposedWidth, font: font) + + attributedText = realignedText + } +} +#elseif canImport(AppKit) +extension Label { + /// Method that will layouts text in a `Farsi` calligraphy friendly way. + /// - Warning: This is a computed heavy operation. + public func toPJString(idealWidth: CGFloat? = nil) throws { + let text = string + + guard let font else { + throw PersianJustifyFailure.getFont + } + + let proposedWidth = idealWidth ?? frame.width + + let realignedText = text.toPJString(fittingWidth: proposedWidth, font: font) + + textStorage?.append(realignedText) + } +} +#endif +extension String { private func splitStringToLines() -> [Line] { replaceDoubleEmptyLines() .splitWithLineSeparator() @@ -122,46 +164,6 @@ extension String { } } -#if canImport(UIKit) -extension Label { - /// Method that will layouts text in a `Farsi` calligraphy friendly way. - /// - Warning: This is a computed heavy operation. - public func toPJString(idealWidth: CGFloat? = nil) throws { - guard let text else { - throw PersianJustifyFailure.getText(self) - } - - guard let font else { - throw PersianJustifyFailure.getFont(self) - } - - let proposedWidth = idealWidth ?? frame.width - - let realignedText = text.toPJString(fittingWidth: proposedWidth, font: font) - - attributedText = realignedText - } -} -#elseif canImport(AppKit) -extension Label { - /// Method that will layouts text in a `Farsi` calligraphy friendly way. - /// - Warning: This is a computed heavy operation. - public func toPJString(idealWidth: CGFloat? = nil) throws { - let text = string - - guard let font else { - throw PersianJustifyFailure.getFont - } - - let proposedWidth = idealWidth ?? frame.width - - let realignedText = text.toPJString(fittingWidth: proposedWidth, font: font) - - textStorage?.append(realignedText) - } -} -#endif - public enum PersianJustifyFailure: LocalizedError { /// Failure to get font from the given view. case getFont(View) From 7b621d90b4813628c3563752880f4dfea327c8c7 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 13:39:35 +0100 Subject: [PATCH 59/61] Remove `getFont` and related test methods. --- .../Extensions/View.getFont.swift | 23 ------- Tests/PersianJustifyTests/GetFontTests.swift | 69 ------------------- 2 files changed, 92 deletions(-) delete mode 100644 Sources/PersianJustify/Extensions/View.getFont.swift delete mode 100644 Tests/PersianJustifyTests/GetFontTests.swift diff --git a/Sources/PersianJustify/Extensions/View.getFont.swift b/Sources/PersianJustify/Extensions/View.getFont.swift deleted file mode 100644 index 53ee49d..0000000 --- a/Sources/PersianJustify/Extensions/View.getFont.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -#if canImport(UIKit) -import UIKit -#elseif canImport(AppKit) -import AppKit -#endif - -private let key = "font" - -extension View { - func getFont() -> Font? { - // Opaque pointer to font in the given view - let fontSelector = Selector(key) - - // Arguably this defensive check can be omitted, - // Since if a view has a font property, we can fetch font from - guard responds(to: fontSelector) else { - return nil - } - - return value(forKey: key) as? Font - } -} diff --git a/Tests/PersianJustifyTests/GetFontTests.swift b/Tests/PersianJustifyTests/GetFontTests.swift deleted file mode 100644 index 088a5d3..0000000 --- a/Tests/PersianJustifyTests/GetFontTests.swift +++ /dev/null @@ -1,69 +0,0 @@ -import XCTest -@testable import PersianJustify - -#if canImport(UIKit) -final class GetFontTests: XCTestCase { - - func testUILabelGetFont() { - let sut = UILabel() - let expFont = UIFont.systemFont(ofSize: 100) - sut.font = expFont - - XCTAssertEqual(sut.font, expFont) - XCTAssertEqual(sut.getFont(), expFont) - XCTAssertEqual(sut.getFont(), sut.font) - } - - func testUITextFieldGetFont() { - let sut = UITextField() - let expFont = UIFont.systemFont(ofSize: 100) - sut.font = expFont - - XCTAssertEqual(sut.font, expFont) - XCTAssertEqual(sut.getFont(), expFont) - XCTAssertEqual(sut.getFont(), sut.font) - } - - func testUITextViewGetFont() { - let sut = UITextView() - let expFont = UIFont.systemFont(ofSize: 100) - sut.font = expFont - - XCTAssertEqual(sut.font, expFont) - XCTAssertEqual(sut.getFont(), expFont) - XCTAssertEqual(sut.getFont(), sut.font) - } - - func testUIViewGetFont() { - let sut = UIView() - XCTAssertNil(sut.getFont()) - } -} -#endif - -#if canImport(AppKit) -final class GetFontTests: XCTestCase { - - // There is no such a thing as `NSLabel` in the `AppKit` - - func testNSTextFieldGetFont() { - let sut = NSTextField() - let expFont = NSFont.systemFont(ofSize: 100) - sut.font = expFont - - XCTAssertEqual(sut.font, expFont) - XCTAssertEqual(sut.getFont(), expFont) - XCTAssertEqual(sut.getFont(), sut.font) - } - - func testUITextViewGetFont() { - let sut = NSTextView() - let expFont = NSFont.systemFont(ofSize: 100) - sut.font = expFont - - XCTAssertEqual(sut.font, expFont) - XCTAssertEqual(sut.getFont(), expFont) - XCTAssertEqual(sut.getFont(), sut.font) - } -} -#endif From 84e8034049f6a4ab3a496740cff69536b7c664b5 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 13:42:52 +0100 Subject: [PATCH 60/61] Add `debugDescription` for errorDescription in `PersianJustifyFailure`. --- Sources/PersianJustify/PersianJustify.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/PersianJustify/PersianJustify.swift b/Sources/PersianJustify/PersianJustify.swift index 8117f99..52157dd 100644 --- a/Sources/PersianJustify/PersianJustify.swift +++ b/Sources/PersianJustify/PersianJustify.swift @@ -174,10 +174,10 @@ public enum PersianJustifyFailure: LocalizedError { public var errorDescription: String? { switch self { case let .getFont(view): - return "Failure to get font from \(view)" + return "Failure to get font from \(view.debugDescription)" case let .getText(view): - return "Failure to get text from the \(view)" + return "Failure to get text from the \(view.debugDescription)" } } } From 0fca2aab3136d13924a2fee022f4cb70475af854 Mon Sep 17 00:00:00 2001 From: Sajad Vishkai Date: Thu, 21 Mar 2024 13:44:27 +0100 Subject: [PATCH 61/61] Rename `PersianJustifyFailure` to `PersianJustifyError`. --- Sources/PersianJustify/PersianJustify.swift | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/PersianJustify/PersianJustify.swift b/Sources/PersianJustify/PersianJustify.swift index 52157dd..390e3f5 100644 --- a/Sources/PersianJustify/PersianJustify.swift +++ b/Sources/PersianJustify/PersianJustify.swift @@ -31,11 +31,11 @@ extension Label { /// - Warning: This is a computed heavy operation. public func toPJString(idealWidth: CGFloat? = nil) throws { guard let text else { - throw PersianJustifyFailure.getText(self) + throw PersianJustifyError.getTextFailure(self) } guard let font else { - throw PersianJustifyFailure.getFont(self) + throw PersianJustifyError.getFontFailure(self) } let proposedWidth = idealWidth ?? frame.width @@ -164,20 +164,20 @@ extension String { } } -public enum PersianJustifyFailure: LocalizedError { - /// Failure to get font from the given view. - case getFont(View) - +public enum PersianJustifyError: LocalizedError { /// Failure to get text from the given view. - case getText(View) + case getTextFailure(View) + + /// Failure to get font from the given view. + case getFontFailure(View) public var errorDescription: String? { switch self { - case let .getFont(view): - return "Failure to get font from \(view.debugDescription)" - - case let .getText(view): + case let .getTextFailure(view): return "Failure to get text from the \(view.debugDescription)" + + case let .getFontFailure(view): + return "Failure to get font from \(view.debugDescription)" } } }