url: add URLPattern implementation

Co-authored-by: Daniel Lemire <daniel@lemire.me>
PR-URL: https://github.com/nodejs/node/pull/56452
Reviewed-By: Daniel Lemire <daniel@lemire.me>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Jordan Harband <ljharb@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
This commit is contained in:
Yagiz Nizipli 2025-01-30 14:38:19 -05:00 committed by Node.js GitHub Bot
parent 063e67c2ff
commit 43c593c428
14 changed files with 1073 additions and 0 deletions

View File

@ -2118,6 +2118,13 @@ constructor][`new URL(input)`] or the legacy [`url.parse()`][] to be parsed.
The thrown error object typically has an additional property `'input'` that
contains the URL that failed to parse.
<a id="ERR_INVALID_URL_PATTERN"></a>
### `ERR_INVALID_URL_PATTERN`
An invalid URLPattern was passed to the [WHATWG][WHATWG URL API] \[`URLPattern`
constructor]\[`new URLPattern(input)`] to be parsed.
<a id="ERR_INVALID_URL_SCHEME"></a>
### `ERR_INVALID_URL_SCHEME`

View File

@ -714,6 +714,129 @@ Parses a string as a URL. If `base` is provided, it will be used as the base
URL for the purpose of resolving non-absolute `input` URLs. Returns `null`
if `input` is not a valid.
### Class: `URLPattern`
> Stability: 1 - Experimental
<!-- YAML
added: REPLACEME
-->
The `URLPattern` API provides an interface to match URLs or parts of URLs
against a pattern.
```js
const myPattern = new URLPattern('https://nodejs.org/docs/latest/api/*.html');
console.log(myPattern.exec('https://nodejs.org/docs/latest/api/dns.html'));
// Prints:
// {
// "hash": { "groups": { "0": "" }, "input": "" },
// "hostname": { "groups": {}, "input": "nodejs.org" },
// "inputs": [
// "https://nodejs.org/docs/latest/api/dns.html"
// ],
// "password": { "groups": { "0": "" }, "input": "" },
// "pathname": { "groups": { "0": "dns" }, "input": "/docs/latest/api/dns.html" },
// "port": { "groups": {}, "input": "" },
// "protocol": { "groups": {}, "input": "https" },
// "search": { "groups": { "0": "" }, "input": "" },
// "username": { "groups": { "0": "" }, "input": "" }
// }
console.log(myPattern.test('https://nodejs.org/docs/latest/api/dns.html'));
// Prints: true
```
#### `new URLPattern()`
Instantiate a new empty `URLPattern` object.
#### `new URLPattern(string[, baseURL][, options])`
* `string` {string} A URL string
* `baseURL` {string | undefined} A base URL string
* `options` {Object} Options
Parse the `string` as a URL, and use it to instantiate a new
`URLPattern` object.
If `baseURL` is not specified, it defaults to `undefined`.
An option can have `ignoreCase` boolean attribute which enables
case-insensitive matching if set to true.
The constructor can throw a `TypeError` to indicate parsing failure.
#### `new URLPattern(objg[, baseURL][, options])`
* `obj` {Object} An input pattern
* `baseURL` {string | undefined} A base URL string
* `options` {Object} Options
Parse the `Object` as an input pattern, and use it to instantiate a new
`URLPattern` object. The object members can be any of `protocol`, `username`,
`password`, `hostname`, `port`, `pathname`, `search`, `hash` or `baseURL`.
If `baseURL` is not specified, it defaults to `undefined`.
An option can have `ignoreCase` boolean attribute which enables
case-insensitive matching if set to true.
The constructor can throw a `TypeError` to indicate parsing failure.
#### `urlPattern.exec(input[, baseURL])`
* `input` {string | Object} A URL or URL parts
* `baseURL` {string | undefined} A base URL string
Input can be a string or an object providing the individual URL parts. The
object members can be any of `protocol`, `username`, `password`, `hostname`,
`port`, `pathname`, `search`, `hash` or `baseURL`.
If `baseURL` is not specified, it will default to `undefined`.
Returns an object with an `inputs` key containing the array of arguments
passed into the function and keys of the URL components which contains the
matched input and matched groups.
```js
const myPattern = new URLPattern('https://nodejs.org/docs/latest/api/*.html');
console.log(myPattern.exec('https://nodejs.org/docs/latest/api/dns.html'));
// Prints:
// {
// "hash": { "groups": { "0": "" }, "input": "" },
// "hostname": { "groups": {}, "input": "nodejs.org" },
// "inputs": [
// "https://nodejs.org/docs/latest/api/dns.html"
// ],
// "password": { "groups": { "0": "" }, "input": "" },
// "pathname": { "groups": { "0": "dns" }, "input": "/docs/latest/api/dns.html" },
// "port": { "groups": {}, "input": "" },
// "protocol": { "groups": {}, "input": "https" },
// "search": { "groups": { "0": "" }, "input": "" },
// "username": { "groups": { "0": "" }, "input": "" }
// }
```
#### `urlPattern.test(input[, baseURL])`
* `input` {string | Object} A URL or URL parts
* `baseURL` {string | undefined} A base URL string
Input can be a string or an object providing the individual URL parts. The
object members can be any of `protocol`, `username`, `password`, `hostname`,
`port`, `pathname`, `search`, `hash` or `baseURL`.
If `baseURL` is not specified, it will default to `undefined`.
Returns a boolean indicating if the input matches the current pattern.
```js
const myPattern = new URLPattern('https://nodejs.org/docs/latest/api/*.html');
console.log(myPattern.test('https://nodejs.org/docs/latest/api/dns.html'));
// Prints: true
```
### Class: `URLSearchParams`
<!-- YAML

