Everywhere: Hoist the Libraries folder to the top-level

This commit is contained in:
Timothy Flynn 2024-11-09 12:25:08 -05:00 committed by Andreas Kling
commit 93712b24bf
Notes: github-actions[bot] 2024-11-10 11:51:52 +00:00
4547 changed files with 104 additions and 113 deletions

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/HTML/AbstractWorker.h>
#include <LibWeb/HTML/EventNames.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/workers.html#handler-abstractworker-onerror
WebIDL::CallbackType* AbstractWorker::onerror()
{
return this_event_target().event_handler_attribute(HTML::EventNames::error);
}
// https://html.spec.whatwg.org/multipage/workers.html#handler-abstractworker-onerror
void AbstractWorker::set_onerror(WebIDL::CallbackType* event_handler)
{
this_event_target().set_event_handler_attribute(HTML::EventNames::error, event_handler);
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/DOM/EventTarget.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/workers.html#abstractworker
class AbstractWorker {
public:
virtual ~AbstractWorker() = default;
WebIDL::CallbackType* onerror();
void set_onerror(WebIDL::CallbackType*);
protected:
virtual DOM::EventTarget& this_event_target() = 0;
};
}

View file

@ -0,0 +1,4 @@
// https://html.spec.whatwg.org/multipage/workers.html#abstractworker
interface mixin AbstractWorker {
attribute EventHandler onerror;
};

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
namespace Web::HTML {
enum class ActivateTab {
Yes,
No,
};
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (c) 2023, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Bitmap.h>
#include <LibJS/Heap/Heap.h>
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/HTML/AnimatedBitmapDecodedImageData.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(AnimatedBitmapDecodedImageData);
ErrorOr<JS::NonnullGCPtr<AnimatedBitmapDecodedImageData>> AnimatedBitmapDecodedImageData::create(JS::Realm& realm, Vector<Frame>&& frames, size_t loop_count, bool animated)
{
return realm.heap().allocate<AnimatedBitmapDecodedImageData>(realm, move(frames), loop_count, animated);
}
AnimatedBitmapDecodedImageData::AnimatedBitmapDecodedImageData(Vector<Frame>&& frames, size_t loop_count, bool animated)
: m_frames(move(frames))
, m_loop_count(loop_count)
, m_animated(animated)
{
}
AnimatedBitmapDecodedImageData::~AnimatedBitmapDecodedImageData() = default;
RefPtr<Gfx::ImmutableBitmap> AnimatedBitmapDecodedImageData::bitmap(size_t frame_index, Gfx::IntSize) const
{
if (frame_index >= m_frames.size())
return nullptr;
return m_frames[frame_index].bitmap;
}
int AnimatedBitmapDecodedImageData::frame_duration(size_t frame_index) const
{
if (frame_index >= m_frames.size())
return 0;
return m_frames[frame_index].duration;
}
Optional<CSSPixels> AnimatedBitmapDecodedImageData::intrinsic_width() const
{
return m_frames.first().bitmap->width();
}
Optional<CSSPixels> AnimatedBitmapDecodedImageData::intrinsic_height() const
{
return m_frames.first().bitmap->height();
}
Optional<CSSPixelFraction> AnimatedBitmapDecodedImageData::intrinsic_aspect_ratio() const
{
return CSSPixels(m_frames.first().bitmap->width()) / CSSPixels(m_frames.first().bitmap->height());
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2023, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGfx/ImmutableBitmap.h>
#include <LibWeb/HTML/DecodedImageData.h>
namespace Web::HTML {
class AnimatedBitmapDecodedImageData final : public DecodedImageData {
JS_CELL(AnimatedBitmapDecodedImageData, DecodedImageData);
JS_DECLARE_ALLOCATOR(AnimatedBitmapDecodedImageData);
public:
struct Frame {
RefPtr<Gfx::ImmutableBitmap> bitmap;
int duration { 0 };
};
static ErrorOr<JS::NonnullGCPtr<AnimatedBitmapDecodedImageData>> create(JS::Realm&, Vector<Frame>&&, size_t loop_count, bool animated);
virtual ~AnimatedBitmapDecodedImageData() override;
virtual RefPtr<Gfx::ImmutableBitmap> bitmap(size_t frame_index, Gfx::IntSize = {}) const override;
virtual int frame_duration(size_t frame_index) const override;
virtual size_t frame_count() const override { return m_frames.size(); }
virtual size_t loop_count() const override { return m_loop_count; }
virtual bool is_animated() const override { return m_animated; }
virtual Optional<CSSPixels> intrinsic_width() const override;
virtual Optional<CSSPixels> intrinsic_height() const override;
virtual Optional<CSSPixelFraction> intrinsic_aspect_ratio() const override;
private:
AnimatedBitmapDecodedImageData(Vector<Frame>&&, size_t loop_count, bool animated);
Vector<Frame> m_frames;
size_t m_loop_count { 0 };
bool m_animated { false };
};
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2022, the SerenityOS developers.
* Copyright (c) 2024, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ScopeGuard.h>
#include <LibWeb/HTML/AnimationFrameCallbackDriver.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(AnimationFrameCallbackDriver);
void AnimationFrameCallbackDriver::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_callbacks);
visitor.visit(m_executing_callbacks);
}
WebIDL::UnsignedLong AnimationFrameCallbackDriver::add(Callback handler)
{
auto id = ++m_animation_frame_callback_identifier;
m_callbacks.set(id, handler);
return id;
}
bool AnimationFrameCallbackDriver::remove(WebIDL::UnsignedLong id)
{
return m_callbacks.remove(id);
}
bool AnimationFrameCallbackDriver::has_callbacks() const
{
return !m_callbacks.is_empty();
}
void AnimationFrameCallbackDriver::run(double now)
{
AK::ScopeGuard guard { [&]() { m_executing_callbacks.clear(); } };
m_executing_callbacks = move(m_callbacks);
for (auto& [id, callback] : m_executing_callbacks)
callback->function()(now);
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2022, the SerenityOS developers.
* Copyright (c) 2024, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <LibJS/Heap/GCPtr.h>
#include <LibJS/Heap/HeapFunction.h>
#include <LibWeb/WebIDL/Types.h>
namespace Web::HTML {
class AnimationFrameCallbackDriver final : public JS::Cell {
JS_CELL(AnimationFrameCallbackDriver, JS::Cell);
JS_DECLARE_ALLOCATOR(AnimationFrameCallbackDriver);
using Callback = JS::NonnullGCPtr<JS::HeapFunction<void(double)>>;
public:
[[nodiscard]] WebIDL::UnsignedLong add(Callback handler);
bool remove(WebIDL::UnsignedLong);
bool has_callbacks() const;
void run(double now);
private:
virtual void visit_edges(Cell::Visitor&) override;
// https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#animation-frame-callback-identifier
WebIDL::UnsignedLong m_animation_frame_callback_identifier { 0 };
OrderedHashMap<WebIDL::UnsignedLong, Callback> m_callbacks;
OrderedHashMap<WebIDL::UnsignedLong, Callback> m_executing_callbacks;
};
}

View file

@ -0,0 +1,9 @@
#import <HighResolutionTime/DOMHighResTimeStamp.idl>
callback FrameRequestCallback = undefined (DOMHighResTimeStamp time);
// https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#animationframeprovider
interface mixin AnimationFrameProvider {
unsigned long requestAnimationFrame(FrameRequestCallback callback);
undefined cancelAnimationFrame(unsigned long handle);
};

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2020, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/HTML/AttributeNames.h>
namespace Web::HTML {
namespace AttributeNames {
#define __ENUMERATE_HTML_ATTRIBUTE(name) FlyString name;
ENUMERATE_HTML_ATTRIBUTES
#undef __ENUMERATE_HTML_ATTRIBUTE
void initialize_strings()
{
static bool s_initialized = false;
VERIFY(!s_initialized);
#define __ENUMERATE_HTML_ATTRIBUTE(name) \
name = #name##_fly_string;
ENUMERATE_HTML_ATTRIBUTES
#undef __ENUMERATE_HTML_ATTRIBUTE
// NOTE: Special cases for C++ keywords.
class_ = "class"_fly_string;
for_ = "for"_fly_string;
default_ = "default"_fly_string;
char_ = "char"_fly_string;
// NOTE: Special cases for attributes with dashes in them.
accept_charset = "accept-charset"_fly_string;
http_equiv = "http-equiv"_fly_string;
s_initialized = true;
}
}
// https://html.spec.whatwg.org/#boolean-attribute
bool is_boolean_attribute(FlyString const& attribute)
{
// NOTE: For web compatibility, this matches the list of attributes which Chromium considers to be booleans,
// excluding attributes that are only used by Chromium itself:
// https://source.chromium.org/chromium/chromium/src/+/460b7c003cf89fc9493e721701906f19e5f6a387:chrome/test/chromedriver/element_commands.cc;l=48-94
return attribute.equals_ignoring_ascii_case(AttributeNames::allowfullscreen)
|| attribute.equals_ignoring_ascii_case(AttributeNames::async)
|| attribute.equals_ignoring_ascii_case(AttributeNames::autofocus)
|| attribute.equals_ignoring_ascii_case(AttributeNames::autoplay)
|| attribute.equals_ignoring_ascii_case(AttributeNames::checked)
|| attribute.equals_ignoring_ascii_case(AttributeNames::compact)
|| attribute.equals_ignoring_ascii_case(AttributeNames::controls)
|| attribute.equals_ignoring_ascii_case(AttributeNames::declare)
|| attribute.equals_ignoring_ascii_case(AttributeNames::default_)
|| attribute.equals_ignoring_ascii_case(AttributeNames::defaultchecked)
|| attribute.equals_ignoring_ascii_case(AttributeNames::defaultselected)
|| attribute.equals_ignoring_ascii_case(AttributeNames::defer)
|| attribute.equals_ignoring_ascii_case(AttributeNames::disabled)
|| attribute.equals_ignoring_ascii_case(AttributeNames::ended)
|| attribute.equals_ignoring_ascii_case(AttributeNames::formnovalidate)
|| attribute.equals_ignoring_ascii_case(AttributeNames::hidden)
|| attribute.equals_ignoring_ascii_case(AttributeNames::indeterminate)
|| attribute.equals_ignoring_ascii_case(AttributeNames::iscontenteditable)
|| attribute.equals_ignoring_ascii_case(AttributeNames::ismap)
|| attribute.equals_ignoring_ascii_case(AttributeNames::itemscope)
|| attribute.equals_ignoring_ascii_case(AttributeNames::loop)
|| attribute.equals_ignoring_ascii_case(AttributeNames::multiple)
|| attribute.equals_ignoring_ascii_case(AttributeNames::muted)
|| attribute.equals_ignoring_ascii_case(AttributeNames::nohref)
|| attribute.equals_ignoring_ascii_case(AttributeNames::nomodule)
|| attribute.equals_ignoring_ascii_case(AttributeNames::noresize)
|| attribute.equals_ignoring_ascii_case(AttributeNames::noshade)
|| attribute.equals_ignoring_ascii_case(AttributeNames::novalidate)
|| attribute.equals_ignoring_ascii_case(AttributeNames::nowrap)
|| attribute.equals_ignoring_ascii_case(AttributeNames::open)
|| attribute.equals_ignoring_ascii_case(AttributeNames::paused)
|| attribute.equals_ignoring_ascii_case(AttributeNames::playsinline)
|| attribute.equals_ignoring_ascii_case(AttributeNames::readonly)
|| attribute.equals_ignoring_ascii_case(AttributeNames::required)
|| attribute.equals_ignoring_ascii_case(AttributeNames::reversed)
|| attribute.equals_ignoring_ascii_case(AttributeNames::seeking)
|| attribute.equals_ignoring_ascii_case(AttributeNames::selected)
|| attribute.equals_ignoring_ascii_case(AttributeNames::truespeed)
|| attribute.equals_ignoring_ascii_case(AttributeNames::willvalidate);
}
}

View file

@ -0,0 +1,288 @@
/*
* Copyright (c) 2020-2021, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/FlyString.h>
namespace Web::HTML {
namespace AttributeNames {
#define ENUMERATE_HTML_ATTRIBUTES \
__ENUMERATE_HTML_ATTRIBUTE(abbr) \
__ENUMERATE_HTML_ATTRIBUTE(accept) \
__ENUMERATE_HTML_ATTRIBUTE(accept_charset) \
__ENUMERATE_HTML_ATTRIBUTE(accesskey) \
__ENUMERATE_HTML_ATTRIBUTE(action) \
__ENUMERATE_HTML_ATTRIBUTE(align) \
__ENUMERATE_HTML_ATTRIBUTE(alink) \
__ENUMERATE_HTML_ATTRIBUTE(allow) \
__ENUMERATE_HTML_ATTRIBUTE(allowfullscreen) \
__ENUMERATE_HTML_ATTRIBUTE(alt) \
__ENUMERATE_HTML_ATTRIBUTE(archive) \
__ENUMERATE_HTML_ATTRIBUTE(async) \
__ENUMERATE_HTML_ATTRIBUTE(as) \
__ENUMERATE_HTML_ATTRIBUTE(autocomplete) \
__ENUMERATE_HTML_ATTRIBUTE(autofocus) \
__ENUMERATE_HTML_ATTRIBUTE(autoplay) \
__ENUMERATE_HTML_ATTRIBUTE(axis) \
__ENUMERATE_HTML_ATTRIBUTE(background) \
__ENUMERATE_HTML_ATTRIBUTE(behavior) \
__ENUMERATE_HTML_ATTRIBUTE(bgcolor) \
__ENUMERATE_HTML_ATTRIBUTE(border) \
__ENUMERATE_HTML_ATTRIBUTE(cellpadding) \
__ENUMERATE_HTML_ATTRIBUTE(cellspacing) \
__ENUMERATE_HTML_ATTRIBUTE(char_) \
__ENUMERATE_HTML_ATTRIBUTE(charoff) \
__ENUMERATE_HTML_ATTRIBUTE(charset) \
__ENUMERATE_HTML_ATTRIBUTE(checked) \
__ENUMERATE_HTML_ATTRIBUTE(cite) \
__ENUMERATE_HTML_ATTRIBUTE(class_) \
__ENUMERATE_HTML_ATTRIBUTE(classid) \
__ENUMERATE_HTML_ATTRIBUTE(clear) \
__ENUMERATE_HTML_ATTRIBUTE(code) \
__ENUMERATE_HTML_ATTRIBUTE(codebase) \
__ENUMERATE_HTML_ATTRIBUTE(codetype) \
__ENUMERATE_HTML_ATTRIBUTE(color) \
__ENUMERATE_HTML_ATTRIBUTE(cols) \
__ENUMERATE_HTML_ATTRIBUTE(colspan) \
__ENUMERATE_HTML_ATTRIBUTE(compact) \
__ENUMERATE_HTML_ATTRIBUTE(content) \
__ENUMERATE_HTML_ATTRIBUTE(contenteditable) \
__ENUMERATE_HTML_ATTRIBUTE(controls) \
__ENUMERATE_HTML_ATTRIBUTE(coords) \
__ENUMERATE_HTML_ATTRIBUTE(crossorigin) \
__ENUMERATE_HTML_ATTRIBUTE(data) \
__ENUMERATE_HTML_ATTRIBUTE(datetime) \
__ENUMERATE_HTML_ATTRIBUTE(declare) \
__ENUMERATE_HTML_ATTRIBUTE(default_) \
__ENUMERATE_HTML_ATTRIBUTE(defaultchecked) \
__ENUMERATE_HTML_ATTRIBUTE(defaultselected) \
__ENUMERATE_HTML_ATTRIBUTE(defer) \
__ENUMERATE_HTML_ATTRIBUTE(dir) \
__ENUMERATE_HTML_ATTRIBUTE(direction) \
__ENUMERATE_HTML_ATTRIBUTE(dirname) \
__ENUMERATE_HTML_ATTRIBUTE(disabled) \
__ENUMERATE_HTML_ATTRIBUTE(download) \
__ENUMERATE_HTML_ATTRIBUTE(enctype) \
__ENUMERATE_HTML_ATTRIBUTE(ended) \
__ENUMERATE_HTML_ATTRIBUTE(event) \
__ENUMERATE_HTML_ATTRIBUTE(face) \
__ENUMERATE_HTML_ATTRIBUTE(fetchpriority) \
__ENUMERATE_HTML_ATTRIBUTE(for_) \
__ENUMERATE_HTML_ATTRIBUTE(form) \
__ENUMERATE_HTML_ATTRIBUTE(formaction) \
__ENUMERATE_HTML_ATTRIBUTE(formenctype) \
__ENUMERATE_HTML_ATTRIBUTE(formmethod) \
__ENUMERATE_HTML_ATTRIBUTE(formnovalidate) \
__ENUMERATE_HTML_ATTRIBUTE(formtarget) \
__ENUMERATE_HTML_ATTRIBUTE(frame) \
__ENUMERATE_HTML_ATTRIBUTE(frameborder) \
__ENUMERATE_HTML_ATTRIBUTE(headers) \
__ENUMERATE_HTML_ATTRIBUTE(height) \
__ENUMERATE_HTML_ATTRIBUTE(hidden) \
__ENUMERATE_HTML_ATTRIBUTE(high) \
__ENUMERATE_HTML_ATTRIBUTE(href) \
__ENUMERATE_HTML_ATTRIBUTE(hreflang) \
__ENUMERATE_HTML_ATTRIBUTE(hspace) \
__ENUMERATE_HTML_ATTRIBUTE(http_equiv) \
__ENUMERATE_HTML_ATTRIBUTE(id) \
__ENUMERATE_HTML_ATTRIBUTE(imagesizes) \
__ENUMERATE_HTML_ATTRIBUTE(imagesrcset) \
__ENUMERATE_HTML_ATTRIBUTE(indeterminate) \
__ENUMERATE_HTML_ATTRIBUTE(inert) \
__ENUMERATE_HTML_ATTRIBUTE(integrity) \
__ENUMERATE_HTML_ATTRIBUTE(is) \
__ENUMERATE_HTML_ATTRIBUTE(iscontenteditable) \
__ENUMERATE_HTML_ATTRIBUTE(ismap) \
__ENUMERATE_HTML_ATTRIBUTE(itemscope) \
__ENUMERATE_HTML_ATTRIBUTE(kind) \
__ENUMERATE_HTML_ATTRIBUTE(label) \
__ENUMERATE_HTML_ATTRIBUTE(lang) \
__ENUMERATE_HTML_ATTRIBUTE(language) \
__ENUMERATE_HTML_ATTRIBUTE(link) \
__ENUMERATE_HTML_ATTRIBUTE(list) \
__ENUMERATE_HTML_ATTRIBUTE(loading) \
__ENUMERATE_HTML_ATTRIBUTE(longdesc) \
__ENUMERATE_HTML_ATTRIBUTE(loop) \
__ENUMERATE_HTML_ATTRIBUTE(low) \
__ENUMERATE_HTML_ATTRIBUTE(lowsrc) \
__ENUMERATE_HTML_ATTRIBUTE(marginheight) \
__ENUMERATE_HTML_ATTRIBUTE(marginwidth) \
__ENUMERATE_HTML_ATTRIBUTE(max) \
__ENUMERATE_HTML_ATTRIBUTE(maxlength) \
__ENUMERATE_HTML_ATTRIBUTE(media) \
__ENUMERATE_HTML_ATTRIBUTE(method) \
__ENUMERATE_HTML_ATTRIBUTE(min) \
__ENUMERATE_HTML_ATTRIBUTE(minlength) \
__ENUMERATE_HTML_ATTRIBUTE(multiple) \
__ENUMERATE_HTML_ATTRIBUTE(muted) \
__ENUMERATE_HTML_ATTRIBUTE(name) \
__ENUMERATE_HTML_ATTRIBUTE(nohref) \
__ENUMERATE_HTML_ATTRIBUTE(nomodule) \
__ENUMERATE_HTML_ATTRIBUTE(nonce) \
__ENUMERATE_HTML_ATTRIBUTE(noresize) \
__ENUMERATE_HTML_ATTRIBUTE(noshade) \
__ENUMERATE_HTML_ATTRIBUTE(novalidate) \
__ENUMERATE_HTML_ATTRIBUTE(nowrap) \
__ENUMERATE_HTML_ATTRIBUTE(onabort) \
__ENUMERATE_HTML_ATTRIBUTE(onafterprint) \
__ENUMERATE_HTML_ATTRIBUTE(onauxclick) \
__ENUMERATE_HTML_ATTRIBUTE(onbeforeprint) \
__ENUMERATE_HTML_ATTRIBUTE(onbeforeunload) \
__ENUMERATE_HTML_ATTRIBUTE(onblur) \
__ENUMERATE_HTML_ATTRIBUTE(oncancel) \
__ENUMERATE_HTML_ATTRIBUTE(oncanplay) \
__ENUMERATE_HTML_ATTRIBUTE(oncanplaythrough) \
__ENUMERATE_HTML_ATTRIBUTE(onchange) \
__ENUMERATE_HTML_ATTRIBUTE(onclick) \
__ENUMERATE_HTML_ATTRIBUTE(onclose) \
__ENUMERATE_HTML_ATTRIBUTE(oncontextmenu) \
__ENUMERATE_HTML_ATTRIBUTE(oncuechange) \
__ENUMERATE_HTML_ATTRIBUTE(ondblclick) \
__ENUMERATE_HTML_ATTRIBUTE(ondrag) \
__ENUMERATE_HTML_ATTRIBUTE(ondragend) \
__ENUMERATE_HTML_ATTRIBUTE(ondragenter) \
__ENUMERATE_HTML_ATTRIBUTE(ondragleave) \
__ENUMERATE_HTML_ATTRIBUTE(ondragover) \
__ENUMERATE_HTML_ATTRIBUTE(ondragstart) \
__ENUMERATE_HTML_ATTRIBUTE(ondrop) \
__ENUMERATE_HTML_ATTRIBUTE(ondurationchange) \
__ENUMERATE_HTML_ATTRIBUTE(onemptied) \
__ENUMERATE_HTML_ATTRIBUTE(onended) \
__ENUMERATE_HTML_ATTRIBUTE(onerror) \
__ENUMERATE_HTML_ATTRIBUTE(onfocus) \
__ENUMERATE_HTML_ATTRIBUTE(onfocusin) \
__ENUMERATE_HTML_ATTRIBUTE(onfocusout) \
__ENUMERATE_HTML_ATTRIBUTE(onformdata) \
__ENUMERATE_HTML_ATTRIBUTE(onhashchange) \
__ENUMERATE_HTML_ATTRIBUTE(oninput) \
__ENUMERATE_HTML_ATTRIBUTE(oninvalid) \
__ENUMERATE_HTML_ATTRIBUTE(onkeydown) \
__ENUMERATE_HTML_ATTRIBUTE(onkeypress) \
__ENUMERATE_HTML_ATTRIBUTE(onkeyup) \
__ENUMERATE_HTML_ATTRIBUTE(onlanguagechange) \
__ENUMERATE_HTML_ATTRIBUTE(onload) \
__ENUMERATE_HTML_ATTRIBUTE(onloadeddata) \
__ENUMERATE_HTML_ATTRIBUTE(onloadedmetadata) \
__ENUMERATE_HTML_ATTRIBUTE(onloadstart) \
__ENUMERATE_HTML_ATTRIBUTE(onmessage) \
__ENUMERATE_HTML_ATTRIBUTE(onmessageerror) \
__ENUMERATE_HTML_ATTRIBUTE(onmousedown) \
__ENUMERATE_HTML_ATTRIBUTE(onmouseenter) \
__ENUMERATE_HTML_ATTRIBUTE(onmouseleave) \
__ENUMERATE_HTML_ATTRIBUTE(onmousemove) \
__ENUMERATE_HTML_ATTRIBUTE(onmouseout) \
__ENUMERATE_HTML_ATTRIBUTE(onmouseover) \
__ENUMERATE_HTML_ATTRIBUTE(onmouseup) \
__ENUMERATE_HTML_ATTRIBUTE(onoffline) \
__ENUMERATE_HTML_ATTRIBUTE(ononline) \
__ENUMERATE_HTML_ATTRIBUTE(onpagehide) \
__ENUMERATE_HTML_ATTRIBUTE(onpageshow) \
__ENUMERATE_HTML_ATTRIBUTE(onpause) \
__ENUMERATE_HTML_ATTRIBUTE(onplay) \
__ENUMERATE_HTML_ATTRIBUTE(onplaying) \
__ENUMERATE_HTML_ATTRIBUTE(onpopstate) \
__ENUMERATE_HTML_ATTRIBUTE(onprogress) \
__ENUMERATE_HTML_ATTRIBUTE(onratechange) \
__ENUMERATE_HTML_ATTRIBUTE(onrejectionhandled) \
__ENUMERATE_HTML_ATTRIBUTE(onreset) \
__ENUMERATE_HTML_ATTRIBUTE(onresize) \
__ENUMERATE_HTML_ATTRIBUTE(onscroll) \
__ENUMERATE_HTML_ATTRIBUTE(onsecuritypolicyviolation) \
__ENUMERATE_HTML_ATTRIBUTE(onseeked) \
__ENUMERATE_HTML_ATTRIBUTE(onseeking) \
__ENUMERATE_HTML_ATTRIBUTE(onselect) \
__ENUMERATE_HTML_ATTRIBUTE(onselectionchange) \
__ENUMERATE_HTML_ATTRIBUTE(onslotchange) \
__ENUMERATE_HTML_ATTRIBUTE(onstalled) \
__ENUMERATE_HTML_ATTRIBUTE(onstorage) \
__ENUMERATE_HTML_ATTRIBUTE(onsubmit) \
__ENUMERATE_HTML_ATTRIBUTE(onsuspend) \
__ENUMERATE_HTML_ATTRIBUTE(ontimeupdate) \
__ENUMERATE_HTML_ATTRIBUTE(ontoggle) \
__ENUMERATE_HTML_ATTRIBUTE(onunhandledrejection) \
__ENUMERATE_HTML_ATTRIBUTE(onunload) \
__ENUMERATE_HTML_ATTRIBUTE(onvolumechange) \
__ENUMERATE_HTML_ATTRIBUTE(onwaiting) \
__ENUMERATE_HTML_ATTRIBUTE(onwebkitanimationend) \
__ENUMERATE_HTML_ATTRIBUTE(onwebkitanimationiteration) \
__ENUMERATE_HTML_ATTRIBUTE(onwebkitanimationstart) \
__ENUMERATE_HTML_ATTRIBUTE(onwebkittransitionend) \
__ENUMERATE_HTML_ATTRIBUTE(onwheel) \
__ENUMERATE_HTML_ATTRIBUTE(open) \
__ENUMERATE_HTML_ATTRIBUTE(optimum) \
__ENUMERATE_HTML_ATTRIBUTE(pattern) \
__ENUMERATE_HTML_ATTRIBUTE(paused) \
__ENUMERATE_HTML_ATTRIBUTE(ping) \
__ENUMERATE_HTML_ATTRIBUTE(placeholder) \
__ENUMERATE_HTML_ATTRIBUTE(playsinline) \
__ENUMERATE_HTML_ATTRIBUTE(popover) \
__ENUMERATE_HTML_ATTRIBUTE(poster) \
__ENUMERATE_HTML_ATTRIBUTE(preload) \
__ENUMERATE_HTML_ATTRIBUTE(readonly) \
__ENUMERATE_HTML_ATTRIBUTE(referrerpolicy) \
__ENUMERATE_HTML_ATTRIBUTE(rel) \
__ENUMERATE_HTML_ATTRIBUTE(required) \
__ENUMERATE_HTML_ATTRIBUTE(rev) \
__ENUMERATE_HTML_ATTRIBUTE(reversed) \
__ENUMERATE_HTML_ATTRIBUTE(rows) \
__ENUMERATE_HTML_ATTRIBUTE(rowspan) \
__ENUMERATE_HTML_ATTRIBUTE(rules) \
__ENUMERATE_HTML_ATTRIBUTE(scheme) \
__ENUMERATE_HTML_ATTRIBUTE(scope) \
__ENUMERATE_HTML_ATTRIBUTE(scrollamount) \
__ENUMERATE_HTML_ATTRIBUTE(scrolldelay) \
__ENUMERATE_HTML_ATTRIBUTE(scrolling) \
__ENUMERATE_HTML_ATTRIBUTE(seeking) \
__ENUMERATE_HTML_ATTRIBUTE(selected) \
__ENUMERATE_HTML_ATTRIBUTE(shadowrootclonable) \
__ENUMERATE_HTML_ATTRIBUTE(shadowrootdelegatesfocus) \
__ENUMERATE_HTML_ATTRIBUTE(shadowrootmode) \
__ENUMERATE_HTML_ATTRIBUTE(shadowrootserializable) \
__ENUMERATE_HTML_ATTRIBUTE(shape) \
__ENUMERATE_HTML_ATTRIBUTE(size) \
__ENUMERATE_HTML_ATTRIBUTE(sizes) \
__ENUMERATE_HTML_ATTRIBUTE(slot) \
__ENUMERATE_HTML_ATTRIBUTE(span) \
__ENUMERATE_HTML_ATTRIBUTE(src) \
__ENUMERATE_HTML_ATTRIBUTE(srcdoc) \
__ENUMERATE_HTML_ATTRIBUTE(srclang) \
__ENUMERATE_HTML_ATTRIBUTE(srcset) \
__ENUMERATE_HTML_ATTRIBUTE(standby) \
__ENUMERATE_HTML_ATTRIBUTE(start) \
__ENUMERATE_HTML_ATTRIBUTE(step) \
__ENUMERATE_HTML_ATTRIBUTE(style) \
__ENUMERATE_HTML_ATTRIBUTE(summary) \
__ENUMERATE_HTML_ATTRIBUTE(tabindex) \
__ENUMERATE_HTML_ATTRIBUTE(target) \
__ENUMERATE_HTML_ATTRIBUTE(text) \
__ENUMERATE_HTML_ATTRIBUTE(title) \
__ENUMERATE_HTML_ATTRIBUTE(truespeed) \
__ENUMERATE_HTML_ATTRIBUTE(type) \
__ENUMERATE_HTML_ATTRIBUTE(usemap) \
__ENUMERATE_HTML_ATTRIBUTE(valign) \
__ENUMERATE_HTML_ATTRIBUTE(value) \
__ENUMERATE_HTML_ATTRIBUTE(valuetype) \
__ENUMERATE_HTML_ATTRIBUTE(version) \
__ENUMERATE_HTML_ATTRIBUTE(vlink) \
__ENUMERATE_HTML_ATTRIBUTE(vspace) \
__ENUMERATE_HTML_ATTRIBUTE(width) \
__ENUMERATE_HTML_ATTRIBUTE(willvalidate) \
__ENUMERATE_HTML_ATTRIBUTE(wrap)
#define __ENUMERATE_HTML_ATTRIBUTE(name) extern FlyString name;
ENUMERATE_HTML_ATTRIBUTES
#undef __ENUMERATE_HTML_ATTRIBUTE
void initialize_strings();
}
bool is_boolean_attribute(FlyString const& attribute);
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Assertions.h>
namespace Web::HTML {
enum class AudioPlayState {
Paused,
Playing,
};
enum class MuteState {
Muted,
Unmuted,
};
constexpr MuteState invert_mute_state(MuteState mute_state)
{
switch (mute_state) {
case MuteState::Muted:
return MuteState::Unmuted;
case MuteState::Unmuted:
return MuteState::Muted;
}
VERIFY_NOT_REACHED();
}
}

View file

@ -0,0 +1,119 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/IDAllocator.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/VM.h>
#include <LibMedia/Audio/Loader.h>
#include <LibWeb/Bindings/AudioTrackPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/HTML/AudioTrack.h>
#include <LibWeb/HTML/AudioTrackList.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Platform/AudioCodecPlugin.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(AudioTrack);
static IDAllocator s_audio_track_id_allocator;
AudioTrack::AudioTrack(JS::Realm& realm, JS::NonnullGCPtr<HTMLMediaElement> media_element, NonnullRefPtr<Audio::Loader> loader)
: PlatformObject(realm)
, m_media_element(media_element)
, m_audio_plugin(Platform::AudioCodecPlugin::create(move(loader)).release_value_but_fixme_should_propagate_errors())
{
m_audio_plugin->on_playback_position_updated = [this](auto position) {
if (auto* paintable = m_media_element->paintable())
paintable->set_needs_display();
auto playback_position = static_cast<double>(position.to_milliseconds()) / 1000.0;
m_media_element->set_current_playback_position(playback_position);
};
m_audio_plugin->on_decoder_error = [this](String error_message) {
m_media_element->set_decoder_error(move(error_message));
};
}
AudioTrack::~AudioTrack()
{
auto id = m_id.to_number<int>();
VERIFY(id.has_value());
s_audio_track_id_allocator.deallocate(id.value());
}
void AudioTrack::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(AudioTrack);
auto id = s_audio_track_id_allocator.allocate();
m_id = String::number(id);
}
void AudioTrack::play(Badge<HTMLAudioElement>)
{
m_audio_plugin->resume_playback();
}
void AudioTrack::pause(Badge<HTMLAudioElement>)
{
m_audio_plugin->pause_playback();
}
AK::Duration AudioTrack::duration()
{
return m_audio_plugin->duration();
}
void AudioTrack::seek(double position, MediaSeekMode seek_mode)
{
// FIXME: Implement seeking mode.
(void)seek_mode;
m_audio_plugin->seek(position);
}
void AudioTrack::update_volume()
{
m_audio_plugin->set_volume(m_media_element->effective_media_volume());
}
void AudioTrack::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_media_element);
visitor.visit(m_audio_track_list);
}
// https://html.spec.whatwg.org/multipage/media.html#dom-audiotrack-enabled
void AudioTrack::set_enabled(bool enabled)
{
// On setting, it must enable the track if the new value is true, and disable it otherwise. (If the track is no
// longer in an AudioTrackList object, then the track being enabled or disabled has no effect beyond changing the
// value of the attribute on the AudioTrack object.)
if (m_enabled == enabled)
return;
if (m_audio_track_list) {
// Whenever an audio track in an AudioTrackList that was disabled is enabled, and whenever one that was enabled
// is disabled, the user agent must queue a media element task given the media element to fire an event named
// change at the AudioTrackList object.
m_media_element->queue_a_media_element_task([this]() {
m_audio_track_list->dispatch_event(DOM::Event::create(realm(), HTML::EventNames::change));
});
}
m_enabled = enabled;
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/Time.h>
#include <LibMedia/Audio/Forward.h>
#include <LibWeb/Bindings/PlatformObject.h>
namespace Web::HTML {
class AudioTrack final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(AudioTrack, Bindings::PlatformObject);
JS_DECLARE_ALLOCATOR(AudioTrack);
public:
virtual ~AudioTrack() override;
void set_audio_track_list(Badge<AudioTrackList>, JS::GCPtr<AudioTrackList> audio_track_list) { m_audio_track_list = audio_track_list; }
void play(Badge<HTMLAudioElement>);
void pause(Badge<HTMLAudioElement>);
AK::Duration duration();
void seek(double, MediaSeekMode);
void update_volume();
String const& id() const { return m_id; }
String const& kind() const { return m_kind; }
String const& label() const { return m_label; }
String const& language() const { return m_language; }
bool enabled() const { return m_enabled; }
void set_enabled(bool enabled);
private:
AudioTrack(JS::Realm&, JS::NonnullGCPtr<HTMLMediaElement>, NonnullRefPtr<Audio::Loader>);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
// https://html.spec.whatwg.org/multipage/media.html#dom-audiotrack-id
String m_id;
// https://html.spec.whatwg.org/multipage/media.html#dom-audiotrack-kind
String m_kind;
// https://html.spec.whatwg.org/multipage/media.html#dom-audiotrack-label
String m_label;
// https://html.spec.whatwg.org/multipage/media.html#dom-audiotrack-language
String m_language;
// https://html.spec.whatwg.org/multipage/media.html#dom-audiotrack-enabled
bool m_enabled { false };
JS::NonnullGCPtr<HTMLMediaElement> m_media_element;
JS::GCPtr<AudioTrackList> m_audio_track_list;
NonnullOwnPtr<Platform::AudioCodecPlugin> m_audio_plugin;
};
}

View file

@ -0,0 +1,9 @@
// https://html.spec.whatwg.org/multipage/media.html#audiotrack
[Exposed=Window]
interface AudioTrack {
readonly attribute DOMString id;
readonly attribute DOMString kind;
readonly attribute DOMString label;
readonly attribute DOMString language;
attribute boolean enabled;
};

View file

@ -0,0 +1,126 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/Bindings/AudioTrackListPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/AudioTrackList.h>
#include <LibWeb/HTML/EventNames.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(AudioTrackList);
AudioTrackList::AudioTrackList(JS::Realm& realm)
: DOM::EventTarget(realm, MayInterfereWithIndexedPropertyAccess::Yes)
{
}
void AudioTrackList::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(AudioTrackList);
}
// https://html.spec.whatwg.org/multipage/media.html#dom-tracklist-item
JS::ThrowCompletionOr<Optional<JS::PropertyDescriptor>> AudioTrackList::internal_get_own_property(JS::PropertyKey const& property_name) const
{
// To determine the value of an indexed property for a given index index in an AudioTrackList or VideoTrackList
// object list, the user agent must return the AudioTrack or VideoTrack object that represents the indexth track
// in list.
if (property_name.is_number()) {
if (auto index = property_name.as_number(); index < m_audio_tracks.size()) {
JS::PropertyDescriptor descriptor;
descriptor.value = m_audio_tracks.at(index);
return descriptor;
}
}
return Base::internal_get_own_property(property_name);
}
void AudioTrackList::add_track(Badge<HTMLMediaElement>, JS::NonnullGCPtr<AudioTrack> audio_track)
{
m_audio_tracks.append(audio_track);
audio_track->set_audio_track_list({}, this);
}
void AudioTrackList::remove_all_tracks(Badge<HTMLMediaElement>)
{
m_audio_tracks.clear();
}
// https://html.spec.whatwg.org/multipage/media.html#dom-audiotracklist-gettrackbyid
JS::GCPtr<AudioTrack> AudioTrackList::get_track_by_id(StringView id) const
{
// The AudioTrackList getTrackById(id) and VideoTrackList getTrackById(id) methods must return the first AudioTrack
// or VideoTrack object (respectively) in the AudioTrackList or VideoTrackList object (respectively) whose identifier
// is equal to the value of the id argument (in the natural order of the list, as defined above).
auto it = m_audio_tracks.find_if([&](auto const& audio_track) {
return audio_track->id() == id;
});
// When no tracks match the given argument, the methods must return null.
if (it == m_audio_tracks.end())
return nullptr;
return *it;
}
bool AudioTrackList::has_enabled_track() const
{
auto it = m_audio_tracks.find_if([&](auto const& audio_track) {
return audio_track->enabled();
});
return it != m_audio_tracks.end();
}
// https://html.spec.whatwg.org/multipage/media.html#handler-tracklist-onchange
void AudioTrackList::set_onchange(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::change, event_handler);
}
// https://html.spec.whatwg.org/multipage/media.html#handler-tracklist-onchange
WebIDL::CallbackType* AudioTrackList::onchange()
{
return event_handler_attribute(HTML::EventNames::change);
}
// https://html.spec.whatwg.org/multipage/media.html#handler-tracklist-onaddtrack
void AudioTrackList::set_onaddtrack(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::addtrack, event_handler);
}
// https://html.spec.whatwg.org/multipage/media.html#handler-tracklist-onaddtrack
WebIDL::CallbackType* AudioTrackList::onaddtrack()
{
return event_handler_attribute(HTML::EventNames::addtrack);
}
// https://html.spec.whatwg.org/multipage/media.html#handler-tracklist-onremovetrack
void AudioTrackList::set_onremovetrack(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::removetrack, event_handler);
}
// https://html.spec.whatwg.org/multipage/media.html#handler-tracklist-onremovetrack
WebIDL::CallbackType* AudioTrackList::onremovetrack()
{
return event_handler_attribute(HTML::EventNames::removetrack);
}
void AudioTrackList::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_audio_tracks);
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Badge.h>
#include <AK/String.h>
#include <LibJS/Heap/MarkedVector.h>
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/HTML/AudioTrack.h>
namespace Web::HTML {
class AudioTrackList final : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(AudioTrackList, DOM::EventTarget);
JS_DECLARE_ALLOCATOR(AudioTrackList);
public:
void add_track(Badge<HTMLMediaElement>, JS::NonnullGCPtr<AudioTrack>);
void remove_all_tracks(Badge<HTMLMediaElement>);
// https://html.spec.whatwg.org/multipage/media.html#dom-audiotracklist-length
size_t length() const { return m_audio_tracks.size(); }
JS::GCPtr<AudioTrack> get_track_by_id(StringView id) const;
bool has_enabled_track() const;
template<typename Callback>
void for_each_enabled_track(Callback&& callback)
{
for (auto& audio_track : m_audio_tracks) {
if (audio_track->enabled())
callback(*audio_track);
}
}
void set_onchange(WebIDL::CallbackType*);
WebIDL::CallbackType* onchange();
void set_onaddtrack(WebIDL::CallbackType*);
WebIDL::CallbackType* onaddtrack();
void set_onremovetrack(WebIDL::CallbackType*);
WebIDL::CallbackType* onremovetrack();
private:
explicit AudioTrackList(JS::Realm&);
virtual void visit_edges(Visitor&) override;
virtual void initialize(JS::Realm&) override;
virtual JS::ThrowCompletionOr<Optional<JS::PropertyDescriptor>> internal_get_own_property(JS::PropertyKey const& property_name) const override;
Vector<JS::NonnullGCPtr<AudioTrack>> m_audio_tracks;
};
}

View file

@ -0,0 +1,15 @@
#import <DOM/EventHandler.idl>
#import <DOM/EventTarget.idl>
#import <HTML/AudioTrack.idl>
// https://html.spec.whatwg.org/multipage/media.html#audiotracklist
[Exposed=Window]
interface AudioTrackList : EventTarget {
readonly attribute unsigned long length;
getter AudioTrack (unsigned long index);
AudioTrack? getTrackById(DOMString id);
attribute EventHandler onchange;
attribute EventHandler onaddtrack;
attribute EventHandler onremovetrack;
};

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2024, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/BeforeUnloadEventPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/BeforeUnloadEvent.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(BeforeUnloadEvent);
JS::NonnullGCPtr<BeforeUnloadEvent> BeforeUnloadEvent::create(JS::Realm& realm, FlyString const& event_name, DOM::EventInit const& event_init)
{
return realm.heap().allocate<BeforeUnloadEvent>(realm, realm, event_name, event_init);
}
BeforeUnloadEvent::BeforeUnloadEvent(JS::Realm& realm, FlyString const& event_name, DOM::EventInit const& event_init)
: DOM::Event(realm, event_name, event_init)
{
}
BeforeUnloadEvent::~BeforeUnloadEvent() = default;
void BeforeUnloadEvent::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(BeforeUnloadEvent);
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2024, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
#include <LibWeb/DOM/Event.h>
namespace Web::HTML {
class BeforeUnloadEvent final : public DOM::Event {
WEB_PLATFORM_OBJECT(BeforeUnloadEvent, DOM::Event);
JS_DECLARE_ALLOCATOR(BeforeUnloadEvent);
public:
[[nodiscard]] static JS::NonnullGCPtr<BeforeUnloadEvent> create(JS::Realm&, FlyString const& event_name, DOM::EventInit const& = {});
BeforeUnloadEvent(JS::Realm&, FlyString const& event_name, DOM::EventInit const&);
virtual ~BeforeUnloadEvent() override;
String const& return_value() const { return m_return_value; }
void set_return_value(String const& return_value) { m_return_value = return_value; }
private:
virtual void initialize(JS::Realm&) override;
String m_return_value;
};
}

View file

@ -0,0 +1,7 @@
#import <DOM/Event.idl>
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-beforeunloadevent-interface
[Exposed=Window]
interface BeforeUnloadEvent : Event {
attribute DOMString returnValue;
};

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/Bindings/BroadcastChannelPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/BroadcastChannel.h>
#include <LibWeb/HTML/EventNames.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(BroadcastChannel);
JS::NonnullGCPtr<BroadcastChannel> BroadcastChannel::construct_impl(JS::Realm& realm, FlyString const& name)
{
return realm.heap().allocate<BroadcastChannel>(realm, realm, name);
}
BroadcastChannel::BroadcastChannel(JS::Realm& realm, FlyString const& name)
: DOM::EventTarget(realm)
, m_channel_name(name)
{
}
void BroadcastChannel::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(BroadcastChannel);
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-broadcastchannel-name
FlyString BroadcastChannel::name()
{
// The name getter steps are to return this's channel name.
return m_channel_name;
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-broadcastchannel-close
void BroadcastChannel::close()
{
// The close() method steps are to set this's closed flag to true.
m_closed_flag = true;
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#handler-broadcastchannel-onmessage
void BroadcastChannel::set_onmessage(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::message, event_handler);
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#handler-broadcastchannel-onmessage
WebIDL::CallbackType* BroadcastChannel::onmessage()
{
return event_handler_attribute(HTML::EventNames::message);
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#handler-broadcastchannel-onmessageerror
void BroadcastChannel::set_onmessageerror(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::messageerror, event_handler);
}
// https://html.spec.whatwg.org/multipage/web-messaging.html#handler-broadcastchannel-onmessageerror
WebIDL::CallbackType* BroadcastChannel::onmessageerror()
{
return event_handler_attribute(HTML::EventNames::messageerror);
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2024, Jamie Mansfield <jmansfield@cadixdev.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/DOM/EventTarget.h>
namespace Web::HTML {
class BroadcastChannel final : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(BroadcastChannel, DOM::EventTarget);
JS_DECLARE_ALLOCATOR(BroadcastChannel);
public:
[[nodiscard]] static JS::NonnullGCPtr<BroadcastChannel> construct_impl(JS::Realm&, FlyString const& name);
FlyString name();
void close();
void set_onmessage(WebIDL::CallbackType*);
WebIDL::CallbackType* onmessage();
void set_onmessageerror(WebIDL::CallbackType*);
WebIDL::CallbackType* onmessageerror();
private:
BroadcastChannel(JS::Realm&, FlyString const& name);
virtual void initialize(JS::Realm&) override;
FlyString m_channel_name;
bool m_closed_flag { false };
};
}

View file

@ -0,0 +1,14 @@
#import <DOM/EventHandler.idl>
#import <DOM/EventTarget.idl>
// https://html.spec.whatwg.org/multipage/web-messaging.html#broadcastchannel
[Exposed=(Window,Worker)]
interface BroadcastChannel : EventTarget {
constructor(DOMString name);
readonly attribute DOMString name;
[FIXME] undefined postMessage(any message);
undefined close();
attribute EventHandler onmessage;
attribute EventHandler onmessageerror;
};

View file

@ -0,0 +1,519 @@
/*
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Bindings/PrincipalHostDefined.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/HTMLCollection.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/DOMURL/DOMURL.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/BrowsingContextGroup.h>
#include <LibWeb/HTML/CrossOrigin/OpenerPolicy.h>
#include <LibWeb/HTML/DocumentState.h>
#include <LibWeb/HTML/HTMLAnchorElement.h>
#include <LibWeb/HTML/HTMLDocument.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/NavigableContainer.h>
#include <LibWeb/HTML/SandboxingFlagSet.h>
#include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WindowProxy.h>
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
#include <LibWeb/Layout/BreakNode.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/Paintable.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(BrowsingContext);
// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#matches-about:blank
bool url_matches_about_blank(URL::URL const& url)
{
// A URL matches about:blank if its scheme is "about", its path contains a single string "blank", its username and password are the empty string, and its host is null.
return url.scheme() == "about"sv
&& url.paths().size() == 1 && url.paths()[0] == "blank"sv
&& url.username().is_empty()
&& url.password().is_empty()
&& url.host().has<Empty>();
}
// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#matches-about:srcdoc
bool url_matches_about_srcdoc(URL::URL const& url)
{
// A URL matches about:srcdoc if its scheme is "about", its path contains a single string "srcdoc", its query is null, its username and password are the empty string, and its host is null.
return url.scheme() == "about"sv
&& url.paths().size() == 1 && url.paths()[0] == "srcdoc"sv
&& !url.query().has_value()
&& url.username().is_empty()
&& url.password().is_empty()
&& url.host().has<Empty>();
}
// https://html.spec.whatwg.org/multipage/document-sequences.html#determining-the-origin
URL::Origin determine_the_origin(Optional<URL::URL> const& url, SandboxingFlagSet sandbox_flags, Optional<URL::Origin> source_origin)
{
// 1. If sandboxFlags has its sandboxed origin browsing context flag set, then return a new opaque origin.
if (has_flag(sandbox_flags, SandboxingFlagSet::SandboxedOrigin)) {
return URL::Origin {};
}
// 2. If url is null, then return a new opaque origin.
if (!url.has_value()) {
return URL::Origin {};
}
// 3. If url is about:srcdoc, then:
if (url == "about:srcdoc"sv) {
// 1. Assert: sourceOrigin is non-null.
VERIFY(source_origin.has_value());
// 2. Return sourceOrigin.
return source_origin.release_value();
}
// 4. If url matches about:blank and sourceOrigin is non-null, then return sourceOrigin.
if (url_matches_about_blank(*url) && source_origin.has_value())
return source_origin.release_value();
// 5. Return url's origin.
return url->origin();
}
// https://html.spec.whatwg.org/multipage/document-sequences.html#creating-a-new-auxiliary-browsing-context
WebIDL::ExceptionOr<BrowsingContext::BrowsingContextAndDocument> BrowsingContext::create_a_new_auxiliary_browsing_context_and_document(JS::NonnullGCPtr<Page> page, JS::NonnullGCPtr<HTML::BrowsingContext> opener)
{
// 1. Let openerTopLevelBrowsingContext be opener's top-level traversable's active browsing context.
auto opener_top_level_browsing_context = opener->top_level_traversable()->active_browsing_context();
// 2. Let group be openerTopLevelBrowsingContext's group.
auto group = opener_top_level_browsing_context->group();
// 3. Assert: group is non-null, as navigating invokes this directly.
VERIFY(group);
// 4. Set browsingContext and document be the result of creating a new browsing context and document with opener's active document, null, and group.
auto [browsing_context, document] = TRY(create_a_new_browsing_context_and_document(page, opener->active_document(), nullptr, *group));
// 5. Set browsingContext's is auxiliary to true.
browsing_context->m_is_auxiliary = true;
// 6. Append browsingContext to group.
group->append(browsing_context);
// 7. Set browsingContext's opener browsing context to opener.
browsing_context->set_opener_browsing_context(opener);
// 8. Set browsingContext's virtual browsing context group ID to openerTopLevelBrowsingContext's virtual browsing context group ID.
browsing_context->m_virtual_browsing_context_group_id = opener_top_level_browsing_context->m_virtual_browsing_context_group_id;
// 9. Set browsingContext's opener origin at creation to opener's active document's origin.
browsing_context->m_opener_origin_at_creation = opener->active_document()->origin();
// 10. Return browsingContext and document.
return BrowsingContext::BrowsingContextAndDocument { browsing_context, document };
}
static void populate_with_html_head_body(JS::NonnullGCPtr<DOM::Document> document)
{
auto html_node = MUST(DOM::create_element(document, HTML::TagNames::html, Namespace::HTML));
auto head_element = MUST(DOM::create_element(document, HTML::TagNames::head, Namespace::HTML));
MUST(html_node->append_child(head_element));
auto body_element = MUST(DOM::create_element(document, HTML::TagNames::body, Namespace::HTML));
MUST(html_node->append_child(body_element));
MUST(document->append_child(html_node));
}
// https://html.spec.whatwg.org/multipage/document-sequences.html#creating-a-new-browsing-context
WebIDL::ExceptionOr<BrowsingContext::BrowsingContextAndDocument> BrowsingContext::create_a_new_browsing_context_and_document(JS::NonnullGCPtr<Page> page, JS::GCPtr<DOM::Document> creator, JS::GCPtr<DOM::Element> embedder, JS::NonnullGCPtr<BrowsingContextGroup> group)
{
auto& vm = group->vm();
// 1. Let browsingContext be a new browsing context.
JS::NonnullGCPtr<BrowsingContext> browsing_context = *vm.heap().allocate_without_realm<BrowsingContext>(page);
// 2. Let unsafeContextCreationTime be the unsafe shared current time.
[[maybe_unused]] auto unsafe_context_creation_time = HighResolutionTime::unsafe_shared_current_time();
// 3. Let creatorOrigin be null.
Optional<URL::Origin> creator_origin = {};
// 4. Let creatorBaseURL be null.
Optional<URL::URL> creator_base_url = {};
// 5. If creator is non-null, then:
if (creator) {
// 1. Set creatorOrigin to creator's origin.
creator_origin = creator->origin();
// 2. Set creatorBaseURL to creator's document base URL.
creator_base_url = creator->base_url();
// 3. Set browsingContext's virtual browsing context group ID to creator's browsing context's top-level browsing context's virtual browsing context group ID.
VERIFY(creator->browsing_context());
browsing_context->m_virtual_browsing_context_group_id = creator->browsing_context()->top_level_browsing_context()->m_virtual_browsing_context_group_id;
}
// 6. Let sandboxFlags be the result of determining the creation sandboxing flags given browsingContext and embedder.
SandboxingFlagSet sandbox_flags = {};
// 7. Let origin be the result of determining the origin given about:blank, sandboxFlags, and creatorOrigin.
auto origin = determine_the_origin(URL::URL("about:blank"sv), sandbox_flags, creator_origin);
// FIXME: 8. Let permissionsPolicy be the result of creating a permissions policy given embedder and origin. [PERMISSIONSPOLICY]
// FIXME: 9. Let agent be the result of obtaining a similar-origin window agent given origin, group, and false.
JS::GCPtr<Window> window;
// 10. Let realm execution context be the result of creating a new JavaScript realm given agent and the following customizations:
auto realm_execution_context = Bindings::create_a_new_javascript_realm(
Bindings::main_thread_vm(),
[&](JS::Realm& realm) -> JS::Object* {
auto window_proxy = realm.heap().allocate<WindowProxy>(realm, realm);
browsing_context->set_window_proxy(window_proxy);
// - For the global object, create a new Window object.
window = Window::create(realm);
return window.ptr();
},
[&](JS::Realm&) -> JS::Object* {
// - For the global this binding, use browsingContext's WindowProxy object.
return browsing_context->window_proxy();
});
// 11. Let topLevelCreationURL be about:blank if embedder is null; otherwise embedder's relevant settings object's top-level creation URL.
auto top_level_creation_url = !embedder ? URL::URL("about:blank") : relevant_settings_object(*embedder).top_level_creation_url;
// 12. Let topLevelOrigin be origin if embedder is null; otherwise embedder's relevant settings object's top-level origin.
auto top_level_origin = !embedder ? origin : relevant_settings_object(*embedder).origin();
// 13. Set up a window environment settings object with about:blank, realm execution context, null, topLevelCreationURL, and topLevelOrigin.
WindowEnvironmentSettingsObject::setup(
page,
URL::URL("about:blank"),
move(realm_execution_context),
{},
top_level_creation_url,
top_level_origin);
// 14. Let loadTimingInfo be a new document load timing info with its navigation start time set to the result of calling
// coarsen time with unsafeContextCreationTime and the new environment settings object's cross-origin isolated capability.
auto load_timing_info = DOM::DocumentLoadTimingInfo();
load_timing_info.navigation_start_time = HighResolutionTime::coarsen_time(
unsafe_context_creation_time,
verify_cast<WindowEnvironmentSettingsObject>(Bindings::principal_host_defined_environment_settings_object(window->realm())).cross_origin_isolated_capability() == CanUseCrossOriginIsolatedAPIs::Yes);
// 15. Let document be a new Document, with:
auto document = HTML::HTMLDocument::create(window->realm());
// Non-standard
window->set_associated_document(*document);
// type: "html"
document->set_document_type(DOM::Document::Type::HTML);
// content type: "text/html"
document->set_content_type("text/html"_string);
// mode: "quirks"
document->set_quirks_mode(DOM::QuirksMode::Yes);
// origin: origin
document->set_origin(origin);
// browsing context: browsingContext
document->set_browsing_context(browsing_context);
// FIXME: permissions policy: permissionsPolicy
// FIXME: active sandboxing flag set: sandboxFlags
// load timing info: loadTimingInfo
document->set_load_timing_info(load_timing_info);
// is initial about:blank: true
document->set_is_initial_about_blank(true);
// Spec issue: https://github.com/whatwg/html/issues/10261
document->set_ready_to_run_scripts();
// about base URL: creatorBaseURL
document->set_about_base_url(creator_base_url);
// allow declarative shadow roots: true
document->set_allow_declarative_shadow_roots(true);
// 16. If creator is non-null, then:
if (creator) {
// 1. Set document's referrer to the serialization of creator's URL.
document->set_referrer(MUST(String::from_byte_string(creator->url().serialize())));
// 2. Set document's policy container to a clone of creator's policy container.
document->set_policy_container(creator->policy_container());
// 3. If creator's origin is same origin with creator's relevant settings object's top-level origin,
if (creator->origin().is_same_origin(creator->relevant_settings_object().top_level_origin)) {
// then set document's opener policy to creator's browsing context's top-level browsing context's active document's opener policy.
VERIFY(creator->browsing_context());
VERIFY(creator->browsing_context()->top_level_browsing_context()->active_document());
document->set_opener_policy(creator->browsing_context()->top_level_browsing_context()->active_document()->opener_policy());
}
}
// 17. Assert: document's URL and document's relevant settings object's creation URL are about:blank.
VERIFY(document->url() == "about:blank"sv);
VERIFY(document->relevant_settings_object().creation_url == "about:blank"sv);
// 18. Mark document as ready for post-load tasks.
document->set_ready_for_post_load_tasks(true);
// 19. Populate with html/head/body given document.
populate_with_html_head_body(*document);
// 20. Make active document.
document->make_active();
// 21. Completely finish loading document.
document->completely_finish_loading();
// 22. Return browsingContext and document.
return BrowsingContext::BrowsingContextAndDocument { browsing_context, document };
}
BrowsingContext::BrowsingContext(JS::NonnullGCPtr<Page> page)
: m_page(page)
{
}
BrowsingContext::~BrowsingContext() = default;
void BrowsingContext::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_page);
visitor.visit(m_window_proxy);
visitor.visit(m_group);
visitor.visit(m_first_child);
visitor.visit(m_last_child);
visitor.visit(m_next_sibling);
visitor.visit(m_previous_sibling);
visitor.visit(m_opener_browsing_context);
}
// https://html.spec.whatwg.org/multipage/document-sequences.html#bc-traversable
JS::NonnullGCPtr<HTML::TraversableNavigable> BrowsingContext::top_level_traversable() const
{
// A browsing context's top-level traversable is its active document's node navigable's top-level traversable.
auto traversable = active_document()->navigable()->top_level_traversable();
VERIFY(traversable);
VERIFY(traversable->is_top_level_traversable());
return *traversable;
}
// https://html.spec.whatwg.org/multipage/browsers.html#top-level-browsing-context
bool BrowsingContext::is_top_level() const
{
// FIXME: Remove this. The active document's navigable is sometimes null when it shouldn't be, failing assertions.
return true;
// A top-level browsing context is a browsing context whose active document's node navigable is a traversable navigable.
return active_document() != nullptr && active_document()->navigable() != nullptr && active_document()->navigable()->is_traversable();
}
JS::GCPtr<BrowsingContext> BrowsingContext::top_level_browsing_context() const
{
auto const* start = this;
// 1. If start's active document is not fully active, then return null.
if (!start->active_document()->is_fully_active()) {
return nullptr;
}
// 2. Let navigable be start's active document's node navigable.
auto navigable = start->active_document()->navigable();
// 3. While navigable's parent is not null, set navigable to navigable's parent.
while (navigable->parent()) {
navigable = navigable->parent();
}
// 4. Return navigable's active browsing context.
return navigable->active_browsing_context();
}
DOM::Document const* BrowsingContext::active_document() const
{
auto* window = active_window();
if (!window)
return nullptr;
return &window->associated_document();
}
DOM::Document* BrowsingContext::active_document()
{
auto* window = active_window();
if (!window)
return nullptr;
return &window->associated_document();
}
// https://html.spec.whatwg.org/multipage/browsers.html#active-window
HTML::Window* BrowsingContext::active_window()
{
return m_window_proxy->window();
}
// https://html.spec.whatwg.org/multipage/browsers.html#active-window
HTML::Window const* BrowsingContext::active_window() const
{
return m_window_proxy->window();
}
HTML::WindowProxy* BrowsingContext::window_proxy()
{
return m_window_proxy.ptr();
}
HTML::WindowProxy const* BrowsingContext::window_proxy() const
{
return m_window_proxy.ptr();
}
void BrowsingContext::set_window_proxy(JS::GCPtr<WindowProxy> window_proxy)
{
m_window_proxy = move(window_proxy);
}
BrowsingContextGroup* BrowsingContext::group()
{
return m_group;
}
BrowsingContextGroup const* BrowsingContext::group() const
{
return m_group;
}
void BrowsingContext::set_group(BrowsingContextGroup* group)
{
m_group = group;
}
// https://html.spec.whatwg.org/multipage/browsers.html#bcg-remove
void BrowsingContext::remove()
{
// 1. Assert: browsingContext's group is non-null, because a browsing context only gets discarded once.
VERIFY(group());
// 2. Let group be browsingContext's group.
JS::NonnullGCPtr<BrowsingContextGroup> group = *this->group();
// 3. Set browsingContext's group to null.
set_group(nullptr);
// 4. Remove browsingContext from group's browsing context set.
group->browsing_context_set().remove(*this);
// 5. If group's browsing context set is empty, then remove group from the user agent's browsing context group set.
// NOTE: This is done by ~BrowsingContextGroup() when the refcount reaches 0.
}
// https://html.spec.whatwg.org/multipage/origin.html#one-permitted-sandboxed-navigator
BrowsingContext const* BrowsingContext::the_one_permitted_sandboxed_navigator() const
{
// FIXME: Implement this.
return nullptr;
}
JS::GCPtr<BrowsingContext> BrowsingContext::first_child() const
{
return m_first_child;
}
JS::GCPtr<BrowsingContext> BrowsingContext::next_sibling() const
{
return m_next_sibling;
}
// https://html.spec.whatwg.org/multipage/document-sequences.html#ancestor-browsing-context
bool BrowsingContext::is_ancestor_of(BrowsingContext const& potential_descendant) const
{
// A browsing context potentialDescendant is said to be an ancestor of a browsing context potentialAncestor if the following algorithm returns true:
// 1. Let potentialDescendantDocument be potentialDescendant's active document.
auto const* potential_descendant_document = potential_descendant.active_document();
// 2. If potentialDescendantDocument is not fully active, then return false.
if (!potential_descendant_document->is_fully_active())
return false;
// 3. Let ancestorBCs be the list obtained by taking the browsing context of the active document of each member of potentialDescendantDocument's ancestor navigables.
for (auto const& ancestor : potential_descendant_document->ancestor_navigables()) {
auto ancestor_browsing_context = ancestor->active_browsing_context();
// 4. If ancestorBCs contains potentialAncestor, then return true.
if (ancestor_browsing_context == this)
return true;
}
// 5. Return false.
return false;
}
// https://html.spec.whatwg.org/multipage/document-sequences.html#familiar-with
bool BrowsingContext::is_familiar_with(BrowsingContext const& other) const
{
// A browsing context A is familiar with a second browsing context B if the following algorithm returns true:
auto const& A = *this;
auto const& B = other;
// 1. If A's active document's origin is same origin with B's active document's origin, then return true.
if (A.active_document()->origin().is_same_origin(B.active_document()->origin()))
return true;
// 2. If A's top-level browsing context is B, then return true.
if (A.top_level_browsing_context() == &B)
return true;
// 3. If B is an auxiliary browsing context and A is familiar with B's opener browsing context, then return true.
if (B.opener_browsing_context() != nullptr && A.is_familiar_with(*B.opener_browsing_context()))
return true;
// 4. If there exists an ancestor browsing context of B whose active document has the same origin as the active document of A, then return true.
// NOTE: This includes the case where A is an ancestor browsing context of B.
// If B's active document is not fully active then it cannot have ancestor browsing context
if (!B.active_document()->is_fully_active())
return false;
for (auto const& ancestor : B.active_document()->ancestor_navigables()) {
if (ancestor->active_document()->origin().is_same_origin(A.active_document()->origin()))
return true;
}
// 5. Return false.
return false;
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#snapshotting-target-snapshot-params
SandboxingFlagSet determine_the_creation_sandboxing_flags(BrowsingContext const&, JS::GCPtr<DOM::Element>)
{
// FIXME: Populate this once we have the proper flag sets on BrowsingContext
return {};
}
bool BrowsingContext::has_navigable_been_destroyed() const
{
auto navigable = active_document()->navigable();
return !navigable || navigable->has_been_destroyed();
}
}

View file

@ -0,0 +1,180 @@
/*
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <AK/Noncopyable.h>
#include <AK/RefPtr.h>
#include <AK/WeakPtr.h>
#include <LibGfx/Bitmap.h>
#include <LibGfx/Rect.h>
#include <LibGfx/Size.h>
#include <LibJS/Forward.h>
#include <LibJS/Heap/Cell.h>
#include <LibURL/Origin.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/NavigableContainer.h>
#include <LibWeb/HTML/SandboxingFlagSet.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWeb/HTML/TokenizedFeatures.h>
#include <LibWeb/HTML/VisibilityState.h>
#include <LibWeb/Platform/Timer.h>
#include <LibWeb/TreeNode.h>
namespace Web::HTML {
class BrowsingContext final : public JS::Cell {
JS_CELL(BrowsingContext, JS::Cell);
JS_DECLARE_ALLOCATOR(BrowsingContext);
public:
struct BrowsingContextAndDocument {
JS::NonnullGCPtr<BrowsingContext> browsing_context;
JS::NonnullGCPtr<DOM::Document> document;
};
static WebIDL::ExceptionOr<BrowsingContextAndDocument> create_a_new_browsing_context_and_document(JS::NonnullGCPtr<Page> page, JS::GCPtr<DOM::Document> creator, JS::GCPtr<DOM::Element> embedder, JS::NonnullGCPtr<BrowsingContextGroup> group);
static WebIDL::ExceptionOr<BrowsingContextAndDocument> create_a_new_auxiliary_browsing_context_and_document(JS::NonnullGCPtr<Page> page, JS::NonnullGCPtr<HTML::BrowsingContext> opener);
virtual ~BrowsingContext() override;
JS::NonnullGCPtr<HTML::TraversableNavigable> top_level_traversable() const;
JS::GCPtr<BrowsingContext> first_child() const;
JS::GCPtr<BrowsingContext> next_sibling() const;
bool is_ancestor_of(BrowsingContext const&) const;
bool is_familiar_with(BrowsingContext const&) const;
template<typename Callback>
TraversalDecision for_each_in_inclusive_subtree(Callback callback) const
{
if (callback(*this) == TraversalDecision::Break)
return TraversalDecision::Break;
for (auto child = first_child(); child; child = child->next_sibling()) {
if (child->for_each_in_inclusive_subtree(callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
}
template<typename Callback>
TraversalDecision for_each_in_inclusive_subtree(Callback callback)
{
if (callback(*this) == TraversalDecision::Break)
return TraversalDecision::Break;
for (auto child = first_child(); child; child = child->next_sibling()) {
if (child->for_each_in_inclusive_subtree(callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
}
template<typename Callback>
TraversalDecision for_each_in_subtree(Callback callback) const
{
for (auto child = first_child(); child; child = child->next_sibling()) {
if (child->for_each_in_inclusive_subtree(callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
}
template<typename Callback>
TraversalDecision for_each_in_subtree(Callback callback)
{
for (auto child = first_child(); child; child = child->next_sibling()) {
if (child->for_each_in_inclusive_subtree(callback) == TraversalDecision::Break)
return TraversalDecision::Break;
}
return TraversalDecision::Continue;
}
bool is_top_level() const;
bool is_auxiliary() const { return m_is_auxiliary; }
DOM::Document const* active_document() const;
DOM::Document* active_document();
HTML::WindowProxy* window_proxy();
HTML::WindowProxy const* window_proxy() const;
void set_window_proxy(JS::GCPtr<WindowProxy>);
HTML::Window* active_window();
HTML::Window const* active_window() const;
Page& page() { return m_page; }
Page const& page() const { return m_page; }
u64 virtual_browsing_context_group_id() const { return m_virtual_browsing_context_group_id; }
JS::GCPtr<BrowsingContext> top_level_browsing_context() const;
BrowsingContextGroup* group();
BrowsingContextGroup const* group() const;
void set_group(BrowsingContextGroup*);
// https://html.spec.whatwg.org/multipage/browsers.html#bcg-remove
void remove();
// https://html.spec.whatwg.org/multipage/origin.html#one-permitted-sandboxed-navigator
BrowsingContext const* the_one_permitted_sandboxed_navigator() const;
bool has_navigable_been_destroyed() const;
JS::GCPtr<BrowsingContext> opener_browsing_context() const { return m_opener_browsing_context; }
void set_opener_browsing_context(JS::GCPtr<BrowsingContext> browsing_context) { m_opener_browsing_context = browsing_context; }
void set_is_popup(TokenizedFeature::Popup is_popup) { m_is_popup = is_popup; }
private:
explicit BrowsingContext(JS::NonnullGCPtr<Page>);
virtual void visit_edges(Cell::Visitor&) override;
JS::NonnullGCPtr<Page> m_page;
// https://html.spec.whatwg.org/multipage/document-sequences.html#browsing-context
JS::GCPtr<HTML::WindowProxy> m_window_proxy;
// https://html.spec.whatwg.org/multipage/browsers.html#opener-browsing-context
JS::GCPtr<BrowsingContext> m_opener_browsing_context;
// https://html.spec.whatwg.org/multipage/document-sequences.html#opener-origin-at-creation
Optional<URL::Origin> m_opener_origin_at_creation;
// https://html.spec.whatwg.org/multipage/browsers.html#is-popup
TokenizedFeature::Popup m_is_popup { TokenizedFeature::Popup::No };
// https://html.spec.whatwg.org/multipage/document-sequences.html#is-auxiliary
bool m_is_auxiliary { false };
// https://html.spec.whatwg.org/multipage/document-sequences.html#browsing-context-initial-url
Optional<URL::URL> m_initial_url;
// https://html.spec.whatwg.org/multipage/document-sequences.html#virtual-browsing-context-group-id
u64 m_virtual_browsing_context_group_id = { 0 };
// https://html.spec.whatwg.org/multipage/browsers.html#tlbc-group
JS::GCPtr<BrowsingContextGroup> m_group;
JS::GCPtr<BrowsingContext> m_first_child;
JS::GCPtr<BrowsingContext> m_last_child;
JS::GCPtr<BrowsingContext> m_next_sibling;
JS::GCPtr<BrowsingContext> m_previous_sibling;
};
URL::Origin determine_the_origin(Optional<URL::URL> const&, SandboxingFlagSet, Optional<URL::Origin> source_origin);
SandboxingFlagSet determine_the_creation_sandboxing_flags(BrowsingContext const&, JS::GCPtr<DOM::Element> embedder);
// FIXME: Find a better home for these
bool url_matches_about_blank(URL::URL const& url);
bool url_matches_about_srcdoc(URL::URL const& url);
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/BrowsingContextGroup.h>
#include <LibWeb/Page/Page.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(BrowsingContextGroup);
// https://html.spec.whatwg.org/multipage/browsers.html#browsing-context-group-set
static HashTable<JS::NonnullGCPtr<BrowsingContextGroup>>& user_agent_browsing_context_group_set()
{
static HashTable<JS::NonnullGCPtr<BrowsingContextGroup>> set;
return set;
}
BrowsingContextGroup::BrowsingContextGroup(JS::NonnullGCPtr<Web::Page> page)
: m_page(page)
{
user_agent_browsing_context_group_set().set(*this);
}
BrowsingContextGroup::~BrowsingContextGroup()
{
user_agent_browsing_context_group_set().remove(*this);
}
void BrowsingContextGroup::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_page);
visitor.visit(m_browsing_context_set);
}
// https://html.spec.whatwg.org/multipage/document-sequences.html#creating-a-new-browsing-context-group-and-document
auto BrowsingContextGroup::create_a_new_browsing_context_group_and_document(JS::NonnullGCPtr<Page> page) -> WebIDL::ExceptionOr<BrowsingContextGroupAndDocument>
{
// 1. Let group be a new browsing context group.
// 2. Append group to the user agent's browsing context group set.
auto group = Bindings::main_thread_vm().heap().allocate_without_realm<BrowsingContextGroup>(page);
// 3. Let browsingContext and document be the result of creating a new browsing context and document with null, null, and group.
auto [browsing_context, document] = TRY(BrowsingContext::create_a_new_browsing_context_and_document(page, nullptr, nullptr, group));
// 4. Append browsingContext to group.
group->append(browsing_context);
// 5. Return group and document.
return BrowsingContextGroupAndDocument { group, document };
}
// https://html.spec.whatwg.org/multipage/browsers.html#bcg-append
void BrowsingContextGroup::append(BrowsingContext& browsing_context)
{
VERIFY(browsing_context.is_top_level());
// 1. Append browsingContext to group's browsing context set.
m_browsing_context_set.set(browsing_context);
// 2. Set browsingContext's group to group.
browsing_context.set_group(this);
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/NonnullRefPtr.h>
#include <AK/WeakPtr.h>
#include <LibJS/Heap/Cell.h>
#include <LibWeb/Forward.h>
namespace Web::HTML {
class BrowsingContextGroup final : public JS::Cell {
JS_CELL(BrowsingContextGroup, JS::Cell);
JS_DECLARE_ALLOCATOR(BrowsingContextGroup);
public:
struct BrowsingContextGroupAndDocument {
JS::NonnullGCPtr<HTML::BrowsingContextGroup> browsing_context;
JS::NonnullGCPtr<DOM::Document> document;
};
static WebIDL::ExceptionOr<BrowsingContextGroupAndDocument> create_a_new_browsing_context_group_and_document(JS::NonnullGCPtr<Page>);
~BrowsingContextGroup();
Page& page() { return m_page; }
Page const& page() const { return m_page; }
auto& browsing_context_set() { return m_browsing_context_set; }
auto const& browsing_context_set() const { return m_browsing_context_set; }
void append(BrowsingContext&);
private:
explicit BrowsingContextGroup(JS::NonnullGCPtr<Web::Page>);
virtual void visit_edges(Cell::Visitor&) override;
// https://html.spec.whatwg.org/multipage/browsers.html#browsing-context-group-set
OrderedHashTable<JS::NonnullGCPtr<BrowsingContext>> m_browsing_context_set;
JS::NonnullGCPtr<Page> m_page;
};
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2023, Srikavin Ramkumar <me@srikavin.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/HTML/CORSSettingAttribute.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
CORSSettingAttribute cors_setting_attribute_from_keyword(Optional<String> const& keyword)
{
if (!keyword.has_value()) {
// its missing value default is the No CORS state
return CORSSettingAttribute::NoCORS;
}
if (keyword->bytes_as_string_view().equals_ignoring_ascii_case("use-credentials"sv)) {
return CORSSettingAttribute::UseCredentials;
}
// The attribute's invalid value default is the Anonymous state
return CORSSettingAttribute::Anonymous;
}
// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attribute-credentials-mode
Fetch::Infrastructure::Request::CredentialsMode cors_settings_attribute_credentials_mode(CORSSettingAttribute attribute)
{
switch (attribute) {
// -> No CORS
// -> Anonymous
case CORSSettingAttribute::NoCORS:
case CORSSettingAttribute::Anonymous:
// "same-origin"
return Fetch::Infrastructure::Request::CredentialsMode::SameOrigin;
// -> Use Credentials
case CORSSettingAttribute::UseCredentials:
// "include"
return Fetch::Infrastructure::Request::CredentialsMode::Include;
}
VERIFY_NOT_REACHED();
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023, Srikavin Ramkumar <me@srikavin.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/String.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attribute
enum class CORSSettingAttribute {
NoCORS,
Anonymous,
UseCredentials
};
[[nodiscard]] CORSSettingAttribute cors_setting_attribute_from_keyword(Optional<String> const& keyword);
[[nodiscard]] Fetch::Infrastructure::Request::CredentialsMode cors_settings_attribute_credentials_mode(CORSSettingAttribute);
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/HTML/ImageData.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvascompositing
class CanvasCompositing {
public:
virtual ~CanvasCompositing() = default;
virtual float global_alpha() const = 0;
virtual void set_global_alpha(float) = 0;
protected:
CanvasCompositing() = default;
};
}

View file

@ -0,0 +1,6 @@
// https://html.spec.whatwg.org/multipage/canvas.html#canvascompositing
interface mixin CanvasCompositing {
// compositing
attribute unrestricted double globalAlpha; // (default 1.0)
[FIXME] attribute DOMString globalCompositeOperation; // (default "source-over")
};

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/HTML/Canvas/CanvasDrawImage.h>
#include <LibWeb/HTML/ImageBitmap.h>
#include <LibWeb/SVG/SVGImageElement.h>
namespace Web::HTML {
static void default_source_size(CanvasImageSource const& image, float& source_width, float& source_height)
{
image.visit(
[&source_width, &source_height](JS::Handle<HTMLImageElement> const& source) {
if (source->immutable_bitmap()) {
source_width = source->immutable_bitmap()->width();
source_height = source->immutable_bitmap()->height();
} else {
// FIXME: This is very janky and not correct.
source_width = source->width();
source_height = source->height();
}
},
[&source_width, &source_height](JS::Handle<SVG::SVGImageElement> const& source) {
if (source->current_image_bitmap()) {
source_width = source->current_image_bitmap()->width();
source_height = source->current_image_bitmap()->height();
} else {
// FIXME: This is very janky and not correct.
source_width = source->width()->anim_val()->value();
source_height = source->height()->anim_val()->value();
}
},
[&source_width, &source_height](JS::Handle<HTML::HTMLVideoElement> const& source) {
if (auto const bitmap = source->bitmap(); bitmap) {
source_width = bitmap->width();
source_height = bitmap->height();
} else {
source_width = source->video_width();
source_height = source->video_height();
}
},
[&source_width, &source_height](JS::Handle<HTMLCanvasElement> const& source) {
if (source->surface()) {
source_width = source->surface()->size().width();
source_height = source->surface()->size().height();
} else {
source_width = source->width();
source_height = source->height();
}
},
[&source_width, &source_height](auto const& source) {
if (source->bitmap()) {
source_width = source->bitmap()->width();
source_height = source->bitmap()->height();
} else {
source_width = source->width();
source_height = source->height();
}
});
}
WebIDL::ExceptionOr<void> CanvasDrawImage::draw_image(Web::HTML::CanvasImageSource const& image, float destination_x, float destination_y)
{
// If not specified, the dw and dh arguments must default to the values of sw and sh, interpreted such that one CSS pixel in the image is treated as one unit in the output bitmap's coordinate space.
// If the sx, sy, sw, and sh arguments are omitted, then they must default to 0, 0, the image's intrinsic width in image pixels, and the image's intrinsic height in image pixels, respectively.
// If the image has no intrinsic dimensions, then the concrete object size must be used instead, as determined using the CSS "Concrete Object Size Resolution" algorithm, with the specified size having
// neither a definite width nor height, nor any additional constraints, the object's intrinsic properties being those of the image argument, and the default object size being the size of the output bitmap.
float source_width;
float source_height;
default_source_size(image, source_width, source_height);
return draw_image_internal(image, 0, 0, source_width, source_height, destination_x, destination_y, source_width, source_height);
}
WebIDL::ExceptionOr<void> CanvasDrawImage::draw_image(Web::HTML::CanvasImageSource const& image, float destination_x, float destination_y, float destination_width, float destination_height)
{
// If the sx, sy, sw, and sh arguments are omitted, then they must default to 0, 0, the image's intrinsic width in image pixels, and the image's intrinsic height in image pixels, respectively.
// If the image has no intrinsic dimensions, then the concrete object size must be used instead, as determined using the CSS "Concrete Object Size Resolution" algorithm, with the specified size having
// neither a definite width nor height, nor any additional constraints, the object's intrinsic properties being those of the image argument, and the default object size being the size of the output bitmap.
float source_width;
float source_height;
default_source_size(image, source_width, source_height);
return draw_image_internal(image, 0, 0, source_width, source_height, destination_x, destination_y, destination_width, destination_height);
}
WebIDL::ExceptionOr<void> CanvasDrawImage::draw_image(Web::HTML::CanvasImageSource const& image, float source_x, float source_y, float source_width, float source_height, float destination_x, float destination_y, float destination_width, float destination_height)
{
return draw_image_internal(image, source_x, source_y, source_width, source_height, destination_x, destination_y, destination_width, destination_height);
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/HTMLCanvasElement.h>
#include <LibWeb/HTML/HTMLImageElement.h>
#include <LibWeb/HTML/HTMLVideoElement.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvasimagesource
// NOTE: This is the Variant created by the IDL wrapper generator, and needs to be updated accordingly.
using CanvasImageSource = Variant<JS::Handle<HTMLImageElement>, JS::Handle<SVG::SVGImageElement>, JS::Handle<HTMLCanvasElement>, JS::Handle<ImageBitmap>, JS::Handle<HTMLVideoElement>>;
// https://html.spec.whatwg.org/multipage/canvas.html#canvasdrawimage
class CanvasDrawImage {
public:
virtual ~CanvasDrawImage() = default;
WebIDL::ExceptionOr<void> draw_image(CanvasImageSource const&, float destination_x, float destination_y);
WebIDL::ExceptionOr<void> draw_image(CanvasImageSource const&, float destination_x, float destination_y, float destination_width, float destination_height);
WebIDL::ExceptionOr<void> draw_image(CanvasImageSource const&, float source_x, float source_y, float source_width, float source_height, float destination_x, float destination_y, float destination_width, float destination_height);
virtual WebIDL::ExceptionOr<void> draw_image_internal(CanvasImageSource const&, float source_x, float source_y, float source_width, float source_height, float destination_x, float destination_y, float destination_width, float destination_height) = 0;
protected:
CanvasDrawImage() = default;
};
}

View file

@ -0,0 +1,22 @@
#import <HTML/HTMLCanvasElement.idl>
#import <HTML/HTMLImageElement.idl>
#import <HTML/HTMLVideoElement.idl>
#import <HTML/ImageBitmap.idl>
#import <SVG/SVGImageElement.idl>
typedef (HTMLImageElement or
SVGImageElement or
// FIXME: We should use HTMLOrSVGImageElement instead of HTMLImageElement
HTMLVideoElement or
HTMLCanvasElement or
ImageBitmap
// FIXME: OffscreenCanvas
// FIXME: VideoFrame
) CanvasImageSource;
// https://html.spec.whatwg.org/multipage/canvas.html#canvasdrawimage
interface mixin CanvasDrawImage {
undefined drawImage(CanvasImageSource image, unrestricted double dx, unrestricted double dy);
undefined drawImage(CanvasImageSource image, unrestricted double dx, unrestricted double dy, unrestricted double dw, unrestricted double dh);
undefined drawImage(CanvasImageSource image, unrestricted double sx, unrestricted double sy, unrestricted double sw, unrestricted double sh, unrestricted double dx, unrestricted double dy, unrestricted double dw, unrestricted double dh);
};

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteString.h>
#include <LibWeb/HTML/Path2D.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvasdrawpath
class CanvasDrawPath {
public:
virtual ~CanvasDrawPath() = default;
virtual void begin_path() = 0;
virtual void fill(StringView fill_rule) = 0;
virtual void fill(Path2D& path, StringView fill_rule) = 0;
virtual void stroke() = 0;
virtual void stroke(Path2D const& path) = 0;
virtual void clip(StringView fill_rule) = 0;
virtual void clip(Path2D& path, StringView fill_rule) = 0;
virtual bool is_point_in_path(double x, double y, StringView fill_rule) = 0;
virtual bool is_point_in_path(Path2D const& path, double x, double y, StringView fill_rule) = 0;
protected:
CanvasDrawPath() = default;
};
}

View file

@ -0,0 +1,27 @@
#import <HTML/Path2D.idl>
// https://html.spec.whatwg.org/multipage/canvas.html#canvasfillrule
enum CanvasFillRule { "nonzero", "evenodd" };
// https://html.spec.whatwg.org/multipage/canvas.html#canvasdrawpath
interface mixin CanvasDrawPath {
undefined beginPath();
// FIXME: `DOMString` should be `CanvasFillRule`
undefined fill(optional DOMString fillRule = "nonzero");
// FIXME: `DOMString` should be `CanvasFillRule`
undefined fill(Path2D path, optional DOMString fillRule = "nonzero");
undefined stroke();
undefined stroke(Path2D path);
// FIXME: `DOMString` should be `CanvasFillRule`
undefined clip(optional DOMString fillRule = "nonzero");
// FIXME: `DOMString` should be `CanvasFillRule`
undefined clip(Path2D path, optional DOMString fillRule = "nonzero");
// FIXME: `DOMString` should be `CanvasFillRule`
boolean isPointInPath(unrestricted double x, unrestricted double y, optional DOMString fillRule = "nonzero");
// FIXME: `DOMString` should be `CanvasFillRule`
boolean isPointInPath(Path2D path, unrestricted double x, unrestricted double y, optional DOMString fillRule = "nonzero");
[FIXME] boolean isPointInStroke(unrestricted double x, unrestricted double y);
[FIXME] boolean isPointInStroke(Path2D path, unrestricted double x, unrestricted double y);
};

View file

@ -0,0 +1,142 @@
/*
* Copyright (c) 2020-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/HTML/Canvas/CanvasState.h>
#include <LibWeb/HTML/CanvasGradient.h>
#include <LibWeb/HTML/CanvasPattern.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvasfillstrokestyles
template<typename IncludingClass>
class CanvasFillStrokeStyles {
public:
~CanvasFillStrokeStyles() = default;
using FillOrStrokeStyleVariant = Variant<String, JS::Handle<CanvasGradient>, JS::Handle<CanvasPattern>>;
void set_fill_style(FillOrStrokeStyleVariant style)
{
auto& realm = static_cast<IncludingClass&>(*this).realm();
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-fillstyle
style.visit(
// 1. If the given value is a string, then:
[&](String const& string) {
// 1. Let context be this's canvas attribute's value, if that is an element; otherwise null.
auto parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm), string);
// 2. Let parsedValue be the result of parsing the given value with context if non-null.
// FIXME: Parse a color value
// https://drafts.csswg.org/css-color/#parse-a-css-color-value
auto style_value = parser.parse_as_css_value(CSS::PropertyID::Color);
if (style_value && style_value->has_color()) {
auto parsedValue = style_value->to_color(OptionalNone());
// 4. Set this's fill style to parsedValue.
my_drawing_state().fill_style = parsedValue;
} else {
// 3. If parsedValue is failure, then return.
return;
}
// 5. Return.
return;
},
[&](auto fill_or_stroke_style) {
// FIXME: 2. If the given value is a CanvasPattern object that is marked as not origin-clean, then set this's origin-clean flag to false.
// 3. Set this's fill style to the given value.
my_drawing_state().fill_style = fill_or_stroke_style;
});
}
FillOrStrokeStyleVariant fill_style() const
{
return my_drawing_state().fill_style.to_js_fill_or_stroke_style();
}
void set_stroke_style(FillOrStrokeStyleVariant style)
{
auto& realm = static_cast<IncludingClass&>(*this).realm();
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-strokestyle
style.visit(
// 1. If the given value is a string, then:
[&](String const& string) {
// 1. Let context be this's canvas attribute's value, if that is an element; otherwise null.
auto parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm), string);
// 2. Let parsedValue be the result of parsing the given value with context if non-null.
// FIXME: Parse a color value
// https://drafts.csswg.org/css-color/#parse-a-css-color-value
auto style_value = parser.parse_as_css_value(CSS::PropertyID::Color);
if (style_value && style_value->has_color()) {
auto parsedValue = style_value->to_color(OptionalNone());
// 4. Set this's stroke style to parsedValue.
my_drawing_state().stroke_style = parsedValue;
} else {
// 3. If parsedValue is failure, then return.
return;
}
// 5. Return.
return;
},
[&](auto fill_or_stroke_style) {
// FIXME: 2. If the given value is a CanvasPattern object that is marked as not origin-clean, then set this's origin-clean flag to false.
// 3. Set this's stroke style to the given value.
my_drawing_state().fill_style = fill_or_stroke_style;
});
}
FillOrStrokeStyleVariant stroke_style() const
{
return my_drawing_state().stroke_style.to_js_fill_or_stroke_style();
}
WebIDL::ExceptionOr<JS::NonnullGCPtr<CanvasGradient>> create_radial_gradient(double x0, double y0, double r0, double x1, double y1, double r1)
{
auto& realm = static_cast<IncludingClass&>(*this).realm();
return CanvasGradient::create_radial(realm, x0, y0, r0, x1, y1, r1);
}
JS::NonnullGCPtr<CanvasGradient> create_linear_gradient(double x0, double y0, double x1, double y1)
{
auto& realm = static_cast<IncludingClass&>(*this).realm();
return CanvasGradient::create_linear(realm, x0, y0, x1, y1).release_value_but_fixme_should_propagate_errors();
}
JS::NonnullGCPtr<CanvasGradient> create_conic_gradient(double start_angle, double x, double y)
{
auto& realm = static_cast<IncludingClass&>(*this).realm();
return CanvasGradient::create_conic(realm, start_angle, x, y).release_value_but_fixme_should_propagate_errors();
}
WebIDL::ExceptionOr<JS::GCPtr<CanvasPattern>> create_pattern(CanvasImageSource const& image, StringView repetition)
{
auto& realm = static_cast<IncludingClass&>(*this).realm();
return CanvasPattern::create(realm, image, repetition);
}
protected:
CanvasFillStrokeStyles() = default;
private:
CanvasState::DrawingState& my_drawing_state() { return reinterpret_cast<IncludingClass&>(*this).drawing_state(); }
CanvasState::DrawingState const& my_drawing_state() const { return reinterpret_cast<IncludingClass const&>(*this).drawing_state(); }
};
}

View file

@ -0,0 +1,14 @@
#import <HTML/CanvasGradient.idl>
#import <HTML/CanvasPattern.idl>
#import <HTML/HTMLCanvasElement.idl>
#import <HTML/HTMLImageElement.idl>
// https://html.spec.whatwg.org/multipage/canvas.html#canvasfillstrokestyles
interface mixin CanvasFillStrokeStyles {
attribute (DOMString or CanvasGradient or CanvasPattern) strokeStyle;
attribute (DOMString or CanvasGradient or CanvasPattern) fillStyle;
CanvasGradient createLinearGradient(double x0, double y0, double x1, double y1);
CanvasGradient createRadialGradient(double x0, double y0, double r0, double x1, double y1, double r1);
CanvasGradient createConicGradient(double startAngle, double x, double y);
CanvasPattern? createPattern(CanvasImageSource image, [LegacyNullToEmptyString] DOMString repetition);
};

View file

@ -0,0 +1,5 @@
// https://html.spec.whatwg.org/multipage/canvas.html#canvasfilters
interface mixin CanvasFilters {
// filters
[FIXME] attribute DOMString filter; // (default "none")
};

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/HTML/ImageData.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvasimagedata
class CanvasImageData {
public:
virtual ~CanvasImageData() = default;
virtual WebIDL::ExceptionOr<JS::NonnullGCPtr<ImageData>> create_image_data(int width, int height, Optional<ImageDataSettings> const& settings = {}) const = 0;
virtual WebIDL::ExceptionOr<JS::NonnullGCPtr<ImageData>> create_image_data(ImageData const&) const = 0;
virtual WebIDL::ExceptionOr<JS::GCPtr<ImageData>> get_image_data(int x, int y, int width, int height, Optional<ImageDataSettings> const& settings = {}) const = 0;
virtual void put_image_data(ImageData const&, float x, float y) = 0;
protected:
CanvasImageData() = default;
};
}

View file

@ -0,0 +1,12 @@
#import <HTML/ImageData.idl>
// https://html.spec.whatwg.org/multipage/canvas.html#canvasimagedata
interface mixin CanvasImageData {
ImageData createImageData([EnforceRange] long sw, [EnforceRange] long sh, optional ImageDataSettings settings = {});
ImageData createImageData(ImageData imagedata);
ImageData getImageData([EnforceRange] long sx, [EnforceRange] long sy, [EnforceRange] long sw, [EnforceRange] long sh, optional ImageDataSettings settings = {});
undefined putImageData(ImageData imagedata, [EnforceRange] long dx, [EnforceRange] long dy);
[FIXME] undefined putImageData(ImageData imagedata, [EnforceRange] long dx, [EnforceRange] long dy, [EnforceRange] long dirtyX, [EnforceRange] long dirtyY, [EnforceRange] long dirtyWidth, [EnforceRange] long dirtyHeight);
};

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2023, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/HTML/ImageData.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvasimagesmoothing
class CanvasImageSmoothing {
public:
virtual ~CanvasImageSmoothing() = default;
virtual bool image_smoothing_enabled() const = 0;
virtual void set_image_smoothing_enabled(bool) = 0;
virtual Bindings::ImageSmoothingQuality image_smoothing_quality() const = 0;
virtual void set_image_smoothing_quality(Bindings::ImageSmoothingQuality) = 0;
protected:
CanvasImageSmoothing() = default;
};
}

View file

@ -0,0 +1,8 @@
#import <HTML/CanvasRenderingContext2D.idl>
// https://html.spec.whatwg.org/multipage/canvas.html#canvasimagesmoothing
interface mixin CanvasImageSmoothing {
// image smoothing
attribute boolean imageSmoothingEnabled; // (default true)
attribute ImageSmoothingQuality imageSmoothingQuality; // (default low)
};

View file

@ -0,0 +1,441 @@
/*
* Copyright (c) 2020-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Vector2.h>
#include <LibWeb/HTML/Canvas/CanvasPath.h>
namespace Web::HTML {
Gfx::AffineTransform CanvasPath::active_transform() const
{
if (m_canvas_state.has_value())
return m_canvas_state->drawing_state().transform;
return {};
}
void CanvasPath::ensure_subpath(float x, float y)
{
if (m_path.is_empty())
m_path.move_to(Gfx::FloatPoint { x, y });
}
void CanvasPath::close_path()
{
m_path.close();
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-moveto
void CanvasPath::move_to(float x, float y)
{
// 1. If either of the arguments are infinite or NaN, then return.
if (!isfinite(x) || !isfinite(y))
return;
// 2. Create a new subpath with the specified point as its first (and only) point.
m_path.move_to(Gfx::FloatPoint { x, y });
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-lineto
void CanvasPath::line_to(float x, float y)
{
// 1. If either of the arguments are infinite or NaN, then return.
if (!isfinite(x) || !isfinite(y))
return;
if (m_path.is_empty()) {
// 2. If the object's path has no subpaths, then ensure there is a subpath for (x, y).
ensure_subpath(x, y);
} else {
// 3. Otherwise, connect the last point in the subpath to the given point (x, y) using a straight line,
// and then add the given point (x, y) to the subpath.
m_path.line_to(Gfx::FloatPoint { x, y });
}
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-quadraticcurveto
void CanvasPath::quadratic_curve_to(float cpx, float cpy, float x, float y)
{
// 1. If any of the arguments are infinite or NaN, then return.
if (!isfinite(cpx) || !isfinite(cpy) || !isfinite(x) || !isfinite(y))
return;
// 2. Ensure there is a subpath for (cpx, cpy)
ensure_subpath(cpx, cpy);
// 3. Connect the last point in the subpath to the given point (x, y) using a quadratic Bézier curve with control point (cpx, cpy).
// 4. Add the given point (x, y) to the subpath.
m_path.quadratic_bezier_curve_to(Gfx::FloatPoint { cpx, cpy }, Gfx::FloatPoint { x, y });
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-beziercurveto
void CanvasPath::bezier_curve_to(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y)
{
// 1. If any of the arguments are infinite or NaN, then return.
if (!isfinite(cp1x) || !isfinite(cp1y) || !isfinite(cp2x) || !isfinite(cp2y) || !isfinite(x) || !isfinite(y))
return;
// 2. Ensure there is a subpath for (cp1x, cp1y)
ensure_subpath(cp1x, cp1y);
// 3. Connect the last point in the subpath to the given point (x, y) using a cubic Bézier curve with control poits (cp1x, cp1y) and (cp2x, cp2y).
// 4. Add the point (x, y) to the subpath.
m_path.cubic_bezier_curve_to(
Gfx::FloatPoint { cp1x, cp1y }, Gfx::FloatPoint { cp2x, cp2y }, Gfx::FloatPoint { x, y });
}
WebIDL::ExceptionOr<void> CanvasPath::arc(float x, float y, float radius, float start_angle, float end_angle, bool counter_clockwise)
{
if (radius < 0)
return WebIDL::IndexSizeError::create(m_self->realm(), MUST(String::formatted("The radius provided ({}) is negative.", radius)));
return ellipse(x, y, radius, radius, 0, start_angle, end_angle, counter_clockwise);
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-ellipse
WebIDL::ExceptionOr<void> CanvasPath::ellipse(float x, float y, float radius_x, float radius_y, float rotation, float start_angle, float end_angle, bool counter_clockwise)
{
// 1. If any of the arguments are infinite or NaN, then return.
if (!isfinite(x) || !isfinite(y) || !isfinite(radius_x) || !isfinite(radius_y) || !isfinite(rotation) || !isfinite(start_angle) || !isfinite(end_angle))
return {};
// 2. If either radiusX or radiusY are negative, then throw an "IndexSizeError" DOMException.
if (radius_x < 0)
return WebIDL::IndexSizeError::create(m_self->realm(), MUST(String::formatted("The major-axis radius provided ({}) is negative.", radius_x)));
if (radius_y < 0)
return WebIDL::IndexSizeError::create(m_self->realm(), MUST(String::formatted("The minor-axis radius provided ({}) is negative.", radius_y)));
// "If counterclockwise is false and endAngle startAngle is greater than or equal to 2π,
// or, if counterclockwise is true and startAngle endAngle is greater than or equal to 2π,
// then the arc is the whole circumference of this ellipse"
// Also draw the full ellipse if making a non-zero whole number of turns.
if (constexpr float tau = M_PI * 2; (!counter_clockwise && (end_angle - start_angle) >= tau)
|| (counter_clockwise && (start_angle - end_angle) >= tau)
|| (start_angle != end_angle && fmodf(start_angle - end_angle, tau) == 0)) {
start_angle = 0;
// FIXME: elliptical_arc_to() incorrectly handles the case where the start/end points are very close.
// So we slightly fudge the numbers here to correct for that.
end_angle = tau * 0.9999f;
counter_clockwise = false;
} else {
start_angle = fmodf(start_angle, tau);
end_angle = fmodf(end_angle, tau);
}
// Then, figure out where the ends of the arc are.
// To do so, we can pretend that the center of this ellipse is at (0, 0),
// and the whole coordinate system is rotated `rotation` radians around the x axis, centered on `center`.
// The sign of the resulting relative positions is just whether our angle is on one of the left quadrants.
float sin_rotation;
float cos_rotation;
AK::sincos(rotation, sin_rotation, cos_rotation);
auto resolve_point_with_angle = [&](float angle) {
auto tan_relative = tanf(angle);
auto tan2 = tan_relative * tan_relative;
auto ab = radius_x * radius_y;
auto a2 = radius_x * radius_x;
auto b2 = radius_y * radius_y;
auto sqrt = sqrtf(b2 + a2 * tan2);
auto relative_x_position = ab / sqrt;
auto relative_y_position = ab * tan_relative / sqrt;
// Make sure to set the correct sign
// -1 if 0 ≤ θ < 90° or 270°< θ ≤ 360°
// 1 if 90° < θ< 270°
float sn = cosf(angle) >= 0 ? 1 : -1;
relative_x_position *= sn;
relative_y_position *= sn;
// Now rotate it (back) around the center point by 'rotation' radians, then move it back to our actual origin.
auto relative_rotated_x_position = relative_x_position * cos_rotation - relative_y_position * sin_rotation;
auto relative_rotated_y_position = relative_x_position * sin_rotation + relative_y_position * cos_rotation;
return Gfx::FloatPoint { relative_rotated_x_position + x, relative_rotated_y_position + y };
};
auto start_point = resolve_point_with_angle(start_angle);
auto end_point = resolve_point_with_angle(end_angle);
float delta_theta;
if (counter_clockwise) {
delta_theta = start_angle - end_angle;
} else {
delta_theta = end_angle - start_angle;
}
if (delta_theta < 0)
delta_theta += AK::Pi<float> * 2;
// 3. If canvasPath's path has any subpaths, then add a straight line from the last point in the subpath to the start point of the arc.
if (!m_path.is_empty())
m_path.line_to(start_point);
else
m_path.move_to(start_point);
// 4. Add the start and end points of the arc to the subpath, and connect them with an arc.
m_path.elliptical_arc_to(
Gfx::FloatPoint { end_point },
Gfx::FloatSize { radius_x, radius_y },
rotation,
delta_theta > AK::Pi<float>, !counter_clockwise);
return {};
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-arcto
WebIDL::ExceptionOr<void> CanvasPath::arc_to(double x1, double y1, double x2, double y2, double radius)
{
// 1. If any of the arguments are infinite or NaN, then return.
if (!isfinite(x1) || !isfinite(y1) || !isfinite(x2) || !isfinite(y2) || !isfinite(radius))
return {};
// 2. Ensure there is a subpath for (x1, y1).
ensure_subpath(x1, y1);
// 3. If radius is negative, then throw an "IndexSizeError" DOMException.
if (radius < 0)
return WebIDL::IndexSizeError::create(m_self->realm(), MUST(String::formatted("The radius provided ({}) is negative.", radius)));
auto transform = active_transform();
// 4. Let the point (x0, y0) be the last point in the subpath,
// transformed by the inverse of the current transformation matrix
// (so that it is in the same coordinate system as the points passed to the method).
// Point (x0, y0)
auto p0 = transform.inverse().value_or(Gfx::AffineTransform()).map(m_path.last_point());
// Point (x1, y1)
auto p1 = Gfx::FloatPoint { x1, y1 };
// Point (x2, y2)
auto p2 = Gfx::FloatPoint { x2, y2 };
// 5. If the point (x0, y0) is equal to the point (x1, y1),
// or if the point (x1, y1) is equal to the point (x2, y2),
// or if radius is zero, then add the point (x1, y1) to the subpath,
// and connect that point to the previous point (x0, y0) by a straight line.
if (p0 == p1 || p1 == p2 || radius == 0) {
m_path.line_to(p1);
return {};
}
auto v1 = Gfx::FloatVector2 { p0.x() - p1.x(), p0.y() - p1.y() };
auto v2 = Gfx::FloatVector2 { p2.x() - p1.x(), p2.y() - p1.y() };
auto cos_theta = v1.dot(v2) / (v1.length() * v2.length());
// 6. Otherwise, if the points (x0, y0), (x1, y1), and (x2, y2) all lie on a single straight line,
// then add the point (x1, y1) to the subpath,
// and connect that point to the previous point (x0, y0) by a straight line.
if (-1 == cos_theta || 1 == cos_theta) {
m_path.line_to(p1);
return {};
}
// 7. Otherwise, let The Arc be the shortest arc given by circumference of the circle that has radius radius,
// and that has one point tangent to the half-infinite line that crosses the point (x0, y0) and ends at the point (x1, y1),
// and that has a different point tangent to the half-infinite line that ends at the point (x1, y1) and crosses the point (x2, y2).
// The points at which this circle touches these two lines are called the start and end tangent points respectively.
auto adjacent = radius / static_cast<double>(tan(acos(cos_theta) / 2));
auto factor1 = adjacent / static_cast<double>(v1.length());
auto x3 = static_cast<double>(p1.x()) + factor1 * static_cast<double>(p0.x() - p1.x());
auto y3 = static_cast<double>(p1.y()) + factor1 * static_cast<double>(p0.y() - p1.y());
auto start_tangent = Gfx::FloatPoint { x3, y3 };
auto factor2 = adjacent / static_cast<double>(v2.length());
auto x4 = static_cast<double>(p1.x()) + factor2 * static_cast<double>(p2.x() - p1.x());
auto y4 = static_cast<double>(p1.y()) + factor2 * static_cast<double>(p2.y() - p1.y());
auto end_tangent = Gfx::FloatPoint { x4, y4 };
// Connect the point (x0, y0) to the start tangent point by a straight line, adding the start tangent point to the subpath.
m_path.line_to(start_tangent);
bool const large_arc = false; // always small since tangent points define arc endpoints and lines meet at (x1, y1)
auto cross_product = v1.x() * v2.y() - v1.y() * v2.x();
bool const sweep = cross_product < 0; // right-hand rule, true means clockwise
// and then connect the start tangent point to the end tangent point by The Arc, adding the end tangent point to the subpath.
m_path.arc_to(end_tangent, radius, large_arc, sweep);
return {};
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-rect
void CanvasPath::rect(double x, double y, double w, double h)
{
// 1. If any of the arguments are infinite or NaN, then return.
if (!isfinite(x) || !isfinite(y) || !isfinite(w) || !isfinite(h))
return;
// 2. Create a new subpath containing just the four points (x, y), (x+w, y), (x+w, y+h), (x, y+h), in that order, with those four points connected by straight lines.
m_path.move_to(Gfx::FloatPoint { x, y });
m_path.line_to(Gfx::FloatPoint { x + w, y });
m_path.line_to(Gfx::FloatPoint { x + w, y + h });
m_path.line_to(Gfx::FloatPoint { x, y + h });
// 3. Mark the subpath as closed.
m_path.close();
// 4. Create a new subpath with the point (x, y) as the only point in the subpath.
m_path.move_to(Gfx::FloatPoint { x, y });
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-roundrect
WebIDL::ExceptionOr<void> CanvasPath::round_rect(double x, double y, double w, double h, Variant<double, Geometry::DOMPointInit, Vector<Variant<double, Geometry::DOMPointInit>>> radii)
{
using Radius = Variant<double, Geometry::DOMPointInit>;
// 1. If any of x, y, w, or h are infinite or NaN, then return.
if (!isfinite(x) || !isfinite(y) || !isfinite(w) || !isfinite(h))
return {};
// 2. If radii is an unrestricted double or DOMPointInit, then set radii to « radii ».
if (radii.has<double>() || radii.has<Geometry::DOMPointInit>()) {
Vector<Radius> radii_list;
if (radii.has<double>())
radii_list.append(radii.get<double>());
else
radii_list.append(radii.get<Geometry::DOMPointInit>());
radii = radii_list;
}
// 3. If radii is not a list of size one, two, three, or four, then throw a RangeError.
if (radii.get<Vector<Radius>>().is_empty() || radii.get<Vector<Radius>>().size() > 4)
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::RangeError, "roundRect: Can have between 1 and 4 radii"sv };
// 4. Let normalizedRadii be an empty list.
Vector<Geometry::DOMPointInit> normalized_radii;
// 5. For each radius of radii:
for (auto const& radius : radii.get<Vector<Radius>>()) {
// 5.1. If radius is a DOMPointInit:
if (radius.has<Geometry::DOMPointInit>()) {
auto const& radius_as_dom_point = radius.get<Geometry::DOMPointInit>();
// 5.1.1. If radius["x"] or radius["y"] is infinite or NaN, then return.
if (!isfinite(radius_as_dom_point.x) || !isfinite(radius_as_dom_point.y))
return {};
// 5.1.2. If radius["x"] or radius["y"] is negative, then throw a RangeError.
if (radius_as_dom_point.x < 0 || radius_as_dom_point.y < 0)
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::RangeError, "roundRect: Radius can't be negative"sv };
// 5.1.3. Otherwise, append radius to normalizedRadii.
normalized_radii.append(radius_as_dom_point);
}
// 5.2. If radius is a unrestricted double:
if (radius.has<double>()) {
auto radius_as_double = radius.get<double>();
// 5.2.1. If radius is infinite or NaN, then return.
if (!isfinite(radius_as_double))
return {};
// 5.2.2. If radius is negative, then throw a RangeError.
if (radius_as_double < 0)
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::RangeError, "roundRect: Radius can't be negative"sv };
// 5.2.3. Otherwise append «[ "x" → radius, "y" → radius ]» to normalizedRadii.
normalized_radii.append(Geometry::DOMPointInit { radius_as_double, radius_as_double });
}
}
// 6. Let upperLeft, upperRight, lowerRight, and lowerLeft be null.
Geometry::DOMPointInit upper_left {};
Geometry::DOMPointInit upper_right {};
Geometry::DOMPointInit lower_right {};
Geometry::DOMPointInit lower_left {};
// 7. If normalizedRadii's size is 4, then set upperLeft to normalizedRadii[0], set upperRight to normalizedRadii[1], set lowerRight to normalizedRadii[2], and set lowerLeft to normalizedRadii[3].
if (normalized_radii.size() == 4) {
upper_left = normalized_radii.at(0);
upper_right = normalized_radii.at(1);
lower_right = normalized_radii.at(2);
lower_left = normalized_radii.at(3);
}
// 8. If normalizedRadii's size is 3, then set upperLeft to normalizedRadii[0], set upperRight and lowerLeft to normalizedRadii[1], and set lowerRight to normalizedRadii[2].
if (normalized_radii.size() == 3) {
upper_left = normalized_radii.at(0);
upper_right = lower_left = normalized_radii.at(1);
lower_right = normalized_radii.at(2);
}
// 9. If normalizedRadii's size is 2, then set upperLeft and lowerRight to normalizedRadii[0] and set upperRight and lowerLeft to normalizedRadii[1].
if (normalized_radii.size() == 2) {
upper_left = lower_right = normalized_radii.at(0);
upper_right = lower_left = normalized_radii.at(1);
}
// 10. If normalizedRadii's size is 1, then set upperLeft, upperRight, lowerRight, and lowerLeft to normalizedRadii[0].
if (normalized_radii.size() == 1)
upper_left = upper_right = lower_right = lower_left = normalized_radii.at(0);
// 11. Corner curves must not overlap. Scale all radii to prevent this:
// 11.1. Let top be upperLeft["x"] + upperRight["x"].
double top = upper_left.x + upper_right.x;
// 11.2. Let right be upperRight["y"] + lowerRight["y"].
double right = upper_right.y + lower_right.y;
// 11.3. Let bottom be lowerRight["x"] + lowerLeft["x"].
double bottom = lower_right.x + lower_left.x;
// 11.4. Let left be upperLeft["y"] + lowerLeft["y"].
double left = upper_left.y + lower_left.y;
// 11.5. Let scale be the minimum value of the ratios w / top, h / right, w / bottom, h / left.
double scale = AK::min(AK::min(w / top, h / right), AK::min(w / bottom, h / left));
// 11.6. If scale is less than 1, then set the x and y members of upperLeft, upperRight, lowerLeft, and lowerRight to their current values multiplied by scale.
if (scale < 1) {
upper_left.x *= scale;
upper_left.y *= scale;
upper_right.x *= scale;
upper_right.y *= scale;
lower_left.x *= scale;
lower_left.y *= scale;
lower_right.x *= scale;
lower_right.y *= scale;
}
// 12. Create a new subpath:
bool large_arc = false;
bool sweep = true;
// 12.1. Move to the point (x + upperLeft["x"], y).
m_path.move_to(Gfx::FloatPoint { x + upper_left.x, y });
// 12.2. Draw a straight line to the point (x + w upperRight["x"], y).
m_path.line_to(Gfx::FloatPoint { x + w - upper_right.x, y });
// 12.3. Draw an arc to the point (x + w, y + upperRight["y"]).
m_path.elliptical_arc_to(Gfx::FloatPoint { x + w, y + upper_right.y }, { upper_right.x, upper_right.y }, 0, large_arc, sweep);
// 12.4. Draw a straight line to the point (x + w, y + h lowerRight["y"]).
m_path.line_to(Gfx::FloatPoint { x + w, y + h - lower_right.y });
// 12.5. Draw an arc to the point (x + w lowerRight["x"], y + h).
m_path.elliptical_arc_to(Gfx::FloatPoint { x + w - lower_right.x, y + h }, { lower_right.x, lower_right.y }, 0, large_arc, sweep);
// 12.6. Draw a straight line to the point (x + lowerLeft["x"], y + h).
m_path.line_to(Gfx::FloatPoint { x + lower_left.x, y + h });
// 12.7. Draw an arc to the point (x, y + h lowerLeft["y"]).
m_path.elliptical_arc_to(Gfx::FloatPoint { x, y + h - lower_left.y }, { lower_left.x, lower_left.y }, 0, large_arc, sweep);
// 12.8. Draw a straight line to the point (x, y + upperLeft["y"]).
m_path.line_to(Gfx::FloatPoint { x, y + upper_left.y });
// 12.9. Draw an arc to the point (x + upperLeft["x"], y).
m_path.elliptical_arc_to(Gfx::FloatPoint { x + upper_left.x, y }, { upper_left.x, upper_left.y }, 0, large_arc, sweep);
// 13. Mark the subpath as closed.
m_path.close();
// 14. Create a new subpath with the point (x, y) as the only point in the subpath.
m_path.move_to(Gfx::FloatPoint { x, y });
return {};
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGfx/Path.h>
#include <LibWeb/Geometry/DOMPointReadOnly.h>
#include <LibWeb/HTML/Canvas/CanvasState.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvaspath
class CanvasPath {
public:
~CanvasPath() = default;
void close_path();
void move_to(float x, float y);
void line_to(float x, float y);
void quadratic_curve_to(float cx, float cy, float x, float y);
void bezier_curve_to(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y);
WebIDL::ExceptionOr<void> arc_to(double x1, double y1, double x2, double y2, double radius);
void rect(double x, double y, double w, double h);
WebIDL::ExceptionOr<void> round_rect(double x, double y, double w, double h, Variant<double, Geometry::DOMPointInit, Vector<Variant<double, Geometry::DOMPointInit>>> radii = { 0 });
WebIDL::ExceptionOr<void> arc(float x, float y, float radius, float start_angle, float end_angle, bool counter_clockwise);
WebIDL::ExceptionOr<void> ellipse(float x, float y, float radius_x, float radius_y, float rotation, float start_angle, float end_angle, bool counter_clockwise);
Gfx::Path& path() { return m_path; }
Gfx::Path const& path() const { return m_path; }
protected:
explicit CanvasPath(Bindings::PlatformObject& self)
: m_self(self)
{
}
explicit CanvasPath(Bindings::PlatformObject& self, CanvasState const& canvas_state)
: m_self(self)
, m_canvas_state(canvas_state)
{
}
private:
Gfx::AffineTransform active_transform() const;
void ensure_subpath(float x, float y);
JS::NonnullGCPtr<Bindings::PlatformObject> m_self;
Optional<CanvasState const&> m_canvas_state;
Gfx::Path m_path;
};
}

View file

@ -0,0 +1,15 @@
#import <Geometry/DOMPointReadOnly.idl>
// https://html.spec.whatwg.org/multipage/canvas.html#canvaspath
interface mixin CanvasPath {
undefined closePath();
undefined moveTo(unrestricted double x, unrestricted double y);
undefined lineTo(unrestricted double x, unrestricted double y);
undefined quadraticCurveTo(unrestricted double cpx, unrestricted double cpy, unrestricted double x, unrestricted double y);
undefined bezierCurveTo(unrestricted double cp1x, unrestricted double cp1y, unrestricted double cp2x, unrestricted double cp2y, unrestricted double x, unrestricted double y);
undefined arcTo(unrestricted double x1, unrestricted double y1, unrestricted double x2, unrestricted double y2, unrestricted double radius);
undefined rect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h);
undefined roundRect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h, optional (unrestricted double or DOMPointInit or sequence<(unrestricted double or DOMPointInit)>) radii = 0);
undefined arc(unrestricted double x, unrestricted double y, unrestricted double radius, unrestricted double startAngle, unrestricted double endAngle, optional boolean counterclockwise = false);
undefined ellipse(unrestricted double x, unrestricted double y, unrestricted double radiusX, unrestricted double radiusY, unrestricted double rotation, unrestricted double startAngle, unrestricted double endAngle, optional boolean counterclockwise = false);
};

View file

@ -0,0 +1,123 @@
/*
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/HTML/Canvas/CanvasState.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvaspathdrawingstyles
template<typename IncludingClass>
class CanvasPathDrawingStyles {
public:
~CanvasPathDrawingStyles() = default;
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-linewidth
void set_line_width(float line_width)
{
// On setting, zero, negative, infinite, and NaN values must be ignored, leaving the value unchanged;
if (line_width <= 0 || !isfinite(line_width))
return;
// other values must change the current value to the new value.
my_drawing_state().line_width = line_width;
}
float line_width() const
{
// On getting, it must return the current value.
return my_drawing_state().line_width;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-linecap
void set_line_cap(Bindings::CanvasLineCap line_cap)
{
// On setting, the current value must be changed to the new value.
my_drawing_state().line_cap = line_cap;
}
Bindings::CanvasLineCap line_cap() const
{
// On getting, it must return the current value.
return my_drawing_state().line_cap;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-linejoin
void set_line_join(Bindings::CanvasLineJoin line_join)
{
// On setting, the current value must be changed to the new value.
my_drawing_state().line_join = line_join;
}
Bindings::CanvasLineJoin line_join() const
{
// On getting, it must return the current value.
return my_drawing_state().line_join;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-miterlimit
void set_miter_limit(float miter_limit)
{
// On setting, zero, negative, infinite, and NaN values must be ignored, leaving the value unchanged;
if (miter_limit <= 0 || !isfinite(miter_limit))
return;
// other values must change the current value to the new value.
my_drawing_state().miter_limit = miter_limit;
}
float miter_limit() const
{
// On getting, it must return the current value.
return my_drawing_state().miter_limit;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-setlinedash
void set_line_dash(Vector<double> segments)
{
// The setLineDash(segments) method, when invoked, must run these steps:
// 1. If any value in segments is not finite (e.g. an Infinity or a NaN value), or if any value is negative (less than zero), then return
// (without throwing an exception; user agents could show a message on a developer console, though, as that would be helpful for debugging).
for (auto const& segment : segments) {
if (!isfinite(segment) || segment < 0)
return;
}
// 2. If the number of elements in segments is odd, then let segments be the concatenation of two copies of segments.
if (segments.size() % 2 == 1)
segments.extend(segments);
// 3. Let the object's dash list be segments.
my_drawing_state().dash_list = segments;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-getlinedash
Vector<double> get_line_dash()
{
// When the getLineDash() method is invoked, it must return a sequence whose values are the values of the object's dash list, in the same order.
return my_drawing_state().dash_list;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-linedashoffset
void set_line_dash_offset(float line_dash_offset)
{
// On setting, infinite and NaN values must be ignored, leaving the value unchanged;
if (!isfinite(line_dash_offset))
return;
// other values must change the current value to the new value.
my_drawing_state().line_dash_offset = line_dash_offset;
}
float line_dash_offset() const
{
// On getting, it must return the current value.
return my_drawing_state().line_dash_offset;
}
protected:
CanvasPathDrawingStyles() = default;
private:
CanvasState::DrawingState& my_drawing_state() { return reinterpret_cast<IncludingClass&>(*this).drawing_state(); }
CanvasState::DrawingState const& my_drawing_state() const { return reinterpret_cast<IncludingClass const&>(*this).drawing_state(); }
};
}

View file

@ -0,0 +1,15 @@
// https://html.spec.whatwg.org/multipage/canvas.html#canvaslinecap
// enum CanvasLineCap { "butt", "round", "square" };
// enum CanvasLineJoin { "round", "bevel", "miter" };
// https://html.spec.whatwg.org/multipage/canvas.html#canvaspathdrawingstyles
interface mixin CanvasPathDrawingStyles {
attribute unrestricted double lineWidth;
attribute CanvasLineCap lineCap;
attribute CanvasLineJoin lineJoin;
attribute unrestricted double miterLimit;
undefined setLineDash(sequence<unrestricted double> segments);
sequence<unrestricted double> getLineDash();
attribute unrestricted double lineDashOffset;
};

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvasrect
class CanvasRect {
public:
virtual ~CanvasRect() = default;
virtual void fill_rect(float x, float y, float width, float height) = 0;
virtual void stroke_rect(float x, float y, float width, float height) = 0;
virtual void clear_rect(float x, float y, float width, float height) = 0;
protected:
CanvasRect() = default;
};
}

View file

@ -0,0 +1,6 @@
// https://html.spec.whatwg.org/multipage/canvas.html#canvasrect
interface mixin CanvasRect {
undefined clearRect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h);
undefined fillRect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h);
undefined strokeRect(unrestricted double x, unrestricted double y, unrestricted double w, unrestricted double h);
};

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2024, İbrahim UYSAL <uysalibov@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/HTML/Canvas/CanvasState.h>
#include <LibWeb/HTML/CanvasGradient.h>
#include <LibWeb/HTML/CanvasPattern.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvasshadowstyles
template<typename IncludingClass>
class CanvasShadowStyles {
public:
~CanvasShadowStyles() = default;
virtual float shadow_offset_x() const = 0;
virtual void set_shadow_offset_x(float offsetX) = 0;
virtual float shadow_offset_y() const = 0;
virtual void set_shadow_offset_y(float offsetY) = 0;
virtual String shadow_color() const = 0;
virtual void set_shadow_color(String color) = 0;
protected:
CanvasShadowStyles() = default;
private:
CanvasState::DrawingState& my_drawing_state() { return reinterpret_cast<IncludingClass&>(*this).drawing_state(); }
CanvasState::DrawingState const& my_drawing_state() const { return reinterpret_cast<IncludingClass const&>(*this).drawing_state(); }
};
}

View file

@ -0,0 +1,8 @@
// https://html.spec.whatwg.org/multipage/canvas.html#canvasshadowstyles
interface mixin CanvasShadowStyles {
// shadows
attribute unrestricted double shadowOffsetX; // (default 0)
attribute unrestricted double shadowOffsetY; // (default 0)
[FIXME] attribute unrestricted double shadowBlur; // (default 0)
attribute DOMString shadowColor; // (default transparent black)
};

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Painter.h>
#include <LibWeb/HTML/Canvas/CanvasState.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-save
void CanvasState::save()
{
// The save() method steps are to push a copy of the current drawing state onto the drawing state stack.
m_drawing_state_stack.append(m_drawing_state);
if (auto* painter = painter_for_canvas_state())
painter->save();
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-restore
void CanvasState::restore()
{
// The restore() method steps are to pop the top entry in the drawing state stack, and reset the drawing state it describes. If there is no saved state, then the method must do nothing.
if (m_drawing_state_stack.is_empty())
return;
m_drawing_state = m_drawing_state_stack.take_last();
if (auto* painter = painter_for_canvas_state())
painter->restore();
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-reset
void CanvasState::reset()
{
// The reset() method steps are to reset the rendering context to its default state.
reset_to_default_state();
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-iscontextlost
bool CanvasState::is_context_lost()
{
// The isContextLost() method steps are to return this's context lost.
return m_context_lost;
}
NonnullRefPtr<Gfx::PaintStyle> CanvasState::FillOrStrokeStyle::to_gfx_paint_style()
{
return m_fill_or_stroke_style.visit(
[&](Gfx::Color color) -> NonnullRefPtr<Gfx::PaintStyle> {
if (!m_color_paint_style)
m_color_paint_style = Gfx::SolidColorPaintStyle::create(color).release_value_but_fixme_should_propagate_errors();
return m_color_paint_style.release_nonnull();
},
[&](auto handle) {
return handle->to_gfx_paint_style();
});
}
Gfx::Color CanvasState::FillOrStrokeStyle::to_color_but_fixme_should_accept_any_paint_style() const
{
return as_color().value_or(Gfx::Color::Black);
}
Optional<Gfx::Color> CanvasState::FillOrStrokeStyle::as_color() const
{
if (auto* color = m_fill_or_stroke_style.get_pointer<Gfx::Color>())
return *color;
return {};
}
}

View file

@ -0,0 +1,119 @@
/*
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibGfx/AffineTransform.h>
#include <LibGfx/Color.h>
#include <LibGfx/Font/Font.h>
#include <LibGfx/PaintStyle.h>
#include <LibGfx/Path.h>
#include <LibGfx/WindingRule.h>
#include <LibWeb/Bindings/CanvasRenderingContext2DPrototype.h>
#include <LibWeb/HTML/CanvasGradient.h>
#include <LibWeb/HTML/CanvasPattern.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvasstate
class CanvasState {
public:
virtual ~CanvasState() = default;
virtual Gfx::Painter* painter_for_canvas_state() = 0;
virtual Gfx::Path& path_for_canvas_state() = 0;
void save();
void restore();
void reset();
bool is_context_lost();
using FillOrStrokeVariant = Variant<Gfx::Color, JS::Handle<CanvasGradient>, JS::Handle<CanvasPattern>>;
struct FillOrStrokeStyle {
FillOrStrokeStyle(Gfx::Color color)
: m_fill_or_stroke_style(color)
{
}
FillOrStrokeStyle(JS::Handle<CanvasGradient> gradient)
: m_fill_or_stroke_style(gradient)
{
}
FillOrStrokeStyle(JS::Handle<CanvasPattern> pattern)
: m_fill_or_stroke_style(pattern)
{
}
NonnullRefPtr<Gfx::PaintStyle> to_gfx_paint_style();
Optional<Gfx::Color> as_color() const;
Gfx::Color to_color_but_fixme_should_accept_any_paint_style() const;
using JsFillOrStrokeStyle = Variant<String, JS::Handle<CanvasGradient>, JS::Handle<CanvasPattern>>;
JsFillOrStrokeStyle to_js_fill_or_stroke_style() const
{
return m_fill_or_stroke_style.visit(
[&](Gfx::Color color) -> JsFillOrStrokeStyle {
return color.to_string(Gfx::Color::HTMLCompatibleSerialization::Yes);
},
[&](auto handle) -> JsFillOrStrokeStyle {
return handle;
});
}
private:
FillOrStrokeVariant m_fill_or_stroke_style;
RefPtr<Gfx::PaintStyle> m_color_paint_style { nullptr };
};
// https://html.spec.whatwg.org/multipage/canvas.html#drawing-state
struct DrawingState {
Gfx::AffineTransform transform;
FillOrStrokeStyle fill_style { Gfx::Color::Black };
FillOrStrokeStyle stroke_style { Gfx::Color::Black };
float shadow_offset_x { 0.0f };
float shadow_offset_y { 0.0f };
Gfx::Color shadow_color { Gfx::Color::Transparent };
float line_width { 1 };
Bindings::CanvasLineCap line_cap { Bindings::CanvasLineCap::Butt };
Bindings::CanvasLineJoin line_join { Bindings::CanvasLineJoin::Miter };
float miter_limit { 10 };
Vector<double> dash_list;
float line_dash_offset { 0 };
bool image_smoothing_enabled { true };
Bindings::ImageSmoothingQuality image_smoothing_quality { Bindings::ImageSmoothingQuality::Low };
float global_alpha = { 1 };
RefPtr<CSS::CSSStyleValue> font_style_value { nullptr };
RefPtr<Gfx::Font const> current_font { nullptr };
Bindings::CanvasTextAlign text_align { Bindings::CanvasTextAlign::Start };
Bindings::CanvasTextBaseline text_baseline { Bindings::CanvasTextBaseline::Alphabetic };
};
DrawingState& drawing_state() { return m_drawing_state; }
DrawingState const& drawing_state() const { return m_drawing_state; }
void clear_drawing_state_stack() { m_drawing_state_stack.clear(); }
void reset_drawing_state() { m_drawing_state = {}; }
virtual void reset_to_default_state() = 0;
protected:
CanvasState() = default;
private:
DrawingState m_drawing_state;
Vector<DrawingState> m_drawing_state_stack;
// https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-context-lost
bool m_context_lost { false };
};
}

View file

@ -0,0 +1,7 @@
// https://html.spec.whatwg.org/multipage/canvas.html#canvasstate
interface mixin CanvasState {
undefined save();
undefined restore();
undefined reset();
boolean isContextLost();
};

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteString.h>
#include <AK/Optional.h>
#include <LibWeb/HTML/TextMetrics.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvastext
class CanvasText {
public:
virtual ~CanvasText() = default;
virtual void fill_text(StringView, float x, float y, Optional<double> max_width) = 0;
virtual void stroke_text(StringView, float x, float y, Optional<double> max_width) = 0;
virtual JS::NonnullGCPtr<TextMetrics> measure_text(StringView text) = 0;
protected:
CanvasText() = default;
};
}

View file

@ -0,0 +1,8 @@
#import <HTML/TextMetrics.idl>
// https://html.spec.whatwg.org/multipage/canvas.html#canvastext
interface mixin CanvasText {
undefined fillText(DOMString text, unrestricted double x, unrestricted double y, optional unrestricted double maxWidth);
undefined strokeText(DOMString text, unrestricted double x, unrestricted double y, optional unrestricted double maxWidth);
TextMetrics measureText(DOMString text);
};

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/CSS/StyleValues/ShorthandStyleValue.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/Canvas/CanvasState.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvastextdrawingstyles
template<typename IncludingClass>
class CanvasTextDrawingStyles {
public:
~CanvasTextDrawingStyles() = default;
ByteString font() const
{
// When font style value is empty return default string
if (!my_drawing_state().font_style_value) {
return "10px sans-serif";
}
// On getting, the font attribute must return the serialized form of the current font of the context (with no 'line-height' component).
auto const& font_style_value = my_drawing_state().font_style_value->as_shorthand();
auto font_style = font_style_value.longhand(CSS::PropertyID::FontStyle);
auto font_weight = font_style_value.longhand(CSS::PropertyID::FontWeight);
auto font_size = font_style_value.longhand(CSS::PropertyID::FontSize);
auto font_family = font_style_value.longhand(CSS::PropertyID::FontFamily);
return ByteString::formatted("{} {} {} {}", font_style->to_string(), font_weight->to_string(), font_size->to_string(), font_family->to_string());
}
void set_font(StringView font)
{
// The font IDL attribute, on setting, must be parsed as a CSS <'font'> value (but without supporting property-independent style sheet syntax like 'inherit'),
// and the resulting font must be assigned to the context, with the 'line-height' component forced to 'normal', with the 'font-size' component converted to CSS pixels,
// and with system fonts being computed to explicit values.
// FIXME: with the 'line-height' component forced to 'normal'
// FIXME: with the 'font-size' component converted to CSS pixels
auto parsing_context = CSS::Parser::ParsingContext { reinterpret_cast<IncludingClass&>(*this).realm() };
auto font_style_value_result = parse_css_value(parsing_context, font, CSS::PropertyID::Font);
// If the new value is syntactically incorrect (including using property-independent style sheet syntax like 'inherit' or 'initial'), then it must be ignored, without assigning a new font value.
// NOTE: ShorthandStyleValue should be the only valid option here. We implicitly VERIFY this below.
if (!font_style_value_result || !font_style_value_result->is_shorthand()) {
return;
}
my_drawing_state().font_style_value = font_style_value_result.release_nonnull();
// Load font with font style value properties
auto const& font_style_value = my_drawing_state().font_style_value->as_shorthand();
auto& canvas_element = reinterpret_cast<IncludingClass&>(*this).canvas_element();
auto& font_style = *font_style_value.longhand(CSS::PropertyID::FontStyle);
auto& font_weight = *font_style_value.longhand(CSS::PropertyID::FontWeight);
auto& font_width = *font_style_value.longhand(CSS::PropertyID::FontWidth);
auto& font_size = *font_style_value.longhand(CSS::PropertyID::FontSize);
auto& font_family = *font_style_value.longhand(CSS::PropertyID::FontFamily);
auto font_list = canvas_element.document().style_computer().compute_font_for_style_values(&canvas_element, {}, font_family, font_size, font_style, font_weight, font_width);
my_drawing_state().current_font = font_list->first();
}
Bindings::CanvasTextAlign text_align() const { return my_drawing_state().text_align; }
void set_text_align(Bindings::CanvasTextAlign text_align) { my_drawing_state().text_align = text_align; }
Bindings::CanvasTextBaseline text_baseline() const { return my_drawing_state().text_baseline; }
void set_text_baseline(Bindings::CanvasTextBaseline text_baseline) { my_drawing_state().text_baseline = text_baseline; }
protected:
CanvasTextDrawingStyles() = default;
private:
CanvasState::DrawingState& my_drawing_state() { return reinterpret_cast<IncludingClass&>(*this).drawing_state(); }
CanvasState::DrawingState const& my_drawing_state() const { return reinterpret_cast<IncludingClass const&>(*this).drawing_state(); }
};
}

View file

@ -0,0 +1,24 @@
#import <HTML/CanvasRenderingContext2D.idl>
// https://html.spec.whatwg.org/multipage/canvas.html#canvastextalign
// enum CanvasTextAlign { "start", "end", "left", "right", "center" };
// enum CanvasTextBaseline { "top", "hanging", "middle", "alphabetic", "ideographic", "bottom" };
enum CanvasDirection { "ltr", "rtl", "inherit" };
enum CanvasFontKerning { "auto", "normal", "none" };
enum CanvasFontStretch { "ultra-condensed", "extra-condensed", "condensed", "semi-condensed", "normal", "semi-expanded", "expanded", "extra-expanded", "ultra-expanded" };
enum CanvasFontVariantCaps { "normal", "small-caps", "all-small-caps", "petite-caps", "all-petite-caps", "unicase", "titling-caps" };
enum CanvasTextRendering { "auto", "optimizeSpeed", "optimizeLegibility", "geometricPrecision" };
// https://html.spec.whatwg.org/multipage/canvas.html#canvastextdrawingstyles
interface mixin CanvasTextDrawingStyles {
attribute DOMString font; // (default 10px sans-serif)
attribute CanvasTextAlign textAlign; // (default: "start")
attribute CanvasTextBaseline textBaseline; // (default: "alphabetic")
[FIXME] attribute CanvasDirection direction; // (default: "inherit")
[FIXME] attribute DOMString letterSpacing; // (default: "0px")
[FIXME] attribute CanvasFontKerning fontKerning; // (default: "auto")
[FIXME] attribute CanvasFontStretch fontStretch; // (default: "normal")
[FIXME] attribute CanvasFontVariantCaps fontVariantCaps; // (default: "normal")
[FIXME] attribute CanvasTextRendering textRendering; // (default: "auto")
[FIXME] attribute DOMString wordSpacing; // (default: "0px")
};

View file

@ -0,0 +1,146 @@
/*
* Copyright (c) 2020-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Debug.h>
#include <LibGfx/Painter.h>
#include <LibWeb/Geometry/DOMMatrix.h>
#include <LibWeb/HTML/Canvas/CanvasPath.h>
#include <LibWeb/HTML/Canvas/CanvasState.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/canvas.html#canvastransform
template<typename IncludingClass>
class CanvasTransform {
public:
~CanvasTransform() = default;
Gfx::Path& mutable_path() { return static_cast<IncludingClass&>(*this).path(); }
void scale(float sx, float sy)
{
dbgln_if(CANVAS_RENDERING_CONTEXT_2D_DEBUG, "CanvasTransform::scale({}, {})", sx, sy);
if (!isfinite(sx) || !isfinite(sy))
return;
my_drawing_state().transform.scale(sx, sy);
flush_transform();
mutable_path().transform(Gfx::AffineTransform().scale(1.0 / sx, 1.0 / sy));
}
void translate(float tx, float ty)
{
dbgln_if(CANVAS_RENDERING_CONTEXT_2D_DEBUG, "CanvasTransform::translate({}, {})", tx, ty);
if (!isfinite(tx) || !isfinite(ty))
return;
my_drawing_state().transform.translate(tx, ty);
flush_transform();
mutable_path().transform(Gfx::AffineTransform().translate(-tx, -ty));
}
void rotate(float radians)
{
dbgln_if(CANVAS_RENDERING_CONTEXT_2D_DEBUG, "CanvasTransform::rotate({})", radians);
if (!isfinite(radians))
return;
my_drawing_state().transform.rotate_radians(radians);
flush_transform();
mutable_path().transform(Gfx::AffineTransform().rotate_radians(-radians));
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-transform
void transform(double a, double b, double c, double d, double e, double f)
{
// 1. If any of the arguments are infinite or NaN, then return.
if (!isfinite(a) || !isfinite(b) || !isfinite(c) || !isfinite(d) || !isfinite(e) || !isfinite(f))
return;
// 2. Replace the current transformation matrix with the result of multiplying the current transformation matrix with the matrix described by:
// a c e
// b d f
// 0 0 1
auto transform = Gfx::AffineTransform(a, b, c, d, e, f);
my_drawing_state().transform.multiply(transform);
if (auto inverse = transform.inverse(); inverse.has_value()) {
mutable_path().transform(inverse.value());
}
flush_transform();
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-gettransform
WebIDL::ExceptionOr<JS::NonnullGCPtr<Geometry::DOMMatrix>> get_transform()
{
auto& realm = static_cast<IncludingClass&>(*this).realm();
auto transform = my_drawing_state().transform;
Geometry::DOMMatrix2DInit init = { transform.a(), transform.b(), transform.c(), transform.d(), transform.e(), transform.f(), {}, {}, {}, {}, {}, {} };
return Geometry::DOMMatrix::create_from_dom_matrix_2d_init(realm, init);
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-settransform
void set_transform(double a, double b, double c, double d, double e, double f)
{
// 1. If any of the arguments are infinite or NaN, then return.
if (!isfinite(a) || !isfinite(b) || !isfinite(c) || !isfinite(d) || !isfinite(e) || !isfinite(f))
return;
// 2. Reset the current transformation matrix to the identity matrix.
my_drawing_state().transform = {};
flush_transform();
// 3. Invoke the transform(a, b, c, d, e, f) method with the same arguments.
transform(a, b, c, d, e, f);
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-settransform-matrix
WebIDL::ExceptionOr<void> set_transform(Geometry::DOMMatrix2DInit& init)
{
// 1. Let matrix be the result of creating a DOMMatrix from the 2D dictionary transform.
auto& realm = static_cast<IncludingClass&>(*this).realm();
auto matrix = TRY(Geometry::DOMMatrix::create_from_dom_matrix_2d_init(realm, init));
// 2. If one or more of matrix's m11 element, m12 element, m21 element, m22 element, m41 element, or m42 element are infinite or NaN, then return.
if (!isfinite(matrix->m11()) || !isfinite(matrix->m12()) || !isfinite(matrix->m21()) || !isfinite(matrix->m22()) || !isfinite(matrix->m41()) || !isfinite(matrix->m42()))
return {};
auto original_transform = my_drawing_state().transform;
// 3. Reset the current transformation matrix to matrix.
auto transform = Gfx::AffineTransform { static_cast<float>(matrix->a()), static_cast<float>(matrix->b()), static_cast<float>(matrix->c()), static_cast<float>(matrix->d()), static_cast<float>(matrix->e()), static_cast<float>(matrix->f()) };
my_drawing_state().transform = transform;
mutable_path().transform(original_transform);
flush_transform();
return {};
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-resettransform
void reset_transform()
{
// The resetTransform() method, when invoked, must reset the current transformation matrix to the identity matrix.
my_drawing_state().transform = {};
flush_transform();
}
void flush_transform()
{
if (auto* painter = static_cast<IncludingClass&>(*this).painter())
painter->set_transform(my_drawing_state().transform);
}
protected:
CanvasTransform() = default;
private:
CanvasState::DrawingState& my_drawing_state() { return reinterpret_cast<IncludingClass&>(*this).drawing_state(); }
CanvasState::DrawingState const& my_drawing_state() const { return reinterpret_cast<IncludingClass const&>(*this).drawing_state(); }
};
}

View file

@ -0,0 +1,14 @@
#import <Geometry/DOMMatrix.idl>
// https://html.spec.whatwg.org/multipage/canvas.html#canvastransform
interface mixin CanvasTransform {
undefined scale(unrestricted double x, unrestricted double y);
undefined rotate(unrestricted double radians);
undefined translate(unrestricted double x, unrestricted double y);
undefined transform(unrestricted double a, unrestricted double b, unrestricted double c, unrestricted double d, unrestricted double e, unrestricted double f);
[NewObject] DOMMatrix getTransform();
undefined setTransform(unrestricted double a, unrestricted double b, unrestricted double c, unrestricted double d, unrestricted double e, unrestricted double f);
undefined setTransform(optional DOMMatrix2DInit transform = {});
undefined resetTransform();
};

View file

@ -0,0 +1,5 @@
// https://html.spec.whatwg.org/multipage/canvas.html#canvasuserinterface
interface mixin CanvasUserInterface {
[FIXME] undefined drawFocusIfNeeded(Element element);
[FIXME] undefined drawFocusIfNeeded(Path2D path, Element element);
};

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/QuickSort.h>
#include <LibWeb/Bindings/CanvasGradientPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/CanvasGradient.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(CanvasGradient);
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-createradialgradient
WebIDL::ExceptionOr<JS::NonnullGCPtr<CanvasGradient>> CanvasGradient::create_radial(JS::Realm& realm, double x0, double y0, double r0, double x1, double y1, double r1)
{
// If either of r0 or r1 are negative, then an "IndexSizeError" DOMException must be thrown.
if (r0 < 0)
return WebIDL::IndexSizeError::create(realm, "The r0 passed is less than 0"_string);
if (r1 < 0)
return WebIDL::IndexSizeError::create(realm, "The r1 passed is less than 0"_string);
auto radial_gradient = TRY_OR_THROW_OOM(realm.vm(), Gfx::CanvasRadialGradientPaintStyle::create(Gfx::FloatPoint { x0, y0 }, r0, Gfx::FloatPoint { x1, y1 }, r1));
return realm.heap().allocate<CanvasGradient>(realm, realm, *radial_gradient);
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-createlineargradient
WebIDL::ExceptionOr<JS::NonnullGCPtr<CanvasGradient>> CanvasGradient::create_linear(JS::Realm& realm, double x0, double y0, double x1, double y1)
{
auto linear_gradient = TRY_OR_THROW_OOM(realm.vm(), Gfx::CanvasLinearGradientPaintStyle::create(Gfx::FloatPoint { x0, y0 }, Gfx::FloatPoint { x1, y1 }));
return realm.heap().allocate<CanvasGradient>(realm, realm, *linear_gradient);
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-createconicgradient
WebIDL::ExceptionOr<JS::NonnullGCPtr<CanvasGradient>> CanvasGradient::create_conic(JS::Realm& realm, double start_angle, double x, double y)
{
auto conic_gradient = TRY_OR_THROW_OOM(realm.vm(), Gfx::CanvasConicGradientPaintStyle::create(Gfx::FloatPoint { x, y }, start_angle));
return realm.heap().allocate<CanvasGradient>(realm, realm, *conic_gradient);
}
CanvasGradient::CanvasGradient(JS::Realm& realm, Gfx::GradientPaintStyle& gradient)
: PlatformObject(realm)
, m_gradient(gradient)
{
}
CanvasGradient::~CanvasGradient() = default;
void CanvasGradient::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(CanvasGradient);
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-canvasgradient-addcolorstop
WebIDL::ExceptionOr<void> CanvasGradient::add_color_stop(double offset, StringView color)
{
// 1. If the offset is less than 0 or greater than 1, then throw an "IndexSizeError" DOMException.
if (offset < 0 || offset > 1)
return WebIDL::IndexSizeError::create(realm(), "CanvasGradient color stop offset out of bounds"_string);
// 2. Let parsed color be the result of parsing color.
auto parsed_color = Color::from_string(color);
// 3. If parsed color is failure, throw a "SyntaxError" DOMException.
if (!parsed_color.has_value())
return WebIDL::SyntaxError::create(realm(), "Could not parse color for CanvasGradient"_string);
// 4. Place a new stop on the gradient, at offset offset relative to the whole gradient, and with the color parsed color.
TRY_OR_THROW_OOM(realm().vm(), m_gradient->add_color_stop(offset, parsed_color.value()));
// FIXME: If multiple stops are added at the same offset on a gradient, then they must be placed in the order added,
// with the first one closest to the start of the gradient, and each subsequent one infinitesimally further along
// towards the end point (in effect causing all but the first and last stop added at each point to be ignored).
return {};
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGfx/PaintStyle.h>
#include <LibWeb/Bindings/PlatformObject.h>
namespace Web::HTML {
class CanvasGradient final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(CanvasGradient, Bindings::PlatformObject);
JS_DECLARE_ALLOCATOR(CanvasGradient);
public:
static WebIDL::ExceptionOr<JS::NonnullGCPtr<CanvasGradient>> create_radial(JS::Realm&, double x0, double y0, double r0, double x1, double y1, double r1);
static WebIDL::ExceptionOr<JS::NonnullGCPtr<CanvasGradient>> create_linear(JS::Realm&, double x0, double y0, double x1, double y1);
static WebIDL::ExceptionOr<JS::NonnullGCPtr<CanvasGradient>> create_conic(JS::Realm&, double start_angle, double x, double y);
WebIDL::ExceptionOr<void> add_color_stop(double offset, StringView color);
~CanvasGradient();
NonnullRefPtr<Gfx::PaintStyle> to_gfx_paint_style() { return m_gradient; }
private:
CanvasGradient(JS::Realm&, Gfx::GradientPaintStyle& gradient);
virtual void initialize(JS::Realm&) override;
NonnullRefPtr<Gfx::GradientPaintStyle> m_gradient;
};
}

View file

@ -0,0 +1,6 @@
// https://html.spec.whatwg.org/#canvasgradient
[Exposed=(Window,Worker)]
interface CanvasGradient {
// opaque object
undefined addColorStop(double offset, DOMString color);
};

View file

@ -0,0 +1,153 @@
/*
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Bitmap.h>
#include <LibWeb/Bindings/CanvasPatternPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/CanvasPattern.h>
#include <LibWeb/HTML/CanvasRenderingContext2D.h>
#include <LibWeb/HTML/ImageBitmap.h>
#include <LibWeb/SVG/SVGImageElement.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(CanvasPattern);
void CanvasPatternPaintStyle::paint(Gfx::IntRect physical_bounding_box, PaintFunction paint) const
{
// 1. Create an infinite transparent black bitmap.
// *waves magic wand 🪄*
// Done!
// 2. Place a copy of the image on the bitmap, anchored such that its top left corner
// is at the origin of the coordinate space, with one coordinate space unit per CSS pixel of the image,
// then place repeated copies of this image horizontally to the left and right, if the repetition behavior
// is "repeat-x", or vertically up and down, if the repetition behavior is "repeat-y", or in all four directions
// all over the bitmap, if the repetition behavior is "repeat".
// FIMXE: If the original image data is a bitmap image, then the value painted at a point in the area of
// the repetitions is computed by filtering the original image data. When scaling up, if the imageSmoothingEnabled
// attribute is set to false, then the image must be rendered using nearest-neighbor interpolation.
// Otherwise, the user agent may use any filtering algorithm (for example bilinear interpolation or nearest-neighbor).
// User agents which support multiple filtering algorithms may use the value of the imageSmoothingQuality attribute
// to guide the choice of filtering algorithm. When such a filtering algorithm requires a pixel value from outside
// the original image data, it must instead use the value from wrapping the pixel's coordinates to the original
// image's dimensions. (That is, the filter uses 'repeat' behavior, regardless of the value of the pattern's repetition behavior.)
// FIXME: 3. Transform the resulting bitmap according to the pattern's transformation matrix.
// FIXME: 4. Transform the resulting bitmap again, this time according to the current transformation matrix.
// 5. Replace any part of the image outside the area in which the pattern is to be rendered with transparent black.
// 6. The resulting bitmap is what is to be rendered, with the same origin and same scale.
auto const bitmap_width = m_immutable_bitmap->width();
auto const bitmap_height = m_immutable_bitmap->height();
paint([=, this](auto point) {
point.translate_by(physical_bounding_box.location());
point = [&]() -> Gfx::IntPoint {
switch (m_repetition) {
case Repetition::NoRepeat: {
return point;
}
case Repetition::Repeat: {
return {
point.x() % bitmap_width,
point.y() % bitmap_height
};
}
case Repetition::RepeatX: {
return {
point.x() % bitmap_width,
point.y()
};
}
case Repetition::RepeatY: {
return {
point.x(),
point.y() % bitmap_height
};
}
default:
VERIFY_NOT_REACHED();
}
}();
if (m_immutable_bitmap->rect().contains(point))
return m_immutable_bitmap->get_pixel(point.x(), point.y());
return Gfx::Color();
});
}
CanvasPattern::CanvasPattern(JS::Realm& realm, CanvasPatternPaintStyle& pattern)
: PlatformObject(realm)
, m_pattern(pattern)
{
}
CanvasPattern::~CanvasPattern() = default;
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-createpattern
WebIDL::ExceptionOr<JS::GCPtr<CanvasPattern>> CanvasPattern::create(JS::Realm& realm, CanvasImageSource const& image, StringView repetition)
{
auto parse_repetition = [&](auto repetition) -> Optional<CanvasPatternPaintStyle::Repetition> {
if (repetition == "repeat"sv)
return CanvasPatternPaintStyle::Repetition::Repeat;
if (repetition == "repeat-x"sv)
return CanvasPatternPaintStyle::Repetition::RepeatX;
if (repetition == "repeat-y"sv)
return CanvasPatternPaintStyle::Repetition::RepeatY;
if (repetition == "no-repeat"sv)
return CanvasPatternPaintStyle::Repetition::NoRepeat;
return {};
};
// 1. Let usability be the result of checking the usability of image.
auto usability = TRY(check_usability_of_image(image));
// 2. If usability is bad, then return null.
if (usability == CanvasImageSourceUsability::Bad)
return JS::GCPtr<CanvasPattern> {};
// 3. Assert: usability is good.
VERIFY(usability == CanvasImageSourceUsability::Good);
// 4. If repetition is the empty string, then set it to "repeat".
if (repetition.is_empty())
repetition = "repeat"sv;
// 5. If repetition is not identical to one of "repeat", "repeat-x", "repeat-y", or "no-repeat",
// then throw a "SyntaxError" DOMException.
auto repetition_value = parse_repetition(repetition);
if (!repetition_value.has_value())
return WebIDL::SyntaxError::create(realm, "Repetition value is not valid"_string);
// Note: Bitmap won't be null here, as if it were it would have "bad" usability.
auto bitmap = image.visit(
[](JS::Handle<HTMLImageElement> const& source) -> RefPtr<Gfx::ImmutableBitmap> { return source->immutable_bitmap(); },
[](JS::Handle<SVG::SVGImageElement> const& source) -> RefPtr<Gfx::ImmutableBitmap> { return source->current_image_bitmap(); },
[](JS::Handle<HTMLCanvasElement> const& source) -> RefPtr<Gfx::ImmutableBitmap> { return Gfx::ImmutableBitmap::create_snapshot_from_painting_surface(*source->surface()); },
[](JS::Handle<HTMLVideoElement> const& source) -> RefPtr<Gfx::ImmutableBitmap> { return Gfx::ImmutableBitmap::create(*source->bitmap()); },
[](JS::Handle<ImageBitmap> const& source) -> RefPtr<Gfx::ImmutableBitmap> { return Gfx::ImmutableBitmap::create(*source->bitmap()); });
// 6. Let pattern be a new CanvasPattern object with the image image and the repetition behavior given by repetition.
auto pattern = TRY_OR_THROW_OOM(realm.vm(), CanvasPatternPaintStyle::create(*bitmap, *repetition_value));
// FIXME: 7. If image is not origin-clean, then mark pattern as not origin-clean.
// 8. Return pattern.
return realm.heap().allocate<CanvasPattern>(realm, realm, *pattern);
}
void CanvasPattern::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(CanvasPattern);
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGfx/PaintStyle.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/HTML/Canvas/CanvasDrawImage.h>
namespace Web::HTML {
class CanvasPatternPaintStyle final : public Gfx::PaintStyle {
public:
enum class Repetition {
Repeat,
RepeatX,
RepeatY,
NoRepeat
};
static ErrorOr<NonnullRefPtr<CanvasPatternPaintStyle>> create(Gfx::ImmutableBitmap const& bitmap, Repetition repetition)
{
return adopt_nonnull_ref_or_enomem(new (nothrow) CanvasPatternPaintStyle(bitmap, repetition));
}
virtual void paint(Gfx::IntRect physical_bounding_box, PaintFunction paint) const override;
private:
CanvasPatternPaintStyle(Gfx::ImmutableBitmap const& immutable_bitmap, Repetition repetition)
: m_immutable_bitmap(immutable_bitmap)
, m_repetition(repetition)
{
}
NonnullRefPtr<Gfx::ImmutableBitmap> m_immutable_bitmap;
Repetition m_repetition { Repetition::Repeat };
};
class CanvasPattern final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(CanvasPattern, Bindings::PlatformObject);
JS_DECLARE_ALLOCATOR(CanvasPattern);
public:
static WebIDL::ExceptionOr<JS::GCPtr<CanvasPattern>> create(JS::Realm&, CanvasImageSource const& image, StringView repetition);
~CanvasPattern();
NonnullRefPtr<Gfx::PaintStyle> to_gfx_paint_style() { return m_pattern; }
private:
CanvasPattern(JS::Realm&, CanvasPatternPaintStyle&);
virtual void initialize(JS::Realm&) override;
NonnullRefPtr<CanvasPatternPaintStyle> m_pattern;
};
}

View file

@ -0,0 +1,6 @@
// https://html.spec.whatwg.org/#canvaspattern
[Exposed=(Window,Worker)]
interface CanvasPattern {
// opaque object
[FIXME] undefined setTransform(optional DOMMatrix2DInit transform = {});
};

View file

@ -0,0 +1,837 @@
/*
* Copyright (c) 2020-2024, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2023, MacDue <macdue@dueutil.tech>
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/OwnPtr.h>
#include <LibGfx/DeprecatedPainter.h>
#include <LibGfx/PainterSkia.h>
#include <LibGfx/Quad.h>
#include <LibGfx/Rect.h>
#include <LibUnicode/Segmenter.h>
#include <LibWeb/Bindings/CanvasRenderingContext2DPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/CanvasRenderingContext2D.h>
#include <LibWeb/HTML/HTMLCanvasElement.h>
#include <LibWeb/HTML/HTMLImageElement.h>
#include <LibWeb/HTML/ImageBitmap.h>
#include <LibWeb/HTML/ImageData.h>
#include <LibWeb/HTML/Path2D.h>
#include <LibWeb/HTML/TextMetrics.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Platform/FontPlugin.h>
#include <LibWeb/SVG/SVGImageElement.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(CanvasRenderingContext2D);
JS::NonnullGCPtr<CanvasRenderingContext2D> CanvasRenderingContext2D::create(JS::Realm& realm, HTMLCanvasElement& element)
{
return realm.heap().allocate<CanvasRenderingContext2D>(realm, realm, element);
}
CanvasRenderingContext2D::CanvasRenderingContext2D(JS::Realm& realm, HTMLCanvasElement& element)
: PlatformObject(realm)
, CanvasPath(static_cast<Bindings::PlatformObject&>(*this), *this)
, m_element(element)
{
}
CanvasRenderingContext2D::~CanvasRenderingContext2D() = default;
void CanvasRenderingContext2D::initialize(JS::Realm& realm)
{
Base::initialize(realm);
set_prototype(&Bindings::ensure_web_prototype<Bindings::CanvasRenderingContext2DPrototype>(realm, "CanvasRenderingContext2D"_string));
}
void CanvasRenderingContext2D::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_element);
}
HTMLCanvasElement& CanvasRenderingContext2D::canvas_element()
{
return *m_element;
}
HTMLCanvasElement const& CanvasRenderingContext2D::canvas_element() const
{
return *m_element;
}
JS::NonnullGCPtr<HTMLCanvasElement> CanvasRenderingContext2D::canvas_for_binding() const
{
return *m_element;
}
Gfx::Path CanvasRenderingContext2D::rect_path(float x, float y, float width, float height)
{
auto top_left = Gfx::FloatPoint(x, y);
auto top_right = Gfx::FloatPoint(x + width, y);
auto bottom_left = Gfx::FloatPoint(x, y + height);
auto bottom_right = Gfx::FloatPoint(x + width, y + height);
Gfx::Path path;
path.move_to(top_left);
path.line_to(top_right);
path.line_to(bottom_right);
path.line_to(bottom_left);
path.line_to(top_left);
return path;
}
void CanvasRenderingContext2D::fill_rect(float x, float y, float width, float height)
{
fill_internal(rect_path(x, y, width, height), Gfx::WindingRule::EvenOdd);
}
void CanvasRenderingContext2D::clear_rect(float x, float y, float width, float height)
{
if (auto* painter = this->painter()) {
auto rect = Gfx::FloatRect(x, y, width, height);
painter->clear_rect(rect, Color::Transparent);
did_draw(rect);
}
}
void CanvasRenderingContext2D::stroke_rect(float x, float y, float width, float height)
{
stroke_internal(rect_path(x, y, width, height));
}
// 4.12.5.1.14 Drawing images, https://html.spec.whatwg.org/multipage/canvas.html#drawing-images
WebIDL::ExceptionOr<void> CanvasRenderingContext2D::draw_image_internal(CanvasImageSource const& image, float source_x, float source_y, float source_width, float source_height, float destination_x, float destination_y, float destination_width, float destination_height)
{
// 1. If any of the arguments are infinite or NaN, then return.
if (!isfinite(source_x) || !isfinite(source_y) || !isfinite(source_width) || !isfinite(source_height) || !isfinite(destination_x) || !isfinite(destination_y) || !isfinite(destination_width) || !isfinite(destination_height))
return {};
// 2. Let usability be the result of checking the usability of image.
auto usability = TRY(check_usability_of_image(image));
// 3. If usability is bad, then return (without drawing anything).
if (usability == CanvasImageSourceUsability::Bad)
return {};
auto bitmap = image.visit(
[](JS::Handle<HTMLImageElement> const& source) -> RefPtr<Gfx::ImmutableBitmap> {
return source->immutable_bitmap();
},
[](JS::Handle<SVG::SVGImageElement> const& source) -> RefPtr<Gfx::ImmutableBitmap> {
return source->current_image_bitmap();
},
[](JS::Handle<HTMLCanvasElement> const& source) -> RefPtr<Gfx::ImmutableBitmap> {
auto surface = source->surface();
if (!surface)
return {};
return Gfx::ImmutableBitmap::create_snapshot_from_painting_surface(*surface);
},
[](JS::Handle<HTMLVideoElement> const& source) -> RefPtr<Gfx::ImmutableBitmap> { return Gfx::ImmutableBitmap::create(*source->bitmap()); },
[](JS::Handle<ImageBitmap> const& source) -> RefPtr<Gfx::ImmutableBitmap> {
return Gfx::ImmutableBitmap::create(*source->bitmap());
});
if (!bitmap)
return {};
// 4. Establish the source and destination rectangles as follows:
// If not specified, the dw and dh arguments must default to the values of sw and sh, interpreted such that one CSS pixel in the image is treated as one unit in the output bitmap's coordinate space.
// If the sx, sy, sw, and sh arguments are omitted, then they must default to 0, 0, the image's intrinsic width in image pixels, and the image's intrinsic height in image pixels, respectively.
// If the image has no intrinsic dimensions, then the concrete object size must be used instead, as determined using the CSS "Concrete Object Size Resolution" algorithm, with the specified size having
// neither a definite width nor height, nor any additional constraints, the object's intrinsic properties being those of the image argument, and the default object size being the size of the output bitmap.
// The source rectangle is the rectangle whose corners are the four points (sx, sy), (sx+sw, sy), (sx+sw, sy+sh), (sx, sy+sh).
// The destination rectangle is the rectangle whose corners are the four points (dx, dy), (dx+dw, dy), (dx+dw, dy+dh), (dx, dy+dh).
// NOTE: Implemented in drawImage() overloads
// The source rectangle is the rectangle whose corners are the four points (sx, sy), (sx+sw, sy), (sx+sw, sy+sh), (sx, sy+sh).
auto source_rect = Gfx::FloatRect { source_x, source_y, source_width, source_height };
// The destination rectangle is the rectangle whose corners are the four points (dx, dy), (dx+dw, dy), (dx+dw, dy+dh), (dx, dy+dh).
auto destination_rect = Gfx::FloatRect { destination_x, destination_y, destination_width, destination_height };
// When the source rectangle is outside the source image, the source rectangle must be clipped
// to the source image and the destination rectangle must be clipped in the same proportion.
auto clipped_source = source_rect.intersected(bitmap->rect().to_type<float>());
auto clipped_destination = destination_rect;
if (clipped_source != source_rect) {
clipped_destination.set_width(clipped_destination.width() * (clipped_source.width() / source_rect.width()));
clipped_destination.set_height(clipped_destination.height() * (clipped_source.height() / source_rect.height()));
}
// 5. If one of the sw or sh arguments is zero, then return. Nothing is painted.
if (source_width == 0 || source_height == 0)
return {};
// 6. Paint the region of the image argument specified by the source rectangle on the region of the rendering context's output bitmap specified by the destination rectangle, after applying the current transformation matrix to the destination rectangle.
auto scaling_mode = Gfx::ScalingMode::NearestNeighbor;
if (drawing_state().image_smoothing_enabled) {
// FIXME: Honor drawing_state().image_smoothing_quality
scaling_mode = Gfx::ScalingMode::BilinearBlend;
}
if (auto* painter = this->painter()) {
painter->draw_bitmap(destination_rect, *bitmap, source_rect.to_rounded<int>(), scaling_mode, drawing_state().global_alpha);
did_draw(destination_rect);
}
// 7. If image is not origin-clean, then set the CanvasRenderingContext2D's origin-clean flag to false.
if (image_is_not_origin_clean(image))
m_origin_clean = false;
return {};
}
void CanvasRenderingContext2D::did_draw(Gfx::FloatRect const&)
{
// FIXME: Make use of the rect to reduce the invalidated area when possible.
if (!canvas_element().paintable())
return;
canvas_element().paintable()->set_needs_display(InvalidateDisplayList::No);
}
Gfx::Painter* CanvasRenderingContext2D::painter()
{
if (!canvas_element().surface()) {
if (!canvas_element().allocate_painting_surface())
return nullptr;
canvas_element().document().invalidate_display_list();
m_painter = make<Gfx::PainterSkia>(*canvas_element().surface());
}
return m_painter.ptr();
}
Gfx::Path CanvasRenderingContext2D::text_path(StringView text, float x, float y, Optional<double> max_width)
{
if (max_width.has_value() && max_width.value() <= 0)
return {};
auto& drawing_state = this->drawing_state();
auto font = current_font();
Gfx::Path path;
path.move_to({ x, y });
path.text(Utf8View { text }, *font);
auto text_width = path.bounding_box().width();
Gfx::AffineTransform transform = {};
// https://html.spec.whatwg.org/multipage/canvas.html#text-preparation-algorithm:
// 6. If maxWidth was provided and the hypothetical width of the inline box in the hypothetical line box
// is greater than maxWidth CSS pixels, then change font to have a more condensed font (if one is
// available or if a reasonably readable one can be synthesized by applying a horizontal scale
// factor to the font) or a smaller font, and return to the previous step.
if (max_width.has_value() && text_width > float(*max_width)) {
auto horizontal_scale = float(*max_width) / text_width;
transform = Gfx::AffineTransform {}.scale({ horizontal_scale, 1 });
text_width *= horizontal_scale;
}
// Apply text align
// FIXME: CanvasTextAlign::Start and CanvasTextAlign::End currently do not nothing for right-to-left languages:
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-textalign-start
// Default alignment of draw_text is left so do nothing by CanvasTextAlign::Start and CanvasTextAlign::Left
if (drawing_state.text_align == Bindings::CanvasTextAlign::Center) {
transform = Gfx::AffineTransform {}.set_translation({ -text_width / 2, 0 }).multiply(transform);
}
if (drawing_state.text_align == Bindings::CanvasTextAlign::End || drawing_state.text_align == Bindings::CanvasTextAlign::Right) {
transform = Gfx::AffineTransform {}.set_translation({ -text_width, 0 }).multiply(transform);
}
// Apply text baseline
// FIXME: Implement CanvasTextBasline::Hanging, Bindings::CanvasTextAlign::Alphabetic and Bindings::CanvasTextAlign::Ideographic for real
// right now they are just handled as textBaseline = top or bottom.
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-textbaseline-hanging
// Default baseline of draw_text is top so do nothing by CanvasTextBaseline::Top and CanvasTextBasline::Hanging
if (drawing_state.text_baseline == Bindings::CanvasTextBaseline::Middle) {
transform = Gfx::AffineTransform {}.set_translation({ 0, font->pixel_size() / 2 }).multiply(transform);
}
if (drawing_state.text_baseline == Bindings::CanvasTextBaseline::Top || drawing_state.text_baseline == Bindings::CanvasTextBaseline::Hanging) {
transform = Gfx::AffineTransform {}.set_translation({ 0, font->pixel_size() }).multiply(transform);
}
return path.copy_transformed(transform);
}
void CanvasRenderingContext2D::fill_text(StringView text, float x, float y, Optional<double> max_width)
{
fill_internal(text_path(text, x, y, max_width), Gfx::WindingRule::Nonzero);
}
void CanvasRenderingContext2D::stroke_text(StringView text, float x, float y, Optional<double> max_width)
{
stroke_internal(text_path(text, x, y, max_width));
}
void CanvasRenderingContext2D::begin_path()
{
path().clear();
}
void CanvasRenderingContext2D::stroke_internal(Gfx::Path const& path)
{
auto* painter = this->painter();
if (!painter)
return;
paint_shadow_for_stroke_internal(path);
auto& state = drawing_state();
// FIXME: Honor state's line_cap, line_join, miter_limit, dash_list, and line_dash_offset.
if (auto color = state.stroke_style.as_color(); color.has_value()) {
painter->stroke_path(path, color->with_opacity(state.global_alpha), state.line_width);
} else {
painter->stroke_path(path, state.stroke_style.to_gfx_paint_style(), state.line_width, state.global_alpha);
}
did_draw(path.bounding_box());
}
void CanvasRenderingContext2D::stroke()
{
stroke_internal(path());
}
void CanvasRenderingContext2D::stroke(Path2D const& path)
{
stroke_internal(path.path());
}
static Gfx::WindingRule parse_fill_rule(StringView fill_rule)
{
if (fill_rule == "evenodd"sv)
return Gfx::WindingRule::EvenOdd;
if (fill_rule == "nonzero"sv)
return Gfx::WindingRule::Nonzero;
dbgln("Unrecognized fillRule for CRC2D.fill() - this problem goes away once we pass an enum instead of a string");
return Gfx::WindingRule::Nonzero;
}
void CanvasRenderingContext2D::fill_internal(Gfx::Path const& path, Gfx::WindingRule winding_rule)
{
auto* painter = this->painter();
if (!painter)
return;
paint_shadow_for_fill_internal(path, winding_rule);
auto path_to_fill = path;
path_to_fill.close_all_subpaths();
auto& state = this->drawing_state();
if (auto color = state.fill_style.as_color(); color.has_value()) {
painter->fill_path(path_to_fill, color->with_opacity(state.global_alpha), winding_rule);
} else {
painter->fill_path(path_to_fill, state.fill_style.to_gfx_paint_style(), state.global_alpha, winding_rule);
}
did_draw(path_to_fill.bounding_box());
}
void CanvasRenderingContext2D::fill(StringView fill_rule)
{
fill_internal(path(), parse_fill_rule(fill_rule));
}
void CanvasRenderingContext2D::fill(Path2D& path, StringView fill_rule)
{
fill_internal(path.path(), parse_fill_rule(fill_rule));
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-createimagedata
WebIDL::ExceptionOr<JS::NonnullGCPtr<ImageData>> CanvasRenderingContext2D::create_image_data(int width, int height, Optional<ImageDataSettings> const& settings) const
{
// 1. If one or both of sw and sh are zero, then throw an "IndexSizeError" DOMException.
if (width == 0 || height == 0)
return WebIDL::IndexSizeError::create(realm(), "Width and height must not be zero"_string);
int abs_width = abs(width);
int abs_height = abs(height);
// 2. Let newImageData be a new ImageData object.
// 3. Initialize newImageData given the absolute magnitude of sw, the absolute magnitude of sh, settings set to settings, and defaultColorSpace set to this's color space.
auto image_data = TRY(ImageData::create(realm(), abs_width, abs_height, settings));
// 4. Initialize the image data of newImageData to transparent black.
// ... this is handled by ImageData::create()
// 5. Return newImageData.
return image_data;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-createimagedata-imagedata
WebIDL::ExceptionOr<JS::NonnullGCPtr<ImageData>> CanvasRenderingContext2D::create_image_data(ImageData const& image_data) const
{
// 1. Let newImageData be a new ImageData object.
// 2. Initialize newImageData given the value of imagedata's width attribute, the value of imagedata's height attribute, and defaultColorSpace set to the value of imagedata's colorSpace attribute.
// FIXME: Set defaultColorSpace to the value of image_data's colorSpace attribute
// 3. Initialize the image data of newImageData to transparent black.
// NOTE: No-op, already done during creation.
// 4. Return newImageData.
return TRY(ImageData::create(realm(), image_data.width(), image_data.height()));
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-getimagedata
WebIDL::ExceptionOr<JS::GCPtr<ImageData>> CanvasRenderingContext2D::get_image_data(int x, int y, int width, int height, Optional<ImageDataSettings> const& settings) const
{
// 1. If either the sw or sh arguments are zero, then throw an "IndexSizeError" DOMException.
if (width == 0 || height == 0)
return WebIDL::IndexSizeError::create(realm(), "Width and height must not be zero"_string);
// 2. If the CanvasRenderingContext2D's origin-clean flag is set to false, then throw a "SecurityError" DOMException.
if (!m_origin_clean)
return WebIDL::SecurityError::create(realm(), "CanvasRenderingContext2D is not origin-clean"_string);
// ImageData initialization requires positive width and height
// https://html.spec.whatwg.org/multipage/canvas.html#initialize-an-imagedata-object
int abs_width = abs(width);
int abs_height = abs(height);
// 3. Let imageData be a new ImageData object.
// 4. Initialize imageData given sw, sh, settings set to settings, and defaultColorSpace set to this's color space.
auto image_data = TRY(ImageData::create(realm(), abs_width, abs_height, settings));
// NOTE: We don't attempt to create the underlying bitmap here; if it doesn't exist, it's like copying only transparent black pixels (which is a no-op).
if (!canvas_element().surface())
return image_data;
auto const snapshot = Gfx::ImmutableBitmap::create_snapshot_from_painting_surface(*canvas_element().surface());
// 5. Let the source rectangle be the rectangle whose corners are the four points (sx, sy), (sx+sw, sy), (sx+sw, sy+sh), (sx, sy+sh).
auto source_rect = Gfx::Rect { x, y, abs_width, abs_height };
// NOTE: The spec doesn't seem to define this behavior, but MDN does and the WPT tests
// assume it works this way.
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData#sw
if (width < 0 || height < 0) {
source_rect = source_rect.translated(min(width, 0), min(height, 0));
}
auto source_rect_intersected = source_rect.intersected(snapshot->rect());
// 6. Set the pixel values of imageData to be the pixels of this's output bitmap in the area specified by the source rectangle in the bitmap's coordinate space units, converted from this's color space to imageData's colorSpace using 'relative-colorimetric' rendering intent.
// NOTE: Internally we must use premultiplied alpha, but ImageData should hold unpremultiplied alpha. This conversion
// might result in a loss of precision, but is according to spec.
// See: https://html.spec.whatwg.org/multipage/canvas.html#premultiplied-alpha-and-the-2d-rendering-context
ASSERT(snapshot->alpha_type() == Gfx::AlphaType::Premultiplied);
ASSERT(image_data->bitmap().alpha_type() == Gfx::AlphaType::Unpremultiplied);
auto painter = Gfx::Painter::create(image_data->bitmap());
painter->draw_bitmap(image_data->bitmap().rect().to_type<float>(), *snapshot, source_rect_intersected, Gfx::ScalingMode::NearestNeighbor, drawing_state().global_alpha);
// 7. Set the pixels values of imageData for areas of the source rectangle that are outside of the output bitmap to transparent black.
// NOTE: No-op, already done during creation.
// 8. Return imageData.
return image_data;
}
void CanvasRenderingContext2D::put_image_data(ImageData const& image_data, float x, float y)
{
if (auto* painter = this->painter()) {
auto dst_rect = Gfx::FloatRect(x, y, image_data.width(), image_data.height());
painter->draw_bitmap(dst_rect, Gfx::ImmutableBitmap::create(image_data.bitmap()), image_data.bitmap().rect(), Gfx::ScalingMode::NearestNeighbor, 1.0f);
did_draw(dst_rect);
}
}
// https://html.spec.whatwg.org/multipage/canvas.html#reset-the-rendering-context-to-its-default-state
void CanvasRenderingContext2D::reset_to_default_state()
{
auto surface = canvas_element().surface();
// 1. Clear canvas's bitmap to transparent black.
if (surface) {
painter()->clear_rect(surface->rect().to_type<float>(), Color::Transparent);
}
// 2. Empty the list of subpaths in context's current default path.
path().clear();
// 3. Clear the context's drawing state stack.
clear_drawing_state_stack();
// 4. Reset everything that drawing state consists of to their initial values.
reset_drawing_state();
if (surface)
did_draw(surface->rect().to_type<float>());
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-measuretext
JS::NonnullGCPtr<TextMetrics> CanvasRenderingContext2D::measure_text(StringView text)
{
// The measureText(text) method steps are to run the text preparation
// algorithm, passing it text and the object implementing the CanvasText
// interface, and then using the returned inline box must return a new
// TextMetrics object with members behaving as described in the following
// list:
auto prepared_text = prepare_text(text);
auto metrics = TextMetrics::create(realm());
// FIXME: Use the font that was used to create the glyphs in prepared_text.
auto font = current_font();
// width attribute: The width of that inline box, in CSS pixels. (The text's advance width.)
metrics->set_width(prepared_text.bounding_box.width());
// actualBoundingBoxLeft attribute: The distance parallel to the baseline from the alignment point given by the textAlign attribute to the left side of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going left from the given alignment point.
metrics->set_actual_bounding_box_left(-prepared_text.bounding_box.left());
// actualBoundingBoxRight attribute: The distance parallel to the baseline from the alignment point given by the textAlign attribute to the right side of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going right from the given alignment point.
metrics->set_actual_bounding_box_right(prepared_text.bounding_box.right());
// fontBoundingBoxAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the ascent metric of the first available font, in CSS pixels; positive numbers indicating a distance going up from the given baseline.
metrics->set_font_bounding_box_ascent(font->baseline());
// fontBoundingBoxDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the descent metric of the first available font, in CSS pixels; positive numbers indicating a distance going down from the given baseline.
metrics->set_font_bounding_box_descent(prepared_text.bounding_box.height() - font->baseline());
// actualBoundingBoxAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the top of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going up from the given baseline.
metrics->set_actual_bounding_box_ascent(font->baseline());
// actualBoundingBoxDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the bottom of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going down from the given baseline.
metrics->set_actual_bounding_box_descent(prepared_text.bounding_box.height() - font->baseline());
// emHeightAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the highest top of the em squares in the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the top of that em square (so this value will usually be positive). Zero if the given baseline is the top of that em square; half the font size if the given baseline is the middle of that em square.
metrics->set_em_height_ascent(font->baseline());
// emHeightDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the lowest bottom of the em squares in the inline box, in CSS pixels; positive numbers indicating that the given baseline is above the bottom of that em square. (Zero if the given baseline is the bottom of that em square.)
metrics->set_em_height_descent(prepared_text.bounding_box.height() - font->baseline());
// hangingBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the hanging baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the hanging baseline. (Zero if the given baseline is the hanging baseline.)
metrics->set_hanging_baseline(font->baseline());
// alphabeticBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the alphabetic baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the alphabetic baseline. (Zero if the given baseline is the alphabetic baseline.)
metrics->set_font_bounding_box_ascent(0);
// ideographicBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the ideographic-under baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the ideographic-under baseline. (Zero if the given baseline is the ideographic-under baseline.)
metrics->set_font_bounding_box_ascent(0);
return metrics;
}
RefPtr<Gfx::Font const> CanvasRenderingContext2D::current_font()
{
// When font style value is empty load default font
if (!drawing_state().font_style_value) {
set_font("10px sans-serif"sv);
}
// Get current loaded font
return drawing_state().current_font;
}
// https://html.spec.whatwg.org/multipage/canvas.html#text-preparation-algorithm
CanvasRenderingContext2D::PreparedText CanvasRenderingContext2D::prepare_text(ByteString const& text, float max_width)
{
// 1. If maxWidth was provided but is less than or equal to zero or equal to NaN, then return an empty array.
if (max_width <= 0 || max_width != max_width) {
return {};
}
// 2. Replace all ASCII whitespace in text with U+0020 SPACE characters.
StringBuilder builder { text.length() };
for (auto c : text) {
builder.append(Infra::is_ascii_whitespace(c) ? ' ' : c);
}
auto replaced_text = MUST(builder.to_string());
// 3. Let font be the current font of target, as given by that object's font attribute.
auto font = current_font();
// 4. Apply the appropriate step from the following list to determine the value of direction:
// 4.1. If the target object's direction attribute has the value "ltr": Let direction be 'ltr'.
// 4.2. If the target object's direction attribute has the value "rtl": Let direction be 'rtl'.
// 4.3. If the target object's font style source object is an element: Let direction be the directionality of the target object's font style source object.
// 4.4. If the target object's font style source object is a Document with a non-null document element: Let direction be the directionality of the target object's font style source object's document element.
// 4.5. Otherwise: Let direction be 'ltr'.
// FIXME: Once we have CanvasTextDrawingStyles, implement directionality.
// 5. Form a hypothetical infinitely-wide CSS line box containing a single inline box containing the text text, with its CSS properties set as follows:
// 'direction' -> direction
// 'font' -> font
// 'font-kerning' -> target's fontKerning
// 'font-stretch' -> target's fontStretch
// 'font-variant-caps' -> target's fontVariantCaps
// 'letter-spacing' -> target's letterSpacing
// SVG text-rendering -> target's textRendering
// 'white-space' -> 'pre'
// 'word-spacing' -> target's wordSpacing
// ...and with all other properties set to their initial values.
// FIXME: Actually use a LineBox here instead of, you know, using the default font and measuring its size (which is not the spec at all).
// FIXME: Once we have CanvasTextDrawingStyles, add the CSS attributes.
size_t height = font->pixel_size();
// 6. If maxWidth was provided and the hypothetical width of the inline box in the hypothetical line box is greater than maxWidth CSS pixels, then change font to have a more condensed font (if one is available or if a reasonably readable one can be synthesized by applying a horizontal scale factor to the font) or a smaller font, and return to the previous step.
// FIXME: Record the font size used for this piece of text, and actually retry with a smaller size if needed.
// 7. The anchor point is a point on the inline box, and the physical alignment is one of the values left, right, and center. These variables are determined by the textAlign and textBaseline values as follows:
// Horizontal position:
// 7.1. If textAlign is left, if textAlign is start and direction is 'ltr' or if textAlign is end and direction is 'rtl': Let the anchor point's horizontal position be the left edge of the inline box, and let physical alignment be left.
// 7.2. If textAlign is right, if textAlign is end and direction is 'ltr' or if textAlign is start and direction is 'rtl': Let the anchor point's horizontal position be the right edge of the inline box, and let physical alignment be right.
// 7.3. If textAlign is center: Let the anchor point's horizontal position be half way between the left and right edges of the inline box, and let physical alignment be center.
// Vertical position:
// 7.4. If textBaseline is top: Let the anchor point's vertical position be the top of the em box of the first available font of the inline box.
// 7.5. If textBaseline is hanging: Let the anchor point's vertical position be the hanging baseline of the first available font of the inline box.
// 7.6. If textBaseline is middle: Let the anchor point's vertical position be half way between the bottom and the top of the em box of the first available font of the inline box.
// 7.7. If textBaseline is alphabetic: Let the anchor point's vertical position be the alphabetic baseline of the first available font of the inline box.
// 7.8. If textBaseline is ideographic: Let the anchor point's vertical position be the ideographic-under baseline of the first available font of the inline box.
// 7.9. If textBaseline is bottom: Let the anchor point's vertical position be the bottom of the em box of the first available font of the inline box.
// FIXME: Once we have CanvasTextDrawingStyles, handle the alignment and baseline.
Gfx::FloatPoint anchor { 0, 0 };
auto physical_alignment = Gfx::TextAlignment::CenterLeft;
auto glyph_run = Gfx::shape_text(anchor, 0, replaced_text.code_points(), *font, Gfx::GlyphRun::TextType::Ltr);
// 8. Let result be an array constructed by iterating over each glyph in the inline box from left to right (if any), adding to the array, for each glyph, the shape of the glyph as it is in the inline box, positioned on a coordinate space using CSS pixels with its origin is at the anchor point.
PreparedText prepared_text { glyph_run, physical_alignment, { 0, 0, static_cast<int>(glyph_run->width()), static_cast<int>(height) } };
// 9. Return result, physical alignment, and the inline box.
return prepared_text;
}
void CanvasRenderingContext2D::clip_internal(Gfx::Path& path, Gfx::WindingRule winding_rule)
{
auto* painter = this->painter();
if (!painter)
return;
path.close_all_subpaths();
painter->clip(path, winding_rule);
}
void CanvasRenderingContext2D::clip(StringView fill_rule)
{
clip_internal(path(), parse_fill_rule(fill_rule));
}
void CanvasRenderingContext2D::clip(Path2D& path, StringView fill_rule)
{
clip_internal(path.path(), parse_fill_rule(fill_rule));
}
static bool is_point_in_path_internal(Gfx::Path path, double x, double y, StringView fill_rule)
{
return path.contains(Gfx::FloatPoint(x, y), parse_fill_rule(fill_rule));
}
bool CanvasRenderingContext2D::is_point_in_path(double x, double y, StringView fill_rule)
{
return is_point_in_path_internal(path(), x, y, fill_rule);
}
bool CanvasRenderingContext2D::is_point_in_path(Path2D const& path, double x, double y, StringView fill_rule)
{
return is_point_in_path_internal(path.path(), x, y, fill_rule);
}
// https://html.spec.whatwg.org/multipage/canvas.html#check-the-usability-of-the-image-argument
WebIDL::ExceptionOr<CanvasImageSourceUsability> check_usability_of_image(CanvasImageSource const& image)
{
// 1. Switch on image:
auto usability = TRY(image.visit(
// HTMLOrSVGImageElement
[](JS::Handle<HTMLImageElement> const& image_element) -> WebIDL::ExceptionOr<Optional<CanvasImageSourceUsability>> {
// FIXME: If image's current request's state is broken, then throw an "InvalidStateError" DOMException.
// If image is not fully decodable, then return bad.
if (!image_element->immutable_bitmap())
return { CanvasImageSourceUsability::Bad };
// If image has an intrinsic width or intrinsic height (or both) equal to zero, then return bad.
if (image_element->immutable_bitmap()->width() == 0 || image_element->immutable_bitmap()->height() == 0)
return { CanvasImageSourceUsability::Bad };
return Optional<CanvasImageSourceUsability> {};
},
// FIXME: Don't duplicate this for HTMLImageElement and SVGImageElement.
[](JS::Handle<SVG::SVGImageElement> const& image_element) -> WebIDL::ExceptionOr<Optional<CanvasImageSourceUsability>> {
// FIXME: If image's current request's state is broken, then throw an "InvalidStateError" DOMException.
// If image is not fully decodable, then return bad.
if (!image_element->current_image_bitmap())
return { CanvasImageSourceUsability::Bad };
// If image has an intrinsic width or intrinsic height (or both) equal to zero, then return bad.
if (image_element->current_image_bitmap()->width() == 0 || image_element->current_image_bitmap()->height() == 0)
return { CanvasImageSourceUsability::Bad };
return Optional<CanvasImageSourceUsability> {};
},
[](JS::Handle<HTML::HTMLVideoElement> const& video_element) -> WebIDL::ExceptionOr<Optional<CanvasImageSourceUsability>> {
// If image's readyState attribute is either HAVE_NOTHING or HAVE_METADATA, then return bad.
if (video_element->ready_state() == HTML::HTMLMediaElement::ReadyState::HaveNothing || video_element->ready_state() == HTML::HTMLMediaElement::ReadyState::HaveMetadata) {
return { CanvasImageSourceUsability::Bad };
}
return Optional<CanvasImageSourceUsability> {};
},
// HTMLCanvasElement
// FIXME: OffscreenCanvas
[](JS::Handle<HTMLCanvasElement> const& canvas_element) -> WebIDL::ExceptionOr<Optional<CanvasImageSourceUsability>> {
// If image has either a horizontal dimension or a vertical dimension equal to zero, then throw an "InvalidStateError" DOMException.
if (canvas_element->width() == 0 || canvas_element->height() == 0)
return WebIDL::InvalidStateError::create(canvas_element->realm(), "Canvas width or height is zero"_string);
return Optional<CanvasImageSourceUsability> {};
},
// ImageBitmap
// FIXME: VideoFrame
[](JS::Handle<ImageBitmap> const& image_bitmap) -> WebIDL::ExceptionOr<Optional<CanvasImageSourceUsability>> {
if (image_bitmap->is_detached())
return WebIDL::InvalidStateError::create(image_bitmap->realm(), "Image bitmap is detached"_string);
return Optional<CanvasImageSourceUsability> {};
}));
if (usability.has_value())
return usability.release_value();
// 2. Return good.
return { CanvasImageSourceUsability::Good };
}
// https://html.spec.whatwg.org/multipage/canvas.html#the-image-argument-is-not-origin-clean
bool image_is_not_origin_clean(CanvasImageSource const& image)
{
// An object image is not origin-clean if, switching on image's type:
return image.visit(
// HTMLOrSVGImageElement
[](JS::Handle<HTMLImageElement> const&) {
// FIXME: image's current request's image data is CORS-cross-origin.
return false;
},
[](JS::Handle<SVG::SVGImageElement> const&) {
// FIXME: image's current request's image data is CORS-cross-origin.
return false;
},
[](JS::Handle<HTML::HTMLVideoElement> const&) {
// FIXME: image's media data is CORS-cross-origin.
return false;
},
// HTMLCanvasElement
[](OneOf<JS::Handle<HTMLCanvasElement>, JS::Handle<ImageBitmap>> auto const&) {
// FIXME: image's bitmap's origin-clean flag is false.
return false;
});
}
bool CanvasRenderingContext2D::image_smoothing_enabled() const
{
return drawing_state().image_smoothing_enabled;
}
void CanvasRenderingContext2D::set_image_smoothing_enabled(bool enabled)
{
drawing_state().image_smoothing_enabled = enabled;
}
Bindings::ImageSmoothingQuality CanvasRenderingContext2D::image_smoothing_quality() const
{
return drawing_state().image_smoothing_quality;
}
void CanvasRenderingContext2D::set_image_smoothing_quality(Bindings::ImageSmoothingQuality quality)
{
drawing_state().image_smoothing_quality = quality;
}
float CanvasRenderingContext2D::global_alpha() const
{
return drawing_state().global_alpha;
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-globalalpha
void CanvasRenderingContext2D::set_global_alpha(float alpha)
{
// 1. If the given value is either infinite, NaN, or not in the range 0.0 to 1.0, then return.
if (!isfinite(alpha) || alpha < 0.0f || alpha > 1.0f) {
return;
}
// 2. Otherwise, set this's global alpha to the given value.
drawing_state().global_alpha = alpha;
}
float CanvasRenderingContext2D::shadow_offset_x() const
{
return drawing_state().shadow_offset_x;
}
void CanvasRenderingContext2D::set_shadow_offset_x(float offsetX)
{
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-shadowoffsetx
drawing_state().shadow_offset_x = offsetX;
}
float CanvasRenderingContext2D::shadow_offset_y() const
{
return drawing_state().shadow_offset_y;
}
void CanvasRenderingContext2D::set_shadow_offset_y(float offsetY)
{
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-shadowoffsety
drawing_state().shadow_offset_y = offsetY;
}
String CanvasRenderingContext2D::shadow_color() const
{
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-shadowcolor
return drawing_state().shadow_color.to_string(Gfx::Color::HTMLCompatibleSerialization::Yes);
}
void CanvasRenderingContext2D::set_shadow_color(String color)
{
auto& realm = static_cast<CanvasRenderingContext2D&>(*this).realm();
// 1. Let context be this's canvas attribute's value, if that is an element; otherwise null.
auto parser = CSS::Parser::Parser::create(CSS::Parser::ParsingContext(realm), color);
auto style_value = parser.parse_as_css_value(CSS::PropertyID::Color);
// 2. Let parsedValue be the result of parsing the given value with context if non-null.
if (style_value && style_value->has_color()) {
auto parsedValue = style_value->to_color(OptionalNone());
// 4. Set this's shadow color to parsedValue.
drawing_state().shadow_color = parsedValue;
} else {
// 3. If parsedValue is failure, then return.
return;
}
}
void CanvasRenderingContext2D::paint_shadow_for_fill_internal(Gfx::Path const& path, Gfx::WindingRule winding_rule)
{
auto* painter = this->painter();
if (!painter)
return;
auto path_to_fill = path;
path_to_fill.close_all_subpaths();
auto& state = this->drawing_state();
painter->save();
Gfx::AffineTransform transform;
transform.translate(state.shadow_offset_x, state.shadow_offset_y);
painter->set_transform(transform);
painter->fill_path(path_to_fill, state.shadow_color.with_opacity(state.global_alpha), winding_rule);
painter->restore();
did_draw(path_to_fill.bounding_box());
}
void CanvasRenderingContext2D::paint_shadow_for_stroke_internal(Gfx::Path const& path)
{
auto* painter = this->painter();
if (!painter)
return;
auto& state = drawing_state();
painter->save();
Gfx::AffineTransform transform;
transform.translate(state.shadow_offset_x, state.shadow_offset_y);
painter->set_transform(transform);
painter->stroke_path(path, state.shadow_color.with_opacity(state.global_alpha), state.line_width);
painter->restore();
did_draw(path.bounding_box());
}
}

View file

@ -0,0 +1,161 @@
/*
* Copyright (c) 2020-2024, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/Variant.h>
#include <LibGfx/AffineTransform.h>
#include <LibGfx/Color.h>
#include <LibGfx/Forward.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Path.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/HTML/Canvas/CanvasCompositing.h>
#include <LibWeb/HTML/Canvas/CanvasDrawImage.h>
#include <LibWeb/HTML/Canvas/CanvasDrawPath.h>
#include <LibWeb/HTML/Canvas/CanvasFillStrokeStyles.h>
#include <LibWeb/HTML/Canvas/CanvasImageData.h>
#include <LibWeb/HTML/Canvas/CanvasImageSmoothing.h>
#include <LibWeb/HTML/Canvas/CanvasPath.h>
#include <LibWeb/HTML/Canvas/CanvasPathDrawingStyles.h>
#include <LibWeb/HTML/Canvas/CanvasRect.h>
#include <LibWeb/HTML/Canvas/CanvasShadowStyles.h>
#include <LibWeb/HTML/Canvas/CanvasState.h>
#include <LibWeb/HTML/Canvas/CanvasText.h>
#include <LibWeb/HTML/Canvas/CanvasTextDrawingStyles.h>
#include <LibWeb/HTML/Canvas/CanvasTransform.h>
#include <LibWeb/HTML/CanvasGradient.h>
#include <LibWeb/Layout/InlineNode.h>
#include <LibWeb/Layout/LineBox.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::HTML {
class CanvasRenderingContext2D
: public Bindings::PlatformObject
, public CanvasPath
, public CanvasState
, public CanvasTransform<CanvasRenderingContext2D>
, public CanvasFillStrokeStyles<CanvasRenderingContext2D>
, public CanvasShadowStyles<CanvasRenderingContext2D>
, public CanvasRect
, public CanvasDrawPath
, public CanvasText
, public CanvasDrawImage
, public CanvasImageData
, public CanvasImageSmoothing
, public CanvasCompositing
, public CanvasPathDrawingStyles<CanvasRenderingContext2D>
, public CanvasTextDrawingStyles<CanvasRenderingContext2D> {
WEB_PLATFORM_OBJECT(CanvasRenderingContext2D, Bindings::PlatformObject);
JS_DECLARE_ALLOCATOR(CanvasRenderingContext2D);
public:
[[nodiscard]] static JS::NonnullGCPtr<CanvasRenderingContext2D> create(JS::Realm&, HTMLCanvasElement&);
virtual ~CanvasRenderingContext2D() override;
virtual void fill_rect(float x, float y, float width, float height) override;
virtual void stroke_rect(float x, float y, float width, float height) override;
virtual void clear_rect(float x, float y, float width, float height) override;
virtual WebIDL::ExceptionOr<void> draw_image_internal(CanvasImageSource const&, float source_x, float source_y, float source_width, float source_height, float destination_x, float destination_y, float destination_width, float destination_height) override;
virtual void begin_path() override;
virtual void stroke() override;
virtual void stroke(Path2D const& path) override;
virtual void fill_text(StringView, float x, float y, Optional<double> max_width) override;
virtual void stroke_text(StringView, float x, float y, Optional<double> max_width) override;
virtual void fill(StringView fill_rule) override;
virtual void fill(Path2D& path, StringView fill_rule) override;
virtual WebIDL::ExceptionOr<JS::NonnullGCPtr<ImageData>> create_image_data(int width, int height, Optional<ImageDataSettings> const& settings = {}) const override;
virtual WebIDL::ExceptionOr<JS::NonnullGCPtr<ImageData>> create_image_data(ImageData const& image_data) const override;
virtual WebIDL::ExceptionOr<JS::GCPtr<ImageData>> get_image_data(int x, int y, int width, int height, Optional<ImageDataSettings> const& settings = {}) const override;
virtual void put_image_data(ImageData const&, float x, float y) override;
virtual void reset_to_default_state() override;
JS::NonnullGCPtr<HTMLCanvasElement> canvas_for_binding() const;
virtual JS::NonnullGCPtr<TextMetrics> measure_text(StringView text) override;
virtual void clip(StringView fill_rule) override;
virtual void clip(Path2D& path, StringView fill_rule) override;
virtual bool is_point_in_path(double x, double y, StringView fill_rule) override;
virtual bool is_point_in_path(Path2D const& path, double x, double y, StringView fill_rule) override;
virtual bool image_smoothing_enabled() const override;
virtual void set_image_smoothing_enabled(bool) override;
virtual Bindings::ImageSmoothingQuality image_smoothing_quality() const override;
virtual void set_image_smoothing_quality(Bindings::ImageSmoothingQuality) override;
virtual float global_alpha() const override;
virtual void set_global_alpha(float) override;
virtual float shadow_offset_x() const override;
virtual void set_shadow_offset_x(float) override;
virtual float shadow_offset_y() const override;
virtual void set_shadow_offset_y(float) override;
virtual String shadow_color() const override;
virtual void set_shadow_color(String) override;
HTMLCanvasElement& canvas_element();
HTMLCanvasElement const& canvas_element() const;
[[nodiscard]] Gfx::Painter* painter();
private:
explicit CanvasRenderingContext2D(JS::Realm&, HTMLCanvasElement&);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
virtual Gfx::Painter* painter_for_canvas_state() override { return painter(); }
virtual Gfx::Path& path_for_canvas_state() override { return path(); }
struct PreparedText {
RefPtr<Gfx::GlyphRun> glyph_run;
Gfx::TextAlignment physical_alignment;
Gfx::IntRect bounding_box;
};
void did_draw(Gfx::FloatRect const&);
RefPtr<Gfx::Font const> current_font();
PreparedText prepare_text(ByteString const& text, float max_width = INFINITY);
[[nodiscard]] Gfx::Path rect_path(float x, float y, float width, float height);
[[nodiscard]] Gfx::Path text_path(StringView text, float x, float y, Optional<double> max_width);
void stroke_internal(Gfx::Path const&);
void fill_internal(Gfx::Path const&, Gfx::WindingRule);
void clip_internal(Gfx::Path&, Gfx::WindingRule);
void paint_shadow_for_fill_internal(Gfx::Path const&, Gfx::WindingRule);
void paint_shadow_for_stroke_internal(Gfx::Path const&);
JS::NonnullGCPtr<HTMLCanvasElement> m_element;
OwnPtr<Gfx::Painter> m_painter;
// https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-origin-clean
bool m_origin_clean { true };
};
enum class CanvasImageSourceUsability {
Bad,
Good,
};
WebIDL::ExceptionOr<CanvasImageSourceUsability> check_usability_of_image(CanvasImageSource const&);
bool image_is_not_origin_clean(CanvasImageSource const&);
}

View file

@ -0,0 +1,61 @@
#import <HTML/HTMLCanvasElement.idl>
#import <HTML/Canvas/CanvasCompositing.idl>
#import <HTML/Canvas/CanvasDrawImage.idl>
#import <HTML/Canvas/CanvasDrawPath.idl>
#import <HTML/Canvas/CanvasFillStrokeStyles.idl>
#import <HTML/Canvas/CanvasFilters.idl>
#import <HTML/Canvas/CanvasImageData.idl>
#import <HTML/Canvas/CanvasImageSmoothing.idl>
#import <HTML/Canvas/CanvasPath.idl>
#import <HTML/Canvas/CanvasPathDrawingStyles.idl>
#import <HTML/Canvas/CanvasTextDrawingStyles.idl>
#import <HTML/Canvas/CanvasRect.idl>
#import <HTML/Canvas/CanvasShadowStyles.idl>
#import <HTML/Canvas/CanvasState.idl>
#import <HTML/Canvas/CanvasText.idl>
#import <HTML/Canvas/CanvasTransform.idl>
#import <HTML/Canvas/CanvasUserInterface.idl>
enum PredefinedColorSpace { "srgb", "display-p3" };
dictionary CanvasRenderingContext2DSettings {
boolean alpha = true;
boolean desynchronized = false;
PredefinedColorSpace colorSpace = "srgb";
boolean willReadFrequently = false;
};
enum ImageSmoothingQuality { "low", "medium", "high" };
// FIXME: This should be in CanvasPathDrawingStyles.idl but then it is not exported
enum CanvasLineCap { "butt", "round", "square" };
enum CanvasLineJoin { "round", "bevel", "miter" };
// FIXME: This should be in CanvasTextDrawingStyles.idl but then it is not exported
enum CanvasTextAlign { "start", "end", "left", "right", "center" };
enum CanvasTextBaseline { "top", "hanging", "middle", "alphabetic", "ideographic", "bottom" };
// https://html.spec.whatwg.org/multipage/canvas.html#canvasrenderingcontext2d
[Exposed=Window]
interface CanvasRenderingContext2D {
[ImplementedAs=canvas_for_binding] readonly attribute HTMLCanvasElement canvas;
[FIXME] CanvasRenderingContext2DSettings getContextAttributes();
};
CanvasRenderingContext2D includes CanvasState;
CanvasRenderingContext2D includes CanvasTransform;
CanvasRenderingContext2D includes CanvasCompositing;
CanvasRenderingContext2D includes CanvasImageSmoothing;
CanvasRenderingContext2D includes CanvasFillStrokeStyles;
CanvasRenderingContext2D includes CanvasShadowStyles;
CanvasRenderingContext2D includes CanvasFilters;
CanvasRenderingContext2D includes CanvasRect;
CanvasRenderingContext2D includes CanvasDrawPath;
CanvasRenderingContext2D includes CanvasUserInterface;
CanvasRenderingContext2D includes CanvasText;
CanvasRenderingContext2D includes CanvasDrawImage;
CanvasRenderingContext2D includes CanvasImageData;
CanvasRenderingContext2D includes CanvasPathDrawingStyles;
CanvasRenderingContext2D includes CanvasTextDrawingStyles;
CanvasRenderingContext2D includes CanvasPath;

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/CloseEventPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/CloseEvent.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(CloseEvent);
JS::NonnullGCPtr<CloseEvent> CloseEvent::create(JS::Realm& realm, FlyString const& event_name, CloseEventInit const& event_init)
{
return realm.heap().allocate<CloseEvent>(realm, realm, event_name, event_init);
}
WebIDL::ExceptionOr<JS::NonnullGCPtr<CloseEvent>> CloseEvent::construct_impl(JS::Realm& realm, FlyString const& event_name, CloseEventInit const& event_init)
{
return create(realm, event_name, event_init);
}
CloseEvent::CloseEvent(JS::Realm& realm, FlyString const& event_name, CloseEventInit const& event_init)
: DOM::Event(realm, event_name, event_init)
, m_was_clean(event_init.was_clean)
, m_code(event_init.code)
, m_reason(event_init.reason)
{
}
CloseEvent::~CloseEvent() = default;
void CloseEvent::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(CloseEvent);
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2021, Dex <dexes.ttp@gmail.com>
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
#include <LibWeb/DOM/Event.h>
namespace Web::HTML {
struct CloseEventInit : public DOM::EventInit {
bool was_clean { false };
u16 code { 0 };
String reason;
};
class CloseEvent : public DOM::Event {
WEB_PLATFORM_OBJECT(CloseEvent, DOM::Event);
JS_DECLARE_ALLOCATOR(CloseEvent);
public:
[[nodiscard]] static JS::NonnullGCPtr<CloseEvent> create(JS::Realm&, FlyString const& event_name, CloseEventInit const& event_init = {});
static WebIDL::ExceptionOr<JS::NonnullGCPtr<CloseEvent>> construct_impl(JS::Realm&, FlyString const& event_name, CloseEventInit const& event_init);
virtual ~CloseEvent() override;
bool was_clean() const { return m_was_clean; }
u16 code() const { return m_code; }
String reason() const { return m_reason; }
private:
CloseEvent(JS::Realm&, FlyString const& event_name, CloseEventInit const& event_init);
virtual void initialize(JS::Realm&) override;
bool m_was_clean { false };
u16 m_code { 0 };
String m_reason;
};
}

View file

@ -0,0 +1,17 @@
#import <DOM/Event.idl>
// https://websockets.spec.whatwg.org/#the-closeevent-interface
[Exposed=*]
interface CloseEvent : Event {
constructor(DOMString type, optional CloseEventInit eventInitDict = {});
readonly attribute boolean wasClean;
readonly attribute unsigned short code;
readonly attribute USVString reason;
};
dictionary CloseEventInit : EventInit {
boolean wasClean = false;
unsigned short code = 0;
USVString reason = "";
};

View file

@ -0,0 +1,173 @@
/*
* Copyright (c) 2024, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/TypeCasts.h>
#include <LibWeb/Bindings/CloseWatcherPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/EventDispatcher.h>
#include <LibWeb/DOM/IDLEventListener.h>
#include <LibWeb/HTML/CloseWatcher.h>
#include <LibWeb/HTML/CloseWatcherManager.h>
#include <LibWeb/HTML/EventHandler.h>
#include <LibWeb/HTML/HTMLIFrameElement.h>
#include <LibWeb/HTML/Window.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(CloseWatcher);
// https://html.spec.whatwg.org/multipage/interaction.html#establish-a-close-watcher
JS::NonnullGCPtr<CloseWatcher> CloseWatcher::establish(HTML::Window& window)
{
// 1. Assert: window's associated Document is fully active.
VERIFY(window.associated_document().is_fully_active());
// 2. Let closeWatcher be a new close watcher
auto close_watcher = window.heap().allocate<CloseWatcher>(window.realm(), window.realm());
// 3. Let manager be window's associated close watcher manager
auto manager = window.close_watcher_manager();
// 4 - 6. Moved to CloseWatcherManager::add
manager->add(close_watcher);
// 7. Return close_watcher.
return close_watcher;
}
// https://html.spec.whatwg.org/multipage/interaction.html#dom-closewatcher
WebIDL::ExceptionOr<JS::NonnullGCPtr<CloseWatcher>> CloseWatcher::construct_impl(JS::Realm& realm, CloseWatcherOptions const& options)
{
auto& window = verify_cast<HTML::Window>(realm.global_object());
// NOTE: Not in spec explicitly, but this should account for detached iframes too. See /close-watcher/frame-removal.html WPT.
auto navigable = window.navigable();
if (navigable && navigable->has_been_destroyed())
return WebIDL::InvalidStateError::create(realm, "The iframe has been detached"_string);
// 1. If this's relevant global object's associated Document is not fully active, then return an "InvalidStateError" DOMException.
if (!window.associated_document().is_fully_active())
return WebIDL::InvalidStateError::create(realm, "The document is not fully active."_string);
// 2. Let close_watcher be the result of establishing a close watcher
auto close_watcher = establish(window);
// 3. If options["signal"] exists, then:
if (options.signal) {
// FIXME: 3.1 If options["signal"]'s aborted, then destroy closeWatcher.
// FIXME: 3.2 Add the following steps to options["signal"]:
}
return close_watcher;
}
CloseWatcher::CloseWatcher(JS::Realm& realm)
: DOM::EventTarget(realm)
{
}
// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-request-close
bool CloseWatcher::request_close()
{
// 1. If closeWatcher is not active, then return.
if (!m_is_active)
return true;
// 2. If closeWatcher's is running cancel action is true, then return true.
if (m_is_running_cancel_action)
return true;
// 3. Let window be closeWatcher's window.
auto& window = verify_cast<HTML::Window>(realm().global_object());
// 4. If window's associated Document is not fully active, then return true.
if (!window.associated_document().is_fully_active())
return true;
// 5. Let canPreventClose be true if window's close watcher manager's groups's size is less than window's close watcher manager's allowed number of groups,
// and window has history-action activation; otherwise false.
auto manager = window.close_watcher_manager();
bool can_prevent_close = manager->can_prevent_close() && window.has_history_action_activation();
// 6. Set closeWatcher's is running cancel action to true.
m_is_running_cancel_action = true;
// 7. Let shouldContinue be the result of running closeWatcher's cancel action given canPreventClose.
bool should_continue = dispatch_event(DOM::Event::create(realm(), HTML::EventNames::cancel, { .cancelable = can_prevent_close }));
// 8. Set closeWatcher's is running cancel action to false.
m_is_running_cancel_action = false;
// 9. If shouldContinue is false, then:
if (!should_continue) {
// 9.1 Assert: canPreventClose is true.
VERIFY(can_prevent_close);
// 9.2 Consume history-action user activation given window.
window.consume_history_action_user_activation();
return false;
}
// 10. Close closeWatcher.
close();
// 11. Return true.
return true;
}
// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-close
void CloseWatcher::close()
{
// 1. If closeWatcher is not active, then return.
if (!m_is_active)
return;
// 2. If closeWatcher's window's associated Document is not fully active, then return.
if (!verify_cast<HTML::Window>(realm().global_object()).associated_document().is_fully_active())
return;
// 3. Destroy closeWatcher.
destroy();
// 4. Run closeWatcher's close action.
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::close));
}
// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-destroy
void CloseWatcher::destroy()
{
// 1. Let manager be closeWatcher's window's close watcher manager.
auto manager = verify_cast<HTML::Window>(realm().global_object()).close_watcher_manager();
// 2-3. Moved to CloseWatcherManager::remove
manager->remove(*this);
m_is_active = false;
}
void CloseWatcher::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(CloseWatcher);
}
void CloseWatcher::set_oncancel(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::cancel, event_handler);
}
WebIDL::CallbackType* CloseWatcher::oncancel()
{
return event_handler_attribute(HTML::EventNames::cancel);
}
void CloseWatcher::set_onclose(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::close, event_handler);
}
WebIDL::CallbackType* CloseWatcher::onclose()
{
return event_handler_attribute(HTML::EventNames::close);
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2024, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <LibWeb/DOM/EventTarget.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/interaction.html#closewatcheroptions
struct CloseWatcherOptions {
JS::GCPtr<DOM::AbortSignal> signal;
};
// https://html.spec.whatwg.org/multipage/interaction.html#the-closewatcher-interface
class CloseWatcher final : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(CloseWatcher, DOM::EventTarget);
JS_DECLARE_ALLOCATOR(CloseWatcher);
public:
static WebIDL::ExceptionOr<JS::NonnullGCPtr<CloseWatcher>> construct_impl(JS::Realm&, CloseWatcherOptions const& = {});
[[nodiscard]] static JS::NonnullGCPtr<CloseWatcher> establish(HTML::Window&);
bool request_close();
void close();
void destroy();
virtual ~CloseWatcher() override = default;
void set_oncancel(WebIDL::CallbackType*);
WebIDL::CallbackType* oncancel();
void set_onclose(WebIDL::CallbackType*);
WebIDL::CallbackType* onclose();
private:
CloseWatcher(JS::Realm&);
virtual void initialize(JS::Realm&) override;
bool m_is_running_cancel_action { false };
bool m_is_active { true };
};
}

View file

@ -0,0 +1,19 @@
#import <DOM/EventTarget.idl>
#import <DOM/EventHandler.idl>
// https://html.spec.whatwg.org/multipage/interaction.html#closewatcher
[Exposed=Window]
interface CloseWatcher : EventTarget {
constructor(optional CloseWatcherOptions options = {});
undefined requestClose();
undefined close();
undefined destroy();
attribute EventHandler oncancel;
attribute EventHandler onclose;
};
dictionary CloseWatcherOptions {
AbortSignal signal;
};

View file

@ -0,0 +1,120 @@
/*
* Copyright (c) 2024, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/TypeCasts.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/EventDispatcher.h>
#include <LibWeb/DOM/IDLEventListener.h>
#include <LibWeb/HTML/CloseWatcher.h>
#include <LibWeb/HTML/CloseWatcherManager.h>
#include <LibWeb/HTML/Window.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(CloseWatcherManager);
JS::NonnullGCPtr<CloseWatcherManager> CloseWatcherManager::create(JS::Realm& realm)
{
return realm.heap().allocate<CloseWatcherManager>(realm, realm);
}
CloseWatcherManager::CloseWatcherManager(JS::Realm& realm)
: PlatformObject(realm)
{
}
void CloseWatcherManager::add(JS::NonnullGCPtr<CloseWatcher> close_watcher)
{
// If manager's groups's size is less than manager's allowed number of groups
if (m_groups.size() < m_allowed_number_of_groups) {
// then append « closeWatcher » to manager's groups.
JS::MarkedVector<JS::NonnullGCPtr<CloseWatcher>> new_group(realm().heap());
new_group.append(close_watcher);
m_groups.append(move(new_group));
} else {
// Assert: manager's groups's size is at least 1 in this branch, since manager's allowed number of groups is always at least 1.
VERIFY(!m_groups.is_empty());
// Append closeWatcher to manager's groups's last item.
m_groups.last().append(close_watcher);
}
// Set manager's next user interaction allows a new group to true.
m_next_user_interaction_allows_a_new_group = true;
}
void CloseWatcherManager::remove(CloseWatcher const& close_watcher)
{
// 2. For each group of manager's groups: remove closeWatcher from group
for (auto& group : m_groups) {
group.remove_first_matching([&close_watcher](JS::NonnullGCPtr<CloseWatcher>& entry) {
return entry.ptr() == &close_watcher;
});
}
// 3. Remove any item from manager's group that is empty
m_groups.remove_all_matching([](auto& group) { return group.is_empty(); });
}
// https://html.spec.whatwg.org/multipage/interaction.html#process-close-watchers
bool CloseWatcherManager::process_close_watchers()
{
// 1. Let processedACloseWatcher be false.
bool processed_a_close_watcher = false;
// 2. If window's close watcher manager's groups is not empty:
if (!m_groups.is_empty()) {
// 2.1 Let group be the last item in window's close watcher manager's groups.
auto& group = m_groups.last();
// Ambiguous spec wording. We copy the groups to avoid modifying the original while iterating.
// See https://github.com/whatwg/html/issues/10240
JS::MarkedVector<JS::NonnullGCPtr<CloseWatcher>> group_copy(realm().heap());
group_copy.ensure_capacity(group.size());
for (auto& close_watcher : group) {
group_copy.append(close_watcher);
}
// 2.2 For each closeWatcher of group, in reverse order:
for (auto it = group_copy.rbegin(); it != group_copy.rend(); ++it) {
// 2.1.1 Set processedACloseWatcher to true.
processed_a_close_watcher = true;
// 2.1.2 Let shouldProceed be the result of requesting to close closeWatcher.
bool should_proceed = (*it)->request_close();
// 2.1.3 If shouldProceed is false, then break;
if (!should_proceed)
break;
}
}
// 3. If window's close watcher manager's allowed number of groups is greater than 1, decrement it by 1.
if (m_allowed_number_of_groups > 1)
m_allowed_number_of_groups--;
// 4. Return processedACloseWatcher.
return processed_a_close_watcher;
}
// https://html.spec.whatwg.org/multipage/interaction.html#notify-the-close-watcher-manager-about-user-activation
void CloseWatcherManager::notify_about_user_activation()
{
// 1. Let manager be window's close watcher manager.
// 2. If manager's next user interaction allows a new group is true, then increment manager's allowed number of groups.
if (m_next_user_interaction_allows_a_new_group)
m_allowed_number_of_groups++;
// 3. Set manager's next user interaction allows a new group to false.
m_next_user_interaction_allows_a_new_group = false;
}
// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-request-close
bool CloseWatcherManager::can_prevent_close()
{
// 5. Let canPreventClose be true if window's close watcher manager's groups's size is less than window's close watcher manager's allowed number of groups...
return m_groups.size() < m_allowed_number_of_groups;
}
void CloseWatcherManager::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_groups);
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2024, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <LibWeb/DOM/EventTarget.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/interaction.html#close-watcher-manager
class CloseWatcherManager final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(CloseWatcherManager, Bindings::PlatformObject);
JS_DECLARE_ALLOCATOR(CloseWatcherManager);
public:
[[nodiscard]] static JS::NonnullGCPtr<CloseWatcherManager> create(JS::Realm&);
void add(JS::NonnullGCPtr<CloseWatcher>);
void remove(CloseWatcher const&);
bool process_close_watchers();
void notify_about_user_activation();
bool can_prevent_close();
private:
explicit CloseWatcherManager(JS::Realm&);
virtual void visit_edges(Cell::Visitor&) override;
Vector<Vector<JS::NonnullGCPtr<CloseWatcher>>> m_groups;
uint32_t m_allowed_number_of_groups { 1 };
bool m_next_user_interaction_allows_a_new_group { true };
};
}

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2023, Bastiaan van der Plaat <bastiaan.v.d.plaat@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
namespace Web::HTML {
enum class ColorPickerUpdateState {
Update,
Closed,
};
}

View file

@ -0,0 +1,260 @@
/*
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/NativeFunction.h>
#include <LibJS/Runtime/Object.h>
#include <LibJS/Runtime/PropertyDescriptor.h>
#include <LibJS/Runtime/PropertyKey.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/HTML/CrossOrigin/AbstractOperations.h>
#include <LibWeb/HTML/Location.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/WebIDL/DOMException.h>
namespace Web::HTML {
// 7.2.3.1 CrossOriginProperties ( O ), https://html.spec.whatwg.org/multipage/browsers.html#crossoriginproperties-(-o-)
Vector<CrossOriginProperty> cross_origin_properties(Variant<HTML::Location const*, HTML::Window const*> const& object)
{
// 1. Assert: O is a Location or Window object.
return object.visit(
// 2. If O is a Location object, then return « { [[Property]]: "href", [[NeedsGet]]: false, [[NeedsSet]]: true }, { [[Property]]: "replace" } ».
[](HTML::Location const*) -> Vector<CrossOriginProperty> {
return {
{ .property = "href"_string, .needs_get = false, .needs_set = true },
{ .property = "replace"_string },
};
},
// 3. Return « { [[Property]]: "window", [[NeedsGet]]: true, [[NeedsSet]]: false }, { [[Property]]: "self", [[NeedsGet]]: true, [[NeedsSet]]: false }, { [[Property]]: "location", [[NeedsGet]]: true, [[NeedsSet]]: true }, { [[Property]]: "close" }, { [[Property]]: "closed", [[NeedsGet]]: true, [[NeedsSet]]: false }, { [[Property]]: "focus" }, { [[Property]]: "blur" }, { [[Property]]: "frames", [[NeedsGet]]: true, [[NeedsSet]]: false }, { [[Property]]: "length", [[NeedsGet]]: true, [[NeedsSet]]: false }, { [[Property]]: "top", [[NeedsGet]]: true, [[NeedsSet]]: false }, { [[Property]]: "opener", [[NeedsGet]]: true, [[NeedsSet]]: false }, { [[Property]]: "parent", [[NeedsGet]]: true, [[NeedsSet]]: false }, { [[Property]]: "postMessage" } ».
[](HTML::Window const*) -> Vector<CrossOriginProperty> {
return {
{ .property = "window"_string, .needs_get = true, .needs_set = false },
{ .property = "self"_string, .needs_get = true, .needs_set = false },
{ .property = "location"_string, .needs_get = true, .needs_set = true },
{ .property = "close"_string },
{ .property = "closed"_string, .needs_get = true, .needs_set = false },
{ .property = "focus"_string },
{ .property = "blur"_string },
{ .property = "frames"_string, .needs_get = true, .needs_set = false },
{ .property = "length"_string, .needs_get = true, .needs_set = false },
{ .property = "top"_string, .needs_get = true, .needs_set = false },
{ .property = "opener"_string, .needs_get = true, .needs_set = false },
{ .property = "parent"_string, .needs_get = true, .needs_set = false },
{ .property = "postMessage"_string },
};
});
}
// https://html.spec.whatwg.org/multipage/browsers.html#cross-origin-accessible-window-property-name
bool is_cross_origin_accessible_window_property_name(JS::PropertyKey const& property_key)
{
// A JavaScript property name P is a cross-origin accessible window property name if it is "window", "self", "location", "close", "closed", "focus", "blur", "frames", "length", "top", "opener", "parent", "postMessage", or an array index property name.
static Array<DeprecatedFlyString, 13> property_names {
"window"sv, "self"sv, "location"sv, "close"sv, "closed"sv, "focus"sv, "blur"sv, "frames"sv, "length"sv, "top"sv, "opener"sv, "parent"sv, "postMessage"sv
};
return (property_key.is_string() && any_of(property_names, [&](auto const& name) { return property_key.as_string() == name; })) || property_key.is_number();
}
// 7.2.3.2 CrossOriginPropertyFallback ( P ), https://html.spec.whatwg.org/multipage/browsers.html#crossoriginpropertyfallback-(-p-)
JS::ThrowCompletionOr<JS::PropertyDescriptor> cross_origin_property_fallback(JS::VM& vm, JS::PropertyKey const& property_key)
{
// 1. If P is "then", @@toStringTag, @@hasInstance, or @@isConcatSpreadable, then return PropertyDescriptor{ [[Value]]: undefined, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }.
auto property_key_is_then = property_key.is_string() && property_key.as_string() == vm.names.then.as_string();
auto property_key_is_allowed_symbol = property_key.is_symbol()
&& (property_key.as_symbol() == vm.well_known_symbol_to_string_tag()
|| property_key.as_symbol() == vm.well_known_symbol_has_instance()
|| property_key.as_symbol() == vm.well_known_symbol_is_concat_spreadable());
if (property_key_is_then || property_key_is_allowed_symbol)
return JS::PropertyDescriptor { .value = JS::js_undefined(), .writable = false, .enumerable = false, .configurable = true };
// 2. Throw a "SecurityError" DOMException.
return throw_completion(WebIDL::SecurityError::create(*vm.current_realm(), MUST(String::formatted("Can't access property '{}' on cross-origin object", property_key))));
}
// 7.2.3.3 IsPlatformObjectSameOrigin ( O ), https://html.spec.whatwg.org/multipage/nav-history-apis.html#isplatformobjectsameorigin-(-o-)
// https://whatpr.org/html/9893/nav-history-apis.html#isplatformobjectsameorigin-(-o-)
bool is_platform_object_same_origin(JS::Object const& object)
{
// 1. Return true if the current principal settings object's origin is same origin-domain with O's relevant settings object's origin, and false otherwise.
return HTML::current_principal_settings_object().origin().is_same_origin_domain(HTML::relevant_settings_object(object).origin());
}
// 7.2.3.4 CrossOriginGetOwnPropertyHelper ( O, P ), https://html.spec.whatwg.org/multipage/nav-history-apis.html#crossorigingetownpropertyhelper-(-o,-p-)
// https://whatpr.org/html/9893/nav-history-apis.html#crossorigingetownpropertyhelper-(-o,-p-)
Optional<JS::PropertyDescriptor> cross_origin_get_own_property_helper(Variant<HTML::Location*, HTML::Window*> const& object, JS::PropertyKey const& property_key)
{
auto& realm = *Bindings::main_thread_vm().current_realm();
auto const* object_ptr = object.visit([](auto* o) { return static_cast<JS::Object const*>(o); });
auto const object_const_variant = object.visit([](auto* o) { return Variant<HTML::Location const*, HTML::Window const*> { o }; });
// 1. Let crossOriginKey be a tuple consisting of the current principal settings object, O's relevant settings object, and P.
auto cross_origin_key = CrossOriginKey {
.current_principal_settings_object = (FlatPtr)&HTML::current_principal_settings_object(),
.relevant_settings_object = (FlatPtr)&HTML::relevant_settings_object(*object_ptr),
.property_key = property_key,
};
// SameValue(e.[[Property]], P) can never be true at step 2.1 if P is not a string due to the different type, so we can return early.
if (!property_key.is_string()) {
return {};
}
auto const& property_key_string = MUST(FlyString::from_deprecated_fly_string(property_key.as_string()));
// 2. For each e of CrossOriginProperties(O):
for (auto const& entry : cross_origin_properties(object_const_variant)) {
if (entry.property != property_key_string)
continue;
// 1. If SameValue(e.[[Property]], P) is true, then:
auto& cross_origin_property_descriptor_map = object.visit([](auto* o) -> CrossOriginPropertyDescriptorMap& { return o->cross_origin_property_descriptor_map(); });
// 1. If the value of the [[CrossOriginPropertyDescriptorMap]] internal slot of O contains an entry whose key is crossOriginKey, then return that entry's value.
auto it = cross_origin_property_descriptor_map.find(cross_origin_key);
if (it != cross_origin_property_descriptor_map.end())
return it->value;
// 2. Let originalDesc be OrdinaryGetOwnProperty(O, P).
auto original_descriptor = MUST((object_ptr->JS::Object::internal_get_own_property)(property_key));
// 3. Let crossOriginDesc be undefined.
auto cross_origin_descriptor = JS::PropertyDescriptor {};
// 4. If e.[[NeedsGet]] and e.[[NeedsSet]] are absent, then:
if (!entry.needs_get.has_value() && !entry.needs_set.has_value()) {
// 1. Let value be originalDesc.[[Value]].
auto value = original_descriptor->value;
// 2. If IsCallable(value) is true, then set value to an anonymous built-in function, created in the current Realm Record, that performs the same steps as the IDL operation P on object O.
if (value->is_function()) {
value = JS::NativeFunction::create(
realm, [function = JS::make_handle(*value)](auto& vm) {
return JS::call(vm, function.value(), JS::js_undefined(), vm.running_execution_context().arguments.span());
},
0, "");
}
// 3. Set crossOriginDesc to PropertyDescriptor{ [[Value]]: value, [[Enumerable]]: false, [[Writable]]: false, [[Configurable]]: true }.
cross_origin_descriptor = JS::PropertyDescriptor { .value = value, .writable = false, .enumerable = false, .configurable = true };
}
// 5. Otherwise:
else {
// 1. Let crossOriginGet be undefined.
Optional<JS::GCPtr<JS::FunctionObject>> cross_origin_get;
// 2. If e.[[NeedsGet]] is true, then set crossOriginGet to an anonymous built-in function, created in the current Realm Record, that performs the same steps as the getter of the IDL attribute P on object O.
if (*entry.needs_get) {
cross_origin_get = JS::NativeFunction::create(
realm, [object_ptr, getter = JS::make_handle(*original_descriptor->get)](auto& vm) {
return JS::call(vm, getter.cell(), object_ptr, vm.running_execution_context().arguments.span());
},
0, "");
}
// 3. Let crossOriginSet be undefined.
Optional<JS::GCPtr<JS::FunctionObject>> cross_origin_set;
// If e.[[NeedsSet]] is true, then set crossOriginSet to an anonymous built-in function, created in the current Realm Record, that performs the same steps as the setter of the IDL attribute P on object O.
if (*entry.needs_set) {
cross_origin_set = JS::NativeFunction::create(
realm, [object_ptr, setter = JS::make_handle(*original_descriptor->set)](auto& vm) {
return JS::call(vm, setter.cell(), object_ptr, vm.running_execution_context().arguments.span());
},
0, "");
}
// 5. Set crossOriginDesc to PropertyDescriptor{ [[Get]]: crossOriginGet, [[Set]]: crossOriginSet, [[Enumerable]]: false, [[Configurable]]: true }.
cross_origin_descriptor = JS::PropertyDescriptor { .get = cross_origin_get, .set = cross_origin_set, .enumerable = false, .configurable = true };
}
// 6. Create an entry in the value of the [[CrossOriginPropertyDescriptorMap]] internal slot of O with key crossOriginKey and value crossOriginDesc.
cross_origin_property_descriptor_map.set(cross_origin_key, cross_origin_descriptor);
// 7. Return crossOriginDesc.
return cross_origin_descriptor;
}
// 3. Return undefined.
return {};
}
// 7.2.3.5 CrossOriginGet ( O, P, Receiver ), https://html.spec.whatwg.org/multipage/browsers.html#crossoriginget-(-o,-p,-receiver-)
JS::ThrowCompletionOr<JS::Value> cross_origin_get(JS::VM& vm, JS::Object const& object, JS::PropertyKey const& property_key, JS::Value receiver)
{
// 1. Let desc be ? O.[[GetOwnProperty]](P).
auto descriptor = TRY(object.internal_get_own_property(property_key));
// 2. Assert: desc is not undefined.
VERIFY(descriptor.has_value());
// 3. If IsDataDescriptor(desc) is true, then return desc.[[Value]].
if (descriptor->is_data_descriptor())
return *descriptor->value;
// 4. Assert: IsAccessorDescriptor(desc) is true.
VERIFY(descriptor->is_accessor_descriptor());
// 5. Let getter be desc.[[Get]].
auto& getter = descriptor->get;
// 6. If getter is undefined, then throw a "SecurityError" DOMException.
if (!getter.has_value())
return throw_completion(WebIDL::SecurityError::create(*vm.current_realm(), MUST(String::formatted("Can't get property '{}' on cross-origin object", property_key))));
// 7. Return ? Call(getter, Receiver).
return JS::call(vm, *getter, receiver);
}
// 7.2.3.6 CrossOriginSet ( O, P, V, Receiver ), https://html.spec.whatwg.org/multipage/browsers.html#crossoriginset-(-o,-p,-v,-receiver-)
JS::ThrowCompletionOr<bool> cross_origin_set(JS::VM& vm, JS::Object& object, JS::PropertyKey const& property_key, JS::Value value, JS::Value receiver)
{
// 1. Let desc be ? O.[[GetOwnProperty]](P).
auto descriptor = TRY(object.internal_get_own_property(property_key));
// 2. Assert: desc is not undefined.
VERIFY(descriptor.has_value());
// 3. If desc.[[Set]] is present and its value is not undefined, then:
if (descriptor->set.has_value() && *descriptor->set) {
// FIXME: Spec issue, `setter` isn't being defined.
// 1. Perform ? Call(setter, Receiver, «V»).
TRY(JS::call(vm, *descriptor->set, receiver, value));
// 2. Return true.
return true;
}
// 4. Throw a "SecurityError" DOMException.
return throw_completion(WebIDL::SecurityError::create(*vm.current_realm(), MUST(String::formatted("Can't set property '{}' on cross-origin object", property_key))));
}
// 7.2.3.7 CrossOriginOwnPropertyKeys ( O ), https://html.spec.whatwg.org/multipage/browsers.html#crossoriginownpropertykeys-(-o-)
JS::MarkedVector<JS::Value> cross_origin_own_property_keys(Variant<HTML::Location const*, HTML::Window const*> const& object)
{
auto& event_loop = HTML::main_thread_event_loop();
auto& vm = event_loop.vm();
// 1. Let keys be a new empty List.
auto keys = JS::MarkedVector<JS::Value> { vm.heap() };
// 2. For each e of CrossOriginProperties(O), append e.[[Property]] to keys.
for (auto& entry : cross_origin_properties(object))
keys.append(JS::PrimitiveString::create(vm, move(entry.property)));
// 3. Return the concatenation of keys and « "then", @@toStringTag, @@hasInstance, @@isConcatSpreadable ».
keys.append(JS::PrimitiveString::create(vm, vm.names.then.as_string()));
keys.append(vm.well_known_symbol_to_string_tag());
keys.append(vm.well_known_symbol_has_instance());
keys.append(vm.well_known_symbol_is_concat_spreadable());
return keys;
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <AK/Variant.h>
#include <LibJS/Forward.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/CrossOrigin/CrossOriginPropertyDescriptorMap.h>
namespace Web::HTML {
Vector<CrossOriginProperty> cross_origin_properties(Variant<HTML::Location const*, HTML::Window const*> const&);
bool is_cross_origin_accessible_window_property_name(JS::PropertyKey const&);
JS::ThrowCompletionOr<JS::PropertyDescriptor> cross_origin_property_fallback(JS::VM&, JS::PropertyKey const&);
bool is_platform_object_same_origin(JS::Object const&);
Optional<JS::PropertyDescriptor> cross_origin_get_own_property_helper(Variant<HTML::Location*, HTML::Window*> const&, JS::PropertyKey const&);
JS::ThrowCompletionOr<JS::Value> cross_origin_get(JS::VM&, JS::Object const&, JS::PropertyKey const&, JS::Value receiver);
JS::ThrowCompletionOr<bool> cross_origin_set(JS::VM&, JS::Object&, JS::PropertyKey const&, JS::Value, JS::Value receiver);
JS::MarkedVector<JS::Value> cross_origin_own_property_keys(Variant<HTML::Location const*, HTML::Window const*> const&);
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Forward.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Traits.h>
#include <LibJS/Forward.h>
#include <LibJS/Runtime/PropertyKey.h>
namespace Web::HTML {
struct CrossOriginProperty {
String property;
Optional<bool> needs_get {};
Optional<bool> needs_set {};
};
struct CrossOriginKey {
FlatPtr current_principal_settings_object;
FlatPtr relevant_settings_object;
JS::PropertyKey property_key;
};
using CrossOriginPropertyDescriptorMap = HashMap<CrossOriginKey, JS::PropertyDescriptor>;
}
namespace AK {
template<>
struct Traits<Web::HTML::CrossOriginKey> : public DefaultTraits<Web::HTML::CrossOriginKey> {
static unsigned hash(Web::HTML::CrossOriginKey const& key)
{
return pair_int_hash(
Traits<JS::PropertyKey>::hash(key.property_key),
pair_int_hash(ptr_hash(key.current_principal_settings_object), ptr_hash(key.relevant_settings_object)));
}
static bool equals(Web::HTML::CrossOriginKey const& a, Web::HTML::CrossOriginKey const& b)
{
return a.current_principal_settings_object == b.current_principal_settings_object
&& a.relevant_settings_object == b.relevant_settings_object
&& Traits<JS::PropertyKey>::equals(a.property_key, b.property_key);
}
};
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/origin.html#cross-origin-opener-policy-value
enum class OpenerPolicyValue {
UnsafeNone,
SameOriginAllowPopups,
SameOrigin,
SameOriginPlusCOEP,
};
// https://html.spec.whatwg.org/multipage/origin.html#cross-origin-opener-policy
struct OpenerPolicy {
// A value, which is an opener policy value, initially "unsafe-none".
OpenerPolicyValue value { OpenerPolicyValue::UnsafeNone };
// A reporting endpoint, which is string or null, initially null.
Optional<String> reporting_endpoint;
// A report-only value, which is an opener policy value, initially "unsafe-none".
OpenerPolicyValue report_only_value { OpenerPolicyValue::UnsafeNone };
// A report-only reporting endpoint, which is a string or null, initially null.
Optional<String> report_only_reporting_endpoint;
};
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibURL/Origin.h>
#include <LibURL/URL.h>
#include <LibWeb/HTML/CrossOrigin/OpenerPolicy.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/origin.html#coop-enforcement-result
struct OpenerPolicyEnforcementResult {
// A boolean needs a browsing context group switch, initially false.
bool needs_a_browsing_context_group_switch { false };
// A boolean would need a browsing context group switch due to report-only, initially false.
bool would_need_a_browsing_context_group_switch_due_to_report_only { false };
// A URL url.
URL::URL url;
// An origin origin.
URL::Origin origin;
// An opener policy.
OpenerPolicy opener_policy;
// A boolean current context is navigation source.
bool current_context_is_navigation_source { false };
};
}

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Assertions.h>
#include <AK/Vector.h>
#include <LibJS/Runtime/PropertyKey.h>
#include <LibURL/Origin.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/CrossOrigin/AbstractOperations.h>
#include <LibWeb/HTML/CrossOrigin/Reporting.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/origin.html#coop-check-access-report
void check_if_access_between_two_browsing_contexts_should_be_reported(
BrowsingContext const& accessor,
BrowsingContext const* accessed,
JS::PropertyKey const& property_key,
EnvironmentSettingsObject const& environment)
{
// FIXME: Spec bug: https://github.com/whatwg/html/issues/10192
if (!accessed)
return;
// 1. If propertyKey is not a cross-origin accessible window property name, then return.
if (!is_cross_origin_accessible_window_property_name(property_key))
return;
// 2. Assert: accessor's active document and accessed's active document are both fully active.
VERIFY(accessor.active_document()->is_fully_active());
VERIFY(accessed->active_document()->is_fully_active());
// 3. Let accessorTopDocument be accessor's top-level browsing context's active document.
auto* accessor_top_document = accessor.top_level_browsing_context()->active_document();
// 4. Let accessorInclusiveAncestorOrigins be the list obtained by taking the origin of the active document of each of accessor's active document's inclusive ancestor navigables.
Vector<URL::Origin> accessor_inclusive_ancestor_origins = {};
auto accessor_inclusive_ancestors = accessor.active_document()->ancestor_navigables();
accessor_inclusive_ancestor_origins.ensure_capacity(accessor_inclusive_ancestors.size());
for (auto const& ancestor : accessor_inclusive_ancestors) {
VERIFY(ancestor != nullptr);
VERIFY(ancestor->active_document() != nullptr);
accessor_inclusive_ancestor_origins.append(ancestor->active_document()->origin());
}
// 5. Let accessedTopDocument be accessed's top-level browsing context's active document.
VERIFY(accessed->top_level_browsing_context() != nullptr);
auto* accessed_top_document = accessed->top_level_browsing_context()->active_document();
// 6. Let accessedInclusiveAncestorOrigins be the list obtained by taking the origin of the active document of each of accessed's active document's inclusive ancestor navigables.
Vector<URL::Origin> accessed_inclusive_ancestor_origins = {};
auto accessed_inclusive_ancestors = accessed->active_document()->ancestor_navigables();
accessed_inclusive_ancestor_origins.ensure_capacity(accessed_inclusive_ancestors.size());
for (auto const& ancestor : accessed_inclusive_ancestors) {
VERIFY(ancestor != nullptr);
VERIFY(ancestor->active_document() != nullptr);
accessed_inclusive_ancestor_origins.append(ancestor->active_document()->origin());
}
// 7. If any of accessorInclusiveAncestorOrigins are not same origin with accessorTopDocument's origin, or if any of accessedInclusiveAncestorOrigins are not same origin with accessedTopDocument's origin, then return.
for (auto const& origin : accessor_inclusive_ancestor_origins)
if (!origin.is_same_origin(accessor_top_document->origin()))
return;
for (auto const& origin : accessed_inclusive_ancestor_origins)
if (!origin.is_same_origin(accessed_top_document->origin()))
return;
// 8. If accessor's top-level browsing context's virtual browsing context group ID is accessed's top-level browsing context's virtual browsing context group ID, then return.
if (accessor.top_level_browsing_context()->virtual_browsing_context_group_id() == accessed->top_level_browsing_context()->virtual_browsing_context_group_id())
return;
// 9. Let accessorAccessedRelationship be a new accessor-accessed relationship with value none.
auto accessor_accessed_relationship = AccessorAccessedRelationship::None;
// 10. If accessed's top-level browsing context's opener browsing context is accessor or is an ancestor of accessor, then set accessorAccessedRelationship to accessor is opener.
if (accessor.is_ancestor_of(*accessed->top_level_browsing_context()->opener_browsing_context()))
accessor_accessed_relationship = AccessorAccessedRelationship::AccessorIsOpener;
// 11. If accessor's top-level browsing context's opener browsing context is accessed or is an ancestor of accessed, then set accessorAccessedRelationship to accessor is openee.
if (accessed->is_ancestor_of(*accessor.top_level_browsing_context()->opener_browsing_context()))
accessor_accessed_relationship = AccessorAccessedRelationship::AccessorIsOpener;
// 12. Queue violation reports for accesses, given accessorAccessedRelationship, accessorTopDocument's opener policy, accessedTopDocument's opener policy, accessor's active document's URL, accessed's active document's URL, accessor's top-level browsing context's initial URL, accessed's top-level browsing context's initial URL, accessor's active document's origin, accessed's active document's origin, accessor's top-level browsing context's opener origin at creation, accessed's top-level browsing context's opener origin at creation, accessorTopDocument's referrer, accessedTopDocument's referrer, propertyKey, and environment.
(void)environment;
(void)accessor_accessed_relationship;
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Forward.h>
#include <LibWeb/Forward.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/origin.html#accessor-accessed-relationship
enum class AccessorAccessedRelationship {
AccessorIsOpener,
AccessorIsOpenee,
None,
};
void check_if_access_between_two_browsing_contexts_should_be_reported(BrowsingContext const& accessor, BrowsingContext const* accessed, JS::PropertyKey const&, EnvironmentSettingsObject const&);
}

View file

@ -0,0 +1,18 @@
/*
* Copyright (c) 2024, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
namespace Web::HTML {
void CustomElementDefinition::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_constructor);
visitor.visit(m_lifecycle_callbacks);
}
}

View file

@ -0,0 +1,114 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/WebIDL/CallbackType.h>
namespace Web::HTML {
struct AlreadyConstructedCustomElementMarker {
};
// https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-definition
class CustomElementDefinition : public JS::Cell {
JS_CELL(CustomElementDefinition, JS::Cell);
JS_DECLARE_ALLOCATOR(CustomElementDefinition);
using LifecycleCallbacksStorage = OrderedHashMap<FlyString, JS::GCPtr<WebIDL::CallbackType>>;
using ConstructionStackStorage = Vector<Variant<JS::Handle<DOM::Element>, AlreadyConstructedCustomElementMarker>>;
static JS::NonnullGCPtr<CustomElementDefinition> create(JS::Realm& realm, String const& name, String const& local_name, WebIDL::CallbackType& constructor, Vector<String>&& observed_attributes, LifecycleCallbacksStorage&& lifecycle_callbacks, bool form_associated, bool disable_internals, bool disable_shadow)
{
return realm.heap().allocate<CustomElementDefinition>(realm, name, local_name, constructor, move(observed_attributes), move(lifecycle_callbacks), form_associated, disable_internals, disable_shadow);
}
~CustomElementDefinition() = default;
String const& name() const { return m_name; }
String const& local_name() const { return m_local_name; }
WebIDL::CallbackType& constructor() { return *m_constructor; }
WebIDL::CallbackType const& constructor() const { return *m_constructor; }
Vector<String> const& observed_attributes() const { return m_observed_attributes; }
LifecycleCallbacksStorage const& lifecycle_callbacks() const { return m_lifecycle_callbacks; }
ConstructionStackStorage& construction_stack() { return m_construction_stack; }
ConstructionStackStorage const& construction_stack() const { return m_construction_stack; }
bool form_associated() const { return m_form_associated; }
bool disable_internals() const { return m_disable_internals; }
bool disable_shadow() const { return m_disable_shadow; }
private:
CustomElementDefinition(String const& name, String const& local_name, WebIDL::CallbackType& constructor, Vector<String>&& observed_attributes, LifecycleCallbacksStorage&& lifecycle_callbacks, bool form_associated, bool disable_internals, bool disable_shadow)
: m_name(name)
, m_local_name(local_name)
, m_constructor(constructor)
, m_observed_attributes(move(observed_attributes))
, m_lifecycle_callbacks(move(lifecycle_callbacks))
, m_form_associated(form_associated)
, m_disable_internals(disable_internals)
, m_disable_shadow(disable_shadow)
{
}
virtual void visit_edges(Visitor& visitor) override;
// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-name
// A name
// A valid custom element name
String m_name;
// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-local-name
// A local name
// A local name
String m_local_name;
// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-constructor
// A Web IDL CustomElementConstructor callback function type value wrapping the custom element constructor
JS::NonnullGCPtr<WebIDL::CallbackType> m_constructor;
// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-observed-attributes
// A list of observed attributes
// A sequence<DOMString>
Vector<String> m_observed_attributes;
// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-lifecycle-callbacks
// A collection of lifecycle callbacks
// A map, whose keys are the strings "connectedCallback", "disconnectedCallback", "adoptedCallback", "attributeChangedCallback",
// "formAssociatedCallback", "formDisabledCallback", "formResetCallback", and "formStateRestoreCallback".
// The corresponding values are either a Web IDL Function callback function type value, or null.
// By default the value of each entry is null.
LifecycleCallbacksStorage m_lifecycle_callbacks;
// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-construction-stack
// A construction stack
// A list, initially empty, that is manipulated by the upgrade an element algorithm and the HTML element constructors.
// Each entry in the list will be either an element or an already constructed marker.
ConstructionStackStorage m_construction_stack;
// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-form-associated
// A form-associated boolean
// If this is true, user agent treats elements associated to this custom element definition as form-associated custom elements.
bool m_form_associated { false };
// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-disable-internals
// A disable internals boolean
// Controls attachInternals().
bool m_disable_internals { false };
// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-disable-shadow
// A disable shadow boolean
// Controls attachShadow().
bool m_disable_shadow { false };
};
}

View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2023, Srikavin Ramkumar <me@srikavin.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringView.h>
#include <AK/Utf8View.h>
#include <LibWeb/HTML/CustomElements/CustomElementName.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-core-concepts:prod-pcenchar
static bool is_pcen_char(u32 code_point)
{
return code_point == '-'
|| code_point == '.'
|| (code_point >= '0' && code_point <= '9')
|| code_point == '_'
|| (code_point >= 'a' && code_point <= 'z')
|| code_point == 0xb7
|| (code_point >= 0xc0 && code_point <= 0xd6)
|| (code_point >= 0xd8 && code_point <= 0xf6)
|| (code_point >= 0xf8 && code_point <= 0x37d)
|| (code_point >= 0x37f && code_point <= 0x1fff)
|| (code_point >= 0x200c && code_point <= 0x200d)
|| (code_point >= 0x203f && code_point <= 0x2040)
|| (code_point >= 0x2070 && code_point <= 0x218f)
|| (code_point >= 0x2c00 && code_point <= 0x2fef)
|| (code_point >= 0x3001 && code_point <= 0xD7ff)
|| (code_point >= 0xf900 && code_point <= 0xfdcf)
|| (code_point >= 0xfdf0 && code_point <= 0xfffd)
|| (code_point >= 0x10000 && code_point <= 0xeffff);
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
bool is_valid_custom_element_name(StringView name)
{
// name must not be any of the following:
if (name.is_one_of(
"annotation-xml"sv,
"color-profile"sv,
"font-face"sv,
"font-face-src"sv,
"font-face-uri"sv,
"font-face-format"sv,
"font-face-name"sv,
"missing-glyph"sv)) {
return false;
}
// name must match the PotentialCustomElementName production:
// PotentialCustomElementName ::=
// [a-z] (PCENChar)* '-' (PCENChar)*
auto code_points = Utf8View { name };
auto it = code_points.begin();
if (code_points.is_empty() || *it < 'a' || *it > 'z')
return false;
++it;
bool found_hyphen = false;
for (; it != code_points.end(); ++it) {
if (!is_pcen_char(*it))
return false;
if (*it == '-')
found_hyphen = true;
}
return found_hyphen;
}
}

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2023, Srikavin Ramkumar <me@srikavin.me>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/StringView.h>
namespace Web::HTML {
bool is_valid_custom_element_name(StringView name);
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
namespace Web::HTML::CustomElementReactionNames {
#define __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(name) FlyString name;
ENUMERATE_CUSTOM_ELEMENT_REACTION_NAMES
#undef __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME
void initialize_strings()
{
static bool s_initialized = false;
VERIFY(!s_initialized);
#define __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(name) \
name = #name##_fly_string;
ENUMERATE_CUSTOM_ELEMENT_REACTION_NAMES
#undef __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME
s_initialized = true;
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/FlyString.h>
namespace Web::HTML::CustomElementReactionNames {
// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-lifecycle-callbacks
#define ENUMERATE_CUSTOM_ELEMENT_REACTION_NAMES \
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(connectedCallback) \
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(disconnectedCallback) \
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(adoptedCallback) \
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(attributeChangedCallback) \
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formAssociatedCallback) \
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formDisabledCallback) \
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formResetCallback) \
__ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(formStateRestoreCallback)
#define __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME(name) extern FlyString name;
ENUMERATE_CUSTOM_ELEMENT_REACTION_NAMES
#undef __ENUMERATE_CUSTOM_ELEMENT_REACTION_NAME
void initialize_strings();
}

View file

@ -0,0 +1,431 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/FunctionObject.h>
#include <LibJS/Runtime/Iterator.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibWeb/Bindings/CustomElementRegistryPrototype.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/ShadowRoot.h>
#include <LibWeb/HTML/CustomElements/CustomElementName.h>
#include <LibWeb/HTML/CustomElements/CustomElementReactionNames.h>
#include <LibWeb/HTML/CustomElements/CustomElementRegistry.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Namespace.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(CustomElementRegistry);
JS_DEFINE_ALLOCATOR(CustomElementDefinition);
CustomElementRegistry::CustomElementRegistry(JS::Realm& realm)
: Bindings::PlatformObject(realm)
{
}
CustomElementRegistry::~CustomElementRegistry() = default;
void CustomElementRegistry::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(CustomElementRegistry);
}
void CustomElementRegistry::visit_edges(Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_custom_element_definitions);
visitor.visit(m_when_defined_promise_map);
}
// https://webidl.spec.whatwg.org/#es-callback-function
static JS::ThrowCompletionOr<JS::NonnullGCPtr<WebIDL::CallbackType>> convert_value_to_callback_function(JS::VM& vm, JS::Value value)
{
// FIXME: De-duplicate this from the IDL generator.
// 1. If the result of calling IsCallable(V) is false and the conversion to an IDL value is not being performed due to V being assigned to an attribute whose type is a nullable callback function that is annotated with [LegacyTreatNonObjectAsNull], then throw a TypeError.
if (!value.is_function())
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAFunction, value.to_string_without_side_effects());
// 2. Return the IDL callback function type value that represents a reference to the same object that V represents, with the incumbent settings object as the callback context.
return vm.heap().allocate_without_realm<WebIDL::CallbackType>(value.as_object(), HTML::incumbent_settings_object());
}
// https://webidl.spec.whatwg.org/#es-sequence
static JS::ThrowCompletionOr<Vector<String>> convert_value_to_sequence_of_strings(JS::VM& vm, JS::Value value)
{
// FIXME: De-duplicate this from the IDL generator.
// An ECMAScript value V is converted to an IDL sequence<T> value as follows:
// 1. If V is not an Object, throw a TypeError.
if (!value.is_object())
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObject, value.to_string_without_side_effects());
// 2. Let method be ? GetMethod(V, @@iterator).
auto method = TRY(value.get_method(vm, vm.well_known_symbol_iterator()));
// 3. If method is undefined, throw a TypeError.
if (!method)
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotIterable, value.to_string_without_side_effects());
// 4. Return the result of creating a sequence from V and method.
// https://webidl.spec.whatwg.org/#create-sequence-from-iterable
// To create an IDL value of type sequence<T> given an iterable iterable and an iterator getter method, perform the following steps:
// 1. Let iter be ? GetIterator(iterable, sync, method).
// FIXME: The WebIDL spec is out of date - it should be using GetIteratorFromMethod.
auto iterator = TRY(JS::get_iterator_from_method(vm, value, *method));
// 2. Initialize i to be 0.
Vector<String> sequence_of_strings;
// 3. Repeat
for (;;) {
// 1. Let next be ? IteratorStep(iter).
auto next = TRY(JS::iterator_step(vm, iterator));
// 2. If next is false, then return an IDL sequence value of type sequence<T> of length i, where the value of the element at index j is Sj.
if (!next)
return sequence_of_strings;
// 3. Let nextItem be ? IteratorValue(next).
auto next_item = TRY(JS::iterator_value(vm, *next));
// 4. Initialize Si to the result of converting nextItem to an IDL value of type T.
// https://webidl.spec.whatwg.org/#es-DOMString
// An ECMAScript value V is converted to an IDL DOMString value by running the following algorithm:
// 1. If V is null and the conversion is to an IDL type associated with the [LegacyNullToEmptyString] extended attribute, then return the DOMString value that represents the empty string.
// NOTE: This doesn't apply.
// 2. Let x be ? ToString(V).
// 3. Return the IDL DOMString value that represents the same sequence of code units as the one the ECMAScript String value x represents.
auto string_value = TRY(next_item.to_string(vm));
sequence_of_strings.append(move(string_value));
// 5. Set i to i + 1.
}
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-define
JS::ThrowCompletionOr<void> CustomElementRegistry::define(String const& name, WebIDL::CallbackType* constructor, ElementDefinitionOptions options)
{
auto& realm = this->realm();
auto& vm = this->vm();
// 1. If IsConstructor(constructor) is false, then throw a TypeError.
if (!JS::Value(constructor->callback).is_constructor())
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAConstructor, JS::Value(constructor->callback).to_string_without_side_effects());
// 2. If name is not a valid custom element name, then throw a "SyntaxError" DOMException.
if (!is_valid_custom_element_name(name))
return JS::throw_completion(WebIDL::SyntaxError::create(realm, MUST(String::formatted("'{}' is not a valid custom element name"sv, name))));
// 3. If this CustomElementRegistry contains an entry with name name, then throw a "NotSupportedError" DOMException.
auto existing_definition_with_name_iterator = m_custom_element_definitions.find_if([&name](auto const& definition) {
return definition->name() == name;
});
if (existing_definition_with_name_iterator != m_custom_element_definitions.end())
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, MUST(String::formatted("A custom element with name '{}' is already defined"sv, name))));
// 4. If this CustomElementRegistry contains an entry with constructor constructor, then throw a "NotSupportedError" DOMException.
auto existing_definition_with_constructor_iterator = m_custom_element_definitions.find_if([&constructor](auto const& definition) {
return definition->constructor().callback == constructor->callback;
});
if (existing_definition_with_constructor_iterator != m_custom_element_definitions.end())
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "The given constructor is already in use by another custom element"_string));
// 5. Let localName be name.
String local_name = name;
// 6. Let extends be the value of the extends member of options, or null if no such member exists.
auto& extends = options.extends;
// 7. If extends is not null, then:
if (extends.has_value()) {
// 1. If extends is a valid custom element name, then throw a "NotSupportedError" DOMException.
if (is_valid_custom_element_name(extends.value()))
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, MUST(String::formatted("'{}' is a custom element name, only non-custom elements can be extended"sv, extends.value()))));
// 2. If the element interface for extends and the HTML namespace is HTMLUnknownElement (e.g., if extends does not indicate an element definition in this specification), then throw a "NotSupportedError" DOMException.
if (DOM::is_unknown_html_element(extends.value()))
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, MUST(String::formatted("'{}' is an unknown HTML element"sv, extends.value()))));
// 3. Set localName to extends.
local_name = extends.value();
}
// 8. If this CustomElementRegistry's element definition is running flag is set, then throw a "NotSupportedError" DOMException.
if (m_element_definition_is_running)
return JS::throw_completion(WebIDL::NotSupportedError::create(realm, "Cannot recursively define custom elements"_string));
// 9. Set this CustomElementRegistry's element definition is running flag.
m_element_definition_is_running = true;
// 10. Let formAssociated be false.
bool form_associated = false;
// 11. Let disableInternals be false.
bool disable_internals = false;
// 12. Let disableShadow be false.
bool disable_shadow = false;
// 13. Let observedAttributes be an empty sequence<DOMString>.
Vector<String> observed_attributes;
// NOTE: This is not in the spec, but is required because of how we catch the exception by using a lambda, meaning we need to define this variable outside of it to use it later.
CustomElementDefinition::LifecycleCallbacksStorage lifecycle_callbacks;
// 14. Run the following substeps while catching any exceptions:
auto get_definition_attributes_from_constructor = [&]() -> JS::ThrowCompletionOr<void> {
// 1. Let prototype be ? Get(constructor, "prototype").
auto prototype_value = TRY(constructor->callback->get(vm.names.prototype));
// 2. If prototype is not an Object, then throw a TypeError exception.
if (!prototype_value.is_object())
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotAnObject, prototype_value.to_string_without_side_effects());
auto& prototype = prototype_value.as_object();
// 3. Let lifecycleCallbacks be a map with the keys "connectedCallback", "disconnectedCallback", "adoptedCallback", and "attributeChangedCallback", each of which belongs to an entry whose value is null.
lifecycle_callbacks.set(CustomElementReactionNames::connectedCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::disconnectedCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::adoptedCallback, {});
lifecycle_callbacks.set(CustomElementReactionNames::attributeChangedCallback, {});
// 4. For each of the keys callbackName in lifecycleCallbacks, in the order listed in the previous step:
for (auto const& callback_name : { CustomElementReactionNames::connectedCallback, CustomElementReactionNames::disconnectedCallback, CustomElementReactionNames::adoptedCallback, CustomElementReactionNames::attributeChangedCallback }) {
// 1. Let callbackValue be ? Get(prototype, callbackName).
auto callback_value = TRY(prototype.get(callback_name.to_deprecated_fly_string()));
// 2. If callbackValue is not undefined, then set the value of the entry in lifecycleCallbacks with key callbackName to the result of converting callbackValue to the Web IDL Function callback type. Rethrow any exceptions from the conversion.
if (!callback_value.is_undefined()) {
auto callback = TRY(convert_value_to_callback_function(vm, callback_value));
lifecycle_callbacks.set(callback_name, callback);
}
}
// 5. If the value of the entry in lifecycleCallbacks with key "attributeChangedCallback" is not null, then:
auto attribute_changed_callback_iterator = lifecycle_callbacks.find(CustomElementReactionNames::attributeChangedCallback);
VERIFY(attribute_changed_callback_iterator != lifecycle_callbacks.end());
if (attribute_changed_callback_iterator->value) {
// 1. Let observedAttributesIterable be ? Get(constructor, "observedAttributes").
auto observed_attributes_iterable = TRY(constructor->callback->get(JS::PropertyKey { "observedAttributes" }));
// 2. If observedAttributesIterable is not undefined, then set observedAttributes to the result of converting observedAttributesIterable to a sequence<DOMString>. Rethrow any exceptions from the conversion.
if (!observed_attributes_iterable.is_undefined())
observed_attributes = TRY(convert_value_to_sequence_of_strings(vm, observed_attributes_iterable));
}
// 6. Let disabledFeatures be an empty sequence<DOMString>.
Vector<String> disabled_features;
// 7. Let disabledFeaturesIterable be ? Get(constructor, "disabledFeatures").
auto disabled_features_iterable = TRY(constructor->callback->get(JS::PropertyKey { "disabledFeatures" }));
// 8. If disabledFeaturesIterable is not undefined, then set disabledFeatures to the result of converting disabledFeaturesIterable to a sequence<DOMString>. Rethrow any exceptions from the conversion.
if (!disabled_features_iterable.is_undefined())
disabled_features = TRY(convert_value_to_sequence_of_strings(vm, disabled_features_iterable));
// 9. Set disableInternals to true if disabledFeatures contains "internals".
disable_internals = disabled_features.contains_slow("internals"sv);
// 10. Set disableShadow to true if disabledFeatures contains "shadow".
disable_shadow = disabled_features.contains_slow("shadow"sv);
// 11. Let formAssociatedValue be ? Get( constructor, "formAssociated").
auto form_associated_value = TRY(constructor->callback->get(JS::PropertyKey { "formAssociated" }));
// 12. Set formAssociated to the result of converting formAssociatedValue to a boolean. Rethrow any exceptions from the conversion.
// NOTE: Converting to a boolean cannot throw with ECMAScript.
// https://webidl.spec.whatwg.org/#es-boolean
// An ECMAScript value V is converted to an IDL boolean value by running the following algorithm:
// 1. Let x be the result of computing ToBoolean(V).
// 2. Return the IDL boolean value that is the one that represents the same truth value as the ECMAScript Boolean value x.
form_associated = form_associated_value.to_boolean();
// 13. If formAssociated is true, for each of "formAssociatedCallback", "formResetCallback", "formDisabledCallback", and "formStateRestoreCallback" callbackName:
if (form_associated) {
for (auto const& callback_name : { CustomElementReactionNames::formAssociatedCallback, CustomElementReactionNames::formResetCallback, CustomElementReactionNames::formDisabledCallback, CustomElementReactionNames::formStateRestoreCallback }) {
// 1. Let callbackValue be ? Get(prototype, callbackName).
auto callback_value = TRY(prototype.get(callback_name.to_deprecated_fly_string()));
// 2. If callbackValue is not undefined, then set the value of the entry in lifecycleCallbacks with key callbackName to the result of converting callbackValue to the Web IDL Function callback type. Rethrow any exceptions from the conversion.
if (!callback_value.is_undefined())
lifecycle_callbacks.set(callback_name, TRY(convert_value_to_callback_function(vm, callback_value)));
}
}
return {};
};
auto maybe_exception = get_definition_attributes_from_constructor();
// Then, perform the following substep, regardless of whether the above steps threw an exception or not:
// 1. Unset this CustomElementRegistry's element definition is running flag.
m_element_definition_is_running = false;
// Finally, if the first set of substeps threw an exception, then rethrow that exception (thus terminating this algorithm). Otherwise, continue onward.
if (maybe_exception.is_throw_completion())
return maybe_exception.release_error();
// 15. Let definition be a new custom element definition with name name, local name localName, constructor constructor, observed attributes observedAttributes, lifecycle callbacks lifecycleCallbacks, form-associated formAssociated, disable internals disableInternals, and disable shadow disableShadow.
auto definition = CustomElementDefinition::create(realm, name, local_name, *constructor, move(observed_attributes), move(lifecycle_callbacks), form_associated, disable_internals, disable_shadow);
// 16. Add definition to this CustomElementRegistry.
m_custom_element_definitions.append(definition);
// 17. Let document be this CustomElementRegistry's relevant global object's associated Document.
auto& document = verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document();
// 18. Let upgrade candidates be all elements that are shadow-including descendants of document, whose namespace is the HTML namespace and whose local name is localName, in shadow-including tree order.
// Additionally, if extends is non-null, only include elements whose is value is equal to name.
Vector<JS::Handle<DOM::Element>> upgrade_candidates;
document.for_each_shadow_including_descendant([&](DOM::Node& inclusive_descendant) {
if (!is<DOM::Element>(inclusive_descendant))
return TraversalDecision::Continue;
auto& inclusive_descendant_element = static_cast<DOM::Element&>(inclusive_descendant);
if (inclusive_descendant_element.namespace_uri() == Namespace::HTML && inclusive_descendant_element.local_name() == local_name && (!extends.has_value() || inclusive_descendant_element.is_value() == name))
upgrade_candidates.append(JS::make_handle(inclusive_descendant_element));
return TraversalDecision::Continue;
});
// 19. For each element element in upgrade candidates, enqueue a custom element upgrade reaction given element and definition.
for (auto& element : upgrade_candidates)
element->enqueue_a_custom_element_upgrade_reaction(definition);
// 20. If this CustomElementRegistry's when-defined promise map contains an entry with key name:
auto promise_when_defined_iterator = m_when_defined_promise_map.find(name);
if (promise_when_defined_iterator != m_when_defined_promise_map.end()) {
// 1. Let promise be the value of that entry.
auto promise = promise_when_defined_iterator->value;
// 2. Resolve promise with constructor.
WebIDL::resolve_promise(realm, promise, constructor->callback);
// 3. Delete the entry with key name from this CustomElementRegistry's when-defined promise map.
m_when_defined_promise_map.remove(name);
}
return {};
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-get
Variant<JS::Handle<WebIDL::CallbackType>, JS::Value> CustomElementRegistry::get(String const& name) const
{
// 1. If this CustomElementRegistry contains an entry with name name, then return that entry's constructor.
auto existing_definition_iterator = m_custom_element_definitions.find_if([&name](auto const& definition) {
return definition->name() == name;
});
if (!existing_definition_iterator.is_end())
return JS::make_handle((*existing_definition_iterator)->constructor());
// 2. Otherwise, return undefined.
return JS::js_undefined();
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-getname
Optional<String> CustomElementRegistry::get_name(JS::Handle<WebIDL::CallbackType> const& constructor) const
{
// 1. If this CustomElementRegistry contains an entry with constructor constructor, then return that entry's name.
auto existing_definition_iterator = m_custom_element_definitions.find_if([&constructor](auto const& definition) {
return definition->constructor().callback == constructor.cell()->callback;
});
if (!existing_definition_iterator.is_end())
return (*existing_definition_iterator)->name();
// 2. Return null.
return {};
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-whendefined
WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> CustomElementRegistry::when_defined(String const& name)
{
auto& realm = this->realm();
// 1. If name is not a valid custom element name, then return a new promise rejected with a "SyntaxError" DOMException.
if (!is_valid_custom_element_name(name))
return WebIDL::create_rejected_promise(realm, WebIDL::SyntaxError::create(realm, MUST(String::formatted("'{}' is not a valid custom element name"sv, name))));
// 2. If this CustomElementRegistry contains an entry with name name, then return a new promise resolved with that entry's constructor.
auto existing_definition_iterator = m_custom_element_definitions.find_if([&name](JS::Handle<CustomElementDefinition> const& definition) {
return definition->name() == name;
});
if (existing_definition_iterator != m_custom_element_definitions.end())
return WebIDL::create_resolved_promise(realm, (*existing_definition_iterator)->constructor().callback);
// 3. Let map be this CustomElementRegistry's when-defined promise map.
// NOTE: Not necessary.
// 4. If map does not contain an entry with key name, create an entry in map with key name and whose value is a new promise.
// 5. Let promise be the value of the entry in map with key name.
JS::GCPtr<WebIDL::Promise> promise;
auto existing_promise_iterator = m_when_defined_promise_map.find(name);
if (existing_promise_iterator != m_when_defined_promise_map.end()) {
promise = existing_promise_iterator->value;
} else {
promise = WebIDL::create_promise(realm);
m_when_defined_promise_map.set(name, *promise);
}
// 5. Return promise.
VERIFY(promise);
return JS::NonnullGCPtr { *promise };
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-upgrade
void CustomElementRegistry::upgrade(JS::NonnullGCPtr<DOM::Node> root) const
{
// 1. Let candidates be a list of all of root's shadow-including inclusive descendant elements, in shadow-including tree order.
Vector<JS::Handle<DOM::Element>> candidates;
root->for_each_shadow_including_inclusive_descendant([&](DOM::Node& inclusive_descendant) {
if (!is<DOM::Element>(inclusive_descendant))
return TraversalDecision::Continue;
auto& inclusive_descendant_element = static_cast<DOM::Element&>(inclusive_descendant);
candidates.append(JS::make_handle(inclusive_descendant_element));
return TraversalDecision::Continue;
});
// 2. For each candidate of candidates, try to upgrade candidate.
for (auto& candidate : candidates)
candidate->try_to_upgrade();
}
JS::GCPtr<CustomElementDefinition> CustomElementRegistry::get_definition_with_name_and_local_name(String const& name, String const& local_name) const
{
auto definition_iterator = m_custom_element_definitions.find_if([&](JS::Handle<CustomElementDefinition> const& definition) {
return definition->name() == name && definition->local_name() == local_name;
});
return definition_iterator.is_end() ? nullptr : definition_iterator->ptr();
}
JS::GCPtr<CustomElementDefinition> CustomElementRegistry::get_definition_from_new_target(JS::FunctionObject const& new_target) const
{
auto definition_iterator = m_custom_element_definitions.find_if([&](JS::Handle<CustomElementDefinition> const& definition) {
return definition->constructor().callback.ptr() == &new_target;
});
return definition_iterator.is_end() ? nullptr : definition_iterator->ptr();
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/RefCounted.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
namespace Web::HTML {
struct ElementDefinitionOptions {
Optional<String> extends;
};
// https://html.spec.whatwg.org/multipage/custom-elements.html#customelementregistry
class CustomElementRegistry : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(CustomElementRegistry, Bindings::PlatformObject);
JS_DECLARE_ALLOCATOR(CustomElementRegistry);
public:
virtual ~CustomElementRegistry() override;
JS::ThrowCompletionOr<void> define(String const& name, WebIDL::CallbackType* constructor, ElementDefinitionOptions options);
Variant<JS::Handle<WebIDL::CallbackType>, JS::Value> get(String const& name) const;
Optional<String> get_name(JS::Handle<WebIDL::CallbackType> const& constructor) const;
WebIDL::ExceptionOr<JS::NonnullGCPtr<WebIDL::Promise>> when_defined(String const& name);
void upgrade(JS::NonnullGCPtr<DOM::Node> root) const;
JS::GCPtr<CustomElementDefinition> get_definition_with_name_and_local_name(String const& name, String const& local_name) const;
JS::GCPtr<CustomElementDefinition> get_definition_from_new_target(JS::FunctionObject const& new_target) const;
private:
CustomElementRegistry(JS::Realm&);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Visitor&) override;
// Every CustomElementRegistry has a set of custom element definitions, initially empty. In general, algorithms in this specification look up elements in the registry by any of name, local name, or constructor.
Vector<JS::NonnullGCPtr<CustomElementDefinition>> m_custom_element_definitions;
// https://html.spec.whatwg.org/multipage/custom-elements.html#element-definition-is-running
// Every CustomElementRegistry also has an element definition is running flag which is used to prevent reentrant invocations of element definition. It is initially unset.
bool m_element_definition_is_running { false };
// https://html.spec.whatwg.org/multipage/custom-elements.html#when-defined-promise-map
// Every CustomElementRegistry also has a when-defined promise map, mapping valid custom element names to promises. It is used to implement the whenDefined() method.
OrderedHashMap<String, JS::NonnullGCPtr<WebIDL::Promise>> m_when_defined_promise_map;
};
}

View file

@ -0,0 +1,17 @@
#import <DOM/Node.idl>
// https://html.spec.whatwg.org/#customelementregistry
[Exposed=Window]
interface CustomElementRegistry {
[CEReactions] undefined define(DOMString name, CustomElementConstructor constructor, optional ElementDefinitionOptions options = {});
(CustomElementConstructor or undefined) get(DOMString name);
DOMString? getName(CustomElementConstructor constructor);
Promise<CustomElementConstructor> whenDefined(DOMString name);
[CEReactions] undefined upgrade(Node root);
};
callback CustomElementConstructor = HTMLElement ();
dictionary ElementDefinitionOptions {
DOMString extends;
};

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/DOMParserPrototype.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/DOM/XMLDocument.h>
#include <LibWeb/HTML/DOMParser.h>
#include <LibWeb/HTML/HTMLDocument.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/XML/XMLDocumentBuilder.h>
namespace Web::HTML {
JS_DEFINE_ALLOCATOR(DOMParser);
WebIDL::ExceptionOr<JS::NonnullGCPtr<DOMParser>> DOMParser::construct_impl(JS::Realm& realm)
{
return realm.heap().allocate<DOMParser>(realm, realm);
}
DOMParser::DOMParser(JS::Realm& realm)
: PlatformObject(realm)
{
}
DOMParser::~DOMParser() = default;
void DOMParser::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(DOMParser);
}
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-domparser-parsefromstring
JS::NonnullGCPtr<DOM::Document> DOMParser::parse_from_string(StringView string, Bindings::DOMParserSupportedType type)
{
// FIXME: 1. Let compliantString to the result of invoking the Get Trusted Type compliant string algorithm with TrustedHTML, this's relevant global object, string, "DOMParser parseFromString", and "script".
// 2. Let document be a new Document, whose content type is type and url is this's relevant global object's associated Document's URL.
JS::GCPtr<DOM::Document> document;
// 3. Switch on type:
if (type == Bindings::DOMParserSupportedType::Text_Html) {
// -> "text/html"
document = HTML::HTMLDocument::create(realm(), verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document().url());
document->set_content_type(Bindings::idl_enum_to_string(type));
// 1. Parse HTML from a string given document and compliantString. FIXME: Use compliantString.
document->parse_html_from_a_string(string);
} else {
// -> Otherwise
document = DOM::XMLDocument::create(realm(), verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document().url());
document->set_content_type(Bindings::idl_enum_to_string(type));
document->set_document_type(DOM::Document::Type::XML);
// 1. Create an XML parser parse, associated with document, and with XML scripting support disabled.
XML::Parser parser(string, { .resolve_external_resource = resolve_xml_resource });
XMLDocumentBuilder builder { *document, XMLScriptingSupport::Disabled };
// 2. Parse compliantString using parser. FIXME: Use compliantString.
auto result = parser.parse_with_listener(builder);
// 3. If the previous step resulted in an XML well-formedness or XML namespace well-formedness error, then:
if (result.is_error() || builder.has_error()) {
// NOTE: The XML parsing can produce nodes before it hits an error, just remove them.
// 1. Assert: document has no child nodes.
document->remove_all_children(true);
// 2. Let root be the result of creating an element given document, "parsererror", and "http://www.mozilla.org/newlayout/xml/parsererror.xml".
auto root = DOM::create_element(*document, "parsererror"_fly_string, "http://www.mozilla.org/newlayout/xml/parsererror.xml"_fly_string).release_value_but_fixme_should_propagate_errors();
// FIXME: 3. Optionally, add attributes or children to root to describe the nature of the parsing error.
// 4. Append root to document.
MUST(document->append_child(*root));
}
}
// 3. Return document.
return *document;
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Heap/GCPtr.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Forward.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#domparser
class DOMParser final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(DOMParser, Bindings::PlatformObject);
JS_DECLARE_ALLOCATOR(DOMParser);
public:
static WebIDL::ExceptionOr<JS::NonnullGCPtr<DOMParser>> construct_impl(JS::Realm&);
virtual ~DOMParser() override;
JS::NonnullGCPtr<DOM::Document> parse_from_string(StringView, Bindings::DOMParserSupportedType type);
private:
explicit DOMParser(JS::Realm&);
virtual void initialize(JS::Realm&) override;
};
}

View file

@ -0,0 +1,17 @@
#import <DOM/Document.idl>
enum DOMParserSupportedType {
"text/html",
"text/xml",
"application/xml",
"application/xhtml+xml",
"image/svg+xml"
};
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#domparser
[Exposed=Window]
interface DOMParser {
constructor();
Document parseFromString(DOMString string, DOMParserSupportedType type);
};

Some files were not shown because too many files have changed in this diff Show more