#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author : 陈坤泽
# @Email  : 877362867@qq.com
# @Date   : 2021/08/31 09:56

from pyxllib.prog.pupil import check_install_package

check_install_package('win32com', 'pypiwin32')

import json
import os
import re

import pythoncom
from win32com.client import constants
import win32com.client as win32

from pyxllib.prog.pupil import DictTool, EnchantBase, EnchantCvt, run_once
from pyxllib.text.pupil import strwidth
from pyxllib.debug.specialist import File, Dir, get_etag, browser


def __docx():
    """ python-docx 相关封装
    """
    pass


class DocxTools:
    @classmethod
    def to_pdf(cls, docx_file, pdf_file=None):
        check_install_package('docx2pdf')  # 安装不成功的时候可以考虑加参数：--user
        import docx2pdf

        if pdf_file is None:
            pdf_file = docx_file.with_suffix('.pdf')

        docx2pdf.convert(str(docx_file), str(pdf_file))
        return pdf_file

    @classmethod
    def merge(cls, master_file, toc, *, outline='demote'):
        """ 合并多份docx文件

        :param master_file: 要合并到哪个主文件
            注意如果这个文件已存在，会被替换，重置
        :param toc: 类似fitz的table of contents，用一个n*3的表格表示新文档的格式
            list，每个元素三列：标题级别，标题名称，(可选)对应文件内容
        :param outline: 原来每份子文档里的标题，插入到新文档中的处理规则
            demote：降级
            clear：清除

        这个功能还有些局限性，后面要扩展鲁棒性
        TODO 增加一个支持将原文档标题降级的功能，降到toc之后
        """
        app = EnchantWin32WordApplication.get_app()

        master_doc = app.new_doc(master_file)
        for item in toc:
            lvl, title, file = item

            # 1 加一个标题
            r = master_doc.Paragraphs.Add().Range
            r.InsertBefore(title)
            r.Style = master_doc.Styles(f'标题 {lvl}')

            # 2 拷贝完整的内容
            if file:
                file = File(file)
                member_doc = app.open_doc(file)
                member_doc.Activate()

                # 处理原来文档的目录级别
                if outline == 'demote':
                    member_doc.outline_demote(lvl)
                elif outline == 'clear':
                    # 降10级就相当于清除所有原来的标题
                    member_doc.outline_demote(10)

                app.Selection.WholeStory()
                app.Selection.Copy()
                master_doc.Activate()
                app.Selection.EndKey(Unit=app.wd('Story'))  # 跳到文档末尾
                app.Selection.Paste()
                member_doc.Close()
        master_doc.save()
        master_doc.Close(True)


