marked.js本体をカスタマイズして、オレオレ変換ルールを追加する（今回はtextileのテーブル風）

21時就寝、3時起床がデフォになりつつある、majirouです。

* marked.jsの字句解析器(lexer)と構文解析器(parser)を弄って、独自の記述方法を実装します。
* ザッカーバーグの「たぶん動くと思うからリリースしようぜ！」的精神で、結果が出ることを優先にしています。
* ソースコード中の`...`は前略、中略、以下略の意です。
* 実装内容は以下。
  * textileのテーブル表記で`<table>`タグが生成される。
  * rowspan,colspanに対応させる。
  * `<th>`を縦のカラムに対応させる。
   
```
|_. ABC|DEF|
|/2 GHI|JKL|
|\3 MNO|PQR|
```

と書くと、以下のように変換させたい。

```html
<table>
 <tr>
   <th>ABC</th>
   <td>DEF</td>
 </tr>
 <tr>
   <td rowspan="2">GHI</td>
   <td>JKL</td>
 </tr>
 <tr>
   <td colspan="3">MNO</td>
   <td>PQR</td>
 </tr>
</table>
```

# カスタマイズ結果

いつもの通り、まず結果から。

* marked.jsに追記、改造した箇所の差分です
* ５箇所ほどなので、各変更箇所にそれぞれ(1),...(5)として、記事内の参照印とします。

<details><summary>カスタマイズしたmarked.jsの差分</summary><div>

```diff
// (1)
var block = {
  ...
- text: /^[^\n]+/
+ text: /^[^\n]+/,
+ textileTable: noop
}

...

// (2)
block.tables = merge({}, block.gfm, {
  ...
  nptable: /^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,
- table: /^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/
+ table: /^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/,
+ textileTable: /^\|(((((_\.|\/[0-9]+|\\[0-9]+)?\s+).*|.+)\|)+\n)+/
});

...

// (3)
Lexer.prototype.token = function(src, top) {
  ...
  while (src) {
    ...
+    if (cap = this.rules.textileTable.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'textileTable',
+        text: cap[0]
+      });
+      continue;
+    }

...

// (4)
Renderer.prototype.tablecell = function(content, flags) {
  var type = flags.header ? 'th' : 'td';
-  var tag = flags.align
-    ? '<' + type + ' align="' + flags.align + '">'
-    : '<' + type + '>';
-  return tag + content + '</' + type + '>\n';
+  // rowspan, colspanを追記するため
+  var attr = [];
+  if (flags.align != null) {
+    attr.push(` align="${flags.align}"`)
+  }
+  if (flags.rowspan != null) {
+    attr.push(` rowspan="${flags.rowspan}"`)
+  }
+  if (flags.colspan != null) {
+    attr.push(` colspan="${flags.colspan}"`)
+  }
+  return '<' + type + attr.join('') + '>'+ content + '</' + type + '>\n';
};

...

// (5)
Parser.prototype.tok = function() {
  switch (this.token.type) {
    ...
+   case 'textileTable': {
+     var body = '',
+         i,
+         ii,
+         rows = this.token.text.split('\n'),
+         cols,
+         tempCols = '',
+         max,
+         regexpTh = /^_\. +.*/ ,
+         regexpRowspan = /^\/[0-9]+ +.*/ ,
+         regexpColspan = /^\\[0-9]+ +.*/ ,
+         temp
+         ;
+     for (i = 0; i < rows.length; i++) {
+       cols = rows[i].split('|')
+       max = cols.length-1
+       for (ii = 1; ii < max; ii++) {
+         if (regexpTh.test(cols[ii])) {
+           tempCols += this.renderer.tablecell(
+             cols[ii].replace(/^_\. /,''),
+             { header: true }
+           )
+         } else if(regexpRowspan.exec(cols[ii])) {
+           temp = cols[ii].split(' ')
+           const rowspan = temp.shift().substr(1)
+           tempCols += this.renderer.tablecell(
+             temp.join(''),
+             { header: false , rowspan }
+           )
+         } else if(regexpColspan.exec(cols[ii])) {
+           temp = cols[ii].split(' ')
+           const colspan = temp.shift().substr(1)
+           tempCols += this.renderer.tablecell(
+             temp.join(''),
+             { header: false , colspan }
+           )
+         } else {
+           // 普通のtd
+           tempCols += this.renderer.tablecell(cols[ii], { header: false })
+         }
+       }
+       body += this.renderer.tablerow(tempCols)
+       tempCols = ''
+     }
+     return '<table>' + body + '</table>'
+   }

...
```
</div>

# 経緯

* 記事管理をするために、Markdownで編集するウェブアプリを実装してほしい
* でも、**テーブルは、セル結合したいから『textile』風に書きたい**

という要望を受けました。

`marked.js customize`などで検索すると、該当記事がちらほら出てきますが、marked.jsに用意されているルールをオーバーライドする方法が紹介されています。

