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

test

parent 8210845d
Loading
Loading
Loading
Loading
+9 −1
Changes for editor/widgets/textbox/textbox.cpp: 9 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -554,8 +554,16 @@ extern "C" {
            std::optional<std::string> toLlmsBlock() const override {
                if (content.empty()) return std::nullopt;
                std::string plain = llms::stripHtml(content);
                if (plain.empty()) return std::nullopt;
                int level = llms::headingLevelFor(fontSize, fontWeight);
                return level ? llms::mdHeading(level, plain) : plain;
                // A box-level heading style only applies when the stripped
                // content is still a single line of plain text. Content that
                // already carries its own Markdown structure (a pasted <h1-6>
                // or <table> converted by stripHtml()) keeps that structure
                // instead of being wrapped in the box's heading level too.
                if (level && plain.find('\n') == std::string::npos)
                    return llms::mdHeading(level, plain);
                return plain;
            }

            // --- JSON Properties Helpers ---
+151 −15
Changes for src/llms_text.h: 151 added lines, 15 removed lines.
Original line number Diff line number Diff line
@@ -31,16 +31,20 @@
// a widget's own text into a plain-text/Markdown fragment for llms.txt.

#include <string>
#include <vector>
#include <algorithm>
#include <cctype>
#include <htmlpp/html.h>

namespace blogi::webedit::llms {

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

    namespace detail {
        inline void stripHtmlInto(libhtmlpp::Element *el, std::string &out) {
            for (libhtmlpp::Element *cur = el; cur; cur = cur->nextElement()) {
                if (cur->getType() == libhtmlpp::TextEl) {
                    std::string text = static_cast<libhtmlpp::TextElement*>(cur)->getText();

        inline std::string collapseWhitespace(const std::string &text) {
            std::string collapsed;
            bool inSpace = false;
            for (unsigned char c : text) {
@@ -53,15 +57,148 @@ namespace blogi::webedit::llms {
                }
            }
            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;
            if (start == std::string::npos) return "";
            return collapsed.substr(start, collapsed.find_last_not_of(' ') - start + 1);
        }

        inline void appendInline(const std::string &piece, std::string &out) {
            if (piece.empty()) return;
            if (!out.empty() && out.back() != ' ' && out.back() != '\n') out += ' ';
            out += piece;
        }

        // Closes off the current paragraph/heading/table so the next block
        // starts on its own blank line, the same convention FAQ's toLlmsBlock()
        // uses to join mdHeading(...) and answer text (editor/widgets/faq/faq.cpp).
        inline void breakBlock(std::string &out) {
            if (out.empty()) return;
            size_t end = out.find_last_not_of(" \t\n");
            out.erase(end == std::string::npos ? 0 : end + 1);
            out += "\n\n";
        }

        inline void stripHtmlInto(libhtmlpp::Element *el, std::string &out);

        inline std::string flattenInline(libhtmlpp::Element *el) {
            std::string out;
            stripHtmlInto(el, out);
            return out;
        }

        inline int headingLevelForTag(const std::string &tag) {
            if (tag.size() == 2 && tag[0] == 'h' && tag[1] >= '1' && tag[1] <= '6')
                return tag[1] - '0';
            return 0;
        }

        // Markdown table cells can't carry a literal "|" or a line break, so
        // fold them away the same way plain-text flattening already does.
        inline std::string tableCellText(libhtmlpp::Element *cell) {
            std::string text = flattenInline(cell);
            std::string sanitized;
            sanitized.reserve(text.size());
            for (char c : text) {
                if (c == '|') sanitized += "\\|";
                else if (c == '\n') sanitized += ' ';
                else sanitized += c;
            }
            return sanitized;
        }

        struct TableRow {
            std::vector<std::string> cells;
            bool isHeader = false;
        };

        inline void collectTableRows(libhtmlpp::Element *el, std::vector<TableRow> &rows) {
            for (libhtmlpp::Element *cur = el; cur; cur = cur->nextElement()) {
                if (cur->getType() != libhtmlpp::HtmlEl) continue;
                auto *hel = static_cast<libhtmlpp::HtmlElement*>(cur);
                std::string tag = hel->getTagname();
                if (tag == "tr") {
                    TableRow row;
                    for (libhtmlpp::Element *cellNode = hel->firstChild(); cellNode; cellNode = cellNode->nextElement()) {
                        if (cellNode->getType() != libhtmlpp::HtmlEl) continue;
                        auto *cellEl = static_cast<libhtmlpp::HtmlElement*>(cellNode);
                        std::string cellTag = cellEl->getTagname();
                        if (cellTag != "td" && cellTag != "th") continue;
                        if (cellTag == "th") row.isHeader = true;
                        row.cells.push_back(tableCellText(cellEl));
                    }
                    if (!row.cells.empty()) rows.push_back(std::move(row));
                } else if (tag == "thead" || tag == "tbody" || tag == "tfoot") {
                    if (libhtmlpp::Element *child = hel->firstChild())
                        collectTableRows(child, rows);
                }
            }
        }

        // Mirrors the Table widget's own toLlmsBlock() (editor/widgets/table/table.cpp)
        // so a <table> pasted into rich text renders as the same Markdown shape
        // a native Table widget would: "| a | b |" rows with a "| --- |" rule
        // under the header row.
        inline std::string tableToMarkdown(libhtmlpp::HtmlElement *tableEl) {
            std::vector<TableRow> rows;
            if (libhtmlpp::Element *child = tableEl->firstChild())
                collectTableRows(child, rows);
            if (rows.empty()) return "";

            size_t numCols = 0;
            for (const auto &row : rows) numCols = std::max(numCols, row.cells.size());
            if (numCols == 0) return "";

            auto rowLine = [numCols](const TableRow &row) {
                std::string line = "|";
                for (size_t c = 0; c < numCols; ++c) {
                    line += " ";
                    if (c < row.cells.size()) line += row.cells[c];
                    line += " |";
                }
                return line;
            };

            std::string md = rowLine(rows[0]);
            if (rows[0].isHeader) {
                md += "\n|";
                for (size_t c = 0; c < numCols; ++c) md += " --- |";
            }
            for (size_t r = 1; r < rows.size(); ++r)
                md += "\n" + rowLine(rows[r]);
            return md;
        }

        inline void stripHtmlInto(libhtmlpp::Element *el, std::string &out) {
            for (libhtmlpp::Element *cur = el; cur; cur = cur->nextElement()) {
                if (cur->getType() == libhtmlpp::TextEl) {
                    appendInline(collapseWhitespace(static_cast<libhtmlpp::TextElement*>(cur)->getText()), out);
                } else if (cur->getType() == libhtmlpp::HtmlEl) {
                    libhtmlpp::HtmlElement *hel = static_cast<libhtmlpp::HtmlElement*>(cur);
                    auto *hel = static_cast<libhtmlpp::HtmlElement*>(cur);
                    std::string tag = hel->getTagname();
                    if (tag == "style" || tag == "script") continue;

                    // Pasted/rich-text headings and tables get converted to their
                    // Markdown equivalents (same as the dedicated Table widget and
                    // TextBox's own box-level heading detection produce) instead of
                    // being flattened to plain text, so they still read as the same
                    // type once they land in llms.txt.
                    if (int level = headingLevelForTag(tag)) {
                        std::string text = flattenInline(hel->firstChild());
                        if (!text.empty()) {
                            breakBlock(out);
                            out += mdHeading(level, text);
                            out += "\n\n";
                        }
                        continue;
                    }
                    if (tag == "table") {
                        std::string table = tableToMarkdown(hel);
                        if (!table.empty()) {
                            breakBlock(out);
                            out += table;
                            out += "\n\n";
                        }
                        continue;
                    }
                    if (libhtmlpp::Element *child = hel->firstChild())
                        stripHtmlInto(child, out);
                }
@@ -71,7 +208,8 @@ namespace blogi::webedit::llms {

    // Strips inline HTML markup that TextBox/CustomHtml content may carry (e.g.
    // a pasted "<span style=...>...</span>" fragment), collapsing it to plain
    // text. Falls back to the raw string on malformed input.
    // text — except headings (h1-h6) and tables, which convert to Markdown
    // instead of being flattened. Falls back to the raw string on malformed input.
    inline std::string stripHtml(const std::string &html) {
        if (html.find('<') == std::string::npos) return html;
        try {
@@ -79,6 +217,8 @@ namespace blogi::webedit::llms {
            libhtmlpp::Element &root = parsed.parse();
            std::string out;
            detail::stripHtmlInto(&root, out);
            size_t end = out.find_last_not_of(" \t\n");
            out.erase(end == std::string::npos ? 0 : end + 1);
            return out;
        } catch (...) {
            return html;
@@ -115,8 +255,4 @@ namespace blogi::webedit::llms {
        return 0;
    }

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

}