タグ別アーカイブ: HTML5

Search RegexプラグインでWordPress投稿を正規表現検索置換

※今回の記事の内容、特に自己責任で試してね。投稿記事の破損についてなど一切の責任は負いません。

そもそもの問題

過去にこのような投稿を上げていた。

小ネタ:WordPressがHTML5に対応して、過去記事の見出しに困る

WordPressがHTML5に対応した際、それまで記事部分での見出しレベルトップはh3であったのが、h2に繰り上げとなった。その影響で過去投稿をいちいち引っ張り出してきて、手動で修正するのが面倒だよねという話。
それに対して、見出しレベルを変更するこんなプラグインもありますよと紹介していたのがこの投稿。

WordPressテーマ変更に伴う記事のルート見出しレベルの変動を吸収するプラグイン WP hn Convert

記事内でも書いている通り、このプラグインを使ってWordPress関数the_content()をリプレースする形だと、以降も見出しトップレベルをh3にして投稿をし続けなければならず、この時は採用を断念していた。結局、見出しトップレベルの問題は全投稿の見出しを手動で書き換えることで解決した。

そんな貴方にオススメ。Search Regexプラグイン!

3年くらい経って、解決法に気付いた。全投稿を正規表現を用いて検索・置換するプラグインがあれば、この問題は容易に解決することが出来たのではないだろうか。そういう痒いところに手が届くプラグイン、きっとあるはず。
ありました。それがSearch Regexプラグイン。早速インストールして、具体的な指定方法を考えてみよう。

こんな感じで、インストール後は"ツール"メニューに出現

こんな感じで、インストール後は”ツール”メニューに出現

まず何より、左下にあるRegexチェックボックスにチェックを入れよう。これに気付かず、しばらく格闘してしまった(ココの表示がボールドになっているの、おかしくない?)。Search patternフィールドに検索文字列を、Replace patternフィールドに置換文字列を入力する。Replaceボタンを押すと、置換例を表示してくれるけれどデータベースに反映はしない。Replace & Saveボタンは押すと取り返しのつかないボタン。データベースに即時反映するヤツなので、前者のボタンで置換例を見て微調整しつつ、上手くいっているようだったら後者のボタンを押すようにしよう。

正規表現を使って見出しレベルを一段ずつ上げる

では実際にやってみよう。見出しレベルが一段ずつ上がるので、たとえば先にh6を置換してh5にして…というように下からやっていってしまうと、最終的に全部h2になってしまう(笑)。h3をh2に、その次にh4をh3に…という順序で手をつけよう。

 
Search pattern:/<h3>(.*?)<\/h3>/
Replace pattern:<h2>$1</h2>

このようなパターンで、h3の見出しが見出し語そのままh2に昇格する。Replace pattern側の$1というのが参照文字(preg_replaceの解説なども参照)で、h3タグに挟まれた文字を一旦預かり、置換パターン側に展開してくれる。もし預かる箇所が2ヶ所以上だったら、$2、$3…という形で指定が出来る。ちなみに$0にはh3タグも含んだ文字列全体が格納されている。

Search Regexによる置換イメージ

Search Regexによる置換イメージ

この置換を、次はh4…とどんどんやっていけば良い。

自サイトの内部リンクからtarget=”_blank”を外す

次の課題。昔のWEB制作ではひたすらリンクにtarget=”_blank”を付与していたものである。とりあえず付けられるなら付けておくか、といった感覚で、WordPressのエディタでもチェックボックス1つで付けられてしまうものだからかかさず付けていた。
それが時代の変遷とともに訪問者の閲覧環境がモバイルデバイス中心になると、モバイルブラウザ上でウィンドウやタブを何枚も開くことは難しく、即ユーザビリティの低下となるため、頻繁にtarget=”_blank”を使うサイトは忌避されユーザの離脱率が高くなるという問題が顕在化した。そのため、最近は内部リンクについてはtarget=”_blank”をつけないページ作りが正解になっているらしい。
では、過去の投稿の内部リンクからtarget=”_blank”をはがすパターンについて考えてみよう。

 
Search pattern:/<a\shref="http:\/\/akisi\.tabiyaku\.net([^>]*?)\starget="_blank">/
Replace pattern:<a href="http://akisi.tabiyaku.net$1>

このサイトのアドレスがhttp://akisi.tabiyaku.netから始まるので前方一致に入れているわけで、自身のサイトで流用する場合は適宜自サイトのアドレスに変えて欲しい。また、このパターンはtarget=”_blank”がaタグの最後に来るという性善説で考えているので、流用の際にはそこも注意である。

その他Search Regexの応用例として、Tabnabbing防止にtarget=”_blank”付の外部リンクに自動的にrel=”noopener”を付ける処理を出そうかとも思ったのだけれども、なんだか分岐の可能性が多そうなのでやめました(元々rel属性に何かの値が指定されていた場合など)。我こそはと思う方はやってみて、上手いやり方を教えて下さいな。


W3CのHTML5.1勧告でsection直下h1の見出しレベル自動解釈が無効に?

一応結論だけ先に書いておくと、WEB制作上の影響はほぼ無いのでは?という話。

HTML5と見出しレベルの解釈

HTML5が世に放たれた際に、それまでのHTML4.01ないしXHTML1.0の宣言を行ったページでは不可能であったアウトラインの作り方として、セクション毎にh1を頂点とした独立した見出し階層を作り上げても、ページ全体で自動的に適切な見出しレベルに調整して解釈してくれるというものがあった。
たとえば、下のようなコード。

