2

How do I convert an absolute url to a relative url?

Converting relative to absolute is trival

const rel = "../images/doggy.jpg"
const base = "https://example.com/pages/page.html?foo=bar#baz";

function relToAbs(baseHref, relHref) {
  const url = new URL(rel, base);
  return `${url.origin}${url.pathname}`;
}

console.log(base, rel);

Going the opposite direction seems to require a bunch of code.

Here's what I tried.

const pageURL1 = 'https://example.com/bar/baz.html?bla=blap#hash';
const pageURL2 = 'https://example.com/bar/?bla=blap#hash';
const absURL1 = 'https://example.com/bar/image/doggy.jpg';
const absURL2 = 'https://example.com/image/kitty.jpg';
const absURL3 = 'https://example.com/bar/birdy.jpg';

const tests = [
  { pageHref: pageURL1, absHref: absURL1, expected: 'image/doggy.jpg', },
  { pageHref: pageURL1, absHref: absURL2, expected: '../image/kitty.jpg', },
  { pageHref: pageURL1, absHref: absURL3, expected: 'birdy.jpg', },
  { pageHref: pageURL2, absHref: absURL1, expected: 'image/doggy.jpg', },
  { pageHref: pageURL2, absHref: absURL2, expected: '../image/kitty.jpg', },
  { pageHref: pageURL2, absHref: absURL3, expected: 'birdy.jpg', },
];

for (const {pageHref, absHref, expected} of tests) {
  const actual = absToRel(pageHref, absHref);
  console.log(absHref, actual, actual === expected ? 'pass' : 'FAIL');
}


function absToRel(pageHref, absHref) {
  const pagePaths = dirPaths(pageHref).slice(0, -1);
  const absPaths = dirPaths(absHref);
  if (pagePaths[0] !== absPaths[0]) {
    return absHref;  // not same domain
  }
  // remove same paths
  const firstDiffNdx = firstNonMatchingNdx(pagePaths, absPaths);
  pagePaths.splice(0, firstDiffNdx);
  absPaths.splice(0, firstDiffNdx);

  // for each path still on page add a ..
  return [
    ...(new Array(pagePaths.length).fill('..')),
    ...absPaths,
  ].join('/');
}

function firstNonMatchingNdx(a, b) {
  let ndx = 0;
  while (ndx < a.length && a[ndx] === b[ndx]) {
    ++ndx;
  }
  return ndx;
}

function dirPaths(href) {
  const url = new URL(href);
  return [url.origin, ...url.pathname.split('/')];
}
gman
  • 100,619
  • 31
  • 269
  • 393
  • 1
    *Two cents note*: technically there can be an infinite number of such relative urls. For instance `new URL("/foo/../bar/../baz/../index2.html", baseURL)` will generate `[base-url]/index2.html`. I guess what you want is the shortest path. – Kaiido Mar 02 '20 at 07:26
  • Yes of course. Yet pretty much every path API has such a fuction and no one ever needed to point that out. Like [this one](https://docs.python.org/2/library/os.path.html#os.path.relpath) and [this one](https://learn.microsoft.com/en-us/dotnet/api/system.io.path.getrelativepath?view=netcore-3.1) and [this one](https://nodejs.org/api/path.html#path_path_relative_from_to) and [this one](https://ruby-doc.org/stdlib-2.5.3/libdoc/pathname/rdoc/Pathname.html#method-i-relative_path_from) etc etc etc... – gman Mar 02 '20 at 15:28

1 Answers1

3

You can use the built-in URL class to help with parsing the URL. That way your code can just focus on the individual path elements and ignore search params, domains, schemas, etc...

Here's what I came up with.

  1. Remove the common directories
  2. Prepend .. for each page path we need to travel up from to reach the nearest ancestry directory
  3. Catch the edge case that these files are in the same directory

I'm not promising this is perfect or foolproof but hopefully it is close and gets you closer to what you are looking for.

function pathParts(path) {
  let parts = path.split('/');
  let start = 0;
  let end = parts.length;

  // Remove initial blank part from root /
  if( !parts[0] ) {
    start = 1;
  }

  // Remove trailing / part
  if( !parts[parts.length - 1] ) {
    end -= 1;
  }

  return parts.slice(start, end);
}

function absToRel(pageHref, absHref) {
  var pageUrl = new URL(pageHref);
  var absUrl = new URL(absHref);

  // If the urls have different schemas or domain, relative is not possible
  if( pageUrl.origin !== absUrl.origin ) {
    return absHref;
  }

  var pagePath = pathParts(pageUrl.pathname)
  var absPath = pathParts(absUrl.pathname)

  // Remove common root paths
  while( absPath.length && pagePath[0] === absPath[0] ) {
    absPath.shift();
    pagePath.shift();
  }

  // For each path segment left on pagePath, we'll need a relative ..
  pagePath.fill('..');

  let relPath = [
    ...pagePath,
    ...absPath
  ].join('/');

  // If the paths do not require traversal up, use current directory relative path
  if( relPath[0] !== '.' ) {
    relPath = './' + relPath;
  }

  return relPath;
}
Spidy
  • 39,723
  • 15
  • 65
  • 83
  • Oh and if the search and hash are important in the final result, just append them back on to the final relative url. `absUrl.search` and `absUrl.hash` – Spidy Feb 23 '20 at 04:28