├── .gitignore ├── .swift-version ├── Config ├── app.json ├── crypto.json ├── droplet.json └── server.json ├── Package.pins ├── Package.resolved ├── Package.swift ├── Procfile ├── Public ├── images │ ├── Card.png │ ├── Icon_120x120.png │ ├── Icon_152x152.png │ ├── Icon_167x167.png │ ├── Icon_16x16.png │ ├── Icon_180x180.png │ ├── Icon_32x32.png │ ├── Icon_523x523.png │ ├── Icon_550x550.png │ ├── Icon_96x96.png │ └── wtf-logo.svg └── styles │ └── main.css ├── README.md ├── Resources └── Views │ ├── about.leaf │ ├── base.leaf │ ├── hero.leaf │ ├── input.leaf │ ├── output.leaf │ └── page.leaf ├── Sources ├── App │ ├── Controllers │ │ └── OutputController.swift │ ├── Models │ │ ├── Errors │ │ │ └── InvalidConstraintError.swift │ │ ├── ModelRepresentations │ │ │ ├── HtmlDescriptions.swift │ │ │ ├── LeafNodes.swift │ │ │ └── NumberFormatter.swift │ │ ├── Parser Models │ │ │ ├── Attribute.swift │ │ │ ├── Color.swift │ │ │ ├── Constant.swift │ │ │ ├── Constraint.swift │ │ │ ├── ConstraintGroup.swift │ │ │ ├── Footnote.swift │ │ │ ├── Instance.swift │ │ │ └── Multiplier.swift │ │ └── Requests │ │ │ └── ConstraintLogRequest.swift │ ├── Parsing │ │ ├── ConstraintsParser+AutoLayoutPrimitives.swift │ │ ├── ConstraintsParser+EquationConstraint.swift │ │ ├── ConstraintsParser+Examples.swift │ │ ├── ConstraintsParser+Info.swift │ │ ├── ConstraintsParser+Instances.swift │ │ ├── ConstraintsParser+Primitives.swift │ │ ├── ConstraintsParser+VFLConstraint.swift │ │ ├── ConstraintsParser.swift │ │ └── Partials │ │ │ ├── AnonymousConstraint.swift │ │ │ └── PartialInstance.swift │ ├── Routes.swift │ ├── SwiftExtensions │ │ ├── CharacterExtensions.swift │ │ ├── CharacterSetExtensions.swift │ │ └── StringExtensions.swift │ ├── app.swift │ ├── boot.swift │ └── configure.swift └── Run │ └── main.swift ├── TODO.md ├── Tests ├── AppTests │ ├── Application+Testing.swift │ ├── ColorTests.swift │ ├── Inputs │ │ ├── Input+Custom.swift │ │ ├── Input+Error.swift │ │ ├── Input+GitHubIssues.swift │ │ ├── Input+Papertrail.swift │ │ └── Input+StackOverflow.swift │ ├── ParserTests.swift │ ├── ViewTests.swift │ └── __Snapshots__ │ │ ├── ParserTests │ │ ├── testCustomInputs.1.json │ │ ├── testCustomInputs.2.json │ │ ├── testCustomInputs.3.json │ │ ├── testCustomInputs.4.json │ │ ├── testCustomInputs.5.json │ │ ├── testCustomInputs.6.json │ │ ├── testCustomInputs.7.json │ │ ├── testCustomInputs.8.json │ │ ├── testGitHubIssues.1.json │ │ ├── testGitHubIssues.2.json │ │ ├── testGitHubIssues.3.json │ │ ├── testGitHubIssues.4.json │ │ ├── testGitHubIssues.5.json │ │ ├── testGitHubIssues.6.json │ │ ├── testGitHubIssues.7.json │ │ ├── testGitHubIssues.8.json │ │ ├── testGitHubIssues.9.json │ │ ├── testStackOverflowInputs.1.json │ │ ├── testStackOverflowInputs.2.json │ │ ├── testStackOverflowInputs.3.json │ │ └── testStackOverflowInputs.4.json │ │ └── ViewTests │ │ ├── testAbout.1.txt │ │ ├── testHome.1.txt │ │ ├── testPostError.1.txt │ │ ├── testPostError.2.txt │ │ ├── testPostOutput.1.txt │ │ ├── testPostOutput.2.txt │ │ ├── testPostOutput.3.txt │ │ ├── testPostOutput.4.txt │ │ ├── testPostOutput.5.txt │ │ ├── testPostOutput.6.txt │ │ ├── testPostOutput.7.txt │ │ ├── testPostOutput.8.txt │ │ ├── testPostOutput.9.txt │ │ ├── testQueryOutput.1.txt │ │ ├── testQueryOutput.2.txt │ │ ├── testQueryOutput.3.txt │ │ ├── testQueryOutput.4.txt │ │ ├── testQueryOutput.5.txt │ │ ├── testQueryOutput.6.txt │ │ ├── testQueryOutput.7.txt │ │ └── testQueryOutput.8.txt └── LinuxMain.swift ├── circle.yml └── license /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ## MacOS ## 3 | .DS_Store 4 | 5 | ## Xcode ## 6 | DerivedData/ 7 | xcuserdata 8 | *.xcodeproj 9 | 10 | ## Swift Package Manager ## 11 | Packages 12 | .swiftpm 13 | .build 14 | 15 | ### Vapor ### 16 | Config/secrets 17 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.1 2 | -------------------------------------------------------------------------------- /Config/app.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /Config/crypto.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": { 3 | "method": "sha256", 4 | "encoding": "hex" 5 | }, 6 | 7 | "cipher": { 8 | "method": "aes256", 9 | "encoding": "base64", 10 | "key": "ACm9cvzBKVLbc3+yOhBoDnXIbPMGQ0M/GyvK/fAOmBQ=" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Config/droplet.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "The type of server to use for handling requests.", 3 | "//": "engine: Vapor's blazing fast Engine HTTP server.", 4 | "server": "engine", 5 | 6 | "//": "The type of client to use for requesting data from other services.", 7 | "//": "engine: Vapor's blazing fast Engine HTTP client.", 8 | "//": "foundation: A wrapper around Foundation's URLSession.", 9 | "client": "engine", 10 | 11 | "//": "The type of console to use for displaying information and prompting input.", 12 | "//": "terminal: Vapor's default terminal console.", 13 | "console": "terminal", 14 | 15 | "//": "The type of logger to use for recording logs, warnings, errors, etc.", 16 | "//": "console: Vapor's default logger sends logs directly to the chosen console.", 17 | "log": "console", 18 | 19 | "//": "The type of hasher to use for hashing messages.", 20 | "//": "crypto: Vapor's default hasher powered by OpenSSL (configure in crypto.json)", 21 | "//": "bcrypt: Performant BCrypt hashing implementation (configure in bcrypt.json)", 22 | "hash": "crypto", 23 | 24 | "//": "The type of cipher to use for encrypting and decrypting messages.", 25 | "//": "crypto: Vapor's default cipher powered by OpenSSL (configure in crypto.json)", 26 | "cipher": "crypto", 27 | 28 | "//": "The type of view renderer that drop.view will use", 29 | "//": "leaf: Pure Swift templating language created for Vapor.", 30 | "//": "static: Simply return the view at the supplied path", 31 | "view": "leaf", 32 | 33 | "//": "Choose which middleware are enabled (and in which order).", 34 | "//": "error: Catches errors thrown in your application and returns a nice response.", 35 | "//": "date: Adds the 'Date' header to HTTP requests.", 36 | "//": "file: Catches 404 errors and checks for files in the Public/ folder", 37 | "middleware": [ 38 | "error", 39 | "date", 40 | "file" 41 | ], 42 | 43 | "//": "Choose which commands this application can run", 44 | "commands": [] 45 | } 46 | -------------------------------------------------------------------------------- /Config/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "The $PORT:8080 call tells the json file to see if there", 3 | "//": "is any value at the 'PORT' environment variable.", 4 | "//": "If there is no value there, it will fallback to '8080'", 5 | "port": "$PORT:8080", 6 | 7 | "host": "0.0.0.0", 8 | 9 | "//": "It's very rare that a server manages its own TLS.", 10 | "//": "More commonly, vapor is served behind a proxy like nginx.", 11 | "securityLayer": "none" 12 | } 13 | -------------------------------------------------------------------------------- /Package.pins: -------------------------------------------------------------------------------- 1 | { 2 | "autoPin": true, 3 | "pins": [ 4 | { 5 | "package": "BCrypt", 6 | "reason": null, 7 | "repositoryURL": "https://github.com/vapor/bcrypt.git", 8 | "version": "1.1.0" 9 | }, 10 | { 11 | "package": "Bits", 12 | "reason": null, 13 | "repositoryURL": "https://github.com/vapor/bits.git", 14 | "version": "1.1.0" 15 | }, 16 | { 17 | "package": "CTLS", 18 | "reason": null, 19 | "repositoryURL": "https://github.com/vapor/ctls.git", 20 | "version": "1.1.1" 21 | }, 22 | { 23 | "package": "Console", 24 | "reason": null, 25 | "repositoryURL": "https://github.com/vapor/console.git", 26 | "version": "2.1.0" 27 | }, 28 | { 29 | "package": "Core", 30 | "reason": null, 31 | "repositoryURL": "https://github.com/vapor/core.git", 32 | "version": "2.1.2" 33 | }, 34 | { 35 | "package": "Crypto", 36 | "reason": null, 37 | "repositoryURL": "https://github.com/vapor/crypto.git", 38 | "version": "2.1.0" 39 | }, 40 | { 41 | "package": "Debugging", 42 | "reason": null, 43 | "repositoryURL": "https://github.com/vapor/debugging.git", 44 | "version": "1.1.0" 45 | }, 46 | { 47 | "package": "Engine", 48 | "reason": null, 49 | "repositoryURL": "https://github.com/vapor/engine.git", 50 | "version": "2.2.0" 51 | }, 52 | { 53 | "package": "JSON", 54 | "reason": null, 55 | "repositoryURL": "https://github.com/vapor/json.git", 56 | "version": "2.0.2" 57 | }, 58 | { 59 | "package": "Leaf", 60 | "reason": null, 61 | "repositoryURL": "https://github.com/vapor/leaf.git", 62 | "version": "2.0.1" 63 | }, 64 | { 65 | "package": "LeafProvider", 66 | "reason": null, 67 | "repositoryURL": "https://github.com/vapor/leaf-provider.git", 68 | "version": "1.1.0" 69 | }, 70 | { 71 | "package": "Multipart", 72 | "reason": null, 73 | "repositoryURL": "https://github.com/vapor/multipart.git", 74 | "version": "2.0.0" 75 | }, 76 | { 77 | "package": "Node", 78 | "reason": null, 79 | "repositoryURL": "https://github.com/vapor/node.git", 80 | "version": "2.0.4" 81 | }, 82 | { 83 | "package": "Random", 84 | "reason": null, 85 | "repositoryURL": "https://github.com/vapor/random.git", 86 | "version": "1.2.0" 87 | }, 88 | { 89 | "package": "Routing", 90 | "reason": null, 91 | "repositoryURL": "https://github.com/vapor/routing.git", 92 | "version": "2.0.1" 93 | }, 94 | { 95 | "package": "Sockets", 96 | "reason": null, 97 | "repositoryURL": "https://github.com/vapor/sockets.git", 98 | "version": "2.1.0" 99 | }, 100 | { 101 | "package": "Sparse", 102 | "reason": null, 103 | "repositoryURL": "https://github.com/johnpatrickmorgan/sparse.git", 104 | "version": "0.2.0" 105 | }, 106 | { 107 | "package": "TLS", 108 | "reason": null, 109 | "repositoryURL": "https://github.com/vapor/tls.git", 110 | "version": "2.1.0" 111 | }, 112 | { 113 | "package": "Vapor", 114 | "reason": null, 115 | "repositoryURL": "https://github.com/vapor/vapor.git", 116 | "version": "2.1.3" 117 | } 118 | ], 119 | "version": 1 120 | } -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Console", 6 | "repositoryURL": "https://github.com/vapor/console.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "74cfbea629d4aac34a97cead2447a6870af1950b", 10 | "version": "3.1.1" 11 | } 12 | }, 13 | { 14 | "package": "Core", 15 | "repositoryURL": "https://github.com/vapor/core.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "1782c550512dc5a43d0bca405e28fc386d932bbf", 19 | "version": "3.10.0" 20 | } 21 | }, 22 | { 23 | "package": "Crypto", 24 | "repositoryURL": "https://github.com/vapor/crypto.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "105c2f875588bf40dd24c00cef3644bf8e327770", 28 | "version": "3.4.1" 29 | } 30 | }, 31 | { 32 | "package": "CwlCatchException", 33 | "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "7cd2f8cacc4d22f21bc0b2309c3b18acf7957b66", 37 | "version": "1.2.0" 38 | } 39 | }, 40 | { 41 | "package": "CwlPreconditionTesting", 42 | "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "c228db5d2ad1b01ebc84435e823e6cca4e3db98b", 46 | "version": "1.2.0" 47 | } 48 | }, 49 | { 50 | "package": "DatabaseKit", 51 | "repositoryURL": "https://github.com/vapor/database-kit.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "8f352c8e66dab301ab9bfef912a01ce1361ba1e4", 55 | "version": "1.3.3" 56 | } 57 | }, 58 | { 59 | "package": "HTTP", 60 | "repositoryURL": "https://github.com/vapor/http.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "0464b715a4b59f54078bcf7a4b424767b03db5a5", 64 | "version": "3.4.0" 65 | } 66 | }, 67 | { 68 | "package": "Leaf", 69 | "repositoryURL": "https://github.com/vapor/leaf.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "d35f54cbac723e673f9bd5078361eea74049c8d7", 73 | "version": "3.0.2" 74 | } 75 | }, 76 | { 77 | "package": "Multipart", 78 | "repositoryURL": "https://github.com/vapor/multipart.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "fb216c5a8ef07dcd90aec8a4155e86c831acce97", 82 | "version": "3.1.3" 83 | } 84 | }, 85 | { 86 | "package": "Nimble", 87 | "repositoryURL": "https://github.com/Quick/Nimble.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "6abeb3f5c03beba2b9e4dbe20886e773b5b629b6", 91 | "version": "8.0.4" 92 | } 93 | }, 94 | { 95 | "package": "Quick", 96 | "repositoryURL": "https://github.com/Quick/Quick.git", 97 | "state": { 98 | "branch": null, 99 | "revision": "33682c2f6230c60614861dfc61df267e11a1602f", 100 | "version": "2.2.0" 101 | } 102 | }, 103 | { 104 | "package": "Routing", 105 | "repositoryURL": "https://github.com/vapor/routing.git", 106 | "state": { 107 | "branch": null, 108 | "revision": "d76f339c9716785e5079af9d7075d28ff7da3d92", 109 | "version": "3.1.0" 110 | } 111 | }, 112 | { 113 | "package": "Service", 114 | "repositoryURL": "https://github.com/vapor/service.git", 115 | "state": { 116 | "branch": null, 117 | "revision": "fa5b5de62bd68bcde9a69933f31319e46c7275fb", 118 | "version": "1.0.2" 119 | } 120 | }, 121 | { 122 | "package": "Sparse", 123 | "repositoryURL": "https://github.com/johnpatrickmorgan/sparse.git", 124 | "state": { 125 | "branch": "master", 126 | "revision": "7c360cc062cf14ee993c6de5b28e04f0892818ac", 127 | "version": null 128 | } 129 | }, 130 | { 131 | "package": "swift-nio", 132 | "repositoryURL": "https://github.com/apple/swift-nio.git", 133 | "state": { 134 | "branch": null, 135 | "revision": "8da5c5a4e6c5084c296b9f39dc54f00be146e0fa", 136 | "version": "1.14.2" 137 | } 138 | }, 139 | { 140 | "package": "swift-nio-ssl", 141 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 142 | "state": { 143 | "branch": null, 144 | "revision": "0f3999f3e3c359cc74480c292644c3419e44a12f", 145 | "version": "1.4.0" 146 | } 147 | }, 148 | { 149 | "package": "swift-nio-ssl-support", 150 | "repositoryURL": "https://github.com/apple/swift-nio-ssl-support.git", 151 | "state": { 152 | "branch": null, 153 | "revision": "c02eec4e0e6d351cd092938cf44195a8e669f555", 154 | "version": "1.0.0" 155 | } 156 | }, 157 | { 158 | "package": "swift-nio-zlib-support", 159 | "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", 160 | "state": { 161 | "branch": null, 162 | "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", 163 | "version": "1.0.0" 164 | } 165 | }, 166 | { 167 | "package": "SnapshotTesting", 168 | "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing.git", 169 | "state": { 170 | "branch": "master", 171 | "revision": "69d029ca721fed87c8a655377716c69ebe985e4f", 172 | "version": null 173 | } 174 | }, 175 | { 176 | "package": "TemplateKit", 177 | "repositoryURL": "https://github.com/vapor/template-kit.git", 178 | "state": { 179 | "branch": null, 180 | "revision": "4370aa99c01fc19cc8272b67bf7204b2d2063680", 181 | "version": "1.5.0" 182 | } 183 | }, 184 | { 185 | "package": "URLEncodedForm", 186 | "repositoryURL": "https://github.com/vapor/url-encoded-form.git", 187 | "state": { 188 | "branch": null, 189 | "revision": "20f68fbe7fac006d4d0617ea4edcba033227359e", 190 | "version": "1.1.0" 191 | } 192 | }, 193 | { 194 | "package": "Validation", 195 | "repositoryURL": "https://github.com/vapor/validation.git", 196 | "state": { 197 | "branch": null, 198 | "revision": "4de213cf319b694e4ce19e5339592601d4dd3ff6", 199 | "version": "2.1.1" 200 | } 201 | }, 202 | { 203 | "package": "Vapor", 204 | "repositoryURL": "https://github.com/vapor/vapor.git", 205 | "state": { 206 | "branch": null, 207 | "revision": "642f3d4d1f0eafad651c85524d0d1c698b55399f", 208 | "version": "3.3.3" 209 | } 210 | }, 211 | { 212 | "package": "WebSocket", 213 | "repositoryURL": "https://github.com/vapor/websocket.git", 214 | "state": { 215 | "branch": null, 216 | "revision": "d85e5b6dce4d04065865f77385fc3324f98178f6", 217 | "version": "1.1.2" 218 | } 219 | } 220 | ] 221 | }, 222 | "version": 1 223 | } 224 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "wtfautolayout", 6 | platforms: [ 7 | .macOS(.v10_13) 8 | ], 9 | dependencies: [ 10 | .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), 11 | .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0"), 12 | .package(url: "https://github.com/johnpatrickmorgan/sparse.git", .branch("master")), 13 | .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", .branch("master")) 14 | ], 15 | targets: [ 16 | .target(name: "App", dependencies: ["Leaf", "Vapor", "Sparse"]), 17 | .target(name: "Run", dependencies: ["App"]), 18 | .testTarget(name: "AppTests", dependencies: ["App", "SnapshotTesting"]) 19 | ] 20 | ) 21 | 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: Run serve --env $ENVIRONMENT --hostname 0.0.0.0 --port $PORT 2 | -------------------------------------------------------------------------------- /Public/images/Card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpatrickmorgan/wtfautolayout/d663a4ff425791f26ee972a1d794de6de307c409/Public/images/Card.png -------------------------------------------------------------------------------- /Public/images/Icon_120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpatrickmorgan/wtfautolayout/d663a4ff425791f26ee972a1d794de6de307c409/Public/images/Icon_120x120.png -------------------------------------------------------------------------------- /Public/images/Icon_152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpatrickmorgan/wtfautolayout/d663a4ff425791f26ee972a1d794de6de307c409/Public/images/Icon_152x152.png -------------------------------------------------------------------------------- /Public/images/Icon_167x167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpatrickmorgan/wtfautolayout/d663a4ff425791f26ee972a1d794de6de307c409/Public/images/Icon_167x167.png -------------------------------------------------------------------------------- /Public/images/Icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpatrickmorgan/wtfautolayout/d663a4ff425791f26ee972a1d794de6de307c409/Public/images/Icon_16x16.png -------------------------------------------------------------------------------- /Public/images/Icon_180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpatrickmorgan/wtfautolayout/d663a4ff425791f26ee972a1d794de6de307c409/Public/images/Icon_180x180.png -------------------------------------------------------------------------------- /Public/images/Icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpatrickmorgan/wtfautolayout/d663a4ff425791f26ee972a1d794de6de307c409/Public/images/Icon_32x32.png -------------------------------------------------------------------------------- /Public/images/Icon_523x523.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpatrickmorgan/wtfautolayout/d663a4ff425791f26ee972a1d794de6de307c409/Public/images/Icon_523x523.png -------------------------------------------------------------------------------- /Public/images/Icon_550x550.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpatrickmorgan/wtfautolayout/d663a4ff425791f26ee972a1d794de6de307c409/Public/images/Icon_550x550.png -------------------------------------------------------------------------------- /Public/images/Icon_96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnpatrickmorgan/wtfautolayout/d663a4ff425791f26ee972a1d794de6de307c409/Public/images/Icon_96x96.png -------------------------------------------------------------------------------- /Public/images/wtf-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Public/styles/main.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | padding: 0; 4 | margin: 0; 5 | height: 100%; 6 | } 7 | 8 | body { 9 | background-color: #f0f0f0; 10 | } 11 | 12 | body, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | p, 19 | a, 20 | .initial { 21 | font-family: 'Fira Sans', Helvetica, sans-serif; 22 | } 23 | 24 | h2 { 25 | font-size: 160%; 26 | font-weight: 200; 27 | line-height: 1.1; 28 | } 29 | 30 | h2 strong { 31 | font-weight: 300; 32 | } 33 | 34 | p { 35 | font-weight: 300; 36 | } 37 | 38 | p a { 39 | font-weight: 400; 40 | } 41 | 42 | code { 43 | font-family: 'Fira Mono', sans-serif; 44 | font-size: 90%; 45 | background: #F6F6F6; 46 | border-style: solid; 47 | border-color: #EEE; 48 | border-width: 1px; 49 | padding: 2px; 50 | font-weight: 300; 51 | } 52 | 53 | span { 54 | display: inline-block; 55 | vertical-align: middle; 56 | line-height: normal; 57 | } 58 | 59 | a { 60 | text-decoration: none; 61 | font-weight: 500; 62 | } 63 | 64 | a:hover { 65 | color: #777777; 66 | } 67 | 68 | textarea { 69 | width: 100%; 70 | resize: none; 71 | font-size: 85%; 72 | } 73 | 74 | footer { 75 | width: 100%; 76 | position: fixed; 77 | bottom: 0; 78 | } 79 | 80 | .mono { 81 | font-family: 'Fira Mono', sans-serif; 82 | } 83 | 84 | .main { 85 | max-width: 1024px; 86 | margin: auto; 87 | } 88 | 89 | .description { 90 | font-weight: 300; 91 | line-height: 1.75; 92 | margin: 8px; 93 | padding: 0px; 94 | flex-grow: 6; 95 | width: 320px; 96 | } 97 | 98 | .about { 99 | max-width: 800px; 100 | margin: auto; 101 | } 102 | 103 | .tab { 104 | font-weight: 400; 105 | } 106 | 107 | .logo { 108 | width: 100px; 109 | height: 100px; 110 | -webkit-transition: width 0.4s, height 0.4s, -webkit-transform 0.4s; 111 | -moz-transition: width 0.4s, height 0.4s, -moz-transform 0.4s; 112 | -o-transition: width 0.4s, height 0.4s, -o-transform 0.4s; 113 | -ms-transition: width 0.4s, height 0.4s, -ms-transform 0.4s; 114 | transition: width 0.4s, height 0.4s, transform 0.4s; 115 | } 116 | 117 | .logo:hover { 118 | width: 100px; 119 | height: 100px; 120 | -webkit-transform: rotate(-10deg); 121 | -moz-transform: rotate(-10deg); 122 | -o-transform: rotate(-10deg); 123 | -ms-transform: rotate(-10deg); 124 | transform: rotate(-10deg); 125 | } 126 | 127 | .flex-container { 128 | display: -webkit-flex; 129 | display: flex; 130 | -webkit-justify-content: center; 131 | justify-content: center; 132 | -webkit-align-items: center; 133 | align-items: center; 134 | } 135 | 136 | .bumf { 137 | font-size: 12px; 138 | margin-top: 0; 139 | margin: 0; 140 | padding: 0; 141 | -webkit-mask-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0))); 142 | max-height: 150px; 143 | } 144 | 145 | .star-button { 146 | border-width: 0px; 147 | overflow: hidden; 148 | } 149 | 150 | .alert { 151 | white-space: nowrap; 152 | overflow: auto; 153 | } 154 | 155 | .row { 156 | background: #FF00FF; 157 | -webkit-justify-content: space-between; 158 | justify-content: space-between; 159 | -webkit-flex-wrap: wrap; 160 | flex-wrap: wrap; 161 | } 162 | 163 | .pictorial { 164 | flex-grow: 2; 165 | } 166 | 167 | .icon { 168 | margin: 0; 169 | padding: 0; 170 | } 171 | 172 | .diagram-line { 173 | stroke: #000000; 174 | stroke-linecap: square; 175 | stroke-width: 3px; 176 | } 177 | 178 | .initial { 179 | font-size: 55px; 180 | font-weight: 600; 181 | } 182 | 183 | .suffix { 184 | font-size: 18px; 185 | font-weight: 600; 186 | } 187 | 188 | .pictorial { 189 | min-width: 320px; 190 | } 191 | 192 | .relationship { 193 | font-size: 300%; 194 | font-weight: 600; 195 | } 196 | 197 | .constant, 198 | .multiplier { 199 | font-size: 200%; 200 | font-weight: 600; 201 | } 202 | 203 | .tooltip { 204 | position: absolute; 205 | left: 0; 206 | top: 65px; 207 | z-index: 12; 208 | text-align: left; 209 | background-color: #f4f9ff; 210 | } 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | WTF Auto Layout? 6 | ================ 7 | 8 | ![Swift](https://img.shields.io/badge/Swift-5.1-green.svg?style=flat) ![Vapor](https://img.shields.io/badge/Vapor-3.3-green.svg?style=flat) 9 | 10 | This is the code driving [www.wtfautolayout.com](https://www.wtfautolayout.com), which parses error logs from Auto Layout on iOS and macOS and provides more intuitive visual descriptions of the conflicting constraints. It was built using [Swift](https://swift.org/), [Vapor](https://vapor.codes) and [Sparse](https://github.com/johnpatrickmorgan/Sparse). 11 | 12 | After installing Vapor, run `vapor xcode` to generate an xcode project. The `Run` target will serve the site locally at `localhost:8080`. 13 | 14 | ------- 15 | 16 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/T6T114GWOT) 17 | -------------------------------------------------------------------------------- /Resources/Views/about.leaf: -------------------------------------------------------------------------------- 1 | #set("pageName") {about} 2 | 3 | #set("content") { 4 | 5 | #embed("hero") 6 | 7 | 8 |
9 |
10 |
11 | Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
12 | (
13 |   "<NSAutoresizingMaskLayoutConstraint:0x1f5b3d10 h=--& v=--& V:[UIView:0x1f5a2f70(460)]>",
14 |   "<NSLayoutConstraint:0x1f5a3c80 V:[UIView:0x1f5a31b0]-(385)-| (Names: '|':UIView:0x1f5a3120 )>",
15 | )
16 |
17 |
18 |

