この文書は,António Afonso]による,Markuper: The Opera Unite Service template library の邦訳です.

This article is licensed under a Creative Commons Attribution, Non Commercial - Share Alike 2.5 license

初めに

Markuper は Unite アプリの開発を補助するテンプレートライブラリです.

通常,Unite アプリを開発するとき,すべてのコンテンツを WebServerResponse.write* 関数を使って書き出さなくてはなりません.これでは,例えばデザイナがページのレイアウトを変えたいと思ったときなど,コンテンツを書き換えるのが面倒です.また,ビジネスロジックのレイヤーと見た目のレイヤーを分離してつくらないとごっちゃになってしまいます.

Markuper テンプレートライブラリは JavaScript のコードと HTML 文書とを結ぶための書式を導入することで,これらの問題の解決を目指しています.この文書では,Markuper ライブラリの主要な機能の使い方について紹介します.

文書の構成は以下のようになっています.

  1. 初めに
  2. 初めてのテンプレート
    1. Unite アプリへのライブラリの組込み方法
    2. 単純な HTML ファイルの出力
  3. JavaScript の変数をテンプレートで使う
  4. Markuper と DOM
    1. サンプルコード
      1. XPath
      2. CSS セレクタ
  5. HTML を操作する
    1. プレゼンテーションロジック
    2. HTML と JavaScript の関数を結びつける
    3. data-* 属性
      1. HTML を文字列に変える
      2. ソースコードのシンタックスハイライト
      3. HTML の要素の追加と削除
    4. 組込の data-* 属性
      1. 配列やオブジェクトの繰り返し処理 ( data-list 属性 )
      2. 要素の削除 ( data-remove/keep-if 属性 )
      3. ほかのテンプレートの取込 ( data-import 属性 )
  6. サンプルコードのリスト

初めてのテンプレート

まずは,ライブラリの挙動の説明として簡単な HTML ファイルを出力させてみます.この程度なら WebServerResponse.writeFile, を直接呼ぶのと大差ありませんが,デモとしてみてください.

Unite アプリへのライブラリの組込み方法

テンプレートを使うコードに取り掛かる前に,Unite アプリに Markuper ライブラリを組み込みましょう.エントリポイントの index.html ファイルか,もしくは config.xmlwidgetfile でエントリポイントとして指定したファイルに script 要素として template.js を追加します.ここまでの下ごしらえしたサンプルをダウンロードすることもできます.

Markuper ライブラリは File I/O API を使っているので,下記のように config.xmlfeature 要素を追加しておく必要があります.

<feature name="http://xmlns.opera.com/fileio"></feature>

単純な HTML ファイルの出力

まずは下記のような出力用の単純な HTML ファイルを作り,Unite アプリのトップの templates/ ディレクトリに保存します.

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  Markuper Tutorial
</body>
</html>

Unite アプリへのリクエストを処理するために,opera.io.webserver_request イベントを受理するようにしておきます.前掲のサンプルアプリのコードでは,scripts/main.js ファイルでその処理をしています.

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;
  var template = new Markuper( 'templates/tutorial.html' );
  response.write( template.html() );
  response.close();
}

Markuper オブジェクトのコンストラクタの引数として,出力させるファイルのパスを渡します.Markuper オブジェクトの html 関数はテンプレートファイルを文字列化して返すので,response オブジェクトに渡して出力させます.図 1 に出力結果を示します.

単純なテンプレートの出力

図 1: 単純なテンプレートの出力

一つ目のサンプルアプリの全コード

JavaScript の変数をテンプレートで使う

ただの HTML ファイルを出力させるだけならテンプレートライブラリを使う意味はほとんどないでしょう.テンプレートライブラリの真価は JavaScript の変数と組み合わせて使うことにあります.

Markuper オブジェクトのコンストラクタは,テンプレートファイルのパスのほかに,第2引数としてテンプレートで置き換える値を持たせたオブジェクトを渡すことができます.この第2引数に渡すオブジェクトは JSON 形式のオブジェクトなので,階層構造を持たせることもできます.

テンプレート内では,引数のオブジェクトのプロパティ名を {{path.to.variable}} のように波かっこ二つで囲って表します.

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <p>
    This variable is further down the data object hierarchy:
    '{{further.down.the.hierarchy}}'
  </p>
</body>
</html>

