便利ページ：Javascriptでちょっとした便利な機能を作ってみた

ちょっとしたことをプログラミングすればすぐできることなんだけど、そのために実装するのが面倒だったり、それがしょっちゅう必要な場面に遭遇することがあります。また、バッチファイルを用意するにしても、その置き場所を忘れたり、管理が面倒だったりします。
そこで、Javascriptで実装して、いつでもブラウザを開けば使えるようにします。

以下のような機能を組み込みます。一部の機能は、サーバと連携するため、RESTfulなサーバの構築が必要なものがあります。

・QRコード
　　QRコードを生成します。
・エンコード
　　Base64エンコード/デコード、URLエンコード/デコード、HTMLエンティティのエンコードをします。
・パスワード
　　乱数を使ってパスワードを生成します。
・バイト配列
　　16進数文字列をみやすくしたり、C言語形式、Java形式にします。
・日時
　　日時をUnixタイムに変換したり、1時間後などのDurationを演算できます。
・基数
　　2進数から16進数まで、基数の変換をします。
・クリップ
　　ちょっとした文字列を記憶します。
・整形
　　JSON、Javascript、HTML、CSSのコードを見やすい形に整形します。
・スクレイピング
　　Webページをスクレイピングします。ただし、サーバの準備が必要です。
・通知
　　LINE通知やGmail送信をします。ただし、サーバの準備が必要です。

Gitにも上げておきました。
　https://github.com/poruruba/utilities

参考までに、以下にデモとしてアクセスできるようにしておきました。
　https://poruruba.github.io/utilities/

全体で利用しているJavascript外部モジュールは以下の通りです。

・Bootstrap(v3.4.1)
　https://getbootstrap.com/docs/3.4/
・Vue
　https://jp.vuejs.org/index.html
・js-cookie
　https://github.com/js-cookie/js-cookie
・clipboard.js
　https://github.com/zenorocha/clipboard.js

（2019/4/20：追記）
HTMLエンティティのエンコードを追加しました。
(2019/5/12)
日時のページが使いにくかったので改良しました。

