📊

markdown テーブルを表形式データに変換

2024/09/09に公開

はじめに

本記事では「Markdown テーブルの文字列を、表計算ソフト(例:Microsoft ExcelGoogle Sheets など)のデータに変換する」ことを目的とし、その実装を考えていきます。なお 実際の動き(利用方法)としては、下記のようになります。

  1. markdown テーブルのテキストを、コピーしてフォームに貼り付け
  2. フォーム内容を基に、tsv( Tab-Separated Values ) 形式のテキストデータを作成する
  3. クリップボードを経由して、表計算ソフトへ貼り付け(markdown テーブルでの行列構造を維持)

クリップボードについての話は、最下段に補足として乗せておきます。

その他紹介

本記事と逆のパターンも用意していますので、興味がある方は下記記事を参照してください。

https://zenn.dev/nonaka101/articles/excel-to-markdown

なお 本記事で扱った内容は、下記場所にて使っています。

https://nonaka101.github.io/jig-a/

事前準備

ここでは設計を始める前段階として、目的や条件等の詳細を考えていきます。

環境の定義

本記事では JavaScript を使って処理していきます。

なお JavaScript 特有の機能を使うわけではないので、他言語でも 設計作成 の内容を流用することは可能です。本記事で主に必要となる機能は、下記のとおりです。

  • 文字列のトリム処理や、「改行コード」や「タブ文字」といった特殊文字を扱う
  • 配列を操作する(分割、統合、スライス、マップなど)

条件の定義

ここでは、実装する機能の入出力データをどうするかについて定義します。

入力値となる文字列はmarkdownテーブル形式とする

入力として受け取る markdown の文字列について、下記のような最小構成を基本として考えます。

基本的なmarkdownテーブル
| 表 | 列A | 列B |
| --- | --- | --- |
| 行1 | セルA1 | セルB1 |
| 行2 | セルA2 | セルB2 |
装飾されたパターンにも対応すること

また下記のようなパターンにも対応し、上記の基本構成と同じ結果を出力するようにします。

パターン1:揃え方向を指定したテーブル
| 表 | 列A | 列B |
| :---: | :--- | ---: |
| 行1 | セルA1 | セルB1 |
| 行2 | セルA2 | セルB2 |
パターン2:スペースやハイフンで見栄えを調整したテーブル
|  表 | 列A    | 列B   |
| --- | ----- | ----- |
| 行1 | セルA1 | セルB1 |
| 行2 | セルA2 | セルB2 |
行列の数は固定でなく様々なケースに対応すること

入力値となるテーブルデータ(文字列)ですが、3 ✕ 3 テーブルの場合もあれば、2 ✕ 50100 ✕ 10 といったケースもあると仮定します。つまり 入力されるデータの行列の数は可変とし、様々なケースに対応できるようにします。

オプション項目:エスケープ処理付きのデータにも対応すること

markdown テーブルは | を列区切りとして利用しています。なのでデータ自体に | が含まれる場合は、エスケープ処理(この場合は \\|)を行う必要があります。

列A 列B
行1 セル|A1 セル|B1
行2 セル|A2 セルB2
エスケープ処理された上記テーブル
| 表 | 列A | 列B |
| --- | --- | --- |
| 行1 | セル\\|A1 | セル\\|B1 |
| 行2 | セル\\|A2 | セルB2 |

なお このエスケープ処理は、markdown のルールに関係するものであって、表形式データにには不要なものとなります。なので出力結果は、エスケープ処理のない状態でなければなりません。

上記データを表形式に変換した場合
表	列A	列B
行1	セル|A1	セル|B1
行2	セル|A2	セルB2

本記事では、この「エスケープ処理されたデータを想定する」ことをオプション条件として設定します。

出力する表形式データはタブ文字と改行コードによるものとする

表計算ソフトは、Excel や GoogleSheets など様々あります。下図は markdown テーブルとそのテキスト、そして GoogleSheets に移し替えた時のイメージ図となります。

列A 列B
行1 セルA1 セルB1
行2 セルA2 セルB2
上記テーブルのテキスト
| 表 | 列A | 列B |
| --- | --- | --- |
| 行1 | セルA1 | セルB1 |
| 行2 | セルA2 | セルB2 |

3✕3の表データ、内容については上記参照
GoogleSheets の表データ

このように行列構造を維持したままペーストするためには、一般的な表計算ソフトがサポートしている tsv( Tab-Separated Values ) を利用します。

下記は列方向をタブ文字で、行方向を改行コードで区切ったデータとなります。この形式を表計算ソフトにペーストすると、行列構造を維持した状態で貼り付けることができます。