19 | Auto Layout on iOS and macOS is awesome. But when it goes wrong, the error logs are a pain to decipher. 20 |

21 |

22 | This site helps you visualize the conflicting constraints in the logs. It was built using Swift, Vapor and Sparse; and the source code is available on GitHub. If you found it useful, please give it a star. 23 |

24 | 25 |
26 | } 27 | 28 | #embed("page") 29 | -------------------------------------------------------------------------------- /Resources/Views/base.leaf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WTF Auto Layout? 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 47 | #get(head) 48 | 49 | 50 | #get(body) 51 | 52 | 53 | -------------------------------------------------------------------------------- /Resources/Views/hero.leaf: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 |

Why The Failure,
Auto Layout?

7 |
8 |
9 | -------------------------------------------------------------------------------- /Resources/Views/input.leaf: -------------------------------------------------------------------------------- 1 | #if(error) { #set("pageName") {error} } else { #set("pageName") {home} } 2 | 3 | #set("content") { 4 | #embed("hero") 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | Example 13 | 14 |
15 | 16 | #if(error) {
#(error)
} 17 |
18 | 19 | } 20 | 21 | #embed("page") 22 | -------------------------------------------------------------------------------- /Resources/Views/output.leaf: -------------------------------------------------------------------------------- 1 | #set("pageName") {output} 2 | 3 | #set("definitions") { 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | } 36 | 37 | #set("content") { 38 | 39 | 40 | 41 | #for(constraint in constraints) { 42 |
43 |
44 |
45 |
46 | 47 | 48 | #if(constraint.first.attribute.includesMargin) { 49 | 50 | } 51 | #(constraint.first.instance.initial) 52 | #(constraint.first.instance.suffix) 53 | 54 | 55 |
56 | #(constraint.first.instance.name)#(constraint.first.instance.suffix).#(constraint.first.attribute.name)
57 | #(constraint.first.instance.className)
58 | #(constraint.first.instance.address) 59 |
60 |
61 |
62 | #(constraint.relation) 63 |
64 | #if(constraint.second) { 65 |
66 | 67 | 68 | #if(constraint.second.attribute.includesMargin) { 69 | 70 | } 71 | #(constraint.second.instance.initial) 72 | #(constraint.second.instance.suffix) 73 | 74 | 75 |
76 | #(constraint.second.instance.name)#(constraint.second.instance.suffix).#(constraint.second.attribute.name)
77 | #(constraint.second.instance.className)
78 | #(constraint.second.instance.address) 79 |
80 |
81 | } 82 | #if(constraint.multiplier) { 83 |
84 | #(constraint.multiplier) 85 |
86 | } 87 | #if(constraint.constant) { 88 |
89 | #(constraint.constant.prefix)#(constraint.constant.value) 90 |
91 | } 92 |
93 |
94 |
95 | #get(constraint.description)#(constraint.footnote.marker) 96 |
97 |
98 | } 99 | #for(footnote in footnotes) { 100 |

#(footnote.marker) #get(footnote.text)

