├── .gitignore ├── generate_readme ├── Tests ├── 4.playground.expected ├── 4.playground ├── 1.playground ├── 1.playground.expected ├── 2.playground ├── 2.playground.expected ├── 3.playground └── 3.playground.expected ├── test ├── package.json ├── LICENSE ├── README.swift ├── README.markdown └── Playdown.swift /.gitignore: -------------------------------------------------------------------------------- 1 | Tests/tmp.out 2 | *.log 3 | NSRange_subranges.swift 4 | -------------------------------------------------------------------------------- /generate_readme: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | swift Playdown.swift README.swift > README.markdown 3 | -------------------------------------------------------------------------------- /Tests/4.playground.expected: -------------------------------------------------------------------------------- 1 | This should be the first line of output. 2 | This is another line. -------------------------------------------------------------------------------- /Tests/4.playground: -------------------------------------------------------------------------------- 1 | /*: This is an optional comment 2 | This should be the first line of output. 3 | This is another line. 4 | */ -------------------------------------------------------------------------------- /Tests/1.playground: -------------------------------------------------------------------------------- 1 | println("Playdown") 2 | 3 | //: Convert a Swift playground to Markdown 4 | //: Perfect for blog posts 5 | var i = 0 6 | i++ 7 | 8 | /*: 9 | # Use Markdown in your comments! 10 | Check the documentation at Apple's website 11 | */ 12 | println(i) 13 | -------------------------------------------------------------------------------- /Tests/1.playground.expected: -------------------------------------------------------------------------------- 1 | ```swift 2 | println("Playdown") 3 | ``` 4 | 5 | Convert a Swift playground to Markdown 6 | Perfect for blog posts 7 | 8 | ```swift 9 | var i = 0 10 | i++ 11 | ``` 12 | 13 | # Use Markdown in your comments! 14 | Check the documentation at Apple's website 15 | 16 | ```swift 17 | println(i) 18 | ``` 19 | -------------------------------------------------------------------------------- /Tests/2.playground: -------------------------------------------------------------------------------- 1 | //: Playdown - noun: A place where people convert Swift Playgrounds to Markdown 2 | println("Playdown converts your Swift playgrounds to Markdown") 3 | 4 | /*: 5 | # Usage 6 | Playdown was made to be run from the Terminal. 7 | 8 | 1. Download Playdown.swift 9 | 2. Run it with `swift Playdown.swift YourPlayground/Contents.swift` 10 | */ 11 | println("hey") -------------------------------------------------------------------------------- /Tests/2.playground.expected: -------------------------------------------------------------------------------- 1 | Playdown - noun: A place where people convert Swift Playgrounds to Markdown 2 | ```swift 3 | println("Playdown converts your Swift playgrounds to Markdown") 4 | ``` 5 | 6 | # Usage 7 | Playdown was made to be run from the Terminal. 8 | 9 | 1. Download Playdown.swift 10 | 2. Run it with `swift Playdown.swift YourPlayground/Contents.swift` 11 | 12 | ```swift 13 | println("hey") 14 | ``` -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | INPUT=$(ls Tests/*.playground) 2 | tmp="Tests/tmp.out" 3 | 4 | for file in $INPUT 5 | do 6 | swift Playdown.swift "$file" > "$tmp" 7 | fn="Tests/"$(basename "$file")".expected" 8 | diff -Bwu "$tmp" "$fn" 9 | 10 | if [ $? -ne 0 ] 11 | then 12 | echo "$file - failed" 13 | echo 14 | echo "Output" 15 | echo "======" 16 | echo 17 | swift Playdown.swift "$file" 18 | echo 19 | else 20 | echo "$file - passed" 21 | fi 22 | done 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playdown", 3 | "version": "0.0.3", 4 | "description": "Convert Swift Playgrounds to Markdown", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "./test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/matthewpalmer/Playdown.git" 15 | }, 16 | "keywords": [ 17 | "ios", 18 | "swift", 19 | "playground", 20 | "markdown" 21 | ], 22 | "author": "Matthew Palmer", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/matthewpalmer/Playdown/issues" 26 | }, 27 | "homepage": "https://github.com/matthewpalmer/Playdown", 28 | "bin": { 29 | "playdown": "./Playdown.swift" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 matthewpalmer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Tests/3.playground: -------------------------------------------------------------------------------- 1 | //: Playdown - *noun*: A place where people convert Swift Playgrounds to Markdown 2 | //: > This README was converted from the playground at `tests/3.playground`! 3 | 4 | println("Playdown converts your Swift playgrounds to Markdown") 5 | 6 | /*: 7 | # Usage 8 | Playdown was made to be run from the Terminal. 9 | 10 | 1. Download Playdown.swift 11 | 2. Run it with `swift Playdown.swift YourPlayground/Contents.swift` 12 | */ 13 | 14 | func use() { 15 | Playdown.download() 16 | Playdown.run("YourPlayground/Contents.swift") // Works for any .swift file! 17 | } 18 | 19 | /*: 20 | # Features 21 | 22 | * Convert Playground Markup Language to a Markdown document, perfect for blog posts 23 | * Support for lots of Markdown features, like headings, lists, block quotes, code blocks, inline styles, and links. 24 | * Supports Github Flavored Markdown, though I'm happy to accept PRs for improvements. 25 | */ 26 | 27 | func cool() -> Bool { 28 | return Playdown.headings() 29 | .lists() 30 | .blockQuote() 31 | .codeBlocks() 32 | .inlineStyles() 33 | .links() == true 34 | } 35 | 36 | /*: 37 | # Tests 38 | We need more tests! 39 | 40 | If Playdown doesn't work well for one of your playgrounds, please open a pull request with your playground and the expected output. 41 | 42 | To run the tests, you can use the `test` script in the root folder of this project. 43 | */ 44 | 45 | func test() -> String { 46 | return "We would love if you contributed more tests!" 47 | } 48 | -------------------------------------------------------------------------------- /README.swift: -------------------------------------------------------------------------------- 1 | //: # Playdown 2 | //: **Playdown** — *noun*: A place where people convert Swift Playgrounds to Markdown 3 | 4 | println("This README was converted from README.swift!") 5 | 6 | /*: 7 | # Usage 8 | Playdown was made to be run from the Terminal. 9 | 10 | Install with npm, and run Playdown on any Swift file 11 | 12 | ``` 13 | $ npm install -g playdown 14 | $ playdown Contents.swift 15 | ``` 16 | 17 | Alternatively, you can download `Playdown.swift`, put it in the right directory, and run it with `swift Playdown.swift Contents.swift`. 18 | */ 19 | 20 | func use() { 21 | npm.install("playdown", options: "-g") 22 | Terminal.run("playdown Contents.swift") // Works for any .swift file! 23 | } 24 | 25 | /*: 26 | # Features 27 | 28 | * Convert a playground to a Markdown document, perfect for blog posts 29 | * Support for lots of Markdown features, like headings, lists, block quotes, styles, and links. 30 | * Supports Github Flavored Markdown 31 | */ 32 | 33 | func cool() -> Bool { 34 | return Playdown.headings() 35 | .lists() 36 | .blockQuote() 37 | .codeBlocks() 38 | .inlineStyles() 39 | .links() == true 40 | } 41 | 42 | /*: 43 | # Tests 44 | If Playdown doesn't work well for one of your playgrounds, please open a pull request with your playground and the expected output. 45 | 46 | To run the tests, you can use the `test` script in the root folder of this project. 47 | */ 48 | 49 | func test() -> (Test, String) { 50 | return (./test, "We would love if you contributed more tests!") 51 | } 52 | -------------------------------------------------------------------------------- /Tests/3.playground.expected: -------------------------------------------------------------------------------- 1 | Playdown - *noun*: A place where people convert Swift Playgrounds to Markdown 2 | > This README was converted from the playground at `tests/3.playground`! 3 | 4 | ```swift 5 | println("Playdown converts your Swift playgrounds to Markdown") 6 | ``` 7 | 8 | # Usage 9 | Playdown was made to be run from the Terminal. 10 | 11 | 1. Download Playdown.swift 12 | 2. Run it with `swift Playdown.swift YourPlayground/Contents.swift` 13 | 14 | ```swift 15 | func use() { 16 | Playdown.download() 17 | Playdown.run("YourPlayground/Contents.swift") // Works for any .swift file! 18 | } 19 | ``` 20 | 21 | # Features 22 | 23 | * Convert Playground Markup Language to a Markdown document, perfect for blog posts 24 | * Support for lots of Markdown features, like headings, lists, block quotes, code blocks, inline styles, and links. 25 | * Supports Github Flavored Markdown, though I'm happy to accept PRs for improvements. 26 | 27 | ```swift 28 | func cool() -> Bool { 29 | return Playdown.headings() 30 | .lists() 31 | .blockQuote() 32 | .codeBlocks() 33 | .inlineStyles() 34 | .links() == true 35 | } 36 | ``` 37 | 38 | # Tests 39 | We need more tests! 40 | 41 | If Playdown doesn't work well for one of your playgrounds, please open a pull request with your playground and the expected output. 42 | 43 | To run the tests, you can use the `test` script in the root folder of this project. 44 | 45 | ```swift 46 | func test() -> String { 47 | return "We would love if you contributed more tests!" 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Playdown 2 | **Playdown** — *noun*: A place where people convert Swift Playgrounds to Markdown 3 | 4 | ```swift 5 | println("This README was converted from README.swift!") 6 | 7 | ``` 8 | 9 | # Usage 10 | Playdown was made to be run from the Terminal. 11 | 12 | Install with npm, and run Playdown on any Swift file 13 | 14 | ``` 15 | $ npm install -g playdown 16 | $ playdown Contents.swift 17 | ``` 18 | 19 | Alternatively, you can download `Playdown.swift`, put it in the right directory, and run it with `swift Playdown.swift Contents.swift`. 20 | 21 | 22 | ```swift 23 | func use() { 24 | npm.install("playdown", options: "-g") 25 | Terminal.run("playdown Contents.swift") // Works for any .swift file! 26 | } 27 | 28 | ``` 29 | 30 | # Features 31 | 32 | * Convert a playground to a Markdown document, perfect for blog posts 33 | * Support for lots of Markdown features, like headings, lists, block quotes, styles, and links. 34 | * Supports Github Flavored Markdown 35 | 36 | 37 | ```swift 38 | func cool() -> Bool { 39 | return Playdown.headings() 40 | .lists() 41 | .blockQuote() 42 | .codeBlocks() 43 | .inlineStyles() 44 | .links() == true 45 | } 46 | 47 | ``` 48 | 49 | # Tests 50 | If Playdown doesn't work well for one of your playgrounds, please open a pull request with your playground and the expected output. 51 | 52 | To run the tests, you can use the `test` script in the root folder of this project. 53 | 54 | 55 | ```swift 56 | func test() -> (Test, String) { 57 | return (./test, "We would love if you contributed more tests!") 58 | } 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /Playdown.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xcrun swift 2 | 3 | import Foundation 4 | 5 | /** 6 | * StreamReader is used for reading from files, among other things. 7 | * c/o Airspeed Velocity 8 | * http://stackoverflow.com/questions/24581517/read-a-file-url-line-by-line-in-swift 9 | * http://stackoverflow.com/questions/29540593/read-a-file-line-by-line-in-swift-1-2 10 | */ 11 | class StreamReader { 12 | 13 | let encoding : UInt 14 | let chunkSize : Int 15 | 16 | var fileHandle : NSFileHandle! 17 | let buffer : NSMutableData! 18 | let delimData : NSData! 19 | var atEof : Bool = false 20 | 21 | init?(path: String, delimiter: String = "\n", encoding : UInt = NSUTF8StringEncoding, chunkSize : Int = 4096) { 22 | self.chunkSize = chunkSize 23 | self.encoding = encoding 24 | 25 | if let fileHandle = NSFileHandle(forReadingAtPath: path), 26 | delimData = delimiter.dataUsingEncoding(NSUTF8StringEncoding), 27 | buffer = NSMutableData(capacity: chunkSize) 28 | { 29 | self.fileHandle = fileHandle 30 | self.delimData = delimData 31 | self.buffer = buffer 32 | } else { 33 | self.fileHandle = nil 34 | self.delimData = nil 35 | self.buffer = nil 36 | return nil 37 | } 38 | } 39 | 40 | deinit { 41 | self.close() 42 | } 43 | 44 | /// Return next line, or nil on EOF. 45 | func nextLine() -> String? { 46 | precondition(fileHandle != nil, "Attempt to read from closed file") 47 | 48 | if atEof { 49 | return nil 50 | } 51 | 52 | // Read data chunks from file until a line delimiter is found: 53 | var range = buffer.rangeOfData(delimData, options: [], range: NSMakeRange(0, buffer.length)) 54 | while range.location == NSNotFound { 55 | let tmpData = fileHandle.readDataOfLength(chunkSize) 56 | if tmpData.length == 0 { 57 | // EOF or read error. 58 | atEof = true 59 | if buffer.length > 0 { 60 | // Buffer contains last line in file (not terminated by delimiter). 61 | let line = NSString(data: buffer, encoding: encoding) 62 | 63 | buffer.length = 0 64 | return line as String? 65 | } 66 | // No more lines. 67 | return nil 68 | } 69 | buffer.appendData(tmpData) 70 | range = buffer.rangeOfData(delimData, options: [], range: NSMakeRange(0, buffer.length)) 71 | } 72 | 73 | // Convert complete line (excluding the delimiter) to a string: 74 | let line = NSString(data: buffer.subdataWithRange(NSMakeRange(0, range.location)), 75 | encoding: encoding) 76 | // Remove line (and the delimiter) from the buffer: 77 | buffer.replaceBytesInRange(NSMakeRange(0, range.location + range.length), withBytes: nil, length: 0) 78 | 79 | return line as String? 80 | } 81 | 82 | /// Start reading from the beginning of file. 83 | func rewind() -> Void { 84 | fileHandle.seekToFileOffset(0) 85 | buffer.length = 0 86 | atEof = false 87 | } 88 | 89 | /// Close the underlying file. No reading must be done after calling this method. 90 | func close() -> Void { 91 | fileHandle?.closeFile() 92 | fileHandle = nil 93 | } 94 | } 95 | 96 | extension StreamReader : SequenceType { 97 | func generate() -> AnyGenerator { 98 | return anyGenerator{ 99 | return self.nextLine() 100 | } 101 | } 102 | } 103 | 104 | extension String { 105 | func substringsMatchingPattern(let pattern: String, let options: NSRegularExpressionOptions, let matchGroup: Int) throws -> [String] { 106 | let range = NSMakeRange(0, (self as NSString).length) 107 | let regex = try NSRegularExpression(pattern: pattern, options: options) 108 | let matches = regex.matchesInString(self, options: [], range: range) 109 | 110 | var output: [String] = [] 111 | 112 | for match in matches { 113 | let matchRange = match.rangeAtIndex(matchGroup) 114 | let matchString = (self as NSString).substringWithRange(matchRange) 115 | output.append(matchString as String) 116 | } 117 | 118 | return output 119 | } 120 | 121 | func matchesPattern(let pattern: String, let options: NSRegularExpressionOptions) throws -> Bool { 122 | let range = NSMakeRange(0, (self as NSString).length) 123 | let regex = try NSRegularExpression(pattern: pattern, options: options) 124 | let matches = regex.firstMatchInString(self, options: [], range: range) 125 | 126 | if matches == nil { 127 | return false 128 | } else { 129 | return true 130 | } 131 | } 132 | 133 | func subrangesMatchingPattern(let pattern: String, let options: NSRegularExpressionOptions) throws -> [NSRange] { 134 | let range = NSMakeRange(0, (self as NSString).length) 135 | let regex = try NSRegularExpression(pattern: pattern, options: options) 136 | let matches = regex.matchesInString(self, options: [], range: range) 137 | return matches.map { return $0.rangeAtIndex(0) } 138 | } 139 | } 140 | 141 | struct Playdown { 142 | let streamReader: StreamReader! 143 | let SingleLineTextBeginningPattern = "^//:" 144 | let MultilineTextBeginningPattern = "/\\*:" 145 | let MultilineTextEndingPattern = "\\*/" 146 | let MarkdownCodeStartDelimiter = "```swift" 147 | let MarkdownCodeEndDelimiter = "```\n" 148 | 149 | enum LineType { 150 | case SingleLineText, MultilineText, SwiftCode 151 | } 152 | 153 | init(filename: String) { 154 | streamReader = StreamReader(path: filename) 155 | } 156 | 157 | func markdown() throws { 158 | var lineState: LineType = .SwiftCode 159 | var previousLineState: LineType? = nil 160 | 161 | let options = NSRegularExpressionOptions.AllowCommentsAndWhitespace 162 | 163 | for line in streamReader { 164 | let singleLineBeginning = try line.matchesPattern(SingleLineTextBeginningPattern, options: options) 165 | let multiLineBeginning = try line.matchesPattern(MultilineTextBeginningPattern, options: options) 166 | let multiLineEnding = try line.matchesPattern(MultilineTextEndingPattern, options: options) 167 | 168 | // Switch into a regular-text line if necessary 169 | if singleLineBeginning { 170 | lineState = .SingleLineText 171 | } else if multiLineBeginning { 172 | lineState = .MultilineText 173 | } else if lineState == .MultilineText { 174 | lineState = .MultilineText 175 | } else { 176 | lineState = .SwiftCode 177 | } 178 | 179 | let outputText: String! 180 | 181 | if previousLineState == nil { 182 | // This is the first line 183 | 184 | switch lineState { 185 | case .SingleLineText: 186 | outputText = stringByStrippingSingleLineTextMetacharactersFromString(line) 187 | case .MultilineText: 188 | // The first line of a multiline comment is never displayed (it's an optional comment) 189 | outputText = "" // stringByStrippingSingleLineTextMetacharactersFromString(line) 190 | default: 191 | if !singleLineBeginning && !multiLineBeginning { 192 | outputText = try stringByAlteringCodeFencing(line) 193 | } else { 194 | outputText = line 195 | } 196 | } 197 | } else { 198 | // This is a regular line 199 | // Old state -> Current state 200 | 201 | switch (previousLineState!, lineState) { 202 | // Swift code -> Other 203 | case (.SwiftCode, .SwiftCode): 204 | outputText = line 205 | case (.SwiftCode, .SingleLineText): 206 | outputText = MarkdownCodeEndDelimiter + stringByStrippingSingleLineTextMetacharactersFromString(line) 207 | case (.SwiftCode, .MultilineText): 208 | // The first line of a multiline comment is never displayed (it's an optional comment) 209 | outputText = MarkdownCodeEndDelimiter + "" // stringByStrippingMultilineTextMetacharactersFromString(line) 210 | 211 | // Single line -> Other 212 | case (.SingleLineText, .SwiftCode): 213 | outputText = try stringByAlteringCodeFencing(line) 214 | case (.SingleLineText, .SingleLineText): 215 | outputText = stringByStrippingSingleLineTextMetacharactersFromString(line) 216 | case (.SingleLineText, .MultilineText): 217 | // The first line of a multiline comment is never displayed (it's an optional comment) 218 | outputText = "" // stringByStrippingMultilineTextMetacharactersFromString(line) 219 | 220 | // Multiline -> Other 221 | case (.MultilineText, .SwiftCode): 222 | outputText = try stringByAlteringCodeFencing(line) 223 | case (.MultilineText, .SingleLineText): 224 | outputText = stringByStrippingSingleLineTextMetacharactersFromString(line) 225 | case (.MultilineText, .MultilineText): 226 | outputText = stringByStrippingMultilineTextMetacharactersFromString(line) 227 | } 228 | 229 | } 230 | 231 | print(outputText) 232 | 233 | previousLineState = lineState 234 | 235 | // Handle switching out of modes 236 | if multiLineEnding { 237 | // Only handle multi-line ending if we were previously in multiline mode 238 | if let previous = previousLineState where previous == .MultilineText { 239 | previousLineState = .MultilineText 240 | lineState = .SwiftCode 241 | } 242 | } 243 | } 244 | 245 | // Handle the closing tags 246 | if lineState == .SwiftCode && previousLineState == .SwiftCode { 247 | print(MarkdownCodeEndDelimiter) 248 | } 249 | } 250 | 251 | func stringByStrippingSingleLineTextMetacharactersFromString(string: String) -> String { 252 | return string.stringByReplacingOccurrencesOfString("//: ", withString: "") 253 | } 254 | 255 | func stringByStrippingMultilineTextMetacharactersFromString(string: String) -> String { 256 | let strippedLine = string.stringByReplacingOccurrencesOfString("/*:", withString: "") 257 | .stringByReplacingOccurrencesOfString("*/", withString: "") 258 | return strippedLine 259 | } 260 | 261 | func stringByAlteringCodeFencing(string: String) throws -> String { 262 | let outputText: String 263 | 264 | // Add a newline between the markdown delimiter if necessary 265 | if try string.matchesPattern("\\n", options: []) || (string as NSString).length == 0 { 266 | // Empty line 267 | outputText = "\n" + MarkdownCodeStartDelimiter + string 268 | } else { 269 | outputText = MarkdownCodeStartDelimiter + "\n" + string 270 | } 271 | 272 | return outputText 273 | } 274 | } 275 | 276 | enum Error: String, ErrorType, CustomStringConvertible { 277 | case FilenameRequired = "Filename Required" 278 | var description: String { 279 | return self.rawValue 280 | } 281 | } 282 | 283 | struct Main { 284 | init() throws { 285 | if Process.arguments.count < 2 { 286 | throw Error.FilenameRequired 287 | } 288 | 289 | let filename = Process.arguments[1] 290 | let playdown = Playdown(filename: filename) 291 | try playdown.markdown() 292 | } 293 | } 294 | 295 | do { 296 | let _ = try Main() 297 | } catch { 298 | print(error) 299 | exit(1) 300 | } --------------------------------------------------------------------------------