タブ文字で列方向、改行コードで行方向を区切った状態
表	列A	列B
行1	セルA1	セルB1
行2	セルA2	セルB2

つまり、与えられた markdown テーブルからこの形式の文字列を作り出すことが、本記事のメインテーマとなります。よって、出力する条件は下記のようになります。

  • 各行は改行コード(例:\n)によって区切られる
  • 各列はタブ文字(例:\t)によって区切られる
上記例の正規表現版
表\t列A\t列B\n行1\tセルA1\tセルB1\n行2\tセルA2\tセルB2

設計

ここからは事前準備で決めた内容を基に、どのように実装していくかを考えていきます。

入力値の考察

条件定義:入力値についてで触れたように、入力される文字列は固定でなく、下記のような振れ幅があります。

  • スペースやハイフンで、見栄えを整えているパターン
  • セパレート行にて、列の揃えを指定しているパターン
  • 行列数は可変(3 ✕ 3, 2 ✕ 50, 100 ✕ 10 など)
  • エスケープ処理されたデータが含まれることがある(オプション)

こうした振れ幅がある一方で、共通している箇所を考えてみます。

  • 表全体
    • 2行目はセパレート行となる(本件処理には不要な情報)
    • 各行は改行コード(例:\n)によって区切られる
  • 行単位
    • 行頭や行末にある文字は、必ず | となる
    • 内側にある | は、データを区切る役割を持っている
  • セル単位
    • 文字列の端にある空白は、見栄えのための装飾でありデータではない(≒ トリム処理が必要)

大枠となる流れ

上記考察から、本記事では「必須となる処理」と「オプション処理」の 2 つにわけて作成する方向で考えていきます。

  1. markdown 文字列を tsv 形式のテキストデータへ変換する処理
  2. エスケープ処理された文字列を想定した変換処理(オプション)

ベースとなる処理は 1 番に任せ、それをラップする形で 2 番の処理を作るような形です。

ベースとなる変換処理

ベースとなる処理では、エスケープ処理は一旦置いておき、下記の手順で進めていきます。

  1. 特殊文字を統一しておく(例:改行コードは LF(\n) に合わせておく)
  2. 改行コードで、行単位に分割する(配列形式で格納)
  3. 配列の2番目にある要素(≒ セパレート行)を削除
  4. 各行単位での処理
    1. | を区切り文字に、セル単位に分割(配列形式)
    2. 配列の最初と末尾の要素を除いた形で再取得(≒ 行頭行末の | 分を除外)
    3. 各要素内でトリム処理(見栄え調整のスペースを除去)
    4. 配列要素をタブ文字 \t で繋げ、1つの文字列に統合
  5. 各行を改行コード \n で繋げ、1つの文字列に統合(≒ 出力データ)

なお 説明に使うサンプルデータとして、下記を用いることにします。

手順説明で使うサンプル
| 表 | 列A | 列B |
| --- | --- | --- |
| 行1 | セルA1 | セルB1 |
| 行2 | セルA2 | セルB2 |

1:特殊文字の統一

まずは後方で行う処理をスムーズに行うために、特殊文字の表現を統一しておきます。例えば改行コードは OS によって、LF(\n) や CRLF(\r\n) のように表記ブレが起きるケースがあります。

これを片方(今回は \n)に合わせておくことで、後の処理をスムーズにしておきます。

改行コードの統一
| 表 | 列A | 列B |\n| --- | --- | --- |\n| 行1 | セルA1 | セルB1 |\n| 行2 | セルA2 | セルB2 |

2:行単位に分割

次に行うのは、行毎に処理を行えるよう配列形式に分割することです。先ほど表現を合わせておいた改行コード(\n)を区切り文字とし、要素を分割します。

行単位に分割
[
  '| 表 | 列A | 列B |',
  '| --- | --- | --- |',
  '| 行1 | セルA1 | セルB1 |',
  '| 行2 | セルA2 | セルB2 |'
]

3:セパレート行の情報を削除

先程の段階で 4 つの要素に分割されたテーブルデータですが、ここでは 2 番目にあるセパレート行に注目します。

この | --- | --- | --- | という文字列は、markdown においては 表頭(ヘッダー)と表体(ボディ)を分ける役割を持っています。また、この文字列を弄ることで各列の揃え位置を調整することもできるのですが・・・表形式データに変換するという本件においては、処理に不要な情報となります。

そのため、配列データからセパレート行の情報を削除します。

セパレート行の削除
[
  '| 表 | 列A | 列B |',
  '| 行1 | セルA1 | セルB1 |',
  '| 行2 | セルA2 | セルB2 |'
]

