読者です 読者をやめる 読者になる 読者になる

FIELD NOTES: 書を持って街へ出よう

合同会社フィールドワークス プログラマ兼代表のブログ

Evernoteで図入りのMarkdown文書を書いてMarkedでプレビューする方法(Mac OS X)

HOWTO プログラミング

EvernoteにMarkdown記法でメモを書くのは便利なのですが, せっかくEvernoteを使っているのに図を手軽に扱えないのは残念なので,ひと工夫してみました。

f:id:fet:20131221194358p:plain

使い方

以下が作成したPythonスクリプトです。

<EVERNOTE_USER_NAME>の部分は,Evernoteのアカウント名に置き換えてください。

EvernoteにMarkdown形式で記述した文書をMardedでプレビューするためのMac OS ...

これをAutomatorでサービスとして登録します。

f:id:fet:20131221194051p:plain

Evernoteでページを編集中に登録したサービスを呼び出せば, Marked.appで表示されます。

f:id:fet:20131221194117p:plain

仕組み

Mac版のEvernoteでは,Evernoteのデータを以下のディレクトリ配下に格納します。

 「~/Library/Containers/com.evernote.Evernote/Data/Library/Application Support/Evernote/accounts/Evernote/<ユーザ名>/content」

編集中のページに対応するディレクトリがどれかは, Apple event経由でEvernoteに問い合わせてもわからない様なので, 更新日時が最新のディレクトリを編集中のページに対応するディレクトリと見なします。

ページの内容はcontent.htmlというファイルに反映されます。 ページに挿入した画像は,同じディレクトリ内に画像ファイルとして保存されます (⌘+Sキーを押せば,編集内容は即座にファイルに保存されるようです)。

html2textを使って,content.htmlをMarkdown形式に変換し, 同じディレクトリにファイルとして保存します。 この部分の処理がhtml2textの本来の使い方から外れていてトリッキーですが, Markdown表記のテキストはP要素の内容としてそのまま出力されます。 画像は,context.htmlの中ではIMG要素として表現されるので, 結果的に「![](<画像ファイル名>)」形式に変換されます。

次に作成したMarkdownファイルをMarked.appで開くのですが, 画像ファイルと同じディレクトリにあるので画像も表示されます。

参考にさせていただいた記事

PDF帳票を作成するためのWEB APIを提供開始しました

製品情報 ニュース

クラウド上のシステムからPDF帳票を作成するための実験的なサービス Field Reports for Cloud を提供開始しました。

このサービスで提供するField Reports APIを利用すれば,Google App Engine (GAE), Heroku, Salesforce.com (SFDC) などのPaaS上に構築されたシステムから,PDF帳票を作成することが可能になります。

Field Reports APIを利用するには,Field Reports for Cloud のサイト で利用者登録を行い,APIキーを取得する必要があります。

PDF帳票を作成する際には,JSON形式で記述したAPIパラメータ(レンダリング・パラメータ)を所定のURLにPOSTします。 リクエストの応答として,サーバ側で生成したPDFが返されます。

おおよそ以下2点の要件を満たす動作環境であれば,利用できるはずです。

  • 任意のJSON文字列を組み立てることができること。
  • 作成したJSON文字列を所定のURLへPOST(アップロード)できること。

このサービスは実験的な提供のため,現時点での利用料は無料です。

後述するようにトランザクション数などに制限を掛けてはいますが,Field Reports Standardの機能をほぼそのまま利用することができます。 トランザクション数がそれほど多くないシステムからであれば,かなり実用的に使うことができるのではないかと思います。

システム構成

実行環境としては,さくらVPSの2Gを利用しています。

使用している主なソフトウェアの構成は,以下のとおりです。

項目 ソフトウェア名
Webサーバ nginx
アプリケーションサーバ web2py
帳票ライブラリ Field Reports Standard for Linux


利用制限について

このサービスは,API呼び出し時のパラメータによりサーバに掛かる負荷が大きく変わります。 そこで,以下のような利用制限を設定しています。

  • 1日当たり(24時間以内)の呼び出し回数は,30件まで
  • 作成する帳票のページ数は,10ページまで

現在のところ,さくらVPS 2Gでまかなえる範囲内でこのサービスを運用していきたいと考えています。 サーバの負荷状況によっては,新規の利用者登録を停止したり上記の制限を変更する可能性があることを, あらかじめご了承願いたいと思います。

