0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

投稿記事の Markdown 中のコードをファイルとして取り出す (Swift版)

Posted at

投稿記事の 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: "&#x20;", with: " ")
            fn = fn.replacingOccurrences(of: "&#x26;", 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())
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?