4:各行単位の処理

ここからは、各行単位で同じ処理をすることになります。説明では最初の要素(| 表 | 列A | 列B |)を使って話を進めていきます。

4-1:セル単位に分割

まずは各セル単位で処理していくため、パイプ記号 | を区切り文字として配列形式に分割します。

セル単位に分割
[
  '',
  ' 表 ',
  ' 列A ',
  ' 列B ',
  ''
]

上記結果において、2つの点に注目してください。

  1. |行頭行末にあったことで空要素ができていること
  2. _表_, _列A_のようにデータの前後にスペースが含まれていること
4-2:配列の再取得(スライス)

先ほど配列形式に分割したデータの内、最初と最後に空要素が含まれていました。これは | 表 | 列A | 列B | の行頭行末の | によるもので、処理には不要な情報です。

そのため、この 2 つを除いた形で配列を再設定(再取得)します。

スライス
[
  ' 表 ',
  ' 列A ',
  ' 列B '
]
4-3:トリム処理

先程の段階で、データに絞った要素を配列に収めることができました。ですがこれらデータは、(スペースを _ に置き換えてみると)_表_, _列A_ のように前後にスペースが含まれていることが考えられます。

列A というデータの場合を考えてみると、文字列の先頭末尾にあるスペース _列A_ は、データではないため取り除く必要があります。一方で、スペースがデータ内側にある場合 列_A 、これはデータの一部なので そのままにしておく必要があります。

このように 文字列の先頭末尾にあるスペースに絞って、トリム処理を行います。

トリム処理
[
  '表',
  '列A',
  '列B'
]
4-4:タブ文字による配列の統合

ここまでで、行単位でのデータの整形処理(データのみを抽出しトリム処理)は完成しました。もう配列形式である必要は無いため、ここでは タブ文字(\t)で繋げることで 1 つの文字列に統合します。

タブ文字による統合
'表\t列A\t列B'

各行単位での処理は、ここで終了です。表全体は下記のようになりました。

表全体
[
  '表\t列A\t列B',
  '行1\tセルA1\tセルB1',
  '行2\tセルA2\tセルB2'
]

5:改行コードによる配列の統合

最後に、各要素を改行コード(\n)で繋げて 1 つの文字列にしてしまえば、ベースとなる変換処理は完成です。

改行コードによる統合
'表\t列A\t列B\n行1\tセルA1\tセルB1\n行2\tセルA2\tセルB2'

エスケープ処理を想定した変換処理(オプション)

ベースとなる変換処理は完成したので、次はこれを用いてオプション項目「エスケープ処理を想定した処理」を考えていきます。この場合、ベースとなる処理を邪魔しないようにすればいいだけなので、手順としては下記の 3 段階になります。

  1. エスケープ処理された文字列を、一旦別の文字列に置換することで退避させる
  2. ベースとなる変換処理
  3. 1番で行った退避処理を、エスケープ処理を除いた形で復元化

なお 説明に使うサンプルデータとして、下記を用いることにします。

エスケープ処理付きのサンプル
| 表 | 列A | 列B |
| --- | --- | --- |
| 行1 | セル\\|A1 | セル\\|B1 |
| 行2 | セル\\|A2 | セルB2 |

1:処理に不都合な文字列を退避(無害化)

ベースとなる変換処理においては、「4-1:セル単位に分割」で | を区切り文字として配列形式に分割していました。なのでエスケープ処理された \\| が残っていると、ここで挙動がおかしくなる恐れがあります。

なので エスケープ処理された \| と列区切りの役割を持つ | が識別できるこの段階で、片方を別の文字列に置き換えることで無害化します。ここでは \\|@@PIPE@@ といった、通常使われることのない文字列に置換しておきます。

エスケープ処理された箇所を無害化
| 表 | 列A | 列B |
| --- | --- | --- |
| 行1 | セル@@PIPE@@A1 | セル@@PIPE@@B1 |
| 行2 | セル@@PIPE@@A2 | セルB2 |

2:ベースとなる変換処理

前の段階で無害化したため、変換処理に突っ込んでも問題なくなりました。下記はベースとなる変換処理で処理を行った結果になります。
(わかりやすいよう、正規表現でなくタブと改行によるデータにしています)

変換処理
表	列A	列B
行1	セル@@PIPE@@A1	セル@@PIPE@@B1
行2	セル@@PIPE@@A2	セルB2

3:退避(無害化)していた文字列を復元

