ローカルのmarkdownファイルを変更監視しつつSocket.IOでリアルタイムプレビュー

Amon2::Liteでmarkdownその他のリアルタイムプレビュー - すぎゃーんメモで、markdownとかのリアルタイムプレビューできるものを作って試してみたもの、やっぱりテキストエリアでmarkdownを書くってことはあまりしないなー、と思い。
大抵は使い慣れたエディタを使って編集すると思うので、それが変更されたときにブラウザ上で自動更新される方が嬉しいような気がして、

  1. ファイルの変更監視でmarkdownファイルの編集->保存を検知
  2. markdown->html変換したものをSocket.IOでリアルタイム更新

というものを作ってみた。

Node版

まずはNodeで。実行時引数で監視対象ファイルを指定し、fs.watchFileでそれを500msごとに監視、変更あれば変換したHTMLを普通にSocket.IOでクライアント側に通知する。

#!/usr/bin/env node
var fs   = require('fs');
var http = require('http');
var md   = require('markdown');

var target = process.argv[2];
if (! target) {
    console.error('usage: ' + process.argv.join(' ') + ' <filename>');
    process.exit(1);
}

var server = http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end(
        '<!DOCTYPE html><html><head><script type="text/javascript" src="/socket.io/socket.io.js"></script>' +
        '<script type="text/javascript">var socket = io.connect();socket.on("change", function (html) { document.getElementById("preview").innerHTML = html; });</script>' +
        '</head><body><div id="preview"></div></body></html>'
    );
});
server.listen(3000);

var io = require('socket.io').listen(server);
fs.stat(target, function (err, stat) {
    if (err) { throw err; }
    if(! stat.isFile()) {
        console.error(target + ' is not file');
        process.exit(1);
    }

    fs.watchFile(target, { interval: 500 }, function (curr, prev) {
        fs.readFile(target, 'utf8', function (err, text) {
            if (err) { throw err; }
            io.sockets.emit('change', md.parse(text));
        });
    });
});

ブラウザで開いた状態で、エディタから監視対象に指定したファイルを変更し保存すると勝手に更新される。

Perl

そういえばPerlでもPlack::Middleware::SocketIOっていうSocket.IOのサーバ実装のモジュールがあったはず、と思ったらいつの間にかDEPRECATEDになっていた。PocketIOというのを作っているのでそちらを代わりに使え、ということらしい。
GitHub - vti/pocketio: SocketIO PSGI App
まだCPANに上がってないしなんかテストがこけてしまったけどとりあえず無理矢理使ってみた。

#!/usr/bin/env plackup
use strict;
use warnings;

use Encode 'decode_utf8';
use Plack::Builder;
use PocketIO;
use Path::Class 'file';
use Amon2::Lite;
use Filesys::Notify::Simple;
use Text::Markdown 'markdown';

shift; # psgi
my $target = shift or die;
-f $target         or die;
$target = file($target);

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

builder {
    mount '/socket.io' => PocketIO->new(
        handler => sub {
            my $self = shift;
            my $watcher = Filesys::Notify::Simple->new(['.']);
            while (1) {
                $watcher->wait(
                    sub {
                        my @events = @_;
                        for my $event (@events) {
                            next if $event->{path} ne $target->absolute;
                            my $text = $target->slurp;
                            $self->send_message({
                                html => decode_utf8(markdown($text)),
                            });
                        }
                    },
                );
            }
        },
    );
    mount '/' => __PACKAGE__->to_app;
};

__DATA__

@@ index.tt
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>preview</title>
    <script type="text/javascript" src="https://raw.github.com/LearnBoost/socket.io-client/master/dist/socket.io.js"></script>
    <script type="text/javascript">
      var socket = io.connect();
      socket.on('message', function (msg) {
        document.getElementById('preview').innerHTML = msg.html
      });
    </script>
  </head>
  <body>
    <div id="preview"></div>
  </body>
</html>

ファイルの変更監視にFilesys::Notify::Simpleを使用。ちょっと書き方はイケてないかも知れないけど、とりあえずNode版と同じようには動作した。拡張子で判定とかやれば他の記法のも簡単に同じようなもの出来るかな。


https://gist.github.com/1098762