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

js support import

parent 3094e403
Loading
Loading
Loading
Loading
+1 −1
Changes for editor/AI_HTML_TO_BLOGI_XML.md: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -70,7 +70,7 @@ Regeln:
1. Bei Unsicherheit lieber `CustomHtml` statt falsches Widget.
2. Keine leeren Widgets ohne Inhalt erzeugen.
3. ID/Klassen aus HTML nur uebernehmen, wenn sie fuer Funktion oder Styling relevant sind.
4. Script-Tags nicht als aktive Scripts uebernehmen; stattdessen Struktur sichern.
4. Script-Tags: externe `<script src="http(s)://...">` und Inline-Scripts (kein `src`, JS-Typ oder kein `type`) werden als aktives `GlobalJs` uebernommen und beim Rendern der Seite ausgefuehrt; `<script type="application/ld+json">`/`"text/template">`-artige Nicht-JS-Payloads werden ausgeschlossen. Da das importierte HTML-Element-`id`-Attribut nie in die Ausgabe uebernommen wird (jedes Widget bekommt stattdessen eine generierte `we-<uuid>`-Klasse), werden `getElementById("x")`/`querySelector("#x")`/`querySelectorAll("#x")`-Aufrufe im uebernommenen JS automatisch auf `.we-<uuid>` umgeschrieben, sofern `x` einer beim Import gesehenen id entspricht -- unbekannte ids bleiben unveraendert. Klassenbasierte Lookups (`getElementsByClassName`, `querySelector(".x")`) werden nicht umgeschrieben, da eine Quell-Klasse mehrere Elemente treffen kann, von denen jedes seine eigene uuid bekommt.

## Prompt-Vorlage fuer eine KI

+131 −15
Changes for editor/src/htmlimport.cpp: 131 added lines, 15 removed lines.
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@
#include <algorithm>
#include <cctype>
#include <cstring>
#include <regex>
#include <sstream>
#include <stack>

@@ -665,6 +666,83 @@ std::map<std::string,std::string> resolveCustomProperties(
    return env;
}

// Sets @p w's instance_id, generating one first if @p newInstanceId (whatever
// extractCustomCss came back with) is empty -- extractCustomCss only mints a
// uuid when there's leftover CustomCss text to key it off of, but an element
// with an original `id` needs a stable `we-<uuid>` class regardless, since
// that id is otherwise dropped entirely and imported inline script would
// have no other way to address the element (see rewriteJsIdReferences).
// Records the id -> uuid mapping into @p idToInstanceId so that rewrite can
// find it.
void finalizeInstanceId(tinyxml2::XMLElement *w, const std::string &elemId,
                         std::string &newInstanceId,
                         std::map<std::string,std::string> &idToInstanceId)
{
    if (newInstanceId.empty() && !elemId.empty()) {
        uuid::uuid id;
        id.generate();
        newInstanceId = id.c_str();
    }
    if (!newInstanceId.empty()) {
        w->SetAttribute("instance_id", newInstanceId.c_str());
        if (!elemId.empty()) idToInstanceId[elemId] = newInstanceId;
    }
}

// Runs every match of @p pattern (capturing the quote character as group 1,
// via backreference, and the looked-up id as group 2) through
// @p idToInstanceId, replacing the whole match with @p makeReplacement's
// result when found and leaving it untouched otherwise.
std::string rewriteMatchedCalls(const std::string &text, const std::regex &pattern,
                                 const std::map<std::string,std::string> &idToInstanceId,
                                 const std::function<std::string(const std::string&)> &makeReplacement)
{
    std::string out;
    out.reserve(text.size());
    size_t lastEnd = 0;
    for (auto it = std::sregex_iterator(text.begin(), text.end(), pattern),
              end = std::sregex_iterator();
         it != end; ++it) {
        const std::smatch &m = *it;
        size_t pos = static_cast<size_t>(m.position(0));
        out.append(text, lastEnd, pos - lastEnd);
        auto found = idToInstanceId.find(m[2].str());
        out += (found == idToInstanceId.end()) ? m[0].str() : makeReplacement(found->second);
        lastEnd = pos + static_cast<size_t>(m.length(0));
    }
    out.append(text, lastEnd, text.size() - lastEnd);
    return out;
}