変換処理が終了したので、| を戻しても問題なくなりました。1 番では \\| という形になっていましたが、表形式データではエスケープ処理を取り除いた | の形で復元します。

無害化した文字列を復元
表	列A	列B
行1	セル|A1	セル|B1
行2	セル|A2	セルB2

これでオプションとなる「エスケープ処理を想定した変換処理」は完成となります。

作成

ここからは設計で決めた内容を基に、コードに起こしていきます。設計で説明した通り、大枠として 2 つの関数を作成することになります。

  • ベースとなる、markdown 文字列を表形式データ(≒ tsv 形式のテキスト)に変換する関数
  • (上記をラップする形で)エスケープ処理付きを想定した変換関数

ベースとなる変換関数

まずは必須条件を満たす、表形式データを markdown テーブルに変換する関数を作成していきます。

ここでは関数名を markdown2excel() とします。テスト文を付けると、下記のようになります。

関数名とテスト文
function markdown2excel(mdTable) {
  // markdown テーブル(文字列)を、表形式データ(文字列)に変換して返す
}

// テスト
const mdTable = '| 表 | 列A | 列B |\n| --- | --- | --- |\n| 行1 | セルA1 | セルB1 |\n| 行2 | セルA2 | セルB2 |';
console.log(markdown2excel(mdTable));
/* ↓ 出力結果
表	列A	列B
行1	セルA1	セルB1
行2	セルA2	セルB2
*/

特殊文字の表現を統一

最初に行うのは、OS により表記ブレが起きうる特殊文字を統一です。

ここでは改行コードの LF(\n) と CRLF(\r\n) を、\n に統一しておきます。

特殊文字の統一
function markdown2excel(mdTable) {
  // 改行コードを \n に統一
  const normalizedStr = mdTable.replace(/\r\n/g, '\n');
}

テーブルデータを配列に

次に、テーブルデータを配列形式に格納します。入力として受け取る文字列は、\n で行を区切っているので、それらを split() で分割し、一次配列にします。

行を改行コード単位で分割
function markdown2excel(mdTable) {
  // 改行コードを \n に統一
  const normalizedStr = mdTable.replace(/\r\n/g, '\n');

  // 改行ごとに分割して、行ごとの配列を作成
  const rows = normalizedStr.split('\n');
}
テスト文でのrowsの中身
[
  '| 表 | 列A | 列B |',
  '| --- | --- | --- |',
  '| 行1 | セルA1 | セルB1 |',
  '| 行2 | セルA2 | セルB2 |'
]

セパレート行の除去

次に処理に不要な | --- | --- | --- | の要素を削除します。配列要素の操作には、splice(start, deleteCount) を使います。

配列を操作しセパレート行要素を削除
function markdown2excel(mdTable) {
  // 改行コードを \n に統一
  const normalizedStr = mdTable.replace(/\r\n/g, '\n');

  // 改行ごとに分割して、行ごとの配列を作成(セパレート行は除去)
  const rows = normalizedStr.split('\n');
  rows.splice(1,1);
}
テスト文でのrowsの中身
[
  '| 表 | 列A | 列B |',
  '| 行1 | セルA1 | セルB1 |',
  '| 行2 | セルA2 | セルB2 |'
]

各行単位でmarkdownをtsv形式に変換

現在 rows の各要素は | 表 | 列A | 列B | のようになっています。具体的には、下記のとおりです。

  • | でデータを区切っている(文頭と文末の | は無視)
  • データ文字列の前後にはトリムすべきスペースが含まれている

これを最終的には、タブ文字 \t でデータを区切った文字列に変えたいわけです。各行で行う処理は同じなので、ここでは map() 関数を使うことにします。

map関数で、各行ごとに同じ処理を適用
function markdown2excel(mdTable) {
  // 改行コードを \n に統一
  const normalizedStr = mdTable.replace(/\r\n/g, '\n');

  // 改行ごとに分割して、行ごとの配列を作成(セパレート行は除去)
  const rows = normalizedStr.split('\n');
  rows.splice(1,1);

  // 行処理
  const excelRows = rows.map(row => {
    // `|` で分割し、最初と末尾の要素を除去し、トリム処理して、タブ文字で繋げる
  });
}

ここからは、row を操作して処理していきます。この中に入っているのは、| 表 | 列A | 列B | といった文字列です。

必要な作業は、まず | を区切り文字に配列へ分割することです。ただし配列の最初と最後の要素は、必ず空要素となり、データとしては不要なので除去しなければなりません。

ここでは split()slice() を組み合わせて処理します。

