Commit bec2bd4b authored by jan.koester's avatar jan.koester
Browse files

Use htmlimport.h API in AI import to properly split it into widgets

parent bb8d8d6a
Loading
Loading
Loading
Loading
+8 −139
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@

#include "edit.h"
#include "plugin.h"
#include "htmlimport.h"

#include "blogi.h"
#include "theme.h"
@@ -1261,148 +1262,16 @@ void blogi::webedit::WebEditor::importHtmlToWidgets(const std::string &html) {
    CurDoc.Clear();
    CurRootEl = nullptr;

    // Parse the HTML using libhtmlpp (proper HTML parser)
    libhtmlpp::HtmlString htmlStr(html);
    libhtmlpp::Element *root = nullptr;
    try {
        root = &htmlStr.parse();
    } catch (...) {
        // Fallback: import as single CustomHtml widget
        CurDoc.Clear();
        CurRootEl = CurDoc.NewElement("WebEditor");
        CurRootEl->SetAttribute("Version", "1.0");
        CurDoc.InsertEndChild(CurRootEl);

        tinyxml2::XMLElement *w = CurDoc.NewElement("CustomHtml");
        tinyxml2::XMLElement *htmlContent = CurDoc.NewElement("HtmlContent");
        htmlContent->SetText(html.c_str());
        w->InsertEndChild(htmlContent);
        CurRootEl->InsertEndChild(w);

        loadWidgetsFromXml(CurRootEl, nullptr);
        return;
    }

    // Collect all <style> elements from the DOM and parse CSS
    libhtmlpp::CSSStyleSheet stylesheet;
    std::stack<libhtmlpp::Element*> searchStack;
    searchStack.push(root);
    while (!searchStack.empty()) {
        libhtmlpp::Element *cur = searchStack.top();
        searchStack.pop();
        while (cur) {
            if (cur->getType() == libhtmlpp::HtmlEl) {
                libhtmlpp::HtmlElement *hel = static_cast<libhtmlpp::HtmlElement*>(cur);
                std::string tagName = hel->getTagname();
                std::transform(tagName.begin(), tagName.end(), tagName.begin(),
                    [](unsigned char c) { return std::tolower(c); });
                if (tagName == "style") {
                    // Get text content of style element
                    std::string cssText;
                    libhtmlpp::Element *styleChild = hel->firstChild();
                    while (styleChild) {
                        if (styleChild->getType() == libhtmlpp::TextEl) {
                            cssText += static_cast<libhtmlpp::TextElement*>(styleChild)->getText();
                        }
                        styleChild = styleChild->nextElement();
                    }
                    if (!cssText.empty()) {
                        libhtmlpp::CSSStyleSheet partial;
                        partial.parse(cssText);
                        for (const auto &rule : partial.getRules()) {
                            stylesheet.addRule(rule);
                        }
                    }
                }
                // Also search children of non-style elements
                if (tagName != "style") {
                    libhtmlpp::Element *child = hel->firstChild();
                    if (child) searchStack.push(child);
                }
            }
            cur = cur->nextElement();
        }
    }

    // Build widget XML
    CurDoc.Clear();
    CurRootEl = CurDoc.NewElement("WebEditor");
    CurRootEl->SetAttribute("Version", "1.0");
    CurDoc.InsertEndChild(CurRootEl);

    // Emit global CSS rules that can't be matched to individual elements:
    // :root (CSS variables), @keyframes, @font-face, *, html, body,
    // ::selection, ::-webkit-scrollbar, and other pseudo-elements.
    std::string outXml;
    std::string globalCss;
    for (const auto &rule : stylesheet.getRules()) {
        const std::string &sel = rule.getSelector();
        if (sel.empty()) continue;

        bool isGlobal = false;

        if (sel[0] == '@') {
            // @media with inner selectors are handled per-element in mergeStylesheetRules.
            // Everything else (@keyframes, @font-face, @import, @charset, bare @media
            // with :root/::-scrollbar, etc.) is global.
            // Check if it has parentheses with an inner selector — those go per-element.
            size_t parenClose = std::string::npos;
            size_t parenDepth = 0;
            for (size_t i = 0; i < sel.size(); ++i) {
                if (sel[i] == '(') ++parenDepth;
                else if (sel[i] == ')') {
                    if (parenDepth > 0) --parenDepth;
                    if (parenDepth == 0) { parenClose = i; break; }
                }
            }
            if (parenClose != std::string::npos && parenClose + 1 < sel.size()) {
                // Has inner selector (e.g. "@media (...) .class") → per-element, skip
                // unless the inner selector is :root or other global pseudo
                size_t innerStart = sel.find_first_not_of(" \t\n\r", parenClose + 1);
                if (innerStart != std::string::npos) {
                    std::string inner = sel.substr(innerStart);
                    std::string innerLower = inner;
                    std::transform(innerLower.begin(), innerLower.end(), innerLower.begin(),
                        [](unsigned char c) { return std::tolower(c); });
                    if (innerLower.compare(0, 5, ":root") == 0 ||
                        innerLower.find("::") != std::string::npos ||
                        innerLower == "*" || innerLower == "html" || innerLower == "body") {
                        isGlobal = true;
                    }
                }
            } else {
                // No inner selector: @keyframes, @font-face, @import, @charset, etc.
                isGlobal = true;
            }
        } else {
            std::string selLower = sel;
            std::transform(selLower.begin(), selLower.end(), selLower.begin(),
                [](unsigned char c) { return std::tolower(c); });
            if (selLower == ":root" || selLower == "*" || selLower == "html" ||
                selLower == "body" || selLower.compare(0, 5, ":root") == 0 ||
                selLower.find("::") != std::string::npos) {
                isGlobal = true;
            }
        }

        if (isGlobal) {
            globalCss += rule.serialize(true);
            globalCss += "\n";
        }
    }

    if (!globalCss.empty()) {
    if (blogi::htmlimport::convertHtmlToWidgetXml(html, outXml, globalCss)) {
        GlobalCss = globalCss;
        CurRootEl->SetAttribute("GlobalCss", GlobalCss.c_str());
    } else {
        GlobalCss.clear();
        CurDoc.Parse(outXml.c_str());
        CurRootEl = CurDoc.RootElement();
        if (CurRootEl) {
            loadWidgetsFromXml(CurRootEl, nullptr);
        }

    if (root) {
        htmlElementToWidgetXml(root, CurDoc, CurRootEl, stylesheet);
    }

    // Load the generated widget XML into the live tree
    loadWidgetsFromXml(CurRootEl, nullptr);
}

void blogi::webedit::WebEditor::saveDocumentXml(libhttppp::HttpRequest &curreq) {
+4 −2
Original line number Diff line number Diff line
@@ -720,7 +720,8 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html,
                                               std::string &outXml,
                                               std::string &outGlobalCss)
{
    libhtmlpp::HtmlString htmlStr(html);
    libhtmlpp::HtmlPage page;
    libhtmlpp::HtmlElement index;
    libhtmlpp::Element *root = nullptr;

    tinyxml2::XMLDocument xmlDoc;
@@ -730,7 +731,8 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html,
    xmlDoc.InsertEndChild(xmlRoot);

    try {
        root = &htmlStr.parse();
        page.loadString(index, html);
        root = &index;
    } catch (...) {
        // Fallback: single CustomHtml widget with raw HTML
        tinyxml2::XMLElement *w = xmlDoc.NewElement("CustomHtml");