かさね打ちもどき overstrike

Kernighan & Plauger Software Tools (1976) の 2.3 節はバックスペースを解釈してラインプリンタでタイプライタのようなかさね打ちをするプログラムを扱っています。そのプログラムでは、バックスペースを見つけるごとにかさね打ち用の行を新しく出力する単純な方法をとっています。

ラインプリンタはともかくとして、他の出力方法としてプリンタ用の Postscript を組むか、HTML を組む方法もありでしょう。けれど、ここではひねくれて、 ANSI 端末のエスケープ・シーケンスを利用して、かさね打ちもどきを端末出力でやってみることにします。使うエスケープ・シーケンスは太字、下線、赤色の 3 つとします。話を簡単にするために、7ビット ASCII だけを対象にします。

#include <string>
#include <iostream>

void splitline (std::string const& src, std::string& norm, std::string& strike);
void ansiattr (std::string const& norm, std::string const& strike, std::string& dst);

int main (int argc, char* argv[])
{
    std::string srcline;
    while (std::getline (std::cin, srcline)) {
        std::string normline;
        std::string strikeline;
        std::string dstline;
        splitline (srcline, normline, strikeline);
        ansiattr (normline, strikeline, dstline);
        std::cout << dstline << std::endl;
    }
    return EXIT_SUCCESS;
}

まず、入力行ごとに、2つの行へ内容を分割します。一方は、重ね打ちされる行で、もう一つは重ね打ちする行とします。普段は重ね打ちされる行へ文字を移していき、重ね打ちする行にはブランクを追加します。バックスペースによって、重ね打ちする行への書き出しを始めて、後退した分の出力が終わると、普段の重ね打ちされる行への文字追加に戻します。

void splitline (std::string const& src, std::string& norm, std::string& strike)
{
    int backs = 0;
    for (auto c : src) {
        if ('\b' == c) {
            if (! strike.empty ()) {
                ++backs;
                strike.pop_back ();
            }
        }
        else if (backs > 0) {
            strike.push_back (c);
            --backs;
        }
        else {
            norm.push_back (c);
            strike.push_back (' ');
        }
    }
}

続いて、重ね打ちされる行と重ね打ちする行の両方を比較しながら、ANSI エスケープ・シーケンスを組んでいきます。今回は、楽をして、一文字ごとに文字属性をつけては戻してを繰り返すことにします。

void ansiattr (std::string const& norm, std::string const& strike, std::string& dst)
{
    std::string ansibold ("\033[1m");
    std::string ansiunder ("\033[4m");
    std::string ansistrike ("\033[9m");
    std::string ansired ("\033[31m");
    std::string ansinorm ("\033[0m");
    for (int i = 0; i < norm.size (); ++i) {
        if (i >= strike.size () || ' ' == strike[i])
            dst.push_back (norm[i]);
        else if ('_' == strike[i]) {
            dst.append (ansiunder);
            dst.push_back (norm[i]);
            dst.append (ansinorm);
        }
        else if ('-' == strike[i]) {
            dst.append (ansistrike);
            dst.push_back (norm[i]);
            dst.append (ansinorm);
        }
        else if (strike[i] != norm[i]) {
            dst.append (ansired);
            dst.push_back (strike[i]);
            dst.append (ansinorm);
        }
        else if (strike[i] == norm[i]) {
            dst.append (ansibold);
            dst.push_back (norm[i]);
            dst.append (ansinorm);
        }
        else
            dst.push_back (norm[i]);
    }
}

試しに、Perl でバックスペース混じり文字列を書いて、このプログラムへ渡すと、下線、かさね線、太字、赤色文字になって表示されます。下線を引くにはアンダースコアをかさね打ちします。かさね線を引くにはマイナスをかさね打ちします。太字にするには、xterm と同じで同じ文字をかさね打ちします。赤色文字にするには、別の文字をかさね打ちします。赤色の場合は、かさね打ちした方の文字を表示します。

$ perl -E 'say "ex. u\b_n\b_der\b\b\b___ strike\b\b\b\b\b\b------ b\bbo\bold\b\bld *\br**\b\bed."' | ./overstrike