Loading editor/src/htmlimport.cpp +11 −1 Changes for editor/src/htmlimport.cpp: 11 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -1477,7 +1477,17 @@ void blogi::htmlimport::htmlElementToWidgetXml( // <li> at render time -- see list.cpp's // Render(), which renders `items` text // entries and child widgets independently. childFrames.push_back({liEl, w, nullptr, elementEnv}); // Bounded to just this one <li> (stop = // its next sibling) -- unlike every other // childFrames push here, liEl still has its // own nextElement() chain running through // the REST of this <ul>'s <li> siblings, // each already visited independently by // this very while-loop; without this bound // the workStack would walk straight through // them too, reprocessing (and duplicating) // every later sibling <li>. childFrames.push_back({liEl, w, liEl->nextElement(), elementEnv}); } } } Loading tests/htmlimport_smoke.cpp +69 −0 Changes for tests/htmlimport_smoke.cpp: 69 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -392,6 +392,75 @@ int main(){ // --- XML-illegal codepoints in fetched/inline JS don't break the // generated document --- // --- <figure>/<picture>/<source>: images no longer lost inside an // opaque CustomHtml blob --- // A real site (bautenschutz-wetzlar.de) wraps each step of an image // gallery in <li><figure><img></figure>...</li>; before this fix, // <figure> (and <picture>) had no dedicated tag branch, so the whole // subtree -- image included -- fell through to the generic CustomHtml // fallback, invisible both in the editor and to the "import external // images into MediaDB" step (which only ever scans real Image widgets). std::cout << "=== <figure>/<picture>/<source> decompose like <div> ===" << std::endl; { std::string xml = importXml( "<figure class=\"card\"><img src=\"/step1.jpg\" alt=\"Step 1\">" "<figcaption>Step 1</figcaption></figure>"); check(xml.find("<Image src=\"/step1.jpg\" alt=\"Step 1\"") != std::string::npos, "an <img> inside a <figure> becomes a real Image widget"); check(xml.find("CustomHtml") == std::string::npos, "a <figure> with a plain <img>+<figcaption> produces no CustomHtml fallback"); } { std::string xml = importXml( "<picture><source srcset=\"/big.jpg\" media=\"(min-width: 800px)\">" "<img src=\"/small.jpg\" alt=\"pic\"></picture>"); check(xml.find("<Image src=\"/small.jpg\" alt=\"pic\"") != std::string::npos, "a <picture>'s fallback <img> becomes a real Image widget"); check(xml.find("CustomHtml") == std::string::npos, "<picture>/<source> produce no CustomHtml fallback"); } // --- rich <li> content decomposes into real child widgets --- // Before this fix, <ul>/<ol> handling always flattened every <li>'s // entire content into a single plain-text Item via serializeInnerHtml, // regardless of what was inside -- so an <li> wrapping e.g. an image // gallery step was never decomposed into real, editable widgets, and // any <img> inside it was invisible the same way as the <figure> case // above. A plain-text <li> (no child elements) still becomes an Item, // unchanged -- see the existing List tests above (e.g. "<li>a</li>"). std::cout << "=== rich <li> content decomposes into real child widgets ===" << std::endl; { std::string html = "<ul>" "<li><figure><img src=\"/a.jpg\" alt=\"A\"></figure></li>" "<li><figure><img src=\"/b.jpg\" alt=\"B\"></figure></li>" "</ul>"; std::string xml = importXml(html); check(xml.find("<Item>") == std::string::npos, "a rich <li> (nested element content) does not become a plain-text Item"); check(xml.find("<Image src=\"/a.jpg\" alt=\"A\"") != std::string::npos, "first rich <li>'s image becomes a real Image widget"); check(xml.find("<Image src=\"/b.jpg\" alt=\"B\"") != std::string::npos, "second rich <li>'s image becomes a real Image widget"); size_t firstA = xml.find("src=\"/a.jpg\""); size_t lastA = xml.rfind("src=\"/a.jpg\""); check(firstA != std::string::npos && firstA == lastA, "first rich <li>'s image is not duplicated (regression guard: pushing an <li> onto " "the work stack must stop before its next sibling <li>, not walk the whole list)"); } { // A plain-text <li> and a rich <li> in the same list are each // handled independently -- one becomes an Item, the other a widget. std::string html = "<ul><li>plain text</li>" "<li><div class=\"card\"><img src=\"/c.jpg\"></div></li></ul>"; std::string xml = importXml(html); check(xml.find("<Item>plain text</Item>") != std::string::npos, "a plain-text <li> alongside a rich one still becomes an Item"); check(xml.find("<Image src=\"/c.jpg\"") != std::string::npos, "a rich <li> alongside a plain-text one still decomposes into a real widget"); } // A real minified external JS bundle was found embedding a raw U+FFFE // (a noncharacter, used as a compact literal for a reversed-BOM check) // directly in a string literal -- valid JS, valid UTF-8, but XML 1.0 Loading Loading
editor/src/htmlimport.cpp +11 −1 Changes for editor/src/htmlimport.cpp: 11 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -1477,7 +1477,17 @@ void blogi::htmlimport::htmlElementToWidgetXml( // <li> at render time -- see list.cpp's // Render(), which renders `items` text // entries and child widgets independently. childFrames.push_back({liEl, w, nullptr, elementEnv}); // Bounded to just this one <li> (stop = // its next sibling) -- unlike every other // childFrames push here, liEl still has its // own nextElement() chain running through // the REST of this <ul>'s <li> siblings, // each already visited independently by // this very while-loop; without this bound // the workStack would walk straight through // them too, reprocessing (and duplicating) // every later sibling <li>. childFrames.push_back({liEl, w, liEl->nextElement(), elementEnv}); } } } Loading
tests/htmlimport_smoke.cpp +69 −0 Changes for tests/htmlimport_smoke.cpp: 69 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -392,6 +392,75 @@ int main(){ // --- XML-illegal codepoints in fetched/inline JS don't break the // generated document --- // --- <figure>/<picture>/<source>: images no longer lost inside an // opaque CustomHtml blob --- // A real site (bautenschutz-wetzlar.de) wraps each step of an image // gallery in <li><figure><img></figure>...</li>; before this fix, // <figure> (and <picture>) had no dedicated tag branch, so the whole // subtree -- image included -- fell through to the generic CustomHtml // fallback, invisible both in the editor and to the "import external // images into MediaDB" step (which only ever scans real Image widgets). std::cout << "=== <figure>/<picture>/<source> decompose like <div> ===" << std::endl; { std::string xml = importXml( "<figure class=\"card\"><img src=\"/step1.jpg\" alt=\"Step 1\">" "<figcaption>Step 1</figcaption></figure>"); check(xml.find("<Image src=\"/step1.jpg\" alt=\"Step 1\"") != std::string::npos, "an <img> inside a <figure> becomes a real Image widget"); check(xml.find("CustomHtml") == std::string::npos, "a <figure> with a plain <img>+<figcaption> produces no CustomHtml fallback"); } { std::string xml = importXml( "<picture><source srcset=\"/big.jpg\" media=\"(min-width: 800px)\">" "<img src=\"/small.jpg\" alt=\"pic\"></picture>"); check(xml.find("<Image src=\"/small.jpg\" alt=\"pic\"") != std::string::npos, "a <picture>'s fallback <img> becomes a real Image widget"); check(xml.find("CustomHtml") == std::string::npos, "<picture>/<source> produce no CustomHtml fallback"); } // --- rich <li> content decomposes into real child widgets --- // Before this fix, <ul>/<ol> handling always flattened every <li>'s // entire content into a single plain-text Item via serializeInnerHtml, // regardless of what was inside -- so an <li> wrapping e.g. an image // gallery step was never decomposed into real, editable widgets, and // any <img> inside it was invisible the same way as the <figure> case // above. A plain-text <li> (no child elements) still becomes an Item, // unchanged -- see the existing List tests above (e.g. "<li>a</li>"). std::cout << "=== rich <li> content decomposes into real child widgets ===" << std::endl; { std::string html = "<ul>" "<li><figure><img src=\"/a.jpg\" alt=\"A\"></figure></li>" "<li><figure><img src=\"/b.jpg\" alt=\"B\"></figure></li>" "</ul>"; std::string xml = importXml(html); check(xml.find("<Item>") == std::string::npos, "a rich <li> (nested element content) does not become a plain-text Item"); check(xml.find("<Image src=\"/a.jpg\" alt=\"A\"") != std::string::npos, "first rich <li>'s image becomes a real Image widget"); check(xml.find("<Image src=\"/b.jpg\" alt=\"B\"") != std::string::npos, "second rich <li>'s image becomes a real Image widget"); size_t firstA = xml.find("src=\"/a.jpg\""); size_t lastA = xml.rfind("src=\"/a.jpg\""); check(firstA != std::string::npos && firstA == lastA, "first rich <li>'s image is not duplicated (regression guard: pushing an <li> onto " "the work stack must stop before its next sibling <li>, not walk the whole list)"); } { // A plain-text <li> and a rich <li> in the same list are each // handled independently -- one becomes an Item, the other a widget. std::string html = "<ul><li>plain text</li>" "<li><div class=\"card\"><img src=\"/c.jpg\"></div></li></ul>"; std::string xml = importXml(html); check(xml.find("<Item>plain text</Item>") != std::string::npos, "a plain-text <li> alongside a rich one still becomes an Item"); check(xml.find("<Image src=\"/c.jpg\"") != std::string::npos, "a rich <li> alongside a plain-text one still decomposes into a real widget"); } // A real minified external JS bundle was found embedding a raw U+FFFE // (a noncharacter, used as a compact literal for a reversed-BOM check) // directly in a string literal -- valid JS, valid UTF-8, but XML 1.0 Loading