// Rewrites `.getElementById("x")` / `.querySelector("#x")` /
// `.querySelectorAll("#x")` calls in imported script text so they still
// resolve after import -- see convertHtmlToWidgetXml's doc comment
// (htmlimport.h) for the full rationale. An id with no entry in
// @p idToInstanceId (never seen during import, or the element that had it
// didn't become its own widget) is left untouched -- best-effort, same
// philosophy as this file's URL rewriting elsewhere.
std::string rewriteJsIdReferences(const std::string &js,
                                   const std::map<std::string,std::string> &idToInstanceId)
{
    if (js.empty() || idToInstanceId.empty()) return js;

    // Group 1 is the opening quote, re-required as the closing quote via the
    // \1 backreference (so a `"..."`-quoted call can't be closed by a stray
    // `'`); group 2 is the id itself. rewriteMatchedCalls always reads
    // group 2.
    static const std::regex byId(R"(\.getElementById\(\s*(['"])([^'"]*)\1\s*\))");
    static const std::regex qsAll(R"(\.querySelectorAll\(\s*(['"])#([^'"]*)\1\s*\))");
    static const std::regex qs(R"(\.querySelector\(\s*(['"])#([^'"]*)\1\s*\))");

    std::string out = rewriteMatchedCalls(js, byId, idToInstanceId,
        [](const std::string &uuid) { return ".querySelector(\".we-" + uuid + "\")"; });
    out = rewriteMatchedCalls(out, qsAll, idToInstanceId,
        [](const std::string &uuid) { return ".querySelectorAll(\".we-" + uuid + "\")"; });
    out = rewriteMatchedCalls(out, qs, idToInstanceId,
        [](const std::string &uuid) { return ".querySelector(\".we-" + uuid + "\")"; });
    return out;
}

} // namespace

std::string blogi::htmlimport::extractCustomCss(
@@ -849,6 +927,7 @@ void blogi::htmlimport::htmlElementToWidgetXml(
    tinyxml2::XMLElement *parentXml,
    const libhtmlpp::CSSStyleSheet &sheet,
    std::set<std::string> &seenMediaBlocks,
    std::map<std::string,std::string> &idToInstanceId,
    libhtmlpp::Element *stopBefore,
    const std::map<std::string,std::string> &rootCustomProperties)
{
@@ -990,7 +1069,7 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                applyFlexLayoutAttributes(w, cssProps, knownCss);
                std::string newInstanceId;
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, newInstanceId);
                if (!newInstanceId.empty()) w->SetAttribute("instance_id", newInstanceId.c_str());
                finalizeInstanceId(w, elemId, newInstanceId, idToInstanceId);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
@@ -1035,7 +1114,7 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                applyFlexLayoutAttributes(w, cssProps, knownCss);
                std::string newInstanceId;
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, newInstanceId);
                if (!newInstanceId.empty()) w->SetAttribute("instance_id", newInstanceId.c_str());
                finalizeInstanceId(w, elemId, newInstanceId, idToInstanceId);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
@@ -1090,7 +1169,7 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                    }
                    std::string newInstanceId;
                    std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, newInstanceId);
                    if (!newInstanceId.empty()) w->SetAttribute("instance_id", newInstanceId.c_str());
                    finalizeInstanceId(w, elemId, newInstanceId, idToInstanceId);
                    if (!custom.empty()) {
                        tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                        ccEl->SetText(custom.c_str());
@@ -1110,7 +1189,7 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                    applyFlexLayoutAttributes(w, cssProps, knownCss);
                    std::string newInstanceId;
                    std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, newInstanceId);
                    if (!newInstanceId.empty()) w->SetAttribute("instance_id", newInstanceId.c_str());
                    finalizeInstanceId(w, elemId, newInstanceId, idToInstanceId);
                    if (!custom.empty()) {
                        tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                        ccEl->SetText(custom.c_str());
@@ -1145,7 +1224,7 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                    "display","visibility"};
                std::string newInstanceId;
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, newInstanceId);
                if (!newInstanceId.empty()) w->SetAttribute("instance_id", newInstanceId.c_str());
                finalizeInstanceId(w, elemId, newInstanceId, idToInstanceId);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
@@ -1167,7 +1246,7 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                std::vector<std::string> knownCss = {"width","height","display"};
                std::string newInstanceId;
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, newInstanceId);
                if (!newInstanceId.empty()) w->SetAttribute("instance_id", newInstanceId.c_str());
                finalizeInstanceId(w, elemId, newInstanceId, idToInstanceId);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
@@ -1312,7 +1391,7 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                    "display","visibility"};
                std::string newInstanceId;
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, newInstanceId);
                if (!newInstanceId.empty()) w->SetAttribute("instance_id", newInstanceId.c_str());
                finalizeInstanceId(w, elemId, newInstanceId, idToInstanceId);
                std::string structureCss = collectTableStructureCss(sheet, usedClasses);
                if (!structureCss.empty()) {
                    if (!custom.empty()) custom += " ";
@@ -1364,7 +1443,7 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                applyFlexLayoutAttributes(w, cssProps, knownCss);
                std::string newInstanceId;
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, newInstanceId);
                if (!newInstanceId.empty()) w->SetAttribute("instance_id", newInstanceId.c_str());
                finalizeInstanceId(w, elemId, newInstanceId, idToInstanceId);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
