Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
081c98b
Remove comments.
sajacl Mar 18, 2024
24b2e1c
Remove extra white spaces.
sajacl Mar 18, 2024
3b80603
Revisit file in order to improve readability.
sajacl Mar 18, 2024
1201368
Introduce `ArabicStringEvaluator`.
sajacl Mar 18, 2024
19ec1d6
Introduce `ExtenderCharacter`.
sajacl Mar 18, 2024
a907f6f
Introduce `MiniSpaceCharacter `.
sajacl Mar 18, 2024
6b1ec6c
Introduce `NewLine `.
sajacl Mar 18, 2024
63fd8cb
Introduce `SpaceCharacter `.
sajacl Mar 18, 2024
a5e16ec
Introduce `Line`.
sajacl Mar 18, 2024
165a042
Introduce `Word`.
sajacl Mar 18, 2024
f93975f
Introduce `HeaderTypes`.
sajacl Mar 18, 2024
96ce0c4
Remove unneeded variables.
sajacl Mar 18, 2024
24ef91c
Introduce `splitStringToLines` method.
sajacl Mar 18, 2024
92e2724
Introduce `replaceDoubleEmptyLines` method.
sajacl Mar 18, 2024
9152d2b
Introduce `justify` method.
sajacl Mar 18, 2024
dea74ac
Remove `getJustifiedLine` method.
sajacl Mar 18, 2024
e126f60
Remove `getExtendedWords ` method.
sajacl Mar 18, 2024
57130d4
Remove `isArabic` computed property.
sajacl Mar 18, 2024
d78d161
Remove `getWords ` method.
sajacl Mar 18, 2024
8819450
Remove `getTotalWidth ` method.
sajacl Mar 18, 2024
d542b74
Remove `getWordWidth` method.
sajacl Mar 18, 2024
eaecc58
Remove `getRange ` method.
sajacl Mar 18, 2024
6fcb92f
Remove `isSupportExtender` method.
sajacl Mar 18, 2024
bcf1176
Remove `hasRoomForNextWord ` method.
sajacl Mar 18, 2024
ce47538
Remove `joinWithSpace ` method.
sajacl Mar 18, 2024
b328b5f
Introduce `justifyLine ` method.
sajacl Mar 18, 2024
871ac88
Refactor `toPJString` method.
sajacl Mar 18, 2024
c996e36
Add `MeasurementTests` file.
sajacl Mar 18, 2024
35e0daa
Introduce `MeasurementTests ` `UIKit` test case.
sajacl Mar 18, 2024
cc4c4bd
Introduce `MeasurementTests ` `AppKit ` test case.
sajacl Mar 18, 2024
69226c6
Set base line for `AppKit.MeasurementTests`.
sajacl Mar 18, 2024
aea4b04
Set base line for `UIKit.MeasurementTests`.
sajacl Mar 18, 2024
d96853a
Revisit [Word] extension.
sajacl Mar 18, 2024
c52ee61
Revisit `SpaceCharacter`.
sajacl Mar 18, 2024
37c7562
Update call site.
sajacl Mar 18, 2024
cb653a1
Revisit `NewLine`.
sajacl Mar 18, 2024
c3d52f2
Update call site.
sajacl Mar 18, 2024
cd7cceb
feat: Implemented "Create swift.yml" to add automated tests.
HappyIosDeveloper Mar 19, 2024
07857b9
feat: Updated the Swift tools version to "5.10.0" on iml file to help…
HappyIosDeveloper Mar 19, 2024
a6dc74b
GitHub: Downgraded the Swift tool version to fix the GitHub automatio…
HappyIosDeveloper Mar 19, 2024
fe45392
revert: Restored the Swift tool version to the 5.10 due to ruining th…
HappyIosDeveloper Mar 19, 2024
385c01e
Merge remote-tracking branch 'upstream/main' into rework-package
sajacl Mar 19, 2024
27e0a28
Adopt new method signature.
sajacl Mar 19, 2024
3039c71
Removed unused files.
sajacl Mar 19, 2024
937b3c5
Introduce new method `linesProcessing`.
sajacl Mar 19, 2024
60ac543
Update call site, remove foreach and use new method.
sajacl Mar 19, 2024
ccf2080
build: use release configurations for the tests
MojtabaHs Mar 21, 2024
26adb63
Revert "Introduce new method `linesProcessing`."
sajacl Mar 21, 2024
e794367
Revert "Update call site, remove foreach and use new method."
sajacl Mar 21, 2024
4a1d246
Merge remote-tracking branch 'upstream/main' into rework-package
sajacl Mar 21, 2024
e565ab4
Move `Typealias` file.
sajacl Mar 21, 2024
15a7621
Specify target path.
sajacl Mar 21, 2024
9660b7e
Update MesurementTests.swift
sajacl Mar 21, 2024
bfee432
Update MesurementTests.swift
sajacl Mar 21, 2024
5358f93
Add a new typealias named `Label`.
sajacl Mar 21, 2024
3f6f92a
Update method documentation.
sajacl Mar 21, 2024
e5db0c6
Remove default font argument value.
sajacl Mar 21, 2024
2cdb47b
Introduce new error `PersianJustifyFailure`.
sajacl Mar 21, 2024
39f66d8
Add new method on `Label` named `toPJString`.
sajacl Mar 21, 2024
1b85866
Decouple public interfaces from privates.
sajacl Mar 21, 2024
7b621d9
Remove `getFont` and related test methods.
sajacl Mar 21, 2024
84e8034
Add `debugDescription` for errorDescription in `PersianJustifyFailure`.
sajacl Mar 21, 2024
0fca2aa
Rename `PersianJustifyFailure` to `PersianJustifyError`.
sajacl Mar 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>classNames</key>
<dict>
<key>MeasurementTests</key>
<dict>
<key>testPerformance()</key>
<dict>
<key>com.apple.dt.XCTMetric_Memory.physical</key>
<dict>
<key>baselineAverage</key>
<real>13.107200</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>classNames</key>
<dict>
<key>MeasurementTests</key>
<dict>
<key>testPerformance()</key>
<dict>
<key>com.apple.dt.XCTMetric_CPU.cycles</key>
<dict>
<key>baselineAverage</key>
<real>3585.980600</real>
<key>baselineIntegrationDisplayName</key>
<string>Local Baseline</string>
</dict>
</dict>
</dict>
</dict>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>runDestinationsByUUID</key>
<dict>
<key>599589E8-9A65-4A13-A7CA-EA286CFECAE6</key>
<dict>
<key>localComputer</key>
<dict>
<key>busSpeedInMHz</key>
<integer>0</integer>
<key>cpuCount</key>
<integer>1</integer>
<key>cpuKind</key>
<string>Apple M1 Pro</string>
<key>cpuSpeedInMHz</key>
<integer>0</integer>
<key>logicalCPUCoresPerPackage</key>
<integer>10</integer>
<key>modelCode</key>
<string>MacBookPro18,1</string>
<key>physicalCPUCoresPerPackage</key>
<integer>10</integer>
<key>platformIdentifier</key>
<string>com.apple.platform.macosx</string>
</dict>
<key>targetArchitecture</key>
<string>arm64e</string>
</dict>
<key>99F2CE76-E54F-4D94-A4E3-79239E11F46F</key>
<dict>
<key>localComputer</key>
<dict>
<key>busSpeedInMHz</key>
<integer>0</integer>
<key>cpuCount</key>
<integer>1</integer>
<key>cpuKind</key>
<string>Apple M1 Pro</string>
<key>cpuSpeedInMHz</key>
<integer>0</integer>
<key>logicalCPUCoresPerPackage</key>
<integer>10</integer>
<key>modelCode</key>
<string>MacBookPro18,1</string>
<key>physicalCPUCoresPerPackage</key>
<integer>10</integer>
<key>platformIdentifier</key>
<string>com.apple.platform.macosx</string>
</dict>
<key>targetArchitecture</key>
<string>arm64</string>
<key>targetDevice</key>
<dict>
<key>modelCode</key>
<string>iPhone16,1</string>
<key>platformIdentifier</key>
<string>com.apple.platform.iphonesimulator</string>
</dict>
</dict>
</dict>
</dict>
</plist>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ 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"]
),
],
dependencies: [
.package(url: "https://github.com/ArtSabintsev/FontBlaster", from: "5.3.0"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.15.4"),
],
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",
path: "Sources"
),
.testTarget(
name: "PersianJustifyTests",
dependencies: [
Expand Down
17 changes: 17 additions & 0 deletions Sources/ArabicCharacterEvaluator.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
14 changes: 14 additions & 0 deletions Sources/Characters/ExtenderCharacter.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
32 changes: 32 additions & 0 deletions Sources/Characters/LineBreakCharacter.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
18 changes: 18 additions & 0 deletions Sources/Characters/MiniSpaceCharacter.swift
Original file line number Diff line number Diff line change
@@ -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 = "‌"

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
}
}
30 changes: 30 additions & 0 deletions Sources/Characters/SpaceCharacter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

/// Wrapper around a character which is used to append a space.
struct SpaceCharacter {
fileprivate 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)
}
}

extension String.SubSequence {
func splitWithSpaceSeparator() -> [Substring] {
split(separator: SpaceCharacter._character)
}
}
127 changes: 127 additions & 0 deletions Sources/CoreTextExtensible/Line.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
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] {
_line.splitWithSpaceSeparator()
.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 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)
}
}

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, +)
}
}
Loading