楽譜描画ライブラリVexFlowを使ってみた

最近、楽譜学習アプリを作っており楽譜を描画する必要があるので、なにか便利なライブラリがないか調べていたら VexFlowというJavaScript製のライブラリがあることを知りました。

f:id:mrk21:20180603232441p:plain

このライブラリは、JavaScriptで五線や音符を操作するAPIを呼び出すことでSVGもしくはHTML5 Canvasを用いて描画するようです。

たとえば、以下のようなコードを書くと次のような楽譜がSVGで描画されます。

import { Flow as VF } from 'vexflow';

const div = document.getElementById('container');
const renderer = new VF.Renderer(div, VF.Renderer.Backends.SVG);
renderer.resize(500, 200);

const context = renderer.getContext();
const stave = new VF.Stave(10, 40, 400);

stave.addClef("treble").addTimeSignature("4/4");
stave.setContext(context).draw();

const notes = [
  new VF.StaveNote({clef: "treble", keys: ["c/4"], duration: "q" }),
  new VF.StaveNote({clef: "treble", keys: ["d/4"], duration: "q" }),
  new VF.StaveNote({clef: "treble", keys: ["b/4"], duration: "qr" }),
  new VF.StaveNote({clef: "treble", keys: ["c/4", "e/4", "g/4"], duration: "q" })
];

const voice = new VF.Voice({num_beats: 4,  beat_value: 4});
voice.addTickables(notes);

const formatter = new VF.Formatter().joinVoices([voice]).format([voice], 400);
voice.draw(context, stave);

f:id:mrk21:20180603232247p:plain

静的な楽譜だけではなくて、アニメーションやインタラクティブなものも作れます。SVGで描画した場合はアニメーションはCSSで設定できるようです。また描画要素をグループ化できるのでわりと複雑なこともできそうです。


音符の動的追加

f:id:mrk21:20180603232904g:plain


音符のアニメーション

f:id:mrk21:20180604001147g:plain

楽譜ファイルフォーマットのMusicXMLにも対応しているようなので、楽譜を描画したい場合はこのライブラリを使えば良さそうです。

なお、調査する上で書いたコードは以下にあります。

github.com

GitHubのContributionsを埋めるゲームを全クリした

去年の4月から1日1コミットを目標にしていましたが、先日全部埋めることができました。

f:id:mrk21:20150415112811p:plain

正直いって結構しんどかったですが、常に進捗を意識できたのと、コミットの単位を短くなるようにできたのは良かったと思います。

しかし、体調があまり良くない時は正直休んだほうがいいし、コミットすることが一番大事みたいになりがちで、パフォーマンスなどを考えると良いとはいえないことも確かです。

現在もこの目標を継続中ですが、今後はやめるかもしれません。

WebMock のしくみを調査

前回のエントリーで cpp-netlib の client をモックにすげ替えることに成功しましたが、 任意のレスポンスを返すためにはどのようしたらいいか、という点は述べていませんでした。 このモックは、RubyWebMock のような設計にしようと考えているので、 WebMock がどのような設計になっているのかを調査しました。なお、調査した WebMock のバージョンは 1.20.4 となります。

WebMock 概要

まず、WebMock を用いて http://www.example.com/ にGETメソッドでアクセスした場合に、メッセージボディが "test" であるレスポンスを返すためには以下のようにします。

require 'net/http'
require 'webmock'

webmock::api.stub_request(:get, 'www.example.com').to_return(body: "test")

client = Net::HTTP.start("www.example.com")
response = client.get("/")
puts response.body # test

これにより、Net::HTTPhttp://www.example.com/ にアクセスしなくなり、かわりにモックで設定したレスポンスを返すようになります。

モックオブジェクトの生成と管理

ここで、モックオブジェクトの生成を行う WebMock::API::stub_request() メソッドは何をしているのでしょうか? このメソッドの定義は、以下のようになっています。

webmock/api.rb

module WebMock
  module API
    # ...
    def stub_request(method, uri)
      WebMock::StubRegistry.instance.
        register_request_stub(WebMock::RequestStub.new(method, uri))
    end
    # ...
  end
end

まず、WebMock::RequestStub というモックオブジェクトを生成します。 このオブジェクトでは、to_return() というレスポンスを設定するメソッドや、 with() メソッドというリクエストの条件を設定するメソッドが定義されています。 また、リクエストの条件は request_pattern プロパティに WebMock::RequestPattern というオブジェクトが保持されています。

webmock/request_stub.rb

