├── .gitignore ├── Example ├── 1. Introduction.md ├── 2. Derived collections of enum cases.md ├── 3. Warning and error diagnostic directives.md └── 4. In-place collection element removal.md ├── LICENSE ├── Package.swift ├── README.md └── Sources └── Playmaker ├── App.swift ├── FixedStrings.swift ├── Page.swift ├── SafeWriting.swift └── main.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | .DS_Store 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /Example/1. Introduction.md: -------------------------------------------------------------------------------- 1 | # What’s new in Swift 4.2 2 | 3 | * Created by [Paul Hudson](https://twitter.com/twostraws) – [Hacking with Swift](https://www.hackingwithswift.com) 4 | * Last update: April 12th 2018 5 | 6 | This playground is designed to showcase new features introduced with Swift 4.2. I already [wrote an article on it](https://www.hackingwithswift.com/articles/77/whats-new-in-swift-4-2), but it's a lot more fun to see things in action and experiment yourself. 7 | 8 | If you hit problems or have questions, you're welcome to tweet me [@twostraws](https://twitter.com/twostraws) or email . 9 | 10 | - important: Until a version of Xcode ships with Swift 4.2, you should [download the latest Swift trunk development snapshot](https://swift.org/blog/4-2-release-process/) and activate it inside your current Xcode version. 11 | 12 |   13 | -------------------------------------------------------------------------------- /Example/2. Derived collections of enum cases.md: -------------------------------------------------------------------------------- 1 | ## Derived collections of enum cases 2 | 3 | [SE-0194](https://github.com/apple/swift-evolution/blob/master/proposals/0194-derived-collection-of-enum-cases.md) introduces a new `CaseIterable` protocol that automatically generates an array property of all cases in an enum. 4 | 5 | Prior to Swift 4.2 this either took hacks, hand-coding, or Sourcery code generation to accomplish, but now all you need to do is make your enum conform to the `CaseIterable` protocol. At compile time, Swift will automatically generate an `allCases` property that is an array of all your enum’s cases, in the order you defined them. 6 | 7 | For example, this creates an enum of pasta shapes and asks Swift to automatically generate an `allCases` array for it: 8 | 9 | enum Pasta: CaseIterable { 10 | case cannelloni, fusilli, linguine, tagliatelle 11 | } 12 | 13 | You can then go ahead and use that property as a regular array – it will be a `[Pasta]` given the code above, so we could print it like this: 14 | 15 | for shape in Pasta.allCases { 16 | print("I like eating \(shape).") 17 | } 18 | 19 | This automatic synthesis of `allCases` will only take place for enums that do not use associated values. Adding those automatically wouldn’t make sense, however if you want you can add it yourself: 20 | 21 | enum Car: String, CaseIterable { 22 | static var allCases: [Car] { 23 | return [.ford, .toyota, .jaguar, .bmw, .porsche(convertible: false), .porsche(convertible: true)] 24 | } 25 | 26 | case ford, toyota, jaguar, bmw 27 | case porsche(convertible: Bool) 28 | } 29 | 30 | In that code example, you can see I added a raw value of `String` to the enum just fine – this new feature doesn’t change the way raw values work. 31 | 32 | At this time, Swift is unable to synthesize the `allCases` property if any of your enum cases are marked unavailable. So, if you need `allCases` then you’ll need to add it yourself, like this: 33 | 34 | enum Direction: CaseIterable { 35 | static var allCases: [Direction] { 36 | return [.north, .south, .east, .west] 37 | } 38 | 39 | case north, south, east, west 40 | 41 | @available(*, unavailable) 42 | case all 43 | } 44 | 45 | - important: You need to add `CaseIterable` to the original declaration of your enum rather than an extension in order for the `allCases` array to be synthesized. This means you can’t use extensions to retroactively make existing enums conform to the protocol. 46 | -------------------------------------------------------------------------------- /Example/3. Warning and error diagnostic directives.md: -------------------------------------------------------------------------------- 1 | ## Warning and error diagnostic directives 2 | 3 | [SE-0196](https://github.com/apple/swift-evolution/blob/master/proposals/0196-diagnostic-directives.md) introduces new compiler directives that help us mark issues in our code. These will be familiar to any developers who had used Objective-C previously, but as of Swift 4.2 we can enjoy them in Swift too. 4 | 5 | The two new directives are `#warning` and `#error`: the former will force Xcode to issue a warning when building your code, and the latter will issue a compile error so your code won’t build at all. Both of these are useful for different reasons: 6 | 7 | * `#warning` is mainly useful as a reminder to yourself or others that some work is incomplete. Xcode templates often use `#warning` to mark method stubs that you should replace with your own code. 8 | * `#error` is mainly useful if you ship a library that requires other developers to provide some data. For example, an authentication key for a web API – you want users to include their own key, so using `#error` will force them to change that code before continuing. 9 | 10 | Both of these work in the same way: `#warning("Some message")` and `#error("Some message")`. For example: 11 | 12 | func encrypt(_ string: String, with password: String) -> String { 13 | #warning("This is terrible method of encryption") 14 | return password + String(string.reversed()) + password 15 | } 16 | 17 | struct Configuration { 18 | var apiKey: String { 19 | #error("Please enter your API key below then delete this line.") 20 | return "Enter your key here" 21 | } 22 | } 23 | 24 | Both `#warning` and `#error` work alongside the existing `#if` compiler directive, and will only be triggered if the condition being evaluated is true. For example: 25 | 26 | #if os(macOS) 27 | #error("MyLibrary is not supported on macOS.") 28 | #endif 29 | 30 | -------------------------------------------------------------------------------- /Example/4. In-place collection element removal.md: -------------------------------------------------------------------------------- 1 | ## In-place collection element removal 2 | 3 | [SE-0197](https://github.com/apple/swift-evolution/blob/master/proposals/0197-remove-where.md) introduces a new `removeAll(where:)` method that performs a high-performance, in-place filter for collections. You give it a closure condition to run, and it will strip out all objects that fail the condition. 4 | 5 | For example, if you have a collection of names and want to remove people called “Terry”, you’d use this: 6 | 7 | var pythons = ["John", "Michael", "Graham", "Terry", "Eric", "Terry"] 8 | pythons.removeAll { $0.hasPrefix("Terry") } 9 | print(pythons) 10 | 11 | Now, you might very well think that you could accomplish that by using `filter()` like this: 12 | 13 | pythons = pythons.filter { !$0.hasPrefix("Terry") } 14 | 15 | However, that doesn’t use memory very efficiently, it specifies what you *don’t* want rather than what you *want*, and more advanced in-place solutions come with a range of complexities that are off-putting to novices. Ben Cohen, the author of SE-0197, gave a talk at [dotSwift 2018](https://www.dotconferences.com/2018/01/ben-cohen-extending-the-standard-library) where he discussed the implementation of this proposal in more detail – if you’re keen to learn why it’s so efficient, you should start there! 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paul Hudson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "playmaker", 8 | dependencies: [ 9 | // Dependencies declare other packages that this package depends on. 10 | // .package(url: /* package url */, from: "1.0.0"), 11 | ], 12 | targets: [ 13 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 14 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 15 | .target( 16 | name: "playmaker", 17 | dependencies: []), 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playmaker 2 | 3 |

