Commit d3b9e553 authored by Jan Köster's avatar Jan Köster
Browse files

fixes

parent 1e2c2f71
Loading
Loading
Loading
Loading
+145 −46
Changes for editor/src/webedit_api.cpp: 145 added lines, 46 removed lines.
Original line number Diff line number Diff line
@@ -4698,75 +4698,154 @@ std::string webedit::Api::exportTreeXml(DocumentState &doc) {
}

namespace {
    // Widget attributes that carry human-readable content (button/faq/image labels etc.)
    // rather than styling. Element text (tinyxml2 GetText()) is picked up separately —
    // that's how content-heavy widgets (Textbox, Article, List items, Table cells) store
    // their body text.
    const std::set<std::string> kLlmsTextAttributes = {
        "label", "title", "question", "answer", "alt",
        "header_label",  // Accordion panel header (accordion.cpp)
        "button_label",  // Popup trigger button text (popup.cpp)
        "survey_title",  // Survey widget's visitor-facing display title (survey.cpp)
    };
    // Approximates a CSS length ("28px", "1.55rem", "1.2em", "18") in pixels,
    // treating rem/em as 16px/unit — good enough to rank TextBox sizes against
    // each other for heading-level detection.
    double parseFontSizePx(const std::string &size) {
        size_t i = 0;
        while (i < size.size() && (std::isdigit(static_cast<unsigned char>(size[i])) || size[i] == '.' || size[i] == '-')) ++i;
        if (i == 0) return 0;
        double value = 0;
        try { value = std::stod(size.substr(0, i)); } catch (...) { return 0; }
        std::string unit = size.substr(i);
        if (unit == "rem" || unit == "em") return value * 16.0;
        return value;
    }

