Home

日記
WikiParserのサンプルWikiをアップデートしました (16:07)Edit

今までの実験をすべて盛り込んで、

  • 通常時は、PHP版のWikiParserを使ってパース
  • 編集中は、JavaScript版のWikiParserを使ってリアルタイムプレビュー
  • さらにAjaxを使って、定期的に編集中の内容をサーバーに保存

というバージョンに変えました。

ってやったところ、JavaScript版のパーサーにバグがあることが判明したんで、ソースはまだ載せてません(まあJavaScriptのソースは見れるけど)。もうちょいデバッグしてからまとめます。

ちなみにローカル環境では、保存間隔をもっと短く(ほぼリアルタイムに)して使ってたりしますけど、レンダリング結果の確認と保存の手間が省けると、Wikiはメモ環境としてなかなか快適。これで、KamiWikiくらい使いやすいインターフェース付きだったらもっと便利なんだろうけどな。

まあどうせそのうち.NETでデスクトップ版を作るから、そっちでUIは凝ればいいか。

Published At2005-04-14 00:00Updated At2005-04-14 00:00

日記
あーだるい (10:56)Edit

金、土曜日は社員旅行で箱根。昼から酒飲んで温泉に入って朝まで麻雀コース。翌日は帰りの電車の時間まで、坂だらけの箱根の街をうろうろして体力を消耗。日曜日にずいぶん寝たんだけど、いまだに体力が回復していない。だるだる。何もやる気がせん。

Published At2005-04-18 00:00Updated At2005-04-18 00:00

日記
tDiaryのアンカーリンク時にドキュメントタイトルを表示する (09:44)Edit

highlight.rbのhighlightElement関数の最後の行に、

document.title = highlightElem.innerHTML.replace(/<[^>]+?>/g, '');

なんてものを追加するだけ。

JavaScriptによるタイトルの書き換えなんで、SEO的には意味がないんだけど、最近のソーシャルブックマークサービスなんかでは、document.titleをJavaScriptで取得しているものが多いんで(ちなみにMM/Memoではやってないけど、del.icio.usとかはてなブックマークとかはそういうbookmarkletを用意していたはず)、そういう場合にはきちんとアンカーレベルのタイトルが拾われるようになって便利かも。

どちらかというと

if (highlightElem.tagName == 'H3') {
document.title = highlightElem.innerHTML.replace(/<[^>]+?>/g, '');
}

の方がいいかもしれない。そうしないとツッコミとかのアンカーでもタイトルが変わってしまう。

Published At2005-04-19 00:00Updated At2005-04-19 00:00

日記
アソシエイト広告ツール 機能追加 (16:05)Edit

asin=????という形でASINを直接付与することで、指定したASINの情報を取得できるようにしました。って、やったところで誰が使うのかって気もするけど、まあ作っておけばそのうち使い道も出てくるかも。

<script type="text/javascript" charset="euc-jp" src="http://1470.net/api/asin_cm.php?mode=js;associate=ishinao-22;asin=4091792758"></script>

なんて書くと、

なんて感じになるんですよ。CSSを独自定義したい場合は、class=myclassなんてつけると、標準のスタイル指定がなくなって代わりにdiv class="myclass"で囲まれるようになるんで、あとは独自CSS定義で適当に。

Published At2005-04-19 00:00Updated At2005-04-19 00:00

日記
BMediaNode: PHPプログラマの技量を知りたいとき (17:15)Edit

ちなみに私がPHPプログラマの技量を知りたい時には、ファイル数・構成は無制限で以下のような仕様のスクリプトを書いてもらいます。

で書かれている仕様のスクリプトを試しに書いてみた。所要時間は見直しを含めて30分くらいか。エラー処理に関する仕様は多少適当というか、解釈が違っているかも。

index.php

