@@ -320,6 +320,8 @@ impl Url {
320320
321321 /// Parse a string as an URL, with this URL as the base URL.
322322 ///
323+ /// The inverse of this is [`make_relative`].
324+ ///
323325 /// Note: a trailing slash is significant.
324326 /// Without it, the last path component is considered to be a “file” name
325327 /// to be removed to get at the “directory” that is used as the base:
@@ -349,11 +351,144 @@ impl Url {
349351 /// with this URL as the base URL, a [`ParseError`] variant will be returned.
350352 ///
351353 /// [`ParseError`]: enum.ParseError.html
354+ /// [`make_relative`]: #method.make_relative
352355 #[ inline]
353356 pub fn join ( & self , input : & str ) -> Result < Url , crate :: ParseError > {
354357 Url :: options ( ) . base_url ( Some ( self ) ) . parse ( input)
355358 }
356359
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+
357492 /// Return a default `ParseOptions` that can fully configure the URL parser.
358493 ///
359494 /// # Examples
0 commit comments