user128511
user128511

Reputation:

How to convert absolute url to relative

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('/')];
}

Upvotes: 3

Views: 2258

Answers (2)

Adam Gawne-Cain
Adam Gawne-Cain

Reputation: 1720

@Spidy's answer is good. I used it with a check that the input URL-s are not already relative as that would make the URL constructor throw an exception. Here is my simple test for relative URL-s...

function isRelativeUrl(href) {
    return href !== new URL(href, "http://a/b").toString();
}

Upvotes: 0

Spidy
Spidy

Reputation: 40002

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;
}

Upvotes: 4

Related Questions