「投稿記事の Markdown 中のコードをファイルとして取り出す」の Swift 版です。
記事に、全体は小さいけど、多数のファイルで構成されたソースコードを入れるとき
- 記事にするときの手間
- 利用するときの手間
を軽減する目的で作りました。Swift の Command Line Tool です。
ファイルから Markdown へ
f2m.swift
import Foundation
import AppKit
// ############
var suffix_map = Dictionary<String, String>()
let code_suffix_table = [
["applescript", "scpt"],
["ada", "ada"],
["awk", "awk"],
["bash", "bash"],
["bat", "bat"],
["bpf", "bpf"],
["cl", "cl"],
["css", "css"],
["csharp", "cs"],
["cuda", "cu"],
["c++", "c", " cc", " cpp", " c++", " hpp", " h++"],
["d", "d"],
["diff", "diff"],
["ecl", "ecl"],
["elisp", "el"],
["elm", "elm"],
["email", "eml"],
["eps", "eps"],
["erl", "erl"],
["erb", "erb"],
["fea", "fea"],
["ff", "ff"],
["fortran", "f", " f90", " f95", " f03", " f15"],
["fsharp", "fs"],
["gd", "gd"],
["glsl", "glsl", " glslv", " frag", " vert"],
["go", "go"],
["groovy", "groovy", " gvy", " gy", " gsh"],
["haml", "haml"],
["hack", "hack"],
["hcl", "hcl"],
["hlsl", "hlsl"],
["hql", "hql"],
["hs", "hs"],
["html", "html"],
["hx", "hx"],
["hy", "hy"],
["idlang", "idl"],
["java", "java"],
["js", "js"],
["json", "json'"],
["ksh", "ksh"],
["latex", "latex"],
["lhs", "lhs"],
["llvm", "ll"],
["lua", "lua"],
["make", "mak", " mk"],
["mathematica", " nb"],
["matlab", "mat"],
["md", "md"],
["mf", "mf"],
["minizinc", "mzn"],
["mkd", "mkd"],
["moon", "moon"],
["nim", "nim"],
["objcpp", "mm"],
["ocaml", " mli"],
["pascal", "p", " pas", " pp"],
["patch", "patch"],
["php", "php"],
["pl", "pl"],
["plist", "plist"],
["ps", "ps"],
["py", "py"],
["pyx", "pyx"],
["rb", "rb"],
["rs", "rs"],
["saas", "saas"],
["sas", "sas"],
["scala", "scala"],
["scss", "scss"],
["sed", "sed"],
["sh", "sh"],
["sql", "sql"],
["st", "st"],
["swift", "swift"],
["tcl", "tcl"],
["tex", "tex"],
["toml", "toml"],
["ts", "ts"],
["tsx", "tsx"],
["vb", "vb"],
["xml", "xml"],
["yaml", "yaml", " yml"],
["yang", "yang"]
]
func make_suffix_map()
{
for cs in code_suffix_table {
let code = cs[0]
for sfx in cs[1..<cs.count] {
suffix_map[sfx] = code
}
}
}
make_suffix_map()
// ############
var cmd_args = CommandLine.arguments
var program = cmd_args.removeFirst()
var program_paths = program.split(separator: "/")
var program_name = program_paths.last
var output_file = "-"
func usage() -> Never {
print("""
使用方法: \(String(program_name!)) [オプション] ファイル [ファイル...]
オプション:
-o FILE 出力するファイルを指定する(デフォルト: 標準出力 "-")
-X クリップボードへコピーする
""")
exit(1)
}
func cmdArgParse() -> Bool {
var flag_pastboard = false
while cmd_args.count > 0 {
var arg = cmd_args[0]
if arg.removeFirst() != "-" {
break
}
if arg.count == 0 {
break
}
cmd_args.removeFirst()
while arg.count > 0 {
switch (arg.removeFirst())
{
case "o":
output_file = cmd_args.removeFirst()
case "X":
flag_pastboard = true
default:
usage()
}
}
}
if cmd_args.count == 0 {
usage()
}
return flag_pastboard
}
func main() -> Int32 {
let pastboard = cmdArgParse()
var md = ""
var md_sep = ""
for fn in cmd_args {
let data = FileManager.default.contents(atPath: fn)
if data == nil {
print("読み込み失敗: \(fn)")
return 2
}
let fext = URL(fileURLWithPath: fn).pathExtension
let code = suffix_map[fext] ?? "text"
md += md_sep
md += "```\(code):\(fn)\n"
let src = String(data: data!, encoding: .utf8)!
for var line in src.components(separatedBy: .newlines) {
if line.prefix(3) == "```" {
line = "\\" + line
}
md += line + "\n"
}
md += "```\n"
md_sep = "\n"
}
if pastboard {
NSPasteboard.general.clearContents()
if !NSPasteboard.general.setString(md, forType: .string) {
print("クリップボードへのテキスト設定に失敗しました")
return 2
}
return 0
}
if output_file == "-" {
print(md)
} else if !FileManager.default.createFile(atPath: output_file, contents: md.data(using: .utf8)) {
print("書き込み失敗: \(output_file)")
return 2
}
return 0
}
exit(main())
Markdown からファイルへ
m2f.swift
import Foundation
import AppKit
// ############
var auto_filename = false
var output_dir = "."
var exclude_modes: [String] = []
// ############
class MyWebLoader {
var sem = DispatchSemaphore(value: 0)
var error: Error?
var response: HTTPURLResponse?
var data: Data?
init(_ string: String) {
let url = URL(string: string)!
let req = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: req) { dat, res, err in
self.error = err
self.response = res as? HTTPURLResponse
self.data = dat
self.sem.signal()
}
task.resume()
sem.wait()
}
}
// ############
struct FileData {
var type = ""
var name = ""
var data = ""
}
func parseMarkdown(_ md: String) -> [FileData] {
var files: [FileData] = []
var file = FileData()
var avail = false
var code = false
var index = 0
for var line in md.components(separatedBy: .newlines) {
if line.prefix(4) == "\\```" {
if code {
line.removeFirst()
file.data += "\(line)\n"
}
continue
}
if line.prefix(3) != "```" {
if code {
file.data += "\(line)\n"
}
continue
}
code = !code
if !code {
if avail {
files.append(file)
}
file = FileData()
avail = false
continue
}
let cf = line.dropFirst(3)
let cfs = cf.split(separator: ":", maxSplits: 1)
let ct = String(cfs[0]).trimmingCharacters(in: .whitespacesAndNewlines)
if ct.count == 0 {
continue
}
file.type = ct
if cfs.count > 1 {
var fn = String(cfs[1]).trimmingCharacters(in: .whitespacesAndNewlines)
fn = fn.replacingOccurrences(of: " ", with: " ")
fn = fn.replacingOccurrences(of: "&", with: "&")
avail = !exclude_modes.contains(ct)
file.name = fn
} else {
avail = auto_filename
file.name = "code:\(index).\(ct)"
index += 1
}
}
return files
}
// ############
func readMarkdown(_ input: String) -> String? {
if input == "-" {
// 標準入力からの読み込み
return String(data: FileHandle.standardInput.readDataToEndOfFile(), encoding: .utf8)
}
let input_s = input.split(separator: ":")
switch (input_s[0]) {
case "http":
fallthrough
case "https":
// Web からの読み込み
let hl = MyWebLoader(input)
if hl.error != nil {
print("読み込み失敗: \(input)")
print("エラー: \(hl.error!.localizedDescription)")
return nil
}
if hl.response != nil && !(200...209).contains(hl.response!.statusCode) {
print("読み込み失敗: \(input)")
print("応答: \(hl.response!.statusCode)")
return nil
}
return String(data: hl.data!, encoding: .utf8)!
default:
// ファイルからの読み込み
return String(data: FileManager.default.contents(atPath: input)!, encoding: .utf8)
}
}
// ############
var cmd_args = CommandLine.arguments
var program = cmd_args.removeFirst()
var program_paths = program.split(separator: "/")
var program_name = program_paths.last
func usage() -> Never {
print("""
使用方法: \(String(program_name!)) [オプション] {-X|入力}
オプション:
-c console も対象にする
-m math も対象にする
-n ファイル名なしも対象にする
-o DIR 出力するディレクトリを指定する(デフォルト: ".")
-X クリップボードのテキストを使用する
入力は以下のどれか
http(s): で始まる Web サイトのアドレス
ファイル名 または 標準入力 "-"
""")
exit(1)
}
func cmdArgParse() -> Bool {
var flag_console = false
var flag_math = false
var flag_pastboard = false
while cmd_args.count > 0 {
var arg = cmd_args[0]
if arg.removeFirst() != "-" {
break
}
if arg.count == 0 {
break
}
cmd_args.removeFirst()
while arg.count > 0 {
switch (arg.removeFirst())
{
case "c":
flag_console = true
case "m":
flag_math = true
case "n":
auto_filename = true
case "o":
output_dir = cmd_args.removeFirst()
case "X":
flag_pastboard = true
default:
usage()
}
}
}
if !flag_console {
exclude_modes.append("console")
}
if !flag_math {
exclude_modes.append("math")
}
if output_dir.last != "/" {
output_dir += "/"
}
return flag_pastboard
}
func main() -> Int32 {
var md: String?
if cmdArgParse() {
if cmd_args.count > 0 {
usage()
}
md = NSPasteboard.general.string(forType: .string)
if md == nil {
print("クリップボードからテキストの取得に失敗しました")
return 2
}
} else {
if cmd_args.count < 1 {
usage()
}
let input = cmd_args[0]
md = readMarkdown(input)
if md == nil {
print("読み込み失敗: \(input)")
return 2
}
}
var ecd: Int32 = 0
let pmd = parseMarkdown(md!)
for md in pmd {
let path = output_dir + md.name
let pdir = (path as NSString).deletingLastPathComponent
print(path)
try! FileManager.default.createDirectory(atPath: pdir, withIntermediateDirectories: true)
if !FileManager.default.createFile(atPath: path, contents: md.data.data(using: .utf8)) {
print("書き込み失敗: \(path)")
ecd = 2
}
}
return ecd
}
exit(main())