﻿#!/usr/local/bin/python2.7
# -*- coding: utf-8 -*-

import sys
import re
import cgi
import getopt


# ============================================================
# Usage
# ============================================================

def usage():
    print """SYNOPSIS
    narou2html.py [-i shift_jis|utf-8|euc]
                  [-e shift_jis|utf-8|euc]
                  [-br]
                  [readfile.txt [writefile.html]]

DESCRIPTION
    Convert Narou-style novel text to HTML.

    Input and output are decoded/encoded according to the
    specified character encodings. Internally, all processing
    is performed as Unicode.

    Input line endings are normalized to LF.

OPTIONS
    -i ENCODING
        Input encoding.

        shift_jis
        utf-8
        euc

        Default: shift_jis

    -e ENCODING
        Output encoding.

        shift_jis
        utf-8
        euc

        Default: utf-8

    -br
        In normal paragraphs, use ￥￥ as an explicit
        line-break marker instead of actual line endings.

        Without -br:
            actual line endings -> <br>

        With -br:
            ￥￥ -> <br>

        Dialogue and monologue line endings are converted
        to <br> regardless of this option.

    -h
        Show this help.

EXAMPLES
    # Standard input -> standard output
    narou2html.py

    # novel.txt -> standard output
    narou2html.py novel.txt

    # novel.txt -> novel.html
    narou2html.py novel.txt novel.html

    # Shift_JIS input -> UTF-8 output
    narou2html.py -i shift_jis -e utf-8 novel.txt novel.html

    # UTF-8 input -> Shift_JIS output
    narou2html.py -i utf-8 -e shift_jis novel.txt novel.html

    # EUC-JP input -> UTF-8 output
    narou2html.py -i euc -e utf-8 novel.txt novel.html

    # Use ￥￥ as explicit line-break markers
    narou2html.py -br novel.txt novel.html
"""


# ============================================================
# Ruby
#
# |漢字《かんじ》
#
# ->
#
# <ruby>漢字<rt>かんじ</rt></ruby>
# ============================================================

def convert_ruby(text):

    pattern = ur"\|([^《]+)《([^》]+)》"

    def replace(match):

        base = match.group(1)
        ruby = match.group(2)

        return (
            u"<ruby>{0}"
            u"<rt>{1}</rt>"
            u"</ruby>"
        ).format(base, ruby)

    return re.sub(pattern, replace, text)


# ============================================================
# Convert one block
# ============================================================

def convert_block(block, br_mode):

    # 改行だけを除去する。
    #
    # 全角スペースなどは原稿の表現として保持する。
    block = block.strip(u"\n")

    if not block:
        return u""

    # --------------------------------------------------------
    # Horizontal rule
    #
    # 行全体が「━━」だけの場合
    #
    # ━━
    #
    # ->
    #
    # <hr>
    # --------------------------------------------------------

    if block == u"━━":

        return u"<hr class=\"story-divider\">"

    # --------------------------------------------------------
    # Dialogue
    #
    # 「～」
    # --------------------------------------------------------

    if block.startswith(u"「") and block.endswith(u"」"):

        block = block.replace(
            u"\n",
            u"<br>\n"
        )

        return (
            u'<div class="dialogue">{0}</div>'
        ).format(block)

    # --------------------------------------------------------
    # Monologue
    #
    # （～）
    # --------------------------------------------------------

    if block.startswith(u"（") and block.endswith(u"）"):

        block = block.replace(
            u"\n",
            u"<br>\n"
        )

        return (
            u'<div class="monologue">{0}</div>'
        ).format(block)

    # --------------------------------------------------------
    # Waka
    #
    # 行頭が全角スペース2文字
    # --------------------------------------------------------

    if block.startswith(u"　　"):

        return (
            u'<div class="waka">{0}</div>'
        ).format(block)

    # --------------------------------------------------------
    # Normal paragraph
    #
    # -br なし:
    #     実際の改行を <br> にする
    #
    # -br あり:
    #     ￥￥ を <br> にする
    #
    # -br ありの場合、￥￥に続く原稿上の改行も
    # 「改行のための記号」として消費する。
    # --------------------------------------------------------

    if br_mode:

        # ￥￥ + 改行
        block = re.sub(
            ur"￥￥\n",
            u"<br>\n",
            block
        )

        # ブロック末尾の ￥￥
        block = re.sub(
            ur"￥￥$",
            u"<br>",
            block
        )

    else:

        # 実際の改行
        block = block.replace(
            u"\n",
            u"<br>\n"
        )

    return (
        u"<p>{0}</p>"
    ).format(block)


# ============================================================
# Paragraph conversion
#
# 空行でブロックを区切る
#
# 入力テキストは事前に LF に正規化済み
# ============================================================

def convert_paragraphs(text, br_mode):

    result = []

    # --------------------------------------------------------
    # ｛｛ ... ｝｝ を先に分離する
    #
    # グループ1: 通常の本文
    # グループ2: 和歌
    #
    # re.DOTALL により改行を含めてマッチさせる
    # --------------------------------------------------------

    parts = re.split(
        ur"｛｛\n?(.*?)\n?｝｝",
        text,
        flags=re.DOTALL
    )

    # parts は、
    #
    # [通常本文, 和歌, 通常本文, 和歌, 通常本文, ...]
    #
    # という構造になる。

    for index, part in enumerate(parts):

        # ----------------------------------------------------
        # 奇数番目は ｛｛｝｝ で囲まれた和歌
        # ----------------------------------------------------

        if index % 2 == 1:

            # 外側の改行だけを除去。
            # 内部の空行・全角スペースは保持する。
            waka = part.strip(u"\n")

            if waka:

                result.append(
                    u'<div class="waka">{0}</div>'.format(waka)
                )

        # ----------------------------------------------------
        # 偶数番目は通常本文
        # ----------------------------------------------------

        else:

            blocks = re.split(
                ur"\n[ \t　]*\n",
                part
            )

            for block in blocks:

                html = convert_block(block, br_mode)

                if html:
                    result.append(html)

    # 出力の改行は LF
    return u"\n\n".join(result)


