2009年6月7日日曜日

Json.NETとStringTemplateでお気楽HTML出力

暇だったんですよね。で、暇つぶしにJSONからStringTemplateを通してHTMLをはき出させてみたんです。何となくですけど、適当にデータを定義しておいて、テンプレートに当てはめてHTMLを出力するっていうツールがないものかと探してみたんだけど、どーにも楽ちんそうなのが見あたらなくて。あ、ちなみに5月の話です。

データはXMLでも良かったんだけど、書くのが面倒になるのもやだし、テンプレート解釈とかは作りたくないし、ってことで、Json.NET - James Newton-KingStringTemplate Template Engineで書いてみたっす。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Antlr.StringTemplate;
using System.IO;
using Newtonsoft.Json.Linq;

namespace STHtml
{
  public class STGenerator
  {
    private const string INPUT_PATH = "input";
    private const string ROOT_PROPERTY = "templateItems";
    private const string FILENAME_PROPERTY = "output_path";
    private const string FILENAME_FORMAT = "output{0}.txt";

    private STSetting _setting;

    public STGenerator(STSetting setting)
    {
      _setting = setting;
    }

    public void Execute(IOutput output)
    {
      var group = new StringTemplateGroup("htmlTemplates", INPUT_PATH);
      var json = LoadJSON();

      if (json == null)
        return;

      // 
      var list = (from item in json[ROOT_PROPERTY]
                  select item).ToList();

      foreach (var item in list)
      {
        // テンプレートの読み込み
        var st = group.GetInstanceOf(_setting.TemplateFileName);

        // ファイル名は?
        var filePath = GetOutputFilePath(item, list.IndexOf(item));

        // 生成
        var generated = GenerateFromJSON(st, item);
        if (generated == null)
          return;

        // 出力
        output.Print(filePath, generated);
        Console.WriteLine("output -> " + filePath);

      }
      Console.WriteLine("{0} file(s) template generated.", list.Count());
    }

    private string GetOutputFilePath(JToken item, int itemIndex)
    {
      // ファイル名は?
      var fileName = (from t in item
                      select item[FILENAME_PROPERTY]).FirstOrDefault();
      var filePath = Path.Combine(_setting.OutputPath,
        fileName != null ?
          fileName.ToString().Replace("\"", "") :
          string.Format(FILENAME_FORMAT, itemIndex)
      );

      return filePath;
    }

    private JToken LoadJSON()
    {
      string jsonText;
      JObject json = null;

      try
      {

        jsonText = File.ReadAllText(Path.Combine(INPUT_PATH, _setting.JsonFileName));
        json = JObject.Parse(jsonText);
      }
      catch (Exception e)
      {
        Console.WriteLine("データファイルの書式が間違ってるか、ファイルがないか...");
        Console.WriteLine(e.Message);
        json = null;
      }

      return json;
    }

    private string GenerateFromJSON(StringTemplate st, JToken json)
    {
      try
      {
        // JsonからDictionaryに変換
        // JObjectからそのままはStringTemplateが無理さ
        var dict = JsonToDictionary(json) as Dictionary<string, object>;
        foreach (var kv in dict)
        {
          st.SetAttribute(kv.Key, kv.Value);
        }
      }
      catch (Exception e)
      {
        Console.WriteLine("変換に失敗したよ。テンプレートがおかしいと思われる。");
        Console.WriteLine(e.Message);
      }

      return st.ToString();
    }

    private object JsonToDictionary(JToken token)
    {
      Dictionary<string, object> result = new Dictionary<string, object>();

      // なんか美しくないね。
      // 値セットと値戻しが同列だしな~。まぁ、いっか。
      foreach (var node in token)
      {
        if (node is JProperty)
        {
          // プロパティ型(name:value)なら取得
          var prop = node as JProperty;
          // 配列([...])かオブジェクト({...})なら再帰
          if (prop.Value.Type == JsonTokenType.Array ||
              prop.Value.Type == JsonTokenType.Object)
          {
            result[prop.Name] = JsonToDictionary(prop);
          }
          else
          {
            // その他の値型なら文字列化
            var value = prop.Value.ToString()
                                  .Replace("\"", "");

            result[prop.Name] = value;
          }
        }
        else if (node is JArray)
        {
          // 配列型なら戻す(再帰の時にしか処理しないもん)
          var arr = node as JArray;
          var list = new List<object>();
          foreach (var item in arr)
          {
            list.Add(JsonToDictionary(item as JToken));
          }
          return list;
        }
      }

      return result;
    }
  }
}

説明するのが面倒なんでソースです。こんな適当な感じでもそれなりに動くよ~。JsonからHTMLへの埋め込み時にEncodeかかってないからその辺は気をつけましょう。

使い方なんですけど(使う人がいるとは思えないけど)。

まずはinputフォルダのdata.jsonファイルにデータを書き込んでいきます。例えば↓こんな感じです。

{templateItems:
  [
    {
      output_path:'test1.html',
      title:'ページ1',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'2.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'3.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    },
    {
      output_path:'test2.html',
      title:'ページ2',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    },
    {
      output_path:'test3.html',
      title:'ページ3',
      subject:'うぎょぎょ<br />ぼへ',
      gallery:[
        {src:'1.jpg', alt:'', title:'説明文を書きましょう'},
        {src:'2.jpg', alt:'', title:'説明文を書きましょう'}
      ]
    }

  ]
}

全体が一つのObjectです。で、ルートではtemplateItemsっていう名前の配列を定義がルールです。templateItems配列に入れる各オブジェクト(アイテムオブジェクト)の書式は気をつけて統一する必要あり。こんな時にJSONスキーマが役立つんだろうな~。面倒なので気をつけるっていうルールで。アイテムオブジェクトはどんな形式でもOK、のはず。最初に提示したソースのJsonToDictionary関数が再帰でその辺上手くやってくれるようにしてます。でも、JArray型とJProperty型以外は想定してないので、まぁ、その辺は雰囲気で。

あ、アイテムオブジェクトに絶対に入れなきゃ行けないのが出力ファイル名をセットしたoutput_path。上記の例だとoutputフォルダにそのまま出す感じなんですが、例えば"output_path:’folder1/default.html’"とかってしておくと、outputフォルダ内にfolder1フォルダを作成して、その中にdefault.htmlを出力します。

次にテンプレートファイルとして↓こんなのを用意。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  <meta name="keywords" content="">
  <title>$title$</title>
</head>
<body>
<h1>$title$</h1>
<div>
  <h2>$subject$</h2>
  <ul>
    $gallery:{g|
    <li><img src="$g.src$" alt="$g.alt$" title="$g.title$" /></li>
    }$
  </ul>
</div>
</body>
</html>

後は実行するだけ。そうすると↓こんなのが出力されました。

sthtml1 sthtml2

またしてもプロジェクトファイルは添付しておくのでご自由に。