事始めに Twitter クライアントを書いてみたよ

g:twitter:id:kageroh_:20090416:1239872427
正式にバージョン管理を開始。下記のコードは古い恐れがあります。

全然 XUL 要素使ってないけどね!
http://kgr.s56.xrea.com/misc/xwitter.zip

使い方
alt+UとかEscとかを押してみよう!

こってり書き忘れてたけど、username, password は xwitter.mod に書いてね! あと、起動の仕方は手前味噌だけど、id:kageroh_:20090409:1239282039 が簡単だと思うよ!

><キーワードハイライトつけた。xwitter.mod に正規表現で書く。

  • xwitter
    • application.ini
[App]
Vendor=kageroh
Name=Xwitter
Version=0.1
BuildID=20090408
ID=kagerohs122@hotmail.com

[Gecko]
MinVersion=1.9
MaxVersion=1.9.0.*
content xwitter file:content/
      • content

リファクタリングした。

できる範囲で JS 分離した。すっきりー!

<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<?xml-stylesheet href="xwitter.css" type="text/css"?>
<!DOCTYPE window [
	<!ENTITY % xwitter SYSTEM 'xwitter.mod'> %xwitter;
]>
<xul:window id="main" title="Xwitter" screenX="&x;" screenY="&y;"
	xmlns="http://www.w3.org/1999/xhtml"
	xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
	<xul:vbox id="box" width="&w;" height="&h;" style="overflow:auto"><xul:box /></xul:vbox>
	<form id="status_update_form" method="post" action="&protocol;://twitter.com/status/update">
		<fieldset>
			<legend for="status" accesskey="u">status</legend>
			<textarea id="status" rows="2" cols="40"></textarea>
			<input type="button" id="update-submit" value="update" />
			<input type="hidden" id="in_reply_to_status_id" />
		</fieldset>
	</form>
	<input id="url" />
<xul:script src="&protocol;://&username;:&password;@twitter.com/account/verify_credentials.json" />
<xul:script src="xwitter.js" />
<xul:script>
const protocol = '&protocol;'
Xwitter.initialize();
Xwitter.EXP = (new RegExp).compile(/&regexp;/);
window.setInterval(Xwitter.refresh(), &sec; * 1000);
</xul:script>
</xul:window>
@charset 'utf-8';
@namespace url("http://www.w3.org/1999/xhtml");

* {
	letter-spacing: 1px;
	font-size: 12px;
	line-height: 1.5;
}

html {
	background-color: #9ae4e8;
	margin-bottom: 20px;
	display: none;
}

body,
div.section {
	padding: 9px;
}

body {
	color: #333;
	background-color: white;
	font-family: monospace;
}

div.section {
	border-width: 0 0 1px;
	border-color: #d2dada;
	border-style: dashed;
	clear: both;
	min-height: 50px;
}

div.section:hover {
	background-color: #f7f7f7;
}

p {
	margin: 4px 16px 0 57px;
}

p.highlight {
	background-color: yellow;
}

p.protect {
	text-indent: 21px;
	background-image: url('data:image/gif;base64,R0lGODlhEAAVAPYAAP/v0f/v0M6jS5mZmfDVoc6iSvC9WMvKyu+8VqR6KP/u0PDGc+WyTPC/YPC/Xv/TeuayTP/QcuWxTP/gof/fn/C+W5iYmPDLgf/Vf//PcP/RdPC/XMCSNPDFcfDBY//NZ//Qb//Yif/nuO/Id/DEbc6jSv/emP/aj6R7KO/EbfDBZfDJfv/ip6R7KfDIe//Zjv/Yh//bkvDDav/Te//OavDQkfC9Wf/clu/Oi//kr//emu/LgP/Xg//VgfDMhfDKfv/jqfDAYvDDafDGdfDIefDBZvC+XO+/YfDSlv/blO/DbfDNhu/DafDNisCTNcCSNcGTNc2iSsrJyf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAFMALAAAAAAQABUAAAexgFOCg4SFhoeIiYlSB1IDilKRjJGPhpKVkQcWlo6ElIUCUVGhogUFAqeDAgEAAQquAK2sBlCCUQRINThLFysjC0pMELVTBQEiOUATOkkvMD0zBk6CAgRNPjsuLU4kQipHDE+CJQAsFCYxCVAYDxoZ0rYEP0RDHQkcHg0bFQzEUQA3ToTggeJJBBA0PiCYNuXWghQyigRxYMQGAgQSGE55AuWJRyhOOkIBqaikyZMoDQUCADs=');
	background-repeat: no-repeat;
}

