Function call in CoffeeScript

Here’s a little hassle in CoffeeScript syntax. Calling a function with parameter but without parentheses is fine.

Below sample code works.

express = require 'express'
app = express()
app.configure ->
  app.use express.bodyParser()
  app.use express.methodOverride()
  app.use app.router

app.get '/', (req, res) ->
  res.send { ok: true }

app.listen 3000

Calling a function with no parameter and parentheses is bad.

Same sample code except one little difference will cause the app to hang!! Notice that there is no parenthesis used on methodOverride.

express = require 'express'
app = express()
app.configure ->
  app.use express.bodyParser()
  app.use express.methodOverride #no parentheses
  app.use app.router

app.get '/', (req, res) ->
  res.send { ok: true }

app.listen 3000

So remember always use parentheses when a function has no parameter and it will save you time on debugging weird problems suddenly showing up in your codes.

Posted in Node.js | Tagged , | Comments Off on Function call in CoffeeScript

HTML5 and Facebook

Mark Zuckerberg said that Facebook wasted two years betting on HTML5 app instead of native app.

My own experience in using Facebook on iPhone and Android does not really show major difference.

HTML5 is looking better for many apps scenarios increasingly IMO.

Posted in General | Tagged , | Comments Off on HTML5 and Facebook

WebSocket with socket.io

Here’s a sample chat using socket.io. You can find the codes here. Live Demo. The style sheet is modified to work on iPhone, Android, Chrome, etc.

The server echoes back all messages sent by each user in a browser. Socket.io falls back gracefully with browser’s capability on supporting websocket.

server.js using Express 3, Consolidate, Dust and Socket.IO. Socket.IO listens on port 8080 while Express listens on port 3000.

var express = require('express')
  , http = require('http')
  , _ = require('underscore')._
  , engines = require('consolidate')
  , path = require('path');

var app = express();
var server = http.createServer(app);
var io = require('socket.io').listen(server);

io.set('log level', 1);

app.engine('dust', engines.dust);

app.configure(function(){
  app.set('port', process.env.PORT || 3000);
  app.set('views', __dirname + '/');
  app.set('view engine', 'dust');
  app.use(express.favicon());
  app.use(express.logger('default'));
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.cookieParser('your secret here'));
  app.use(express.session());
  app.use(app.router);
  app.use(express.static(path.join(__dirname, '/public')));
});

app.configure('production', function(){
  app.use(express.errorHandler());
});

app.get('/', function(req, res) {
  res.render('index', {});
});

io.configure('production', function() {
  io.enable('browser client minification');
  io.enable('browser client gzip');
  io.enable('browser client etag');
  io.enable('log');
  io.set('transports', [
    'websocket'
   ,'flashsocket'
   ,'htmlfile'
   ,'xhr-polling'
   ,'json-polling'
  ]);
});

server.listen(8080);
console.log("io server listening on port 8080");

app.listen(app.get('port'), function(){
  console.log("express server listening on port " + app.get('port'));
});

var nicknames = {};

io.sockets.on('connection', function(socket) {
  socket.on('message', function(msg) {
    socket.broadcast.emit('message', socket.nickname, msg);
  });

  socket.on('nickname', function(nick, fn) {
    if (nicknames[nick]) {
      fn(true);
    } else {
      fn(false);
      nicknames[nick] = socket.nickname = nick;
      socket.broadcast.emit('announcement', nick + ' connected');
      io.sockets.emit('nicknames', nicknames);
    }
  });

  socket.on('disconnect', function() {
    if (!socket.nickname) return;

    delete nicknames[socket.nickname];
    socket.broadcast.emit('announcement', socket.nickname + ' disconnected');
    socket.broadcast.emit('nicknames', nicknames);
  });

});

Client index.html