<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/base.css" type="text/css">
<meta name="description" content="サンプル株式会社の商品一覧ページです。">
<meta name="keywords" content="サンプル株式会社,ネットショップ">
<title>当社商品の一覧 | サンプル株式会社</title>
</head>
<body>
<div id="wrapper">
<header>
<h1>サンプル株式会社</h1>
</header>
<div id="main">
<article>
<h1>サンプル株式会社の商品一覧</h1>
<p>当社が取り扱う商品の一覧です。</p>
<ul id="itemlist">
<li>
<article>
<h1>商品A</h1>
<p>さわやかなミントの香りの牛車です(牛は食べられません)</p>
<p>100,000円</p>
<p><a href="http://xxxxxxxx.xxx/item.php?id=1" title="商品Aの商品詳細">>>商品詳細へ</a></p>
</article>
</li><li>
<section>
<h1>商品B</h1>
<p>官位です。これがあれば思いのまま(返品不可)</p>
<p>3,000円</p>
<p><a href="http://xxxxxxxx.xxx/item.php?id=2" title="商品Bの商品詳細">>>商品詳細へ</a></p>
</section>
</li><li>
<section>
<h1>商品C</h1>
<p>古今和歌集です。新版が出たので在庫一掃セールです</p>
<p>16,000円</p>
<p><a href="http://xxxxxxxx.xxx/item.php?id=10" title="商品Cの商品詳細">>>商品詳細へ</a></p>
</section>
</li><li>
<section>
<h1>商品D</h1>
<p>商品説明</p>
<p>100円</p>
<p><a href="http://xxxxxxxx.xxx/item.php?id=15" title="商品Dの商品詳細">>>商品詳細へ</a></p>
</section>
</li></ul>
</article>
<article>
<h1>当社のモットー</h1>
<p>適当な仕事。深刻な社会的被害。</p>
</article>
</div>
<footer>
<p>サンプル株式会社 2012-2017 all rights reserved.</p>
</footer>
</div>
</body>
</html>

このコードを、htmlコードを入力すると自動的に見出しの階層構造を抽出してくれるWEBサービスHTML 5 Outlinerに与えると次のような結果が帰ってくる。

3階層のアウトライン

3階層のアウトライン

つまり各所で指定した見出しレベル(h1)以外にも、sectionタグやarticleタグなどのセクショニング・コンテンツを示すタグを参考に見出し階層を推測してくれているのだ。これが大体のブラウザや検索エンジンの解釈となる。
一方、従来的なhtmlのように、明示的に見出しレベルを指定(h1,h2,h3)しても同様の結果が帰ってくる。

<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/base.css" type="text/css">
<meta name="description" content="サンプル株式会社の商品一覧ページです。">
<meta name="keywords" content="サンプル株式会社,ネットショップ">
<title>当社商品の一覧 | サンプル株式会社</title>
</head>
<body>
<div id="wrapper">
<header>
<h1>サンプル株式会社</h1>
</header>
<div id="main">
<article>
<h2>サンプル株式会社の商品一覧</h2>
<p>当社が取り扱う商品の一覧です。</p>
<ul id="itemlist">
<li>
<article>
<h3>商品A</h3>
<p>さわやかなミントの香りの牛車です(牛は食べられません)</p>
<p>100,000円</p>
<p><a href="http://xxxxxxxx.xxx/item.php?id=1" title="商品Aの商品詳細">>>商品詳細へ</a></p>
</article>
</li><li>
<section>
<h3>商品B</h3>
<p>官位です。これがあれば思いのまま(返品不可)</p>
<p>3,000円</p>
<p><a href="http://xxxxxxxx.xxx/item.php?id=2" title="商品Bの商品詳細">>>商品詳細へ</a></p>
</section>
</li><li>
<section>
<h3>商品C</h3>
<p>古今和歌集です。新版が出たので在庫一掃セールです</p>
<p>16,000円</p>
<p><a href="http://xxxxxxxx.xxx/item.php?id=10" title="商品Cの商品詳細">>>商品詳細へ</a></p>
</section>
</li><li>
<section>
<h3>商品D</h3>
<p>商品説明</p>
<p>100円</p>
<p><a href="http://xxxxxxxx.xxx/item.php?id=15" title="商品Dの商品詳細">>>商品詳細へ</a></p>
</section>
</li></ul>
</article>
<article>
<h2>当社のモットー</h2>
<p>適当な仕事。深刻な社会的被害。</p>
</article>
</div>
<footer>
<p>サンプル株式会社 2012-2017 all rights reserved.</p>
</footer>
</div>
</body>
</html>
やはり3階層のアウトライン

やはり3階層のアウトライン

この場合sectionやarticleなどのセクショニング・コンテンツを示すタグは、見出し階層の推測において無視されている。
HTML5では、このようにセクションを表すタグを使って見出しレベルを推測させることもできるし、従来のhtmlのようにあくまで明示された見出し階層のみを推測の根拠とさせることもできる。

W3CのHTML5.1勧告で自動解釈は撤回

そのような状況であったのだが、2016年11月1日に公開されたW3CのHTML5.1勧告で、様々なタグや属性の追加とともにある1つの仕様の削除について言及されていた。

The use of nested section elements each with an h1 to create an outline.