101 | } 102 | #if(permalink) { 103 | Permalink 104 | } 105 | } 106 | 107 | #embed("page") 108 | -------------------------------------------------------------------------------- /Resources/Views/page.leaf: -------------------------------------------------------------------------------- 1 | #set("body") { 2 | 3 | #get(definitions) 4 | 5 | 6 | 7 |
8 |
9 | Home 10 | About 11 |
12 |
13 | 14 |
15 | 16 | 17 | 18 |
19 |
20 | #get(content) 21 |
22 |
23 | 24 | 25 | 26 |
27 | 28 | 39 | } 40 | 41 | #embed("base") 42 | -------------------------------------------------------------------------------- /Sources/App/Controllers/OutputController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Vapor 3 | import Sparse 4 | 5 | class OutputController { 6 | 7 | let input: String 8 | let includePermalink: Bool 9 | 10 | init(input: String, includePermalink: Bool = true) { 11 | self.input = input 12 | self.includePermalink = includePermalink 13 | } 14 | 15 | func view(req: Request) throws -> Future { 16 | 17 | let trimmedInput = input 18 | .trimmingLogAffixes() 19 | .removingBlankLines() 20 | .trimmingCharacters(in: .whitespaces) 21 | 22 | do { 23 | let group = try ConstraintsParser.parse(input: trimmedInput) 24 | let node = group.leafNode(includePermalink: includePermalink) 25 | 26 | return try req.view().render("output", node) 27 | 28 | } catch let error as ParserError { 29 | 30 | let context = [ 31 | "prefill": trimmedInput, 32 | "error": error.description 33 | ] 34 | 35 | let logger = try req.make(Logger.self) 36 | logger.error(["PARSING ERROR:", trimmedInput, error.description].joined(separator: "\n")) 37 | 38 | return try req.view().render("input", context) 39 | } 40 | } 41 | } 42 | 43 | private extension String { 44 | 45 | func trimmingLogAffixes() -> String { 46 | 47 | return self.trimmingLogSuffix().trimmingLogPrefix() 48 | } 49 | 50 | func trimmingLogPrefix() -> String { 51 | 52 | let prefixEnd1 = "refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)" 53 | let prefixEnd2 = "(2) find the code that added the unwanted constraint or constraints and fix it." 54 | 55 | guard let range = range(of: prefixEnd1) ?? range(of: prefixEnd2) else { 56 | return self 57 | } 58 | 59 | return String(self[range.upperBound...]) 60 | } 61 | 62 | func trimmingLogSuffix() -> String { 63 | 64 | let suffixStart = "Will attempt to recover by breaking constraint" 65 | 66 | guard let range = range(of: suffixStart) else { 67 | return self 68 | } 69 | 70 | return String(self[.. String { 6 | 7 | let one = first.htmlDescription(annotation: annotations[first.layoutItem]) 8 | let two = second?.htmlDescription(annotation: second.flatMap { annotations[$0.layoutItem] }) 9 | let hideMultiplicand = multiplier.value == 1.0 10 | let multiplicand = hideMultiplicand ? nil : multiplier.htmlDescription() 11 | let hideSummand = constant.value == 0.0 && second != nil 12 | let summand = hideSummand ? nil : constant.htmlDescription(includePositivePrefix: second != nil) 13 | 14 | return [ 15 | one, 16 | relation.htmlDescription(), 17 | two, 18 | multiplicand, 19 | summand 20 | ].compactMap { $0 }.joined(separator: " ") + "." 21 | } 22 | } 23 | 24 | extension LayoutItemAttribute { 25 | 26 | func htmlDescription(annotation: Annotation? = nil) -> String { 27 | 28 | let underlineColor = annotation?.color ?? .defaultColor 29 | return "\(layoutItem.prettyName)\(annotation?.uniquingSuffix ?? "")'s \(attribute.htmlDescription())" 30 | } 31 | } 32 | 33 | extension Attribute { 34 | 35 | func htmlDescription() -> String { 36 | 37 | switch self { 38 | case .leading: return "leading edge" 39 | case .trailing: return "trailing edge" 40 | case .top: return "top edge" 41 | case .bottom: return "bottom edge" 42 | case .centerX: return "horizontal center" 43 | case .centerY: return "vertical center" 44 | case .width: return "width" 45 | case .height: return "height" 46 | case .leadingMargin: return "leading margin" 47 | case .trailingMargin: return "trailing margin" 48 | case .topMargin: return "top margin" 49 | case .bottomMargin: return "bottom margin" 50 | case .centerXWithinMargins: return "horizontal center (within its margins)" 51 | case .centerYWithinMargins: return "vertical center (within its margins)" 52 | case .firstBaseline: return "first line of text's baseline" 53 | case .lastBaseline: return "last line of text's baseline" 54 | } 55 | } 56 | } 57 | 58 | extension Constraint.Relation { 59 | 60 | func htmlDescription() -> String { 61 | switch self { 62 | case .equal: 63 | return "should equal" 64 | case .lessThanOrEqual: 65 | return "should be at most" 66 | case .greaterThanOrEqual: 67 | return "should be at least" 68 | } 69 | } 70 | } 71 | 72 | extension Multiplier { 73 | 74 | func htmlDescription() -> String { 75 | 76 | return "multiplied by \(format(number: value, maximumFractionDigits: 3))" 77 | } 78 | } 79 | 80 | extension Constant { 81 | 82 | func htmlDescription(includePositivePrefix: Bool = false) -> String { 83 | 84 | let positivePrefix = includePositivePrefix ? "plus" : nil 85 | let prefix: String? = value == 0.0 ? nil : value > 0.0 ? positivePrefix : "minus" 86 | let number = format(number: abs(value)) 87 | 88 | return [prefix, number].compactMap { $0 }.joined(separator: " ") 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/App/Models/ModelRepresentations/LeafNodes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Vapor 3 | 4 | private let maximumPermalinkLength = 2000 5 | 6 | extension ConstraintGroup { 7 | 8 | struct LeafNode: Codable { 9 | let constraints: [Constraint.LeafNode] 10 | let permalink: String? 11 | let footnotes: [Footnote.LeafNode] 12 | } 13 | 14 | func leafNode(includePermalink: Bool = false) -> LeafNode { 15 | 16 | var permalink: String? { 17 | let trimmed = raw.addingPercentEncoding(withAllowedCharacters: .urlQueryArgumentAllowed) 18 | return (trimmed?.count ?? 0) < maximumPermalinkLength ? trimmed : nil 19 | } 20 | 21 | return .init( 22 | constraints: constraints.map { $0.leafNode(annotations: annotations) }, 23 | permalink: includePermalink ? permalink : nil, 24 | footnotes: footnotes.map { $0.leafNode() } 25 | ) 26 | } 27 | } 28 | 29 | extension Constraint { 30 | 31 | struct LeafNode: Codable { 32 | let identity: Instance.LeafNode 33 | let first: LayoutItemAttribute.LeafNode 34 | let second: LayoutItemAttribute.LeafNode? 35 | let relation: Relation.LeafNode 36 | let constant: Constant.LeafNode? 37 | let multiplier: Multiplier.LeafNode 38 | let description: String 39 | let footnote: Footnote.LeafNode? 40 | } 41 | 42 | func leafNode(annotations: [Instance: Annotation]) -> LeafNode { 43 | 44 | let showConstant = constant.value != 0.0 || second == nil 45 | return .init( 46 | identity: identity.leafNode(), 47 | first: first.leafNode(annotation: annotations[first.layoutItem]), 48 | second: second?.leafNode(annotation: second.flatMap { annotations[$0.layoutItem] }), 49 | relation: relation.leafNode(), 50 | constant: showConstant ? constant.leafNode(includePositivePrefix: second != nil) : nil, 51 | multiplier: multiplier.leafNode(), 52 | description: htmlDescription(annotations: annotations), 53 | footnote: footnote?.leafNode() 54 | ) 55 | } 56 | } 57 | 58 | extension LayoutItemAttribute { 59 | 60 | struct LeafNode: Codable { 61 | let instance: Instance.LeafNode 62 | let attribute: Attribute.LeafNode 63 | } 64 | 65 | func leafNode(annotation: Annotation?) -> LeafNode { 66 | 67 | return .init( 68 | instance: layoutItem.leafNode(annotation: annotation), 69 | attribute: attribute.leafNode() 70 | ) 71 | } 72 | } 73 | 74 | extension Instance { 75 | 76 | struct LeafNode: Codable { 77 | let address: String 78 | let className: String 79 | let name: String 80 | let suffix: String? 81 | let color: Color.LeafNode 82 | let initial: String 83 | let identifier: String? 84 | } 85 | 86 | func leafNode(annotation: Annotation? = nil) -> LeafNode { 87 | let firstAlphanumeric = prettyName.unicodeScalars.first { CharacterSet.alphanumerics.contains($0) } 88 | let initial = firstAlphanumeric.map { String($0).uppercased() } ?? "" 89 | return .init( 90 | address: address, 91 | className: className, 92 | name: prettyName, 93 | suffix: annotation.map { $0.uniquingSuffix }, 94 | color: (annotation?.color ?? .defaultColor).leafNode(), 95 | initial: initial, 96 | identifier: identifier 97 | ) 98 | } 99 | } 100 | 101 | extension Attribute { 102 | 103 | struct LeafNode: Codable { 104 | let name: String 105 | let includesMargin: Bool 106 | } 107 | 108 | func leafNode() -> LeafNode { 109 | 110 | return .init( 111 | name: rawValue, 112 | includesMargin: includesMargin 113 | ) 114 | } 115 | } 116 | 117 | extension Constraint.Relation { 118 | 119 | typealias LeafNode = String 120 | 121 | func leafNode() -> LeafNode { 122 | return rawValue 123 | } 124 | } 125 | 126 | extension Multiplier { 127 | 128 | typealias LeafNode = String? 129 | 130 | func leafNode() -> LeafNode { 131 | guard value != 1.0 else { return nil } 132 | return "* \(format(number: value, maximumFractionDigits: 3))" 133 | } 134 | } 135 | 136 | extension Constant { 137 | 138 | struct LeafNode: Codable { 139 | let value: String 140 | let prefix: String? 141 | } 142 | 143 | func leafNode(includePositivePrefix: Bool) -> LeafNode { 144 | 145 | return .init( 146 | value: format(number: abs(value)), 147 | prefix: value < 0 ? "- " : includePositivePrefix ? "+ " : nil 148 | ) 149 | } 150 | } 151 | 152 | extension Color { 153 | 154 | typealias LeafNode = String 155 | 156 | func leafNode() -> LeafNode { 157 | 158 | return rgb 159 | } 160 | } 161 | 162 | extension Footnote { 163 | 164 | struct LeafNode: Codable { 165 | let marker: String 166 | let text: String 167 | } 168 | 169 | func leafNode() -> LeafNode { 170 | 171 | return .init( 172 | marker: marker, 173 | text: htmlText 174 | ) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Sources/App/Models/ModelRepresentations/NumberFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private let formatter = NumberFormatter() 4 | 5 | func format(number: Double, maximumFractionDigits: Int = 1) -> String { 6 | 7 | formatter.maximumFractionDigits = maximumFractionDigits 8 | formatter.minimumIntegerDigits = 1 9 | formatter.paddingPosition = .beforePrefix 10 | formatter.paddingCharacter = "0" 11 | 12 | return formatter.string(from: NSNumber(value: number)) ?? String(format: "%.1f", number) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/App/Models/Parser Models/Attribute.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Attribute: String { 4 | 5 | enum Axis: String { 6 | 7 | case horizontal 8 | case vertical 9 | } 10 | 11 | enum PinPoint { 12 | 13 | case start 14 | case end 15 | case center 16 | case extent 17 | case firstBaseline 18 | case lastBaseline 19 | } 20 | 21 | case leading 22 | case trailing 23 | case top 24 | case bottom 25 | case centerX 26 | case centerY 27 | case width 28 | case height 29 | case leadingMargin 30 | case trailingMargin 31 | case topMargin 32 | case bottomMargin 33 | case centerXWithinMargins 34 | case centerYWithinMargins 35 | case firstBaseline 36 | case lastBaseline 37 | 38 | static let allCases: [Attribute] = [ 39 | .leading, 40 | .trailing, 41 | .top, 42 | .bottom, 43 | .centerX, 44 | .centerY, 45 | .width, 46 | .height, 47 | .leadingMargin, 48 | .trailingMargin, 49 | .topMargin, 50 | .bottomMargin, 51 | .centerXWithinMargins, 52 | .centerYWithinMargins, 53 | .firstBaseline, 54 | .lastBaseline, 55 | ] 56 | 57 | var alternativeLabels: [String] { 58 | switch self { 59 | case .leading: 60 | return ["left", "minX"] 61 | case .trailing: 62 | return ["right", "maxX"] 63 | case .centerX: 64 | return ["midX"] 65 | case .centerY: 66 | return ["midY"] 67 | case .top: 68 | return ["minY"] 69 | case .bottom: 70 | return ["maxY"] 71 | case .leadingMargin: 72 | return ["leftMargin", "minXMargin"] 73 | case .trailingMargin: 74 | return ["rightMargin", "maxXMargin"] 75 | case .centerXWithinMargins: 76 | return ["midXWithinMargins"] 77 | case .centerYWithinMargins: 78 | return ["midYWithinMargins"] 79 | case .topMargin: 80 | return ["minYMargin"] 81 | case .bottomMargin: 82 | return ["maxYMargin"] 83 | case .lastBaseline: 84 | return ["baseline"] 85 | default: 86 | return [] 87 | } 88 | } 89 | 90 | var labels: [String] { 91 | return [rawValue] + alternativeLabels 92 | } 93 | 94 | init(axis: Axis, pinPoint: PinPoint, includesMargin: Bool = false) throws { 95 | 96 | switch (pinPoint, axis, includesMargin) { 97 | case (.start, .horizontal, false): self = .leading 98 | case (.end, .horizontal, false): self = .trailing 99 | case (.start, .vertical, false): self = .top 100 | case (.end, .vertical, false): self = .bottom 101 | case (.center, .horizontal, false): self = .centerX 102 | case (.center, .vertical, false): self = .centerY 103 | case (.extent, .horizontal, _): self = .width 104 | case (.extent, .vertical, _): self = .height 105 | case (.start, .horizontal, true): self = .leadingMargin 106 | case (.end, .horizontal, true): self = .trailingMargin 107 | case (.start, .vertical, true): self = .topMargin 108 | case (.end, .vertical, true): self = .bottomMargin 109 | case (.center, .horizontal, true): self = .centerXWithinMargins 110 | case (.center, .vertical, true): self = .centerYWithinMargins 111 | case (.firstBaseline, .vertical, _): self = .firstBaseline 112 | case (.lastBaseline, .vertical, _): self = .lastBaseline 113 | default: 114 | throw InvalidConstraintError("\(axis) \(pinPoint)\(includesMargin ? "Margin" : "") is not a valid attribute") 115 | } 116 | } 117 | 118 | private var info: (pinPoint: PinPoint, axis: Axis, includesMargin: Bool) { 119 | 120 | switch self { 121 | case .leading: return (.start, .horizontal, false) 122 | case .trailing: return (.end, .horizontal, false) 123 | case .top: return (.start, .vertical, false) 124 | case .bottom: return (.end, .vertical, false) 125 | case .centerX: return (.center, .horizontal, false) 126 | case .centerY: return (.center, .vertical, false) 127 | case .width: return (.extent, .horizontal, false) 128 | case .height: return (.extent, .vertical, false) 129 | case .leadingMargin: return (.start, .horizontal, true) 130 | case .trailingMargin: return (.end, .horizontal, true) 131 | case .topMargin: return (.start, .vertical, true) 132 | case .bottomMargin: return (.end, .vertical, true) 133 | case .centerXWithinMargins: return (.center, .horizontal, true) 134 | case .centerYWithinMargins: return (.center, .vertical, true) 135 | case .firstBaseline: return (.firstBaseline, .vertical, false) 136 | case .lastBaseline: return (.lastBaseline, .vertical, false) 137 | } 138 | } 139 | var pinPoint: PinPoint { 140 | return info.pinPoint 141 | } 142 | var axis: Axis { 143 | return info.axis 144 | } 145 | var includesMargin: Bool { 146 | return info.includesMargin 147 | } 148 | } 149 | 150 | -------------------------------------------------------------------------------- /Sources/App/Models/Parser Models/Color.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Color { 4 | 5 | let r: Int 6 | let g: Int 7 | let b: Int 8 | 9 | init(r: Int, g: Int, b: Int) { 10 | 11 | self.r = r 12 | self.g = g 13 | self.b = b 14 | } 15 | 16 | init(hex: Int) { 17 | 18 | let r = (hex & 0xFF0000) >> 16 19 | let g = (hex & 0x00FF00) >> 8 20 | let b = hex & 0x0000FF 21 | 22 | self.init(r: r, g: g, b: b) 23 | } 24 | 25 | var hex: String { 26 | 27 | return String(format: "#%02X%02X%02X", r, g, b) 28 | } 29 | 30 | var rgb: String { 31 | 32 | return "rgb(\(r),\(g),\(b))" 33 | } 34 | } 35 | 36 | extension Color: ExpressibleByIntegerLiteral { 37 | 38 | init(integerLiteral value: Int) { 39 | 40 | self.init(hex: value) 41 | } 42 | } 43 | 44 | extension Color { 45 | 46 | static func flatColor(index: Int) -> Color { 47 | 48 | let colors = [ 49 | turquoise, 50 | peterRiver, 51 | sunflower, 52 | alizarin, 53 | emerald, 54 | amethyst, 55 | carrot, 56 | creamCorn, 57 | greenSea, 58 | belizeHole, 59 | orange, 60 | pomegranate, 61 | dodgerBlue, 62 | nephritis, 63 | wysteria, 64 | pumpkin, 65 | ] 66 | return colors[abs(index) % colors.count] 67 | } 68 | 69 | static let defaultColor = turquoise 70 | 71 | static let turquoise: Color = 0x1abc9c 72 | static let greenSea: Color = 0x16a085 73 | 74 | static let emerald: Color = 0x2ecc71 75 | static let nephritis: Color = 0x27ae60 76 | 77 | static let peterRiver: Color = 0x3498db 78 | static let belizeHole: Color = 0x2980b9 79 | static let dodgerBlue: Color = 0x19b5fe 80 | 81 | static let amethyst: Color = 0x9b59b6 82 | static let wysteria: Color = 0x8e44ad 83 | 84 | static let creamCorn: Color = 0xf5d76e 85 | static let sunflower: Color = 0xf1c40f 86 | static let orange: Color = 0xf39c12 87 | 88 | static let carrot: Color = 0xe67e22 89 | static let pumpkin: Color = 0xd35400 90 | 91 | static let alizarin: Color = 0xe74c3c 92 | static let pomegranate: Color = 0xc0392b 93 | } 94 | -------------------------------------------------------------------------------- /Sources/App/Models/Parser Models/Constant.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Constant { 4 | 5 | let value: Double 6 | 7 | init(_ value: Double = 0.0) { 8 | 9 | self.value = value 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/App/Models/Parser Models/Constraint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Constraint { 4 | 5 | enum Origin { 6 | case user 7 | case encapsulatedLayout 8 | case autoresizingMask 9 | case stackView 10 | } 11 | 12 | enum Relation: String { 13 | 14 | case equal = "==" 15 | case lessThanOrEqual = "<=" 16 | case greaterThanOrEqual = ">=" 17 | 18 | static let allCases: [Relation] = [ 19 | .equal, 20 | .lessThanOrEqual, 21 | .greaterThanOrEqual 22 | ] 23 | } 24 | 25 | let identity: Instance 26 | let first: LayoutItemAttribute 27 | let second: LayoutItemAttribute? 28 | let relation: Relation 29 | let constant: Constant 30 | let multiplier: Multiplier 31 | let origin: Origin 32 | } 33 | 34 | extension Constraint { 35 | 36 | var layoutItemAttributes: [LayoutItemAttribute] { 37 | return [ 38 | first, 39 | second 40 | ].compactMap { $0 } 41 | } 42 | 43 | var layoutItems: [Instance] { 44 | return layoutItemAttributes.map { $0.layoutItem } 45 | } 46 | } 47 | 48 | extension Constraint { 49 | 50 | var footnote: Footnote? { 51 | switch origin { 52 | case .autoresizingMask: 53 | return .autoresizingMask 54 | case .encapsulatedLayout: 55 | return .encapsulatedLayout 56 | case .stackView: 57 | return .stackView 58 | case .user: 59 | return nil 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/App/Models/Parser Models/ConstraintGroup.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Random 3 | 4 | typealias Annotation = (color: Color, uniquingSuffix: String) 5 | 6 | struct ConstraintGroup { 7 | 8 | let raw: String 9 | let constraints: [Constraint] 10 | let annotations: [Instance : Annotation] 11 | 12 | private static func annotations(for layoutItems: [Instance], seed: Int) -> [Instance : Annotation] { 13 | 14 | let names = layoutItems.map { $0.prettyName } 15 | 16 | var uniquingSuffixes = [String]() 17 | var counts = [String : Int]() 18 | 19 | for name in names { 20 | let number = (counts[name] ?? 0) + 1 21 | counts[name] = number 22 | uniquingSuffixes.append(String(number)) 23 | } 24 | 25 | var annotations = [Instance : Annotation]() 26 | for (i, item) in layoutItems.enumerated() { 27 | // Adding a seed value ensures the color scheme can differ per input while 28 | // remaining deterministic. 29 | let color = Color.flatColor(index: i + seed) 30 | 31 | let requiresUniquing = (counts[item.prettyName] ?? 0) > 1 32 | let uniquingSuffix = requiresUniquing ? uniquingSuffixes[i] : "" 33 | 34 | annotations[item] = (color: color, uniquingSuffix: uniquingSuffix) 35 | } 36 | 37 | return annotations 38 | } 39 | 40 | init(_ constraints: [Constraint], raw: String) { 41 | 42 | self.raw = raw 43 | self.constraints = constraints 44 | let layoutItems = constraints.flatMap { $0.layoutItems }.removingDuplicates() 45 | self.annotations = ConstraintGroup.annotations(for: Array(layoutItems), seed: raw.count) 46 | } 47 | } 48 | 49 | extension ConstraintGroup { 50 | 51 | var includesAutoResizingMaskConstraints: Bool { 52 | return constraints.contains(where: { $0.origin == .autoresizingMask }) 53 | } 54 | 55 | var footnotes: [Footnote] { 56 | return constraints.compactMap { $0.footnote }.removingDuplicates() 57 | } 58 | } 59 | 60 | private extension Collection where Element: Hashable { 61 | 62 | func removingDuplicates() -> [Element] { 63 | var seen: Set = [] 64 | var uniques: [Element] = [] 65 | for element in self { 66 | guard !seen.contains(element) else { continue } 67 | seen.insert(element) 68 | uniques.append(element) 69 | } 70 | return uniques 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /Sources/App/Models/Parser Models/Footnote.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Footnote { 4 | 5 | case autoresizingMask 6 | case encapsulatedLayout 7 | case stackView 8 | } 9 | 10 | extension Footnote { 11 | 12 | var marker: String { 13 | switch self { 14 | case .autoresizingMask: 15 | return "*" 16 | case .encapsulatedLayout: 17 | return "†" 18 | case .stackView: 19 | return "‡" 20 | } 21 | } 22 | 23 | var htmlText: String { 24 | switch self { 25 | case .autoresizingMask: 26 | return "This constraint was translated from an autoresizing mask. If this was not intended, try setting translatesAutoresizingMaskIntoConstraints to false on this view." 27 | case .encapsulatedLayout: 28 | return "This constraint was added by a table or collection view to enforce its cell size." 29 | case .stackView: 30 | return "This constraint was added by a stack view to enforce its spacing, distribution or alignment." 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/App/Models/Parser Models/Instance.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Instance { 4 | 5 | let className: String 6 | let address: String 7 | let identifier: String? 8 | } 9 | 10 | extension Instance: Equatable, Hashable { 11 | 12 | public static func ==(lhs: Instance, rhs: Instance) -> Bool { 13 | 14 | return lhs.address == rhs.address 15 | } 16 | 17 | public func hash(into hasher: inout Hasher) { 18 | 19 | address.hash(into: &hasher) 20 | } 21 | } 22 | 23 | extension Instance { 24 | 25 | var shortClassName: String { 26 | return className.components(separatedBy: ".").lazy.filter({ !$0.isEmpty }).last ?? className 27 | } 28 | 29 | var prettyTypeName: String { 30 | 31 | let prefixToTrim = "UI" 32 | 33 | guard shortClassName.hasPrefix(prefixToTrim) else { 34 | return shortClassName 35 | } 36 | 37 | return String(shortClassName[prefixToTrim.endIndex...]) 38 | } 39 | 40 | var prettyName: String { 41 | 42 | return identifier ?? prettyTypeName 43 | } 44 | } 45 | 46 | struct LayoutItemAttribute { 47 | 48 | let layoutItem: Instance 49 | let attribute: Attribute 50 | } 51 | -------------------------------------------------------------------------------- /Sources/App/Models/Parser Models/Multiplier.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct Multiplier { 4 | 5 | let value: Double 6 | 7 | init(_ value: Double = 1.0) { 8 | 9 | self.value = value 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/App/Models/Requests/ConstraintLogRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ConstraintLogRequest: Codable { 4 | 5 | let constraintlog: String 6 | } 7 | -------------------------------------------------------------------------------- /Sources/App/Parsing/ConstraintsParser+AutoLayoutPrimitives.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Sparse 3 | 4 | // MARK: - Auto Layout Primitives 5 | 6 | extension ConstraintsParser { 7 | 8 | static let identifierCharacter = characterNot(in: .newlines) 9 | .named("valid identifier character") 10 | 11 | static let relation = Constraint.Relation.allCases 12 | .map(parserForRelation) 13 | .reduce(pureError(), { $1.otherwise($0) }) 14 | .named("relation") 15 | 16 | static let attribute: Parser = Attribute.allCases 17 | .map(parserForAttribute) 18 | .reduce(pureError(), { $1.otherwise($0) }) 19 | .named("attribute") 20 | 21 | static let dotAttribute = dot.skipThen(attribute) 22 | .named("dot attribute") 23 | 24 | static let number = numberString.map(parseDouble) 25 | .named("number") 26 | 27 | static let spacing = number.otherwise(spacedNumber) 28 | 29 | static let defaultedRelation = optional(relation).map { $0 ?? .equal } 30 | 31 | static let extent = defaultedRelation.then(spacing) 32 | } 33 | 34 | private extension ConstraintsParser { 35 | 36 | static let sign = optional(string("+")).skipThen(optional(string("-"))) 37 | static let signedInteger = sign.thenSkip(wss).then(integer).map { "\($0 ?? "")\($1)" } 38 | static let exponent = character("e").then(signedInteger).map { "\($0)\($1)" } 39 | .named("exponent") 40 | static let postDecimal = character(".").then(integer).map { "\($0)\($1)" } 41 | static let decimal = signedInteger.then(optional(postDecimal)).map { "\($0)\($1 ?? "")" } 42 | static let numberString = decimal.then(optional(exponent)).map { "\($0)\($1 ?? "")" } 43 | 44 | static let spacingTerm = string("NSSpace") 45 | .otherwise(string("NSLayoutAnchorConstraintSpace")) 46 | static let spacedNumber = spacingTerm.skipThen(string("(")).skipThen(number).thenSkip(")") 47 | 48 | static func parserForAttribute(_ attribute: Attribute) -> Parser { 49 | let attributeParser = attribute.labels.reduce(pureError(), { string($1).otherwise($0) }) 50 | return attributeParser.map { _ in return attribute } 51 | } 52 | 53 | static func parserForRelation(_ relation: Constraint.Relation) -> Parser { 54 | return string(relation.rawValue).map { _ in return relation } 55 | } 56 | 57 | static func parseDouble(_ input: String) throws -> Double { 58 | guard let result = Double(input) else { 59 | throw InvalidConstraintError("'\(input)' is not a valid number") 60 | } 61 | 62 | return result 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/App/Parsing/ConstraintsParser+EquationConstraint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Sparse 3 | 4 | // MARK: - Equation Constraint 5 | // (e.g. "UIImageView:0x7fd382f50.top == UITableViewCell:0x7fd39b20.top") 6 | 7 | extension ConstraintsParser { 8 | 9 | static let equationConstraint = layoutItemAttribute.thenSkip(wss).then(relation) 10 | .thenSkip(wss).then(preMultiplier).then(optional(layoutItemAttribute)) 11 | .thenSkip(wss).then(postMultiplier).then(anyConstant).then(optionalInfo) 12 | .map { 13 | try AnonymousConstraint( 14 | first: $0.0.0.0.0.0, 15 | relation: $0.0.0.0.0.1, 16 | multiplier: $0.0.0.0.1 ?? $0.0.1 ?? Multiplier(), 17 | second: $0.0.0.1, 18 | constant: $0.1, 19 | names: $1 20 | ) 21 | } 22 | } 23 | 24 | private extension ConstraintsParser { 25 | 26 | static let layoutItemAttribute = partialInstance.then(dotAttribute) 27 | static let anyConstant = extent.map { Constant($1) }.otherwise(constant) 28 | static let constant = number.map(Constant.init).otherwise(pure(Constant())) 29 | static let preMultiplier = optional(number.thenSkip(string("*")).map(Multiplier.init)) 30 | .named("prefixed multiplier") 31 | static let postMultiplier = optional(string("*").skipThen(wss).skipThen(number).map(Multiplier.init)) 32 | .named("postfixed multiplier") 33 | } 34 | -------------------------------------------------------------------------------- /Sources/App/Parsing/ConstraintsParser+Examples.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Random 3 | 4 | extension ConstraintsParser { 5 | 6 | private static let example1 = """ 7 | ( 8 | "", 9 | "", 10 | "", 11 | "\" 12 | ) 13 | """ 14 | 15 | private static let example2 = """ 16 | ( 17 | "", 18 | "= 50 (active, names: name_label:0x104590 )>" 19 | ) 20 | """ 21 | 22 | private static let example3 = """ 23 | ( 24 | "", 25 | "", 26 | "" 27 | ) 28 | """ 29 | 30 | static func randomExample() -> String { 31 | let examples = [ 32 | example1, 33 | example2, 34 | example3 35 | ] 36 | return examples.random! 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/App/Parsing/ConstraintsParser+Info.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Sparse 3 | 4 | // MARK: - Info 5 | // (e.g. "(Names: '|':UIView:0x1f5a3120 )") 6 | // (e.g. "(active, names: LoansListViewOptionsLabel...:0x7fa4d60764b0, '|':UIView:0x7fa4d6079490 )") 7 | 8 | extension ConstraintsParser { 9 | 10 | static let optionalInfo = optional(info).map { $0 ?? [:] } 11 | } 12 | 13 | extension ConstraintsParser { 14 | 15 | static let active = string("active") 16 | static let names = string("names:").otherwise(string("Names:")).thenSkip(wss) 17 | static let key = many(characterNot(in: CharacterSet(charactersIn: ":' "))).asString() 18 | static let quotableKey = optional(character("'")).skipThen(key).thenSkip(optional(character("'"))) 19 | .named("key") 20 | static let keyValuePair = infoInstance.map { ($0.className, $0) }.otherwise((quotableKey.thenSkip(character(":")).then(infoInstance))) 21 | .named("key value pair") 22 | static let commaSeparator = string(", ") 23 | static let keyValuePairs = many(keyValuePair, separator: commaSeparator).map { Dictionary($0, uniquingKeysWith: { $1 }) } 24 | static let namesDictionary = names.skipThen(wss).skipThen(keyValuePairs).thenSkip(wss) 25 | static let emptyInfo = character("(").skipThen(active).thenSkip(character(")")).map { _ in return [String: Instance]() } 26 | static let nonEmptyInfo = character("(").skipThen(optional(active.then(commaSeparator))).skipThen(namesDictionary).thenSkip(wss).thenSkip(character(")")) 27 | static let info = wss.skipThen(nonEmptyInfo.otherwise(emptyInfo)) 28 | .named("info") 29 | } 30 | -------------------------------------------------------------------------------- /Sources/App/Parsing/ConstraintsParser+Instances.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Sparse 3 | 4 | // MARK: - Instances 5 | // (e.g. "UIImageView:0x7fd382f50 'avatar'") 6 | 7 | extension ConstraintsParser { 8 | 9 | static let instance = className.then(address).thenSkip(optional(fileLocation)).thenSkip(wss).then(optional(identifier)) 10 | .map(flatten) 11 | .map { Instance(className: $0, address: $1, identifier: $2) } 12 | .named("instance") 13 | 14 | static let infoInstance = classOrName.then(address).thenSkip(optional(fileLocation)).thenSkip(wss).then(optional(identifier)) 15 | .map(flatten) 16 | .map { Instance(className: $0, address: $1, identifier: $2) } 17 | .named("info instance") 18 | 19 | static let constraintInstance = className.then(address).thenSkip(optional(fileLocation)).thenSkip(al1ws) 20 | .then(isFromAutoresizingMask.thenSkip(wss)) 21 | .then(optional(identifier.thenSkip(al1ws))) 22 | .map(flatten) 23 | .map { (Instance(className: $0, address: $1, identifier: $3), $2) } 24 | .named("constraint instance") 25 | 26 | static let partialInstance = instance.map({ PartialInstance.instance($0) }).otherwise(partialInstanceIdentifier) 27 | } 28 | 29 | private extension ConstraintsParser { 30 | 31 | static let classNameCharacter = character(condition: isClassNameCharacter) 32 | .named("valid class name character") 33 | static let className = many(classNameCharacter, untilSkipping: colon).asString() 34 | static let address = string("0x").then(many(hexit)).map { "\($0.0)\(String($0.1))" } 35 | .named("memory address") 36 | static let identifier = many(identifierCharacter.butNot(sqm), boundedBy: sqm).asString() 37 | .named("identifier") 38 | static let fileLocation = character("@").skipThen(many(anyCharacter(), untilSkipping: character("#")).asString()).then(integer) 39 | .named("file location") 40 | 41 | static let partialIdentifierCharacter = characterNot(in: "[]|()<>\":") 42 | .named("valid partial identifier character") 43 | static let partialInstanceIdentifier = many(identifierCharacter.and(partialIdentifierCharacter), untilSkipping: dotAttribute.withoutConsuming()).asString() 44 | .map { PartialInstance.identifier($0) } 45 | 46 | static let classOrName = many(identifierCharacter, untilSkipping: colon).asString() 47 | 48 | static let isFlexible = character(in: "&-").map { $0 == "&" } 49 | static let (h, v) = (string("h="), string("v=")) 50 | static let autoresizingIntro = h.skipThen(exactly(3, isFlexible)).thenSkip(wss) 51 | .thenSkip(v).then(exactly(3, isFlexible)).thenSkip(wss) 52 | static let isFromAutoresizingMask = optional(autoresizingIntro).map { $0 != nil } 53 | .named("autoresizing mask info") 54 | 55 | static func isClassNameCharacter(_ character: Character) -> Bool { 56 | return CharacterSet.classNameCharacters.contains(character.unicodeScalar()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/App/Parsing/ConstraintsParser+Primitives.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Sparse 3 | 4 | // MARK: - Primitives 5 | 6 | extension ConstraintsParser { 7 | 8 | static let wss = whitespaces() 9 | static let al1ws = atLeastOne(whitespace()).asString() 10 | static let wsnls = whitespacesOrNewlines() 11 | static let colon = character(":") 12 | static let dot = character(".") 13 | static let sqm = character("'") 14 | static let dash = character("-") 15 | static let hexit = character(in: "0123456789abcdefABCDEF") 16 | .named("hexit") 17 | static let digit = character(in: "0123456789") 18 | .named("digit") 19 | static let integer = atLeastOne(digit).asString() 20 | } 21 | -------------------------------------------------------------------------------- /Sources/App/Parsing/ConstraintsParser+VFLConstraint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Sparse 3 | 4 | // MARK: - VFL Constraint 5 | // (e.g. "V:[UIView:0x1f5a31b0]-(385)-| (Names: '|':UIView:0x1f5a3120 )") 6 | 7 | extension ConstraintsParser { 8 | 9 | static let vflSpaceConstraint = vflAxis.then(vflBoundedEntity).thenSkip(dash).then(vflExtent).thenSkip(dash).then(vflBoundedEntity).thenSkip(vflDirection).then(optionalInfo) 10 | .map(flatten).map(AnonymousConstraint.init) 11 | 12 | static let vflExtentConstraint = vflAxis.thenSkip(character("[")).then(vflEntity).then(vflExtent).thenSkip(character("]")).thenSkip(vflDirection).then(optionalInfo) 13 | .map(flatten).map(AnonymousConstraint.init) 14 | 15 | static let vflConstraint = vflExtentConstraint.otherwise(vflSpaceConstraint) 16 | static let vflExtent = character("(").skipThen(extent).thenSkip(character(")")) 17 | } 18 | 19 | // MARK: - Private 20 | 21 | private extension ConstraintsParser { 22 | 23 | static let hAxis = character("H").map { _ in Attribute.Axis.horizontal } 24 | static let vAxis = character("V").map { _ in Attribute.Axis.vertical } 25 | static let superview = character("|") 26 | static let vflAxis = hAxis.otherwise(vAxis).thenSkip(colon) 27 | static let vflIdentifierCharacter = characterNot(in: "[]|()") 28 | static let vflIdentifier = many(identifierCharacter.and(vflIdentifierCharacter)).asString() 29 | static let vflEntity = instance.map({ PartialInstance.instance($0) }).otherwise(vflIdentifier.map({ PartialInstance.identifier($0) })) 30 | static let vflBoundedEntity = character("[").skipThen(vflEntity).thenSkip(character("]")).otherwise(string("|").map({ _ in PartialInstance.superview })) 31 | static let vflDirection = optional(string("(LTR)").otherwise(string("(RTL)"))) 32 | } 33 | -------------------------------------------------------------------------------- /Sources/App/Parsing/ConstraintsParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Sparse 3 | 4 | // MARK:- Complete constraints log parser. 5 | 6 | enum ConstraintsParser { 7 | 8 | static func parse(input: String) throws -> ConstraintGroup { 9 | 10 | let stream = Stream(input) 11 | let constraints = try constraintsLog.parse(stream) 12 | 13 | return ConstraintGroup(constraints, raw: input) 14 | } 15 | } 16 | 17 | // MARK: - Private 18 | 19 | private extension ConstraintsParser { 20 | 21 | static let constraintsLog = wsnls.thenSkip(optional(character("("))).skipThen(wsnls).skipThen(constraints) 22 | .thenSkip(wsnls).thenSkip(optional(character(")"))).thenSkip(wsnls).thenSkip(end()) 23 | 24 | static let constraint = constraintInstance.thenSkip(wss).then(equationConstraint.otherwise(vflConstraint)).map { $1.deanonymized(instance: $0.0, isFromAutoresizingMask: $0.1) } 25 | static let boundedConstraint = string("\"<").skipThen(constraint).thenSkip(string(">\"")) 26 | static let constraintSeparator = character(",").skipThen(wsnls).named("constraint separator") 27 | static let constraints = many(boundedConstraint, separator: constraintSeparator) 28 | } 29 | -------------------------------------------------------------------------------- /Sources/App/Parsing/Partials/AnonymousConstraint.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct AnonymousConstraint { 4 | 5 | let first: LayoutItemAttribute 6 | let second: LayoutItemAttribute? 7 | let relation: Constraint.Relation 8 | let constant: Constant 9 | let multiplier: Multiplier 10 | } 11 | 12 | extension AnonymousConstraint { 13 | 14 | init(first: (partialInstance: PartialInstance, attribute: Attribute), relation: Constraint.Relation, multiplier: Multiplier, second: (partialInstance: PartialInstance, attribute: Attribute)?, constant: Constant, names: [String:Instance]) throws { 15 | 16 | let instance1 = try first.partialInstance.getInstance(names: names) 17 | 18 | self.init( 19 | first: LayoutItemAttribute(layoutItem: instance1, 20 | attribute: first.attribute), 21 | second: try second.map { LayoutItemAttribute(layoutItem: try $0.partialInstance.getInstance(names: names), 22 | attribute: $0.attribute) }, 23 | relation: relation, 24 | constant: constant, 25 | multiplier: multiplier 26 | ) 27 | } 28 | 29 | 30 | init(axis: Attribute.Axis, partialInstance2: PartialInstance, comparison: (relation: Constraint.Relation, amount: Double), partialInstance1: PartialInstance, names: [String:Instance]) throws { 31 | 32 | let instance1 = try partialInstance1.getInstance(names: names) 33 | let instance2 = try partialInstance2.getInstance(names: names) 34 | 35 | let pinpoints: (Attribute.PinPoint, Attribute.PinPoint) = { 36 | switch (partialInstance1, partialInstance2) { 37 | case (.superview, _): 38 | return (.end, .end) 39 | case (_, .superview): 40 | return (.start, .start) 41 | default: 42 | return (.start, .end) 43 | } 44 | }() 45 | 46 | let attribute1 = try Attribute(axis: axis, pinPoint: pinpoints.0) 47 | let attribute2 = try Attribute(axis: axis, pinPoint: pinpoints.1) 48 | 49 | self.init(first: LayoutItemAttribute(layoutItem: instance1, 50 | attribute: attribute1), 51 | second: LayoutItemAttribute(layoutItem: instance2, 52 | attribute: attribute2), 53 | relation: comparison.relation, 54 | constant: Constant(comparison.amount), 55 | multiplier: Multiplier()) 56 | } 57 | 58 | init(axis: Attribute.Axis, partialInstance: PartialInstance, comparison: (relation: Constraint.Relation, amount: Double), names: [String:Instance]) throws { 59 | 60 | let instance = try partialInstance.getInstance(names: names) 61 | let attribute = try Attribute(axis: axis, pinPoint: .extent) 62 | let first = LayoutItemAttribute(layoutItem: instance, attribute: attribute) 63 | 64 | self.init( 65 | first: first, 66 | second: nil, 67 | relation: comparison.relation, 68 | constant: Constant(comparison.amount), 69 | multiplier: Multiplier() 70 | ) 71 | } 72 | } 73 | 74 | extension AnonymousConstraint { 75 | 76 | func deanonymized(instance: Instance, isFromAutoresizingMask: Bool) -> Constraint { 77 | 78 | let origin: Constraint.Origin 79 | if isFromAutoresizingMask { 80 | origin = .autoresizingMask 81 | } else if let identifier = instance.identifier, identifier.hasPrefix("UIView-Encapsulated-Layout") { 82 | origin = .encapsulatedLayout 83 | } else if let identifier = instance.identifier, identifier.hasPrefix("UISV") { 84 | origin = .stackView 85 | } else { 86 | origin = .user 87 | } 88 | 89 | return Constraint( 90 | identity: instance, 91 | first: first, 92 | second: second, 93 | relation: relation, 94 | constant: constant, 95 | multiplier: multiplier, 96 | origin: origin 97 | ) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/App/Parsing/Partials/PartialInstance.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum PartialInstance { 4 | 5 | case instance(Instance) 6 | case identifier(String) 7 | case superview 8 | } 9 | 10 | extension PartialInstance { 11 | 12 | func getInstance(names: [String: Instance]) throws -> Instance { 13 | switch self { 14 | case .instance(let instance): 15 | return instance 16 | case .identifier(let identifier): 17 | guard let instance = names[identifier] else { 18 | throw InvalidConstraintError("Missing info for \(identifier)") 19 | } 20 | return instance 21 | case .superview: 22 | guard let instance = names["|"] else { 23 | throw InvalidConstraintError("Missing info for '|' (superview)") 24 | } 25 | return instance 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/App/Routes.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | public func routes(_ router: Router) throws { 4 | 5 | router.get { (req: Request) -> Future in 6 | if let input = (try? req.query.get(String.self, at: "constraintlog")) { 7 | return try OutputController(input: input, includePermalink: false).view(req: req) 8 | } 9 | 10 | let showExample = (try? req.query.get(Bool.self, at: "example")) ?? false 11 | if showExample { 12 | let context = ["prefill": ConstraintsParser.randomExample()] 13 | return try req.view().render("input", context) 14 | } else { 15 | return try req.view().render("input") 16 | } 17 | } 18 | 19 | router.post { (req: Request) -> Future in 20 | let input = try req.content.syncDecode(ConstraintLogRequest.self).constraintlog 21 | return try OutputController(input: input).view(req: req) 22 | } 23 | 24 | router.get("about") { req in 25 | return try req.view().render("about") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/App/SwiftExtensions/CharacterExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Character { 4 | 5 | func unicodeScalar() -> UnicodeScalar { 6 | 7 | let characterString = String(self) 8 | let scalars = characterString.unicodeScalars 9 | 10 | return scalars[scalars.startIndex] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/App/SwiftExtensions/CharacterSetExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension CharacterSet { 4 | 5 | static var classNameCharacters: CharacterSet = alphanumerics.union(CharacterSet(charactersIn: "$._")) 6 | 7 | static var urlQueryArgumentAllowed: CharacterSet = urlQueryAllowed.subtracting(CharacterSet(charactersIn: "=&")) 8 | } 9 | -------------------------------------------------------------------------------- /Sources/App/SwiftExtensions/StringExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | 5 | func removingBlankLines() -> String { 6 | 7 | return self 8 | .components(separatedBy: .newlines) 9 | .filter { !CharacterSet(charactersIn: $0).isSubset(of: .whitespaces) } 10 | .joined(separator: "\n") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/App/app.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | /// Creates an instance of Application. This is called from main.swift in the run target. 4 | public func app(_ env: Environment) throws -> Application { 5 | var config = Config.default() 6 | var env = env 7 | var services = Services.default() 8 | try configure(&config, &env, &services) 9 | let app = try Application(config: config, environment: env, services: services) 10 | try boot(app) 11 | return app 12 | } 13 | -------------------------------------------------------------------------------- /Sources/App/boot.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | /// Called after your application has initialized. 4 | public func boot(_ app: Application) throws { 5 | // your code here 6 | } 7 | -------------------------------------------------------------------------------- /Sources/App/configure.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | import Vapor 3 | 4 | /// Called before your application initializes. 5 | public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws { 6 | /// Register providers first 7 | try services.register(LeafProvider()) 8 | 9 | /// Register routes to the router 10 | let router = EngineRouter.default() 11 | try routes(router) 12 | services.register(router, as: Router.self) 13 | 14 | /// Use Leaf for rendering views 15 | config.prefer(LeafRenderer.self, for: ViewRenderer.self) 16 | 17 | /// Register middleware 18 | var middlewares = MiddlewareConfig() // Create _empty_ middleware config 19 | middlewares.use(FileMiddleware.self) // Serves files from `Public/` directory 20 | middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response 21 | services.register(middlewares) 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Run/main.swift: -------------------------------------------------------------------------------- 1 | import App 2 | 3 | try app(.detect()).run() 4 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | TODO 2 | ==== 3 | 4 | - Clean up HTML / CSS 5 | - More comprehensive tests 6 | - Add documentation 7 | - Differentiation of various UIStackView constraints 8 | - Automatically parse on drop 9 | - Links for raising issues 10 | - Show info tooltip about constraint on hover 11 | - Easier permalink copying 12 | - Improve output layout on Firefox 13 | - Support RTL (right-to-left) 14 | -------------------------------------------------------------------------------- /Tests/AppTests/Application+Testing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Vapor 3 | 4 | extension Application { 5 | 6 | func make(request: HTTPRequest) throws -> Response { 7 | let wrappedRequest = Request(http: request, using: self) 8 | let responder = try make(Responder.self) 9 | return try responder.respond(to: wrappedRequest).wait() 10 | } 11 | 12 | func get( 13 | url: URLRepresentable = URL.root, 14 | version: HTTPVersion = .init(major: 1, minor: 1), 15 | headers: HTTPHeaders = .init() 16 | ) throws -> Response { 17 | let request = HTTPRequest(method: .GET, url: url, version: version, headers: headers, body: HTTPBody()) 18 | return try make(request: request) 19 | } 20 | 21 | func postJSON( 22 | url: URLRepresentable = URL.root, 23 | version: HTTPVersion = .init(major: 1, minor: 1), 24 | headers: HTTPHeaders = .init(), 25 | body: T 26 | ) throws -> Response { 27 | let encoder = JSONEncoder() 28 | let body = try HTTPBody(data: encoder.encode(body)) 29 | var amendedHeaders = headers 30 | amendedHeaders.add(name: "Content-Type", value: "application/json") 31 | let request = HTTPRequest(method: .POST, url: url, version: version, headers: amendedHeaders, body: body) 32 | return try make(request: request) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/AppTests/ColorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import App 3 | 4 | class ColorTests: XCTestCase { 5 | 6 | static let allTests = [ 7 | ("testHexConversion", testHexConversion) 8 | ] 9 | 10 | func testHexConversion() { 11 | 12 | typealias Expectation = (input: Int, output: String) 13 | 14 | let expectations: [Expectation] = [ 15 | (0x1abc9c, "#1abc9c"), 16 | (0x16a085, "#16a085"), 17 | (0x2ecc71, "#2ecc71"), 18 | (0x27ae60, "#27ae60"), 19 | (0x3498db, "#3498db"), 20 | (0x2980b9, "#2980b9"), 21 | (0x19b5fe, "#19b5fe"), 22 | (0x9b59b6, "#9b59b6"), 23 | (0x8e44ad, "#8e44ad"), 24 | (0xf5d76e, "#f5d76e"), 25 | (0xf1c40f, "#f1c40f"), 26 | (0xf39c12, "#f39c12"), 27 | (0xe67e22, "#e67e22"), 28 | (0xd35400, "#d35400"), 29 | (0xe74c3c, "#e74c3c"), 30 | (0xc0392b, "#c0392b") 31 | ] 32 | 33 | for expectation in expectations { 34 | let output = Color(hex: expectation.input).hex 35 | XCTAssertEqual(output.lowercased(), expectation.output) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/AppTests/Inputs/Input+Custom.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Input { 4 | 5 | static let custom1 = """ 6 | ( 7 | "", 8 | "", 9 | "", 10 | "" 11 | ) 12 | """ 13 | 14 | static let custom2 = """ 15 | ( 16 | "", 17 | "", 18 | "" 19 | ) 20 | """ 21 | 22 | static let custom3 = """ 23 | ( 24 | "", 25 | "", 26 | "", 27 | "", 28 | "" 29 | ) 30 | """ 31 | 32 | static let custom4 = """ 33 | ( 34 | "<_UILayoutSupportConstraint:0x1272e5820 V:[_UILayoutGuide:0x1271a5500(0)]>", 35 | "<_UILayoutSupportConstraint:0x1272cd510 _UILayoutGuide:0x1271a5500.bottom == UIView:0x128321830.bottom>", 36 | "", 37 | "", 38 | "", 39 | "" 40 | ) 41 | """ 42 | 43 | static let custom5 = """ 44 | ( 45 | "", 46 | "", 47 | "", 48 | "" 49 | ) 50 | """ 51 | 52 | static let custom6 = """ 53 | ( 54 | "", 55 | "", 56 | "", 57 | "", 58 | "", 59 | "", 60 | "", 61 | "=11)-[App.LineView:0x7f9500290110] (active)>", 62 | "", 63 | "", 64 | "" 65 | ) 66 | """ 67 | 68 | static let custom7 = """ 69 | ( 70 | "", 71 | "", 72 | "", 73 | "" 74 | ) 75 | """ 76 | 77 | static let custom8 = """ 78 | ( 79 | "" 80 | ) 81 | """ 82 | 83 | static let custom9 = """ 84 | Unable to simultaneously satisfy constraints. 85 | Probably at least one of the constraints in the following list is one you don't want. 86 | Try this: 87 | (1) look at each constraint and try to figure out which you don't expect; 88 | (2) find the code that added the unwanted constraint or constraints and fix it. 89 | ( 90 | "", 91 | "", 92 | "", 93 | "", 94 | "", 95 | "", 96 | "", 97 | "", 98 | "", 99 | "", 100 | "" 101 | ) 102 | 103 | Will attempt to recover by breaking constraint 104 | 105 | 106 | Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. 107 | The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in may also be helpful. 108 | """ 109 | } 110 | -------------------------------------------------------------------------------- /Tests/AppTests/Inputs/Input+Error.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Input { 4 | 5 | static let invalidHexitError = """ 6 | ( 7 | "" 8 | ) 9 | """ 10 | } 11 | -------------------------------------------------------------------------------- /Tests/AppTests/Inputs/Input+GitHubIssues.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Input { 4 | 5 | /// https://github.com/johnpatrickmorgan/wtfautolayout/issues/1 6 | static let issue1 = """ 7 | ( 8 | "" 9 | ) 10 | """ 11 | 12 | /// https://github.com/johnpatrickmorgan/wtfautolayout/issues/2 13 | static let issue2 = """ 14 | ( 15 | "", 16 | "", 17 | "", 18 | "", 19 | "", 20 | "", 21 | "" 22 | ) 23 | """ 24 | 25 | /// https://github.com/johnpatrickmorgan/wtfautolayout/issues/4 26 | static let issue4 = """ 27 | ( 28 | "= 44.0>", 29 | "", 30 | "", 31 | "", 32 | "", 33 | "", 34 | "", 35 | "" 36 | ) 37 | """ 38 | 39 | /// https://github.com/johnpatrickmorgan/wtfautolayout/issues/5 40 | static let issue5 = """ 41 | ( 42 | "", 43 | "", 44 | "", 45 | "", 46 | "", 47 | "", 48 | "", 49 | "", 50 | "= UIImageView:0x123d8e1f0.trailing>" 51 | ) 52 | """ 53 | 54 | /// https://github.com/johnpatrickmorgan/wtfautolayout/issues/10 55 | static let issue10 = """ 56 | ( 57 | "", 58 | "", 59 | "", 60 | "", 61 | "", 62 | "", 63 | "", 64 | "", 65 | "", 66 | "", 67 | "<_UISystemBaselineConstraint:0x6000033499e0 'UISV-spacing' V:[UIStackView:0x7fa67350c680]-(NSLayoutAnchorConstraintSpace(8))-[UIButton:0x7fa67350bf50'E-posta adresini mi unutt...'] (active)>", 68 | "", 69 | "", 70 | "", 71 | "" 72 | ) 73 | """ 74 | 75 | static let issue13 = """ 76 | "", 77 | "", 78 | "", 79 | "", 80 | "" 81 | """ 82 | 83 | static let issue16 = """ 84 | "" 85 | """ 86 | 87 | static let issue18 = """ 88 | "", 89 | "", 90 | "", 91 | "", 92 | "", 93 | "", 94 | "", 95 | "" 96 | """ 97 | 98 | static let issue20 = """ 99 | ( 100 | "<_UISystemBaselineConstraint:0x2828051d0 V:|-(>=NSLayoutAnchorConstraintSpace(8))-[UILabel:0x111b35c20] (active, names: '|':ModuleName.ClassName:0x111b35a60 )>", 101 | "", 102 | "" 103 | ) 104 | """ 105 | } 106 | -------------------------------------------------------------------------------- /Tests/AppTests/Inputs/Input+Papertrail.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Input { 4 | 5 | // NOTE: This file is a collection of parsing errors from anonymised 6 | // server logs. 7 | 8 | // NSLayoutAnchorConstraintSpace(8) after equation constraint. 9 | static let papertrail1 = """ 10 | ( 11 | "<_UISystemBaselineConstraint:0x600002d1f4e0 SomeView:0x7fceff1c07a0.bottom <= UILayoutGuide:0x60000306d340'UIViewSafeAreaLayoutGuide'.bottom NSLayoutAnchorConstraintSpace(8) (active)>", 12 | "", 13 | "", 14 | "", 15 | "" 16 | ) 17 | """ 18 | 19 | // Single-quote mark in identifier 20 | static let papertrail2 = """ 21 | ( 22 | "", 23 | "", 24 | "", 25 | "", 26 | "", 27 | "" 28 | ) 29 | """ 30 | } 31 | -------------------------------------------------------------------------------- /Tests/AppTests/Inputs/Input+StackOverflow.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Input { 4 | 5 | static let so45163786 = """ 6 | ( 7 | "", 8 | "", 9 | "" 10 | ) 11 | """ 12 | 13 | static let so45452294 = """ 14 | ( 15 | "", 16 | "", 17 | "" 18 | ) 19 | """ 20 | 21 | static let so45474345 = """ 22 | ( 23 | "", 24 | "", 25 | "", 26 | "", 27 | "", 28 | "", 29 | "", 30 | "", 31 | "", 32 | "", 33 | "", 34 | "", 35 | "", 36 | "" 37 | ) 38 | """ 39 | 40 | static let so45487045 = """ 41 | ( 42 | "", 43 | "", 44 | "" 45 | ) 46 | """ 47 | } 48 | -------------------------------------------------------------------------------- /Tests/AppTests/ParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import App 3 | import Sparse 4 | import SnapshotTesting 5 | 6 | class ParserTests: XCTestCase { 7 | 8 | static let allTests = [ 9 | ("testCustomInputs", testCustomInputs), 10 | ("testStackOverflowInputs", testStackOverflowInputs) 11 | ] 12 | 13 | func testCustomInputs() throws { 14 | 15 | let inputs = [ 16 | Input.custom1, 17 | Input.custom2, 18 | Input.custom3, 19 | Input.custom4, 20 | Input.custom5, 21 | Input.custom6, 22 | Input.custom7, 23 | Input.custom8 24 | ] 25 | 26 | for input in inputs { 27 | let parsed = try ConstraintsParser.parse(input: input) 28 | assertSnapshot(matching: parsed.leafNode(), as: .json) 29 | } 30 | } 31 | 32 | func testGitHubIssues() throws { 33 | 34 | let inputs = [ 35 | Input.issue1, 36 | Input.issue2, 37 | Input.issue4, 38 | Input.issue5, 39 | Input.issue10, 40 | Input.issue13, 41 | Input.issue16, 42 | Input.issue18, 43 | Input.issue20 44 | ] 45 | 46 | for input in inputs { 47 | let parsed = try ConstraintsParser.parse(input: input) 48 | assertSnapshot(matching: parsed.leafNode(), as: .json) 49 | } 50 | } 51 | 52 | func testStackOverflowInputs() throws { 53 | 54 | let inputs = [ 55 | Input.so45163786, 56 | Input.so45452294, 57 | Input.so45474345, 58 | Input.so45487045 59 | ] 60 | 61 | for input in inputs { 62 | let parsed = try ConstraintsParser.parse(input: input) 63 | assertSnapshot(matching: parsed.leafNode(), as: .json) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/AppTests/ViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import App 3 | import Vapor 4 | import SnapshotTesting 5 | 6 | class ViewTests: XCTestCase { 7 | 8 | override static func setUp() { 9 | // SnapshotTesting.record = true 10 | } 11 | 12 | static let allTests = [ 13 | ("testHome", testHome), 14 | ("testAbout", testAbout), 15 | ("testPostError", testPostError), 16 | ("testPostOutput", testPostOutput), 17 | ("testQueryOutput", testQueryOutput), 18 | ] 19 | 20 | func testHome() throws { 21 | let app = try App.app(.testing) 22 | let response = try app.get() 23 | 24 | assertSnapshot(matching: response.description, as: .lines) 25 | } 26 | 27 | func testAbout() throws { 28 | let app = try App.app(.testing) 29 | let url = URL(string: "/about")! 30 | let response = try app.get(url: url) 31 | 32 | assertSnapshot(matching: response.description, as: .lines) 33 | } 34 | 35 | func testPostError() throws { 36 | let inputs = [ 37 | Input.invalidHexitError 38 | ] 39 | 40 | for input in inputs { 41 | let app = try App.app(.testing) 42 | let body = ConstraintLogRequest(constraintlog: input) 43 | let response = try app.postJSON(body: body) 44 | 45 | assertSnapshot(matching: response.description, as: .lines) 46 | } 47 | } 48 | 49 | func testPostOutput() throws { 50 | let inputs = [ 51 | Input.custom1, 52 | Input.custom2, 53 | Input.custom3, 54 | Input.custom4, 55 | Input.custom5, 56 | Input.custom6, 57 | Input.custom7, 58 | Input.custom8, 59 | Input.custom9 60 | ] 61 | 62 | for input in inputs { 63 | let app = try App.app(.testing) 64 | let body = ConstraintLogRequest(constraintlog: input) 65 | let response = try app.postJSON(body: body) 66 | 67 | assertSnapshot(matching: response.description, as: .lines) 68 | } 69 | } 70 | 71 | func testQueryOutput() throws { 72 | let inputs = [ 73 | Input.custom1, 74 | Input.custom2, 75 | Input.custom3, 76 | Input.custom4, 77 | Input.custom5, 78 | Input.custom6, 79 | Input.custom7, 80 | Input.custom8 81 | ] 82 | 83 | for input in inputs { 84 | let app = try App.app(.testing) 85 | let log = input.addingPercentEncoding(withAllowedCharacters: .urlQueryArgumentAllowed)! 86 | let url = URL(string: "/?constraintlog=\(log)")! 87 | let response = try app.get(url: url) 88 | 89 | assertSnapshot(matching: response.description, as: .lines) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testCustomInputs.1.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "value" : "24" 6 | }, 7 | "description" : "StackView<\/code>'s height should equal 24.", 8 | "first" : { 9 | "attribute" : { 10 | "includesMargin" : false, 11 | "name" : "height" 12 | }, 13 | "instance" : { 14 | "address" : "0x7fa4d0dece70", 15 | "className" : "UIStackView", 16 | "color" : "rgb(241,196,15)", 17 | "initial" : "S", 18 | "name" : "StackView", 19 | "suffix" : "" 20 | } 21 | }, 22 | "identity" : { 23 | "address" : "0x6080004835c0", 24 | "className" : "NSLayoutConstraint", 25 | "color" : "rgb(26,188,156)", 26 | "initial" : "N", 27 | "name" : "NSLayoutConstraint" 28 | }, 29 | "relation" : "==" 30 | }, 31 | { 32 | "constant" : { 33 | "prefix" : "+ ", 34 | "value" : "21" 35 | }, 36 | "description" : "StackView<\/code>'s top edge should equal HeaderView<\/code>'s top edge plus 21.", 37 | "first" : { 38 | "attribute" : { 39 | "includesMargin" : false, 40 | "name" : "top" 41 | }, 42 | "instance" : { 43 | "address" : "0x7fa4d0dece70", 44 | "className" : "UIStackView", 45 | "color" : "rgb(241,196,15)", 46 | "initial" : "S", 47 | "name" : "StackView", 48 | "suffix" : "" 49 | } 50 | }, 51 | "identity" : { 52 | "address" : "0x608000687440", 53 | "className" : "NSLayoutConstraint", 54 | "color" : "rgb(26,188,156)", 55 | "initial" : "N", 56 | "name" : "NSLayoutConstraint" 57 | }, 58 | "relation" : "==", 59 | "second" : { 60 | "attribute" : { 61 | "includesMargin" : false, 62 | "name" : "top" 63 | }, 64 | "instance" : { 65 | "address" : "0x7fa4d0df1370", 66 | "className" : "Proto.HeaderView", 67 | "color" : "rgb(231,76,60)", 68 | "initial" : "H", 69 | "name" : "HeaderView", 70 | "suffix" : "" 71 | } 72 | } 73 | }, 74 | { 75 | "constant" : { 76 | "prefix" : "+ ", 77 | "value" : "9" 78 | }, 79 | "description" : "HeaderView<\/code>'s bottom edge should equal StackView<\/code>'s bottom edge plus 9.", 80 | "first" : { 81 | "attribute" : { 82 | "includesMargin" : false, 83 | "name" : "bottom" 84 | }, 85 | "instance" : { 86 | "address" : "0x7fa4d0df1370", 87 | "className" : "Proto.HeaderView", 88 | "color" : "rgb(231,76,60)", 89 | "initial" : "H", 90 | "name" : "HeaderView", 91 | "suffix" : "" 92 | } 93 | }, 94 | "identity" : { 95 | "address" : "0x608000890860", 96 | "className" : "NSLayoutConstraint", 97 | "color" : "rgb(26,188,156)", 98 | "initial" : "N", 99 | "name" : "NSLayoutConstraint" 100 | }, 101 | "relation" : "==", 102 | "second" : { 103 | "attribute" : { 104 | "includesMargin" : false, 105 | "name" : "bottom" 106 | }, 107 | "instance" : { 108 | "address" : "0x7fa4d0dece70", 109 | "className" : "UIStackView", 110 | "color" : "rgb(241,196,15)", 111 | "initial" : "S", 112 | "name" : "StackView", 113 | "suffix" : "" 114 | } 115 | } 116 | }, 117 | { 118 | "constant" : { 119 | "value" : "0" 120 | }, 121 | "description" : "HeaderView<\/code>'s height should equal 0.", 122 | "first" : { 123 | "attribute" : { 124 | "includesMargin" : false, 125 | "name" : "height" 126 | }, 127 | "instance" : { 128 | "address" : "0x7fa4d0df1370", 129 | "className" : "Proto.HeaderView", 130 | "color" : "rgb(231,76,60)", 131 | "initial" : "H", 132 | "name" : "HeaderView", 133 | "suffix" : "" 134 | } 135 | }, 136 | "footnote" : { 137 | "marker" : "†", 138 | "text" : "This constraint was added by a table or collection view to enforce its cell size." 139 | }, 140 | "identity" : { 141 | "address" : "0x600000a85d70", 142 | "className" : "NSLayoutConstraint", 143 | "color" : "rgb(26,188,156)", 144 | "identifier" : "UIView-Encapsulated-Layout-Height", 145 | "initial" : "U", 146 | "name" : "UIView-Encapsulated-Layout-Height" 147 | }, 148 | "relation" : "==" 149 | } 150 | ], 151 | "footnotes" : [ 152 | { 153 | "marker" : "†", 154 | "text" : "This constraint was added by a table or collection view to enforce its cell size." 155 | } 156 | ] 157 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testCustomInputs.2.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "description" : "MyCircleView<\/code>'s leading edge should equal name_label<\/code>'s leading edge.", 5 | "first" : { 6 | "attribute" : { 7 | "includesMargin" : false, 8 | "name" : "leading" 9 | }, 10 | "instance" : { 11 | "address" : "0x7fa4d0e8b8e0", 12 | "className" : "MyCircleView", 13 | "color" : "rgb(241,196,15)", 14 | "initial" : "M", 15 | "name" : "MyCircleView", 16 | "suffix" : "" 17 | } 18 | }, 19 | "identity" : { 20 | "address" : "0x600000685780", 21 | "className" : "NSLayoutConstraint", 22 | "color" : "rgb(26,188,156)", 23 | "initial" : "N", 24 | "name" : "NSLayoutConstraint" 25 | }, 26 | "relation" : "==", 27 | "second" : { 28 | "attribute" : { 29 | "includesMargin" : false, 30 | "name" : "leading" 31 | }, 32 | "instance" : { 33 | "address" : "0x7fa4d0e9e480", 34 | "className" : "name_label", 35 | "color" : "rgb(231,76,60)", 36 | "initial" : "N", 37 | "name" : "name_label", 38 | "suffix" : "" 39 | } 40 | } 41 | }, 42 | { 43 | "constant" : { 44 | "prefix" : "+ ", 45 | "value" : "6" 46 | }, 47 | "description" : "MyCircleView<\/code>'s leading edge should equal OverViewFactorLabel<\/code>'s leading edge plus 6.", 48 | "first" : { 49 | "attribute" : { 50 | "includesMargin" : false, 51 | "name" : "leading" 52 | }, 53 | "instance" : { 54 | "address" : "0x7fa4d0e8b8e0", 55 | "className" : "MyCircleView", 56 | "color" : "rgb(241,196,15)", 57 | "initial" : "M", 58 | "name" : "MyCircleView", 59 | "suffix" : "" 60 | } 61 | }, 62 | "identity" : { 63 | "address" : "0x6000006857d0", 64 | "className" : "NSLayoutConstraint", 65 | "color" : "rgb(26,188,156)", 66 | "initial" : "N", 67 | "name" : "NSLayoutConstraint" 68 | }, 69 | "relation" : "==", 70 | "second" : { 71 | "attribute" : { 72 | "includesMargin" : false, 73 | "name" : "leading" 74 | }, 75 | "instance" : { 76 | "address" : "0x7fa4d0e8b450", 77 | "className" : "OverViewFactorLabel", 78 | "color" : "rgb(46,204,113)", 79 | "initial" : "O", 80 | "name" : "OverViewFactorLabel", 81 | "suffix" : "" 82 | } 83 | } 84 | }, 85 | { 86 | "description" : "name_label<\/code>'s leading edge should equal OverViewFactorLabel<\/code>'s leading edge.", 87 | "first" : { 88 | "attribute" : { 89 | "includesMargin" : false, 90 | "name" : "leading" 91 | }, 92 | "instance" : { 93 | "address" : "0x7fa4d0e9e480", 94 | "className" : "name_label", 95 | "color" : "rgb(231,76,60)", 96 | "initial" : "N", 97 | "name" : "name_label", 98 | "suffix" : "" 99 | } 100 | }, 101 | "identity" : { 102 | "address" : "0x600000482e90", 103 | "className" : "NSLayoutConstraint", 104 | "color" : "rgb(26,188,156)", 105 | "initial" : "N", 106 | "name" : "NSLayoutConstraint" 107 | }, 108 | "relation" : "==", 109 | "second" : { 110 | "attribute" : { 111 | "includesMargin" : false, 112 | "name" : "leading" 113 | }, 114 | "instance" : { 115 | "address" : "0x7fa4d0e8b450", 116 | "className" : "OverViewFactorLabel", 117 | "color" : "rgb(46,204,113)", 118 | "initial" : "O", 119 | "name" : "OverViewFactorLabel", 120 | "suffix" : "" 121 | } 122 | } 123 | } 124 | ], 125 | "footnotes" : [ 126 | 127 | ] 128 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testCustomInputs.3.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "prefix" : "+ ", 6 | "value" : "3" 7 | }, 8 | "description" : "View1<\/code>'s bottom edge should equal VeryLargeLabelWithBorder<\/code>'s bottom edge plus 3.", 9 | "first" : { 10 | "attribute" : { 11 | "includesMargin" : false, 12 | "name" : "bottom" 13 | }, 14 | "instance" : { 15 | "address" : "0x7fa4d6079490", 16 | "className" : "UIView", 17 | "color" : "rgb(142,68,173)", 18 | "initial" : "V", 19 | "name" : "View", 20 | "suffix" : "1" 21 | } 22 | }, 23 | "identity" : { 24 | "address" : "0x608000880dc0", 25 | "className" : "NSLayoutConstraint", 26 | "color" : "rgb(26,188,156)", 27 | "initial" : "N", 28 | "name" : "NSLayoutConstraint" 29 | }, 30 | "relation" : "==", 31 | "second" : { 32 | "attribute" : { 33 | "includesMargin" : false, 34 | "name" : "bottom" 35 | }, 36 | "instance" : { 37 | "address" : "0x7fa4d60764b0", 38 | "className" : "VeryLargeLabelWithBorder...", 39 | "color" : "rgb(211,84,0)", 40 | "initial" : "V", 41 | "name" : "VeryLargeLabelWithBorder", 42 | "suffix" : "" 43 | } 44 | } 45 | }, 46 | { 47 | "constant" : { 48 | "prefix" : "+ ", 49 | "value" : "3" 50 | }, 51 | "description" : "VeryLargeLabelWithBorder<\/code>'s top edge should equal View1<\/code>'s top edge plus 3.", 52 | "first" : { 53 | "attribute" : { 54 | "includesMargin" : false, 55 | "name" : "top" 56 | }, 57 | "instance" : { 58 | "address" : "0x7fa4d60764b0", 59 | "className" : "VeryLargeLabelWithBorder...", 60 | "color" : "rgb(211,84,0)", 61 | "initial" : "V", 62 | "name" : "VeryLargeLabelWithBorder", 63 | "suffix" : "" 64 | } 65 | }, 66 | "identity" : { 67 | "address" : "0x60800068f000", 68 | "className" : "NSLayoutConstraint", 69 | "color" : "rgb(26,188,156)", 70 | "initial" : "N", 71 | "name" : "NSLayoutConstraint" 72 | }, 73 | "relation" : "==", 74 | "second" : { 75 | "attribute" : { 76 | "includesMargin" : false, 77 | "name" : "top" 78 | }, 79 | "instance" : { 80 | "address" : "0x7fa4d6079490", 81 | "className" : "UIView", 82 | "color" : "rgb(142,68,173)", 83 | "initial" : "V", 84 | "name" : "View", 85 | "suffix" : "1" 86 | } 87 | } 88 | }, 89 | { 90 | "constant" : { 91 | "value" : "0" 92 | }, 93 | "description" : "View2<\/code>'s height should equal 0.", 94 | "first" : { 95 | "attribute" : { 96 | "includesMargin" : false, 97 | "name" : "height" 98 | }, 99 | "instance" : { 100 | "address" : "0x7fa4d6079060", 101 | "className" : "UIView", 102 | "color" : "rgb(26,188,156)", 103 | "initial" : "V", 104 | "name" : "View", 105 | "suffix" : "2" 106 | } 107 | }, 108 | "identity" : { 109 | "address" : "0x608000695130", 110 | "className" : "NSLayoutConstraint", 111 | "color" : "rgb(26,188,156)", 112 | "initial" : "N", 113 | "name" : "NSLayoutConstraint" 114 | }, 115 | "relation" : "==" 116 | }, 117 | { 118 | "description" : "View2<\/code>'s bottom edge should equal View1<\/code>'s bottom edge.", 119 | "first" : { 120 | "attribute" : { 121 | "includesMargin" : false, 122 | "name" : "bottom" 123 | }, 124 | "instance" : { 125 | "address" : "0x7fa4d6079060", 126 | "className" : "UIView", 127 | "color" : "rgb(26,188,156)", 128 | "initial" : "V", 129 | "name" : "View", 130 | "suffix" : "2" 131 | } 132 | }, 133 | "identity" : { 134 | "address" : "0x608000887620", 135 | "className" : "NSLayoutConstraint", 136 | "color" : "rgb(26,188,156)", 137 | "initial" : "N", 138 | "name" : "NSLayoutConstraint" 139 | }, 140 | "relation" : "==", 141 | "second" : { 142 | "attribute" : { 143 | "includesMargin" : false, 144 | "name" : "bottom" 145 | }, 146 | "instance" : { 147 | "address" : "0x7fa4d6079490", 148 | "className" : "UIView", 149 | "color" : "rgb(142,68,173)", 150 | "initial" : "V", 151 | "name" : "View", 152 | "suffix" : "1" 153 | } 154 | } 155 | }, 156 | { 157 | "description" : "View1<\/code>'s top edge should equal View2<\/code>'s top edge.", 158 | "first" : { 159 | "attribute" : { 160 | "includesMargin" : false, 161 | "name" : "top" 162 | }, 163 | "instance" : { 164 | "address" : "0x7fa4d6079490", 165 | "className" : "UIView", 166 | "color" : "rgb(142,68,173)", 167 | "initial" : "V", 168 | "name" : "View", 169 | "suffix" : "1" 170 | } 171 | }, 172 | "identity" : { 173 | "address" : "0x608000699be0", 174 | "className" : "NSLayoutConstraint", 175 | "color" : "rgb(26,188,156)", 176 | "initial" : "N", 177 | "name" : "NSLayoutConstraint" 178 | }, 179 | "relation" : "==", 180 | "second" : { 181 | "attribute" : { 182 | "includesMargin" : false, 183 | "name" : "top" 184 | }, 185 | "instance" : { 186 | "address" : "0x7fa4d6079060", 187 | "className" : "UIView", 188 | "color" : "rgb(26,188,156)", 189 | "initial" : "V", 190 | "name" : "View", 191 | "suffix" : "2" 192 | } 193 | } 194 | } 195 | ], 196 | "footnotes" : [ 197 | 198 | ] 199 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testCustomInputs.4.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "value" : "0" 6 | }, 7 | "description" : "_UILayoutGuide<\/code>'s height should equal 0.", 8 | "first" : { 9 | "attribute" : { 10 | "includesMargin" : false, 11 | "name" : "height" 12 | }, 13 | "instance" : { 14 | "address" : "0x1271a5500", 15 | "className" : "_UILayoutGuide", 16 | "color" : "rgb(22,160,133)", 17 | "initial" : "U", 18 | "name" : "_UILayoutGuide", 19 | "suffix" : "" 20 | } 21 | }, 22 | "identity" : { 23 | "address" : "0x1272e5820", 24 | "className" : "_UILayoutSupportConstraint", 25 | "color" : "rgb(26,188,156)", 26 | "initial" : "U", 27 | "name" : "_UILayoutSupportConstraint" 28 | }, 29 | "relation" : "==" 30 | }, 31 | { 32 | "description" : "_UILayoutGuide<\/code>'s bottom edge should equal View<\/code>'s bottom edge.", 33 | "first" : { 34 | "attribute" : { 35 | "includesMargin" : false, 36 | "name" : "bottom" 37 | }, 38 | "instance" : { 39 | "address" : "0x1271a5500", 40 | "className" : "_UILayoutGuide", 41 | "color" : "rgb(22,160,133)", 42 | "initial" : "U", 43 | "name" : "_UILayoutGuide", 44 | "suffix" : "" 45 | } 46 | }, 47 | "identity" : { 48 | "address" : "0x1272cd510", 49 | "className" : "_UILayoutSupportConstraint", 50 | "color" : "rgb(26,188,156)", 51 | "initial" : "U", 52 | "name" : "_UILayoutSupportConstraint" 53 | }, 54 | "relation" : "==", 55 | "second" : { 56 | "attribute" : { 57 | "includesMargin" : false, 58 | "name" : "bottom" 59 | }, 60 | "instance" : { 61 | "address" : "0x128321830", 62 | "className" : "UIView", 63 | "color" : "rgb(41,128,185)", 64 | "initial" : "V", 65 | "name" : "View", 66 | "suffix" : "" 67 | } 68 | } 69 | }, 70 | { 71 | "constant" : { 72 | "value" : "230" 73 | }, 74 | "description" : "CollectionView<\/code>'s height should equal 230.", 75 | "first" : { 76 | "attribute" : { 77 | "includesMargin" : false, 78 | "name" : "height" 79 | }, 80 | "instance" : { 81 | "address" : "0x126963e00", 82 | "className" : "UICollectionView", 83 | "color" : "rgb(243,156,18)", 84 | "initial" : "C", 85 | "name" : "CollectionView", 86 | "suffix" : "" 87 | } 88 | }, 89 | "identity" : { 90 | "address" : "0x127225340", 91 | "className" : "NSLayoutConstraint", 92 | "color" : "rgb(26,188,156)", 93 | "initial" : "N", 94 | "name" : "NSLayoutConstraint" 95 | }, 96 | "relation" : "==" 97 | }, 98 | { 99 | "constant" : { 100 | "prefix" : "+ ", 101 | "value" : "87" 102 | }, 103 | "description" : "_UILayoutGuide<\/code>'s top edge should equal CollectionView<\/code>'s bottom edge plus 87.", 104 | "first" : { 105 | "attribute" : { 106 | "includesMargin" : false, 107 | "name" : "top" 108 | }, 109 | "instance" : { 110 | "address" : "0x1271a5500", 111 | "className" : "_UILayoutGuide", 112 | "color" : "rgb(22,160,133)", 113 | "initial" : "U", 114 | "name" : "_UILayoutGuide", 115 | "suffix" : "" 116 | } 117 | }, 118 | "identity" : { 119 | "address" : "0x128325fd0", 120 | "className" : "NSLayoutConstraint", 121 | "color" : "rgb(26,188,156)", 122 | "initial" : "N", 123 | "name" : "NSLayoutConstraint" 124 | }, 125 | "relation" : "==", 126 | "second" : { 127 | "attribute" : { 128 | "includesMargin" : false, 129 | "name" : "bottom" 130 | }, 131 | "instance" : { 132 | "address" : "0x126963e00", 133 | "className" : "UICollectionView", 134 | "color" : "rgb(243,156,18)", 135 | "initial" : "C", 136 | "name" : "CollectionView", 137 | "suffix" : "" 138 | } 139 | } 140 | }, 141 | { 142 | "constant" : { 143 | "value" : "568" 144 | }, 145 | "description" : "View<\/code>'s height should equal 568.", 146 | "first" : { 147 | "attribute" : { 148 | "includesMargin" : false, 149 | "name" : "height" 150 | }, 151 | "instance" : { 152 | "address" : "0x128321830", 153 | "className" : "UIView", 154 | "color" : "rgb(41,128,185)", 155 | "initial" : "V", 156 | "name" : "View", 157 | "suffix" : "" 158 | } 159 | }, 160 | "footnote" : { 161 | "marker" : "†", 162 | "text" : "This constraint was added by a table or collection view to enforce its cell size." 163 | }, 164 | "identity" : { 165 | "address" : "0x1272e51f0", 166 | "className" : "NSLayoutConstraint", 167 | "color" : "rgb(26,188,156)", 168 | "identifier" : "UIView-Encapsulated-Layout-Height", 169 | "initial" : "U", 170 | "name" : "UIView-Encapsulated-Layout-Height" 171 | }, 172 | "relation" : "==" 173 | }, 174 | { 175 | "description" : "CollectionView<\/code>'s bottom edge should equal View<\/code>'s bottom edge.", 176 | "first" : { 177 | "attribute" : { 178 | "includesMargin" : false, 179 | "name" : "bottom" 180 | }, 181 | "instance" : { 182 | "address" : "0x126963e00", 183 | "className" : "UICollectionView", 184 | "color" : "rgb(243,156,18)", 185 | "initial" : "C", 186 | "name" : "CollectionView", 187 | "suffix" : "" 188 | } 189 | }, 190 | "footnote" : { 191 | "marker" : "*", 192 | "text" : "This constraint was translated from an autoresizing mask. If this was not intended, try setting translatesAutoresizingMaskIntoConstraints<\/code> to false on this view." 193 | }, 194 | "identity" : { 195 | "address" : "0x1272ed5f0", 196 | "className" : "NSAutoresizingMaskLayoutConstraint", 197 | "color" : "rgb(26,188,156)", 198 | "identifier" : "UIView-Encapsulated-Layout-Top", 199 | "initial" : "U", 200 | "name" : "UIView-Encapsulated-Layout-Top" 201 | }, 202 | "relation" : "==", 203 | "second" : { 204 | "attribute" : { 205 | "includesMargin" : false, 206 | "name" : "bottom" 207 | }, 208 | "instance" : { 209 | "address" : "0x128321830", 210 | "className" : "UIView", 211 | "color" : "rgb(41,128,185)", 212 | "initial" : "V", 213 | "name" : "View", 214 | "suffix" : "" 215 | } 216 | } 217 | } 218 | ], 219 | "footnotes" : [ 220 | { 221 | "marker" : "†", 222 | "text" : "This constraint was added by a table or collection view to enforce its cell size." 223 | }, 224 | { 225 | "marker" : "*", 226 | "text" : "This constraint was translated from an autoresizing mask. If this was not intended, try setting translatesAutoresizingMaskIntoConstraints<\/code> to false on this view." 227 | } 228 | ] 229 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testCustomInputs.5.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "value" : "70" 6 | }, 7 | "description" : "View<\/code>'s width should equal 70.", 8 | "first" : { 9 | "attribute" : { 10 | "includesMargin" : false, 11 | "name" : "width" 12 | }, 13 | "instance" : { 14 | "address" : "0x7f95000e5ac0", 15 | "className" : "UIView", 16 | "color" : "rgb(41,128,185)", 17 | "initial" : "V", 18 | "name" : "View", 19 | "suffix" : "" 20 | } 21 | }, 22 | "footnote" : { 23 | "marker" : "*", 24 | "text" : "This constraint was translated from an autoresizing mask. If this was not intended, try setting translatesAutoresizingMaskIntoConstraints<\/code> to false on this view." 25 | }, 26 | "identity" : { 27 | "address" : "0x60000089a130", 28 | "className" : "NSAutoresizingMaskLayoutConstraint", 29 | "color" : "rgb(26,188,156)", 30 | "initial" : "N", 31 | "name" : "NSAutoresizingMaskLayoutConstraint" 32 | }, 33 | "relation" : "==" 34 | }, 35 | { 36 | "constant" : { 37 | "value" : "54" 38 | }, 39 | "description" : "DetailViewFeatureItemCell<\/code>'s width should equal 54.", 40 | "first" : { 41 | "attribute" : { 42 | "includesMargin" : false, 43 | "name" : "width" 44 | }, 45 | "instance" : { 46 | "address" : "0x7f95000e5e50", 47 | "className" : "DetailViewFeatureItemCell...", 48 | "color" : "rgb(243,156,18)", 49 | "initial" : "D", 50 | "name" : "DetailViewFeatureItemCell", 51 | "suffix" : "" 52 | } 53 | }, 54 | "identity" : { 55 | "address" : "0x6000008989c0", 56 | "className" : "NSLayoutConstraint", 57 | "color" : "rgb(26,188,156)", 58 | "initial" : "N", 59 | "name" : "NSLayoutConstraint" 60 | }, 61 | "relation" : "==" 62 | }, 63 | { 64 | "description" : "DetailViewFeatureItemCell<\/code>'s horizontal center should equal View<\/code>'s horizontal center.", 65 | "first" : { 66 | "attribute" : { 67 | "includesMargin" : false, 68 | "name" : "centerX" 69 | }, 70 | "instance" : { 71 | "address" : "0x7f95000e5e50", 72 | "className" : "DetailViewFeatureItemCell...", 73 | "color" : "rgb(243,156,18)", 74 | "initial" : "D", 75 | "name" : "DetailViewFeatureItemCell", 76 | "suffix" : "" 77 | } 78 | }, 79 | "identity" : { 80 | "address" : "0x600000898b50", 81 | "className" : "NSLayoutConstraint", 82 | "color" : "rgb(26,188,156)", 83 | "initial" : "N", 84 | "name" : "NSLayoutConstraint" 85 | }, 86 | "relation" : "==", 87 | "second" : { 88 | "attribute" : { 89 | "includesMargin" : false, 90 | "name" : "centerX" 91 | }, 92 | "instance" : { 93 | "address" : "0x7f95000e5ac0", 94 | "className" : "UIView", 95 | "color" : "rgb(41,128,185)", 96 | "initial" : "V", 97 | "name" : "View", 98 | "suffix" : "" 99 | } 100 | } 101 | }, 102 | { 103 | "constant" : { 104 | "prefix" : "- ", 105 | "value" : "2" 106 | }, 107 | "description" : "View<\/code>'s trailing edge should equal DetailViewFeatureItemCell<\/code>'s trailing edge minus 2.", 108 | "first" : { 109 | "attribute" : { 110 | "includesMargin" : false, 111 | "name" : "trailing" 112 | }, 113 | "instance" : { 114 | "address" : "0x7f95000e5ac0", 115 | "className" : "UIView", 116 | "color" : "rgb(41,128,185)", 117 | "initial" : "V", 118 | "name" : "View", 119 | "suffix" : "" 120 | } 121 | }, 122 | "identity" : { 123 | "address" : "0x600000898c90", 124 | "className" : "NSLayoutConstraint", 125 | "color" : "rgb(26,188,156)", 126 | "initial" : "N", 127 | "name" : "NSLayoutConstraint" 128 | }, 129 | "relation" : "==", 130 | "second" : { 131 | "attribute" : { 132 | "includesMargin" : false, 133 | "name" : "trailing" 134 | }, 135 | "instance" : { 136 | "address" : "0x7f95000e5e50", 137 | "className" : "DetailViewFeatureItemCell...", 138 | "color" : "rgb(243,156,18)", 139 | "initial" : "D", 140 | "name" : "DetailViewFeatureItemCell", 141 | "suffix" : "" 142 | } 143 | } 144 | } 145 | ], 146 | "footnotes" : [ 147 | { 148 | "marker" : "*", 149 | "text" : "This constraint was translated from an autoresizing mask. If this was not intended, try setting translatesAutoresizingMaskIntoConstraints<\/code> to false on this view." 150 | } 151 | ] 152 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testCustomInputs.7.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "value" : "60" 6 | }, 7 | "description" : "ImageView<\/code>'s height should equal 60.", 8 | "first" : { 9 | "attribute" : { 10 | "includesMargin" : false, 11 | "name" : "height" 12 | }, 13 | "instance" : { 14 | "address" : "0x7fd382f50", 15 | "className" : "UIImageView", 16 | "color" : "rgb(41,128,185)", 17 | "initial" : "I", 18 | "name" : "ImageView", 19 | "suffix" : "" 20 | } 21 | }, 22 | "identity" : { 23 | "address" : "0x69c2f0", 24 | "className" : "NSLayoutConstraint", 25 | "color" : "rgb(26,188,156)", 26 | "initial" : "N", 27 | "name" : "NSLayoutConstraint" 28 | }, 29 | "relation" : "==" 30 | }, 31 | { 32 | "description" : "ImageView<\/code>'s top edge should equal TableViewCell<\/code>'s top edge.", 33 | "first" : { 34 | "attribute" : { 35 | "includesMargin" : false, 36 | "name" : "top" 37 | }, 38 | "instance" : { 39 | "address" : "0x7fd382f50", 40 | "className" : "UIImageView", 41 | "color" : "rgb(41,128,185)", 42 | "initial" : "I", 43 | "name" : "ImageView", 44 | "suffix" : "" 45 | } 46 | }, 47 | "identity" : { 48 | "address" : "0x69a8b0", 49 | "className" : "NSLayoutConstraint", 50 | "color" : "rgb(26,188,156)", 51 | "initial" : "N", 52 | "name" : "NSLayoutConstraint" 53 | }, 54 | "relation" : "==", 55 | "second" : { 56 | "attribute" : { 57 | "includesMargin" : false, 58 | "name" : "top" 59 | }, 60 | "instance" : { 61 | "address" : "0x7fd39b20", 62 | "className" : "UITableViewCell", 63 | "color" : "rgb(243,156,18)", 64 | "initial" : "T", 65 | "name" : "TableViewCell", 66 | "suffix" : "" 67 | } 68 | } 69 | }, 70 | { 71 | "description" : "TableViewCell<\/code>'s bottom edge should equal ImageView<\/code>'s bottom edge.", 72 | "first" : { 73 | "attribute" : { 74 | "includesMargin" : false, 75 | "name" : "bottom" 76 | }, 77 | "instance" : { 78 | "address" : "0x7fd39b20", 79 | "className" : "UITableViewCell", 80 | "color" : "rgb(243,156,18)", 81 | "initial" : "T", 82 | "name" : "TableViewCell", 83 | "suffix" : "" 84 | } 85 | }, 86 | "identity" : { 87 | "address" : "0x697ca0", 88 | "className" : "NSLayoutConstraint", 89 | "color" : "rgb(26,188,156)", 90 | "initial" : "N", 91 | "name" : "NSLayoutConstraint" 92 | }, 93 | "relation" : "==", 94 | "second" : { 95 | "attribute" : { 96 | "includesMargin" : false, 97 | "name" : "bottom" 98 | }, 99 | "instance" : { 100 | "address" : "0x7fd382f50", 101 | "className" : "UIImageView", 102 | "color" : "rgb(41,128,185)", 103 | "initial" : "I", 104 | "name" : "ImageView", 105 | "suffix" : "" 106 | } 107 | } 108 | }, 109 | { 110 | "constant" : { 111 | "value" : "80" 112 | }, 113 | "description" : "TableViewCell<\/code>'s height should equal 80.", 114 | "first" : { 115 | "attribute" : { 116 | "includesMargin" : false, 117 | "name" : "height" 118 | }, 119 | "instance" : { 120 | "address" : "0x7fd39b20", 121 | "className" : "UITableViewCell", 122 | "color" : "rgb(243,156,18)", 123 | "initial" : "T", 124 | "name" : "TableViewCell", 125 | "suffix" : "" 126 | } 127 | }, 128 | "footnote" : { 129 | "marker" : "†", 130 | "text" : "This constraint was added by a table or collection view to enforce its cell size." 131 | }, 132 | "identity" : { 133 | "address" : "0x697d40", 134 | "className" : "NSLayoutConstraint", 135 | "color" : "rgb(26,188,156)", 136 | "identifier" : "UIView-Encapsulated-Layout-Height", 137 | "initial" : "U", 138 | "name" : "UIView-Encapsulated-Layout-Height" 139 | }, 140 | "relation" : "==" 141 | } 142 | ], 143 | "footnotes" : [ 144 | { 145 | "marker" : "†", 146 | "text" : "This constraint was added by a table or collection view to enforce its cell size." 147 | } 148 | ] 149 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testCustomInputs.8.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "prefix" : "+ ", 6 | "value" : "7" 7 | }, 8 | "description" : "ListViewItemLabel<\/code>'s top edge should equal ListViewItemLabel<\/code>'s bottom edge plus 7.", 9 | "first" : { 10 | "attribute" : { 11 | "includesMargin" : false, 12 | "name" : "top" 13 | }, 14 | "instance" : { 15 | "address" : "0x7f950028e5b0", 16 | "className" : "ListViewItemLabel...", 17 | "color" : "rgb(243,156,18)", 18 | "initial" : "L", 19 | "name" : "ListViewItemLabel", 20 | "suffix" : "" 21 | } 22 | }, 23 | "identity" : { 24 | "address" : "0x608000696990", 25 | "className" : "NSLayoutConstraint", 26 | "color" : "rgb(26,188,156)", 27 | "initial" : "N", 28 | "name" : "NSLayoutConstraint" 29 | }, 30 | "relation" : "==", 31 | "second" : { 32 | "attribute" : { 33 | "includesMargin" : false, 34 | "name" : "bottom" 35 | }, 36 | "instance" : { 37 | "address" : "0x7f950028e5b0", 38 | "className" : "ListViewItemLabel...", 39 | "color" : "rgb(243,156,18)", 40 | "initial" : "L", 41 | "name" : "ListViewItemLabel", 42 | "suffix" : "" 43 | } 44 | } 45 | } 46 | ], 47 | "footnotes" : [ 48 | 49 | ] 50 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testGitHubIssues.1.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "value" : "5" 6 | }, 7 | "description" : "progress background<\/code>'s height should equal 5.", 8 | "first" : { 9 | "attribute" : { 10 | "includesMargin" : false, 11 | "name" : "height" 12 | }, 13 | "instance" : { 14 | "address" : "0x1089a5290", 15 | "className" : "progress background", 16 | "color" : "rgb(243,156,18)", 17 | "initial" : "P", 18 | "name" : "progress background", 19 | "suffix" : "" 20 | } 21 | }, 22 | "identity" : { 23 | "address" : "0x17468b860", 24 | "className" : "NSLayoutConstraint", 25 | "color" : "rgb(26,188,156)", 26 | "initial" : "N", 27 | "name" : "NSLayoutConstraint" 28 | }, 29 | "relation" : "==" 30 | } 31 | ], 32 | "footnotes" : [ 33 | 34 | ] 35 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testGitHubIssues.2.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "value" : "234" 6 | }, 7 | "description" : "AWPProductOnMapView<\/code>'s height should equal 234.", 8 | "first" : { 9 | "attribute" : { 10 | "includesMargin" : false, 11 | "name" : "height" 12 | }, 13 | "instance" : { 14 | "address" : "0x4dbb090", 15 | "className" : "AWPProductOnMapView", 16 | "color" : "rgb(155,89,182)", 17 | "initial" : "A", 18 | "name" : "AWPProductOnMapView", 19 | "suffix" : "" 20 | } 21 | }, 22 | "identity" : { 23 | "address" : "0x181af7c0", 24 | "className" : "NSAutoresizingMaskLayoutConstraint", 25 | "color" : "rgb(26,188,156)", 26 | "initial" : "N", 27 | "name" : "NSAutoresizingMaskLayoutConstraint" 28 | }, 29 | "relation" : "==" 30 | }, 31 | { 32 | "description" : "ImageView<\/code>'s width should equal ImageView<\/code>'s height multiplied by 0.385.", 33 | "first" : { 34 | "attribute" : { 35 | "includesMargin" : false, 36 | "name" : "width" 37 | }, 38 | "instance" : { 39 | "address" : "0x4db3270", 40 | "className" : "UIImageView", 41 | "color" : "rgb(230,126,34)", 42 | "initial" : "I", 43 | "name" : "ImageView", 44 | "suffix" : "" 45 | } 46 | }, 47 | "identity" : { 48 | "address" : "0x181cef70", 49 | "className" : "NSLayoutConstraint", 50 | "color" : "rgb(26,188,156)", 51 | "initial" : "N", 52 | "name" : "NSLayoutConstraint" 53 | }, 54 | "multiplier" : "* 0.385", 55 | "relation" : "==", 56 | "second" : { 57 | "attribute" : { 58 | "includesMargin" : false, 59 | "name" : "height" 60 | }, 61 | "instance" : { 62 | "address" : "0x4db3270", 63 | "className" : "UIImageView", 64 | "color" : "rgb(230,126,34)", 65 | "initial" : "I", 66 | "name" : "ImageView", 67 | "suffix" : "" 68 | } 69 | } 70 | }, 71 | { 72 | "constant" : { 73 | "value" : "85" 74 | }, 75 | "description" : "ImageView<\/code>'s width should equal 85.", 76 | "first" : { 77 | "attribute" : { 78 | "includesMargin" : false, 79 | "name" : "width" 80 | }, 81 | "instance" : { 82 | "address" : "0x4db3270", 83 | "className" : "UIImageView", 84 | "color" : "rgb(230,126,34)", 85 | "initial" : "I", 86 | "name" : "ImageView", 87 | "suffix" : "" 88 | } 89 | }, 90 | "identity" : { 91 | "address" : "0x181d2d20", 92 | "className" : "NSLayoutConstraint", 93 | "color" : "rgb(26,188,156)", 94 | "initial" : "N", 95 | "name" : "NSLayoutConstraint" 96 | }, 97 | "relation" : "==" 98 | }, 99 | { 100 | "constant" : { 101 | "prefix" : "+ ", 102 | "value" : "25" 103 | }, 104 | "description" : "View<\/code>'s bottom edge should equal ImageView<\/code>'s bottom edge plus 25.", 105 | "first" : { 106 | "attribute" : { 107 | "includesMargin" : false, 108 | "name" : "bottom" 109 | }, 110 | "instance" : { 111 | "address" : "0x4db5710", 112 | "className" : "UIView", 113 | "color" : "rgb(245,215,110)", 114 | "initial" : "V", 115 | "name" : "View", 116 | "suffix" : "" 117 | } 118 | }, 119 | "identity" : { 120 | "address" : "0x4dc7b00", 121 | "className" : "NSLayoutConstraint", 122 | "color" : "rgb(26,188,156)", 123 | "initial" : "N", 124 | "name" : "NSLayoutConstraint" 125 | }, 126 | "relation" : "==", 127 | "second" : { 128 | "attribute" : { 129 | "includesMargin" : false, 130 | "name" : "bottom" 131 | }, 132 | "instance" : { 133 | "address" : "0x4db3270", 134 | "className" : "UIImageView", 135 | "color" : "rgb(230,126,34)", 136 | "initial" : "I", 137 | "name" : "ImageView", 138 | "suffix" : "" 139 | } 140 | } 141 | }, 142 | { 143 | "constant" : { 144 | "prefix" : "+ ", 145 | "value" : "2" 146 | }, 147 | "description" : "ImageView<\/code>'s top edge should equal View<\/code>'s top edge plus 2.", 148 | "first" : { 149 | "attribute" : { 150 | "includesMargin" : false, 151 | "name" : "top" 152 | }, 153 | "instance" : { 154 | "address" : "0x4db3270", 155 | "className" : "UIImageView", 156 | "color" : "rgb(230,126,34)", 157 | "initial" : "I", 158 | "name" : "ImageView", 159 | "suffix" : "" 160 | } 161 | }, 162 | "identity" : { 163 | "address" : "0x4dc7bd0", 164 | "className" : "NSLayoutConstraint", 165 | "color" : "rgb(26,188,156)", 166 | "initial" : "N", 167 | "name" : "NSLayoutConstraint" 168 | }, 169 | "relation" : "==", 170 | "second" : { 171 | "attribute" : { 172 | "includesMargin" : false, 173 | "name" : "top" 174 | }, 175 | "instance" : { 176 | "address" : "0x4db5710", 177 | "className" : "UIView", 178 | "color" : "rgb(245,215,110)", 179 | "initial" : "V", 180 | "name" : "View", 181 | "suffix" : "" 182 | } 183 | } 184 | }, 185 | { 186 | "description" : "AWPProductOnMapView<\/code>'s bottom edge should equal View<\/code>'s bottom edge.", 187 | "first" : { 188 | "attribute" : { 189 | "includesMargin" : false, 190 | "name" : "bottom" 191 | }, 192 | "instance" : { 193 | "address" : "0x4dbb090", 194 | "className" : "AWPProductOnMapView", 195 | "color" : "rgb(155,89,182)", 196 | "initial" : "A", 197 | "name" : "AWPProductOnMapView", 198 | "suffix" : "" 199 | } 200 | }, 201 | "identity" : { 202 | "address" : "0x4dc85a0", 203 | "className" : "NSLayoutConstraint", 204 | "color" : "rgb(26,188,156)", 205 | "initial" : "N", 206 | "name" : "NSLayoutConstraint" 207 | }, 208 | "relation" : "==", 209 | "second" : { 210 | "attribute" : { 211 | "includesMargin" : false, 212 | "name" : "bottom" 213 | }, 214 | "instance" : { 215 | "address" : "0x4db5710", 216 | "className" : "UIView", 217 | "color" : "rgb(245,215,110)", 218 | "initial" : "V", 219 | "name" : "View", 220 | "suffix" : "" 221 | } 222 | } 223 | }, 224 | { 225 | "description" : "View<\/code>'s top edge should equal AWPProductOnMapView<\/code>'s top edge.", 226 | "first" : { 227 | "attribute" : { 228 | "includesMargin" : false, 229 | "name" : "top" 230 | }, 231 | "instance" : { 232 | "address" : "0x4db5710", 233 | "className" : "UIView", 234 | "color" : "rgb(245,215,110)", 235 | "initial" : "V", 236 | "name" : "View", 237 | "suffix" : "" 238 | } 239 | }, 240 | "identity" : { 241 | "address" : "0x4dc8630", 242 | "className" : "NSLayoutConstraint", 243 | "color" : "rgb(26,188,156)", 244 | "initial" : "N", 245 | "name" : "NSLayoutConstraint" 246 | }, 247 | "relation" : "==", 248 | "second" : { 249 | "attribute" : { 250 | "includesMargin" : false, 251 | "name" : "top" 252 | }, 253 | "instance" : { 254 | "address" : "0x4dbb090", 255 | "className" : "AWPProductOnMapView", 256 | "color" : "rgb(155,89,182)", 257 | "initial" : "A", 258 | "name" : "AWPProductOnMapView", 259 | "suffix" : "" 260 | } 261 | } 262 | } 263 | ], 264 | "footnotes" : [ 265 | 266 | ] 267 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testGitHubIssues.6.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "value" : "86" 6 | }, 7 | "description" : "View<\/code>'s height should equal 86.", 8 | "first" : { 9 | "attribute" : { 10 | "includesMargin" : false, 11 | "name" : "height" 12 | }, 13 | "instance" : { 14 | "address" : "0x10f754f20", 15 | "className" : "UIView", 16 | "color" : "rgb(39,174,96)", 17 | "initial" : "V", 18 | "name" : "View", 19 | "suffix" : "" 20 | } 21 | }, 22 | "footnote" : { 23 | "marker" : "*", 24 | "text" : "This constraint was translated from an autoresizing mask. If this was not intended, try setting translatesAutoresizingMaskIntoConstraints<\/code> to false on this view." 25 | }, 26 | "identity" : { 27 | "address" : "0x283c8f570", 28 | "className" : "NSAutoresizingMaskLayoutConstraint", 29 | "color" : "rgb(26,188,156)", 30 | "initial" : "N", 31 | "name" : "NSAutoresizingMaskLayoutConstraint" 32 | }, 33 | "relation" : "==" 34 | }, 35 | { 36 | "description" : "Cancel<\/code>'s top edge should equal View<\/code>'s top edge.", 37 | "first" : { 38 | "attribute" : { 39 | "includesMargin" : false, 40 | "name" : "top" 41 | }, 42 | "instance" : { 43 | "address" : "0x10f755100", 44 | "className" : "UIButton", 45 | "color" : "rgb(142,68,173)", 46 | "identifier" : "Cancel", 47 | "initial" : "C", 48 | "name" : "Cancel", 49 | "suffix" : "" 50 | } 51 | }, 52 | "identity" : { 53 | "address" : "0x283c8c820", 54 | "className" : "NSLayoutConstraint", 55 | "color" : "rgb(26,188,156)", 56 | "initial" : "N", 57 | "name" : "NSLayoutConstraint" 58 | }, 59 | "relation" : "==", 60 | "second" : { 61 | "attribute" : { 62 | "includesMargin" : false, 63 | "name" : "top" 64 | }, 65 | "instance" : { 66 | "address" : "0x10f754f20", 67 | "className" : "UIView", 68 | "color" : "rgb(39,174,96)", 69 | "initial" : "V", 70 | "name" : "View", 71 | "suffix" : "" 72 | } 73 | } 74 | }, 75 | { 76 | "constant" : { 77 | "value" : "52" 78 | }, 79 | "description" : "Cancel<\/code>'s height should equal 52.", 80 | "first" : { 81 | "attribute" : { 82 | "includesMargin" : false, 83 | "name" : "height" 84 | }, 85 | "instance" : { 86 | "address" : "0x10f755100", 87 | "className" : "UIButton", 88 | "color" : "rgb(142,68,173)", 89 | "identifier" : "Cancel", 90 | "initial" : "C", 91 | "name" : "Cancel", 92 | "suffix" : "" 93 | } 94 | }, 95 | "identity" : { 96 | "address" : "0x283c8c870", 97 | "className" : "NSLayoutConstraint", 98 | "color" : "rgb(26,188,156)", 99 | "initial" : "N", 100 | "name" : "NSLayoutConstraint" 101 | }, 102 | "relation" : "==" 103 | }, 104 | { 105 | "description" : "UIViewLayoutMarginsGuide<\/code>'s bottom edge should equal Cancel<\/code>'s bottom edge.", 106 | "first" : { 107 | "attribute" : { 108 | "includesMargin" : false, 109 | "name" : "bottom" 110 | }, 111 | "instance" : { 112 | "address" : "0x2826f9880", 113 | "className" : "UILayoutGuide", 114 | "color" : "rgb(211,84,0)", 115 | "identifier" : "UIViewLayoutMarginsGuide", 116 | "initial" : "U", 117 | "name" : "UIViewLayoutMarginsGuide", 118 | "suffix" : "" 119 | } 120 | }, 121 | "identity" : { 122 | "address" : "0x283c8c8c0", 123 | "className" : "NSLayoutConstraint", 124 | "color" : "rgb(26,188,156)", 125 | "initial" : "N", 126 | "name" : "NSLayoutConstraint" 127 | }, 128 | "relation" : "==", 129 | "second" : { 130 | "attribute" : { 131 | "includesMargin" : false, 132 | "name" : "bottom" 133 | }, 134 | "instance" : { 135 | "address" : "0x10f755100", 136 | "className" : "UIButton", 137 | "color" : "rgb(142,68,173)", 138 | "identifier" : "Cancel", 139 | "initial" : "C", 140 | "name" : "Cancel", 141 | "suffix" : "" 142 | } 143 | } 144 | }, 145 | { 146 | "constant" : { 147 | "prefix" : "+ ", 148 | "value" : "42" 149 | }, 150 | "description" : "View<\/code>'s bottom edge should equal UIViewLayoutMarginsGuide<\/code>'s bottom edge plus 42.", 151 | "first" : { 152 | "attribute" : { 153 | "includesMargin" : false, 154 | "name" : "bottom" 155 | }, 156 | "instance" : { 157 | "address" : "0x10f754f20", 158 | "className" : "UIView", 159 | "color" : "rgb(39,174,96)", 160 | "initial" : "V", 161 | "name" : "View", 162 | "suffix" : "" 163 | } 164 | }, 165 | "identity" : { 166 | "address" : "0x283c8c730", 167 | "className" : "NSLayoutConstraint", 168 | "color" : "rgb(26,188,156)", 169 | "identifier" : "UIView-bottomMargin-guide-constraint", 170 | "initial" : "U", 171 | "name" : "UIView-bottomMargin-guide-constraint" 172 | }, 173 | "relation" : "==", 174 | "second" : { 175 | "attribute" : { 176 | "includesMargin" : false, 177 | "name" : "bottom" 178 | }, 179 | "instance" : { 180 | "address" : "0x2826f9880", 181 | "className" : "UILayoutGuide", 182 | "color" : "rgb(211,84,0)", 183 | "identifier" : "UIViewLayoutMarginsGuide", 184 | "initial" : "U", 185 | "name" : "UIViewLayoutMarginsGuide", 186 | "suffix" : "" 187 | } 188 | } 189 | } 190 | ], 191 | "footnotes" : [ 192 | { 193 | "marker" : "*", 194 | "text" : "This constraint was translated from an autoresizing mask. If this was not intended, try setting translatesAutoresizingMaskIntoConstraints<\/code> to false on this view." 195 | } 196 | ] 197 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testGitHubIssues.7.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "prefix" : "+ ", 6 | "value" : "10" 7 | }, 8 | "description" : "StackView<\/code>'s leading edge should equal LayoutGuide<\/code>'s leading edge plus 10.", 9 | "first" : { 10 | "attribute" : { 11 | "includesMargin" : false, 12 | "name" : "leading" 13 | }, 14 | "instance" : { 15 | "address" : "0x7feb4e8d58b0", 16 | "className" : "App_Debug.StackView", 17 | "color" : "rgb(230,126,34)", 18 | "initial" : "S", 19 | "name" : "StackView", 20 | "suffix" : "" 21 | } 22 | }, 23 | "identity" : { 24 | "address" : "0x7feb4e8fa260", 25 | "className" : "SnapKit.LayoutConstraint", 26 | "color" : "rgb(26,188,156)", 27 | "initial" : "L", 28 | "name" : "LayoutConstraint" 29 | }, 30 | "relation" : "==", 31 | "second" : { 32 | "attribute" : { 33 | "includesMargin" : false, 34 | "name" : "leading" 35 | }, 36 | "instance" : { 37 | "address" : "0x7feb50d2aa70", 38 | "className" : "UILayoutGuide", 39 | "color" : "rgb(245,215,110)", 40 | "initial" : "L", 41 | "name" : "LayoutGuide", 42 | "suffix" : "" 43 | } 44 | } 45 | } 46 | ], 47 | "footnotes" : [ 48 | 49 | ] 50 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testGitHubIssues.9.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "prefix" : "+ ", 6 | "value" : "8" 7 | }, 8 | "description" : "Label<\/code>'s top edge should be at least ClassName<\/code>'s top edge plus 8.", 9 | "first" : { 10 | "attribute" : { 11 | "includesMargin" : false, 12 | "name" : "top" 13 | }, 14 | "instance" : { 15 | "address" : "0x111b35c20", 16 | "className" : "UILabel", 17 | "color" : "rgb(243,156,18)", 18 | "initial" : "L", 19 | "name" : "Label", 20 | "suffix" : "" 21 | } 22 | }, 23 | "identity" : { 24 | "address" : "0x2828051d0", 25 | "className" : "_UISystemBaselineConstraint", 26 | "color" : "rgb(26,188,156)", 27 | "initial" : "U", 28 | "name" : "_UISystemBaselineConstraint" 29 | }, 30 | "relation" : ">=", 31 | "second" : { 32 | "attribute" : { 33 | "includesMargin" : false, 34 | "name" : "top" 35 | }, 36 | "instance" : { 37 | "address" : "0x111b35a60", 38 | "className" : "ModuleName.ClassName", 39 | "color" : "rgb(192,57,43)", 40 | "initial" : "C", 41 | "name" : "ClassName", 42 | "suffix" : "" 43 | } 44 | } 45 | }, 46 | { 47 | "description" : "Label<\/code>'s vertical center should equal ClassName<\/code>'s vertical center.", 48 | "first" : { 49 | "attribute" : { 50 | "includesMargin" : false, 51 | "name" : "centerY" 52 | }, 53 | "instance" : { 54 | "address" : "0x111b35c20", 55 | "className" : "UILabel", 56 | "color" : "rgb(243,156,18)", 57 | "initial" : "L", 58 | "name" : "Label", 59 | "suffix" : "" 60 | } 61 | }, 62 | "identity" : { 63 | "address" : "0x282805130", 64 | "className" : "NSLayoutConstraint", 65 | "color" : "rgb(26,188,156)", 66 | "initial" : "N", 67 | "name" : "NSLayoutConstraint" 68 | }, 69 | "relation" : "==", 70 | "second" : { 71 | "attribute" : { 72 | "includesMargin" : false, 73 | "name" : "centerY" 74 | }, 75 | "instance" : { 76 | "address" : "0x111b35a60", 77 | "className" : "ModuleName.ClassName", 78 | "color" : "rgb(192,57,43)", 79 | "initial" : "C", 80 | "name" : "ClassName", 81 | "suffix" : "" 82 | } 83 | } 84 | }, 85 | { 86 | "constant" : { 87 | "value" : "0" 88 | }, 89 | "description" : "ClassName<\/code>'s height should equal 0.", 90 | "first" : { 91 | "attribute" : { 92 | "includesMargin" : false, 93 | "name" : "height" 94 | }, 95 | "instance" : { 96 | "address" : "0x111b35a60", 97 | "className" : "ModuleName.ClassName", 98 | "color" : "rgb(192,57,43)", 99 | "initial" : "C", 100 | "name" : "ClassName", 101 | "suffix" : "" 102 | } 103 | }, 104 | "footnote" : { 105 | "marker" : "†", 106 | "text" : "This constraint was added by a table or collection view to enforce its cell size." 107 | }, 108 | "identity" : { 109 | "address" : "0x282803ed0", 110 | "className" : "NSLayoutConstraint", 111 | "color" : "rgb(26,188,156)", 112 | "identifier" : "UIView-Encapsulated-Layout-Height", 113 | "initial" : "U", 114 | "name" : "UIView-Encapsulated-Layout-Height" 115 | }, 116 | "relation" : "==" 117 | } 118 | ], 119 | "footnotes" : [ 120 | { 121 | "marker" : "†", 122 | "text" : "This constraint was added by a table or collection view to enforce its cell size." 123 | } 124 | ] 125 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testStackOverflowInputs.1.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "value" : "0" 6 | }, 7 | "description" : "_UIButtonBarStackView<\/code>'s width should be at most 0.", 8 | "first" : { 9 | "attribute" : { 10 | "includesMargin" : false, 11 | "name" : "width" 12 | }, 13 | "instance" : { 14 | "address" : "0x7fb573cebf90", 15 | "className" : "_UIButtonBarStackView", 16 | "color" : "rgb(155,89,182)", 17 | "initial" : "U", 18 | "name" : "_UIButtonBarStackView", 19 | "suffix" : "" 20 | } 21 | }, 22 | "identity" : { 23 | "address" : "0x610000099cd0", 24 | "className" : "NSLayoutConstraint", 25 | "color" : "rgb(26,188,156)", 26 | "initial" : "N", 27 | "name" : "NSLayoutConstraint" 28 | }, 29 | "relation" : "<=" 30 | }, 31 | { 32 | "constant" : { 33 | "prefix" : "+ ", 34 | "value" : "15" 35 | }, 36 | "description" : "UIViewLayoutMarginsGuide<\/code>'s leading edge should equal _UIButtonBarStackView<\/code>'s leading edge plus 15.", 37 | "first" : { 38 | "attribute" : { 39 | "includesMargin" : false, 40 | "name" : "leading" 41 | }, 42 | "instance" : { 43 | "address" : "0x61000038ce60", 44 | "className" : "UILayoutGuide", 45 | "color" : "rgb(230,126,34)", 46 | "identifier" : "UIViewLayoutMarginsGuide", 47 | "initial" : "U", 48 | "name" : "UIViewLayoutMarginsGuide", 49 | "suffix" : "" 50 | } 51 | }, 52 | "identity" : { 53 | "address" : "0x610000098ec0", 54 | "className" : "NSLayoutConstraint", 55 | "color" : "rgb(26,188,156)", 56 | "identifier" : "UIView-leftMargin-guide-constraint", 57 | "initial" : "U", 58 | "name" : "UIView-leftMargin-guide-constraint" 59 | }, 60 | "relation" : "==", 61 | "second" : { 62 | "attribute" : { 63 | "includesMargin" : false, 64 | "name" : "leading" 65 | }, 66 | "instance" : { 67 | "address" : "0x7fb573cebf90", 68 | "className" : "_UIButtonBarStackView", 69 | "color" : "rgb(155,89,182)", 70 | "initial" : "U", 71 | "name" : "_UIButtonBarStackView", 72 | "suffix" : "" 73 | } 74 | } 75 | }, 76 | { 77 | "constant" : { 78 | "prefix" : "+ ", 79 | "value" : "15" 80 | }, 81 | "description" : "_UIButtonBarStackView<\/code>'s trailing edge should equal UIViewLayoutMarginsGuide<\/code>'s trailing edge plus 15.", 82 | "first" : { 83 | "attribute" : { 84 | "includesMargin" : false, 85 | "name" : "trailing" 86 | }, 87 | "instance" : { 88 | "address" : "0x7fb573cebf90", 89 | "className" : "_UIButtonBarStackView", 90 | "color" : "rgb(155,89,182)", 91 | "initial" : "U", 92 | "name" : "_UIButtonBarStackView", 93 | "suffix" : "" 94 | } 95 | }, 96 | "identity" : { 97 | "address" : "0x610000099000", 98 | "className" : "NSLayoutConstraint", 99 | "color" : "rgb(26,188,156)", 100 | "identifier" : "UIView-rightMargin-guide-constraint", 101 | "initial" : "U", 102 | "name" : "UIView-rightMargin-guide-constraint" 103 | }, 104 | "relation" : "==", 105 | "second" : { 106 | "attribute" : { 107 | "includesMargin" : false, 108 | "name" : "trailing" 109 | }, 110 | "instance" : { 111 | "address" : "0x61000038ce60", 112 | "className" : "UILayoutGuide", 113 | "color" : "rgb(230,126,34)", 114 | "identifier" : "UIViewLayoutMarginsGuide", 115 | "initial" : "U", 116 | "name" : "UIViewLayoutMarginsGuide", 117 | "suffix" : "" 118 | } 119 | } 120 | } 121 | ], 122 | "footnotes" : [ 123 | 124 | ] 125 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testStackOverflowInputs.2.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "prefix" : "+ ", 6 | "value" : "13" 7 | }, 8 | "description" : "TagView<\/code>'s leading edge should equal QuestionDetailHearderView<\/code>'s leading edge plus 13.", 9 | "first" : { 10 | "attribute" : { 11 | "includesMargin" : false, 12 | "name" : "leading" 13 | }, 14 | "instance" : { 15 | "address" : "0x103850290", 16 | "className" : "ZhiMa_iOS.TagView", 17 | "color" : "rgb(211,84,0)", 18 | "initial" : "T", 19 | "name" : "TagView", 20 | "suffix" : "" 21 | } 22 | }, 23 | "identity" : { 24 | "address" : "0x1740b5ae0", 25 | "className" : "SnapKit.LayoutConstraint", 26 | "color" : "rgb(26,188,156)", 27 | "initial" : "L", 28 | "name" : "LayoutConstraint" 29 | }, 30 | "relation" : "==", 31 | "second" : { 32 | "attribute" : { 33 | "includesMargin" : false, 34 | "name" : "leading" 35 | }, 36 | "instance" : { 37 | "address" : "0x10384fed0", 38 | "className" : "ZhiMa_iOS.QuestionDetailHearderView", 39 | "color" : "rgb(26,188,156)", 40 | "initial" : "Q", 41 | "name" : "QuestionDetailHearderView", 42 | "suffix" : "" 43 | } 44 | } 45 | }, 46 | { 47 | "constant" : { 48 | "prefix" : "- ", 49 | "value" : "13" 50 | }, 51 | "description" : "TagView<\/code>'s trailing edge should equal QuestionDetailHearderView<\/code>'s trailing edge minus 13.", 52 | "first" : { 53 | "attribute" : { 54 | "includesMargin" : false, 55 | "name" : "trailing" 56 | }, 57 | "instance" : { 58 | "address" : "0x103850290", 59 | "className" : "ZhiMa_iOS.TagView", 60 | "color" : "rgb(211,84,0)", 61 | "initial" : "T", 62 | "name" : "TagView", 63 | "suffix" : "" 64 | } 65 | }, 66 | "identity" : { 67 | "address" : "0x1740b7e20", 68 | "className" : "SnapKit.LayoutConstraint", 69 | "color" : "rgb(26,188,156)", 70 | "initial" : "L", 71 | "name" : "LayoutConstraint" 72 | }, 73 | "relation" : "==", 74 | "second" : { 75 | "attribute" : { 76 | "includesMargin" : false, 77 | "name" : "trailing" 78 | }, 79 | "instance" : { 80 | "address" : "0x10384fed0", 81 | "className" : "ZhiMa_iOS.QuestionDetailHearderView", 82 | "color" : "rgb(26,188,156)", 83 | "initial" : "Q", 84 | "name" : "QuestionDetailHearderView", 85 | "suffix" : "" 86 | } 87 | } 88 | }, 89 | { 90 | "constant" : { 91 | "value" : "0" 92 | }, 93 | "description" : "QuestionDetailHearderView<\/code>'s width should equal 0.", 94 | "first" : { 95 | "attribute" : { 96 | "includesMargin" : false, 97 | "name" : "width" 98 | }, 99 | "instance" : { 100 | "address" : "0x10384fed0", 101 | "className" : "ZhiMa_iOS.QuestionDetailHearderView", 102 | "color" : "rgb(26,188,156)", 103 | "initial" : "Q", 104 | "name" : "QuestionDetailHearderView", 105 | "suffix" : "" 106 | } 107 | }, 108 | "footnote" : { 109 | "marker" : "†", 110 | "text" : "This constraint was added by a table or collection view to enforce its cell size." 111 | }, 112 | "identity" : { 113 | "address" : "0x17009fcc0", 114 | "className" : "NSLayoutConstraint", 115 | "color" : "rgb(26,188,156)", 116 | "identifier" : "UIView-Encapsulated-Layout-Width", 117 | "initial" : "U", 118 | "name" : "UIView-Encapsulated-Layout-Width" 119 | }, 120 | "relation" : "==" 121 | } 122 | ], 123 | "footnotes" : [ 124 | { 125 | "marker" : "†", 126 | "text" : "This constraint was added by a table or collection view to enforce its cell size." 127 | } 128 | ] 129 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ParserTests/testStackOverflowInputs.4.json: -------------------------------------------------------------------------------- 1 | { 2 | "constraints" : [ 3 | { 4 | "constant" : { 5 | "value" : "0" 6 | }, 7 | "description" : "_UIButtonBarStackView<\/code>'s width should be at most 0.", 8 | "first" : { 9 | "attribute" : { 10 | "includesMargin" : false, 11 | "name" : "width" 12 | }, 13 | "instance" : { 14 | "address" : "0x7fb573cebf90", 15 | "className" : "_UIButtonBarStackView", 16 | "color" : "rgb(155,89,182)", 17 | "initial" : "U", 18 | "name" : "_UIButtonBarStackView", 19 | "suffix" : "" 20 | } 21 | }, 22 | "identity" : { 23 | "address" : "0x610000099cd0", 24 | "className" : "NSLayoutConstraint", 25 | "color" : "rgb(26,188,156)", 26 | "initial" : "N", 27 | "name" : "NSLayoutConstraint" 28 | }, 29 | "relation" : "<=" 30 | }, 31 | { 32 | "constant" : { 33 | "prefix" : "+ ", 34 | "value" : "15" 35 | }, 36 | "description" : "UIViewLayoutMarginsGuide<\/code>'s leading edge should equal _UIButtonBarStackView<\/code>'s leading edge plus 15.", 37 | "first" : { 38 | "attribute" : { 39 | "includesMargin" : false, 40 | "name" : "leading" 41 | }, 42 | "instance" : { 43 | "address" : "0x61000038ce60", 44 | "className" : "UILayoutGuide", 45 | "color" : "rgb(230,126,34)", 46 | "identifier" : "UIViewLayoutMarginsGuide", 47 | "initial" : "U", 48 | "name" : "UIViewLayoutMarginsGuide", 49 | "suffix" : "" 50 | } 51 | }, 52 | "identity" : { 53 | "address" : "0x610000098ec0", 54 | "className" : "NSLayoutConstraint", 55 | "color" : "rgb(26,188,156)", 56 | "identifier" : "UIView-leftMargin-guide-constraint", 57 | "initial" : "U", 58 | "name" : "UIView-leftMargin-guide-constraint" 59 | }, 60 | "relation" : "==", 61 | "second" : { 62 | "attribute" : { 63 | "includesMargin" : false, 64 | "name" : "leading" 65 | }, 66 | "instance" : { 67 | "address" : "0x7fb573cebf90", 68 | "className" : "_UIButtonBarStackView", 69 | "color" : "rgb(155,89,182)", 70 | "initial" : "U", 71 | "name" : "_UIButtonBarStackView", 72 | "suffix" : "" 73 | } 74 | } 75 | }, 76 | { 77 | "constant" : { 78 | "prefix" : "+ ", 79 | "value" : "15" 80 | }, 81 | "description" : "_UIButtonBarStackView<\/code>'s trailing edge should equal UIViewLayoutMarginsGuide<\/code>'s trailing edge plus 15.", 82 | "first" : { 83 | "attribute" : { 84 | "includesMargin" : false, 85 | "name" : "trailing" 86 | }, 87 | "instance" : { 88 | "address" : "0x7fb573cebf90", 89 | "className" : "_UIButtonBarStackView", 90 | "color" : "rgb(155,89,182)", 91 | "initial" : "U", 92 | "name" : "_UIButtonBarStackView", 93 | "suffix" : "" 94 | } 95 | }, 96 | "identity" : { 97 | "address" : "0x610000099000", 98 | "className" : "NSLayoutConstraint", 99 | "color" : "rgb(26,188,156)", 100 | "identifier" : "UIView-rightMargin-guide-constraint", 101 | "initial" : "U", 102 | "name" : "UIView-rightMargin-guide-constraint" 103 | }, 104 | "relation" : "==", 105 | "second" : { 106 | "attribute" : { 107 | "includesMargin" : false, 108 | "name" : "trailing" 109 | }, 110 | "instance" : { 111 | "address" : "0x61000038ce60", 112 | "className" : "UILayoutGuide", 113 | "color" : "rgb(230,126,34)", 114 | "identifier" : "UIViewLayoutMarginsGuide", 115 | "initial" : "U", 116 | "name" : "UIViewLayoutMarginsGuide", 117 | "suffix" : "" 118 | } 119 | } 120 | } 121 | ], 122 | "footnotes" : [ 123 | 124 | ] 125 | } -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ViewTests/testAbout.1.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | content-type: text/html; charset=utf-8 3 | content-length: 5475 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | WTF Auto Layout? 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 | Home 68 | About 69 |
70 |
71 | 72 |
73 | 74 | 75 | 76 |
77 |
78 | 79 | 80 | 81 | 82 |
83 | 84 |
85 |

