<filename>meditor/output.py
import os.path
import logging
import json
import subprocess
from collections import OrderedDict

from pygments import highlight
from pygments.lexers import get_lexer_for_filename
from pygments.formatters import HtmlFormatter
from pygments.util import ClassNotFound

import markdown
import mdx_mathjax
import mdx_graphviz
import docutils_graphviz

from docutils.core import publish_string
from docutils.core import publish_cmdline
from docutils.core import publish_cmdline_to_binary
from docutils.writers.odf_odt import Writer, Reader
from docutils.writers import html5_polyglot
from docutils.parsers.rst import directives

from . import __data_path__, __home_data_path__

logger = logging.getLogger(__name__)

default_overrides = {
    'input_encoding': 'utf-8',
    'output_encoding': 'utf-8',
}


def get_rst_themes():
    """
    result: { 'theme': theme_dict, ... }
    """
    themes_dirs = [
        os.path.join(__home_data_path__, 'themes', 'reStructuredText'),
        os.path.join(__data_path__, 'themes', 'reStructuredText'),
    ]
    themes = OrderedDict()
    for themes_dir in themes_dirs:
        if os.path.exists(themes_dir):
            for theme in os.listdir(themes_dir):
                theme_json = os.path.join(themes_dir, theme, 'theme.json')
                if os.path.exists(theme_json):
                    try:
                        styles = json.load(open(theme_json))
                        for name, style in styles.items():
                            style['stylesheet_dirs'] = [os.path.dirname(theme_json)]
                            themes[name] = style
                    except Exception as err:
                        logger.error(err)
                        continue
    return themes


def get_theme_settings(theme):
    """
    docutils writer will load css file.
    """
    stylesheet = {}
    search_paths = [
        os.path.abspath(os.path.dirname(os.path.dirname(html5_polyglot.__file__))),
    ]
    docutils_theme_path = ''
    for path in search_paths:
        if os.path.exists(os.path.join(path, 'html5_polyglot', 'template.txt')):
            docutils_theme_path = path
            break
    logger.debug('docutils theme path: %s' % docutils_theme_path)

    stylesheet['stylesheet_dirs'] = [
        os.path.join(docutils_theme_path, 'html4css1'),
        os.path.join(docutils_theme_path, 'html5_polyglot'),
    ]

    pygments_path = os.path.join(__home_data_path__, 'themes', 'reStructuredText', 'pygments.css')
    if os.path.exists(pygments_path):
        stylesheet['stylesheet_path'] = pygments_path
        stylesheet['syntax_highlight'] = 'short'
    # docutils default theme
    if not theme or theme == 'default':
        return stylesheet

    # third part theme
    themes = get_rst_themes()
    styles = themes.get(theme)

    # stylesheet_path : css file path
    # syntax_highlight: short
    # template: template file path
    stylesheet['stylesheet_dirs'].extend(styles['stylesheet_dirs'])
    if 'syntax_highlight' in styles:
        stylesheet['syntax_highlight'] = styles['syntax_highlight']
    if 'stylesheet_path' in styles:
        css_paths = styles['stylesheet_path'].split(',')
        if 'stylesheet_path' in stylesheet:
            css_paths += stylesheet['stylesheet_path'].split(',')
        stylesheet['stylesheet_path'] = ','.join(css_paths)
    if 'template' in styles:
        old_path = styles['template']
        new_path = os.path.abspath(
            os.path.join(__home_data_path__,
                         'themes', 'reStructuredText',
                         theme,
                         old_path))
        stylesheet['template'] = new_path
    return stylesheet


def rst2htmlcode(rst_text, theme=None, settings={}):
    # register graphviz directive
    directives.register_directive('dot', docutils_graphviz.Graphviz)

    output = None
    try:
        overrides = {}
        mathjax = settings.get('mathjax')
        if mathjax:
            overrides['math_output'] = ' '.join(['MathJax', mathjax])
            del settings['mathjax']
        overrides.update(default_overrides)
        overrides.update(settings)
        overrides.update(get_theme_settings(theme))
        logger.debug(overrides)
        output = publish_string(
            rst_text,
            writer_name='html5',
            settings_overrides=overrides,
        )
    except Exception as err:
        logger.error(err)
        output = str(err)
    return output


def rst2html(rst_file, filename, theme=None, settings={}):
    # register graphviz directive
    directives.register_directive('dot', docutils_graphviz.Graphviz)

    output = None
    try:
        overrides = {}
        overrides['math_output'] = ' '.join([
            'MathJax',
            'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'
        ])
        overrides.update(default_overrides)
        overrides.update(settings)
        overrides.update(get_theme_settings(theme))
        logger.debug(overrides)
        output = publish_cmdline(
            writer_name='html5',
            settings_overrides=overrides,
            argv=[
                rst_file,
                filename,
            ]
        )
    except Exception as err:
        logger.error(err)
        output = err
    return output


def rst2odt(rst_file, filename, theme=None, settings={}):
    # register graphviz directive
    directives.register_directive('dot', docutils_graphviz.Graphviz)

    output = None
    try:
        overrides = {}
        overrides.update(default_overrides)
        overrides.update(settings)
        overrides.update(get_theme_settings(theme))
        logger.debug(overrides)
        writer = Writer()
        reader = Reader()
        output = publish_cmdline_to_binary(
            reader=reader,
            writer=writer,
            settings_overrides=overrides,
            argv=[
                rst_file,
                filename,
            ]
        )
    except Exception as err:
        logger.error(err)
        output = err
    return output