<?php
define('ACTION_URL', './index.php');
define('INTERNAL_ENCODING', 'UTF-8');
mb_internal_encoding(INTERNAL_ENCODING);
set_include_path('.');
main();
function main()
{
header('content-type: text/html; charset=' . INTERNAL_ENCODING);
switch ($_SERVER['REQUEST_METHOD']) {
case 'POST':
$zip = importZip();
$errorMsg = validateZip($zip);
if ($errorMsg) {
showInputForm($zip, $errorMsg);
} else {
if (isset($_POST['btnNext'])) {
showCheckForm($zip);
} elseif (isset($_POST['btnSave'])) {
saveZip($zip);
showResultPage($zip);
} else {
showInputForm($zip);
}
}
break;
default:
showInputForm();
break;
}
die;
}
function importZip()
{
$zip = isset($_POST['zip']) ? $_POST['zip'] : '';
$zip = trim(mb_convert_kana($zip, 'KVas'));
$zip = str_replace('-', '', $zip);
return $zip;
}
function validateZip($zip)
{
if ($zip == '') {return '入力されていません';}
if (!preg_match('|^[0-9]{7}$|', $zip)) {return '入力内容が正しくありません';}
return NULL;
}
function saveZip($zip)
{
// zipを保存
}
function showInputForm($zip = '', $errorMsg = NULL)
{
$actionUrl = ACTION_URL;
include('tmpl/form.tmpl');
}
function showCheckForm($zip = '')
{
$actionUrl = ACTION_URL;
include('tmpl/check.tmpl');
}
function showResultPage($zip = '')
{
$actionUrl = ACTION_URL;
include('tmpl/result.tmpl');
}
function h($str)
{
return htmlspecialchars($str);
}
?>

tmpl/form.tmpl

<form method="POST" action="<?=h($actionUrl) ?>">
郵便番号: <input type="text" name="zip" value="<?=h($zip) ?>" size="10">
※半角数値7桁で入力してください
<br>
<?php if (isset($errorMsg)) : ?>
<p><?=h($errorMsg) ?></p>
<?php endif ?>
<input type="submit" name="btnNext" value="次へ">
</form>

tmpl/check.tmpl

<form method="POST" action="<?=h($actionUrl) ?>">
<input type="hidden" name="zip" value="<?=h($zip) ?>">
郵便番号: <?=substr($zip, 0, 3) . '-' . substr($zip, 3) ?>
<br>
<p>上記でよろしいですか?</p>
<input type="submit" name="btnBack" value="戻る">
<input type="submit" name="btnSave" value="次へ">
</form>

tmpl/result.tmpl

入力された郵便番号は <?=substr($zip, 0, 3) . '-' . substr($zip, 3) ?>です。

HTMLヘッダ・フッタとかは省略。まあ必要ならば適当にテンプレートで補えばいいし。ソースの文字コードはUTF-8。スクリプトの頭で強制的にmb_internal_encodingをセットするのは、いまいち設定がどうなっているかわからないサーバーで動かすための生活の知恵。

って感じで、俺ならOOPは使わずに書くなー。というか、PHPの利点はこの程度のコードならば(1回こっきりスクラッチから書くならば)こんな感じで手軽に書けるところだと思うんだけど。

まあこういうコードしか書けない(クラスを使ったコードが理解できない&PEAR等を使いこなせない)ってんじゃ大規模なアプリケーションを書くときについて行けなくなるだろうけど。

というわけで、暇なPHPプログラマの方、この仕様で自分が書くならこう書く、というサンプルを書いてみませんか。ちなみにクラスをネームスペース代わり程度に使ったサンプルも試しに書いてみた。

index2.php