    void appendLlmsText(std::string &out, const std::string &text) {
        std::string trimmed = text;
        size_t start = trimmed.find_first_not_of(" \t\r\n");
        size_t end = trimmed.find_last_not_of(" \t\r\n");
        if (start == std::string::npos) return;
        trimmed = trimmed.substr(start, end - start + 1);
        if (trimmed.empty()) return;
        if (!out.empty()) out += "\n\n";
        out += trimmed;
    bool isBoldWeight(const std::string &weight) {
        return weight == "bold" || weight == "700" || weight == "800" || weight == "900";
    }

    // Strips HTML markup from a fragment by parsing it and concatenating text nodes,
    // falling back to the raw string if it isn't valid HTML.
    std::string stripHtml(const std::string &html) {
        if (html.find('<') == std::string::npos) return html;
        try {
            libhtmlpp::HtmlString parsed(html);
            libhtmlpp::Element &root = parsed.parse();
            std::string out;
            std::vector<libhtmlpp::Element*> stack;
            stack.push_back(&root);
            while (!stack.empty()) {
                libhtmlpp::Element *el = stack.back();
                stack.pop_back();
    // 0 = regular paragraph, 1..3 = heading level. Blogi's TextBox widget has
    // no semantic "this is a heading" flag — every heading, subheading and
    // paragraph is the same widget type styled differently — so level is
    // inferred from size/weight the way a human skims a page visually.
    int headingLevelFor(const std::string &fontSize, const std::string &fontWeight) {
        double px = parseFontSizePx(fontSize);
        if (px >= 32) return 1;
        if (px >= 22) return 2;
        if (px >= 18 && isBoldWeight(fontWeight)) return 3;
        return 0;
    }

    std::string mdHeading(int level, const std::string &text) {
        return std::string(static_cast<size_t>(level), '#') + " " + text;
    }

    // Strips inline HTML markup that TextBox/HtmlContent text may carry (e.g. a
    // pasted "<span style=...>...</span>" fragment), collapsing it to plain text.
    void stripHtmlInto(libhtmlpp::Element *el, std::string &out) {
        for (libhtmlpp::Element *cur = el; cur; cur = cur->nextElement()) {
            if (cur->getType() == libhtmlpp::TextEl) {
                        if (!out.empty()) out += " ";
                        out += static_cast<libhtmlpp::TextElement*>(cur)->getText();
                std::string text = static_cast<libhtmlpp::TextElement*>(cur)->getText();
                std::string collapsed;
                bool inSpace = false;
                for (unsigned char c : text) {
                    if (std::isspace(c)) {
                        if (!inSpace) collapsed += ' ';
                        inSpace = true;
                    } else {
                        collapsed += static_cast<char>(c);
                        inSpace = false;
                    }
                }
                size_t start = collapsed.find_first_not_of(' ');
                if (start == std::string::npos) continue;
                collapsed = collapsed.substr(start, collapsed.find_last_not_of(' ') - start + 1);
                if (collapsed.empty()) continue;
                if (!out.empty() && out.back() != ' ') out += ' ';
                out += collapsed;
            } else if (cur->getType() == libhtmlpp::HtmlEl) {
                libhtmlpp::HtmlElement *hel = static_cast<libhtmlpp::HtmlElement*>(cur);
                std::string tag = hel->getTagname();
                        if (tag != "style" && tag != "script" && hel->firstChild())
                            stack.push_back(hel->firstChild());
                if (tag == "style" || tag == "script") continue;
                if (libhtmlpp::Element *child = hel->firstChild())
                    stripHtmlInto(child, out);
            }
        }
    }

    std::string stripHtml(const std::string &html) {
        if (html.find('<') == std::string::npos) return html;
        try {
            libhtmlpp::HtmlString parsed(html);
            libhtmlpp::Element &root = parsed.parse();
            std::string out;
            stripHtmlInto(&root, out);
            return out;
        } catch (...) {
            return html;
        }
    }

    void walkLlmsXml(const tinyxml2::XMLElement *el, std::string &out) {
    void appendBlock(std::vector<std::string> &blocks, const std::string &text) {
        size_t start = text.find_first_not_of(" \t\r\n");
        if (start == std::string::npos) return;
        size_t end = text.find_last_not_of(" \t\r\n");
        std::string trimmed = text.substr(start, end - start + 1);
        if (!trimmed.empty()) blocks.push_back(trimmed);
    }

    // Walks the widget XML tree directly, turning widget semantics into
    // Markdown blocks: FAQ questions/Accordion headers/large-or-bold TextBox
    // content become headings, everything else becomes a paragraph. This
    // preserves structure (question vs. answer, heading vs. body) that the
    // fully-rendered HTML flattens into identically generic <div>s.
    void walkLlmsXml(const tinyxml2::XMLElement *el, std::vector<std::string> &blocks) {
        for (const tinyxml2::XMLElement *cur = el; cur; cur = cur->NextSiblingElement()) {
            std::string tag = cur->Name() ? cur->Name() : "";
            if (tag == "CustomCss") continue;
            if (tag == "CustomCss" || tag == "CssContent" || tag == "JsContent") continue;

            if (tag == "TextBox") {
                const char *text = cur->GetText();
                if (text && text[0]) {
                    std::string plain = stripHtml(text);
                    const char *fontSize = cur->Attribute("font_size");
                    const char *fontWeight = cur->Attribute("font_weight");
                    int level = headingLevelFor(fontSize ? fontSize : "", fontWeight ? fontWeight : "");
                    appendBlock(blocks, level ? mdHeading(level, plain) : plain);
                }
                continue; // TextBox's content lives in its text node, not XML children
            }

            if (tag == "FAQ") {
                if (const char *title = cur->Attribute("title"))
                    if (title[0]) appendBlock(blocks, mdHeading(2, title));
                for (const tinyxml2::XMLElement *item = cur->FirstChildElement("FAQItem"); item; item = item->NextSiblingElement("FAQItem")) {
                    if (const char *q = item->Attribute("question"))
                        if (q[0]) appendBlock(blocks, mdHeading(3, q));
                    if (const char *a = item->Attribute("answer"))
                        if (a[0]) appendBlock(blocks, a);
                }
                continue;
            }

            if (tag == "Accordion") {
                if (const char *headerLabel = cur->Attribute("header_label"))
                    if (headerLabel[0]) appendBlock(blocks, mdHeading(3, headerLabel));
                if (const tinyxml2::XMLElement *child = cur->FirstChildElement())
                    walkLlmsXml(child, blocks);
                continue;
            }

            for (const tinyxml2::XMLAttribute *attr = cur->FirstAttribute(); attr; attr = attr->Next()) {
                if (kLlmsTextAttributes.count(attr->Name()) && attr->Value() && attr->Value()[0])
                    appendLlmsText(out, attr->Value());
            if (tag == "Survey") {
                if (const char *surveyTitle = cur->Attribute("survey_title"))
                    if (surveyTitle[0]) appendBlock(blocks, mdHeading(2, surveyTitle));
                continue;
            }

            if (tag == "HtmlContent") {
                const char *text = cur->GetText();
            if (text && text[0])
                appendLlmsText(out, stripHtml(text));
                if (text && text[0]) appendBlock(blocks, stripHtml(text));
                continue;
            }

            // Purely visual/decorative — no text worth surfacing in llms.txt.
            if (tag == "Button" || tag == "Image" || tag == "GoogleReviews" || tag == "GoogleMaps") continue;

            // Structural wrapper (Container/Grid/Section/List/Item/Slider/...): recurse.
            if (const tinyxml2::XMLElement *child = cur->FirstChildElement())
                walkLlmsXml(child, out);
                walkLlmsXml(child, blocks);
        }
    }
}
@@ -4779,9 +4858,29 @@ std::string webedit::Api::generateLlmsTxt(const std::string &xml) {
    const tinyxml2::XMLElement *root = xmlDoc.RootElement();
    if (!root) return "";

    std::string out;
    std::vector<std::string> blocks;
    if (const tinyxml2::XMLElement *child = root->FirstChildElement())
        walkLlmsXml(child, out);
        walkLlmsXml(child, blocks);

    // llms.txt is expected to be Markdown with at least one H1: promote the
    // first heading found (whatever level) to H1 if the page has headings but
    // none is already top-level.
    bool hasH1 = false;
    int firstHeadingIdx = -1;
    for (size_t i = 0; i < blocks.size(); ++i) {
        if (blocks[i].size() > 1 && blocks[i][0] == '#' && blocks[i][1] == ' ') { hasH1 = true; break; }
        if (firstHeadingIdx < 0 && !blocks[i].empty() && blocks[i][0] == '#') firstHeadingIdx = static_cast<int>(i);
    }
    if (!hasH1 && firstHeadingIdx >= 0) {
        std::string &h = blocks[static_cast<size_t>(firstHeadingIdx)];
        h = "#" + h.substr(h.find_first_not_of('#'));
    }

    std::string out;
    for (const auto &block : blocks) {
        if (!out.empty()) out += "\n\n";
        out += block;
    }
    return out;
}