Why The Failure,
Auto Layout?

86 |
87 |
88 | 89 | 90 | 91 |
92 |
93 |
94 | Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
95 | (
96 |   "<NSAutoresizingMaskLayoutConstraint:0x1f5b3d10 h=--& v=--& V:[UIView:0x1f5a2f70(460)]>",
97 |   "<NSLayoutConstraint:0x1f5a3c80 V:[UIView:0x1f5a31b0]-(385)-| (Names: '|':UIView:0x1f5a3120 )>",
98 | )
99 |
100 |
101 |

102 | Auto Layout on iOS and macOS is awesome. But when it goes wrong, the error logs are a pain to decipher. 103 |

104 |

105 | This site helps you visualize the conflicting constraints in the logs. It was built using Swift, Vapor and Sparse; and the source code is available on GitHub. If you found it useful, please give it a star. 106 |

107 | 108 |
109 | 110 |
111 |
112 | 113 | 114 | 115 |
116 | 117 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ViewTests/testHome.1.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | content-type: text/html; charset=utf-8 3 | content-length: 4417 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | WTF Auto Layout? 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 | Home 68 | About 69 |
70 |
71 | 72 |
73 | 74 | 75 | 76 |
77 |
78 | 79 | 80 | 81 |
82 | 83 |
84 |

