Loading editor/html/index.html +6 −0 Changes for editor/html/index.html: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -307,6 +307,12 @@ <dialog id="import-html-dialog"> <h3 data-i18n="I18N_IMPORT_HTML">HTML importieren</h3> <div id="import-html-step1"> <div class="set-field"> <input type="url" id="import-html-url" data-i18n-placeholder="I18N_IMPORT_HTML_URL_PLACEHOLDER" placeholder="https://beispiel.de/seite"> <button type="button" id="btn-import-html-load-url" class="set-save-btn" data-i18n="I18N_IMPORT_HTML_LOAD_URL">Von URL laden</button> </div> <p id="import-html-url-status" class="set-status" style="display:none"></p> <p class="hint" style="text-align:center" data-i18n="I18N_IMPORT_HTML_OR">oder</p> <label class="file-upload-label">📂 <span data-i18n="I18N_UPLOAD_FILE">Datei wählen</span><input type="file" id="import-html-file" accept=".html,.htm,.txt" style="display:none"></label> <textarea id="import-html-content" rows="15" placeholder="HTML hier einfügen..."></textarea> <div class="set-field"> Loading editor/html/js/api.js +11 −1 Changes for editor/html/js/api.js: 11 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -535,12 +535,22 @@ var EditorApi = (function() { return request('POST', '/api/document/parse-html', { html: html }); }, importHtml: function(html, mode, parentUuid, sections, mediaImport) { // Fetches @p url server-side and parses it exactly like parseHtml, but // also resolves document-relative resource references (img/link/script/a) // against it. Response includes "html" (the fetched page) and "base_url" // (echoed back) so the caller can stash both and pass base_url to // importHtml unchanged, instead of fetching the page a second time. parseHtmlUrl: function(url) { return request('POST', '/api/document/parse-html', { url: url }); }, importHtml: function(html, mode, parentUuid, sections, mediaImport, baseUrl) { var data = { html: html }; if (mode) data.mode = mode; if (parentUuid) data.parent_uuid = parentUuid; if (sections) data.sections = sections; if (mediaImport) data.media_import = mediaImport; if (baseUrl) data.base_url = baseUrl; return request('POST', '/api/document/import-html', data); }, Loading editor/html/js/editor.js +65 −2 Changes for editor/html/js/editor.js: 65 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ var currentDocLastPublishTarget = ''; // target name last successfully published to, see applyPreferredPublishTarget var importHtmlPendingSource = ''; // raw HTML staged in the import dialog, once past step 1 var importHtmlPendingCandidates = []; // last parse-html result for the block picker var importHtmlPendingBaseUrl = ''; // set when importHtmlPendingSource was fetched from a URL, see loadImportHtmlFromUrl var saveDialogMode = 'save'; var projectsCache = []; var currentProjectId = ''; Loading Loading @@ -533,13 +534,57 @@ function openImportHtmlDialog() { document.getElementById('import-html-content').value = ''; document.getElementById('import-html-url').value = ''; setImportHtmlUrlStatus(''); importHtmlPendingSource = ''; importHtmlPendingCandidates = []; importHtmlPendingBaseUrl = ''; resetImportHtmlMediaOptions(); showImportHtmlStep(1); document.getElementById('import-html-dialog').showModal(); } function setImportHtmlUrlStatus(text, isError) { var el = document.getElementById('import-html-url-status'); el.textContent = text || ''; el.style.display = text ? '' : 'none'; el.className = 'set-status' + (isError ? ' error' : ''); } // Fetches @p url server-side (via EditorApi.parseHtmlUrl) and feeds the // result into the same block-picker step used by paste/upload, so the // rest of the import flow (candidate tree, media-import options, confirm) // doesn't need to know the HTML came from a URL -- only the stashed // importHtmlPendingBaseUrl does, and only to be echoed back to the // server on confirm so relative resource references keep resolving. function loadImportHtmlFromUrl(url) { var btn = document.getElementById('btn-import-html-load-url'); btn.disabled = true; setImportHtmlUrlStatus(I18n.t('I18N_LOADING_URL', 'URL wird geladen...')); EditorApi.parseHtmlUrl(url).then(function(resp) { btn.disabled = false; setImportHtmlUrlStatus(''); var html = resp.html || ''; importHtmlPendingBaseUrl = resp.base_url || url; importHtmlPendingSource = html; document.getElementById('import-html-content').value = html; importHtmlPendingCandidates = resp.candidates || []; if (!importHtmlPendingCandidates.length) { importHtmlDirectImport(html); return; } renderImportHtmlCandidates(importHtmlPendingCandidates); document.getElementById('import-html-global-css-hint').style.display = resp.global_css ? '' : 'none'; showImportHtmlStep(2); }).catch(function(err) { btn.disabled = false; setImportHtmlUrlStatus( I18n.t('I18N_IMPORT_FAILED', 'Import fehlgeschlagen') + ': ' + ((err && err.error) || ''), true); }); } // --- HTML import dialog: "import external images into MediaDB" options --- function resetImportHtmlMediaOptions() { Loading Loading @@ -672,6 +717,10 @@ // everything rather than blocking on an empty picker. function goToImportHtmlStep2(html) { importHtmlPendingSource = html; // Paste/upload/manual edits have no known origin URL -- only // loadImportHtmlFromUrl sets importHtmlPendingBaseUrl, and only for // HTML it fetched itself unmodified. importHtmlPendingBaseUrl = ''; EditorApi.parseHtml(html).then(function(resp) { importHtmlPendingCandidates = resp.candidates || []; if (!importHtmlPendingCandidates.length) { Loading @@ -692,7 +741,7 @@ // Fallback used when the picker has nothing to show: import the whole // document directly instead of leaving the dialog stuck. function importHtmlDirectImport(html) { EditorApi.importHtml(html).then(function() { EditorApi.importHtml(html, null, null, null, null, importHtmlPendingBaseUrl).then(function() { document.getElementById('import-html-dialog').close(); Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); Loading Loading @@ -903,6 +952,20 @@ document.getElementById('revisions-dialog').close(); }); // HTML Import dialog: import from URL (step 1, alternative to paste/upload) document.getElementById('btn-import-html-load-url').addEventListener('click', function() { var url = document.getElementById('import-html-url').value.trim(); if (!url) return; loadImportHtmlFromUrl(url); }); document.getElementById('import-html-url').addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); document.getElementById('btn-import-html-load-url').click(); } }); // HTML Import dialog: paste/upload (step 1) -> block picker (step 2) -> import document.getElementById('import-html-file').addEventListener('change', function() { var file = this.files[0]; Loading Loading @@ -972,7 +1035,7 @@ } } var mediaImport = getImportHtmlMediaImportOptions(); EditorApi.importHtml(html, null, null, sections, mediaImport).then(function(resp) { EditorApi.importHtml(html, null, null, sections, mediaImport, importHtmlPendingBaseUrl).then(function(resp) { document.getElementById('import-html-dialog').close(); Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); Loading editor/src/htmlimport.cpp +99 −1 Changes for editor/src/htmlimport.cpp: 99 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -991,6 +991,102 @@ void blogi::htmlimport::htmlElementToWidgetXml( namespace { // scheme://host:port for @p base, reconstructed the same way HttpUrl::print() // does (always with an explicit port) so a re-parse of the result round-trips. std::string urlOrigin(const libhttppp::HttpUrl &base) { return (base.getProtocol() == libhttppp::HttpUrl::HTTPS ? "https://" : "http://") + base.getHost() + ":" + std::to_string(base.getPort()); } // Resolves a possibly-relative reference taken from a page fetched from // @p baseUrl (an <img src>, <a href>, ... value) into an absolute URL. // Already-absolute http(s) URLs, protocol-relative ("//host/..."), fragment- // only ("#..."), and non-fetchable schemes (data:, mailto:, javascript:, // tel:) are passed through -- the last group deliberately, since resolving // them against baseUrl would produce a nonsensical http(s) URL. Malformed // baseUrl (shouldn't happen -- it's always the URL we ourselves just fetched // the page from) falls back to returning @p ref unchanged. std::string resolveUrl(const std::string &baseUrl, const std::string &ref) { if (ref.empty() || ref[0] == '#') return ref; if (ref.compare(0, 7, "http://") == 0 || ref.compare(0, 8, "https://") == 0) return ref; if (ref.compare(0, 5, "data:") == 0 || ref.compare(0, 7, "mailto:") == 0 || ref.compare(0, 11, "javascript:") == 0 || ref.compare(0, 4, "tel:") == 0) return ref; libhttppp::HttpUrl base; try { base = libhttppp::HttpUrl(baseUrl); } catch (...) { return ref; } if (ref.compare(0, 2, "//") == 0) return (base.getProtocol() == libhttppp::HttpUrl::HTTPS ? "https:" : "http:") + ref; if (ref[0] == '/') return urlOrigin(base) + ref; // Relative to the directory of the base path (everything up to, and // including, its last '/'). const std::string &basePath = base.getPath(); std::string combined = basePath.substr(0, basePath.find_last_of('/') + 1) + ref; std::vector<std::string> segs; std::stringstream ss(combined); std::string seg; while (std::getline(ss, seg, '/')) { if (seg.empty() || seg == ".") continue; if (seg == "..") { if (!segs.empty()) segs.pop_back(); } else segs.push_back(seg); } std::string path = "/"; for (size_t i = 0; i < segs.size(); ++i) { path += segs[i]; if (i + 1 < segs.size()) path += "/"; } return urlOrigin(base) + path; } // Walks the whole parsed document (unlike collectTopLevelElements, this // includes <head> -- external stylesheets/scripts live there) and rewrites // every document-relative resource/link reference to absolute, in place, so // the rest of the import pipeline (which only ever looks at the attribute // value, see e.g. the http(s)-prefix checks around the <link>/<script> // fetch loop in convertHtmlToWidgetXml, or the plain SetAttribute("src", ...) // in htmlElementToWidgetXml's Image/Video branches) never has to know the // page came from a URL rather than being pasted/uploaded already-absolute. void rewriteRelativeUrls(libhtmlpp::Element *node, const std::string &baseUrl) { while (node) { int type = node->getType(); if (type == libhtmlpp::ScriptEL) { auto *sel = static_cast<libhtmlpp::ScriptElement*>(node); std::string src = sel->getAtributte("src"); if (!src.empty()) sel->setAttribute("src", resolveUrl(baseUrl, src)); } else if (type == libhtmlpp::HtmlEl || type == libhtmlpp::SvgEL || type == libhtmlpp::TextAreaEL) { auto *hel = static_cast<libhtmlpp::HtmlElement*>(node); std::string tag = hel->getTagname(); std::transform(tag.begin(), tag.end(), tag.begin(), [](unsigned char c) { return std::tolower(c); }); if (tag == "img" || tag == "source" || tag == "video" || tag == "audio") { std::string src = hel->getAtributte("src"); if (!src.empty()) hel->setAttribute("src", resolveUrl(baseUrl, src)); } else if (tag == "link" || tag == "a") { std::string href = hel->getAtributte("href"); if (!href.empty()) hel->setAttribute("href", resolveUrl(baseUrl, href)); } rewriteRelativeUrls(hel->firstChild(), baseUrl); } node = node->nextElement(); } } // Collects, in document order, the "importable" element children of a given // node: direct children of <body> (after transparently unwrapping <html>) at // the document root, or a node's own direct children when called deeper in Loading Loading @@ -1166,7 +1262,8 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, std::string &outGlobalCss, std::string &outGlobalJs, const std::set<std::string> *selectedPaths, ImportStats *outStats) ImportStats *outStats, const std::string &baseUrl) { libhtmlpp::HtmlPage page; libhtmlpp::HtmlElement index; Loading @@ -1181,6 +1278,7 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, try { page.loadString(index, html); root = &index; if (!baseUrl.empty()) rewriteRelativeUrls(root, baseUrl); } catch (...) { // Fallback: single CustomHtml widget with raw HTML tinyxml2::XMLElement *w = xmlDoc.NewElement("CustomHtml"); Loading editor/src/htmlimport.h +11 −2 Changes for editor/src/htmlimport.h: 11 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -127,13 +127,22 @@ namespace htmlimport { * the whole document is imported (today's behavior). Selecting both an * ancestor and one of its descendants imports the descendant twice * (once as part of the ancestor's subtree, once standalone); this is * intentionally not deduplicated -- deselect the ancestor instead. */ * intentionally not deduplicated -- deselect the ancestor instead. * If @p baseUrl is non-empty (the page was fetched from a URL rather * than pasted/uploaded), document-relative references -- <img src>, * <source src>, <video>/<audio> src, <a href>, <link href>, <script * src> -- are resolved against it before anything else runs, so * relative resources on the original page still work once hosted by * blogi (see rewriteRelativeUrls in htmlimport.cpp). Already-absolute * references and non-http(s) schemes (data:, mailto:, javascript:, * fragment-only "#...") are left untouched. */ bool convertHtmlToWidgetXml(const std::string &html, std::string &outXml, std::string &outGlobalCss, std::string &outGlobalJs, const std::set<std::string> *selectedPaths = nullptr, ImportStats *outStats = nullptr); ImportStats *outStats = nullptr, const std::string &baseUrl = ""); } // namespace htmlimport } // namespace blogi Loading
editor/html/index.html +6 −0 Changes for editor/html/index.html: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -307,6 +307,12 @@ <dialog id="import-html-dialog"> <h3 data-i18n="I18N_IMPORT_HTML">HTML importieren</h3> <div id="import-html-step1"> <div class="set-field"> <input type="url" id="import-html-url" data-i18n-placeholder="I18N_IMPORT_HTML_URL_PLACEHOLDER" placeholder="https://beispiel.de/seite"> <button type="button" id="btn-import-html-load-url" class="set-save-btn" data-i18n="I18N_IMPORT_HTML_LOAD_URL">Von URL laden</button> </div> <p id="import-html-url-status" class="set-status" style="display:none"></p> <p class="hint" style="text-align:center" data-i18n="I18N_IMPORT_HTML_OR">oder</p> <label class="file-upload-label">📂 <span data-i18n="I18N_UPLOAD_FILE">Datei wählen</span><input type="file" id="import-html-file" accept=".html,.htm,.txt" style="display:none"></label> <textarea id="import-html-content" rows="15" placeholder="HTML hier einfügen..."></textarea> <div class="set-field"> Loading
editor/html/js/api.js +11 −1 Changes for editor/html/js/api.js: 11 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -535,12 +535,22 @@ var EditorApi = (function() { return request('POST', '/api/document/parse-html', { html: html }); }, importHtml: function(html, mode, parentUuid, sections, mediaImport) { // Fetches @p url server-side and parses it exactly like parseHtml, but // also resolves document-relative resource references (img/link/script/a) // against it. Response includes "html" (the fetched page) and "base_url" // (echoed back) so the caller can stash both and pass base_url to // importHtml unchanged, instead of fetching the page a second time. parseHtmlUrl: function(url) { return request('POST', '/api/document/parse-html', { url: url }); }, importHtml: function(html, mode, parentUuid, sections, mediaImport, baseUrl) { var data = { html: html }; if (mode) data.mode = mode; if (parentUuid) data.parent_uuid = parentUuid; if (sections) data.sections = sections; if (mediaImport) data.media_import = mediaImport; if (baseUrl) data.base_url = baseUrl; return request('POST', '/api/document/import-html', data); }, Loading
editor/html/js/editor.js +65 −2 Changes for editor/html/js/editor.js: 65 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -15,6 +15,7 @@ var currentDocLastPublishTarget = ''; // target name last successfully published to, see applyPreferredPublishTarget var importHtmlPendingSource = ''; // raw HTML staged in the import dialog, once past step 1 var importHtmlPendingCandidates = []; // last parse-html result for the block picker var importHtmlPendingBaseUrl = ''; // set when importHtmlPendingSource was fetched from a URL, see loadImportHtmlFromUrl var saveDialogMode = 'save'; var projectsCache = []; var currentProjectId = ''; Loading Loading @@ -533,13 +534,57 @@ function openImportHtmlDialog() { document.getElementById('import-html-content').value = ''; document.getElementById('import-html-url').value = ''; setImportHtmlUrlStatus(''); importHtmlPendingSource = ''; importHtmlPendingCandidates = []; importHtmlPendingBaseUrl = ''; resetImportHtmlMediaOptions(); showImportHtmlStep(1); document.getElementById('import-html-dialog').showModal(); } function setImportHtmlUrlStatus(text, isError) { var el = document.getElementById('import-html-url-status'); el.textContent = text || ''; el.style.display = text ? '' : 'none'; el.className = 'set-status' + (isError ? ' error' : ''); } // Fetches @p url server-side (via EditorApi.parseHtmlUrl) and feeds the // result into the same block-picker step used by paste/upload, so the // rest of the import flow (candidate tree, media-import options, confirm) // doesn't need to know the HTML came from a URL -- only the stashed // importHtmlPendingBaseUrl does, and only to be echoed back to the // server on confirm so relative resource references keep resolving. function loadImportHtmlFromUrl(url) { var btn = document.getElementById('btn-import-html-load-url'); btn.disabled = true; setImportHtmlUrlStatus(I18n.t('I18N_LOADING_URL', 'URL wird geladen...')); EditorApi.parseHtmlUrl(url).then(function(resp) { btn.disabled = false; setImportHtmlUrlStatus(''); var html = resp.html || ''; importHtmlPendingBaseUrl = resp.base_url || url; importHtmlPendingSource = html; document.getElementById('import-html-content').value = html; importHtmlPendingCandidates = resp.candidates || []; if (!importHtmlPendingCandidates.length) { importHtmlDirectImport(html); return; } renderImportHtmlCandidates(importHtmlPendingCandidates); document.getElementById('import-html-global-css-hint').style.display = resp.global_css ? '' : 'none'; showImportHtmlStep(2); }).catch(function(err) { btn.disabled = false; setImportHtmlUrlStatus( I18n.t('I18N_IMPORT_FAILED', 'Import fehlgeschlagen') + ': ' + ((err && err.error) || ''), true); }); } // --- HTML import dialog: "import external images into MediaDB" options --- function resetImportHtmlMediaOptions() { Loading Loading @@ -672,6 +717,10 @@ // everything rather than blocking on an empty picker. function goToImportHtmlStep2(html) { importHtmlPendingSource = html; // Paste/upload/manual edits have no known origin URL -- only // loadImportHtmlFromUrl sets importHtmlPendingBaseUrl, and only for // HTML it fetched itself unmodified. importHtmlPendingBaseUrl = ''; EditorApi.parseHtml(html).then(function(resp) { importHtmlPendingCandidates = resp.candidates || []; if (!importHtmlPendingCandidates.length) { Loading @@ -692,7 +741,7 @@ // Fallback used when the picker has nothing to show: import the whole // document directly instead of leaving the dialog stuck. function importHtmlDirectImport(html) { EditorApi.importHtml(html).then(function() { EditorApi.importHtml(html, null, null, null, null, importHtmlPendingBaseUrl).then(function() { document.getElementById('import-html-dialog').close(); Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); Loading Loading @@ -903,6 +952,20 @@ document.getElementById('revisions-dialog').close(); }); // HTML Import dialog: import from URL (step 1, alternative to paste/upload) document.getElementById('btn-import-html-load-url').addEventListener('click', function() { var url = document.getElementById('import-html-url').value.trim(); if (!url) return; loadImportHtmlFromUrl(url); }); document.getElementById('import-html-url').addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); document.getElementById('btn-import-html-load-url').click(); } }); // HTML Import dialog: paste/upload (step 1) -> block picker (step 2) -> import document.getElementById('import-html-file').addEventListener('change', function() { var file = this.files[0]; Loading Loading @@ -972,7 +1035,7 @@ } } var mediaImport = getImportHtmlMediaImportOptions(); EditorApi.importHtml(html, null, null, sections, mediaImport).then(function(resp) { EditorApi.importHtml(html, null, null, sections, mediaImport, importHtmlPendingBaseUrl).then(function(resp) { document.getElementById('import-html-dialog').close(); Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); Loading
editor/src/htmlimport.cpp +99 −1 Changes for editor/src/htmlimport.cpp: 99 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -991,6 +991,102 @@ void blogi::htmlimport::htmlElementToWidgetXml( namespace { // scheme://host:port for @p base, reconstructed the same way HttpUrl::print() // does (always with an explicit port) so a re-parse of the result round-trips. std::string urlOrigin(const libhttppp::HttpUrl &base) { return (base.getProtocol() == libhttppp::HttpUrl::HTTPS ? "https://" : "http://") + base.getHost() + ":" + std::to_string(base.getPort()); } // Resolves a possibly-relative reference taken from a page fetched from // @p baseUrl (an <img src>, <a href>, ... value) into an absolute URL. // Already-absolute http(s) URLs, protocol-relative ("//host/..."), fragment- // only ("#..."), and non-fetchable schemes (data:, mailto:, javascript:, // tel:) are passed through -- the last group deliberately, since resolving // them against baseUrl would produce a nonsensical http(s) URL. Malformed // baseUrl (shouldn't happen -- it's always the URL we ourselves just fetched // the page from) falls back to returning @p ref unchanged. std::string resolveUrl(const std::string &baseUrl, const std::string &ref) { if (ref.empty() || ref[0] == '#') return ref; if (ref.compare(0, 7, "http://") == 0 || ref.compare(0, 8, "https://") == 0) return ref; if (ref.compare(0, 5, "data:") == 0 || ref.compare(0, 7, "mailto:") == 0 || ref.compare(0, 11, "javascript:") == 0 || ref.compare(0, 4, "tel:") == 0) return ref; libhttppp::HttpUrl base; try { base = libhttppp::HttpUrl(baseUrl); } catch (...) { return ref; } if (ref.compare(0, 2, "//") == 0) return (base.getProtocol() == libhttppp::HttpUrl::HTTPS ? "https:" : "http:") + ref; if (ref[0] == '/') return urlOrigin(base) + ref; // Relative to the directory of the base path (everything up to, and // including, its last '/'). const std::string &basePath = base.getPath(); std::string combined = basePath.substr(0, basePath.find_last_of('/') + 1) + ref; std::vector<std::string> segs; std::stringstream ss(combined); std::string seg; while (std::getline(ss, seg, '/')) { if (seg.empty() || seg == ".") continue; if (seg == "..") { if (!segs.empty()) segs.pop_back(); } else segs.push_back(seg); } std::string path = "/"; for (size_t i = 0; i < segs.size(); ++i) { path += segs[i]; if (i + 1 < segs.size()) path += "/"; } return urlOrigin(base) + path; } // Walks the whole parsed document (unlike collectTopLevelElements, this // includes <head> -- external stylesheets/scripts live there) and rewrites // every document-relative resource/link reference to absolute, in place, so // the rest of the import pipeline (which only ever looks at the attribute // value, see e.g. the http(s)-prefix checks around the <link>/<script> // fetch loop in convertHtmlToWidgetXml, or the plain SetAttribute("src", ...) // in htmlElementToWidgetXml's Image/Video branches) never has to know the // page came from a URL rather than being pasted/uploaded already-absolute. void rewriteRelativeUrls(libhtmlpp::Element *node, const std::string &baseUrl) { while (node) { int type = node->getType(); if (type == libhtmlpp::ScriptEL) { auto *sel = static_cast<libhtmlpp::ScriptElement*>(node); std::string src = sel->getAtributte("src"); if (!src.empty()) sel->setAttribute("src", resolveUrl(baseUrl, src)); } else if (type == libhtmlpp::HtmlEl || type == libhtmlpp::SvgEL || type == libhtmlpp::TextAreaEL) { auto *hel = static_cast<libhtmlpp::HtmlElement*>(node); std::string tag = hel->getTagname(); std::transform(tag.begin(), tag.end(), tag.begin(), [](unsigned char c) { return std::tolower(c); }); if (tag == "img" || tag == "source" || tag == "video" || tag == "audio") { std::string src = hel->getAtributte("src"); if (!src.empty()) hel->setAttribute("src", resolveUrl(baseUrl, src)); } else if (tag == "link" || tag == "a") { std::string href = hel->getAtributte("href"); if (!href.empty()) hel->setAttribute("href", resolveUrl(baseUrl, href)); } rewriteRelativeUrls(hel->firstChild(), baseUrl); } node = node->nextElement(); } } // Collects, in document order, the "importable" element children of a given // node: direct children of <body> (after transparently unwrapping <html>) at // the document root, or a node's own direct children when called deeper in Loading Loading @@ -1166,7 +1262,8 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, std::string &outGlobalCss, std::string &outGlobalJs, const std::set<std::string> *selectedPaths, ImportStats *outStats) ImportStats *outStats, const std::string &baseUrl) { libhtmlpp::HtmlPage page; libhtmlpp::HtmlElement index; Loading @@ -1181,6 +1278,7 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, try { page.loadString(index, html); root = &index; if (!baseUrl.empty()) rewriteRelativeUrls(root, baseUrl); } catch (...) { // Fallback: single CustomHtml widget with raw HTML tinyxml2::XMLElement *w = xmlDoc.NewElement("CustomHtml"); Loading
editor/src/htmlimport.h +11 −2 Changes for editor/src/htmlimport.h: 11 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -127,13 +127,22 @@ namespace htmlimport { * the whole document is imported (today's behavior). Selecting both an * ancestor and one of its descendants imports the descendant twice * (once as part of the ancestor's subtree, once standalone); this is * intentionally not deduplicated -- deselect the ancestor instead. */ * intentionally not deduplicated -- deselect the ancestor instead. * If @p baseUrl is non-empty (the page was fetched from a URL rather * than pasted/uploaded), document-relative references -- <img src>, * <source src>, <video>/<audio> src, <a href>, <link href>, <script * src> -- are resolved against it before anything else runs, so * relative resources on the original page still work once hosted by * blogi (see rewriteRelativeUrls in htmlimport.cpp). Already-absolute * references and non-http(s) schemes (data:, mailto:, javascript:, * fragment-only "#...") are left untouched. */ bool convertHtmlToWidgetXml(const std::string &html, std::string &outXml, std::string &outGlobalCss, std::string &outGlobalJs, const std::set<std::string> *selectedPaths = nullptr, ImportStats *outStats = nullptr); ImportStats *outStats = nullptr, const std::string &baseUrl = ""); } // namespace htmlimport } // namespace blogi