これはつまり、sectionやarticleのようなセクショニング・コンテンツを示すタグは、ページ内の最上階層で使われるもの以外、h1を頂点とした見出しレベルを構成してはいけなくなったということである。
問題をより簡潔に表現するならば、ページ内でh1は1回しか使うなということ。HTML5の登場以前からコーディングを行っている人間には常識のことであるし、登場以降もh1の濫用には何かペナルティがあるのではないかという認識の人間も多かったろう。彼らの時代が再びやって来たのだ。さあ新人コーダをいじめよう!

W3C勧告が当面のWEB制作上影響が無い理由

W3Cと言えば、WEB業界で使われる技術の標準化を行う団体で、HTML5が世に出たての頃など、ドキュメントも少ないので何か分からないタグの使用方法などあったらとにかくW3Cのページを見に行ったものである。そのW3Cのお墨付きがあるのだから、これからブラウザや検索エンジンの解釈も今回の勧告を基準にしたものに変わるのではないかということで、HTML5の登場とともにh1濫用型のコーディングに切り替えた私自身もビックリした(もう納品しちゃってるし…)。
ただ今回の経緯などを調べるうちに分かってきたのだが、確かにHTML5登場当時にW3CはWEB標準化の最前線と言ってほぼ間違いない団体であったが、それは次世代標準の提唱に頓挫してWHATWGという別の団体が提唱していたHTML5を全面的に受け容れることにした経緯によるもので、以降WHATWGとW3Cは協調路線を取っておらず、主要ブラウザなどの実装はWHATWGの提唱するHTML Living Standardのみを基準にしているようなのである(まあ実際、WHATWGの正体がブラウザベンダーの寄り合いのようなものなので当たり前である)。
ではW3C勧告が何の意味を持っているのかというと、実質何の意味も持たない。WHATWG側からHTML Living Standardの剽窃だとか欠陥フォークだとか、現在進行形で厳しい評価を受け続けている。

ということで、ページのアウトラインの形成においてHTML5.1勧告が世に出た後も依然2通りのやり方が使えるのは間違いない。一応、見出しレベルをタグの数字で明示するやり方だとW3Cの要求も満たすことが出来るということが両者の違い。セクション毎にh1を置くやり方はコンテンツの追加時に見出しレベルを気にせず追加できるというメリットがあるし、タグの数字で明示するやり方はソースコードを人間が見て一目で階層が分かり易い(ただし実際にどう解釈されているかを反映していない)というメリットがある。お好きな方でこれからも大丈夫。


Conceptual:WEBサービスページの共通部分をtwigテンプレート化してみる

twigの実践編というわけでもないが、これまでConceptualのページに載せていたWEBサービスページの共通部分(ヘッダー、広告、フッターなど)をtwigテンプレートとして抽出する作業を行ったので、その過程など紹介してみようと思う。

共通部分の抽出

公開しているプログラムのうち、OnlyCSViewEXIFviewerについてはページ構造がほぼ同じだ。現在の2プログラムの画面を並べてみると、下の画像のような感じになる。共通部分と書いてある所はほぼソースが同じであり、真ん中の白い部分(コントローラ部分)とスクリプトの部分だけが違うということになる。

共通部分を持つ2ページ

共通部分を持つ2ページ

テンプレートエンジンで共通部分を抽出する場合、Smarty2.xのような”閉じhead前切り”パラダイムのものだと、ヘッダー部分、メイン部分、フッター部分と必要ファイルが3つになってしまうだろう。今回はテンプレートの継承機能のあるTwigを用いるため、ヘッダーとフッターを含んだ基礎部分のテンプレート(base.html.twig)にメイン部分に対応するブロックを作り、子テンプレートでブロック部分をオーバーライドするという、2ファイルでの解決を図ることにする。

ロジックとビューの分離

加えて、今回手を入れるプログラムは相当昔に作ったものなので、PHP入門書の最初のサンプルにありがちな、ロジックとビューが混在している状態になっている。テンプレートエンジンらしく、これの分離も同時に行ってみたい。

EXIFviewer手入れ前のソース

ロジックとビューの分離の説明に丁度良かったので、今回例として出すのはEXIFviewerの方にする。

 
//手入れ前index.php(1ファイル)
 
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="Description" content="EXIFviewerはローカルのJPEGファイルをアップロードすることなくサイズ、位置情報、撮影情報等のEXIFデータを確認できるWEBサービスです。">
<meta name="Keywords" content="PHP,EXIF,WEBサービス,JPEG,画像,位置情報">
<link rel="stylesheet" href="base.css" type="text/css">
.
.
<script>
$(function(){
$("#filesel").change(function(){
var reader = new FileReader();
reader.readAsDataURL(document.getElementById("filesel").files[0]);
reader.onload = function(e){
$("#path").val(e.target.result);
}
if(window.createObjectURL){
var ourl = window.createObjectURL(document.getElementById("filesel").files[0]);
} else if(window.URL) {
var ourl = window.URL.createObjectURL(document.getElementById("filesel").files[0]);
} else {
var ourl = window.webkitURL.createObjectURL(document.getElementById("filesel").files[0]);
}
$("#viewer").empty().html('<img src="' + ourl + '">');
$("#blob").val(ourl);
});//end of fileselchange
 
//Googlemap関連コード(省略)
 
});//end of jQueryready
</script>
<title>EXIFviewer JPEGファイルのEXIF情報をチェックするWEBサービス</title>
.
.
</head>
<body>
<div id="main">
	<nav><div id="back"><a href="http://akisi.tabiyaku.net/" title="AkisiのWEB制作日記に戻る">AkisiのWEB制作日記に戻る</a></div></nav>