Why The Failure,
Auto Layout?

85 |
86 |
87 | 88 | 89 | 90 | 91 |
92 |
93 | 94 | 95 | Example 96 | 97 |
98 | 99 | 100 |
101 | 102 | 103 |
104 |
105 | 106 | 107 | 108 |
109 | 110 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ViewTests/testPostError.1.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | content-type: text/html; charset=utf-8 3 | content-length: 4926 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | WTF Auto Layout? 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 | Home 68 | About 69 |
70 |
71 | 72 |
73 | 74 | 75 | 76 |
77 |
78 | 79 | 80 | 81 |
82 | 83 |
84 |

Why The Failure,
Auto Layout?

85 |
86 |
87 | 88 | 89 | 90 | 91 |
92 |
93 | 94 | 97 | Example 98 | 99 |
100 | 101 |
Line 2, Column 26
102 | "<NSLayoutConstraint:0x61h00099cd0 _UIButtonBarStackView:0x7fb573cxbf90.width <= 0   (active)>"
103 | ~~~~~~~~~~~~~~~~~~~~~~~~~^
104 | Expected: hexit in constraint instance.memory address
105 | Expected: '@' in constraint instance.file location
106 | Expected: whitespace in constraint instance
107 |
108 | 109 | 110 |
111 |
112 | 113 | 114 | 115 |
116 | 117 | 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /Tests/AppTests/__Snapshots__/ViewTests/testPostError.2.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | content-type: text/html; charset=utf-8 3 | content-length: 4749 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | WTF Auto Layout? 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
66 |
67 | Home 68 | About 69 |
70 |
71 | 72 |
73 | 74 | 75 | 76 |
77 |
78 | 79 | 80 | 81 |
82 | 83 |
84 |

