Skip to content
Merged
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
5 changes: 5 additions & 0 deletions experiments/WebScene.NativeEngine.Probe/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ if(WEBSCENE_NATIVE_ENGINE_ENABLE_V8)
LABELS "native;css;syntax;resource;security;performance;lifecycle")
add_test(NAME webscene_native_idle_v8_platform COMMAND webscene_native_engine_tests)
add_test(NAME webscene_native_dom_punctuation_keyboard COMMAND webscene_native_engine_tests)
add_test(NAME webscene_native_text_control_select_all COMMAND webscene_native_engine_tests)
add_test(NAME webscene_native_shortcut_activation COMMAND webscene_native_engine_tests)
add_test(NAME webscene_native_select_popup_context COMMAND webscene_native_engine_tests)
add_test(NAME webscene_native_html_select_add COMMAND webscene_native_engine_tests)
Expand Down Expand Up @@ -907,6 +908,10 @@ if(WEBSCENE_NATIVE_ENGINE_ENABLE_V8)
ENVIRONMENT "WEBSCENE_NATIVE_ENGINE_TEST_FILTER=idle-v8-platform")
set_tests_properties(webscene_native_dom_punctuation_keyboard PROPERTIES
ENVIRONMENT "WEBSCENE_NATIVE_ENGINE_TEST_FILTER=dom-punctuation-keyboard")
set_tests_properties(webscene_native_text_control_select_all PROPERTIES
ENVIRONMENT "WEBSCENE_NATIVE_ENGINE_TEST_FILTER=text-control-select-all"
TIMEOUT 30
LABELS "native;runtime;input;security;performance;lifecycle")
set_tests_properties(webscene_native_shortcut_activation PROPERTIES
ENVIRONMENT "WEBSCENE_NATIVE_ENGINE_TEST_FILTER=shortcut-activation"
TIMEOUT 30
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Generated by tools/webidl-v8-bindings/generate.mjs. Do not edit.
// Exposure manifest SHA-256: 062d2720b7d3bba71dc78ccab5b888f0fbeb25a03dc2a9b5c65709abd108bf69
// Exposure manifest SHA-256: 953ce70922f2281f59114019b4cbb112cf9667ab6c3012b53e8d6078d26d9de5
// Inputs: @webref/idl 3.82.1, webidl2 24.5.0.

enum class generated_dom_interface : uint8_t {
Expand Down Expand Up @@ -4342,6 +4342,12 @@ void install_generated_dom_templates(v8::Local<v8::Context>)
isolate, element_set_selection_range, v8::Local<v8::Value>(),
generated_HTMLElement_signature, 2,
v8::ConstructorBehavior::kThrow));
generated_HTMLElement_template->PrototypeTemplate()->Set(
js_string(isolate, "setRangeText"),
v8::FunctionTemplate::New(
isolate, element_set_range_text, v8::Local<v8::Value>(),
generated_HTMLElement_signature, 1,
v8::ConstructorBehavior::kThrow));
generated_HTMLElement_template->PrototypeTemplate()->Set(
js_string(isolate, "click"),
v8::FunctionTemplate::New(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,8 @@ struct dom_node final {
std::string value;
size_t selection_start{0};
size_t selection_end{0};
uint8_t selection_start_utf16_suboffset{0};
uint8_t selection_end_utf16_suboffset{0};
text_selection_direction selection_direction{text_selection_direction::none};
bool selection_explicitly_set{false};
bool selectedness_initialized{false};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,9 @@ inline void set_select_value(dom_node& select,std::string_view value) {
select.mutable_form_control().selection_explicitly_empty=!matched;
}

inline bool is_text_control(const dom_node* node)
inline bool supports_text_selection(const dom_node* node)
{
if (node == nullptr || (node->tag != "input" && node->tag != "textarea")
|| node->attributes.contains("disabled")) {
if (node == nullptr || (node->tag != "input" && node->tag != "textarea")) {
return false;
}
if (node->tag == "textarea") return true;
Expand All @@ -155,6 +154,12 @@ inline bool is_text_control(const dom_node* node)
&& type->second != "color";
}

inline bool is_text_control(const dom_node* node)
{
return supports_text_selection(node)
&& !node->attributes.contains("disabled");
}

// Initialize the live value once from authored markup. No HTML parser is needed.
inline void ensure_text_value(dom_node& node) {
auto& control=node.mutable_form_control();
Expand All @@ -178,6 +183,8 @@ inline void ensure_text_value(dom_node& node) {
}
control.value_initialized=true;
control.selection_start=control.selection_end=control.value.size();
control.selection_start_utf16_suboffset=0U;
control.selection_end_utf16_suboffset=0U;
control.selection_direction=text_selection_direction::none;
}

Expand All @@ -195,4 +202,126 @@ inline size_t next_utf8_boundary(const std::string& value,size_t index) {
return index;
}

inline std::pair<size_t,size_t> utf8_scalar_extent(
std::string_view value,size_t index) {
if(index>=value.size()) return {0U,0U};
const auto first=static_cast<unsigned char>(value[index]);
size_t bytes=1U;
size_t utf16_units=1U;
if((first&0xe0U)==0xc0U) bytes=2U;
else if((first&0xf0U)==0xe0U) bytes=3U;
else if((first&0xf8U)==0xf0U) {bytes=4U;utf16_units=2U;}
if(index+bytes>value.size()) return {1U,1U};
for(size_t offset=1U;offset<bytes;++offset)
if((static_cast<unsigned char>(value[index+offset])&0xc0U)!=0x80U)
return {1U,1U};
return {bytes,utf16_units};
}

// Form-control storage remains byte-oriented so edits never split UTF-8.
// Selection APIs expose browser UTF-16 code-unit offsets at the JS boundary.
struct utf8_selection_offset {
size_t byte_offset{};
uint8_t utf16_suboffset{};
};

inline size_t utf16_offset_from_utf8_selection(
std::string_view value,utf8_selection_offset selection) {
const auto byte_offset=selection.byte_offset;
const auto limit=std::min(byte_offset,value.size());
size_t bytes=0U,units=0U;
while(bytes<limit) {
const auto [length,utf16_units]=utf8_scalar_extent(value,bytes);
if(length==0U || bytes+length>limit) break;
bytes+=length;
units+=utf16_units;
}
if(bytes<value.size()) {
const auto [length,utf16_units]=utf8_scalar_extent(value,bytes);
if(length!=0U)
units+=std::min<size_t>(selection.utf16_suboffset,utf16_units-1U);
}
return units;
}

inline utf8_selection_offset utf8_selection_from_utf16_offset(
std::string_view value,size_t utf16_offset) {
size_t bytes=0U,units=0U;
while(bytes<value.size() && units<utf16_offset) {
const auto [length,utf16_units]=utf8_scalar_extent(value,bytes);
if(length==0U) break;
if(units+utf16_units>utf16_offset)
return {bytes,static_cast<uint8_t>(utf16_offset-units)};
bytes+=length;
units+=utf16_units;
}
return {bytes,0U};
}

inline bool selection_offset_less(
utf8_selection_offset left,utf8_selection_offset right) {
return left.byte_offset<right.byte_offset
|| (left.byte_offset==right.byte_offset
&& left.utf16_suboffset<right.utf16_suboffset);
}

inline bool selection_offset_equal(
utf8_selection_offset left,utf8_selection_offset right) {
return left.byte_offset==right.byte_offset
&& left.utf16_suboffset==right.utf16_suboffset;
}

inline std::vector<uint16_t> utf16_units_from_utf8(std::string_view value) {
std::vector<uint16_t> result;
result.reserve(value.size());
for(size_t index=0;index<value.size();) {
const auto [length,utf16_length]=utf8_scalar_extent(value,index);
if(length==0U) break;
uint32_t scalar=static_cast<unsigned char>(value[index]);
if(length>1U) {
scalar&=length==2U?0x1fU:length==3U?0x0fU:0x07U;
for(size_t offset=1U;offset<length;++offset)
scalar=(scalar<<6U)|(static_cast<unsigned char>(value[index+offset])&0x3fU);
}
if(utf16_length==2U) {
scalar-=0x10000U;
result.push_back(static_cast<uint16_t>(0xd800U+(scalar>>10U)));
result.push_back(static_cast<uint16_t>(0xdc00U+(scalar&0x3ffU)));
} else result.push_back(static_cast<uint16_t>(scalar));
index+=length;
}
return result;
}

inline void append_wtf8_scalar(std::string& result,uint32_t scalar) {
if(scalar<0x80U) result.push_back(static_cast<char>(scalar));
else if(scalar<0x800U) {
result.push_back(static_cast<char>(0xc0U|(scalar>>6U)));
result.push_back(static_cast<char>(0x80U|(scalar&0x3fU)));
} else if(scalar<0x10000U) {
result.push_back(static_cast<char>(0xe0U|(scalar>>12U)));
result.push_back(static_cast<char>(0x80U|((scalar>>6U)&0x3fU)));
result.push_back(static_cast<char>(0x80U|(scalar&0x3fU)));
} else {
result.push_back(static_cast<char>(0xf0U|(scalar>>18U)));
result.push_back(static_cast<char>(0x80U|((scalar>>12U)&0x3fU)));
result.push_back(static_cast<char>(0x80U|((scalar>>6U)&0x3fU)));
result.push_back(static_cast<char>(0x80U|(scalar&0x3fU)));
}
}

inline std::string wtf8_from_utf16_units(const std::vector<uint16_t>& units) {
std::string result;
result.reserve(units.size()*3U);
for(size_t index=0;index<units.size();++index) {
uint32_t scalar=units[index];
if(scalar>=0xd800U && scalar<=0xdbffU && index+1U<units.size()
&& units[index+1U]>=0xdc00U && units[index+1U]<=0xdfffU) {
scalar=0x10000U+((scalar-0xd800U)<<10U)+(units[++index]-0xdc00U);
}
append_wtf8_scalar(result,scalar);
}
return result;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,9 @@ struct v8_dom_runtime::implementation final {
element->PrototypeTemplate()->Set(
js_string(isolate, "setSelectionRange"),
v8::FunctionTemplate::New(isolate, element_set_selection_range));
element->PrototypeTemplate()->Set(
js_string(isolate, "setRangeText"),
v8::FunctionTemplate::New(isolate, element_set_range_text));
element->PrototypeTemplate()->Set(
js_string(isolate, "click"),
v8::FunctionTemplate::New(isolate, element_click));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,8 @@
control.value_initialized = true;
control.selection_start = value.size();
control.selection_end = value.size();
control.selection_start_utf16_suboffset = 0U;
control.selection_end_utf16_suboffset = 0U;
control.selection_direction = text_selection_direction::none;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4443,6 +4443,83 @@
}
}

static void element_set_range_text(const v8::FunctionCallbackInfo<v8::Value>& info)
{
auto* self = current(info.GetIsolate());
auto* node = unwrap_node(info.This());
if (node == nullptr || !forms::supports_text_selection(node)) return;
forms::ensure_text_value(*node);
auto& control = node->mutable_form_control();
const auto old_start = forms::utf16_offset_from_utf8_selection(
control.value,
{control.selection_start, control.selection_start_utf16_suboffset});
const auto old_end = forms::utf16_offset_from_utf8_selection(
control.value,
{control.selection_end, control.selection_end_utf16_suboffset});
const auto replacement = info.Length() > 0
? forms::utf16_units_from_utf8(to_wtf8(info.GetIsolate(), info[0]))
: std::vector<uint16_t>{};
auto start = old_start;
auto end = old_end;
const auto context = info.GetIsolate()->GetCurrentContext();
if (info.Length() >= 3) {
start = static_cast<size_t>(std::max(
0, info[1]->Int32Value(context).FromMaybe(0)));
end = static_cast<size_t>(std::max(
0, info[2]->Int32Value(context).FromMaybe(0)));
}
auto units = forms::utf16_units_from_utf8(control.value);
start = std::min(start, units.size());
end = std::min(end, units.size());
if (start > end) std::swap(start, end);
units.erase(
units.begin() + static_cast<std::ptrdiff_t>(start),
units.begin() + static_cast<std::ptrdiff_t>(end));
units.insert(
units.begin() + static_cast<std::ptrdiff_t>(start),
replacement.begin(), replacement.end());
control.value = forms::wtf8_from_utf16_units(units);
control.value_initialized = true;
control.dirty_value = true;

const auto replacement_end = start + replacement.size();
auto selection_start = old_start;
auto selection_end = old_end;
const auto mode = info.Length() > 3
? to_utf8(info.GetIsolate(), info[3])
: std::string{"preserve"};
if (mode == "select") {
selection_start = start;
selection_end = replacement_end;
} else if (mode == "start") {
selection_start = selection_end = start;
} else if (mode == "end") {
selection_start = selection_end = replacement_end;
} else {
const auto delta = static_cast<std::ptrdiff_t>(replacement.size())
- static_cast<std::ptrdiff_t>(end - start);
const auto adjust = [&](size_t position) {
if (position <= start) return position;
if (position >= end) return static_cast<size_t>(
static_cast<std::ptrdiff_t>(position) + delta);
return start;
};
selection_start = adjust(old_start);
selection_end = adjust(old_end);
}
const auto start_offset = forms::utf8_selection_from_utf16_offset(
control.value, selection_start);
const auto end_offset = forms::utf8_selection_from_utf16_offset(
control.value, selection_end);
control.selection_start = start_offset.byte_offset;
control.selection_start_utf16_suboffset = start_offset.utf16_suboffset;
control.selection_end = end_offset.byte_offset;
control.selection_end_utf16_suboffset = end_offset.utf16_suboffset;
control.selection_direction = text_selection_direction::none;
control.selection_explicitly_set = true;
self->document.mark_dirty();
}

static void element_select(const v8::FunctionCallbackInfo<v8::Value>& info)
{
auto* self = current(info.GetIsolate());
Expand All @@ -4458,7 +4535,8 @@
local_context,
js_string(info.GetIsolate(), "value")).ToLocal(&value)
&& !value->IsNullOrUndefined()) {
length = static_cast<int>(to_utf8(info.GetIsolate(), value).size());
length = static_cast<int>(decode_wtf8_to_utf16(
to_wtf8(info.GetIsolate(), value)).size());
}
v8::Local<v8::Value> arguments[] = {
v8::Integer::New(info.GetIsolate(), 0),
Expand Down
Loading
Loading