def get_md_themes():
    """
    result: { 'theme': theme_dict, ... }
    """
    themes_dirs = [
        os.path.join(__home_data_path__, 'themes', 'Markdown'),
        os.path.join(__data_path__, 'themes', 'Markdown'),
    ]
    themes = OrderedDict()
    for themes_dir in themes_dirs:
        if os.path.isdir(themes_dir):
            for theme_dir in os.listdir(themes_dir):
                theme_dir = os.path.join(themes_dir, theme_dir)
                if os.path.isdir(theme_dir):
                    for theme in os.listdir(theme_dir):
                        name, ext = os.path.splitext(theme)
                        if ext.lower() == '.css':
                            themes[name] = os.path.join(theme_dir, theme)
    return themes


def md2htmlcode(markup_file, theme=None, settings={}):
    mathjax = settings.get('mathjax')
    extensions = [
        'markdown.extensions.abbr',
        'markdown.extensions.attr_list',
        'markdown.extensions.def_list',
        'markdown.extensions.fenced_code',
        'markdown.extensions.footnotes',
        # 'markdown.extensions.md_in_html',
        'markdown.extensions.tables',
        'markdown.extensions.admonition',
        'markdown.extensions.codehilite',
        # 'markdown.extensions.legacy_attrs',
        # 'markdown.extensions.legacy_em',
        'markdown.extensions.meta',
        'markdown.extensions.nl2br',
        'markdown.extensions.sane_lists',
        'markdown.extensions.smarty',
        'markdown.extensions.toc',
        'markdown.extensions.wikilinks',
    ]
    extension_configs = {
    }

    if mathjax:
        extensions.append(mdx_mathjax.MathJaxExtension(asciimath_escape=True))
    extensions.append(mdx_graphviz.makeExtension())

    try:
        overrides = {}
        logger.debug(overrides)
        body = markdown.markdown(
            markup_file, output_format='html5',
            extensions=extensions,
            extension_configs=extension_configs,
        )
    except Exception as err:
        logger.error(err)
        body = err

    themes = get_md_themes()
    theme_path = themes.get(theme, 'default')
    theme_css = ''
    if os.path.exists(theme_path):
        with open(theme_path) as f:
            theme_css = f.read()

    pygments_path = os.path.join(
        __home_data_path__, 'themes', 'Markdown', 'pygments.css')
    pygment_css = ''
    if os.path.exists(pygments_path):
        with open(pygments_path) as f:
            pygment_css = f.read()

    html = []
    html.append('<!DOCTYPE html>')
    html.append('<html>')
    html.append('<head>')
    head = []
    head.append('<meta charset="UTF-8" />')

    if mathjax:
        mathjax_config = """<script>
MathJax = {
    loader: {load: ['input/asciimath', 'output/chtml', 'ui/menu']},
    svg: {
        fontCache: 'global'
    },
    options: {
    renderActions: {
        /* add a new named action not to override the original 'find' action */
        find_script_mathtex: [10, function (doc) {
            for (const node of document.querySelectorAll('script[type^="math/tex"], script[type^="math/asciimath"]')) {
                const display = !!node.type.match('/; *mode=display/');
                const math = new doc.options.MathItem(node.textContent, doc.inputJax[0], display);
                const text = document.createTextNode('');
                node.parentNode.replaceChild(text, node);
                math.start = {node: text, delim: '', n: 0};
                math.end = {node: text, delim: '', n: 0};
                doc.math.push(math);
            }
        }, '']
    }}
};
</script>"""
        head.append(mathjax_config)
        head.append(mathjax)

    head.append('<style type="text/css">')
    if theme_css:
        head.append(theme_css)
    if pygment_css:
        head.append(pygment_css)
    head.append('</style>')
    html.extend(head)

    html.append('</head>')
    html.append('<body>')
    html.append(body)
    html.append('</body>')
    html.append('</html>')

    return '\n'.join(html)


def md2html(md_file, filename, theme):
    with open(md_file, encoding='UTF-8') as f:
        md_text = f.read()
    settings = {}
    settings['mathjax'] = """<script id="MathJax-script" async
    src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>"""
    html = md2htmlcode(md_text, theme=theme, settings=settings)
    with open(filename, 'wt') as f:
        f.write(html)


def htmlcode(text, filepath):
    try:
        lexer = get_lexer_for_filename(filepath, stripall=True)
    except ClassNotFound:
        lexer = get_lexer_for_filename(filepath + '.txt', stripall=True)
    formatter = HtmlFormatter(linenos='inline', full=True, filename=filepath)

    return highlight(text, lexer, formatter)


def graphviz2htmlcode(markup_text, theme=None, settings={}):
    import base64
    import graphviz

    filetype = settings.get('filetype', 'svg')
    alt = settings.get('filename', 'dot-file')

    try:
        output = ''
        src = graphviz.Source(markup_text)
        # TODO: add Waiting Dialog
        output = src.pipe(format=filetype)

        if filetype == 'svg':
            img = '<div class="graphviz">%s</div>' % output.decode('utf-8')
        elif filetype == 'png':
            output = base64.b64encode(output).decode()
            data_path = "data:image/%s;base64,%s" % (filetype, output)
            img = '<img src="%s" alt="%s" />' % (data_path, alt)
    except subprocess.CalledProcessError as exec_err:
        errs = []
        errs.append('%s' % exec_err)
        exec_err.stderr and errs.append(exec_err.stderr.decode())
        errs.append(output)
        img = '<br />'.join(errs)
        logger.error(img)
    except Exception as err:
        img = '%s<br />%s' % (err, output)
        logger.error(img)

    html = []
    html.append('<!DOCTYPE html>')
    html.append('<html>')
    html.append('<head>')

    head = []
    head.append('<meta charset="UTF-8" />')

    html.extend(head)
    html.append('</head>')
    html.append('<body>')
    html.append(img)
    html.append('</body>')
    html.append('</html>')

    return '\n'.join(html)