View File

@ -32,6 +32,7 @@ const {
decodeURIComponent,
} = primordials;
const { URLPattern } = internalBinding('url_pattern');
const { inspect } = require('internal/util/inspect');
const {
encodeStr,
@ -1574,6 +1575,7 @@ module.exports = {
toPathIfFileURL,
installObjectURLMethods,
URL,
URLPattern,
URLSearchParams,
URLParse: URL.parse,
domainToASCII,

View File

@ -30,6 +30,7 @@ const {
decodeURIComponent,
} = primordials;
const { URLPattern } = internalBinding('url_pattern');
const { toASCII } = internalBinding('encoding_binding');
const { encodeStr, hexTable } = require('internal/querystring');
const querystring = require('querystring');
@ -1029,6 +1030,7 @@ module.exports = {
// WHATWG API
URL,
URLPattern,
URLSearchParams,
domainToASCII,
domainToUnicode,

View File

@ -146,6 +146,7 @@
'src/node_trace_events.cc',
'src/node_types.cc',
'src/node_url.cc',
'src/node_url_pattern.cc',
'src/node_util.cc',
'src/node_v8.cc',
'src/node_wasi.cc',
@ -275,6 +276,7 @@
'src/node_stat_watcher.h',
'src/node_union_bytes.h',
'src/node_url.h',
'src/node_url_pattern.h',
'src/node_version.h',
'src/node_v8.h',
'src/node_v8_platform-inl.h',

View File

@ -78,6 +78,7 @@
V(async_ids_stack_string, "async_ids_stack") \
V(attributes_string, "attributes") \
V(base_string, "base") \
V(base_url_string, "baseURL") \
V(bits_string, "bits") \
V(block_list_string, "blockList") \
V(buffer_string, "buffer") \
@ -179,6 +180,9 @@
V(get_data_clone_error_string, "_getDataCloneError") \
V(get_shared_array_buffer_id_string, "_getSharedArrayBufferId") \
V(gid_string, "gid") \
V(groups_string, "groups") \
V(has_regexp_groups_string, "hasRegExpGroups") \
V(hash_string, "hash") \
V(h2_string, "h2") \
V(handle_string, "handle") \
V(hash_algorithm_string, "hashAlgorithm") \
@ -186,13 +190,16 @@
V(homedir_string, "homedir") \
V(host_string, "host") \
V(hostmaster_string, "hostmaster") \
V(hostname_string, "hostname") \
V(http_1_1_string, "http/1.1") \
V(id_string, "id") \
V(identity_string, "identity") \
V(ignore_case_string, "ignoreCase") \
V(ignore_string, "ignore") \
V(infoaccess_string, "infoAccess") \
V(inherit_string, "inherit") \
V(input_string, "input") \
V(inputs_string, "inputs") \
V(internal_binding_string, "internalBinding") \
V(internal_string, "internal") \
V(ipv4_string, "IPv4") \
@ -280,6 +287,7 @@
V(parse_error_string, "Parse Error") \
V(password_string, "password") \
V(path_string, "path") \
V(pathname_string, "pathname") \
V(pending_handle_string, "pendingHandle") \
V(permission_string, "permission") \
V(pid_string, "pid") \
@ -295,6 +303,7 @@
V(priority_string, "priority") \
V(process_string, "process") \
V(promise_string, "promise") \
V(protocol_string, "protocol") \
V(prototype_string, "prototype") \
V(psk_string, "psk") \
V(pubkey_string, "pubkey") \
@ -323,6 +332,7 @@
V(scopeid_string, "scopeid") \
V(script_id_string, "scriptId") \
V(script_name_string, "scriptName") \
V(search_string, "search") \
V(serial_number_string, "serialNumber") \
V(serial_string, "serial") \
V(servername_string, "servername") \

View File

@ -4,6 +4,7 @@
#include "node_builtins.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_url_pattern.h"
#include "util.h"
#include <string>
@ -87,6 +88,7 @@
V(types) \
V(udp_wrap) \
V(url) \
V(url_pattern) \
V(util) \
V(uv) \
V(v8) \

View File

@ -90,6 +90,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_INVALID_STATE, Error) \
V(ERR_INVALID_THIS, TypeError) \
V(ERR_INVALID_URL, TypeError) \
V(ERR_INVALID_URL_PATTERN, TypeError) \
V(ERR_INVALID_URL_SCHEME, TypeError) \
V(ERR_LOAD_SQLITE_EXTENSION, Error) \
V(ERR_MEMORY_ALLOCATION_FAILED, Error) \
@ -99,6 +100,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_MISSING_PLATFORM_FOR_WORKER, Error) \
V(ERR_MODULE_NOT_FOUND, Error) \
V(ERR_NON_CONTEXT_AWARE_DISABLED, Error) \
V(ERR_OPERATION_FAILED, TypeError) \
V(ERR_OUT_OF_RANGE, RangeError) \
V(ERR_REQUIRE_ASYNC_MODULE, Error) \
V(ERR_SCRIPT_EXECUTION_INTERRUPTED, Error) \

View File

@ -180,6 +180,7 @@ class ExternalReferenceRegistry {
V(tty_wrap) \
V(udp_wrap) \
V(url) \
V(url_pattern) \
V(util) \
V(pipe_wrap) \
V(sea) \

787
src/node_url_pattern.cc Normal file
View File

@ -0,0 +1,787 @@
#include "node_url_pattern.h"
#include "base_object-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "node.h"
#include "node_errors.h"
#include "node_mem-inl.h"
#include "path.h"
#include "util-inl.h"
namespace node::url_pattern {
using v8::Array;
using v8::Context;
using v8::DontDelete;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Global;
using v8::Isolate;
using v8::Local;
using v8::LocalVector;
using v8::MaybeLocal;
using v8::Name;
using v8::NewStringType;
using v8::Object;
using v8::PropertyAttribute;
using v8::ReadOnly;
using v8::RegExp;
using v8::String;
using v8::Value;
std::optional<URLPatternRegexProvider::regex_type>
URLPatternRegexProvider::create_instance(std::string_view pattern,
bool ignore_case) {
auto isolate = Isolate::GetCurrent();
auto env = Environment::GetCurrent(isolate);
int flags = RegExp::Flags::kUnicodeSets;
if (ignore_case) {
flags |= static_cast<int>(RegExp::Flags::kIgnoreCase);
}
Local<String> local_pattern;
if (!String::NewFromUtf8(
isolate, pattern.data(), NewStringType::kNormal, pattern.size())
.ToLocal(&local_pattern)) {
return std::nullopt;
}
Local<RegExp> regexp;
if (!RegExp::New(
env->context(), local_pattern, static_cast<RegExp::Flags>(flags))
.ToLocal(&regexp)) {
return std::nullopt;
}
return Global<RegExp>(isolate, regexp);
}
bool URLPatternRegexProvider::regex_match(std::string_view input,
const regex_type& pattern) {
auto isolate = Isolate::GetCurrent();
auto env = Environment::GetCurrent(isolate);
Local<String> local_input;
if (!String::NewFromUtf8(
isolate, input.data(), NewStringType::kNormal, input.size())
.ToLocal(&local_input)) {
return false;
}
Local<Object> result_object;
if (!pattern.Get(isolate)
->Exec(env->context(), local_input)
.ToLocal(&result_object)) {
return false;
}
// RegExp::Exec returns null if there is no match.
return !result_object->IsNull();
}
std::optional<std::vector<std::optional<std::string>>>
URLPatternRegexProvider::regex_search(std::string_view input,
const regex_type& global_pattern) {
auto isolate = Isolate::GetCurrent();
auto env = Environment::GetCurrent(isolate);
Local<String> local_input;
if (!String::NewFromUtf8(
isolate, input.data(), NewStringType::kNormal, input.size())
.ToLocal(&local_input)) {
return std::nullopt;
}
Local<Object> exec_result_object;
auto pattern = global_pattern.Get(isolate);
if (!pattern->Exec(env->context(), local_input)
.ToLocal(&exec_result_object) ||
exec_result_object->IsNull()) {
return std::nullopt;
}
DCHECK(exec_result_object->IsArray());
auto exec_result = exec_result_object.As<Array>();
size_t len = exec_result->Length();
std::vector<std::optional<std::string>> result;
result.reserve(len);
for (size_t i = 1; i < len; i++) {
Local<Value> entry;
if (!exec_result->Get(env->context(), i).ToLocal(&entry)) {
return std::nullopt;
}
if (entry->IsUndefined()) {
result.emplace_back(std::nullopt);
} else if (entry->IsString()) {
Utf8Value utf8_entry(isolate, entry.As<String>());
result.emplace_back(utf8_entry.ToString());
} else {
UNREACHABLE("v8::RegExp::Exec return a non-string, non-undefined value.");
}
}
return result;
}
URLPattern::URLPattern(Environment* env,
Local<Object> object,
ada::url_pattern<URLPatternRegexProvider>&& url_pattern)
: BaseObject(env, object), url_pattern_(std::move(url_pattern)) {
MakeWeak();
}
void URLPattern::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackFieldWithSize("protocol", url_pattern_.get_protocol().size());
tracker->TrackFieldWithSize("username", url_pattern_.get_username().size());
tracker->TrackFieldWithSize("password", url_pattern_.get_password().size());
tracker->TrackFieldWithSize("hostname", url_pattern_.get_hostname().size());
tracker->TrackFieldWithSize("pathname", url_pattern_.get_pathname().size());
tracker->TrackFieldWithSize("search", url_pattern_.get_search().size());
tracker->TrackFieldWithSize("hash", url_pattern_.get_hash().size());
}
void URLPattern::New(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (!args.IsConstructCall()) {
THROW_ERR_CONSTRUCT_CALL_REQUIRED(env);
return;
}
// If no arguments are passed, then we parse the empty URLPattern.
if (args.Length() == 0) {
auto init = ada::url_pattern_init{};
auto url_pattern = ada::parse_url_pattern<URLPatternRegexProvider>(init);
CHECK(url_pattern);
new URLPattern(env, args.This(), std::move(*url_pattern));
return;
}
std::optional<ada::url_pattern_init> init{};
std::optional<std::string> input{};
std::optional<std::string> base_url{};
std::optional<ada::url_pattern_options> options{};
// Following patterns are supported:
// - new URLPattern(input)
// - new URLPattern(input, baseURL)
// - new URLPattern(input, options)
// - new URLPattern(input, baseURL, options)
if (args[0]->IsString()) {
BufferValue input_buffer(env->isolate(), args[0]);
CHECK_NOT_NULL(*input_buffer);
input = input_buffer.ToString();
} else if (args[0]->IsObject()) {
init = URLPatternInit::FromJsObject(env, args[0].As<Object>());
} else {
THROW_ERR_INVALID_ARG_TYPE(env, "Input must be an object or a string");
return;
}
// The next argument can be baseURL or options.
if (args.Length() > 1) {
if (args[1]->IsString()) {
BufferValue base_url_buffer(env->isolate(), args[1]);
CHECK_NOT_NULL(*base_url_buffer);
base_url = base_url_buffer.ToString();
} else if (args[1]->IsObject()) {
CHECK(!options.has_value());
options = URLPatternOptions::FromJsObject(env, args[1].As<Object>());
if (!options) {
THROW_ERR_INVALID_ARG_TYPE(env, "options.ignoreCase must be a boolean");
return;
}
} else {
THROW_ERR_MISSING_ARGS(env, "baseURL or options must be provided");
return;
}
// Only remaining argument can be options.
if (args.Length() > 2) {
if (!args[2]->IsObject()) {
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return;
}
CHECK(!options.has_value());
options = URLPatternOptions::FromJsObject(env, args[2].As<Object>());
if (!options) {
THROW_ERR_INVALID_ARG_TYPE(env, "options.ignoreCase must be a boolean");
return;
}
}
}
// Either url_pattern_init or input as a string must be provided.
CHECK_IMPLIES(init.has_value(), !input.has_value());
std::string_view base_url_view{};
if (base_url) base_url_view = {base_url->data(), base_url->size()};
ada::url_pattern_input arg0;
if (init.has_value()) {
arg0 = std::move(*init);
} else {
arg0 = std::move(*input);
}
auto url_pattern = parse_url_pattern<URLPatternRegexProvider>(
arg0,
base_url ? &base_url_view : nullptr,
options.has_value() ? &options.value() : nullptr);
if (!url_pattern) {
THROW_ERR_INVALID_URL_PATTERN(env, "Failed to constuct URLPattern");
return;
}
new URLPattern(env, args.This(), std::move(*url_pattern));
}
MaybeLocal<Value> URLPattern::URLPatternInit::ToJsObject(
Environment* env, const ada::url_pattern_init& init) {
auto isolate = env->isolate();
auto context = env->context();
auto result = Object::New(isolate);
if (init.protocol) {
Local<Value> protocol;
if (!ToV8Value(context, *init.protocol).ToLocal(&protocol) ||
result->Set(context, env->protocol_string(), protocol).IsNothing()) {
return {};
}
}
if (init.username) {
Local<Value> username;
if (!ToV8Value(context, *init.username).ToLocal(&username) ||
result->Set(context, env->username_string(), username).IsNothing()) {
return {};
}
}
if (init.password) {
Local<Value> password;
if (!ToV8Value(context, *init.password).ToLocal(&password) ||
result->Set(context, env->password_string(), password).IsNothing()) {
return {};
}
}
if (init.hostname) {
Local<Value> hostname;
if (!ToV8Value(context, *init.hostname).ToLocal(&hostname) ||
result->Set(context, env->hostname_string(), hostname).IsNothing()) {
return {};
}
}
if (init.port) {
Local<Value> port;
if (!ToV8Value(context, *init.port).ToLocal(&port) ||
result->Set(context, env->port_string(), port).IsNothing()) {
return {};
}
}
if (init.pathname) {
Local<Value> pathname;
if (!ToV8Value(context, *init.pathname).ToLocal(&pathname) ||
result->Set(context, env->pathname_string(), pathname).IsNothing()) {
return {};
}
}
if (init.search) {
Local<Value> search;
if (!ToV8Value(context, *init.search).ToLocal(&search) ||
result->Set(context, env->search_string(), search).IsNothing()) {
return {};
}
}
if (init.hash) {
Local<Value> hash;
if (!ToV8Value(context, *init.hash).ToLocal(&hash) ||
result->Set(context, env->hash_string(), hash).IsNothing()) {
return {};
}
}
if (init.base_url) {
Local<Value> base_url;
if (!ToV8Value(context, *init.base_url).ToLocal(&base_url) ||
result->Set(context, env->base_url_string(), base_url).IsNothing()) {
return {};
}
}
return result;
}
ada::url_pattern_init URLPattern::URLPatternInit::FromJsObject(
Environment* env, Local<Object> obj) {
ada::url_pattern_init init{};
Local<String> components[] = {
env->protocol_string(),
env->username_string(),
env->password_string(),
env->hostname_string(),
env->port_string(),
env->pathname_string(),
env->search_string(),
env->hash_string(),
env->base_url_string(),
};
auto isolate = env->isolate();
const auto set_parameter = [&](std::string_view key, std::string_view value) {
if (key == "protocol") {
init.protocol = std::string(value);
} else if (key == "username") {
init.username = std::string(value);
} else if (key == "password") {
init.password = std::string(value);
} else if (key == "hostname") {
init.hostname = std::string(value);
} else if (key == "port") {
init.port = std::string(value);
} else if (key == "pathname") {
init.pathname = std::string(value);
} else if (key == "search") {
init.search = std::string(value);
} else if (key == "hash") {
init.hash = std::string(value);
} else if (key == "baseURL") {
init.base_url = std::string(value);
}
};
Local<Value> value;
for (const auto& component : components) {
Utf8Value key(isolate, component);
if (obj->Get(env->context(), component).ToLocal(&value)) {
if (value->IsString()) {
Utf8Value utf8_value(isolate, value);
set_parameter(key.ToStringView(), utf8_value.ToStringView());
}
}
}
return init;
}
MaybeLocal<Object> URLPattern::URLPatternComponentResult::ToJSObject(
Environment* env, const ada::url_pattern_component_result& result) {
auto isolate = env->isolate();
auto context = env->context();
auto parsed_group = Object::New(isolate);
for (const auto& [group_key, group_value] : result.groups) {
Local<String> key;
if (!String::NewFromUtf8(isolate,
group_key.c_str(),
NewStringType::kNormal,
group_key.size())
.ToLocal(&key)) {
return {};
}
Local<Value> value;
if (group_value) {
if (!ToV8Value(env->context(), *group_value).ToLocal(&value)) {
return {};
}
} else {
value = Undefined(isolate);
}
USE(parsed_group->Set(context, key, value));
}
Local<Value> input;
if (!ToV8Value(env->context(), result.input).ToLocal(&input)) {
return {};
}
Local<Name> names[] = {env->input_string(), env->groups_string()};
Local<Value> values[] = {input, parsed_group};
DCHECK_EQ(arraysize(names), arraysize(values));
return Object::New(
isolate, Object::New(isolate), names, values, arraysize(names));
}
MaybeLocal<Value> URLPattern::URLPatternResult::ToJSValue(
Environment* env, const ada::url_pattern_result& result) {
auto isolate = env->isolate();
Local<Name> names[] = {
env->inputs_string(),
env->protocol_string(),
env->username_string(),
env->password_string(),
env->hostname_string(),
env->port_string(),
env->pathname_string(),
env->search_string(),
env->hash_string(),
};
LocalVector<Value> inputs(isolate, result.inputs.size());
size_t index = 0;
for (auto& input : result.inputs) {
if (std::holds_alternative<std::string_view>(input)) {
auto input_str = std::get<std::string_view>(input);
if (!ToV8Value(env->context(), input_str).ToLocal(&inputs[index])) {
return {};
}
} else {
DCHECK(std::holds_alternative<ada::url_pattern_init>(input));
auto init = std::get<ada::url_pattern_init>(input);
if (!URLPatternInit::ToJsObject(env, init).ToLocal(&inputs[index])) {
return {};
}
}
index++;
}
LocalVector<Value> values(isolate, arraysize(names));
values[0] = Array::New(isolate, inputs.data(), inputs.size());
if (!URLPatternComponentResult::ToJSObject(env, result.protocol)
.ToLocal(&values[1])) {
return {};
}
if (!URLPatternComponentResult::ToJSObject(env, result.username)
.ToLocal(&values[2])) {
return {};
}
if (!URLPatternComponentResult::ToJSObject(env, result.password)
.ToLocal(&values[3])) {
return {};
}
if (!URLPatternComponentResult::ToJSObject(env, result.hostname)
.ToLocal(&values[4])) {
return {};
}
if (!URLPatternComponentResult::ToJSObject(env, result.port)
.ToLocal(&values[5])) {
return {};
}
if (!URLPatternComponentResult::ToJSObject(env, result.pathname)
.ToLocal(&values[6])) {
return {};
}
if (!URLPatternComponentResult::ToJSObject(env, result.search)
.ToLocal(&values[7])) {
return {};
}
if (!URLPatternComponentResult::ToJSObject(env, result.hash)
.ToLocal(&values[8])) {
return {};
}
return Object::New(
isolate, Object::New(isolate), names, values.data(), values.size());
}
std::optional<ada::url_pattern_options>
URLPattern::URLPatternOptions::FromJsObject(Environment* env,
Local<Object> obj) {
ada::url_pattern_options options{};
Local<Value> ignore_case;
if (obj->Get(env->context(), env->ignore_case_string())
.ToLocal(&ignore_case)) {
if (!ignore_case->IsBoolean()) {
return std::nullopt;
}
options.ignore_case = ignore_case->IsTrue();
}
return options;
}
MaybeLocal<Value> URLPattern::Hash() const {
auto context = env()->context();
return ToV8Value(context, url_pattern_.get_hash());
}
MaybeLocal<Value> URLPattern::Hostname() const {
auto context = env()->context();
return ToV8Value(context, url_pattern_.get_hostname());
}
MaybeLocal<Value> URLPattern::Password() const {
auto context = env()->context();
return ToV8Value(context, url_pattern_.get_password());
}
MaybeLocal<Value> URLPattern::Pathname() const {
auto context = env()->context();
return ToV8Value(context, url_pattern_.get_pathname());
}
MaybeLocal<Value> URLPattern::Port() const {
auto context = env()->context();
return ToV8Value(context, url_pattern_.get_port());
}
MaybeLocal<Value> URLPattern::Protocol() const {
auto context = env()->context();
return ToV8Value(context, url_pattern_.get_protocol());
}
MaybeLocal<Value> URLPattern::Search() const {
auto context = env()->context();
return ToV8Value(context, url_pattern_.get_search());
}
MaybeLocal<Value> URLPattern::Username() const {
auto context = env()->context();
return ToV8Value(context, url_pattern_.get_username());
}
bool URLPattern::HasRegExpGroups() const {
return url_pattern_.has_regexp_groups();
}
// Instance methods
MaybeLocal<Value> URLPattern::Exec(Environment* env,
const ada::url_pattern_input& input,
std::optional<std::string_view>& baseURL) {
if (auto result = url_pattern_.exec(input, baseURL ? &*baseURL : nullptr)) {
if (result->has_value()) {
return URLPatternResult::ToJSValue(env, result->value());
}
return Null(env->isolate());
}
return {};
}
bool URLPattern::Test(Environment* env,
const ada::url_pattern_input& input,
std::optional<std::string_view>& baseURL) {
if (auto result = url_pattern_.test(input, baseURL ? &*baseURL : nullptr)) {
return *result;
}
THROW_ERR_OPERATION_FAILED(env, "Failed to test URLPattern");
return false;
}
// V8 Methods
void URLPattern::Exec(const FunctionCallbackInfo<Value>& args) {
URLPattern* url_pattern;
ASSIGN_OR_RETURN_UNWRAP(&url_pattern, args.This());
auto env = Environment::GetCurrent(args);
ada::url_pattern_input input;
std::optional<std::string> baseURL{};
std::string input_base;
if (args.Length() == 0) {
input = ada::url_pattern_init{};
} else if (args[0]->IsString()) {
Utf8Value input_value(env->isolate(), args[0].As<String>());
input_base = input_value.ToString();
input = std::string_view(input_base);
} else if (args[0]->IsObject()) {
input = URLPatternInit::FromJsObject(env, args[0].As<Object>());
} else {
THROW_ERR_INVALID_ARG_TYPE(
env, "URLPattern input needs to be a string or an object");
return;
}
if (args.Length() > 1) {
if (!args[1]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(env, "baseURL must be a string");
return;
}
Utf8Value base_url_value(env->isolate(), args[1].As<String>());
baseURL = base_url_value.ToStringView();
}
Local<Value> result;
std::optional<std::string_view> baseURL_opt =
baseURL ? std::optional<std::string_view>(*baseURL) : std::nullopt;
if (!url_pattern->Exec(env, input, baseURL_opt).ToLocal(&result)) {
THROW_ERR_OPERATION_FAILED(env, "Failed to exec URLPattern");
return;
}
args.GetReturnValue().Set(result);
}
void URLPattern::Test(const FunctionCallbackInfo<Value>& args) {
URLPattern* url_pattern;
ASSIGN_OR_RETURN_UNWRAP(&url_pattern, args.This());
auto env = Environment::GetCurrent(args);
ada::url_pattern_input input;
std::optional<std::string> baseURL{};
std::string input_base;
if (args.Length() == 0) {
input = ada::url_pattern_init{};
} else if (args[0]->IsString()) {
Utf8Value input_value(env->isolate(), args[0].As<String>());
input_base = input_value.ToString();
input = std::string_view(input_base);
} else if (args[0]->IsObject()) {
input = URLPatternInit::FromJsObject(env, args[0].As<Object>());
} else {
THROW_ERR_INVALID_ARG_TYPE(
env, "URLPattern input needs to be a string or an object");
return;
}
if (args.Length() > 1) {
if (!args[1]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(env, "baseURL must be a string");
return;
}
Utf8Value base_url_value(env->isolate(), args[1].As<String>());
baseURL = base_url_value.ToStringView();
}
std::optional<std::string_view> baseURL_opt =
baseURL ? std::optional<std::string_view>(*baseURL) : std::nullopt;
args.GetReturnValue().Set(url_pattern->Test(env, input, baseURL_opt));
}
void URLPattern::Protocol(const FunctionCallbackInfo<Value>& info) {
URLPattern* url_pattern;
ASSIGN_OR_RETURN_UNWRAP(&url_pattern, info.This());
Local<Value> result;
if (url_pattern->Protocol().ToLocal(&result)) {
info.GetReturnValue().Set(result);
}
}
void URLPattern::Username(const FunctionCallbackInfo<Value>& info) {
URLPattern* url_pattern;
ASSIGN_OR_RETURN_UNWRAP(&url_pattern, info.This());
Local<Value> result;
if (url_pattern->Username().ToLocal(&result)) {
info.GetReturnValue().Set(result);
}
}
void URLPattern::Password(const FunctionCallbackInfo<Value>& info) {
URLPattern* url_pattern;
ASSIGN_OR_RETURN_UNWRAP(&url_pattern, info.This());
Local<Value> result;
if (url_pattern->Password().ToLocal(&result)) {
info.GetReturnValue().Set(result);
}
}
void URLPattern::Hostname(const FunctionCallbackInfo<Value>& info) {
URLPattern* url_pattern;
ASSIGN_OR_RETURN_UNWRAP(&url_pattern, info.This());
Local<Value> result;
if (url_pattern->Hostname().ToLocal(&result)) {
info.GetReturnValue().Set(result);
}
}
void URLPattern::Port(const FunctionCallbackInfo<Value>& info) {
URLPattern* url_pattern;
ASSIGN_OR_RETURN_UNWRAP(&url_pattern, info.This());
Local<Value> result;
if (url_pattern->Port().ToLocal(&result)) {
info.GetReturnValue().Set(result);
}
}
void URLPattern::Pathname(const FunctionCallbackInfo<Value>& info) {
URLPattern* url_pattern;
ASSIGN_OR_RETURN_UNWRAP(&url_pattern, info.This());
Local<Value> result;
if (url_pattern->Pathname().ToLocal(&result)) {
info.GetReturnValue().Set(result);
}
}
void URLPattern::Search(const FunctionCallbackInfo<Value>& info) {
URLPattern* url_pattern;
ASSIGN_OR_RETURN_UNWRAP(&url_pattern, info.This());
Local<Value> result;
if (url_pattern->Search().ToLocal(&result)) {
info.GetReturnValue().Set(result);
}
}
void URLPattern::Hash(const FunctionCallbackInfo<Value>& info) {
URLPattern* url_pattern;
ASSIGN_OR_RETURN_UNWRAP(&url_pattern, info.This());
Local<Value> result;
if (url_pattern->Hash().ToLocal(&result)) {
info.GetReturnValue().Set(result);
}
}
void URLPattern::HasRegexpGroups(const FunctionCallbackInfo<Value>& info) {
URLPattern* url_pattern;
ASSIGN_OR_RETURN_UNWRAP(&url_pattern, info.This());
info.GetReturnValue().Set(url_pattern->HasRegExpGroups());
}
static void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(URLPattern::New);
registry->Register(URLPattern::Protocol);
registry->Register(URLPattern::Username);
registry->Register(URLPattern::Password);
registry->Register(URLPattern::Hostname);
registry->Register(URLPattern::Port);
registry->Register(URLPattern::Pathname);
registry->Register(URLPattern::Search);
registry->Register(URLPattern::Hash);
registry->Register(URLPattern::HasRegexpGroups);
registry->Register(URLPattern::Exec);
registry->Register(URLPattern::Test);
}
static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate();
auto attributes = static_cast<PropertyAttribute>(ReadOnly | DontDelete);
auto ctor_tmpl = NewFunctionTemplate(isolate, URLPattern::New);
auto instance_template = ctor_tmpl->InstanceTemplate();
auto prototype_template = ctor_tmpl->PrototypeTemplate();
ctor_tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "URLPattern"));
instance_template->SetInternalFieldCount(URLPattern::kInternalFieldCount);
prototype_template->SetAccessorProperty(
env->protocol_string(),
FunctionTemplate::New(isolate, URLPattern::Protocol),
Local<FunctionTemplate>(),
attributes);
prototype_template->SetAccessorProperty(
env->username_string(),
FunctionTemplate::New(isolate, URLPattern::Username),
Local<FunctionTemplate>(),
attributes);
prototype_template->SetAccessorProperty(
env->password_string(),
FunctionTemplate::New(isolate, URLPattern::Password),
Local<FunctionTemplate>(),
attributes);
prototype_template->SetAccessorProperty(
env->hostname_string(),
FunctionTemplate::New(isolate, URLPattern::Hostname),
Local<FunctionTemplate>(),
attributes);
prototype_template->SetAccessorProperty(
env->port_string(),
FunctionTemplate::New(isolate, URLPattern::Port),
Local<FunctionTemplate>(),
attributes);
prototype_template->SetAccessorProperty(
env->pathname_string(),
FunctionTemplate::New(isolate, URLPattern::Pathname),
Local<FunctionTemplate>(),
attributes);
prototype_template->SetAccessorProperty(
env->search_string(),
FunctionTemplate::New(isolate, URLPattern::Search),
Local<FunctionTemplate>(),
attributes);
prototype_template->SetAccessorProperty(
env->hash_string(),
FunctionTemplate::New(isolate, URLPattern::Hash),
Local<FunctionTemplate>(),
attributes);
prototype_template->SetAccessorProperty(
env->has_regexp_groups_string(),
FunctionTemplate::New(isolate, URLPattern::HasRegexpGroups),
Local<FunctionTemplate>(),
attributes);
SetProtoMethodNoSideEffect(isolate, ctor_tmpl, "exec", URLPattern::Exec);
SetProtoMethodNoSideEffect(isolate, ctor_tmpl, "test", URLPattern::Test);
SetConstructorFunction(context, target, "URLPattern", ctor_tmpl);
}
} // namespace node::url_pattern
NODE_BINDING_CONTEXT_AWARE_INTERNAL(url_pattern, node::url_pattern::Initialize)
NODE_BINDING_EXTERNAL_REFERENCE(url_pattern,
node::url_pattern::RegisterExternalReferences)

