Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions meta/plugins/wpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ def _(args: WptArgs):
cmd = [
"./wpt",
"run",
"paper_muncher",
"vaev",
"--webdriver-binary",
"paper_muncher_webdriver",
"vaev-webdriver",
"--test-type=reftest",
"--no-fail-on-unexpected",
] + args.args
Expand Down
11 changes: 8 additions & 3 deletions project.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@
"git": "https://github.com/cute-engineering/cat.git",
"tag": "v0.11.0"
},
"cute-engineering/ce-bootfs": {
"commit": "87ad7796d29b6e36adebc7810b69c9b8dd4d0208",
"git": "https://github.com/cute-engineering/ce-bootfs.git",
"tag": "main"
},
"cute-engineering/ce-heap": {
"commit": "918dd7962dbcd703a883c9fcf658b496f9e3502e",
"git": "https://github.com/cute-engineering/ce-heap.git",
"tag": "v1.1.0"
},
"cute-engineering/ce-libc": {
"commit": "d5703dc5442bb9460eb7a99368c9c8b71791d629",
"commit": "3ed3147c168046594a92d7862feed99f5c558ac2",
"git": "https://github.com/cute-engineering/ce-libc.git",
"tag": "v1.1.1"
},
Expand Down Expand Up @@ -42,12 +47,12 @@
"tag": "main"
},
"skift-org/hideo": {
"commit": "e2be582bd8b168d9541ae19b6105f741c5dd7716",
"commit": "eb9ef17d03ee0abff1e142c467d3b6e04ca1564f",
"git": "https://github.com/skift-org/hideo.git",
"tag": "main"
},
"skift-org/karm": {
"commit": "93c3aaf3959abbeb05176b8d64ecbf80dfa2ac0a",
"commit": "6ce43866d37365b2954b127b8a34e7e091c6d2ad",
"git": "https://github.com/skift-org/karm.git",
"tag": "main"
}
Expand Down
19 changes: 19 additions & 0 deletions src/vaev-engine/dom/element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,25 @@ export struct Element : Node {
return sb.take();
}

// MARK: Element -----------------------------------------------------------

// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
bool isVoidElement() const {
return qualifiedName == Html::AREA_TAG or
qualifiedName == Html::BASE_TAG or
qualifiedName == Html::BR_TAG or
qualifiedName == Html::COL_TAG or
qualifiedName == Html::EMBED_TAG or
qualifiedName == Html::HR_TAG or
qualifiedName == Html::IMG_TAG or
qualifiedName == Html::INPUT_TAG or
qualifiedName == Html::LINK_TAG or
qualifiedName == Html::META_TAG or
qualifiedName == Html::SOURCE_TAG or
qualifiedName == Html::TRACK_TAG or
qualifiedName == Html::WBR_TAG;
}

// MARK: Pseudo Elements ---------------------------------------------------