module WebMock
  class RequestStub
    attr_accessor :request_pattern
    
    def initialize(method, uri)
      @request_pattern = RequestPattern.new(method, uri)
      # ...
    end
    
    def with(params = {}, &block)
      @request_pattern.with(params, &block)
      # ...
    end
    
    def to_return(*response_hashes, &block)
      # ...
    end
    # ...
  end
end

次に、WebMock::StubRegistry というモックオブジェクトを管理するオブジェクトの register_request_stub() メソッドに生成したモックオブジェクトを渡しています。 このメソッドでは渡された WebMock::RequestStub オブジェクトを request_stubs プロパティに追加します。request_stubs に保持されているモックオブジェクトは必要に応じて探索されます。

webmock/stub_registry.rb

module WebMock
  class StubRegistry
    include Singleton
    
    attr_accessor :request_stubs
    
    def initialize
      reset!
    end
    # ...
    def reset!
      self.request_stubs = []
    end
    # ...
    def register_request_stub(stub)
      request_stubs.insert(0, stub)
      stub
    end
    # ...
  end
end

モックオブジェクトの探索と実行

では Net::HTTP がリクエストを送るときは、どのような処理になっているのでしょうか?

WebMockは require されると、Net::HTTPWebMock::HttpLibAdapters::NetHttpAdapter@webMockNetHTTP に置き換えます。そして、Net::HTTP#request() メソッドが呼ばれた時に以下の動作をするようになります。

webmock/http_lib_adapters/net_http.rb

module WebMock
  module HttpLibAdapters
    class NetHttpAdapter < HttpLibAdapter
      # ...
      @webMockNetHTTP = Class.new(Net::HTTP) do
        # ...
        def request(request, body = nil, &block)
          request_signature = WebMock::NetHTTPUtility.request_signature_from_request(self, request, body)
          # ...
          if webmock_response = WebMock::StubRegistry.instance.response_for_request(request_signature)
            # ...
          elsif WebMock.net_connect_allowed?(request_signature.uri)
            # ...
          else
            raise WebMock::NetConnectNotAllowedError.new(request_signature)
          end
        end
        # ...
      end
      # ...
    end
  end
end

まず、WebMock::NetHTTPUtility::request_signature_from_request() メソッドにより WebMock::RequestSignature オブジェクト request_signature を生成します。このオブジェクトは、各アダプターのリクエストを正規化し保持するためのものです。

つぎに、生成した request_signature オブジェクトを WebMock::StubRegistry#response_for_request() メソッドに渡し、 内部で request_stub_for() メソッドを呼び出します。 そして、request_signature オブジェクトに一致するモックオブジェクトを request_stubs の中から各モックオブジェクトの request_pattern プロパティの matches?() メソッドを用いて線形探索します。

最後に、探索したモックオブジェクトに設定されているレスポンスを返します。このとき、一致するモックオブジェクトが存在しなければ、例外を投げます。ただし、WebMock::Config#allow_net_connecttrue の時は、実際にHTTPアクセスをします。

webmock/stub_registry.rb

module WebMock
  class StubRegistry
    # ...
    def response_for_request(request_signature)
      stub = request_stub_for(request_signature)
      stub ? evaluate_response_for_request(stub.response, request_signature) : nil
    end
    
    private
    
    def request_stub_for(request_signature)
      (global_stubs + request_stubs).detect { |registered_request_stub|
        registered_request_stub.request_pattern.matches?(request_signature)
      }
    end
    
    def evaluate_response_for_request(response, request_signature)
      response.dup.evaluate(request_signature)
    end
  end
end

WebMock::RequestPattern について

上記でモックオブジェクトの探索方法について述べましたが、具体的にはどのように request_signature に一致しているか判定しているのでしょうか?

WebMock::RequestPattern の定義は下記のようになっています。

webmock/request_pattern.rb

