script/dom/html/links/htmlhyperlinkelementutils.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use html5ever::{local_name, ns};
6use js::context::{JSContext, NoGC};
7use net_traits::blob_url_store::UrlWithBlobClaim;
8use script_bindings::cell::DomRefCell;
9use servo_url::ServoUrl;
10
11use crate::dom::bindings::conversions::DerivedFrom;
12use crate::dom::bindings::inheritance::Castable;
13use crate::dom::bindings::str::{DOMString, USVString};
14use crate::dom::element::attributes::storage::AttrRef;
15use crate::dom::element::{AttributeMutation, Element};
16use crate::dom::node::NodeTraits;
17use crate::dom::urlhelper::UrlHelper;
18use crate::url::ensure_blob_referenced_by_url_is_kept_alive;
19
20pub(crate) trait HyperlinkElement {
21 fn get_url(&self) -> &DomRefCell<Option<UrlWithBlobClaim>>;
22}
23
24/// <https://html.spec.whatwg.org/multipage/#htmlhyperlinkelementutils>
25pub(crate) trait HyperlinkElementTraits {
26 fn get_hash(&self, no_gc: &NoGC) -> USVString;
27 fn set_hash(&self, cx: &mut JSContext, value: USVString);
28 fn get_host(&self, no_gc: &NoGC) -> USVString;
29 fn set_host(&self, cx: &mut JSContext, value: USVString);
30 fn get_hostname(&self, no_gc: &NoGC) -> USVString;
31 fn set_hostname(&self, cx: &mut JSContext, value: USVString);
32 fn get_href(&self, no_gc: &NoGC) -> USVString;
33 fn get_origin(&self, no_gc: &NoGC) -> USVString;
34 fn get_password(&self, no_gc: &NoGC) -> USVString;
35 fn set_password(&self, cx: &mut JSContext, value: USVString);
36 fn get_pathname(&self, no_gc: &NoGC) -> USVString;
37 fn set_pathname(&self, cx: &mut JSContext, value: USVString);
38 fn get_port(&self, no_gc: &NoGC) -> USVString;
39 fn set_port(&self, cx: &mut JSContext, value: USVString);
40 fn get_protocol(&self, no_gc: &NoGC) -> USVString;
41 fn set_protocol(&self, cx: &mut JSContext, value: USVString);
42 fn get_search(&self, no_gc: &NoGC) -> USVString;
43 fn set_search(&self, cx: &mut JSContext, value: USVString);
44 fn get_username(&self, no_gc: &NoGC) -> USVString;
45 fn set_url(&self, no_gc: &NoGC);
46 fn set_username(&self, cx: &mut JSContext, value: USVString);
47 fn update_href(&self, cx: &mut JSContext, url: String);
48 fn reinitialize_url(&self, no_gc: &NoGC);
49 fn attribute_mutated_for_hyperlinks(
50 &self,
51 no_gc: &NoGC,
52 attr: AttrRef<'_>,
53 mutation: AttributeMutation,
54 );
55 fn full_href_url_for_user_interface(&self) -> Option<ServoUrl>;
56}
57
58impl<T: HyperlinkElement + DerivedFrom<Element> + Castable + NodeTraits> HyperlinkElementTraits
59 for T
60{
61 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hash>
62 fn get_hash(&self, no_gc: &NoGC) -> USVString {
63 // Step 1. Reinitialize url.
64 self.reinitialize_url(no_gc);
65
66 // Step 2. Let url be this's url.
67 match *self.get_url().borrow() {
68 // Step 3. If url is null, or url's fragment is either null or the empty string, return
69 // the empty string.
70 None => USVString(String::new()),
71 Some(ref url) if url.fragment().is_none() || url.fragment() == Some("") => {
72 USVString(String::new())
73 },
74 Some(ref url) => {
75 // Step 4. Return "#", followed by url's fragment.
76 UrlHelper::Hash(url)
77 },
78 }
79 }
80
81 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hash>
82 fn set_hash(&self, cx: &mut JSContext, value: USVString) {
83 // Step 1. Reinitialize url.
84 self.reinitialize_url(cx.no_gc());
85
86 // Step 2. Let url be this's url.
87 // Step 3. If url is null, then return.
88 let url = {
89 let mut url = self.get_url().safe_borrow_mut(cx.no_gc());
90 let Some(url) = url.as_mut() else {
91 return;
92 };
93
94 // Step 4. If the given value is the empty string, set url's fragment to null.
95 // Note this step is taken care of by UrlHelper::SetHash when the value is Some
96 // Steps 5. Otherwise:
97 // Step 5.1. Let input be the given value with a single leading "#" removed, if any.
98 // Step 5.2. Set url's fragment to the empty string.
99 // Note these steps are taken care of by UrlHelper::SetHash
100 // Step 5.4. Basic URL parse input, with url as url and fragment state as state
101 // override.
102 UrlHelper::SetHash(url, value);
103 url.as_str().to_owned()
104 };
105
106 // Step 6. Update href.
107 self.update_href(cx, url);
108 }
109
110 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-host>
111 fn get_host(&self, no_gc: &NoGC) -> USVString {
112 // Step 1. Reinitialize url.
113 self.reinitialize_url(no_gc);
114
115 // Step 2. Let url be this's url.
116 match *self.get_url().borrow() {
117 // Step 3. If url or url's host is null, return the empty string.
118 None => USVString(String::new()),
119 Some(ref url) => {
120 if url.host().is_none() {
121 USVString(String::new())
122 } else {
123 // Step 4. If url's port is null, return url's host, serialized.
124 // Step 5. Return url's host, serialized, followed by ":" and url's port,
125 // serialized.
126 UrlHelper::Host(url)
127 }
128 },
129 }
130 }
131
132 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-host>
133 fn set_host(&self, cx: &mut JSContext, value: USVString) {
134 // Step 1. Reinitialize url.
135 self.reinitialize_url(cx.no_gc());
136
137 // Step 2. Let url be this's url.
138 let url = {
139 let mut url = self.get_url().safe_borrow_mut(cx.no_gc());
140 let url = match url.as_mut() {
141 // Step 3. If url or url's host is null, return the empty string.
142 Some(ref url) if url.cannot_be_a_base() => return,
143 None => return,
144 Some(url) => url,
145 };
146
147 // Step 4. Basic URL parse the given value, with url as url and host state as state
148 // override.
149 UrlHelper::SetHost(url, value);
150 url.as_str().to_owned()
151 };
152
153 // Step 5. Update href.
154 self.update_href(cx, url);
155 }
156
157 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hostname>
158 fn get_hostname(&self, no_gc: &NoGC) -> USVString {
159 // Step 1. Reinitialize url.
160 self.reinitialize_url(no_gc);
161
162 // Step 2. Let url be this's url.
163 match *self.get_url().borrow() {
164 // Step 3. If url or url's host is null, return the empty string.
165 None => USVString(String::new()),
166 Some(ref url) => {
167 // Step 4. Return url's host, serialized.
168 UrlHelper::Hostname(url)
169 },
170 }
171 }
172
173 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-hostname>
174 fn set_hostname(&self, cx: &mut JSContext, value: USVString) {
175 // Step 1. Reinitialize url.
176 self.reinitialize_url(cx.no_gc());
177
178 // Step 2. Let url be this's url.
179 let url = {
180 let mut url = self.get_url().safe_borrow_mut(cx.no_gc());
181 let url = match url.as_mut() {
182 // Step 3. If url is null or url has an opaque path, then return.
183 None => return,
184 Some(ref url) if url.cannot_be_a_base() => return,
185 Some(url) => url,
186 };
187
188 // Step 4. Basic URL parse the given value, with url as url and hostname state as state
189 // override.
190 UrlHelper::SetHostname(url, value);
191 url.as_str().to_owned()
192 };
193
194 // Step 5. Update href.
195 self.update_href(cx, url);
196 }
197
198 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-href>
199 fn get_href(&self, no_gc: &NoGC) -> USVString {
200 // Step 1. Reinitialize url.
201 self.reinitialize_url(no_gc);
202
203 // Step 2. Let url be this's url.
204 USVString(match *self.get_url().borrow() {
205 None => {
206 // Step 3. If url is null and this has no href content attribute, return the
207 // empty string.
208 // Step 4. Otherwise, if url is null, return this's href content attribute's value.
209 self.upcast::<Element>()
210 .get_attribute_string_value(&local_name!("href"))
211 .unwrap_or_default()
212 },
213 // Step 5. Return url, serialized.
214 Some(ref url) => url.as_str().to_owned(),
215 })
216 }
217
218 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-origin>
219 fn get_origin(&self, no_gc: &NoGC) -> USVString {
220 // Step 1. Reinitialize url.
221 self.reinitialize_url(no_gc);
222
223 USVString(match *self.get_url().borrow() {
224 // Step 2. If this's url is null, return the empty string.
225 None => String::new(),
226 // Step 3. Return the serialization of this's url's origin.
227 Some(ref url) => url.origin().ascii_serialization().into_owned(),
228 })
229 }
230
231 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-password>
232 fn get_password(&self, no_gc: &NoGC) -> USVString {
233 // Step 1. Reinitialize url.
234 self.reinitialize_url(no_gc);
235
236 // Step 2. Let url be this's url.
237 match *self.get_url().borrow() {
238 // Step 3. If url is null, then return the empty string.
239 None => USVString(String::new()),
240 // Steps 4. Return url's password.
241 Some(ref url) => UrlHelper::Password(url),
242 }
243 }
244
245 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-password>
246 fn set_password(&self, cx: &mut JSContext, value: USVString) {
247 // Step 1. Reinitialize url.
248 self.reinitialize_url(cx.no_gc());
249
250 let url = {
251 // Step 2. Let url be this's url.
252 let mut url = self.get_url().safe_borrow_mut(cx.no_gc());
253 let url = match url.as_mut() {
254 // Step 3. If url is null or url cannot have a username/password/port, then return.
255 None => return,
256 Some(ref url) if url.host().is_none() || url.cannot_be_a_base() => return,
257 Some(url) => url,
258 };
259
260 // Step 4. Set the password, given url and the given value.
261 UrlHelper::SetPassword(url, value);
262 url.as_str().to_owned()
263 };
264
265 // Step 5. Update href.
266 self.update_href(cx, url);
267 }
268
269 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-pathname>
270 fn get_pathname(&self, no_gc: &NoGC) -> USVString {
271 // Step 1. Reinitialize url.
272 self.reinitialize_url(no_gc);
273
274 // Step 2. Let url be this's url.
275 match *self.get_url().borrow() {
276 // Step 3. If url is null, then return the empty string.
277 None => USVString(String::new()),
278 // Steps 4. Return the result of URL path serializing url.
279 Some(ref url) => UrlHelper::Pathname(url),
280 }
281 }
282
283 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-pathname>
284 fn set_pathname(&self, cx: &mut JSContext, value: USVString) {
285 // Step 1. Reinitialize url.
286 self.reinitialize_url(cx.no_gc());
287
288 // Step 2. Let url be this's url.
289 let url = {
290 let mut url = self.get_url().safe_borrow_mut(cx.no_gc());
291 let url = match url.as_mut() {
292 // Step 3. If url is null or url has an opaque path, then return.
293 None => return,
294 Some(ref url) if url.cannot_be_a_base() => return,
295 Some(url) => url,
296 };
297
298 // Step 4. Set url's path to the empty list.
299 // Step 5. Basic URL parse the given value, with url as url and path start state as state override.
300 UrlHelper::SetPathname(url, value);
301 url.as_str().to_owned()
302 };
303
304 // Step 6. Update href.
305 self.update_href(cx, url);
306 }
307
308 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-port>
309 fn get_port(&self, no_gc: &NoGC) -> USVString {
310 // Step 1. Reinitialize url.
311 self.reinitialize_url(no_gc);
312
313 // Step 2. Let url be this's url.
314 match *self.get_url().borrow() {
315 // Step 3. If url or url's port is null, return the empty string.
316 None => USVString(String::new()),
317 // Step 4. Return url's port, serialized.
318 Some(ref url) => UrlHelper::Port(url),
319 }
320 }
321
322 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-port>
323 fn set_port(&self, cx: &mut JSContext, value: USVString) {
324 // Step 1. Reinitialize url.
325 self.reinitialize_url(cx.no_gc());
326
327 // Step 2. Let url be this's url.
328 let url = {
329 let mut url = self.get_url().safe_borrow_mut(cx.no_gc());
330 let url = match url.as_mut() {
331 // Step 3. If url is null or url cannot have a username/password/port, then return.
332 // https://url.spec.whatwg.org/#cannot-have-a-username-password-port
333 // > A URL cannot have a username/password/port if its host is
334 // > null or the empty string, or its scheme is "file".
335 None => return,
336 Some(ref url)
337 if url.host().is_none() || url.cannot_be_a_base() || url.scheme() == "file" =>
338 {
339 return;
340 },
341 Some(url) => url,
342 };
343
344 // Step 4. If the given value is the empty string, then set url's port to null.
345 // Step 5. Otherwise, basic URL parse the given value, with url as url and port state as
346 // state override.
347 UrlHelper::SetPort(url, value);
348 url.as_str().to_owned()
349 };
350
351 // Step 6. Update href.
352 self.update_href(cx, url);
353 }
354
355 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-protocol>
356 fn get_protocol(&self, no_gc: &NoGC) -> USVString {
357 // Step 1. Reinitialize url.
358 self.reinitialize_url(no_gc);
359
360 match *self.get_url().borrow() {
361 // Step 2. If this's url is null, return ":".
362 None => USVString(":".to_owned()),
363 // Step 3. Return this's url's scheme, followed by ":".
364 Some(ref url) => UrlHelper::Protocol(url),
365 }
366 }
367
368 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-protocol>
369 fn set_protocol(&self, cx: &mut JSContext, value: USVString) {
370 // Step 1. Reinitialize url.
371 self.reinitialize_url(cx.no_gc());
372
373 let url = {
374 let mut url = self.get_url().safe_borrow_mut(cx.no_gc());
375 let url = match url.as_mut() {
376 // Step 2. If this's url is null, then return.
377 None => return,
378 Some(url) => url,
379 };
380
381 // Step 3. Basic URL parse the given value, followed by ":", with this's url as url and
382 // scheme start state as state override.
383 UrlHelper::SetProtocol(url, value);
384 url.as_str().to_owned()
385 };
386
387 // Step 4. Update href.
388 self.update_href(cx, url);
389 }
390
391 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-search>
392 fn get_search(&self, no_gc: &NoGC) -> USVString {
393 // Step 1. Reinitialize url.
394 self.reinitialize_url(no_gc);
395
396 // Step 2. Let url be this's url.
397 match *self.get_url().borrow() {
398 // Step 3. If url is null, or url's query is either null or the empty string, return the
399 // empty string.
400 // Step 4. Return "?", followed by url's query.
401 // Note: This is handled in UrlHelper::Search
402 None => USVString(String::new()),
403 Some(ref url) => UrlHelper::Search(url),
404 }
405 }
406
407 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-search>
408 fn set_search(&self, cx: &mut JSContext, value: USVString) {
409 // Step 1. Reinitialize url.
410 self.reinitialize_url(cx.no_gc());
411
412 // Step 2. Let url be this's url.
413 let url = {
414 let mut url = self.get_url().safe_borrow_mut(cx.no_gc());
415 let url = match url.as_mut() {
416 // Step 3. If url is null, terminate these steps.
417 None => return,
418 Some(url) => url,
419 };
420
421 // Step 4. If the given value is the empty string, set url's query to null.
422 // Step 5. Otherwise:
423 // Note: Inner steps are handled by UrlHelper::SetSearch
424 UrlHelper::SetSearch(url, value);
425 url.as_str().to_owned()
426 };
427
428 // Step 6. Update href.
429 self.update_href(cx, url);
430 }
431
432 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-username>
433 fn get_username(&self, no_gc: &NoGC) -> USVString {
434 // Step 1. Reinitialize url.
435 self.reinitialize_url(no_gc);
436
437 match *self.get_url().borrow() {
438 // Step 2. If this's url is null, return the empty string.
439 None => USVString(String::new()),
440 // Step 3. Return this's url's username.
441 Some(ref url) => UrlHelper::Username(url),
442 }
443 }
444
445 /// <https://html.spec.whatwg.org/multipage/#concept-hyperlink-url-set>
446 fn set_url(&self, no_gc: &NoGC) {
447 // Step 1. Set this element's url to null.
448 *self.get_url().safe_borrow_mut(no_gc) = None;
449
450 let attribute = self
451 .upcast::<Element>()
452 .get_attribute_string_ref(&local_name!("href"));
453
454 // Step 2. If this element's href content attribute is absent, then return.
455 let Some(attribute) = attribute else {
456 return;
457 };
458 let attribute = attribute.as_attr_ref();
459
460 let document = self.owner_document();
461 let global = self.owner_global();
462
463 // Step 3. Let url be the result of encoding-parsing a URL given this element's href content
464 // attribute's value, relative to this element's node document.
465 let url = document.encoding_parse_a_url(&attribute.value());
466
467 // Step 4. If url is not failure, then set this element's url to url.
468 if let Ok(url) = url {
469 *self.get_url().safe_borrow_mut(no_gc) =
470 Some(ensure_blob_referenced_by_url_is_kept_alive(&global, url));
471 }
472 }
473
474 /// <https://html.spec.whatwg.org/multipage/#dom-hyperlink-username>
475 fn set_username(&self, cx: &mut JSContext, value: USVString) {
476 // Step 1. Reinitialize url.
477 self.reinitialize_url(cx.no_gc());
478
479 // Step 2. Let url be this's url.
480 let url = {
481 let mut url = self.get_url().safe_borrow_mut(cx.no_gc());
482 let url = match url.as_mut() {
483 // Step 3. If url is null or url cannot have a username/password/port, then return.
484 None => return,
485 Some(ref url) if url.host().is_none() || url.cannot_be_a_base() => return,
486 Some(url) => url,
487 };
488
489 // Step 4. Set the username, given url and the given value.
490 UrlHelper::SetUsername(url, value);
491 url.as_str().to_owned()
492 };
493
494 // Step 5. Update href.
495 self.update_href(cx, url);
496 }
497
498 /// <https://html.spec.whatwg.org/multipage/#api-for-a-and-area-elements:update-href>
499 fn update_href(&self, cx: &mut JSContext, url: String) {
500 // > To update href for an HTMLAnchorElement or HTMLAreaElement element,
501 // > set the element's href content attribute's value to the element's url, serialized.
502 self.upcast::<Element>().set_string_attribute(
503 cx,
504 &local_name!("href"),
505 DOMString::from(url),
506 );
507 }
508
509 /// <https://html.spec.whatwg.org/multipage/#reinitialise-url>
510 fn reinitialize_url(&self, no_gc: &NoGC) {
511 // Step 1. If the element's url is non-null, its scheme is "blob", and it has an opaque
512 // path, then terminate these steps.
513 match *self.get_url().borrow() {
514 Some(ref url) if url.scheme() == "blob" && url.cannot_be_a_base() => return,
515 _ => (),
516 }
517
518 // Step 2. Set the url.
519 self.set_url(no_gc);
520 }
521
522 /// <https://html.spec.whatwg.org/multipage/#api-for-a-and-area-elements:concept-element-attributes-change-ext>
523 fn attribute_mutated_for_hyperlinks(
524 &self,
525 no_gc: &NoGC,
526 attr: AttrRef<'_>,
527 mutation: AttributeMutation,
528 ) {
529 // Step 1. If namespace is not null, then return.
530 if attr.namespace() != &ns!() {
531 return;
532 }
533 // Step 2. If oldValue equals value, then return.
534 if mutation.old_value(attr) == mutation.new_value(attr).map(|value| value.to_string()) {
535 return;
536 }
537 // Step 3. If localName is href, then set the url given element.
538 if attr.local_name() == &local_name!("href") {
539 self.set_url(no_gc);
540 }
541 // Step 4. If localName is href, referrerpolicy, or rel,
542 // then consider speculative loads given element's node document.
543 // TODO
544 }
545
546 /// Get the full URL of the `href` attribute of this hyperlink element, returning
547 /// `None` if the URL could not be parsed against the `Document`'s base URL.
548 fn full_href_url_for_user_interface(&self) -> Option<ServoUrl> {
549 let href = self
550 .upcast::<Element>()
551 .get_attribute_string_ref(&local_name!("href"))?;
552 self.owner_document()
553 .base_url()
554 .join(href.value().as_ref())
555 .ok()
556 }
557}