Hello World

コマンドラインツールのcurlを使って,APIを呼び出してみます。

まず,JSON形式のパラメータを作成してファイルに保存します(ファイル名:hello.json)。 <APIキー>の部分には,取得したAPIキーを記入します。

{
    "settings": {
        "api-key": <APIキー>
    },
    "template": {"paper": "A4"},
    "context": {
        "hello_1": {
            "new": "Tx",
            "value": "Hello, World!",
            "rect": [100, 700, 400, 750],
            "font": "/Times-Roman"
        },
        "hello_2": {
            "new": "Tx",
            "value": "Hello, World!",
            "rect": [100, 600, 400, 650],
            "font": "/Helvetica-Oblique"
        },
        "hello_3": {
            "new": "Tx",
            "value": "Hello, World!",
            "rect": [100, 500, 400, 550],
            "font": "/Courier-Bold"
        },
        "hello_4": {
            "new": "Tx",
            "value": "ABCDEFGHIJKLMN",
            "rect": [100, 400, 400, 450],
            "font": "/ZapfDingbats"
        },
        "hello_5": {
            "new": "Tx",
            "value": "こんにちは世界",
            "rect": [100, 300, 400, 350],
            "font": "/KozGo-Medium"
        }
    }
}

hello.jsonがカレンドディレクトリに存在する状態で,以下のようにcurlコマンドを実行します。

$ curl -k https://labs.field-works.co.jp/webapi/render --data @hello.json -o hello.pdf

成功すれば,以下のようなPDFが作成されます。

テンプレートを利用した帳票作成

本格的な帳票の作成には,テンプレートの利用が必須になります。

Field Reports ではテンプレートをPDF形式で作成しますが, 作成したテンプレートファイルを何らかの方法でサーバに送り込む必要があります。

それには,以下の3種類の方法が考えられます。

  1. Field Reports for Cloud サーバ上に置き,ローカルパスを指定する。
  2. 利用者側が準備したサーバ上に置き,URLを指定する。
  3. data URI scheme文字列として,レンダリングパラメータにインラインで埋め込む。

現在のところ,利用者が任意のファイルをField Reports for Cloud サーバ上に置くことを許可していませんので, 1の方法で利用できるのは,前もって準備してある数種類のサンプルのみです。

2の方法を取る場合は,以下のようにテンプレートの場所を指示します。

"template": {"src": "http://example.com/template.pdf"}

3の方法を取る場合は,テンプレートファイルをBase64エンコードした上で,レンダリングパラメータに埋め込みます。

"template": {"src": "data:application/pdf;base64,AAAoAAAARgAAAMD....."}

同様に,画像ファイルやフォントファイルなどもURLかdata URI scheme文字列で指定することができます。

Gmailで自動返信メールシステムを構築する方法

プログラミング

Gmail Labsで公開されている「返信定型文」と「フィルタ」機能を組み合わせて自動返信メールを実現することはできますが,いくつか制限があります。

  • 自動返信メールの返信元アドレスに「+canned.response」が付く。
  • 返信文にあいての名前などを動的に挿入することができない。

そこで,Google Apps Script を使って自動返信メールシステムを作成してみました。 Googleドキュメントのスプレッドシートをベースとし,その中にスクリプトや返信文書のテンプレートなどのシートを置くものとします。

スクリプトの作成

以下に,今回作成したスクリプトを示します。

/**
 * 自動応答メールを送信する。
 *
 * Created by: Field Works, LLC / 合同会社フィールドワークス
 * Reference: http://www.field-works.co.jp/
 * Date: 2012/03/10
 */

var BCC = 'admin@example.co.jp';

// 書式変換
function format(templ, params) {
  return templ.replace(/\${(.*?)}/g, function($0, $1) {
    return (params[$1] && typeof(params[$1]) != "object") ?
      params[$1].toString() : JSON.stringify(params[$1]);
  });
}

// 「template」シートからメール本文を取得する。
function getMessage(ss, row, params) {
  var templ = ss.getSheetByName('template');
  var r = templ.getRange('A2:B99');
  var subject = r.getCell(row, 1).getValue();
  var body = r.getCell(row, 2).getValue();
  return {subject: format(subject, params), body: format(body, params)};
}