module WebMock
  class RequestPattern
    attr_reader :method_pattern, :uri_pattern, :body_pattern, :headers_pattern
    
    def initialize(method, uri, options = {})
      @method_pattern  = MethodPattern.new(method)
      @uri_pattern     = create_uri_pattern(uri)
      @body_pattern    = nil
      @headers_pattern = nil
      @with_block      = nil
      assign_options(options)
    end
    
    def with(options = {}, &block)
      raise ArgumentError.new('#with method invoked with no arguments. Either options hash or block must be specified.') if options.empty? && !block_given?
      assign_options(options)
      @with_block = block
      self
    end
    
    def matches?(request_signature)
      content_type = request_signature.headers['Content-Type'] if request_signature.headers
      content_type = content_type.split(';').first if content_type
      @method_pattern.matches?(request_signature.method) &&
        @uri_pattern.matches?(request_signature.uri) &&
        (@body_pattern.nil? || @body_pattern.matches?(request_signature.body, content_type || "")) &&
        (@headers_pattern.nil? || @headers_pattern.matches?(request_signature.headers)) &&
        (@with_block.nil? || @with_block.call(request_signature))
    end
    # ...
  end
  
  class MethodPattern
    # ...
  end
  
  class URIPattern
    # ...
  end
  
  class BodyPattern
    # ...
  end
  
  class HeadersPattern
    # ...
  end
end

これを見ると、まずオブジェクト生成時の対象HTTPメソッド・URL、with() メソッドで設定された条件(リクエストHTTPヘッダの有無)をもとにパターンを表すオブジェクトを生成し保持します。

そして matches?() メソッドで、これらパターンすべてにマッチする時に一致したと判断し、true を返します。

さいごに

というわけで、WebMock のしくみがだいたいわかったので、これを参考に設計していきます。

cpp-netlib にモックがほしい

C++でネットワークプログラミングをするときは、cpp-netlib という Boost.Asio をベース としたライブラリを使っているのですが、モックがほしいことがあります。 例えば下記のコードで、http://www.boost.org に実際にアクセスするのではなく任意のレスポンスを返したい。cpp-netlib のモックライブラリがないか探しましたが、どうやら無いようなので作成する ことにしました。なお、cpp-netlib のバージョンは 0.11.1RC2 となります。

main.cpp

#include <iostream>
#include <boost/network/protocol/http/client.hpp>

int main() {
    namespace network = boost::network;
    namespace http = network::http;
    using tag = http::tags::http_async_8bit_tcp_resolve;
    using client_type = http::basic_client<tag,1,1>;
    
    client_type::request request("http://www.boost.org");
    request << network::header("Connection","close");
    client_type client;
    client_type::response response = client.get(request);
    
    std::cout << "#response" << std::endl;
    std::cout << http::status(response) << std::endl;
    std::cout << http::body(response) << std::endl;
    
    for (auto && h: http::headers(response)) {
        std::cout << h.first << ": " << h.second << std::endl;
    }
    
    return 0;
}

boost::network::http::basic_client の調査

まず、http::basic_client では、http::basic_client_facade を継承していて、get() などのメンバ関数はこのクラスで定義されています。 これらのメンバ関数ではネットワークアクセスの詳細は http::basic_client_impl クラスの request_skeleton() メンバ関数に委譲されています。

cpp-netlib/boost/network/protocol/http/client.hpp

template <class Tag, unsigned version_major, unsigned version_minor>
struct basic_client : basic_client_facade<Tag, version_major, version_minor> {
    // ...
};

cpp-netlib/boost/network/protocol/http/client/facade.hpp

template <class Tag, unsigned version_major, unsigned version_minor>
struct basic_client_facade {
    // ...
    typedef basic_client_impl<Tag, version_major, version_minor> pimpl_type;
    // ...
    response get(
        request const & request,
        body_callback_function_type body_handler = body_callback_function_type()
    ) {
        return pimpl->request_skeleton(
            request,
            "GET",
            true,
            body_handler,
            body_generator_function_type()
        );
    }
    // ...
protected:
    boost::shared_ptr<pimpl_type> pimpl;
    // ...
};

次に、basic_client_impl クラスは http::impl::client_base::type クラスを継承していて、Tag テンプレート引数により特殊化して実装を分けています。 例えば、Taghttp::tags::http_async_8bit_tcp_resolve の場合は、http::impl::async_client が選択されます。request_skeleton() はこの client_base::type クラスで定義されます。

cpp-netlib/boost/network/protocol/http/client/pimpl.hpp

namespace impl {
    // ...
    template <class Tag, unsigned version_major, unsigned version_minor,
        class Enable = void>
    struct client_base {
        typedef unsupported_tag<Tag> type;
    };
    
    template <class Tag, unsigned version_major, unsigned version_minor>
    struct client_base<Tag, version_major, version_minor,
        typename enable_if<is_async<Tag> >::type> {
        typedef async_client<Tag, version_major, version_minor> type;
    };
    
    template <class Tag, unsigned version_major, unsigned version_minor>
    struct client_base<Tag, version_major, version_minor,
        typename enable_if<is_sync<Tag> >::type> {
        typedef sync_client<Tag, version_major, version_minor> type;
    };
}