<!DOCTYPE html>
<html>
<head>
<link href="style.css" rel="stylesheet" type="text/css"/>
<script src="http://code.jquery.com/jquery-1.7.1.min.js" type="text/javascript"></script>
<script src="socket.io.js" type="text/javascript"></script>
<script type="text/javascript">
  var socket = io.connect('http://69.55.55.81:8080');

  socket.on('connect', function() {
    console.log('connected');
    $('#chat').addClass('connected');
  });

  socket.on('announcement', function(msg) {
    console.log(msg);
    $('#lines').append($('<p>').append($('<em>').text(msg)));
  });

  socket.on('nicknames', function(nicknames) {
    $('#nicknames').empty().append($('<span>Online: </span>'));
    for (var i in nicknames) {
      $('#nicknames').append($('<b>').text(nicknames[i]));
    }
  });

  socket.on('message', message);

  socket.on('reconnect', function() {
    console.log('reconnect');
    $('#lines').remove();
    message('System', 'Reconnected to the server');
  });

  socket.on('reconnecting', function() {
    message('System', 'Attempting to reconnect to the server');
  });

  socket.on('error', function(e) {
    message('System', e ? e : 'An unknown error occured');
  });

  function message(from, msg) {
    console.log(msg);
    $('#lines').append($('<p>').append($('<b>').text(from), msg));
  }

  $(function() {
    $('#set-nickname').submit(function(e) {
      socket.emit('nickname', $('#nick').val(), function(set) {
        if (!set) {
          clear();
          return $('#chat').addClass('nickname-set');
        }
        $('#nickname-err').css('visibility', 'visible');
      });
      return false;
    });

    $('#send-message').submit(function() {
      console.log('submit');
      message('me', $('#message').val());
      socket.emit('message', $('#message').val());
      clear();
      $('#lines').get(0).scrollTop = 10000000;
      return false;
    });

    function clear() {
      console.log('focus');
      $('#message').val('').focus();
    };
  });
</script>
</head>
<body>
<div id="chat">
  <div id="nickname">
    <form class="wrap" id="set-nickname">
      <p>Please type in your nickname and press enter.</p>
      <input id="nick" type="text"/>
      <p id="nickname-err">Nickname already in use</p>
    </form>
  </div>
  <div id="connecting">
    <span class="wrap">Connecting to socket.io server</span>
  </div>
  <div id="messages">
    <div id="nicknames"></div>
    <div id="lines"></div>
  </div>
  <form id="send-message">
    <input id="message" type="text"/>
    <button type="button" text="Send"/>
  </form>
</div>

</body>
</html>
Posted in Node.js | Tagged , , , , | Comments Off on WebSocket with socket.io

Testing views in couchdb with curl

Couchdb has REST APIs and it works well with curl.

Let’s build a contact database in Futon with following:

Documents

{"_id": "bill", "name": "bill", "groups": ["friends"], "type": "contact"}
{"_id": "john", "name": "john", "groups": ["friends", "coworkers"], "type": "contact"}
{"_id": "111-222-3333", "phone": "home", "contact_id": "bill", "type": "phone"}
{"_id": "222-333-4444", "phone": "mobile", "contact_id": "bill", "type": "phone"}
{"_id": "333-444-5555", "phone": "mobile", "contact_id": "john", "type": "phone"}
{"_id": "444-555-6666", "phone": "work", "contact_id": "john", "type": "phone"}
{"_id": "friends", "desc": "friends group", "type": "group"}
{"_id": "coworkers", "desc": "friends group", "type": "group"}

Views

{
   "_id": "_design/contacts",
   "_rev": "12-d069646c5b9ff9caf55c1fa89e3f7ea4",
   "language": "javascript",
   "views": {
       "group_members": {
          "map": "function(doc) {
             if (doc.type == 'contact') {
                for(var i in doc.groups) {
                  emit([doc.groups[i],0], null);
                }
             } else if (doc.type == 'group') {
               emit([doc._id,1], null);
             }
           }"

        },
        "details": {
           "map": "function(doc) {
              if (doc.type == 'contact') {
                emit([doc._id,0], null);
              }
              else if (doc.type == 'phone') {
                emit([doc.contact_id, 1], doc.phoneType);
              }
           }"

        }
    }
}

Let’s query for all members of the friends group. Notice the key is an array.

$ curl 'http://localhost:5984/contacts/_design/contacts/_view/group_members?startkey=\["friends"\]&endkey=\["friends"\{\}\]'

Results

{"total_rows":5,"offset":2,"rows":[
{"id":"bill","key":["friends",0],"value":null},
{"id":"john","key":["friends",0],"value":null},
{"id":"friends","key":["friends",1],"value":null}
]}

Let’s query for all members of the friends group with include_docs=true.

$ curl 'http://localhost:5984/contacts/_design/contacts/_view/group_members?startkey=\["friends"\]&endkey=\["friends"\{\}\]&include_docs=true'

Results

{"total_rows":5,"offset":2,"rows":[
{"id":"bill","key":["friends",0],"value":null,"doc":{"_id":"bill","_rev":"1-8358d8caf24c130a9c7ce6994c05abf7","name":"bill","groups":["friends"],"type":"contact"}},
{"id":"john","key":["friends",0],"value":null,"doc":{"_id":"john","_rev":"1-4f66287ab931de7591eb83cbd924dead","name":"john","groups":["friends","coworkers"],"type":"contact"}},
{"id":"friends","key":["friends",1],"value":null,"doc":{"_id":"friends","_rev":"1-276a9b9789261919c80dd3cf147aa93e","desc":"friends group","type":"group"}}
]}

