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

Qiita は、全体は小さいけど、多数のファイルで構成されたソースコードを記事に入れようと思うと

- 記事にするときも手間
- 利用するときも手間

なので、そういうものは記事にしにくい側面があります。そこで、（Qiita で完結する前提で）手間を軽減したいわけですが、ファイルから Markdown にするものと、その逆があれば少しは良くなるでしょう。既に誰かやっててもよさそうな気がしますが、探せなかったので作ることにしました。

変換処理は

- ファイルから Markdown へ
   - テキスト ファイルの前後に「\`\`\`」の行を追加
      - ファイルの種類に応じたコードの挿入
         - 種類の判定が必要
      - ファイル内行頭「\`\`\`」
         - 元のコードをうまく表示する法方を見つけられなかったので「\」を先頭に追加

- Markdown からファイルへ
   - Markdown の「\`\`\`」から「\`\`\`」までをテキスト ファイルにする
      - ファイル内行頭「\`\`\`」を復元

の簡素なものにしています。

<details><summary>Python スクリプト: ファイルから Markdown へ</summary><div>

```python:f2m.py
#!/usr/bin/env python

import os
import sys
import argparse

mode_map = {
    'applescript': ('.scpt',),
    'ada': ('.ada',),
    #'armasm': ('.s',),
    'awk': ('.awk',),
    'bash': ('.bash',),
    'bat': ('.bat',),
    'bpf': ('.bpf',),
    'cl': ('.cl',),
    'css': ('.css',),
    'csharp': ('.cs',),
    'cuda': ('.cu',),
    'c++': ('.c', '.cc', '.cpp', 'c++',
            '.h', '.hpp', 'h++'), # '.hh'
    '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',), # '.hh'
    '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',),
    #'m68k': ('.s',),
    'make': ('.mak', '.mk'),
    #'mathematica': ('.m',),
    'matlab': ('.mat',),
    'md': ('.md',),
    'mf': ('.mf',),
    'minizinc': ('.mzn',),
    'mkd': ('.mkd',),
    #'ml': ('.ml',),
    'moon': ('.moon',),
    #'nasm': ('.s',),
    'nim': ('.nim',),
    #'objc': ('.m',), # '.h'
    'objcpp': ('.mm',), # '.hh'
    'ocaml': ('.mli',), # '.ml'
    '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', '.sc'),
    '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',),
}

suffix_map = {}
for k in mode_map:
    for x in mode_map[k]:
        if x in suffix_map:
            raise Exception(x, suffix_map[x], k)
        suffix_map[x] = k

parser = argparse.ArgumentParser()
parser.add_argument('-o', '--output', default=None)
parser.add_argument('files', metavar='FILE', type=str, nargs='+')
args = parser.parse_args()

md = ''
for fn in args.files:
    fn = fn.replace('&', '&#x26;')
    fn = fn.replace(' ', '&#x20;')

    ct = 'text'
    sfx = os.path.splitext(fn)[1]
    fnl = fn.lower()
    if fnl == 'makefile':
        ct = 'makefile'
    elif fnl == 'gnumakefile':
        ct = 'gnumake'
    elif sfx in suffix_map:
        ct = suffix_map[sfx.lower()]

    md = md + ('\n```%s:%s\n' % (ct, fn))
    for line in open(fn).readlines():
        if len(line) > 3 and line[:3] == '```':
            line = '\\' + line
        md = md + line
    md = md + '```\n'

o = args.output
if o is None or o == '-':
    o = sys.stdout
else:
    o = open(o, 'w')

o.write(md)
```

</div></details>

<details><summary>Python スクリプト: Markdown からファイルへ</summary><div>

```python:m2f.py
#!/usr/bin/env python

import os
import sys
import argparse
import urllib.request

parser = argparse.ArgumentParser()
parser.add_argument('-c', '--console', action='store_true', default=False)
parser.add_argument('-m', '--math', action='store_true', default=False)
parser.add_argument('-o', '--outputdir', metavar='DIR', default=None)
parser.add_argument('markdown', metavar='URL/FILE', type=str, default=None)
args = parser.parse_args()

lines = []
if args.markdown is None:
    lines = sys.stdin.readlines()
elif args.markdown == '-':
    lines = sys.stdin.readlnes()
elif args.markdown.split(':')[0] in ('http', 'https'):
    with urllib.request.urlopen(args.markdown) as res:
        lines = []
        for line in res.read().decode().split('\n'):
            lines.append(line + '\n')
else:
    lines = open(args.markdown).readlines()

odir = args.outputdir
if odir is None:
    odir = '.'
if odir[-1] != '/':
    odir += '/'

exclude_modes = []
if not args.console:
    exclude_modes.append('console')
if not args.math:
    exclude_modes.append('math')

index = 0
output = None
for line in lines:
    if len(line) > 3 and line[:3] == '```':
        if output is None:
            code = line[3:].split(':', 1)
            mode = code[0].strip().lower()
            if len(mode) == 0:
                continue
            if mode in exclude_modes:
                continue
            fname = ''
            if len(code) > 1:
                fname = code[1].strip()
            if len(fname) == 0:
                fname = 'code:%d.%s' % (index, mode)
                index += 1
            fname = fname.replace('&#x20;', ' ')
            fname = fname.replace('&#x26;', '&')
            path = odir + fname
            os.makedirs(os.path.dirname(path), exist_ok=True)
            print(path)
            output = open(path, 'w')
            continue

        output.close()
        output = None
        continue

    if output is None:
        continue

    if len(line) > 4 and line[:4] == '\\```':
        line = line[1:]
    output.write(line)
```

</div></details>

[こちらの記事](https://qiita.com/ikiuo/items/2dc82e76337010f935a1)の取得例

```console:
$ python3 m2f.py -o sample https://qiita.com/ikiuo/items/2dc82e76337010f935a1.md
sample/main.swift
sample/zlibwrapper.m
sample/project-Bridging-Header.h
```