<div id="upperad">
<script type="text/javascript">
 
//adsenseコード
 
</script>
</div>
<div id="instruction">
<h1>セットした画像のEXIFデータを表示</h1>
</div>
<div id="controlls">
<form>
<p><input type="file" id="filesel" type="image/jpeg"></p>
</form>
<form action="index.php" method="post">
<div id = "viewer">
<?php
if(!empty($_POST["path"])){
echo '<img src="' . $_POST["path"] . '">';
}
?>
</div>
<input type="hidden" name="blob" id ="blob">
<input type="hidden" name="path" id ="path">
<p><input type="submit" value="表示" class="btn btn-primary"></p>
</form>
</div>
<div id="tablearea">
<?php
if(!empty($_POST["blob"])){
if($exif = exif_read_data($_POST["blob"],NULL,true)){
echo "<h2>EXIF情報</h2>";
echo "<table class='exiftable table table-striped'><thead><tr><th>キー名</th><th>値</th></tr></thead><tbody>";
foreach ($exif as $key => $section) {
    foreach ($section as $name => $val) {
	if(is_array($val)){
	foreach ($val as $valname => $vval) {
	echo "<tr><td>" . htmlentities($key,ENT_QUOTES,"UTF-8") . ' ' . htmlentities($name,ENT_QUOTES,"UTF-8") . ' ' .  htmlentities($valname,ENT_QUOTES,"UTF-8") . "</td><td id=" . '"' . htmlentities($name . $valname,ENT_QUOTES,"UTF-8") . '">' . "$vval</td></tr>";
	}
	} else {
	echo "<tr><td>" . htmlentities($key,ENT_QUOTES,"UTF-8") . ' ' . htmlentities($name,ENT_QUOTES,"UTF-8") . "</td><td id=" . '"' . htmlentities($name,ENT_QUOTES,"UTF-8") . '">' . htmlentities($val,ENT_QUOTES,"UTF-8") . "</td></tr>";
	}
    }
}
echo "</tbody></table>";
} else {
echo "画像のEXIFデータが見つかりませんでした。";
}
} else {
echo "画像のEXIFデータが見つかりませんでした。";
}
?>
</div>
<div id="googlemapdivarea">
<div id="googlemapdiv"></div>
</div>
<div id="lowerad">
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
</div>
</div>
</body>
</html>

いわゆる、初回アクセス時にはPOSTの値(”blob”と”path”)が空であるため例外処理として初期画面を吐いているタイプのページだ。1ファイルでまとまってくれているので扱い易いが、echo命令によってテーブルを構成するhtmlタグの細切れを吐いている部分は一見して訳が分からない。html修正時にミスも起こり易いだろう。

EXIFviewer手入れ後のソース

index.phpにはテンプレートの呼び出しとロジックを押し込み、base.html.twigとindex.html.twigの2ファイルに値だけ渡す

 
//index.php
 
require_once("Twig/Autoloader.php");
Twig_Autoloader::register();
$loader = new Twig_Loader_Filesystem("twigtmp/");
$twig = new Twig_Environment($loader, array("cache" => "cache/"));
$blob = "";
$exif = array();
if(isset($_POST["blob"])){
$blob = $_POST["blob"];
}
if(!empty($_POST["path"])){
$exif = exif_read_data($_POST["path"],NULL,true);
}
$page = array(
"description" => "EXIFviewerはローカルのJPEGファイルをアップロードすることなくサイズ、位置情報、撮影情報等のEXIFデータを確認できるWEBサービスです。",
"keywords" => "PHP,EXIF,WEBサービス,JPEG,画像,位置情報",
"title" => "EXIFviewer JPEGファイルのEXIF情報をチェックするWEBサービス",
"blob" => $blob,
"exif" => $exif
);
$template = $twig->loadTemplate("index.html.twig");
$template->display($page);

続いて呼ばれる側のテンプレート2ファイル(実際名指しされるのはindex.html.twigの方だけだけれど)。

 
<!-- base.html.twig -->
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/base.css" type="text/css">
{% if description is not empty %}
<meta name="description" content="{{ description }}">
{% else %}
<meta name="description" content="AkisiのWEB制作日記の提供するウェブサービス">
{% endif %}
{% if keywords is not empty %}
<meta name="keywords" content="{{ keywords }}">
{% else %}
<meta name="keywords" content="ウェブサービス,WEB">
{% endif %}
.
.
<title>{{ title }}</title>
{% block head %}
{% endblock head %}
.
.
</head>
<body>
<div id="main">
	<nav><div id="back"><a href="http://akisi.tabiyaku.net/" title="AkisiのWEB制作日記に戻る">AkisiのWEB制作日記に戻る</a></div></nav>
<div id="upperad">
<script>
 
//adsenseコード
 
</script>
</div>
{% block main %}
{% endblock main %}
<div id="lowerad">
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
</div>
</div>
</body>
</html>
 