Let’s get Bill’s contact details.

$ curl -v 'http://localhost:5984/contacts/_design/contacts/_view/details?startkey=\["bill"\]&endkey=\["bill",\{\}\]&include_docs=false'

Results

{"total_rows":6,"offset":0,"rows":[
{"id":"bill","key":["bill",0],"value":null},
{"id":"111-222-3333","key":["bill",1],"value":null},
{"id":"222-333-4444","key":["bill",1],"value":null}
]}

Bill’s contact details with include_docs=true

$ curl -v 'http://localhost:5984/contacts/_design/contacts/_view/details?startkey=\["bill"\]&endkey=\["bill",\{\}\]&include_docs=true'

Results

{"total_rows":6,"offset":0,"rows":[
{"id":"bill","key":["bill",0],"value":null,"doc":{"_id":"bill","_rev":"1-8358d8caf24c130a9c7ce6994c05abf7","name":"bill","groups":["friends"],"type":"contact"}},
{"id":"111-222-3333","key":["bill",1],"value":null,"doc":{"_id":"111-222-3333","_rev":"1-5fc019b84d86073895fd126bdc3f373f","phone":"home","contact_id":"bill","type":"phone"}},
{"id":"222-333-4444","key":["bill",1],"value":null,"doc":{"_id":"222-333-4444","_rev":"1-730af0703a373ca2e29d793548c79d62","phone":"mobile","contact_id":"bill","type":"phone"}}
]}
Posted in NoSQL | Tagged , , , , | Comments Off on Testing views in couchdb with curl

TODO List with node.js, express, dust, backbone, iscroll & jquery mobile

Here’s a small project to play with a few modules in Node. Backbone provides a simple MVC and it’s possible to share the models and views in both client and server side.

It can support one page web app with all views pre-generated and sent to client browser or traditional web app where each page is rendered by server.

The project is hosted in github¬†and here’s a live demo.

Posted in Node.js | Tagged , , , , | Comments Off on TODO List with node.js, express, dust, backbone, iscroll & jquery mobile

Typo 6.1 Exporter for WordPress

I wrote two rake scripts to dump posts from Typo as planned in my last post.

Here’re the steps.

1. Dump Typo’s posts into blog.xml

$ cp words.rake export.rake /path/to/typo/lib/tasks/
$ cd /path/to/typo/
$ rake words[tags.rb]
$ vi lib/tasks/tags.rb # edit generated tags hash
$ rake export[blog.xml]

2. Go to WordPress admin page and activate WordPress Importer tool
3. Upload blog.xml file and follow the import instruction

words.rake

desc "dump a hash of words"
task :words, [:file] => [:environment] do |t, args|
  articles = Article.find(:all)
  words = {}
  articles.each do |a|
    a.title.split(/[^a-zA-Z]/).each do |w|
      l = w.downcase
      words[l] = w.capitalize if l != '' && !words[l]
    end if a.title
  end

  sorted = words.keys.sort

  # dump words for manual selection
  File.open(File.join(File.dirname(__FILE__), args.file), "w") do |f|
    f.write("$tags = {\n");
    sorted.each do |k|
      f.write("'#{k}'=>'#{words[k]}',\n");
    end
    f.write("}\n");
  end
end

tags.rb

$tags = {
'acer'=>'Acer',
'adsense'=>'Adsense',
'apache'=>'Apache',
'architecture'=>'Architecture',
'bears'=>'Bears',
'bees'=>'Bees'
}

export.rake

