diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index f81bb46086..e39a350ef2 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -86,6 +86,7 @@ set(SOURCES Layout/LayoutWidget.cpp Layout/LineBox.cpp Layout/LineBoxFragment.cpp + Loader/FrameLoader.cpp Loader/ImageResource.cpp Loader/Resource.cpp Loader/ResourceLoader.cpp diff --git a/Libraries/LibWeb/Frame.cpp b/Libraries/LibWeb/Frame.cpp index 3e72cacb99..00688836fe 100644 --- a/Libraries/LibWeb/Frame.cpp +++ b/Libraries/LibWeb/Frame.cpp @@ -33,11 +33,13 @@ namespace Web { Frame::Frame() + : m_loader(*this) { } Frame::Frame(PageView& page_view) - : m_page_view(page_view.make_weak_ptr()) + : m_loader(*this) + , m_page_view(page_view.make_weak_ptr()) { } @@ -57,6 +59,9 @@ void Frame::set_document(Document* document) if (m_document) m_document->attach_to_frame({}, *this); + + if (on_set_document) + on_set_document(m_document); } void Frame::set_size(const Gfx::Size& size) @@ -98,4 +103,14 @@ void Frame::did_scroll(Badge) }); } +void Frame::scroll_to_anchor(const String& fragment) +{ + // FIXME: We should be able to scroll iframes to an anchor, too! + if (!m_page_view) + return; + // FIXME: This logic is backwards, the work should be done in here, + // and then we just request that the "view" scrolls to a certain content offset. + m_page_view->scroll_to_anchor(fragment); +} + } diff --git a/Libraries/LibWeb/Frame.h b/Libraries/LibWeb/Frame.h index eda008379b..637ebbfbb7 100644 --- a/Libraries/LibWeb/Frame.h +++ b/Libraries/LibWeb/Frame.h @@ -30,8 +30,10 @@ #include #include #include +#include #include #include +#include #include namespace Web { @@ -57,17 +59,29 @@ public: void set_size(const Gfx::Size&); void set_needs_display(const Gfx::Rect&); + Function on_set_needs_display; + Function on_title_change; + Function on_load_start; + Function on_favicon_change; + Function on_set_document; void set_viewport_rect(const Gfx::Rect&); Gfx::Rect viewport_rect() const { return m_viewport_rect; } void did_scroll(Badge); + FrameLoader& loader() { return m_loader; } + const FrameLoader& loader() const { return m_loader; } + + void scroll_to_anchor(const String&); + private: Frame(); explicit Frame(PageView&); + FrameLoader m_loader; + WeakPtr m_page_view; RefPtr m_document; Gfx::Size m_size; diff --git a/Libraries/LibWeb/Loader/FrameLoader.cpp b/Libraries/LibWeb/Loader/FrameLoader.cpp new file mode 100644 index 0000000000..aaf7dc55ac --- /dev/null +++ b/Libraries/LibWeb/Loader/FrameLoader.cpp @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2020, Andreas Kling + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web { + +FrameLoader::FrameLoader(Frame& frame) + : m_frame(frame) +{ +} + +FrameLoader::~FrameLoader() +{ +} + +static RefPtr create_markdown_document(const ByteBuffer& data, const URL& url) +{ + auto markdown_document = Markdown::Document::parse(data); + if (!markdown_document) + return nullptr; + + return parse_html_document(markdown_document->render_to_html(), url); +} + +static RefPtr create_text_document(const ByteBuffer& data, const URL& url) +{ + auto document = adopt(*new Document(url)); + + auto html_element = document->create_element("html"); + document->append_child(html_element); + + auto head_element = document->create_element("head"); + html_element->append_child(head_element); + auto title_element = document->create_element("title"); + head_element->append_child(title_element); + + auto title_text = document->create_text_node(url.basename()); + title_element->append_child(title_text); + + auto body_element = document->create_element("body"); + html_element->append_child(body_element); + + auto pre_element = create_element(document, "pre"); + body_element->append_child(pre_element); + + pre_element->append_child(document->create_text_node(String::copy(data))); + return document; +} + +static RefPtr create_image_document(const ByteBuffer& data, const URL& url) +{ + auto document = adopt(*new Document(url)); + + auto image_decoder = Gfx::ImageDecoder::create(data.data(), data.size()); + auto bitmap = image_decoder->bitmap(); + ASSERT(bitmap); + + auto html_element = create_element(document, "html"); + document->append_child(html_element); + + auto head_element = create_element(document, "head"); + html_element->append_child(head_element); + auto title_element = create_element(document, "title"); + head_element->append_child(title_element); + + auto basename = LexicalPath(url.path()).basename(); + auto title_text = adopt(*new Text(document, String::format("%s [%dx%d]", basename.characters(), bitmap->width(), bitmap->height()))); + title_element->append_child(title_text); + + auto body_element = create_element(document, "body"); + html_element->append_child(body_element); + + auto image_element = create_element(document, "img"); + image_element->set_attribute(HTML::AttributeNames::src, url.to_string()); + body_element->append_child(image_element); + + return document; +} + +static RefPtr create_gemini_document(const ByteBuffer& data, const URL& url) +{ + auto markdown_document = Gemini::Document::parse({ (const char*)data.data(), data.size() }, url); + + return parse_html_document(markdown_document->render_to_html(), url); +} + +String encoding_from_content_type(const String& content_type) +{ + auto offset = content_type.index_of("charset="); + if (offset.has_value()) + return content_type.substring(offset.value() + 8, content_type.length() - offset.value() - 8).to_lowercase(); + + return "utf-8"; +} + +String mime_type_from_content_type(const String& content_type) +{ + auto offset = content_type.index_of(";"); + if (offset.has_value()) + return content_type.substring(0, offset.value()).to_lowercase(); + + return content_type; +} + +static String guess_mime_type_based_on_filename(const URL& url) +{ + if (url.path().ends_with(".png")) + return "image/png"; + if (url.path().ends_with(".gif")) + return "image/gif"; + if (url.path().ends_with(".md")) + return "text/markdown"; + if (url.path().ends_with(".html") || url.path().ends_with(".htm")) + return "text/html"; + return "text/plain"; +} + +RefPtr FrameLoader::create_document_from_mime_type(const ByteBuffer& data, const URL& url, const String& mime_type, const String& encoding) +{ + if (mime_type.starts_with("image/")) + return create_image_document(data, url); + if (mime_type == "text/plain") + return create_text_document(data, url); + if (mime_type == "text/markdown") + return create_markdown_document(data, url); + if (mime_type == "text/gemini") + return create_gemini_document(data, url); + if (mime_type == "text/html") { + if (m_use_old_parser) + return parse_html_document(data, url, encoding); + HTMLDocumentParser parser(data, encoding); + parser.run(url); + return parser.document(); + } + return nullptr; +} + +bool FrameLoader::load(const URL& url) +{ + dbg() << "FrameLoader::load: " << url; + + if (!url.is_valid()) { + load_error_page(url, "Invalid URL"); + return false; + } + + ResourceLoader::the().load( + url, + [this, url](auto data, auto& response_headers) { + // FIXME: Also check HTTP status code before redirecting + auto location = response_headers.get("Location"); + if (location.has_value()) { + load(location.value()); + return; + } + + if (data.is_null()) { + load_error_page(url, "No data"); + return; + } + + String encoding = "utf-8"; + String mime_type; + + auto content_type = response_headers.get("Content-Type"); + if (content_type.has_value()) { + dbg() << "Content-Type header: _" << content_type.value() << "_"; + encoding = encoding_from_content_type(content_type.value()); + mime_type = mime_type_from_content_type(content_type.value()); + } else { + dbg() << "No Content-Type header to go on! Guessing based on filename..."; + mime_type = guess_mime_type_based_on_filename(url); + } + + dbg() << "I believe this content has MIME type '" << mime_type << "', encoding '" << encoding << "'"; + auto document = create_document_from_mime_type(data, url, mime_type, encoding); + ASSERT(document); + frame().set_document(document); + + if (!url.fragment().is_empty()) + frame().scroll_to_anchor(url.fragment()); + + if (frame().on_title_change) + frame().on_title_change(document->title()); + }, + [this, url](auto error) { + load_error_page(url, error); + }); + + if (frame().on_load_start) + frame().on_load_start(url); + + if (url.protocol() != "file" && url.protocol() != "about") { + URL favicon_url; + favicon_url.set_protocol(url.protocol()); + favicon_url.set_host(url.host()); + favicon_url.set_port(url.port()); + favicon_url.set_path("/favicon.ico"); + + ResourceLoader::the().load( + favicon_url, + [this, favicon_url](auto data, auto&) { + dbg() << "Favicon downloaded, " << data.size() << " bytes from " << favicon_url; + auto decoder = Gfx::ImageDecoder::create(data.data(), data.size()); + auto bitmap = decoder->bitmap(); + if (!bitmap) { + dbg() << "Could not decode favicon " << favicon_url; + return; + } + dbg() << "Decoded favicon, " << bitmap->size(); + if (frame().on_favicon_change) + frame().on_favicon_change(*bitmap); + }); + } + + return true; +} + +void FrameLoader::load_error_page(const URL& failed_url, const String& error) +{ + auto error_page_url = "file:///res/html/error.html"; + ResourceLoader::the().load( + error_page_url, + [this, failed_url, error](auto data, auto&) { + ASSERT(!data.is_null()); + auto html = String::format( + String::copy(data).characters(), + escape_html_entities(failed_url.to_string()).characters(), + escape_html_entities(error).characters()); + auto document = parse_html_document(html, failed_url); + ASSERT(document); + frame().set_document(document); + if (frame().on_title_change) + frame().on_title_change(document->title()); + }, + [](auto error) { + dbg() << "Failed to load error page: " << error; + ASSERT_NOT_REACHED(); + }); +} + +} diff --git a/Libraries/LibWeb/Loader/FrameLoader.h b/Libraries/LibWeb/Loader/FrameLoader.h new file mode 100644 index 0000000000..c2c504e5b7 --- /dev/null +++ b/Libraries/LibWeb/Loader/FrameLoader.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020, Andreas Kling + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include + +namespace Web { + +class FrameLoader { +public: + explicit FrameLoader(Frame&); + ~FrameLoader(); + + bool load(const URL&); + + Frame& frame() { return m_frame; } + const Frame& frame() const { return m_frame; } + + void set_use_old_parser(bool b) { m_use_old_parser = b; } + +private: + void load_error_page(const URL& failed_url, const String& error_message); + RefPtr create_document_from_mime_type(const ByteBuffer&, const URL&, const String& mime_type, const String& encoding); + + Frame& m_frame; + + bool m_use_old_parser { false }; +}; + +} diff --git a/Libraries/LibWeb/PageView.cpp b/Libraries/LibWeb/PageView.cpp index 00c36afe5d..4b15377098 100644 --- a/Libraries/LibWeb/PageView.cpp +++ b/Libraries/LibWeb/PageView.cpp @@ -32,10 +32,8 @@ #include #include #include -#include #include #include -#include #include #include #include @@ -46,11 +44,9 @@ #include #include #include -#include -#include -#include -#include #include +#include +#include #include //#define SELECTION_DEBUG @@ -60,6 +56,20 @@ namespace Web { PageView::PageView() : m_main_frame(Web::Frame::create(*this)) { + main_frame().on_set_document = [this](auto* document) { + if (on_set_document) + on_set_document(document); + layout_and_sync_size(); + update(); + }; + main_frame().on_title_change = [this](auto& title) { + if (on_title_change) + on_title_change(title); + }; + main_frame().on_load_start = [this](auto& url) { + if (on_load_start) + on_load_start(url); + }; main_frame().on_set_needs_display = [this](auto& content_rect) { if (content_rect.is_empty()) { update(); @@ -78,39 +88,6 @@ PageView::~PageView() { } -void PageView::set_document(Document* new_document) -{ - RefPtr old_document = document(); - - if (new_document == old_document) - return; - - if (old_document) - old_document->on_layout_updated = nullptr; - - main_frame().set_document(new_document); - - if (on_set_document) - on_set_document(new_document); - - if (new_document) { - new_document->on_layout_updated = [this] { - layout_and_sync_size(); - update(); - }; - } - -#ifdef HTML_DEBUG - if (document != nullptr) { - dbgprintf("\033[33;1mLayout tree before layout:\033[0m\n"); - ::dump_tree(*layout_root()); - } -#endif - - layout_and_sync_size(); - update(); -} - void PageView::layout_and_sync_size() { if (!document()) @@ -330,233 +307,12 @@ void PageView::reload() load(main_frame().document()->url()); } -static RefPtr create_markdown_document(const ByteBuffer& data, const URL& url) +bool PageView::load(const URL& url) { - auto markdown_document = Markdown::Document::parse(data); - if (!markdown_document) - return nullptr; - - return parse_html_document(markdown_document->render_to_html(), url); -} - -static RefPtr create_text_document(const ByteBuffer& data, const URL& url) -{ - auto document = adopt(*new Document(url)); - - auto html_element = document->create_element("html"); - document->append_child(html_element); - - auto head_element = document->create_element("head"); - html_element->append_child(head_element); - auto title_element = document->create_element("title"); - head_element->append_child(title_element); - - auto title_text = document->create_text_node(url.basename()); - title_element->append_child(title_text); - - auto body_element = document->create_element("body"); - html_element->append_child(body_element); - - auto pre_element = create_element(document, "pre"); - body_element->append_child(pre_element); - - pre_element->append_child(document->create_text_node(String::copy(data))); - return document; -} - -static RefPtr create_image_document(const ByteBuffer& data, const URL& url) -{ - auto document = adopt(*new Document(url)); - - auto image_decoder = Gfx::ImageDecoder::create(data.data(), data.size()); - auto bitmap = image_decoder->bitmap(); - ASSERT(bitmap); - - auto html_element = create_element(document, "html"); - document->append_child(html_element); - - auto head_element = create_element(document, "head"); - html_element->append_child(head_element); - auto title_element = create_element(document, "title"); - head_element->append_child(title_element); - - auto basename = LexicalPath(url.path()).basename(); - auto title_text = adopt(*new Text(document, String::format("%s [%dx%d]", basename.characters(), bitmap->width(), bitmap->height()))); - title_element->append_child(title_text); - - auto body_element = create_element(document, "body"); - html_element->append_child(body_element); - - auto image_element = create_element(document, "img"); - image_element->set_attribute(HTML::AttributeNames::src, url.to_string()); - body_element->append_child(image_element); - - return document; -} - -static RefPtr create_gemini_document(const ByteBuffer& data, const URL& url) -{ - auto markdown_document = Gemini::Document::parse({ (const char*)data.data(), data.size() }, url); - - return parse_html_document(markdown_document->render_to_html(), url); -} - -String encoding_from_content_type(const String& content_type) -{ - auto offset = content_type.index_of("charset="); - if (offset.has_value()) - return content_type.substring(offset.value() + 8, content_type.length() - offset.value() - 8).to_lowercase(); - - return "utf-8"; -} - -String mime_type_from_content_type(const String& content_type) -{ - auto offset = content_type.index_of(";"); - if (offset.has_value()) - return content_type.substring(0, offset.value()).to_lowercase(); - - return content_type; -} - -static String guess_mime_type_based_on_filename(const URL& url) -{ - if (url.path().ends_with(".png")) - return "image/png"; - if (url.path().ends_with(".gif")) - return "image/gif"; - if (url.path().ends_with(".md")) - return "text/markdown"; - if (url.path().ends_with(".html") || url.path().ends_with(".htm")) - return "text/html"; - return "text/plain"; -} - -RefPtr PageView::create_document_from_mime_type(const ByteBuffer& data, const URL& url, const String& mime_type, const String& encoding) -{ - if (mime_type.starts_with("image/")) - return create_image_document(data, url); - if (mime_type == "text/plain") - return create_text_document(data, url); - if (mime_type == "text/markdown") - return create_markdown_document(data, url); - if (mime_type == "text/gemini") - return create_gemini_document(data, url); - if (mime_type == "text/html") { - if (m_use_old_parser) - return parse_html_document(data, url, encoding); - HTMLDocumentParser parser(data, encoding); - parser.run(url); - return parser.document(); - } - return nullptr; -} - -void PageView::load(const URL& url) -{ - dbg() << "PageView::load: " << url; - - if (!url.is_valid()) { - load_error_page(url, "Invalid URL"); - return; - } - if (window()) window()->set_override_cursor(GUI::StandardCursor::None); - if (on_load_start) - on_load_start(url); - - ResourceLoader::the().load( - url, - [this, url](auto data, auto& response_headers) { - // FIXME: Also check HTTP status code before redirecting - auto location = response_headers.get("Location"); - if (location.has_value()) { - load(location.value()); - return; - } - - if (data.is_null()) { - load_error_page(url, "No data"); - return; - } - - String encoding = "utf-8"; - String mime_type; - - auto content_type = response_headers.get("Content-Type"); - if (content_type.has_value()) { - dbg() << "Content-Type header: _" << content_type.value() << "_"; - encoding = encoding_from_content_type(content_type.value()); - mime_type = mime_type_from_content_type(content_type.value()); - } else { - dbg() << "No Content-Type header to go on! Guessing based on filename..."; - mime_type = guess_mime_type_based_on_filename(url); - } - - dbg() << "I believe this content has MIME type '" << mime_type << "', encoding '" << encoding << "'"; - auto document = create_document_from_mime_type(data, url, mime_type, encoding); - ASSERT(document); - set_document(document); - - if (!url.fragment().is_empty()) - scroll_to_anchor(url.fragment()); - - if (on_title_change) - on_title_change(document->title()); - }, - [this, url](auto error) { - load_error_page(url, error); - }); - - if (url.protocol() != "file" && url.protocol() != "about") { - URL favicon_url; - favicon_url.set_protocol(url.protocol()); - favicon_url.set_host(url.host()); - favicon_url.set_port(url.port()); - favicon_url.set_path("/favicon.ico"); - - ResourceLoader::the().load( - favicon_url, - [this, favicon_url](auto data, auto&) { - dbg() << "Favicon downloaded, " << data.size() << " bytes from " << favicon_url; - auto decoder = Gfx::ImageDecoder::create(data.data(), data.size()); - auto bitmap = decoder->bitmap(); - if (!bitmap) { - dbg() << "Could not decode favicon " << favicon_url; - return; - } - dbg() << "Decoded favicon, " << bitmap->size(); - if (on_favicon_change) - on_favicon_change(*bitmap); - }); - } - - this->scroll_to_top(); -} - -void PageView::load_error_page(const URL& failed_url, const String& error) -{ - auto error_page_url = "file:///res/html/error.html"; - ResourceLoader::the().load( - error_page_url, - [this, failed_url, error](auto data, auto&) { - ASSERT(!data.is_null()); - auto html = String::format( - String::copy(data).characters(), - escape_html_entities(failed_url.to_string()).characters(), - escape_html_entities(error).characters()); - auto document = parse_html_document(html, failed_url); - ASSERT(document); - set_document(document); - if (on_title_change) - on_title_change(document->title()); - }, - [](auto error) { - dbg() << "Failed to load error page: " << error; - ASSERT_NOT_REACHED(); - }); + return main_frame().loader().load(url); } const LayoutDocument* PageView::layout_root() const @@ -601,6 +357,16 @@ void PageView::scroll_to_anchor(const StringView& name) window()->set_override_cursor(GUI::StandardCursor::None); } +void PageView::set_use_old_parser(bool use_old_parser) +{ + main_frame().loader().set_use_old_parser(use_old_parser); +} + +void PageView::load_empty_document() +{ + main_frame().set_document(nullptr); +} + Document* PageView::document() { return main_frame().document(); @@ -611,6 +377,11 @@ const Document* PageView::document() const return main_frame().document(); } +void PageView::set_document(Document* document) +{ + main_frame().set_document(document); +} + void PageView::dump_selection(const char* event_name) { UNUSED_PARAM(event_name); diff --git a/Libraries/LibWeb/PageView.h b/Libraries/LibWeb/PageView.h index 4151448e4e..d2fa30fdb2 100644 --- a/Libraries/LibWeb/PageView.h +++ b/Libraries/LibWeb/PageView.h @@ -40,10 +40,13 @@ public: virtual ~PageView() override; // FIXME: Remove this once the new parser is ready. - void set_use_old_parser(bool use_old_parser) { m_use_old_parser = use_old_parser; } + void set_use_old_parser(bool use_old_parser); + + void load_empty_document(); Document* document(); const Document* document() const; + void set_document(Document*); const LayoutDocument* layout_root() const; @@ -53,8 +56,7 @@ public: const Web::Frame& main_frame() const { return *m_main_frame; } void reload(); - void load(const URL&); - void load_error_page(const URL&, const String& error); + bool load(const URL&); void scroll_to_anchor(const StringView&); URL url() const; @@ -98,8 +100,6 @@ private: bool m_should_show_line_box_borders { false }; bool m_in_mouse_selection { false }; - - bool m_use_old_parser { false }; }; }