// 送信結果を「log」シートに記録する。
function setResult(ss, params) {
  var log = ss.getSheetByName('log');
  var r = log.getRange('A2:H999');
  // 空いている行を見つける。
  for (var i = 1; i <= r.getNumRows(); ++i) {
    if (r.getCell(i, 1).getValue() == "") {
      break;
    }
  }
  r.getCell(i, 1).setValue(params.TIMESTAMP);
  r.getCell(i, 2).setValue(params.COMPANY);
  r.getCell(i, 3).setValue(params.NAME);
  r.getCell(i, 4).setValue(params.ADDRESS);
  r.getCell(i, 5).setValue(params.TEL);
  r.getCell(i, 6).setValue(params.COMMENT);
  r.getCell(i, 7).setValue(params.RESULT);
  r.getCell(i, 8).setValue(params.ERROR);
}

// メール送信
function sendEMail(to, subject, body, opt) {
  try {
    MailApp.sendEmail(to, subject, body, opt);
  } catch (e) {
    return {result: 'NG', message: e.message};
  }
  return {result: 'OK', message: ""};
}

// メール本文からパラメータを抽出する。
function extractMessage(body)
{
  var lines = body.split("\n");
  var name = "";
  var company = "";
  var address = "";
  var tel = "";
  var comment = "";
  for (var i = 0; i < lines.length; ++i) {
    var line = lines[i];
    if (line.match(/^お名前: (.+)/)) {
      name = RegExp.$1;
    }
    if (line.match(/^会社/団体名: (.+)/)) {
      company = RegExp.$1;
    }
    if (line.match(/^メールアドレス: (.+)/)) {
      address = RegExp.$1;
    }
    if (line.match(/^電話番号: (.+)/)) {
      tel = RegExp.$1;
    }
    if (line.match(/^メッセージ: (.+)/)) {
      comment = RegExp.$1;
      while (lines[i+1] != "") {
        comment += "\n";
        comment += lines[i+1];
        ++i;
      }
    }
  }
  return {NAME: name, COMPANY: company, ADDRESS: address, TEL: tel,
               TO: name + " <" + address + ">", COMMENT: comment}
}

// 新着メールに対して,自動応答メールを送信する。
function checkMail() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var threads = GmailApp.getInboxThreads();
  for (var i = 0; i < threads.length; i++) {
    Utilities.sleep(1000);
    var msgs = threads[i].getMessages();
    for (var j = 0; j < msgs.length; j++) {
      var msg = msgs[j];
      // 新着メールを確認する。
      if (msg.isUnread() && msg.getTo() == "info+q@example.co.jp") {
        // メール本文からパラメータを抽出する。
        var params = extractMessage(msgs[j].getBody());
        // タイムスタンプを加える。
        params["TIMESTAMP"] = msgs[j].getDate();
        // 応答メールのテンプレートを取得する。
        var send = getMessage(ss, 1, params);
        // 応答メールを送信する。
        if (params.ADDRESS != "") {
          var ret = sendEMail(params.TO, send.subject, send.body, {bcc: BCC});
          // 送信結果を加える。
          params["RESULT"] = ret.result;
          params["ERROR"] = ret.message;
          // 送信結果を記録する。
          Logger.log(params);
          setResult(ss, params);
        }
        // メールを既読にし,アーカイブへ移動させる。
        msg.markRead();
        threads[i].moveToArchive();
      }
    }
  }
}

Gmailメッセージの取得

checkMail()では,Gmailの受信ボックスをチェックし自動返信対象となるメールの着信を確認します。

Gmailに届いたメールのスレッドやメッセージを取得するには,「GmailApp」オブジェクトを使います。 受信トレイのスレッドを取得するには,以下のメソッドを使用します。

var threads = GmailApp.getInboxThreads()

さらに,スレッドの中からメッセージの配列を取り出します。

var msgs = threads[i].getMessages();

メッセージ配列から取り出したGmailMessageオブジェクトが1通のメールメッセージに対応します。

var msg = msgs[j];

次に,Gmailに届いたメールの中から,自動返信の対象となるメッセージを探します。 今回は,未読かつ info+q@example.co.jp 宛に届いたメールという条件で振り分けています。