<!-- index.html.twig -->
{% extends "base.html.twig" %}
{% block head %}
<link rel="stylesheet" href="css/index.css" type="text/css">
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>
<script type="text/javascript">
$(function(){
 
$("#filesel").change(function(){
var reader = new FileReader();
reader.readAsDataURL(document.getElementById("filesel").files[0]);
reader.onload = function(e){
$("#path").val(e.target.result);
}
if(window.createObjectURL){
var ourl = window.createObjectURL(document.getElementById("filesel").files[0]);
} else if(window.URL) {
var ourl = window.URL.createObjectURL(document.getElementById("filesel").files[0]);
} else {
var ourl = window.webkitURL.createObjectURL(document.getElementById("filesel").files[0]);
}
$("#viewer").empty().html('<img src="' + ourl + '">');
$("#blob").val(ourl);
});//end of fileselchange
 
//Googlemap関連コード(省略)
 
});//end of jQueryready
</script>
{% endblock head %}
{% block main %}
<div id="instruction">
<h1>セットした画像のEXIFデータを表示</h1>
</div>
<div id="controlls">
<form>
<p><input type="file" id="filesel" type="image/jpeg"></p>
</form>
<form action="index.php" method="post">
<div id = "viewer">
{% if blob is not empty %}
<img src="{{ blob }}">
{% endif %}
</div>
<input type="hidden" name="blob" id ="blob">
<input type="hidden" name="path" id ="path">
<p>
<span><input type="submit" value="表示" class="btn btn-primary"></span>
</p>
</form>
</div>
<div id="tablearea">
{% if exif is not empty %}
<h2>EXIF情報</h2>
<table class='exiftable table table-striped'><thead><tr><th>キー名</th><th></th></tr></thead><tbody>
{% for key, section in exif %}
{% for name, val in section %}
{% if val is iterable %}
{% for valname, vval in val %}
<tr><td>{{ key }} {{ name }} {{ valname }}</td><td id="{{ name }}{{ valname }}">{{ vval }}</td></tr>
{% endfor %}
{% else %}
<tr><td>{{ key }} {{ name }}</td><td id="{{ name }}">{{ val }}</td></tr>
{% endif %}
{% endfor %}
{% endfor %}
</tbody></table>
{% else %}
画像のEXIFデータが見つかりませんでした。
{% endif %}
</div>
<div id="googlemapdivarea">
<div id="googlemapdiv"></div>
</div>
{% endblock main %}

テンプレートのcssは、共通部分のためのbase.cssと、index.cssに分ける。手入れ前のソースでは色付きハイライトされているPHP部分がなくなり、その代わりにtwigのfor inで配列を回している。テンプレート関係で新しく登場したのは、for inで回す際にキー値にもアクセスする場合の、{% for key, value in array %}という書き方。それから、配列に要素があるかどうか調べるのには{% if array is iterable %}のように書く、といったことくらいだろうか。
テンプレートの使い分け判断に使う値が変数の中にあるようであれば(あるいは変数自身の存在で判断できるのであれば)変数のみをそのままテンプレートに渡し、そうでないようならロジック側で切り替え判断のためのフラグを値で用意し、変数とフラグを合わせてテンプレートに渡す、このようにしてロジックとビューの分離をはかるわけである。

あとは、テンプレート中のJavaScriptに波括弧が含まれる場合{% raw %}{% endraw %}で囲むのを忘れずに。

TwigをLinuxの公開サーバに上げる段になってハマり易いこと

Twigで個人的にハマったこと。MAMPなどのローカル環境で動作したプログラムを公開サーバにアップロードしてみると、以下のようなエラーが出て動かないということがあった。

Class ‘Twig_Loader_Filesystem’ not found

原因は、ざっくり言ってしまうとLinuxが大文字小文字の扱いにうるさいからだった。たとえばAutoloader.phpへのパスが”twig/Twig/Autoloader.php”だった場合、”Twig/Twig/Autoloader.php”というように指定してしまうと、OSXやWindowsなどの環境では理解してくれるものの、Linuxサーバ上では理解をしてくれない。
また、Autoloader.phpの上位のディレクトリ名も、”Twig”でないといけない。思う所あって”twig”というように小文字のディレクトリにしてアップロードしていたから、”twig/Autoloader.php”という正しいパスを指定してもエラーが出てしまっていることに当惑した。”Twig”ディレクトリは勝手にリネームしてはいけない。それが今回の失敗の教訓。


WordPressテーマ変更に伴う記事のルート見出しレベルの変動を吸収するプラグイン WP hn Convert

随分前に愚痴った、WordPressのテーマを変更すると記事のルート見出しレベルが変動し、過去記事資産が活かせないという問題。この問題がまだ根本的解決に至っておらず、古いWordPressサイトを新しいテーマに移行する際には常に悩まされている。
公式がTwenty Twelveを出してきて以降は、テーマ制作者の暗黙の了解として記事タイトルがh1、記事内で最もレベルの高い見出しがh2というように揃えてきているように思う。これは記事に対してHTML5のセマンティックを結びつける場合、記事部分で新しくセクショニングコンテンツ(article)を始めるというのが合理的だからであろう。あるいは、SEO対応を売りにするWordPressテーマなどでは、single.phpにおける最初のh1を記事タイトルに充てているため、不都合なく記事資産を共有できるようになっているのである。

the_content()をラッピングする方法

