LibWeb: Implement the ClipboardItem API

Spec: https://w3c.github.io/clipboard-apis/#clipboard-item-interface
This commit is contained in:
Feng Yu 2024-11-30 20:23:38 -08:00 committed by Tim Ledbetter
commit b3edbd7bf2
Notes: github-actions[bot] 2024-12-20 15:30:21 +00:00
12 changed files with 484 additions and 0 deletions

View file

@ -0,0 +1,208 @@
/*
* Copyright (c) 2024, Feng Yu <f3n67u@outlook.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Clipboard/ClipboardItem.h>
#include <LibWeb/FileAPI/Blob.h>
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::Clipboard {
GC_DEFINE_ALLOCATOR(ClipboardItem);
// https://w3c.github.io/clipboard-apis/#dom-clipboarditem-clipboarditem
WebIDL::ExceptionOr<GC::Ref<ClipboardItem>> ClipboardItem::construct_impl(JS::Realm& realm, OrderedHashMap<String, GC::Root<WebIDL::Promise>> 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<ClipboardItem>(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<String> 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 representations isCustom flag to isCustom.
// 8. Set representations MIME type to mimeType.
// 9. Set representations 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<GC::Ref<WebIDL::Promise>> 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 representations MIME type is mimeType and representations isCustom is isCustom, then:
if (representation.mime_type == mime_type_serialized && representation.is_custom == is_custom) {
// 1. Let representationDataPromise be the representations 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<JS::Value> {
// 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<FileAPI::Blob>(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<JS::Value> {
// 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);
}
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2024, Feng Yu <f3n67u@outlook.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGC/Ptr.h>
#include <LibJS/Runtime/PromiseCapability.h>
#include <LibWeb/Bindings/ClipboardItemPrototype.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/DataTransfer.h>
#include <LibWeb/MimeSniff/MimeType.h>
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<WebIDL::Promise> data; // The actual data for this representation.
};
static WebIDL::ExceptionOr<GC::Ref<ClipboardItem>> construct_impl(JS::Realm&, OrderedHashMap<String, GC::Root<WebIDL::Promise>> const& items, ClipboardItemOptions const& options = {});
virtual ~ClipboardItem() override;
Bindings::PresentationStyle presentation_style() const { return m_presentation_style; }
Vector<String> const& types() const { return m_types; }
WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> 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<String> m_types;
Vector<Representation> m_representations;
};
}

View file

@ -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<DOMString, ClipboardItemData> items,
optional ClipboardItemOptions options = {});
readonly attribute PresentationStyle presentationStyle;
// FIXME: Should be a FrozenArray<DOMString>
readonly attribute sequence<DOMString> types;
Promise<Blob> getType(DOMString type);
static boolean supports(DOMString type);
};
enum PresentationStyle { "unspecified", "inline", "attachment" };
dictionary ClipboardItemOptions {
PresentationStyle presentationStyle = "unspecified";
};