上にあげた例では,namefurther.down.the.hierarchy の二つの JavaScript の変数をテンプレートに埋め込んでいます.二つの埋め込まれた文字列は,Markuper オブジェクトのコンストラクタの第2引数で渡されたオブジェクトの対応する変数の値で置き換えられます.

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name    : 'Template',
    further :
    {
      down  :
      {
        the :
        {
          hierarchy:  'yes it is!'
        }
      }
    }
  };
  var template = new Markuper( 'templates/tutorial.html', data );

  response.write( template.parse().html() );
  response.close();
}

JavaScript のファイル内では,テンプレートで参照するデータオブジェクトを作り,適切な値をセットします.

JSON オブジェクトとしてコンストラクタに渡される data 変数に,二つのプロパティ name, further.down.the.hierachy を定義してそれぞれ文字列 Templateyes it is! をセットしています.

HTML での出力を得る前に,明示的に parse() 関数を呼んで,テンプレートの値を置換えます.

出力結果は以下のようになります.

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Template Tutorial</H1>
  <P>
    This variable is further down the data object hierarchy: 
    'yes it is!'
  </P>
</BODY>
</HTML>

二つ目のサンプルアプリの全コード

Markuper と DOM

Markuper テンプレートエンジンは DOM をベースにしているため,通常の Web ページの制作と同様に DOM API を利用する jQuery や YUI のようなライブラリやコードを用いてテンプレートを操作することができます.

テンプレート内の Elemenet を操作する関数として,xpathselect 関数の二つがあります.操作するノードの取得に xpath 関数では XPath を用い,select関数ではCSS 3 セレクタを用います.

サンプルコード

まず,今回の HTML テンプレートファイルは templates/tutorial.html とします.

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <div id="div1">
    This is going to be <span>removed</span>
  </div>
</body>
</html>

XPath

XPath を用いた要素の選択は次のようにします.

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name    : 'Template'
  };
  var template = new Markuper( 'templates/tutorial.html', data );

  var span = template.xpath( "//div[@id='div1']/span[1]" )[0];
  span.parentNode.removeChild( span );
    
  response.write( template.parse().html() );
  response.close();
}

CSS セレクタ

CSS セレクタでの要素の選択は次のようにします.

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name    : 'Template'
  };
  var template = new Markuper( 'templates/tutorial.html', data );

  var span = template.select( "#div1 > span" )[0];
  span.parentNode.removeChild( span );

  response.write( template.parse().html() );
  response.close();
}

出力結果は以下のようになります:

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>{{name}} Tutorial</H1>
  <DIV id="div1">
    This is going to be
  </DIV>
</BODY>
</HTML>

三つ目のサンプルアプリの全コードのダウンロード

HTML を操作する

プレゼンテーションロジック

ここまで,ロジックレイヤーをプレゼンテーションのレイヤーから効率的に切り離す方法を見てきました.JavaScript にすべてのロジックを実装し,テンプレートファイルに生成した値を埋め込むという手法です.

さらに,ロジックレイヤー自体もビジネスとプレゼンテーションの二つの領域に分けることができます.ビジネスロジックでは解こうとしている問題や特定の領域のモデルのルールを扱い,一方のプレゼンテーションロジックでは,ビジネスロジックで生成したデータをユーザにたいしてどのように提示するかについて扱います.

ここで言う「プレゼンテーション」とは,「 HTML は構造を,CSS は見た目を」という意味の「見た目」ではなく,デスクトップアプリやあるいは本格的なウェブアプリの開発における「プレゼンテーション層」を意味しています.デスクトップアプリのモデルでは,プレゼンテーション,アプリケーション,ストレージの三層に分けられ,HTML や CSS, JavaScript はプレゼンテーションレイヤーに位置付けられます.詳細は Webアプリに関する Wikipedia 英語版の記事を参照してください.

HTML と JavaScript の関数を結びつける

プレゼンテーションロジックのレイヤーを実現するために Markuper ライブラリはあらゆる HTML の要素を JavaScript のコードから操作するための仕組みを実装しています.どの JavaScript の関数がどの要素をコントロールするかをその要素の属性に結びつけます.

data-* 属性

HTML の要素と JavaScript の関数とをバインドするためには,まずテンプレートオブジェクトの registerDataAttribute 関数を,data-* 属性の名前と,その属性を持つ要素に結び付けるコールバック関数とを引数にして呼び出します.

