Rubyのslim風の表記をHTMLに変換するプログラムをJavaScriptで作った

#考えたきっかけ

前回の投稿「[Rubyのslim風の表記をHTMLに変換するPHPスクリプトを作った](http://qiita.com/gotto/items/b2595ed02e78768a2722)」で作ったスクリプトが思いのほか便利だったので、JavaScriptで書き直してみた。

#使い方

Node.jsをインストールしたコンピュータで次のように実行すると、ソースファイルがHTMLに変換されて標準出力に出力される。swim.jsはこの投稿の最後にある。
`node swim.js ソースファイル`

```text:ソースファイルの例
!DOCTYPE html
html lang="ja"
  head
    meta charset="UTF-8"
    title|テストページ
  body
    h1|テストページ
    div
      p|このページはテストページです。
```

```html:変換した結果
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>
      テストページ
    </title>
  </head>
  <body>
    <h1>
      テストページ
    </h1>
    <div>
      <p>
        このページはテストページです。
      </p>
    </div>
  </body>
</html>
```

#書式

HTMLから<と>と閉じタグを取り除き、テキストの前に|を書く。

タグの階層はインデントで表現する。インデントはスペースでもタブでもいい。ただし、インデントにスペースとタブが混在してはいけない。

いくつか短縮形がある。短縮形を活用すると結構短く書けるようになる。

```text:通常形
div class="abc def"|テキスト
```

classは.で表現できる。

```text:短縮形
div.abc.def|テキスト
```

divは省略できる。

```text:さらに短縮形
.abc.def|タイトル
```

テキストを含まない階層はまとめて書ける。

```text:通常形
li
  a href="..."
    span|ここをクリック
```

```text:短縮形
li>a href="...">span|ここをクリック
```

テキストは|に続けて書く。

```text:一行のテキスト
p|テキスト
```

```text:複数行のテキスト
p
  |これがテキスト。
  |続けて書く。
  |そのままHTMLに出力されるので、
  |小なり記号は&lt;のように書く。
```

/で始まる行はHTMLには出力されない。

```text:コメント
/ コメント
```

/?で始まる行はJavaScriptのコードとして実行される。

```text:JavaScriptのコード
/? process.stdout.write("OK");
```

#変換スクリプト

swim.jsは次のとおり。ファイルを監視して、swimファイルが更新されたら自動的にhtmlファイルも更新されるようにしたら便利かも。

```js:swim.js
"use strict";

var Swim = function(source, indentUnit) {
  this.source = source;
  this.indentUnit = indentUnit === undefined ? "  ": indentUnit;
};

Swim.prototype.makeHtml = function() {
  var stack = [];
  var fs = require("fs");
  var self = this;
  fs.readFile(this.source,"utf-8", function(error, content) {
    if (error) {
      console.log(error);
      return;
    }
    var lines = content.split("\n");
    for (var i = 0; i < lines.length; i++) {
      var line = lines[i].replace(/\s+$/,'');
      var name = line.replace(/^\s+/,'');
      var indent = line.length - name.length;
      if (name.slice(0, 1) == "/") {
        if (name.slice(1, 2) == "?") {
          eval(name.slice(2));
        }
        continue;
      }
      var text = "";
      var x = name.indexOf("|");
      if (x >= 0) {
        text = name.slice(x + 1);
        name = name.slice(0, x);
      }
      if (stack.length == 0) {
        stack[0] = [indent, name];
        self.printNodeOpen(stack, text);
      }
      else if (indent == stack[0][0]) {
        self.printNodeClose(stack);
        stack[0] = [indent, name];
        self.printNodeOpen(stack, text);
      }
      else if (indent > stack[0][0]) {
        stack.unshift([indent, name]);
        self.printNodeOpen(stack, text);
      }
      else {
        while (stack.length > 0 && indent <= stack[0][0]) {
          self.printNodeClose(stack);
          stack.shift();
        }
        stack.unshift([indent, name]);
        self.printNodeOpen(stack, text);
      }
    }
    while (stack.length > 0) {
      self.printNodeClose(stack);
      stack.shift();
    }
  });
};

Swim.prototype.printNodeOpen = function(stack, text) {
  var indentLevel = stack[0][0];
  var tags = stack[0][1].split(">");
  var tag = tags.pop();
  for (var i = 0; i < tags.length; i++) {
    this.printNode(indentLevel + i, tags[i], true, "");
  }
  this.printNode(indentLevel + tags.length, tag, true, text);
  if (tags.length >= 1) {
    this.printNode(indentLevel + tags.length, tag, false, "");
    for (i = tags.length - 1; i >= 1 ; i--) {
      this.printNode(indentLevel + i, tags[i], false, "");
    }
  }
};

Swim.prototype.printNodeClose = function(stack) {
  var indentLevel = stack[0][0];
  var pair = stack[0][1].split(">");
  var tag = pair[0]; 
  this.printNode(indentLevel, tag, false, "");
};

Swim.prototype.printNode = function(indentLevel, tag, isOpen, value) {
  var indent = this.indentUnit.repeat(indentLevel);
  var attr = "";
  var x = tag.indexOf(" ");
  if (x >= 0) {
    attr = tag.slice(x + 1);
    tag = tag.slice(0, x);
    if (attr != "") {
      attr = " "+attr;
    }
  }
  var className = "";
  if (tag.indexOf(".") >= 0) {
    var classList = tag.split(".");
    tag = classList.shift();
    if (tag == "") {
      tag = "div";
    }
    if (classList.length >= 1) {
      className = " class=\""+classList.join(" ")+"\"";
    }
  }
  var type = "";
  var x = tag.indexOf("-");
  if (x >= 0) {
    type = tag.slice(x + 1);
    tag = tag.slice(0, x);
    if (type != "") {
      type = " type=\""+type+"\"";
    }
  }
  if (tag == "") {
    if (isOpen) {
      if (value != "") {
        this.print(indent+value+"\n");
      }
    }
  }
  else if (
    tag == "!DOCTYPE" || tag == "meta" || tag == "link" ||
    tag == "img" || tag == "input"
  ) {
    if (isOpen) {
      this.print(indent+"<"+tag+type+className+attr+">\n");
    }
  }
  else if (isOpen) {
    this.print(indent+"<"+tag+type+className+attr+">\n");
    if (value != "") {
      this.print(indent+this.indentUnit+value+"\n");
    }
  }
  else {
    this.print(indent+"</"+tag+">\n");
  }
};

Swim.prototype.print = function(str) {
  process.stdout.write(str);
}

if (process.argv.length == 3) {
  var source = process.argv[2];
  var swim = new Swim(source);
  swim.makeHtml();
}
```