Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/url-utils/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
node_modules/
coverage/
.nyc_output/
20 changes: 20 additions & 0 deletions packages/url-utils/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,25 @@ module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/node'
],
overrides: [
{
files: ['*.ts'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module'
},
rules: {
// Disable rules that conflict with TypeScript
'no-unused-vars': 'off',
'no-undef': 'off',
// TypeScript supports method overloads which ESLint sees as duplicates
'no-dupe-class-members': 'off',
// TypeScript files use PascalCase for classes and kebab-case for filenames
'ghost/filenames/match-regex': 'off',
'ghost/filenames/match-exported-class': 'off'
}
}
]
};
7 changes: 7 additions & 0 deletions packages/url-utils/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules/
dist/
*.log
.DS_Store
coverage/
.nyc_output/
.eslintcache
3 changes: 3 additions & 0 deletions packages/url-utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import UrlUtils from './lib/UrlUtils';

export default UrlUtils;
645 changes: 645 additions & 0 deletions packages/url-utils/lib/UrlUtils.ts

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions packages/url-utils/lib/utils/absolute-to-relative.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// require the whatwg compatible URL library (same behaviour in node and browser)
import {URL} from 'url';
import stripSubdirectoryFromPath from './strip-subdirectory-from-path';

export interface AbsoluteToRelativeOptions {
ignoreProtocol?: boolean;
withoutSubdirectory?: boolean;
assetsOnly?: boolean;
staticImageUrlPrefix?: string;
}

/**
* Convert an absolute URL to a root-relative path if it matches the supplied root domain.
*
* @param {string} url Absolute URL to convert to relative if possible
* @param {string} rootUrl Absolute URL to which the returned relative URL will match the domain root
* @param {Object} [options] Options that affect the conversion
* @param {boolean} [options.ignoreProtocol=true] Ignore protocol when matching url to root
* @param {boolean} [options.withoutSubdirectory=false] Strip the root subdirectory from the returned path
* @returns {string} The passed-in url or a relative path
*/
export const absoluteToRelative = function absoluteToRelative(url: string, rootUrl: string, _options: AbsoluteToRelativeOptions = {}): string {
const defaultOptions: AbsoluteToRelativeOptions = {
ignoreProtocol: true,
withoutSubdirectory: false,
assetsOnly: false,
staticImageUrlPrefix: 'content/images'
};
const options = Object.assign({}, defaultOptions, _options);

if (options.assetsOnly) {
const staticImageUrlPrefixRegex = new RegExp(options.staticImageUrlPrefix!);
if (!url.match(staticImageUrlPrefixRegex)) {
return url;
}
}

let parsedUrl: URL;
let parsedRoot: URL | undefined;

try {
parsedUrl = new URL(url, 'http://relative');
parsedRoot = parsedUrl.origin === 'null' ? undefined : new URL(rootUrl || parsedUrl.origin);

// return the url as-is if it was relative or non-http
if (parsedUrl.origin === 'null' || parsedUrl.origin === 'http://relative') {
return url;
}
} catch (e) {
return url;
}

const matchesHost = parsedUrl.host === parsedRoot!.host;
const matchesProtocol = parsedUrl.protocol === parsedRoot!.protocol;
const matchesPath = parsedUrl.pathname.indexOf(parsedRoot!.pathname) === 0;

if (matchesHost && (options.ignoreProtocol || matchesProtocol) && matchesPath) {
let path = parsedUrl.href.replace(parsedUrl.origin, '');

if (options.withoutSubdirectory) {
path = stripSubdirectoryFromPath(path, rootUrl);
}

return path;
}

return url;
};

export default absoluteToRelative;
47 changes: 47 additions & 0 deletions packages/url-utils/lib/utils/absolute-to-transform-ready.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {URL} from 'url';
import absoluteToRelative from './absolute-to-relative';

export interface AbsoluteToTransformReadyOptions {
replacementStr?: string;
withoutSubdirectory?: boolean;
assetsOnly?: boolean;
staticImageUrlPrefix?: string;
}