template <class Tag, unsigned version_major, unsigned version_minor>
struct basic_client_impl :
    impl::client_base<Tag, version_major, version_minor>::type {
    // ...
};

cpp-netlib/boost/network/protocol/http/client/async_impl.hpp

namespace impl {
template <class Tag, unsigned version_major, unsigned version_minor>
struct async_client : connection_policy<Tag, version_major, version_minor>::type {
    // ...
    basic_response<Tag> const request_skeleton(
        basic_request<Tag> const & request_,
        string_type const & method,
        bool get_body,
        body_callback_function_type callback,
        body_generator_function_type generator
    );
    // ...
};

最後に、client_baseクラスを特殊化するにあたって、Tag がどのように定義されているかを調べます。 Tag の定義は http::tagstags にあります。 これらを見ると、非同期通信かどうかTPCかどうかなどの特性を型として定義し、型リストを用いて組み合わせ、BOOST_NETWORK_DEFINE_TAG マクロで型リストの各型をすべて継承する型を作成しているようです。

つまり、mock という特性型を作成し、http_mock_8bit_tcp_resolve というタグ型を作り、client_base を特殊化し、mock_client というクライアント型を作成すれば良さそうです。

cpp-netlib/boost/network/protocol/http/tags.hpp

struct http {
  typedef mpl::true_::type is_http;
};
// ...
typedef mpl::vector<http, client, simple, async, tcp, default_string>
    http_async_8bit_tcp_resolve_tags;
// ...
BOOST_NETWORK_DEFINE_TAG(http_async_8bit_tcp_resolve);
// ...

cpp-netlib/boost/network/tags.hpp

// ...
struct async {
  typedef mpl::true_::type is_async;
};
// ...
template <class Tag>
struct components;

// Tag Definition Macro Helper
#ifndef BOOST_NETWORK_DEFINE_TAG
#define BOOST_NETWORK_DEFINE_TAG(name)                                        \
  struct name                                                                 \
      : mpl::inherit_linearly<name##_tags,                                    \
                              mpl::inherit<mpl::placeholders::_1,             \
                                           mpl::placeholders::_2> >::type {}; \
  template <>                                                                 \
  struct components<name> {                                                   \
    typedef name##_tags type;                                                 \
  };
#endif  // BOOST_NETWORK_DEFINE_TAG
// ...

モックの作成

まず、タグ型を定義します。

mock.hpp

namespace boost { namespace network {
namespace tags {
    struct mock {
        using is_mock = mpl::true_::type;
    };
}
namespace http { namespace tags {
    using http_mock_8bit_tcp_resolve_tags =
        mpl::vector<http, client, simple, mock, tcp, default_string>;
    
    BOOST_NETWORK_DEFINE_TAG(http_mock_8bit_tcp_resolve);
}}
}

次に、これを用いて client_base を特殊化します。

mock.hpp

namespace boost { namespace network {
    template <class Tag, class Enable = void>
    struct is_mock : mpl::false_ {};
    
    template <class Tag>
    struct is_mock<Tag, typename enable_if<typename Tag::is_mock>::type> : mpl::true_ {};
    
namespace http { namespace impl {
    template <class Tag, unsigned version_major, unsigned version_minor>
    struct client_base<Tag, version_major, version_minor,
        typename enable_if<is_mock<Tag>>::type>
    {
        using type = mock_client<Tag, version_major, version_minor>;
    };
}}
}}

最後に、mock_client を作成し、request_skeleton() では適当なレスポンスを作成し返すようにします。

mock.hpp

// ...
namespace boost { namespace network { namespace http { namespace impl {
    template <class Tag, unsigned version_major, unsigned version_minor>
    struct mock_client {
        using resolver_type = typename resolver<Tag>::type;
        using string_type = typename string<Tag>::type;
        using body_callback_function_type = function<
            void (iterator_range<char const *> const &, system::error_code const &)>;
        using body_generator_function_type = function<bool(string_type &)>;
        
        mock_client(
            bool cache_resolved,
            bool follow_redirect,
            bool always_verify_peer,
            int timeout,
            boost::shared_ptr<boost::asio::io_service> service,
            optional<string_type> const & certificate_filename,
            optional<string_type> const & verify_path,
            optional<string_type> const & certificate_file,
            optional<string_type> const & private_key_file
        ) {}
        
        void wait_complete() {}
        