# ============================================================
# Main conversion
# ============================================================

def convert(text, br_mode):

    # --------------------------------------------------------
    # Escape HTML
    #
    # 原稿中の < > & などを安全にする
    # --------------------------------------------------------

    text = cgi.escape(text, quote=True)

    # --------------------------------------------------------
    # Ruby
    # --------------------------------------------------------

    text = convert_ruby(text)

    # --------------------------------------------------------
    # Paragraph / dialogue / monologue / waka
    # --------------------------------------------------------

    text = convert_paragraphs(text, br_mode)

    return text


# ============================================================
# Main
# ============================================================

def main():

    # --------------------------------------------------------
    # Default encodings
    # --------------------------------------------------------

    input_encoding = "shift_jis"
    output_encoding = "utf-8"

    # 通常段落の改行方式
    #
    # False:
    #     実際の改行を <br> にする
    #
    # True:
    #     ￥￥ を <br> にする
    br_mode = False

    # --------------------------------------------------------
    # Encoding map
    # --------------------------------------------------------

    encoding_map = {
        "shift_jis": "shift_jis",
        "utf-8":     "utf-8",
        "euc":       "euc_jp",
    }

    # --------------------------------------------------------
    # -br option
    #
    # getopt は -br を通常の短縮オプションとして扱わないため、
    # 先に手動で取り除く。
    # --------------------------------------------------------

    argv = sys.argv[1:]

    if "-br" in argv:

        br_mode = True

        argv = [
            arg for arg in argv
            if arg != "-br"
        ]

    # --------------------------------------------------------
    # Option parsing
    # --------------------------------------------------------

    try:

        opts, args = getopt.getopt(
            argv,
            "hi:e:"
        )

    except getopt.GetoptError as err:

        sys.stderr.write(
            "{0}\n".format(err)
        )

        usage()
        sys.exit(1)

    for opt, value in opts:

        # ----------------------------------------------------
        # Help
        # ----------------------------------------------------

        if opt == "-h":

            usage()
            sys.exit(0)

        # ----------------------------------------------------
        # Input encoding
        # ----------------------------------------------------

        elif opt == "-i":

            if value not in encoding_map:

                sys.stderr.write(
                    "Unknown input encoding: {0}\n"
                    .format(value)
                )

                sys.stderr.write(
                    "Available encodings: "
                    "shift_jis, utf-8, euc\n"
                )

                sys.exit(1)

            input_encoding = encoding_map[value]

        # ----------------------------------------------------
        # Output encoding
        # ----------------------------------------------------

        elif opt == "-e":

            if value not in encoding_map:

                sys.stderr.write(
                    "Unknown output encoding: {0}\n"
                    .format(value)
                )

                sys.stderr.write(
                    "Available encodings: "
                    "shift_jis, utf-8, euc\n"
                )

                sys.exit(1)

            output_encoding = encoding_map[value]

    # --------------------------------------------------------
    # File arguments
    # --------------------------------------------------------

    if len(args) > 2:

        usage()
        sys.exit(1)

    # --------------------------------------------------------
    # Input
    # --------------------------------------------------------

    if len(args) >= 1:

        # Read from file
        with open(args[0], "rb") as f:
            data = f.read()

    else:

        # Read from standard input
        data = sys.stdin.read()

    # --------------------------------------------------------
    # Decode
    #
    # bytes -> Unicode
    # --------------------------------------------------------

    try:

        text = data.decode(input_encoding)

    except UnicodeDecodeError as err:

        sys.stderr.write(
            "Cannot decode input as {0}: {1}\n"
            .format(input_encoding, err)
        )

        sys.exit(1)

    # --------------------------------------------------------
    # Normalize line endings
    #
    # CRLF -> LF
    # CR   -> LF
    #
    # 以後、内部では改行コードを LF に統一する
    # --------------------------------------------------------

    text = text.replace(u"\r\n", u"\n")
    text = text.replace(u"\r", u"\n")

    # --------------------------------------------------------
    # Convert
    #
    # Internal processing is Unicode
    # --------------------------------------------------------

    html = convert(text, br_mode)

    # --------------------------------------------------------
    # Encode
    #
    # Unicode -> requested output encoding
    # --------------------------------------------------------

    try:

        output = html.encode(output_encoding)

    except UnicodeEncodeError as err:

        sys.stderr.write(
            "Cannot encode output as {0}: {1}\n"
            .format(output_encoding, err)
        )

        sys.exit(1)

    # --------------------------------------------------------
    # Output
    # --------------------------------------------------------

    if len(args) >= 2:

        # Write to file
        with open(args[1], "wb") as f:
            f.write(output)

    else:

        # Write to standard output
        sys.stdout.write(output)


# ============================================================
# Entry point
# ============================================================

if __name__ == "__main__":
    main()