データを配列に格納
function markdown2excel(mdTable) {
  // 改行コードを \n に統一
  const normalizedStr = mdTable.replace(/\r\n/g, '\n');

  // 改行ごとに分割して、行ごとの配列を作成(セパレート行は除去)
  const rows = normalizedStr.split('\n');
  rows.splice(1,1);

  // 行処理:文字列を '|' で区切り、最端要素を除去し、空白をトリムし、タブ文字で繋げた文字列に変換
  const excelRows = rows.map(row => {
    return row                    // '| A | B | | C |'
      .split('|')                 // ["", " A ", " B ", " "," C ",""]
      .slice(1, -1)               // [" A ", " B ", " "," C "]
  });
}

ここまでで、| A | B | | C |[" A ", " B ", " "," C "] のように空欄セルを踏まえた形でデータを配列に格納できました。残るはトリム処理(セルとなる各要素に対し、更に map() 関数で処理させます)と、タブ文字 \t で文字列に繋げる処理となります。

各行単位処理の完成形
function markdown2excel(mdTable) {
  // 改行コードを \n に統一
  const normalizedStr = mdTable.replace(/\r\n/g, '\n');

  // 改行ごとに分割して、行ごとの配列を作成(セパレート行は除去)
  const rows = normalizedStr.split('\n');
  rows.splice(1,1);

  // 行処理:文字列を '|' で区切り、最端要素を除去し、空白をトリムし、タブ文字で繋げた文字列に変換
  const excelRows = rows.map(row => {
    return row                    // '| A | B | | C |'
      .split('|')                 // ["", " A ", " B ", " "," C ",""]
      .slice(1, -1)               // [" A ", " B ", " "," C "]
      .map(cell => cell.trim())   // ["A", "B", "","C"]
      .join('\t');                // 'A\tB\t\tC'
  });
}
テスト文でのexcelRowsの中身
[
  '表\t列A\t列B',
  '行1\tセルA1\tセルB1',
  '行2\tセルA2\tセルB2'
]

表全体を一つの文字列にして完成

後はこの配列を、改行コード \n で繋げる形で文字列化すれば完成です。これまでのコードに組み込み、関数として完成したものが下記になります。

改行コードで繋げた文字列を返して完成
function markdown2excel(mdTable) {
  // 改行コードを \n に統一
  const normalizedStr = mdTable.replace(/\r\n/g, '\n');

  // 改行ごとに分割して、行ごとの配列を作成(セパレート行は除去)
  const rows = normalizedStr.split('\n');
  rows.splice(1,1);

  // 行処理:文字列を '|' で区切り、最端要素を除去し、空白をトリムし、タブ文字で繋げた文字列に変換
  const excelRows = rows.map(row => {
    return row                    // '| A | B | | C |'
      .split('|')                 // ["", " A ", " B ", " "," C ",""]
      .slice(1, -1)               // [" A ", " B ", " "," C "]
      .map(cell => cell.trim())   // ["A", "B", "","C"]
      .join('\t');                // 'A\tB\t\tC'
  });

  // 各行を改行でつなぎ合わせて最終結果を返す
  return excelRows.join('\n');
}

エスケープ処理付き変換関数(オプション)

ここからはベースとなる変換関数をラップする形で、エスケープ処理を想定した変換関数を作成していきます。

手順としては、下記のようになります。

  1. 文字列に含まれる、処理に不都合な文字列を無害化(一時的に別の文字列に置換)
  2. 変換処理
  3. 1番で行った処理を、エスケープ処理を外す形で復元

無害化が必要な文字列としては、各行データを配列にする際に利用する | を想定します。

関数名は markdown2excelWithEscaped() とします。テスト文も付けると、下記のようになります。

関数名とテスト文
function markdown2excelWithEscaped(mdTable) {
  // (エスケープ処理された)markdown テーブルを、(エスケープシーケンスを抜いた形の)表形式データに変換して返す
}

// テスト
const mdTable2 = '| 表 | 列A | 列B |\n| :---: | :--- | ---: |\n| 行1 | セル\\|A1 | セル\\|B1 |\n| 行2 | セル\\|A2 | セルB2 |';
console.log(markdown2excelWithEscaped(mdTable2));
/* ↓ 出力結果
表	列A	列B
行1	セル|A1	セル|B1
行2	セル|A2	セルB2
*/

置換処理について

JavaScript で文字列を置換する主な方法としては、下記の2つが考えられます。

  1. replace(pattern, replacement) 関数による置換
  2. {string}.split(separator1).join(separator2) のように、配列化からの統合

