@@ -320,6 +320,8 @@ impl Url {
320
320
321
321
/// Parse a string as an URL, with this URL as the base URL.
322
322
///
323
+ /// The inverse of this is [`make_relative`].
324
+ ///
323
325
/// Note: a trailing slash is significant.
324
326
/// Without it, the last path component is considered to be a “file” name
325
327
/// to be removed to get at the “directory” that is used as the base:
@@ -349,11 +351,144 @@ impl Url {
349
351
/// with this URL as the base URL, a [`ParseError`] variant will be returned.
350
352
///
351
353
/// [`ParseError`]: enum.ParseError.html
354
+ /// [`make_relative`]: #method.make_relative
352
355
#[ inline]
353
356
pub fn join ( & self , input : & str ) -> Result < Url , crate :: ParseError > {
354
357
Url :: options ( ) . base_url ( Some ( self ) ) . parse ( input)
355
358
}
356
359
360
+ /// Creates a relative URL if possible, with this URL as the base URL.
361
+ ///
362
+ /// This is the inverse of [`join`].
363
+ ///
364
+ /// # Examples
365
+ ///
366
+ /// ```rust
367
+ /// use url::Url;
368
+ /// # use url::ParseError;
369
+ ///
370
+ /// # fn run() -> Result<(), ParseError> {
371
+ /// let base = Url::parse("https://example.net/a/b.html")?;
372
+ /// let url = Url::parse("https://example.net/a/c.png")?;
373
+ /// let relative = base.make_relative(&url);
374
+ /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("c.png"));
375
+ ///
376
+ /// let base = Url::parse("https://example.net/a/b/")?;
377
+ /// let url = Url::parse("https://example.net/a/b/c.png")?;
378
+ /// let relative = base.make_relative(&url);
379
+ /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("c.png"));
380
+ ///
381
+ /// let base = Url::parse("https://example.net/a/b/")?;
382
+ /// let url = Url::parse("https://example.net/a/d/c.png")?;
383
+ /// let relative = base.make_relative(&url);
384
+ /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("../d/c.png"));
385
+ ///
386
+ /// let base = Url::parse("https://example.net/a/b.html?c=d")?;
387
+ /// let url = Url::parse("https://example.net/a/b.html?e=f")?;
388
+ /// let relative = base.make_relative(&url);
389
+ /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("?e=f"));
390
+ /// # Ok(())
391
+ /// # }
392
+ /// # run().unwrap();
393
+ /// ```
394
+ ///
395
+ /// # Errors
396
+ ///
397
+ /// If this URL can't be a base for the given URL, `None` is returned.
398
+ /// This is for example the case if the scheme, host or port are not the same.
399
+ ///
400
+ /// [`join`]: #method.join
401
+ pub fn make_relative ( & self , url : & Url ) -> Option < String > {
402
+ if self . cannot_be_a_base ( ) {
403
+ return None ;
404
+ }
405
+
406
+ // Scheme, host and port need to be the same
407
+ if self . scheme ( ) != url. scheme ( ) || self . host ( ) != url. host ( ) || self . port ( ) != url. port ( ) {
408
+ return None ;
409
+ }
410
+
411
+ // We ignore username/password at this point
412
+
413
+ // The path has to be transformed
414
+ let mut relative = String :: new ( ) ;
415
+
416
+ // Extract the filename of both URIs, these need to be handled separately
417
+ fn extract_path_filename ( s : & str ) -> ( & str , & str ) {
418
+ let last_slash_idx = s. rfind ( '/' ) . unwrap_or ( 0 ) ;
419
+ let ( path, filename) = s. split_at ( last_slash_idx) ;
420
+ if filename. is_empty ( ) {
421
+ ( path, "" )
422
+ } else {
423
+ ( path, & filename[ 1 ..] )
424
+ }
425
+ }
426
+
427
+ let ( base_path, base_filename) = extract_path_filename ( self . path ( ) ) ;
428
+ let ( url_path, url_filename) = extract_path_filename ( url. path ( ) ) ;
429
+
430
+ let mut base_path = base_path. split ( '/' ) . peekable ( ) ;
431
+ let mut url_path = url_path. split ( '/' ) . peekable ( ) ;
432
+
433
+ // Skip over the common prefix
434
+ while base_path. peek ( ) . is_some ( ) && base_path. peek ( ) == url_path. peek ( ) {
435
+ base_path. next ( ) ;
436
+ url_path. next ( ) ;
437
+ }
438
+
439
+ // Add `..` segments for the remainder of the base path
440
+ for base_path_segment in base_path {
441
+ // Skip empty last segments
442
+ if base_path_segment. is_empty ( ) {
443
+ break ;
444
+ }
445
+
446
+ if !relative. is_empty ( ) {
447
+ relative. push ( '/' ) ;
448
+ }
449
+
450
+ relative. push_str ( ".." ) ;
451
+ }
452
+
453
+ // Append the remainder of the other URI
454
+ for url_path_segment in url_path {
455
+ if !relative. is_empty ( ) {
456
+ relative. push ( '/' ) ;
457
+ }
458
+
459
+ relative. push_str ( url_path_segment) ;
460
+ }
461
+
462
+ // Add the filename if they are not the same
463
+ if base_filename != url_filename {
464
+ // If the URIs filename is empty this means that it was a directory
465
+ // so we'll have to append a '/'.
466
+ //
467
+ // Otherwise append it directly as the new filename.
468
+ if url_filename. is_empty ( ) {
469
+ relative. push ( '/' ) ;
470
+ } else {
471
+ if !relative. is_empty ( ) {
472
+ relative. push ( '/' ) ;
473
+ }
474
+ relative. push_str ( url_filename) ;
475
+ }
476
+ }
477
+
478
+ // Query and fragment are only taken from the other URI
479
+ if let Some ( query) = url. query ( ) {
480
+ relative. push ( '?' ) ;
481
+ relative. push_str ( query) ;
482
+ }
483
+
484
+ if let Some ( fragment) = url. fragment ( ) {
485
+ relative. push ( '#' ) ;
486
+ relative. push_str ( fragment) ;
487
+ }
488
+
489
+ Some ( relative)
490
+ }
491
+
357
492
/// Return a default `ParseOptions` that can fully configure the URL parser.
358
493
///
359
494
/// # Examples
0 commit comments