em {
	color: #0084b4;
	font-style: normal;
	font-weight: bolder;
}

div.section input {
	background-color: transparent;
	display: block;
	float: right;
	width: 16px;
	height: 16px;
	cursor: pointer;
}

input[title] {
	background-image: url('data:image/gif;base64,R0lGODlhEAAQAPQAAP////v7+/f39/Pz8+/v7+rq6ubm5uLi4t7e3tra2tLS0s7OzsrKysXFxcHBwb29vbm5ubW1tbGxsa2trampqaWlpaCgoJycnJiYmJSUlP///wAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1Qgz94cGFja2UgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAKP3hwYWNrZXQgZW5kPSJ3Ij8+ACH5BAEHABoALAAAAAAQABAAAAV3oCaOWkGQKBo5aUtgl9CiEPM08yhcAWHNhQSD8gAAIhNGoiBSXCqRhyJgFCweEYvloGFYqMawcYBJjBwVgXh8UaAeE/C4wmhR1GEDpTWgrAN7KQgQAAcTEgUAE0woC1EWCgsWDhJmKA4ZOCICDxkLKQyMJAefIiEAOw==');
}

input[class="true"] {
	background-image: url('data:image/gif;base64,R0lGODlhEAAQAPYAAPrv3vrt1vnr1/nq1PrpzfnoyPjnzfjlyvfkyfjkxvfjw/fhv/fgwfbgvvfgtfbeufbfs/betvbcsPXcq//kNvXasPXZr//hOPXZo/XYq//gNP/gN/TYrPTYqvXYpf/fOv/fMf/eOf7dOf/eNfTWoP7dNv3bNfPTmf7aNf3ZMvzWMvzWM/LOlPPPi/vUMvrSNPjNLvfLN/DGevbJOPfKLvHGbe/EePfJLvTGNPLEVe7AcPTFMfPDOvDBV/TEMe/AZvTDLfDBTfPCPu69WvG+P/C+RPG+PPG+O/C9Re+8S/G9Pe67WvC8Pu24V++6P/C5Oe+5O+24Ue+4NO22Uey0SP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C1hNUCBEYXRhWE1Qgz94cGFja2UgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAKP3hwYWNrZXQgZW5kPSJ3Ij8+ACH5BAEHAFUALAAAAAAQABAAAAeHgFWCg1UEhIeIMieIjABQQYyILDAwGZGESSMjPZEDDyQ6MRcXMzYeDQKCEFNKOzcvH7EfLjQ4R00FVRJSGr0hv8AgTAqDHE8lJcC/Jk4IhxZEItLTSAyMQyjKKTWRRRvf4DmMBzwUKkZGKxRCjBVAQD8GC0s+PgmILVERhB1UGIgTGAVwMCgQADs=');
}

input[class="reply"] {
	margin-top: 6px;
	background-image: url('data:image/gif;base64,R0lGODlhEAAQAPIAALW1tbu7u8PDw8rKytTU1Nvb2+Tk5Orq6iH/C0lDQ1JHQkcxMDEyv3BydAAAAhQAAAA4dmNndAAAAawAAAAwbmRpbgAAAdwAAAA4ZHNjbQAAAkwAAALyWFlaIAAAAAAAAHRLAAA+HQAAA8tYWVogAAAAAAAAWnMAAKymAAAXJlhZWiAAAAAAAAAoGAAAFVcAALgzWFlaIAAAAAAAAPNSAAEAAAABFs9zZjMyAAAAAAABDEIAAAXe///zJgAAB5IAAP2R///7ov///aMAAAPcAADAbGN1cnYAAAAAAAAAAQHNAAB2Y2d0ACH5BAkAAAgALAAAAAAQABAAAARGEElZyLwYHWAzNkFQeFcRCKhAHJ4BpEIoeiY6DPKYgQF74AGDZyOUEAADkoE1GQCYpMlGF52oqpdBB4sYFLlJLsLwxVIREQA7');
	clear: right;
}

