├── Examples
├── Mac
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj
│ │ └── Main.storyboard
│ ├── Info.plist
│ └── ViewController.swift
├── SoulverTextKit.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Mac Example.xcscheme
└── iOS
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── Info.plist
│ ├── SceneDelegate.swift
│ └── ViewController.swift
├── Images
├── AfterEquals.png
├── AfterPipe.png
├── AfterTab.png
└── Example.gif
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── SoulverTextKit
│ ├── Helpers
│ └── StringByParagraphs.swift
│ └── SoulverTextKit.swift
└── Tests
├── LinuxMain.swift
└── SoulverTextKitTests
├── SoulverTextKitTests.swift
└── XCTestManifests.swift
/Examples/Mac/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SoulverTextKit
4 | //
5 | // Created by Zac Cohan on 9/1/21.
6 | //
7 |
8 | import Cocoa
9 |
10 | @main
11 | class AppDelegate: NSObject, NSApplicationDelegate {
12 |
13 | func applicationDidFinishLaunching(_ aNotification: Notification) {
14 | // Insert code here to initialize your application
15 | }
16 |
17 | func applicationWillTerminate(_ aNotification: Notification) {
18 | // Insert code here to tear down your application
19 | }
20 |
21 |
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/Examples/Mac/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Examples/Mac/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "2x",
51 | "size" : "512x512"
52 | }
53 | ],
54 | "info" : {
55 | "author" : "xcode",
56 | "version" : 1
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Examples/Mac/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/Mac/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
--------------------------------------------------------------------------------
/Examples/Mac/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSMainStoryboardFile
26 | Main
27 | NSPrincipalClass
28 | NSApplication
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Examples/Mac/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // SoulverTextKit
4 | //
5 | // Created by Zac Cohan on 9/1/21.
6 | //
7 |
8 | import Cocoa
9 | import SoulverTextKit
10 |
11 | class ViewController: NSViewController {
12 |
13 | /// Grab a standard text view
14 | @IBOutlet var textView: NSTextView!
15 |
16 | /// Create one of these things
17 | var paragraphCalculator: SoulverTextKit.ParagraphCalculator!
18 |
19 | /// Choose what character distinguishes a calculating paragraph
20 | let style = AnswerPosition.afterTab
21 |
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 |
25 | self.setupSoulverTextKit()
26 |
27 | // Looks better
28 | self.textView.textContainerInset = NSSize(width: 10.0, height: 15.0)
29 |
30 | }
31 |
32 | func setupSoulverTextKit() {
33 |
34 | paragraphCalculator = ParagraphCalculator(answerPosition: self.style, textStorage: self.textView.textStorage!, textContainer: self.textView.textContainer!)
35 |
36 | // Setup the text view to send us relevant delegate messages
37 | self.textView.delegate = self
38 | self.textView.layoutManager!.delegate = self
39 |
40 | // Set some default expressions
41 | self.textView.string = [
42 | "123 + 456",
43 | "10 USD in EUR",
44 | "today + 3 weeks"
45 | ].expressionStringFor(style: self.style)
46 |
47 | // let soulverTextKit know we changed the textView's text
48 | paragraphCalculator.textDidChange()
49 |
50 |
51 | }
52 |
53 | }
54 |
55 | extension ViewController : NSLayoutManagerDelegate, NSTextViewDelegate {
56 |
57 | func textDidChange(_ notification: Notification) {
58 |
59 | // Let us know when the text changes, so we can evaluate any changed paragraph if necessary
60 | paragraphCalculator.textDidChange()
61 | }
62 |
63 | func layoutManager(_ layoutManager: NSLayoutManager, textContainer: NSTextContainer, didChangeGeometryFrom oldSize: NSSize) {
64 |
65 | // Let us know when the text view changes size, so we can change update the formatting if necessary
66 | paragraphCalculator.layoutDidChange()
67 | }
68 |
69 | func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool {
70 |
71 | // Check with us to see if the user should be able to edit parts of the paragraph. For example, we don't allow the user to edit results on Soulver lines themselves
72 |
73 | switch paragraphCalculator.shouldAllowReplacementFor(affectedCharRange: affectedCharRange, replacementString: replacementString) {
74 | case .allow:
75 | return true
76 | case .deny:
77 | NSSound.beep()
78 | return false
79 | case .setIntertionPoint(range: let range):
80 | textView.setSelectedRange(range)
81 | return false
82 | }
83 |
84 | }
85 |
86 |
87 | }
88 |
89 |
90 |
--------------------------------------------------------------------------------
/Examples/SoulverTextKit.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 11CEDE5425AB5A400045AB53 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CEDE5325AB5A400045AB53 /* AppDelegate.swift */; };
11 | 11CEDE5625AB5A400045AB53 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CEDE5525AB5A400045AB53 /* SceneDelegate.swift */; };
12 | 11CEDE5825AB5A400045AB53 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CEDE5725AB5A400045AB53 /* ViewController.swift */; };
13 | 11CEDE5B25AB5A400045AB53 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 11CEDE5925AB5A400045AB53 /* Main.storyboard */; };
14 | 11CEDE5D25AB5A410045AB53 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 11CEDE5C25AB5A410045AB53 /* Assets.xcassets */; };
15 | 11CEDE6025AB5A410045AB53 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 11CEDE5E25AB5A410045AB53 /* LaunchScreen.storyboard */; };
16 | 11D5A5C925C759DC00941F25 /* SoulverTextKit in Frameworks */ = {isa = PBXBuildFile; productRef = 11D5A5C825C759DC00941F25 /* SoulverTextKit */; };
17 | 11D5A5CB25C759E200941F25 /* SoulverTextKit in Frameworks */ = {isa = PBXBuildFile; productRef = 11D5A5CA25C759E200941F25 /* SoulverTextKit */; };
18 | 11E0805225AA105100DD964C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E0805125AA105100DD964C /* AppDelegate.swift */; };
19 | 11E0805425AA105100DD964C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E0805325AA105100DD964C /* ViewController.swift */; };
20 | 11E0805625AA105200DD964C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 11E0805525AA105200DD964C /* Assets.xcassets */; };
21 | 11E0805925AA105200DD964C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 11E0805725AA105200DD964C /* Main.storyboard */; };
22 | /* End PBXBuildFile section */
23 |
24 | /* Begin PBXCopyFilesBuildPhase section */
25 | 11CEDE6E25AB5B4A0045AB53 /* Embed Frameworks */ = {
26 | isa = PBXCopyFilesBuildPhase;
27 | buildActionMask = 2147483647;
28 | dstPath = "";
29 | dstSubfolderSpec = 10;
30 | files = (
31 | );
32 | name = "Embed Frameworks";
33 | runOnlyForDeploymentPostprocessing = 0;
34 | };
35 | 11E080A025AA2E1000DD964C /* Embed Frameworks */ = {
36 | isa = PBXCopyFilesBuildPhase;
37 | buildActionMask = 2147483647;
38 | dstPath = "";
39 | dstSubfolderSpec = 10;
40 | files = (
41 | );
42 | name = "Embed Frameworks";
43 | runOnlyForDeploymentPostprocessing = 0;
44 | };
45 | /* End PBXCopyFilesBuildPhase section */
46 |
47 | /* Begin PBXFileReference section */
48 | 11CEDE5125AB5A400045AB53 /* iOS Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS Example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
49 | 11CEDE5325AB5A400045AB53 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
50 | 11CEDE5525AB5A400045AB53 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
51 | 11CEDE5725AB5A400045AB53 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
52 | 11CEDE5A25AB5A400045AB53 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
53 | 11CEDE5C25AB5A410045AB53 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
54 | 11CEDE5F25AB5A410045AB53 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
55 | 11CEDE6125AB5A410045AB53 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
56 | 11D5A5C725C7598100941F25 /* .. */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ..; sourceTree = ""; };
57 | 11E0804E25AA105100DD964C /* Mac Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mac Example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
58 | 11E0805125AA105100DD964C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
59 | 11E0805325AA105100DD964C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
60 | 11E0805525AA105200DD964C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
61 | 11E0805825AA105200DD964C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
62 | 11E0805A25AA105200DD964C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
63 | /* End PBXFileReference section */
64 |
65 | /* Begin PBXFrameworksBuildPhase section */
66 | 11CEDE4E25AB5A400045AB53 /* Frameworks */ = {
67 | isa = PBXFrameworksBuildPhase;
68 | buildActionMask = 2147483647;
69 | files = (
70 | 11D5A5CB25C759E200941F25 /* SoulverTextKit in Frameworks */,
71 | );
72 | runOnlyForDeploymentPostprocessing = 0;
73 | };
74 | 11E0804B25AA105100DD964C /* Frameworks */ = {
75 | isa = PBXFrameworksBuildPhase;
76 | buildActionMask = 2147483647;
77 | files = (
78 | 11D5A5C925C759DC00941F25 /* SoulverTextKit in Frameworks */,
79 | );
80 | runOnlyForDeploymentPostprocessing = 0;
81 | };
82 | /* End PBXFrameworksBuildPhase section */
83 |
84 | /* Begin PBXGroup section */
85 | 11CEDE5225AB5A400045AB53 /* iOS */ = {
86 | isa = PBXGroup;
87 | children = (
88 | 11CEDE5325AB5A400045AB53 /* AppDelegate.swift */,
89 | 11CEDE5525AB5A400045AB53 /* SceneDelegate.swift */,
90 | 11CEDE5725AB5A400045AB53 /* ViewController.swift */,
91 | 11CEDE5925AB5A400045AB53 /* Main.storyboard */,
92 | 11CEDE5C25AB5A410045AB53 /* Assets.xcassets */,
93 | 11CEDE5E25AB5A410045AB53 /* LaunchScreen.storyboard */,
94 | 11CEDE6125AB5A410045AB53 /* Info.plist */,
95 | );
96 | path = iOS;
97 | sourceTree = "";
98 | };
99 | 11E0804525AA105100DD964C = {
100 | isa = PBXGroup;
101 | children = (
102 | 11D5A5C725C7598100941F25 /* .. */,
103 | 11E0805025AA105100DD964C /* Mac */,
104 | 11CEDE5225AB5A400045AB53 /* iOS */,
105 | 11E0804F25AA105100DD964C /* Products */,
106 | );
107 | sourceTree = "";
108 | };
109 | 11E0804F25AA105100DD964C /* Products */ = {
110 | isa = PBXGroup;
111 | children = (
112 | 11E0804E25AA105100DD964C /* Mac Example.app */,
113 | 11CEDE5125AB5A400045AB53 /* iOS Example.app */,
114 | );
115 | name = Products;
116 | sourceTree = "";
117 | };
118 | 11E0805025AA105100DD964C /* Mac */ = {
119 | isa = PBXGroup;
120 | children = (
121 | 11E0805125AA105100DD964C /* AppDelegate.swift */,
122 | 11E0805325AA105100DD964C /* ViewController.swift */,
123 | 11E0805525AA105200DD964C /* Assets.xcassets */,
124 | 11E0805725AA105200DD964C /* Main.storyboard */,
125 | 11E0805A25AA105200DD964C /* Info.plist */,
126 | );
127 | path = Mac;
128 | sourceTree = "";
129 | };
130 | /* End PBXGroup section */
131 |
132 | /* Begin PBXNativeTarget section */
133 | 11CEDE5025AB5A400045AB53 /* iOS Example */ = {
134 | isa = PBXNativeTarget;
135 | buildConfigurationList = 11CEDE6225AB5A410045AB53 /* Build configuration list for PBXNativeTarget "iOS Example" */;
136 | buildPhases = (
137 | 11CEDE4D25AB5A400045AB53 /* Sources */,
138 | 11CEDE4E25AB5A400045AB53 /* Frameworks */,
139 | 11CEDE4F25AB5A400045AB53 /* Resources */,
140 | 11CEDE6E25AB5B4A0045AB53 /* Embed Frameworks */,
141 | );
142 | buildRules = (
143 | );
144 | dependencies = (
145 | );
146 | name = "iOS Example";
147 | packageProductDependencies = (
148 | 11D5A5CA25C759E200941F25 /* SoulverTextKit */,
149 | );
150 | productName = "iOS Example";
151 | productReference = 11CEDE5125AB5A400045AB53 /* iOS Example.app */;
152 | productType = "com.apple.product-type.application";
153 | };
154 | 11E0804D25AA105100DD964C /* Mac Example */ = {
155 | isa = PBXNativeTarget;
156 | buildConfigurationList = 11E0805E25AA105200DD964C /* Build configuration list for PBXNativeTarget "Mac Example" */;
157 | buildPhases = (
158 | 11E0804A25AA105100DD964C /* Sources */,
159 | 11E0804B25AA105100DD964C /* Frameworks */,
160 | 11E0804C25AA105100DD964C /* Resources */,
161 | 11E080A025AA2E1000DD964C /* Embed Frameworks */,
162 | );
163 | buildRules = (
164 | );
165 | dependencies = (
166 | );
167 | name = "Mac Example";
168 | packageProductDependencies = (
169 | 11D5A5C825C759DC00941F25 /* SoulverTextKit */,
170 | );
171 | productName = SoulverTextKit;
172 | productReference = 11E0804E25AA105100DD964C /* Mac Example.app */;
173 | productType = "com.apple.product-type.application";
174 | };
175 | /* End PBXNativeTarget section */
176 |
177 | /* Begin PBXProject section */
178 | 11E0804625AA105100DD964C /* Project object */ = {
179 | isa = PBXProject;
180 | attributes = {
181 | LastSwiftUpdateCheck = 1230;
182 | LastUpgradeCheck = 1240;
183 | TargetAttributes = {
184 | 11CEDE5025AB5A400045AB53 = {
185 | CreatedOnToolsVersion = 12.3;
186 | };
187 | 11E0804D25AA105100DD964C = {
188 | CreatedOnToolsVersion = 12.3;
189 | };
190 | };
191 | };
192 | buildConfigurationList = 11E0804925AA105100DD964C /* Build configuration list for PBXProject "SoulverTextKit" */;
193 | compatibilityVersion = "Xcode 9.3";
194 | developmentRegion = en;
195 | hasScannedForEncodings = 0;
196 | knownRegions = (
197 | en,
198 | Base,
199 | );
200 | mainGroup = 11E0804525AA105100DD964C;
201 | packageReferences = (
202 | );
203 | productRefGroup = 11E0804F25AA105100DD964C /* Products */;
204 | projectDirPath = "";
205 | projectRoot = "";
206 | targets = (
207 | 11E0804D25AA105100DD964C /* Mac Example */,
208 | 11CEDE5025AB5A400045AB53 /* iOS Example */,
209 | );
210 | };
211 | /* End PBXProject section */
212 |
213 | /* Begin PBXResourcesBuildPhase section */
214 | 11CEDE4F25AB5A400045AB53 /* Resources */ = {
215 | isa = PBXResourcesBuildPhase;
216 | buildActionMask = 2147483647;
217 | files = (
218 | 11CEDE6025AB5A410045AB53 /* LaunchScreen.storyboard in Resources */,
219 | 11CEDE5D25AB5A410045AB53 /* Assets.xcassets in Resources */,
220 | 11CEDE5B25AB5A400045AB53 /* Main.storyboard in Resources */,
221 | );
222 | runOnlyForDeploymentPostprocessing = 0;
223 | };
224 | 11E0804C25AA105100DD964C /* Resources */ = {
225 | isa = PBXResourcesBuildPhase;
226 | buildActionMask = 2147483647;
227 | files = (
228 | 11E0805625AA105200DD964C /* Assets.xcassets in Resources */,
229 | 11E0805925AA105200DD964C /* Main.storyboard in Resources */,
230 | );
231 | runOnlyForDeploymentPostprocessing = 0;
232 | };
233 | /* End PBXResourcesBuildPhase section */
234 |
235 | /* Begin PBXSourcesBuildPhase section */
236 | 11CEDE4D25AB5A400045AB53 /* Sources */ = {
237 | isa = PBXSourcesBuildPhase;
238 | buildActionMask = 2147483647;
239 | files = (
240 | 11CEDE5825AB5A400045AB53 /* ViewController.swift in Sources */,
241 | 11CEDE5425AB5A400045AB53 /* AppDelegate.swift in Sources */,
242 | 11CEDE5625AB5A400045AB53 /* SceneDelegate.swift in Sources */,
243 | );
244 | runOnlyForDeploymentPostprocessing = 0;
245 | };
246 | 11E0804A25AA105100DD964C /* Sources */ = {
247 | isa = PBXSourcesBuildPhase;
248 | buildActionMask = 2147483647;
249 | files = (
250 | 11E0805425AA105100DD964C /* ViewController.swift in Sources */,
251 | 11E0805225AA105100DD964C /* AppDelegate.swift in Sources */,
252 | );
253 | runOnlyForDeploymentPostprocessing = 0;
254 | };
255 | /* End PBXSourcesBuildPhase section */
256 |
257 | /* Begin PBXVariantGroup section */
258 | 11CEDE5925AB5A400045AB53 /* Main.storyboard */ = {
259 | isa = PBXVariantGroup;
260 | children = (
261 | 11CEDE5A25AB5A400045AB53 /* Base */,
262 | );
263 | name = Main.storyboard;
264 | sourceTree = "";
265 | };
266 | 11CEDE5E25AB5A410045AB53 /* LaunchScreen.storyboard */ = {
267 | isa = PBXVariantGroup;
268 | children = (
269 | 11CEDE5F25AB5A410045AB53 /* Base */,
270 | );
271 | name = LaunchScreen.storyboard;
272 | sourceTree = "";
273 | };
274 | 11E0805725AA105200DD964C /* Main.storyboard */ = {
275 | isa = PBXVariantGroup;
276 | children = (
277 | 11E0805825AA105200DD964C /* Base */,
278 | );
279 | name = Main.storyboard;
280 | sourceTree = "";
281 | };
282 | /* End PBXVariantGroup section */
283 |
284 | /* Begin XCBuildConfiguration section */
285 | 11CEDE6325AB5A410045AB53 /* Debug */ = {
286 | isa = XCBuildConfiguration;
287 | buildSettings = {
288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
289 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
290 | CODE_SIGN_STYLE = Automatic;
291 | DEVELOPMENT_TEAM = SBA8TXW9N6;
292 | INFOPLIST_FILE = iOS/Info.plist;
293 | IPHONEOS_DEPLOYMENT_TARGET = 14.3;
294 | LD_RUNPATH_SEARCH_PATHS = (
295 | "$(inherited)",
296 | "@executable_path/Frameworks",
297 | );
298 | PRODUCT_BUNDLE_IDENTIFIER = app.soulver.iOS.SoulverTextKitExample;
299 | PRODUCT_NAME = "$(TARGET_NAME)";
300 | SDKROOT = iphoneos;
301 | SWIFT_VERSION = 5.0;
302 | TARGETED_DEVICE_FAMILY = "1,2";
303 | };
304 | name = Debug;
305 | };
306 | 11CEDE6425AB5A410045AB53 /* Release */ = {
307 | isa = XCBuildConfiguration;
308 | buildSettings = {
309 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
310 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
311 | CODE_SIGN_STYLE = Automatic;
312 | DEVELOPMENT_TEAM = SBA8TXW9N6;
313 | INFOPLIST_FILE = iOS/Info.plist;
314 | IPHONEOS_DEPLOYMENT_TARGET = 14.3;
315 | LD_RUNPATH_SEARCH_PATHS = (
316 | "$(inherited)",
317 | "@executable_path/Frameworks",
318 | );
319 | PRODUCT_BUNDLE_IDENTIFIER = app.soulver.iOS.SoulverTextKitExample;
320 | PRODUCT_NAME = "$(TARGET_NAME)";
321 | SDKROOT = iphoneos;
322 | SWIFT_VERSION = 5.0;
323 | TARGETED_DEVICE_FAMILY = "1,2";
324 | VALIDATE_PRODUCT = YES;
325 | };
326 | name = Release;
327 | };
328 | 11E0805C25AA105200DD964C /* Debug */ = {
329 | isa = XCBuildConfiguration;
330 | buildSettings = {
331 | ALWAYS_SEARCH_USER_PATHS = NO;
332 | CLANG_ANALYZER_NONNULL = YES;
333 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
334 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
335 | CLANG_CXX_LIBRARY = "libc++";
336 | CLANG_ENABLE_MODULES = YES;
337 | CLANG_ENABLE_OBJC_ARC = YES;
338 | CLANG_ENABLE_OBJC_WEAK = YES;
339 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
340 | CLANG_WARN_BOOL_CONVERSION = YES;
341 | CLANG_WARN_COMMA = YES;
342 | CLANG_WARN_CONSTANT_CONVERSION = YES;
343 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
344 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
345 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
346 | CLANG_WARN_EMPTY_BODY = YES;
347 | CLANG_WARN_ENUM_CONVERSION = YES;
348 | CLANG_WARN_INFINITE_RECURSION = YES;
349 | CLANG_WARN_INT_CONVERSION = YES;
350 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
351 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
352 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
353 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
354 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
355 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
356 | CLANG_WARN_STRICT_PROTOTYPES = YES;
357 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
358 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
359 | CLANG_WARN_UNREACHABLE_CODE = YES;
360 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
361 | COPY_PHASE_STRIP = NO;
362 | DEBUG_INFORMATION_FORMAT = dwarf;
363 | ENABLE_STRICT_OBJC_MSGSEND = YES;
364 | ENABLE_TESTABILITY = YES;
365 | GCC_C_LANGUAGE_STANDARD = gnu11;
366 | GCC_DYNAMIC_NO_PIC = NO;
367 | GCC_NO_COMMON_BLOCKS = YES;
368 | GCC_OPTIMIZATION_LEVEL = 0;
369 | GCC_PREPROCESSOR_DEFINITIONS = (
370 | "DEBUG=1",
371 | "$(inherited)",
372 | );
373 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
374 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
375 | GCC_WARN_UNDECLARED_SELECTOR = YES;
376 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
377 | GCC_WARN_UNUSED_FUNCTION = YES;
378 | GCC_WARN_UNUSED_VARIABLE = YES;
379 | MACOSX_DEPLOYMENT_TARGET = 10.15;
380 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
381 | MTL_FAST_MATH = YES;
382 | ONLY_ACTIVE_ARCH = YES;
383 | SDKROOT = macosx;
384 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
385 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
386 | };
387 | name = Debug;
388 | };
389 | 11E0805D25AA105200DD964C /* Release */ = {
390 | isa = XCBuildConfiguration;
391 | buildSettings = {
392 | ALWAYS_SEARCH_USER_PATHS = NO;
393 | CLANG_ANALYZER_NONNULL = YES;
394 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
395 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
396 | CLANG_CXX_LIBRARY = "libc++";
397 | CLANG_ENABLE_MODULES = YES;
398 | CLANG_ENABLE_OBJC_ARC = YES;
399 | CLANG_ENABLE_OBJC_WEAK = YES;
400 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
401 | CLANG_WARN_BOOL_CONVERSION = YES;
402 | CLANG_WARN_COMMA = YES;
403 | CLANG_WARN_CONSTANT_CONVERSION = YES;
404 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
405 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
406 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
407 | CLANG_WARN_EMPTY_BODY = YES;
408 | CLANG_WARN_ENUM_CONVERSION = YES;
409 | CLANG_WARN_INFINITE_RECURSION = YES;
410 | CLANG_WARN_INT_CONVERSION = YES;
411 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
412 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
413 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
414 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
415 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
416 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
417 | CLANG_WARN_STRICT_PROTOTYPES = YES;
418 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
419 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
420 | CLANG_WARN_UNREACHABLE_CODE = YES;
421 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
422 | COPY_PHASE_STRIP = NO;
423 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
424 | ENABLE_NS_ASSERTIONS = NO;
425 | ENABLE_STRICT_OBJC_MSGSEND = YES;
426 | GCC_C_LANGUAGE_STANDARD = gnu11;
427 | GCC_NO_COMMON_BLOCKS = YES;
428 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
429 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
430 | GCC_WARN_UNDECLARED_SELECTOR = YES;
431 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
432 | GCC_WARN_UNUSED_FUNCTION = YES;
433 | GCC_WARN_UNUSED_VARIABLE = YES;
434 | MACOSX_DEPLOYMENT_TARGET = 10.15;
435 | MTL_ENABLE_DEBUG_INFO = NO;
436 | MTL_FAST_MATH = YES;
437 | SDKROOT = macosx;
438 | SWIFT_COMPILATION_MODE = wholemodule;
439 | SWIFT_OPTIMIZATION_LEVEL = "-O";
440 | };
441 | name = Release;
442 | };
443 | 11E0805F25AA105200DD964C /* Debug */ = {
444 | isa = XCBuildConfiguration;
445 | buildSettings = {
446 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
447 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
448 | CODE_SIGN_STYLE = Automatic;
449 | COMBINE_HIDPI_IMAGES = YES;
450 | DEVELOPMENT_TEAM = SBA8TXW9N6;
451 | ENABLE_HARDENED_RUNTIME = YES;
452 | INFOPLIST_FILE = Mac/Info.plist;
453 | LD_RUNPATH_SEARCH_PATHS = (
454 | "$(inherited)",
455 | "@executable_path/../Frameworks",
456 | );
457 | PRODUCT_BUNDLE_IDENTIFIER = app.soulver.mac.SoulverTextKitExample;
458 | PRODUCT_NAME = "$(TARGET_NAME)";
459 | SWIFT_VERSION = 5.0;
460 | };
461 | name = Debug;
462 | };
463 | 11E0806025AA105200DD964C /* Release */ = {
464 | isa = XCBuildConfiguration;
465 | buildSettings = {
466 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
467 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
468 | CODE_SIGN_STYLE = Automatic;
469 | COMBINE_HIDPI_IMAGES = YES;
470 | DEVELOPMENT_TEAM = SBA8TXW9N6;
471 | ENABLE_HARDENED_RUNTIME = YES;
472 | INFOPLIST_FILE = Mac/Info.plist;
473 | LD_RUNPATH_SEARCH_PATHS = (
474 | "$(inherited)",
475 | "@executable_path/../Frameworks",
476 | );
477 | PRODUCT_BUNDLE_IDENTIFIER = app.soulver.mac.SoulverTextKitExample;
478 | PRODUCT_NAME = "$(TARGET_NAME)";
479 | SWIFT_VERSION = 5.0;
480 | };
481 | name = Release;
482 | };
483 | /* End XCBuildConfiguration section */
484 |
485 | /* Begin XCConfigurationList section */
486 | 11CEDE6225AB5A410045AB53 /* Build configuration list for PBXNativeTarget "iOS Example" */ = {
487 | isa = XCConfigurationList;
488 | buildConfigurations = (
489 | 11CEDE6325AB5A410045AB53 /* Debug */,
490 | 11CEDE6425AB5A410045AB53 /* Release */,
491 | );
492 | defaultConfigurationIsVisible = 0;
493 | defaultConfigurationName = Release;
494 | };
495 | 11E0804925AA105100DD964C /* Build configuration list for PBXProject "SoulverTextKit" */ = {
496 | isa = XCConfigurationList;
497 | buildConfigurations = (
498 | 11E0805C25AA105200DD964C /* Debug */,
499 | 11E0805D25AA105200DD964C /* Release */,
500 | );
501 | defaultConfigurationIsVisible = 0;
502 | defaultConfigurationName = Release;
503 | };
504 | 11E0805E25AA105200DD964C /* Build configuration list for PBXNativeTarget "Mac Example" */ = {
505 | isa = XCConfigurationList;
506 | buildConfigurations = (
507 | 11E0805F25AA105200DD964C /* Debug */,
508 | 11E0806025AA105200DD964C /* Release */,
509 | );
510 | defaultConfigurationIsVisible = 0;
511 | defaultConfigurationName = Release;
512 | };
513 | /* End XCConfigurationList section */
514 |
515 | /* Begin XCSwiftPackageProductDependency section */
516 | 11D5A5C825C759DC00941F25 /* SoulverTextKit */ = {
517 | isa = XCSwiftPackageProductDependency;
518 | productName = SoulverTextKit;
519 | };
520 | 11D5A5CA25C759E200941F25 /* SoulverTextKit */ = {
521 | isa = XCSwiftPackageProductDependency;
522 | productName = SoulverTextKit;
523 | };
524 | /* End XCSwiftPackageProductDependency section */
525 | };
526 | rootObject = 11E0804625AA105100DD964C /* Project object */;
527 | }
528 |
--------------------------------------------------------------------------------
/Examples/SoulverTextKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Examples/SoulverTextKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Examples/SoulverTextKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "SoulverCore",
6 | "repositoryURL": "https://github.com/soulverteam/SoulverCore",
7 | "state": {
8 | "branch": null,
9 | "revision": "659f47b1693799445ed08e8713d4241eced1b170",
10 | "version": "1.2.1"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/Examples/SoulverTextKit.xcodeproj/xcshareddata/xcschemes/Mac Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Examples/iOS/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // iOS Example
4 | //
5 | // Created by Zac Cohan on 10/1/21.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | // Override point for customization after application launch.
15 | return true
16 | }
17 |
18 | // MARK: UISceneSession Lifecycle
19 |
20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
21 | // Called when a new scene session is being created.
22 | // Use this method to select a configuration to create the new scene with.
23 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
24 | }
25 |
26 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
27 | // Called when the user discards a scene session.
28 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
29 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
30 | }
31 |
32 |
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/Examples/iOS/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Examples/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Examples/iOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/iOS/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Examples/iOS/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/Examples/iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 | UISceneStoryboardFile
37 | Main
38 |
39 |
40 |
41 |
42 | UIApplicationSupportsIndirectInputEvents
43 |
44 | UILaunchStoryboardName
45 | LaunchScreen
46 | UIMainStoryboardFile
47 | Main
48 | UIRequiredDeviceCapabilities
49 |
50 | armv7
51 |
52 | UISupportedInterfaceOrientations
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationLandscapeLeft
56 | UIInterfaceOrientationLandscapeRight
57 |
58 | UISupportedInterfaceOrientations~ipad
59 |
60 | UIInterfaceOrientationPortrait
61 | UIInterfaceOrientationPortraitUpsideDown
62 | UIInterfaceOrientationLandscapeLeft
63 | UIInterfaceOrientationLandscapeRight
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/Examples/iOS/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // iOS Example
4 | //
5 | // Created by Zac Cohan on 10/1/21.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
15 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
16 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
17 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
18 | guard let _ = (scene as? UIWindowScene) else { return }
19 | }
20 |
21 | func sceneDidDisconnect(_ scene: UIScene) {
22 | // Called as the scene is being released by the system.
23 | // This occurs shortly after the scene enters the background, or when its session is discarded.
24 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
25 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
26 | }
27 |
28 | func sceneDidBecomeActive(_ scene: UIScene) {
29 | // Called when the scene has moved from an inactive state to an active state.
30 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
31 | }
32 |
33 | func sceneWillResignActive(_ scene: UIScene) {
34 | // Called when the scene will move from an active state to an inactive state.
35 | // This may occur due to temporary interruptions (ex. an incoming phone call).
36 | }
37 |
38 | func sceneWillEnterForeground(_ scene: UIScene) {
39 | // Called as the scene transitions from the background to the foreground.
40 | // Use this method to undo the changes made on entering the background.
41 | }
42 |
43 | func sceneDidEnterBackground(_ scene: UIScene) {
44 | // Called as the scene transitions from the foreground to the background.
45 | // Use this method to save data, release shared resources, and store enough scene-specific state information
46 | // to restore the scene back to its current state.
47 | }
48 |
49 |
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/Examples/iOS/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // iOS Example
4 | //
5 | // Created by Zac Cohan on 10/1/21.
6 | //
7 |
8 | import UIKit
9 | import SoulverTextKit
10 |
11 | class ViewController: UIViewController {
12 |
13 | /// Grab a standard text view
14 | @IBOutlet weak var textView: UITextView!
15 |
16 | /// Create one of these things
17 | var paragraphCalculator: SoulverTextKit.ParagraphCalculator!
18 |
19 | /// Choose what character distinguishes a calculating paragraph
20 | let answerPosition = AnswerPosition.afterEquals
21 |
22 | override func viewDidLoad() {
23 | super.viewDidLoad()
24 |
25 | self.setupSoulverTextKit()
26 |
27 | // Looks better
28 | self.textView.textContainerInset = UIEdgeInsets(top: 20.0, left: 5.0, bottom: 0.0, right: 5.0)
29 |
30 | }
31 |
32 | func setupSoulverTextKit() {
33 |
34 | paragraphCalculator = ParagraphCalculator(answerPosition: answerPosition, textStorage: self.textView.textStorage, textContainer: self.textView.textContainer)
35 |
36 | // Setup the text view to send us relevant delegate messages
37 | self.textView.delegate = self
38 | self.textView.layoutManager.delegate = self
39 |
40 | // Set somef default expressions
41 | self.textView.text = [
42 | "123 + 456",
43 | "10 USD in EUR",
44 | "today + 3 weeks"
45 | ].expressionStringFor(style: self.answerPosition)
46 |
47 | // let soulverTextKit know we changed the textView's text
48 | paragraphCalculator.textDidChange()
49 |
50 | }
51 |
52 | }
53 |
54 | extension ViewController : NSLayoutManagerDelegate, UITextViewDelegate {
55 |
56 |
57 | func textViewDidChange(_ textView: UITextView) {
58 |
59 | // Let us know when the text changes, so we can evaluate any changed paragraphs if necessary
60 | paragraphCalculator.textDidChange()
61 | }
62 |
63 | func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
64 |
65 | // Check with us to see if the user should be able to edit parts of the paragraph. For example, we don't allow the user to edit results on Soulver lines themselves
66 |
67 | switch paragraphCalculator.shouldAllowReplacementFor(affectedCharRange: range, replacementString: text) {
68 | case .allow:
69 | return true
70 | case .deny:
71 | return false
72 | case .setIntertionPoint(range: let range):
73 | textView.selectedRange = range
74 | return false
75 | }
76 |
77 | }
78 |
79 | func layoutManager(_ layoutManager: NSLayoutManager, textContainer: NSTextContainer, didChangeGeometryFrom oldSize: CGSize) {
80 |
81 | // Let us know when the text view changes size, so we can change update the formatting if necessary
82 | paragraphCalculator.layoutDidChange()
83 | }
84 |
85 |
86 | }
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/Images/AfterEquals.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulverteam/SoulverTextKit/fe9f909c6652b7caf4973465f4b3fc5d9647f219/Images/AfterEquals.png
--------------------------------------------------------------------------------
/Images/AfterPipe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulverteam/SoulverTextKit/fe9f909c6652b7caf4973465f4b3fc5d9647f219/Images/AfterPipe.png
--------------------------------------------------------------------------------
/Images/AfterTab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulverteam/SoulverTextKit/fe9f909c6652b7caf4973465f4b3fc5d9647f219/Images/AfterTab.png
--------------------------------------------------------------------------------
/Images/Example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soulverteam/SoulverTextKit/fe9f909c6652b7caf4973465f4b3fc5d9647f219/Images/Example.gif
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Zac Cohan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SoulverTextKit",
8 | platforms: [
9 | .macOS(.v10_14),
10 | .iOS(.v13),
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, and make them visible to other packages.
14 | .library(
15 | name: "SoulverTextKit",
16 | targets: ["SoulverTextKit"]),
17 | ],
18 | dependencies: [
19 | // Dependencies declare other packages that this package depends on.
20 | // .package(url: /* package url */, from: "1.0.0"),
21 | .package(url: "https://github.com/soulverteam/SoulverCore", from: "1.2.1")
22 | ],
23 | targets: [
24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
25 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
26 | .target(
27 | name: "SoulverTextKit",
28 | dependencies: ["SoulverCore"]),
29 | .testTarget(
30 | name: "SoulverTextKitTests",
31 | dependencies: ["SoulverTextKit"]),
32 | ]
33 | )
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SoulverTextKit
2 |
3 | 
4 | 
5 | 
6 |
7 | SoulverTextKit lets you add a line-by-line calculation feature to any NSTextView or UITextView. It uses [SoulverCore](https://soulver.app/core) for number crunching, which also provides unit conversions, date & times calculations, and more.
8 |
9 |
10 |
11 | ## Requirements
12 |
13 | - Xcode 11+
14 | - Swift 5+
15 |
16 | ## Supported Platforms
17 |
18 | - macOS 10.14.4+
19 | - iOS/iPadOS 13+
20 |
21 | ## Installation
22 |
23 | SoulverTextKit is distributed using the [Swift Package Manager](https://swift.org/package-manager). To install it into a project, simply add it as a dependency within your `Package.swift` manifest:
24 |
25 | ```swift
26 | let package = Package(
27 | ...
28 | dependencies: [
29 | .package(url: "https://github.com/soulverteam/SoulverTextKit.git", from: "0.0.1")
30 | ],
31 | ...
32 | )
33 | ```
34 |
35 | [Or add the package in Xcode.](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app)
36 |
37 | ## Usage
38 |
39 | There are 3 steps to integrate SoulverTextKit in your project. Examples for both NSTextView & UITextView are provided in this repository.
40 |
41 | ### Step 1
42 | #### Import SoulverTextKit in your text view delegate
43 |
44 | ```swift
45 | import SoulverTextKit
46 | ```
47 | ### Step 2
48 | #### Create an instance variable of ParagraphCalculator and initialize it with your TextView's NSTextStorage and NSTextContainer:
49 |
50 | ```swift
51 |
52 | @IBOutlet var textView: NSTextView!
53 | var paragraphCalculator: SoulverTextKit.ParagraphCalculator!
54 |
55 | override func viewDidLoad() {
56 | super.viewDidLoad()
57 | self.paragraphCalculator = ParagraphCalculator(answerPosition: .afterEquals, textStorage: self.textView.textStorage, textContainer: self.textView.textContainer)
58 | }
59 |
60 | ```
61 |
62 | ### Step 3
63 | #### Implement NS/UITextView textDidChange and NSLayoutManager didChangeGeometry delegate methods
64 |
65 | ```swift
66 |
67 | func textDidChange(_ notification: Notification) {
68 |
69 | // Let us know when the text changes, so we can evaluate any changed lines if necessary
70 | paragraphCalculator.textDidChange()
71 | }
72 |
73 | func layoutManager(_ layoutManager: NSLayoutManager, textContainer: NSTextContainer, didChangeGeometryFrom oldSize: NSSize) {
74 |
75 | // Let us know when the text view changes size, so we can change update the formatting if necessary
76 | paragraphCalculator.layoutDidChange()
77 | }
78 | ```
79 |
80 | ### Step 4 (optional)
81 | #### Prevent the user editing the result of a paragraph
82 |
83 | ```swift
84 | func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool {
85 |
86 | // Check with us to see if the user should be able to edit parts of the paragraph.
87 | switch paragraphCalculator.shouldAllowReplacementFor(affectedCharRange: affectedCharRange, replacementString: replacementString) {
88 | case .allow:
89 | return true
90 | case .deny:
91 | NSSound.beep()
92 | return false
93 | case .setIntertionPoint(range: let range):
94 | textView.setSelectedRange(range)
95 | return false
96 | }
97 |
98 | }
99 | ```
100 |
101 |
102 | ## Styles
103 |
104 | There are 3 built-in styles for calculation paragraphs: `afterTab`, `afterPipe` and `afterEquals`. Choose your preferred style when creating the `ParagraphCalculator`.
105 |
106 | #### After Tab
107 |
108 |
109 |
110 | #### After Pipe
111 |
112 |
113 |
114 | #### After Equals
115 |
116 |
117 |
118 | ## License
119 |
120 | Copyright (c) 2021 Zac Cohan.
121 | SoulverTextKit is distributed under the MIT License.
122 | The use of the [SoulverCore](https://soulver.app/core) math engine in commercial software requires a special license. You can also modify ParagraphCalculator to use another math engine like [Math.js](https://github.com/josdejong/mathjs) or [Expression](https://github.com/nicklockwood/Expression).
--------------------------------------------------------------------------------
/Sources/SoulverTextKit/Helpers/StringByParagraphs.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringByParagraphs.swift
3 | // SoulverTextKit
4 | //
5 | // Created by Zac Cohan on 15/10/18.
6 | // Copyright © 2018 Zac Cohan. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | typealias ParagraphIndex = Int
12 | typealias CharacterIndex = Int
13 |
14 | /* A view on a string that lets you access paragraphs by index */
15 | internal class StringByParagraphs: CustomDebugStringConvertible {
16 |
17 | internal enum ParagraphIndexGranularity {
18 | case wholeParagraph
19 | case contents //no newparagraph character
20 | }
21 |
22 | internal let contents: String
23 |
24 | fileprivate let cache: ParagraphCache
25 |
26 | internal init(contents: String) {
27 |
28 | self.contents = contents
29 | self.cache = ParagraphCache()
30 |
31 | self.loadMetrics()
32 | }
33 |
34 |
35 | internal var contentEnd: Int {
36 | return self.cache.paragraphs.last?.range.upperBound ?? 0
37 | }
38 |
39 | internal var paragraphCount: Int {
40 | return self.cache.paragraphs.count
41 | }
42 |
43 | internal func rangeOfParagraphContainingLocation(_ location: CharacterIndex, includeNewParagraph: Bool = false) -> NSRange {
44 |
45 | if location > self.contentEnd {
46 | assertionFailure("Bad paragraph range request")
47 | return .zero
48 | }
49 |
50 | if let foundParagraph = _binarySearchForParagraphContaining(location: location) {
51 | return includeNewParagraph ? foundParagraph.range : foundParagraph.contentsRange
52 | }
53 |
54 | assertionFailure("Could not find the paragraph at \(location)")
55 | return .zero
56 | }
57 |
58 |
59 | internal func contentsRangeOfParagraphContainingLocation(_ location: CharacterIndex) -> NSRange {
60 | return self.rangeOfParagraphContainingLocation(location, includeNewParagraph: false)
61 | }
62 |
63 | internal func contentsRangeOfParagraphAtIndex(_ paragraphIndex: ParagraphIndex) -> NSRange {
64 | return self.cache.getAtIndex(paragraphIndex).contentsRange
65 | }
66 |
67 | internal func rangeOfParagraphAtIndex(_ paragraphIndex: ParagraphIndex) -> NSRange {
68 | return self.cache.getAtIndex(paragraphIndex).range
69 | }
70 |
71 | internal func paragraphIndexesContainingRange(_ range: NSRange, ignoreFinalTouchedParagraph: Bool) -> Range {
72 |
73 | if (self.cache.paragraphs.isEmpty) {
74 | return 0..<0
75 | }
76 | let firstRange = rangeOfParagraphContainingLocation(range.location)
77 |
78 | // Handle the case where we've specified an entire paragraph range
79 | if firstRange == range {
80 | let paragraph = self.cache.getAtLocation(firstRange.location)
81 |
82 | //the case of |123\n| is technically both paragraph 0 and paragraph 1
83 | if paragraph.index != self.paragraphCount - 1 && !ignoreFinalTouchedParagraph {
84 |
85 | let nextParagraph = self.cache.getAtIndex(paragraph.index + 1)
86 | if NSIntersectionRange(range, paragraph.range).length == paragraph.range.length {
87 | return paragraph.index ..< nextParagraph.index + 1
88 | }
89 | }
90 |
91 | return paragraph.index ..< paragraph.index + 1
92 | }
93 |
94 | let lastRange = range.length > 0
95 | ? rangeOfParagraphContainingLocation(range.location + range.length)
96 | : firstRange
97 |
98 | let firstParagraph = self.cache.getAtLocation(firstRange.location)
99 | var lastParagraph = self.cache.getAtLocation(lastRange.location)
100 |
101 | if ignoreFinalTouchedParagraph && firstParagraph.index != lastParagraph.index && lastParagraph.index > 0 {
102 |
103 | /* Handle the case where we've specified the entire paragraph above and it's including the last paragraph when it really shouldn't */
104 | /* ie: |123\n456\n|789 shouldn't touch paragraph 2 */
105 |
106 | let paragraphBeforeLast = self.cache.getAtIndex(lastParagraph.index - 1)
107 |
108 | if paragraphBeforeLast.range.upperBound == range.upperBound {
109 | lastParagraph = paragraphBeforeLast
110 | }
111 | }
112 |
113 | return firstParagraph.index.. Int {
118 |
119 | if (self.cache.paragraphs.isEmpty) {
120 | return 0
121 | }
122 | let range = rangeOfParagraphContainingLocation(location)
123 | let paragraph = self.cache.getAtLocation(range.location)
124 | return paragraph.index
125 | }
126 |
127 | internal subscript(index: Int, granularity: ParagraphIndexGranularity) -> String {
128 | get {
129 | let paragraph = self.cache.getAtIndex(index)
130 | let range = granularity == .contents ? paragraph.contentsRange : paragraph.range
131 | return (self.contents as NSString).substring(with: range)
132 | }
133 | }
134 |
135 | // MARK: - Comparing with other Indexed Strings
136 |
137 | internal func indexesDifferingFrom(stringByParagraphs: StringByParagraphs) -> IndexSet {
138 |
139 | var differingIndexes = IndexSet()
140 |
141 | for paragraphIndex in 0 ..< self.paragraphCount {
142 |
143 | if paragraphIndex < stringByParagraphs.paragraphCount {
144 | if self[paragraphIndex, .contents] != stringByParagraphs[paragraphIndex, .contents] {
145 | differingIndexes.insert(paragraphIndex)
146 | }
147 | }
148 | else {
149 | // this index doesn't even exist in the other string
150 | differingIndexes.insert(paragraphIndex)
151 | }
152 | }
153 |
154 | return differingIndexes
155 |
156 | }
157 |
158 |
159 | // MARK: - Global to Local Range Conversion
160 |
161 | internal func globalRangeFor(localRange: NSRange, on paragraphIndex: ParagraphIndex) -> NSRange {
162 |
163 | let paragraphRange = self.rangeOfParagraphAtIndex(paragraphIndex)
164 | let globalLocation = paragraphRange.location + localRange.location
165 | return NSMakeRange(globalLocation, localRange.length)
166 |
167 | }
168 |
169 | internal func localRangeFor(globalRange: NSRange, on paragraphIndex: ParagraphIndex) -> NSRange? {
170 |
171 | let paragraphRange = self.rangeOfParagraphContainingLocation(globalRange.location)
172 |
173 | let startIndex = globalRange.location - paragraphRange.location
174 | var length = globalRange.length
175 |
176 | if length + startIndex > paragraphRange.upperBound {
177 | length = paragraphRange.upperBound - startIndex
178 | }
179 |
180 | return NSMakeRange(startIndex, length)
181 |
182 | }
183 |
184 |
185 |
186 | // MARK: - Loading Cache & Searching
187 |
188 | private func _binarySearchForParagraphContaining(location: CharacterIndex) -> ParagraphCache.Paragraph? {
189 |
190 | var lowerBound = 0
191 | var upperBound = self.paragraphCount
192 |
193 | while lowerBound < upperBound {
194 | let midIndex = lowerBound + (upperBound - lowerBound) / 2
195 | let midIndexParagraph = self.cache.paragraphs[midIndex]
196 |
197 | if midIndexParagraph.range.contains(location) || (midIndexParagraph.range.length == 0 && midIndexParagraph.range.lowerBound == location) {
198 | return midIndexParagraph
199 | } else if midIndexParagraph.range.location < location {
200 | lowerBound = midIndex + 1
201 | } else {
202 | upperBound = midIndex
203 | }
204 | }
205 |
206 |
207 | // At the end of the contents (i.e equal to contentEnd is fine, it's the last paragraph)
208 | if let lastParagraph = self.cache.paragraphs.last, lastParagraph.range.upperBound == location {
209 | return lastParagraph
210 | }
211 |
212 | return nil
213 |
214 | }
215 |
216 | private func loadMetrics() {
217 |
218 | let string = self.contents as NSString
219 |
220 | var location: Int = 0
221 | var lastContentsEnd: Int?
222 | var index: Int = 0
223 | let max = string.length
224 | while (location < max) {
225 | var paragraphStart: Int = 0
226 | var paragraphEnd: Int = 0
227 | var contentsEnd: Int = 0
228 | string.getParagraphStart(¶graphStart, end: ¶graphEnd, contentsEnd: &contentsEnd,
229 | for: NSMakeRange(location, 0))
230 | let r = NSMakeRange(paragraphStart, paragraphEnd - paragraphStart)
231 | let contents = string.substring(with: r)
232 |
233 | let paragraph = ParagraphCache.Paragraph(index: index, range: r,
234 | contentsRange: NSMakeRange(paragraphStart, contentsEnd - paragraphStart), contents: contents)
235 |
236 |
237 | cache.addParagraph(paragraph)
238 | index += 1
239 | location = NSMaxRange(r)
240 | lastContentsEnd = contentsEnd
241 | }
242 | if (lastContentsEnd == nil || lastContentsEnd != location) {
243 | // Last paragraph ended with an end of paragraph character, add another empty paragraph to represent this
244 | let r = NSMakeRange(location, 0)
245 | let paragraph = ParagraphCache.Paragraph(index: index, range: r, contentsRange: r, contents: "")
246 | cache.addParagraph(paragraph)
247 | }
248 |
249 | }
250 |
251 | internal var debugDescription: String {
252 |
253 | var description = "\(type(of: self)) (\(Unmanaged.passUnretained(self).toOpaque())))\n"
254 | description += "'\(self.contents)'"
255 |
256 | return description
257 |
258 | }
259 |
260 | }
261 |
262 |
263 | fileprivate class ParagraphCache {
264 |
265 | class Paragraph {
266 | var index: Int
267 |
268 | /** Range of the entire paragraph, including end of paragraph character */
269 | var range: NSRange
270 |
271 | /** Range of paragraph contents, not including end of paragraph character */
272 | var contentsRange: NSRange
273 |
274 | var contents: String //includes new line character on the end \n
275 |
276 | init(index: Int, range: NSRange, contentsRange: NSRange, contents: String) {
277 | self.index = index
278 | self.range = range
279 | self.contentsRange = contentsRange
280 | self.contents = contents
281 | }
282 |
283 |
284 | }
285 |
286 | /** Paragraphs keyed by range location */
287 | var paragraphsByLocation: [Int: Paragraph] = Dictionary()
288 | /** Paragraphs by index */
289 | var paragraphs: [Paragraph] = Array()
290 |
291 | func getAtIndex(_ index: Int) -> Paragraph {
292 |
293 | if index < self.paragraphs.count {
294 | return paragraphs[index]
295 | }
296 |
297 | print ("Error: requesting paragraph range metrics for out of bounds paragraph index (\(index), paragraph count is \(self.paragraphs.count))")
298 |
299 | return Paragraph(index: 0, range: NSMakeRange(0, 0), contentsRange: NSMakeRange(0, 0), contents: "")
300 |
301 |
302 | }
303 |
304 | func getAtLocation(_ location: Int) -> Paragraph {
305 | return paragraphsByLocation[location]!
306 | }
307 |
308 | func count() -> Int {
309 | return paragraphs.count
310 | }
311 |
312 | func isEmpty() -> Bool {
313 | return paragraphs.isEmpty
314 | }
315 |
316 | func invalidate() {
317 | paragraphsByLocation.removeAll(keepingCapacity: true)
318 | paragraphs.removeAll(keepingCapacity: true)
319 | }
320 |
321 | func addParagraph(_ paragraph: Paragraph) {
322 | paragraphs.append(paragraph)
323 | paragraphsByLocation[paragraph.range.location] = paragraph
324 | }
325 |
326 |
327 |
328 | }
329 |
330 |
--------------------------------------------------------------------------------
/Sources/SoulverTextKit/SoulverTextKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SoulverTextKit.swift
3 | // SoulverTextKit
4 | //
5 | // Created by Zac Cohan on 31/1/21.
6 | //
7 |
8 |
9 | #if canImport(UIKit)
10 | import UIKit
11 | #elseif canImport(AppKit)
12 | import AppKit
13 | #endif
14 |
15 | /// The Soulver number-crunching math engine that this package depends on
16 | import SoulverCore
17 |
18 | /// What makes a Soulver line a Soulver line in your text view?
19 | public enum AnswerPosition {
20 |
21 | /// right align answers after a tab
22 | case afterTab
23 |
24 | /// left align answers to a pipe character | positioned a standard distance from the right of the text view
25 | case afterPipe
26 |
27 | /// insert answers directly after = following the expression
28 | case afterEquals
29 |
30 | /// this string must be present for this line to be processed as a Soulver line
31 | var divider: String {
32 | switch self {
33 | case .afterTab:
34 | return "\t"
35 | case .afterPipe:
36 | return "\t| "
37 | case .afterEquals:
38 | return " = "
39 | }
40 | }
41 |
42 | /// when this character is typed, the line automatically becomes a Soulver line
43 | var trigger: String {
44 | switch self {
45 | case .afterTab:
46 | return "\t"
47 | case .afterPipe:
48 | return "|"
49 | case .afterEquals:
50 | return "="
51 | }
52 | }
53 |
54 | }
55 |
56 | public enum TextReplacementDecision {
57 | case allow
58 | case deny
59 | case setIntertionPoint(range: NSRange)
60 | }
61 |
62 |
63 | /// Use this object to add Soulver-like calculation abilities to a standard NSTextView or UITextView
64 | public class ParagraphCalculator {
65 |
66 | private var stringByParagraphs: StringByParagraphs
67 | private let calculator = Calculator(customization: .standard)
68 |
69 | let answerPosition: AnswerPosition
70 | let textStorage: NSTextStorage
71 | let textContainer: NSTextContainer
72 |
73 | public init(answerPosition: AnswerPosition, textStorage: NSTextStorage, textContainer: NSTextContainer) {
74 |
75 | self.answerPosition = answerPosition
76 | self.textStorage = textStorage
77 | self.textContainer = textContainer
78 |
79 | self.stringByParagraphs = StringByParagraphs(contents: textStorage.string)
80 |
81 | }
82 |
83 | // MARK: - Please call the following methods (when appropriate)
84 |
85 | public func textDidChange() {
86 |
87 | // Hold onto the previous state before the text changed
88 | let previousStringByParagraphs = self.stringByParagraphs
89 |
90 | // Update to the new state of the textStorage
91 | self.stringByParagraphs = StringByParagraphs(contents: textStorage.string)
92 |
93 | // Determine which lines have been edited
94 | let editedLines = self.stringByParagraphs.indexesDifferingFrom(stringByParagraphs: previousStringByParagraphs)
95 |
96 | // And which of those lines are actually Soulver lines
97 | let editedSoulverLines = self.indexesOfSoulverLines.intersection(editedLines)
98 |
99 | // Re-evaluate those lines and reformat
100 | self.evaluateLinesAt(indexes: editedSoulverLines)
101 |
102 | // the tab stops need to be updated for certain answer position styles after editing
103 | self.reformatPargraphStyleAt(paragraphIndexes: editedSoulverLines)
104 |
105 |
106 | }
107 |
108 | public func layoutDidChange() {
109 |
110 | /// Updates the tab stop size to keep the results hugging the right side of the text container
111 | self.reformatPargraphStyleAt(paragraphIndexes: self.indexesOfSoulverLines)
112 |
113 | }
114 |
115 | public func shouldAllowReplacementFor(affectedCharRange: NSRange, replacementString: String?) -> TextReplacementDecision {
116 |
117 | if self.rangeIntersectsSoulverLine(range: affectedCharRange) {
118 |
119 | if let replacementString = replacementString, replacementString == "\n" {
120 |
121 | // You're allowed to insert a new line from position 0 on a line
122 | if self.stringByParagraphs.rangeOfParagraphContainingLocation(affectedCharRange.lowerBound).location == affectedCharRange.location {
123 | return .allow
124 | }
125 |
126 | // Manually insert a new line below, rather than breaking up the existing line
127 | if let newInsertionPoint = self.insertLine(belowLineContaining: affectedCharRange) {
128 | return .setIntertionPoint(range: newInsertionPoint)
129 | }
130 |
131 | }
132 |
133 | // No editing a result please
134 | else if self.rangeIsInsideResult(range: affectedCharRange) {
135 | return .deny
136 | }
137 |
138 | }
139 | else if replacementString == self.answerPosition.trigger {
140 |
141 | self.makeSoulverLineAt(lineIndex: self.stringByParagraphs.indexOfParagraphContainingLocation(affectedCharRange.location))
142 |
143 | return .setIntertionPoint(range: NSMakeRange(affectedCharRange.lowerBound, 0))
144 |
145 | }
146 |
147 | return .allow
148 |
149 | }
150 |
151 | // MARK: - Evaluation
152 |
153 | private func evaluateLinesAt(indexes: IndexSet) {
154 |
155 | guard indexes.isNotEmpty else {
156 | return
157 | }
158 |
159 | for lineIndex in indexes.reversed() {
160 |
161 | guard let expression = self.expressionOn(lineIndex: lineIndex), let resultRange = self.resultRangeOn(lineIndex: lineIndex) else {
162 | continue
163 | }
164 |
165 | let newResult = calculator.calculate(expression).stringValue
166 |
167 | if let oldResult = self.resultOn(lineIndex: lineIndex), oldResult == newResult {
168 | // these results are identical, skip updating the text storage
169 | continue
170 | }
171 |
172 | textStorage.replaceCharacters(in: resultRange, with: newResult)
173 |
174 | }
175 |
176 | // Update our indexed string with the new ranges
177 | self.stringByParagraphs = StringByParagraphs(contents: textStorage.string)
178 |
179 | }
180 |
181 | // MARK: - Formatting
182 |
183 | private var paragraphStyle: NSParagraphStyle {
184 |
185 | let paragraphStyle = NSMutableParagraphStyle()
186 |
187 | paragraphStyle.paragraphSpacing = 6.0
188 |
189 | switch self.answerPosition {
190 | case .afterTab:
191 | paragraphStyle.tabStops = [
192 | NSTextTab(textAlignment: .right, location: self.textContainer.rightEdgeTabPoint, options: [:]),
193 | ]
194 |
195 | case .afterPipe:
196 | paragraphStyle.tabStops = [
197 | NSTextTab(textAlignment: .left, location: self.textContainer.standardAnwswerColumnSizeTabPoint, options: [:]),
198 | ]
199 | case .afterEquals:
200 | break
201 | }
202 |
203 | return paragraphStyle
204 |
205 | }
206 |
207 | private func reformatPargraphStyleAt(paragraphIndexes: IndexSet) {
208 |
209 | guard paragraphIndexes.isNotEmpty else {
210 | return
211 | }
212 |
213 | let paragraphStyle = self.paragraphStyle
214 |
215 | for lineIndex in paragraphIndexes.reversed() {
216 |
217 | let lineRange = self.stringByParagraphs.rangeOfParagraphAtIndex(lineIndex)
218 |
219 | self.textStorage.addAttributes([.paragraphStyle : paragraphStyle], range: lineRange)
220 |
221 | }
222 |
223 | }
224 |
225 |
226 |
227 | // MARK: - Which lines in the text view are Soulver lines?
228 |
229 | private func isSoulverLineOn(lineIndex: LineIndex) -> Bool {
230 |
231 | let line = self.stringByParagraphs[lineIndex, .contents]
232 | return line.components(separatedBy: self.answerPosition.divider).count == 2
233 |
234 | }
235 |
236 | private var indexesOfSoulverLines: IndexSet {
237 |
238 | var indexSet = IndexSet()
239 |
240 | for lineIndex in (0 ..< stringByParagraphs.paragraphCount) {
241 |
242 | if self.isSoulverLineOn(lineIndex: lineIndex) {
243 | indexSet.insert(lineIndex)
244 | }
245 |
246 | }
247 |
248 | return indexSet
249 |
250 | }
251 |
252 |
253 | // MARK: - Inserting lines & making Soulver lines
254 |
255 | private func makeSoulverLineAt(lineIndex: LineIndex) {
256 |
257 | let rangeOfParagraph = self.stringByParagraphs.contentsRangeOfParagraphAtIndex(lineIndex)
258 |
259 | let attributes = self.textStorage.attributes(at: rangeOfParagraph.lowerBound, effectiveRange: nil)
260 |
261 | self.textStorage.insert(NSAttributedString(string: self.answerPosition.divider, attributes: attributes), at: rangeOfParagraph.upperBound)
262 |
263 | self.textDidChange()
264 |
265 | }
266 |
267 | /// Manually insert a new line below the line containing the given range
268 | /// - Returns: the new insertion point for the text view
269 | private func insertLine(belowLineContaining range: NSRange) -> NSRange? {
270 |
271 | if let lineIndex = IndexSet(self.stringByParagraphs.paragraphIndexesContainingRange(range, ignoreFinalTouchedParagraph: true)).last {
272 |
273 | let rangeOfParagraph = self.stringByParagraphs.contentsRangeOfParagraphAtIndex(lineIndex)
274 |
275 | let attributes = self.textStorage.attributes(at: rangeOfParagraph.lowerBound, effectiveRange: nil)
276 |
277 | self.textStorage.insert(NSAttributedString(string: "\n", attributes: attributes), at: rangeOfParagraph.upperBound)
278 |
279 | self.textDidChange()
280 |
281 | return self.stringByParagraphs.contentsRangeOfParagraphAtIndex(lineIndex + 1)
282 | }
283 |
284 | return nil
285 |
286 | }
287 |
288 | // MARK: - Utility (getting expressions, results and their ranges)
289 |
290 | private func rangeIntersectsSoulverLine(range: NSRange) -> Bool {
291 |
292 | // Which lines are included in this range?
293 | let affectedLines = IndexSet(self.stringByParagraphs.paragraphIndexesContainingRange(range, ignoreFinalTouchedParagraph: true))
294 |
295 | if self.indexesOfSoulverLines.intersection(affectedLines).count > 0 {
296 | return true
297 | }
298 |
299 | return false
300 |
301 | }
302 |
303 | private func rangeIntersectsExpression(range: NSRange) -> Bool {
304 |
305 | // Which lines are included in this range?
306 | let affectedLines = self.stringByParagraphs.paragraphIndexesContainingRange(range, ignoreFinalTouchedParagraph: true)
307 |
308 | // If it's just one line, and the line is a Soulver line with a result
309 | if affectedLines.count == 1, let editingLineIndex = affectedLines.first, let expressionRange = self.expressionRangeOn(lineIndex: editingLineIndex) {
310 |
311 | // Check the result is not in the edited range
312 | let intersection = NSIntersectionRange(expressionRange, range)
313 |
314 | if intersection.location > 0 {
315 | return true
316 | }
317 |
318 | }
319 |
320 | return false
321 | }
322 |
323 | private func rangeIsInsideResult(range: NSRange) -> Bool {
324 |
325 | // Which lines are included in this range?
326 | let affectedLines = self.stringByParagraphs.paragraphIndexesContainingRange(range, ignoreFinalTouchedParagraph: true)
327 |
328 | // If it's just one line, and the line is a Soulver line with a result
329 | if affectedLines.count == 1, let editingLineIndex = affectedLines.first, let resultRange = self.resultRangeOn(lineIndex: editingLineIndex) {
330 |
331 | if range.location >= resultRange.location {
332 |
333 | // Check the result is not in the edited range
334 | let intersection = NSIntersectionRange(resultRange, range)
335 |
336 | if intersection.location > 0 {
337 | return true
338 | }
339 |
340 | }
341 | }
342 |
343 | return false
344 |
345 | }
346 |
347 | private func expressionOn(lineIndex: LineIndex) -> String? {
348 |
349 | let line = self.stringByParagraphs[lineIndex, .contents]
350 | return line.components(separatedBy: answerPosition.divider)[safe: 0]
351 |
352 | }
353 |
354 | private func resultOn(lineIndex: LineIndex) -> String? {
355 |
356 | let line = self.stringByParagraphs[lineIndex, .contents]
357 | return line.components(separatedBy: answerPosition.divider)[safe: 1]
358 |
359 | }
360 |
361 |
362 | private func expressionRangeOn(lineIndex: LineIndex) -> NSRange? {
363 |
364 | guard self.isSoulverLineOn(lineIndex: lineIndex) else {
365 | return nil
366 | }
367 |
368 | let line = self.stringByParagraphs[lineIndex, .contents]
369 |
370 | if let localExpressionRange = line.components(separatedBy: self.answerPosition.divider)[safe: 0]?.completeStringRange {
371 |
372 | let globalResultRange = self.stringByParagraphs.globalRangeFor(localRange: localExpressionRange, on: lineIndex)
373 |
374 | return globalResultRange
375 | }
376 |
377 | return nil
378 |
379 | }
380 |
381 |
382 | private func resultRangeOn(lineIndex: LineIndex) -> NSRange? {
383 |
384 | let line = self.stringByParagraphs[lineIndex, .contents]
385 |
386 | if let resultRange = line.components(separatedBy: self.answerPosition.divider)[safe: 1] {
387 |
388 | let localResultRange = NSMakeRange(line.count - resultRange.count, resultRange.count)
389 | let globalResultRange = self.stringByParagraphs.globalRangeFor(localRange: localResultRange, on: lineIndex)
390 |
391 | return globalResultRange
392 | }
393 |
394 | return nil
395 |
396 | }
397 |
398 | }
399 |
400 |
401 | private extension NSTextContainer {
402 |
403 | var rightEdgeTabPoint: CGFloat {
404 | return self.size.width - self.lineFragmentPadding * 2
405 | }
406 |
407 | var standardAnwswerColumnSizeTabPoint: CGFloat {
408 | return self.size.width - 200.0
409 | }
410 | }
411 |
412 |
413 | public extension Array where Element == String {
414 |
415 | func expressionStringFor(style: AnswerPosition) -> String {
416 |
417 | var placeholderString = ""
418 |
419 | for expression in self {
420 |
421 | if placeholderString.isNotEmpty {
422 | placeholderString.append("\n")
423 | }
424 |
425 | placeholderString.append(expression + style.divider)
426 | }
427 |
428 | return placeholderString
429 |
430 | }
431 |
432 | }
433 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import SoulverTextKitTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += SoulverTextKitTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/SoulverTextKitTests/SoulverTextKitTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import SoulverTextKit
3 |
4 | final class SoulverTextKitTests: XCTestCase {
5 | func testExample() {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | XCTAssertEqual(SoulverTextKit().text, "Hello, World!")
10 | }
11 |
12 | static var allTests = [
13 | ("testExample", testExample),
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/Tests/SoulverTextKitTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(SoulverTextKitTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------