1番については、正規表現を使えるのが特徴です。今回ですと文字列全体を対象とするため g フラグによるグローバル検索を使います。また、| は正規表現において「論理和」の役割を持つため /\\|/g、今回はエスケープ付きなので 関数に渡す場合には replace(/\\\\|/g, '@@PIPE@@') のようにしてあげる必要があります。

一方、2番の方法は正規表現を用いないのが特徴です。置換対象となる文字列(今回は \\|)を区切り文字として配列に分割し、置換先文字列(今回は @@PIPE@@)で文字列として繋ぎなおす処理を行います。

置換対象が \\| のみの現状において、本記事では 2 番の処理を採用します。

個人メモ:エスケープシーケンスについて

markdown のみを考えると、本来 \| でも問題はありません。しかし JavaScript で \ を扱う際の事情から、置換対象は \| でなく \\| にしています。
それに関係づけるため、本記事の逆パターンである「表形式データを markdown テーブルに変換」においても、エスケープ処理には \| でなく \\| といった形に出力するよう調整しています。

これは \| を含むデータを文字列として解釈した際に、意味のないバックスラッシュとして無視されることが関係しています。

文字列リテラルの中の \ は、自動的にエスケープシーケンスとして扱われます。その際、\| という定義されてないエスケープシーケンスと判断され、\ は無視されてしまうようです。

これを防ぐためには、バックスラッシュそのものを文字として認識させるために、\\ のようにエスケープ処理が必要となります。

エスケープシーケンス
console.log('|');     // -> '|'
console.log('\|');    // -> '|'  : 未定義のエスケープシーケンスとして無視される
console.log('\\|');   // -> '\|'
console.log('\\\|');  // -> '\|' : 2つはエスケープシーケンス、残る一つは無視される

変換テーブルの準備

本処理にあたって、必要となる情報は下記の 3 つです。

  • エスケープ処理対象となる文字列
  • 一時退避用の無害化文字列
  • エスケープ処理した文字列

今回は、| のみ考えるので、上記内容をまとめると下表になります。

項目
もとの文字列 original |
退避用の文字列 evacuation @@PIPE@@
処理済み文字列 escaped \\|

今後パターンが増えるかもしれないことを考慮して、この 3 つの情報をオブジェクトとして纏め、配列形式で格納しておきます。

変換テーブルを作成し、3種のデータを準備
function markdown2excelWithEscaped(mdTable) {
  const conversionTable = [
    {
      original: '|', 
      evacuation: '@@PIPE@@',
      escaped: '\\|'
    },
  ];
}

退避用文字列への置換処理

次に変換テーブルをループ(複数パターンを想定)させ、処理に不都合な文字列を退避用文字列に置換します。ここでは escaped にある文字列を区切り文字として配列に分割し、evacuation にある文字列で繋げることで置換処理を行っています。

退避用文字列へ置換
function markdown2excelWithEscaped(mdTable) {
  const conversionTable = [
    {
      original: '|', 
      evacuation: '@@PIPE@@',
      escaped: '\\|'
    },
  ];

  // 退避用文字列に置換
  conversionTable.forEach(replacement => {
    mdTable = mdTable.split(replacement.escaped).join(replacement.evacuation);
  });
}

// テスト
const mdTable2 = '| 表 | 列A | 列B |\n| :---: | :--- | ---: |\n| 行1 | セル\\|A1 | セル\\|B1 |\n| 行2 | セル\\|A2 | セルB2 |';
console.log(markdown2excelWithEscaped(mdTable2));
/* ↓ この段階だと、下記のようになる
| 表 | 列A | 列B |
| --- | --- | --- |
| 行1 | セル@@PIPE@@A1 | セル@@PIPE@@B1 |
| 行2 | セル@@PIPE@@A2 | セルB2 |
*/
変換処理後に復元化

処理に不都合な \\|@@PIPE@@ に変わっているため、ベースとなる変換関数 markdown2excel() に渡しても問題なくなりました。

ベースとなる変換関数に渡す
function markdown2excelWithEscaped(mdTable) {
  const conversionTable = [
    {
      original: '|', 
      evacuation: '@@PIPE@@',
      escaped: '\\|'
    },
  ];

  // 退避用文字列に置換
  conversionTable.forEach(replacement => {
    mdTable = mdTable.split(replacement.escaped).join(replacement.evacuation);
  });

  // ベース処理
  mdTable = markdown2excel(mdTable);
}

// テスト
const mdTable2 = '| 表 | 列A | 列B |\n| :---: | :--- | ---: |\n| 行1 | セル\\|A1 | セル\\|B1 |\n| 行2 | セル\\|A2 | セルB2 |';
console.log(markdown2excelWithEscaped(mdTable2));
/* ↓ この段階だと、下記のようになる
表	列A	列B
行1	セル@@PIPE@@A1	セル@@PIPE@@B1
行2	セル@@PIPE@@A2	セルB2
*/