img {
	margin-right: 9px;
	float: left;
	width: 48px;
	height: 48px;
}

textarea {
	display: block;
}

form,
#url {
	position: fixed;
	opacity: .8;
}

form {
	background-color: white;
	display: none;
	top: 0;
}

#url {
	bottom: 0;
	width: 100%;
}
        • xwitter.js

いちお、replaceChild で置換した古いノードの removeEventListener した。

function $(id) { return document.getElementById(id); }
function $$(tagName, context) { return (context || document).getElementsByTagName(tagName); }
function $D() { return (new Date).toGMTString()
  .replace(/( |:|,)/g, function($_, $1) {
    switch ($1) {
    case ' ': return '+';
    case ':': return '%3A';
    case ',': return '%2C';
    }
  });
}
var Xwitter = {
  GMT: $D(),
  DOC: document.implementation.createDocument('', '', null),
  XSL: new XSLTProcessor,
  HTT: (new RegExp).compile(/(https?:\/\/[-_.!~*\'()\w;\/?:\@&=+\$,%#]+)/),
  URL: $('url'),
  BOX: $('box'),
  FRM: $('status_update_form'),
  TXT: $('status'),
  BTN: $('update-submit'),
  HDN: $('in_reply_to_status_id'),

  initialize: function() {
    this.DOC.async = false;
    this.DOC.load('xwitter.xsl');
    this.XSL.importStylesheet(this.DOC);
    this.BTN.addEventListener('click', function() {
      Xwitter.update();
    }, false);
    window.addEventListener('keydown', function(event) {
      switch (event.keyCode) {
      case KeyEvent.DOM_VK_ESCAPE:
        Xwitter.escape();
        return;
      }
      if (event.altKey)
        switch (event.keyCode) {
        case KeyEvent.DOM_VK_U:
          Xwitter.focus();
          return;
        }
      if (event.ctrlKey)
        switch (event.keyCode) {
        case KeyEvent.DOM_VK_R:
          Xwitter.refresh();
          return;
        case KeyEvent.DOM_VK_W:
          window.close();
          return;
        }
    }, false);
  },

  refresh: function() {
    var xhr = new XMLHttpRequest;
    xhr.onreadystatechange = function() {
      if (xhr.readyState !== 4 || xhr.status !== 200) return;
      var old = Xwitter.BOX.replaceChild(Xwitter.XSL.transformToFragment(xhr.responseXML, document), Xwitter.BOX.firstChild);
      for (var c, i = 0, e = $$('input'); c = e[i++];)
        c.addEventListener('click', Xwitter[c.alt ? 'reply' : 'fav'], false);
      for (var c, i = 0, e = $$('p'); c = e[i++];) {
        c.addEventListener('dblclick', Xwitter.findURL, false);
        if (Xwitter.EXP.test(c.textContent)) c.className += ' highlight';
      }
      Xwitter.BOX.firstChild.style.display = 'block';
      Xwitter.free(old);
      old = null;
      Xwitter.notify();
    };
    xhr.open('get', protocol + '://twitter.com/statuses/friends_timeline.xml?since=' + Xwitter.GMT);
    xhr.send(null);
    Xwitter.wait();
    Xwitter.GMT = $D();
    return arguments.callee;
  },

  free: function(old) {
    for (var c, i = 0, e = $$('input', old); c = e[i++];)
      c.removeEventListener('click', Xwitter[c.alt ? 'reply' : 'fav'], false);
    for (var c, i = 0, e = $$('p', old); c = e[i++];)
      c.removeEventListener('dblclick', Xwitter.findURL, false);
  },

  fav: function() {
    var elm = this, fav = elm.className === 'true';
    var xhr = new XMLHttpRequest;
    xhr.onreadystatechange = function() {
      if (xhr.readyState !== 4 || xhr.status !== 200) return;
      elm.className = !fav;
      Xwitter.notify();
    };
    xhr.open('post', [
      protocol,
      '://twitter.com/favourings/',
      fav ? 'destroy' : 'create',
      '/',
      elm.title,
      '.xml'
      ].join(''));
    xhr.send(null);
    Xwitter.wait();
  },

  reply: function() {
    Xwitter.HDN.value = this.title;
    Xwitter.TXT.value = '@' + this.alt + ' ';
    Xwitter.focus();
  },

  update: function() {
    var xhr = new XMLHttpRequest;
    xhr.onreadystatechange = function() {
      if (xhr.readyState !== 4 || xhr.status !== 200) return;
      Xwitter.refresh();
      Xwitter.escape();
      Xwitter.notify();
    };
    xhr.open('post', [
      protocol,
      '://twitter.com/statuses/update.xml?',
      this.TXT.id,
      '=',
      encodeURIComponent(this.TXT.value),
      '&',
      this.HDN.id,
      '=',
      this.HDN.value,
      '&source=xwitter'
      ].join(''));
    xhr.send(null);
    Xwitter.wait();
  },

  focus: function() {
    this.FRM.style.display = 'block';
    this.TXT.focus();
  },

  escape: function() {
    this.FRM.style.display = 'none';
    this.TXT.value = this.HDN.value = '';
  },

  findURL: function() {
    if (!Xwitter.HTT.exec(this.textContent)) return;
    var url = RegExp.$1;
    var xhr = new XMLHttpRequest;
    xhr.onreadystatechange = function() {
      if (xhr.readyState !== 4) return;
      Xwitter.URL.value = xhr.status !== 200 ? url : eval('(' + xhr.responseText + ')').url || url;
      Xwitter.URL.select();
      Xwitter.notify();
    };
    xhr.open('get', 'http://ss-o.net/api/reurl.json?url=' + encodeURIComponent(url));
    xhr.send(null);
    Xwitter.wait();
  },

  wait: function() {
    Xwitter.BOX.style.cursor = 'wait';
  },

  notify: function() {
    Xwitter.BOX.style.cursor = 'auto';
  }
};
        • xwitter.mod
<?xml encoding="utf-8"?>
<!ENTITY username '***************'>
<!ENTITY password '***************'>
<!ENTITY sec '90'>
<!ENTITY w '480'>
<!ENTITY h '640'>
<!ENTITY x '0'>
<!ENTITY y '0'>
<!ENTITY regexp '\n'>
<!ENTITY protocol 'https'>
        • xwitter.xsl
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns="http://www.w3.org/1999/xhtml"
	version="1.0">
	<xsl:output method="xml"
		encoding="utf-8"
		omit-xml-declaration="no"
		doctype-public="-//W3C//DTD XHTML 1.1//EN"
		doctype-system="http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"
		indent="no"
		media-type="application/xhtml+xml" />

	<xsl:template match="statuses">
		<html xml:lang="ja">
			<head profile="http://www.w3.org/2003/g/data-view">
				<title>Xwitter</title>
				<link rel="transformation" href="http://www.kanzaki.com/parts/xh2rdf.xsl" />
			</head>
			<body>
				<xsl:apply-templates select="status" />
			</body>
		</html>
	</xsl:template>

	<xsl:template match="status">
		<div class="section" id="{generate-id()}">
			<xsl:apply-templates select="user" />
		</div>
	</xsl:template>

	<xsl:template match="user">
		<img src="{profile_image_url}" alt="" />
		<input type="hidden" title="{../id}" class="{../favorited}" />
		<input type="hidden" title="{../id}" class="reply" alt="{screen_name}" />
		<em><xsl:value-of select="screen_name" /></em>
		<p>
			<xsl:if test="protected = 'true'">
				<xsl:attribute name="class">protect</xsl:attribute>
			</xsl:if>
			<xsl:value-of select="../text" />
		</p>
	</xsl:template>

</xsl:stylesheet>
      • icons
        • default
    • defaults
      • preferences
        • prefs.js
pref('toolkit.defaultChromeURI', 'chrome://xwitter/content/main.xul');
pref('browser.dom.window.dump.enabled', true);
// pref('network.proxy.autoconfig_url', 'http://example.org/proxy.pac');
// pref('network.proxy.type', 2);