CSSのみで作る動的Webインタフェース

データベースから引き出した特定の項目を提示し、 修正が必要ならその場合のみ編集用のフォームを出すインタフェースを考える。

値提示と編集用フォーム

まずは単純なデータベースの値更新から考え、 それを実用的なものへと発展させていく。

実験用データベースの作成と更新手順

ただ1つのカラムを持つテーブル v を、データベースファイル val.sq3 に作成する。

sqlite3 val.sq3
CREATE TABLE v(val TEXT);

ここに4つの値、'foo'、'bar'、'foo'、'baz' を格納する。

INSERT INTO v VALUES('foo');
INSERT INTO v VALUES('bar');
INSERT INTO v VALUES('foo');
INSERT INTO v VALUES('baz');

'foo' が2重登録されているが、内部的には rowid の異なる行が格納される。

SELECT rowid,* FROM v;
1|foo
2|bar
3|foo
4|baz

さて、シェルスクリプトからの操作を単純化するため REPLACE INTO ... での値挿入と既存値更新の方法を考える。 2個目の 'foo' を更新したい場合は以下のように rowid を含めて REPLACE 文を使う。

REPLACE INTO v(rowid, val) VALUES(3, 'foo2');
SELECT rowid,* FROM v;
1|foo
2|bar
3|foo2
4|baz

同じ構文で新規の値を入れるには rowid に NULL を指定すればよい。

REPLACE INTO v(rowid, val) VALUES(NULL, 'foo');
SELECT rowid,* FROM v;
1|foo
2|bar
3|foo2
4|baz
5|foo

以降ではこのデータベースの特定の行の値を更新する CGI インタラクションについて説明する。

更新のためのフォーム

上記のデータベース中の rowid=3 の行のカラムの、 シェルスクリプト+CGIによる表示・修正・削除を考える。 シェル変数 rowid に 3が、 シェル変数 name にカラム名、シェル変数 val にその値が代入されているとする。 なお、いずれの変数の値もHTMLエスケープされているものとする。

3つの場合にそれぞれ違う出力が必要である。そうすると、データベース中の なんらかの値に対して「表示」、「編集」する場合や、 新たなレコードを「新規入力」したい場合に別々のページ出力が必要になる。 これは対話的操作の手順が増えることになる。これを避けて、単一のページで 3種の操作ができるようにしたい。

CSS3を利用した擬似動的対話操作

HTML文書に記述する A、B、C の3つのテキストがある。 これを条件つきでいずれか1つだけ表示するようにしたい。 このような場合に便利なのが、CSS3で利用できる :checked 擬似クラスと 一般兄弟セレクタ ~ である。まずは以下の例を見よう。 ~ (CSSセレクタ) :checked (CSS)

abc.html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Displaying A, B, C</title>
<style type="text/css">
<!--
span.edit, span.confirm {		/* 標準では完全透明で不可視 */
  opacity: 0.0; visibility: hidden;}
input[value="edit"]:checked ~ span.edit {/* value="edit" のボタンチェックで */ 
  opacity: 1.0; visibility: visible;}	/* 透明解除したうえで可視化する */
input[value="rm"]:checked ~ span.confirm {
  opacity: 1.0; transition: 3s; /* value="rm" のボタンチェックで透明解除 */
  visibility: visible;}		/* 不可視解除 */
input[value="rm"]:checked ~ span.value {
  background: red;}		/* value="rm" のボタンチェックで背景赤に */
input[value="edit"]:checked ~ span.value {
  display: none;}		/* value="edit" のボタンチェックで表示なしに */
-->
</style>
</head>

<body>
<p>
<form>
<input name="action" type="radio" value="keep">Aだけ出す
<input name="action" type="radio" value="edit">Bだけ出す
<input name="action" type="radio" value="rm">AとCを出す<br>
<span class="value">A</span>
<span class="edit">B</span>
<span class="confirm">C</span><br>
<input name="reset" type="reset" value="reset">
</form>
</p>

</body>
</html>

この HTML 文書をCSS3対応のブラウザで開いた場合の様子を示す。 ラジオボタンにチェックのない状態(または第1ラジオボタンをチェックした状態)では、 <span class="value"> で囲まれたものだけが見えている。

初期状態

Aだけ出す Bだけ出す AとCを出す
A