[公式](https://github.com/markedjs/marked/blob/master/docs/USING_PRO.md) でも「独自ルール追加なら、オーバーライドで！」って書いてありますが、今回やりたいのはそれではありません。以下は公式のオーバーライドの例。

```js
Example: Overriding default heading token by adding an embedded anchor tag like on GitHub.

// Create reference instance
var myMarked = require('marked');

// Get reference
var renderer = new myMarked.Renderer();

// Override function
renderer.heading = function (text, level) {
  var escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');

  return `
          <h${level}>
            <a name="${escapedText}" class="anchor" href="#${escapedText}">
              <span class="header-link"></span>
            </a>
            ${text}
          </h${level}>`;
};

// Run marked
console.log(myMarked('# heading+', { renderer: renderer }));
```

# カスタマイズ説明

まずは、ざっくり何をしているかを掴むため全体像を把握します。

* **メイン処理**： `marked`関数がメイン処理と思われます。
* **解析処理**： 大きく分けて、字句解析`Lexer`と構文解析`Parser`があり、入力テキストを解析（分類）していきます。
* **レベルとルール**： htmlなので、`block`と`inline`に２レベルに分けられ、さらにその中に、「この構文なら、こういうHTMLを書き出す」といったルールを定義をし、処理を行うようです。
* **HTML変換**： `Renderer`にて、上記レベル配下の定義に沿って、変換していくようです。

## marked関数

* `lexer`で字句解析のルールになるトークンを取得。
* それを元に`parse`処理をする`done`関数を定義し、実行。
* コードをハイライト化するためにいろいろ分岐していますが、今回は割愛。
* ソースを見る限り、`lexer`,`parser`それぞれを見様見真似でカスタマイズできそうとあたりをつける。

```js
function marked(src, opt, callback) {
    ...
    try {
      tokens = Lexer.lex(src, opt);
    } catch (e) {
      return callback(e);
    }
    ...
    var done = function(err) {
      ...
      try {
        out = Parser.parse(tokens, opt);
      } catch (e) {
        err = e;
      }

      opt.highlight = highlight;

      return err
        ? callback(err)
        : callback(null, out);
    };
...
```

### Lexer

* Lexer関数にて、字句解析処理の`Lexer.prototype.token`があり、この中で正規表現を使って、入力されたテキストを解析しています。
  1. `exec`で判別 
  2. その長さ分のテキストを切り出し
  3. 最終的に`tokens`配列に追加<br>
  この連想配列の要素は、MD→HTMLのレンダリング時に使用します。`type`はその際の分岐で使うので必須
* カスタマイズ箇所は(3)が該当し、textileのtable表記のテキストを`type: 'textileTable'`として、`tokens`に追加します。
 
```js
Lexer.prototype.token = function(src, top) {
  ...
    // heading
    if (cap = this.rules.heading.exec(src)) {
      src = src.substring(cap[0].length);
      this.tokens.push({
        type: 'heading',
        depth: cap[1].length,
        text: cap[2]
      });
      continue;
    }
    ...
    // (3)
    // textile table
    if (cap = this.rules.textileTable.exec(src)) {
      src = src.substring(cap[0].length);
      this.tokens.push({
        type: 'textileTable',
        text: cap[0]
      });
      continue;
    }
```


### Lexer ルール

* 字句解析のルールは以下のように、`block`と`inline`が宣言されていて、これに独自のカスタマイズルールを追加して、字句解析時に引っ掛けます。
* 今回は、`table`を真似るので、blockレベルに対して追加します。
* カスタマイズ箇所は(1)で、`textileTable`とし、table同様の`noop`を指定しました。
  * `noop`自体は`function noop() {}`と定義されており、その名の通り何もしないことになります。
  
```js
/**
 * Block-Level Grammar
 */
var block { 
  ...
  heading: /^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,
  ...
  table: noop,
  ...
  // (1)
  textileTable: noop
};

...

/**
 * Expose Block Rules
 */

Lexer.rules = block;

...
```

### Parser

* 構文解析を行い、それに対応した処理を行わせるようで、switch文で分岐しています。
* カスタマイズ箇所は(5)で、`case 'textileTable'`時に、tableタブを作成するようにしています。
  * `_.`始まりなら`<th>`にする
  * `/`始まりならそのあとの数値分rowspanを指定する。<br>など力技で処理しています。あとは、割愛。
* **（あとで気づいたこと） 本来、Renderサイドでやるタグ生成処理を、このパーサー部で力技として、やってました。**

```js
Parser.prototype.tok = function() {
  switch (this.token.type) {
    case 'space': {
      return '';
    }
    ...
    // (5)
    case 'textileTable': {
      ...
    }
```

## Renderer

* `tablecell`レンダーが、`colspan`,`rowspan`に対応していいので改造。
* カスタマイズ箇所は(4)になります。
 
```js
Renderer.prototype.tablecell = function(content, flags) {
  var type = flags.header ? 'th' : 'td';
  // var tag = flags.align
  //   ? '<' + type + ' align="' + flags.align + '">'
  //   : '<' + type + '>';
  // return tag + content + '</' + type + '>\n';

  var attr = [];
  if (flags.align != null) {
    attr.push(` align="${flags.align}"`)
  }
  if (flags.rowspan != null) {
    attr.push(` rowspan="${flags.rowspan}"`)
  }
  if (flags.colspan != null) {
    attr.push(` colspan="${flags.colspan}"`)
  }
  return '<' + type + attr.join('') + '>'+ content + '</' + type + '>\n';
};
```

# 感想

* markdown楽なんですが、今回のようにテーブルを結合したいとか、ヘッダーを縦一列にしたいという場合、textileの方が融通が効くなぁ〜という印象。
* 大学の授業で、コンパイラとか作った時以来で久しぶりにlexerという単語に出くわし、「（詳細は覚えてないけど）過去の勉強した」という経験は大事だなと再認識。
* 正規表現さえあれば、力技で基本どうにかなる。
* できなかったら、htmlを直書きすべし。