export const absoluteToTransformReady = function (url: string, root: string, _options?: AbsoluteToTransformReadyOptions): string {
const defaultOptions: AbsoluteToTransformReadyOptions = {
replacementStr: '__GHOST_URL__',
withoutSubdirectory: true
};
const options = Object.assign({}, defaultOptions, _options);

// return relative urls as-is
try {
const parsedURL = new URL(url, 'http://relative');
if (parsedURL.origin === 'http://relative') {
return url;
}
} catch (e) {
// url was unparseable
return url;
}

// convert to relative with stripped subdir
// always returns root-relative starting with forward slash
const relativeUrl = absoluteToRelative(url, root, options);

// return still absolute urls as-is (eg. external site, mailto, etc)
try {
const parsedURL = new URL(relativeUrl, 'http://relative');
if (parsedURL.origin !== 'http://relative') {
return url;
}
} catch (e) {
// url was unparseable
return url;
}

return `${options.replacementStr}${relativeUrl}`;
};

export default absoluteToTransformReady;
5 changes: 5 additions & 0 deletions packages/url-utils/lib/utils/deduplicate-double-slashes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function deduplicateDoubleSlashes(url: string): string {
return url.replace(/\/\//g, '/');
}

export default deduplicateDoubleSlashes;
31 changes: 31 additions & 0 deletions packages/url-utils/lib/utils/deduplicate-subdirectory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {URL} from 'url';

/**
* Remove duplicated directories from the start of a path or url's path
*
* @param {string} url URL or pathname with possible duplicate subdirectory
* @param {string} rootUrl Root URL with an optional subdirectory
* @returns {string} URL or pathname with any duplicated subdirectory removed
*/
const deduplicateSubdirectory = function deduplicateSubdirectory(url: string, rootUrl: string): string {
// force root url to always have a trailing-slash for consistent behaviour
if (!rootUrl.endsWith('/')) {
rootUrl = `${rootUrl}/`;
}

const parsedRoot = new URL(rootUrl);

// do nothing if rootUrl does not have a subdirectory
if (parsedRoot.pathname === '/') {
return url;
}

const subdir = parsedRoot.pathname.replace(/(^\/|\/$)+/g, '');
// we can have subdirs that match TLDs so we need to restrict matches to
// duplicates that start with a / or the beginning of the url
const subdirRegex = new RegExp(`(^|/)${subdir}/${subdir}(/|$)`);

return url.replace(subdirRegex, `$1${subdir}/`);
};

export default deduplicateSubdirectory;
27 changes: 27 additions & 0 deletions packages/url-utils/lib/utils/html-absolute-to-relative.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import htmlTransform from './html-transform';
import absoluteToRelative from './absolute-to-relative';

interface HtmlAbsoluteToRelativeOptions {
assetsOnly?: boolean;
ignoreProtocol?: boolean;
staticImageUrlPrefix?: string;
earlyExitMatchStr?: string;
}

function htmlAbsoluteToRelative(html = '', siteUrl: string, _options?: HtmlAbsoluteToRelativeOptions): string {
const defaultOptions: HtmlAbsoluteToRelativeOptions = {assetsOnly: false, ignoreProtocol: true};
const options = Object.assign({}, defaultOptions, _options || {});

// exit early and avoid parsing if the content does not contain the siteUrl
options.earlyExitMatchStr = options.ignoreProtocol ? siteUrl.replace(/http:|https:/, '') : siteUrl;
options.earlyExitMatchStr = options.earlyExitMatchStr.replace(/\/$/, '');

// need to ignore itemPath because absoluteToRelative doesn't take that option
const transformFunction = function (_url: string, _siteUrl: string, _itemPath: string | null, __options: any) {
return absoluteToRelative(_url, _siteUrl, __options);
};

return htmlTransform(html, siteUrl, transformFunction, '', options);
}

export default htmlAbsoluteToRelative;
27 changes: 27 additions & 0 deletions packages/url-utils/lib/utils/html-absolute-to-transform-ready.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import htmlTransform from './html-transform';
import absoluteToTransformReady from './absolute-to-transform-ready';

export interface HtmlAbsoluteToTransformReadyOptions {
assetsOnly?: boolean;
ignoreProtocol?: boolean;
staticImageUrlPrefix?: string;
earlyExitMatchStr?: string;
}

export const htmlAbsoluteToTransformReady = function (html = '', siteUrl: string, _options?: HtmlAbsoluteToTransformReadyOptions): string {
const defaultOptions: HtmlAbsoluteToTransformReadyOptions = {assetsOnly: false, ignoreProtocol: true};
const options = Object.assign({}, defaultOptions, _options || {});

// exit early and avoid parsing if the content does not contain the siteUrl
options.earlyExitMatchStr = options.ignoreProtocol ? siteUrl.replace(/http:|https:/, '') : siteUrl;
options.earlyExitMatchStr = options.earlyExitMatchStr.replace(/\/$/, '');

// need to ignore itemPath because absoluteToRelative doesn't take that option
const transformFunction = function (_url: string, _siteUrl: string, _itemPath: string | null, __options: any) {
return absoluteToTransformReady(_url, _siteUrl, __options);
};

return htmlTransform(html, siteUrl, transformFunction, '', options);
};

export default htmlAbsoluteToTransformReady;
24 changes: 24 additions & 0 deletions packages/url-utils/lib/utils/html-relative-to-absolute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import htmlTransform from './html-transform';
import relativeToAbsolute from './relative-to-absolute';

interface HtmlRelativeToAbsoluteOptions {
assetsOnly?: boolean;
secure?: boolean;
staticImageUrlPrefix?: string;
earlyExitMatchStr?: string;
}

function htmlRelativeToAbsolute(html = '', siteUrl: string, itemPath: string | HtmlRelativeToAbsoluteOptions | null, _options?: HtmlRelativeToAbsoluteOptions): string {
const defaultOptions: HtmlRelativeToAbsoluteOptions = {assetsOnly: false, secure: false};
const options = Object.assign({}, defaultOptions, _options || {});

// exit early and avoid parsing if the content does not contain an attribute we might transform
options.earlyExitMatchStr = 'href=|src=|srcset=';
if (options.assetsOnly) {
options.earlyExitMatchStr = options.staticImageUrlPrefix;
}

return htmlTransform(html, siteUrl, relativeToAbsolute, itemPath as string | null, options);
}

export default htmlRelativeToAbsolute;
37 changes: 37 additions & 0 deletions packages/url-utils/lib/utils/html-relative-to-transform-ready.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import htmlTransform from './html-transform';
import relativeToTransformReady from './relative-to-transform-ready';

export interface HtmlRelativeToTransformReadyOptions {
replacementStr?: string;
assetsOnly?: boolean;
staticImageUrlPrefix?: string;
secure?: boolean;
earlyExitMatchStr?: string;
}

export const htmlRelativeToTransformReady = function (html = '', root: string, itemPath?: string | HtmlRelativeToTransformReadyOptions | null, _options?: HtmlRelativeToTransformReadyOptions): string {
// itemPath is optional, if it's an object may be the options param instead
let actualItemPath: string | null | undefined = itemPath as string | null | undefined;
if (typeof itemPath === 'object' && !_options) {
_options = itemPath as HtmlRelativeToTransformReadyOptions;
actualItemPath = null;
}

const defaultOptions: HtmlRelativeToTransformReadyOptions = {
replacementStr: '__GHOST_URL__'
};
const overrideOptions: HtmlRelativeToTransformReadyOptions = {
secure: false
};
const options = Object.assign({}, defaultOptions, _options, overrideOptions);

// exit early and avoid parsing if the content does not contain an attribute we might transform
options.earlyExitMatchStr = 'href=|src=|srcset=';
if (options.assetsOnly) {
options.earlyExitMatchStr = options.staticImageUrlPrefix;
}

return htmlTransform(html, root, relativeToTransformReady, actualItemPath as string | null, options);
};

export default htmlRelativeToTransformReady;
20 changes: 20 additions & 0 deletions packages/url-utils/lib/utils/html-to-transform-ready.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import htmlRelativeToAbsolute from './html-relative-to-absolute';
import htmlAbsoluteToTransformReady from './html-absolute-to-transform-ready';

interface HtmlToTransformReadyOptions {
assetsOnly?: boolean;
staticImageUrlPrefix?: string;
replacementStr?: string;
}

function htmlToTransformReady(html: string, siteUrl: string, itemPath?: string | HtmlToTransformReadyOptions | null, options?: HtmlToTransformReadyOptions): string {
let actualItemPath: string | null | undefined = itemPath as string | null | undefined;
if (typeof itemPath === 'object' && !options) {
options = itemPath as HtmlToTransformReadyOptions;
actualItemPath = null;
}
const absolute = htmlRelativeToAbsolute(html, siteUrl, actualItemPath || null, options);
return htmlAbsoluteToTransformReady(absolute, siteUrl, options);
}

export default htmlToTransformReady;
Loading