class Document:
    """ 这个库写英文文档还不错。但不能做中文，字体会错乱。
    """

    def __init__(self, docx_file=None):
        """
        Args:
            docx_file:
                已有的word文件路径：打开
                还没创建的word文件路径：在个别功能需要的时候，会自动创建
                None：在临时文件夹生成一个默认的word文件
        """
        import docx
        # pip install python-docx

        if docx_file is None:
            self.docx_file = File(..., Dir.TEMP, suffix='.docx')
        else:
            self.docx_file = File(docx_file)
        if self.docx_file:
            self.doc = docx.Document(str(docx_file))
        else:
            self.doc = docx.Document()

    def write(self):
        Dir(self.docx_file.parent).ensure_dir()
        self.doc.save(str(self.docx_file))

    def to_pdf(self, pdf_file=None):
        self.write()
        pdf_file = DocxTools.to_pdf(self.docx_file, pdf_file)
        return pdf_file

    def to_fitzdoc(self):
        """ 获得 fitz的pdf文档对象
        :return: FitzDoc对象
        """
        from pyxllib.file.pdflib import FitzDoc
        pdf_file = self.to_pdf()
        doc = FitzDoc(pdf_file)
        return doc

    def to_images(self, file_fmt='{filestem}_{number}.png', *args, scale=1, **kwargs):
        doc = self.to_fitzdoc()
        files = doc.to_images(doc.src_file.parent, file_fmt, *args, scale=scale, **kwargs)
        return files

    def browser(self):
        """ 转pdf，使用浏览器的查看效果
        """
        pdf_file = self.to_pdf()
        browser(pdf_file)

    def display(self):
        """ 转图片，使用jupyter环境的查看效果
        """
        from pyxllib.cv.expert import xlpil
        xlpil.enchant()

        # 转图片，并且裁剪，加边框输出
        doc = self.to_fitzdoc()
        for i in range(doc.page_count):
            page = doc.load_page(i)
            print('= ' * 10 + f' {page} ' + '= ' * 10)
            img = page.get_pil_image()
            img.trim(border=5).plot_border().display()
            del page

    def to_labelmes(self, dst_dir=None, file_fmt='{filestem}_{number}.png', *, views=(0, 0, 1, 0), scale=1,
                    advance=False, indent=None):
        """ 转labelme格式查看

        本质是把docx转成pdf，利用pdf的解析生成labelme格式的标准框查看

        :param views: 详见to_labelmes的描述
            各位依次代表是否显示对应细粒度的标注：blocks、lines、spans、chars
        :param bool|dict advance: 是否开启“高级”功能，开启后能获得下划线等属性，但速度会慢很多
            源生的fitz pdf解析是处理不了下划线的，开启高级功能后，有办法通过特殊手段实现下划线的解析
            默认会修正目前已知的下划线、颜色偏差问题
            dict类型：以后功能多了，可能要支持自定义搭配，比如只复原unberline，但不管颜色偏差
        """
        from pyxllib.data.labelme import LabelmeDict

        # 1 转成图片，及json标注
        doc = self.to_fitzdoc()
        imfiles = doc.to_images(dst_dir, file_fmt, scale=scale)

        # 2 高级功能
        def is_color(x):
            return x and sum(x)

        def to_labelmes_advance():
            m = 50  # 匹配run时，上文关联的文字长度，越长越严格

            # 1 将带有下划线的run对象，使用特殊的hash规则存储起来
            content = []  # 使用已遍历到的文本内容作为hash值
            elements = {}
            for p in self.paragraphs:
                for r in p.runs:
                    # 要去掉空格，不然可能对不上。试过strip不行。分段会不太一样，还是把所有空格删了靠谱。
                    content.append(re.sub(r'\s+', '', r.text))
                    if r.underline or is_color(r.font.color.rgb):  # 对有下划线、颜色的对象进行存储
                        # print(r.text + ',', r.underline, r.font.color.rgb, ''.join(content))
                        etag = get_etag(''.join(content)[-m:])  # 全部字符都判断的话太严格了，所以减小区间~
                        elements[etag] = r

            # 2 检查json标注中为span的，其哈希规则是否有对应，则要单独设置扩展属性
            content = ''
            for i, file in enumerate(imfiles):
                page = doc.load_page(i)
                lmdict = LabelmeDict.gen_data(file)
                lmdict['shapes'] = page.get_labelme_shapes('dict', views=views, scale=scale)

                for sp in lmdict['shapes']:
                    attrs = DictTool.json_loads(sp['label'], 'label')
                    if attrs['category_name'] == 'span':
                        content += re.sub(r'\s+', '', attrs['text'])
                        etag = get_etag(content[-m:])
                        # print(content)
                        if etag in elements:
                            # print(content)
                            r = elements[etag]  # 对应的原run对象
                            attrs = DictTool.json_loads(sp['label'])
                            x = r.underline
                            if x:
                                attrs['underline'] = int(x)
                            x = r.font.color.rgb
                            if is_color(x):
                                attrs['color'] = list(x)
                            sp['label'] = json.dumps(attrs)
                file.with_suffix('.json').write(lmdict, indent=indent)

        # 3 获得json
        if advance:
            to_labelmes_advance()
        else:
            doc.to_labelmes(imfiles, views=views, scale=scale)

    def __getattr__(self, item):
        # 属性：
        # core_properties
        # element
        # inline_shapes
        # paragraphs
        # part
        # sections
        # settings
        # styles
        # tables
        # 方法：
        # add_heading
        # add_page_break
        # add_paragraph
        # add_picture
        # add_section
        # add_table
        # save
        return getattr(self.doc, item)


def __win32_word():
    """ 使用win32com调用word

    vba的文档：示例代码更多，vba语法也更熟悉，但显示的功能更不全
        https://docs.microsoft.com/en-us/office/vba/api/word.saveas2
    .net的文档：功能显示更全，应该是所有COM接口都有但示例代码更少、更不熟系
        https://docs.microsoft.com/en-us/dotnet/api/microsoft.office.interop.word.documentclass.saveas2?view=word-pia
    """
    pass