        basic_response<Tag> const request_skeleton(
            basic_request<Tag> const & request,
            string_type const & method,
            bool get_body,
            body_callback_function_type callback,
            body_generator_function_type generator
        ) {
            std::cout << "#request" << std::endl;
            std::cout << method << std::endl;
            std::cout << request.uri().string() << std::endl;
            for (auto && h: network::headers(request)) {
                std::cout << h.first << ": " << h.second << std::endl;
            }
            
            basic_response<Tag> result;
            result
                << http::status(200)
                << network::header("Content-Type","text/plane")
                << network::header("Content-Length","4")
                << network::body("test");
            return result;
        }
    };
}}}}

これで、モックができました。実際に使用してみます。

main.cpp

#include <iostream>
#include <boost/network/protocol/http/client.hpp>
#include "mock.hpp"

int main() {
    namespace network = boost::network;
    namespace http = network::http;
#if 1
    using tag = http::tags::http_mock_8bit_tcp_resolve;
#else
    using tag = http::tags::http_async_8bit_tcp_resolve;
#endif
    using client_type = http::basic_client<tag,1,1>;
    
    client_type::request request("http://www.boost.org");
    request << network::header("Connection","close");
    client_type client;
    client_type::response response = client.get(request);
    
    std::cout << "#response" << std::endl;
    std::cout << http::status(response) << std::endl;
    std::cout << http::body(response) << std::endl;
    
    for (auto && h: http::headers(response)) {
        std::cout << h.first << ": " << h.second << std::endl;
    }
    
    return 0;
}

実行結果

$ ./main
#request
GET
http://www.boost.org
Connection: close
#response
200
test
Content-Length: 4
Content-Type: text/plane

どうやらうまくいったようです。あとは任意のレスポンスを注入する機能を実装すればOKです。

FirebrewのAnsibleモジュールを作ってみた

FirefoxのアドオンをCUIから管理するツールを作ってみたで紹介したFirebrewのAnsibleモジュールを作ってみました。GitHubにて公開しています。

インストール方法

このモジュールを使用したい Ansible Playbook があるディレクトリ直下にlibrary/ディレクトリを作成し、そこにコピーするだけです。

$ cd <Ansible playbookのあるディレクトリ>
$ mkdir -p library/
$ cd library/
$ git clone https://github.com/mrk21/ansible-lib.git
./
├── library/
│   └── ansible-lib/
├── inventory.ini
└── site.yml

使用方法

基本的には次のように使用します。

---
- name: Vimperator のインストール
  firebrew: name=Vimperator state=present

- name: Japanese Language Pack のアンインストール
  firebrew: name='Japanese Language Pack' state=absent

詳細なオプション

パラメータ 必須 デフォルト値 有効な値 概要
name はい アドオン名
state いいえ present present, absent インストールするかアンインストールするか
base_dir いいえ profiles.ini のあるディレクトリ
profile いいえ プロファイル名
firefox いいえ Firefoxコマンドのパス

FirefoxのアドオンをCUIから管理するツールを作ってみた

先日、Firebrewというツールを作成し、RubyGemsにて公開しました。 Githubにて開発しています。

コレは何?

FirefoxアドオンをCUIからインストールしたりアンインストールしたりできる、いわゆるパッケージマネージャーと呼ばれるものです。 これを用いれば各種アドオンのインストール・アンインストールを自動化することができます。 たとえば、Firefoxのキラーアドオンの一つであるVimperatorをインストールする場合は以下のようにします。

$ firebrew install Vimperator

なぜ作ったの?

現在ぼくは、VimやGitなど基本的な開発環境のインストール・設定はプロビジョニングツールであるAnsibleを用いて行っています。 これにより新しい環境への移行が簡単になります。

この基本的な開発環境にはブラウザであるFirefoxも入っているので、インストールおよび設定を自動化したいと考えました。

Firefoxのインストールは、MacであればHomebrew Cask、Linuxであればapt-getやyumなどの各種パッケージマネージャーを用いれば良さそうです。 Vimperatorの設定ファイルである.vimperatorrcなどは指定の場所にコピーすれば問題ないでしょう。 しかし、アドオンのインストールを自動化する既存のよい方法がありませんでした。

そんななか、Firefox開発元のMozillaThe generic AMO APIというアドオンの情報を取得できるAPIを公開していることがわかりました。 このAPIを用いればアドオンのGUIDやダウンロード元URLなどを取得することが出来ます。これはアドオンの自動インストールを行うにあたって十分な情報です。

そして、アドオンのインストール・アンインストールをするにあたって、インストール先のプロファイルパスの取得にはprofiles.iniを、 インストールされているアドオン一覧を取得するには、プロファイルディレクトリのextensions.jsonをそれぞれ参照すれば良さそうです。

これらを踏まえて、Firefoxのアドオンのパッケージマネージャーを作成することにしました。

どうやって使うの?

さて、肝心の使用方法ですが、まずはFirebrewのインストールを行わなければなりません。 幸いにもFirebrewはRubyのgemとして作成されているので、インストールはgemコマンドを用いるだけです(ただし実行にOpenSSLを必要とするので、Windowsだと面倒くさいかもしれません)。

$ gem install firebrew

Firebrewのインストールが終わったところで使用方法を解説しましょう。 まず、firebrewコマンドの基本的な構造は下記のとおりです。

$ firebrew [--help] [--version]
           [--base-dir=<path>] [--profile=<name>] [--firefox=<path>]
           <command> [<args>]

そして、主な機能として以下があります。

