サポンテ 勉強ノート

サポンテの勉強ノート・読書メモなどを晒します。

TSV(Excel または他の表計算ソフト)から reStructuredText の表に変換するコードジェネレータ

はじめに

 Excel のアドインで reStructuredText の表を作成するものがあります。しかしサポンテの持っている MacExcel の入っているものと、そうでないものがあるのです。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>