@@ -1423,7 +1502,7 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                        "visibility","text-decoration","cursor","text-align"};
                    std::string newInstanceId;
                    std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, newInstanceId);
                    if (!newInstanceId.empty()) w->SetAttribute("instance_id", newInstanceId.c_str());
                    finalizeInstanceId(w, elemId, newInstanceId, idToInstanceId);
                    if (!custom.empty()) {
                        tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                        ccEl->SetText(custom.c_str());
@@ -1484,7 +1563,7 @@ void blogi::htmlimport::htmlElementToWidgetXml(
                    "padding","margin","line-height"};
                std::string newInstanceId;
                std::string custom = extractCustomCss(cssProps, knownCss, mediaRules, newInstanceId);
                if (!newInstanceId.empty()) w->SetAttribute("instance_id", newInstanceId.c_str());
                finalizeInstanceId(w, elemId, newInstanceId, idToInstanceId);
                if (!custom.empty()) {
                    tinyxml2::XMLElement *ccEl = doc.NewElement("CustomCss");
                    ccEl->SetText(custom.c_str());
@@ -2190,6 +2269,36 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html,
                            if (outStats) outStats->jsFailed++;
                        }
                    }
                } else if (src.empty()) {
                    // Inline script -- collect its own source text, unless
                    // its type marks it as non-JS payload (JSON-LD, a
                    // page-builder's <script type="text/template"> clone
                    // source, etc.): executing those as JS would just throw,
                    // and dumping raw JSON/HTML into globalJs serves nobody.
                    std::string type = sel->getAtributte("type");
                    std::transform(type.begin(), type.end(), type.begin(),
                        [](unsigned char c) { return std::tolower(c); });
                    bool isJs = type.empty() || type == "text/javascript" ||
                                type == "application/javascript" ||
                                type == "module" || type == "text/ecmascript" ||
                                type == "application/ecmascript";
                    if (isJs) {
                        std::string text = sel->getScript();
                        if (!text.empty()) {
                            // Dropped whole rather than truncated when it
                            // would blow the budget -- a truncated script is
                            // far more likely to break every script
                            // concatenated after it (unterminated string/
                            // brace) than one left out cleanly.
                            if (globalJs.size() + text.size() > kMaxTotalJsBytes) {
                                if (outStats) outStats->jsFailed++;
                            } else {
                                if (!globalJs.empty()) globalJs += "\n;\n";
                                globalJs += text;
                                if (outStats) outStats->jsFetched++;
                            }
                        }
                    }
                }
                cur = cur->nextElement();
                continue;
@@ -2304,14 +2413,15 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html,
    if (!globalCss.empty()) {
        xmlRoot->SetAttribute("GlobalCss", globalCss.c_str());
    }
    if (!globalJs.empty()) {
        xmlRoot->SetAttribute("GlobalJs", globalJs.c_str());
    }

    // Shared across every top-level node imported below (not per-node) so
    // an @media block already emitted via one node's widgets isn't repeated
    // by another's -- see collectApproximateMatches's dedup comment.
    std::set<std::string> seenMediaBlocks;
    // Populated by htmlElementToWidgetXml as elements with an original `id`
    // get converted; used below to rewrite id-based DOM lookups in globalJs
    // once every element's instance_id is known (see rewriteJsIdReferences).
    std::map<std::string,std::string> idToInstanceId;

    if (root) {
        if (selectedPaths) {
@@ -2319,12 +2429,18 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html,
                libhtmlpp::Element *el = resolveImportPath(root, path);
                if (el) {
                    htmlElementToWidgetXml(el, xmlDoc, xmlRoot, stylesheet, seenMediaBlocks,
                                            el->nextElement(), rootEnv);
                                            idToInstanceId, el->nextElement(), rootEnv);
                }
            }
        } else {
            htmlElementToWidgetXml(root, xmlDoc, xmlRoot, stylesheet, seenMediaBlocks, nullptr, rootEnv);
            htmlElementToWidgetXml(root, xmlDoc, xmlRoot, stylesheet, seenMediaBlocks,
                                    idToInstanceId, nullptr, rootEnv);
        }
    }

    globalJs = rewriteJsIdReferences(globalJs, idToInstanceId);
    if (!globalJs.empty()) {
        xmlRoot->SetAttribute("GlobalJs", globalJs.c_str());
    }

    tinyxml2::XMLPrinter printer;
