Markdown記法をBacklog記法に変換するものを作った

2019.07.29

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

BacklogプロジェクトでWikiの編集などにBacklog記法を使うことが多々ありますよね。
ですが私はMarkdown記法の方が好きなんです。
プロジェクト設定で、Backlog記法しか使えなくても使いたいんです。

禁断の何かに落ちたので変換するものを作りました。

ツールについて

ソースコードはここにあります。

例えばこのようなファイルがあったとします。

sample.md

## Hello
World

このファイルに対してツールを使います。
そうすることで標準出力に、Backlog記法に変換したものが吐きこ出させます。

$ npx md2bg sample.md

** Hello
World

技術的な話

引数として受け取ったMarkdown形式のファイルをASTにパースします。
そして、ASTを元に、Backlog記法に変換して、それを最後に標準出力に吐き出します。

ASTとは

ASTは英語で、Abstract Structure Tree、日本語で、抽象構文木と言います。
血液検査で出てくるASTはアスパラギン酸アミノ基転移酵素だそうです。誠に残念なことに、肝機能の話は出てきません。

プログラムを木構造にそのまま変換したものがSyntax Tree(構文木)です。
Syntax Treeから不要なトークン((とか、不要な空白とか、JavaScriptの文末のセミコロンとか)を取り除いたものがAbstract Structure Treeです。
抽象化されているため、ASTから元のコードに戻すことはできません。

Makrodown => AST

unified.jsのプラグインである、remarkjsのパッケージである、remark-parseを使用してASTにパースします。
ASTのフォーマットについては、ここで公開されているのでここを見てました。

下記のコードでMarkdownファイルをASTに変換します。

import * as fs from 'fs'
import * as path from 'path'
import unified from 'unified'
import markdown from 'remark-parse'
import breaks from 'remark-breaks'

const parser = (file: string): any => {
  const filePath = path.resolve(file)
  const content = fs.readFileSync(filePath, 'utf-8')
  const processor = unified()
    .use(markdown, {})
    .use(breaks)
  return processor.parse(content)
}

たとえば、このようなファイルをASTに変換するとこうなります。

# Hello World
**This** is Test markdown File.

- List01
  - list11
  - list12
- List02
{
  type: 'root',
  children: [
    {
      type: 'heading',
      depth: 1,
      children: [Array],
      position: [Position]
    },
    { type: 'paragraph', children: [Array], position: [Position] },
    {
      type: 'list',
      ordered: false,
      start: null,
      spread: false,
      children: [Array],
      position: [Position]
    }
  ],
  position: {
    start: { line: 1, column: 1, offset: 0 },
    end: { line: 8, column: 1, offset: 87 }
  }
}

ASTへのパースはパッケージが公開されているので簡単にできました。
JavaScriptの場合はパースされたASTはただのオブジェクトです。
なのでこれを元に、Backlog記法に変換していきいます。

AST => Backlog Syntax

ASTの各ノードにtypeがあり、これを元に要素がヘッダーなのか、リストなのかなどを判断できます。
実際には、ヘッダーは、type: heading、リストは、type: list、斜体はtype: emphasisといった具合です。
そしてヘッダーの要素を追うためにはさらにノードを辿っていく必要があります。
例えば## Headerroot => heading => paragraph => text のように辿っていきます。

なのでプログラムを書くに当たって再帰的な処理が必要になります。
プログラムについての説明は特にないですが、Markdownのフォーマットと、Backlog記法のフォーマットで合致しない部分が幾らか出たのでそれに関しては諦めて処理をせずに空文字を返しています。
実際のコードはこのようにしています(一部のみ掲載)。

traversal.ts

import { compiler } from './compiler'

const traversal = (node, options = {}) => {
  if (compiler[node.type] === undefined) {
    return ''
  }
  if (options !== undefined) {
    return compiler[node.type](node, options)
  }
  return compiler[node.type](node)
}
export { traversal }

compiler.ts

import { traversal } from './traversal'

const compiler = {
  text: (yields): string => yields.value,
  paragraph: (yields): string => {
    return yields.children.map(item => traversal(item)).join('')
  },
  heading: (yields): string => {
    return `${'*'.repeat(yields.depth)} ${yields.children.map(item => traversal(item)).join('')}`
  },
  strong: (yields): string => {
    return `''${yields.children.map(item => traversal(item)).join('')}''`
  },
  emphasis: (yields): string => {
    return `'''${yields.children.map(item => traversal(item)).join('')}'''`
  },
  code: (yields): string => {
    return `{code}\n${yields.value}\n{/code}`
  },
}

export { compiler }

再帰を経て、再帰を経て、再起を経続けることでBacklog記法に変換できますね。

さいごに

なければ作ればいいじゃないという気持ちとか、ASTへの理解を深めたいという気持ちとかが混ざり合って、1つのツールが出来上がりました。
そういうことでした。