class EnchantWin32WordApplication(EnchantBase):
    @classmethod
    @run_once
    def enchant(cls, app, recursion_enchant=False):
        """
        :param app: win32的类是临时生成的，需要给一个参考对象，才方便type(word)算出类型
        :param recursion_enchant: 是否递归，对目前有的各种子类扩展功能都绑定上
            默认关闭，如果影响到性能，可以关闭，后面运行中需要时手动设定enchant
            开启，能方便业务层开发

            之前有想过可以生成doc里的时候再enchant这些对象，但如果是批量处理脚本，每次建立doc都判断我觉得也麻烦
            长痛不如短痛，建立app的时候就把所有对象enchant更方便
        """
        # app
        _cls = type(app)
        names = cls.check_enchant_names([_cls], ignore_case=True)
        exclude_names = {'get_app'}
        cls._enchant(_cls, names - exclude_names)

        if recursion_enchant:
            # 建一个临时文件，把各种需要绑定的对象都生成绑定一遍
            # 确保初始化稍微慢点，但后面就方便了
            doc = app.Documents.Add()
            EnchantWin32WordDocument.enchant(doc)

            doc.Activate()
            rng = doc.Range()  # 全空的文档，有区间[0,1)
            EnchantWin32WordRange.enchant(rng)

            doc.Hyperlinks.Add(rng, 'url')  # 因为全空，这里会自动生成对应的明文url
            EnchantWin32WordHyperlink.enchant(doc.Hyperlinks(1))

            # 处理完关闭文档，不用保存
            doc.Close(False)

    @classmethod
    def get_app(cls, app=None, *, visible=None, display_alerts=0, recursion_enchant=True):
        """
        Args:
            app: 可以自定义在外部使用Dispatch、DispatchEx等形式给入已初始化好的app
            visible: 是否可见
            display_alerts: 是否关闭警告
            recursion_enchant: 是否递归执行enchant
        """
        # 1 get app
        name = 'WORD.APPLICATION'
        if app is None:
            try:
                app = win32.GetActiveObject(name)
            except pythoncom.com_error:
                pass
        if app is None:
            try:
                # 名称用大写，会比较兼容旧的word2013等版本
                # 尽量静态调度，才能获得 from win32com.client import constants 的常量
                app = win32.gencache.EnsureDispatch(name)
            except TypeError:
                # 实在不行，就用动态调度
                app = win32.dynamic.Dispatch(name)

        # 2 enchant
        cls.enchant(app, recursion_enchant=recursion_enchant)

        if visible is not None:
            app.Visible = visible
        if display_alerts is not None:
            app.DisplayAlerts = display_alerts  # 不警告

        return app

    @staticmethod
    def check_close(app, outfile):
        """ 检查是否有指定名称的文件被打开，将其关闭，避免new_doc等操作出现问题
        """
        outfile = File(outfile)
        for x in app.Documents:
            # 有可能文件来自onedrive，这里用safe_init更合理
            if File.safe_init(x.Name, x.Path) == outfile:
                x.Close()

    @staticmethod
    def open_doc(app, file_name):
        """ 打开已有的文件
        """
        doc = app.Documents.Open(str(file_name))
        return doc

    @staticmethod
    def new_doc(app, file=None):
        """ 创建一个新的文件
        Args:
            file: 文件路径
                空：新建一个doc，到时候保存会默认到临时文件夹
                不存在的文件名：新建对应的空文件
                已存在的文件名：重置、覆盖一个新的空文件

        使用该函数，会自动执行EnchantWin32WordDocument扩展。
        """
        if file is None:
            file = File(..., Dir.TEMP, suffix='.docx')
        else:
            file = File(file)

        doc = app.Documents.Add()  # 创建新的word文档
        doc.save(file)
        return doc

    @staticmethod
    def wd(app, name, part=None):
        """ 输入字符串名称，获得对应的常量值

        :param name: 必须省略前缀wd。这个函数叫wd，就是帮忙省略掉前缀wd的意思
        :param part: 特定组别的枚举值，可以输入特殊的name模式来取值
        """
        if part is None:
            return getattr(constants, 'wd' + name)
        else:
            raise ValueError