+33 −6
Changes for editor/src/htmlimport.h: 33 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -135,13 +135,22 @@ namespace htmlimport {
     *  cascade/inheritance) and threaded down to its children -- see
     *  resolveCustomProperties in htmlimport.cpp. libhtmlpp::Element has no
     *  parent pointer, so this is threaded top-down through the traversal
     *  rather than looked up via ancestor walk. */
     *  rather than looked up via ancestor walk.
     *
     *  @p idToInstanceId records, for every converted element that had an
     *  original `id` attribute, the `we-<uuid>` instance id blogi assigned it
     *  (see finalizeInstanceId in htmlimport.cpp) -- the only stable way to
     *  address that element in the rendered output, since the original id
     *  itself is never re-emitted. convertHtmlToWidgetXml uses the
     *  accumulated map afterwards to rewrite id-based DOM lookups in
     *  collected inline script text (see rewriteJsIdReferences). */
    void htmlElementToWidgetXml(
        libhtmlpp::Element *htmlEl,
        tinyxml2::XMLDocument &doc,
        tinyxml2::XMLElement *parentXml,
        const libhtmlpp::CSSStyleSheet &sheet,
        std::set<std::string> &seenMediaBlocks,
        std::map<std::string,std::string> &idToInstanceId,
        libhtmlpp::Element *stopBefore = nullptr,
        const std::map<std::string,std::string> &rootCustomProperties = {});

@@ -168,11 +177,29 @@ namespace htmlimport {

    /** Convert raw HTML string to a widget XML string (<WebEditor>...).
     *  Returns true on success, writes the XML to @p outXml, optional
     *  global CSS to @p outGlobalCss, and the concatenated contents of any
     *  externally hosted <script src="http(s)://..."> tags (each fetched via
     *  fetchExternalResource, same limits/safety as external stylesheets) to
     *  @p outGlobalJs -- inline <script> blocks (no src) are left untouched,
     *  matching existing behavior. If @p selectedPaths is non-null, only
     *  global CSS to @p outGlobalCss, and page-level JS to @p outGlobalJs:
     *  the concatenated contents of externally hosted <script src="http(s)://
     *  ..."> tags (each fetched via fetchExternalResource, same limits/safety
     *  as external stylesheets) plus every inline <script> block (no src)
     *  whose type is a recognized JS type or unset -- <script
     *  type="application/ld+json">/"text/template">-style non-JS payloads are
     *  never included, since executing them would just throw. Both are
     *  subject to the same combined size budget (kMaxTotalJsBytes); a script
     *  that alone would exceed the remaining budget is dropped whole rather
     *  than truncated (a truncated script is far more likely to break every
     *  script concatenated after it than one dropped cleanly). Since every
     *  imported element's original `id` attribute is discarded in favor of a
     *  generated `we-<uuid>` instance id/class (see finalizeInstanceId), the
     *  collected script text has `.getElementById("x")` / `.querySelector(
     *  "#x")` / `.querySelectorAll("#x")` calls rewritten to address that
     *  same element's `.we-<uuid>` class instead, wherever `x` matches an
     *  id blogi actually saw (see rewriteJsIdReferences) -- an id with no
     *  match (never seen, or the element didn't become its own widget) is
     *  left untouched, best-effort like this file's URL rewriting elsewhere.
     *  Class-based lookups (getElementsByClassName, `querySelector(".x")`)
     *  are deliberately not rewritten: a source-page class can match many
     *  elements, each of which becomes its own distinct uuid on import, so
     *  there's no sound one-to-one substitution. If @p selectedPaths is non-null, only
     *  the nodes (see ImportNode::path, at any depth) named in the set are
     *  converted/imported, each as its own top-level element -- otherwise
     *  the whole document is imported (today's behavior). Selecting both an
+7 −4
Changes for editor/src/webedit_api.h: 7 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -125,10 +125,13 @@ namespace webedit {
            // <style> tag by renderTree().
            std::string globalCss;
            // Concatenated contents of externally hosted <script src="...">
            // tags collected from an imported HTML page -- round-tripped via
            // the <WebEditor> root's GlobalJs attribute and appended as a
            // <script> tag by renderTree(). Inline <script> blocks are not
            // included (see convertHtmlToWidgetXml's doc comment).
            // tags plus inline <script> blocks (JS-typed or untyped; JSON-LD/
            // template-typed payloads excluded) collected from an imported
            // HTML page, with id-based DOM lookups already rewritten to the
            // imported elements' generated we-<uuid> classes -- round-tripped
            // via the <WebEditor> root's GlobalJs attribute and appended as a
            // <script> tag by renderTree() (see convertHtmlToWidgetXml's doc
            // comment).
            std::string globalJs;
            // Name of the publish target (e.g. "staticpage") last used for a
            // successful publish -- round-tripped via the <WebEditor> root's