後は @@PIPE@@ をエスケープ処理されてない元の文字列 | に置き換えれば、関数の返り値が完成します。ここでの置換処理は evacuation 文字列を区切りに配列化し、original 文字列で繋げることで行います。

復元化して完成
function markdown2excelWithEscaped(mdTable) {
  const conversionTable = [
    {
      original: '|', 
      evacuation: '@@PIPE@@',
      escaped: '\\|'
    },
  ];

  // 退避用文字列に置換
  conversionTable.forEach(replacement => {
    mdTable = mdTable.split(replacement.escaped).join(replacement.evacuation);
  });

  // ベース処理
  mdTable = markdown2excel(mdTable);

  // 退避させてた文字列をエスケープ処理付きで復元
  conversionTable.forEach(replacement => {
    mdTable = mdTable.split(replacement.evacuation).join(replacement.original);
  });

  return mdTable;
}

// テスト
const mdTable2 = '| 表 | 列A | 列B |\n| :---: | :--- | ---: |\n| 行1 | セル\\|A1 | セル\\|B1 |\n| 行2 | セル\\|A2 | セルB2 |';
console.log(markdown2excelWithEscaped(mdTable2));
/* ↓ 返り値は、目標としていたデータと一致
表	列A	列B
行1	セル|A1	セル|B1
行2	セル|A2	セルB2
*/

補足

コード全文

コード全文(長いので格納しています)
関数
function markdown2excel(mdTable) {
  // 改行コードを \n に統一
  const normalizedStr = mdTable.replace(/\r\n/g, '\n');

  // 改行ごとに分割して、行ごとの配列を作成(セパレート行は除去)
  const rows = normalizedStr.split('\n');
  rows.splice(1,1);

  // 行処理:文字列を '|' で区切り、最端要素を除去し、空白をトリムし、タブ文字で繋げた文字列に変換
  const excelRows = rows.map(row => {
    return row                    // '| A | B | | C |'
      .split('|')                 // ["", " A ", " B ", " "," C ",""]
      .slice(1, -1)               // [" A ", " B ", " "," C "]
      .map(cell => cell.trim())   // ["A", "B", "","C"]
      .join('\t');                // 'A\tB\t\tC'
  });

  // 各行を改行でつなぎ合わせて最終結果を返す
  return excelRows.join('\n');
}

function markdown2excelWithEscaped(mdTable) {
  const conversionTable = [
    {
      original: '|', 
      evacuation: '@@PIPE@@',
      escaped: '\\|'
    },
  ];

  // 退避用文字列に置換
  conversionTable.forEach(replacement => {
    mdTable = mdTable.split(replacement.escaped).join(replacement.evacuation);
  });

  // ベース処理
  mdTable = markdown2excel(mdTable);

  // 退避させてた文字列をエスケープ処理付きで復元
  conversionTable.forEach(replacement => {
    mdTable = mdTable.split(replacement.evacuation).join(replacement.original);
  });

  return mdTable;
}

const mdTable1 = '| 表 | 列A | 列B |\n| --- | --- | --- |\n| 行1 | セルA1 | セルB1 |\n| 行2 | セルA2 | セルB2 |';
console.log(markdown2excel(mdTable1));
/* ↓ 出力結果
表	列A	列B
行1	セルA1	セルB1
行2	セルA2	セルB2
*/

const mdTable2 = '| 表 | 列A | 列B |\n| :---: | :--- | ---: |\n| 行1 | セル\\|A1 | セル\\|B1 |\n| 行2 | セル\\|A2 | セルB2 |';
console.log(markdown2excelWithEscaped(mdTable2));
/* ↓ 出力結果
表	列A	列B
行1	セル|A1	セル|B1
行2	セル|A2	セルB2
*/

実際に使用する際の注意事項

本記事では置換処理についてで説明したように、上手く処理できないという理由から \\| のパターンで置換処理しています。

ですが実際に使用する際には \| で処理できるケースがあります。例として、私が作成している簡易ツール集 では textarea 要素を使って入力を受け付けており、\| の形で処理できています。

これはスクリプト上で用意した文字列リテラルでは自動的にエスケープ処理されるのに対し、textarea 等に入力されたデータは生の文字列として受け入れているためです。
(別解として String.raw を使えば、エスケープ処理されない生の文字列を扱えます)