  • アドオンのインストール
  • アドオンのアンインストール
  • 公開されているアドオンの検索
  • アドオンの詳細な情報の取得
  • インストール済みアドオンの取得

以下、これらをそれぞれ解説していきます。

アドオンのインストール

名前がnameであるアドオンをインストールします。

$ firebrew install <name>

アドオンのアンインストール

名前がnameであるアドオンをアンインストールします。

$ firebrew uninstall <name>

公開されているアドオンの検索

名前がtermにマッチするアドオン一覧を表示します。

$ firebrew search <term>

アドオンの詳細な情報の取得

名前がnameであるアドオンの詳細な情報を表示します。

$ firebrew info <name>

インストール済みアドオンの取得

インストール済みアドオンの一覧を表示します。

$ firebrew list

また、以下のオプションが使用可能です。

--base-dir

profiles.iniがあるディレクトリを指定します。

-d <path>, --base-dir=<path>

デフォルト値は下記のとおりです。

プラットフォーム
Mac OS X ~/Library/Application Support/Firefox
Linux ~/.mozilla/firefox
Windows %APPDATA%\Mozilla\Firefox

この値はFIREBREW_FIREFOX_PROFILE_BASE_DIR環境変数で上書き可能です。

--profile-name

対象となるFirefoxプロファイル名を指定します。

-p <name>, --profile=<name>

デフォルト値はdefaultで、FIREBREW_FIREFOX_PROFILE環境変数で上書き可能です。

--firefox

firefoxコマンドのパスを指定します。

-f <path>, --firefox=<path>

デフォルト値は下記のとおりです。

プラットフォーム
Mac OS X /Applications/Firefox.app/Contents/MacOS/firefox-bin
Linux /usr/bin/firefox
Windows %PROGRAMFILES%\Mozilla Firefox\firefox.exe または %PROGRAMFILES(X86)%\Mozilla Firefox\firefox.exe

この値はFIREBREW_FIREFOX環境変数で上書き可能です。

最後に

これでFirefoxの構築を自動化できるようになりました。 また、せっかくAnsibleを使っているので、Ansibleモジュールを作ってみようかと思います。

※2014/07/18 追記: Firebrew用のAnsibleモジュールを作成しました → FirebrewのAnsibleモジュールを作ってみた

indentLineプラグインを導入するとYAMLの構文強調がおかしくなる

Vimにはインデント階層を可視化してくれる indentLine というプラグインがあります。 このプラグイン、便利なので僕も使っているのですが、ひとつ問題点があります。このプラグインを導入すると、YAMLの構文強調がおかしくなるのです。

Fig. 1 indentLine無効時

f:id:mrk21:20140625182816p:plain

Fig. 2 indentLine有効時

f:id:mrk21:20140625182825p:plain

ふーむ、どうやらインデントされたブロックマッピングのキー部分がハイライトされてないですね。これはどういうことだろうと、indentLineのソースを読むと、after/plugin/indentLine.vimsyntax match ...部分が悪さをしていることが分かりました。

Fig. 3 after/plugin/indentLine.vim, 81行目

execute 'syntax match IndentLine /\%(^\s\+\)\@<=\%'.i.pattern.' / containedin=ALL conceal cchar=' . g:indentLine_char

つまり、indentLineの上記コードにおいて、/^\s\+/を喰っちゃってるので、ハイライトがされなくなったわけです。 この問題を解決するためには、ブロックマッピングのマッチングルールを変更する必要があるわけですが、どうやってやろう…