<?php
define('INTERNAL_ENCODING', 'UTF-8');
mb_internal_encoding(INTERNAL_ENCODING);
set_include_path('.');
main();
function main()
{
header('content-type: text/html; charset=' . INTERNAL_ENCODING);
$form =& new ZipForm();
$form->execute();
}
class ZipForm
{
var $_actionUrl;
var $_zip;
var $_errorMsg;
var $_formMode;
function ZipForm($options = array())
{
$this->__construct($options);
}
function __construct($options = array())
{
$this->_actionUrl = isset($options['actionUrl']) ?
$options['actionUrl'] : $_SERVER['PHP_SELF'];
$this->_zip = '';
$this->_errorMsg = NULL;
$this->_formMode = 'form';
}
function getZip()
{
return $this->_zip;
}
function getErrorMsg()
{
return $this->_errorMsg;
}
function getMode()
{
return $this->_formMode;
}
function execute()
{
switch ($_SERVER['REQUEST_METHOD']) {
case 'POST':
$this->import();
if ($this->isValid()) {
switch ($this->getMode()) {
case 'check':
$this->showCheckForm();
break;
case 'save':
$this->save();
$this->showResultPage();
break;
case 'input':
default:
$this->showInputForm();
break;
}
} else {
$this->showInputForm();
}
break;
default:
$this->showInputForm();
break;
}
}
function import()
{
$this->_zip = isset($_POST['zip']) ? $_POST['zip'] : '';
$this->_zip = trim(mb_convert_kana($this->_zip, 'KVas'));
$this->_zip = str_replace('-', '', $this->_zip);
if (isset($_POST['btnNext'])) {
$this->_formMode = 'check';
} elseif (isset($_POST['btnSave'])) {
$this->_formMode = 'save';
} elseif (isset($_POST['btnBack'])) {
$this->_formMode = 'input';
} else {
$this->_formMode = 'input';
}
}
function isValid()
{
$this->_errorMsg = NULL;
if ($this->_zip == '') {
$this->_errorMsg = '入力されていません';
} elseif (!preg_match('|^[0-9]{7}$|', $this->_zip)) {
$this->_errorMsg = '入力内容が正しくありません';
}
return !isset($this->_errorMsg);
}
function showInputForm()
{
$actionUrl = $this->_actionUrl;
$zip = $this->_zip;
$errorMsg = $this->_errorMsg;
include('tmpl/form.tmpl');
}
function showCheckForm()
{
$actionUrl = $this->_actionUrl;
$zip = $this->_zip;
$errorMsg = $this->_errorMsg;
include('tmpl/check.tmpl');
}
function showResultPage()
{
$actionUrl = $this->_actionUrl;
$zip = $this->_zip;
$errorMsg = $this->_errorMsg;
include('tmpl/result.tmpl');
}
function save()
{
// zipを保存するコードをここに書く
}
}
function h($str)
{
return htmlspecialchars($str);
}
?>

元の関数型コードをできるだけ流用しようとしたせいで、設計ポリシーがなんとも中途半端な感じだね。execute相当の処理を外出しにするかクラス内部で完結させるか迷った結果、キメラのようなインターフェースになっている。

もしちゃんと作るならば、表示系はクラスの中に含ませずに、クラス自体はDataObject的に作るだろうな。で、表示系は外部コード+テンプレートライブラリにお任せって感じで。

HTML_QuickForm+Smartyを使ったサンプルとかも書いてみようかと思ったけど、最近使ってないんでパス。最近は自前のフレームワークの中に安住してしまっているんで、他のフレームワークを使うのがだるいんだよなー。

Published At2005-04-19 00:00Updated At2005-04-19 00:00

日記
tDiaryのアンカーリンク時にドキュメントタイトルを表示する その2 (13:11)Edit

昨日のバージョンから諸々追加して、結局こうなった。

  • highlight.rb.diff
23,27d22
<
<         if (highlightElem.tagName == 'H3') {
<           var h1 = document.getElementsByTagName('H1')[0];
<           document.title = (highlightElem.innerHTML + ' - ' + h1.innerHTML).replace(/<[^>]+?>/g, '').substr(2);
<         }

