はじめに
Excel のアドインで reStructuredText の表を作成するものがあります。しかしサポンテの持っている Mac は Excel の入っているものと、そうでないものがあるのです。TSV から作成できれば、どんな表計算ソフトでも(ほぼ)対応できるはずです。
先日来、味をしめた方法でツールを作ってみます。
コードジェネレータ
左の入力欄に、サンプルのように TSV 文字列をコピペして、convert ボタンをクリックしてください。右の入力欄に、生成した reStructuredText 表現が書き出されます。
サンプル文字列がすでに入力されているので、convert ボタンをクリックするだけで動作を確認できます。
生成されるコード例
以下のようなコードが出力されます。
+--+------+-----+ |Id|Name |Price| +==+======+=====+ |1 |Apple |200 | +--+------+-----+ |2 |Banana|150 | +--+------+-----+ |3 |Citrus|300 | +--+------+-----+
ソースコード
念のため、ソースコードも載っけておきます。JSFiddle がサービス終了しちゃうかもしれませんからね。
それに、オフラインで使いたいという要件もあるかもしれません(その場合、CDN のところは修正してください)。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>TSV (from Excel or other) to reStructuredText Table</title> <style> .outer { display: flex; justify-content: center; align-items: center; } #app { margin: 0 auto; display: inline-block; text-align: left; height: 95%; } textarea { font-family: monospace; } @media (prefers-color-scheme: dark) { body { background-color: #333; color: #fff; } textarea { background-color: #444; color: #fff; } } </style> </head> <body> <div class="outer"> <div id="app"> <table> <tr> <td rowspan="3"> <textarea name="" id="src" cols="30" rows="10" v-model="src" placeholder="Excel などからコピペしてください"></textarea> </td> <td></td> <td rowspan="3"> <textarea name="" id="result" cols="30" rows="10" v-model="result"></textarea> </td> </tr> <tr> <td> <input type="radio" id="g" v-model="ln" value="grid"><label for="g">Grid</label><br /> <input type="radio" id="s" v-model="ln" value="simple"><label for="s">Simple</label><br /> <input type="radio" id="l" v-model="ln" value="list"><label for="l">List</label><br /> <input type="radio" id="c" v-model="ln" value="csv"><label for="c">CSV</label> </td> </tr> <tr> <td> <button v-on:click="convert">convert</button><br /> <button v-on:click="copyResult">copy</button> </td> </tr> </table> </div> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script> <script> var vm = new Vue({ el: "#app", data: { src: "Id\tName\tPrice\n1\tApple\t200\n2\tBanana\t150\n3\tCitrus\t300", result: "", ln: "grid" }, methods: { convert: function (e) { var ret = ''; let lines = this.getLines(); if (this.ln === 'grid') { ret = this.createGridTable(lines); } else if (this.ln === 'simple') { ret = this.createSimpleTable(lines); } else if (this.ln === 'list') { ret = this.createListTable(lines); } else if (this.ln === 'csv') { ret = this.createCsvTable(lines); } this.result = ret; }, createGridTable: function (lines) { let colWidths = this.getColWidths(lines); var ret = ""; var hr = []; var hr2 = []; colWidths.forEach(w => {hr.push("-".repeat(w));}); colWidths.forEach(w => {hr2.push("=".repeat(w));}); ret = '+' + hr.join('+') + "+\n"; var header = []; lines[0].forEach((col, idx) => {header.push(this.padright(this.escapeUnderscore(col), colWidths[idx]));}); ret += '|' + header.join('|') + "|\n"; ret += '+' + hr2.join('+') + "+\n"; lines.forEach((line, row) => { if (row === 0) {return;} var csv = []; line.forEach((col, idx) => {csv.push(this.padright(this.escapeUnderscore(col), colWidths[idx]));}); ret += '|' + csv.join('|') + "|\n"; ret += '+' + hr.join('+') + "+\n"; }); return ret; }, createSimpleTable: function (lines) { let colWidths = this.getColWidths(lines); var ret = ""; var hr = []; colWidths.forEach(w => {hr.push("=".repeat(w));}); ret += hr.join(' ') + "\n"; var header = []; lines[0].forEach((col, idx) => {header.push(this.padright(this.escapeUnderscore(col), colWidths[idx]));}); ret += header.join(' ') + "\n"; ret += hr.join(' ') + "\n"; lines.forEach((line, row) => { if (row === 0) {return;} var csv = []; line.forEach((col, idx) => {csv.push(this.padright(this.escapeUnderscore(col), colWidths[idx]));}); ret += csv.join(' ') + "\n"; }); ret += hr.join(' ') + "\n"; return ret; }, createListTable: function (lines) { var ret = ".. list-table::\n"; ret += " :header-rows: 1\n\n"; lines.forEach(line => { line.forEach((col, idx) => { if (idx === 0) { ret += " * - "; } else { ret += " - "; } ret += this.escapeUnderscore(col) + "\n"; }); }); return ret; }, createCsvTable: function (lines) { var ret = ".. csv-table::\n"; ret += " :header-rows: 1\n\n"; lines.forEach(line => { var csv = []; line.forEach((col, idx) => { csv.push(this.escapeUnderscore(col)); }); ret += " " + csv.join(',') + "\n"; }); return ret; }, getLines: function () { var linesSrc = this.src.split("\n"); var lines = []; linesSrc.forEach(line => { if (line.trim() ==='') {return;} lines.push(line.trim().split("\t")); }); return lines; }, getColWidths: function (lines) { let ret = new Array(lines[0].length).fill(0); lines.forEach(line => { line.forEach((col, idx) => { let len = this.strLen(this.escapeUnderscore(col)); ret[idx] = Math.max(len, ret[idx]); }); }); return ret; }, strLen: function (str) { let len = 0; for (let i = 0; i < str.length; i++) { (str[i].match(/[ -~]/)) ? len += 1 : len += 2; } return len; }, padright: function (text, width, padString = ' ') { let len = width - this.strLen(text); return text + padString.repeat(len); }, escapeUnderscore: function (str) { return str.replace(/_/g, '\\_'); }, copyResult: function () { navigator.clipboard .writeText(this.result) .then(() => { console.log('copied!') }) .catch(e => { console.error(e) }); } } }); </script> </body> </html>