(続編）
　[便利ページ：自分のQiita記事を一覧表示](https://qiita.com/poruruba/items/f68455b2d937d2f052f4)
　[便利ページ：元号を変換してみた](https://qiita.com/poruruba/items/8aea492473a029a3865a)
　[便利ページ：トレンドキーワードを取得してみた](https://qiita.com/poruruba/items/2b4d978216131f5afa6a)
　[便利ページ：Javascriptで暗号化など追加](https://qiita.com/poruruba/items/fb2442a6e4a679eae6e5)
　[便利ページ：Javascriptでバイナリファイル操作](https://qiita.com/poruruba/items/18f91780195abadbbb89)
　[便利ページ：Javascriptでカラーピッカー](https://qiita.com/poruruba/items/8c5ee3c56210b0b35e4a)
　[便利ページ：JavascriptでQRコードスキャン](https://qiita.com/poruruba/items/59c3e6771aa9d957bfd0)
　[便利ページ：データURLを生成する](https://qiita.com/poruruba/items/0d57d5b157a97a79dba6)
　[便利ページ：Javascriptでアイコンファイルを生成する](https://qiita.com/poruruba/items/268eb6b1f58bce3cff51)
　[便利ページ：Web Bluetooth APIでBLEデバイスに接続](https://qiita.com/poruruba/items/653e9bbf6b725438442e)
　[便利ページ：WebSocketをサクッと触れるページを作ってみた](https://qiita.com/poruruba/items/684892652710d17a7c61)
　[便利ページ：JavascriptでHTML5をサクッと試す](https://qiita.com/poruruba/items/38d743cceedb240eff00)
　[便利ページ：javascriptで画像をOCRする](https://qiita.com/poruruba/items/7e4f63588ef10bdea44c)
　[便利ページ：JavascriptでYAMLからJSONに変換](https://qiita.com/poruruba/items/ae209d99a837ecd79c2e)
　[便利ページ：OpenID Connectのログインページを追加](https://qiita.com/poruruba/items/8958774b93befb49523d)
　[便利ページ：ASN.1をデコード](https://qiita.com/poruruba/items/0dfee3b61d6593d9dbb7)
　[便利ページ：COMポート出力のコンソールを作る](https://qiita.com/poruruba/items/c8fb8461068cb80fe9d1)
　[便利ページ：Leafletを使って緯度経度の表示や距離を表示する](https://qiita.com/poruruba/items/ffdf8e29a147c272f142)
　[便利ページ：plus codeのエンコード・デコード](https://qiita.com/poruruba/items/8cc7cfab29ba428c5468)
　[便利ページ：グラフ表示](https://qiita.com/poruruba/items/7d169cb02df9cdccc9f8)
　[便利ページ：カスタムQRコードの生成](https://qiita.com/poruruba/items/7b7e1413468c6bf94e56)

# QRコード

以下のJavascript外部モジュールを使っています。

・qrcode.js
https://davidshimjs.github.io/qrcodejs/

※qrcode.min.jsだと長い文字列でエラーになるようで、minがつかないqrcode.jsを使うようにしました。

ソースコード抜粋です。

```start.js
            $('#qrcode_area').empty();
            new QRCode($("#qrcode_area")[0], this.qrcode_input);
```

# エンコード

Base64もURLエンコードも、いずれもJavascriptに標準機能としてあるため、それを使っています。

```start.js
        base64_encode: function(encode){
            try{
                if( encode )
                    this.base64_output = btoa(this.base64_input);
                else
                    this.base64_output = atob(this.base64_input);
            }catch( error ){
                alert(error);
            }
        },
        url_encode: function(encode){
            try{
                if( encode )
                    this.url_output = encodeURIComponent(this.url_input);
                else
                    this.url_output = decodeURIComponent(this.url_input);
            }catch( error ){
                alert(error);
            }
        },
        html_encode: function(space){
            const html_entities = {
                '\"': '&quot;',
                '&': '&amp;',
                '\'': '&apos;',
                '<': '&lt;',
                '>': '&gt;',
                ' ': space ? '&nbsp;' : undefined,
            };

            this.html_output = this.html_input.split('').map((entity) => {
                return html_entities[entity] || entity;
            }).join('');
        },
```

# パスワード

ランダムなパスワードを生成したい時がよくありますよね。さらに、数字を入れろだの、記号を入れろだの、いろいろ注文を付けてくることがよくあります。
ですので、そういった特異な文字を含めるかどうかを指定できるようにしました。記号については、サンプルでいくつかの種類を選んでおきました。

```start.js
        passwd_create: function(){
            var passwd_num = Number(this.passwd_num);
            var passwd_number_num = this.passwd_check_number ? Number(this.passwd_number_num) : 0;
            var passwd_symbol_num = this.passwd_check_symbol ? Number(this.passwd_symbol_num) : 0;
            if( passwd_num < 1 || passwd_num < (passwd_number_num + passwd_symbol_num) ){
                alert('入力が不正です。');
                return;
            }
            if( (!this.passwd_check_lower_letter && !this.passwd_check_upper_letter) && passwd_num != (passwd_number_num + passwd_symbol_num)){
                alert('入力が不正です。');
                return;
            }

            var kind = Array(passwd_num);
            kind.fill(0);
            for( var i = 0 ; i < passwd_number_num ; i++ )
                kind[i] = 'n';
            for( var i = 0 ; i < passwd_symbol_num ; i++ )
                kind[passwd_number_num + i] = 's';

            for( var i = 0 ; i < passwd_num ; i++ ){
                var index = make_random(passwd_num - 1);
                if( index == i || kind[i] == kind[index] )
                    continue;
                var temp = kind[i];
                kind[i] = kind[index];
                kind[index] = temp;
            }

            const number_pattern = '0123456789';
            var alpha_pattern = '';
            if( this.passwd_check_lower_letter ){
                if( this.passwd_check_ecept_lO )
                    alpha_pattern += "abcdefghijkmnopqrstuvwxyz";
                else
                    alpha_pattern += "abcdefghijklmnopqrstuvwxyz";
            }
            if( this.passwd_check_upper_letter ){
                if( this.passwd_check_ecept_lO )
                    alpha_pattern += "ABCDEFGHJKLMNPQRSTUVWXYZ";
                else
                    alpha_pattern += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
            }

            var passwd = '';
            for( var i = 0 ; i < kind.length ; i++ ){
                if( kind[i] == 'n' ){
                    var index = make_random(number_pattern.length - 1);
                    passwd += number_pattern.charAt(index);
                }else if( kind[i] == 's' ){
                    var pattern = this.passwd_symbol_pattern;
                    var index = make_random(pattern.length - 1);
                    passwd += pattern.charAt(index);
                }else{
                    var index = make_random(alpha_pattern.length - 1);
                    passwd += alpha_pattern.charAt(index);
                }
            }

            this.passwd_password = passwd;
        },
```

# バイト配列

プログラミングしていると、16進数の文字列を扱うことが多いのですが、スペースを入れて見やすくしたり、C言語やJava言語の初期化子に指定したくなったりして整形が必要なケースがあります。
ソースコードはちょっと長いので割愛。

# 日時

日時をUnixタイムで扱いたいことがよくあります。それから、1日後の時間が欲しくてmoment.jsを使いたい時があります。

当然ながら、以下の有名なJavascript外部モジュールを使っています。

・Moment.js(v.2.10.3)
　https://momentjs.com/

```start.js
        /* 日時 */
        date_get_now: function(target){
            if( target == 'base')
                this.date_moment = moment();
            else
                this.date_moment_after = moment();
        },
        date_input_dialog: function(target){
            this.date_input_mode = target;
            var temp;
            if( target == 'base')
                temp = moment(this.date_moment);
            else
                temp = moment(this.date_moment_after);
            this.date_input_date = temp.format('YYYY-MM-DD');
            this.date_input_time = temp.format('HH:mm:ss');
            this.dialog_open('#date_input_dialog');
        },
        date_process: function(){
            var temp = moment(this.date_moment);
            if( this.date_duration_unit == 'year' )
                temp.add(Number(this.date_duration), 'years');
            else if( this.date_duration_unit == 'month' )
                temp.add(Number(this.date_duration), 'months');
            else if( this.date_duration_unit == 'day' )
                temp.add(Number(this.date_duration), 'days');
            else if( this.date_duration_unit == 'hour' )
                temp.add(Number(this.date_duration), 'hours');
            else if( this.date_duration_unit == 'minute' )
                temp.add(Number(this.date_duration), 'minutes');
            else if( this.date_duration_unit == 'second' )
                temp.add(Number(this.date_duration), 'seconds');
            this.date_moment_after = temp;
        },
        date_input_process: function(target){
            var date;
            if( target == 'free'){
                if( !this.date_input_free ){
                    alert('入力値が不正です。');
                    return;
                }
                date = this.date_input_free;
            }else{
                if( !this.date_input_date || !this.date_input_time ){
                    alert('入力値が不正です。');
                    return;
                }
                date = this.date_input_date + ' ' + this.date_input_time;
            }

            if( this.date_input_mode == 'base'){
                if( !isNaN(date) )
                    this.date_moment = moment(Number(date));
                else
                    this.date_moment = moment(date);
            }else{
                if( !isNaN(date) )
                    this.date_moment_after = moment(Number(date));
                else
                    this.date_moment_after = moment(date);
            }
            this.dialog_close('#date_input_dialog');
        },
        date_elapsed_process: function(){
            var base = moment(this.date_moment);
            var after = moment(this.date_moment_after);
            if( this.date_duration_unit == 'year' )
                this.date_duration = after.diff(base, 'years');
            else if(this.date_duration_unit == 'month')
                this.date_duration = after.diff(base, 'months');
            else if(this.date_duration_unit == 'day')
                this.date_duration = after.diff(base, 'days');
            else if(this.date_duration_unit == 'hour')
                this.date_duration = after.diff(base, 'hours');
            else if(this.date_duration_unit == 'minute')
                this.date_duration = after.diff(base, 'minutes');
            else if(this.date_duration_unit == 'second')
                this.date_duration = after.diff(base, 'seconds');
        },
```

# 基数

10進数、2進数、8進数、16進数を相互に変換します。
また、決まった桁数だけ0埋めする機能も付けました。

```start.js
        cardinal_convart: function(radix){
            var base = 0;
            if( radix == 10){
                base = parseInt(this.cardinal_decimal, 10);
            }else if( radix == 2){
                base = parseInt(this.cardinal_binary, 2);
            }else if( radix == 16){
                base = parseInt(this.cardinal_hexadecimal, 16);
            }
            this.base_decimal = Math.floor(base);

            this.cardinal_update();
        },
        cardinal_shift(radix, shift){
            if( shift == 'right' )
                this.base_decimal = Math.floor(this.base_decimal / radix);
            else
                this.base_decimal = Math.floor(this.base_decimal * radix);

            this.cardinal_update();
        },
        cardinal_update: function(){
            this.cardinal_decimal = this.base_decimal;

            var temp = this.base_decimal.toString(2);
            if( this.cardinal_check_binary ){
                for( var i = temp.length ; i < this.cardinal_binary_num; i++ )
                    temp = '0' + temp;
            }
            this.cardinal_binary = temp;
            var temp = this.base_decimal.toString(16);
            if( this.cardinal_check_hexadecimal ){
                for( var i = temp.length ; i < this.cardinal_hexadecimal_num; i++ )
                    temp = '0' + temp;
            }
            this.cardinal_hexadecimal = temp;
        },
```

# 整形

JSON、Javascript、HTML、CSS を見やすく整形します。
以下のJavascript外部モジュールを使っています。JSONはJavascriptの標準機能を使っています。

・js-beautify(v1.9.1)
https://github.com/beautify-web/js-beautify

```start.js
        arrange_process: function(type){
            try{
                if( type == 'json' )
                    this.json_inout = JSON.stringify(JSON.parse(this.json_inout), null, '\t');
                else if( type == 'javascript')
                    this.js_inout = js_beautify(this.js_inout, { indent_size: 2, space_in_empty_paren: true });
                else if( type == 'css')
                    this.css_inout = css_beautify(this.css_inout, { indent_size: 2, space_in_empty_paren: true });
                else if( type == 'html')
                   this.html_inout = html_beautify(this.html_inout, { indent_size: 2, space_in_empty_paren: true });
            }catch( error ){
                alert(error);
            }
        },
```

# スクレイピング

よく見るWebページを毎回URLを入力して見に行くのが面倒だったので、ボタン一つでWebページから必要な情報だけを取り出します。
ただし、実際の処理はサーバ側で行うようにしていますので、RESTfulサーバの立ち上げが必要です。

今回は、京急線の運行情報をスクレーピングします。
といっても、以下の再掲です。

　[京急線の運行情報をLINE Notifyする](https://qiita.com/poruruba/items/49956b3db54ca7689b8c)

クライアントJavascript側は、単にサーバにPOSTしているだけですので、ソースコードは割愛。
サーバ側の方で、アクセス者を限定するため、ApiKeyを指定するようにしています。

# 通知

任意のメールアドレスまたはLINEアカウントに、メッセージを送信します。
RESTfulサーバの立ち上げが必要です。
メールについては、Gmailアカウントが必要です。
また、LINE通知先はサーバ側で指定するのですが、とりあえず1か所のみにしています。自分に送るぐらいかなあと思いまして。

クライアント側はサーバにPOSTしているだけなので、ソースコードは割愛。

# サーバ側

通知とスクレイピング用にRESTfulサーバを立ち上げます。
ぜひ以下も参考にしてください。

[SwaggerでRESTful環境を構築する](https://qiita.com/poruruba/items/72d8c85f8923bc78b918)
[SwaggerでLambdaのデバッグ環境を作る(1)](https://qiita.com/poruruba/items/e3a46787c79517a815f9)

サーバ側はこんな感じです。

利用モジュール
・cheerio
・node-fetch
・url
・nodemailer

```index.js
const cheerio = require('cheerio');
const fetch = require('node-fetch');
const { URLSearchParams } = require('url');
const nodemailer = require('nodemailer');

const Response = require('../../helpers/response');
const LINE_PERSONAL_ACCESS_TOKEN = process.env.LINE_PERSONAL_ACCESS_TOKEN || 【LINEパーソナルアクセストークン】;
const GMAIL_USER = process.env.GMAIL_USER || 【Gmailログインメールアドレス】;
const GMAIL_PASSWORD = process.env.GMAIL_PASSWORD || 【Gmailログインパスワード】;
const GMAIL_FROM_ADDRESS = process.env.GMAIL_FROM_ADDRESS || 【Gmail送信元メールアドレス】;

const SERVER_APIKEY = 【ApiKey】;

exports.handler = async (event, context, callback) => {
    if( event.path == '/notify-line'){
        var body = JSON.parse(event.body);
        if( body.apikey != SERVER_APIKEY )
            throw 'apikey mismatch';

        return line_notify(body.message, LINE_PERSONAL_ACCESS_TOKEN)
        .then(json =>{
            return new Response({ result: 'OK'});
        });
    }else
    if( event.path == '/notify-gmail' ){
        var body = JSON.parse(event.body);
        if( body.apikey != SERVER_APIKEY )
            throw 'apikey mismatch';

        var message = {
            from : GMAIL_FROM_ADDRESS,
            to : body.mail_address,
            subject : 'Message form Utilities.Notify',
            text : body.message
        };
        var smtpConfig = {
            host: 'smtp.gmail.com',
            port: 465,
            secure: true,
            auth: {
                user : GMAIL_USER,
                pass : GMAIL_PASSWORD
            }
        };
        var transporter = nodemailer.createTransport(smtpConfig);
        return new Promise((resolve, reject) =>{
            transporter.sendMail(message, (err, response) => {
                if( err )
                    return reject(err);                
                return resolve(new Response({ result : 'OK' } ));
            });
        });
    }else
    if( event.path == '/scraping-keikyu'){
        var body = JSON.parse(event.body);
        if( body.apikey != SERVER_APIKEY )
            throw 'apikey mismatch';

        return scraping_keikyu()
        .then( message =>{
            return new Response( { result: 'OK', message : message });
        });
    }
};

function line_notify(message, token){
    var body = {
        message: message
    };
    return do_post_token('https://notify-api.line.me/api/notify', body, token);
}

function do_post_token(url, body, token){
    var data = new URLSearchParams();
    for( var name in body )
        data.append(name, body[name]);

    return fetch(url, {
        method : 'POST',
        body : data,
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Bearer ' + token }
    })
    .then((response) => {
        if( response.status != 200 )
            throw 'status is not 200';
        return response.json();
    });
}

function do_get(url, qs){
    var params = new URLSearchParams();
    for( var key in qs )
        params.set(key, qs[key] );

    var p = params.toString();
    if( p )
        url += '?' + p;
    return fetch(url, {
        method : 'GET',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
    })
    .then((response) => {
        if( response.status != 200 )
            throw 'status is not 200';
        return response.text();
    });
}

function scraping_keikyu(){
    return do_get('http://unkou.keikyu.co.jp/?from=mail')
    .then(text =>{
        const $ = cheerio.load(text);
        return $(".unko-panel").text().trim();
    });
}
```

以下の部分は環境に合わせて指定してください。

【LINEパーソナルアクセストークン】
　LINE通知を行いたい相手のパーソナルアクセストークンです。LINE Notifyを使っています。まあ、自分ので。詳細は、他のいろいろな方の紹介ページを参考にしていただくか、以下を参考にしてください。

　[京急線の運行情報をLINE Notifyする](https://qiita.com/poruruba/items/49956b3db54ca7689b8c)

【Gmailログインメールアドレス】
【Gmailログインパスワード】
　これらは、Gmail通知で使うアカウント情報です。

【Gmail送信元メールアドレス】
　Gmailで送信するときの送信元メールアドレスです。

【ApiKey】
　念のため、サーバが変な攻撃に使われてしまわないように、任意のApiKeyを指定しておきます。ブラウザから通知・スクレイピングするときに同じApiKeyを指定するようにします。

# 最後に

生活している中で、ちょっとした便利なツールがあるといいなあと思ったら、また追加していこうと思います。

以上