一方、記事を出力するタイミングで見出しにテキスト処理を噛ませるという方法があり、こちらはWP hn Convertというプラグインを配布されている方がいる
このプラグインを導入し、テーマ中のthe_content()の記述部分を全てthe_hn_converted_content(数字)とすると、数字で指定した分だけ見出しレベルを移動してくれる。たとえば記事タイトルをh2にして書かれたテーマをh1からにするには、the_hn_converted_content(-1)のようにすると、以下の見出しh3をh2、h4をh3というように全体的な見出しレベルに変動を与えてくれるのである。

でも根本的な解決ではない

このプラグインで対処することも可能なのだが、そうすると記事を書く側としてはルート見出しレベルをこれまで通りのレベルにし続けなければならない。
また、試していないので何とも言えないのだが、たとえばコードスニペット中に書かれたhtmlの見出しレベルにも適用されてしまうのではという危惧もある。通常の日記ブログなどでは問題が無いだろうが、技術系のブログにとっては大きな問題となってしまうのではないか。
ということで、上手い解決法がないかと依然悩んでいる。データベースを直接たたいて置換というのも、怖い話だなあ。


Conceptual:シンプルなPHP製EXIF表示サービスEXIFviewerを公開

わけあって写真の位置情報なんかを弄るプログラムが必要になって、EXIFについて調べ直している。

調べ直しているというのは、以前に一度PHPにおけるEXIFの扱いなどについて調べていて、DamniPhoneEXIFという誰も得しないWEBサービスに応用したことがあるからだ。DamniPhoneEXIFはPHP標準のEXIFクラスを使わずに、PELというライブラリを使っている。PELを使わないとEXIFデータの書き込みが出来ないためであるが、初期のiPhone3Gで起こる位置情報の経度の東西が逆転するバグを解決するためだけのサービスとして作ったので、そこから色々な機能を建て増していくには不適当だった。それなら単純に標準のクラスでEXIF表示するサービスから作り直そう、ということでEXIFviewerを作った。

キモは、ユーザのローカルファイルを一旦HTML5のDataURLで受けているため、サーバに画像のコピーを作らずに画面リロード後の確認用画像の表示が出来ているというところ。これはOnlyCSViewでもやっていたことなので、詳しいやり方についてはOnlyCSViewの作成エントリを参照してほしい。

ところでEXIF形式って何?

EXIF形式というのは、JPEGやTIFFなどの画像ファイルに画像についての情報を付与してまとめたデータ形式だ。デジカメメーカーが主体となって、写真の撮影状況(撮影日、しぼり、感度、位置情報etc)のデータを画像に埋め込めるようにしたもの。正確に言うと、埋め込むわけではなく新たなファイル形式の大括りを作って、そこに画像の情報と当の画像ファイルをパッケージ化する。そのようにして出来たファイルの拡張子は.jpgや.tiffそのままなので、一見するとEXIFの情報が入ったファイルとの区別がつかない。かくして、デジカメユーザは知らず知らずに、EXIF形式のお世話になっている場合が多い。
ではどのように情報を格納するかというと、TIFF形式以来のファイル先頭へのタグ付けにより、情報を格納している。まずTIFFという名前が、(Tagged Image File Format)の略であり、この形式が登場したことにより、画像ファイルに画像の情報を入れられるようになった。JPEGファイルは同様のタグ付け形式のJFIFというフォーマットをほぼ標準としており、このJFIFをもっと大げさにしたものがEXIFというわけだ。

EXIF形式の構造 ざっくばらんに

つまり、EXIFはTIFFの落とし子だ。ファイルのバイナリデータの一番最初には、TIFFヘッダと呼ばれる8バイトのデータがある。この8バイトのデータのうち4バイトは、次のまとまりへの参照(ファイルの頭からの距離)を格納している。このように、ひとまとまりの情報の中に、他のまとまりの場所情報をセットで含んでいるのが、TIFF形式の特徴だ。
EXIFにどんなまとまりがあるかというと、IFD(Image File Directory)という名前のまとまりがあり、そのまとまりの中にさらに複数の情報がぎっしり詰まっている。
IFDの中にはEXIF IFDというものや、GPS IFDというものがあり、EXIF情報をいただきたい場合この場所にアクセスして情報を見る必要があるわけです。ややこしいね。

PHPのEXIFクラスでのアクセス

先程紹介したPelでは、イメージファイルをコンストラクタに与えて、ゲッターメソッドで一つ一つのまとまりの鍵を開けてアクセスしなければならない。DamniPhoneEXIFでGPSの東西を変えるだけでも、こんなに入れ子のプログラムができてしまった。

<?php
require_once('PelJpeg.php');
if(!empty($_POST["path"])){
$pj = new PelJpeg($_POST["path"]);
if($pjexif = $pj->getExif()){
if($pjtiff = $pjexif->getTiff()){
if($pjifd = $pjtiff->getIfd()){
if($pjgps = $pjifd->getSubIfd(PelIfd::GPS)){
if($gpsarray = $pjgps->getEntries()){
if(isset($gpsarray[3]) && isset($gpsarray[4])){
if(($gpsarray[4]->getText()) == "W"){
$gpsarray[3]->setText("E");
echo "converted west to east.<br>";
}
else if(($gpsarray[3]->getText()) == "E"){
$gpsarray[3]->setValue("W");
echo "converted east to west.<br>";
}
}
}
}
}
}
}
}
?>

PHPのexif_read_data関数では、引数にまず欲しいデータより上位のまとまりを指定して、ごっそりまとまりを取得した後には、foreachや添字でアクセスできる。