desc "export to wordpress xml file"
task :export, [:file] => [:environment] do |t, args|
  require File.join(File.dirname(__FILE__), 'tags')

  b = Blog.default
  s = b.settings;
  articles = Article.find(:all)
 
  File.open(args.file, "w") do |f|
    f.write('<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
   xmlns:excerpt="http://wordpress.org/export/1.2/excerpt/"
   xmlns:content="http://purl.org/rss/1.0/modules/content/"
   xmlns:wfw="http://wellformedweb.org/CommentAPI/"
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:wp="http://wordpress.org/export/1.2/"
>
<channel>');
    f.write("\n<title>#{s["blog_name"]}</title>\n");
    f.write("<link>#{b.base_url}</link>\n");
    f.write("<description>#{s["blog_subtitle"]}</description>\n");
    f.write("<pubDate>#{DateTime.now}</pubDate>\n");
    f.write("<language>en-US</language>\n");
    f.write("<wp:wxr_version>1.2</wp:wxr_version>\n");
    f.write("<wp:base_site_url>#{b.base_url}</wp:base_site_url>\n");
    f.write("<wp:base_blog_url>#{b.base_url}</wp:base_blog_url>\n");

    users = User.find(:all)
    users.each do |u|
      f.write("<wp:author>\n");
      f.write("<wp:author_id>#{u.id}</wp:author_id>\n");
      f.write("<wp:author_login>#{u.login}</wp:author_login>\n");
      f.write("<wp:author_email>#{u.email}</wp:author_email>\n");
      f.write("<wp:author_display_name>#{u.name}</wp:author_display_name>\n");
      f.write("</wp:author>\n");
    end

    i = 1
    categories = Category.find(:all)
    categories.each do |c|
      f.write("<wp:category><wp:term_id>#{i}</wp:term_id><wp:category_nicename>#{c.name.downcase}</wp:category_nicename><wp:category_parent></wp:category_parent><wp:cat_name><![CDATA[#{c.name}]]></wp:cat_name></wp:category>\n");
      i += 1
    end

    $tags.each do |k,v|
      f.write("<wp:tag><wp:term_id>#{i}</wp:term_id><wp:tag_slug>#{k}</wp:tag_slug><wp:tag_name><![CDATA[#{v}]]></wp:tag_name></wp:tag>\n");
      i += 1
    end

    f.write("<generator>http://competo.com</generator>\n");

    articles.each do |a|
      f.write("<item>\n");
      f.write("<title>#{a.title}</title>\n");
      #f.write("<link>#{a.permalink}</link>\n");
      f.write("<pubDate>#{a.published_at}</pubDate>\n");
      f.write("<dc:creator>#{a.author}</dc:creator>\n");
      f.write('<guid isPermalink="false">' + "#{a.permalink}</guid>\n");
      f.write("<description></description>\n");

      # change embeded images path
      content = a.body.gsub('src="/archives/', 'src="/wp-content/uploads/')

      f.write("<content:encoded><![CDATA[#{content}]]></content:encoded>\n");
      f.write("<excerpt:encoded><![CDATA[#{a.excerpt}]]></excerpt:encoded>\n");
      f.write("<wp:post_id>#{a.id}</wp:post_id>\n");
      f.write("<wp:post_date>#{a.created_at}</wp:post_date>\n");
      f.write("<wp:comment_status>closed</wp:comment_status>\n");
      f.write("<wp:ping_status>closed</wp:ping_status>\n");
      f.write("<wp:post_name>#{a.permalink}</wp:post_name>\n");
      f.write("<wp:status>publish</wp:status>\n");
      f.write("<wp:post_parent>0</wp:post_parent>\n");
      f.write("<wp:menu_order>0</wp:menu_order>\n");
      f.write("<wp:post_type>post</wp:post_type>\n");
      f.write("<wp:post_password></wp:post_password>\n");
      f.write("<wp:is_sticky>0</wp:is_sticky>\n");

      a.categories.each do |c|
        f.write('<category domain="category" nicename="' + "#{c.name.downcase}" + '"><![CDATA[' + "#{c.name}]]></category>\n");
      end if a.categories

      aa = a.body.split(/[^a-zA-Z]/).map{|x| x.downcase}.uniq
      aa.each do |w|
        t = $tags[w]
        f.write('<category domain="post_tag" nicename="' + "#{w}" + '"><![CDATA[' + "#{t}]]></category>\n") if t
      end if a.body

      # ignore comments
      f.write("</item>\n");
    end
    f.write("</channel>\n");
    f.write("</rss>\n");
  end
end
Posted in General | Tagged , , , | Comments Off on Typo 6.1 Exporter for WordPress

Migrating a blog from Typo 6 to WordPress 3.4 (imported)

Finally got time to switch our blog engine to much popular WordPress. There’s no existing tool that can convert a Typo blog to WordPress.

Here’s my draft plan to do it myself.

  1. Write a sample post (this one) in WordPress.
  2. Export this new blog and use its format as guide for import format.
  3. Write a ruby script to dump data from Postgresql into a file in export format
  4. Import the file

Add a few html tags for testing…

I will post the results later.

[update]

Export XML file

Posted in General | Tagged , , , | 2 Comments

Migrating a blog from Typo 6 to WordPress 3.4

Finally got time to switch our blog engine to much popular WordPress. There’s no existing tool that can convert a Typo blog to WordPress.

Here’s my draft plan to do it myself.

  1. Write a sample post (this one) in WordPress.
  2. Export this new blog and use its format as guide for import format.
  3. Write a ruby script to dump data from Postgresql into a file in export format
  4. Import the file

Add a few html tags for testing…

I will post the results later.

[update]

Export XML file

The imported post is shown here with comment, categories and tags intact.

Posted in General | Tagged , , , | 2 Comments