class EnchantWin32WordDocument(EnchantBase):
    @classmethod
    @run_once
    def enchant(cls, doc):
        _cls = type(doc)
        names = cls.check_enchant_names([_cls], ignore_case=True)
        propertys = {'n_page'}
        cls._enchant(_cls, propertys, EnchantCvt.staticmethod2property)
        cls._enchant(_cls, names - propertys)

    @staticmethod
    def save(doc, file_name=None, fmt=None, retain=False, **kwargs):
        """ 我自己简化的保存接口

        :param file_name: 保存到指定路径，如果带有后缀
        :param fmt: 毕竟是底层的com接口，不能做的太智能吧。连通过文件名后缀自动选择格式的功能都没有，要手动指定。
            为了方便，对这些进行智能自动处理，得到一个合理的save接口。
        :param retain: SaveAs2的机制：如果目标格式仍是word支持的，则doc会切换到目标文件。否则doc保留原文件对象。
            这里retain若打开，则会自动做切换，保留原文件对象
        :return: 跟retain有关，可能会"重置", outfile
            默认返回 outfile
            开启retain时，返回 outfile, doc
        """

        # 1 辅助函数
        def save_format(fmt):
            """ 枚举值映射，word保存类型的枚举

            >> _('.html')
            8
            >> _('Pdf')
            17
            >> _('wdFormatFilteredHTML')
            10
            """
            # 复杂格式可能无法完美支持所有功能。比如复杂的pdf无法使用SaveAs2实现，要用ExportAsFixedFormat。
            # 有些情况可能无法使用gencache，导致没有constants，所以默认可以直接映射到整数，避免使用到constants
            common = {'doc': 0,
                      'html': 8,
                      'txt': 2,
                      'docx': 16,
                      'pdf': 17}
            name = common.get(fmt.lower().lstrip('.'), fmt)
            if isinstance(name, int):
                return name
            else:
                return getattr(constants, 'wd' + name)

        # 2 确认要存储的文件格式
        if isinstance(fmt, str):
            fmt = fmt.lower().lstrip('.')
        elif file_name is not None:
            fmt = File(file_name).suffix[1:].lower()
        elif doc.Path:
            fmt = os.path.splitext(doc.Name)[1][1:].lower()
        else:
            fmt = 'docx'

        # 3 保存一份原始的文件路径
        origin_file = File(doc.Name, doc.Path) if doc.Path else None

        # 4 如果有指定保存文件路径
        if file_name is not None:
            outfile = File(file_name)
            if outfile.suffix[1:].lower() != fmt:
                # 已有文件名，但这里指定的fmt不同于原文件，则认为是要另存为一个同名的不同格式文件
                outfile = File(outfile.stem, outfile.parent, suffix=fmt)
            doc.SaveAs2(str(outfile), save_format(fmt), **kwargs)
        # 5 如果没指定保存文件路径
        else:
            if doc.Path:
                outfile = File(doc.Name, doc.Path, suffix='.' + fmt)
                doc.SaveAs2(str(outfile), save_format(outfile.suffix), **kwargs)
            else:
                etag = get_etag(doc.Content)
                outfile = File(etag, Dir.TEMP, suffix=fmt)
                doc.SaveAs2(str(outfile), save_format(fmt), **kwargs)

        # 6 是否恢复原doc
        cur_file = File(doc.Name, doc.Path)  # 当前文件不一定是目标文件f，如果是pdf等格式也不会切换过去
        if retain and origin_file and origin_file != cur_file:
            app = doc.Application
            doc.Close()
            doc = app.open_doc(origin_file)

        # 7 返回值
        if retain:
            return outfile, doc
        else:
            return outfile

    # 先暂时不开启 doc.chars
    # @staticmethod
    # def chars(doc):
    #     return doc.Range().chars

    @staticmethod
    def n_page(doc):
        return doc.ActiveWindow.Panes(1).Pages.Count

    @staticmethod
    def browser(doc, file_name=None, fmt='html', retain=False):
        """ 这个函数可能会导致原doc指向对象被销毁，建议要不追返回值doc继续使用
        """
        res = doc.save(file_name, fmt, retain=retain)

        if retain:
            outfile, doc = res
        else:
            outfile = res

        browser(outfile)
        return doc

    @staticmethod
    def add_section_size(doc, factor=1):
        """ 增加每节长度的标记
        一般在这里计算比在html计算方便
        """
        from humanfriendly import format_size

        n = doc.Paragraphs.Count
        style_names, text_lens = [], []
        for p in doc.Paragraphs:
            style_names.append(str(p.Style))
            text_lens.append(strwidth(p.Range.Text))

        for i, p in enumerate(doc.Paragraphs):
            name = style_names[i]
            if name.startswith('标题'):
                cumulate_size = 0
                for j in range(i + 1, n):
                    if style_names[j] != name:
                        cumulate_size += text_lens[j - 1]
                    else:
                        break
                if cumulate_size:
                    size = format_size(cumulate_size * factor).replace(' ', '').replace('bytes', 'B')
                    r = p.Range
                    doc.Range(r.Start, r.End - 1).InsertAfter(f'，{size}')

    @staticmethod
    def outline_demote(doc, demote_level):
        """ 标题降级，降低level层 """
        for p in doc.Paragraphs:
            p.Range.demote(demote_level)

    @staticmethod
    def set_style(doc, obj, name):
        """ 给Paragraph、Range等按名称设置样式

        :param obj: 当前doc下某个含有Style成员属性的子对象
        :param name: 样式名称
        """
        setattr(obj, 'Style', doc.Styles(name))

    @staticmethod
    def add_paragraph(doc, text='', style=None):
        """ 自定义的插入段落

        默认的插入比较麻烦，新建段落、插入文本、设置格式要多步实现，这里封装支持在一步进行多种操作
        """
        p = doc.Paragraphs.Add()
        if text:
            p.Range.InsertBefore(text)
        if style:
            p.Style = doc.Styles(style)
        return p


