BareGit
#include "markdown_renderer.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <variant>

#include <macrodown.h>
#include <mw/url.hpp>
#include <mw/utils.hpp>
#include <nodes.h>

namespace
{

std::optional<std::string> literalText(const macrodown::Node& node)
{
    if(const auto* text = std::get_if<macrodown::Text>(&node.data))
    {
        return text->content;
    }
    const auto* group = std::get_if<macrodown::Group>(&node.data);
    if(group == nullptr)
    {
        return std::nullopt;
    }
    std::string result;
    for(const std::unique_ptr<macrodown::Node>& child : group->children)
    {
        auto child_text = literalText(*child);
        if(!child_text)
        {
            return std::nullopt;
        }
        result += *child_text;
    }
    return result;
}

mw::E<void> validateUrls(const macrodown::Node& node)
{
    if(const auto* macro = std::get_if<macrodown::Macro>(&node.data))
    {
        if(macro->name == "link" || macro->name == "img")
        {
            if(macro->arguments.empty())
            {
                return std::unexpected(mw::httpError(
                    422, "Markdown links require an absolute URL"));
            }
            auto text = literalText(*macro->arguments.front());
            auto url = text ? mw::URL::fromStr(*text)
                            : mw::E<mw::URL>(std::unexpected(
                                  mw::runtimeError("Computed URL")));
            if(!text || !url ||
               (url->scheme() != "http" && url->scheme() != "https") ||
               url->host().empty())
            {
                return std::unexpected(mw::httpError(
                    422,
                    "Markdown links and images require literal HTTP or "
                    "HTTPS URLs"));
            }
        }
        for(const std::unique_ptr<macrodown::Node>& argument :
            macro->arguments)
        {
            auto valid = validateUrls(*argument);
            if(!valid)
            {
                return valid;
            }
        }
    }
    else if(const auto* group = std::get_if<macrodown::Group>(&node.data))
    {
        for(const std::unique_ptr<macrodown::Node>& child : group->children)
        {
            auto valid = validateUrls(*child);
            if(!valid)
            {
                return valid;
            }
        }
    }
    return {};
}

void escapeText(macrodown::Node& node)
{
    if(auto* text = std::get_if<macrodown::Text>(&node.data))
    {
        text->content = mw::escapeHTML(text->content);
    }
    else if(auto* macro = std::get_if<macrodown::Macro>(&node.data))
    {
        for(const std::unique_ptr<macrodown::Node>& argument :
            macro->arguments)
        {
            escapeText(*argument);
        }
    }
    else if(auto* group = std::get_if<macrodown::Group>(&node.data))
    {
        for(const std::unique_ptr<macrodown::Node>& child : group->children)
        {
            escapeText(*child);
        }
    }
}

} // namespace

RenderedHtml::RenderedHtml(std::string value)
        : value_(std::move(value))
{}

const std::string& RenderedHtml::value() const
{
    return value_;
}

mw::E<RenderedHtml> MarkdownRenderer::render(
    const std::string& source) const
{
    try
    {
        macrodown::MacroDown renderer;
        std::unique_ptr<macrodown::Node> tree = renderer.parse(source);
        auto valid = validateUrls(*tree);
        if(!valid)
        {
            return std::unexpected(std::move(valid.error()));
        }
        escapeText(*tree);
        return RenderedHtml(renderer.render(*tree));
    }
    catch(const std::exception& error)
    {
        return std::unexpected(mw::httpError(
            422, "Markdown could not be rendered: " +
            std::string(error.what())));
    }
}