続いて2つ目のラジオボタンにチェックが入った場合は、 <span class="edit"> で囲まれたものだけが見える。

第2ラジオボタンをチェックした状態

Aだけ出す Bだけ出す AとCを出す
B

さらに、3つ目のラジオボタンにチェックが入った場合は、 <span class="value"> で囲まれたものが背景色赤になって現れ、 <span class="confirm"> で囲まれたものも現れる。

第3ラジオボタンをチェックした状態

Aだけ出す Bだけ出す AとCを出す
A C

この挙動は、出力HTMLの冒頭で定義されたCSS記述による。 この定義の要点を以下に示す。

label要素によるラジオボタンの操作性向上

input 要素によるチェックボックス(type="checkbox")や ラジオボタン(type="radio") をそのまま使うとクリックすべき○や□が小さいため操作しづらい。 ボタンにラベル文字列を結び付け、 文字列クリックでも対応するボタンのチェックを可能にできる。

リスト:abc.html の input 要素の並びを以下のように変更すると操作性が向上する。

<input id="action.keep" name="action" type="radio" value="keep"><label
 for="action.keep">Aだけ出す</label>
<input id="action.edit" name="action" type="radio" value="edit"><label
 for="action.edit">Bだけ出す</label>
<input id="action.cfm" name="action" type="radio" value="rm"><label
 for="action.cfm">AとCを出す</label>

input 要素に文書中で一意に定まる id を属性指定し、label 要素の for 属性にその id を設定することで結び付けられる。

表示・編集・削除・新規入力インタフェースの実装例

abc.html の仕組みを利用して、先述の実験用データベース val.sq3 の任意の行の値を操作できるものを作成した CGI スクリプト editv.cgi を示す。 行の指定はrowidで行なうものとする。

editv.cgi

#!/bin/sh
myname=`basename $0`
mydir=`dirname $0`
cd $mydir				# scriptと同じディレクトリに移動
DB=db/val.sq3 . $mydir/cgilib2-sh	# 使用DBファイルを db/val.sq3 に

if [ -n "$1" ]; then	# $1 はURL直打ちできるので数字でない場合も考慮する
  r=${1%%[!0-9]*}; r=${r##*[!0-9]}	# $1 から数字以外を除去
  # ★A★ 与えられた rowid でもう一度 rowid を取り直してみる
  rowid=`query "SELECT rowid FROM v WHERE rowid=$1;"`
fi
title=${rowid:+"Edit $rowid"}		# $rowid は "" か整数になる
title=${title:-"List"}			# $rowid が空なら "List" に
m4 -D_TITLE_="$title" -D_ACTION_="$myname" editv-head.m4.html	# 【1】

if [ -z "$rowid" ]; then		# 有効なrowid指定がなければ新規入力
  echo "<p>valの新規入力: <input type=\"text\" name=\"val\"></p>"
  #★B★
  val=`getpar val | sed "s/'/''/g"`	# SQLクォートする
  rid=`getpar rowid`			# hiddenで入力された場合
  rid=${rid%%[!0-9]*}; rid=${rid##*[!0-9]}	# 数字以外を除去
  case `getpar action` in	# ★C★ラジオボタン action の値で処理切り替え
    "")	# 新規入力
      [ -n "$val" ] &&		# $val が空でなければINSERT
	  query "INSERT INTO v VALUES('$val');" &&
	  echo "<p>New record '`echo \"$val\"`' inserted."
      ;;
    edit)
      [ -n "$rid" ] &&		# hidden指定のrowidレコードを更新
	  query "REPLACE INTO v(rowid, val) VALUES($rid, '$val');" &&
	  echo "<p>Update rowid($rid)=`escape \"$val\"`.</p>"
      ;;
    rm)
      [ -n "$rid" ] &&		# hidden指定のrowidレコードを削除
	  if [ x"`getpar confirm`" = x"yes" ]; then
	    query "DELETE FROM v WHERE rowid=$rid;" &&
		echo "<p>Delete rowid($rid).</p>"
	  fi ;;
  esac
  echo "<p>既存レコード一覧(クリックして編集):<br>"
  # ★D★		.mode html でのSELECT結果1個分は以下のようになる
  query<<-EOF  |			#  <TR><TD>1</TD>
	.mode html			-- <TD>データ</TD>
	SELECT rowid, val FROM v;	-- </TR>
	EOF
  # query結果が次のsedへの入力。sed操作でHTMLタグを外し、
  # <a href="$myname?$rowid">$rowid:$val</a> に置換する
  sed -e "/^<TR>/N;			# <TR>で始まる行と次の行を連結
	s/\n//;				# 連結行にある改行を削除
	s|^<TR><TD>\([0-9]*\)</TD>|<a href=\"$myname?\1\">\1:|
	s|<TD>\(.*\)</TD>$|\1</a> |;	# valのカラムからTDタグを外す
	/^<\/TR>$/d;			# </TR> のみの行を削除"
  echo "</p>"