登録するコールバック関数には次の四つの引数が渡されます.

  1. node: 指定した data 属性を持つ要素
  2. data: テンプレートオブジェクトのコンストラクタに与えられたデータオブジェクト
  3. key: 指定した data 属性の値(文字列)
  4. value: key の値が,data オブジェクトのインデックスになっているとき,その値.

HTML を文字列に変える

このサンプルでは ソースコードを表示する機能のように,指定した HTML の要素の内容を文字列に変換します.

まず,JavaScript 側 ( scripts/main.js ) は以下のようになります.

opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
  var response = event.connection.response;
    
  var data =
  {
    name    : 'Template',
  };
  var template = new Markuper( 'templates/tutorial.html', data );
    
  template.registerDataAttribute( 'show-html', function( node, data, key )
  {
    if( key == 'true' )
    {
      node.textContent = node.innerHTML;
    }
  });
  response.write( template.parse().html() );
  response.close();
}

テンプレートファイル ( templates/tutorial.html ) は以下のようになります.

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <pre data-show-html="true">
    <div id="header"></div>
    <div id="content">
      <p>paragraph</p>
    </div>
    <div id="footer"></div>
  </pre>
</body>
</html>

最終的な出力は次のようになります.

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <PRE data-show-html="true">
    &lt;DIV id="header"&gt;&lt;/DIV&gt;
    &lt;DIV id="content"&gt;
      &lt;P&gt;paragraph&lt;/P&gt;
    &lt;/DIV&gt;
    &lt;DIV id="footer"&gt;&lt;/DIV&gt;
  </PRE>
</BODY>
</HTML>

四番目のサンプルアプリの全コードのダウンロード.

ソースコードのシンタックスハイライト

次は,要素の内容を書き換えるもう少し複雑なサンプルとして,JavaScript の関数を toString() 関数で文字列化して,シンタックスハイライトを施すことにします.

まず今回のサンプルコードは scripts/main.js になります.

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;
  var data =
  {
    name    : 'Template',
    func    : function foo()
    {
      var baz = 3;

      return 'bar';
    }
  };
  var template = new Markuper( 'templates/tutorial.html', data );
  template.registerDataAttribute( 'list-code', function( node, data, key, value )
  {
    var keywords = ['function', 'var', 'return'];
    var regexp = new RegExp( keywords.join('|'), 'g' );
    value = value.toString().replace( regexp, function( keyword )
    {
      return '<span style="color: blue">' + keyword + '</span>';
    });
    node.innerHTML = value;
  });
  response.write( template.parse().html() );
  response.close();
}

表示用のテンプレートは templates/tutorial.html になります.

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <pre data-list-code="func"></pre>
</body>
</html>

出力結果は次のようになります.

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <PRE data-list-code="func"><SPAN style="color: blue">function</SPAN> foo()
  {
    <SPAN style="color: blue">var</SPAN> baz = 3;

    <SPAN style="color: blue">return</SPAN> 'bar';
  }</PRE>
</BODY>
</HTML>

五番目のサンプルアプリの全コードをダウンロード

HTML の要素の追加と削除

このサンプルでは,あるノードの data-handler 属性に LaTeX のような値を与えて,その値から HTML の要素を生成し,最終的にそのノード自体を削除するというものです.

まず,JavaScript のコードは scripts/main.js になります.

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name    : 'Template',
  };
  var template = new Markuper( 'templates/tutorial.html', data );

  template.registerDataAttribute( 'header', function( node, data, key )
  {
    var types =
    {
      'section'           : 'h1',
      'subsection'        : 'h2',
      'subsubsection'     : 'h3',
      'paragraph'         : 'h4',
      'subparagraph'      : 'h5'
    }

    var header = document.createElement( types[key] );
    header.textContent = node.textContent;

    node.parentNode.insertBefore( header, node );
    node.parentNode.removeChild( node );
  });

  response.write( template.parse().html() );
  response.close();
}

それから,テンプレートファイルは templates/tutorial.html になります.

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <p data-header="section">Section</p>
  <p>This is a section</p>
  <p data-header="subsection">SubSection</p>
  <p>This is a subsection</p>
  <p data-header="paragraph">Paragraph</p>
  <p>This is a paragraph</p>
</body>
</html>

