| @@ -2,8 +2,9 @@ import Foundation |
| 2 | |
2 | |
| 3 | // `org-babel-tangle` (Org 9.8.7 ob-tangle.el): source blocks written to the files their |
3 | // `org-babel-tangle` (Org 9.8.7 ob-tangle.el): source blocks written to the files their |
| 4 | // `:tangle` names, with `:noweb`, `:padline`, `:shebang`, `:mkdirp`, `:tangle-mode`, |
4 | // `:tangle` names, with `:noweb`, `:padline`, `:shebang`, `:mkdirp`, `:tangle-mode`, |
| 5 | // `:prologue` and `:epilogue`, and `:var` for shells, Python and Emacs Lisp. Comments |
5 | // `:prologue` and `:epilogue`, `:var` for shells, Python and Emacs Lisp, and `:comments` for |
| 6 | // (`:comments`) and header values Org would evaluate as Lisp aren't supported. |
6 | // languages whose comment syntax is known. Header values Org would evaluate as Lisp aren't |
| |
7 | // supported. |
| 7 | |
8 | |
| 8 | public enum Tangle { |
9 | public enum Tangle { |
| 9 | /// All blocks (C-c C-v t), the block at an offset (C-u C-c C-v t), or the blocks going to |
10 | /// All blocks (C-c C-v t), the block at an offset (C-u C-c C-v t), or the blocks going to |
| @@ -42,16 +43,29 @@ public enum Tangle { |
| 42 | do { |
43 | do { |
| 43 | var order: [String] = [] |
44 | var order: [String] = [] |
| 44 | var specs: [String: [(language: String?, body: String, params: Babel.Params)]] = [:] |
45 | var specs: [String: [(language: String?, body: String, params: Babel.Params)]] = [:] |
| 45 | func add(_ block: SrcBlockInfo, _ params: Babel.Params) throws { |
46 | // `org-babel-tangle-collect-blocks`: blocks numbered from 1 under each heading. |
| |
47 | var counters: [Int: Int] = [:] |
| |
48 | var lastHeading: Int?? = .none |
| |
49 | var counter = 0 |
| |
50 | for block in model.srcBlocks { |
| |
51 | if lastHeading == .some(block.heading) { counter += 1 } else { (counter, lastHeading) = (1, .some(block.heading)) } |
| |
52 | counters[block.range.lowerBound] = counter |
| |
53 | } |
| |
54 | func add(_ block: SrcBlockInfo, _ params: Babel.Params, counter: Int) throws { |
| 46 | guard let file = try fileName(path: path, language: block.language, tangle: params.single[":tangle"] ?? "no") else { return } |
55 | guard let file = try fileName(path: path, language: block.language, tangle: params.single[":tangle"] ?? "no") else { return } |
| 47 | let body = try body(block, params: params, model: model, text: ns) |
56 | let parsed = tree ?? OrgParser.parse(text) |
| |
57 | let links = { (other: SrcBlockInfo, otherParams: Babel.Params) in |
| |
58 | try Self.links(other, params: otherParams, counter: counters[other.range.lowerBound] ?? 1, path: path, model: model, tree: parsed, text: ns) |
| |
59 | } |
| |
60 | let body = try spec(block, body: try body(block, params: params, model: model, text: ns, links: links), params: params, counter: counter, |
| |
61 | path: path, model: model, tree: parsed, text: ns) |
| 48 | if specs[file] == nil { order.append(file) } |
62 | if specs[file] == nil { order.append(file) } |
| 49 | specs[file, default: []].append((block.language, body, params)) |
63 | specs[file, default: []].append((block.language, body, params)) |
| 50 | } |
64 | } |
| 51 | switch scope { |
65 | switch scope { |
| 52 | case .block(let offset): |
66 | case .block(let offset): |
| 53 | guard let block = block(at: offset, in: model) else { throw Babel.Failure.message("Point is not in a source code block") } |
67 | guard let block = block(at: offset, in: model) else { throw Babel.Failure.message("Point is not in a source code block") } |
| 54 | try add(block, try params(block, model: model, text: ns)) |
68 | try add(block, try params(block, model: model, text: ns), counter: 1) |
| 55 | case .all, .target: |
69 | case .all, .target: |
| 56 | var target: String? |
70 | var target: String? |
| 57 | if case .target(let offset) = scope { |
71 | if case .target(let offset) = scope { |
| @@ -63,7 +77,7 @@ public enum Tangle { |
| 63 | let tangle = params.single[":tangle"] ?? "no" |
77 | let tangle = params.single[":tangle"] ?? "no" |
| 64 | if tangle == "no" || (block.language == nil && tangle == "yes") { continue } |
78 | if tangle == "no" || (block.language == nil && tangle == "yes") { continue } |
| 65 | if let target, target != tangle { continue } |
79 | if let target, target != tangle { continue } |
| 66 | try add(block, params) |
80 | try add(block, params, counter: counters[block.range.lowerBound] ?? 1) |
| 67 | } |
81 | } |
| 68 | } |
82 | } |
| 69 | var files: [File] = [] |
83 | var files: [File] = [] |
| @@ -85,7 +99,7 @@ public enum Tangle { |
| 85 | contents += shebang + "\n" |
99 | contents += shebang + "\n" |
| 86 | sheBanged = true |
100 | sheBanged = true |
| 87 | } |
101 | } |
| 88 | contents += spec.body + "\n" |
102 | contents += spec.body |
| 89 | blocks += 1 |
103 | blocks += 1 |
| 90 | } |
104 | } |
| 91 | files.append(File(path: file, contents: contents, mode: mode, makeDirectories: makeDirectories)) |
105 | files.append(File(path: file, contents: contents, mode: mode, makeDirectories: makeDirectories)) |
| @@ -117,9 +131,6 @@ public enum Tangle { |
| 117 | for (key, value) in params.single where value.hasPrefix("(") && key != ":tangle-mode" { |
131 | for (key, value) in params.single where value.hasPrefix("(") && key != ":tangle-mode" { |
| 118 | throw Babel.Failure.message("\(key) \(value) is Lisp to evaluate, which tangling doesn't support yet; nothing was tangled.") |
132 | throw Babel.Failure.message("\(key) \(value) is Lisp to evaluate, which tangling doesn't support yet; nothing was tangled.") |
| 119 | } |
133 | } |
| 120 | if let comments = params.single[":comments"], comments != "no" { |
| |
| 121 | throw Babel.Failure.message(":comments \(comments) isn't supported yet; nothing was tangled.") |
| |
| 122 | } |
| |
| 123 | return params |
134 | return params |
| 124 | } |
135 | } |
| 125 | |
136 | |
| @@ -146,14 +157,15 @@ public enum Tangle { |
| 146 | /// `org-babel-tangle-single-block`'s body: noweb references expanded or stripped, the |
157 | /// `org-babel-tangle-single-block`'s body: noweb references expanded or stripped, the |
| 147 | /// language's expansion with `:var`, `:prologue` and `:epilogue`, `-r` coderefs taken |
158 | /// language's expansion with `:var`, `:prologue` and `:epilogue`, `-r` coderefs taken |
| 148 | /// out, and indentation and surrounding blank space trimmed. |
159 | /// out, and indentation and surrounding blank space trimmed. |
| 149 | static func body(_ block: SrcBlockInfo, params: Babel.Params, model: DocumentModel, text: NSString) throws -> String { |
160 | static func body(_ block: SrcBlockInfo, params: Babel.Params, model: DocumentModel, text: NSString, |
| |
161 | links: ((SrcBlockInfo, Babel.Params) throws -> (begin: String, end: String))? = nil) throws -> String { |
| 150 | var body = Babel.normalizedBody(block) |
162 | var body = Babel.normalizedBody(block) |
| 151 | let noweb = params.single[":noweb"] |
163 | let noweb = params.single[":noweb"] |
| 152 | if Babel.nowebTangles(noweb) { |
164 | if Babel.nowebTangles(noweb) { |
| 153 | if noweb == "strip-tangle" { |
165 | if noweb == "strip-tangle" { |
| 154 | body = body.replacingOccurrences(of: "<<([^ \\t\\n](?:.*?[^ \\t\\n])?)>>", with: "", options: .regularExpression) |
166 | body = body.replacingOccurrences(of: "<<([^ \\t\\n](?:.*?[^ \\t\\n])?)>>", with: "", options: .regularExpression) |
| 155 | } else { |
167 | } else { |
| 156 | body = try Babel.expandNoweb(block, params: params, model: model, text: text, tangling: true) |
168 | body = try Babel.expandNoweb(block, params: params, model: model, text: text, tangling: true, links: links) |
| 157 | } |
169 | } |
| 158 | } |
170 | } |
| 159 | if params.single[":no-expand"] == nil { |
171 | if params.single[":no-expand"] == nil { |
| @@ -174,6 +186,91 @@ public enum Tangle { |
| 174 | : body.replacingOccurrences(of: "^[ \\t\\n\\r]+", with: "", options: .regularExpression) |
186 | : body.replacingOccurrences(of: "^[ \\t\\n\\r]+", with: "", options: .regularExpression) |
| 175 | } |
187 | } |
| 176 | |
188 | |
| |
189 | /// `org-babel-spec-to-string`: the block's text in the file, with its `:comments`. |
| |
190 | static func spec(_ block: SrcBlockInfo, body: String, params: Babel.Params, counter: Int, path: String, model: DocumentModel, tree: OrgTree, text: NSString) throws -> String { |
| |
191 | let comments = params.single[":comments"] ?? "no" |
| |
192 | guard comments != "no" else { return body + "\n" } |
| |
193 | let language = block.language ?? "" |
| |
194 | let linked = ["both", "link", "yes", "noweb"].contains(comments) |
| |
195 | var out = "" |
| |
196 | if comments == "org" || comments == "both" { |
| |
197 | let prose = orgComment(block, model: model, text: text) |
| |
198 | if prose.contains(where: { !$0.isWhitespace }) { out += try commentRegion(prose, language: language) + "\n" } |
| |
199 | } |
| |
200 | let link = linked ? try links(block, params: params, counter: counter, path: path, model: model, tree: tree, text: text) : nil |
| |
201 | if let link { out += try commentRegion(link.begin, language: language) + "\n" } |
| |
202 | out += body + "\n" |
| |
203 | if let link { out += try commentRegion(link.end, language: language) + "\n" } |
| |
204 | return out |
| |
205 | } |
| |
206 | |
| |
207 | /// `org-babel-tangle-comment-format-beg` and `-end` for a block: `[[LINK][NAME]]` and |
| |
208 | /// `NAME ends here`, the link relative to the tangled file's folder. |
| |
209 | static func links(_ block: SrcBlockInfo, params: Babel.Params, counter: Int, path: String, model: DocumentModel, tree: OrgTree, text: NSString) throws -> (begin: String, end: String) { |
| |
210 | let directory = (path as NSString).deletingLastPathComponent |
| |
211 | let tangle = params.single[":tangle"] ?? "no" |
| |
212 | let target = tangle.contains("/") ? (expand(tangle, in: directory) as NSString).deletingLastPathComponent : directory |
| |
213 | let start = beginLine(block, text: text) |
| |
214 | let context = EditContext(revision: 0, text: text as String, tree: tree, selection: [start..<start]) |
| |
215 | let stored = Links.store(in: context, path: relativePath(path, from: target)).link |
| |
216 | let name = block.name ?? ((block.heading.map { model.headings[$0].title.trimmingCharacters(in: .whitespaces) } ?? "No heading") + ":\(counter)") |
| |
217 | return ("[[\(stored)][\(name)]]", "\(name) ends here") |
| |
218 | } |
| |
219 | |
| |
220 | /// `file-relative-name`. |
| |
221 | static func relativePath(_ path: String, from directory: String) -> String { |
| |
222 | let to = (path as NSString).standardizingPath.split(separator: "/") |
| |
223 | let from = (directory as NSString).standardizingPath.split(separator: "/") |
| |
224 | let common = zip(to, from).prefix { $0 == $1 }.count |
| |
225 | return (Array(repeating: "..", count: from.count - common) + to.dropFirst(common).map(String.init)).joined(separator: "/") |
| |
226 | } |
| |
227 | |
| |
228 | /// The start of the block's `#+begin_src` line. |
| |
229 | static func beginLine(_ block: SrcBlockInfo, text: NSString) -> Int { |
| |
230 | let regex = try! NSRegularExpression(pattern: "^[ \\t]*#\\+begin_src", options: [.anchorsMatchLines, .caseInsensitive]) |
| |
231 | return regex.firstMatch(in: text as String, range: NSRange(block.range))?.range.location ?? block.range.lowerBound |
| |
232 | } |
| |
233 | |
| |
234 | /// The prose `:comments org` takes: from the heading's title, or the end of the block |
| |
235 | /// before, to the block, without its common indentation. |
| |
236 | static func orgComment(_ block: SrcBlockInfo, model: DocumentModel, text: NSString) -> String { |
| |
237 | let begin = beginLine(block, text: text) |
| |
238 | var start = 0 |
| |
239 | if let heading = block.heading { |
| |
240 | let line = text.substring(with: NSRange(model.headings[heading].headingRange)) |
| |
241 | start = model.headings[heading].headingRange.lowerBound + (line.firstMatch(of: #/^\*+ /#).map { $0.output.utf16.count } ?? 0) |
| |
242 | } |
| |
243 | let end = try! NSRegularExpression(pattern: "^[ \\t]*#\\+end_src", options: [.anchorsMatchLines, .caseInsensitive]) |
| |
244 | for other in model.srcBlocks where other.range.lowerBound < begin { |
| |
245 | if let m = end.firstMatch(in: text as String, range: NSRange(other.range)), NSMaxRange(m.range) <= begin { start = max(start, NSMaxRange(m.range)) } |
| |
246 | } |
| |
247 | return removeIndentation(text.substring(with: NSRange(location: start, length: max(0, begin - start)))) |
| |
248 | } |
| |
249 | |
| |
250 | /// Comment syntax of the language's major mode (`org-src-get-lang-mode`), as |
| |
251 | /// `comment-region` writes it. |
| |
252 | static let commentSyntax: [String: (start: String, end: String)] = { |
| |
253 | var table: [String: (String, String)] = [:] |
| |
254 | for language in ["emacs-lisp", "elisp", "lisp", "scheme"] { table[language] = (";; ", "") } |
| |
255 | for language in ["python", "ruby", "perl", "conf", "toml", "makefile", "awk", "tcl"] { table[language] = ("# ", "") } |
| |
256 | for language in shells.subtracting(["fish"]) { table[language] = ("# ", "") } |
| |
257 | for language in ["js", "java", "cpp", "C++"] { table[language] = ("// ", "") } |
| |
258 | for language in ["c", "C", "css"] { table[language] = ("/* ", " */") } |
| |
259 | for language in ["lua", "sql", "sqlite"] { table[language] = ("-- ", "") } |
| |
260 | for language in ["latex"] { table[language] = ("%% ", "") } |
| |
261 | return table |
| |
262 | }() |
| |
263 | |
| |
264 | /// `comment-region` over `text`: each line that isn't blank, commented. |
| |
265 | static func commentRegion(_ text: String, language: String) throws -> String { |
| |
266 | guard let syntax = commentSyntax[language] else { |
| |
267 | throw Babel.Failure.message(":comments needs the comment syntax of \(language.isEmpty ? "the block's language" : language), which isn't known yet; nothing was tangled.") |
| |
268 | } |
| |
269 | return text.components(separatedBy: "\n").map { line in |
| |
270 | line.contains(where: { !$0.isWhitespace }) ? syntax.start + line + syntax.end : line |
| |
271 | }.joined(separator: "\n") |
| |
272 | } |
| |
273 | |
| 177 | /// `org-remove-indentation`. |
274 | /// `org-remove-indentation`. |
| 178 | static func removeIndentation(_ s: String) -> String { |
275 | static func removeIndentation(_ s: String) -> String { |
| 179 | let lines = s.components(separatedBy: "\n") |
276 | let lines = s.components(separatedBy: "\n") |