if (msg.isUnread() && msg.getTo() == "info+q@example.co.jp") {

自動返信処理が終わったメッセージは既読状態にしておかないと,なんどもメールを送信してしまいます。 処理の最後で既読状態にし,さらにアーカイブへ移動させています。 これは,受信トレイに残るメールを減らして次回以降の無駄な処理を省くためです。

msg.markRead(); threads[i].moveToArchive();

GmailMessageオブジェクトからは,getSubject()で件名,getBody()でメール本文のように様々なプロパティを取り出すことができます。 詳しくは,Google Apps Scriptのリファレンス・マニュアルを参照してください。

なお,スレッドを1件処理する度に1秒間のスリープを入れていますが, これはスリープを入れずに実行したところ「アクセスが頻繁すぎる」旨の警告が出されたためです。 実際には1秒は長すぎると思いますが,余裕を見て1秒に設定しています。

メール本文の解析

メール本文は,以下のフォーマットで送られてくるものとしています。

お名前: <名前>

メールアドレス: <メールアドレス>

会社/団体名: <会社名>

電話番号: <電話番号>

メッセージ: <メッセージ>

extractMessage(body)関数では,メール本文の内容を解析し必要な情報を取り出します。 解析結果は,NAME, COMPANY, ADDRESS, TEL, TO, COMMENT を属性としたオブジェクトとして返します。

返信文の組み立て

スプレッドシートの「template」シートを作成し,返信文のテンプレートを置きます。

「template」シートの1行目は見出しで,2行目に返信文の件名と本文を記述します。 件名や本文の中で「${NAME}」の様に記述すると,受信メールから抜き出した情報に置き換わります。

templateシート

応答メールの送信と送信結果の記録

sendEMail(to, subject, body, opt)関数の中では,MailApp.sendEmail()を使ってメールを送信します。

setResult(ss, params)関数では,「log」シートに送信結果を書き込み記録として残します。

logシート

設定手順

スプレッドシートの作成

Googleドキュメントでスプレッドシートを作成し任意の名前をつける(ここでは「自動応答メール」とした)。

templateシートを作成し,1行目に見出し(Subject, Body)を記述する。

logシートを作成し,同じく1行目に見出し(タイムスタンプ, 会社/団体名, お名前, メールアドレス, 電話番号, ご質問/ご意見, 結果, エラーメッセージ)を記述する。

「ツール/スクリプトエディタ…」メニューを選択肢,スクリプトエディタを開く。 「コード」欄に上記のスクリプトを貼り付ける。

スクリプトの修正

スクリプト中のBCCの設定(不要の場合は,空文字列にすれば良いと思います)

var BCC = 'admin@example.co.jp';

や対象メールの判定条件

if (msg.isUnread() && msg.getTo() == "info+q@example.co.jp") {

を必要に応じて修正します。

また,メール本文の書式も異なると思いますので,extractMessage()の中身を修正します。

テンプレートの修正

メールの件名と本文を修正します。

トリガーの設定

最初に手動で「checkMail」を実行します。

以下のような警告が出るはずなので「Authorize」ボタンを押して認証を通します。 この作業は,スクリプトを修正する度に必要になります。

トリガーの設定

手動で実行して動作確認ができたら,トリガー実行の設定を行います。

「リソース/現在のスクリプトのトリガー…」メニューを選択して,下記のダイアログを表示させます。 15分間隔に設定しました。

参考

MacVimをコマンドラインから起動する方法

HOWTO

MacVimにはmvimコマンドが付属していて,ターミナルからMacVimを起動する事ができます。 香り屋版でもそのまま使えたので,便利に使っていました。

Lionになってから,MacVimが起動していない状態でmvimを実行すると,以下のようなエラーが出力されるようになりました。

$ 2012-08-08 09:06:18.768 MacVim[2088:407] ApplePersistenceIgnoreState: 
 Existing state will not be touched. New state will be written to
 /var/folders/0h/_jh2qmy52b96v_f87h_y1jhr0000gn/T/org.vim.MacVim.savedState

特に実害はなかったのですが,回避策としてopenコマンドを使うことにしました。

$ open -a MacVim <ファイル名>

aliasを定義すれば,タイプ量を減らせます。

alias gvim='open -a MacVim'

mvimコマンドとの違いとして,復数のファイルを指定した場合の振る舞いが異なります。

mvimで起動した場合はタブを一つのみ作成しますが,openコマンドで起動した場合はファイル数分のタブを作成します。