else
  valfile=$tmpd/val.$$			# ★E★
  escape "`query \"SELECT val FROM v WHERE rowid=$rowid;\"`" > $valfile
  m4 -D_ROWID_="$rowid" \
     -D_VAL_="syscmd(\`cat $valfile')" editv-form.m4.html	# 【2】
fi
m4 editv-foot.m4.html			# 【3】 各閉じタグをまとめて出力

このスクリプトでは、m4 に与えるソースとして3つのファイルを使用している。

editv-head.m4.html editv-form.m4.html editv-foot.m4.html
editv.cgi
ヘッダ
フォーム (出力HTML)
フッタ

実際に動かした画面を示したあとで、プログラムの重要な点を解説する。

editv.cgi の稼動設定

実験用データベースの作成と更新手順」で作成した val.sq3 をこのCGIで利用する手順を示す。まず、val.sq3 を書き込み可能にする。

chmod a+w val.sq3
mkdir -m 1777 db
mv val.sq3 db

この例では val.sq3 ファイルを World Writable にしているが、httpd の動作プロセスのグループに合わせられるならば val.sq3 をそのグループに chgrp したうえで chmod g+w の方がよい。

httpd から書き込みできる状態にできたらブラウザを用い、この CGI の URL を開くと以下のような画面が現れる。

初期アクセス画面

List

valの新規入力:

既存レコード一覧(クリックして編集):
1:foo 2:bar 3:foo2 4:baz 5:foo

「1:foo」から続く部分は、rowid:val の並びとなっている。たとえば、「1:foo」をクリックすると次の画面に遷移する。

表示画面

Edit 1


1: foo

この画面から「○修正」にチェックを入れると値の表示が入力窓に変わる。

編集画面

Edit 1


1: 消去の場合の確認:

「○削除」にチェックを入れると赤背景の値の表示に変わると同時に、 削除確認のチェックボックスが現れる。

削除確認画面

Edit 1


1: foo 本当に消しますか(確認)

editv.cgi で使用する外部HTMLファイル

スクリプトから出力するまとまった量の HTML 文は別ファイルに置き、m4 コマンドでキーワード置換をからめて出力している。それが editv.cgi リスト中の【1】、【2】、【3】の部分である。 それぞれで読み込んでいるファイル editv-head.m4.html、editv-form.m4.html、editv-foot.m4.html を以下に示す。

editv-head.m4.html

Content-Type: text/html; charset=utf-8

<!DOCTYPE html>
<html lang="ja">
<head><title>_TITLE_</title></head>
<style type="text/css">
<!--
span.edit, span.confirm {/* 標準: 完全透明; 不可視  */
  opacity: 0.0; visibility: hidden;}
input[value="edit"]:checked ~ span.edit {	/* B の部分を見せる */
  opacity: 1.0; visibility: visible;}	/* edit ボタンチェックで透明解除 */
input[value="rm"]:checked ~ span.confirm {	/* C の部分を見せる */
  opacity: 1.0; transition: 3s; /* value="rm" のボタンチェックで透明解除 */
  visibility: visible;}
input[value="rm"]:checked ~ span.value {	/* A の部分を赤に変える */
  background: red;}		/* value="rm" のボタンチェックで背景赤に */
input[value="edit"]:checked ~ span.value {
  display: none;}		/* value="edit" のボタンチェックで表示なしに */
-->
</style>
<body>
<h1>_TITLE_</h1>
<form action="_ACTION_" method="POST">

editv-form.m4.html

<p>
<input id="action.keep" name="action" type="radio"
 value="keep"><label for="action.keep">温存</label>
