diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 811736f30a2..d023a527a66 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -33,6 +33,7 @@ set(SOURCES Bindings/SyntheticHostDefined.cpp Clipboard/Clipboard.cpp Clipboard/ClipboardEvent.cpp + Clipboard/ClipboardItem.cpp Compression/CompressionStream.cpp Compression/DecompressionStream.cpp Crypto/Crypto.cpp diff --git a/Libraries/LibWeb/Clipboard/ClipboardItem.cpp b/Libraries/LibWeb/Clipboard/ClipboardItem.cpp new file mode 100644 index 00000000000..b060238c7f6 --- /dev/null +++ b/Libraries/LibWeb/Clipboard/ClipboardItem.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2024, Feng Yu + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace Web::Clipboard { + +GC_DEFINE_ALLOCATOR(ClipboardItem); + +// https://w3c.github.io/clipboard-apis/#dom-clipboarditem-clipboarditem +WebIDL::ExceptionOr> ClipboardItem::construct_impl(JS::Realm& realm, OrderedHashMap> const& items, ClipboardItemOptions const& options) +{ + // 1. If items is empty, then throw a TypeError. + if (items.is_empty()) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Items cannot be empty"sv }; + + // 2. If options is empty, then set options["presentationStyle"] = "unspecified". + // NOTE: This step is handled by presentationStyle's default value in ClipboardItemOptions. + + // 3. Set this's clipboard item to a new clipboard item. + auto clipboard_item = realm.create(realm); + + // 4. Set this's clipboard item's presentation style to options["presentationStyle"]. + clipboard_item->m_presentation_style = options.presentation_style; + + // 5. Let types be a list of DOMString. + Vector types; + + // 6. For each (key, value) in items: + for (auto const& [key, value] : items) { + // 2. Let isCustom be false. + bool is_custom = false; + + // 3. If key starts with `"web "` prefix, then: + auto key_without_prefix = key; + if (key.starts_with_bytes(WEB_CUSTOM_FORMAT_PREFIX)) { + // 1. Remove `"web "` prefix and assign the remaining string to key. + key_without_prefix = MUST(key.substring_from_byte_offset(WEB_CUSTOM_FORMAT_PREFIX.length())); + + // 2. Set isCustom to true. + is_custom = true; + } + + // 5. Let mimeType be the result of parsing a MIME type given key. + auto mime_type = MimeSniff::MimeType::parse(key_without_prefix); + + // 6. If mimeType is failure, then throw a TypeError. + if (!mime_type.has_value()) { + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Invalid MIME type: {}", key)) }; + } + + auto mime_type_serialized = mime_type->serialized(); + + // 7. If this's clipboard item's list of representations contains a representation whose MIME type + // is mimeType and whose [representation/isCustom] is isCustom, then throw a TypeError. + auto existing = clipboard_item->m_representations.find_if([&](auto const& item) { + return item.mime_type == mime_type_serialized && item.is_custom == is_custom; + }); + if (!existing.is_end()) { + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Duplicate MIME type: {}", key)) }; + } + + // 11. Let mimeTypeString be the result of serializing a MIME type with mimeType. + // 12. If isCustom is true, prefix mimeTypeString with `"web "`. + auto mime_type_string = is_custom ? MUST(String::formatted("{}{}", WEB_CUSTOM_FORMAT_PREFIX, mime_type_serialized)) : mime_type_serialized; + + // 13. Add mimeTypeString to types. + types.append(move(mime_type_string)); + + // 1. Let representation be a new representation. + // 4. Set representation’s isCustom flag to isCustom. + // 8. Set representation’s MIME type to mimeType. + // 9. Set representation’s data to value. + // 10. Append representation to this's clipboard item's list of representations. + clipboard_item->m_representations.empend(move(mime_type_serialized), is_custom, *value); + } + + // 7. Set this's types array to the result of running create a frozen array from types. + clipboard_item->m_types = types; + + return clipboard_item; +} + +// https://w3c.github.io/clipboard-apis/#dom-clipboarditem-gettype +WebIDL::ExceptionOr> ClipboardItem::get_type(String const& type) +{ + // 1. Let realm be this's relevant realm. + auto& realm = HTML::relevant_realm(*this); + + // 2. Let isCustom be false. + bool is_custom = false; + + // 3. If type starts with `"web "` prefix, then: + auto type_without_prefix = type; + if (type.starts_with_bytes(WEB_CUSTOM_FORMAT_PREFIX)) { + // 1. Remove `"web "` prefix and assign the remaining string to type. + type_without_prefix = MUST(type.substring_from_byte_offset(WEB_CUSTOM_FORMAT_PREFIX.length())); + + // 2. Set isCustom to true. + is_custom = true; + } + + // 4. Let mimeType be the result of parsing a MIME type given type. + auto mime_type = MimeSniff::MimeType::parse(type_without_prefix); + + // 5. If mimeType is failure, then throw a TypeError. + if (!mime_type.has_value()) { + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Invalid MIME type: {}", type)) }; + } + + auto mime_type_serialized = mime_type->serialized(); + + // 6. Let itemTypeList be this's clipboard item's list of representations. + auto const& item_type_list = m_representations; + + // 7. Let p be a new promise in realm. + auto promise = WebIDL::create_promise(realm); + + // 8. For each representation in itemTypeList: + for (auto const& representation : item_type_list) { + // 1. If representation’s MIME type is mimeType and representation’s isCustom is isCustom, then: + if (representation.mime_type == mime_type_serialized && representation.is_custom == is_custom) { + // 1. Let representationDataPromise be the representation’s data. + auto representation_data_promise = representation.data; + + // 2. React to representationDataPromise: + WebIDL::react_to_promise( + *representation_data_promise, + GC::create_function(realm.heap(), [&realm, promise, mime_type_serialized](JS::Value value) -> WebIDL::ExceptionOr { + // 1. If v is a DOMString, then follow the below steps: + if (value.is_string()) { + // 1. Let dataAsBytes be the result of UTF-8 encoding v. + auto utf8_string = value.as_string().utf8_string(); + auto data_as_bytes = MUST(ByteBuffer::copy(utf8_string.bytes())); + + // 2. Let blobData be a Blob created using dataAsBytes with its type set to mimeType, serialized. + auto blob_data = FileAPI::Blob::create(realm, data_as_bytes, mime_type_serialized); + + // 3. Resolve p with blobData. + WebIDL::resolve_promise(realm, promise, blob_data); + } + // 2. If v is a Blob, then follow the below steps: + if (value.is_object() && is(value.as_object())) { + // 1. Resolve p with v. + WebIDL::resolve_promise(realm, promise, value); + } + + return JS::js_undefined(); + }), + // 2. If representationDataPromise was rejected, then: + GC::create_function(realm.heap(), [&realm, type, promise](JS::Value) -> WebIDL::ExceptionOr { + // 1. Reject p with "NotFoundError" DOMException in realm. + WebIDL::reject_promise(realm, promise, WebIDL::NotFoundError::create(realm, MUST(String::formatted("No data found for MIME type: {}", type)))); + + return JS::js_undefined(); + })); + + // 3. Return p. + return promise; + } + } + + // 9. Reject p with "NotFoundError" DOMException in realm. + WebIDL::reject_promise(realm, promise, WebIDL::NotFoundError::create(realm, MUST(String::formatted("No data found for MIME type: {}", type)))); + + // 10. Return p. + return promise; +} + +// https://w3c.github.io/clipboard-apis/#dom-clipboarditem-supports +bool ClipboardItem::supports(JS::VM&, String const& type) +{ + // 1. If type is in mandatory data types or optional data types, then return true. + // 2. If not, then return false. + // TODO: Implement optional data types, like web custom formats and image/svg+xml. + return any_of(MANDATORY_DATA_TYPES, [&](auto supported) { return supported == type; }); +} + +ClipboardItem::ClipboardItem(JS::Realm& realm) + : Bindings::PlatformObject(realm) +{ +} + +ClipboardItem::~ClipboardItem() = default; + +void ClipboardItem::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + WEB_SET_PROTOTYPE_FOR_INTERFACE(ClipboardItem); +} + +void ClipboardItem::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + for (auto& representation : m_representations) { + visitor.visit(representation.data); + } +} + +} diff --git a/Libraries/LibWeb/Clipboard/ClipboardItem.h b/Libraries/LibWeb/Clipboard/ClipboardItem.h new file mode 100644 index 00000000000..8e24942cab2 --- /dev/null +++ b/Libraries/LibWeb/Clipboard/ClipboardItem.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024, Feng Yu + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Web::Clipboard { + +constexpr auto WEB_CUSTOM_FORMAT_PREFIX = "web "sv; + +inline constexpr Array MANDATORY_DATA_TYPES = { + "text/plain"sv, "text/html"sv, "image/png"sv +}; + +struct ClipboardItemOptions { + Bindings::PresentationStyle presentation_style { Bindings::PresentationStyle::Unspecified }; +}; + +// https://w3c.github.io/clipboard-apis/#clipboard-item-interface +class ClipboardItem : public Bindings::PlatformObject { + WEB_PLATFORM_OBJECT(ClipboardItem, Bindings::PlatformObject); + GC_DECLARE_ALLOCATOR(ClipboardItem); + +public: + struct Representation { + String mime_type; // The MIME type (e.g., "text/plain"). + bool is_custom; // Whether this is a web custom format. + GC::Ref data; // The actual data for this representation. + }; + + static WebIDL::ExceptionOr> construct_impl(JS::Realm&, OrderedHashMap> const& items, ClipboardItemOptions const& options = {}); + + virtual ~ClipboardItem() override; + + Bindings::PresentationStyle presentation_style() const { return m_presentation_style; } + + Vector const& types() const { return m_types; } + + WebIDL::ExceptionOr> get_type(String const& type); + + static bool supports(JS::VM&, String const& type); + +private: + ClipboardItem(JS::Realm&); + + virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Cell::Visitor&) override; + + Bindings::PresentationStyle m_presentation_style; + Vector m_types; + Vector m_representations; +}; + +} diff --git a/Libraries/LibWeb/Clipboard/ClipboardItem.idl b/Libraries/LibWeb/Clipboard/ClipboardItem.idl new file mode 100644 index 00000000000..542b88e65b5 --- /dev/null +++ b/Libraries/LibWeb/Clipboard/ClipboardItem.idl @@ -0,0 +1,22 @@ +typedef Promise<(DOMString or Blob)> ClipboardItemData; + +// https://w3c.github.io/clipboard-apis/#clipboard-item-interface +[SecureContext, Exposed=Window] +interface ClipboardItem { + constructor(record items, + optional ClipboardItemOptions options = {}); + + readonly attribute PresentationStyle presentationStyle; + // FIXME: Should be a FrozenArray + readonly attribute sequence types; + + Promise getType(DOMString type); + + static boolean supports(DOMString type); +}; + +enum PresentationStyle { "unspecified", "inline", "attachment" }; + +dictionary ClipboardItemOptions { + PresentationStyle presentationStyle = "unspecified"; +}; diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 1dbc892ad51..27d13ab4523 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -84,6 +84,7 @@ enum class XMLHttpRequestResponseType; namespace Web::Clipboard { class Clipboard; +class ClipboardItem; } namespace Web::Compression { diff --git a/Libraries/LibWeb/idl_files.cmake b/Libraries/LibWeb/idl_files.cmake index 34dc23a850d..856c7f71fa4 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -9,6 +9,7 @@ libweb_js_bindings(Animations/DocumentTimeline) libweb_js_bindings(Animations/KeyframeEffect) libweb_js_bindings(Clipboard/Clipboard) libweb_js_bindings(Clipboard/ClipboardEvent) +libweb_js_bindings(Clipboard/ClipboardItem) libweb_js_bindings(Compression/CompressionStream) libweb_js_bindings(Compression/DecompressionStream) libweb_js_bindings(Crypto/Crypto) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp index 9c1629cd740..5bf2323dcb2 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp @@ -45,6 +45,7 @@ static bool is_platform_object(Type const& type) "CanvasGradient"sv, "CanvasPattern"sv, "CanvasRenderingContext2D"sv, + "ClipboardItem"sv, "CloseWatcher"sv, "CryptoKey"sv, "DataTransfer"sv, @@ -247,6 +248,9 @@ CppType idl_type_name_to_cpp_type(Type const& type, Interface const& interface) if (type.name() == "Function") return { .name = "GC::Ref", .sequence_storage_type = SequenceStorageType::MarkedVector }; + if (type.name() == "Promise") + return { .name = "GC::Root", .sequence_storage_type = SequenceStorageType::MarkedVector }; + if (type.name() == "sequence") { auto& parameterized_type = verify_cast(type); auto& sequence_type = parameterized_type.parameters().first(); @@ -4663,6 +4667,7 @@ void generate_constructor_implementation(IDL::Interface const& interface, String #include #include #include +#include #include #include #include diff --git a/Tests/LibWeb/Text/expected/Clipboard/clipboarditem.txt b/Tests/LibWeb/Text/expected/Clipboard/clipboarditem.txt new file mode 100644 index 00000000000..70dbc3c123c --- /dev/null +++ b/Tests/LibWeb/Text/expected/Clipboard/clipboarditem.txt @@ -0,0 +1,3 @@ +types: text/plain,text/uri-list +getType('text/plain'): hello +getType('text/uri-list'): https://example.com diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index 52d5ae7ddfb..a61ef9f9177 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -60,6 +60,7 @@ ChannelMergerNode CharacterData Clipboard ClipboardEvent +ClipboardItem CloseEvent CloseWatcher Comment diff --git a/Tests/LibWeb/Text/expected/wpt-import/clipboard-apis/clipboard-item.https.txt b/Tests/LibWeb/Text/expected/wpt-import/clipboard-apis/clipboard-item.https.txt new file mode 100644 index 00000000000..b77aaa1e71b --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/clipboard-apis/clipboard-item.https.txt @@ -0,0 +1,34 @@ +Harness status: OK + +Found 28 tests + +17 Pass +11 Fail +Fail ClipboardItem({string, Blob}) succeeds with different types +Pass ClipboardItem() succeeds with empty options +Pass ClipboardItem({}) fails with empty dictionary input +Pass ClipboardItem(Blob) fails +Pass ClipboardItem() fails with null input +Pass ClipboardItem() fails with no input +Fail types() returns correct values +Fail getType(DOMString valid type) succeeds with correct output +Fail getType(DOMString invalid type) succeeds with correct output +Fail getType(DOMString type) rejects correctly when querying for missing type +Fail getType(DOMString valid type) converts DOMString to Blob +Fail getType(DOMString invalid type) converts DOMString to Blob +Pass supports(text/plain) returns true +Pass supports(text/html) returns true +Pass supports(image/png) returns true +Fail supports(text/uri-list) returns true +Fail supports(image/svg+xml) returns true +Fail supports(web foo/bar) returns true +Fail supports(web text/html) returns true +Pass supports(web ) returns false +Pass supports(web) returns false +Pass supports(web foo) returns false +Pass supports(foo/bar) returns false +Pass supports(weB text/html) returns false +Pass supports( web text/html) returns false +Pass supports(not a/real type) returns false +Pass supports() returns false +Pass supports( ) returns false \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/Clipboard/clipboarditem.html b/Tests/LibWeb/Text/input/Clipboard/clipboarditem.html new file mode 100644 index 00000000000..d4da5a61eab --- /dev/null +++ b/Tests/LibWeb/Text/input/Clipboard/clipboarditem.html @@ -0,0 +1,20 @@ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/clipboard-apis/clipboard-item.https.html b/Tests/LibWeb/Text/input/wpt-import/clipboard-apis/clipboard-item.https.html new file mode 100644 index 00000000000..5183e5e0c16 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/clipboard-apis/clipboard-item.https.html @@ -0,0 +1,124 @@ + + +ClipboardItem tests + + + +