Loading editor/html/index.html +7 −0 Original line number Diff line number Diff line Loading @@ -333,6 +333,13 @@ <select id="import-html-media-album" disabled></select> </div> </div> <div class="set-field"> <label><input type="checkbox" id="import-html-extra-css-enabled"> <span data-i18n="I18N_IMPORT_HTML_EXTRA_CSS_ENABLE">Zusätzliches CSS anhängen (z.B. nicht automatisch geladene Stylesheets)</span></label> </div> <div id="import-html-extra-css-options" style="display:none"> <label class="file-upload-label">📂 <span data-i18n="I18N_UPLOAD_FILE">Datei wählen</span><input type="file" id="import-html-extra-css-file" accept=".css,.txt" multiple style="display:none"></label> <textarea id="import-html-extra-css-content" rows="6" placeholder="CSS hier einfügen..."></textarea> </div> </div> <div id="import-html-step2" style="display:none"> <p class="hint" data-i18n="I18N_IMPORT_HTML_PICK_HINT">Welche Bereiche sollen importiert werden?</p> Loading editor/html/js/api.js +17 −5 Original line number Diff line number Diff line Loading @@ -531,8 +531,17 @@ var EditorApi = (function() { }, // HTML import/export parseHtml: function(html) { return request('POST', '/api/document/parse-html', { html: html }); // extraCss (optional): CSS the user attached separately from the // HTML itself -- e.g. a stylesheet the auto-fetcher couldn't reach // (a relative/local <link href>, or a CORS/network failure) -- see // handleParseHtml/convertHtmlToWidgetXml's extraCss parameter. Merged // into the same stylesheet used for per-element style resolution, // not just appended to the resulting global_css. parseHtml: function(html, extraCss) { var data = { html: html }; if (extraCss) data.extra_css = extraCss; return request('POST', '/api/document/parse-html', data); }, // Fetches @p url server-side and parses it exactly like parseHtml, but Loading @@ -540,17 +549,20 @@ var EditorApi = (function() { // 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 }); parseHtmlUrl: function(url, extraCss) { var data = { url: url }; if (extraCss) data.extra_css = extraCss; return request('POST', '/api/document/parse-html', data); }, importHtml: function(html, mode, parentUuid, sections, mediaImport, baseUrl) { importHtml: function(html, mode, parentUuid, sections, mediaImport, baseUrl, extraCss) { 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; if (extraCss) data.extra_css = extraCss; return request('POST', '/api/document/import-html', data); }, Loading editor/html/js/editor.js +50 −4 Original line number Diff line number Diff line Loading @@ -544,6 +544,7 @@ importHtmlPendingCandidates = []; importHtmlPendingBaseUrl = ''; resetImportHtmlMediaOptions(); resetImportHtmlExtraCss(); showImportHtmlStep(1); document.getElementById('import-html-dialog').showModal(); } Loading @@ -555,6 +556,24 @@ el.className = 'set-status' + (isError ? ' error' : ''); } // CSS the user attaches separately from the HTML itself -- e.g. a // stylesheet the auto-fetcher couldn't reach (a relative/local <link // href> with no known base URL, or a CORS/network failure). Read fresh // from the field on every use (unlike importHtmlPendingBaseUrl, it // doesn't need to survive a server round-trip) and merged straight into // the same stylesheet used for per-element style resolution -- see // convertHtmlToWidgetXml's extraCss parameter. function resetImportHtmlExtraCss() { document.getElementById('import-html-extra-css-enabled').checked = false; document.getElementById('import-html-extra-css-options').style.display = 'none'; document.getElementById('import-html-extra-css-content').value = ''; } function getImportHtmlExtraCss() { if (!document.getElementById('import-html-extra-css-enabled').checked) return ''; return document.getElementById('import-html-extra-css-content').value.trim(); } // --- CSS import dialog (paste/upload/URL) -- unlike HTML import there is // no widget tree/block picker: the result always merges straight into the // document's page-level GlobalCss (see handleImportCss), so both the Loading Loading @@ -595,7 +614,7 @@ 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) { EditorApi.parseHtmlUrl(url, getImportHtmlExtraCss()).then(function(resp) { btn.disabled = false; setImportHtmlUrlStatus(''); var html = resp.html || ''; Loading Loading @@ -755,7 +774,7 @@ // loadImportHtmlFromUrl sets importHtmlPendingBaseUrl, and only for // HTML it fetched itself unmodified. importHtmlPendingBaseUrl = ''; EditorApi.parseHtml(html).then(function(resp) { EditorApi.parseHtml(html, getImportHtmlExtraCss()).then(function(resp) { importHtmlPendingCandidates = resp.candidates || []; if (!importHtmlPendingCandidates.length) { importHtmlDirectImport(html); Loading @@ -775,7 +794,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, null, null, null, null, importHtmlPendingBaseUrl).then(function() { EditorApi.importHtml(html, null, null, null, null, importHtmlPendingBaseUrl, getImportHtmlExtraCss()).then(function() { document.getElementById('import-html-dialog').close(); Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); Loading Loading @@ -1057,6 +1076,33 @@ } }); document.getElementById('import-html-extra-css-enabled').addEventListener('change', function() { document.getElementById('import-html-extra-css-options').style.display = this.checked ? '' : 'none'; }); // Reads each selected file in order, appending its text to the extra- // CSS textarea as it finishes (so pasted content and multiple file // selections all accumulate rather than overwrite each other). document.getElementById('import-html-extra-css-file').addEventListener('change', function() { var files = Array.prototype.slice.call(this.files); this.value = ''; if (!files.length) return; var textarea = document.getElementById('import-html-extra-css-content'); function readNext(i) { if (i >= files.length) return; var reader = new FileReader(); reader.onload = function(e) { var css = e.target.result; if (css.trim()) { textarea.value = (textarea.value ? textarea.value + '\n' : '') + css; } readNext(i + 1); }; reader.readAsText(files[i]); } readNext(0); }); document.getElementById('btn-import-html-confirm').addEventListener('click', function() { var html = importHtmlPendingSource; if (!html) return; Loading @@ -1069,7 +1115,7 @@ } } var mediaImport = getImportHtmlMediaImportOptions(); EditorApi.importHtml(html, null, null, sections, mediaImport, importHtmlPendingBaseUrl).then(function(resp) { EditorApi.importHtml(html, null, null, sections, mediaImport, importHtmlPendingBaseUrl, getImportHtmlExtraCss()).then(function(resp) { document.getElementById('import-html-dialog').close(); Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); Loading editor/src/htmlimport.cpp +24 −1 Original line number Diff line number Diff line Loading @@ -2031,7 +2031,8 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, std::string &outGlobalJs, const std::set<std::string> *selectedPaths, ImportStats *outStats, const std::string &baseUrl) const std::string &baseUrl, const std::string &extraCss) { libhtmlpp::HtmlPage page; libhtmlpp::HtmlElement index; Loading Loading @@ -2096,6 +2097,28 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, std::string globalJs; libhtmlpp::CSSStyleSheet stylesheet; // User-attached CSS -- e.g. a stylesheet the auto-fetcher couldn't reach // (a relative/local <link href> with no baseUrl, or a CORS/network // failure) that the import dialog lets the user paste/upload alongside // the HTML itself (see webedit_api.cpp's handleParseHtml/handleImportHtml). // Merged in before any of this page's own <style>/<link> content below, // so this page's own rules keep the final say for any selector both // define -- same "last one wins" cascade approximation this file already // relies on elsewhere (see collectApproximateMatches). if (!extraCss.empty()) { std::string extra = extraCss; if (!baseUrl.empty()) { extra = rewriteCssUrls(extra, [&](const std::string &ref) { return resolveUrl(baseUrl, ref); }); } libhtmlpp::CSSStyleSheet partial; partial.parse(extra); for (const auto &rule : partial.getRules()) { stylesheet.addRule(rule); } } std::stack<libhtmlpp::Element*> searchStack; searchStack.push(root); while (!searchStack.empty()) { Loading editor/src/htmlimport.h +12 −2 Original line number Diff line number Diff line Loading @@ -164,14 +164,24 @@ namespace htmlimport { * relative resources on the original page still work once hosted by * blogi (see rewriteRelativeUrls/rewriteCssUrls in htmlimport.cpp). * Already-absolute references and non-http(s) schemes (data:, mailto:, * javascript:, fragment-only "#...") are left untouched. */ * javascript:, fragment-only "#...") are left untouched. * If @p extraCss is non-empty, it is parsed and merged into the same * stylesheet used for per-element style resolution (not just appended * to @p outGlobalCss afterwards) -- BEFORE the document's own <style>/ * <link> content, so the document's own rules still win any selector * both define. Meant for CSS the import dialog's user attaches * separately (e.g. a stylesheet the auto-fetcher couldn't reach: a * relative/local <link href> with no @p baseUrl, or a CORS/network * failure) -- see convertCssToGlobalCss for importing a stylesheet on * its own, with no HTML document alongside it. */ 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, const std::string &baseUrl = ""); const std::string &baseUrl = "", const std::string &extraCss = ""); /** Convert a standalone CSS stylesheet (pasted/uploaded/fetched on its * own, not part of an HTML document) into global CSS text, using the Loading Loading
editor/html/index.html +7 −0 Original line number Diff line number Diff line Loading @@ -333,6 +333,13 @@ <select id="import-html-media-album" disabled></select> </div> </div> <div class="set-field"> <label><input type="checkbox" id="import-html-extra-css-enabled"> <span data-i18n="I18N_IMPORT_HTML_EXTRA_CSS_ENABLE">Zusätzliches CSS anhängen (z.B. nicht automatisch geladene Stylesheets)</span></label> </div> <div id="import-html-extra-css-options" style="display:none"> <label class="file-upload-label">📂 <span data-i18n="I18N_UPLOAD_FILE">Datei wählen</span><input type="file" id="import-html-extra-css-file" accept=".css,.txt" multiple style="display:none"></label> <textarea id="import-html-extra-css-content" rows="6" placeholder="CSS hier einfügen..."></textarea> </div> </div> <div id="import-html-step2" style="display:none"> <p class="hint" data-i18n="I18N_IMPORT_HTML_PICK_HINT">Welche Bereiche sollen importiert werden?</p> Loading
editor/html/js/api.js +17 −5 Original line number Diff line number Diff line Loading @@ -531,8 +531,17 @@ var EditorApi = (function() { }, // HTML import/export parseHtml: function(html) { return request('POST', '/api/document/parse-html', { html: html }); // extraCss (optional): CSS the user attached separately from the // HTML itself -- e.g. a stylesheet the auto-fetcher couldn't reach // (a relative/local <link href>, or a CORS/network failure) -- see // handleParseHtml/convertHtmlToWidgetXml's extraCss parameter. Merged // into the same stylesheet used for per-element style resolution, // not just appended to the resulting global_css. parseHtml: function(html, extraCss) { var data = { html: html }; if (extraCss) data.extra_css = extraCss; return request('POST', '/api/document/parse-html', data); }, // Fetches @p url server-side and parses it exactly like parseHtml, but Loading @@ -540,17 +549,20 @@ var EditorApi = (function() { // 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 }); parseHtmlUrl: function(url, extraCss) { var data = { url: url }; if (extraCss) data.extra_css = extraCss; return request('POST', '/api/document/parse-html', data); }, importHtml: function(html, mode, parentUuid, sections, mediaImport, baseUrl) { importHtml: function(html, mode, parentUuid, sections, mediaImport, baseUrl, extraCss) { 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; if (extraCss) data.extra_css = extraCss; return request('POST', '/api/document/import-html', data); }, Loading
editor/html/js/editor.js +50 −4 Original line number Diff line number Diff line Loading @@ -544,6 +544,7 @@ importHtmlPendingCandidates = []; importHtmlPendingBaseUrl = ''; resetImportHtmlMediaOptions(); resetImportHtmlExtraCss(); showImportHtmlStep(1); document.getElementById('import-html-dialog').showModal(); } Loading @@ -555,6 +556,24 @@ el.className = 'set-status' + (isError ? ' error' : ''); } // CSS the user attaches separately from the HTML itself -- e.g. a // stylesheet the auto-fetcher couldn't reach (a relative/local <link // href> with no known base URL, or a CORS/network failure). Read fresh // from the field on every use (unlike importHtmlPendingBaseUrl, it // doesn't need to survive a server round-trip) and merged straight into // the same stylesheet used for per-element style resolution -- see // convertHtmlToWidgetXml's extraCss parameter. function resetImportHtmlExtraCss() { document.getElementById('import-html-extra-css-enabled').checked = false; document.getElementById('import-html-extra-css-options').style.display = 'none'; document.getElementById('import-html-extra-css-content').value = ''; } function getImportHtmlExtraCss() { if (!document.getElementById('import-html-extra-css-enabled').checked) return ''; return document.getElementById('import-html-extra-css-content').value.trim(); } // --- CSS import dialog (paste/upload/URL) -- unlike HTML import there is // no widget tree/block picker: the result always merges straight into the // document's page-level GlobalCss (see handleImportCss), so both the Loading Loading @@ -595,7 +614,7 @@ 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) { EditorApi.parseHtmlUrl(url, getImportHtmlExtraCss()).then(function(resp) { btn.disabled = false; setImportHtmlUrlStatus(''); var html = resp.html || ''; Loading Loading @@ -755,7 +774,7 @@ // loadImportHtmlFromUrl sets importHtmlPendingBaseUrl, and only for // HTML it fetched itself unmodified. importHtmlPendingBaseUrl = ''; EditorApi.parseHtml(html).then(function(resp) { EditorApi.parseHtml(html, getImportHtmlExtraCss()).then(function(resp) { importHtmlPendingCandidates = resp.candidates || []; if (!importHtmlPendingCandidates.length) { importHtmlDirectImport(html); Loading @@ -775,7 +794,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, null, null, null, null, importHtmlPendingBaseUrl).then(function() { EditorApi.importHtml(html, null, null, null, null, importHtmlPendingBaseUrl, getImportHtmlExtraCss()).then(function() { document.getElementById('import-html-dialog').close(); Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); Loading Loading @@ -1057,6 +1076,33 @@ } }); document.getElementById('import-html-extra-css-enabled').addEventListener('change', function() { document.getElementById('import-html-extra-css-options').style.display = this.checked ? '' : 'none'; }); // Reads each selected file in order, appending its text to the extra- // CSS textarea as it finishes (so pasted content and multiple file // selections all accumulate rather than overwrite each other). document.getElementById('import-html-extra-css-file').addEventListener('change', function() { var files = Array.prototype.slice.call(this.files); this.value = ''; if (!files.length) return; var textarea = document.getElementById('import-html-extra-css-content'); function readNext(i) { if (i >= files.length) return; var reader = new FileReader(); reader.onload = function(e) { var css = e.target.result; if (css.trim()) { textarea.value = (textarea.value ? textarea.value + '\n' : '') + css; } readNext(i + 1); }; reader.readAsText(files[i]); } readNext(0); }); document.getElementById('btn-import-html-confirm').addEventListener('click', function() { var html = importHtmlPendingSource; if (!html) return; Loading @@ -1069,7 +1115,7 @@ } } var mediaImport = getImportHtmlMediaImportOptions(); EditorApi.importHtml(html, null, null, sections, mediaImport, importHtmlPendingBaseUrl).then(function(resp) { EditorApi.importHtml(html, null, null, sections, mediaImport, importHtmlPendingBaseUrl, getImportHtmlExtraCss()).then(function(resp) { document.getElementById('import-html-dialog').close(); Canvas.clearSelection(); DocumentTree.clearSelection(); PropertiesPanel.clear(); Loading
editor/src/htmlimport.cpp +24 −1 Original line number Diff line number Diff line Loading @@ -2031,7 +2031,8 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, std::string &outGlobalJs, const std::set<std::string> *selectedPaths, ImportStats *outStats, const std::string &baseUrl) const std::string &baseUrl, const std::string &extraCss) { libhtmlpp::HtmlPage page; libhtmlpp::HtmlElement index; Loading Loading @@ -2096,6 +2097,28 @@ bool blogi::htmlimport::convertHtmlToWidgetXml(const std::string &html, std::string globalJs; libhtmlpp::CSSStyleSheet stylesheet; // User-attached CSS -- e.g. a stylesheet the auto-fetcher couldn't reach // (a relative/local <link href> with no baseUrl, or a CORS/network // failure) that the import dialog lets the user paste/upload alongside // the HTML itself (see webedit_api.cpp's handleParseHtml/handleImportHtml). // Merged in before any of this page's own <style>/<link> content below, // so this page's own rules keep the final say for any selector both // define -- same "last one wins" cascade approximation this file already // relies on elsewhere (see collectApproximateMatches). if (!extraCss.empty()) { std::string extra = extraCss; if (!baseUrl.empty()) { extra = rewriteCssUrls(extra, [&](const std::string &ref) { return resolveUrl(baseUrl, ref); }); } libhtmlpp::CSSStyleSheet partial; partial.parse(extra); for (const auto &rule : partial.getRules()) { stylesheet.addRule(rule); } } std::stack<libhtmlpp::Element*> searchStack; searchStack.push(root); while (!searchStack.empty()) { Loading
editor/src/htmlimport.h +12 −2 Original line number Diff line number Diff line Loading @@ -164,14 +164,24 @@ namespace htmlimport { * relative resources on the original page still work once hosted by * blogi (see rewriteRelativeUrls/rewriteCssUrls in htmlimport.cpp). * Already-absolute references and non-http(s) schemes (data:, mailto:, * javascript:, fragment-only "#...") are left untouched. */ * javascript:, fragment-only "#...") are left untouched. * If @p extraCss is non-empty, it is parsed and merged into the same * stylesheet used for per-element style resolution (not just appended * to @p outGlobalCss afterwards) -- BEFORE the document's own <style>/ * <link> content, so the document's own rules still win any selector * both define. Meant for CSS the import dialog's user attaches * separately (e.g. a stylesheet the auto-fetcher couldn't reach: a * relative/local <link href> with no @p baseUrl, or a CORS/network * failure) -- see convertCssToGlobalCss for importing a stylesheet on * its own, with no HTML document alongside it. */ 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, const std::string &baseUrl = ""); const std::string &baseUrl = "", const std::string &extraCss = ""); /** Convert a standalone CSS stylesheet (pasted/uploaded/fetched on its * own, not part of an HTML document) into global CSS text, using the Loading