├── .gitattributes ├── .gitignore ├── DYLabelDemo ├── DYLabelDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── DYLabelDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── DYLabelDemo-Bridging-Header.h │ ├── HTMLFastParseSupport │ ├── C_HTML_Parser.c │ ├── C_HTML_Parser.h │ ├── FormatToAttributedString.h │ ├── FormatToAttributedString.m │ ├── Stack.c │ ├── Stack.h │ ├── entities.c │ ├── entities.h │ ├── t_format.h │ └── t_tag.h │ ├── Info.plist │ └── ViewController.swift ├── Images ├── Example.png └── paragraph_frames.png ├── LICENSE ├── Package.swift ├── README.md └── Sources └── DYLabel.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | DYLabelDemo/DYLabelDemo/HTMLFastParseSupport/* linguist-vendored=true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/swift,macos,xcode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,macos,xcode 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Swift ### 35 | # Xcode 36 | # 37 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 38 | 39 | ## User settings 40 | xcuserdata/ 41 | 42 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 43 | *.xcscmblueprint 44 | *.xccheckout 45 | 46 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 47 | build/ 48 | DerivedData/ 49 | *.moved-aside 50 | *.pbxuser 51 | !default.pbxuser 52 | *.mode1v3 53 | !default.mode1v3 54 | *.mode2v3 55 | !default.mode2v3 56 | *.perspectivev3 57 | !default.perspectivev3 58 | 59 | ## Obj-C/Swift specific 60 | *.hmap 61 | 62 | ## App packaging 63 | *.ipa 64 | *.dSYM.zip 65 | *.dSYM 66 | 67 | ## Playgrounds 68 | timeline.xctimeline 69 | playground.xcworkspace 70 | 71 | # Swift Package Manager 72 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 73 | # Packages/ 74 | # Package.pins 75 | # Package.resolved 76 | # *.xcodeproj 77 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 78 | # hence it is not needed unless you have added a package configuration file to your project 79 | # .swiftpm 80 | 81 | .build/ 82 | 83 | # CocoaPods 84 | # We recommend against adding the Pods directory to your .gitignore. However 85 | # you should judge for yourself, the pros and cons are mentioned at: 86 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 87 | # Pods/ 88 | # Add this line if you want to avoid checking in source code from the Xcode workspace 89 | # *.xcworkspace 90 | 91 | # Carthage 92 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 93 | # Carthage/Checkouts 94 | 95 | Carthage/Build/ 96 | 97 | # Accio dependency management 98 | Dependencies/ 99 | .accio/ 100 | 101 | # fastlane 102 | # It is recommended to not store the screenshots in the git repo. 103 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 104 | # For more information about the recommended setup visit: 105 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 106 | 107 | fastlane/report.xml 108 | fastlane/Preview.html 109 | fastlane/screenshots/**/*.png 110 | fastlane/test_output 111 | 112 | # Code Injection 113 | # After new code Injection tools there's a generated folder /iOSInjectionProject 114 | # https://github.com/johnno1962/injectionforxcode 115 | 116 | iOSInjectionProject/ 117 | 118 | ### Xcode ### 119 | # Xcode 120 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 121 | 122 | 123 | 124 | 125 | ## Gcc Patch 126 | /*.gcno 127 | 128 | ### Xcode Patch ### 129 | *.xcodeproj/* 130 | !*.xcodeproj/project.pbxproj 131 | !*.xcodeproj/xcshareddata/ 132 | !*.xcworkspace/contents.xcworkspacedata 133 | **/xcshareddata/WorkspaceSettings.xcsettings 134 | 135 | # End of https://www.toptal.com/developers/gitignore/api/swift,macos,xcode -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2216827425DA784C008BBE7A /* DYLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2216827325DA784C008BBE7A /* DYLabel.swift */; }; 11 | 22F34CFF2173F85600126C56 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F34CFE2173F85600126C56 /* AppDelegate.swift */; }; 12 | 22F34D012173F85600126C56 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F34D002173F85600126C56 /* ViewController.swift */; }; 13 | 22F34D042173F85600126C56 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 22F34D022173F85600126C56 /* Main.storyboard */; }; 14 | 22F34D062173F85800126C56 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 22F34D052173F85800126C56 /* Assets.xcassets */; }; 15 | 22F34D092173F85800126C56 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 22F34D072173F85800126C56 /* LaunchScreen.storyboard */; }; 16 | 22F34D1D2173F8D800126C56 /* Stack.c in Sources */ = {isa = PBXBuildFile; fileRef = 22F34D132173F8D800126C56 /* Stack.c */; }; 17 | 22F34D1E2173F8D800126C56 /* entities.c in Sources */ = {isa = PBXBuildFile; fileRef = 22F34D142173F8D800126C56 /* entities.c */; }; 18 | 22F34D1F2173F8D800126C56 /* C_HTML_Parser.c in Sources */ = {isa = PBXBuildFile; fileRef = 22F34D152173F8D800126C56 /* C_HTML_Parser.c */; }; 19 | 22F34D202173F8D800126C56 /* FormatToAttributedString.m in Sources */ = {isa = PBXBuildFile; fileRef = 22F34D192173F8D800126C56 /* FormatToAttributedString.m */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | 2216827325DA784C008BBE7A /* DYLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DYLabel.swift; path = ../../Sources/DYLabel.swift; sourceTree = ""; }; 24 | 22F34CFB2173F85600126C56 /* DYLabelDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DYLabelDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 22F34CFE2173F85600126C56 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 26 | 22F34D002173F85600126C56 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 27 | 22F34D032173F85600126C56 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 28 | 22F34D052173F85800126C56 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 29 | 22F34D082173F85800126C56 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 30 | 22F34D0A2173F85800126C56 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 31 | 22F34D122173F8D700126C56 /* DYLabelDemo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "DYLabelDemo-Bridging-Header.h"; sourceTree = ""; }; 32 | 22F34D132173F8D800126C56 /* Stack.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = Stack.c; sourceTree = ""; }; 33 | 22F34D142173F8D800126C56 /* entities.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = entities.c; sourceTree = ""; }; 34 | 22F34D152173F8D800126C56 /* C_HTML_Parser.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = C_HTML_Parser.c; sourceTree = ""; }; 35 | 22F34D162173F8D800126C56 /* Stack.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Stack.h; sourceTree = ""; }; 36 | 22F34D172173F8D800126C56 /* t_format.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = t_format.h; sourceTree = ""; }; 37 | 22F34D182173F8D800126C56 /* FormatToAttributedString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FormatToAttributedString.h; sourceTree = ""; }; 38 | 22F34D192173F8D800126C56 /* FormatToAttributedString.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FormatToAttributedString.m; sourceTree = ""; }; 39 | 22F34D1A2173F8D800126C56 /* C_HTML_Parser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = C_HTML_Parser.h; sourceTree = ""; }; 40 | 22F34D1B2173F8D800126C56 /* t_tag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = t_tag.h; sourceTree = ""; }; 41 | 22F34D1C2173F8D800126C56 /* entities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = entities.h; sourceTree = ""; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFrameworksBuildPhase section */ 45 | 22F34CF82173F85600126C56 /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | /* End PBXFrameworksBuildPhase section */ 53 | 54 | /* Begin PBXGroup section */ 55 | 22F34CF22173F85600126C56 = { 56 | isa = PBXGroup; 57 | children = ( 58 | 22F34CFD2173F85600126C56 /* DYLabelDemo */, 59 | 22F34CFC2173F85600126C56 /* Products */, 60 | ); 61 | sourceTree = ""; 62 | }; 63 | 22F34CFC2173F85600126C56 /* Products */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 22F34CFB2173F85600126C56 /* DYLabelDemo.app */, 67 | ); 68 | name = Products; 69 | sourceTree = ""; 70 | }; 71 | 22F34CFD2173F85600126C56 /* DYLabelDemo */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 22F34CFE2173F85600126C56 /* AppDelegate.swift */, 75 | 22F34D002173F85600126C56 /* ViewController.swift */, 76 | 2216827325DA784C008BBE7A /* DYLabel.swift */, 77 | 22F34D022173F85600126C56 /* Main.storyboard */, 78 | 22F34D052173F85800126C56 /* Assets.xcassets */, 79 | 22F34D072173F85800126C56 /* LaunchScreen.storyboard */, 80 | 22F34D0A2173F85800126C56 /* Info.plist */, 81 | 22F34D122173F8D700126C56 /* DYLabelDemo-Bridging-Header.h */, 82 | 22F34D212173F8DF00126C56 /* HTMLFastParseSupport */, 83 | ); 84 | path = DYLabelDemo; 85 | sourceTree = ""; 86 | }; 87 | 22F34D212173F8DF00126C56 /* HTMLFastParseSupport */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 22F34D152173F8D800126C56 /* C_HTML_Parser.c */, 91 | 22F34D1A2173F8D800126C56 /* C_HTML_Parser.h */, 92 | 22F34D142173F8D800126C56 /* entities.c */, 93 | 22F34D1C2173F8D800126C56 /* entities.h */, 94 | 22F34D182173F8D800126C56 /* FormatToAttributedString.h */, 95 | 22F34D192173F8D800126C56 /* FormatToAttributedString.m */, 96 | 22F34D132173F8D800126C56 /* Stack.c */, 97 | 22F34D162173F8D800126C56 /* Stack.h */, 98 | 22F34D172173F8D800126C56 /* t_format.h */, 99 | 22F34D1B2173F8D800126C56 /* t_tag.h */, 100 | ); 101 | path = HTMLFastParseSupport; 102 | sourceTree = ""; 103 | }; 104 | /* End PBXGroup section */ 105 | 106 | /* Begin PBXNativeTarget section */ 107 | 22F34CFA2173F85600126C56 /* DYLabelDemo */ = { 108 | isa = PBXNativeTarget; 109 | buildConfigurationList = 22F34D0D2173F85800126C56 /* Build configuration list for PBXNativeTarget "DYLabelDemo" */; 110 | buildPhases = ( 111 | 22F34CF72173F85600126C56 /* Sources */, 112 | 22F34CF82173F85600126C56 /* Frameworks */, 113 | 22F34CF92173F85600126C56 /* Resources */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = DYLabelDemo; 120 | productName = DYLabelDemo; 121 | productReference = 22F34CFB2173F85600126C56 /* DYLabelDemo.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | 22F34CF32173F85600126C56 /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastSwiftUpdateCheck = 1000; 131 | LastUpgradeCheck = 1000; 132 | ORGANIZATIONNAME = "Salman Husain"; 133 | TargetAttributes = { 134 | 22F34CFA2173F85600126C56 = { 135 | CreatedOnToolsVersion = 10.0; 136 | LastSwiftMigration = 1000; 137 | }; 138 | }; 139 | }; 140 | buildConfigurationList = 22F34CF62173F85600126C56 /* Build configuration list for PBXProject "DYLabelDemo" */; 141 | compatibilityVersion = "Xcode 9.3"; 142 | developmentRegion = en; 143 | hasScannedForEncodings = 0; 144 | knownRegions = ( 145 | en, 146 | Base, 147 | ); 148 | mainGroup = 22F34CF22173F85600126C56; 149 | productRefGroup = 22F34CFC2173F85600126C56 /* Products */; 150 | projectDirPath = ""; 151 | projectRoot = ""; 152 | targets = ( 153 | 22F34CFA2173F85600126C56 /* DYLabelDemo */, 154 | ); 155 | }; 156 | /* End PBXProject section */ 157 | 158 | /* Begin PBXResourcesBuildPhase section */ 159 | 22F34CF92173F85600126C56 /* Resources */ = { 160 | isa = PBXResourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | 22F34D092173F85800126C56 /* LaunchScreen.storyboard in Resources */, 164 | 22F34D062173F85800126C56 /* Assets.xcassets in Resources */, 165 | 22F34D042173F85600126C56 /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXSourcesBuildPhase section */ 172 | 22F34CF72173F85600126C56 /* Sources */ = { 173 | isa = PBXSourcesBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | 22F34D1E2173F8D800126C56 /* entities.c in Sources */, 177 | 22F34D202173F8D800126C56 /* FormatToAttributedString.m in Sources */, 178 | 22F34D012173F85600126C56 /* ViewController.swift in Sources */, 179 | 2216827425DA784C008BBE7A /* DYLabel.swift in Sources */, 180 | 22F34D1D2173F8D800126C56 /* Stack.c in Sources */, 181 | 22F34CFF2173F85600126C56 /* AppDelegate.swift in Sources */, 182 | 22F34D1F2173F8D800126C56 /* C_HTML_Parser.c in Sources */, 183 | ); 184 | runOnlyForDeploymentPostprocessing = 0; 185 | }; 186 | /* End PBXSourcesBuildPhase section */ 187 | 188 | /* Begin PBXVariantGroup section */ 189 | 22F34D022173F85600126C56 /* Main.storyboard */ = { 190 | isa = PBXVariantGroup; 191 | children = ( 192 | 22F34D032173F85600126C56 /* Base */, 193 | ); 194 | name = Main.storyboard; 195 | sourceTree = ""; 196 | }; 197 | 22F34D072173F85800126C56 /* LaunchScreen.storyboard */ = { 198 | isa = PBXVariantGroup; 199 | children = ( 200 | 22F34D082173F85800126C56 /* Base */, 201 | ); 202 | name = LaunchScreen.storyboard; 203 | sourceTree = ""; 204 | }; 205 | /* End PBXVariantGroup section */ 206 | 207 | /* Begin XCBuildConfiguration section */ 208 | 22F34D0B2173F85800126C56 /* Debug */ = { 209 | isa = XCBuildConfiguration; 210 | buildSettings = { 211 | ALWAYS_SEARCH_USER_PATHS = NO; 212 | CLANG_ANALYZER_NONNULL = YES; 213 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 214 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 215 | CLANG_CXX_LIBRARY = "libc++"; 216 | CLANG_ENABLE_MODULES = YES; 217 | CLANG_ENABLE_OBJC_ARC = YES; 218 | CLANG_ENABLE_OBJC_WEAK = YES; 219 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 220 | CLANG_WARN_BOOL_CONVERSION = YES; 221 | CLANG_WARN_COMMA = YES; 222 | CLANG_WARN_CONSTANT_CONVERSION = YES; 223 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 224 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 225 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 226 | CLANG_WARN_EMPTY_BODY = YES; 227 | CLANG_WARN_ENUM_CONVERSION = YES; 228 | CLANG_WARN_INFINITE_RECURSION = YES; 229 | CLANG_WARN_INT_CONVERSION = YES; 230 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 231 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 232 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 233 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 234 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 235 | CLANG_WARN_STRICT_PROTOTYPES = YES; 236 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 237 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 238 | CLANG_WARN_UNREACHABLE_CODE = YES; 239 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 240 | CODE_SIGN_IDENTITY = "iPhone Developer"; 241 | COPY_PHASE_STRIP = NO; 242 | DEBUG_INFORMATION_FORMAT = dwarf; 243 | ENABLE_STRICT_OBJC_MSGSEND = YES; 244 | ENABLE_TESTABILITY = YES; 245 | GCC_C_LANGUAGE_STANDARD = gnu11; 246 | GCC_DYNAMIC_NO_PIC = NO; 247 | GCC_NO_COMMON_BLOCKS = YES; 248 | GCC_OPTIMIZATION_LEVEL = 0; 249 | GCC_PREPROCESSOR_DEFINITIONS = ( 250 | "DEBUG=1", 251 | "$(inherited)", 252 | ); 253 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 254 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 255 | GCC_WARN_UNDECLARED_SELECTOR = YES; 256 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 257 | GCC_WARN_UNUSED_FUNCTION = YES; 258 | GCC_WARN_UNUSED_VARIABLE = YES; 259 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 260 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 261 | MTL_FAST_MATH = YES; 262 | ONLY_ACTIVE_ARCH = YES; 263 | SDKROOT = iphoneos; 264 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 265 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 266 | }; 267 | name = Debug; 268 | }; 269 | 22F34D0C2173F85800126C56 /* Release */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | ALWAYS_SEARCH_USER_PATHS = NO; 273 | CLANG_ANALYZER_NONNULL = YES; 274 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 275 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 276 | CLANG_CXX_LIBRARY = "libc++"; 277 | CLANG_ENABLE_MODULES = YES; 278 | CLANG_ENABLE_OBJC_ARC = YES; 279 | CLANG_ENABLE_OBJC_WEAK = YES; 280 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 281 | CLANG_WARN_BOOL_CONVERSION = YES; 282 | CLANG_WARN_COMMA = YES; 283 | CLANG_WARN_CONSTANT_CONVERSION = YES; 284 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 286 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 287 | CLANG_WARN_EMPTY_BODY = YES; 288 | CLANG_WARN_ENUM_CONVERSION = YES; 289 | CLANG_WARN_INFINITE_RECURSION = YES; 290 | CLANG_WARN_INT_CONVERSION = YES; 291 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 292 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 293 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 294 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 295 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 296 | CLANG_WARN_STRICT_PROTOTYPES = YES; 297 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 298 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 299 | CLANG_WARN_UNREACHABLE_CODE = YES; 300 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 301 | CODE_SIGN_IDENTITY = "iPhone Developer"; 302 | COPY_PHASE_STRIP = NO; 303 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 304 | ENABLE_NS_ASSERTIONS = NO; 305 | ENABLE_STRICT_OBJC_MSGSEND = YES; 306 | GCC_C_LANGUAGE_STANDARD = gnu11; 307 | GCC_NO_COMMON_BLOCKS = YES; 308 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 309 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 310 | GCC_WARN_UNDECLARED_SELECTOR = YES; 311 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 312 | GCC_WARN_UNUSED_FUNCTION = YES; 313 | GCC_WARN_UNUSED_VARIABLE = YES; 314 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 315 | MTL_ENABLE_DEBUG_INFO = NO; 316 | MTL_FAST_MATH = YES; 317 | SDKROOT = iphoneos; 318 | SWIFT_COMPILATION_MODE = wholemodule; 319 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 320 | VALIDATE_PRODUCT = YES; 321 | }; 322 | name = Release; 323 | }; 324 | 22F34D0E2173F85800126C56 /* Debug */ = { 325 | isa = XCBuildConfiguration; 326 | buildSettings = { 327 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 328 | CLANG_ENABLE_MODULES = YES; 329 | CODE_SIGN_STYLE = Automatic; 330 | DEVELOPMENT_TEAM = XBKLZPN8YT; 331 | INFOPLIST_FILE = DYLabelDemo/Info.plist; 332 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 333 | LD_RUNPATH_SEARCH_PATHS = ( 334 | "$(inherited)", 335 | "@executable_path/Frameworks", 336 | ); 337 | PRODUCT_BUNDLE_IDENTIFIER = com.CarbonDev.DYLabelDemo; 338 | PRODUCT_NAME = "$(TARGET_NAME)"; 339 | SWIFT_OBJC_BRIDGING_HEADER = "DYLabelDemo/DYLabelDemo-Bridging-Header.h"; 340 | SWIFT_VERSION = 4.2; 341 | TARGETED_DEVICE_FAMILY = "1,2"; 342 | }; 343 | name = Debug; 344 | }; 345 | 22F34D0F2173F85800126C56 /* Release */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 349 | CLANG_ENABLE_MODULES = YES; 350 | CODE_SIGN_STYLE = Automatic; 351 | DEVELOPMENT_TEAM = XBKLZPN8YT; 352 | INFOPLIST_FILE = DYLabelDemo/Info.plist; 353 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 354 | LD_RUNPATH_SEARCH_PATHS = ( 355 | "$(inherited)", 356 | "@executable_path/Frameworks", 357 | ); 358 | PRODUCT_BUNDLE_IDENTIFIER = com.CarbonDev.DYLabelDemo; 359 | PRODUCT_NAME = "$(TARGET_NAME)"; 360 | SWIFT_OBJC_BRIDGING_HEADER = "DYLabelDemo/DYLabelDemo-Bridging-Header.h"; 361 | SWIFT_VERSION = 4.2; 362 | TARGETED_DEVICE_FAMILY = "1,2"; 363 | }; 364 | name = Release; 365 | }; 366 | /* End XCBuildConfiguration section */ 367 | 368 | /* Begin XCConfigurationList section */ 369 | 22F34CF62173F85600126C56 /* Build configuration list for PBXProject "DYLabelDemo" */ = { 370 | isa = XCConfigurationList; 371 | buildConfigurations = ( 372 | 22F34D0B2173F85800126C56 /* Debug */, 373 | 22F34D0C2173F85800126C56 /* Release */, 374 | ); 375 | defaultConfigurationIsVisible = 0; 376 | defaultConfigurationName = Release; 377 | }; 378 | 22F34D0D2173F85800126C56 /* Build configuration list for PBXNativeTarget "DYLabelDemo" */ = { 379 | isa = XCConfigurationList; 380 | buildConfigurations = ( 381 | 22F34D0E2173F85800126C56 /* Debug */, 382 | 22F34D0F2173F85800126C56 /* Release */, 383 | ); 384 | defaultConfigurationIsVisible = 0; 385 | defaultConfigurationName = Release; 386 | }; 387 | /* End XCConfigurationList section */ 388 | }; 389 | rootObject = 22F34CF32173F85600126C56 /* Project object */; 390 | } 391 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DYLabelDemo 4 | // 5 | // Created by Allison Husain on 10/14/18. 6 | // Copyright © 2018 Allison Husain. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | /* 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | f 164 | 165 | */ 166 | 167 | } 168 | 169 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/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 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/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 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/DYLabelDemo-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | #import "FormatToAttributedString.h" 5 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/HTMLFastParseSupport/C_HTML_Parser.c: -------------------------------------------------------------------------------- 1 | // 2 | // C_HTML_Parser.c 3 | // HTMLFastParse 4 | // 5 | // Created by Allison Husain on 4/27/18. 6 | // Copyright © 2018 CarbonDev. All rights reserved. 7 | // 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "C_HTML_Parser.h" 15 | #include "t_tag.h" 16 | #include "t_format.h" 17 | #include "Stack.h" 18 | #include "entities.h" 19 | 20 | //Disable printf 21 | #define printf(fmt, ...) (0) 22 | 23 | //Enable reddit tune. Comment this out to remove them 24 | #define reddit_mode 1; 25 | 26 | 27 | /** 28 | Get the number of bytes that a given charachter will use when displayed (multi-byte unicode charachters need to be handled like this because NSString counts multi-byte chars as single charachters while C does not obviously) 29 | 30 | @param charachter The charachter 31 | @return A value between 0-1 if that charachter is valid 32 | */ 33 | int getVisibleByteEffectForCharachter(unsigned char charachter) { 34 | int firstHighBit = (charachter & 0x80); 35 | if (firstHighBit == 0x0) { 36 | //Regular ASCII 37 | return 1; 38 | }else { 39 | unsigned char secondHighBit = ((charachter << 1) & 0x80); 40 | if (secondHighBit == 0x0) { 41 | //Additional byte charachter (10xxxxxx charachter). Not visible 42 | return 0; 43 | }else { 44 | //This is the start of a multibyte charachter, count it (1+) 45 | unsigned char fourByteTest = charachter & 0b11110000; 46 | if (fourByteTest == 0b11110000) { 47 | //Patch for apple's weirdness with four byte charachters (they're counted as two visible? WHY?!?!?) 48 | return 2; 49 | }else { 50 | //We're multibyte but not a four byte which requires the patch, count normally 51 | return 1; 52 | } 53 | } 54 | } 55 | } 56 | 57 | /** 58 | Tockenize and extract tag info from the input and then output the cleaned string alongisde a tag array with relevant position info 59 | 60 | @param input Input text as a char array 61 | @param inputLength The number of charachters (as bytes) to read, excluding the null byte! 62 | @param displayText The char array to write the clean, display text to 63 | @param completedTags (returned) The array to write the t_format structs to (provides position and tag info). Tags positions are CHARACHTER relative, not byte relative! Usable in NSAttributedString etc 64 | @param numberOfTags (returned) The number of tags discovered 65 | */ 66 | void tokenizeHTML(char input[],size_t inputLength,char displayText[], struct t_tag completedTags[], int* numberOfTags, int* numberOfHumanVisibleCharachters) { 67 | //A stack used for processing tags. The stack size allocates space for x number of POINTERS. Ie this is not creating an overflow vulnerability AFAIK 68 | struct Stack* htmlTags = createStack((int)inputLength); 69 | //Completed / filled tags 70 | //struct t_format completedTags[(int)inputLength]; 71 | int completedTagsPosition = 0; 72 | 73 | //Used to track if we are currently reading the label of an HTML tag 74 | bool isInTag = false; 75 | char *tagNameCharArray = malloc(inputLength * sizeof(char) + 1); //+1 for a null byte 76 | char *tagNameBuffer = &tagNameCharArray[0];//Hack to get our buffer on the stack because it's a very fast allocation 77 | int tagNameCopyPosition = 0; 78 | 79 | //Used to track if we are currently reading an HTML entity 80 | bool isInHTMLEntity = false; 81 | char *htmlEntityCharArray = malloc(inputLength * sizeof(char) + 1); //+1 for a null byte 82 | char *htmlEntityBuffer = &htmlEntityCharArray[0];//Hack to get our buffer on the stack because it's a very fast allocation 83 | int htmlEntityCopyPosition = 0; 84 | 85 | int stringCopyPosition = 0; 86 | //Used for applying tokens, DO NOT USE FOR MEMORY WORK. This is used because NSString handles multibyte charachters as single charachters and not as multiple like we have to 87 | int stringVisiblePosition = 0; 88 | 89 | char previous = 0x00; 90 | //The current index label (i.e. 1,2,3) of the list, USHRT_MAX for unordered 91 | unsigned short currentListValue = 0x00; 92 | 93 | for (int i = 0; i < inputLength; i++) { 94 | char current = input[i]; 95 | if (current == '<') { 96 | isInTag = true; 97 | tagNameCopyPosition = 0; 98 | 99 | //If there's a next charachter (data validation) and it's NOT '/' (i.e. we're an open tag) we want to create a new formatter on the stack 100 | if (i+1 < inputLength && input[i+1] != '/') { 101 | struct t_tag format; 102 | format.tag = NULL; 103 | format.startPosition = stringVisiblePosition; 104 | push(htmlTags,format); 105 | } 106 | 107 | }else if (current == '>') { 108 | //We've hit an unencoded less than which terminates an HTML tag 109 | isInTag = false; 110 | //Terminate the buffer 111 | tagNameBuffer[tagNameCopyPosition] = 0x00; 112 | 113 | //Are we a closing HTML tag (i.e. the first character in our tag is a '/') 114 | if (tagNameBuffer[0] == '/') { 115 | //We are a closing tag, commit 116 | struct t_tag* formatP = pop(htmlTags); 117 | //Make sure we didn't get a NULL from popping an empty stack 118 | if (formatP != 0) { 119 | struct t_tag format = *formatP; 120 | format.endPosition = stringVisiblePosition; 121 | completedTags[completedTagsPosition] = format; 122 | completedTagsPosition++; 123 | } 124 | } 125 | //Are we a self closing tag like
or
? 126 | else if ((tagNameCopyPosition > 0 && tagNameBuffer[tagNameCopyPosition-1] == '/')) { 127 | //These tags are special because they're an action in it of themselves so they both start themselves and commit all in one. 128 | struct t_tag format = *pop(htmlTags); 129 | 130 | /* special cases, take a shortcut and remove the tags */ 131 | if (strncmp(tagNameBuffer, "br/", 3) == 0) { 132 | //We're a
tag, drop a new line into the actual text and remove the tag 133 | //IGNORE THESE WHEN USING THE REDDIT MODE because Reddit already sends a new line after
tags so it's duplicated in effect 134 | #ifndef reddit_mode 135 | displayText[stringCopyPosition] = '\n'; 136 | stringCopyPosition++; 137 | stringVisiblePosition++; 138 | #endif 139 | }else { 140 | //We're not a known case, add the tag into the extracted tag array 141 | long tagNameLength = (tagNameCopyPosition + 1) * sizeof(char); 142 | char *newTagBuffer = malloc(tagNameLength); 143 | strncpy(newTagBuffer,tagNameBuffer,tagNameLength); 144 | 145 | format.tag = newTagBuffer; 146 | format.startPosition = stringVisiblePosition; 147 | format.endPosition = stringVisiblePosition; 148 | 149 | completedTags[completedTagsPosition] = format; 150 | completedTagsPosition++; 151 | } 152 | 153 | 154 | }else { 155 | //No -- so let's push the operation onto our stack 156 | //We've ended the tag definition, so pull the tag from the buffer and push that on to the stack 157 | long tagNameLength = (tagNameCopyPosition + 1) * sizeof(char); 158 | char *newTagBuffer = malloc(tagNameLength); 159 | memset(newTagBuffer, 0x0, tagNameLength); 160 | strncpy(newTagBuffer,tagNameBuffer,tagNameLength); 161 | struct t_tag* formatP = pop(htmlTags); 162 | //Make sure we didn't get a NULL from popping an empty stack 163 | //If we end up failing here the text will be horribly mangled however "broken formatting" IMHO is better than a full crash or worse a sec issue 164 | if (formatP != 0) { 165 | struct t_tag format = *formatP; 166 | format.tag = newTagBuffer; 167 | push(htmlTags,format); 168 | } 169 | 170 | //Add textual descriptors for order/unordered lists 171 | if (strncmp(newTagBuffer, "ol", 2) == 0) { 172 | //Ordered list 173 | currentListValue = 1; 174 | }else if (strncmp(newTagBuffer, "ul", 2) == 0) { 175 | //Unordered list 176 | currentListValue = USHRT_MAX; 177 | }else if (strncmp(newTagBuffer, "li", 2) == 0) { 178 | //Apply current list index 179 | if (currentListValue == USHRT_MAX) { 180 | stringVisiblePosition += 2; 181 | displayText[stringCopyPosition++] = 0xE2; 182 | displayText[stringCopyPosition++] = 0x80; 183 | displayText[stringCopyPosition++] = 0xA2; 184 | displayText[stringCopyPosition++] = ' '; 185 | }else { 186 | int written = sprintf(&displayText[stringCopyPosition], "%i. ",currentListValue); 187 | stringCopyPosition += written; 188 | stringVisiblePosition += written; 189 | currentListValue++; 190 | } 191 | } 192 | } 193 | tagNameCopyPosition = 0; 194 | }else if (current == '&') { 195 | //We are starting an HTML entitiy; 196 | isInHTMLEntity = true; 197 | htmlEntityCopyPosition = 0; 198 | htmlEntityBuffer[htmlEntityCopyPosition] = '&'; 199 | htmlEntityCopyPosition++; 200 | }else if (isInHTMLEntity == true && current == ';') { 201 | //We are finishing an HTML entity 202 | isInHTMLEntity = false; 203 | htmlEntityBuffer[htmlEntityCopyPosition] = ';'; 204 | htmlEntityCopyPosition++; 205 | htmlEntityBuffer[htmlEntityCopyPosition] = 0x00; 206 | htmlEntityCopyPosition++; 207 | 208 | //Are we decoding into a tag (i.e. into the url portion of 209 | if (isInTag) { 210 | //Yes! 211 | size_t numberDecodedBytes = decode_html_entities_utf8(&tagNameBuffer[tagNameCopyPosition], htmlEntityBuffer); 212 | tagNameCopyPosition += numberDecodedBytes; 213 | }else { 214 | //Expand into regular text 215 | size_t numberDecodedBytes = decode_html_entities_utf8(&displayText[stringCopyPosition], htmlEntityBuffer); 216 | for (unsigned long decodedI = 0; decodedI < numberDecodedBytes; decodedI++) { 217 | //Add the visual effect for each characher. This lets us also handle when decode sends back a tag it can't decode. 218 | //Also helpful incase we have codes which decode to multiple charachters, which could happen 219 | stringVisiblePosition += getVisibleByteEffectForCharachter(displayText[stringCopyPosition + decodedI]); 220 | } 221 | 222 | stringCopyPosition += numberDecodedBytes; 223 | } 224 | 225 | 226 | }else { 227 | if (isInTag) { 228 | tagNameBuffer[tagNameCopyPosition] = current; 229 | tagNameCopyPosition++; 230 | }else if (isInHTMLEntity) { 231 | htmlEntityBuffer[htmlEntityCopyPosition] = current; 232 | htmlEntityCopyPosition++; 233 | }else { 234 | 235 | //Don't allow double new lines (thanks redddit for sending these?) 236 | //Don't allow just new lines (happens between blockquotes and p tags, again reddit issue) 237 | //This messes up quote formatting 238 | #ifdef reddit_mode 239 | if ((current != '\n' || previous != '\n') && (current != '\n' || stringVisiblePosition > 1 )) { 240 | #endif 241 | previous = current; 242 | displayText[stringCopyPosition] = current; 243 | stringVisiblePosition+=getVisibleByteEffectForCharachter(current); 244 | stringCopyPosition++; 245 | #ifdef reddit_mode 246 | } 247 | #endif 248 | 249 | } 250 | } 251 | } 252 | 253 | //Check if the last tag is incomplete (i.e. "blah blah 0) { 255 | printf("!!! Found incomplete tag, popping and continuing..."); 256 | pop(htmlTags); 257 | } 258 | 259 | //and now terminate our output. 260 | displayText[stringCopyPosition] = 0x00; 261 | 262 | //Run through the unclosed tags so we can either process them and or free them 263 | while (!isEmpty(htmlTags)) { 264 | struct t_tag* formatP = pop(htmlTags); 265 | //Make sure we didn't get a NULL from popping an empty stack 266 | if (formatP != NULL) { 267 | struct t_tag in = *formatP; 268 | printf("!!! UNCLOSED TAG: %s starts at %i ends at %i\n",in.tag,in.startPosition,in.endPosition); 269 | free(in.tag); 270 | } 271 | } 272 | 273 | //Now print out all tags 274 | 275 | for (int i = 0; i < completedTagsPosition; i++) { 276 | #pragma GCC diagnostic push 277 | #pragma GCC diagnostic ignored "-Wunused-variable" 278 | struct t_tag inTag = completedTags[i]; 279 | printf("TAG: %s starts at %i ends at %i\n",inTag.tag,inTag.startPosition,inTag.endPosition); 280 | #pragma GCC diagnostic pop 281 | } 282 | *numberOfTags = completedTagsPosition; 283 | *numberOfHumanVisibleCharachters = stringVisiblePosition; 284 | 285 | //Release everything that's not necessary 286 | prepareForFree(htmlTags); 287 | free(htmlTags); 288 | free(tagNameCharArray); 289 | free(htmlEntityCharArray); 290 | } 291 | 292 | void print_t_format(struct t_format format) { 293 | printf("Format [%i,%i): Bold %i, Italic %i, Struck %i, Code %i, Exponent %i, Quote %i, H%i, ListNest %i LinkURL %s\n",format.startPosition,format.endPosition,format.isBold,format.isItalics,format.isStruck,format.isCode,format.exponentLevel,format.quoteLevel,format.hLevel,format.listNestLevel,format.linkURL); 294 | } 295 | 296 | 297 | /** 298 | Compare two t_formats. Returns 0 for the same, 1 if different in anyway 299 | 300 | @param format1 The first t_format struct 301 | @param format2 The second t_format struct 302 | @return 0 or 1 303 | */ 304 | int t_format_cmp(struct t_format format1,struct t_format format2) { 305 | //Doubles are 8 bytes, which covers all the boolean properties and a tiny bit of the link pointer 306 | //Tip from one of the LLVM people at WWDC`18 307 | double format1Sum = *(((double*)&format1.isBold)); 308 | double format2Sum = *(((double*)&format2.isBold)); 309 | //Get the next 8 bytes 310 | //double format3Sum = *((1 + (double*)&format1.isBold)); 311 | //double format4Sum = *((1 + (double*)&format2.isBold)); 312 | if (format1Sum != format2Sum /*|| format3Sum != format4Sum*/) { 313 | return 1; 314 | }if (format1.linkURL != format2.linkURL || ((format1.linkURL != NULL && format2.linkURL == NULL) || (format2.linkURL != NULL && format1.linkURL == NULL)) || (format1.linkURL != NULL && format2.linkURL != NULL && strcmp(format1.linkURL, format2.linkURL) != 0)) { 315 | return 1; 316 | }else { 317 | return 0; 318 | } 319 | } 320 | 321 | 322 | /** 323 | Takes in overlapping t_format tags and simplifies them into 1D range suitable for use in NSAttributedString. Destroys inputTags in the process! 324 | 325 | @param inputTags Overlapping tags buffer (given by tokenizeHTML) 326 | @param numberOfInputTags The number of inputTags 327 | @param simplifiedTags (return) Simplified tags buffer (return value) 328 | @param numberOfSimplifiedTags (return) the number of found simplified tags 329 | @param displayTextLength The size of the text that we will be applying these tags to 330 | */ 331 | void makeAttributesLinear(struct t_tag inputTags[], int numberOfInputTags, struct t_format simplifiedTags[], int* numberOfSimplifiedTags, int displayTextLength) { 332 | //Create our state array 333 | size_t bufferSize = displayTextLength * sizeof(struct t_format); 334 | struct t_format *displayTextFormat = malloc(bufferSize); 335 | //Init everything to zero in a single pass memory zero 336 | memset(displayTextFormat, 0, bufferSize); 337 | 338 | //Apply format from each tag 339 | for (int i = 0; i < numberOfInputTags; i++) { 340 | struct t_tag tag = inputTags[i]; 341 | char* tagText = tag.tag; 342 | 343 | if (tagText == NULL) { 344 | printf("NULL TAG TEXT?? SKIPPING!"); 345 | }else if (strncmp(tagText, "strong", 6) == 0) { 346 | //Apply bold to all 347 | for (int j = tag.startPosition; j < tag.endPosition; j++) { 348 | displayTextFormat[j].isBold = 1; 349 | } 350 | }else if (strncmp(tagText, "em", 2) == 0) { 351 | //Apply italics to all 352 | for (int j = tag.startPosition; j < tag.endPosition; j++) { 353 | displayTextFormat[j].isItalics = 1; 354 | } 355 | }else if (strncmp(tagText, "del", 3) == 0) { 356 | //Apply strike to all 357 | for (int j = tag.startPosition; j < tag.endPosition; j++) { 358 | displayTextFormat[j].isStruck = 1; 359 | } 360 | }else if (strncmp(tagText, "code", 4) == 0) { 361 | //Apply CODE! to all 362 | for (int j = tag.startPosition; j < tag.endPosition; j++) { 363 | displayTextFormat[j].isCode = 1; 364 | } 365 | }else if (strncmp(tagText, "blockquote", 10) == 0) { 366 | //Increase quote level 367 | for (int j = tag.startPosition; j < tag.endPosition; j++) { 368 | displayTextFormat[j].quoteLevel++; 369 | } 370 | }else if (strncmp(tagText, "sup", 3) == 0) { 371 | //Increase superscript level 372 | for (int j = tag.startPosition; j < tag.endPosition; j++) { 373 | displayTextFormat[j].exponentLevel++; 374 | } 375 | }else if (tagText[0] == 'h' && tagText[1] >= '1' && tagText[1] <= '6') { 376 | //Set our header level 377 | for (int j = tag.startPosition; j < tag.endPosition; j++) { 378 | displayTextFormat[j].hLevel = tagText[1] - '0'; 379 | } 380 | }else if (strncmp(tagText, "a href=", 7) == 0) { 381 | //We first need to extract the link 382 | long tagTextLength = strlen(tagText); 383 | char *url = malloc(tagTextLength-7); 384 | //Extract the URL 385 | int z = 8; 386 | for (; z < tagTextLength; z++) { 387 | if (tagText[z] == '"') { 388 | break; 389 | }else { 390 | url[z-8] = tagText[z]; 391 | } 392 | } 393 | url[z-8] = 0x00; 394 | 395 | //Set our link 396 | for (int j = tag.startPosition; j < tag.endPosition; j++) { 397 | displayTextFormat[j].linkURL = url; 398 | } 399 | 400 | //If we never got into the loop above (and so url is never stored else where), free it now. 401 | if (tag.endPosition - tag.startPosition <= 0) { 402 | free(url); 403 | } 404 | 405 | }else if (strncmp(tagText, "ol", 2) == 0 || (strncmp(tagText, "ul", 2) == 0)) { 406 | //Apply list intendation 407 | for (int j = tag.startPosition; j < tag.endPosition; j++) { 408 | displayTextFormat[j].listNestLevel++; 409 | } 410 | } 411 | else { 412 | printf("Unknown tag: %s\n",tagText); 413 | } 414 | 415 | 416 | //Destroy inputTags data as warned 417 | free(tag.tag); 418 | tag.tag = NULL; 419 | } 420 | 421 | for (int i = 0; i < displayTextLength; i++) { 422 | //print_t_format(displayTextFormat[i]); 423 | } 424 | printf("--------\n"); 425 | 426 | //Now that each charachter has it's style, let's simplify to a 1D 427 | *numberOfSimplifiedTags = 0; 428 | unsigned int activeStyleStart = 0; 429 | for (int i = 1; i < displayTextLength; i++) { 430 | if (t_format_cmp(displayTextFormat[activeStyleStart], displayTextFormat[i]) != 0) { 431 | //We're different, so commit our previous style (with start and ends) and adopt the current one 432 | displayTextFormat[i-1].startPosition = activeStyleStart; 433 | displayTextFormat[i-1].endPosition = i; 434 | simplifiedTags[*numberOfSimplifiedTags] = displayTextFormat[i-1]; 435 | 436 | if (displayTextFormat[i-1].linkURL) { 437 | simplifiedTags[*numberOfSimplifiedTags].linkURL = malloc(strlen(displayTextFormat[i-1].linkURL) + 1); 438 | memcpy(simplifiedTags[*numberOfSimplifiedTags].linkURL, displayTextFormat[i-1].linkURL, strlen(displayTextFormat[i-1].linkURL) + 1); 439 | } 440 | 441 | print_t_format(displayTextFormat[i-1]); 442 | *numberOfSimplifiedTags+=1; 443 | activeStyleStart = i; 444 | } 445 | } 446 | 447 | //and commit the final style 448 | //We need to make sure we have displayed text otherwise we over/underflow here 449 | if (displayTextLength > 0) { 450 | displayTextFormat[displayTextLength-1].startPosition = activeStyleStart; 451 | displayTextFormat[displayTextLength-1].endPosition = displayTextLength; 452 | simplifiedTags[*numberOfSimplifiedTags] = displayTextFormat[displayTextLength-1]; 453 | if (displayTextFormat[displayTextLength-1].linkURL) { 454 | simplifiedTags[*numberOfSimplifiedTags].linkURL = malloc(strlen(displayTextFormat[displayTextLength-1].linkURL) + 1); 455 | memcpy(simplifiedTags[*numberOfSimplifiedTags].linkURL, displayTextFormat[displayTextLength-1].linkURL, strlen(displayTextFormat[displayTextLength-1].linkURL) + 1); 456 | } 457 | print_t_format(displayTextFormat[displayTextLength-1]); 458 | *numberOfSimplifiedTags+=1; 459 | } 460 | 461 | //now free 462 | for (int i = 0; i < displayTextLength; i++) { 463 | //do we have a linkURL and is it either different from the next one or are we the last one 464 | //this is neccesary so we don't double free the URL 465 | if (displayTextFormat[i].linkURL && ((i + 1 < displayTextLength && displayTextFormat[i+1].linkURL != displayTextFormat[i].linkURL) || (i+1 >= displayTextLength))) { 466 | free(displayTextFormat[i].linkURL); 467 | displayTextFormat[i].linkURL = NULL; 468 | } 469 | } 470 | 471 | free(displayTextFormat); 472 | } 473 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/HTMLFastParseSupport/C_HTML_Parser.h: -------------------------------------------------------------------------------- 1 | // 2 | // C_HTML_Parser.h 3 | // HTMLFastParse 4 | // 5 | // Created by Allison Husain on 4/27/18. 6 | // Copyright © 2018 CarbonDev. All rights reserved. 7 | // 8 | 9 | #ifndef C_HTML_Parser_h 10 | #define C_HTML_Parser_h 11 | 12 | #include 13 | #include "t_tag.h" 14 | #include "t_format.h" 15 | 16 | void tokenizeHTML(char input[],size_t inputLength,char displayText[], struct t_tag completedTags[], int* numberOfTags, int* numberOfHumanVisibleCharachters); 17 | void makeAttributesLinear(struct t_tag inputTags[], int numberOfInputTags, struct t_format simplifiedTags[], int* numberOfSimplifiedTags, int displayTextLength); 18 | 19 | #endif /* C_HTML_Parser_h */ 20 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/HTMLFastParseSupport/FormatToAttributedString.h: -------------------------------------------------------------------------------- 1 | // 2 | // FormatToAttributedString.h 3 | // HTMLFastParse 4 | // 5 | // Created by Allison Husain on 4/28/18. 6 | // Copyright © 2018 CarbonDev. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface FormatToAttributedString : NSObject 13 | -(NSAttributedString *)attributedStringForHTML:(NSString *)htmlInput; 14 | -(void)setDefaultFontColor:(UIColor *)defaultColor; 15 | @end 16 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/HTMLFastParseSupport/FormatToAttributedString.m: -------------------------------------------------------------------------------- 1 | // 2 | // FormatToAttributedString.m 3 | // HTMLFastParse 4 | // 5 | // Created by Allison Husain on 4/28/18. 6 | // Copyright © 2018 CarbonDev. All rights reserved. 7 | // 8 | 9 | #import "FormatToAttributedString.h" 10 | #import "C_HTML_Parser.h" 11 | #import 12 | 13 | @implementation FormatToAttributedString 14 | NSString *standardFontName; 15 | NSString *boldFontName; 16 | NSString *italicFontName; 17 | NSString *italicsBoldFontName; 18 | NSString *codeFontName; 19 | 20 | UIFont *plainFont; 21 | UIFont *boldFont; 22 | UIFont *italicsFont; 23 | UIFont *italicsBoldFont; 24 | UIFont *codeFont; 25 | 26 | 27 | UIColor *defaultFontColor; 28 | UIColor *codeFontColor; 29 | UIColor *containerBackgroundColor; 30 | UIColor *quoteFontColor; 31 | UIColor *linkColor; 32 | 33 | //We pregenerate nested quotes up to four for speed, after that they're dynamically allocated 34 | NSMutableParagraphStyle *quoteParagraphStyle1; 35 | NSMutableParagraphStyle *quoteParagraphStyle2; 36 | NSMutableParagraphStyle *quoteParagraphStyle3; 37 | NSMutableParagraphStyle *quoteParagraphStyle4; 38 | NSMutableParagraphStyle *defaultParagraphStyle; 39 | 40 | //The most basic text font size 41 | CGFloat baseFontSize; 42 | 43 | float quotePadding = 20.0; 44 | 45 | 46 | /** 47 | Create a new Formatter 48 | 49 | @return self 50 | */ 51 | -(id)init { 52 | self = [super init]; 53 | //Configure out colors 54 | codeFontColor = [UIColor colorWithRed:255.0/255 green:0 blue:255.0/255 alpha:1]; 55 | containerBackgroundColor = [UIColor colorWithRed:242.0/255 green:242.0/255 blue:242.0/255 alpha:1]; 56 | quoteFontColor = [UIColor colorWithRed:119.0/255 green:119.0/255 blue:119.0/255 alpha:1]; 57 | linkColor = [UIColor colorWithRed:9.0/255 green:95.0/255 blue:255.0/255 alpha:1]; 58 | defaultFontColor = [UIColor blackColor]; 59 | 60 | //Prepare our common fonts once 61 | codeFontName = @"CourierNewPSMT"; 62 | [self prepareFonts]; 63 | return self; 64 | } 65 | 66 | 67 | /** 68 | Initilize and cache high frquency fonts, colors, and other styles 69 | */ 70 | -(void)prepareFonts { 71 | //Get the user's prefered fontsize from the system and use that as the base 72 | baseFontSize = [UIFont preferredFontForTextStyle:UIFontTextStyleBody].pointSize; 73 | 74 | UIFontDescriptor *fontDescriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:UIFontTextStyleBody]; 75 | 76 | 77 | plainFont = [UIFont systemFontOfSize:baseFontSize weight:UIFontWeightRegular]; 78 | boldFont = [UIFont systemFontOfSize:baseFontSize weight:UIFontWeightBold]; 79 | italicsFont = [UIFont italicSystemFontOfSize:baseFontSize]; 80 | 81 | UIFontDescriptor *boldItalicDescriptor = [fontDescriptor fontDescriptorWithSymbolicTraits:[fontDescriptor symbolicTraits] | UIFontDescriptorTraitBold | UIFontDescriptorTraitItalic]; 82 | italicsBoldFont = [UIFont fontWithDescriptor:boldItalicDescriptor size:baseFontSize]; 83 | 84 | codeFont = [UIFont fontWithName:codeFontName size:baseFontSize]; 85 | 86 | //Cache high frequency quote depths (1-4), after these they'll be dynamically generated 87 | quoteParagraphStyle1 = [self generateParagraphStyleAtLevel:1]; 88 | quoteParagraphStyle2 = [self generateParagraphStyleAtLevel:2]; 89 | quoteParagraphStyle3 = [self generateParagraphStyleAtLevel:3]; 90 | quoteParagraphStyle4 = [self generateParagraphStyleAtLevel:4]; 91 | defaultParagraphStyle = [self defaultParagraphStyle]; 92 | } 93 | 94 | 95 | /** 96 | Override the default font text color (by default this is black). This only needs to be called once 97 | 98 | @param defaultColor The color to change it to 99 | */ 100 | -(void)setDefaultFontColor:(UIColor *)defaultColor { 101 | defaultFontColor = defaultColor; 102 | } 103 | 104 | 105 | /** 106 | Generate an indented "style" 107 | This is used for quote formatting 108 | 109 | @param depth The depth * `quotePadding` is the amount of indent that will be used. Zero means no indent 110 | @return A paragraph style object usuable in attribution 111 | */ 112 | -(NSMutableParagraphStyle *)generateParagraphStyleAtLevel:(int)depth { 113 | NSMutableParagraphStyle *quoteParagraphStyle = [[NSMutableParagraphStyle alloc]init]; 114 | CGFloat levelQuoteIndentPadding = quotePadding * depth; 115 | [quoteParagraphStyle setParagraphSpacing:plainFont.lineHeight/4]; 116 | [quoteParagraphStyle setHeadIndent:levelQuoteIndentPadding]; 117 | [quoteParagraphStyle setFirstLineHeadIndent:levelQuoteIndentPadding]; 118 | [quoteParagraphStyle setTailIndent:-levelQuoteIndentPadding]; 119 | return quoteParagraphStyle; 120 | } 121 | 122 | 123 | /** 124 | Generate the default paragraph style which should be applied to all text 125 | 126 | @return Default paragraph style 127 | */ 128 | -(NSMutableParagraphStyle *)defaultParagraphStyle { 129 | NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc]init]; 130 | [style setParagraphSpacing:plainFont.lineHeight/4]; 131 | 132 | return style; 133 | } 134 | 135 | 136 | /** 137 | Attribute a string of HTML using HTMLFastParse 138 | 139 | @param htmlInput The HTML to attribute 140 | @return The attributed string 141 | */ 142 | -(NSAttributedString *)attributedStringForHTML:(NSString *)htmlInput { 143 | char* input = (char*)[htmlInput UTF8String]; 144 | if (input == nil) { 145 | //Input can be null if htmlInput is also null or if it is not representable in UTF8. We are not going to bother parsing data which requires > 8bits per field because it can't fit in a char 146 | return [[NSAttributedString alloc]initWithString:@"[HTMLFastParse Internal Error]: Either no data was sent to the parser or the data could not be decoded by the system. Please verify the API is being used correctly or report this at https://github.com/shusain93/HTMLFastParse/issues"]; 147 | } 148 | unsigned long inputLength = strlen(input); 149 | 150 | char* displayText = malloc(inputLength * sizeof(char) + 1); //+1 for a null byte 151 | struct t_tag* tokens = malloc(inputLength * sizeof(struct t_tag)); 152 | 153 | int numberOfTags = -1; 154 | int numberOfHumanVisibleCharachters = -1; 155 | tokenizeHTML(input, inputLength, displayText,tokens,&numberOfTags,&numberOfHumanVisibleCharachters); 156 | 157 | struct t_format* finalTokens = malloc(inputLength * sizeof(struct t_format));//&finalTokenBuffer[0]; 158 | int numberOfSimplifiedTags = -1; 159 | makeAttributesLinear(tokens, (int)numberOfTags, finalTokens,&numberOfSimplifiedTags,numberOfHumanVisibleCharachters); 160 | 161 | //Now apply our linear attributes to our attributed string 162 | NSMutableAttributedString *answer = [[NSMutableAttributedString alloc]initWithString:[NSString stringWithUTF8String:displayText]]; 163 | 164 | //Add our default attributes 165 | [answer addAttributes:@{ 166 | NSFontAttributeName : plainFont, 167 | NSParagraphStyleAttributeName : defaultParagraphStyle, 168 | NSBackgroundColorAttributeName : [UIColor clearColor] 169 | } range:NSMakeRange(0, answer.length)]; 170 | //Only format the string if we are sure that everything will line up (if our calculated visible is not the same as attributed sees, everything will be broken and likely will cause a crash 171 | if ([answer length] == numberOfHumanVisibleCharachters) { 172 | for (int i = 0; i < numberOfSimplifiedTags; i++) { 173 | [self addAttributeToString:answer forFormat:finalTokens[i]]; 174 | free(finalTokens[i].linkURL); 175 | } 176 | }else { 177 | NSAttributedString *failureText = [[NSAttributedString alloc]initWithString:@"\n\n\n[HTMLFastParse Internal Error]: HFP detected an issue where NSAttributedString length and the calculated visible length are not equal. Please report this at https://github.com/shusain93/HTMLFastParse/issues"]; 178 | [answer appendAttributedString: failureText]; 179 | } 180 | 181 | //Free and get ready to return 182 | free(displayText); 183 | free(tokens); 184 | free(finalTokens); 185 | return answer; 186 | } 187 | 188 | 189 | /** 190 | Add the attributes to a given attributed string based on a t_format specifier 191 | 192 | @param string The mutable attributed string to work on 193 | @param format The styles to apply (with range data stuffed!) 194 | */ 195 | -(void)addAttributeToString:(NSMutableAttributedString *)string forFormat:(struct t_format)format { 196 | //This is the range of the style 197 | NSRange currentRange = NSMakeRange(format.startPosition, format.endPosition-format.startPosition); 198 | 199 | if (format.isStruck) { 200 | [string addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInteger:NSUnderlineStyleSingle] range:currentRange]; 201 | } 202 | 203 | if (format.quoteLevel > 0 || format.listNestLevel - 1 > 0) { 204 | NSMutableParagraphStyle *quoteParagraphStyle; 205 | //We have the first four cached and after that we'll dynamically generate 206 | unsigned char level = format.quoteLevel + format.listNestLevel - 1 > 0 ? format.listNestLevel - 1 : 0; 207 | switch (level) { 208 | case 1: 209 | quoteParagraphStyle = quoteParagraphStyle1; 210 | break; 211 | case 2: 212 | quoteParagraphStyle = quoteParagraphStyle2; 213 | break; 214 | case 3: 215 | quoteParagraphStyle = quoteParagraphStyle3; 216 | break; 217 | case 4: 218 | quoteParagraphStyle = quoteParagraphStyle4; 219 | break; 220 | 221 | default: 222 | quoteParagraphStyle = [self generateParagraphStyleAtLevel:format.quoteLevel]; 223 | break; 224 | } 225 | [string addAttribute:NSParagraphStyleAttributeName value:quoteParagraphStyle range:currentRange]; 226 | } 227 | 228 | if (format.quoteLevel > 0) { 229 | [string addAttribute:NSForegroundColorAttributeName value:quoteFontColor range:currentRange]; 230 | } 231 | 232 | if (format.linkURL) { 233 | NSString *nsLinkURL = [NSString stringWithUTF8String:format.linkURL]; 234 | if ([NSURL URLWithString:nsLinkURL] != nil) { 235 | [string addAttribute:NSLinkAttributeName value: nsLinkURL range:currentRange]; 236 | [string addAttribute:NSForegroundColorAttributeName value:linkColor range:currentRange]; 237 | } 238 | } 239 | 240 | 241 | 242 | /* Styling that uses fonts. This includes exponents, h#, bold, italics, and any combination thereof. Code formatting skips all of these */ 243 | 244 | if (format.isCode == 1) { 245 | [string addAttribute:NSFontAttributeName value:codeFont range:currentRange]; 246 | [string addAttribute:NSBackgroundColorAttributeName value:containerBackgroundColor range:currentRange]; 247 | [string addAttribute:NSForegroundColorAttributeName value:codeFontColor range:currentRange]; 248 | } 249 | //Check if we can take a shortcut. We don't need dynamic font in this case 250 | else if (format.hLevel == 0 && format.exponentLevel == 0) { 251 | if (format.isBold == 0 && format.isItalics == 0) { 252 | //Plain text 253 | //Do nothing since it's the default as set above 254 | }else if (format.isBold == 1 && format.isItalics == 1) { 255 | //Bold italics 256 | [string addAttribute:NSFontAttributeName value:italicsBoldFont range:currentRange]; 257 | }else if (format.isBold == 1) { 258 | //Bold 259 | [string addAttribute:NSFontAttributeName value:boldFont range:currentRange]; 260 | }else if (format.isItalics == 1) { 261 | //Italics 262 | [string addAttribute:NSFontAttributeName value:italicsFont range:currentRange]; 263 | } 264 | }else { 265 | //We need to generate a dynamic font since at least one of the attributes changes the font size. 266 | CGFloat fontSize = baseFontSize; 267 | //Handle H# 268 | if (format.hLevel > 0) { 269 | //Reddit only supports 1-6, so that's all that's been implmented 270 | switch (format.hLevel) { 271 | case 0: 272 | break; 273 | case 1: 274 | fontSize *= 2; 275 | break; 276 | case 2: 277 | fontSize *= 1.5; 278 | break; 279 | case 3: 280 | fontSize *= 1.17; 281 | break; 282 | case 4: 283 | fontSize *= 1.12; 284 | break; 285 | case 5: 286 | fontSize *= 0.83; 287 | break; 288 | case 6: 289 | fontSize *= 0.75; 290 | break; 291 | default: 292 | //Unexpcted position, so we're not going to apply this style 293 | NSLog(@"Unknown HLevel"); 294 | break; 295 | } 296 | } 297 | //Handle exponent 298 | if (format.exponentLevel > 0) { 299 | fontSize *= 0.75; 300 | float baselineOffset; 301 | if (format.exponentLevel < 3) { 302 | baselineOffset = format.exponentLevel*10; 303 | }else { 304 | baselineOffset = 40; 305 | } 306 | 307 | [string addAttribute:NSBaselineOffsetAttributeName value:[NSNumber numberWithFloat:baselineOffset] range:currentRange]; 308 | } 309 | 310 | 311 | UIFont *customFont; 312 | /* NOTE: USE fontWithSize: and NOT font descriptors because https://stackoverflow.com/q/34954956/1166266 */ 313 | if (format.isBold == 0 && format.isItalics == 0) { 314 | //Plain text 315 | customFont = [plainFont fontWithSize:fontSize]; 316 | }else if (format.isBold == 1 && format.isItalics == 1) { 317 | //Bold italics 318 | customFont = [italicsBoldFont fontWithSize:fontSize]; 319 | }else if (format.isBold == 1) { 320 | //Bold 321 | customFont = [boldFont fontWithSize:fontSize]; 322 | }else if (format.isItalics == 1) { 323 | //Italics 324 | customFont = [italicsFont fontWithSize:fontSize]; 325 | } 326 | 327 | 328 | [string addAttribute:NSFontAttributeName value:customFont range:currentRange]; 329 | } 330 | 331 | if (format.isCode == 0 && format.quoteLevel == 0 && format.linkURL == nil) { 332 | [string addAttribute:NSForegroundColorAttributeName value:defaultFontColor range:currentRange]; 333 | } 334 | } 335 | @end 336 | 337 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/HTMLFastParseSupport/Stack.c: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Allison Husain on 4/27/18. 3 | // 4 | 5 | // C program for array implementation of stack 6 | #include 7 | #include 8 | #include 9 | #include "t_tag.h" 10 | #include "Stack.h" 11 | 12 | // A structure to represent a stack 13 | struct Stack 14 | { 15 | int top; 16 | unsigned capacity; 17 | struct t_tag* array; 18 | }; 19 | 20 | // function to create a stack of given capacity. It initializes size of 21 | // stack as 0 22 | struct Stack* createStack(unsigned capacity) 23 | { 24 | struct Stack* stack = (struct Stack*) malloc(sizeof(struct Stack)); 25 | stack->capacity = capacity; 26 | stack->top = -1; 27 | stack->array = malloc(stack->capacity * sizeof(struct t_tag)); 28 | return stack; 29 | } 30 | 31 | // Stack is full when top is equal to the last index 32 | int isFull(struct Stack* stack) 33 | { return stack->top == stack->capacity - 1; } 34 | 35 | // Stack is empty when top is equal to -1 36 | int isEmpty(struct Stack* stack) 37 | { return stack->top == -1; } 38 | 39 | // Function to add an item to stack. It increases top by 1 40 | void push(struct Stack* stack, struct t_tag item) 41 | { 42 | if (isFull(stack)) 43 | return; 44 | stack->array[++stack->top] = item; 45 | } 46 | 47 | // Function to remove an item from stack. It decreases top by 1 48 | struct t_tag* pop(struct Stack* stack) 49 | { 50 | if (isEmpty(stack)) 51 | return NULL; 52 | return &stack->array[stack->top--]; 53 | } 54 | 55 | void prepareForFree(struct Stack* stack) { 56 | free(stack->array); 57 | } 58 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/HTMLFastParseSupport/Stack.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Allison Husain on 4/27/18. 3 | // 4 | #include "t_tag.h" 5 | #ifndef HTMLTOATTR_STACK_H 6 | #define HTMLTOATTR_STACK_H 7 | 8 | 9 | struct Stack; 10 | struct Stack* createStack(unsigned capacity); 11 | int isFull(struct Stack* stack); 12 | int isEmpty(struct Stack* stack); 13 | void push(struct Stack* stack, struct t_tag); 14 | struct t_tag* pop(struct Stack* stack); 15 | void prepareForFree(struct Stack* stack); 16 | #endif //HTMLTOATTR_STACK_H 17 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/HTMLFastParseSupport/entities.c: -------------------------------------------------------------------------------- 1 | /* Copyright 2012, 2016 Christoph Gärtner 2 | Distributed under the Boost Software License, Version 1.0 3 | 4 | https://stackoverflow.com/a/1082191/1166266 5 | */ 6 | 7 | #include "entities.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define UNICODE_MAX 0x10FFFFul 15 | 16 | static const char *const NAMED_ENTITIES[][2] = { 17 | { "AElig;", "Æ" }, 18 | { "Aacute;", "Á" }, 19 | { "Acirc;", "Â" }, 20 | { "Agrave;", "À" }, 21 | { "Alpha;", "Α" }, 22 | { "Aring;", "Å" }, 23 | { "Atilde;", "Ã" }, 24 | { "Auml;", "Ä" }, 25 | { "Beta;", "Β" }, 26 | { "Ccedil;", "Ç" }, 27 | { "Chi;", "Χ" }, 28 | { "Dagger;", "‡" }, 29 | { "Delta;", "Δ" }, 30 | { "ETH;", "Ð" }, 31 | { "Eacute;", "É" }, 32 | { "Ecirc;", "Ê" }, 33 | { "Egrave;", "È" }, 34 | { "Epsilon;", "Ε" }, 35 | { "Eta;", "Η" }, 36 | { "Euml;", "Ë" }, 37 | { "Gamma;", "Γ" }, 38 | { "Iacute;", "Í" }, 39 | { "Icirc;", "Î" }, 40 | { "Igrave;", "Ì" }, 41 | { "Iota;", "Ι" }, 42 | { "Iuml;", "Ï" }, 43 | { "Kappa;", "Κ" }, 44 | { "Lambda;", "Λ" }, 45 | { "Mu;", "Μ" }, 46 | { "Ntilde;", "Ñ" }, 47 | { "Nu;", "Ν" }, 48 | { "OElig;", "Œ" }, 49 | { "Oacute;", "Ó" }, 50 | { "Ocirc;", "Ô" }, 51 | { "Ograve;", "Ò" }, 52 | { "Omega;", "Ω" }, 53 | { "Omicron;", "Ο" }, 54 | { "Oslash;", "Ø" }, 55 | { "Otilde;", "Õ" }, 56 | { "Ouml;", "Ö" }, 57 | { "Phi;", "Φ" }, 58 | { "Pi;", "Π" }, 59 | { "Prime;", "″" }, 60 | { "Psi;", "Ψ" }, 61 | { "Rho;", "Ρ" }, 62 | { "Scaron;", "Š" }, 63 | { "Sigma;", "Σ" }, 64 | { "THORN;", "Þ" }, 65 | { "Tau;", "Τ" }, 66 | { "Theta;", "Θ" }, 67 | { "Uacute;", "Ú" }, 68 | { "Ucirc;", "Û" }, 69 | { "Ugrave;", "Ù" }, 70 | { "Upsilon;", "Υ" }, 71 | { "Uuml;", "Ü" }, 72 | { "Xi;", "Ξ" }, 73 | { "Yacute;", "Ý" }, 74 | { "Yuml;", "Ÿ" }, 75 | { "Zeta;", "Ζ" }, 76 | { "aacute;", "á" }, 77 | { "acirc;", "â" }, 78 | { "acute;", "´" }, 79 | { "aelig;", "æ" }, 80 | { "agrave;", "à" }, 81 | { "alefsym;", "ℵ" }, 82 | { "alpha;", "α" }, 83 | { "amp;", "&" }, 84 | { "and;", "∧" }, 85 | { "ang;", "∠" }, 86 | { "apos;", "'" }, 87 | { "aring;", "å" }, 88 | { "asymp;", "≈" }, 89 | { "atilde;", "ã" }, 90 | { "auml;", "ä" }, 91 | { "bdquo;", "„" }, 92 | { "beta;", "β" }, 93 | { "brvbar;", "¦" }, 94 | { "bull;", "•" }, 95 | { "cap;", "∩" }, 96 | { "ccedil;", "ç" }, 97 | { "cedil;", "¸" }, 98 | { "cent;", "¢" }, 99 | { "chi;", "χ" }, 100 | { "circ;", "ˆ" }, 101 | { "clubs;", "♣" }, 102 | { "cong;", "≅" }, 103 | { "copy;", "©" }, 104 | { "crarr;", "↵" }, 105 | { "cup;", "∪" }, 106 | { "curren;", "¤" }, 107 | { "dArr;", "⇓" }, 108 | { "dagger;", "†" }, 109 | { "darr;", "↓" }, 110 | { "deg;", "°" }, 111 | { "delta;", "δ" }, 112 | { "diams;", "♦" }, 113 | { "divide;", "÷" }, 114 | { "eacute;", "é" }, 115 | { "ecirc;", "ê" }, 116 | { "egrave;", "è" }, 117 | { "empty;", "∅" }, 118 | { "emsp;", "\xE2\x80\x83" }, 119 | { "ensp;", "\xE2\x80\x82" }, 120 | { "epsilon;", "ε" }, 121 | { "equiv;", "≡" }, 122 | { "eta;", "η" }, 123 | { "eth;", "ð" }, 124 | { "euml;", "ë" }, 125 | { "euro;", "€" }, 126 | { "exist;", "∃" }, 127 | { "fnof;", "ƒ" }, 128 | { "forall;", "∀" }, 129 | { "frac12;", "½" }, 130 | { "frac14;", "¼" }, 131 | { "frac34;", "¾" }, 132 | { "frasl;", "⁄" }, 133 | { "gamma;", "γ" }, 134 | { "ge;", "≥" }, 135 | { "gt;", ">" }, 136 | { "hArr;", "⇔" }, 137 | { "harr;", "↔" }, 138 | { "hearts;", "♥" }, 139 | { "hellip;", "…" }, 140 | { "iacute;", "í" }, 141 | { "icirc;", "î" }, 142 | { "iexcl;", "¡" }, 143 | { "igrave;", "ì" }, 144 | { "image;", "ℑ" }, 145 | { "infin;", "∞" }, 146 | { "int;", "∫" }, 147 | { "iota;", "ι" }, 148 | { "iquest;", "¿" }, 149 | { "isin;", "∈" }, 150 | { "iuml;", "ï" }, 151 | { "kappa;", "κ" }, 152 | { "lArr;", "⇐" }, 153 | { "lambda;", "λ" }, 154 | { "lang;", "〈" }, 155 | { "laquo;", "«" }, 156 | { "larr;", "←" }, 157 | { "lceil;", "⌈" }, 158 | { "ldquo;", "“" }, 159 | { "le;", "≤" }, 160 | { "lfloor;", "⌊" }, 161 | { "lowast;", "∗" }, 162 | { "loz;", "◊" }, 163 | { "lrm;", "\xE2\x80\x8E" }, 164 | { "lsaquo;", "‹" }, 165 | { "lsquo;", "‘" }, 166 | { "lt;", "<" }, 167 | { "macr;", "¯" }, 168 | { "mdash;", "—" }, 169 | { "micro;", "µ" }, 170 | { "middot;", "·" }, 171 | { "minus;", "−" }, 172 | { "mu;", "μ" }, 173 | { "nabla;", "∇" }, 174 | { "nbsp;", "\xC2\xA0" }, 175 | { "ndash;", "–" }, 176 | { "ne;", "≠" }, 177 | { "ni;", "∋" }, 178 | { "not;", "¬" }, 179 | { "notin;", "∉" }, 180 | { "nsub;", "⊄" }, 181 | { "ntilde;", "ñ" }, 182 | { "nu;", "ν" }, 183 | { "oacute;", "ó" }, 184 | { "ocirc;", "ô" }, 185 | { "oelig;", "œ" }, 186 | { "ograve;", "ò" }, 187 | { "oline;", "‾" }, 188 | { "omega;", "ω" }, 189 | { "omicron;", "ο" }, 190 | { "oplus;", "⊕" }, 191 | { "or;", "∨" }, 192 | { "ordf;", "ª" }, 193 | { "ordm;", "º" }, 194 | { "oslash;", "ø" }, 195 | { "otilde;", "õ" }, 196 | { "otimes;", "⊗" }, 197 | { "ouml;", "ö" }, 198 | { "para;", "¶" }, 199 | { "part;", "∂" }, 200 | { "permil;", "‰" }, 201 | { "perp;", "⊥" }, 202 | { "phi;", "φ" }, 203 | { "pi;", "π" }, 204 | { "piv;", "ϖ" }, 205 | { "plusmn;", "±" }, 206 | { "pound;", "£" }, 207 | { "prime;", "′" }, 208 | { "prod;", "∏" }, 209 | { "prop;", "∝" }, 210 | { "psi;", "ψ" }, 211 | { "quot;", "\"" }, 212 | { "rArr;", "⇒" }, 213 | { "radic;", "√" }, 214 | { "rang;", "〉" }, 215 | { "raquo;", "»" }, 216 | { "rarr;", "→" }, 217 | { "rceil;", "⌉" }, 218 | { "rdquo;", "”" }, 219 | { "real;", "ℜ" }, 220 | { "reg;", "®" }, 221 | { "rfloor;", "⌋" }, 222 | { "rho;", "ρ" }, 223 | { "rlm;", "\xE2\x80\x8F" }, 224 | { "rsaquo;", "›" }, 225 | { "rsquo;", "’" }, 226 | { "sbquo;", "‚" }, 227 | { "scaron;", "š" }, 228 | { "sdot;", "⋅" }, 229 | { "sect;", "§" }, 230 | { "shy;", "\xC2\xAD" }, 231 | { "sigma;", "σ" }, 232 | { "sigmaf;", "ς" }, 233 | { "sim;", "∼" }, 234 | { "spades;", "♠" }, 235 | { "sub;", "⊂" }, 236 | { "sube;", "⊆" }, 237 | { "sum;", "∑" }, 238 | { "sup1;", "¹" }, 239 | { "sup2;", "²" }, 240 | { "sup3;", "³" }, 241 | { "sup;", "⊃" }, 242 | { "supe;", "⊇" }, 243 | { "szlig;", "ß" }, 244 | { "tau;", "τ" }, 245 | { "there4;", "∴" }, 246 | { "theta;", "θ" }, 247 | { "thetasym;", "ϑ" }, 248 | { "thinsp;", "\xE2\x80\x89" }, 249 | { "thorn;", "þ" }, 250 | { "tilde;", "˜" }, 251 | { "times;", "×" }, 252 | { "trade;", "™" }, 253 | { "uArr;", "⇑" }, 254 | { "uacute;", "ú" }, 255 | { "uarr;", "↑" }, 256 | { "ucirc;", "û" }, 257 | { "ugrave;", "ù" }, 258 | { "uml;", "¨" }, 259 | { "upsih;", "ϒ" }, 260 | { "upsilon;", "υ" }, 261 | { "uuml;", "ü" }, 262 | { "weierp;", "℘" }, 263 | { "xi;", "ξ" }, 264 | { "yacute;", "ý" }, 265 | { "yen;", "¥" }, 266 | { "yuml;", "ÿ" }, 267 | { "zeta;", "ζ" }, 268 | { "zwj;", "\xE2\x80\x8D" }, 269 | { "zwnj;", "\xE2\x80\x8C" } 270 | }; 271 | 272 | static int cmp(const void *key, const void *value) 273 | { 274 | return strncmp((const char *)key, *(const char *const *)value, 275 | strlen(*(const char *const *)value)); 276 | } 277 | 278 | static const char *get_named_entity(const char *name) 279 | { 280 | const char *const *entity = (const char *const *)bsearch(name, 281 | NAMED_ENTITIES, sizeof NAMED_ENTITIES / sizeof *NAMED_ENTITIES, 282 | sizeof *NAMED_ENTITIES, cmp); 283 | 284 | return entity ? entity[1] : NULL; 285 | } 286 | 287 | static size_t putc_utf8(unsigned long cp, char *buffer) 288 | { 289 | unsigned char *bytes = (unsigned char *)buffer; 290 | 291 | if(cp <= 0x007Ful) 292 | { 293 | bytes[0] = (unsigned char)cp; 294 | return 1; 295 | } 296 | 297 | if(cp <= 0x07FFul) 298 | { 299 | bytes[1] = (unsigned char)((2 << 6) | (cp & 0x3F)); 300 | bytes[0] = (unsigned char)((6 << 5) | (cp >> 6)); 301 | return 2; 302 | } 303 | 304 | if(cp <= 0xFFFFul) 305 | { 306 | bytes[2] = (unsigned char)(( 2 << 6) | ( cp & 0x3F)); 307 | bytes[1] = (unsigned char)(( 2 << 6) | ((cp >> 6) & 0x3F)); 308 | bytes[0] = (unsigned char)((14 << 4) | (cp >> 12)); 309 | return 3; 310 | } 311 | 312 | if(cp <= 0x10FFFFul) 313 | { 314 | bytes[3] = (unsigned char)(( 2 << 6) | ( cp & 0x3F)); 315 | bytes[2] = (unsigned char)(( 2 << 6) | ((cp >> 6) & 0x3F)); 316 | bytes[1] = (unsigned char)(( 2 << 6) | ((cp >> 12) & 0x3F)); 317 | bytes[0] = (unsigned char)((30 << 3) | (cp >> 18)); 318 | return 4; 319 | } 320 | 321 | return 0; 322 | } 323 | 324 | static bool parse_entity( 325 | const char *current, char **to, const char **from) 326 | { 327 | const char *end = strchr(current, ';'); 328 | if(!end) return 0; 329 | 330 | if(current[1] == '#') 331 | { 332 | char *tail = NULL; 333 | int errno_save = errno; 334 | bool hex = current[2] == 'x' || current[2] == 'X'; 335 | 336 | errno = 0; 337 | unsigned long cp = strtoul( 338 | current + (hex ? 3 : 2), &tail, hex ? 16 : 10); 339 | 340 | // do not allow nullbytes to be inserted via HTML entities 341 | bool fail = errno || tail != end || cp > UNICODE_MAX || cp == 0x0; 342 | errno = errno_save; 343 | if(fail) return 0; 344 | 345 | *to += putc_utf8(cp, *to); 346 | *from = end + 1; 347 | 348 | return 1; 349 | } 350 | else 351 | { 352 | const char *entity = get_named_entity(¤t[1]); 353 | if(!entity) return 0; 354 | 355 | size_t len = strlen(entity); 356 | memcpy(*to, entity, len); 357 | 358 | *to += len; 359 | *from = end + 1; 360 | 361 | return 1; 362 | } 363 | } 364 | 365 | size_t decode_html_entities_utf8(char *dest, const char *src) 366 | { 367 | if(!src) src = dest; 368 | 369 | char *to = dest; 370 | const char *from = src; 371 | 372 | for(const char *current; (current = strchr(from, '&'));) 373 | { 374 | memmove(to, from, (size_t)(current - from)); 375 | to += current - from; 376 | 377 | if(parse_entity(current, &to, &from)) 378 | continue; 379 | 380 | from = current; 381 | *to++ = *from++; 382 | } 383 | 384 | size_t remaining = strlen(from); 385 | 386 | memmove(to, from, remaining); 387 | to += remaining; 388 | *to = 0; 389 | 390 | return (size_t)(to - dest); 391 | } 392 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/HTMLFastParseSupport/entities.h: -------------------------------------------------------------------------------- 1 | /* Copyright 2012 Christoph Gärtner 2 | Distributed under the Boost Software License, Version 1.0 3 | 4 | https://stackoverflow.com/a/1082191/1166266 5 | */ 6 | 7 | #ifndef DECODE_HTML_ENTITIES_UTF8_ 8 | #define DECODE_HTML_ENTITIES_UTF8_ 9 | 10 | #include 11 | 12 | extern size_t decode_html_entities_utf8(char *dest, const char *src); 13 | /* Takes input from and decodes into , which should be a buffer 14 | large enough to hold characters. 15 | 16 | If is , input will be taken from , decoding 17 | the entities in-place. 18 | 19 | The function returns the length of the decoded string. 20 | */ 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/HTMLFastParseSupport/t_format.h: -------------------------------------------------------------------------------- 1 | // 2 | // t_format.h 3 | // HTMLFastParse 4 | // 5 | // Created by Allison Husain on 4/27/18. 6 | // Copyright © 2018 CarbonDev. All rights reserved. 7 | // 8 | 9 | #ifndef t_format_h 10 | #define t_format_h 11 | 12 | 13 | 14 | /** 15 | A structure representing a charachter/range's text formatting 16 | */ 17 | struct t_format { 18 | //ZERO MEANS DISABLED 19 | unsigned char isBold; 20 | unsigned char isItalics; 21 | unsigned char isStruck; 22 | unsigned char isCode; 23 | unsigned char exponentLevel; 24 | unsigned char quoteLevel; 25 | unsigned char hLevel; 26 | unsigned char listNestLevel; 27 | /*unsigned short LONG_DOUBLE_BUFFER_REMOVE2; 28 | unsigned short LONG_DOUBLE_BUFFER_REMOVE4; 29 | unsigned short LONG_DOUBLE_BUFFER_REMOVE6; 30 | unsigned char LONG_DOUBLE_BUFFER_REMOVE7;*/ 31 | 32 | char* linkURL; 33 | 34 | unsigned int startPosition; 35 | unsigned int endPosition; 36 | }; 37 | 38 | #endif /* t_format_h */ 39 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/HTMLFastParseSupport/t_tag.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Allison Husain on 4/27/18. 3 | // 4 | 5 | #ifndef HTMLTOATTR_FORMAT_H 6 | #define HTMLTOATTR_FORMAT_H 7 | struct t_tag { 8 | unsigned int startPosition; 9 | unsigned int endPosition; 10 | char* tag; 11 | }; 12 | #endif //HTMLTOATTR_FORMAT_H 13 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /DYLabelDemo/DYLabelDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // DYLabelDemo 4 | // 5 | // Created by Allison Husain on 10/14/18. 6 | // Copyright © 2018 Allison Husain. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController, DYLinkDelegate { 12 | // ^^^^^^^^^^^^^^^^^^ note that we are implmenting DYLinkDelegate, this lets us recieve link touches/holds 13 | 14 | //MARK: DYLinkDelegate methods 15 | 16 | func didClickLink(label: DYLabel, link: DYLink) { 17 | let alert = UIAlertController.init(title: "Link click", message: "Link \(link.url)", preferredStyle: UIAlertController.Style.alert) 18 | alert.addAction(UIAlertAction.init(title: "Cancel", style: UIAlertAction.Style.cancel, handler: nil)) 19 | self.show(alert, sender: nil) 20 | } 21 | 22 | func didLongPressLink(label: DYLabel, link: DYLink) { 23 | let alert = UIAlertController.init(title: "Link long press", message: "Link \(link.url)", preferredStyle: UIAlertController.Style.alert) 24 | alert.addAction(UIAlertAction.init(title: "Cancel", style: UIAlertAction.Style.cancel, handler: nil)) 25 | self.show(alert, sender: nil) 26 | } 27 | 28 | //MARK: Creating the label 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | //Step 0: Get some attributed text. I'm using my library (named HTMLFastParse, also a drop in code library) to create it 32 | let formatter = FormatToAttributedString.init() 33 | let attributedString = formatter.attributedString(forHTML: "Link test first and another link.\n\nDynamic baseline/super script also works correctly")! 34 | 35 | //Step 2: Create the label 36 | let label = DYLabel.init(attributedText: attributedString, backgroundColor: UIColor.white, frame: CGRect.zero) 37 | 38 | //Step 3: Size the label correctly, setup the frame 39 | //Calculate the exact height of the text given a restricting width 40 | let requiredSize = DYLabel.size(of: attributedString, width: self.view.frame.width, estimationHeight: 3000) 41 | 42 | //Step 4: Final configuration 43 | label.frame = CGRect.init(x: 0, y: 40, width: requiredSize.width, height: requiredSize.height) 44 | //Setup our delegate so we recieve clicks and holds 45 | label.dyDelegate = self 46 | 47 | //Step 5: Add it to the view 48 | self.view.addSubview(label) 49 | 50 | //and we're done! 51 | //Uncoment this line to show the debugging rects (useful for accessibility work when using the simulator) 52 | //showRects(label: label) 53 | } 54 | 55 | private func showRects(label:DYLabel) { 56 | label.__enableFrameDebugMode = true 57 | let _ = label.accessibilityElementCount() 58 | for t in (label.__accessibilityElements ?? []).reversed() { 59 | let f = label.convert(t.boundingRect, to: self.view) 60 | let v = UIView.init(frame: f) 61 | v.isUserInteractionEnabled = false 62 | v.backgroundColor = getRandomColor(alpha: 0.5) 63 | self.view.addSubview(v) 64 | } 65 | } 66 | 67 | private func getRandomColor(alpha:CGFloat) -> UIColor{ 68 | let randomRed:CGFloat = CGFloat(drand48()) 69 | let randomGreen:CGFloat = CGFloat(drand48()) 70 | let randomBlue:CGFloat = CGFloat(drand48()) 71 | 72 | return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: alpha) 73 | 74 | } 75 | 76 | } 77 | 78 | -------------------------------------------------------------------------------- /Images/Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhes/DYLabel/e11bc5eda0e251fd8cf3f4c7a7dc43d2da93da6a/Images/Example.png -------------------------------------------------------------------------------- /Images/paragraph_frames.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezhes/DYLabel/e11bc5eda0e251fd8cf3f4c7a7dc43d2da93da6a/Images/paragraph_frames.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Allison Husain 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 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "DYLabel", 7 | defaultLocalization: "en", 8 | platforms: [ 9 | .iOS(.v9), 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "DYLabel", 15 | targets: ["DYLabel"]) 16 | ], 17 | dependencies: [], 18 | targets: [ 19 | .target( 20 | name: "DYLabel", 21 | path: "Sources" 22 | ) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DYLabel 2 | 3 | ![image](Images/Example.png) 4 | 5 | Superscript, links, and more! 6 | 7 | ![image](Images/paragraph_frames.png) 8 | 9 | First class accessibility support with accurate text framing and paragraph based navigation! 10 | 11 | ## Features 12 | * Over 5.5x faster to size, layout, and draw than UILabel 13 | * Always provides an accurate line height (even with superscript, emoji, etc). This is very important as most others (TTTAttributedLabel, AttributedLabel, DTAttributedTextView, etc) often return inaccurate line heights with complex attributed strings. All, for example, fail in different ways with the demo above. 14 | * Supports VoiceOver (with paragraph navigation) and other accessibility technologies 15 | * Supports hyperlinks (both clicking and press-and-hold) 16 | * Supports link font color customization through `NSForegroundColorAttributeName` 17 | * Supports iOS >=9 18 | 19 | ## Using it 20 | 21 | You have two options. First, you can simply add this repo as a Swift Package Manager package and import the `DYLabel` library. This is recommend so that you receive updates. Alternatively, you may simply copy `Sources/DYLabel.swift` into your project. And...that's it! 22 | 23 | Check out `ViewController.swift` in the demo for a crash-course on how to use DYLabel. 24 | 25 | A few key notes: 26 | 27 | * It is highly recommended that you use the convenience init function `init(attributedText attributedTextIn:NSAttributedString, backgroundColor backgroundColorIn:UIColor?, frame:CGRect)` to ensure that everything the label needs is ready from the get go 28 | * If you are not using the convenience init (i.e. using interface builder) you must set `.attributedText` before `draw(_ rect: CGRect)` is called by the OS. An empty string is acceptable however it may not be nil 29 | * To receive link click callbacks, you must implement DYLinkDelegate and then set the `.dyDelegate` property of labels correctly 30 | 31 | 32 | ## Benchmarks 33 | 34 | There are two expensive phases in the drawing of a label. The first is framing/height calculation and the second is the actual drawing and displaying of text. Both will be tested here using a very complex and large attributed string. 35 | 36 | 37 | ``` 38 | func drawMeasure(_ title: String, view:UIView) { 39 | let startTime = CFAbsoluteTimeGetCurrent() 40 | let iterations = 1000 41 | 42 | UIGraphicsBeginImageContext(self.view!.bounds.size) 43 | let context = UIGraphicsGetCurrentContext()! 44 | for _ in 0..<(iterations) { 45 | autoreleasepool { 46 | context.saveGState() 47 | view.draw(self.view!.bounds) 48 | context.restoreGState() 49 | } 50 | } 51 | 52 | UIGraphicsEndImageContext() 53 | 54 | let timeElapsed = (CFAbsoluteTimeGetCurrent() - startTime) * 1000 55 | print("Draw time for \(iterations) iterations (ms) \(title): \(timeElapsed)") 56 | } 57 | 58 | ... 59 | 60 | measure("DYLabel - height") { 61 | let _ = DYLabel.size(of: s!, width: self.view.frame.width) 62 | } 63 | 64 | measure("TTTAttributedLabel - height") { 65 | //Returns incorrect sizes, TTT is broken! Included just for comparison 66 | let _ = TTTAttributedLabel.sizeThatFitsAttributedString(s!, withConstraints: CGSize.init(width: self.view.frame.width, height: CGFloat.greatestFiniteMagnitude), limitedToNumberOfLines: 0) 67 | } 68 | 69 | measure("UILabel - height") { 70 | let _ = s?.boundingRect(with: CGSize.init(width: self.view.frame.width, height: CGFloat.greatestFiniteMagnitude), options: [NSStringDrawingOptions.usesLineFragmentOrigin, NSStringDrawingOptions.usesFontLeading], context: nil) 71 | } 72 | 73 | do { 74 | let l = DYLabel.init(frame: r) 75 | l.attributedText = s 76 | drawMeasure("DYLabel", view: l) 77 | } 78 | 79 | do { 80 | let l = TTTAttributedLabel.init(frame: r) 81 | l.numberOfLines = 0 82 | l.setText(s) 83 | drawMeasure("TTTAttributedLabel", view: l) 84 | } 85 | 86 | do { 87 | let l = UILabel.init(frame: r) 88 | l.numberOfLines = 0 89 | l.attributedText = s 90 | drawMeasure("UILabel", view: l) 91 | } 92 | ``` 93 | 94 | ``` 95 | Runtime for 1000 iterations (ms) DYLabel - height: 2163.864016532898 96 | Runtime for 1000 iterations (ms) TTTAttributedLabel - height: 1994.5310354232788 97 | Runtime for 1000 iterations (ms) UILabel - height: 8317.120909690857 98 | 99 | 100 | Draw time for 1000 iterations (ms) DYLabel: 2859.1389656066895 101 | Draw time for 1000 iterations (ms) TTTAttributedLabel: 3037.634015083313 102 | Draw time for 1000 iterations (ms) UILabel: 19235.720038414 103 | ``` 104 | 105 | In total: DYLabel took 5023ms, TTT took 5032ms, and UILabel took 27552ms. -------------------------------------------------------------------------------- /Sources/DYLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DYLabel.swift 3 | // Dystopia 4 | // 5 | // Created by Allison Husain on 8/25/18. 6 | // Copyright © 2018 Allison Husain. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | /// A representation of plain text being drawn by DYLabel 13 | public class DYText { 14 | public var bounds:CGRect 15 | public var range:CFRange 16 | public init(bounds boundsIn:CGRect, range rangeIn:CFRange) { 17 | bounds = boundsIn 18 | range = rangeIn 19 | } 20 | } 21 | 22 | 23 | /// A representation of a link being drawn by DYLabel 24 | public class DYLink:DYText { 25 | public var url:URL 26 | public init(bounds boundsIn:CGRect, url urlIn:URL, range rangeIn:CFRange) { 27 | url = urlIn 28 | super.init(bounds: boundsIn, range: rangeIn) 29 | } 30 | } 31 | 32 | /// A modified version of CATiledLayer which disables fade 33 | class CAFastFadeTileLayer:CATiledLayer { 34 | override class func fadeDuration() -> CFTimeInterval { 35 | return 0.0 // Normally it’s 0.25 36 | } 37 | } 38 | 39 | /// An internal data structure used for tracking and interacting with this label by Voice Over 40 | class DYAccessibilityElement:UIAccessibilityElement { 41 | weak var superview:UIView? 42 | var boundingRect:CGRect 43 | 44 | 45 | /// Due to the way that iOS translates touches performed by Voice Over, it is sometimes neccesary to include additional information so that the touch can be correctly interpreted. Link for the element is included because the touch may "miss" the link if clicked using VO because VO clicks directly in the center of the rect rather than the origin 46 | var link:DYLink? 47 | 48 | 49 | init(superview superViewIn:UIView, boundingRect bound:CGRect, container:Any) { 50 | superview = superViewIn 51 | boundingRect = bound 52 | 53 | super.init(accessibilityContainer: container) 54 | } 55 | 56 | 57 | /// Fix a bizarre bug? my issue? where the calculated frame is wrong later. YOU MUST SET BOUNDING RECT and superView 58 | override var accessibilityFrame: CGRect { 59 | get { 60 | if let superview = superview { 61 | return UIAccessibility.convertToScreenCoordinates(boundingRect, in: superview) 62 | }else { 63 | return CGRect.zero 64 | } 65 | } 66 | set{} 67 | } 68 | } 69 | 70 | 71 | 72 | /// A custom, high performance label which provides both accessibility and FAST and ACCURATE height calculation 73 | public class DYLabel: UIView { 74 | internal var __accessibilityElements:[DYAccessibilityElement]? = nil 75 | public var __enableFrameDebugMode = false 76 | 77 | internal var links:[DYLink]? = nil 78 | internal var text:[DYText]? = nil 79 | 80 | private let tapGesture = UITapGestureRecognizer() 81 | private let holdGesture = UILongPressGestureRecognizer() 82 | 83 | 84 | /// Should the accessibility label be split into paragraphs for plain text? Regardless of this setting, frames will start and stop for clickable links. (in other words, this is a "read the entire thing in one go" or "read by paragraph" setting) 85 | public var shouldGenerateAccessibilityFramesForParagraphs:Bool = true 86 | 87 | public weak var dyDelegate:DYLinkDelegate? 88 | 89 | 90 | /// This is the queue used for reading and writing all __variables! DO NOT MODIFY THESE VALUES OUTSIDE OF THIS QUEUE! 91 | internal let dataUpdateQueue:DispatchQueue? = DispatchQueue(label:"DYLabel-data-update-queue",qos:.userInteractive) 92 | 93 | //MARK: Tiling 94 | //This code enables the view to be drawn in the background, in tiles. Huge performance win especially on large bodies of text 95 | public override class var layerClass: AnyClass { 96 | return CAFastFadeTileLayer.self 97 | } 98 | 99 | 100 | var tiledLayer: CAFastFadeTileLayer { 101 | return self.layer as! CAFastFadeTileLayer 102 | } 103 | 104 | //ONLY ACCESS THESE VARIABLES ON THE `dataUpdateQueue`!! 105 | internal var __attributedText:NSAttributedString? 106 | internal var mainThreadAttributedText:NSAttributedString? 107 | internal var __frameSetter:CTFramesetter? 108 | 109 | /// Attributed text to draw 110 | /// Warning!! This is not guaranteed to be exactly the text that's currently display but instead what will be drawn 111 | public var attributedText:NSAttributedString? { 112 | get { 113 | return mainThreadAttributedText 114 | } 115 | 116 | set (input) { 117 | if mainThreadAttributedText == input { 118 | //don't bother redrawing/invalidating all our frames if the text is exactly the same 119 | return 120 | } 121 | mainThreadAttributedText = input 122 | dataUpdateQueue?.async { 123 | self.__attributedText = input 124 | //invalidate the frame as we've reset 125 | self.__frameSetter = nil 126 | self.__frameSetterFrame = nil 127 | } 128 | self.setNeedsDisplay() 129 | } 130 | } 131 | 132 | internal var __backgroundColor:UIColor? = UIColor.white 133 | 134 | /// The background color, non-transparent. This is guaranteed to be up to date 135 | public override var backgroundColor: UIColor? { 136 | set (color) { 137 | super.backgroundColor = color 138 | dataUpdateQueue?.async { 139 | self.__backgroundColor = color?.copy() as? UIColor 140 | } 141 | } 142 | 143 | get { 144 | return super.backgroundColor 145 | } 146 | } 147 | 148 | internal var __frame:CGRect = CGRect.zero 149 | internal var __frameSetterFrame:CTFrame? 150 | /// The frame. This is guaranteed to be up to date however it is not guaranteed that this value will be the actual drawn size as the frame is redrawn in the background. This may seem like an error however it is important for layout code that the frame return what it *will be* very soon rather (after a background process) than what it currently is 151 | public override var frame: CGRect { 152 | set (frameIn) { 153 | super.frame = frameIn 154 | let screenHeight = UIScreen.main.bounds.height 155 | let height = min(frameIn.height, screenHeight) 156 | tiledLayer.tileSize = CGSize.init(width: frameIn.width, height: height) 157 | 158 | dataUpdateQueue?.async { 159 | self.__frame = frameIn 160 | 161 | //invalidate old frame 162 | self.__frameSetterFrame = nil 163 | } 164 | } 165 | 166 | get { 167 | return super.frame 168 | } 169 | } 170 | 171 | 172 | //MARK: Life cycle 173 | public override init(frame: CGRect) { 174 | super.init(frame: frame) 175 | setupViews() 176 | } 177 | 178 | public required init?(coder aDecoder: NSCoder) { 179 | super.init(coder: aDecoder) 180 | setupViews() 181 | } 182 | /// Create a DTLabel, the suggested way 183 | /// 184 | /// - Parameters: 185 | /// - attributedTextIn: Attributed string to display 186 | /// - backgroundColorIn: The background color to use. If a background color is set, blending can be disabled which gives a performance boost 187 | /// - frame: The frame 188 | public convenience init(attributedText attributedTextIn:NSAttributedString, backgroundColor backgroundColorIn:UIColor?, frame:CGRect) { 189 | self.init(frame: frame) 190 | self.attributedText = attributedTextIn 191 | 192 | backgroundColor = backgroundColorIn 193 | setupViews() 194 | } 195 | 196 | func setupViews() { 197 | tiledLayer.levelsOfDetail = 1 198 | tiledLayer.contentsScale = 1 199 | 200 | isUserInteractionEnabled = true 201 | tapGesture.addTarget(self, action: #selector(DYLabel.labelTapped(_:))) 202 | holdGesture.addTarget(self, action: #selector(DYLabel.labelHeld(_:))) 203 | addGestureRecognizer(tapGesture) 204 | addGestureRecognizer(holdGesture) 205 | if (backgroundColor == nil) { 206 | self.isOpaque = false 207 | } 208 | } 209 | 210 | //MARK: Interaction 211 | @objc func labelTapped(_ gesture: UITapGestureRecognizer) { 212 | if let link = linkAt(point: gesture.location(in: gesture.view)) { 213 | dyDelegate?.didClickLink(label: self, link: link) 214 | } 215 | } 216 | 217 | @objc func labelHeld(_ gesture:UILongPressGestureRecognizer) { 218 | //cancel the touch, for whatever reason we keep getting events after we present another view on top 219 | gesture.isEnabled.toggle() 220 | if let link = linkAt(point: gesture.location(in: gesture.view)) { 221 | dyDelegate?.didLongPressLink(label: self, link: link) 222 | } 223 | } 224 | 225 | 226 | func newAccessibilityElement(frame:CGRect,label:String,isPlainText:Bool,linkItem:DYLink? = nil) -> DYAccessibilityElement { 227 | let accessibilityElement = DYAccessibilityElement.init(superview: self, boundingRect: frame, container: self) 228 | accessibilityElement.accessibilityValue = label 229 | accessibilityElement.accessibilityTraits = isPlainText ? UIAccessibilityTraits.staticText : UIAccessibilityTraits.link 230 | accessibilityElement.link = linkItem 231 | 232 | 233 | return accessibilityElement 234 | } 235 | 236 | 237 | /// Calculate the frames of plain text, links, and accessibility elements (if needed) 238 | /// THIS IS AN EXPENSIVE OPERATION, especially if voice over is running. This method will attempt to skip itself automatically. If new data must be feteched, call `invalidate()` 239 | func fetchAttributedRectsIfNeeded() { 240 | dataUpdateQueue?.sync { 241 | if links == nil || ( UIAccessibility.isVoiceOverRunning && __accessibilityElements == nil) || self.__enableFrameDebugMode { 242 | guard let attributedText = attributedText else { return } 243 | generateCoreTextCachesIfNeeded() 244 | 245 | let renderBounds = self.bounds 246 | if renderBounds.size.height == 0 { 247 | return 248 | } 249 | 250 | UIGraphicsBeginImageContext(self.bounds.size) 251 | guard let context = UIGraphicsGetCurrentContext() else { return } 252 | drawText(attributedText: attributedText, shouldDraw: false, context: context, layoutRect: renderBounds, shouldStoreFrames: true) 253 | UIGraphicsEndImageContext() 254 | 255 | //Accessibility element generation 256 | // 257 | /// WARNING! THIS SUBROUTINE IS VERY EXPENSIVE! It compacts links and texts into a single array, sorts it (as the links and text arrays are not exactly "sorted"), and then generates new accessibility objects) 258 | // 259 | 260 | var items:[DYText] = links! + text! 261 | items.sort { (a, b) -> Bool in 262 | return a.range.location < b.range.location 263 | } 264 | 265 | let textContent = attributedText.string as NSString 266 | var lastIsText:Bool = (items.first is DYLink) == false 267 | var frames:[CGRect] = [] 268 | var frameLabel = "" 269 | var lastLinkItem:DYLink? = items.first as? DYLink 270 | var nextItemIsNewParagraph:Bool = false 271 | 272 | for item in items { 273 | let currentIsText = (item is DYLink) == false 274 | //if shouldDYLabelParseIntoParagraphs is false, short-circuit the paragraph split mode so the entire thing (except links) is read in one go 275 | if lastIsText != currentIsText || (nextItemIsNewParagraph && shouldGenerateAccessibilityFramesForParagraphs) { 276 | nextItemIsNewParagraph = false 277 | //We've changed frames, commit accessibility element 278 | if var finalRect = frames.first { 279 | for rect in frames { 280 | finalRect = finalRect.union(rect) 281 | } 282 | if frameLabel.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false { 283 | __accessibilityElements?.append(newAccessibilityElement(frame: finalRect, label: frameLabel, isPlainText: lastIsText, linkItem: lastLinkItem)) 284 | } 285 | } 286 | 287 | lastIsText = currentIsText 288 | lastLinkItem = item as? DYLink 289 | frameLabel = "" 290 | frames = [] 291 | } 292 | 293 | nextItemIsNewParagraph = textContent.substring(with: NSRange.init(location: item.range.location, length: item.range.length)).contains("\n") 294 | frameLabel.append(textContent.substring(with: NSRange.init(location: item.range.location, length: item.range.length))) 295 | frames.append(item.bounds) 296 | } 297 | 298 | if frameLabel.isEmpty == false { 299 | //Commit all remaining 300 | if var finalRect = frames.first { 301 | for rect in frames { 302 | finalRect = finalRect.union(rect) 303 | } 304 | if frameLabel.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false { 305 | __accessibilityElements?.append(newAccessibilityElement(frame: finalRect, label: frameLabel, isPlainText: lastIsText, linkItem: lastLinkItem)) 306 | } 307 | } 308 | } 309 | } 310 | } 311 | 312 | } 313 | 314 | 315 | /// Get the link (if any) at a given point. Useful for hit detection. 316 | /// Note, this method will be slightly dishonest and return links that are not *exactly* at the point if VoiceOver is running. This is to patch beavhior of the VO engine's clicking 317 | /// 318 | /// - Parameter point: The point, relative to us (x:0, y:0 is top left of textview) 319 | /// - Returns: A link if there is one. 320 | func linkAt(point:CGPoint) -> DYLink? { 321 | fetchAttributedRectsIfNeeded() 322 | for link in links! { 323 | if link.bounds.contains(point) { 324 | return link 325 | } 326 | } 327 | 328 | //Voiceover doesn't always click "right" on the link but instead on the center of the rect. If VO is running, relax the hit box 329 | if (UIAccessibility.isVoiceOverRunning) { 330 | for accessibilityItem in __accessibilityElements! { 331 | if let link = accessibilityItem.link, accessibilityItem.boundingRect.contains(point) { 332 | return link 333 | } 334 | } 335 | } 336 | return nil 337 | } 338 | 339 | //MARK: Accessibility 340 | public override var isAccessibilityElement: Bool { 341 | get { 342 | return false 343 | } 344 | set {} 345 | } 346 | 347 | public override var accessibilityFrame: CGRect { 348 | get { 349 | if let superview = superview { 350 | return UIAccessibility.convertToScreenCoordinates(self.bounds, in: superview) 351 | }else { 352 | return CGRect.zero 353 | } 354 | } 355 | set {} 356 | } 357 | 358 | public override func accessibilityElementCount() -> Int { 359 | fetchAttributedRectsIfNeeded() 360 | return __accessibilityElements?.count ?? 0 361 | } 362 | 363 | public override func accessibilityElement(at index: Int) -> Any? { 364 | if (index >= __accessibilityElements?.count ?? 0) { 365 | return nil 366 | } 367 | fetchAttributedRectsIfNeeded() 368 | return __accessibilityElements?[index] 369 | } 370 | 371 | public override func index(ofAccessibilityElement element: Any) -> Int { 372 | fetchAttributedRectsIfNeeded() 373 | guard let item = element as? DYAccessibilityElement else { 374 | return -1 375 | } 376 | return __accessibilityElements?.firstIndex(of: item) ?? -1 377 | } 378 | 379 | public override var accessibilityElements: [Any]? { 380 | get { 381 | fetchAttributedRectsIfNeeded() 382 | return __accessibilityElements 383 | } 384 | set{} 385 | } 386 | 387 | //MARK: Rendering, sizing 388 | 389 | /// Converts a CT rect (0,0 is bottom left) to UI rect (0,0 is top left) 390 | /// 391 | /// - Parameter rect: In rect, CT style 392 | /// - Returns: UI style 393 | func convertCTRectToUI(rect:CGRect) -> CGRect { 394 | return CGRect.init(x: rect.minX, y: self.bounds.size.height - rect.maxY, width: rect.size.width, height: rect.size.height) 395 | } 396 | 397 | 398 | /// Get the CoreText relative frame for a given CTRun. This method works around an iOS <=10 CoreText bug in CTRunGetImageBounds 399 | /// https://stackoverflow.com/q/52030633/1166266 400 | /// 401 | /// - Parameters: 402 | /// - run: The run 403 | /// - context: Context, used by CTRunGetImageBounds 404 | /// - Returns: A tight fitting, CT rect that fits around the run 405 | func getCTRectFor(run:CTRun, line:CTLine,origin:CGPoint,context:CGContext) -> CGRect { 406 | let aP = UnsafeMutablePointer.allocate(capacity: 1) 407 | let dP = UnsafeMutablePointer.allocate(capacity: 1) 408 | let lP = UnsafeMutablePointer.allocate(capacity: 1) 409 | defer { 410 | aP.deallocate() 411 | dP.deallocate() 412 | lP.deallocate() 413 | } 414 | let width = CTRunGetTypographicBounds(run, CFRange.init(location: 0, length: 0), aP, dP, lP) 415 | 416 | let q = CTRunGetStringRange(run) 417 | let xOffset = CTLineGetOffsetForStringIndex(line, q.location, nil) 418 | var boundT = CGRect.zero 419 | boundT.size.width = CGFloat(width) 420 | boundT.size.height = aP.pointee + dP.pointee 421 | boundT.origin.x = origin.x + CGFloat(xOffset) 422 | boundT.origin.y = origin.y 423 | boundT.origin.y -= dP.pointee 424 | 425 | return boundT 426 | } 427 | 428 | public override func setNeedsLayout() { 429 | super.setNeedsLayout() 430 | invalidate() 431 | } 432 | 433 | public override func setNeedsDisplay() { 434 | super.setNeedsDisplay() 435 | invalidate() 436 | } 437 | 438 | 439 | /// Invalidate link, text, and accessibility caches 440 | func invalidate() { 441 | links = nil 442 | text = nil 443 | __accessibilityElements = nil 444 | } 445 | 446 | public override func layoutSubviews() { 447 | setNeedsDisplay() 448 | } 449 | 450 | 451 | /// Generate the framesetter and framesetter frame. CALL THIS ONLY FROM dataUpdateQueue!!! 452 | func generateCoreTextCachesIfNeeded() { 453 | guard let drawingAttributed = self.__attributedText else {return} 454 | if self.__frameSetter == nil { 455 | self.__frameSetter = CTFramesetterCreateWithAttributedString(drawingAttributed) 456 | } 457 | 458 | if self.__frameSetterFrame == nil { 459 | let path = CGPath(rect: self.__frame, transform: nil) 460 | self.__frameSetterFrame = CTFramesetterCreateFrame(self.__frameSetter!, CFRangeMake(0, 0), path, nil) 461 | } 462 | } 463 | 464 | public override func draw(_ rect: CGRect) { 465 | //do not call super.draw(rect), not required 466 | guard let ctx = UIGraphicsGetCurrentContext() else { 467 | fatalError() 468 | } 469 | 470 | dataUpdateQueue?.sync { 471 | //explicitly capture self otherwise we can get some weird memory issues if we are deallocated 472 | 473 | if self.__attributedText == nil { 474 | return 475 | } 476 | generateCoreTextCachesIfNeeded() 477 | //Blank the cell completely before drawing. Prevent empty grey line from being drawn 478 | ctx.setFillColor((self.__backgroundColor ?? UIColor.white).cgColor) 479 | ctx.fill(CGRect.init(x: -20, y: -20, width: self.__frame.size.width + 40, height: self.__frame.height + 40)) 480 | 481 | ctx.textMatrix = CGAffineTransform.identity 482 | ctx.translateBy(x: 0, y: self.__frame.size.height) 483 | ctx.scaleBy(x: 1.0, y: -1.0) 484 | if (self.__attributedText != nil) { 485 | self.drawText(attributedText: self.__attributedText!, shouldDraw: true, context: ctx, layoutRect: self.__frame, partialRect: rect, shouldStoreFrames: false) 486 | } 487 | } 488 | } 489 | 490 | 491 | /// Draw the text or don't and just calculate the height 492 | /// 493 | /// - Parameters: 494 | /// - attributedText: Text to draw 495 | /// - shouldDraw: If we should really draw it or just calculate heights 496 | /// - context: Context to draw into or to pretend to draw in 497 | /// - layoutRect: The layout size, or the total frame size 498 | /// - partialRect: (REQUIRED WHEN DRAWING) the portion of text to actually render 499 | /// - shouldStoreFrames: If the frames of various items (links, text, accessibilty elements) should be generated 500 | func drawText(attributedText: NSAttributedString, shouldDraw:Bool, context:CGContext?, layoutRect:CGRect,partialRect:CGRect? = nil, shouldStoreFrames:Bool) { 501 | let iOS13BetaCursorBaselineMoveScalar:CGFloat 502 | let preiOS15StrikethroughPatch:Bool 503 | if #available(iOS 15.0, *) { 504 | iOS13BetaCursorBaselineMoveScalar = 0 505 | preiOS15StrikethroughPatch = false 506 | } else { 507 | //on iOS <15, strikethrough was ignored by attributed strings. 508 | //Later versions handle this correctly, but to get functional striking we need this 509 | preiOS15StrikethroughPatch = true 510 | if #available(iOS 13.0, *) { 511 | //on iOS 13-14, it seems like baseline adjustments are no longer enabled by default 512 | iOS13BetaCursorBaselineMoveScalar = 1 513 | } else { 514 | iOS13BetaCursorBaselineMoveScalar = 0 515 | } 516 | } 517 | 518 | 519 | guard let frame = self.__frameSetterFrame else {return} 520 | if (shouldStoreFrames) { 521 | //Reset link, text storage arrays 522 | links = [] 523 | text = [] 524 | __accessibilityElements = [] 525 | } 526 | 527 | //Fetch our lines, bridging to swift from CFArray 528 | let lines:CFArray = CTFrameGetLines(frame) 529 | let lineCount = CFArrayGetCount(lines) 530 | 531 | //Get the line origin coordinates. These are used for calculating stock line height (w/o baseline modifications) 532 | var lineOrigins = [CGPoint](repeating: CGPoint.zero, count: lineCount) 533 | CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins); 534 | 535 | //Since we're starting from the bottom of the container we need get our bottom offset/padding (so text isn't slammed to the bottom or cut off) 536 | var ascent:CGFloat = 0 537 | var descent:CGFloat = 0 538 | var leading:CGFloat = 0 539 | if lineCount > 0 { 540 | let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, lineCount - 1), to: CTLine.self) 541 | CTLineGetTypographicBounds(line, &ascent, &descent, &leading) 542 | } 543 | 544 | //This variable holds the current draw position, relative to CT origin of the bottom left 545 | var drawYPositionFromOrigin:CGFloat = descent 546 | 547 | //Again, draw the lines in reverse so we don't need look ahead 548 | for lineIndex in (0.. 0 ? lineOrigins[lineIndex - 1].y: layoutRect.height 551 | let currentLineHeight = lastLinePosition - lineOrigins[lineIndex].y 552 | //Throughout the loop below this variable will be updated to the tallest value for the current line 553 | var maxLineHeight:CGFloat = currentLineHeight 554 | 555 | //Grab the current run glyph. This is used for attributed string interop 556 | let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, lineIndex), to: CTLine.self) 557 | let glyphRuns = CTLineGetGlyphRuns(line) 558 | let glyphRunsCount = CFArrayGetCount(glyphRuns) 559 | 560 | for runIndex in 0.. drawYPositionFromOrigin || drawYPositionFromOrigin > pBottomCorrected 586 | //If we're in any of our ranges, draw! 587 | if (minCondition || maxCondition || centerCondition) { 588 | CTRunDraw(run, context!, CFRangeMake(0, 0)) 589 | } 590 | 591 | if preiOS15StrikethroughPatch, let _ = attributesAtPosition.object(forKey: NSAttributedString.Key.strikethroughStyle) { 592 | if ctRect == nil { 593 | ctRect = getCTRectFor(run: run, line: line, origin: context!.textPosition, context: context!) 594 | } 595 | 596 | let strikeYPosition = ctRect!.minY + ctRect!.height/2 597 | context?.move(to: CGPoint.init(x: ctRect!.minX, y: strikeYPosition)) 598 | context?.addLine(to: CGPoint.init(x: ctRect!.maxX, y: strikeYPosition)) 599 | context?.strokePath() 600 | } 601 | 602 | if let _ = attributesAtPosition.object(forKey: DYLabel.Key.FullLineUnderLine) { 603 | if ctRect == nil { 604 | ctRect = getCTRectFor(run: run, line: line, origin: context!.textPosition, context: context!) 605 | } 606 | 607 | if let underlineColor = attributesAtPosition.object(forKey: DYLabel.Key.FullLineUnderLineColor) as? UIColor { 608 | context?.setStrokeColor(underlineColor.cgColor) 609 | } 610 | 611 | let strikeYPosition = ctRect!.minY 612 | let scale = UIScreen.main.scale 613 | 614 | let width = 1 / scale 615 | let offset = width / 2 616 | 617 | let yFinal:CGFloat = max(strikeYPosition - offset, width) 618 | context?.setLineWidth(width) 619 | 620 | context?.beginPath() 621 | 622 | context?.move(to: CGPoint.init(x: 0, y: yFinal)) 623 | context?.addLine(to: CGPoint.init(x: layoutRect.width, y: yFinal)) 624 | context?.strokePath() 625 | } 626 | } 627 | } 628 | 629 | if shouldStoreFrames { 630 | if ctRect == nil { 631 | ctRect = getCTRectFor(run: run, line: line, origin: context!.textPosition, context: context!) 632 | } 633 | 634 | //Extract frames *after* moving the draw head 635 | let runBounds = convertCTRectToUI(rect: ctRect!) 636 | var item:DYText? = nil 637 | 638 | if let url = attributesAtPosition.object(forKey: NSAttributedString.Key.link) { 639 | var link:DYLink? = nil 640 | if let url = url as? URL { 641 | link = DYLink.init(bounds: runBounds, url: url, range: runRange) 642 | links?.append(link!) 643 | }else if let url = URL.init(string: url as? String ?? "") { 644 | link = DYLink.init(bounds: runBounds, url: url, range: runRange) 645 | links?.append(link!) 646 | } 647 | item = link 648 | 649 | }else { 650 | item = DYText.init(bounds: runBounds, range: runRange) 651 | text?.append(item!) 652 | } 653 | 654 | } 655 | 656 | //Check if this glyph run is tallest, and move it if it is 657 | maxLineHeight = max(currentLineHeight + baselineAdjustment, maxLineHeight) 658 | 659 | } 660 | //Move our position because we've completed the drawing of the line which is at most `maxLineHeight` 661 | drawYPositionFromOrigin += maxLineHeight 662 | } 663 | return 664 | } 665 | 666 | /// Calculate the height if it were drawn using `drawText` 667 | /// Uses the same code as drawText except it doesn't draw. 668 | /// 669 | /// - Parameters: 670 | /// - attributedText: The text to calculate the height of 671 | /// - width: The constraining width 672 | /// - estimationHeight: The maximum (guessed) height of this text. If the text is taller than this, it will take multiple attempts to calculate (height doubles). There does not appear to be a performance drop for larger sizes, however the if this size is too large, older devices/iOS versions will yield invalid/too small heights due to CoreText weirdness. 673 | public static func size(of attributedText:NSAttributedString, width:CGFloat, estimationHeight:CGFloat = 30000) -> CGSize { 674 | let framesetter = CTFramesetterCreateWithAttributedString(attributedText) 675 | let textRect = CGRect.init(x: 0, y: 0, width: width, height: estimationHeight) 676 | let path = CGPath(rect: textRect, transform: nil) 677 | let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil) 678 | 679 | //Fetch our lines, bridging to swift from CFArray 680 | let lines:CFArray = CTFrameGetLines(frame) //as [AnyObject] 681 | let lineCount = CFArrayGetCount(lines) 682 | 683 | //Get the line origin coordinates. These are used for calculating stock line height (w/o baseline modifications) 684 | var lineOrigins = [CGPoint](repeating: CGPoint.zero, count: lineCount) 685 | CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins); 686 | 687 | //Since we're starting from the bottom of the container we need get our bottom offset/padding (so text isn't slammed to the bottom or cut off) 688 | var ascent:CGFloat = 0 689 | var descent:CGFloat = 0 690 | var leading:CGFloat = 0 691 | if lineCount > 0 { 692 | let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, lineCount - 1), to: CTLine.self) 693 | let lastLineRange = CTLineGetStringRange(line) 694 | let lastDrawnLength = lastLineRange.location + lastLineRange.length 695 | if lastDrawnLength != attributedText.length { 696 | //Estimation size is too small, try again! 697 | let newEstimationHeight = estimationHeight * 2 698 | print("Estimation size (\(estimationHeight)) too small by \(attributedText.length - lastDrawnLength) characters. Retrying with \(newEstimationHeight)!") 699 | return size(of: attributedText, width: width, estimationHeight: newEstimationHeight) 700 | } 701 | 702 | 703 | CTLineGetTypographicBounds(line, &ascent, &descent, &leading) 704 | } 705 | 706 | //This variable holds the current draw position, relative to CT origin of the bottom left 707 | var drawYPositionFromOrigin:CGFloat = descent 708 | 709 | //Again, draw the lines in reverse so we don't need look ahead 710 | for lineIndex in (0.. 0 ? lineOrigins[lineIndex - 1].y: textRect.height 713 | let currentLineHeight = lastLinePosition - lineOrigins[lineIndex].y 714 | //Throughout the loop below this variable will be updated to the tallest value for the current line 715 | var maxLineHeight:CGFloat = currentLineHeight 716 | 717 | //Grab the current run glyph. This is used for attributed string interoperability 718 | let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, lineIndex), to: CTLine.self) 719 | let glyphRuns = CTLineGetGlyphRuns(line) 720 | let glyphRunsCount = CFArrayGetCount(glyphRuns) 721 | for runIndex in 0..