Why The Failure,
Auto Layout?

85 |
86 |
87 | 88 | 89 | 90 | 91 |
92 |
93 | 94 | 97 | Example 98 | 99 |
100 | 101 |
INVALID CONSTRAINT:
102 | Missing info for ListViewImage-a....
103 |
104 | 105 | 106 |
107 |
108 | 109 | 110 | 111 |
112 | 113 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | 3 | import XCTest 4 | @testable import AppTests 5 | 6 | XCTMain([ 7 | testCase(ParserTests.allTests), 8 | testCase(ViewTests.allTests), 9 | testCase(ColorTests.allTests) 10 | ]) 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | linux: 5 | docker: 6 | - image: swift:4.1 7 | steps: 8 | - checkout 9 | - run: 10 | name: Compile code 11 | command: swift build 12 | - run: 13 | name: Run unit tests 14 | command: swift test 15 | 16 | linux-release: 17 | docker: 18 | - image: swift:4.1 19 | steps: 20 | - checkout 21 | - run: 22 | name: Compile code with optimizations 23 | command: swift build -c release 24 | 25 | workflows: 26 | version: 2 27 | tests: 28 | jobs: 29 | - linux 30 | - linux-release 31 | 32 | nightly: 33 | triggers: 34 | - schedule: 35 | cron: "0 0 * * *" 36 | filters: 37 | branches: 38 | only: 39 | - master 40 | jobs: 41 | - linux 42 | - linux-release 43 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Qutheory, LLC 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 | --------------------------------------------------------------------------------