class EnchantWin32WordRange(EnchantBase):
    """ range是以下标0开始，左闭右开的区间

    当一个区间出现混合属性，比如有的有加粗，有的没加粗时，标记值为 app.wd('Undefined') 9999999
    vba的True是值-1，False是值0
    """

    @classmethod
    @run_once
    def enchant(cls, rng):
        _cls = type(rng)
        names = cls.check_enchant_names([_cls], ignore_case=True)
        propertys = {'chars'}
        cls._enchant(_cls, propertys, EnchantCvt.staticmethod2property)
        cls._enchant(_cls, names - propertys)

    @staticmethod
    def set_hyperlink(rng, url):
        """ 给当前rng添加超链接
        """
        doc = rng.Parent
        doc.Hyperlinks.Add(rng, url)

    @staticmethod
    def chars(rng):
        # 有特殊换行，ch.Text可能会得到 '\r\x07'，为了位置对应，只记录一个字符
        return ''.join([ch.Text[0] for ch in rng.Characters])

    @staticmethod
    def char_range(rng, start=0, end=None):
        """ 定位rng中的子range对象，这里是以可见字符Characters计数的

        :param start: 下标类似切片的规则
        :param end: 见start描述，允许越界，允许负数
            默认不输入表示匹配到末尾
        """
        n = rng.Characters.Count
        if end is None or end > n:
            end = n
        elif end < 0:
            end = n + end
        start_idx, end_idx = rng.Characters(start + 1).Start, rng.Characters(end).End
        return rng.Document.Range(start_idx, end_idx)

    @staticmethod
    def shifting(rng, left=0, right=0):
        """ range左右两边增加偏移量，返回重定位的rng

        常用语段落定位，要在段落末尾增加内容时
        >> rng2 = p.Range.shifting(right=-1)
        """
        return rng.Document.Range(rng.Start + left, rng.End + right)

    @staticmethod
    def demote(rng, demote_level):
        """ 标题降级，降低level层 """
        name = rng.Style.NameLocal  # 获得样式名称
        m = re.match(r'标题 (\d)$', name)
        if m:
            lvl = int(m.group(1))
            new_lvl = lvl + demote_level
            new_style = f'标题 {new_lvl}' if new_lvl < 10 else '正文'
            rng.Style = rng.Parent.Styles(new_style)

    @staticmethod
    def set_font(rng, font_fmt):
        """ 设置字体各种格式

        :param dict|str font_fmt:
            dict，定制格式
                布尔类型：Bold、Italic、Subscript、Superscript
                可布尔的值类型：Underline
                    支持的格式见：https://docs.microsoft.com/en-us/office/vba/api/word.wdunderline
                值类型：Name、Size、Color、UnderlineColor
            str，使用现有样式名
        """
        if isinstance(font_fmt, dict):
            font = rng.Font
            for k, v in font_fmt.items():
                setattr(font, k, v)
        elif isinstance(font_fmt, str):
            rng.Style = rng.Parent.Styles(font_fmt)
        else:
            raise ValueError

    @staticmethod
    def insert_before(rng, text, font_fmt=None):
        """ 对原InsertBefore的功能封装

        :return: 增加返回值，是新插入内容的range定位
        """
        start1, end1 = rng.Start, rng.End
        rng.InsertBefore(text)
        bias = rng.End - end1  # 新插入的内容大小
        new_rng = rng.Document.Range(start1, start1 + bias)
        if font_fmt:
            new_rng.set_font(font_fmt)
        return new_rng

    @staticmethod
    def insert_after(rng, text, font_fmt=None):
        """ 同insert_before，是InsertAfter的重封装
        """
        # 1
        start1, end1 = rng.Start, rng.End

        # 2 往后插入，会排除\r情况
        doc = rng.Document
        ch = doc.Range(end1 - 1, end1).Text
        if ch == '\r':
            end1 -= 1
            rng = doc.Range(start1, end1)

        # 3
        rng.InsertAfter(text)
        bias = rng.End - end1
        new_rng = rng.Document.Range(end1, end1 + bias)
        if font_fmt:
            new_rng.set_font(font_fmt)
        return new_rng


