Loading editor/src/webedit_api.cpp +13 −149 Changes for editor/src/webedit_api.cpp: 13 added lines, 149 removed lines. Original line number Diff line number Diff line Loading @@ -3899,7 +3899,7 @@ webedit::Api::PublishCoreResult webedit::Api::publishToConnectionCore( autoPublishSurveysForDocument(docXml, authid, blogUrl, domain); std::string html = renderTree(doc.root); std::string llmsTxt = generateLlmsTxt(docXml); std::string llmsTxt = generateLlmsTxt(doc.root); // Build the publish command: // [{"authid":"..."}, {"command":"publish","target":"template","html":"...","params":{...}}] Loading Loading @@ -4698,86 +4698,6 @@ std::string webedit::Api::exportTreeXml(DocumentState &doc) { } namespace { // 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; } bool isBoldWeight(const std::string &weight) { return weight == "bold" || weight == "700" || weight == "800" || weight == "900"; } // 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) { 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") 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 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; Loading @@ -4786,81 +4706,25 @@ namespace { 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" || 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; } 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]) 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, blocks); // Walks the live widget tree, asking each widget for its own text via // EditPlugin::toLlmsBlock() and always recursing into children — a widget // can contribute a block of its own (e.g. Accordion's header) AND still // have its children walked (e.g. Accordion's panel content). void collectLlmsBlocks(const blogi::webedit::EditPlugin *el, std::vector<std::string> &blocks) { for (const blogi::webedit::EditPlugin *cur = el; cur; cur = cur->nextElement()) { if (auto block = cur->toLlmsBlock()) appendBlock(blocks, *block); if (const blogi::webedit::EditPlugin *child = cur->getChildElement()) collectLlmsBlocks(child, blocks); } } } std::string webedit::Api::generateLlmsTxt(const std::string &xml) { tinyxml2::XMLDocument xmlDoc; if (xmlDoc.Parse(xml.c_str(), xml.size()) != tinyxml2::XML_SUCCESS) return ""; const tinyxml2::XMLElement *root = xmlDoc.RootElement(); std::string webedit::Api::generateLlmsTxt(const blogi::webedit::EditPlugin *root) { if (!root) return ""; std::vector<std::string> blocks; if (const tinyxml2::XMLElement *child = root->FirstChildElement()) walkLlmsXml(child, blocks); collectLlmsBlocks(root, 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 Loading editor/src/webedit_api.h +1 −1 Changes for editor/src/webedit_api.h: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -241,7 +241,7 @@ namespace webedit { std::string renderTree(const blogi::webedit::EditPlugin *node); std::string exportTreeXml(DocumentState &doc); std::string serializeElementXml(blogi::webedit::EditPlugin *node); std::string generateLlmsTxt(const std::string &xml); std::string generateLlmsTxt(const blogi::webedit::EditPlugin *root); void persistDocumentStateToSession(const std::string &sessionid, DocumentState &doc); void restoreDocumentStateFromSession(const std::string &sessionid, DocumentState &doc); void loadDocumentTreeFromXml(DocumentState &doc, const std::string &xml); Loading Loading
editor/src/webedit_api.cpp +13 −149 Changes for editor/src/webedit_api.cpp: 13 added lines, 149 removed lines. Original line number Diff line number Diff line Loading @@ -3899,7 +3899,7 @@ webedit::Api::PublishCoreResult webedit::Api::publishToConnectionCore( autoPublishSurveysForDocument(docXml, authid, blogUrl, domain); std::string html = renderTree(doc.root); std::string llmsTxt = generateLlmsTxt(docXml); std::string llmsTxt = generateLlmsTxt(doc.root); // Build the publish command: // [{"authid":"..."}, {"command":"publish","target":"template","html":"...","params":{...}}] Loading Loading @@ -4698,86 +4698,6 @@ std::string webedit::Api::exportTreeXml(DocumentState &doc) { } namespace { // 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; } bool isBoldWeight(const std::string &weight) { return weight == "bold" || weight == "700" || weight == "800" || weight == "900"; } // 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) { 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") 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 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; Loading @@ -4786,81 +4706,25 @@ namespace { 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" || 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; } 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]) 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, blocks); // Walks the live widget tree, asking each widget for its own text via // EditPlugin::toLlmsBlock() and always recursing into children — a widget // can contribute a block of its own (e.g. Accordion's header) AND still // have its children walked (e.g. Accordion's panel content). void collectLlmsBlocks(const blogi::webedit::EditPlugin *el, std::vector<std::string> &blocks) { for (const blogi::webedit::EditPlugin *cur = el; cur; cur = cur->nextElement()) { if (auto block = cur->toLlmsBlock()) appendBlock(blocks, *block); if (const blogi::webedit::EditPlugin *child = cur->getChildElement()) collectLlmsBlocks(child, blocks); } } } std::string webedit::Api::generateLlmsTxt(const std::string &xml) { tinyxml2::XMLDocument xmlDoc; if (xmlDoc.Parse(xml.c_str(), xml.size()) != tinyxml2::XML_SUCCESS) return ""; const tinyxml2::XMLElement *root = xmlDoc.RootElement(); std::string webedit::Api::generateLlmsTxt(const blogi::webedit::EditPlugin *root) { if (!root) return ""; std::vector<std::string> blocks; if (const tinyxml2::XMLElement *child = root->FirstChildElement()) walkLlmsXml(child, blocks); collectLlmsBlocks(root, 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 Loading
editor/src/webedit_api.h +1 −1 Changes for editor/src/webedit_api.h: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -241,7 +241,7 @@ namespace webedit { std::string renderTree(const blogi::webedit::EditPlugin *node); std::string exportTreeXml(DocumentState &doc); std::string serializeElementXml(blogi::webedit::EditPlugin *node); std::string generateLlmsTxt(const std::string &xml); std::string generateLlmsTxt(const blogi::webedit::EditPlugin *root); void persistDocumentStateToSession(const std::string &sessionid, DocumentState &doc); void restoreDocumentStateFromSession(const std::string &sessionid, DocumentState &doc); void loadDocumentTreeFromXml(DocumentState &doc, const std::string &xml); Loading