読者です 読者をやめる 読者になる 読者になる

websocket.el + Amon2でリアルタイム Markdown Viewer

emacs websocket amon2

WebSocketがなんなのか全然わかっていませんが、サンプルを参考に
リアルタイム Markdown Viewerを作成してみました。日本語を書くとエラーになるとか、
効率を全く考えていないとか、繰り返しやってるとエラーが出るとか
問題はまだ多数ありますが、いろいろ楽しいことができそうな予感はしました。

デモ動画

初めの 10秒ぐらい戸惑っています。

必要なもの

Emacsは 23以降がいいっぽいです。試したのは 24.1です。

サーバ側

Amon2付属の chat.psgiをちょっと直した程度です。

use strict;
use warnings;
use utf8;
use Amon2::Lite;
use Digest::MD5 ();
use Text::MultiMarkdown qw/markdown/;

get '/' => sub {
    my $c = shift;
    return $c->render('index.tt');
};

my $clients = {};

any '/emacs' => sub {
    my $c = shift;

    $c->websocket(
        sub {
            my $ws = shift;

            $ws->on_receive_message(
                sub {
                    my ($c, $message) = @_;
                    my $markdowned = markdown($message);
                    for (keys %$clients) {
                        $clients->{$_}->send_message($markdowned);
                    }
                }
            );

            $ws->on_eof(
                sub {
                    my $c = shift;
                }
            );
            $ws->on_error(
                sub {
                    my $c = shift;
                }
            );
        }
    );
};

any '/markdown' => sub {
    my ($c) = @_;
    my $id = Digest::SHA1::sha1_hex(rand() . $$ . {} . time);

    $c->websocket(
        sub {
            my $ws = shift;
            $clients->{$id} = $ws;

            $ws->on_receive_message(
                sub {
                    my ( $c, $message ) = @_;
                }
            );
            $ws->on_eof(
                sub {
                    my ($c) = @_;
                    delete $clients->{$id};
                }
            );
            $ws->on_error(
                sub {
                    my ($c) = @_;
                    delete $clients->{$id};
                }
            );
        }
    );

};

# load plugins
__PACKAGE__->load_plugin('Web::WebSocket');
__PACKAGE__->enable_middleware('AccessLog');
__PACKAGE__->enable_middleware('Lint');

__PACKAGE__->to_app(handle_static => 1);

__DATA__

@@ index.tt
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>Realtime Markdown Viewer</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
    <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css">
</head>
<body>
    <div class="container">
        <header><h1>Demo</h1></header>
        <div id="preview"></div>
        <footer>Powered by <a href="http://amon.64p.org/">Amon2::Lite</a></footer>
    </div>
    <script type="text/javascript">
        $(function () {
            var ws = new WebSocket('ws://localhost:5000/markdown');
            ws.onopen = function () {
                console.log('connected');
            };
            ws.onclose = function (ev) {
                console.log('closed');
            };
            ws.onmessage = function (ev) {
                $('#preview').html(ev.data);
            };
            ws.onerror = function (ev) {
                console.log(ev);
            };
        });
    </script>
</body>
</html>

クライアント

追記 2012/AUG/27 マルチバイト文字列対応を追加

;;; realtime-markdown-viewer.el ---

;; Copyright (C) 2012 by Syohei YOSHIDA

;; Author: Syohei YOSHIDA <syohex@gmail.com>
;; URL:

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;;; Code:

(eval-when-compile
  (require 'cl))

(require 'websocket)

(defgroup realtime-markdown-viewer nil
  "Realtime Markdown Viewer"
  :group 'text
  :prefix 'rtmv:)

(defvar rtmv:websocket)

(defun rtmv:init-websocket ()
  (setq rtmv:websocket
        (websocket-open
         "ws://127.0.0.1:5000/emacs"
         :on-message (lambda (websocket frame)
                       (message "%s" (websocket-frame-payload frame)))
         :on-close (lambda (websocket) (setq wstest-closed t)))))

(defun rtmv:send-to-server ()
  (if realtime-markdown-viewer-mode
      (let ((str (buffer-substring-no-properties (point-min) (point-max))))
        (websocket-send-text rtmv:websocket
                             (encode-coding-string str 'raw-text)))))

(defun rtmv:init ()
  (rtmv:init-websocket)
  (add-hook 'post-command-hook 'rtmv:send-to-server nil t))

(defun rtmv:finalize ()
  (websocket-close rtmv:websocket)
  (remove-hook 'post-command-hook 'rtmv:send-to-server t))

(define-minor-mode realtime-markdown-viewer-mode
  "Realtime Markdown Viewer mode"
  :group      'realtime-markdown-viewer
  :init-value nil
  :global     nil
  (if realtime-markdown-viewer-mode
      (rtmv:init)
    (rtmv:finalize)))

(provide 'realtime-markdown-viewer)

;;; realtime-markdown-viewer.el ends here

終わりに

ブラウザの需要は高まる一方なので、WebSocketに限らず
連携できる手段というものは押さえておく必要がある知識
なのではないかと思いました。