├── .gitignore ├── .swift-version ├── .swiftlint.yml ├── .travis.yml ├── Attributed.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Attributed-watchOS.xcscheme │ └── Attributed.xcscheme ├── Attributed ├── Attributed.h ├── Attributed.swift ├── Attributes.swift ├── Info.plist ├── NSString+Attributed.swift ├── Operators.swift └── String+Attributed.swift ├── AttributedLib.podspec ├── AttributedTests ├── AttributedPublicTests.swift ├── AttributesTests.swift ├── Info.plist └── StringExtensionTests.swift ├── CHANGELOG.md ├── LICENSE ├── Package.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | .DS_Store 22 | *.moved-aside 23 | *.xcuserstate 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | .build/ 40 | .swiftpm 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - line_length 3 | - cyclomatic_complexity 4 | - compiler_protocol_init 5 | - force_cast 6 | - colon 7 | - comma 8 | - control_statement 9 | - identifier_name 10 | opt_in_rules: 11 | - empty_count 12 | included: 13 | - Attributed 14 | line_length: 150 15 | file_length: 16 | warning: 500 17 | error: 1200 18 | reporter: "xcode" 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10.2 3 | 4 | script: 5 | - xcodebuild -scheme Attributed build test -destination 'OS=12.0,name=iPhone 7 Plus' 6 | 7 | after_success: 8 | - bash <(curl -s https://codecov.io/bash) 9 | -------------------------------------------------------------------------------- /Attributed.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A53787B0254B376B004CF589 /* String+Attributed.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEE62891DF8F9560079C70C /* String+Attributed.swift */; }; 11 | A53787B1254B376B004CF589 /* NSString+Attributed.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABEF4EB1E98F532003260B7 /* NSString+Attributed.swift */; }; 12 | A53787B2254B376B004CF589 /* Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABEF4ED1E98F62C003260B7 /* Attributes.swift */; }; 13 | A53787B3254B376B004CF589 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEE62871DF8F3BE0079C70C /* Operators.swift */; }; 14 | A53787B4254B376B004CF589 /* Attributed.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3150371EA4A57300A86368 /* Attributed.swift */; }; 15 | A53787B7254B376B004CF589 /* Attributed.h in Headers */ = {isa = PBXBuildFile; fileRef = AAEE62701DF8F3690079C70C /* Attributed.h */; settings = {ATTRIBUTES = (Public, ); }; }; 16 | AA28CE69206A0D5F0025AA29 /* AttributedPublicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA28CE68206A0D5F0025AA29 /* AttributedPublicTests.swift */; }; 17 | AA3150381EA4A57300A86368 /* Attributed.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3150371EA4A57300A86368 /* Attributed.swift */; }; 18 | AA4B73E11E0B798C004C6947 /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4B73E01E0B798C004C6947 /* StringExtensionTests.swift */; }; 19 | AABEF4EC1E98F532003260B7 /* NSString+Attributed.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABEF4EB1E98F532003260B7 /* NSString+Attributed.swift */; }; 20 | AABEF4EE1E98F62C003260B7 /* Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABEF4ED1E98F62C003260B7 /* Attributes.swift */; }; 21 | AAE1A2771E01537500610C40 /* AttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE1A2761E01537500610C40 /* AttributesTests.swift */; }; 22 | AAEE62771DF8F3690079C70C /* Attributed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AAEE626D1DF8F3690079C70C /* Attributed.framework */; }; 23 | AAEE627E1DF8F36A0079C70C /* Attributed.h in Headers */ = {isa = PBXBuildFile; fileRef = AAEE62701DF8F3690079C70C /* Attributed.h */; settings = {ATTRIBUTES = (Public, ); }; }; 24 | AAEE62881DF8F3BE0079C70C /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEE62871DF8F3BE0079C70C /* Operators.swift */; }; 25 | AAEE628A1DF8F9560079C70C /* String+Attributed.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEE62891DF8F9560079C70C /* String+Attributed.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | AAEE62781DF8F3690079C70C /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = AAEE62641DF8F3690079C70C /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = AAEE626C1DF8F3690079C70C; 34 | remoteInfo = Attributed; 35 | }; 36 | /* End PBXContainerItemProxy section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | A53787BC254B376B004CF589 /* Attributed-watchOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = "Attributed-watchOS.framework"; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | AA28CE68206A0D5F0025AA29 /* AttributedPublicTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedPublicTests.swift; sourceTree = ""; }; 41 | AA3150371EA4A57300A86368 /* Attributed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Attributed.swift; sourceTree = ""; }; 42 | AA4B73E01E0B798C004C6947 /* StringExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionTests.swift; sourceTree = ""; }; 43 | AABEF4EB1E98F532003260B7 /* NSString+Attributed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSString+Attributed.swift"; sourceTree = ""; }; 44 | AABEF4ED1E98F62C003260B7 /* Attributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Attributes.swift; sourceTree = ""; }; 45 | AAE1A2761E01537500610C40 /* AttributesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributesTests.swift; sourceTree = ""; }; 46 | AAEE626D1DF8F3690079C70C /* Attributed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Attributed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | AAEE62701DF8F3690079C70C /* Attributed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Attributed.h; sourceTree = ""; }; 48 | AAEE62711DF8F3690079C70C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | AAEE62761DF8F3690079C70C /* AttributedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AttributedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | AAEE627D1DF8F36A0079C70C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | AAEE62871DF8F3BE0079C70C /* Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; }; 52 | AAEE62891DF8F9560079C70C /* String+Attributed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Attributed.swift"; sourceTree = ""; }; 53 | /* End PBXFileReference section */ 54 | 55 | /* Begin PBXFrameworksBuildPhase section */ 56 | A53787B5254B376B004CF589 /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | ); 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | AAEE62691DF8F3690079C70C /* Frameworks */ = { 64 | isa = PBXFrameworksBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | AAEE62731DF8F3690079C70C /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | AAEE62771DF8F3690079C70C /* Attributed.framework in Frameworks */, 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | /* End PBXFrameworksBuildPhase section */ 79 | 80 | /* Begin PBXGroup section */ 81 | A5378806254B3DC5004CF589 /* Frameworks */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | ); 85 | name = Frameworks; 86 | sourceTree = ""; 87 | }; 88 | AABEF4EF1E98F696003260B7 /* Attributed */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | AABEF4ED1E98F62C003260B7 /* Attributes.swift */, 92 | AAEE62871DF8F3BE0079C70C /* Operators.swift */, 93 | AA3150371EA4A57300A86368 /* Attributed.swift */, 94 | ); 95 | name = Attributed; 96 | sourceTree = ""; 97 | }; 98 | AABEF4F01E98F6C6003260B7 /* Extensions */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | AABEF4EB1E98F532003260B7 /* NSString+Attributed.swift */, 102 | AAEE62891DF8F9560079C70C /* String+Attributed.swift */, 103 | ); 104 | name = Extensions; 105 | sourceTree = ""; 106 | }; 107 | AABEF4F11E98F700003260B7 /* Supporting Files */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | AAEE62701DF8F3690079C70C /* Attributed.h */, 111 | AAEE62711DF8F3690079C70C /* Info.plist */, 112 | ); 113 | name = "Supporting Files"; 114 | sourceTree = ""; 115 | }; 116 | AABEF4F21E98F73F003260B7 /* Supporting Files */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | AAEE627D1DF8F36A0079C70C /* Info.plist */, 120 | ); 121 | name = "Supporting Files"; 122 | sourceTree = ""; 123 | }; 124 | AAEE62631DF8F3690079C70C = { 125 | isa = PBXGroup; 126 | children = ( 127 | AAEE626F1DF8F3690079C70C /* Attributed */, 128 | AAEE627A1DF8F3690079C70C /* AttributedTests */, 129 | AAEE626E1DF8F3690079C70C /* Products */, 130 | A5378806254B3DC5004CF589 /* Frameworks */, 131 | ); 132 | sourceTree = ""; 133 | }; 134 | AAEE626E1DF8F3690079C70C /* Products */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | AAEE626D1DF8F3690079C70C /* Attributed.framework */, 138 | AAEE62761DF8F3690079C70C /* AttributedTests.xctest */, 139 | A53787BC254B376B004CF589 /* Attributed-watchOS.framework */, 140 | ); 141 | name = Products; 142 | sourceTree = ""; 143 | }; 144 | AAEE626F1DF8F3690079C70C /* Attributed */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | AABEF4EF1E98F696003260B7 /* Attributed */, 148 | AABEF4F01E98F6C6003260B7 /* Extensions */, 149 | AABEF4F11E98F700003260B7 /* Supporting Files */, 150 | ); 151 | path = Attributed; 152 | sourceTree = ""; 153 | }; 154 | AAEE627A1DF8F3690079C70C /* AttributedTests */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | AA28CE68206A0D5F0025AA29 /* AttributedPublicTests.swift */, 158 | AAE1A2761E01537500610C40 /* AttributesTests.swift */, 159 | AA4B73E01E0B798C004C6947 /* StringExtensionTests.swift */, 160 | AABEF4F21E98F73F003260B7 /* Supporting Files */, 161 | ); 162 | path = AttributedTests; 163 | sourceTree = ""; 164 | }; 165 | /* End PBXGroup section */ 166 | 167 | /* Begin PBXHeadersBuildPhase section */ 168 | A53787B6254B376B004CF589 /* Headers */ = { 169 | isa = PBXHeadersBuildPhase; 170 | buildActionMask = 2147483647; 171 | files = ( 172 | A53787B7254B376B004CF589 /* Attributed.h in Headers */, 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | AAEE626A1DF8F3690079C70C /* Headers */ = { 177 | isa = PBXHeadersBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | AAEE627E1DF8F36A0079C70C /* Attributed.h in Headers */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXHeadersBuildPhase section */ 185 | 186 | /* Begin PBXNativeTarget section */ 187 | A53787AE254B376B004CF589 /* Attributed-watchOS */ = { 188 | isa = PBXNativeTarget; 189 | buildConfigurationList = A53787B9254B376B004CF589 /* Build configuration list for PBXNativeTarget "Attributed-watchOS" */; 190 | buildPhases = ( 191 | A53787AF254B376B004CF589 /* Sources */, 192 | A53787B5254B376B004CF589 /* Frameworks */, 193 | A53787B6254B376B004CF589 /* Headers */, 194 | A53787B8254B376B004CF589 /* Resources */, 195 | ); 196 | buildRules = ( 197 | ); 198 | dependencies = ( 199 | ); 200 | name = "Attributed-watchOS"; 201 | productName = Attributed; 202 | productReference = A53787BC254B376B004CF589 /* Attributed-watchOS.framework */; 203 | productType = "com.apple.product-type.framework"; 204 | }; 205 | AAEE626C1DF8F3690079C70C /* Attributed */ = { 206 | isa = PBXNativeTarget; 207 | buildConfigurationList = AAEE62811DF8F36A0079C70C /* Build configuration list for PBXNativeTarget "Attributed" */; 208 | buildPhases = ( 209 | AAEE62681DF8F3690079C70C /* Sources */, 210 | AAEE62691DF8F3690079C70C /* Frameworks */, 211 | AAEE626A1DF8F3690079C70C /* Headers */, 212 | AAEE626B1DF8F3690079C70C /* Resources */, 213 | ); 214 | buildRules = ( 215 | ); 216 | dependencies = ( 217 | ); 218 | name = Attributed; 219 | productName = Attributed; 220 | productReference = AAEE626D1DF8F3690079C70C /* Attributed.framework */; 221 | productType = "com.apple.product-type.framework"; 222 | }; 223 | AAEE62751DF8F3690079C70C /* AttributedTests */ = { 224 | isa = PBXNativeTarget; 225 | buildConfigurationList = AAEE62841DF8F36A0079C70C /* Build configuration list for PBXNativeTarget "AttributedTests" */; 226 | buildPhases = ( 227 | AAEE62721DF8F3690079C70C /* Sources */, 228 | AAEE62731DF8F3690079C70C /* Frameworks */, 229 | AAEE62741DF8F3690079C70C /* Resources */, 230 | ); 231 | buildRules = ( 232 | ); 233 | dependencies = ( 234 | AAEE62791DF8F3690079C70C /* PBXTargetDependency */, 235 | ); 236 | name = AttributedTests; 237 | productName = AttributedTests; 238 | productReference = AAEE62761DF8F3690079C70C /* AttributedTests.xctest */; 239 | productType = "com.apple.product-type.bundle.unit-test"; 240 | }; 241 | /* End PBXNativeTarget section */ 242 | 243 | /* Begin PBXProject section */ 244 | AAEE62641DF8F3690079C70C /* Project object */ = { 245 | isa = PBXProject; 246 | attributes = { 247 | LastSwiftUpdateCheck = 0810; 248 | LastUpgradeCheck = 1240; 249 | ORGANIZATIONNAME = Attributed; 250 | TargetAttributes = { 251 | A53787AE254B376B004CF589 = { 252 | ProvisioningStyle = Automatic; 253 | }; 254 | AAEE626C1DF8F3690079C70C = { 255 | CreatedOnToolsVersion = 8.1; 256 | LastSwiftMigration = 1020; 257 | ProvisioningStyle = Automatic; 258 | }; 259 | AAEE62751DF8F3690079C70C = { 260 | CreatedOnToolsVersion = 8.1; 261 | LastSwiftMigration = 1020; 262 | ProvisioningStyle = Automatic; 263 | }; 264 | }; 265 | }; 266 | buildConfigurationList = AAEE62671DF8F3690079C70C /* Build configuration list for PBXProject "Attributed" */; 267 | compatibilityVersion = "Xcode 3.2"; 268 | developmentRegion = en; 269 | hasScannedForEncodings = 0; 270 | knownRegions = ( 271 | en, 272 | Base, 273 | ); 274 | mainGroup = AAEE62631DF8F3690079C70C; 275 | productRefGroup = AAEE626E1DF8F3690079C70C /* Products */; 276 | projectDirPath = ""; 277 | projectRoot = ""; 278 | targets = ( 279 | AAEE626C1DF8F3690079C70C /* Attributed */, 280 | AAEE62751DF8F3690079C70C /* AttributedTests */, 281 | A53787AE254B376B004CF589 /* Attributed-watchOS */, 282 | ); 283 | }; 284 | /* End PBXProject section */ 285 | 286 | /* Begin PBXResourcesBuildPhase section */ 287 | A53787B8254B376B004CF589 /* Resources */ = { 288 | isa = PBXResourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | AAEE626B1DF8F3690079C70C /* Resources */ = { 295 | isa = PBXResourcesBuildPhase; 296 | buildActionMask = 2147483647; 297 | files = ( 298 | ); 299 | runOnlyForDeploymentPostprocessing = 0; 300 | }; 301 | AAEE62741DF8F3690079C70C /* Resources */ = { 302 | isa = PBXResourcesBuildPhase; 303 | buildActionMask = 2147483647; 304 | files = ( 305 | ); 306 | runOnlyForDeploymentPostprocessing = 0; 307 | }; 308 | /* End PBXResourcesBuildPhase section */ 309 | 310 | /* Begin PBXSourcesBuildPhase section */ 311 | A53787AF254B376B004CF589 /* Sources */ = { 312 | isa = PBXSourcesBuildPhase; 313 | buildActionMask = 2147483647; 314 | files = ( 315 | A53787B0254B376B004CF589 /* String+Attributed.swift in Sources */, 316 | A53787B1254B376B004CF589 /* NSString+Attributed.swift in Sources */, 317 | A53787B2254B376B004CF589 /* Attributes.swift in Sources */, 318 | A53787B3254B376B004CF589 /* Operators.swift in Sources */, 319 | A53787B4254B376B004CF589 /* Attributed.swift in Sources */, 320 | ); 321 | runOnlyForDeploymentPostprocessing = 0; 322 | }; 323 | AAEE62681DF8F3690079C70C /* Sources */ = { 324 | isa = PBXSourcesBuildPhase; 325 | buildActionMask = 2147483647; 326 | files = ( 327 | AAEE628A1DF8F9560079C70C /* String+Attributed.swift in Sources */, 328 | AABEF4EC1E98F532003260B7 /* NSString+Attributed.swift in Sources */, 329 | AABEF4EE1E98F62C003260B7 /* Attributes.swift in Sources */, 330 | AAEE62881DF8F3BE0079C70C /* Operators.swift in Sources */, 331 | AA3150381EA4A57300A86368 /* Attributed.swift in Sources */, 332 | ); 333 | runOnlyForDeploymentPostprocessing = 0; 334 | }; 335 | AAEE62721DF8F3690079C70C /* Sources */ = { 336 | isa = PBXSourcesBuildPhase; 337 | buildActionMask = 2147483647; 338 | files = ( 339 | AA4B73E11E0B798C004C6947 /* StringExtensionTests.swift in Sources */, 340 | AA28CE69206A0D5F0025AA29 /* AttributedPublicTests.swift in Sources */, 341 | AAE1A2771E01537500610C40 /* AttributesTests.swift in Sources */, 342 | ); 343 | runOnlyForDeploymentPostprocessing = 0; 344 | }; 345 | /* End PBXSourcesBuildPhase section */ 346 | 347 | /* Begin PBXTargetDependency section */ 348 | AAEE62791DF8F3690079C70C /* PBXTargetDependency */ = { 349 | isa = PBXTargetDependency; 350 | target = AAEE626C1DF8F3690079C70C /* Attributed */; 351 | targetProxy = AAEE62781DF8F3690079C70C /* PBXContainerItemProxy */; 352 | }; 353 | /* End PBXTargetDependency section */ 354 | 355 | /* Begin XCBuildConfiguration section */ 356 | A53787BA254B376B004CF589 /* Debug */ = { 357 | isa = XCBuildConfiguration; 358 | buildSettings = { 359 | CLANG_ENABLE_MODULES = YES; 360 | CODE_SIGN_IDENTITY = ""; 361 | CODE_SIGN_STYLE = Automatic; 362 | DEFINES_MODULE = YES; 363 | DEVELOPMENT_TEAM = ""; 364 | DYLIB_COMPATIBILITY_VERSION = 1; 365 | DYLIB_CURRENT_VERSION = 1; 366 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 367 | INFOPLIST_FILE = Attributed/Info.plist; 368 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 369 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 370 | PRODUCT_BUNDLE_IDENTIFIER = attributed.Attributed; 371 | PRODUCT_NAME = "$(TARGET_NAME)"; 372 | PROVISIONING_PROFILE_SPECIFIER = ""; 373 | SDKROOT = watchos; 374 | SKIP_INSTALL = YES; 375 | SUPPORTED_PLATFORMS = "watchsimulator watchos"; 376 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 377 | SWIFT_VERSION = 5.0; 378 | TARGETED_DEVICE_FAMILY = 4; 379 | WATCHOS_DEPLOYMENT_TARGET = 3.0; 380 | }; 381 | name = Debug; 382 | }; 383 | A53787BB254B376B004CF589 /* Release */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | CLANG_ENABLE_MODULES = YES; 387 | CODE_SIGN_IDENTITY = ""; 388 | CODE_SIGN_STYLE = Automatic; 389 | DEFINES_MODULE = YES; 390 | DEVELOPMENT_TEAM = ""; 391 | DYLIB_COMPATIBILITY_VERSION = 1; 392 | DYLIB_CURRENT_VERSION = 1; 393 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 394 | INFOPLIST_FILE = Attributed/Info.plist; 395 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 396 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 397 | PRODUCT_BUNDLE_IDENTIFIER = attributed.Attributed; 398 | PRODUCT_NAME = "$(TARGET_NAME)"; 399 | PROVISIONING_PROFILE_SPECIFIER = ""; 400 | SDKROOT = watchos; 401 | SKIP_INSTALL = YES; 402 | SUPPORTED_PLATFORMS = "watchsimulator watchos"; 403 | SWIFT_VERSION = 5.0; 404 | TARGETED_DEVICE_FAMILY = 4; 405 | WATCHOS_DEPLOYMENT_TARGET = 3.0; 406 | }; 407 | name = Release; 408 | }; 409 | AAEE627F1DF8F36A0079C70C /* Debug */ = { 410 | isa = XCBuildConfiguration; 411 | buildSettings = { 412 | ALWAYS_SEARCH_USER_PATHS = NO; 413 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 414 | CLANG_ANALYZER_NONNULL = YES; 415 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 416 | CLANG_CXX_LIBRARY = "libc++"; 417 | CLANG_ENABLE_MODULES = YES; 418 | CLANG_ENABLE_OBJC_ARC = YES; 419 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 420 | CLANG_WARN_BOOL_CONVERSION = YES; 421 | CLANG_WARN_COMMA = YES; 422 | CLANG_WARN_CONSTANT_CONVERSION = YES; 423 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 424 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 425 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 426 | CLANG_WARN_EMPTY_BODY = YES; 427 | CLANG_WARN_ENUM_CONVERSION = YES; 428 | CLANG_WARN_INFINITE_RECURSION = YES; 429 | CLANG_WARN_INT_CONVERSION = YES; 430 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 431 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 432 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 433 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 434 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 435 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 436 | CLANG_WARN_STRICT_PROTOTYPES = YES; 437 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 438 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 439 | CLANG_WARN_UNREACHABLE_CODE = YES; 440 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 441 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 442 | COPY_PHASE_STRIP = NO; 443 | CURRENT_PROJECT_VERSION = 1; 444 | DEBUG_INFORMATION_FORMAT = dwarf; 445 | ENABLE_STRICT_OBJC_MSGSEND = YES; 446 | ENABLE_TESTABILITY = YES; 447 | GCC_C_LANGUAGE_STANDARD = gnu99; 448 | GCC_DYNAMIC_NO_PIC = NO; 449 | GCC_NO_COMMON_BLOCKS = YES; 450 | GCC_OPTIMIZATION_LEVEL = 0; 451 | GCC_PREPROCESSOR_DEFINITIONS = ( 452 | "DEBUG=1", 453 | "$(inherited)", 454 | ); 455 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 456 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 457 | GCC_WARN_UNDECLARED_SELECTOR = YES; 458 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 459 | GCC_WARN_UNUSED_FUNCTION = YES; 460 | GCC_WARN_UNUSED_VARIABLE = YES; 461 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 462 | MTL_ENABLE_DEBUG_INFO = YES; 463 | ONLY_ACTIVE_ARCH = YES; 464 | SDKROOT = iphoneos; 465 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 466 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 467 | TARGETED_DEVICE_FAMILY = "1,2"; 468 | VERSIONING_SYSTEM = "apple-generic"; 469 | VERSION_INFO_PREFIX = ""; 470 | }; 471 | name = Debug; 472 | }; 473 | AAEE62801DF8F36A0079C70C /* Release */ = { 474 | isa = XCBuildConfiguration; 475 | buildSettings = { 476 | ALWAYS_SEARCH_USER_PATHS = NO; 477 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 478 | CLANG_ANALYZER_NONNULL = YES; 479 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 480 | CLANG_CXX_LIBRARY = "libc++"; 481 | CLANG_ENABLE_MODULES = YES; 482 | CLANG_ENABLE_OBJC_ARC = YES; 483 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 484 | CLANG_WARN_BOOL_CONVERSION = YES; 485 | CLANG_WARN_COMMA = YES; 486 | CLANG_WARN_CONSTANT_CONVERSION = YES; 487 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 488 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 489 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 490 | CLANG_WARN_EMPTY_BODY = YES; 491 | CLANG_WARN_ENUM_CONVERSION = YES; 492 | CLANG_WARN_INFINITE_RECURSION = YES; 493 | CLANG_WARN_INT_CONVERSION = YES; 494 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 495 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 496 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 497 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 498 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 499 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 500 | CLANG_WARN_STRICT_PROTOTYPES = YES; 501 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 502 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 503 | CLANG_WARN_UNREACHABLE_CODE = YES; 504 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 505 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 506 | COPY_PHASE_STRIP = NO; 507 | CURRENT_PROJECT_VERSION = 1; 508 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 509 | ENABLE_NS_ASSERTIONS = NO; 510 | ENABLE_STRICT_OBJC_MSGSEND = YES; 511 | GCC_C_LANGUAGE_STANDARD = gnu99; 512 | GCC_NO_COMMON_BLOCKS = YES; 513 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 514 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 515 | GCC_WARN_UNDECLARED_SELECTOR = YES; 516 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 517 | GCC_WARN_UNUSED_FUNCTION = YES; 518 | GCC_WARN_UNUSED_VARIABLE = YES; 519 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 520 | MTL_ENABLE_DEBUG_INFO = NO; 521 | SDKROOT = iphoneos; 522 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 523 | TARGETED_DEVICE_FAMILY = "1,2"; 524 | VALIDATE_PRODUCT = YES; 525 | VERSIONING_SYSTEM = "apple-generic"; 526 | VERSION_INFO_PREFIX = ""; 527 | }; 528 | name = Release; 529 | }; 530 | AAEE62821DF8F36A0079C70C /* Debug */ = { 531 | isa = XCBuildConfiguration; 532 | buildSettings = { 533 | APPLICATION_EXTENSION_API_ONLY = NO; 534 | CLANG_ENABLE_MODULES = YES; 535 | CODE_SIGN_IDENTITY = ""; 536 | DEFINES_MODULE = YES; 537 | DYLIB_COMPATIBILITY_VERSION = 1; 538 | DYLIB_CURRENT_VERSION = 1; 539 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 540 | INFOPLIST_FILE = Attributed/Info.plist; 541 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 542 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 543 | PRODUCT_BUNDLE_IDENTIFIER = attributed.Attributed; 544 | PRODUCT_NAME = "$(TARGET_NAME)"; 545 | SKIP_INSTALL = YES; 546 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 547 | SWIFT_VERSION = 5.0; 548 | }; 549 | name = Debug; 550 | }; 551 | AAEE62831DF8F36A0079C70C /* Release */ = { 552 | isa = XCBuildConfiguration; 553 | buildSettings = { 554 | APPLICATION_EXTENSION_API_ONLY = NO; 555 | CLANG_ENABLE_MODULES = YES; 556 | CODE_SIGN_IDENTITY = ""; 557 | DEFINES_MODULE = YES; 558 | DYLIB_COMPATIBILITY_VERSION = 1; 559 | DYLIB_CURRENT_VERSION = 1; 560 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 561 | INFOPLIST_FILE = Attributed/Info.plist; 562 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 563 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 564 | PRODUCT_BUNDLE_IDENTIFIER = attributed.Attributed; 565 | PRODUCT_NAME = "$(TARGET_NAME)"; 566 | SKIP_INSTALL = YES; 567 | SWIFT_VERSION = 5.0; 568 | }; 569 | name = Release; 570 | }; 571 | AAEE62851DF8F36A0079C70C /* Debug */ = { 572 | isa = XCBuildConfiguration; 573 | buildSettings = { 574 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 575 | INFOPLIST_FILE = AttributedTests/Info.plist; 576 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 577 | PRODUCT_BUNDLE_IDENTIFIER = attributed.AttributedTests; 578 | PRODUCT_NAME = "$(TARGET_NAME)"; 579 | SWIFT_VERSION = 5.0; 580 | }; 581 | name = Debug; 582 | }; 583 | AAEE62861DF8F36A0079C70C /* Release */ = { 584 | isa = XCBuildConfiguration; 585 | buildSettings = { 586 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 587 | INFOPLIST_FILE = AttributedTests/Info.plist; 588 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 589 | PRODUCT_BUNDLE_IDENTIFIER = attributed.AttributedTests; 590 | PRODUCT_NAME = "$(TARGET_NAME)"; 591 | SWIFT_VERSION = 5.0; 592 | }; 593 | name = Release; 594 | }; 595 | /* End XCBuildConfiguration section */ 596 | 597 | /* Begin XCConfigurationList section */ 598 | A53787B9254B376B004CF589 /* Build configuration list for PBXNativeTarget "Attributed-watchOS" */ = { 599 | isa = XCConfigurationList; 600 | buildConfigurations = ( 601 | A53787BA254B376B004CF589 /* Debug */, 602 | A53787BB254B376B004CF589 /* Release */, 603 | ); 604 | defaultConfigurationIsVisible = 0; 605 | defaultConfigurationName = Release; 606 | }; 607 | AAEE62671DF8F3690079C70C /* Build configuration list for PBXProject "Attributed" */ = { 608 | isa = XCConfigurationList; 609 | buildConfigurations = ( 610 | AAEE627F1DF8F36A0079C70C /* Debug */, 611 | AAEE62801DF8F36A0079C70C /* Release */, 612 | ); 613 | defaultConfigurationIsVisible = 0; 614 | defaultConfigurationName = Release; 615 | }; 616 | AAEE62811DF8F36A0079C70C /* Build configuration list for PBXNativeTarget "Attributed" */ = { 617 | isa = XCConfigurationList; 618 | buildConfigurations = ( 619 | AAEE62821DF8F36A0079C70C /* Debug */, 620 | AAEE62831DF8F36A0079C70C /* Release */, 621 | ); 622 | defaultConfigurationIsVisible = 0; 623 | defaultConfigurationName = Release; 624 | }; 625 | AAEE62841DF8F36A0079C70C /* Build configuration list for PBXNativeTarget "AttributedTests" */ = { 626 | isa = XCConfigurationList; 627 | buildConfigurations = ( 628 | AAEE62851DF8F36A0079C70C /* Debug */, 629 | AAEE62861DF8F36A0079C70C /* Release */, 630 | ); 631 | defaultConfigurationIsVisible = 0; 632 | defaultConfigurationName = Release; 633 | }; 634 | /* End XCConfigurationList section */ 635 | }; 636 | rootObject = AAEE62641DF8F3690079C70C /* Project object */; 637 | } 638 | -------------------------------------------------------------------------------- /Attributed.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Attributed.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Attributed.xcodeproj/xcshareddata/xcschemes/Attributed-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Attributed.xcodeproj/xcshareddata/xcschemes/Attributed.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 64 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /Attributed/Attributed.h: -------------------------------------------------------------------------------- 1 | // 2 | // Attributed.h 3 | // Attributed 4 | // 5 | // Created by Nicholas Maccharoli on 2016/12/08. 6 | // Copyright © 2016年 Attributed. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Attributed. 12 | FOUNDATION_EXPORT double AttributedVersionNumber; 13 | 14 | //! Project version string for Attributed. 15 | FOUNDATION_EXPORT const unsigned char AttributedVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Attributed/Attributed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Attributed.swift 3 | // 4 | // Copyright (c) 2016-2023 Nicholas Maccharoli 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | import UIKit 26 | 27 | /** 28 | Features that exist in Attributed are written as extensions to this class `Attributed`. 29 | */ 30 | 31 | public final class Attributed { 32 | let base: Base 33 | public init(_ base: Base) { 34 | self.base = base 35 | } 36 | } 37 | 38 | /** 39 | Types that have Attributed extensions extend `AttributedCompatible`. 40 | */ 41 | 42 | public protocol AttributedCompatible { 43 | associatedtype CompatibleType 44 | 45 | var at: CompatibleType { get } 46 | } 47 | 48 | public extension AttributedCompatible { 49 | var at: Attributed { 50 | return Attributed(self) 51 | } 52 | } 53 | 54 | /** 55 | Types that wish to implement Attributed extensions should adapt `AttributedCompatible` first 56 | and then write extensions in the format of: 57 | 58 | `extension Attributed where Base == SomeType { ... }` 59 | 60 | The merit of adapting the `AttributedCompatible` protocol 61 | is that methods defined in this mannor are grouped under their own `at` namespace, therefore the chance 62 | of naming collisions is lower and when reading through code that uses the attributed library it is clear 63 | what is an extension that Attributed provides and what is not. 64 | */ 65 | 66 | extension String: AttributedCompatible { } 67 | extension NSString: AttributedCompatible { } 68 | extension NSAttributedString: AttributedCompatible { } 69 | -------------------------------------------------------------------------------- /Attributed/Attributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Attributes.swift 3 | // 4 | // Copyright (c) 2016-2023 Nicholas Maccharoli 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import UIKit 25 | 26 | public struct Attributes { 27 | 28 | public let dictionary: [NSAttributedString.Key: Any] 29 | 30 | public init() { 31 | dictionary = [:] 32 | } 33 | 34 | public init(_ attributesBlock: (Attributes) -> Attributes) { 35 | self = attributesBlock(Attributes()) 36 | } 37 | 38 | internal init(dictionary: [NSAttributedString.Key: Any]) { 39 | self.dictionary = dictionary 40 | } 41 | 42 | public func font(_ font: UIFont) -> Attributes { 43 | return self + Attributes(dictionary: [NSAttributedString.Key.font: font]) 44 | } 45 | 46 | public func kerning(_ kerning: Double) -> Attributes { 47 | return self + Attributes(dictionary: [NSAttributedString.Key.kern: NSNumber(floatLiteral: kerning)]) 48 | } 49 | 50 | public func strikeThroughStyle(_ strikeThroughStyle: NSUnderlineStyle) -> Attributes { 51 | return self + Attributes(dictionary: [NSAttributedString.Key.strikethroughStyle: strikeThroughStyle.rawValue, NSAttributedString.Key.baselineOffset : NSNumber(floatLiteral: 1.5)]) 52 | } 53 | 54 | public func underlineStyle(_ underlineStyle: NSUnderlineStyle) -> Attributes { 55 | return self + Attributes(dictionary: [NSAttributedString.Key.underlineStyle: underlineStyle.rawValue]) 56 | } 57 | 58 | public func strokeColor(_ strokeColor: UIColor) -> Attributes { 59 | return self + Attributes(dictionary: [NSAttributedString.Key.strokeColor: strokeColor]) 60 | } 61 | 62 | public func strokeWidth(_ strokewidth: Double) -> Attributes { 63 | return self + Attributes(dictionary: [NSAttributedString.Key.strokeWidth: NSNumber(floatLiteral: strokewidth)]) 64 | } 65 | 66 | public func foreground(color: UIColor) -> Attributes { 67 | return self + Attributes(dictionary: [NSAttributedString.Key.foregroundColor: color]) 68 | } 69 | 70 | public func background(color: UIColor) -> Attributes { 71 | return self + Attributes(dictionary: [NSAttributedString.Key.backgroundColor: color]) 72 | } 73 | 74 | public func paragraphStyle(_ paragraphStyle: NSParagraphStyle) -> Attributes { 75 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 76 | } 77 | 78 | public func shadow(_ shadow: NSShadow) -> Attributes { 79 | return self + Attributes(dictionary: [NSAttributedString.Key.shadow: shadow]) 80 | } 81 | 82 | public func obliqueness(_ value: CGFloat) -> Attributes { 83 | return self + Attributes(dictionary: [NSAttributedString.Key.obliqueness: value]) 84 | } 85 | 86 | public func link(_ link: String) -> Attributes { 87 | return self + Attributes(dictionary: [NSAttributedString.Key.link: link]) 88 | } 89 | 90 | public func baselineOffset(_ offset: NSNumber) -> Attributes { 91 | return self + Attributes(dictionary: [NSAttributedString.Key.baselineOffset: offset]) 92 | } 93 | } 94 | 95 | // MARK: NSParagraphStyle related 96 | 97 | extension Attributes { 98 | 99 | public func lineSpacing(_ lineSpacing: CGFloat) -> Attributes { 100 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 101 | paragraphStyle.lineSpacing = lineSpacing 102 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 103 | } 104 | 105 | public func paragraphSpacing(_ paragraphSpacing: CGFloat) -> Attributes { 106 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 107 | paragraphStyle.paragraphSpacing = paragraphSpacing 108 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 109 | } 110 | 111 | public func alignment(_ alignment: NSTextAlignment) -> Attributes { 112 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 113 | paragraphStyle.alignment = alignment 114 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 115 | } 116 | 117 | public func firstLineHeadIndent(_ firstLineHeadIndent: CGFloat) -> Attributes { 118 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 119 | paragraphStyle.firstLineHeadIndent = firstLineHeadIndent 120 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 121 | } 122 | 123 | public func headIndent(_ headIndent: CGFloat) -> Attributes { 124 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 125 | paragraphStyle.headIndent = headIndent 126 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 127 | } 128 | 129 | public func tailIndent(_ tailIndent: CGFloat) -> Attributes { 130 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 131 | paragraphStyle.tailIndent = tailIndent 132 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 133 | } 134 | 135 | public func lineBreakMode(_ lineBreakMode: NSLineBreakMode) -> Attributes { 136 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 137 | paragraphStyle.lineBreakMode = lineBreakMode 138 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 139 | } 140 | 141 | public func minimumLineHeight(_ minimumLineHeight: CGFloat) -> Attributes { 142 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 143 | paragraphStyle.minimumLineHeight = minimumLineHeight 144 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 145 | } 146 | 147 | public func maximumLineHeight(_ maximumLineHeight: CGFloat) -> Attributes { 148 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 149 | paragraphStyle.maximumLineHeight = maximumLineHeight 150 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 151 | } 152 | 153 | public func uniformLineHeight(_ uniformLineHeight: CGFloat) -> Attributes { 154 | return maximumLineHeight(uniformLineHeight).minimumLineHeight(uniformLineHeight) 155 | } 156 | 157 | public func baseWritingDirection(_ baseWritingDirection: NSWritingDirection) -> Attributes { 158 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 159 | paragraphStyle.baseWritingDirection = baseWritingDirection 160 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 161 | } 162 | 163 | public func lineHeightMultiple(_ lineHeightMultiple: CGFloat) -> Attributes { 164 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 165 | paragraphStyle.lineHeightMultiple = lineHeightMultiple 166 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 167 | } 168 | 169 | public func paragraphSpacingBefore(_ paragraphSpacingBefore: CGFloat) -> Attributes { 170 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 171 | paragraphStyle.paragraphSpacingBefore = paragraphSpacingBefore 172 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 173 | } 174 | 175 | public func hyphenationFactor(_ hyphenationFactor: Float) -> Attributes { 176 | let paragraphStyle = (dictionary[NSAttributedString.Key.paragraphStyle] ?? NSMutableParagraphStyle.default.mutableCopy()) as! NSMutableParagraphStyle 177 | paragraphStyle.hyphenationFactor = hyphenationFactor 178 | return self + Attributes(dictionary: [NSAttributedString.Key.paragraphStyle: paragraphStyle]) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Attributed/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 2.2.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Attributed/NSString+Attributed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+Attributed.swift 3 | // 4 | // Copyright (c) 2016-2023 Nicholas Maccharoli 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | 26 | extension Attributed where Base == NSString { 27 | 28 | public func attributed(with attributes: Attributes) -> NSAttributedString { 29 | return (base as String).at.attributed(with: attributes) 30 | } 31 | 32 | public func attributed(_ attributeBlock: (Attributes) -> (Attributes)) -> NSAttributedString { 33 | return (base as String).at.attributed(attributeBlock) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Attributed/Operators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Operators.swift 3 | // 4 | // Copyright (c) 2016-2023 Nicholas Maccharoli 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import UIKit 25 | 26 | public func + (lhs: NSAttributedString, rhs: NSAttributedString) -> NSAttributedString { 27 | let result = NSMutableAttributedString() 28 | result.append(lhs) 29 | result.append(rhs) 30 | return NSAttributedString(attributedString: result) 31 | } 32 | 33 | // NSParagraphStyle Addition 34 | 35 | public func + (lhs: NSParagraphStyle, rhs: NSParagraphStyle) -> NSParagraphStyle { 36 | let defaultParagraph = NSParagraphStyle.default 37 | let combinedAttributes = lhs.mutableCopy() as! NSMutableParagraphStyle 38 | 39 | if rhs.lineSpacing != defaultParagraph.lineSpacing { 40 | combinedAttributes.lineSpacing = rhs.lineSpacing 41 | } 42 | 43 | if rhs.paragraphSpacing != defaultParagraph.paragraphSpacing { 44 | combinedAttributes.paragraphSpacing = rhs.paragraphSpacing 45 | } 46 | 47 | if rhs.alignment != defaultParagraph.alignment { 48 | combinedAttributes.alignment = rhs.alignment 49 | } 50 | 51 | if rhs.firstLineHeadIndent != defaultParagraph.firstLineHeadIndent { 52 | combinedAttributes.firstLineHeadIndent = rhs.firstLineHeadIndent 53 | } 54 | 55 | if rhs.headIndent != defaultParagraph.headIndent { 56 | combinedAttributes.headIndent = rhs.headIndent 57 | } 58 | 59 | if rhs.tailIndent != defaultParagraph.tailIndent { 60 | combinedAttributes.tailIndent = rhs.tailIndent 61 | } 62 | 63 | if rhs.lineBreakMode != defaultParagraph.lineBreakMode { 64 | combinedAttributes.lineBreakMode = rhs.lineBreakMode 65 | } 66 | 67 | if rhs.minimumLineHeight != defaultParagraph.minimumLineHeight { 68 | combinedAttributes.minimumLineHeight = rhs.minimumLineHeight 69 | } 70 | 71 | if rhs.maximumLineHeight != defaultParagraph.maximumLineHeight { 72 | combinedAttributes.maximumLineHeight = rhs.maximumLineHeight 73 | } 74 | 75 | if rhs.baseWritingDirection != defaultParagraph.baseWritingDirection { 76 | combinedAttributes.baseWritingDirection = rhs.baseWritingDirection 77 | } 78 | 79 | if rhs.lineHeightMultiple != defaultParagraph.lineHeightMultiple { 80 | combinedAttributes.lineHeightMultiple = rhs.lineHeightMultiple 81 | } 82 | 83 | if rhs.paragraphSpacingBefore != defaultParagraph.paragraphSpacingBefore { 84 | combinedAttributes.paragraphSpacingBefore = rhs.paragraphSpacingBefore 85 | } 86 | 87 | if rhs.hyphenationFactor != defaultParagraph.hyphenationFactor { 88 | combinedAttributes.hyphenationFactor = rhs.hyphenationFactor 89 | } 90 | 91 | if rhs.tabStops != defaultParagraph.tabStops { 92 | combinedAttributes.tabStops = rhs.tabStops 93 | } 94 | 95 | if rhs.defaultTabInterval != defaultParagraph.defaultTabInterval { 96 | combinedAttributes.defaultTabInterval = rhs.defaultTabInterval 97 | } 98 | 99 | if #available(iOS 9.0, *) { 100 | if rhs.allowsDefaultTighteningForTruncation != defaultParagraph.allowsDefaultTighteningForTruncation { 101 | combinedAttributes.allowsDefaultTighteningForTruncation = rhs.allowsDefaultTighteningForTruncation 102 | } 103 | } 104 | 105 | return combinedAttributes 106 | } 107 | 108 | // MARK: Attributes addition 109 | 110 | public func + (lhs: Attributes, rhs: Attributes) -> Attributes { 111 | var combined = lhs.dictionary 112 | for (key, value) in rhs.dictionary { 113 | combined[key] = value 114 | } 115 | 116 | let combinedParagraphStyle: NSParagraphStyle? 117 | let lhsParagraphStyle = lhs.dictionary[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle 118 | let rhsParagraphStyle = rhs.dictionary[NSAttributedString.Key.paragraphStyle] as? NSParagraphStyle 119 | 120 | if let lhsParagraphStyle = lhsParagraphStyle, let rhsParagraphStyle = rhsParagraphStyle { 121 | combinedParagraphStyle = lhsParagraphStyle + rhsParagraphStyle 122 | } else { 123 | combinedParagraphStyle = lhsParagraphStyle ?? rhsParagraphStyle 124 | } 125 | 126 | if let paragraphStyle = combinedParagraphStyle { 127 | combined[NSAttributedString.Key.paragraphStyle] = paragraphStyle 128 | } 129 | 130 | return Attributes(dictionary: combined) 131 | } 132 | -------------------------------------------------------------------------------- /Attributed/String+Attributed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Attributed.swift 3 | // 4 | // Copyright (c) 2016-2023 Nicholas Maccharoli 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import Foundation 25 | import UIKit 26 | 27 | // MARK: Primary Extension 28 | 29 | extension Attributed where Base == String { 30 | 31 | public func attributed(with attributes: Attributes) -> NSAttributedString { 32 | let attributes = attributes.dictionary 33 | return NSAttributedString(string: base, attributes: attributes) 34 | } 35 | 36 | public func attributed(_ attributeBlock: (Attributes) -> (Attributes)) -> NSAttributedString { 37 | let attributes = attributeBlock(Attributes()) 38 | return attributed(with: attributes) 39 | } 40 | } 41 | 42 | public extension Attributed where Base == NSMutableAttributedString { 43 | 44 | func add(_ attributes: Attributes, to range: NSRange) { 45 | base.addAttributes(attributes.dictionary, range: range) 46 | } 47 | 48 | } 49 | 50 | public extension Attributed where Base == NSAttributedString { 51 | 52 | func modified(with attributes: Attributes, for range: NSRange) -> NSAttributedString { 53 | let string = base as NSAttributedString 54 | 55 | let result = NSMutableAttributedString(attributedString: string) 56 | result.at.add(attributes, to: range) 57 | return NSAttributedString(attributedString: result) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /AttributedLib.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'AttributedLib' 3 | s.version = '3.0.0' 4 | s.summary = 'Modern Swift µframework for attributed strings.' 5 | 6 | s.description = <<-DESC 7 | A Modern interface for attributed strings. 8 | 9 | Make working with attributed strings simple and safe. 10 | This is a light-weight third party alternative to the 11 | existing inconvinient and error prone programming interface 12 | to `NSAttributedString`. 13 | 14 | DESC 15 | 16 | s.homepage = 'https://github.com/Nirma/Attributed' 17 | s.license = { :type => 'MIT', :file => 'LICENSE' } 18 | s.author = { 'Nicholas Maccharoli' => 'nmaccharoli@gmail.com' } 19 | s.source = { :git => 'https://github.com/Nirma/Attributed.git', :tag => s.version.to_s } 20 | s.swift_version = '5.0' 21 | s.ios.deployment_target = '9.0' 22 | s.watchos.deployment_target = "3.0" 23 | s.source_files = 'Attributed/*.swift' 24 | 25 | end 26 | 27 | -------------------------------------------------------------------------------- /AttributedTests/AttributedPublicTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributedPublicTests.swift 3 | // 4 | // Copyright (c) 2016-2023 Nicholas Maccharoli 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import XCTest 25 | 26 | // DO NOT USE `@testable import` 27 | // The tests in this file are checking the behaviour from the viewpoint of a normal client of this library 28 | import Attributed 29 | 30 | class AttributedPublicTests: XCTestCase { 31 | func testAttributesDictionary() { 32 | let testAttributes = Attributes() 33 | .font(.systemFont(ofSize: 12.0)) 34 | .background(color: .green) 35 | 36 | let testResult = testAttributes.dictionary.keys.count == 2 37 | 38 | XCTAssert(testResult, "Dictionary is not accessible") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /AttributedTests/AttributesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributesTests.swift 3 | // 4 | // Copyright (c) 2016-2023 Nicholas Maccharoli 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import XCTest 25 | @testable import Attributed 26 | 27 | class AttributesTests: XCTestCase { 28 | func testFont() { 29 | let expected: [String: Any] = [NSAttributedString.Key.font.rawValue: UIFont.systemFont(ofSize: 12.0)] 30 | let attributed = Attributes().font(.systemFont(ofSize: 12.0)) 31 | XCTAssert(attributed.dictionary.keys.first!.rawValue == expected.keys.first!, "Font is broken") 32 | } 33 | 34 | func testBaselineOffset() { 35 | let expected: [String: Any] = [NSAttributedString.Key.baselineOffset.rawValue: NSNumber(value: 1)] 36 | let attributed = Attributes().baselineOffset(1) 37 | XCTAssert(attributed.dictionary.keys.first!.rawValue == expected.keys.first!, "NSBaselineOffsetAttributeName functionality is broken") 38 | } 39 | 40 | 41 | func testKerning() { 42 | let testText = "Smoke Test" 43 | let smokeTestString = testText.at.attributed { $0.kerning(1.0) } 44 | let baseString = NSAttributedString(string: testText, attributes: [.kern: 1.0]) 45 | 46 | XCTAssert(smokeTestString == baseString, "Kern functionality is broken") 47 | } 48 | 49 | func testStrokeColor() { 50 | let testText = "Smoke Test" 51 | let smokeTestString = testText.at.attributed { $0.strokeColor(.green) } 52 | let baseString = NSAttributedString(string: testText, attributes: [ .strokeColor : UIColor.green]) 53 | 54 | XCTAssert(smokeTestString == baseString, "StrokeColor functionality is broken") 55 | } 56 | 57 | func testStrokeWidth() { 58 | let testText = "Smoke Test" 59 | let smokeTestString = testText.at.attributed { $0.strokeWidth(2.0) } 60 | let baseString = NSAttributedString(string: testText, attributes: [ .strokeWidth : 2.0]) 61 | 62 | XCTAssert(smokeTestString == baseString, "Stroke width functionality is broken") 63 | } 64 | 65 | func testUnderlineStyle() { 66 | let testText = "Smoke Test" 67 | let smokeTestString = testText.at.attributed { $0.underlineStyle(NSUnderlineStyle.byWord) } 68 | let baseString = NSAttributedString(string: testText, attributes: [ .underlineStyle : NSUnderlineStyle.byWord.rawValue]) 69 | 70 | XCTAssert(smokeTestString == baseString, "Underline style functionality is broken") 71 | } 72 | 73 | func testForegroundColor() { 74 | let testText = "Smoke Test" 75 | let smokeTestString = testText.at.attributed { $0.foreground(color: .green) } 76 | let baseString = NSAttributedString(string: testText, attributes: [ .foregroundColor : UIColor.green]) 77 | 78 | XCTAssert(smokeTestString == baseString, "Foreground color functionality is broken") 79 | } 80 | 81 | func testBackgroundColor() { 82 | let testText = "Smoke Test" 83 | let smokeTestString = testText.at.attributed { $0.background(color: .green) } 84 | let baseString = NSAttributedString(string: testText, attributes: [ .backgroundColor : UIColor.green]) 85 | 86 | XCTAssert(smokeTestString == baseString, "Background color functionality is broken") 87 | } 88 | 89 | func testObliqueness() { 90 | let testText = "Smoke Test" 91 | let smokeTestString = testText.at.attributed { $0.obliqueness(1.0) } 92 | let baseString = NSAttributedString(string: testText, attributes: [ .obliqueness : 1.0]) 93 | 94 | XCTAssert(smokeTestString == baseString, "Obliqueness functionality is broken") 95 | } 96 | 97 | func testShadow() { 98 | let testText = "Smoke Test" 99 | let shadow = NSShadow() 100 | shadow.shadowBlurRadius = 0.5 101 | let smokeTestString = testText.at.attributed { $0.shadow(shadow) } 102 | let baseString = NSAttributedString(string: testText, attributes: [ .shadow : shadow]) 103 | 104 | XCTAssert(smokeTestString == baseString, "Shadow functionality is broken") 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /AttributedTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /AttributedTests/StringExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtensionTests.swift 3 | // 4 | // Copyright (c) 2016-2023 Nicholas Maccharoli 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | 24 | import XCTest 25 | @testable import Attributed 26 | 27 | 28 | final class StringExtensionTests: XCTestCase { 29 | 30 | func testAttributedWithAttributes() { 31 | let string = "😎 Lorem 😀 ipsum 👀 dolor 👻 sit 🎲 amet, 🎲 consectetur 🔥 adipiscing 🚀 elit." 32 | let attributes = Attributes { 33 | $0.font(.systemFont(ofSize: 12.0)) 34 | .foreground(color: .darkGray) 35 | } 36 | let expected = NSAttributedString(string: string, attributes: attributes.dictionary) 37 | let attributed = string.at.attributed(with: attributes) 38 | XCTAssertEqual(expected, attributed) 39 | 40 | let attributed2 = string.at.attributed(with: attributes.foreground(color: .red)) 41 | XCTAssertNotEqual(expected, attributed2) 42 | } 43 | 44 | func testAttributedWithAttributeBlock() { 45 | let string = "😎 Lorem 😀 ipsum 👀 dolor 👻 sit 🎲 amet, 🎲 consectetur 🔥 adipiscing 🚀 elit." 46 | let attributesBlock: ((Attributes) -> Attributes) = { 47 | $0.font(.systemFont(ofSize: 12.0)) 48 | .foreground(color: .darkGray) 49 | } 50 | let expected = NSAttributedString(string: string, attributes: attributesBlock(Attributes()).dictionary) 51 | let attributed = string.at.attributed(attributesBlock) 52 | XCTAssertEqual(expected, attributed) 53 | 54 | let attributed2 = string.at.attributed { $0.foreground(color: .red) } 55 | XCTAssertNotEqual(expected, attributed2) 56 | } 57 | 58 | } 59 | 60 | 61 | final class NSStringExtensionTests: XCTestCase { 62 | 63 | func testAttributedWithAttributes() { 64 | let string = "😎 Lorem 😀 ipsum 👀 dolor 👻 sit 🎲 amet, 🎲 consectetur 🔥 adipiscing 🚀 elit." as NSString 65 | let attributes = Attributes { 66 | $0.font(.systemFont(ofSize: 12.0)) 67 | .foreground(color: .darkGray) 68 | } 69 | let expected = NSAttributedString(string: (string as String), attributes: attributes.dictionary) 70 | let attributed = string.at.attributed(with: attributes) 71 | XCTAssertEqual(expected, attributed) 72 | 73 | let attributed2 = string.at.attributed(with: attributes.foreground(color: .red)) 74 | XCTAssertNotEqual(expected, attributed2) 75 | } 76 | 77 | func testAttributedWithAttributeBlock() { 78 | let string = "😎 Lorem 😀 ipsum 👀 dolor 👻 sit 🎲 amet, 🎲 consectetur 🔥 adipiscing 🚀 elit." as NSString 79 | let attributesBlock: ((Attributes) -> Attributes) = { 80 | $0.font(.systemFont(ofSize: 12.0)) 81 | .foreground(color: .darkGray) 82 | } 83 | 84 | let expected = NSAttributedString(string: (string as String), attributes: attributesBlock(Attributes()).dictionary) 85 | let attributed = string.at.attributed(attributesBlock) 86 | XCTAssertEqual(expected, attributed) 87 | 88 | let attributed2 = string.at.attributed { $0.foreground(color: .red) } 89 | XCTAssertNotEqual(expected, attributed2) 90 | } 91 | 92 | } 93 | 94 | 95 | final class NSAttributedStringExtensionTests: XCTestCase { 96 | 97 | func testModifiedWithAttributesForRange() { 98 | let string = "😎 Lorem 😀 ipsum 👀 dolor 👻 sit 🎲 amet, 🎲 consectetur 🔥 adipiscing 🚀 elit." 99 | let range = (string as NSString).range(of: "👻 sit 🎲 amet, 🎲 consectetur") 100 | let attributes = Attributes { 101 | $0.font(.systemFont(ofSize: 12.0)) 102 | .foreground(color: .darkGray) 103 | } 104 | let rangeAttributes = Attributes().foreground(color: .red) 105 | let expected = NSMutableAttributedString(string: (string as String), attributes: attributes.dictionary) 106 | expected.addAttributes(rangeAttributes.dictionary, range: range) 107 | let attributed = NSAttributedString(string: (string as String), attributes: attributes.dictionary) 108 | XCTAssertNotEqual(expected.copy() as! NSAttributedString, attributed) 109 | 110 | XCTAssertEqual(expected.copy() as! NSAttributedString, attributed.at.modified(with: rangeAttributes, for: range)) 111 | 112 | // Test againts + operator 113 | let appended = "😎 Lorem 😀 ipsum 👀 dolor ".at.attributed(with: attributes) 114 | + "👻 sit 🎲 amet, 🎲 consectetur".at.attributed(with: attributes + rangeAttributes) 115 | + " 🔥 adipiscing 🚀 elit.".at.attributed(with: attributes) 116 | XCTAssertEqual(appended, attributed.at.modified(with: rangeAttributes, for: range)) 117 | } 118 | 119 | } 120 | 121 | 122 | final class NSMutableAttributedStringExtensionTests: XCTestCase { 123 | 124 | func testAddAttributesToRange() { 125 | let string = "😎 Lorem 😀 ipsum 👀 dolor 👻 sit 🎲 amet, 🎲 consectetur 🔥 adipiscing 🚀 elit." 126 | let range = (string as NSString).range(of: "👻 sit 🎲 amet, 🎲 consectetur") 127 | let attributes = Attributes { 128 | $0.font(.systemFont(ofSize: 12.0)) 129 | .foreground(color: .darkGray) 130 | } 131 | let rangeAttributes = Attributes().foreground(color: .red) 132 | let expected = NSMutableAttributedString(string: (string as String), attributes: attributes.dictionary) 133 | let attributed = NSMutableAttributedString(string: (string as String), attributes: attributes.dictionary) 134 | 135 | expected.addAttributes(rangeAttributes.dictionary, range: range) 136 | XCTAssertNotEqual(expected, attributed) 137 | attributed.at.add(rangeAttributes, to: range) 138 | XCTAssertEqual(expected, attributed) 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | `Attributed` adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | --- 7 | 8 | 9 | ## [2.0.2](https://github.com/Nirma/Attributed/releases/tag/2.0.2) (10/30/2017) 10 | 11 | #### Summary 12 | 13 | This release fixes an issue with the strike-through attribute not working properly. 14 | This was addressed in Issue #47 15 | 16 | ## [2.0.1](https://github.com/Nirma/Attributed/releases/tag/2.0.1) (09/23/2017) 17 | 18 | #### Summary 19 | 20 | This release addresses a cocoapod issue that was in the previous release. 21 | 22 | ## [2.0.0](https://github.com/Nirma/Attributed/releases/tag/2.0.0) (09/20/2017) 23 | 24 | #### Summary 25 | 26 | This release only containted code fixes for Swift 4 / XCode 9 27 | 28 | ## [1.3.0](https://github.com/Nirma/Attributed/releases/tag/1.3.0) (07/20/2017) 29 | 30 | #### Summary 31 | 32 | This is a purely additive release. 33 | 34 | This release adds functionality for supporting the use of `NSBaseLineAttributeName` through this library's interface. 35 | Just call `.baseOffset()` with an `NSNumber` value specifying the offset you wish to use. 36 | 37 | 38 | ## [1.2.0](https://github.com/Nirma/Attributed/releases/tag/1.2.0) (06/16/2017) 39 | 40 | #### Summary 41 | 42 | This is a purely additive release. 43 | 44 | The new functionality is the addition of the function `uniformLineHeight`: 45 | ```swift 46 | func uniformLineHeight(_ uniformLineHeight: CGFloat) -> Attributes { ... } 47 | ``` 48 | 49 | This function is to be used when an attributed string has the same value for both `minimumLineHeight` and `maximumLineHeight` 50 | allowing this to be specified in one line rather than two. 51 | 52 | 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 - 2017 Nicholas Maccharoli 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: "Attributed", 8 | platforms: [.iOS(.v10)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "Attributed", 13 | targets: ["Attributed"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 22 | .target( 23 | name: "Attributed", 24 | dependencies: [], 25 | path: "Attributed", 26 | exclude: ["Info.plist"]), 27 | .testTarget( 28 | name: "AttributedTests", 29 | dependencies: ["Attributed"], 30 | path: "AttributedTests", 31 | exclude: ["Info.plist"]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Attributed 2 | [![Build Status](https://travis-ci.org/Nirma/Attributed.svg?branch=master)](https://travis-ci.org/Nirma/Attributed) 3 | ![CodeCov](https://img.shields.io/codecov/c/github/Nirma/Attributed.svg) 4 | ![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg) 5 | [![CocoaPods compatible](https://img.shields.io/cocoapods/v/AttributedLib.svg)](#cocoapods) 6 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | [![License](http://img.shields.io/:license-mit-blue.svg)](http://doge.mit-license.org) 8 | 9 | µframework for Attributed strings. 10 | 11 | ## What is Attributed? 12 | Attributed aims to be a drop in replacement to the current version of the `NSAttributedString` API. 13 | 14 | The `NSAttributedString` interface has a few shortcomings. If you donʼt know the key and type of value needed to set a certain attribute, you have to spend time checking documentation. Another concern is safety: passing a dictionary of type `[String: Any]` to the constructor of `NSAttributedString` is a potential crash at runtime waiting to happen. 15 | 16 | Attributed provides developers a nicer alternative by extending the current `NSAttributedString` interface with a fluent, strongly typed, and easy to use API. 17 | 18 | # Features 19 | - [x] Create `NSAttributedString` instances with a strongly typed, simple, and fluid interface 20 | - [x] Combine `NSAttributedString`s with `+` 21 | - [x] Partially apply Attributes to parts of an `NSAttributedString` by providing a `Range` 22 | 23 | ### Donʼt see a feature you need? 24 | Feel free to open an [issue](https://github.com/Nirma/Attributed/issues) requesting the feature you want or send over a pull request! 25 | 26 | # Usage 27 | 28 | ### Creating a new `NSAttributedString` by closure composition 29 | 30 | ```swift 31 | 32 | "This is not a string".at.attributed { 33 | return $0.foreground(color: .red) 34 | .font(UIFont(name: "Chalkduster", size: 24.0)!) 35 | .underlineStyle(.styleSingle) 36 | } 37 | ``` 38 | 39 | ### Creating a new `NSAttributedString` by passing an attributes object 40 | 41 | First create an `Attributes` object: 42 | 43 | ```swift 44 | let attributes = Attributes { 45 | return $0.foreground(color: .red) 46 | .font(UIFont(name: "Chalkduster", size: 24.0)!) 47 | .underlineStyle(.styleSingle) 48 | } 49 | ``` 50 | 51 | then simply apply the `Attributes` to a `String`: 52 | 53 | ```swift 54 | "Hello".at.attributed(with: attributes) 55 | ``` 56 | 57 | ### Combining `NSAttributedString` with `+` 58 | This library defines an concatenation operator `+` for concatentating instances of `NSAttributedString`. 59 | `+` works with `NSAttributedString` no different than it does for `String`. 60 | This can be useful for combining `NSAttributedStrings` with different attributes to produce the 61 | desired effect without having to specify ranges to apply different attributes to. 62 | 63 | ```swift 64 | let bodyAttributes = Attributes { 65 | return $0.foreground(color: .purple) 66 | .font(UIFont(name: "Noteworthy-Light", size: 20.0)!) 67 | } 68 | 69 | let authorAttributes = bodyAttributes.foreground(color: .black) 70 | 71 | "I think theres something strangely musical about noise.".at.attributed(with: bodyAttributes) 72 | + "\n - Trent Reznor".at.attributed(with: authorAttributes) 73 | ``` 74 | 75 | ## Installation 76 | 77 | #### Carthage 78 | 79 | If you use Carthage to manage your dependencies, simply add 80 | Attributed to your `Cartfile`: 81 | 82 | ``` 83 | github "Nirma/Attributed" 84 | ``` 85 | 86 | If you use Carthage to build your dependencies, make sure you have added `Attributed.framework` to the "_Linked Frameworks and Libraries_" section of your target, and have included `Attributed.framework` in your Carthage framework copying build phase. 87 | 88 | #### CocoaPods 89 | 90 | If you use CocoaPods to manage your dependencies, simply add 91 | Attributed to your `Podfile`: 92 | 93 | ```ruby 94 | pod 'AttributedLib' 95 | ``` 96 | 97 | ## Requirements 98 | 99 | * Xcode 9.0 100 | * Swift 4.0+ 101 | 102 | ## Contribution 103 | Contributions are more than welcome! 104 | 105 | ## License 106 | 107 | Attributed is free software, and may be redistributed under the terms specified in the [LICENSE] file. 108 | 109 | [LICENSE]: /LICENSE 110 | --------------------------------------------------------------------------------