exif_read_data(ファイル名,
セクション名(省略化、複数の場合カンマ区切り),
配列で取得するかフラグ(省略可、デフォルトfalse,
サムネイル取るかフラグ(省略化、デフォルトfalse)

とりあえず今回のヴァージョンでは全部表に吐き出してしまっている。

<?php
if(!empty($_POST["path"])){
if($exif = exif_read_data($_POST["path"],NULL,true)){
echo "<table class='exiftable table table-striped'><thead><tr><th>キー名</th><th>値</th></tr></thead><tbody>";
foreach ($exif as $key => $section) {
    foreach ($section as $name => $val) {
	if(is_array($val)){
	foreach ($val as $valname => $vval) {
	echo "<tr><td>$key $name $valname</td><td>$vval</td></tr>";
	}
	} else {
	echo "<tr><td>$key $name</td><td>$val</td></tr>";
	}
    }
}
echo "</tbody></table>";
} else {
echo "画像のEXIFデータが見つかりませんでした。";
}
} else {
echo "画像をセットして下さい。";
}
?>

php.netの説明によると、exif_read_data関数の第一引数にはURL形式は与えられないとのこと。けれども試してみたら、普通に外部サイトの画像URLを与えても表示できた。そこで、$_POST[“path”]にはDataURLを与えることにする。画像のセットで起動して、jQueryでフィールドにエンコードしたURLを入れておくだけ。

外部サイトの画像URLが参照できるというのは面白いので、サービスとして機能を増やせる余地がありそうだ。浮気のアリバイ調査に使えますね。


小ネタ:WordPressがHTML5に対応して、過去記事の見出しに困る

WordPressはそのアップデートの歴史の中で、まさにアップトゥデートな技術を取り込み成長してきました。現在のヴァージョン3.5+Twenty Twelveでの売りはレスポンシブデザインですし、Twenty ElevenではHTML5対応という飛躍がありました。

さて、そうなると過去からずっとWordPressを使い続け、記事を投稿している人間にとっては、過去の投稿資産がアップデートで価値を失ってしまわないかと不安になります。何しろ新技術が過去の資産をラッピングして表示するわけで、何かしらの不具合になってもおかしくありません。

そうしたアップデートに伴い発生する不具合(不都合)の例を挙げましょう。Twenty ElevenでのHTML5対応により、HTML5の大きな特徴であるセクショニング・コンテンツの宣言と、セクション宣言の直後の見出しレベルh1からの使用が可能になりました。

それまで、Twenty Tenおよび他のHTML5未対応テーマでは、投稿記事のタイトルが<h2>から始まっていたため、あえて記事中で見出しをつける際には<h3>タグをルートレベルとせざるを得ませんでした。
ところがHTML5ではセクショニング・コンテンツの開始を宣言すれば、文書中のどの場所でも<h1>タグからの階層構造にする事ができます。そして、Twenty Eleven以降の標準テーマでは、投稿記事を<article>タグで囲んでセクショニングコンテンツとするとともに、記事タイトルを<h1>としているのです。

そこで、Twenty Eleven以降のテーマで過去の記事を表示すると、記事タイトルの<h1>タグの下に”空気を読んで”<h3>に揃えておいた見出しタグが来てしまいます。<h2>タグのない、崩れた構造になってしまいます。

WordPressの売りは、テーマの自由自在な変更に代表される、深く考えなくても良いカスタマイズ性だったはずですが、HTML5へのステップアップを挟む事で、記事本文のテーマからの独立性が保証されなくなったと言えます。いまや、そのテーマが記事でどの見出しタグをルートとしているかあらかじめ知っていないと、無闇にテーマを変更する事ができなくなっているのです。

何が言いたいのかというと、投稿記事の見出しを<h3>タグで始める、手癖がついてしまっているのですよね。困った。

こうしたステップアップを挟んだからか、WordPressのテーマプレビュー(Lorem ipsum…)でサンプル文章中に<h1>、<h2>タグも含まれているのですが、特にこれにスタイルを設定していないテーマもよく見かけます。

誰に腹を立てたら良いのか、怒りのやりどころもわからぬままポチポチ過去記事を修正です


File API FileReaderを使った画像のドラッグ&ドロップアップロード

HTML5で新たに搭載されたAPIを利用して、WordPressのメディアアップロード画面のようなドラッグ&ドロップでの画像アップロード機能を実現します。File APIのFileReaderとData URLを使ったAjax POSTの手順は、以前OnlyCSViewを作成したエントリを参考にしていただくとして、今回はさらにドラッグ&ドロップAPIを使ったローカルファイルのセットと、PHP側での受け取ったData URLの保存方法を説明します。

STEP0:HTML5におけるページ内要素のドラッグ&ドロップ

HTML5では、ブラウザ上でのネイティブのドラッグ&ドロップを実現させるための属性として、draggableおよびdropzoneが用意されました。理想的には、draggable=”true”と指定されたページ上の要素をぐぐっと掴んで、dropzone属性の指定がされた要素の上に移動できるという動作の実現が期待されているのですが、dropzone属性に対応しているブラウザがほぼ無いため、ドロップ側の挙動はJavaScriptで実装するしかありません。とりあえずJavaScriptは置いておいて、属性指定だけした動作サンプルを見てみましょう。

draggable=”true”を指定した要素
(対応ブラウザならぐぐっと掴める)
dropzone=”move”を指定した要素
(対応ブラウザならドラッグした要素を
ドロップで受け付ける)

これがページ内要素のドラッグ&ドロップです。おそらく動作していないでしょう(笑)。ちなみにdropzone属性の値は、ドラッグ中の要素を移動させる”move”以外にも、複製する”copy”、ドラッグされた要素のリンクを取得する”link”などがあります。絵に描いた餅ですね。

STEP1:ローカルファイルのドラッグ&ドロップ

今回作成するプログラムのように、ローカルファイルをドラッグ&ドロップする場合には、ドラッグ側にdraggable属性を設定しなくてもOS標準機能でドラッグができています。ただし、通常ブラウザ上にファイルをドロップしたときの処理は、例えば画像ファイルやテキストファイルであればウィンドウ全体を使ったプレビュー表示になりますので、この標準処理をドロップ側でキャンセルし、別の処理をイベントに紐づける必要があります。もちろんJavaScriptを使うことになります。

STEP2:ドロップを受け付ける要素を用意してAjaxイベント付加

では、ドロップを受け付ける要素にdropzone属性の代わりとなるイベントをつけましょう。

//jQueryのCDNでのインポート。ヴァージョンはあまり気にせず。
<script src="http://code.jquery.com/jquery-1.7.2.min.js"></script>
.
.
<script type="text/javascript">
$(function(){
//jQueryに無いイベントdataTransferを付加
$.event.props.push("dataTransfer");
 
//ドロップを受け付ける領域は、適当にclass名をdropzoneとしておく。
var dropzone = $(".dropzone");
 
//FileReaderオブジェクトが無いとそもそも動作しない
//非対応ブラウザではdropzoneをそもそも表示しない心遣い(必須ではありません)
if(!window.FileReader){
          dropzone.hide();
          return false;
        } else {
//心遣いしないのなら、本処理はここから
		dropzone.bind("dragover",function(event){
			event.preventDefault();
			event.stopPropagation();
			return false;
		});
//領域にファイルが入ったら、灰色にハイライトさせる心遣い(必須ではない)
		dropzone.bind("dragenter",function(event){			
			$(this).css("background-color","#CCC");
			return false;
		});
		dropzone.bind("dragleave",function(event){
			$(this).css("background-color","#FFF");
			return false;
		});
//心遣い終わり
 
//本処理
		 var handleDroppedFile = function(event){
			 var thisarea = $(this);
          var file = event.originalEvent.dataTransfer.files[0];
		  var type = file.type;
          var fileReader = new FileReader();
		  fileReader.readAsDataURL(file);
          fileReader.onload = function(event){
 
//心遣い担当
            thisarea.css("background-color","#FFF");
//終わり
			$.ajax({
				type : "POST",
				url : "dropupload.php",
				data: {"type" : type,
"data" : event.target.result},
				success : function(data){
//成功処理を書くならここに。"成功しました"と表示させるとか			
				},
				dataType : "json"
			});
		  }
		  fileReader.onerror = function(stuff){
//心遣い担当
            thisarea.css("background-color","#FFF");
//終わり
		  }
		  event.preventDefault();
		  event.stopPropagation();
          return false;
        }
		   dropzone.bind("drop", handleDroppedFile);
		}
});
</script>

dropzoneには$.bindでもってイベントを結びつけます。ここでは”dragover”,”dragenter”,”dragleave”,”drop”の4種類が出てきますが、領域にファイルが重なったときに処理を行わないのなら、”dragover”と”drop”だけで良いでしょう。dragoverでは、ファイルをブラウザのウィンドウ上にドラッグしたときの標準処理をpreventDefaultでキャンセルし、stopPropagationでイベントが下位要素にも伝わってしまうのを止めます。ちなみに、IEの場合下から上にイベントが伝わっていくらしく、またメソッドではなくプロパティで指定する(event.returnValue = false;/event.cancelBubble = true;)ようです。IE10ではFileReaderがサポートされるみたいなので、対応の手間が増えますね。

fileReaderからreadAsDataURLでファイルをDataURLにする辺りは、以前OnlyCSViewを作成したエントリを参考。今回は$.postではなく$.ajaxを使っているので注意です。

STEP3:PHP側でアップロードの実装

PHP側では、Data URLにされた画像の解凍に一手間が要ります。

//dropupload.php
 
<?php
$filetype = htmlentities($_POST["type"], ENT_QUOTES, "UTF-8");
if(preg_match("/jpeg/",$filetype)){
$filesuffix = ".jpg";
$mimeprefix = "data:image/jpeg;base64,";
} else if(preg_match("/gif/",$filetype)){
$filesuffix = ".gif";
$mimeprefix = "data:image/gif;base64,";
} else if(preg_match("/png/",$filetype)){
$filesuffix = ".png";
$mimeprefix = "data:image/png;base64,";
}
 
//とりあえず一意の名前になってほしいので、日付時刻をコピー先ファイル名にする。
 
$filename = date("YmdHis").$filesuffix;
 
//まあ、仮にアップロード画像を格納するディレクトリがuploads/だったとしたら
$filepathname = "uploads/".$filename;
 
file_put_contents($filepathname,base64_decode(str_replace($mimeprefix, '', $_POST["data"])));
?>

エラーのコールバック処理を入れなければこういう感じです。
base64_decodeという関数でData URLのbase64エンコードを解凍するのですが、このときURLのファイルタイプ部分を取り除いてやらないと、PHP側でうまくコピーできないというのがキモですよ。