|
| 1 | +// |
| 2 | +// main.swift |
| 3 | +// qlmarkdown_cli |
| 4 | +// |
| 5 | +// Created by Sbarex on 18/10/21. |
| 6 | +// |
| 7 | + |
| 8 | +import Cocoa |
| 9 | +import OSLog |
| 10 | + |
| 11 | +let cliUrl = URL(fileURLWithPath: CommandLine.arguments[0]) |
| 12 | + |
| 13 | +var standardError = FileHandle.standardError |
| 14 | + |
| 15 | +extension FileHandle : TextOutputStream { |
| 16 | + public func write(_ string: String) { |
| 17 | + guard let data = string.data(using: .utf8) else { return } |
| 18 | + self.write(data) |
| 19 | + } |
| 20 | +} |
| 21 | + |
| 22 | +func usage(exitCode: Int = -1) { |
| 23 | + let name = cliUrl.lastPathComponent |
| 24 | + print("\(name)") |
| 25 | + print("Usage: \(name) [--app <path>] [-o <file|dir>] [-v] <file> [..]") |
| 26 | + print("\nArguments:") |
| 27 | + print(" -h\tShow this help and exit.") |
| 28 | + print(" -o\t<file|dir> Destination output. If you pass a directory, a new file is created with the name of the processed source with html extension. \n \tThe destination file is always overwritten. If this argument is not provided, the output will be printed to the stdout.") |
| 29 | + print(" -v\tVerbose mode. Valid only with the -o option.") |
| 30 | + // print(" --app\t<path> Set the path of \"QLMarkdown.app\" otherwise assume that \(name) is called from the Contents/Resources of the app bundle.") |
| 31 | + print("\nTo handle multiple files at time you need to pass the -o arguments with a destination folder.") |
| 32 | + print("\nPlease use the main app to customize the rendering settings.") |
| 33 | + |
| 34 | + if exitCode >= 0 { |
| 35 | + exit(Int32(exitCode)) |
| 36 | + } |
| 37 | +} |
| 38 | + |
| 39 | +var appUrl: URL! |
| 40 | +var files: [URL] = [] |
| 41 | +var dest: URL? |
| 42 | +var verbose = false |
| 43 | + |
| 44 | +var i = 1 |
| 45 | +while i < Int(CommandLine.argc) { |
| 46 | + var arg = CommandLine.arguments[i] |
| 47 | + if arg.hasPrefix("-") { |
| 48 | + if arg.hasPrefix("--") { |
| 49 | + // process a --arg |
| 50 | + switch arg { |
| 51 | + case "--help": |
| 52 | + usage(exitCode: 0) |
| 53 | + case "--app": |
| 54 | + let u = CommandLine.arguments[i+1] |
| 55 | + i += 1 |
| 56 | + appUrl = URL(fileURLWithPath: u) |
| 57 | + default: |
| 58 | + print("\(cliUrl.lastPathComponent): illegal option -\(arg)\n", to: &standardError) |
| 59 | + usage(exitCode: 1) |
| 60 | + } |
| 61 | + } else { |
| 62 | + // process a -arg |
| 63 | + arg.removeFirst() |
| 64 | + for (j, arg1) in arg.enumerated() { |
| 65 | + switch arg1 { |
| 66 | + case "h": |
| 67 | + usage(exitCode: 0) |
| 68 | + case "o": |
| 69 | + if j + 1 == arg.count { |
| 70 | + dest = URL(fileURLWithPath: CommandLine.arguments[i+1]) |
| 71 | + i += 1 |
| 72 | + } else { |
| 73 | + print("\(cliUrl.lastPathComponent): option -\(arg1) require a destination path\n", to: &standardError) |
| 74 | + usage(exitCode: 1) |
| 75 | + } |
| 76 | + case "v": |
| 77 | + verbose = true |
| 78 | + default: |
| 79 | + print("\(cliUrl.lastPathComponent): illegal option -\(arg1)\n", to: &standardError) |
| 80 | + usage(exitCode: 1) |
| 81 | + } |
| 82 | + } |
| 83 | + } |
| 84 | + } else { |
| 85 | + files.append(URL(fileURLWithPath: arg)) |
| 86 | + } |
| 87 | + /* |
| 88 | + switch arg { |
| 89 | + case "--help", "-h": |
| 90 | + usage() |
| 91 | + exit(0) |
| 92 | + case "--app": |
| 93 | + let u = CommandLine.arguments[i+1] |
| 94 | + i += 1 |
| 95 | + appUrl = URL(fileURLWithPath: u) |
| 96 | + case "-o": |
| 97 | + dest = URL(fileURLWithPath: CommandLine.arguments[i+1]) |
| 98 | + i += 1 |
| 99 | + case "-v": |
| 100 | + verbose = true |
| 101 | + default: |
| 102 | + if arg.hasPrefix("-") { |
| 103 | + print("\(cliUrl.lastPathComponent): illegal option \(arg)", to: &standardError) |
| 104 | + usage() |
| 105 | + exit(1) |
| 106 | + } |
| 107 | + files.append(URL(fileURLWithPath: arg)) |
| 108 | + } |
| 109 | + */ |
| 110 | + i += 1 |
| 111 | +} |
| 112 | + |
| 113 | +verbose = verbose && dest != nil |
| 114 | + |
| 115 | +if appUrl == nil { |
| 116 | + appUrl = cliUrl.deletingLastPathComponent().deletingLastPathComponent() |
| 117 | +} |
| 118 | + |
| 119 | +let appBundleUrl = appUrl.appendingPathComponent("Contents/Resources") |
| 120 | + |
| 121 | + |
| 122 | +if files.count > 1 { |
| 123 | + var isDir: ObjCBool = false |
| 124 | + if let dest = dest { |
| 125 | + FileManager.default.fileExists(atPath: dest.path, isDirectory: &isDir) |
| 126 | + } |
| 127 | + if !isDir.boolValue { |
| 128 | + print("Error: to process multiple files you must use the -o arguments with a folder path!", to: &standardError) |
| 129 | + exit(1) |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +var n = 0 |
| 134 | +defer { |
| 135 | + if verbose { |
| 136 | + print(n != 1 ? "Processed \(n) files." : "Processed 1 file.") |
| 137 | + } |
| 138 | +} |
| 139 | + |
| 140 | +Settings.appBundleUrl = appBundleUrl |
| 141 | +let settings = Settings.shared |
| 142 | + |
| 143 | +let type = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") ?? "Light" |
| 144 | + |
| 145 | +if verbose { |
| 146 | + print("\(cliUrl.lastPathComponent):") |
| 147 | +} |
| 148 | + |
| 149 | +for url in files { |
| 150 | + let markdown_url: URL |
| 151 | + if let typeIdentifier = (try? url.resourceValues(forKeys: [.typeIdentifierKey]))?.typeIdentifier, typeIdentifier == "org.textbundle.package" { |
| 152 | + if FileManager.default.fileExists(atPath: url.appendingPathComponent("text.md").path) { |
| 153 | + markdown_url = url.appendingPathComponent("text.md") |
| 154 | + } else { |
| 155 | + markdown_url = url.appendingPathComponent("text.markdown") |
| 156 | + } |
| 157 | + } else { |
| 158 | + markdown_url = url |
| 159 | + } |
| 160 | + |
| 161 | + |
| 162 | + do { |
| 163 | + if !FileManager.default.isReadableFile(atPath: markdown_url.path) { |
| 164 | + print("Unable to read the file \(markdown_url.path)", to: &standardError) |
| 165 | + exit(127) |
| 166 | + } |
| 167 | + if verbose { |
| 168 | + print("- processing \(markdown_url.path) ...") |
| 169 | + } |
| 170 | + let text = try settings.render(file: markdown_url, forAppearance: type == "Light" ? .light : .dark, baseDir: markdown_url.deletingLastPathComponent().path, log: nil) |
| 171 | + |
| 172 | + let html = settings.getCompleteHTML(title: url.lastPathComponent, body: text) |
| 173 | + |
| 174 | + var output: URL? |
| 175 | + if let dest = dest { |
| 176 | + var isDir: ObjCBool = false |
| 177 | + FileManager.default.fileExists(atPath: dest.path, isDirectory: &isDir) |
| 178 | + if isDir.boolValue { |
| 179 | + output = dest.appendingPathComponent(url.deletingPathExtension().lastPathComponent).appendingPathExtension("html") |
| 180 | + } else { |
| 181 | + output = dest |
| 182 | + } |
| 183 | + /* |
| 184 | + if !(output?.pathExtension.lowercased().hasPrefix("htm") ?? false) { |
| 185 | + output?.appendPathExtension("html") |
| 186 | + } |
| 187 | + */ |
| 188 | + } |
| 189 | + |
| 190 | + if let output = output { |
| 191 | + try html.write(to: output, atomically: true, encoding: .utf8) |
| 192 | + if verbose { |
| 193 | + print(" ... stored in \(output.path)") |
| 194 | + } |
| 195 | + n += 1 |
| 196 | + } else { |
| 197 | + FileHandle.standardOutput.write(html) |
| 198 | + n += 1 |
| 199 | + } |
| 200 | + } catch { |
| 201 | + print("Error processing \(url.path): \(error.localizedDescription)", to: &standardError) |
| 202 | + exit(1) |
| 203 | + } |
| 204 | +} |
0 commit comments