<input id="action.edit" name="action" type="radio"
 value="edit"><label for="action.edit">修正</label>
<input id="action.cfm" name="action" type="radio"
 value="rm"><label for="action.cfm">削除</label><br>
_ROWID_:
<span class="value">_VAL_</span>				<!-- A -->
<span class="edit"><input type="text" name="val" value="_VAL_">	<!-- B -->
<input type="hidden" name="rowid" value="_ROWID_"></span>
<span class="confirm">本当に消しますか(確認)			<!-- C -->
<label><input type="checkbox"
 name="rm" value="yes">はい</label></span>

editv-foot.m4.html

<p><input type="submit" value="確定">
<input type="reset" value="リセット"></p>
</form>
</body></html>

editv.cgi の仕組み

SQLite3との入出力をシェルスクリプトで扱うための工夫の要点を示す。 リスト editv.cgi 中の★印で示した部分を取り上げる。

★D★ 既存レコード一覧の出力

データベースからの値の出力は HTML エスケープを不備なく行なうために sqlite3 の HTML 出力モードを利用する。値の一覧は横並びで得たいので sqlite3 の出力から <TR>、</TR>、<TD>、</TD> を除去する。同時に、レコードの rowid と val カラムの値を抽出し、

<a href="$myname?$rowid">$rowid:$val</a>

というリンク文字列に変換する作業を sed で行なう。 データベース中の1行分の出力例から加工過程を考える。 「SELECT rowid, val FROM v;」による 1行分の出力は以下のとおり。

<TR><TD>1</TD>
<TD>foo</TD>
</TR>

ここからrowidである「1」とvalである「foo」を取り出し、 1行のリンク文字列に変換する。sed の置換機能を用いて以下のように処理する。

/^<TR>/N 行頭が<TR>なら次の行を sed のパターンスペースに読み込む。つまり、入力例の1行目と 2行目を1つのsedバッファに読み込む。これによってバッファ文字列は パターンスペース(sed) Nコマンド(sed)
<TR><TD>1</TD>\n<TD>foo</TD>

となる。

s/\n// (上記Nによって結合された位置に入る)改行を削除する。 これによって1行目と2行目がくっつき、 sコマンド(sed)
<TR><TD>1</TD><TD>foo</TD>
のようになる。
s|^<TR><TD>\([0-9]*\)</TD>|<a href=\"$myname?\1\">\1:|
<TR><TD>整数</TD> というパターンを
<a href="$myname?整数">整数:
に置換する。ここまでの置換で上記の入力例は
<a href="$myname?1">1:<TD>foo</TD>
に置き換わる。
s|<TD>\(.*\)</TD>$|\1</a> |
<TD>文字列</TD> というパターンを
文字列</a>スペース
に置換する。以上の操作で上記の入力例は
<a href="$myname?1">1:foo</a>
に置き換わる。

ここで生成された「1:foo」というアンカー文字列をクリックすると、 同じCGIスクリプトが第1引数 "1" をともなって起動する。 そこで呼び出されるのが次の★A★の部分である。

★A★ $1によるrowidの取得と正常値確認

$1の値をシェル変数 r に代入したあと、