すると,出力結果は次のようになります.

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <H1>Section</H1>
  <P>This is a section</P>
  <H2>SubSection</H2>
  <P>This is a subsection</P>
  <H4>Paragraph</H4>
  <P>This is a paragraph</P>
</BODY>
</HTML>

六番目のサンプルをダウンロードする

組込の data-* 属性

Markuper ライブラリにはいくつか組込の data-* 属性があり,配列やオブジェクトの操作,ノードの削除やほかのテンプレートの取り込みなど典型的な処理に用いられます.

配列やオブジェクトの繰り返し処理 ( data-list 属性 )

data-list 属性のつけられたノードは,指定された値に応じて繰り返し処理されます.JavaScript の配列から HTML のリストを作るのに使えます.

data-list 属性に指定された値が配列だったときは,配列の要素の数だけノードが作られます.指定された値がオブジェクトだったときは,オブジェクトのプロパティの数だけノードが作られます.

data-list 属性を用いた繰り返し処理では,テンプレートに <data-list>[] と呼ぶ特別なフィールドを使うことができ,処理している配列の個々の要素やオブジェクトにアクセスすることができます.

例えば,data-list="cities" が付加されたノード内では,cities[] という名前で配列の個々の要素にアクセスすることができます.

data-list 属性の値が Object を指定している場合,data-list[] フィールドは 元々のオブジェクトのプロパティに対応する keyvalue というプロパティを持つ Object になります.

サンプルコードでみていきましょう.まずはテンプレートファイル ( templates/tutorial.html ) です.

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <ul>
    <li data-list="cities">
      {{cities[].city}}: {{cities[].temperature}} degrees
    </li>
  </ul>
</body>
</html>

つぎに,JavaScript ファイル ( scripts/main.js ) です.

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name    : 'Template',
    cities  :
    [
      {city: 'Lisbon', temperature: 20},
      {city: 'Oslo'  , temperature: -2}
    ]
  };

  var template = new Markuper( 'templates/tutorial.html', data );

  response.write( template.parse().html() );
  response.close();
}

そして,出力結果は以下のようになります.

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <UL>
    <LI>
      Lisbon: 20 degrees
    </LI>
    <LI>
      Oslo: -2 degrees
    </LI>
  </UL>
</BODY>
</HTML>

七番目のサンプルアプリをダウンロードする.

要素の削除 ( data-remove/keep-if 属性 )

条件によって要素を消したり残したりすることもできます.属性の値として真偽値か, &&|| を使った論理演算を指定します.data-remove-if 属性が付加されているとその値によってノードを消すかどうかが決まり,data-keep-if 属性が付加されているとその真偽値によって残すかどうかが決まります.

まず,JavaScript コードは scripts/main.js になります.

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;
  var isAdmin = true;
  var readAccess = false;

  var data =
  {
    name            : 'Template',
    isAdmin         : false,
    hasReadAccess   : true
  };

  var template = new Markuper( 'templates/tutorial.html', data );

  response.write( template.parse().html() );
  response.close();
}

次に,テンプレートファイルは templates/tutorial.html になります.

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <h1 data-remove-if="false">DRAFT</h1>
  <p data-keep-if="isAdmin">Admin Eyes Only</p>
  <p data-keep-if="hasReadAccess || isAdmin">very important info</p>
</body>
</html>

出力結果は次のようになります.

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <H1>DRAFT</H1>
  <P>very important info</P>
</BODY>
</HTML>

八番目のサンプルアプリをダウンロードする.

ほかのテンプレートの取込 ( data-import 属性 )

大事なことを言い忘れてましたが,Markuper ライブラリではほかのテンプレートを取り込んで指定した要素に追加することができます.

まずは,JavaScript ファイル scripts/main.js をみてみましょう.

opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name            : 'Template'
  };

  var template = new Markuper( 'templates/tutorial.html', data );

  response.write( template.parse().html() );
  response.close();
}

それから,テンプレートファイル templates/tutorial.html をみてみます.

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <div data-import="templates/import.html"></div>
</body>
</html>

このサンプルでは,次のような別のテンプレートファイル templates/import.html を取り込んでいます.

yay! I was imported from {{name}}!!

出力結果は次のようになります.

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <DIV>yay! I was imported from Template!!</DIV>
</BODY>
</HTML>

九番目のサンプルアプリをダウンロードする.

サンプルコードのリスト