class EnchantWin32WordHyperlink(EnchantBase):
    @classmethod
    @run_once
    def enchant(cls, link):
        _cls = type(link)
        names = cls.check_enchant_names([_cls], ignore_case=True)
        propertys = {'netloc', 'name'}
        cls._enchant(_cls, propertys, EnchantCvt.staticmethod2property)
        cls._enchant(_cls, names - propertys)

    @staticmethod
    def netloc(link):
        from urllib.parse import urlparse
        linkp = urlparse(link.Name)  # 链接格式解析
        # netloc = linkp.netloc or Path(linkp.path).name
        netloc = linkp.netloc or linkp.scheme  # 可能是本地文件，此时记录其所在磁盘
        return netloc

    @staticmethod
    def name(link):
        """ 这个是转成明文的完整链接，如果要编码过的，可以取link.Name """
        from urllib.parse import unquote
        return unquote(link.Name)


def rebuild_document_by_word(fmt='html', translate=False, navigation=False, visible=False, quit=None):
    """ 将剪切板的内容粘贴到word重新排版，再转成fmt格式的文档，用浏览器打开

    这个功能只能在windows平台使用，并且必须要安装有Word软件。

    一般用于英文网站，生成双语阅读的模板，再调用谷歌翻译。
    生成的文档如果有需要，一般是手动复制整理到另一个docx文件中。

    Args:
        fmt: 输出文件类型
            常见的可以简写：html、pdf、txt
            其他特殊需求可以用word原变量名：wdFormatDocument
        visible: 是否展示运行过程，如果不展示，默认最后会close文档
        quit: 运行完是否退出应用
        translate: html专用业务功能，表示是否对p拷贝一份notranslate的对象，用于谷歌翻译双语对照
        navigation: 是否增加导航栏
            注意，使用导航栏后，页面就无法直接使用谷歌翻译了
            但可以自己进入_content文件，使用谷歌翻译处理，自覆盖保存
            然后再回到_index文件，刷新即可
    """
    import pyperclip
    from pyxllib.text.xmllib import BeautifulSoup, html_bitran_template, MakeHtmlNavigation

    # 1 保存的临时文件名采用etag
    f = File(get_etag(pyperclip.paste()), Dir.TEMP, suffix=fmt)
    app = EnchantWin32WordApplication.get_app(visible=visible)
    app.check_close(f)
    doc = app.new_doc(f)
    doc.Activate()
    app.Selection.Paste()

    # 2 如果需要，也可以在这个阶段，插入word自动化的操作，而不是后续在html层面操作
    # 统计每节内容长度，每个字母1B，每个汉字2B
    doc.add_section_size()
    file = doc.save(f, fmt)
    doc.Close()

    # 3 html格式扩展功能
    if fmt == 'html':
        # 3.1 默认扩展功能
        s = file.read(encoding='gbk')
        # s = s.replace('\xa0', '')  # 不知道这样去除\xa0行不行，等下次遇到尝试
        bs = BeautifulSoup(s, 'lxml')
        bs.head_add_number()  # 给标题加上编号
        # bs.head_add_size()  # 显示每节内容长短
        content = str(bs)

        # TODO 识别微信、pydoc，然后做一些自动化清理？
        # TODO 过度缩进问题？

        # 3.2 双语对照阅读
        if translate:
            # word生成的html固定是gbk编码
            content = html_bitran_template(content)

        # 原文是gbk，但到谷歌默认是utf8，所以改一改
        # 这样改后问题是word可能又反而有问题了，不过word本来只是跳板，并不是要用word编辑html
        file.write(content, encoding='utf8')

        # 3.3 导航栏功能
        # 作为临时使用可以开，如果要复制到word，并没有必要
        if navigation:
            file = MakeHtmlNavigation.from_file(file, encoding='utf8', number=False, text_catalogue=False)

    if quit:
        app.Quit()

    return file