r=${1%%[!0-9]*}
r=${r##*[!0-9]}

の連続再代入によって0から9以外の文字を削除している。CGI プログラムでは利用者から与えられた情報は、 すべて悪意のある文字列だと仮定して処理する必要がある。 元々整数が指定してあればこの再代入でも値は変わらない。 整数であることを念押しした $r でデータベースを引き直す。

SELECT rowid FROM v WHERE rowid=$1;

では、条件「rowid=$1」に該当するものがあれば $1 自身が返り、なければ空文字列が返る。正しい roiwd 指定ならそれが次の★E★処理に継る。

★E★ 値と入力フォームの出力

ここに来る時点で $rowid にはデータベース中に存在するレコードの rowid が入っている。その行の val カラムの値をSELECT文で得て、 得た結果を、cgilib2 で定義されているシェル関数 escape でHTMLエスケープしておき一時ファイル($valfile)に保存しておく。 これをリスト editv-form.m4.html 内のキーワード _VAL_ の置換先文字列として m4 に置き換えさせる。 また、このときの $rowid の値を hidden で埋め込んでおく。 もし、図「表示画面」で 「確定」ボタンが押された場合には、 以下のような input の値が設定される。

input名
action 「○ 修正」にチェックの場合 → edit
「○ 削除」にチェックの場合 → rm
val修正後の入力値
rowid修正中のレコードのrowid
rm 「本当に消しますか(確認)」がチェックされていれば yes
いなければ空文字列("")

そして、実際の修正が行なわれるのは「確定」ボタンによって 起動されたスクリプトで、このとき処理が★B★の部分に進む。

★B★ rowidの取得と正常値確認

フォームで渡された値は cgilib2 で定義されているシェル関数 getpar で取得できる。

  val=`getpar val | sed "s/'/''/g"`
  rid=`getpar rowid`
  rid=${rid%%[!0-9]*}; rid=${rid##*[!0-9]}

val カラムの修正後の値を得て、それをのちのSQL文で利用するために、 文字列中のシングルクォートを2つに直したうえでシェル変数 val に代入している。また、シェル変数 rid は、$1 のときと同じ方針で rowid にふさわしい整数に矯正している。ただしここでは rowid の引き直しはしていない。のちに発行する UPDATE 文で rowid の値が存在しないものの場合には UPDATE そのものが失敗するだけで 他に影響が出ないからである。

そして実際にデータの修正が行なわれたときに起動したスクリプトは 次の★C★の部分に進む。

★C★ 更新・削除操作

ここに来るときは、スクリプトの第1引数($1)がない場合であり、 それは「初期アクセス画面」にて 新規入力をした場合か、「編集画面」あるいは 「削除確認画面」で「確定」が押された場合である。 後者の場合は input名 action に "edit" または "rm" の値が代入されている。 これを★C★のcase文で切り替える。

  1. "" の場合は編集画面からの新規入力なので、新たなレコードとみなし INSERT 文で処理する。

  2. "edit" の場合は編集画面からの値なので REPLACE 文で rowid と val を同時指定して値を更新する。

  3. "rm" の場合は削除確認画面からのものなので、 input名 rm のチェックボックスがチェックされ、値が "yes" になっている場合のみ DELETE 文で削除する。

練習問題: sqlite3出力からのsedタグ付加

sqlite3 から出力されたデータを sed によって加工し、リンクなどを付加する技巧は色々応用が効く。

次のようなCSVファイルをSQLiteデータベースにインポートする。

bookmarks.csv

title,url
"カットシステム, CUT System",http://www.cutt.co.jp/
東北公益文科大学,http://www.koeki-u.ac.jp/
YaTeX|野鳥,http://www.yatex.org/
SKIP <Shonai Koeki Information Project>,http://skip.koeki-prj.org/
sqlite3 -csv bookmarks.sq3
.import bookmarks.csv bookmarks

タイトル(title)、URL(url)の2項目を

<a href="URL">タイトル</a>

のような形式で、HTML文書 ul 要素内の箇条書形式(li)で一覧出力するスクリプト、 bookmarks.cgi を作成せよ。

練習問題: 解答例

sqlite3 コマンドからの出力を、 シェルスクリプトでカラムごとの値を確実に受け取るには

  1. 出力モードを html にして、td 要素を単位に受け取る。
  2. すべてのカラムを SQLite の hex() 関数で16進文字列化したものをシェルの read で読み取る。

などの方法が考えられる。それぞれを用いた解答例を示す。

sqlite3 -html からの処理

bookmarks テーブルを sqlite3 の html モードで取り出した出力は以下のようになる。

.mode html
.head 0		/* 箇条書にするのでヘッダ不要 */
SELECT * FROM bookmarks;
<TR><TD>カットシステム, CUTT System</TD>
<TD>http://www.cutt.co.jp/</TD>
</TR>
<TR><TD>東北公益文科大学</TD>
<TD>http://www.koeki-u.ac.jp/</TD>
</TR>
<TR><TD>YaTeX|野鳥</TD>
<TD>http://www.yatex.org/</TD>
</TR>
<TR><TD>SKIP &lt;Shonai Koeki Information Project&gt;</TD>
<TD>http://skip.koeki-prj.org/</TD>
</TR>

行の開始は<TR>で、カラムの開始は<TD>となっている。

だが、どの<TD>がどれに対応しているか明確になるように、 SELECT 時の取り出しカラム指定に番号をつけて印にする。

SELECT '1:'||title,
       '2:'||url
FROM bookmarks LIMIT 1;
<TR><TD>1:カットシステム, CUTT System</TD>
<TD>2:http://www.cutt.co.jp/</TD>
</TR>

図: ブックマーク1つ分の出力例

editv.cgiの仕組み」で述べた手法で sed による組み替えを行なう。

sedコマンド
(説明)
ブックマーク出力例の加工後
/^<T[RD]>/N (TRまたはTD開始行を次の行と連結) <TR><TD>1:カットシステム, CUTT System</TD> <TD>2:http://www.cutt.co.jp/</TD> </TR>
/^<\/TR>/d (</TR> 行を削除) <TR><TD>1:カットシステム, CUTT System</TD> <TD>2:http://www.cutt.co.jp/</TD>
s/\n//g (行内の改行文字を削除) <TR><TD>1:カットシステム, CUTT System</TD><TD> 2:http://www.cutt.co.jp/</TD>
s,<TR><TD>1:\(.*\)</TD><TD>2:\(.*\)</TD>, <li><a href="\2">\1</a></li>, (文字列置換) <li><a href="http://www.cutt.co.jp/">カットシステム, CUTT System</a></li>

表中 の部分は表示の都合上折り返しているもので、実際には1行である。

まとめると、ブックマーク出力のための加工の一例は以下のようになる(HTML 出力部分のみ示す)。

#!/bin/sh
echo '<ul>'
sqlite3 -html bookmarks.sq3 \
	"SELECT '1:'||title, '2:'||url FROM bookmarks;" \
    | sed -e"/^<T[RD]>/N" \
	  -e "/^<\/TR>/d" \
	  -e "s/\n//g" \
	  -e 's,<TR><TD>1:\(.*\)</TD><TD>2:\(.*\)</TD>,<li><a href="\2">\1</a></li>,'
echo '</ul>'

これにより以下のような出力が得られる。

箇条書形式のブックマーク出力例

<ul>
<li><a href="http://www.cutt.co.jp/">カットシステム, CUTT System</a></li>
<li><a href="http://www.koeki-u.ac.jp/">東北公益文科大学</a></li>
<li><a href="http://www.yatex.org/">YaTeX|野鳥</a></li>
<li><a href="http://skip.koeki-prj.org/">SKIP &lt;Shonai Koeki Information Project&gt;</a></li>
</ul>

sqlite3 + read による処理

sqlite3の標準カラム区切りは縦棒(|)であるので、カラム値読み取り処理を

sqlite3 DB SELECT文 \
 | while IFS='|' read col1 col2; do ... done

としたいところだが、例題データの3行目にある「YaTeX|野鳥」のように 縦棒そのものを含む値が存在するのでこの方法は使えない。 カラム値にカラム区切り文字が絶対に存在しないと仮定できないのであれば hex 関数で16進文字列化して取り出したものをシェル変数に読み取り、 あとで戻すと同時にHTMLエスケープする。

#!/bin/sh
unhexize() {
  perl -n -e 's/([0-9a-f]{2})/print chr hex $1/gie'
}
escape() {			   # HTMLエスケープ
  printf "%s" "$@" |
      sed -e '; s/\&/\&amp;/g' -e 's/"/\&quot;/g' -e "s/'/\&apos;/g" \
	  -e "s/</\&lt;/g; s/>/\&gt;/g"
}
echo '<ul>'
sqlite3 -separator ' ' bookmarks.sq3 \
	"SELECT hex(title), hex(url) FROM bookmarks;" \
    | while read htitle hurl; do
	title=$(escape "`echo $htitle|unhexize`")
	url=$(escape "`echo $hurl|unhexize`")
	cat<<EOF
<li><a href="$url">$title</a></li>
EOF
      done
echo '</ul>'

ここでは16進文字列を元のバイト並びに戻す処理に Perl を用いる例を示したが、xxd が使えるなら xxd -r -p でもよい。 出力結果は「sqlite3 -html」を利用したものと同じである。

参考文献

  1. World Wide Web Consortium. Selectors Level 3. http://www.w3.org/TR/selectors/
yuuji@koeki-u.ac.jp