tDiaryは必ずH1に日記タイトルってつくよね? ということを前提に、日記タイトルをけつに付けてみた。あと、段落アンカーってCSSで画像とかを表示することがあっても、実体としてのテキストはいつも「_」なんだよね? ってことで、H3のタイトルからタグを除去し、あたま2文字を削ってみた。

というわけで、JavaScriptオンな環境ならば、ここの段落アンカーをクリックすると「[tDiary][MM] tDiaryのアンカーリンク時にドキュメントタイトルを表示する その2 (13:11) - tdiary.ishinao.net」なんて感じになって、その状態でdel.icio.usとかはてなブックマークとかにbookmarkletを使って登録すると、そのタイトルがちゃんと登録されるわけですね。

ちなみにMM/Memoでもbookmarkletで現在ブラウザが表示しているタイトルを渡したい場合は、

memo!

なんて感じ? タイトルだけでなく、選択されている範囲があったらその部分をテキストとして送るようにもしてある。

Published At2005-04-20 00:00Updated At2005-04-20 00:00

日記
BMediaNode: PHPプログラマの力量を計る その2 (18:49)Edit

OOPってのは、要はMVCのことなのか。ってことで、MVCで書いてみた。modelをNULLで返すパターンはあんまり美しくないなー。空のmodelでも返した方がきれいか。でもまああまり深く考えることはやめておこう。

index3.php

<?php
main();
function main()
{
header('content-type: text/html; charset=UTF-8');
$ctrl =& new MyController();
$ctrl->run();
$model =& $ctrl->getModel();
$view =& $ctrl->getView();
$view->display($model);
}
class MyController
{
var $_model;
var $_view;
function run()
{
switch ($_SERVER['REQUEST_METHOD']) {
case 'POST':
$this->_model =& new MyModel();
switch ($this->_getMode()) {
case 'input':
$this->_view =& new MyViewInput();
break;
case 'check':
$this->_view =& new MyViewCheck();
break;
case 'result':
$this->_view =& new MyViewResult();
break;
}
break;
default:
$this->_view =& new MyViewInput();
break;
}
}
function _getMode()
{
if (isset($_POST['btnSave']) && $this->_model->isValid()) {
return 'result';
} elseif (isset($_POST['btnNext']) && $this->_model->isValid()) {
return 'check';
} else {
return 'input';
}
}
function &getModel()
{
return $this->_model;
}
function &getView()
{
return $this->_view;
}
}
class MyModel
{
var $_params = array();
var $_errors = array();
function MyModel()
{
$this->_init();
$this->_validate();
}
function _init()
{
$this->_param = array();
$zip = isset($_POST['zip']) ? $_POST['zip'] : '';
$zip = trim(mb_convert_kana($zip, 'KVas'));
$zip = str_replace('-', '', $zip);
$this->_params['zip'] = $zip;
}
function _validate()
{
$this->_errors = array();
if ($this->_params['zip'] == '') {
$this->_errors[] = '入力されていません';
} elseif (!preg_match('|^[0-9]{7}$|', $this->_params['zip'])) {
$this->_errors[] = '入力内容が正しくありません';
}
}
function getParams()
{
return $this->_params;
}
function isValid()
{
return count($this->_errors) == 0;
}
function getErrors()
{
return $this->_errors;
}
}
class MyViewInput
{
function display($model)
{
if (isset($model)) {
extract($model->getParams());
$errors = $model->getErrors();
} else {
$zip = NULL;
}
include 'templates/input.tmpl';
}
}
class MyViewCheck
{
function display($model)
{
extract($model->getParams());
include 'templates/check.tmpl';
}
}
class MyViewResult
{
function display($model)
{
extract($model->getParams());
include 'templates/result.tmpl';
}
}
function h($str)
{
return htmlspecialchars($str);
}
?>

テンプレートは、昨日のやつとはちょっと変わっているんだけど面倒くさいから省略。エラーが配列になって、actionのURLはテンプレート側で解決($_SERVER['PHP_SELF'])するようにしただけ。