このような挙動の違いがあるため、実際に利用する際にはエスケープ処理の扱いに注意が必要となってきます。下記は生の文字列が使える場合のパターンで、\| でエスケープするようになっています。

生の文字列で処理する場合の関数
生の文字列用の関数
function markdown2excelWithEscaped(mdTable) {
  const conversionTable = [
    {
      original: '|',
      evacuation: '@@PIPE@@',
      escaped: '\\|'
    },
  ];

  // 退避用文字列に置換
  conversionTable.forEach(replacement => {
    mdTable = mdTable.split(replacement.escaped).join(replacement.evacuation);
  });

  // ベース処理
  mdTable = markdown2excel(mdTable);

  // 退避させてた文字列をエスケープ処理付きで復元
  conversionTable.forEach(replacement => {
    mdTable = mdTable.split(replacement.evacuation).join(replacement.original);
  });

  return mdTable;
}

const mdTable1 = '| 表 | 列A | 列B |\n| --- | --- | --- |\n| 行1 | セルA1 | セルB1 |\n| 行2 | セルA2 | セルB2 |';

// 揃え方向を指定したパターン
// const mdTable1 = '| 表 | 列A | 列B |\n| :---: | :--- | ---: |\n| 行1 | セルA1 | セルB1 |\n| 行2 | セルA2 | セルB2 |';

// スペースやハイフンで見栄えを調整したパターン
// const mdTable1 = '|  表 | 列A    | 列B   |\n| --- | ----- | ----- |\n| 行1 | セルA1 | セルB1 |\n| 行2 | セルA2 | セルB2 |';

console.log(markdown2excel(mdTable1));
// ↓ 出力結果
// 表	列A	列B
// 行1	セルA1	セルB1
// 行2	セルA2	セルB2

// エスケープシーケンスを生の文字列として使った検証データ
const mdTable2 = String.raw`| 表 | 列A | 列B |
| :---: | :--- | ---: |
| 行1 | セル\|A1 | セル\|B1 |
| 行2 | セル\|A2 | セルB2 |`;

console.log(markdown2excelWithEscaped(mdTable2));
// ↓ 出力結果
// 表	列A	列B
// 行1	セル|A1	セル|B1
// 行2	セル|A2	セルB2

クリップボードについて

クリップボードの概要

Excel表データをコピーして、GoogleSheets に書式付きでペーストしている

Excel 表をコピーして GoogleSheets にペーストすると、書式(セルや文字の色、太字など)を含めて転写することができます。同じ表計算ソフトとはいえ、何故このようなことができるのでしょう?

それは、コピー&ペースト間にあるクリップボードの仕組みが関係しています。クリップボードは、コピー元となる表データのみを記憶しているのでなく、アプリケーション間での利用を想定して様々な形式の情報を取得しています。そしてペースト側のアプリケーションは、情報を解釈しコピー元を可能な限り再現しようと、利用できる情報を拾ってきます。

この一連の流れを図に表すと、下図のようになります。

Excel表データをクリップボードに格納すると、様々な形式の情報が格納され、ペースト先のアプリケーションに合わせ引き出されている

  • コピー元から、様々な形式の情報を取得する
  • コピー先へは、可能な限り再現しようと情報を利用する

例えば上図のように、表データをコピーしたとします。
テキストエディタにペーストする場合、テキストデータのみ転写され、書式情報や画像は利用できないため抜け落ちることになります。
一方ペイントにペーストした場合は、画像データを転写し、テキストや書式情報は扱えないため抜け落ちることになります。

そして GoogleSheets にペーストした場合ですが、まずテキストデータが転写されます。標準的な表計算ソフトは tsv( Tab-Separated Values ) がサポートされているので、コピー元と同じ行列構造で転写することができます。
更にコピー元を再現する過程で、セルの色やフォント(太字など)といった書式情報も利用可能であるため 転写されます。ただし完全にサポートされるわけではないので、場合によっては一部情報が抜け落ちることはありえます。
(上図では Numbers でセル色が変になっています。こうした場合、「プレーンテキストとして貼り付ける」「値のみ貼り付け」などで引き出す情報を絞って利用した方が良いかもしれません)

このようにデータの解釈がアプリケーション間で行われていることが、クリップボードの特徴となります。

本記事においてクリップボードはどう関わるのか?

本記事においては、markdown テーブルをタブ文字と改行コードで区切られた tsv 形式のテキストデータに変換する処理をしています。

Excel, GoogleSheets へ 行列構造を維持したまま表にペーストするに、この tsv 形式のテキストを使います。標準的な表計算ソフトは、この形式をサポートしてくれているためです。

Discussion