├── .gitignore ├── .swift-version ├── .travis.yml ├── LICENSE ├── Package.pins ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── DoctorPretty │ ├── Alignment.swift │ ├── Atoms.swift │ ├── Doc.swift │ ├── DocFunctor.swift │ ├── DocMonoid.swift │ ├── Enclosed.swift │ ├── Extras.swift │ ├── Fills.swift │ ├── HighLevelCombinators.swift │ ├── Monoids.swift │ ├── RenderPretty.swift │ └── SimpleDoc.swift ├── Tests ├── DoctorPrettyTests │ ├── DoctorPrettySpec.swift │ └── DoctorPrettyTests.swift └── LinuxMain.swift └── default.nix /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | env: 5 | language: generic 6 | sudo: required 7 | dist: trusty 8 | osx_image: xcode9.2 9 | install: 10 | - eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/9f442512a46d7a2af7b850d65a7e9bd31edfb09b/swiftenv-install.sh)" 11 | script: 12 | - swift build 13 | - swift test 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Brandon Kase 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.pins: -------------------------------------------------------------------------------- 1 | { 2 | "autoPin": true, 3 | "pins": [ 4 | { 5 | "package": "Algebra", 6 | "reason": null, 7 | "repositoryURL": "https://github.com/typelift/Algebra.git", 8 | "version": "0.2.0" 9 | }, 10 | { 11 | "package": "Operadics", 12 | "reason": null, 13 | "repositoryURL": "https://github.com/typelift/Operadics.git", 14 | "version": "0.2.3" 15 | }, 16 | { 17 | "package": "SwiftCheck", 18 | "reason": null, 19 | "repositoryURL": "https://github.com/typelift/SwiftCheck.git", 20 | "version": "0.8.0" 21 | }, 22 | { 23 | "package": "Swiftx", 24 | "reason": null, 25 | "repositoryURL": "https://github.com/typelift/Swiftx.git", 26 | "version": "0.5.3" 27 | } 28 | ], 29 | "version": 1 30 | } -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Algebra", 6 | "repositoryURL": "https://github.com/typelift/Algebra.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "e81a20aa543dd0847897b57073a3f939c2124375", 10 | "version": "0.2.0" 11 | } 12 | }, 13 | { 14 | "package": "FileCheck", 15 | "repositoryURL": "https://github.com/trill-lang/FileCheck.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "1c966580c83cf2e41be802992d46fe2edfc5c95f", 19 | "version": "0.0.5" 20 | } 21 | }, 22 | { 23 | "package": "Operadics", 24 | "repositoryURL": "https://github.com/typelift/Operadics.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "c2a14919b3653a39a9bf268c1ae0bf71ad6833fe", 28 | "version": "0.3.0" 29 | } 30 | }, 31 | { 32 | "package": "SwiftCheck", 33 | "repositoryURL": "https://github.com/typelift/SwiftCheck.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "df82fb889864945c64458f38846702af729b3ee4", 37 | "version": "0.9.1" 38 | } 39 | }, 40 | { 41 | "package": "Swiftx", 42 | "repositoryURL": "https://github.com/typelift/Swiftx.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "140f1510ecb8597970c58c5a41a32bda72310d31", 46 | "version": "0.6.0" 47 | } 48 | } 49 | ] 50 | }, 51 | "version": 1 52 | } 53 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "DoctorPretty", 7 | products: [ 8 | .library( 9 | name: "DoctorPretty", 10 | targets: ["DoctorPretty"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/typelift/Algebra.git", .exact("0.2.0")), 14 | .package(url: "https://github.com/typelift/Swiftx.git", .exact("0.6.0")), 15 | .package(url: "https://github.com/typelift/SwiftCheck.git", .exact("0.9.1")) 16 | ], 17 | targets: [ 18 | .target(name: "DoctorPretty", dependencies: ["Algebra", "Swiftx"]), 19 | .testTarget(name: "DoctorPrettyTests", dependencies: ["DoctorPretty", "SwiftCheck"]), 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Doctor Pretty 2 | 3 | > A Swift implementation of the [A prettier printer (Wadler 2003)](https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf) ported from [wl-pprint-annotated](https://github.com/minad/wl-pprint-annotated/blob/master/src/Text/PrettyPrint/Annotated/WL.hs) 4 | 5 | [![Build Status](https://travis-ci.org/bkase/DoctorPretty.svg?branch=master)](https://travis-ci.org/bkase/DoctorPretty) 6 | 7 | ## What is this 8 | 9 | A pretty printer is the dual of a parser -- take some arbitrary AST and output it to a String. This library is a collection of combinators and a primitive called a `Doc` to describe pretty printing some data much like a parser combinator library provides combinators and primitives to describe parsing test into an AST. Interestingly, this implementation efficiently finds the _best_ pretty print. You encode your knowledge of what the _best_ means with your use of various `Doc` combinators. 10 | 11 | For example: Let's say we have some internal structured representation of this Swift code: 12 | 13 | ```swift 14 | func aLongFunction(foo: String, bar: Int, baz: Long) -> (String, Int, Long) { 15 | sideEffect() 16 | return (foo, bar, baz) 17 | } 18 | ``` 19 | 20 | With this library the description that pretty prints the above at a page width of 120 characters. Also prints: 21 | 22 | At a page-width of 40 characters: 23 | 24 | ```swift 25 | func aLongFunction( 26 | foo: String, bar: Int, baz: Long 27 | ) -> (String, Int, Long) { 28 | sideEffect() 29 | return (foo, bar, baz) 30 | } 31 | ``` 32 | 33 | and at a page-width of 20 characters: 34 | 35 | ```swift 36 | func aLongFunction( 37 | foo: String, 38 | bar: Int, 39 | baz: Long 40 | ) -> ( 41 | String, 42 | Int, 43 | Long 44 | ) { 45 | sideEffect() 46 | return ( 47 | foo, 48 | bar, 49 | baz 50 | ) 51 | } 52 | ``` 53 | 54 | See the encoding of this particular document in the [`testSwiftExample` test case](Tests/DoctorPrettyTests/DoctorPrettyTests.swift). 55 | 56 | ## What would I use this for? 57 | 58 | If you're outputting text and you care about the width of the page. Serializing to a `Doc` lets you capture your line-break logic and how your output string looks in one go. 59 | 60 | Why would you output text and care about page width? 61 | 62 | 1. You're building a `gofmt`-type tool for Swift (Note: `gofmt` doesn't pretty-print based on a width, `refmt` (Reason) and `prettier` (JavaScript) do) 63 | 2. You're writing some sort of codegen tool to output Swift code 64 | 3. You're building a source-to-source transpiler 65 | 2. You're outputing help messages in a terminal window for some commandline app (I'm planning to use this for https://github.com/bkase/swift-optparse-applicative) 66 | 67 | ## What is this, actually 68 | 69 | A Swift implementation of the [A prettier printer (Wadler 2003)](https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf) paper (including generally accepted modern enhancements ala [wl-pprint-annotated](https://github.com/minad/wl-pprint-annotated/blob/master/src/Text/PrettyPrint/Annotated/WL.hs). This implementation is close to a direct port of [wl-pprint-annotated](https://github.com/minad/wl-pprint-annotated/blob/master/src/Text/PrettyPrint/Annotated/WL.hs) with some influence from [scala-optparse-applicative's Doc](https://github.com/bmjames/scala-optparse-applicative/blob/master/src/main/scala/net/bmjames/opts/types/Doc.scala) and a few extra Swiftisms. 70 | 71 | ## Basic Usage 72 | 73 | `Doc` is very composable. First of all it's a [`monoid`](https://www.youtube.com/watch?v=6z9QjDUKkCs) with an `.empty` document and the `.concat` case which just puts two documents next to each other. We also have a primitive called `grouped`, which tries this document on a single line, but if it doesn't fit then breaks it up on new-lines. From there we build all high-level combinators up. 74 | 75 | `x <%> y` concats x and y with a space in between if it fits, otherwise puts a line. 76 | 77 | ```swift 78 | .text("foo") <%> .text("bar") 79 | ``` 80 | 81 | pretty-prints under a large page-width: 82 | 83 | ``` 84 | foo bar 85 | ``` 86 | 87 | but when the page-width is set to `5` prints: 88 | 89 | ``` 90 | foo 91 | bar 92 | ``` 93 | 94 | Here are a few more combinators: 95 | 96 | ```swift 97 | /// Concats x and y with a space in between 98 | static func <+>(x: Doc, y: Doc) -> Doc 99 | 100 | /// Behaves like `space` if the output fits the page 101 | /// Otherwise it behaves like line 102 | static var softline 103 | 104 | /// Concats x and y together if it fits 105 | /// Otherwise puts a line in between 106 | static func <%%>(x: Doc, y: Doc) -> Doc 107 | 108 | /// Behaves like `zero` if the output fits the page 109 | /// Otherwise it behaves like line 110 | static var softbreak: Doc 111 | 112 | /// Puts a line between x and y that can be flattened to a space 113 | static func <&>(x: Doc, y: Doc) -> Doc 114 | 115 | /// Puts a line between x and y that can be flattened with no space 116 | static func <&&>(x: Doc, y: Doc) -> Doc 117 | ``` 118 | 119 | There are also combinators for turning collections of documents into "collection"-like pretty-printed primitives such as a square-bracket separated lists: 120 | 121 | ```swift 122 | .text("let x =") <%> [ "foo", "bar", "baz" ].map(Doc.text).list(indent: 4) 123 | ``` 124 | 125 | Pretty-prints at page-width 80 to: 126 | 127 | ```swift 128 | let x = [foo, bar, baz] 129 | ``` 130 | 131 | and at page-width 10 to: 132 | 133 | ```swift 134 | let x = [ 135 | foo, 136 | bar, 137 | baz 138 | ] 139 | ``` 140 | 141 | See the source for more documentation, I have included descriptive doc-comments to explain all the operators (mostly taken from [wl-pprint-annotated](https://github.com/minad/wl-pprint-annotated/blob/master/src/Text/PrettyPrint/Annotated/WL.hs)). 142 | 143 | ## How do I actually pretty print my documents? 144 | 145 | `Doc` has two rendering methods for now: `renderPrettyDefault` prints with a page-width of 100 and `renderPretty` lets you control the page-width. 146 | 147 | These methods don't return `String`s directly -- they return `SimpleDoc` a low-level IR that is close to a string, but high-enough that you can efficiently output to some other output system like stdout or a file. 148 | 149 | For now, `SimpleDoc` has `displayString()` which outputs a `String`, and: 150 | 151 | ```swift 152 | func display(readString: (String) -> M) -> M 153 | ``` 154 | 155 | `display` takes a function that can turn a string into a monoid and then smashes everything together. Because this works for any monoid, you just need to provide a monoid instance for your output formatter (to write to stdout or to a file). 156 | 157 | ## Installation 158 | 159 | With Swift Package Manager, put this inside your `Package.swift`: 160 | 161 | ```swift 162 | .Package(url: "https://github.com/bkase/DoctorPretty.git", 163 | majorVersion: 0, minor: 5) 164 | ``` 165 | 166 | ## How does it work? 167 | 168 | Read the [A prettier printer (Wadler 2003) paper](https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf). 169 | 170 | `Doc` is a recursive enum that captures text and new lines. The interesting case is `.union(longerLines: Doc, shorterLines: Doc)`. This case reifies the notion of "try the longer lines first, then the shorter lines". We can build all sorts of high-level combinators that combine `Doc`s in different ways that eventually reduce to a few strategic `.union`s. 171 | 172 | The renderer keeps a work-list and each rule removes or adds pieces of work to the list and recurses until the list is empty. The best-fit metric proceeds greedily for now, but can be swapped out easily for a more intelligent algorithm in the future. 173 | 174 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/Alignment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Alignment.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/20/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Operadics 11 | 12 | // Alignment and Indentation 13 | extension Doc { 14 | /// Indent all lines of the doc by `i` 15 | public func indent(_ i: IndentLevel) -> Doc { 16 | return (.text(spaces(i)) <> self).hang(i) 17 | } 18 | 19 | /// Hanging indentation 20 | public func hang(_ i: IndentLevel) -> Doc { 21 | return (Doc.nest(i, self)).align() 22 | } 23 | 24 | /// Align this document with the nesting level set to the current column 25 | public func align() -> Doc { 26 | return .column { k in 27 | .nesting { i in .nest(k - i, self) } 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/Atoms.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Atoms.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/20/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Operadics 11 | 12 | extension Doc { 13 | /// Enclose a doc between left and right 14 | public func enclose(left: Doc, right: Doc) -> Doc { 15 | return left <> self <> right 16 | } 17 | 18 | public var squotes: Doc { 19 | return enclose(left: .squote, right: .squote) 20 | } 21 | 22 | public var dquotes: Doc { 23 | return enclose(left: .dquote, right: .dquote) 24 | } 25 | 26 | public var braces: Doc { 27 | return enclose(left: .lbrace, right: .rbrace) 28 | } 29 | 30 | public var parens: Doc { 31 | return enclose(left: .lparen, right: .rparen) 32 | } 33 | 34 | public var angles: Doc { 35 | return enclose(left: .langle, right: .rangle) 36 | } 37 | 38 | public var brackets: Doc { 39 | return enclose(left: .lbracket, right: .rbracket) 40 | } 41 | 42 | public static let squote: Doc = .char("'") 43 | public static let dquote: Doc = .char("\"") 44 | public static let lbrace: Doc = .char("{") 45 | public static let rbrace: Doc = .char("}") 46 | public static let lparen: Doc = .char("(") 47 | public static let rparen: Doc = .char(")") 48 | public static let langle: Doc = .char("<") 49 | public static let rangle: Doc = .char(">") 50 | public static let lbracket: Doc = .char("[") 51 | public static let rbracket: Doc = .char("]") 52 | 53 | public static let space: Doc = .char(" ") 54 | public static let dot: Doc = .char(".") 55 | public static let comma: Doc = .char(",") 56 | public static let semi: Doc = .char(";") 57 | public static let backslash: Doc = .char("\\") 58 | public static let equals: Doc = .char("=") 59 | } 60 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/Doc.swift: -------------------------------------------------------------------------------- 1 | import Swiftx 2 | import Operadics 3 | 4 | public typealias Width = Int 5 | // TODO: What does this int mean, really 6 | public typealias ColumnCount = Int 7 | public typealias IndentLevel = Int 8 | /// The ribbon width is the maximal amount of non-indentation characters on a line 9 | public typealias RibbonWidth = Int 10 | 11 | public indirect enum Doc { 12 | case empty 13 | /// Invariant: char != '\n' 14 | case _char(Character) 15 | /// Invariant: '\n' ∉ text 16 | case _text(length: Int, String) 17 | case _line 18 | /// If flattened then whenFlattened else primary 19 | case flatAlt(primary: Doc, whenFlattened: Doc) 20 | case concat(Doc, Doc) 21 | /// Renders Doc with an increased indent level 22 | /// Note: This only affects line after the first newline 23 | case nest(IndentLevel, Doc) 24 | /// Invariant: longerLines.firstLine.count >= shorterLines.firstLine.count 25 | case union(longerLines: Doc, shorterLines: Doc) 26 | // No support for Annotations for now, I don't think Swift's generics would take kindly to a Doc 27 | // case annotate(A, Doc) 28 | case column((ColumnCount) -> Doc) 29 | case nesting((IndentLevel) -> Doc) 30 | case columns((ColumnCount?) -> Doc) 31 | case ribbon((RibbonWidth?) -> Doc) 32 | 33 | public static func char(_ c: Character) -> Doc { 34 | return c == "\n" ? ._line : ._char(c) 35 | } 36 | 37 | public static func text(_ str: String) -> Doc { 38 | return str == "" ? .empty : ._text(length: str.count, str) 39 | } 40 | 41 | public static var line: Doc { 42 | return .flatAlt(primary: ._line, whenFlattened: .space) 43 | } 44 | 45 | public static var linebreak: Doc { 46 | return .flatAlt(primary: ._line, whenFlattened: .zero) 47 | } 48 | 49 | public static var hardline: Doc { 50 | return ._line 51 | } 52 | 53 | /// Used to specify alternative layouts 54 | /// `doc.grouped` removes all line breaks in `doc`. The resulting line 55 | /// is added if it fits on the page. If it doesn't, it's rendered as is 56 | public var grouped: Doc { 57 | return .union(longerLines: flattened, shorterLines: self) 58 | } 59 | 60 | public var flattened: Doc { 61 | switch self { 62 | case .empty: return self 63 | case ._char(_): return self 64 | case ._text(length: _, _): return self 65 | case ._line: return self 66 | case let .flatAlt(_, whenFlattened): return whenFlattened 67 | case let .concat(x, y): return .concat(x.flattened, y.flattened) 68 | case let .nest(i, x): return .nest(i, x.flattened) 69 | case let .union(x, _): return x.flattened 70 | case let .column(f): return .column { f($0).flattened } 71 | case let .nesting(f): return .nesting { f($0).flattened } 72 | case let .columns(f): return .columns { f($0).flattened } 73 | case let .ribbon(f): return .ribbon { f($0).flattened } 74 | } 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/DocFunctor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocFunctor.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/20/17. 6 | // 7 | // 8 | 9 | // If we decide to add annotation support and make a Doc 10 | /*extension Doc /*: Functor*/ { 11 | func fmap(_ f: @escaping (A) -> B) -> Doc { 12 | switch self { 13 | case .empty: return .empty 14 | case let ._char(c): return ._char(c) 15 | case let ._text(length, str): return ._text(length: length, str) 16 | case ._line: return ._line 17 | case let .flatAlt(primary, whenFlattened): 18 | return .flatAlt(primary: primary.fmap(f), whenFlattened: whenFlattened.fmap(f)) 19 | case let .cat(d1, d2): 20 | return .cat(d1.fmap(f), d2.fmap(f)) 21 | case let .nest(i, d): 22 | return .nest(i, d.fmap(f)) 23 | case let .union(longerLines, shorterLines): 24 | return .union(longerLines: longerLines.fmap(f), shorterLines: shorterLines.fmap(f)) 25 | case let .annotate(a, d): 26 | return .annotate(f(a), d.fmap(f)) 27 | case let .column(g): 28 | return .column { g($0).fmap(f) } 29 | case let .nesting(g): 30 | return .nesting { g($0).fmap(f) } 31 | case let .columns(g): 32 | return .columns { g($0).fmap(f) } 33 | case let .ribbon(g): 34 | return .ribbon { g($0).fmap(f) } 35 | } 36 | } 37 | }*/ 38 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/DocMonoid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DocMonoid.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/20/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import protocol Algebra.Additive 11 | import Operadics 12 | 13 | extension Doc: Additive { 14 | public static func <>(l: Doc, r: Doc) -> Doc { 15 | return .concat(l, r) 16 | } 17 | 18 | public static var zero: Doc { return .empty } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/Enclosed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enclosed.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/21/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Operadics 11 | 12 | extension BidirectionalCollection where Iterator.Element == Doc, IndexDistance == Int { 13 | 14 | /// Intersperses punctuation inside docs 15 | public func punctuate(with punctuation: Doc) -> [Doc] { 16 | if let d = first { 17 | return [d] + self.dropFirst().reduce(into: [Doc]()) { acc, d2 in 18 | acc.append(punctuation) 19 | acc.append(d2) 20 | } 21 | } else { 22 | return [] 23 | } 24 | } 25 | 26 | /// Enclose left right around docs interspersed with separator 27 | /// Tries horizontal, then tries vertical 28 | /// Indenting each element in the list by the indent level 29 | /// Ex: 30 | /// enclose(left: [, right: ], separator: comma, indent: 4) 31 | /// let x = [foo, bar, baz] 32 | /// or 33 | /// let x = [ 34 | /// foo, 35 | /// bar, 36 | /// baz 37 | /// ] 38 | /// Note: The Haskell version sticks the separator at the front 39 | public func enclose(left: Doc, right: Doc, separator: Doc, indent: IndentLevel) -> Doc { 40 | if count == 0 { 41 | return left <> right 42 | } 43 | 44 | let seps = repeatElement(separator, count: count-1) 45 | let last = self[self.index(before: self.endIndex)] 46 | let punctuated = zip(self.dropLast(), seps).map(<>) <> [last] 47 | return ( 48 | .nest(indent, 49 | left <&&> punctuated.sep() 50 | ) <&&> right 51 | ).grouped 52 | } 53 | 54 | /// See @enclose 55 | public func list(indent: IndentLevel) -> Doc { 56 | return enclose(left: Doc.lbracket, right: Doc.rbracket, separator: Doc.comma, indent: indent) 57 | } 58 | 59 | /// See @enclose 60 | public func tupled(indent: IndentLevel) -> Doc { 61 | return enclose(left: Doc.lparen, right: Doc.rparen, separator: Doc.comma, indent: indent) 62 | } 63 | 64 | /// See @enclose 65 | public func semiBraces(indent: IndentLevel) -> Doc { 66 | return enclose(left: Doc.lbrace, right: Doc.rbrace, separator: Doc.semi, indent: indent) 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/Extras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extras.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/20/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public func spaces(_ i: Int) -> String { 12 | return String(repeating: " ", count: i) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/Fills.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fills.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/20/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Operadics 11 | 12 | extension Doc { 13 | /// Render doc then fill until width == i 14 | /// If too large, put in a line-break and then pad 15 | public func fillBreak(_ i: IndentLevel) -> Doc { 16 | return width { w in w > i ? .nest(i, .linebreak) : .text(spaces(i - w)) } 17 | } 18 | 19 | /// Render doc then fill until width == i 20 | public func fill(_ i: IndentLevel) -> Doc { 21 | return width { w in w >= i ? .zero : .text(spaces(i - w)) } 22 | } 23 | 24 | public func width(_ f: @escaping (IndentLevel) -> Doc) -> Doc { 25 | return .column { k1 in self <> .column { k2 in f(k2 - k1) } } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/HighLevelCombinators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HighLevelCombinators.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/20/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Operadics 11 | 12 | infix operator <%>: AdditionPrecedence 13 | infix operator <%%>: AdditionPrecedence 14 | infix operator <&>: AdditionPrecedence 15 | infix operator <&&>: AdditionPrecedence 16 | 17 | extension Doc { 18 | /// Concats x and y with a space in between 19 | public static func <+>(x: Doc, y: Doc) -> Doc { 20 | return x <> space <> y 21 | } 22 | 23 | /// Concats x and y with a space in between if it fits 24 | /// Otherwise puts a line 25 | public static func <%>(x: Doc, y: Doc) -> Doc { 26 | return x <> softline <> y 27 | } 28 | 29 | /// Behaves like `space` if the output fits the page 30 | /// Otherwise it behaves like line 31 | public static var softline: Doc { 32 | return Doc.line.grouped 33 | } 34 | 35 | /// Concats x and y together if it fits 36 | /// Otherwise puts a line in between 37 | public static func <%%>(x: Doc, y: Doc) -> Doc { 38 | return x <> softbreak <> y 39 | } 40 | 41 | /// Behaves like `zero` if the output fits the page 42 | /// Otherwise it behaves like line 43 | public static var softbreak: Doc { 44 | return Doc.linebreak.grouped 45 | } 46 | 47 | /// Puts a line between x and y that can be flattened to a space 48 | public static func <&>(x: Doc, y: Doc) -> Doc { 49 | return x <> .line <> y 50 | } 51 | 52 | /// Puts a line between x and y that can be flattened with no space 53 | public static func <&&>(x: Doc, y: Doc) -> Doc { 54 | return x <> .linebreak <> y 55 | } 56 | } 57 | 58 | extension Sequence where Iterator.Element == Doc { 59 | /// Concat all horizontally if it fits, but if not 60 | /// all vertical 61 | public func sep() -> Doc { 62 | return vsep().grouped 63 | } 64 | 65 | /// Concats all horizontally until end of page 66 | /// then puts a line and repeats 67 | public func fillSep() -> Doc { 68 | return fold(combineDocs: <%>) 69 | } 70 | 71 | /// Concats all horizontally with spaces in between 72 | public func hsep() -> Doc { 73 | return fold(combineDocs: <+>) 74 | } 75 | 76 | /// Concats all vertically, if a group undoes, concat with space 77 | public func vsep() -> Doc { 78 | return fold(combineDocs: <&>) 79 | } 80 | 81 | /// Concats all horizontally no spaces if fits 82 | /// Otherwise all vertically 83 | public func cat() -> Doc { 84 | return vcat().grouped 85 | } 86 | 87 | /// Concats all horizontally until end of page 88 | /// then puts a linebreak and repeats 89 | public func fillCat() -> Doc { 90 | return fold(combineDocs: <%%>) 91 | } 92 | 93 | /// Concats all horizontally with no spaces 94 | public func hcat() -> Doc { 95 | return fold(combineDocs: <>) 96 | } 97 | 98 | /// Concats all vertically, if a group undoes, concat with no space 99 | public func vcat() -> Doc { 100 | return fold(combineDocs: <&&>) 101 | } 102 | 103 | public func fold(combineDocs: (Doc, Doc) -> Doc) -> Doc { 104 | var iter = makeIterator() 105 | if let first = iter.next() { 106 | return IteratorSequence(iter).reduce(first, combineDocs) 107 | } else { 108 | return Doc.zero 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/Monoids.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Monoids.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/20/17. 6 | // 7 | // 8 | 9 | // TODO: Remove this file when Algebra gets these definitions from Swiftz 10 | 11 | import Foundation 12 | import Algebra 13 | import Operadics 14 | 15 | extension Array: Additive { 16 | public static var zero: Array { return [] } 17 | 18 | public static func <>(x: Array, y: Array) -> Array { 19 | return x + y 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/RenderPretty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RenderPretty.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/20/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | typealias CompareStrategy = (IndentLevel, ColumnCount, Width, RibbonWidth) -> (SimpleDoc, () -> SimpleDoc) -> SimpleDoc 12 | 13 | indirect enum List { 14 | case Nil 15 | case Cons(T, List) 16 | } 17 | 18 | // TODO: Tune this datastructure to increase render performance 19 | // Good properties: 20 | // fast prepend 21 | // fast (head, rest) decomposition 22 | // shared substructure 23 | // The obvious choice is a linked list, but maybe a clever sequence 24 | // could be better. Also, perhaps not using the enum-list will speed 25 | // things up more. 26 | typealias Docs = List<(Int, Doc)> 27 | 28 | extension Doc { 29 | public func renderPrettyDefault() -> SimpleDoc { 30 | return renderPretty(ribbonFrac: 0.4, pageWidth: 100) 31 | } 32 | 33 | public func renderPretty(ribbonFrac: Float, pageWidth: Width) -> SimpleDoc { 34 | return renderFits(compareStrategy: Doc.nicest1, ribbonFrac: ribbonFrac, pageWidth: pageWidth) 35 | } 36 | 37 | func renderFits(compareStrategy: @escaping CompareStrategy, ribbonFrac: Float, pageWidth: Width) -> SimpleDoc { 38 | let rounded = round(Float(pageWidth) * ribbonFrac) 39 | let ribbonChars: RibbonWidth = max(0, min(pageWidth, Width(rounded))) 40 | 41 | func best( 42 | currNesting: IndentLevel, 43 | currColumn: ColumnCount, 44 | // This parameter is only used during annotation, but I'm 45 | // keeping it here to simplify adding the annotation case 46 | // in doc if that ever happens 47 | z: @escaping (IndentLevel, ColumnCount) -> SimpleDoc, 48 | indentationDocs: Docs 49 | ) -> SimpleDoc { 50 | switch indentationDocs { 51 | case .Nil: return z(currNesting, currColumn) 52 | case let .Cons(head, rest): 53 | let (indent, doc) = head 54 | 55 | switch doc { 56 | case .empty: 57 | return best(currNesting: currNesting, currColumn: currColumn, z: z, indentationDocs: rest) 58 | case let ._char(c): 59 | let newColumn = currColumn + 1 60 | return SimpleDoc.char(c, best(currNesting: currNesting, currColumn: newColumn, z: z, indentationDocs: rest)) 61 | case let ._text(length, str): 62 | let newColumn = currColumn + length 63 | return SimpleDoc.text(length: length, str, best(currNesting: currNesting, currColumn: newColumn, z: z, indentationDocs: rest)) 64 | case ._line: 65 | return SimpleDoc.line(indent: indent, { best(currNesting: indent, currColumn: indent, z: z, indentationDocs: rest) }) 66 | case let .flatAlt(primary, whenFlattened: _): 67 | return best(currNesting: currNesting, currColumn: currColumn, z: z, indentationDocs: .Cons((indent, primary), rest)) 68 | case let .concat(d1, d2): 69 | return best(currNesting: currNesting, currColumn: currColumn, z: z, indentationDocs: .Cons((indent, d1), .Cons((indent, d2), rest))) 70 | case let .nest(indent_, d): 71 | let newIndent = indent + indent_ 72 | return best(currNesting: currNesting, currColumn: currColumn, z: z, indentationDocs: .Cons((newIndent, d), rest)) 73 | case let .union(longerLines, shorterLines): 74 | return compareStrategy( 75 | currNesting, 76 | currColumn, 77 | pageWidth, 78 | ribbonChars 79 | )( 80 | best(currNesting: currNesting, currColumn: currColumn, z: z, indentationDocs: .Cons((indent, longerLines), rest)), 81 | /// Laziness is needed here to prevent horrible performance! 82 | { () in best(currNesting: currNesting, currColumn: currColumn, z: z, indentationDocs: .Cons((indent, shorterLines), rest)) } 83 | ) 84 | case let .column(f): 85 | return best(currNesting: currNesting, currColumn: currColumn, z: z, indentationDocs: .Cons((indent, f(currColumn)), rest)) 86 | case let .nesting(f): 87 | return best(currNesting: currNesting, currColumn: currColumn, z: z, indentationDocs: .Cons((indent, f(indent)), rest)) 88 | case let .columns(f): 89 | return best(currNesting: currNesting, currColumn: currColumn, z: z, indentationDocs: .Cons((indent, f(.some(pageWidth))), rest)) 90 | case let .ribbon(f): 91 | return best(currNesting: currNesting, currColumn: currColumn, z: z, indentationDocs: .Cons((indent, f(.some(ribbonChars))), rest)) 92 | } 93 | } 94 | } 95 | 96 | return best(currNesting: 0, currColumn: 0, z: { _, _ in SimpleDoc.empty }, indentationDocs: .Cons((0, self), .Nil)) 97 | } 98 | 99 | /// Compares the first two lines of the documents 100 | static func nicest1(nesting: IndentLevel, column: ColumnCount, pageWidth: Width, ribbonWidth: RibbonWidth) -> (SimpleDoc, () -> SimpleDoc) -> SimpleDoc { 101 | return { d1, d2 in 102 | let wid = min(pageWidth - column, ribbonWidth - column + nesting) 103 | 104 | func fits(prefix: Int, w: Int, doc: SimpleDoc) -> Bool { 105 | var _w = w 106 | var _doc = doc 107 | 108 | while true { 109 | switch (_w, _doc) { 110 | case (_, _) where _w < 0: 111 | return false 112 | 113 | case (_, .empty): 114 | return true 115 | 116 | case let (w, .char(_, x)): 117 | _w = w - 1 118 | _doc = x 119 | continue 120 | 121 | case let (w, .text(length, _, x)): 122 | _w = w - length 123 | _doc = x 124 | continue 125 | 126 | case (_, _): 127 | return true 128 | } 129 | } 130 | } 131 | 132 | if fits(prefix: min(nesting, column), w: wid, doc: d1) { 133 | return d1 134 | } else { 135 | return d2() 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/DoctorPretty/SimpleDoc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleDoc.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/20/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Algebra 11 | import Operadics 12 | 13 | /// A post-render IR that is used to dump to a String or a File or process 14 | /// in some other way 15 | public indirect enum SimpleDoc { 16 | case empty 17 | case char(Character, SimpleDoc) 18 | case text(length: Int, String, SimpleDoc) 19 | case line(indent: Int, () -> SimpleDoc) 20 | 21 | public func display(readString: (String) -> M) -> M { 22 | switch self { 23 | case .empty: return M.zero 24 | case let .char(c, rest): return readString(String(c)) <> rest.display(readString: readString) 25 | case let .text(_, s, rest): return readString(s) <> rest.display(readString: readString) 26 | case let .line(indent, rest): 27 | return readString("\n" + spaces(indent)) <> rest().display(readString: readString) 28 | } 29 | } 30 | 31 | public func displayString() -> String { 32 | return display{ [$0] }.joined() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/DoctorPrettyTests/DoctorPrettySpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DoctorPrettySpec.swift 3 | // DoctorPretty 4 | // 5 | // Created by Brandon Kase on 5/21/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | @testable import DoctorPretty 11 | import SwiftCheck 12 | import XCTest 13 | import Operadics 14 | 15 | // mostly from https://github.com/bmjames/scala-optparse-applicative/blob/master/src/test/scala/net/bmjames/opts/test/DocSpec.scala 16 | 17 | extension Doc { 18 | func equals(underPageWidth pageWidth: Width, doc: Doc) -> Bool { 19 | return 20 | self.renderPretty(ribbonFrac: 1.0, pageWidth: pageWidth).displayString() == 21 | doc.renderPretty(ribbonFrac: 1.0, pageWidth: pageWidth).displayString() 22 | } 23 | } 24 | 25 | extension Doc: Arbitrary { 26 | public static var arbitrary: Gen { 27 | let stringDocGen: Gen = String.arbitrary.map{ Doc.text($0) } 28 | let lineOrEmptyGen: Gen = Gen.fromElements(of: [ Doc.line, Doc.empty ]) 29 | return Gen.frequency([ 30 | (3, stringDocGen), 31 | (1, lineOrEmptyGen) 32 | ]).proliferate(withSize: 10) 33 | .map{ $0.cat() } 34 | } 35 | } 36 | 37 | class DocSpecs: XCTestCase { 38 | func testSpecs() { 39 | let posNum = Int.arbitrary.suchThat{ $0 > 0 } 40 | property("text is a monoid homomorphism") <- forAll(posNum, String.arbitrary, String.arbitrary) { (width: Int, s1: String, s2: String) in 41 | return Doc.text(s1 + s2).equals(underPageWidth: width, doc: .text(s1) <> .text(s2)) 42 | } 43 | 44 | property("nesting law") <- forAll(posNum, posNum, posNum, Doc.arbitrary) { (x: Int, y: Int, z: Int, doc: Doc) in 45 | let xs = [x, y, z].sorted() 46 | let (nest1, nest2, width) = (xs[0], xs[1], xs[2]) 47 | 48 | return Doc.nest(nest1 + nest2, doc) 49 | .equals(underPageWidth: width, 50 | doc: Doc.nest(nest1, Doc.nest(nest2, doc))) 51 | } 52 | 53 | property("zero nesting is id") <- forAll(posNum, Doc.arbitrary) { (width: Int, doc: Doc) in 54 | return doc.equals(underPageWidth: width, 55 | doc: Doc.nest(0, doc)) 56 | } 57 | 58 | property("nesting distributes") <- forAll(posNum, posNum, Doc.arbitrary, Doc.arbitrary) { (x: Int, y: Int, doc1: Doc, doc2: Doc) in 59 | let xs = [x, y].sorted() 60 | let (nest, width) = (xs[0], xs[1]) 61 | 62 | return Doc.nest(nest, doc1 <> doc2) 63 | .equals(underPageWidth: width, 64 | doc: Doc.nest(nest, doc1) <> Doc.nest(nest, doc2)) 65 | } 66 | 67 | property("nesting single line is noop") <- forAll(posNum, posNum, String.arbitrary) { (x: Int, y: Int, str: String) in 68 | let xs = [x, y].sorted() 69 | let (nest, width) = (xs[0], xs[1]) 70 | let noNewlines = String(str.filter { $0 != "\n" }) 71 | 72 | return Doc.nest(nest, .text(noNewlines)) 73 | .equals(underPageWidth: width, 74 | doc: .text(noNewlines)) 75 | } 76 | 77 | property("group idempotent") <- forAll(posNum, Doc.arbitrary) { (width: Int, doc: Doc) in 78 | return doc.grouped 79 | .equals(underPageWidth: width, 80 | doc: doc.grouped.grouped) 81 | } 82 | } 83 | 84 | static var allTests = [ 85 | ("testSpecs", testSpecs) 86 | ] 87 | } 88 | 89 | -------------------------------------------------------------------------------- /Tests/DoctorPrettyTests/DoctorPrettyTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DoctorPretty 3 | import Operadics 4 | 5 | // Most tests ported from https://github.com/minad/wl-pprint-annotated/blob/master/test/WLPPrintTests.hs 6 | class DoctorPrettyTests: XCTestCase { 7 | 8 | func testSlow() { 9 | measure { 10 | let doc = (1...30).map { _ in 11 | (1...30).map { _ in "foo" }.map(Doc.text).fillSep() 12 | }.vcat() 13 | _ = doc.renderPrettyDefault() 14 | } 15 | } 16 | 17 | func assertPretty(pageWidth: Width, str: String, doc: Doc, file: StaticString = #file, line: UInt = #line) { 18 | XCTAssertEqual(doc.renderPretty(ribbonFrac: 1.0, pageWidth: pageWidth).displayString(), str, file: file, line: line) 19 | } 20 | 21 | func testSimpleConstructors() { 22 | assertPretty(pageWidth: 80, str: "", doc: Doc.zero) 23 | assertPretty(pageWidth: 80, str: "a", doc: Doc.char("a")) 24 | assertPretty(pageWidth: 80, str: "text...", doc: Doc.text("text...")) 25 | assertPretty(pageWidth: 80, str: "\n", doc: Doc.hardline) 26 | } 27 | 28 | func testFlatAltConstructor() { 29 | assertPretty(pageWidth: 80, str: "x", doc: .flatAlt(primary: .text("x"), whenFlattened: .text("y"))) 30 | assertPretty(pageWidth: 80, str: "y", doc: Doc.flatAlt(primary: .text("x"), whenFlattened: .text("y")).flattened) 31 | } 32 | 33 | func testCat() { 34 | assertPretty(pageWidth: 80, str: "some code", doc: .text("some") <> Doc.space <> .text("code")) 35 | } 36 | 37 | func testNest() { 38 | assertPretty( 39 | pageWidth: 80, 40 | str: "foo bar", 41 | doc: .text("foo") <+> .nest(2, .text("bar")) 42 | ) 43 | 44 | assertPretty( 45 | pageWidth: 80, 46 | str: "foo\n bar", 47 | doc: .text("foo") <> .nest(2, .line <> .text("bar")) 48 | ) 49 | } 50 | 51 | func testUnion() { 52 | assertPretty(pageWidth: 80, str: "foo bar", 53 | doc: .text("foo") <%> .text("bar")) 54 | assertPretty(pageWidth: 5, str: "foo\nbar", 55 | doc: .text("foo") <%> .text("bar")) 56 | } 57 | 58 | func testFuncConstructors() { 59 | assertPretty(pageWidth: 80, str: "foo 4", 60 | doc: .text("foo") <+> .column { .text("\($0)") }) 61 | assertPretty(pageWidth: 80, str: "foo 2", 62 | doc: .text("foo") <+> .nest(2, .nesting { .text("\($0)") })) 63 | assertPretty(pageWidth: 21, str: "foo 21", 64 | doc: .text("foo") <+> .nest(2, .columns { .text("\($0!)") })) 65 | XCTAssertEqual("foo 40", 66 | (.text("foo") <+> .ribbon { .text("\($0!)") }).renderPrettyDefault().displayString()) 67 | } 68 | 69 | func testHang() { 70 | let words = "the hang combinator indents these words !".components(separatedBy: " ").map{ Doc.text($0) } 71 | 72 | assertPretty( 73 | pageWidth: 20, 74 | str: ["the hang combinator", 75 | " indents these", 76 | " words !" ].joined(separator: "\n"), 77 | doc: words.fillSep().hang(4) 78 | ) 79 | } 80 | 81 | func testAlign() { 82 | assertPretty( 83 | pageWidth: 20, 84 | str: ["hi nice", 85 | " world" ].joined(separator: "\n"), 86 | doc: .text("hi") <+> ((.text("nice") <> .linebreak <> .text("world"))).align() 87 | ) 88 | } 89 | 90 | func testPunctuate() { 91 | assertPretty( 92 | pageWidth: 80, 93 | str: "a,b,c", 94 | doc: ["a", "b", "c"] 95 | .map(Doc.char) 96 | .punctuate(with: Doc.comma) 97 | .cat() 98 | ) 99 | } 100 | 101 | func testEncloseSep() { 102 | let list1 = ["foo", "bar", "baz"] 103 | .map(Doc.text) 104 | .list(indent: 4) 105 | let doc = .text("let x =") <%> list1 106 | 107 | assertPretty( 108 | pageWidth: 80, 109 | str: "let x = [foo, bar, baz]", 110 | doc: doc 111 | ) 112 | 113 | assertPretty( 114 | pageWidth: 10, 115 | str: ["let x = [", 116 | " foo,", 117 | " bar,", 118 | " baz", 119 | "]"].joined(separator: "\n"), 120 | doc: doc 121 | ) 122 | 123 | let list2 = [list1].list(indent: 4) 124 | let list3 = [list2].list(indent: 4) 125 | let docBigger = .text("let x =") <%> list3 126 | 127 | assertPretty( 128 | pageWidth: 20, 129 | str: ["let x = [", 130 | " [", 131 | " [", 132 | " foo,", 133 | " bar,", 134 | " baz", 135 | " ]", 136 | " ]", 137 | "]"].joined(separator: "\n"), 138 | doc: docBigger 139 | ) 140 | } 141 | 142 | func testSwiftExample() { 143 | let Indentation = 4 144 | let funName: Doc = .text("aLongFunction") 145 | let args: Doc = [ 146 | ("foo", "String"), 147 | ("bar", "Int"), 148 | ("baz", "Long") 149 | ].map{ (name, typ) in 150 | .text(name) <> .text(":") <%> .text(typ) 151 | }.tupled(indent: Indentation) 152 | let retType: Doc = ["String", "Int", "Long"] 153 | .map(Doc.text) 154 | .tupled(indent: Indentation) 155 | let retValue: Doc = ["foo", "bar", "baz"] 156 | .map(Doc.text) 157 | .tupled(indent: Indentation) 158 | let retExpr: Doc = .text("return") <%> retValue 159 | let body: Doc = [ 160 | .text("sideEffect()"), 161 | retExpr 162 | ].vsep() 163 | let funcBody: Doc = [ 164 | Doc.lbrace, body.indent(Indentation), Doc.rbrace 165 | ].vsep() 166 | let doc: Doc = 167 | .text("func") <> Doc.space <> funName <> args <> Doc.space <> .text("->") <> Doc.space <> retType <%> funcBody 168 | 169 | assertPretty(pageWidth: 120, str: [ 170 | "func aLongFunction(foo: String, bar: Int, baz: Long) -> (String, Int, Long) {", 171 | " sideEffect()", 172 | " return (foo, bar, baz)", 173 | "}" 174 | ].joined(separator: "\n"), doc: doc) 175 | 176 | assertPretty(pageWidth: 40, str: [ 177 | "func aLongFunction(", 178 | " foo: String, bar: Int, baz: Long", 179 | ") -> (String, Int, Long) {", 180 | " sideEffect()", 181 | " return (foo, bar, baz)", 182 | "}" 183 | ].joined(separator: "\n"), doc: doc) 184 | 185 | assertPretty(pageWidth: 20, str: [ 186 | "func aLongFunction(", 187 | " foo: String,", 188 | " bar: Int,", 189 | " baz: Long", 190 | ") -> (", 191 | " String,", 192 | " Int,", 193 | " Long", 194 | ") {", 195 | " sideEffect()", 196 | " return (", 197 | " foo,", 198 | " bar,", 199 | " baz", 200 | " )", 201 | "}" 202 | ].joined(separator: "\n"), doc: doc) 203 | } 204 | 205 | func testLargeDocument() { 206 | let doc = (1...65) 207 | .map { _ in "foo" } 208 | .map(Doc.text) 209 | .fillSep() 210 | 211 | XCTAssertEqual( 212 | """ 213 | foo foo foo foo foo foo foo foo 214 | foo foo foo foo foo foo foo foo 215 | foo foo foo foo foo foo foo foo 216 | foo foo foo foo foo foo foo foo 217 | foo foo foo foo foo foo foo foo 218 | foo foo foo foo foo foo foo foo 219 | foo foo foo foo foo foo foo foo 220 | foo foo foo foo foo foo foo foo 221 | foo 222 | """, 223 | doc.renderPretty(ribbonFrac: 0.4, pageWidth: 80).displayString() 224 | ) 225 | } 226 | 227 | static var allTests = [ 228 | ("testSimpleConstructors", testSimpleConstructors), 229 | ("testFlatAltConstructor", testFlatAltConstructor), 230 | ("testCat", testCat), 231 | ("testNest", testNest), 232 | ("testUnion", testUnion), 233 | ("testFuncConstructors", testFuncConstructors), 234 | ("testHang", testHang), 235 | ("testAlign", testAlign), 236 | ("testPunctuate", testPunctuate), 237 | ("testEncloseSep", testEncloseSep), 238 | ("testSwiftExample", testSwiftExample) 239 | ] 240 | } 241 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DoctorPrettyTests 3 | 4 | XCTMain([ 5 | testCase(DoctorPrettyTests.allTests), 6 | testCase(DocSpecs.allTests) 7 | ]) 8 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | pkgs.stdenv.mkDerivation rec { 3 | name = "DoctorPretty"; 4 | # get deps 5 | buildInputs = [ git swift ]; 6 | src = ./.; 7 | 8 | configurePhase = '' 9 | # Make clones via https work 10 | export GIT_SSL_CAINFO=/etc/ssl/certs/ca-certificates.crt 11 | ''; 12 | 13 | buildPhase = '' 14 | swift build 15 | ''; 16 | 17 | # test 18 | doCheck = true; 19 | checkPhase = '' 20 | SWIFTPM_TEST_DoctorPretty=YES swift test 21 | ''; 22 | 23 | installPhase = '' 24 | touch $out 25 | ''; 26 | } 27 | 28 | --------------------------------------------------------------------------------