より規模が大きいフォームとかに拡張することを考えたプロトタイプとして書くならこういう書き方もありかなー。ただ、これはもともとの仕様を満たすには適材適所を大幅にはみ出ている気がする。

まあ、頭の体操にはいいよね。多分人によっては、MVCの切り分け範囲が違ってくるんだろうな。

ああ

$ctrl->_getMode()が$ctrl->runの中のswitchの条件に依存している。危険危険。

Published At2005-04-20 00:00Updated At2005-04-20 00:00

日記
テーマを追加 (19:31)Edit

MM/Memoの個人ページで使えるテーマとして、Treetop、マーガレット、Digital Gadgets、カーテン、Orange Flower、お花見、Sleepy Kitten、ナトリウムランプを追加。っつーか、Sleepy Kittenを使ってみたかっただけ。

Published At2005-04-20 00:00Updated At2005-04-20 00:00

日記
tDiaryのアンカーリンク時にドキュメントタイトルを表示する その3 (14:43)Edit

段落アンカーの頭は必ずしも1文字ではないようなんで、最初の半角スペースまでを除去するように変更。あと、日記タイトルはh1から取得するのはやめて、tDiary側でJavaScriptに埋め込むようにしてみた。

23,27d22
<
<         if (highlightElem.tagName == 'H3') {
<           var title = "#{@conf.html_title}(#{@date.strftime('%Y-%m-%d')})";
<           document.title = (highlightElem.innerHTML + ' - ' + title).replace(/<[^>]+?>/g, '').replace(/^[^ ]+ (.*)/, '$1');
<         }

これだともしも@conf.html_titleに「"」が含まれているような場合にまずかったりするんだけど、なんか上記の文脈で「"」を適切にエスケープする方法が良くわからなかったんでパス。@conf.html_title.tr('"', '\\"')だとダメなんだよなー。

Rubyのヒアドキュメントと式展開が入れ子になっていて、さらにその結果をJavaScriptの文字列に埋め込む場合、適切なエスケープはどう書くのが正解でしょう?

ネタがかぶっていた

「最初の半角スペースまでを除去する」というネタがかぶっていた

replace(/[^ ]* /, '')

の方がスマートな表現か。

ちなみにdocument.titletitleタグは、title_tag.rbを使っている場合は、その日のすべての段落タイトルが含まれてしまうんで、日記タイトル取得用には使えないんですよねー。

Published At2005-04-21 00:00Updated At2005-04-21 00:00

日記
MM/Memoでも一応対応コードをつけてみた (15:10)Edit

MM/Memoの場合はTypeKey認証を使っている関係上、直接アプリケーションからPOSTするようなツールは使えない(ものすごく頑張れば使えなくもないけど)から、こういうチェックを導入したせいで、その手のツールが使えなくなるという人はいないよね? ブラウザ上でbookmarkletとか使ってGETでパラメータ渡ししている場合は問題ないし。

ちなみにチェック処理の内容は、フォームを生成するときに、

$check_key = md5([ユーザーID] , [秘密の文字列]);

で生成したチェックコードを、

<input type="hidden" name="check_key" value="<?=h($check_key) ?>">

なんて感じで埋め込んでおき、POSTされたときに、

if ($_POST['check_key'] != md5([ユーザーID] , [秘密の文字列])) {
die('認証失敗');
}

しているだけね。認証キーはユーザーIDから生成しなくても、まったくランダムに作ってセッションに保存しておいてもいい。

それにしても、これからの公開Webアプリはみんなこういう対策を標準で取らないとだめなんだろうなー。これから作るフォーム周りのライブラリには、こういう機能を標準で持たせておくべきか。

あ、できればリファラーチェックで弾くような実装はやめてね。俺は基本的にリファラーオフが標準だし。

Published At2005-04-21 00:00Updated At2005-04-21 00:00