void clearPseudoElement() {
Expand Down
1 change: 1 addition & 0 deletions src/vaev-engine/dom/mod.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export import :dom.text;
export import :dom.tokenList;
export import :dom.tree;
export import :dom.window;
export import :dom.serialisation;
7 changes: 7 additions & 0 deletions src/vaev-engine/dom/names.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export Symbol qualifiedAttrNameCased(Str name) {
if (eqCi(Str(#VALUE), name)) \
return Symbol::from(Str(#VALUE));
#include "defs/ns-svg-attr-names.inc"

Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line added at beginning of scope. According to coding guidelines, empty lines at the beginning of scopes should be removed.

Copilot generated this review using guidance from repository custom instructions.
#undef ATTR
return Symbol::from(name);
}
Expand All @@ -103,6 +104,7 @@ export Symbol qualifiedTagNameCased(Str name) {
if (eqCi(Str(#VALUE), name)) \
return Symbol::from(Str(#VALUE));
#include "defs/ns-svg-tag-names.inc"

Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line added at beginning of scope. According to coding guidelines, empty lines at the beginning of scopes should be removed.

Copilot generated this review using guidance from repository custom instructions.
#undef TAG
return Symbol::from(name);
}
Expand All @@ -127,13 +129,18 @@ namespace Dom {

void Dom::QualifiedName::repr(Io::Emit& e) const {
Str displayNamespace = ns.str();
// NOTE: If current node is an element in the HTML namespace, the MathML namespace,
// or the SVG namespace, then let tagname be current node's local name.
// Otherwise, let tagname be current node's qualified name.
// SEE: 13.3.5.2 https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments
if (ns == Html::NAMESPACE) {
displayNamespace = "html";
} else if (ns == Svg::NAMESPACE) {
displayNamespace = "svg";
} else if (ns == MathMl::NAMESPACE) {
displayNamespace = "mathml";
}

Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line added at beginning of scope. According to coding guidelines, empty lines at the beginning of scopes should be removed.

Copilot generated this review using guidance from repository custom instructions.
e("{}:{}", displayNamespace, name);
}

Expand Down
189 changes: 189 additions & 0 deletions src/vaev-engine/dom/serialisation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
export module Vaev.Engine:dom.serialisation;

import Karm.Core;
import Karm.Gc;

import :dom.element;
import :dom.comment;
import :dom.document;
import :dom.documentType;

using namespace Karm;

namespace Vaev::Dom {

// MARK: Escaping --------------------------------------------------------------
// https://html.spec.whatwg.org/multipage/parsing.html#escapingString

export void escapeString(Io::Emit& e, Io::SScan& s, bool attributeMode = false) {
while (not s.ended()) {
auto r = s.next();
// Replace any occurrence of the "&" character by the string "&".
if (r == '&')
e("&");

// Replace any occurrences of the U+00A0 NO-BREAK SPACE character by the string " ".
else if (r == U'\xA0')
e(" ");

// Replace any occurrences of the "<" character by the string "&lt;".
else if (r == '<')
e("&lt;");

// Replace any occurrences of the ">" character by the string "&gt;".
else if (r == '>')
e("&gt;");

// If the algorithm was invoked in the attribute mode, then replace any occurrences of the """ character by the string "&quot;".
else if (attributeMode and r == '"')
e("&quot;");

else
e(r);
}
}

export void escapeString(Io::Emit& e, Str str, bool attributeMode = false) {
Io::SScan s{str};
escapeString(e, s, attributeMode);
}

// MARK: Serialize -------------------------------------------------------------
// https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments

// https://html.spec.whatwg.org/multipage/parsing.html#serializes-as-void
bool _serializeAsVoid(Gc::Ref<Node> node) {
auto el = node->is<Element>();
if (not el)
return false;

// For the purposes of the following algorithm, an element serializes as void
// if its element type is one of the void elements, or is basefont, bgsound, frame, keygen, or param.
return el->isVoidElement() or
el->qualifiedName == Html::BASEFONT_TAG or
el->qualifiedName == Html::BGSOUND_TAG or
el->qualifiedName == Html::FRAME_TAG or
el->qualifiedName == Html::KEYGEN_TAG or
el->qualifiedName == Html::PARAM_TAG;
}

// https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-serialisation-algorithm
export void serializeHtmlFragment(Gc::Ref<Node> node, Io::Emit& e) {
// 1. If the node serializes as void, then return the empty string.
if (_serializeAsVoid(node))
return;

// 3. If the node is a template element, then let the node instead be the template element's template contents (a DocumentFragment node).
// TODO: We don't support DocumentFragment

// 4. If current node is a shadow host, then:
// 1. Let shadow be current node's shadow root.
// 2. If serializableShadowRoots is true and shadow’s serializable is true, or shadowRoots contains shadow, then:
// 1. Append "<template shadowrootmode="".
// 2. If shadow’s mode is "open", append "open". Otherwise, append "closed".
// 3. Append """.
// 4. If shadow’s delegates focus is set, append " shadowrootdelegatesfocus=""".
// 5. If shadow’s serializable is set, append " shadowrootserializable=""".
// 6. If shadow’s clonable is set, append " shadowrootclonable=""".
// 7. If current node’s custom element registry is not shadow’s custom element registry, append " shadowrootcustomelementregistry=""".
// 8. Append ">".
// 9. Append the value of running the HTML fragment serialization algorithm with shadow, serializableShadowRoots, and shadowRoots.
// 10. Append "</template>".
// TODO: We don't have shadow dom support

// 5. For each child node of the node, in tree order:
// 1. Let current node be the child node being processed.
for (auto currentNode : node->iterChildren()) {
// 2. Append the appropriate string:
// If current node is an Element:
if (auto el = currentNode->is<Element>()) {
// - Determine tagname: if in HTML, MathML, or SVG namespace, tagname is local name; otherwise qualified name.
// - Append "<" followed by tagname.
e("<{}", el->qualifiedName);

// - If current node has an is value not present as an attribute, append ' is="<escaped is value>"'.
if (auto isValue = el->getAttribute(Html::IS_ATTR)) {
e(" is=\"");
escapeString(e, isValue.unwrap(), true);
e("\"");
}
// - For each attribute:
for (auto& [name, attr] : el->attributes.iterUnordered()) {
if (name == Html::IS_ATTR)
continue;
// Append space, attribute’s serialized name, "=", quote, escaped value, quote.
e(" {}=\"", name);
escapeString(e, attr->value, true);
e("\"");
}
Comment on lines +105 to +118
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete attribute handling logic. On line 105, the code checks for the is attribute value but then inside the loop on line 112 it skips the is attribute if it's in the attributes map. However, if getAttribute returns a value on line 105, that same attribute should still be in the attributes map, leading to redundant attribute output or skipping. The logic should be clarified to avoid potential duplicate attribute serialization.

Copilot uses AI. Check for mistakes.
// - Append ">".
e(">");

// - If current node serializes as void, continue.
if (_serializeAsVoid(currentNode))
continue;

// - Append the value of running this algorithm on current node
serializeHtmlFragment(currentNode, e);

// then "</tagname>".
e("</{}>", el->qualifiedName);
}

// If current node is a Text node:
else if (auto text = currentNode->is<Text>()) {
auto parent = text->parentNode();
// - If its parent is style, script, xmp, iframe, noembed, noframes, plaintext, or (if scripting enabled) noscript,
if (auto parentElement = parent->is<Element>();
parentElement and
(parentElement->qualifiedName == Html::STYLE_TAG or
parentElement->qualifiedName == Html::SCRIPT_TAG or
parentElement->qualifiedName == Html::XMP_TAG or
parentElement->qualifiedName == Html::IFRAME_TAG or
parentElement->qualifiedName == Html::NOEMBED_TAG or
parentElement->qualifiedName == Html::NOFRAMES_TAG or
parentElement->qualifiedName == Html::PLAINTEXT_TAG or
parentElement->qualifiedName == Html::NOSCRIPT_TAG)) {
// append text literally.
e(text->data());
}
// - Otherwise append escaped text.
else {
escapeString(e, text->data());
}
}

// If current node is a Comment:
else if (auto comment = currentNode->is<Comment>()) {
// - Append "<!--" + data + "-->".
e("<!--{}-->", comment->data());
}

// If current node is a ProcessingInstruction:
// - Append "<?" + target + " " + data + ">".
// TODO: We don't support ProcessingInstruction

// If current node is a DocumentType:
else if (auto doctype = currentNode->is<DocumentType>()) {
// - Append "<!DOCTYPE " + name + ">".
e("<!DOCTYPE {}>", doctype->name);
}
}
}

export String serializeHtmlFragment(Gc::Ref<Node> node) {
// 1. If the node serializes as void, then return the empty string.
if (_serializeAsVoid(node))
return ""s;

// 2. Let s be a string, and initialize it to the empty string.
Io::StringWriter sw;
Io::Emit e{sw};

serializeHtmlFragment(node, e);

// 6. Return s.
return sw.take();
}

} // namespace Vaev::Dom
9 changes: 7 additions & 2 deletions src/vaev-engine/dom/window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,16 @@ export struct Window {
} else {
co_return Error::invalidInput("unsupported intent");
}

Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty line at the beginning of the scope should be removed according to coding guidelines.

Copilot generated this review using guidance from repository custom instructions.
invalidateRender();
co_return Ok();
}

[[clang::coro_wrapper]]
Async::Task<> refreshAsync() {
return loadLocationAsync(document()->url());
Copy link

Copilot AI Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null pointer dereference. The upgrade() call on a weak pointer in refreshAsync() (line 68) could return null if the document has been destroyed. Add error handling to check if the upgrade succeeds before calling url() on it.

Suggested change
return loadLocationAsync(document()->url());
auto doc = document();
if (!doc) {
co_return Error::invalidState("No document to refresh");
}
co_return co_await loadLocationAsync(doc->url());

Copilot uses AI. Check for mistakes.
}

Ref::Url location() const {
return _document.upgrade()->url();
}
Expand All @@ -73,8 +79,7 @@ export struct Window {
Driver::RenderResult& ensureRender() {
if (_render)
return *_render;
Vec2Au viewportSize = {_media.width, _media.height};
_render = Driver::render(_document.upgrade(), _media, {.small = viewportSize});
_render = Driver::render(_document.upgrade(), _media, {.small = _media.viewportSize()});
return *_render;
}

Expand Down
4 changes: 4 additions & 0 deletions src/vaev-engine/style/media.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ export struct Media {
.deviceAspectRatio = settings.paper.width / settings.paper.height,
};
}

Vec2Au viewportSize() const {
return {width, height};
}
};

// MARK: Media Features --------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/vaev-engine/style/specified.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export struct SpecifiedValues {
Integer order;
AlignProps aligns;
Display display;
f16 opacity;
f32 opacity;

// Small Field
Float float_ = Float::NONE;
Expand Down
Loading
Loading