4 | 5 | 6 | 7 | Twitter: @twostraws 8 | 9 |

10 | 11 | This is a simple Swift project to convert a collection of Markdown files to Swift playgrounds that can be used in Xcode. 12 | 13 | To try it yourself, create a directory and place your Markdown files inside. You can add all the normal Markdown formatting you want; any code inside will automatically become editable in the final playground. 14 | 15 | My motivation for this project is simple: it lets anyone create Swift playgrounds directly from Markdown, with no dependencies other than Swift itself. 16 | 17 | 18 | ## Usage 19 | 20 | You can build this project just by running `swift build` from the command line. You can then move the binary from **.build/debug/playmaker** to wherever you want, so you call it as you wish. 21 | 22 | Playmaker takes a directory containing Markdown files and converts them into a single playground file. The resulting file will have the same name as the directory. 23 | 24 | For example, this will convert all Markdown files in **~/Desktop/example** into a single **example.playground**. 25 | 26 | playmaker ~/Desktop/example 27 | 28 | There are some simple rules you should follow 29 | 30 | * Each page of your playground should be one Markdown file. 31 | * Playmaker will only read files that have the extension “.md”. 32 | * To ensure your files are ordered as you want, number them like this: “5. Title Of Page.md”, “05. Title Of Page.md”, or even “005. Title Of Page.md”. The number and dot part won’t appear in the final playground, but will ensure your pages are ordered correctly. 33 | * You must include at least one page called “Introduction”, numbered first. So, “1. Introduction.md”, “001. Introduction.md”, etc. Playmaker will automatically add a table of contents to this page. 34 | * You can include any Markdown you want; it will be sent straight to the playground. 35 | 36 | I have included an example you can try out – look in the Example directory of this project. 37 | 38 | 39 | ## Tips 40 | 41 | On my site I have documented the [Markdown formatting you can add to Swift code](https://www.hackingwithswift.com/example-code/language/how-to-add-markdown-comments-to-your-code), but when making playgrounds there are a couple of extras you should use: 42 | 43 | - Xcode likes to strip out unused whitespace, so if you want to force line breaks you should use the ` ` HTML – Xcode won’t strip that. 44 | - If you want to add callouts, use `- important:`, `- note:`, and `- experiment:` to get special formatting. For example, `- important: Don’t do this` will be highlighted in red. 45 | - The names of your Markdown files will appear in your playground, it’s better to name them “Title of Page” rather than “title-of-page”. 46 | 47 | 48 | ## Contributing 49 | 50 | This was put together as part of a blog post on Swift playgrounds, but I’m happy to accept contributions from other developers. If you’ve found ways to clean up the code, add features, or improve the documentation, please open a pull request. 51 | 52 | 53 | ## License 54 | 55 | The source code for Playmaker is licensed under the MIT License. Note: the Markdown files in the Example directory are taken from my article [what’s new in Swift 4.2](https://www.hackingwithswift.com/articles/77/whats-new-in-swift-4-2) and are *not* included in the MIT license. 56 | 57 | Copyright (c) 2018 Paul Hudson 58 | 59 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 60 | 61 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 62 | 63 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Sources/Playmaker/App.swift: -------------------------------------------------------------------------------- 1 | // 2 | // App.swift 3 | // Playmaker 4 | // 5 | // Created by Paul Hudson on 13/04/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | The Playmaker app, which assembles a collection 12 | of Page instances and writes them all to disk. 13 | */ 14 | struct App { 15 | /** 16 | Stores the directory where Markdown files are 17 | being read from, and where the finished playground 18 | will be written. 19 | */ 20 | private struct Options { 21 | var readURL: URL 22 | var destinationURL: URL 23 | } 24 | 25 | /** 26 | Parses settings, creates Page instances, 27 | checks all the data is good, then writes 28 | the finished playground. 29 | */ 30 | func process(using options: [String]) { 31 | let options = parse(options) 32 | let pages = loadPages(options) 33 | runPreflight(options, pages) 34 | writeOutput(options, using: pages) 35 | } 36 | 37 | /** 38 | Parses options from user input. We only expect one 39 | at this time, which is the directory to convert. 40 | */ 41 | private func parse(_ options: [String]) -> Options { 42 | let readPath = options.first ?? FileManager.default.currentDirectoryPath 43 | 44 | let readURL = URL(fileURLWithPath: readPath) 45 | let playgroundName = readURL.lastPathComponent 46 | let destinationURL = readURL.appendingPathComponent(playgroundName).appendingPathExtension("playground") 47 | return Options(readURL: readURL, destinationURL: destinationURL) 48 | } 49 | 50 | /** 51 | Runs some simple checks to make sure the playground 52 | can be created. 53 | */ 54 | private func runPreflight(_ options: Options, _ pages: [Page]) { 55 | if FileManager.default.fileExists(atPath: options.destinationURL.path) { 56 | quit("\(options.destinationURL.lastPathComponent) already exists.") 57 | } 58 | 59 | if pages.isEmpty { 60 | quit("Unable to locate any pages. Please create at least one page called \"1. Introduction.md\".") 61 | } 62 | 63 | if pages.filter({ $0.title == "Introduction" }).isEmpty { 64 | quit("You must have at least one file called Introduction, e.g. \"1. Introduction.md\" or \"001. Introduction.md\".") 65 | } 66 | 67 | } 68 | 69 | /** 70 | Scans the input directory for Markdown files, and loads 71 | them in ready for processing. 72 | */ 73 | private func loadPages(_ options: Options) -> [Page] { 74 | if let contents = try? FileManager.default.contentsOfDirectory(at: options.readURL, includingPropertiesForKeys: nil, options: []) { 75 | let markdownFiles = contents.filter { $0.pathExtension == "md" } 76 | let sortedFiles = markdownFiles.sorted { $0.path < $1.path } 77 | return sortedFiles.map { Page(url: $0) } 78 | } else { 79 | quit("Unable to read directory \(options.readURL.path).") 80 | } 81 | } 82 | 83 | /** 84 | Writes this playground to disk. 85 | */ 86 | private func writeOutput(_ options: Options, using pages: [Page]) { 87 | // prepare the directories we need to work with 88 | let pagesURL = options.destinationURL.appendingPathComponent("Pages") 89 | let workspaceURL = options.destinationURL.appendingPathComponent("playground.xcworkspace") 90 | 91 | createOutputDirectory(options.destinationURL) 92 | createOutputDirectory(pagesURL) 93 | createOutputDirectory(workspaceURL) 94 | 95 | // write the workspace data 96 | write(App.workspaceData, toFile: workspaceURL.appendingPathComponent("contents.xcworkspacedata")) 97 | 98 | // assemble the user-facing table of contents 99 | let tableOfContents = pages.map { page -> String in 100 | let url = page.title.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? page.title 101 | return "* [\(page.title)](\(url))" 102 | }.joined(separator: "\n") 103 | 104 | // flatten all pages, handling first and last separately 105 | pages.first?.flatten(to: pagesURL, position: .first, toc: tableOfContents) 106 | pages.dropFirst().dropLast().forEach { $0.flatten(to: pagesURL, position: .other) } 107 | pages.last?.flatten(to: pagesURL, position: .last) 108 | 109 | // assemble and write the XML table of contents 110 | let pageList = pages.map { " " }.joined(separator: "\n") 111 | let contents = App.contents.replacingOccurrences(of: "$pages", with: pageList) 112 | write(contents, toFile: options.destinationURL.appendingPathComponent("contents.xcplayground")) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/Playmaker/FixedStrings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FixedStrings.swift 3 | // Playmaker 4 | // 5 | // Created by Paul Hudson on 13/04/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | extension App { 11 | /** 12 | Text that will be written intocontents.xcworkspacedata 13 | */ 14 | static var workspaceData: String { 15 | return """ 16 | 17 | 18 | 19 | 20 | """ 21 | } 22 | 23 | /** 24 | Text that will be written into contents.xcplayground, 25 | where $pages is replaced with actual page data. 26 | */ 27 | static var contents: String { 28 | return """ 29 | 30 | 31 | 32 | $pages 33 | 34 | 35 | """ 36 | } 37 | } 38 | 39 | extension Page { 40 | /** 41 | Navigation bar to be used on the first page. 42 | */ 43 | static var next: String { 44 | return "[Next >](@next)" 45 | } 46 | 47 | /** 48 | Navigation bar to be used on the final page. 49 | */ 50 | static var previousHome: String { 51 | return "[< Previous](@previous)           [Home](Introduction)" 52 | } 53 | 54 | /** 55 | Navigation bar to be used on all other pages. 56 | */ 57 | static var previousHomeNext: String { 58 | return "[< Previous](@previous)           [Home](Introduction)           [Next >](@next)" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/Playmaker/Page.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Page.swift 3 | // Playmaker 4 | // 5 | // Created by Paul Hudson on 13/04/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | This represents one playground page, complete with all its 12 | code and comments. 13 | */ 14 | struct Page { 15 | /** 16 | Whether this is the first page, last page, or 17 | any page in between. 18 | */ 19 | enum Position { 20 | case first, last, other 21 | } 22 | 23 | /** 24 | The title of this page, as extracted from 25 | the filename. 26 | */ 27 | var title: String { 28 | // convert /path/to/028. Hello World.md to just "028. Hello World" 29 | let baseName = url.deletingPathExtension().lastPathComponent 30 | 31 | // remove all leading digits 32 | let regex = try! NSRegularExpression(pattern: "(\\d*\\.)? *(.*)", options: []) 33 | return regex.stringByReplacingMatches(in: baseName, options: [], range: NSRange(location: 0, length: 5), withTemplate: "$2") 34 | } 35 | 36 | /** 37 | The URL to the Markdown file for this page. 38 | */ 39 | var url: URL 40 | 41 | /** 42 | Reads the input Markdown file, transforms it, 43 | and writes it back to disk as Swift. 44 | */ 45 | func flatten(to destination: URL, position: Position, toc: String = "") { 46 | let pageDirectory = destination.appendingPathComponent("\(title).xcplaygroundpage") 47 | createOutputDirectory(pageDirectory) 48 | 49 | let contents = transform(position: position, toc: toc) 50 | write(contents, toFile: pageDirectory.appendingPathComponent("Contents.swift")) 51 | } 52 | 53 | /** 54 | Loads a file's content and transforms it into 55 | Swift, including navigation. 56 | */ 57 | func transform(position: Position, toc: String) -> String { 58 | // make sure we can actually find this file 59 | guard var contents = try? String(contentsOf: url) else { 60 | quit("Unable to load file \(url.path)") 61 | } 62 | 63 | // there are three differet navigation bars depending on whether 64 | // this is the first page, last page, or any other page 65 | let navigationBar: String 66 | 67 | switch position { 68 | case .first: 69 | navigationBar = Page.next 70 | case .last: 71 | navigationBar = Page.previousHome 72 | default: 73 | navigationBar = Page.previousHomeNext 74 | } 75 | 76 | // code can be spaced using tabs or spaces, but we only care about spaces here – Markdown uses 4 77 | contents = contents.replacingOccurrences(of: "\t", with: " ") 78 | 79 | // strip out any whitespace, to avoid extra spacing when files end with code then line breaks 80 | contents = contents.trimmingCharacters(in: .whitespacesAndNewlines) 81 | 82 | let lines = contents.components(separatedBy: "\n") 83 | 84 | // we always start in text mode 85 | var inCode = false 86 | var output = "/*:\n" 87 | 88 | // don't put at navigation bar at the top of the introduction 89 | if position != .first { 90 | output += "\(navigationBar)\n\n" 91 | } 92 | 93 | // now process every line of Markdown 94 | for var line in lines { 95 | // if this is an empty line we shouldn't do anything special – 96 | // just treat it as whatever it was before 97 | let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) 98 | 99 | if trimmed.isEmpty { 100 | output += "\(line)\n" 101 | continue 102 | } 103 | 104 | if line.hasPrefix(" ") { 105 | // this is code; drop the initial spacing and 106 | // optionally switch into code mode 107 | if !inCode { 108 | inCode = true 109 | output += "*/\n" 110 | } 111 | 112 | line = String(line.dropFirst(4)) 113 | } else { 114 | // this isn't code; get out of code mode 115 | if inCode { 116 | inCode = false 117 | output += "/*:\n" 118 | } 119 | } 120 | 121 | output += "\(line)\n" 122 | } 123 | 124 | // make sure we're currently in text mode 125 | if inCode { 126 | output += "/*:\n" 127 | } 128 | 129 | if position == .first { 130 | output += "## Contents\n\(toc)\n" 131 | } 132 | 133 | output += "\n\n\(navigationBar)\n*/" 134 | 135 | // Playgrounds add lots of spacing around code whether we like it or not, 136 | // so make all text and code butt right up against each other 137 | output = output.replacingOccurrences(of: "\n\n*/", with: "\n*/") 138 | output = output.replacingOccurrences(of: "\n\n/*:", with: "\n/*:") 139 | 140 | return output 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/Playmaker/SafeWriting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafeWriting.swift 3 | // Playmaker 4 | // 5 | // Created by Paul Hudson on 13/04/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Quit with a nice message. 12 | */ 13 | func quit(_ message: String) -> Never { 14 | print(message) 15 | exit(1) 16 | } 17 | 18 | /** 19 | Attempt to create a directory, or quit cleanly. 20 | */ 21 | func createOutputDirectory(_ directory: URL) { 22 | do { 23 | try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) 24 | } catch { 25 | quit("Failed to create output directory \(directory.path): \(error.localizedDescription).") 26 | } 27 | } 28 | 29 | /** 30 | Attempt to write a file, or quit cleanly. 31 | */ 32 | func write(_ string: String, toFile filename: URL) { 33 | do { 34 | try string.write(to: filename, atomically: true, encoding: .utf8) 35 | } catch { 36 | quit("Failed to create output file \(filename.path): \(error.localizedDescription).") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Playmaker/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // Playmaker 4 | // 5 | // Created by Paul Hudson on 13/04/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | // fetch any command-line options, missing the 12 | // first one – that's the name of our program 13 | let options = CommandLine.arguments.dropFirst() 14 | 15 | let app = App() 16 | app.process(using: Array(options)) 17 | --------------------------------------------------------------------------------