112
src/node_url_pattern.h Normal file
View File

@ -0,0 +1,112 @@
#ifndef SRC_NODE_URL_PATTERN_H_
#define SRC_NODE_URL_PATTERN_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "ada.h"
#include "base_object.h"
#include "util.h"
#include <v8.h>
#include <optional>
#include <string_view>
namespace node::url_pattern {
// By default, ada::url_pattern doesn't ship with any regex library.
// Ada has a std::regex implementation, but it is considered unsafe and does
// not have a fully compliant ecmascript syntax support. Therefore, Ada
// supports passing custom regex provider that conforms to the following
// class and function structure. For more information, please look into
// url_pattern_regex.h inside github.com/ada-url/ada.
class URLPatternRegexProvider {
public:
using regex_type = v8::Global<v8::RegExp>;
static std::optional<regex_type> create_instance(std::string_view pattern,
bool ignore_case);
static std::optional<std::vector<std::optional<std::string>>> regex_search(
std::string_view input, const regex_type& pattern);
static bool regex_match(std::string_view input, const regex_type& pattern);
};
class URLPattern : public BaseObject {
public:
URLPattern(Environment* env,
v8::Local<v8::Object> object,
ada::url_pattern<URLPatternRegexProvider>&& url_pattern);
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
// V8 APIs
// - Functions
static void Exec(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Test(const v8::FunctionCallbackInfo<v8::Value>& info);
// - Getters
static void Hash(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Hostname(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Password(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Pathname(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Port(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Protocol(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Search(const v8::FunctionCallbackInfo<v8::Value>& info);
static void Username(const v8::FunctionCallbackInfo<v8::Value>& info);
static void HasRegexpGroups(const v8::FunctionCallbackInfo<v8::Value>& info);
void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(URLPattern)
SET_SELF_SIZE(URLPattern)
class URLPatternInit {
public:
static ada::url_pattern_init FromJsObject(Environment* env,
v8::Local<v8::Object> obj);
static v8::MaybeLocal<v8::Value> ToJsObject(
Environment* env, const ada::url_pattern_init& init);
};
class URLPatternOptions {
public:
static std::optional<ada::url_pattern_options> FromJsObject(
Environment* env, v8::Local<v8::Object> obj);
};
class URLPatternResult {
public:
static v8::MaybeLocal<v8::Value> ToJSValue(
Environment* env, const ada::url_pattern_result& result);
};
class URLPatternComponentResult {
public:
static v8::MaybeLocal<v8::Object> ToJSObject(
Environment* env, const ada::url_pattern_component_result& result);
};
private:
ada::url_pattern<URLPatternRegexProvider> url_pattern_;
// Getter methods
v8::MaybeLocal<v8::Value> Hash() const;
v8::MaybeLocal<v8::Value> Hostname() const;
v8::MaybeLocal<v8::Value> Password() const;
v8::MaybeLocal<v8::Value> Pathname() const;
v8::MaybeLocal<v8::Value> Port() const;
v8::MaybeLocal<v8::Value> Protocol() const;
v8::MaybeLocal<v8::Value> Search() const;
v8::MaybeLocal<v8::Value> Username() const;
bool HasRegExpGroups() const;
// Public API
v8::MaybeLocal<v8::Value> Exec(
Environment* env,
const ada::url_pattern_input& input,
std::optional<std::string_view>& baseURL); // NOLINT (runtime/references)
bool Test(
Environment* env,
const ada::url_pattern_input& input,
std::optional<std::string_view>& baseURL); // NOLINT (runtime/references)
};
} // namespace node::url_pattern
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_NODE_URL_PATTERN_H_

View File

@ -74,6 +74,7 @@ expected.beforePreExec = new Set([
'NativeModule internal/querystring',
'NativeModule querystring',
'Internal Binding url',
'Internal Binding url_pattern',
'Internal Binding blob',
'NativeModule internal/url',
'NativeModule util',

View File

@ -14,6 +14,7 @@ import { SymbolsBinding } from './internalBinding/symbols';
import { TimersBinding } from './internalBinding/timers';
import { TypesBinding } from './internalBinding/types';
import { URLBinding } from './internalBinding/url';
import { URLPatternBinding } from "./internalBinding/url_pattern";
import { UtilBinding } from './internalBinding/util';
import { WASIBinding } from './internalBinding/wasi';
import { WorkerBinding } from './internalBinding/worker';
@ -38,6 +39,7 @@ interface InternalBindingMap {
timers: TimersBinding;
types: TypesBinding;
url: URLBinding;
url_pattern: URLPatternBinding;
util: UtilBinding;
wasi: WASIBinding;
worker: WorkerBinding;

View File

@ -0,0 +1,20 @@
export class URLPattern {
protocol: string
username: string
password: string
hostname: string
port: string
pathname: string
search: string
hash: string
constructor(input: Record<string, string> | string, options?: { ignoreCase: boolean });
constructor(input: Record<string, string> | string, baseUrl?: string, options?: { ignoreCase: boolean });
exec(input: string | Record<string, string>, baseURL?: string): null | Record<string, unknown>;
test(input: string | Record<string, string>, baseURL?: string): boolean;
}
export interface URLPatternBinding {
URLPattern: URLPattern;
}