diff --git a/packages/url-utils/.eslintignore b/packages/url-utils/.eslintignore new file mode 100644 index 000000000..2e6b92a0c --- /dev/null +++ b/packages/url-utils/.eslintignore @@ -0,0 +1,2 @@ +lib/ +coverage/ diff --git a/packages/url-utils/index.js b/packages/url-utils/index.js deleted file mode 100644 index b435fc186..000000000 --- a/packages/url-utils/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./lib/UrlUtils'); diff --git a/packages/url-utils/lib/UrlUtils.js b/packages/url-utils/lib/UrlUtils.js deleted file mode 100644 index aaab3ba75..000000000 --- a/packages/url-utils/lib/UrlUtils.js +++ /dev/null @@ -1,544 +0,0 @@ -// Contains all path information to be used throughout the codebase. -const _ = require('lodash'); -const utils = require('./utils'); - -// similar to Object.assign but will not override defaults if a source value is undefined -function assignOptions(target, ...sources) { - const options = sources.map((x) => { - return Object.entries(x) - .filter(([, value]) => value !== undefined) - .reduce((obj, [key, value]) => (obj[key] = value, obj), {}); - }); - return Object.assign(target, ...options); -} -module.exports = class UrlUtils { - /** - * Initialization method to pass in URL configurations - * @param {Object} options - * @param {Function} options.getSubdir - * @param {Function} options.getSiteUrl - * @param {Function} options.getAdminUrl Ghost instance admin URL - * @param {String} [options.baseApiPath='/ghost/api'] static prefix for serving API. Should not te passed in, unless the API is being run under custom URL - * @param {('content' | 'admin')} [options.defaultApiType='content'] default API type to be used - * @param {Object} [options.slugs] object with 2 properties reserved and protected containing arrays of special case slugs - * @param {Number} [options.redirectCacheMaxAge] - * @param {String} [options.staticImageUrlPrefix='content/images'] static prefix for serving images. Should not be passed in, unless customizing ghost instance image storage - */ - constructor(options = {}) { - const defaultOptions = { - slugs: null, - redirectCacheMaxAge: null, - baseApiPath: '/ghost/api', - defaultApiType: 'content', - staticImageUrlPrefix: 'content/images' - }; - - this._config = assignOptions({}, defaultOptions, options); - - this.getSubdir = options.getSubdir; - this.getSiteUrl = options.getSiteUrl; - this.getAdminUrl = options.getAdminUrl; - } - - getProtectedSlugs() { - let subDir = this.getSubdir(); - - if (!_.isEmpty(subDir)) { - return this._config.slugs.concat([subDir.split('/').pop()]); - } else { - return this._config.slugs; - } - } - - /** urlJoin - * Returns a URL/path for internal use in Ghost. - * @param {string} arguments takes arguments and concats those to a valid path/URL. - * @return {string} URL concatinated URL/path of arguments. - */ - urlJoin(...parts) { - return utils.urlJoin(parts, {rootUrl: this.getSiteUrl()}); - } - - // ## createUrl - // Simple url creation from a given path - // Ensures that our urls contain the subdirectory if there is one - // And are correctly formatted as either relative or absolute - // Usage: - // createUrl('/', true) -> http://my-ghost-blog.com/ - // E.g. /blog/ subdir - // createUrl('/welcome-to-ghost/') -> /blog/welcome-to-ghost/ - // Parameters: - // - urlPath - string which must start and end with a slash - // - absolute (optional, default:false) - boolean whether or not the url should be absolute - // Returns: - // - a URL which always ends with a slash - createUrl(urlPath = '/', absolute = false, trailingSlash) { - let base; - - // create base of url, always ends without a slash - if (absolute) { - base = this.getSiteUrl(); - } else { - base = this.getSubdir(); - } - - if (trailingSlash) { - if (!urlPath.match(/\/$/)) { - urlPath += '/'; - } - } - - return this.urlJoin(base, urlPath); - } - - // ## urlFor - // Synchronous url creation for a given context - // Can generate a url for a named path and given path. - // Determines what sort of context it has been given, and delegates to the correct generation method, - // Finally passing to createUrl, to ensure any subdirectory is honoured, and the url is absolute if needed - // Usage: - // urlFor('home', true) -> http://my-ghost-blog.com/ - // E.g. /blog/ subdir - // urlFor({relativeUrl: '/my-static-page/'}) -> /blog/my-static-page/ - // Parameters: - // - context - a string, or json object describing the context for which you need a url - // - data (optional) - a json object containing data needed to generate a url - // - absolute (optional, default:false) - boolean whether or not the url should be absolute - // This is probably not the right place for this, but it's the best place for now - // @TODO: rewrite, very hard to read, create private functions! - urlFor(context, data, absolute) { - let urlPath = '/'; - let imagePathRe; - let knownObjects = ['image', 'nav']; - let baseUrl; - let hostname; - - // this will become really big - let knownPaths = { - home: '/', - sitemap_xsl: '/sitemap.xsl' - }; - - // Make data properly optional - if (_.isBoolean(data)) { - absolute = data; - data = null; - } - - if (_.isObject(context) && context.relativeUrl) { - urlPath = context.relativeUrl; - } else if (_.isString(context) && _.indexOf(knownObjects, context) !== -1) { - if (context === 'image' && data.image) { - urlPath = data.image; - imagePathRe = new RegExp('^' + this.getSubdir() + '/' + this._config.staticImageUrlPrefix); - absolute = imagePathRe.test(data.image) ? absolute : false; - - if (absolute) { - // Remove the sub-directory from the URL because ghostConfig will add it back. - urlPath = urlPath.replace(new RegExp('^' + this.getSubdir()), ''); - baseUrl = this.getSiteUrl().replace(/\/$/, ''); - urlPath = baseUrl + urlPath; - } - - return urlPath; - } else if (context === 'nav' && data.nav) { - urlPath = data.nav.url; - baseUrl = this.getSiteUrl(); - hostname = baseUrl.split('//')[1]; - - // If the hostname is present in the url - if (urlPath.indexOf(hostname) > -1 - // do no not apply, if there is a subdomain, or a mailto link - && !urlPath.split(hostname)[0].match(/\.|mailto:/) - // do not apply, if there is a port after the hostname - && urlPath.split(hostname)[1].substring(0, 1) !== ':') { - // make link relative to account for possible mismatch in http/https etc, force absolute - urlPath = urlPath.split(hostname)[1]; - urlPath = this.urlJoin('/', urlPath); - absolute = true; - } - } - } else if (context === 'home' && absolute) { - urlPath = this.getSiteUrl(); - - // CASE: there are cases where urlFor('home') needs to be returned without trailing - // slash e. g. the `{{@site.url}}` helper. See https://github.com/TryGhost/Ghost/issues/8569 - if (data && data.trailingSlash === false) { - urlPath = urlPath.replace(/\/$/, ''); - } - } else if (context === 'admin') { - let adminUrl = this.getAdminUrl() || this.getSiteUrl(); - let adminPath = '/ghost/'; - - if (absolute) { - urlPath = this.urlJoin(adminUrl, adminPath); - } else { - urlPath = adminPath; - } - } else if (context === 'api') { - let adminUrl = this.getAdminUrl() || this.getSiteUrl(); - let apiPath = this._config.baseApiPath + '/'; - - if (data.type && ['admin', 'content'].includes(data.type)) { - apiPath += data.type; - } else { - apiPath += this._config.defaultApiType; - } - - // Ensure we end with a trailing slash - apiPath += '/'; - - if (absolute) { - urlPath = this.urlJoin(adminUrl, apiPath); - } else { - urlPath = apiPath; - } - } else if (_.isString(context) && _.indexOf(_.keys(knownPaths), context) !== -1) { - // trying to create a url for a named path - urlPath = knownPaths[context]; - } - - // This url already has a protocol so is likely an external url to be returned - // or it is an alternative scheme, protocol-less, or an anchor-only path - if (urlPath && (urlPath.indexOf('://') !== -1 || urlPath.match(/^(\/\/|#|[a-zA-Z0-9-]+:)/))) { - return urlPath; - } - - return this.createUrl(urlPath, absolute); - } - - redirect301(res, redirectUrl) { - res.set({'Cache-Control': 'public, max-age=' + this._config.redirectCacheMaxAge}); - return res.redirect(301, redirectUrl); - } - - redirectToAdmin(status, res, adminPath) { - let redirectUrl = this.urlJoin(this.urlFor('admin', true), adminPath, '/'); - - if (status === 301) { - return this.redirect301(res, redirectUrl); - } - return res.redirect(redirectUrl); - } - - absoluteToRelative(url, options) { - return utils.absoluteToRelative(url, this.getSiteUrl(), options); - } - - relativeToAbsolute(url, options) { - return utils.relativeToAbsolute(url, this.getSiteUrl(), options); - } - - toTransformReady(url, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - return utils.toTransformReady(url, this.getSiteUrl(), itemPath, options); - } - - absoluteToTransformReady(url, options) { - return utils.absoluteToTransformReady(url, this.getSiteUrl(), options); - } - - relativeToTransformReady(url, options) { - return utils.relativeToTransformReady(url, this.getSiteUrl(), options); - } - - transformReadyToAbsolute(url, options) { - return utils.transformReadyToAbsolute(url, this.getSiteUrl(), options); - } - - transformReadyToRelative(url, options) { - return utils.transformReadyToRelative(url, this.getSiteUrl(), options); - } - - htmlToTransformReady(html, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - return utils.htmlToTransformReady(html, this.getSiteUrl(), itemPath, options); - } - - /** - * Convert relative URLs in html into absolute URLs - * @param {string} html - * @param {string} itemPath (path of current context) - * @param {Object} options - * @returns {object} htmlContent - * @description Takes html, blog url and item path and converts relative url into - * absolute urls. Returns an object. The html string can be accessed by calling `html()` on - * the variable that takes the result of this function - */ - htmlRelativeToAbsolute(html, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix - }; - const _options = assignOptions({}, defaultOptions, options || {}); - return utils.htmlRelativeToAbsolute(html, this.getSiteUrl(), itemPath, _options); - } - - htmlRelativeToTransformReady(html, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix - }; - const _options = assignOptions({}, defaultOptions, options || {}); - return utils.htmlRelativeToTransformReady(html, this.getSiteUrl(), itemPath, _options); - } - - htmlAbsoluteToRelative(html, options = {}) { - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix - }; - const _options = assignOptions({}, defaultOptions, options); - return utils.htmlAbsoluteToRelative(html, this.getSiteUrl(), _options); - } - - htmlAbsoluteToTransformReady(html, options = {}) { - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix - }; - const _options = assignOptions({}, defaultOptions, options); - return utils.htmlAbsoluteToTransformReady(html, this.getSiteUrl(), _options); - } - - markdownToTransformReady(markdown, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - return utils.markdownToTransformReady(markdown, this.getSiteUrl(), itemPath, options); - } - - markdownRelativeToAbsolute(markdown, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix - }; - const _options = assignOptions({}, defaultOptions, options || {}); - return utils.markdownRelativeToAbsolute(markdown, this.getSiteUrl(), itemPath, _options); - } - - markdownRelativeToTransformReady(markdown, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix - }; - const _options = assignOptions({}, defaultOptions, options || {}); - return utils.markdownRelativeToTransformReady(markdown, this.getSiteUrl(), itemPath, _options); - } - - markdownAbsoluteToRelative(markdown, options = {}) { - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix - }; - const _options = assignOptions({}, defaultOptions, options); - return utils.markdownAbsoluteToRelative(markdown, this.getSiteUrl(), _options); - } - - markdownAbsoluteToTransformReady(markdown, options) { - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix - }; - const _options = assignOptions({}, defaultOptions, options); - return utils.markdownAbsoluteToTransformReady(markdown, this.getSiteUrl(), _options); - } - - mobiledocToTransformReady(serializedMobiledoc, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const defaultOptions = { - cardTransformers: this._config.cardTransformers - }; - const _options = assignOptions({}, defaultOptions, options || {}); - return utils.mobiledocToTransformReady(serializedMobiledoc, this.getSiteUrl(), itemPath, _options); - } - - mobiledocRelativeToAbsolute(serializedMobiledoc, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix, - cardTransformers: this._config.cardTransformers - }; - const _options = assignOptions({}, defaultOptions, options || {}); - return utils.mobiledocRelativeToAbsolute(serializedMobiledoc, this.getSiteUrl(), itemPath, _options); - } - - mobiledocRelativeToTransformReady(serializedMobiledoc, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix, - cardTransformers: this._config.cardTransformers - }; - const _options = assignOptions({}, defaultOptions, options || {}); - return utils.mobiledocRelativeToTransformReady(serializedMobiledoc, this.getSiteUrl(), itemPath, _options); - } - - mobiledocAbsoluteToRelative(serializedMobiledoc, options = {}) { - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix, - cardTransformers: this._config.cardTransformers - }; - const _options = assignOptions({}, defaultOptions, options); - return utils.mobiledocAbsoluteToRelative(serializedMobiledoc, this.getSiteUrl(), _options); - } - - mobiledocAbsoluteToTransformReady(serializedMobiledoc, options = {}) { - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix, - cardTransformers: this._config.cardTransformers - }; - const _options = assignOptions({}, defaultOptions, options); - return utils.mobiledocAbsoluteToTransformReady(serializedMobiledoc, this.getSiteUrl(), _options); - } - - lexicalToTransformReady(serializedLexical, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const defaultOptions = { - cardTransformers: this._config.cardTransformers - }; - const _options = assignOptions({}, defaultOptions, options || {}); - return utils.lexicalToTransformReady(serializedLexical, this.getSiteUrl(), itemPath, _options); - } - - lexicalRelativeToAbsolute(serializedLexical, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix, - cardTransformers: this._config.cardTransformers - }; - const _options = assignOptions({}, defaultOptions, options || {}); - return utils.lexicalRelativeToAbsolute(serializedLexical, this.getSiteUrl(), itemPath, _options); - } - - lexicalRelativeToTransformReady(serializedLexical, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix, - cardTransformers: this._config.cardTransformers - }; - const _options = assignOptions({}, defaultOptions, options || {}); - return utils.lexicalRelativeToTransformReady(serializedLexical, this.getSiteUrl(), itemPath, _options); - } - - lexicalAbsoluteToRelative(serializedLexical, options = {}) { - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix, - cardTransformers: this._config.cardTransformers - }; - const _options = assignOptions({}, defaultOptions, options); - return utils.lexicalAbsoluteToRelative(serializedLexical, this.getSiteUrl(), _options); - } - - lexicalAbsoluteToTransformReady(serializedLexical, options = {}) { - const defaultOptions = { - assetsOnly: false, - staticImageUrlPrefix: this._config.staticImageUrlPrefix, - cardTransformers: this._config.cardTransformers - }; - const _options = assignOptions({}, defaultOptions, options); - return utils.lexicalAbsoluteToTransformReady(serializedLexical, this.getSiteUrl(), _options); - } - - plaintextToTransformReady(plaintext, options = {}) { - const defaultOptions = { - staticImageUrlPrefix: this._config.staticImageUrlPrefix - }; - const _options = assignOptions({}, defaultOptions, options); - return utils.plaintextToTransformReady(plaintext, this.getSiteUrl(), _options); - } - - /** - * Return whether the provided URL is part of the site (checks if same domain and within subdirectory) - * @param {URL} url - * @param {string} [context] describing the context for which you need to check a url - * @returns {boolean} - */ - isSiteUrl(url, context = 'home') { - const siteUrl = new URL(this.urlFor(context, true)); - if (siteUrl.host === url.host) { - if (url.pathname.startsWith(siteUrl.pathname)) { - return true; - } - return false; - } - return false; - } - - get isSSL() { - return utils.isSSL; - } - - get replacePermalink() { - return utils.replacePermalink; - } - - get deduplicateDoubleSlashes() { - return utils.deduplicateDoubleSlashes; - } - - /** - * If you request **any** image in Ghost, it get's served via - * http://your-blog.com/content/images/2017/01/02/author.png - * - * /content/images/ is a static prefix for serving images! - * - * But internally the image is located for example in your custom content path: - * my-content/another-dir/images/2017/01/02/author.png - */ - get STATIC_IMAGE_URL_PREFIX() { - return this._config.staticImageUrlPrefix; - } - - // expose underlying functions to ease testing - get _utils() { - return utils; - } -}; diff --git a/packages/url-utils/lib/utils/absolute-to-transform-ready.js b/packages/url-utils/lib/utils/absolute-to-transform-ready.js deleted file mode 100644 index 857961b49..000000000 --- a/packages/url-utils/lib/utils/absolute-to-transform-ready.js +++ /dev/null @@ -1,39 +0,0 @@ -const absoluteToRelative = require('./absolute-to-relative'); - -const absoluteToTransformReady = function (url, root, _options) { - const defaultOptions = { - 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}`; -}; - -module.exports = absoluteToTransformReady; diff --git a/packages/url-utils/lib/utils/deduplicate-double-slashes.js b/packages/url-utils/lib/utils/deduplicate-double-slashes.js deleted file mode 100644 index beda744d3..000000000 --- a/packages/url-utils/lib/utils/deduplicate-double-slashes.js +++ /dev/null @@ -1,5 +0,0 @@ -function deduplicateDoubleSlashes(url) { - return url.replace(/\/\//g, '/'); -} - -module.exports = deduplicateDoubleSlashes; diff --git a/packages/url-utils/lib/utils/html-absolute-to-relative.js b/packages/url-utils/lib/utils/html-absolute-to-relative.js deleted file mode 100644 index 357d693cb..000000000 --- a/packages/url-utils/lib/utils/html-absolute-to-relative.js +++ /dev/null @@ -1,20 +0,0 @@ -const htmlTransform = require('./html-transform'); -const absoluteToRelative = require('./absolute-to-relative'); - -function htmlAbsoluteToRelative(html = '', siteUrl, _options) { - const defaultOptions = {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, _siteUrl, _itemPath, __options) { - return absoluteToRelative(_url, _siteUrl, __options); - }; - - return htmlTransform(html, siteUrl, transformFunction, '', options); -} - -module.exports = htmlAbsoluteToRelative; diff --git a/packages/url-utils/lib/utils/html-absolute-to-transform-ready.js b/packages/url-utils/lib/utils/html-absolute-to-transform-ready.js deleted file mode 100644 index 2eb7f4243..000000000 --- a/packages/url-utils/lib/utils/html-absolute-to-transform-ready.js +++ /dev/null @@ -1,20 +0,0 @@ -const htmlTransform = require('./html-transform'); -const absoluteToTransformReady = require('./absolute-to-transform-ready'); - -const htmlAbsoluteToTransformReady = function (html = '', siteUrl, _options) { - const defaultOptions = {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, _siteUrl, _itemPath, __options) { - return absoluteToTransformReady(_url, _siteUrl, __options); - }; - - return htmlTransform(html, siteUrl, transformFunction, '', options); -}; - -module.exports = htmlAbsoluteToTransformReady; diff --git a/packages/url-utils/lib/utils/html-relative-to-absolute.js b/packages/url-utils/lib/utils/html-relative-to-absolute.js deleted file mode 100644 index d0789cf1d..000000000 --- a/packages/url-utils/lib/utils/html-relative-to-absolute.js +++ /dev/null @@ -1,17 +0,0 @@ -const htmlTransform = require('./html-transform'); -const relativeToAbsolute = require('./relative-to-absolute'); - -function htmlRelativeToAbsolute(html = '', siteUrl, itemPath, _options) { - const defaultOptions = {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, options); -} - -module.exports = htmlRelativeToAbsolute; diff --git a/packages/url-utils/lib/utils/html-relative-to-transform-ready.js b/packages/url-utils/lib/utils/html-relative-to-transform-ready.js deleted file mode 100644 index 7eac11dba..000000000 --- a/packages/url-utils/lib/utils/html-relative-to-transform-ready.js +++ /dev/null @@ -1,28 +0,0 @@ -const htmlTransform = require('./html-transform'); -const relativeToTransformReady = require('./relative-to-transform-ready'); - -const htmlRelativeToTransformReady = function (html = '', root, itemPath, _options) { - // itemPath is optional, if it's an object may be the options param instead - if (typeof itemPath === 'object' && !_options) { - _options = itemPath; - itemPath = null; - } - - const defaultOptions = { - replacementStr: '__GHOST_URL__' - }; - const overrideOptions = { - 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, itemPath, options); -}; - -module.exports = htmlRelativeToTransformReady; diff --git a/packages/url-utils/lib/utils/html-to-transform-ready.js b/packages/url-utils/lib/utils/html-to-transform-ready.js deleted file mode 100644 index 8606db730..000000000 --- a/packages/url-utils/lib/utils/html-to-transform-ready.js +++ /dev/null @@ -1,13 +0,0 @@ -const htmlRelativeToAbsolute = require('./html-relative-to-absolute'); -const htmlAbsoluteToTransformReady = require('./html-absolute-to-transform-ready'); - -function htmlToTransformReady(html, siteUrl, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const absolute = htmlRelativeToAbsolute(html, siteUrl, itemPath, options); - return htmlAbsoluteToTransformReady(absolute, siteUrl, options); -} - -module.exports = htmlToTransformReady; diff --git a/packages/url-utils/lib/utils/index.js b/packages/url-utils/lib/utils/index.js deleted file mode 100644 index ce7073b9b..000000000 --- a/packages/url-utils/lib/utils/index.js +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = { - absoluteToRelative: require('./absolute-to-relative'), - absoluteToTransformReady: require('./absolute-to-transform-ready'), - deduplicateDoubleSlashes: require('./deduplicate-double-slashes'), - deduplicateSubdirectory: require('./deduplicate-subdirectory'), - htmlAbsoluteToRelative: require('./html-absolute-to-relative'), - htmlRelativeToAbsolute: require('./html-relative-to-absolute'), - htmlAbsoluteToTransformReady: require('./html-absolute-to-transform-ready'), - htmlRelativeToTransformReady: require('./html-relative-to-transform-ready'), - htmlToTransformReady: require('./html-to-transform-ready'), - isSSL: require('./is-ssl'), - markdownAbsoluteToRelative: require('./markdown-absolute-to-relative'), - markdownRelativeToAbsolute: require('./markdown-relative-to-absolute'), - markdownAbsoluteToTransformReady: require('./markdown-absolute-to-transform-ready'), - markdownRelativeToTransformReady: require('./markdown-relative-to-transform-ready'), - markdownToTransformReady: require('./markdown-to-transform-ready'), - mobiledocAbsoluteToRelative: require('./mobiledoc-absolute-to-relative'), - mobiledocRelativeToAbsolute: require('./mobiledoc-relative-to-absolute'), - mobiledocAbsoluteToTransformReady: require('./mobiledoc-absolute-to-transform-ready'), - mobiledocRelativeToTransformReady: require('./mobiledoc-relative-to-transform-ready'), - mobiledocToTransformReady: require('./mobiledoc-to-transform-ready'), - lexicalAbsoluteToRelative: require('./lexical-absolute-to-relative'), - lexicalRelativeToAbsolute: require('./lexical-relative-to-absolute'), - lexicalAbsoluteToTransformReady: require('./lexical-absolute-to-transform-ready'), - lexicalRelativeToTransformReady: require('./lexical-relative-to-transform-ready'), - lexicalToTransformReady: require('./lexical-to-transform-ready'), - plaintextAbsoluteToTransformReady: require('./plaintext-absolute-to-transform-ready'), - plaintextRelativeToTransformReady: require('./plaintext-relative-to-transform-ready'), - plaintextToTransformReady: require('./plaintext-to-transform-ready'), - relativeToAbsolute: require('./relative-to-absolute'), - relativeToTransformReady: require('./relative-to-transform-ready'), - replacePermalink: require('./replace-permalink'), - stripSubdirectoryFromPath: require('./strip-subdirectory-from-path'), - toTransformReady: require('./to-transform-ready'), - transformReadyToAbsolute: require('./transform-ready-to-absolute'), - transformReadyToRelative: require('./transform-ready-to-relative'), - urlJoin: require('./url-join') -}; diff --git a/packages/url-utils/lib/utils/is-ssl.js b/packages/url-utils/lib/utils/is-ssl.js deleted file mode 100644 index f88bcd8a1..000000000 --- a/packages/url-utils/lib/utils/is-ssl.js +++ /dev/null @@ -1,9 +0,0 @@ -// require the whatwg compatible URL library (same behaviour in node and browser) -const {URL} = require('url'); - -function isSSL(urlToParse) { - const {protocol} = new URL(urlToParse); - return protocol === 'https:'; -} - -module.exports = isSSL; diff --git a/packages/url-utils/lib/utils/lexical-absolute-to-relative.js b/packages/url-utils/lib/utils/lexical-absolute-to-relative.js deleted file mode 100644 index 05e8c0c5e..000000000 --- a/packages/url-utils/lib/utils/lexical-absolute-to-relative.js +++ /dev/null @@ -1,16 +0,0 @@ -const absoluteToRelative = require('./absolute-to-relative'); -const lexicalTransform = require('./lexical-transform'); - -function lexicalAbsoluteToRelative(serializedLexical, siteUrl, _options = {}) { - const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}}; - const overrideOptions = {siteUrl, transformType: 'absoluteToRelative'}; - const options = Object.assign({}, defaultOptions, _options, overrideOptions); - - const transformFunction = function (_url, _siteUrl, _itemPath, __options) { - return absoluteToRelative(_url, _siteUrl, __options); - }; - - return lexicalTransform(serializedLexical, siteUrl, transformFunction, '', options); -} - -module.exports = lexicalAbsoluteToRelative; diff --git a/packages/url-utils/lib/utils/lexical-absolute-to-transform-ready.js b/packages/url-utils/lib/utils/lexical-absolute-to-transform-ready.js deleted file mode 100644 index a9485a97f..000000000 --- a/packages/url-utils/lib/utils/lexical-absolute-to-transform-ready.js +++ /dev/null @@ -1,16 +0,0 @@ -const absoluteToTransformReady = require('./absolute-to-transform-ready'); -const lexicalTransform = require('./lexical-transform'); - -function lexicalAbsoluteToRelative(serializedLexical, siteUrl, _options = {}) { - const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}}; - const overrideOptions = {siteUrl, transformType: 'toTransformReady'}; - const options = Object.assign({}, defaultOptions, _options, overrideOptions); - - const transformFunction = function (_url, _siteUrl, _itemPath, __options) { - return absoluteToTransformReady(_url, _siteUrl, __options); - }; - - return lexicalTransform(serializedLexical, siteUrl, transformFunction, '', options); -} - -module.exports = lexicalAbsoluteToRelative; diff --git a/packages/url-utils/lib/utils/lexical-relative-to-absolute.js b/packages/url-utils/lib/utils/lexical-relative-to-absolute.js deleted file mode 100644 index b0c0a7a9d..000000000 --- a/packages/url-utils/lib/utils/lexical-relative-to-absolute.js +++ /dev/null @@ -1,12 +0,0 @@ -const relativeToAbsolute = require('./relative-to-absolute'); -const lexicalTransform = require('./lexical-transform'); - -function lexicalRelativeToAbsolute(serializedLexical, siteUrl, itemPath, _options = {}) { - const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}}; - const overrideOptions = {siteUrl, itemPath, transformType: 'relativeToAbsolute'}; - const options = Object.assign({}, defaultOptions, _options, overrideOptions); - - return lexicalTransform(serializedLexical, siteUrl, relativeToAbsolute, itemPath, options); -} - -module.exports = lexicalRelativeToAbsolute; diff --git a/packages/url-utils/lib/utils/lexical-relative-to-transform-ready.js b/packages/url-utils/lib/utils/lexical-relative-to-transform-ready.js deleted file mode 100644 index 02d16310b..000000000 --- a/packages/url-utils/lib/utils/lexical-relative-to-transform-ready.js +++ /dev/null @@ -1,12 +0,0 @@ -const relativeToTransformReady = require('./relative-to-transform-ready'); -const lexicalTransform = require('./lexical-transform'); - -function lexicalRelativeToTransformReady(serializedLexical, siteUrl, itemPath, _options = {}) { - const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}}; - const overrideOptions = {siteUrl, transformType: 'toTransformReady'}; - const options = Object.assign({}, defaultOptions, _options, overrideOptions); - - return lexicalTransform(serializedLexical, siteUrl, relativeToTransformReady, itemPath, options); -} - -module.exports = lexicalRelativeToTransformReady; diff --git a/packages/url-utils/lib/utils/lexical-to-transform-ready.js b/packages/url-utils/lib/utils/lexical-to-transform-ready.js deleted file mode 100644 index 2fff3474d..000000000 --- a/packages/url-utils/lib/utils/lexical-to-transform-ready.js +++ /dev/null @@ -1,13 +0,0 @@ -const lexicalRelativeToAbsolute = require('./lexical-relative-to-absolute'); -const lexicalAbsoluteToTransformReady = require('./lexical-absolute-to-transform-ready'); - -function lexicalToTransformReady(lexical, siteUrl, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const absolute = lexicalRelativeToAbsolute(lexical, siteUrl, itemPath, options); - return lexicalAbsoluteToTransformReady(absolute, siteUrl, options); -} - -module.exports = lexicalToTransformReady; diff --git a/packages/url-utils/lib/utils/lexical-transform.js b/packages/url-utils/lib/utils/lexical-transform.js deleted file mode 100644 index 288152fa2..000000000 --- a/packages/url-utils/lib/utils/lexical-transform.js +++ /dev/null @@ -1,77 +0,0 @@ -const _ = require('lodash'); - -// options.transformMap = { -// relativeToAbsolute: { -// url: (url, siteUrl, itemPath, options) => 'transformedUrl', -// html: (html, siteUrl, itemPath, options) => 'transformedHtml', -// } -// } -// options.transformType = 'relativeToAbsolute' - -function lexicalTransform(serializedLexical, siteUrl, transformFunction, itemPath, _options = {}) { - const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}}; - const options = Object.assign({}, defaultOptions, _options, {siteUrl, itemPath}); - - if (!serializedLexical) { - return serializedLexical; - } - - // function only accepts serialized lexical so there's no chance of accidentally - // modifying pass-by-reference objects - const lexical = JSON.parse(serializedLexical); - - if (!lexical?.root?.children) { - return serializedLexical; - } - - // create a map of node types to urlTransformMap objects - // e.g. {'image': {src: 'url', caption: 'html'} - const nodeMap = new Map(); - options.nodes.forEach(node => node.urlTransformMap && nodeMap.set(node.getType(), node.urlTransformMap)); - - const transformProperty = function (obj, propertyPath, transform) { - const propertyValue = _.get(obj, propertyPath); - - if (Array.isArray(propertyValue)) { - propertyValue.forEach((item) => { - // arrays of objects need to be defined as a nested object in the urlTransformMap - // so the `transform` value is that nested object - Object.entries(transform).forEach(([itemPropertyPath, itemTransform]) => { - transformProperty(item, itemPropertyPath, itemTransform); - }); - }); - - return; - } - - if (propertyValue) { - _.set(obj, propertyPath, options.transformMap[options.transformType][transform](propertyValue)); - } - }; - - // recursively walk the Lexical node tree transforming any card data properties and links - const transformChildren = function (children) { - for (const child of children) { - const isCard = child.type && nodeMap.has(child.type); - const isLink = !!child.url; - - if (isCard) { - Object.entries(nodeMap.get(child.type)).forEach(([propertyPath, transform]) => { - transformProperty(child, propertyPath, transform); - }); - } else if (isLink) { - child.url = transformFunction(child.url, siteUrl, itemPath, options); - } - - if (child.children) { - transformChildren(child.children); - } - } - }; - - transformChildren(lexical.root.children); - - return JSON.stringify(lexical); -} - -module.exports = lexicalTransform; diff --git a/packages/url-utils/lib/utils/markdown-absolute-to-relative.js b/packages/url-utils/lib/utils/markdown-absolute-to-relative.js deleted file mode 100644 index 0bd871090..000000000 --- a/packages/url-utils/lib/utils/markdown-absolute-to-relative.js +++ /dev/null @@ -1,25 +0,0 @@ -const markdownTransform = require('./markdown-transform'); -const absoluteToRelative = require('./absolute-to-relative'); -const htmlAbsoluteToRelative = require('./html-absolute-to-relative'); - -function markdownAbsoluteToRelative(markdown = '', siteUrl, _options = {}) { - const defaultOptions = {assetsOnly: false, ignoreProtocol: true}; - const options = Object.assign({}, defaultOptions, _options); - - options.earlyExitMatchStr = options.ignoreProtocol ? siteUrl.replace(/http:|https:/, '') : siteUrl; - options.earlyExitMatchStr = options.earlyExitMatchStr.replace(/\/$/, ''); - - // need to ignore itemPath because absoluteToRelative functions doen't take that option - const transformFunctions = { - html(_url, _siteUrl, _itemPath, __options) { - return htmlAbsoluteToRelative(_url, _siteUrl, __options); - }, - url(_url, _siteUrl, _itemPath, __options) { - return absoluteToRelative(_url, _siteUrl, __options); - } - }; - - return markdownTransform(markdown, siteUrl, transformFunctions, '', options); -} - -module.exports = markdownAbsoluteToRelative; diff --git a/packages/url-utils/lib/utils/markdown-absolute-to-transform-ready.js b/packages/url-utils/lib/utils/markdown-absolute-to-transform-ready.js deleted file mode 100644 index e5133d957..000000000 --- a/packages/url-utils/lib/utils/markdown-absolute-to-transform-ready.js +++ /dev/null @@ -1,25 +0,0 @@ -const markdownTransform = require('./markdown-transform'); -const absoluteToTransformReady = require('./absolute-to-transform-ready'); -const htmlAbsoluteToTransformReady = require('./html-absolute-to-transform-ready'); - -function markdownAbsoluteToTransformReady(markdown = '', siteUrl, _options = {}) { - const defaultOptions = {assetsOnly: false, ignoreProtocol: true}; - const options = Object.assign({}, defaultOptions, _options); - - options.earlyExitMatchStr = options.ignoreProtocol ? siteUrl.replace(/http:|https:/, '') : siteUrl; - options.earlyExitMatchStr = options.earlyExitMatchStr.replace(/\/$/, ''); - - // need to ignore itemPath because absoluteToTransformReady functions doen't take that option - const transformFunctions = { - html(_url, _siteUrl, _itemPath, __options) { - return htmlAbsoluteToTransformReady(_url, _siteUrl, __options); - }, - url(_url, _siteUrl, _itemPath, __options) { - return absoluteToTransformReady(_url, _siteUrl, __options); - } - }; - - return markdownTransform(markdown, siteUrl, transformFunctions, '', options); -} - -module.exports = markdownAbsoluteToTransformReady; diff --git a/packages/url-utils/lib/utils/markdown-relative-to-absolute.js b/packages/url-utils/lib/utils/markdown-relative-to-absolute.js deleted file mode 100644 index 9f9439e7a..000000000 --- a/packages/url-utils/lib/utils/markdown-relative-to-absolute.js +++ /dev/null @@ -1,22 +0,0 @@ -const markdownTransform = require('./markdown-transform'); -const htmlRelativeToAbsolute = require('./html-relative-to-absolute'); -const relativeToAbsolute = require('./relative-to-absolute'); - -function markdownRelativeToAbsolute(markdown = '', siteUrl, itemPath, _options = {}) { - const defaultOptions = {assetsOnly: false}; - const options = Object.assign({}, defaultOptions, _options); - - options.earlyExitMatchStr = '\\]\\([^\\s\\)]|href=|src=|srcset='; - if (options.assetsOnly) { - options.earlyExitMatchStr = options.staticImageUrlPrefix; - } - - const transformFunctions = { - html: htmlRelativeToAbsolute, - url: relativeToAbsolute - }; - - return markdownTransform(markdown, siteUrl, transformFunctions, itemPath, options); -} - -module.exports = markdownRelativeToAbsolute; diff --git a/packages/url-utils/lib/utils/markdown-relative-to-transform-ready.js b/packages/url-utils/lib/utils/markdown-relative-to-transform-ready.js deleted file mode 100644 index 2ec4fd59e..000000000 --- a/packages/url-utils/lib/utils/markdown-relative-to-transform-ready.js +++ /dev/null @@ -1,22 +0,0 @@ -const markdownTransform = require('./markdown-transform'); -const htmlRelativeToTransformReady = require('./html-relative-to-transform-ready'); -const relativeToTransformReady = require('./relative-to-transform-ready'); - -function markdownRelativeToTransformReady(markdown = '', siteUrl, itemPath, _options = {}) { - const defaultOptions = {assetsOnly: false}; - const options = Object.assign({}, defaultOptions, _options); - - options.earlyExitMatchStr = '\\]\\([^\\s\\)]|href=|src=|srcset='; - if (options.assetsOnly) { - options.earlyExitMatchStr = options.staticImageUrlPrefix; - } - - const transformFunctions = { - html: htmlRelativeToTransformReady, - url: relativeToTransformReady - }; - - return markdownTransform(markdown, siteUrl, transformFunctions, itemPath, options); -} - -module.exports = markdownRelativeToTransformReady; diff --git a/packages/url-utils/lib/utils/markdown-to-transform-ready.js b/packages/url-utils/lib/utils/markdown-to-transform-ready.js deleted file mode 100644 index 4ad494ebf..000000000 --- a/packages/url-utils/lib/utils/markdown-to-transform-ready.js +++ /dev/null @@ -1,13 +0,0 @@ -const markdownRelativeToAbsolute = require('./markdown-relative-to-absolute'); -const markdownAbsoluteToTransformReady = require('./markdown-absolute-to-transform-ready'); - -function markdownToTransformReady(markdown, siteUrl, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const absolute = markdownRelativeToAbsolute(markdown, siteUrl, itemPath, options); - return markdownAbsoluteToTransformReady(absolute, siteUrl, options); -} - -module.exports = markdownToTransformReady; diff --git a/packages/url-utils/lib/utils/markdown-transform.js b/packages/url-utils/lib/utils/markdown-transform.js deleted file mode 100644 index 9b6a7e861..000000000 --- a/packages/url-utils/lib/utils/markdown-transform.js +++ /dev/null @@ -1,98 +0,0 @@ -let remark; -const footnotes = require('remark-footnotes'); -const visit = require('unist-util-visit'); - -function replaceLast(find, replace, str) { - const lastIndex = str.lastIndexOf(find); - - if (lastIndex === -1) { - return str; - } - - const begin = str.substring(0, lastIndex); - const end = str.substring(lastIndex + find.length); - - return begin + replace + end; -} - -function markdownTransform(markdown = '', siteUrl, transformFunctions, itemPath, _options = {}) { - const defaultOptions = {assetsOnly: false, ignoreProtocol: true}; - const options = Object.assign({}, defaultOptions, _options); - - if (!markdown || (options.earlyExitMatchStr && !markdown.match(new RegExp(options.earlyExitMatchStr)))) { - return markdown; - } - - const replacements = []; - - if (!remark) { - remark = require('remark'); - } - - const tree = remark() - .use({settings: {commonmark: true}}) - .use(footnotes, {inlineNotes: true}) - .parse(markdown); - - visit(tree, ['link', 'image', 'html'], (node) => { - if (node.type === 'html' && node.value.match(/src|srcset|href/)) { - const oldValue = node.value; - const newValue = transformFunctions.html(node.value, siteUrl, itemPath, options); - - if (newValue !== oldValue) { - replacements.push({ - old: oldValue, - new: newValue, - start: node.position.start.offset, - end: node.position.end.offset - }); - } - } - - if (node.type === 'link' || node.type === 'image') { - const oldValue = node.url; - const newValue = transformFunctions.url(node.url, siteUrl, itemPath, options); - - if (newValue !== oldValue) { - replacements.push({ - old: oldValue, - new: newValue, - start: node.position.start.offset, - end: node.position.end.offset - }); - } - } - }); - - let result = markdown; - let offsetAdjustment = 0; - let nestedAdjustment = 0; - - replacements.forEach((replacement, i) => { - const original = markdown.slice(replacement.start, replacement.end); - // only transform last occurrence of the old string because markdown links and images - // have urls at the end and we see replacements for outermost nested nodes first - const transformed = replaceLast(replacement.old, replacement.new, original); - - let before = result.slice(0, replacement.start + offsetAdjustment); - let after = result.slice(replacement.end + offsetAdjustment, result.length); - - result = before + transformed + after; - - // adjust offset according to new lengths - const nextReplacement = replacements[i + 1]; - const adjustment = transformed.length - original.length; - - if (nextReplacement && nextReplacement.start < replacement.end) { - // next replacement is nested, do not apply any offset adjustments until we're out of nesting - nestedAdjustment = nestedAdjustment + adjustment; - } else { - offsetAdjustment = offsetAdjustment + adjustment + nestedAdjustment; - nestedAdjustment = 0; - } - }); - - return result; -} - -module.exports = markdownTransform; diff --git a/packages/url-utils/lib/utils/mobiledoc-absolute-to-relative.js b/packages/url-utils/lib/utils/mobiledoc-absolute-to-relative.js deleted file mode 100644 index dfc2c7d4e..000000000 --- a/packages/url-utils/lib/utils/mobiledoc-absolute-to-relative.js +++ /dev/null @@ -1,16 +0,0 @@ -const absoluteToRelative = require('./absolute-to-relative'); -const mobiledocTransform = require('./mobiledoc-transform'); - -function mobiledocAbsoluteToRelative(serializedMobiledoc, siteUrl, _options = {}) { - const defaultOptions = {assetsOnly: false, secure: false, cardTransformers: []}; - const overrideOptions = {siteUrl, transformType: 'absoluteToRelative'}; - const options = Object.assign({}, defaultOptions, _options, overrideOptions); - - const transformFunction = function (_url, _siteUrl, _itemPath, __options) { - return absoluteToRelative(_url, _siteUrl, __options); - }; - - return mobiledocTransform(serializedMobiledoc, siteUrl, transformFunction, '', options); -} - -module.exports = mobiledocAbsoluteToRelative; diff --git a/packages/url-utils/lib/utils/mobiledoc-absolute-to-transform-ready.js b/packages/url-utils/lib/utils/mobiledoc-absolute-to-transform-ready.js deleted file mode 100644 index a10aa53bd..000000000 --- a/packages/url-utils/lib/utils/mobiledoc-absolute-to-transform-ready.js +++ /dev/null @@ -1,16 +0,0 @@ -const absoluteToTransformReady = require('./absolute-to-transform-ready'); -const mobiledocTransform = require('./mobiledoc-transform'); - -function mobiledocAbsoluteToRelative(serializedMobiledoc, siteUrl, _options = {}) { - const defaultOptions = {assetsOnly: false, secure: false, cardTransformers: []}; - const overrideOptions = {siteUrl, transformType: 'toTransformReady'}; - const options = Object.assign({}, defaultOptions, _options, overrideOptions); - - const transformFunction = function (_url, _siteUrl, _itemPath, __options) { - return absoluteToTransformReady(_url, _siteUrl, __options); - }; - - return mobiledocTransform(serializedMobiledoc, siteUrl, transformFunction, '', options); -} - -module.exports = mobiledocAbsoluteToRelative; diff --git a/packages/url-utils/lib/utils/mobiledoc-relative-to-absolute.js b/packages/url-utils/lib/utils/mobiledoc-relative-to-absolute.js deleted file mode 100644 index 1d44be9a3..000000000 --- a/packages/url-utils/lib/utils/mobiledoc-relative-to-absolute.js +++ /dev/null @@ -1,12 +0,0 @@ -const relativeToAbsolute = require('./relative-to-absolute'); -const mobiledocTransform = require('./mobiledoc-transform'); - -function mobiledocRelativeToAbsolute(serializedMobiledoc, siteUrl, itemPath, _options = {}) { - const defaultOptions = {assetsOnly: false, secure: false, cardTransformers: []}; - const overrideOptions = {siteUrl, itemPath, transformType: 'relativeToAbsolute'}; - const options = Object.assign({}, defaultOptions, _options, overrideOptions); - - return mobiledocTransform(serializedMobiledoc, siteUrl, relativeToAbsolute, itemPath, options); -} - -module.exports = mobiledocRelativeToAbsolute; diff --git a/packages/url-utils/lib/utils/mobiledoc-relative-to-transform-ready.js b/packages/url-utils/lib/utils/mobiledoc-relative-to-transform-ready.js deleted file mode 100644 index d92c9e733..000000000 --- a/packages/url-utils/lib/utils/mobiledoc-relative-to-transform-ready.js +++ /dev/null @@ -1,12 +0,0 @@ -const relativeToTransformReady = require('./relative-to-transform-ready'); -const mobiledocTransform = require('./mobiledoc-transform'); - -function mobiledocRelativeToTransformReady(serializedMobiledoc, siteUrl, itemPath, _options = {}) { - const defaultOptions = {assetsOnly: false, secure: false, cardTransformers: []}; - const overrideOptions = {siteUrl, transformType: 'toTransformReady'}; - const options = Object.assign({}, defaultOptions, _options, overrideOptions); - - return mobiledocTransform(serializedMobiledoc, siteUrl, relativeToTransformReady, itemPath, options); -} - -module.exports = mobiledocRelativeToTransformReady; diff --git a/packages/url-utils/lib/utils/mobiledoc-to-transform-ready.js b/packages/url-utils/lib/utils/mobiledoc-to-transform-ready.js deleted file mode 100644 index 998c2e966..000000000 --- a/packages/url-utils/lib/utils/mobiledoc-to-transform-ready.js +++ /dev/null @@ -1,13 +0,0 @@ -const mobiledocRelativeToAbsolute = require('./mobiledoc-relative-to-absolute'); -const mobiledocAbsoluteToTransformReady = require('./mobiledoc-absolute-to-transform-ready'); - -function mobiledocToTransformReady(mobiledoc, siteUrl, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const absolute = mobiledocRelativeToAbsolute(mobiledoc, siteUrl, itemPath, options); - return mobiledocAbsoluteToTransformReady(absolute, siteUrl, options); -} - -module.exports = mobiledocToTransformReady; diff --git a/packages/url-utils/lib/utils/mobiledoc-transform.js b/packages/url-utils/lib/utils/mobiledoc-transform.js deleted file mode 100644 index 5253a880d..000000000 --- a/packages/url-utils/lib/utils/mobiledoc-transform.js +++ /dev/null @@ -1,55 +0,0 @@ -function mobiledocTransform(serializedMobiledoc, siteUrl, transformFunction, itemPath, _options = {}) { - const defaultOptions = {assetsOnly: false, secure: false, cardTransformers: []}; - const options = Object.assign({}, defaultOptions, _options, {siteUrl, itemPath}); - - // options.cardTransformers has an object for each card that has a name and multiple - // transformer functions. By collecting the functions we need into a named object it - // reduces the need to loop through and find the transformer for each card later on - const cardTransformers = {}; - options.cardTransformers.forEach((cardTransformer) => { - cardTransformers[cardTransformer.name] = cardTransformer[options.transformType]; - }); - delete options.cardTransformers; - - // function only accepts serialized mobiledoc so there's no chance of accidentally - // modifying pass-by-reference objects - const mobiledoc = JSON.parse(serializedMobiledoc); - - // any mobiledoc links will have an 'a' markup with an 'href' attribute - (mobiledoc.markups || []).forEach((markup) => { - if (markup[0] === 'a' && markup[1]) { - // mobiledoc markup attrs are in an array like ['key', 'value', 'key2', 'value2'] - // we only care about the href attr so loop through and find it so we can get the idx of it's value - let hrefIndex = -1; - - markup[1].forEach((attr, index) => { - if (attr === 'href') { - hrefIndex = index + 1; - } - }); - - if (hrefIndex !== -1) { - const transformedUrl = transformFunction(markup[1][hrefIndex], siteUrl, itemPath, options); - if (transformedUrl) { - markup[1][hrefIndex] = transformedUrl; - } - } - } - }); - - // any other urls will be within card payloads. We can't know what format - // cards may contain so we sub out to card-specific transform functions that - // are passed in as options from the consuming application. - (mobiledoc.cards || []).forEach((card) => { - const [name, payload] = card; - if (cardTransformers[name]) { - // transformers take a payload and return a transformed payload - const transformedPayload = cardTransformers[name](payload, options); - card[1] = transformedPayload; - } - }); - - return JSON.stringify(mobiledoc); -} - -module.exports = mobiledocTransform; diff --git a/packages/url-utils/lib/utils/plaintext-relative-to-transform-ready.js b/packages/url-utils/lib/utils/plaintext-relative-to-transform-ready.js deleted file mode 100644 index a4c207c9e..000000000 --- a/packages/url-utils/lib/utils/plaintext-relative-to-transform-ready.js +++ /dev/null @@ -1,18 +0,0 @@ -const relativeToTransformReady = require('./relative-to-transform-ready'); - -const plaintextRelativeToTransformReady = function plaintextRelativeToTransformReady(plaintext, rootUrl, itemPath, options) { - // itemPath is optional, if it's an object may be the options param instead - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - - // plaintext links look like "Link title [url]" - // those are all we care about so we can do a fast regex here - return plaintext.replace(/ \[(\/.*?)\]/g, function (fullMatch, path) { - const newPath = relativeToTransformReady(`${path}`, rootUrl, itemPath, options); - return ` [${newPath}]`; - }); -}; - -module.exports = plaintextRelativeToTransformReady; diff --git a/packages/url-utils/lib/utils/plaintext-to-transform-ready.js b/packages/url-utils/lib/utils/plaintext-to-transform-ready.js deleted file mode 100644 index 6c2b8d8fe..000000000 --- a/packages/url-utils/lib/utils/plaintext-to-transform-ready.js +++ /dev/null @@ -1,13 +0,0 @@ -const plaintextRelativeToTransformReady = require('./plaintext-relative-to-transform-ready'); -const plaintextAbsoluteToTransformReady = require('./plaintext-absolute-to-transform-ready'); - -function plaintextToTransformReady(plaintext, siteUrl, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const relativeTransformed = plaintextRelativeToTransformReady(plaintext, siteUrl, itemPath, options); - return plaintextAbsoluteToTransformReady(relativeTransformed, siteUrl, options); -} - -module.exports = plaintextToTransformReady; diff --git a/packages/url-utils/lib/utils/relative-to-transform-ready.js b/packages/url-utils/lib/utils/relative-to-transform-ready.js deleted file mode 100644 index 13b85474f..000000000 --- a/packages/url-utils/lib/utils/relative-to-transform-ready.js +++ /dev/null @@ -1,42 +0,0 @@ -const relativeToAbsolute = require('./relative-to-absolute'); - -const relativeToTransformReady = function (url, root, itemPath, _options) { - // itemPath is optional, if it's an object may be the options param instead - if (typeof itemPath === 'object' && !_options) { - _options = itemPath; - itemPath = null; - } - - const defaultOptions = { - replacementStr: '__GHOST_URL__', - staticImageUrlPrefix: 'content/images' - }; - const overrideOptions = { - secure: false - }; - const options = Object.assign({}, defaultOptions, _options, overrideOptions); - - // convert to absolute - const absoluteUrl = relativeToAbsolute(url, root, itemPath, options); - - if (absoluteUrl === url) { - return url; - } - - const rootUrl = new URL(root); - const rootPathname = rootUrl.pathname.replace(/\/$/, ''); - - // only convert to transform-ready if root url has no subdirectory or the subdirectory matches - if (!url.match(/^\//) || rootPathname === '' || url.indexOf(rootPathname) === 0 || url.indexOf(`/${options.staticImageUrlPrefix}`) === 0) { - // replace root with replacement string - const transformedUrl = absoluteUrl - .replace(root, `${options.replacementStr}/`) // always have trailing slash after magic string - .replace(/([^:])\/\//g, '$1/'); - - return transformedUrl; - } - - return url; -}; - -module.exports = relativeToTransformReady; diff --git a/packages/url-utils/lib/utils/replace-permalink.js b/packages/url-utils/lib/utils/replace-permalink.js deleted file mode 100644 index 20846ca14..000000000 --- a/packages/url-utils/lib/utils/replace-permalink.js +++ /dev/null @@ -1,46 +0,0 @@ -const moment = require('moment-timezone'); - -/** - * creates the url path for a post based on blog timezone and permalink pattern - */ -function replacePermalink(permalink, resource, timezone = 'UTC') { - const output = permalink; - const primaryTagFallback = 'all'; - const publishedAtMoment = moment.tz(resource.published_at || Date.now(), timezone); - const permalinkLookUp = { - year: function () { - return publishedAtMoment.format('YYYY'); - }, - month: function () { - return publishedAtMoment.format('MM'); - }, - day: function () { - return publishedAtMoment.format('DD'); - }, - author: function () { - return resource.primary_author.slug; - }, - primary_author: function () { - return resource.primary_author ? resource.primary_author.slug : primaryTagFallback; - }, - primary_tag: function () { - return resource.primary_tag ? resource.primary_tag.slug : primaryTagFallback; - }, - slug: function () { - return resource.slug; - }, - id: function () { - return resource.id; - } - }; - - // replace tags like :slug or :year with actual values - const permalinkKeys = Object.keys(permalinkLookUp); - return output.replace(/(:[a-z_]+)/g, function (match) { - if (permalinkKeys.includes(match.substr(1))) { - return permalinkLookUp[match.substr(1)](); - } - }); -} - -module.exports = replacePermalink; diff --git a/packages/url-utils/lib/utils/to-transform-ready.js b/packages/url-utils/lib/utils/to-transform-ready.js deleted file mode 100644 index 3f25f6789..000000000 --- a/packages/url-utils/lib/utils/to-transform-ready.js +++ /dev/null @@ -1,13 +0,0 @@ -const relativeToAbsolute = require('./relative-to-absolute'); -const absoluteToTransformReady = require('./absolute-to-transform-ready'); - -function toTransformReady(url, siteUrl, itemPath, options) { - if (typeof itemPath === 'object' && !options) { - options = itemPath; - itemPath = null; - } - const absoluteUrl = relativeToAbsolute(url, siteUrl, itemPath, options); - return absoluteToTransformReady(absoluteUrl, siteUrl, options); -} - -module.exports = toTransformReady; diff --git a/packages/url-utils/lib/utils/transform-ready-to-absolute.js b/packages/url-utils/lib/utils/transform-ready-to-absolute.js deleted file mode 100644 index a5f8d10cf..000000000 --- a/packages/url-utils/lib/utils/transform-ready-to-absolute.js +++ /dev/null @@ -1,20 +0,0 @@ -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -const transformReadyToAbsolute = function (str = '', root, _options = {}) { - const defaultOptions = { - replacementStr: '__GHOST_URL__' - }; - const options = Object.assign({}, defaultOptions, _options); - - if (!str || str.indexOf(options.replacementStr) === -1) { - return str; - } - - const replacementRegex = new RegExp(escapeRegExp(options.replacementStr), 'g'); - - return str.replace(replacementRegex, root.replace(/\/$/, '')); -}; - -module.exports = transformReadyToAbsolute; diff --git a/packages/url-utils/lib/utils/transform-ready-to-relative.js b/packages/url-utils/lib/utils/transform-ready-to-relative.js deleted file mode 100644 index 4a73f4d5a..000000000 --- a/packages/url-utils/lib/utils/transform-ready-to-relative.js +++ /dev/null @@ -1,24 +0,0 @@ -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -const transformReadyToRelative = function (str = '', root, _options = {}) { - const defaultOptions = { - replacementStr: '__GHOST_URL__' - }; - const options = Object.assign({}, defaultOptions, _options); - - if (!str || str.indexOf(options.replacementStr) === -1) { - return str; - } - - const rootURL = new URL(root); - // subdir with no trailing slash because we'll always have a trailing slash after the magic string - const subdir = rootURL.pathname.replace(/\/$/, ''); - - const replacementRegex = new RegExp(escapeRegExp(options.replacementStr), 'g'); - - return str.replace(replacementRegex, subdir); -}; - -module.exports = transformReadyToRelative; diff --git a/packages/url-utils/package.json b/packages/url-utils/package.json index 17e004dc8..fa29871d2 100644 --- a/packages/url-utils/package.json +++ b/packages/url-utils/package.json @@ -8,27 +8,34 @@ }, "author": "Ghost Foundation", "license": "MIT", - "main": "index.js", + "main": "lib/index.js", + "types": "lib/index.d.ts", "scripts": { "dev": "echo \"Implement me!\"", + "build": "tsc -p tsconfig.json", + "pretest": "yarn build", "test": "NODE_ENV=testing c8 --all --reporter text --reporter cobertura --reporter html mocha './test/**/*.test.js'", "lint": "eslint . --ext .js --cache", "posttest": "yarn lint" }, "files": [ - "lib/", - "index.js" + "lib/" ], "publishConfig": { "access": "public" }, "devDependencies": { "@tryghost/config-url-helpers": "^1.0.17", + "@types/cheerio": "0.22.35", + "@types/lodash": "4.17.15", + "@types/node": "20.16.3", + "@types/unist": "3.0.3", "c8": "10.1.3", "mocha": "11.7.4", "rewire": "9.0.1", "should": "13.2.3", - "sinon": "21.0.0" + "sinon": "21.0.0", + "typescript": "5.5.4" }, "dependencies": { "cheerio": "^0.22.0", diff --git a/packages/url-utils/src/UrlUtils.ts b/packages/url-utils/src/UrlUtils.ts new file mode 100644 index 000000000..3616e169d --- /dev/null +++ b/packages/url-utils/src/UrlUtils.ts @@ -0,0 +1,601 @@ +import _ from 'lodash'; +import utils from './utils'; +import type {AbsoluteToRelativeOptionsInput} from './utils/absolute-to-relative'; +import type {AbsoluteToTransformReadyOptionsInput} from './utils/absolute-to-transform-ready'; +import type {RelativeToAbsoluteOptionsInput} from './utils/relative-to-absolute'; +import type {RelativeToTransformReadyOptionsInput} from './utils/relative-to-transform-ready'; +import type {HtmlRelativeToAbsoluteOptionsInput} from './utils/html-relative-to-absolute'; +import type {HtmlRelativeToTransformReadyOptionsInput} from './utils/html-relative-to-transform-ready'; +import type {HtmlAbsoluteToRelativeOptionsInput} from './utils/html-absolute-to-relative'; +import type {HtmlAbsoluteToTransformReadyOptionsInput} from './utils/html-absolute-to-transform-ready'; +import type {MarkdownRelativeToAbsoluteOptionsInput} from './utils/markdown-relative-to-absolute'; +import type {MarkdownRelativeToTransformReadyOptionsInput} from './utils/markdown-relative-to-transform-ready'; +import type {MarkdownAbsoluteToRelativeOptionsInput} from './utils/markdown-absolute-to-relative'; +import type {MarkdownAbsoluteToTransformReadyOptionsInput} from './utils/markdown-absolute-to-transform-ready'; +import type {MarkdownToTransformReadyOptions} from './utils/markdown-to-transform-ready'; +import type {MobiledocRelativeToAbsoluteOptions} from './utils/mobiledoc-relative-to-absolute'; +import type {MobiledocRelativeToTransformReadyOptions} from './utils/mobiledoc-relative-to-transform-ready'; +import type {MobiledocAbsoluteToRelativeOptions} from './utils/mobiledoc-absolute-to-relative'; +import type {MobiledocAbsoluteToTransformReadyOptions} from './utils/mobiledoc-absolute-to-transform-ready'; +import type {MobiledocToTransformReadyOptions} from './utils/mobiledoc-to-transform-ready'; +import type {LexicalRelativeToAbsoluteOptions} from './utils/lexical-relative-to-absolute'; +import type {LexicalRelativeToTransformReadyOptions} from './utils/lexical-relative-to-transform-ready'; +import type {LexicalAbsoluteToRelativeOptions} from './utils/lexical-absolute-to-relative'; +import type {LexicalAbsoluteToTransformReadyOptions} from './utils/lexical-absolute-to-transform-ready'; +import type {LexicalToTransformReadyOptions} from './utils/lexical-to-transform-ready'; +import type {PlaintextToTransformReadyOptions} from './utils/plaintext-to-transform-ready'; +import type {TransformReadyReplacementOptionsInput, MobiledocCardTransformer} from './utils/types'; +import type {ToTransformReadyOptions} from './utils/to-transform-ready'; + +const KNOWN_PATHS: Record = { + home: '/', + sitemap_xsl: '/sitemap.xsl' +}; + +type UrlGetter = () => string; + +interface UrlUtilsConfig extends Record { + slugs: string[] | null; + redirectCacheMaxAge: number | null; + baseApiPath: string; + defaultApiType: 'content' | 'admin'; + staticImageUrlPrefix: string; + cardTransformers?: MobiledocCardTransformer[]; +} + +interface UrlUtilsOptions extends Partial { + getSubdir?: UrlGetter; + getSiteUrl?: UrlGetter; + getAdminUrl?: UrlGetter; +} + +interface RedirectResponse { + set(headers: Record): unknown; + redirect(status: number, url: string): unknown; + redirect(url: string): unknown; +} + +type UrlForContext = string | Record; + +interface UrlForNav { + url: string; +} + +interface UrlForData { + image?: string; + nav?: UrlForNav; + type?: 'admin' | 'content'; + trailingSlash?: boolean; + [key: string]: unknown; +} + +function assignOptions>(target: T, ...sources: Array | undefined | null>): T { + for (const source of sources) { + if (!source) { + continue; + } + + for (const [key, value] of Object.entries(source) as Array<[keyof T, T[keyof T]]>) { + if (value !== undefined) { + target[key] = value; + } + } + } + + return target; +} + +function sanitizedMerge>(defaults: T, overrides?: Partial): T { + return assignOptions({...defaults}, overrides); +} + +export default class UrlUtils { + private _config: UrlUtilsConfig; + public getSubdir: UrlGetter; + public getSiteUrl: UrlGetter; + public getAdminUrl: UrlGetter; + + constructor(options: UrlUtilsOptions = {}) { + const defaultOptions: UrlUtilsConfig = { + slugs: null, + redirectCacheMaxAge: null, + baseApiPath: '/ghost/api', + defaultApiType: 'content', + staticImageUrlPrefix: 'content/images' + }; + + const configOverrides: Partial = { + slugs: options.slugs, + redirectCacheMaxAge: options.redirectCacheMaxAge, + baseApiPath: options.baseApiPath, + defaultApiType: options.defaultApiType, + staticImageUrlPrefix: options.staticImageUrlPrefix, + cardTransformers: options.cardTransformers + }; + + this._config = sanitizedMerge(defaultOptions, configOverrides); + + this.getSubdir = options.getSubdir ?? (() => ''); + this.getSiteUrl = options.getSiteUrl ?? (() => ''); + this.getAdminUrl = options.getAdminUrl ?? (() => ''); + } + + getProtectedSlugs(): string[] | null { + const subDir = this.getSubdir(); + + if (this._config.slugs && !_.isEmpty(subDir)) { + const parts = subDir.split('/'); + const lastPart = parts[parts.length - 1]; + return this._config.slugs.concat(lastPart ? [lastPart] : []); + } + + return this._config.slugs; + } + + urlJoin(...parts: string[]): string { + return utils.urlJoin(parts, {rootUrl: this.getSiteUrl()}); + } + + createUrl(urlPath: string = '/', absolute = false, trailingSlash?: boolean): string { + let base: string; + + if (absolute) { + base = this.getSiteUrl(); + } else { + base = this.getSubdir(); + } + + let finalPath = urlPath; + if (trailingSlash) { + if (!finalPath.endsWith('/')) { + finalPath += '/'; + } + } + + return this.urlJoin(base, finalPath); + } + + urlFor(context: UrlForContext, data?: UrlForData | boolean, absolute?: boolean): string { + let urlPath = '/'; + const knownObjects = ['image', 'nav']; + let absoluteFlag = Boolean(absolute); + let dataObj: UrlForData | undefined; + + if (typeof data === 'boolean') { + absoluteFlag = data; + dataObj = undefined; + } else if (data && typeof data === 'object') { + dataObj = data; + } + + if (typeof context === 'object' && context !== null) { + const relativeUrl = (context as {relativeUrl?: unknown}).relativeUrl; + if (typeof relativeUrl === 'string' && relativeUrl) { + urlPath = relativeUrl; + } + } else if (typeof context === 'string' && knownObjects.includes(context)) { + if (context === 'image' && dataObj?.image) { + urlPath = dataObj.image; + const imagePathRe = new RegExp('^' + this.getSubdir() + '/' + this._config.staticImageUrlPrefix); + absoluteFlag = imagePathRe.test(dataObj.image) ? absoluteFlag : false; + + if (absoluteFlag) { + urlPath = urlPath.replace(new RegExp('^' + this.getSubdir()), ''); + const baseUrl = this.getSiteUrl().replace(/\/$/, ''); + urlPath = baseUrl + urlPath; + } + + return urlPath; + } + + if (context === 'nav' && dataObj?.nav) { + urlPath = dataObj.nav.url; + const baseUrl = this.getSiteUrl(); + const hostname = baseUrl.split('//')[1]; + + if (hostname) { + const prefix = urlPath.split(hostname)[0]; + const suffix = urlPath.split(hostname)[1] ?? ''; + const isSubdomain = prefix.match(/\.|mailto:/); + const hasPort = suffix.startsWith(':'); + + if (urlPath.includes(hostname) && !isSubdomain && !hasPort) { + const stripped = urlPath.split(hostname)[1]; + urlPath = this.urlJoin('/', stripped); + absoluteFlag = true; + } + } + } + } else if (context === 'home' && absoluteFlag) { + urlPath = this.getSiteUrl(); + + if (dataObj && dataObj.trailingSlash === false) { + urlPath = urlPath.replace(/\/$/, ''); + } + } else if (context === 'admin') { + const adminUrl = this.getAdminUrl() || this.getSiteUrl(); + const adminPath = '/ghost/'; + + if (absoluteFlag) { + urlPath = this.urlJoin(adminUrl, adminPath); + } else { + urlPath = adminPath; + } + } else if (context === 'api') { + const adminUrl = this.getAdminUrl() || this.getSiteUrl(); + let apiPath = `${this._config.baseApiPath}/`; + + if (dataObj?.type && ['admin', 'content'].includes(dataObj.type)) { + apiPath += dataObj.type; + } else { + apiPath += this._config.defaultApiType; + } + + apiPath += '/'; + + if (absoluteFlag) { + urlPath = this.urlJoin(adminUrl, apiPath); + } else { + urlPath = apiPath; + } + } else if (typeof context === 'string' && Object.prototype.hasOwnProperty.call(KNOWN_PATHS, context)) { + urlPath = KNOWN_PATHS[context]; + } + + const hasProtocol = urlPath.includes('://'); + const isSpecial = /^(\/\/|#|[a-zA-Z0-9-]+:)/.test(urlPath); + if (urlPath && (hasProtocol || isSpecial)) { + return urlPath; + } + + return this.createUrl(urlPath, absoluteFlag); + } + + redirect301(res: RedirectResponse, redirectUrl: string) { + if (this._config.redirectCacheMaxAge !== null) { + res.set({ + 'Cache-Control': `public, max-age=${this._config.redirectCacheMaxAge}` + }); + } + + return res.redirect(301, redirectUrl); + } + + redirectToAdmin(status: number, res: RedirectResponse, adminPath: string) { + const redirectUrl = this.urlJoin(this.urlFor('admin', true), adminPath, '/'); + + if (status === 301) { + return this.redirect301(res, redirectUrl); + } + + return typeof res.redirect === 'function' ? res.redirect(redirectUrl) : undefined; + } + + absoluteToRelative(url: string, options?: AbsoluteToRelativeOptionsInput): string { + return utils.absoluteToRelative(url, this.getSiteUrl(), options); + } + + relativeToAbsolute(url: string, options?: RelativeToAbsoluteOptionsInput): string { + return utils.relativeToAbsolute(url, this.getSiteUrl(), options); + } + + toTransformReady(url: string, itemPath?: string | ToTransformReadyOptions, options?: ToTransformReadyOptions): string { + if (typeof itemPath === 'object' && itemPath !== null && !options) { + options = itemPath; + itemPath = null; + } + return utils.toTransformReady(url, this.getSiteUrl(), itemPath ?? null, options); + } + + absoluteToTransformReady(url: string, options?: AbsoluteToTransformReadyOptionsInput): string { + return utils.absoluteToTransformReady(url, this.getSiteUrl(), options); + } + + relativeToTransformReady(url: string, options?: RelativeToTransformReadyOptionsInput): string { + const defaultOptions: RelativeToTransformReadyOptionsInput = { + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + const mergedOptions = sanitizedMerge(defaultOptions, options ?? undefined); + return utils.relativeToTransformReady(url, this.getSiteUrl(), mergedOptions); + } + + transformReadyToAbsolute(url: string, options?: TransformReadyReplacementOptionsInput): string { + return utils.transformReadyToAbsolute(url, this.getSiteUrl(), options); + } + + transformReadyToRelative(url: string, options?: TransformReadyReplacementOptionsInput): string { + return utils.transformReadyToRelative(url, this.getSiteUrl(), options); + } + + htmlToTransformReady(html: string, itemPath?: string | HtmlRelativeToTransformReadyOptionsInput, options?: HtmlRelativeToTransformReadyOptionsInput): string { + if (typeof itemPath === 'object' && itemPath !== null && !options) { + options = itemPath; + itemPath = null; + } + return utils.htmlToTransformReady(html, this.getSiteUrl(), itemPath ?? null, options); + } + + htmlRelativeToAbsolute(html: string, itemPath?: string | HtmlRelativeToAbsoluteOptionsInput, options?: HtmlRelativeToAbsoluteOptionsInput): string { + if (typeof itemPath === 'object' && itemPath !== null && !options) { + options = itemPath; + itemPath = null; + } + const defaultOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + const mergedOptions = sanitizedMerge(defaultOptions, options ?? undefined); + return utils.htmlRelativeToAbsolute(html, this.getSiteUrl(), itemPath ?? null, mergedOptions); + } + + htmlRelativeToTransformReady(html: string, itemPath?: string | HtmlRelativeToTransformReadyOptionsInput, options?: HtmlRelativeToTransformReadyOptionsInput): string { + if (typeof itemPath === 'object' && itemPath !== null && !options) { + options = itemPath; + itemPath = null; + } + const defaultOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + const mergedOptions = sanitizedMerge(defaultOptions, options ?? undefined); + return utils.htmlRelativeToTransformReady(html, this.getSiteUrl(), itemPath ?? null, mergedOptions); + } + + htmlAbsoluteToRelative(html: string, options: HtmlAbsoluteToRelativeOptionsInput = {}): string { + const defaultOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + const mergedOptions = sanitizedMerge(defaultOptions, options); + return utils.htmlAbsoluteToRelative(html, this.getSiteUrl(), mergedOptions); + } + + htmlAbsoluteToTransformReady(html: string, options: HtmlAbsoluteToTransformReadyOptionsInput = {}): string { + const defaultOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + const mergedOptions = sanitizedMerge(defaultOptions, options); + return utils.htmlAbsoluteToTransformReady(html, this.getSiteUrl(), mergedOptions); + } + + markdownToTransformReady(markdown: string, itemPath?: string | MarkdownToTransformReadyOptions, options?: MarkdownToTransformReadyOptions): string { + if (typeof itemPath === 'object' && itemPath !== null && !options) { + options = itemPath; + itemPath = null; + } + return utils.markdownToTransformReady(markdown, this.getSiteUrl(), itemPath ?? null, options); + } + + markdownRelativeToAbsolute(markdown: string, itemPath?: string | MarkdownRelativeToAbsoluteOptionsInput, options?: MarkdownRelativeToAbsoluteOptionsInput): string { + if (typeof itemPath === 'object' && itemPath !== null && !options) { + options = itemPath; + itemPath = null; + } + const defaultOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + const mergedOptions = sanitizedMerge(defaultOptions, options ?? undefined); + return utils.markdownRelativeToAbsolute(markdown, this.getSiteUrl(), itemPath ?? null, mergedOptions); + } + + markdownRelativeToTransformReady(markdown: string, itemPath?: string | MarkdownRelativeToTransformReadyOptionsInput, options?: MarkdownRelativeToTransformReadyOptionsInput): string { + if (typeof itemPath === 'object' && itemPath !== null && !options) { + options = itemPath; + itemPath = null; + } + const defaultOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + const mergedOptions = sanitizedMerge(defaultOptions, options ?? undefined); + return utils.markdownRelativeToTransformReady(markdown, this.getSiteUrl(), itemPath ?? null, mergedOptions); + } + + markdownAbsoluteToRelative(markdown: string, options: MarkdownAbsoluteToRelativeOptionsInput = {}): string { + const defaultOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + const mergedOptions = sanitizedMerge(defaultOptions, options); + return utils.markdownAbsoluteToRelative(markdown, this.getSiteUrl(), mergedOptions); + } + + markdownAbsoluteToTransformReady(markdown: string, options: MarkdownAbsoluteToTransformReadyOptionsInput = {}): string { + const defaultOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + const mergedOptions = sanitizedMerge(defaultOptions, options); + return utils.markdownAbsoluteToTransformReady(markdown, this.getSiteUrl(), mergedOptions); + } + + mobiledocToTransformReady(serializedMobiledoc: string, itemPath?: string | MobiledocToTransformReadyOptions, options?: MobiledocToTransformReadyOptions): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + const defaultOptions: MobiledocToTransformReadyOptions = {}; + if (this._config.cardTransformers) { + defaultOptions.cardTransformers = this._config.cardTransformers; + } + const mergedOptions = sanitizedMerge(defaultOptions, resolvedOptions ?? undefined); + return utils.mobiledocToTransformReady(serializedMobiledoc, this.getSiteUrl(), resolvedItemPath, mergedOptions); + } + + mobiledocRelativeToAbsolute(serializedMobiledoc: string, itemPath?: string | MobiledocRelativeToAbsoluteOptions, options?: MobiledocRelativeToAbsoluteOptions): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + const defaultOptions: MobiledocRelativeToAbsoluteOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + if (this._config.cardTransformers) { + defaultOptions.cardTransformers = this._config.cardTransformers; + } + const mergedOptions = sanitizedMerge(defaultOptions, resolvedOptions ?? undefined); + return utils.mobiledocRelativeToAbsolute(serializedMobiledoc, this.getSiteUrl(), resolvedItemPath, mergedOptions); + } + + mobiledocRelativeToTransformReady(serializedMobiledoc: string, itemPath?: string | MobiledocRelativeToTransformReadyOptions, options?: MobiledocRelativeToTransformReadyOptions): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + const defaultOptions: MobiledocRelativeToTransformReadyOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + if (this._config.cardTransformers) { + defaultOptions.cardTransformers = this._config.cardTransformers; + } + const mergedOptions = sanitizedMerge(defaultOptions, resolvedOptions ?? undefined); + return utils.mobiledocRelativeToTransformReady(serializedMobiledoc, this.getSiteUrl(), resolvedItemPath, mergedOptions); + } + + mobiledocAbsoluteToRelative(serializedMobiledoc: string, options: MobiledocAbsoluteToRelativeOptions = {}): string { + const defaultOptions: MobiledocAbsoluteToRelativeOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + if (this._config.cardTransformers) { + defaultOptions.cardTransformers = this._config.cardTransformers; + } + const mergedOptions = sanitizedMerge(defaultOptions, options); + return utils.mobiledocAbsoluteToRelative(serializedMobiledoc, this.getSiteUrl(), mergedOptions); + } + + mobiledocAbsoluteToTransformReady(serializedMobiledoc: string, options: MobiledocAbsoluteToTransformReadyOptions = {}): string { + const defaultOptions: MobiledocAbsoluteToTransformReadyOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + if (this._config.cardTransformers) { + defaultOptions.cardTransformers = this._config.cardTransformers; + } + const mergedOptions = sanitizedMerge(defaultOptions, options); + return utils.mobiledocAbsoluteToTransformReady(serializedMobiledoc, this.getSiteUrl(), mergedOptions); + } + + lexicalToTransformReady(serializedLexical: string, itemPath?: string | LexicalToTransformReadyOptions, options?: LexicalToTransformReadyOptions): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + const defaultOptions: LexicalToTransformReadyOptions = {}; + if (this._config.cardTransformers) { + defaultOptions.cardTransformers = this._config.cardTransformers; + } + const mergedOptions = sanitizedMerge(defaultOptions, resolvedOptions ?? undefined); + return utils.lexicalToTransformReady(serializedLexical, this.getSiteUrl(), resolvedItemPath, mergedOptions); + } + + lexicalRelativeToAbsolute(serializedLexical: string, itemPath?: string | LexicalRelativeToAbsoluteOptions, options?: LexicalRelativeToAbsoluteOptions): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + const defaultOptions: LexicalRelativeToAbsoluteOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + if (this._config.cardTransformers) { + defaultOptions.cardTransformers = this._config.cardTransformers; + } + const mergedOptions = sanitizedMerge(defaultOptions, resolvedOptions ?? undefined); + return utils.lexicalRelativeToAbsolute(serializedLexical, this.getSiteUrl(), resolvedItemPath, mergedOptions); + } + + lexicalRelativeToTransformReady(serializedLexical: string, itemPath?: string | LexicalRelativeToTransformReadyOptions, options?: LexicalRelativeToTransformReadyOptions): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + const defaultOptions: LexicalRelativeToTransformReadyOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + if (this._config.cardTransformers) { + defaultOptions.cardTransformers = this._config.cardTransformers; + } + const mergedOptions = sanitizedMerge(defaultOptions, resolvedOptions ?? undefined); + return utils.lexicalRelativeToTransformReady(serializedLexical, this.getSiteUrl(), resolvedItemPath, mergedOptions); + } + + lexicalAbsoluteToRelative(serializedLexical: string, options: LexicalAbsoluteToRelativeOptions = {}): string { + const defaultOptions: LexicalAbsoluteToRelativeOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + if (this._config.cardTransformers) { + defaultOptions.cardTransformers = this._config.cardTransformers; + } + const mergedOptions = sanitizedMerge(defaultOptions, options); + return utils.lexicalAbsoluteToRelative(serializedLexical, this.getSiteUrl(), mergedOptions); + } + + lexicalAbsoluteToTransformReady(serializedLexical: string, options: LexicalAbsoluteToTransformReadyOptions = {}): string { + const defaultOptions: LexicalAbsoluteToTransformReadyOptions = { + assetsOnly: false, + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + if (this._config.cardTransformers) { + defaultOptions.cardTransformers = this._config.cardTransformers; + } + const mergedOptions = sanitizedMerge(defaultOptions, options); + return utils.lexicalAbsoluteToTransformReady(serializedLexical, this.getSiteUrl(), mergedOptions); + } + + plaintextToTransformReady(plaintext: string, options: PlaintextToTransformReadyOptions = {}): string { + const defaultOptions: PlaintextToTransformReadyOptions = { + staticImageUrlPrefix: this._config.staticImageUrlPrefix + }; + const mergedOptions = sanitizedMerge(defaultOptions, options); + return utils.plaintextToTransformReady(plaintext, this.getSiteUrl(), mergedOptions); + } + + isSiteUrl(url: URL, context: string = 'home'): boolean { + const siteUrl = new URL(this.urlFor(context, true)); + if (siteUrl.host === url.host) { + return url.pathname.startsWith(siteUrl.pathname); + } + return false; + } + + get isSSL() { + return utils.isSSL; + } + + get replacePermalink() { + return utils.replacePermalink; + } + + get deduplicateDoubleSlashes() { + return utils.deduplicateDoubleSlashes; + } + + get STATIC_IMAGE_URL_PREFIX(): string { + return this._config.staticImageUrlPrefix; + } + + get _utils() { + return utils; + } +} + +export type {UrlUtilsOptions, UrlUtilsConfig}; diff --git a/packages/url-utils/src/index.ts b/packages/url-utils/src/index.ts new file mode 100644 index 000000000..15c7184fd --- /dev/null +++ b/packages/url-utils/src/index.ts @@ -0,0 +1,6 @@ +export {}; +import UrlUtils from './UrlUtils'; + +export default UrlUtils; +export {UrlUtils}; +export * from './UrlUtils'; diff --git a/packages/url-utils/lib/utils/absolute-to-relative.js b/packages/url-utils/src/utils/absolute-to-relative.ts similarity index 72% rename from packages/url-utils/lib/utils/absolute-to-relative.js rename to packages/url-utils/src/utils/absolute-to-relative.ts index 2433fe308..320af4968 100644 --- a/packages/url-utils/lib/utils/absolute-to-relative.js +++ b/packages/url-utils/src/utils/absolute-to-relative.ts @@ -1,6 +1,14 @@ -// require the whatwg compatible URL library (same behaviour in node and browser) -const {URL} = require('url'); -const stripSubdirectoryFromPath = require('./strip-subdirectory-from-path'); +import {URL} from 'url'; +import stripSubdirectoryFromPath from './strip-subdirectory-from-path'; + +export interface AbsoluteToRelativeOptions { + ignoreProtocol: boolean; + withoutSubdirectory: boolean; + assetsOnly: boolean; + staticImageUrlPrefix: string; +} + +export type AbsoluteToRelativeOptionsInput = Partial; /** * Convert an absolute URL to a root-relative path if it matches the supplied root domain. @@ -12,14 +20,17 @@ const stripSubdirectoryFromPath = require('./strip-subdirectory-from-path'); * @param {boolean} [options.withoutSubdirectory=false] Strip the root subdirectory from the returned path * @returns {string} The passed-in url or a relative path */ -const absoluteToRelative = function absoluteToRelative(url, rootUrl, _options = {}) { - const defaultOptions = { +const absoluteToRelative = function absoluteToRelative(url: string, rootUrl?: string, _options: AbsoluteToRelativeOptionsInput = {}): string { + const defaultOptions: AbsoluteToRelativeOptions = { ignoreProtocol: true, withoutSubdirectory: false, assetsOnly: false, staticImageUrlPrefix: 'content/images' }; - const options = Object.assign({}, defaultOptions, _options); + const options: AbsoluteToRelativeOptions = { + ...defaultOptions, + ..._options + }; if (options.assetsOnly) { const staticImageUrlPrefixRegex = new RegExp(options.staticImageUrlPrefix); @@ -43,6 +54,10 @@ const absoluteToRelative = function absoluteToRelative(url, rootUrl, _options = return url; } + if (!parsedRoot) { + return url; + } + const matchesHost = parsedUrl.host === parsedRoot.host; const matchesProtocol = parsedUrl.protocol === parsedRoot.protocol; const matchesPath = parsedUrl.pathname.indexOf(parsedRoot.pathname) === 0; @@ -60,4 +75,4 @@ const absoluteToRelative = function absoluteToRelative(url, rootUrl, _options = return url; }; -module.exports = absoluteToRelative; +export default absoluteToRelative; diff --git a/packages/url-utils/src/utils/absolute-to-transform-ready.ts b/packages/url-utils/src/utils/absolute-to-transform-ready.ts new file mode 100644 index 000000000..ad55416f2 --- /dev/null +++ b/packages/url-utils/src/utils/absolute-to-transform-ready.ts @@ -0,0 +1,60 @@ +import absoluteToRelative, {type AbsoluteToRelativeOptionsInput} from './absolute-to-relative'; +import type {TransformReadyReplacementOptions, TransformReadyReplacementOptionsInput} from './types'; + +export interface AbsoluteToTransformReadyOptions extends TransformReadyReplacementOptions { + withoutSubdirectory: boolean; + ignoreProtocol: boolean; + assetsOnly: boolean; + staticImageUrlPrefix: string; +} + +export type AbsoluteToTransformReadyOptionsInput = Partial; + +const absoluteToTransformReady = function (url: string, root: string, _options?: AbsoluteToTransformReadyOptionsInput): string { + const defaultOptions: AbsoluteToTransformReadyOptions = { + replacementStr: '__GHOST_URL__', + withoutSubdirectory: true, + ignoreProtocol: true, + assetsOnly: false, + staticImageUrlPrefix: 'content/images' + }; + const options: AbsoluteToTransformReadyOptions = { + ...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, { + ignoreProtocol: options.ignoreProtocol, + withoutSubdirectory: options.withoutSubdirectory, + assetsOnly: options.assetsOnly, + staticImageUrlPrefix: options.staticImageUrlPrefix + } satisfies AbsoluteToRelativeOptionsInput); + + // 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; diff --git a/packages/url-utils/src/utils/deduplicate-double-slashes.ts b/packages/url-utils/src/utils/deduplicate-double-slashes.ts new file mode 100644 index 000000000..afbcc7fdd --- /dev/null +++ b/packages/url-utils/src/utils/deduplicate-double-slashes.ts @@ -0,0 +1,5 @@ +function deduplicateDoubleSlashes(url: string): string { + return url.replace(/\/\//g, '/'); +} + +export default deduplicateDoubleSlashes; diff --git a/packages/url-utils/lib/utils/deduplicate-subdirectory.js b/packages/url-utils/src/utils/deduplicate-subdirectory.ts similarity index 90% rename from packages/url-utils/lib/utils/deduplicate-subdirectory.js rename to packages/url-utils/src/utils/deduplicate-subdirectory.ts index d86ee4a74..c6b9c6e84 100644 --- a/packages/url-utils/lib/utils/deduplicate-subdirectory.js +++ b/packages/url-utils/src/utils/deduplicate-subdirectory.ts @@ -1,4 +1,4 @@ -const {URL} = require('url'); +import {URL} from 'url'; /** * Remove duplicated directories from the start of a path or url's path @@ -7,7 +7,7 @@ const {URL} = require('url'); * @param {string} rootUrl Root URL with an optional subdirectory * @returns {string} URL or pathname with any duplicated subdirectory removed */ -const deduplicateSubdirectory = function deduplicateSubdirectory(url, rootUrl) { +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}/`; @@ -28,4 +28,4 @@ const deduplicateSubdirectory = function deduplicateSubdirectory(url, rootUrl) { return url.replace(subdirRegex, `$1${subdir}/`); }; -module.exports = deduplicateSubdirectory; +export default deduplicateSubdirectory; diff --git a/packages/url-utils/src/utils/html-absolute-to-relative.ts b/packages/url-utils/src/utils/html-absolute-to-relative.ts new file mode 100644 index 000000000..835e19e97 --- /dev/null +++ b/packages/url-utils/src/utils/html-absolute-to-relative.ts @@ -0,0 +1,30 @@ +import absoluteToRelative, {type AbsoluteToRelativeOptionsInput} from './absolute-to-relative'; +import htmlTransform from './html-transform'; +import type {HtmlTransformOptions, HtmlTransformOptionsInput} from './types'; + +export interface HtmlAbsoluteToRelativeOptions extends HtmlTransformOptions { + ignoreProtocol: boolean; +} + +export type HtmlAbsoluteToRelativeOptionsInput = HtmlTransformOptionsInput & AbsoluteToRelativeOptionsInput & Partial>; + +function htmlAbsoluteToRelative(html: string = '', siteUrl: string, _options?: HtmlAbsoluteToRelativeOptionsInput): string { + const defaultOptions: HtmlAbsoluteToRelativeOptions = {assetsOnly: false, secure: false, ignoreProtocol: true}; + const options: HtmlAbsoluteToRelativeOptions = { + ...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?: AbsoluteToRelativeOptionsInput): string { + return absoluteToRelative(_url, _siteUrl, __options); + }; + + return htmlTransform(html, siteUrl, transformFunction, '', options); +} + +export default htmlAbsoluteToRelative; diff --git a/packages/url-utils/src/utils/html-absolute-to-transform-ready.ts b/packages/url-utils/src/utils/html-absolute-to-transform-ready.ts new file mode 100644 index 000000000..895eb81d1 --- /dev/null +++ b/packages/url-utils/src/utils/html-absolute-to-transform-ready.ts @@ -0,0 +1,30 @@ +import absoluteToTransformReady, {type AbsoluteToTransformReadyOptionsInput} from './absolute-to-transform-ready'; +import htmlTransform from './html-transform'; +import type {HtmlTransformOptions, HtmlTransformOptionsInput} from './types'; + +export interface HtmlAbsoluteToTransformReadyOptions extends HtmlTransformOptions { + ignoreProtocol: boolean; +} + +export type HtmlAbsoluteToTransformReadyOptionsInput = HtmlTransformOptionsInput & AbsoluteToTransformReadyOptionsInput & Partial>; + +const htmlAbsoluteToTransformReady = function (html: string = '', siteUrl: string, _options?: HtmlAbsoluteToTransformReadyOptionsInput): string { + const defaultOptions: HtmlAbsoluteToTransformReadyOptions = {assetsOnly: false, secure: false, ignoreProtocol: true}; + const options: HtmlAbsoluteToTransformReadyOptions = { + ...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 absoluteToTransformReady doesn't take that option + const transformFunction = function (_url: string, _siteUrl: string, _itemPath: string | null, __options?: AbsoluteToTransformReadyOptionsInput): string { + return absoluteToTransformReady(_url, _siteUrl, __options); + }; + + return htmlTransform(html, siteUrl, transformFunction, '', options); +}; + +export default htmlAbsoluteToTransformReady; diff --git a/packages/url-utils/src/utils/html-relative-to-absolute.ts b/packages/url-utils/src/utils/html-relative-to-absolute.ts new file mode 100644 index 000000000..1c4ea1c1a --- /dev/null +++ b/packages/url-utils/src/utils/html-relative-to-absolute.ts @@ -0,0 +1,36 @@ +import htmlTransform from './html-transform'; +import type {HtmlTransformOptions, HtmlTransformOptionsInput} from './types'; +import relativeToAbsolute, {type RelativeToAbsoluteOptionsInput} from './relative-to-absolute'; + +export type HtmlRelativeToAbsoluteOptions = HtmlTransformOptions; +export type HtmlRelativeToAbsoluteOptionsInput = HtmlTransformOptionsInput & RelativeToAbsoluteOptionsInput; + +function htmlRelativeToAbsolute( + html: string = '', + siteUrl: string, + itemPath?: string | HtmlRelativeToAbsoluteOptionsInput | null, + _options?: HtmlRelativeToAbsoluteOptionsInput +): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: HtmlRelativeToAbsoluteOptionsInput | undefined = _options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + + const defaultOptions: HtmlRelativeToAbsoluteOptions = {assetsOnly: false, secure: false}; + const options: HtmlRelativeToAbsoluteOptions = { + ...defaultOptions, + ...resolvedOptions + }; + + // 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 ?? 'content/images'; + } + + return htmlTransform(html, siteUrl, relativeToAbsolute, resolvedItemPath, options); +} + +export default htmlRelativeToAbsolute; diff --git a/packages/url-utils/src/utils/html-relative-to-transform-ready.ts b/packages/url-utils/src/utils/html-relative-to-transform-ready.ts new file mode 100644 index 000000000..dd25f9aa4 --- /dev/null +++ b/packages/url-utils/src/utils/html-relative-to-transform-ready.ts @@ -0,0 +1,40 @@ +import htmlTransform from './html-transform'; +import type {HtmlTransformOptions} from './types'; +import relativeToTransformReady, {type RelativeToTransformReadyOptionsInput} from './relative-to-transform-ready'; + +export interface HtmlRelativeToTransformReadyOptions extends HtmlTransformOptions { + replacementStr: string; +} + +export type HtmlRelativeToTransformReadyOptionsInput = RelativeToTransformReadyOptionsInput & Partial; + +const htmlRelativeToTransformReady = function ( + html: string = '', + root: string, + itemPath?: string | HtmlRelativeToTransformReadyOptionsInput | null, + _options?: HtmlRelativeToTransformReadyOptionsInput +): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: HtmlRelativeToTransformReadyOptionsInput | undefined = _options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + + const options: HtmlRelativeToTransformReadyOptions = { + assetsOnly: false, + secure: false, + replacementStr: '__GHOST_URL__', + ...resolvedOptions + }; + + // 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 ?? 'content/images'; + } + + return htmlTransform(html, root, relativeToTransformReady, resolvedItemPath, options); +}; + +export default htmlRelativeToTransformReady; diff --git a/packages/url-utils/src/utils/html-to-transform-ready.ts b/packages/url-utils/src/utils/html-to-transform-ready.ts new file mode 100644 index 000000000..771a3e5e0 --- /dev/null +++ b/packages/url-utils/src/utils/html-to-transform-ready.ts @@ -0,0 +1,23 @@ +import htmlAbsoluteToTransformReady, {type HtmlAbsoluteToTransformReadyOptionsInput} from './html-absolute-to-transform-ready'; +import htmlRelativeToAbsolute, {type HtmlRelativeToAbsoluteOptionsInput} from './html-relative-to-absolute'; + +export type HtmlToTransformReadyOptions = HtmlRelativeToAbsoluteOptionsInput & HtmlAbsoluteToTransformReadyOptionsInput; + +function htmlToTransformReady( + html: string, + siteUrl: string, + itemPath?: string | HtmlToTransformReadyOptions | null, + options?: HtmlToTransformReadyOptions +): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: HtmlToTransformReadyOptions | undefined = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + + const absolute = htmlRelativeToAbsolute(html, siteUrl, resolvedItemPath, resolvedOptions); + return htmlAbsoluteToTransformReady(absolute, siteUrl, resolvedOptions); +} + +export default htmlToTransformReady; diff --git a/packages/url-utils/lib/utils/html-transform.js b/packages/url-utils/src/utils/html-transform.ts similarity index 70% rename from packages/url-utils/lib/utils/html-transform.js rename to packages/url-utils/src/utils/html-transform.ts index 2271174cb..de364ae07 100644 --- a/packages/url-utils/lib/utils/html-transform.js +++ b/packages/url-utils/src/utils/html-transform.ts @@ -1,17 +1,31 @@ -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +import * as cheerio from 'cheerio'; +import type {HtmlTransformOptions, HtmlTransformOptionsInput, UnknownRecord} from './types'; + +type HtmlTransformFunction = (value: string, siteUrl: string, itemPath: string | null, options: HtmlTransformOptions) => string; + +interface AttributeReplacement { + name: string; + originalValue: string; + transformedValue?: string; + skip?: boolean; +} + +type ReplacementMap = Record; + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } -function extractSrcsetUrls(srcset = '') { +function extractSrcsetUrls(srcset: string = ''): string[] { return srcset.split(',').map((part) => { return part.trim().split(/\s+/)[0]; }); } -function extractStyleUrls(style = '') { - const urls = []; +function extractStyleUrls(style: string = ''): string[] { + const urls: string[] = []; const regex = /url\(['|"]([^)]+)['|"]\)/g; - let match; + let match: RegExpExecArray | null; while ((match = regex.exec(style)) !== null) { urls.push(match[1]); @@ -20,15 +34,23 @@ function extractStyleUrls(style = '') { return urls; } -function htmlTransform(html = '', siteUrl, transformFunction, itemPath, _options) { - const defaultOptions = {assetsOnly: false, secure: false}; - const options = Object.assign({}, defaultOptions, _options || {}); +function htmlTransform( + html: string = '', + siteUrl: string, + transformFunction: HtmlTransformFunction, + itemPath: string | null = null, + _options?: HtmlTransformOptionsInput | UnknownRecord +): string { + const defaultOptions: HtmlTransformOptions = {assetsOnly: false, secure: false}; + const options: HtmlTransformOptions = { + ...defaultOptions, + ...(_options as HtmlTransformOptionsInput | undefined) + }; if (!html || (options.earlyExitMatchStr && !html.match(new RegExp(options.earlyExitMatchStr)))) { return html; } - const cheerio = require('cheerio'); const htmlContent = cheerio.load(html, {decodeEntities: false}); // replacements is keyed with the attr name + original relative value so @@ -41,9 +63,9 @@ function htmlTransform(html = '', siteUrl, transformFunction, itemPath, _options // {name: 'href', originalValue: '/test', absoluteValue: '.../test'}, // ] // } - const replacements = {}; + const replacements: ReplacementMap = {}; - function addReplacement(replacement) { + function addReplacement(replacement: AttributeReplacement): void { const key = `${replacement.name}="${replacement.originalValue}"`; if (!replacements[key]) { @@ -57,17 +79,21 @@ function htmlTransform(html = '', siteUrl, transformFunction, itemPath, _options ['href', 'src', 'srcset', 'style'].forEach((attributeName) => { htmlContent('[' + attributeName + ']').each((ix, el) => { // ignore elems and html inside of elements - if (el.name === 'stream' || htmlContent(el).closest('code').length) { + if (('name' in el && el.name === 'stream') || htmlContent(el).closest('code').length) { addReplacement({ name: attributeName, - originalValue: htmlContent(el).attr(attributeName), + originalValue: htmlContent(el).attr(attributeName) ?? '', skip: true }); return; } - el = htmlContent(el); - const originalValue = el.attr(attributeName); + const element = htmlContent(el); + const originalValue = element.attr(attributeName); + + if (!originalValue) { + return; + } if (attributeName === 'srcset' || attributeName === 'style') { let urls; @@ -110,7 +136,8 @@ function htmlTransform(html = '', siteUrl, transformFunction, itemPath, _options // Loop over all replacements and use a regex to replace urls in the original html string. // Allows indentation and formatting to be kept compared to using DOM manipulation and render - for (const [, attrs] of Object.entries(replacements)) { + const replacementEntries = Object.entries(replacements) as Array<[string, AttributeReplacement[]]>; + for (const [, attrs] of replacementEntries) { let skipCount = 0; attrs.forEach(({skip, name, originalValue, transformedValue}) => { @@ -140,4 +167,4 @@ function htmlTransform(html = '', siteUrl, transformFunction, itemPath, _options return html; } -module.exports = htmlTransform; +export default htmlTransform; diff --git a/packages/url-utils/src/utils/index.ts b/packages/url-utils/src/utils/index.ts new file mode 100644 index 000000000..593fef163 --- /dev/null +++ b/packages/url-utils/src/utils/index.ts @@ -0,0 +1,116 @@ +import absoluteToRelative from './absolute-to-relative'; +import absoluteToTransformReady from './absolute-to-transform-ready'; +import deduplicateDoubleSlashes from './deduplicate-double-slashes'; +import deduplicateSubdirectory from './deduplicate-subdirectory'; +import htmlAbsoluteToRelative from './html-absolute-to-relative'; +import htmlRelativeToAbsolute from './html-relative-to-absolute'; +import htmlAbsoluteToTransformReady from './html-absolute-to-transform-ready'; +import htmlRelativeToTransformReady from './html-relative-to-transform-ready'; +import htmlToTransformReady from './html-to-transform-ready'; +import isSSL from './is-ssl'; +import lexicalAbsoluteToRelative from './lexical-absolute-to-relative'; +import lexicalAbsoluteToTransformReady from './lexical-absolute-to-transform-ready'; +import lexicalRelativeToAbsolute from './lexical-relative-to-absolute'; +import lexicalRelativeToTransformReady from './lexical-relative-to-transform-ready'; +import lexicalToTransformReady from './lexical-to-transform-ready'; +import markdownAbsoluteToRelative from './markdown-absolute-to-relative'; +import markdownAbsoluteToTransformReady from './markdown-absolute-to-transform-ready'; +import markdownRelativeToAbsolute from './markdown-relative-to-absolute'; +import markdownRelativeToTransformReady from './markdown-relative-to-transform-ready'; +import markdownToTransformReady from './markdown-to-transform-ready'; +import mobiledocAbsoluteToRelative from './mobiledoc-absolute-to-relative'; +import mobiledocAbsoluteToTransformReady from './mobiledoc-absolute-to-transform-ready'; +import mobiledocRelativeToAbsolute from './mobiledoc-relative-to-absolute'; +import mobiledocRelativeToTransformReady from './mobiledoc-relative-to-transform-ready'; +import mobiledocToTransformReady from './mobiledoc-to-transform-ready'; +import plaintextAbsoluteToTransformReady from './plaintext-absolute-to-transform-ready'; +import plaintextRelativeToTransformReady from './plaintext-relative-to-transform-ready'; +import plaintextToTransformReady from './plaintext-to-transform-ready'; +import relativeToAbsolute from './relative-to-absolute'; +import relativeToTransformReady from './relative-to-transform-ready'; +import replacePermalink from './replace-permalink'; +import stripSubdirectoryFromPath from './strip-subdirectory-from-path'; +import toTransformReady from './to-transform-ready'; +import transformReadyToAbsolute from './transform-ready-to-absolute'; +import transformReadyToRelative from './transform-ready-to-relative'; +import urlJoin from './url-join'; + +export { + absoluteToRelative, + absoluteToTransformReady, + deduplicateDoubleSlashes, + deduplicateSubdirectory, + htmlAbsoluteToRelative, + htmlRelativeToAbsolute, + htmlAbsoluteToTransformReady, + htmlRelativeToTransformReady, + htmlToTransformReady, + isSSL, + lexicalAbsoluteToRelative, + lexicalAbsoluteToTransformReady, + lexicalRelativeToAbsolute, + lexicalRelativeToTransformReady, + lexicalToTransformReady, + markdownAbsoluteToRelative, + markdownAbsoluteToTransformReady, + markdownRelativeToAbsolute, + markdownRelativeToTransformReady, + markdownToTransformReady, + mobiledocAbsoluteToRelative, + mobiledocAbsoluteToTransformReady, + mobiledocRelativeToAbsolute, + mobiledocRelativeToTransformReady, + mobiledocToTransformReady, + plaintextAbsoluteToTransformReady, + plaintextRelativeToTransformReady, + plaintextToTransformReady, + relativeToAbsolute, + relativeToTransformReady, + replacePermalink, + stripSubdirectoryFromPath, + toTransformReady, + transformReadyToAbsolute, + transformReadyToRelative, + urlJoin +}; + +const utils = { + absoluteToRelative, + absoluteToTransformReady, + deduplicateDoubleSlashes, + deduplicateSubdirectory, + htmlAbsoluteToRelative, + htmlRelativeToAbsolute, + htmlAbsoluteToTransformReady, + htmlRelativeToTransformReady, + htmlToTransformReady, + isSSL, + lexicalAbsoluteToRelative, + lexicalAbsoluteToTransformReady, + lexicalRelativeToAbsolute, + lexicalRelativeToTransformReady, + lexicalToTransformReady, + markdownAbsoluteToRelative, + markdownAbsoluteToTransformReady, + markdownRelativeToAbsolute, + markdownRelativeToTransformReady, + markdownToTransformReady, + mobiledocAbsoluteToRelative, + mobiledocAbsoluteToTransformReady, + mobiledocRelativeToAbsolute, + mobiledocRelativeToTransformReady, + mobiledocToTransformReady, + plaintextAbsoluteToTransformReady, + plaintextRelativeToTransformReady, + plaintextToTransformReady, + relativeToAbsolute, + relativeToTransformReady, + replacePermalink, + stripSubdirectoryFromPath, + toTransformReady, + transformReadyToAbsolute, + transformReadyToRelative, + urlJoin +}; + +export default utils; diff --git a/packages/url-utils/src/utils/is-ssl.ts b/packages/url-utils/src/utils/is-ssl.ts new file mode 100644 index 000000000..5feda8251 --- /dev/null +++ b/packages/url-utils/src/utils/is-ssl.ts @@ -0,0 +1,8 @@ +import {URL} from 'url'; + +function isSSL(urlToParse: string): boolean { + const {protocol} = new URL(urlToParse); + return protocol === 'https:'; +} + +export default isSSL; diff --git a/packages/url-utils/src/utils/lexical-absolute-to-relative.ts b/packages/url-utils/src/utils/lexical-absolute-to-relative.ts new file mode 100644 index 000000000..dbd83bac5 --- /dev/null +++ b/packages/url-utils/src/utils/lexical-absolute-to-relative.ts @@ -0,0 +1,32 @@ +import absoluteToRelative, {type AbsoluteToRelativeOptionsInput} from './absolute-to-relative'; +import lexicalTransform from './lexical-transform'; +import type {LexicalTransformOptionsInput} from './types'; + +export type LexicalAbsoluteToRelativeOptions = LexicalTransformOptionsInput & AbsoluteToRelativeOptionsInput; + +function lexicalAbsoluteToRelative( + serializedLexical: string, + siteUrl: string, + _options: LexicalAbsoluteToRelativeOptions = {} +): string { + const overrideOptions: LexicalAbsoluteToRelativeOptions = { + siteUrl, + transformType: 'absoluteToRelative' + }; + const options: LexicalAbsoluteToRelativeOptions = { + assetsOnly: false, + secure: false, + nodes: [], + transformMap: {}, + ..._options, + ...overrideOptions + }; + + const transformFunction = function (_url: string, _siteUrl: string, _itemPath: string | null, __options?: AbsoluteToRelativeOptionsInput): string { + return absoluteToRelative(_url, _siteUrl, __options); + }; + + return lexicalTransform(serializedLexical, siteUrl, transformFunction, null, options); +} + +export default lexicalAbsoluteToRelative; diff --git a/packages/url-utils/src/utils/lexical-absolute-to-transform-ready.ts b/packages/url-utils/src/utils/lexical-absolute-to-transform-ready.ts new file mode 100644 index 000000000..3fcb76411 --- /dev/null +++ b/packages/url-utils/src/utils/lexical-absolute-to-transform-ready.ts @@ -0,0 +1,32 @@ +import absoluteToTransformReady, {type AbsoluteToTransformReadyOptionsInput} from './absolute-to-transform-ready'; +import lexicalTransform from './lexical-transform'; +import type {LexicalTransformOptionsInput} from './types'; + +export type LexicalAbsoluteToTransformReadyOptions = LexicalTransformOptionsInput & AbsoluteToTransformReadyOptionsInput; + +function lexicalAbsoluteToTransformReady( + serializedLexical: string, + siteUrl: string, + _options: LexicalAbsoluteToTransformReadyOptions = {} +): string { + const overrideOptions: LexicalAbsoluteToTransformReadyOptions = { + siteUrl, + transformType: 'toTransformReady' + }; + const options: LexicalAbsoluteToTransformReadyOptions = { + assetsOnly: false, + secure: false, + nodes: [], + transformMap: {}, + ..._options, + ...overrideOptions + }; + + const transformFunction = function (_url: string, _siteUrl: string, _itemPath: string | null, __options?: AbsoluteToTransformReadyOptionsInput): string { + return absoluteToTransformReady(_url, _siteUrl, __options); + }; + + return lexicalTransform(serializedLexical, siteUrl, transformFunction, null, options); +} + +export default lexicalAbsoluteToTransformReady; diff --git a/packages/url-utils/src/utils/lexical-relative-to-absolute.ts b/packages/url-utils/src/utils/lexical-relative-to-absolute.ts new file mode 100644 index 000000000..933f7799f --- /dev/null +++ b/packages/url-utils/src/utils/lexical-relative-to-absolute.ts @@ -0,0 +1,30 @@ +import lexicalTransform from './lexical-transform'; +import relativeToAbsolute, {type RelativeToAbsoluteOptionsInput} from './relative-to-absolute'; +import type {LexicalTransformOptionsInput} from './types'; + +export type LexicalRelativeToAbsoluteOptions = LexicalTransformOptionsInput & RelativeToAbsoluteOptionsInput; + +function lexicalRelativeToAbsolute( + serializedLexical: string, + siteUrl: string, + itemPath: string | null, + _options: LexicalRelativeToAbsoluteOptions = {} +): string { + const overrideOptions: LexicalRelativeToAbsoluteOptions = { + siteUrl, + itemPath, + transformType: 'relativeToAbsolute' + }; + const options: LexicalRelativeToAbsoluteOptions = { + assetsOnly: false, + secure: false, + nodes: [], + transformMap: {}, + ..._options, + ...overrideOptions + }; + + return lexicalTransform(serializedLexical, siteUrl, relativeToAbsolute, itemPath, options); +} + +export default lexicalRelativeToAbsolute; diff --git a/packages/url-utils/src/utils/lexical-relative-to-transform-ready.ts b/packages/url-utils/src/utils/lexical-relative-to-transform-ready.ts new file mode 100644 index 000000000..a27e28211 --- /dev/null +++ b/packages/url-utils/src/utils/lexical-relative-to-transform-ready.ts @@ -0,0 +1,29 @@ +import lexicalTransform from './lexical-transform'; +import relativeToTransformReady, {type RelativeToTransformReadyOptionsInput} from './relative-to-transform-ready'; +import type {LexicalTransformOptionsInput} from './types'; + +export type LexicalRelativeToTransformReadyOptions = LexicalTransformOptionsInput & RelativeToTransformReadyOptionsInput; + +function lexicalRelativeToTransformReady( + serializedLexical: string, + siteUrl: string, + itemPath: string | null, + _options: LexicalRelativeToTransformReadyOptions = {} +): string { + const overrideOptions: LexicalRelativeToTransformReadyOptions = { + siteUrl, + transformType: 'toTransformReady' + }; + const options: LexicalRelativeToTransformReadyOptions = { + assetsOnly: false, + secure: false, + nodes: [], + transformMap: {}, + ..._options, + ...overrideOptions + }; + + return lexicalTransform(serializedLexical, siteUrl, relativeToTransformReady, itemPath, options); +} + +export default lexicalRelativeToTransformReady; diff --git a/packages/url-utils/src/utils/lexical-to-transform-ready.ts b/packages/url-utils/src/utils/lexical-to-transform-ready.ts new file mode 100644 index 000000000..3d6790222 --- /dev/null +++ b/packages/url-utils/src/utils/lexical-to-transform-ready.ts @@ -0,0 +1,23 @@ +import lexicalAbsoluteToTransformReady, {type LexicalAbsoluteToTransformReadyOptions} from './lexical-absolute-to-transform-ready'; +import lexicalRelativeToAbsolute, {type LexicalRelativeToAbsoluteOptions} from './lexical-relative-to-absolute'; + +export type LexicalToTransformReadyOptions = LexicalRelativeToAbsoluteOptions & LexicalAbsoluteToTransformReadyOptions; + +function lexicalToTransformReady( + lexical: string, + siteUrl: string, + itemPath?: string | LexicalToTransformReadyOptions | null, + options?: LexicalToTransformReadyOptions +): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: LexicalToTransformReadyOptions | undefined = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + + const absolute = lexicalRelativeToAbsolute(lexical, siteUrl, resolvedItemPath, resolvedOptions); + return lexicalAbsoluteToTransformReady(absolute, siteUrl, resolvedOptions); +} + +export default lexicalToTransformReady; diff --git a/packages/url-utils/src/utils/lexical-transform.ts b/packages/url-utils/src/utils/lexical-transform.ts new file mode 100644 index 000000000..b8381cdf7 --- /dev/null +++ b/packages/url-utils/src/utils/lexical-transform.ts @@ -0,0 +1,138 @@ +import _ from 'lodash'; +import type { + LexicalNodeConfig, + LexicalTransformOptions, + LexicalTransformOptionsInput, + LexicalTransformRegistry, + LexicalTransformType, + LexicalUrlTransformMap +} from './types'; + +interface LexicalNode { + type?: string; + url?: string; + children?: LexicalNode[]; + [key: string]: unknown; +} + +interface LexicalDocument { + root?: { + children?: LexicalNode[]; + }; +} + +type LexicalTransformFunction = (value: string, siteUrl: string, itemPath: string | null, options: LexicalTransformOptions) => string; + +// options.transformMap = { +// relativeToAbsolute: { +// url: (url, siteUrl, itemPath, options) => 'transformedUrl', +// html: (html, siteUrl, itemPath, options) => 'transformedHtml', +// } +// } +// options.transformType = 'relativeToAbsolute' + +function lexicalTransform( + serializedLexical: string, + siteUrl: string, + transformFunction: LexicalTransformFunction, + itemPath: string | null, + _options: LexicalTransformOptionsInput = {} +): string { + const defaultOptions: LexicalTransformOptions = { + assetsOnly: false, + secure: false, + nodes: [], + transformMap: {}, + transformType: 'relativeToAbsolute', + siteUrl, + itemPath + }; + const options: LexicalTransformOptions = { + ...defaultOptions, + ..._options, + siteUrl, + itemPath + }; + + if (!serializedLexical) { + return serializedLexical; + } + + // function only accepts serialized lexical so there's no chance of accidentally + // modifying pass-by-reference objects + const lexical = JSON.parse(serializedLexical) as LexicalDocument; + + if (!lexical?.root?.children) { + return serializedLexical; + } + + // create a map of node types to urlTransformMap objects + // e.g. {'image': {src: 'url', caption: 'html'} + const nodeMap = new Map(); + options.nodes.forEach((node: LexicalNodeConfig) => { + if (node.urlTransformMap) { + nodeMap.set(node.getType(), node.urlTransformMap); + } + }); + + const getTransformRegistry = (transformType: LexicalTransformType): Record string> | undefined => { + return options.transformMap?.[transformType]; + }; + + const transformProperty = function (obj: LexicalNode, propertyPath: string, transform: string | LexicalUrlTransformMap) { + const propertyValue = _.get(obj, propertyPath); + + if (Array.isArray(propertyValue)) { + propertyValue.forEach((item) => { + // arrays of objects need to be defined as a nested object in the urlTransformMap + // so the `transform` value is that nested object + if (typeof transform === 'object') { + Object.entries(transform).forEach(([itemPropertyPath, itemTransform]) => { + transformProperty(item as LexicalNode, itemPropertyPath, itemTransform); + }); + } + }); + + return; + } + + if (propertyValue && typeof transform === 'string') { + const registry = getTransformRegistry(options.transformType); + const transformer = registry?.[transform]; + if (transformer && typeof propertyValue === 'string') { + _.set(obj, propertyPath, transformer(propertyValue)); + } + } + }; + + // recursively walk the Lexical node tree transforming any card data properties and links + const transformChildren = function (children: LexicalNode[]) { + for (const child of children) { + const isCard = child.type ? nodeMap.has(child.type) : false; + const isLink = typeof child.url === 'string'; + + if (isCard) { + const map = child.type ? nodeMap.get(child.type) : undefined; + if (map) { + Object.entries(map).forEach(([propertyPath, transform]) => { + transformProperty(child, propertyPath, transform); + }); + } + } else if (isLink) { + child.url = transformFunction(child.url as string, siteUrl, itemPath, options); + } + + if (child.children) { + transformChildren(child.children); + } + } + }; + + if (lexical.root?.children) { + transformChildren(lexical.root.children); + } + + return JSON.stringify(lexical); +} + +export default lexicalTransform; diff --git a/packages/url-utils/src/utils/markdown-absolute-to-relative.ts b/packages/url-utils/src/utils/markdown-absolute-to-relative.ts new file mode 100644 index 000000000..74fffebc9 --- /dev/null +++ b/packages/url-utils/src/utils/markdown-absolute-to-relative.ts @@ -0,0 +1,34 @@ +import absoluteToRelative, {type AbsoluteToRelativeOptionsInput} from './absolute-to-relative'; +import htmlAbsoluteToRelative from './html-absolute-to-relative'; +import markdownTransformDefault from './markdown-transform'; +import type {MarkdownTransformOptions, MarkdownTransformOptionsInput} from './types'; + +const markdownTransform = markdownTransformDefault; + +export type MarkdownAbsoluteToRelativeOptions = MarkdownTransformOptions; +export type MarkdownAbsoluteToRelativeOptionsInput = MarkdownTransformOptionsInput & AbsoluteToRelativeOptionsInput; + +function markdownAbsoluteToRelative(markdown: string = '', siteUrl: string, _options: MarkdownAbsoluteToRelativeOptionsInput = {}): string { + const options: MarkdownAbsoluteToRelativeOptions = { + assetsOnly: false, + ignoreProtocol: true, + ..._options + }; + + options.earlyExitMatchStr = options.ignoreProtocol ? siteUrl.replace(/http:|https:/, '') : siteUrl; + options.earlyExitMatchStr = options.earlyExitMatchStr.replace(/\/$/, ''); + + // need to ignore itemPath because absoluteToRelative functions don't take that option + const transformFunctions = { + html(_url: string, _siteUrl: string, _itemPath: string | null, __options?: AbsoluteToRelativeOptionsInput) { + return htmlAbsoluteToRelative(_url, _siteUrl, __options); + }, + url(_url: string, _siteUrl: string, _itemPath: string | null, __options?: AbsoluteToRelativeOptionsInput) { + return absoluteToRelative(_url, _siteUrl, __options); + } + }; + + return markdownTransform(markdown, siteUrl, transformFunctions, '', options); +} + +export default markdownAbsoluteToRelative; diff --git a/packages/url-utils/src/utils/markdown-absolute-to-transform-ready.ts b/packages/url-utils/src/utils/markdown-absolute-to-transform-ready.ts new file mode 100644 index 000000000..749450f7f --- /dev/null +++ b/packages/url-utils/src/utils/markdown-absolute-to-transform-ready.ts @@ -0,0 +1,34 @@ +import absoluteToTransformReady, {type AbsoluteToTransformReadyOptionsInput} from './absolute-to-transform-ready'; +import htmlAbsoluteToTransformReady from './html-absolute-to-transform-ready'; +import markdownTransformDefault from './markdown-transform'; +import type {MarkdownTransformOptions, MarkdownTransformOptionsInput} from './types'; + +const markdownTransform = markdownTransformDefault; + +export type MarkdownAbsoluteToTransformReadyOptions = MarkdownTransformOptions; +export type MarkdownAbsoluteToTransformReadyOptionsInput = MarkdownTransformOptionsInput & AbsoluteToTransformReadyOptionsInput; + +function markdownAbsoluteToTransformReady(markdown: string = '', siteUrl: string, _options: MarkdownAbsoluteToTransformReadyOptionsInput = {}): string { + const options: MarkdownAbsoluteToTransformReadyOptions = { + assetsOnly: false, + ignoreProtocol: true, + ..._options + }; + + options.earlyExitMatchStr = options.ignoreProtocol ? siteUrl.replace(/http:|https:/, '') : siteUrl; + options.earlyExitMatchStr = options.earlyExitMatchStr.replace(/\/$/, ''); + + // need to ignore itemPath because absoluteToTransformReady functions don't take that option + const transformFunctions = { + html(_url: string, _siteUrl: string, _itemPath: string | null, __options?: AbsoluteToTransformReadyOptionsInput) { + return htmlAbsoluteToTransformReady(_url, _siteUrl, __options); + }, + url(_url: string, _siteUrl: string, _itemPath: string | null, __options?: AbsoluteToTransformReadyOptionsInput) { + return absoluteToTransformReady(_url, _siteUrl, __options); + } + }; + + return markdownTransform(markdown, siteUrl, transformFunctions, '', options); +} + +export default markdownAbsoluteToTransformReady; diff --git a/packages/url-utils/src/utils/markdown-relative-to-absolute.ts b/packages/url-utils/src/utils/markdown-relative-to-absolute.ts new file mode 100644 index 000000000..04164fb19 --- /dev/null +++ b/packages/url-utils/src/utils/markdown-relative-to-absolute.ts @@ -0,0 +1,44 @@ +import htmlRelativeToAbsolute from './html-relative-to-absolute'; +import markdownTransformDefault from './markdown-transform'; +import relativeToAbsolute from './relative-to-absolute'; +import type {MarkdownTransformOptions, MarkdownTransformOptionsInput} from './types'; + +const markdownTransform = markdownTransformDefault; + +export type MarkdownRelativeToAbsoluteOptions = MarkdownTransformOptions; +export type MarkdownRelativeToAbsoluteOptionsInput = MarkdownTransformOptionsInput; + +function markdownRelativeToAbsolute( + markdown: string = '', + siteUrl: string, + itemPath?: string | MarkdownRelativeToAbsoluteOptionsInput | null, + _options: MarkdownRelativeToAbsoluteOptionsInput = {} +): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: MarkdownRelativeToAbsoluteOptionsInput = _options; + + if (typeof itemPath === 'object' && itemPath !== null) { + resolvedOptions = itemPath; + resolvedItemPath = null; + } + + const options: MarkdownRelativeToAbsoluteOptions = { + assetsOnly: false, + ignoreProtocol: true, + ...resolvedOptions + }; + + options.earlyExitMatchStr = '\\]\\([^\\s\\)]|href=|src=|srcset='; + if (options.assetsOnly) { + options.earlyExitMatchStr = options.staticImageUrlPrefix ?? 'content/images'; + } + + const transformFunctions = { + html: htmlRelativeToAbsolute, + url: relativeToAbsolute + }; + + return markdownTransform(markdown, siteUrl, transformFunctions, resolvedItemPath, options); +} + +export default markdownRelativeToAbsolute; diff --git a/packages/url-utils/src/utils/markdown-relative-to-transform-ready.ts b/packages/url-utils/src/utils/markdown-relative-to-transform-ready.ts new file mode 100644 index 000000000..a1cff2cb9 --- /dev/null +++ b/packages/url-utils/src/utils/markdown-relative-to-transform-ready.ts @@ -0,0 +1,44 @@ +import htmlRelativeToTransformReady from './html-relative-to-transform-ready'; +import markdownTransformDefault from './markdown-transform'; +import relativeToTransformReady from './relative-to-transform-ready'; +import type {MarkdownTransformOptions, MarkdownTransformOptionsInput} from './types'; + +const markdownTransform = markdownTransformDefault; + +export type MarkdownRelativeToTransformReadyOptions = MarkdownTransformOptions; +export type MarkdownRelativeToTransformReadyOptionsInput = MarkdownTransformOptionsInput; + +function markdownRelativeToTransformReady( + markdown: string = '', + siteUrl: string, + itemPath?: string | MarkdownRelativeToTransformReadyOptionsInput | null, + _options: MarkdownRelativeToTransformReadyOptionsInput = {} +): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: MarkdownRelativeToTransformReadyOptionsInput = _options; + + if (typeof itemPath === 'object' && itemPath !== null) { + resolvedOptions = itemPath; + resolvedItemPath = null; + } + + const options: MarkdownRelativeToTransformReadyOptions = { + assetsOnly: false, + ignoreProtocol: true, + ...resolvedOptions + }; + + options.earlyExitMatchStr = '\\]\\([^\\s\\)]|href=|src=|srcset='; + if (options.assetsOnly) { + options.earlyExitMatchStr = options.staticImageUrlPrefix ?? 'content/images'; + } + + const transformFunctions = { + html: htmlRelativeToTransformReady, + url: relativeToTransformReady + }; + + return markdownTransform(markdown, siteUrl, transformFunctions, resolvedItemPath, options); +} + +export default markdownRelativeToTransformReady; diff --git a/packages/url-utils/src/utils/markdown-to-transform-ready.ts b/packages/url-utils/src/utils/markdown-to-transform-ready.ts new file mode 100644 index 000000000..614f00c57 --- /dev/null +++ b/packages/url-utils/src/utils/markdown-to-transform-ready.ts @@ -0,0 +1,23 @@ +import markdownAbsoluteToTransformReady, {type MarkdownAbsoluteToTransformReadyOptionsInput} from './markdown-absolute-to-transform-ready'; +import markdownRelativeToAbsolute, {type MarkdownRelativeToAbsoluteOptionsInput} from './markdown-relative-to-absolute'; + +export type MarkdownToTransformReadyOptions = MarkdownRelativeToAbsoluteOptionsInput & MarkdownAbsoluteToTransformReadyOptionsInput; + +function markdownToTransformReady( + markdown: string, + siteUrl: string, + itemPath?: string | MarkdownToTransformReadyOptions | null, + options?: MarkdownToTransformReadyOptions +): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: MarkdownToTransformReadyOptions | undefined = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + + const absolute = markdownRelativeToAbsolute(markdown, siteUrl, resolvedItemPath, resolvedOptions); + return markdownAbsoluteToTransformReady(absolute, siteUrl, resolvedOptions); +} + +export default markdownToTransformReady; diff --git a/packages/url-utils/src/utils/markdown-transform.ts b/packages/url-utils/src/utils/markdown-transform.ts new file mode 100644 index 000000000..49f5ee1fd --- /dev/null +++ b/packages/url-utils/src/utils/markdown-transform.ts @@ -0,0 +1,133 @@ +import remarkDefault from 'remark'; +import footnotes from 'remark-footnotes'; +import visit from 'unist-util-visit'; +import type {Node} from 'unist'; +import type {MarkdownTransformOptions, MarkdownTransformOptionsInput} from './types'; + +const remark = remarkDefault; + +interface MarkdownTransformFunctions { + html: (value: string, siteUrl: string, itemPath: string | null, options: MarkdownTransformOptions) => string; + url: (value: string, siteUrl: string, itemPath: string | null, options: MarkdownTransformOptions) => string; +} + +interface MarkdownReplacement { + old: string; + new: string; + start: number; + end: number; +} + +type RemarkNode = { + type?: string; + value?: string; + url?: string; + position?: { + start?: {offset?: number; line?: number; column?: number}; + end?: {offset?: number; line?: number; column?: number}; + }; +}; + +function replaceLast(find: string, replace: string, str: string): string { + const lastIndex = str.lastIndexOf(find); + + if (lastIndex === -1) { + return str; + } + + const begin = str.substring(0, lastIndex); + const end = str.substring(lastIndex + find.length); + + return begin + replace + end; +} + +function markdownTransform( + markdown: string = '', + siteUrl: string, + transformFunctions: MarkdownTransformFunctions, + itemPath: string | null = null, + _options: MarkdownTransformOptionsInput = {} +): string { + const defaultOptions: MarkdownTransformOptions = {assetsOnly: false, ignoreProtocol: true}; + const options: MarkdownTransformOptions = { + ...defaultOptions, + ..._options + }; + + if (!markdown || (options.earlyExitMatchStr && !markdown.match(new RegExp(options.earlyExitMatchStr)))) { + return markdown; + } + + const replacements: MarkdownReplacement[] = []; + + const tree = remark() + .use({settings: {commonmark: true}}) + .use(footnotes, {inlineNotes: true}) + .parse(markdown); + + visit(tree, ['link', 'image', 'html'], (node) => { + const remarkNode = node as RemarkNode; + const startOffset = remarkNode.position?.start?.offset; + const endOffset = remarkNode.position?.end?.offset; + + if (remarkNode.type === 'html' && typeof remarkNode.value === 'string' && /src|srcset|href/.test(remarkNode.value) && typeof startOffset === 'number' && typeof endOffset === 'number') { + const oldValue = remarkNode.value; + const newValue = transformFunctions.html(oldValue, siteUrl, itemPath, options); + + if (newValue !== oldValue) { + replacements.push({ + old: oldValue, + new: newValue, + start: startOffset, + end: endOffset + }); + } + } + + if ((remarkNode.type === 'link' || remarkNode.type === 'image') && typeof remarkNode.url === 'string' && typeof startOffset === 'number' && typeof endOffset === 'number') { + const oldValue = remarkNode.url; + const newValue = transformFunctions.url(oldValue, siteUrl, itemPath, options); + + if (newValue !== oldValue) { + replacements.push({ + old: oldValue, + new: newValue, + start: startOffset, + end: endOffset + }); + } + } + }); + + let result = markdown; + let offsetAdjustment = 0; + let nestedAdjustment = 0; + + replacements.forEach((replacement, i) => { + const original = markdown.slice(replacement.start, replacement.end); + // only transform last occurrence of the old string because markdown links and images + // have urls at the end and we see replacements for outermost nested nodes first + const transformed = replaceLast(replacement.old, replacement.new, original); + + let before = result.slice(0, replacement.start + offsetAdjustment); + let after = result.slice(replacement.end + offsetAdjustment, result.length); + + result = before + transformed + after; + + // adjust offset according to new lengths + const nextReplacement = replacements[i + 1]; + const adjustment = transformed.length - original.length; + + if (nextReplacement && nextReplacement.start < replacement.end) { + // next replacement is nested, do not apply any offset adjustments until we're out of nesting + nestedAdjustment = nestedAdjustment + adjustment; + } else { + offsetAdjustment = offsetAdjustment + adjustment + nestedAdjustment; + nestedAdjustment = 0; + } + }); + + return result; +} + +export default markdownTransform; diff --git a/packages/url-utils/src/utils/mobiledoc-absolute-to-relative.ts b/packages/url-utils/src/utils/mobiledoc-absolute-to-relative.ts new file mode 100644 index 000000000..a9bb7f2a9 --- /dev/null +++ b/packages/url-utils/src/utils/mobiledoc-absolute-to-relative.ts @@ -0,0 +1,31 @@ +import absoluteToRelative, {type AbsoluteToRelativeOptionsInput} from './absolute-to-relative'; +import mobiledocTransform from './mobiledoc-transform'; +import type {MobiledocTransformOptionsInput} from './types'; + +export type MobiledocAbsoluteToRelativeOptions = MobiledocTransformOptionsInput & AbsoluteToRelativeOptionsInput; + +function mobiledocAbsoluteToRelative( + serializedMobiledoc: string, + siteUrl: string, + _options: MobiledocAbsoluteToRelativeOptions = {} +): string { + const overrideOptions: MobiledocAbsoluteToRelativeOptions = { + siteUrl, + transformType: 'absoluteToRelative' + }; + const options: MobiledocAbsoluteToRelativeOptions = { + assetsOnly: false, + secure: false, + cardTransformers: [], + ..._options, + ...overrideOptions + }; + + const transformFunction = function (_url: string, _siteUrl: string, _itemPath: string | null, __options?: AbsoluteToRelativeOptionsInput): string { + return absoluteToRelative(_url, _siteUrl, __options); + }; + + return mobiledocTransform(serializedMobiledoc, siteUrl, transformFunction, '', options); +} + +export default mobiledocAbsoluteToRelative; diff --git a/packages/url-utils/src/utils/mobiledoc-absolute-to-transform-ready.ts b/packages/url-utils/src/utils/mobiledoc-absolute-to-transform-ready.ts new file mode 100644 index 000000000..afb6290a2 --- /dev/null +++ b/packages/url-utils/src/utils/mobiledoc-absolute-to-transform-ready.ts @@ -0,0 +1,31 @@ +import absoluteToTransformReady, {type AbsoluteToTransformReadyOptionsInput} from './absolute-to-transform-ready'; +import mobiledocTransform from './mobiledoc-transform'; +import type {MobiledocTransformOptionsInput} from './types'; + +export type MobiledocAbsoluteToTransformReadyOptions = MobiledocTransformOptionsInput & AbsoluteToTransformReadyOptionsInput; + +function mobiledocAbsoluteToTransformReady( + serializedMobiledoc: string, + siteUrl: string, + _options: MobiledocAbsoluteToTransformReadyOptions = {} +): string { + const overrideOptions: MobiledocAbsoluteToTransformReadyOptions = { + siteUrl, + transformType: 'toTransformReady' + }; + const options: MobiledocAbsoluteToTransformReadyOptions = { + assetsOnly: false, + secure: false, + cardTransformers: [], + ..._options, + ...overrideOptions + }; + + const transformFunction = function (_url: string, _siteUrl: string, _itemPath: string | null, __options?: AbsoluteToTransformReadyOptionsInput): string { + return absoluteToTransformReady(_url, _siteUrl, __options); + }; + + return mobiledocTransform(serializedMobiledoc, siteUrl, transformFunction, '', options); +} + +export default mobiledocAbsoluteToTransformReady; diff --git a/packages/url-utils/src/utils/mobiledoc-relative-to-absolute.ts b/packages/url-utils/src/utils/mobiledoc-relative-to-absolute.ts new file mode 100644 index 000000000..bbc10d237 --- /dev/null +++ b/packages/url-utils/src/utils/mobiledoc-relative-to-absolute.ts @@ -0,0 +1,29 @@ +import relativeToAbsolute, {type RelativeToAbsoluteOptionsInput} from './relative-to-absolute'; +import mobiledocTransform from './mobiledoc-transform'; +import type {MobiledocTransformOptionsInput} from './types'; + +export type MobiledocRelativeToAbsoluteOptions = MobiledocTransformOptionsInput & RelativeToAbsoluteOptionsInput; + +function mobiledocRelativeToAbsolute( + serializedMobiledoc: string, + siteUrl: string, + itemPath: string | null, + _options: MobiledocRelativeToAbsoluteOptions = {} +): string { + const overrideOptions: MobiledocRelativeToAbsoluteOptions = { + siteUrl, + itemPath, + transformType: 'relativeToAbsolute' + }; + const options: MobiledocRelativeToAbsoluteOptions = { + assetsOnly: false, + secure: false, + cardTransformers: [], + ..._options, + ...overrideOptions + }; + + return mobiledocTransform(serializedMobiledoc, siteUrl, relativeToAbsolute, itemPath, options); +} + +export default mobiledocRelativeToAbsolute; diff --git a/packages/url-utils/src/utils/mobiledoc-relative-to-transform-ready.ts b/packages/url-utils/src/utils/mobiledoc-relative-to-transform-ready.ts new file mode 100644 index 000000000..a0dc28f8a --- /dev/null +++ b/packages/url-utils/src/utils/mobiledoc-relative-to-transform-ready.ts @@ -0,0 +1,28 @@ +import mobiledocTransform from './mobiledoc-transform'; +import relativeToTransformReady, {type RelativeToTransformReadyOptionsInput} from './relative-to-transform-ready'; +import type {MobiledocTransformOptionsInput} from './types'; + +export type MobiledocRelativeToTransformReadyOptions = MobiledocTransformOptionsInput & RelativeToTransformReadyOptionsInput; + +function mobiledocRelativeToTransformReady( + serializedMobiledoc: string, + siteUrl: string, + itemPath: string | null, + _options: MobiledocRelativeToTransformReadyOptions = {} +): string { + const overrideOptions: MobiledocRelativeToTransformReadyOptions = { + siteUrl, + transformType: 'toTransformReady' + }; + const options: MobiledocRelativeToTransformReadyOptions = { + assetsOnly: false, + secure: false, + cardTransformers: [], + ..._options, + ...overrideOptions + }; + + return mobiledocTransform(serializedMobiledoc, siteUrl, relativeToTransformReady, itemPath, options); +} + +export default mobiledocRelativeToTransformReady; diff --git a/packages/url-utils/src/utils/mobiledoc-to-transform-ready.ts b/packages/url-utils/src/utils/mobiledoc-to-transform-ready.ts new file mode 100644 index 000000000..4871c2eb8 --- /dev/null +++ b/packages/url-utils/src/utils/mobiledoc-to-transform-ready.ts @@ -0,0 +1,23 @@ +import mobiledocAbsoluteToTransformReady, {type MobiledocAbsoluteToTransformReadyOptions} from './mobiledoc-absolute-to-transform-ready'; +import mobiledocRelativeToAbsolute, {type MobiledocRelativeToAbsoluteOptions} from './mobiledoc-relative-to-absolute'; + +export type MobiledocToTransformReadyOptions = MobiledocRelativeToAbsoluteOptions & MobiledocAbsoluteToTransformReadyOptions; + +function mobiledocToTransformReady( + mobiledoc: string, + siteUrl: string, + itemPath?: string | MobiledocToTransformReadyOptions | null, + options?: MobiledocToTransformReadyOptions +): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: MobiledocToTransformReadyOptions | undefined = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + + const absolute = mobiledocRelativeToAbsolute(mobiledoc, siteUrl, resolvedItemPath, resolvedOptions); + return mobiledocAbsoluteToTransformReady(absolute, siteUrl, resolvedOptions); +} + +export default mobiledocToTransformReady; diff --git a/packages/url-utils/src/utils/mobiledoc-transform.ts b/packages/url-utils/src/utils/mobiledoc-transform.ts new file mode 100644 index 000000000..bdab223a7 --- /dev/null +++ b/packages/url-utils/src/utils/mobiledoc-transform.ts @@ -0,0 +1,97 @@ +import type { + CardTransformer, + MobiledocCardTransformer, + MobiledocTransformOptions, + MobiledocTransformOptionsInput +} from './types'; + +type MobiledocMarkup = [string, string[] | undefined]; +type MobiledocCard = [string, unknown]; + +interface MobiledocDocument { + markups?: MobiledocMarkup[]; + cards?: MobiledocCard[]; +} + +type MobiledocTransformFunction = ( + value: string, + siteUrl: string, + itemPath: string | null, + options: MobiledocTransformOptions +) => string | undefined; + +function mobiledocTransform( + serializedMobiledoc: string, + siteUrl: string, + transformFunction: MobiledocTransformFunction, + itemPath: string | null, + _options: MobiledocTransformOptionsInput = {} +): string { + const defaultOptions: MobiledocTransformOptions = { + assetsOnly: false, + secure: false, + cardTransformers: [], + siteUrl, + itemPath, + transformType: 'relativeToAbsolute' + }; + + const options: MobiledocTransformOptions = { + ...defaultOptions, + ..._options, + siteUrl, + itemPath + }; + + const transformerMap = new Map(); + options.cardTransformers.forEach((cardTransformer: MobiledocCardTransformer) => { + const transformer = cardTransformer[options.transformType]; + if (transformer) { + transformerMap.set(cardTransformer.name, transformer); + } + }); + delete (options as unknown as Record).cardTransformers; + + // function only accepts serialized mobiledoc so there's no chance of accidentally + // modifying pass-by-reference objects + const mobiledoc = JSON.parse(serializedMobiledoc) as MobiledocDocument; + + // any mobiledoc links will have an 'a' markup with an 'href' attribute + (mobiledoc.markups || []).forEach((markup) => { + if (markup[0] === 'a' && markup[1]) { + // mobiledoc markup attrs are in an array like ['key', 'value', 'key2', 'value2'] + // we only care about the href attr so loop through and find it so we can get the idx of it's value + let hrefIndex = -1; + + markup[1].forEach((attr, index) => { + if (attr === 'href') { + hrefIndex = index + 1; + } + }); + + if (hrefIndex !== -1) { + const transformedUrl = transformFunction(markup[1][hrefIndex], siteUrl, itemPath, options); + if (transformedUrl) { + markup[1][hrefIndex] = transformedUrl; + } + } + } + }); + + // any other urls will be within card payloads. We can't know what format + // cards may contain so we sub out to card-specific transform functions that + // are passed in as options from the consuming application. + (mobiledoc.cards || []).forEach((card) => { + const [name, payload] = card; + const transformer = transformerMap.get(name); + if (transformer) { + // transformers take a payload and return a transformed payload + const transformedPayload = transformer(payload, options); + card[1] = transformedPayload; + } + }); + + return JSON.stringify(mobiledoc); +} + +export default mobiledocTransform; diff --git a/packages/url-utils/lib/utils/plaintext-absolute-to-transform-ready.js b/packages/url-utils/src/utils/plaintext-absolute-to-transform-ready.ts similarity index 57% rename from packages/url-utils/lib/utils/plaintext-absolute-to-transform-ready.js rename to packages/url-utils/src/utils/plaintext-absolute-to-transform-ready.ts index a3dbb57fb..916ef8a23 100644 --- a/packages/url-utils/lib/utils/plaintext-absolute-to-transform-ready.js +++ b/packages/url-utils/src/utils/plaintext-absolute-to-transform-ready.ts @@ -1,12 +1,17 @@ -const absoluteToTransformReady = require('./absolute-to-transform-ready'); +import absoluteToTransformReady, {type AbsoluteToTransformReadyOptionsInput} from './absolute-to-transform-ready'; -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } -const plaintextAbsoluteToTransformReady = function plaintextAbsoluteToTransformReady(plaintext, rootUrl, itemPath, options) { +const plaintextAbsoluteToTransformReady = function plaintextAbsoluteToTransformReady( + plaintext: string, + rootUrl: string, + itemPath?: string | AbsoluteToTransformReadyOptionsInput | null, + options?: AbsoluteToTransformReadyOptionsInput +): string { // itemPath is optional, if it's an object may be the options param instead - if (typeof itemPath === 'object' && !options) { + if (typeof itemPath === 'object' && itemPath !== null && !options) { options = itemPath; itemPath = null; } @@ -23,4 +28,4 @@ const plaintextAbsoluteToTransformReady = function plaintextAbsoluteToTransformR }); }; -module.exports = plaintextAbsoluteToTransformReady; +export default plaintextAbsoluteToTransformReady; diff --git a/packages/url-utils/src/utils/plaintext-relative-to-transform-ready.ts b/packages/url-utils/src/utils/plaintext-relative-to-transform-ready.ts new file mode 100644 index 000000000..764efaff5 --- /dev/null +++ b/packages/url-utils/src/utils/plaintext-relative-to-transform-ready.ts @@ -0,0 +1,25 @@ +import relativeToTransformReady, {type RelativeToTransformReadyOptionsInput} from './relative-to-transform-ready'; + +const plaintextRelativeToTransformReady = function plaintextRelativeToTransformReady( + plaintext: string, + rootUrl: string, + itemPath?: string | RelativeToTransformReadyOptionsInput | null, + options?: RelativeToTransformReadyOptionsInput +): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: RelativeToTransformReadyOptionsInput | undefined = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + resolvedItemPath = null; + } + + // plaintext links look like "Link title [url]" + // those are all we care about so we can do a fast regex here + return plaintext.replace(/ \[(\/.*?)\]/g, function (_fullMatch, path) { + const newPath = relativeToTransformReady(`${path}`, rootUrl, resolvedItemPath, resolvedOptions); + return ` [${newPath}]`; + }); +}; + +export default plaintextRelativeToTransformReady; diff --git a/packages/url-utils/src/utils/plaintext-to-transform-ready.ts b/packages/url-utils/src/utils/plaintext-to-transform-ready.ts new file mode 100644 index 000000000..afee41773 --- /dev/null +++ b/packages/url-utils/src/utils/plaintext-to-transform-ready.ts @@ -0,0 +1,24 @@ +import plaintextAbsoluteToTransformReady from './plaintext-absolute-to-transform-ready'; +import type {AbsoluteToTransformReadyOptionsInput} from './absolute-to-transform-ready'; +import plaintextRelativeToTransformReady from './plaintext-relative-to-transform-ready'; + +export type PlaintextToTransformReadyOptions = AbsoluteToTransformReadyOptionsInput; + +function plaintextToTransformReady( + plaintext: string, + siteUrl: string, + itemPath?: string | PlaintextToTransformReadyOptions | null, + options?: PlaintextToTransformReadyOptions +): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: PlaintextToTransformReadyOptions | undefined = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + + const relativeTransformed = plaintextRelativeToTransformReady(plaintext, siteUrl, resolvedItemPath, resolvedOptions); + return plaintextAbsoluteToTransformReady(relativeTransformed, siteUrl, resolvedOptions); +} + +export default plaintextToTransformReady; diff --git a/packages/url-utils/lib/utils/relative-to-absolute.js b/packages/url-utils/src/utils/relative-to-absolute.ts similarity index 67% rename from packages/url-utils/lib/utils/relative-to-absolute.js rename to packages/url-utils/src/utils/relative-to-absolute.ts index 7f5e3ce33..4adf6e799 100644 --- a/packages/url-utils/lib/utils/relative-to-absolute.js +++ b/packages/url-utils/src/utils/relative-to-absolute.ts @@ -1,6 +1,9 @@ -// require the whatwg compatible URL library (same behaviour in node and browser) -const {URL} = require('url'); -const urlJoin = require('./url-join'); +import {URL} from 'url'; +import urlJoin from './url-join'; +import type {SecureOptions, SecureOptionsInput} from './types'; + +export type RelativeToAbsoluteOptions = SecureOptions; +export type RelativeToAbsoluteOptionsInput = SecureOptionsInput; // NOTE: Ghost's relative->absolute handling is a little strange when the rootUrl // includes a subdirectory. Root-relative paths such as /content/image.jpg are @@ -21,24 +24,29 @@ const urlJoin = require('./url-join'); * @param {object} options * @returns {string} The passed in url or an absolute URL using */ -const relativeToAbsolute = function relativeToAbsolute(path, rootUrl, itemPath, _options) { +const relativeToAbsolute = function relativeToAbsolute(path: string, rootUrl: string, itemPath?: string | RelativeToAbsoluteOptionsInput | null, _options?: RelativeToAbsoluteOptionsInput): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: RelativeToAbsoluteOptionsInput | undefined = _options; + // itemPath is optional, if it's an object it may be the options param instead - if (typeof itemPath === 'object' && !_options) { - _options = itemPath; - itemPath = null; + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; } // itemPath could be sent as a full url in which case, extract the pathname - if (itemPath && itemPath.match(/^http/)) { - const itemUrl = new URL(itemPath); - itemPath = itemUrl.pathname; + if (resolvedItemPath && resolvedItemPath.match(/^http/)) { + const itemUrl = new URL(resolvedItemPath); + resolvedItemPath = itemUrl.pathname; } - const defaultOptions = { + const defaultOptions: RelativeToAbsoluteOptions = { assetsOnly: false, staticImageUrlPrefix: 'content/images' }; - const options = Object.assign({}, defaultOptions, _options); + const options: RelativeToAbsoluteOptions = { + ...defaultOptions, + ...resolvedOptions + }; // return the path as-is if it's not an asset path and we're only modifying assets if (options.assetsOnly) { @@ -70,7 +78,7 @@ const relativeToAbsolute = function relativeToAbsolute(path, rootUrl, itemPath, } // return the path as-is if it's not root-relative and we have no itemPath - if (!itemPath && !path.match(/^\//)) { + if (!resolvedItemPath && !path.match(/^\//)) { return path; } @@ -80,7 +88,7 @@ const relativeToAbsolute = function relativeToAbsolute(path, rootUrl, itemPath, } const parsedRootUrl = new URL(rootUrl); - const basePath = path.startsWith('/') ? '' : itemPath; + const basePath = path.startsWith('/') ? '' : (resolvedItemPath ?? ''); const fullPath = urlJoin([parsedRootUrl.pathname, basePath, path], {rootUrl}); const absoluteUrl = new URL(fullPath, rootUrl); @@ -91,4 +99,4 @@ const relativeToAbsolute = function relativeToAbsolute(path, rootUrl, itemPath, return absoluteUrl.toString(); }; -module.exports = relativeToAbsolute; +export default relativeToAbsolute; diff --git a/packages/url-utils/src/utils/relative-to-transform-ready.ts b/packages/url-utils/src/utils/relative-to-transform-ready.ts new file mode 100644 index 000000000..0e2a41a7d --- /dev/null +++ b/packages/url-utils/src/utils/relative-to-transform-ready.ts @@ -0,0 +1,54 @@ +import relativeToAbsolute from './relative-to-absolute'; +import type { + TransformReadyReplacementOptions, + TransformReadyReplacementOptionsInput +} from './types'; + +export interface RelativeToTransformReadyOptions extends TransformReadyReplacementOptions { + staticImageUrlPrefix: string; + secure: boolean; +} + +export type RelativeToTransformReadyOptionsInput = Partial; + +const relativeToTransformReady = function (url: string, root: string, itemPath?: string | RelativeToTransformReadyOptionsInput | null, _options?: RelativeToTransformReadyOptionsInput): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: RelativeToTransformReadyOptionsInput | undefined = _options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + + const options: RelativeToTransformReadyOptions = { + replacementStr: '__GHOST_URL__', + staticImageUrlPrefix: 'content/images', + secure: false, + ...resolvedOptions + }; + + options.secure = false; + + // convert to absolute + const absoluteUrl = relativeToAbsolute(url, root, resolvedItemPath, options); + + if (absoluteUrl === url) { + return url; + } + + const rootUrl = new URL(root); + const rootPathname = rootUrl.pathname.replace(/\/$/, ''); + + // only convert to transform-ready if root url has no subdirectory or the subdirectory matches + if (!url.match(/^\//) || rootPathname === '' || url.indexOf(rootPathname) === 0 || url.indexOf(`/${options.staticImageUrlPrefix}`) === 0) { + // replace root with replacement string + const transformedUrl = absoluteUrl + .replace(root, `${options.replacementStr}/`) // always have trailing slash after magic string + .replace(/([^:])\/\//g, '$1/'); + + return transformedUrl; + } + + return url; +}; + +export default relativeToTransformReady; diff --git a/packages/url-utils/src/utils/replace-permalink.ts b/packages/url-utils/src/utils/replace-permalink.ts new file mode 100644 index 000000000..dd9ce769c --- /dev/null +++ b/packages/url-utils/src/utils/replace-permalink.ts @@ -0,0 +1,55 @@ +import moment from 'moment-timezone'; + +interface PermalinkResource { + published_at?: string | number | Date | null; + slug: string; + id: string; + primary_author?: {slug: string} | null; + primary_tag?: {slug: string} | null; +} + +/** + * creates the url path for a post based on blog timezone and permalink pattern + */ +function replacePermalink(permalink: string, resource: PermalinkResource, timezone: string = 'UTC'): string { + const primaryTagFallback = 'all'; + const publishedAtMoment = moment.tz(resource.published_at || Date.now(), timezone); + const permalinkLookUp: Record string> = { + year() { + return publishedAtMoment.format('YYYY'); + }, + month() { + return publishedAtMoment.format('MM'); + }, + day() { + return publishedAtMoment.format('DD'); + }, + author() { + return resource.primary_author?.slug ?? primaryTagFallback; + }, + primary_author() { + return resource.primary_author?.slug ?? primaryTagFallback; + }, + primary_tag() { + return resource.primary_tag?.slug ?? primaryTagFallback; + }, + slug() { + return resource.slug; + }, + id() { + return resource.id; + } + }; + + // replace tags like :slug or :year with actual values + const permalinkKeys = Object.keys(permalinkLookUp); + return permalink.replace(/(:[a-z_]+)/g, function (match) { + const key = match.slice(1); + if (permalinkKeys.includes(key)) { + return permalinkLookUp[key](); + } + return 'undefined'; + }); +} + +export default replacePermalink; diff --git a/packages/url-utils/lib/utils/strip-subdirectory-from-path.js b/packages/url-utils/src/utils/strip-subdirectory-from-path.ts similarity index 87% rename from packages/url-utils/lib/utils/strip-subdirectory-from-path.js rename to packages/url-utils/src/utils/strip-subdirectory-from-path.ts index c649d681d..56a1a82b5 100644 --- a/packages/url-utils/lib/utils/strip-subdirectory-from-path.js +++ b/packages/url-utils/src/utils/strip-subdirectory-from-path.ts @@ -1,4 +1,4 @@ -const {URL} = require('url'); +import {URL} from 'url'; /** * Removes the directory in the root url from the relative path @@ -7,7 +7,7 @@ const {URL} = require('url'); * @param {string} rootUrl Root URL (eg, 'https://mysite.com/my/subdir/) * @returns {string} Path relative to the rootUrl's path */ -const stripSubdirectoryFromPath = function stripSubdirectoryFromPath(path = '', rootUrl = '') { +const stripSubdirectoryFromPath = function stripSubdirectoryFromPath(path: string = '', rootUrl: string = ''): string { // force root to always have a trailing-slash for consistent behaviour if (!rootUrl.endsWith('/')) { rootUrl = `${rootUrl}/`; @@ -33,4 +33,4 @@ const stripSubdirectoryFromPath = function stripSubdirectoryFromPath(path = '', return path; }; -module.exports = stripSubdirectoryFromPath; +export default stripSubdirectoryFromPath; diff --git a/packages/url-utils/src/utils/to-transform-ready.ts b/packages/url-utils/src/utils/to-transform-ready.ts new file mode 100644 index 000000000..52c4d1027 --- /dev/null +++ b/packages/url-utils/src/utils/to-transform-ready.ts @@ -0,0 +1,23 @@ +import absoluteToTransformReady, {type AbsoluteToTransformReadyOptionsInput} from './absolute-to-transform-ready'; +import relativeToAbsolute, {type RelativeToAbsoluteOptionsInput} from './relative-to-absolute'; + +export type ToTransformReadyOptions = RelativeToAbsoluteOptionsInput & AbsoluteToTransformReadyOptionsInput; + +function toTransformReady( + url: string, + siteUrl: string, + itemPath?: string | ToTransformReadyOptions | null, + options?: ToTransformReadyOptions +): string { + let resolvedItemPath: string | null = typeof itemPath === 'string' ? itemPath : null; + let resolvedOptions: ToTransformReadyOptions | undefined = options; + + if (typeof itemPath === 'object' && itemPath !== null && !resolvedOptions) { + resolvedOptions = itemPath; + } + + const absoluteUrl = relativeToAbsolute(url, siteUrl, resolvedItemPath, resolvedOptions); + return absoluteToTransformReady(absoluteUrl, siteUrl, resolvedOptions); +} + +export default toTransformReady; diff --git a/packages/url-utils/src/utils/transform-ready-to-absolute.ts b/packages/url-utils/src/utils/transform-ready-to-absolute.ts new file mode 100644 index 000000000..ab0b3ad33 --- /dev/null +++ b/packages/url-utils/src/utils/transform-ready-to-absolute.ts @@ -0,0 +1,25 @@ +import type {TransformReadyReplacementOptions, TransformReadyReplacementOptionsInput} from './types'; + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +const transformReadyToAbsolute = function (str: string = '', root: string, _options: TransformReadyReplacementOptionsInput = {}): string { + const defaultOptions: TransformReadyReplacementOptions = { + replacementStr: '__GHOST_URL__' + }; + const options: TransformReadyReplacementOptions = { + ...defaultOptions, + ..._options + }; + + if (!str || str.indexOf(options.replacementStr) === -1) { + return str; + } + + const replacementRegex = new RegExp(escapeRegExp(options.replacementStr), 'g'); + + return str.replace(replacementRegex, root.replace(/\/$/, '')); +}; + +export default transformReadyToAbsolute; diff --git a/packages/url-utils/src/utils/transform-ready-to-relative.ts b/packages/url-utils/src/utils/transform-ready-to-relative.ts new file mode 100644 index 000000000..e10683648 --- /dev/null +++ b/packages/url-utils/src/utils/transform-ready-to-relative.ts @@ -0,0 +1,29 @@ +import type {TransformReadyReplacementOptions, TransformReadyReplacementOptionsInput} from './types'; + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +const transformReadyToRelative = function (str: string = '', root: string, _options: TransformReadyReplacementOptionsInput = {}): string { + const defaultOptions: TransformReadyReplacementOptions = { + replacementStr: '__GHOST_URL__' + }; + const options: TransformReadyReplacementOptions = { + ...defaultOptions, + ..._options + }; + + if (!str || str.indexOf(options.replacementStr) === -1) { + return str; + } + + const rootURL = new URL(root); + // subdir with no trailing slash because we'll always have a trailing slash after the magic string + const subdir = rootURL.pathname.replace(/\/$/, ''); + + const replacementRegex = new RegExp(escapeRegExp(options.replacementStr), 'g'); + + return str.replace(replacementRegex, subdir); +}; + +export default transformReadyToRelative; diff --git a/packages/url-utils/src/utils/types.ts b/packages/url-utils/src/utils/types.ts new file mode 100644 index 000000000..580cd0705 --- /dev/null +++ b/packages/url-utils/src/utils/types.ts @@ -0,0 +1,88 @@ +export type UnknownRecord = Record; + +export interface AssetAwareOptions extends UnknownRecord { + assetsOnly: boolean; + staticImageUrlPrefix?: string; +} + +export type AssetAwareOptionsInput = Partial; + +export interface SecureOptions extends AssetAwareOptions { + secure?: boolean; +} + +export type SecureOptionsInput = Partial; + +export interface TransformReadyReplacementOptions extends UnknownRecord { + replacementStr: string; + withoutSubdirectory?: boolean; +} + +export type TransformReadyReplacementOptionsInput = Partial; + +export interface HtmlTransformOptions extends SecureOptions { + earlyExitMatchStr?: string; +} + +export type HtmlTransformOptionsInput = SecureOptionsInput & {earlyExitMatchStr?: string}; + +export interface MarkdownTransformOptions extends AssetAwareOptions { + ignoreProtocol: boolean; + earlyExitMatchStr?: string; +} + +export type MarkdownTransformOptionsInput = AssetAwareOptionsInput & {ignoreProtocol?: boolean; earlyExitMatchStr?: string}; + +export type MobiledocTransformType = 'relativeToAbsolute' | 'absoluteToRelative' | 'toTransformReady'; + +export type CardTransformer = (payload: unknown, options: UnknownRecord) => unknown; + +export interface MobiledocCardTransformer { + name: string; + relativeToAbsolute?: CardTransformer; + absoluteToRelative?: CardTransformer; + toTransformReady?: CardTransformer; +} + +export interface MobiledocTransformOptions extends SecureOptions { + cardTransformers: MobiledocCardTransformer[]; + siteUrl: string; + itemPath: string | null; + transformType: MobiledocTransformType; +} + +export type MobiledocTransformOptionsInput = Partial> & { + cardTransformers?: MobiledocCardTransformer[]; + siteUrl?: string; + itemPath?: string | null; + transformType?: MobiledocTransformType; +}; + +export type LexicalTransformType = MobiledocTransformType; +export type LexicalTransformFunction = (value: string) => string; +export interface LexicalUrlTransformMap { + [key: string]: string | LexicalUrlTransformMap; +} + +export interface LexicalNodeConfig { + getType(): string; + urlTransformMap?: LexicalUrlTransformMap; +} + +export type LexicalTransformRegistry = Partial>>; + +export interface LexicalTransformOptions extends SecureOptions { + nodes: LexicalNodeConfig[]; + transformMap: LexicalTransformRegistry; + transformType: LexicalTransformType; + siteUrl: string; + itemPath: string | null; +} + +export type LexicalTransformOptionsInput = Partial> & { + nodes?: LexicalNodeConfig[]; + transformMap?: LexicalTransformRegistry; + siteUrl?: string; + itemPath?: string | null; + transformType?: LexicalTransformType; +}; diff --git a/packages/url-utils/lib/utils/url-join.js b/packages/url-utils/src/utils/url-join.ts similarity index 77% rename from packages/url-utils/lib/utils/url-join.js rename to packages/url-utils/src/utils/url-join.ts index 2cf5c0730..bb8af6642 100644 --- a/packages/url-utils/lib/utils/url-join.js +++ b/packages/url-utils/src/utils/url-join.ts @@ -1,4 +1,8 @@ -const deduplicateSubdirectory = require('./deduplicate-subdirectory'); +import deduplicateSubdirectory from './deduplicate-subdirectory'; + +export interface UrlJoinOptions { + rootUrl?: string; +} /** urlJoin * Returns a URL/path for internal use in Ghost. @@ -7,7 +11,7 @@ const deduplicateSubdirectory = require('./deduplicate-subdirectory'); * @param {string} options.rootUrl used for deduplicating any subdirectories * @return {string} URL concatinated URL/path of arguments. */ -function urlJoin(parts, options) { +function urlJoin(parts: string[], options: UrlJoinOptions = {}) { let prefixDoubleSlash = false; // Remove empty item at the beginning @@ -31,7 +35,11 @@ function urlJoin(parts, options) { url = url.replace(/^\//, '//'); } + if (!options.rootUrl) { + return url; + } + return deduplicateSubdirectory(url, options.rootUrl); } -module.exports = urlJoin; +export default urlJoin; diff --git a/packages/url-utils/test/unit/url-utils.test.js b/packages/url-utils/test/unit/url-utils.test.js index fac91cb96..a4a5325e8 100644 --- a/packages/url-utils/test/unit/url-utils.test.js +++ b/packages/url-utils/test/unit/url-utils.test.js @@ -4,7 +4,8 @@ require('../utils'); const assert = require('assert/strict'); const sinon = require('sinon'); -const UrlUtils = require('../../lib/UrlUtils'); +const UrlUtilsModule = require('../../lib/UrlUtils'); +const UrlUtils = UrlUtilsModule.default || UrlUtilsModule.UrlUtils; const configUrlHelpers = require('@tryghost/config-url-helpers'); const constants = { diff --git a/packages/url-utils/test/unit/utils/absolute-to-relative.test.js b/packages/url-utils/test/unit/utils/absolute-to-relative.test.js index eec1861c4..bc93e81ac 100644 --- a/packages/url-utils/test/unit/utils/absolute-to-relative.test.js +++ b/packages/url-utils/test/unit/utils/absolute-to-relative.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('../../utils'); -const absoluteToRelative = require('../../../lib/utils/absolute-to-relative'); +const absoluteToRelative = require('../../../lib/utils/absolute-to-relative').default; describe('utils: absoluteToRelative()', function () { it('ignores relative URLs', function () { diff --git a/packages/url-utils/test/unit/utils/absolute-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/absolute-to-transform-ready.test.js index 741248f87..fb92beab3 100644 --- a/packages/url-utils/test/unit/utils/absolute-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/absolute-to-transform-ready.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('../../utils'); -const absoluteToTransformReady = require('../../../lib/utils/absolute-to-transform-ready'); +const absoluteToTransformReady = require('../../../lib/utils/absolute-to-transform-ready').default; describe('utils: absoluteToTransformReady()', function () { it('ignores relative URLs', function () { diff --git a/packages/url-utils/test/unit/utils/deduplicate-subdirectory.test.js b/packages/url-utils/test/unit/utils/deduplicate-subdirectory.test.js index 4eb62c3f7..fb52f72b8 100644 --- a/packages/url-utils/test/unit/utils/deduplicate-subdirectory.test.js +++ b/packages/url-utils/test/unit/utils/deduplicate-subdirectory.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('../../utils'); -const deduplicateSubdirectory = require('../../../lib/utils/deduplicate-subdirectory'); +const deduplicateSubdirectory = require('../../../lib/utils/deduplicate-subdirectory').default; describe('utils: deduplicateSubdirectory()', function () { describe('with url', function () { diff --git a/packages/url-utils/test/unit/utils/html-absolute-to-relative.test.js b/packages/url-utils/test/unit/utils/html-absolute-to-relative.test.js index 4c3d28a3d..e2b7bd405 100644 --- a/packages/url-utils/test/unit/utils/html-absolute-to-relative.test.js +++ b/packages/url-utils/test/unit/utils/html-absolute-to-relative.test.js @@ -5,7 +5,7 @@ require('../../utils'); const sinon = require('sinon'); const cheerio = require('cheerio'); -const htmlAbsoluteToRelative = require('../../../lib/utils/html-absolute-to-relative'); +const htmlAbsoluteToRelative = require('../../../lib/utils/html-absolute-to-relative').default; describe('utils: htmlAbsoluteToRelative()', function () { const siteUrl = 'http://my-ghost-blog.com'; diff --git a/packages/url-utils/test/unit/utils/html-absolute-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/html-absolute-to-transform-ready.test.js index 35bb59b02..add3e2e0f 100644 --- a/packages/url-utils/test/unit/utils/html-absolute-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/html-absolute-to-transform-ready.test.js @@ -6,8 +6,8 @@ const rewire = require('rewire'); const sinon = require('sinon'); const cheerio = require('cheerio'); -const htmlTransform = rewire('../../../lib/utils/html-transform'); -const htmlAbsoluteToTransformReady = require('../../../lib/utils/html-absolute-to-transform-ready'); +const htmlTransformModule = rewire('../../../lib/utils/html-transform'); +const htmlAbsoluteToTransformReady = require('../../../lib/utils/html-absolute-to-transform-ready').default; describe('utils: htmlAbsoluteToTransformReady()', function () { const siteUrl = 'http://my-ghost-blog.com'; @@ -239,7 +239,7 @@ describe('utils: htmlAbsoluteToTransformReady()', function () { beforeEach(function () { cheerioLoadSpy = sinon.spy(cheerio, 'load'); - rewireRestore = htmlTransform.__set__('cheerio', cheerio); + rewireRestore = htmlTransformModule.__set__('cheerio', cheerio); }); afterEach(function () { diff --git a/packages/url-utils/test/unit/utils/html-relative-to-absolute.test.js b/packages/url-utils/test/unit/utils/html-relative-to-absolute.test.js index 05e363151..46368c2d6 100644 --- a/packages/url-utils/test/unit/utils/html-relative-to-absolute.test.js +++ b/packages/url-utils/test/unit/utils/html-relative-to-absolute.test.js @@ -6,8 +6,8 @@ const sinon = require('sinon'); const rewire = require('rewire'); const cheerio = require('cheerio'); -const htmlTransform = rewire('../../../lib/utils/html-transform'); -const htmlRelativeToAbsolute = require('../../../lib/utils/html-relative-to-absolute'); +const htmlTransformModule = rewire('../../../lib/utils/html-transform'); +const htmlRelativeToAbsolute = require('../../../lib/utils/html-relative-to-absolute').default; describe('utils: htmlRelativeToAbsolute()', function () { const siteUrl = 'http://my-ghost-blog.com'; @@ -247,7 +247,7 @@ describe('utils: htmlRelativeToAbsolute()', function () { beforeEach(function () { cheerioLoadSpy = sinon.spy(cheerio, 'load'); - rewireRestore = htmlTransform.__set__('cheerio', cheerio); + rewireRestore = htmlTransformModule.__set__('cheerio', cheerio); }); afterEach(function () { diff --git a/packages/url-utils/test/unit/utils/html-relative-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/html-relative-to-transform-ready.test.js index e184a5f1a..0f99adad0 100644 --- a/packages/url-utils/test/unit/utils/html-relative-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/html-relative-to-transform-ready.test.js @@ -5,7 +5,7 @@ require('../../utils'); const sinon = require('sinon'); const cheerio = require('cheerio'); -const htmlRelativeToTransformReady = require('../../../lib/utils/html-relative-to-transform-ready'); +const htmlRelativeToTransformReady = require('../../../lib/utils/html-relative-to-transform-ready').default; describe('utils: htmlRelativeToTransformReady()', function () { const siteUrl = 'http://my-ghost-blog.com'; diff --git a/packages/url-utils/test/unit/utils/is-ssl.test.js b/packages/url-utils/test/unit/utils/is-ssl.test.js index eb11d2af0..43a6b31fd 100644 --- a/packages/url-utils/test/unit/utils/is-ssl.test.js +++ b/packages/url-utils/test/unit/utils/is-ssl.test.js @@ -1,4 +1,4 @@ -const isSSL = require('../../../lib/utils/is-ssl'); +const isSSL = require('../../../lib/utils/is-ssl').default; describe('isSSL', function () { it('detects https protocol correctly', function () { diff --git a/packages/url-utils/test/unit/utils/lexical-absolute-to-relative.test.js b/packages/url-utils/test/unit/utils/lexical-absolute-to-relative.test.js index fe551bcc8..500a193da 100644 --- a/packages/url-utils/test/unit/utils/lexical-absolute-to-relative.test.js +++ b/packages/url-utils/test/unit/utils/lexical-absolute-to-relative.test.js @@ -2,8 +2,9 @@ // const testUtils = require('./utils'); require('../../utils'); -const UrlUtils = require('../../../lib/UrlUtils'); -const lexicalAbsoluteToRelative = require('../../../lib/utils/lexical-absolute-to-relative'); +const UrlUtilsModule = require('../../../lib/UrlUtils'); +const UrlUtils = UrlUtilsModule.default || UrlUtilsModule.UrlUtils; +const lexicalAbsoluteToRelative = require('../../../lib/utils/lexical-absolute-to-relative').default; describe('utils: lexicalAbsoluteToRelative()', function () { const siteUrl = 'http://my-ghost-blog.com'; diff --git a/packages/url-utils/test/unit/utils/lexical-absolute-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/lexical-absolute-to-transform-ready.test.js index 10a37b285..4505b009c 100644 --- a/packages/url-utils/test/unit/utils/lexical-absolute-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/lexical-absolute-to-transform-ready.test.js @@ -2,8 +2,9 @@ // const testUtils = require('./utils'); require('../../utils'); -const UrlUtils = require('../../../lib/UrlUtils'); -const lexicalAbsoluteToTransformReady = require('../../../lib/utils/lexical-absolute-to-transform-ready'); +const UrlUtilsModule = require('../../../lib/UrlUtils'); +const UrlUtils = UrlUtilsModule.default || UrlUtilsModule.UrlUtils; +const lexicalAbsoluteToTransformReady = require('../../../lib/utils/lexical-absolute-to-transform-ready').default; describe('utils: lexicalAbsoluteToTransformReady()', function () { const siteUrl = 'http://my-ghost-blog.com'; diff --git a/packages/url-utils/test/unit/utils/lexical-relative-to-absolute.test.js b/packages/url-utils/test/unit/utils/lexical-relative-to-absolute.test.js index 0f4678dee..05536abc4 100644 --- a/packages/url-utils/test/unit/utils/lexical-relative-to-absolute.test.js +++ b/packages/url-utils/test/unit/utils/lexical-relative-to-absolute.test.js @@ -2,8 +2,9 @@ // const testUtils = require('./utils'); require('../../utils'); -const UrlUtils = require('../../../lib/UrlUtils'); -const lexicalRelativeToAbsolute = require('../../../lib/utils/lexical-relative-to-absolute'); +const UrlUtilsModule = require('../../../lib/UrlUtils'); +const UrlUtils = UrlUtilsModule.default || UrlUtilsModule.UrlUtils; +const lexicalRelativeToAbsolute = require('../../../lib/utils/lexical-relative-to-absolute').default; describe('utils: lexicalRelativeToAbsolute()', function () { const siteUrl = 'http://my-ghost-blog.com'; diff --git a/packages/url-utils/test/unit/utils/lexical-relative-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/lexical-relative-to-transform-ready.test.js index ffed06fd1..0e57eb3c9 100644 --- a/packages/url-utils/test/unit/utils/lexical-relative-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/lexical-relative-to-transform-ready.test.js @@ -2,8 +2,9 @@ // const testUtils = require('./utils'); require('../../utils'); -const UrlUtils = require('../../../lib/UrlUtils'); -const lexicalRelativeToTransformReady = require('../../../lib/utils/lexical-relative-to-transform-ready'); +const UrlUtilsModule = require('../../../lib/UrlUtils'); +const UrlUtils = UrlUtilsModule.default || UrlUtilsModule.UrlUtils; +const lexicalRelativeToTransformReady = require('../../../lib/utils/lexical-relative-to-transform-ready').default; describe('utils: lexicalRelativeToTransformReady()', function () { const siteUrl = 'http://my-ghost-blog.com'; diff --git a/packages/url-utils/test/unit/utils/markdown-absolute-to-relative.test.js b/packages/url-utils/test/unit/utils/markdown-absolute-to-relative.test.js index 601077e34..e1e6e6a34 100644 --- a/packages/url-utils/test/unit/utils/markdown-absolute-to-relative.test.js +++ b/packages/url-utils/test/unit/utils/markdown-absolute-to-relative.test.js @@ -8,8 +8,10 @@ const rewire = require('rewire'); const sinon = require('sinon'); const remark = require('remark'); -const markdownTransform = rewire('../../../lib/utils/markdown-transform'); -const markdownAbsoluteToRelative = rewire('../../../lib/utils/markdown-absolute-to-relative'); +const markdownTransformModule = rewire('../../../lib/utils/markdown-transform'); +const markdownTransform = markdownTransformModule.default || markdownTransformModule; +const markdownAbsoluteToRelativeModule = rewire('../../../lib/utils/markdown-absolute-to-relative'); +const markdownAbsoluteToRelative = markdownAbsoluteToRelativeModule.default || markdownAbsoluteToRelativeModule; describe('utils: markdownAbsoluteToRelative()', function () { const siteUrl = 'http://my-ghost-blog.com'; @@ -123,8 +125,8 @@ Testing Inline with **markdown** beforeEach(function () { sandbox = sinon.createSandbox(); remarkSpy = sinon.spy(remark); - markdownTransform.__set__('remark', remarkSpy); - markdownAbsoluteToRelative.__set__('markdownTransform', markdownTransform); + markdownTransformModule.__set__('remark', remarkSpy); + markdownAbsoluteToRelativeModule.__set__('markdownTransform', markdownTransform); }); afterEach(function () { diff --git a/packages/url-utils/test/unit/utils/markdown-absolute-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/markdown-absolute-to-transform-ready.test.js index 2632114e0..7722a4574 100644 --- a/packages/url-utils/test/unit/utils/markdown-absolute-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/markdown-absolute-to-transform-ready.test.js @@ -8,8 +8,10 @@ const rewire = require('rewire'); const sinon = require('sinon'); const remark = require('remark'); -const markdownTransform = rewire('../../../lib/utils/markdown-transform'); -const markdownAbsoluteToTransformReady = rewire('../../../lib/utils/markdown-absolute-to-transform-ready'); +const markdownTransformModule = rewire('../../../lib/utils/markdown-transform'); +const markdownTransform = markdownTransformModule.default || markdownTransformModule; +const markdownAbsoluteToTransformReadyModule = rewire('../../../lib/utils/markdown-absolute-to-transform-ready'); +const markdownAbsoluteToTransformReady = markdownAbsoluteToTransformReadyModule.default || markdownAbsoluteToTransformReadyModule; describe('utils: markdownAbsoluteToTransformReady()', function () { const siteUrl = 'http://my-ghost-blog.com'; @@ -123,8 +125,8 @@ Testing Inline with **markdown** beforeEach(function () { sandbox = sinon.createSandbox(); remarkSpy = sinon.spy(remark); - markdownTransform.__set__('remark', remarkSpy); - markdownAbsoluteToTransformReady.__set__('markdownTransform', markdownTransform); + markdownTransformModule.__set__('remark', remarkSpy); + markdownAbsoluteToTransformReadyModule.__set__('markdownTransform', markdownTransform); }); afterEach(function () { diff --git a/packages/url-utils/test/unit/utils/markdown-relative-to-absolute.test.js b/packages/url-utils/test/unit/utils/markdown-relative-to-absolute.test.js index 39e250f22..bcd83bd9c 100644 --- a/packages/url-utils/test/unit/utils/markdown-relative-to-absolute.test.js +++ b/packages/url-utils/test/unit/utils/markdown-relative-to-absolute.test.js @@ -8,8 +8,10 @@ const sinon = require('sinon'); const rewire = require('rewire'); const remark = require('remark'); -const markdownTransform = rewire('../../../lib/utils/markdown-transform'); -const markdownRelativeToAbsolute = rewire('../../../lib/utils/markdown-relative-to-absolute'); +const markdownTransformModule = rewire('../../../lib/utils/markdown-transform'); +const markdownTransform = markdownTransformModule.default || markdownTransformModule; +const markdownRelativeToAbsoluteModule = rewire('../../../lib/utils/markdown-relative-to-absolute'); +const markdownRelativeToAbsolute = markdownRelativeToAbsoluteModule.default || markdownRelativeToAbsoluteModule; describe('utils: markdownRelativeToAbsolute()', function () { const siteUrl = 'http://my-ghost-blog.com'; @@ -117,8 +119,8 @@ Testing Inline with **markdown** beforeEach(function () { sandbox = sinon.createSandbox(); remarkSpy = sinon.spy(remark); - markdownTransform.__set__('remark', remarkSpy); - markdownRelativeToAbsolute.__set__('markdownTransform', markdownTransform); + markdownTransformModule.__set__('remark', remarkSpy); + markdownRelativeToAbsoluteModule.__set__('markdownTransform', markdownTransform); }); afterEach(function () { diff --git a/packages/url-utils/test/unit/utils/markdown-relative-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/markdown-relative-to-transform-ready.test.js index a783f307b..4b497b903 100644 --- a/packages/url-utils/test/unit/utils/markdown-relative-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/markdown-relative-to-transform-ready.test.js @@ -8,8 +8,10 @@ const sinon = require('sinon'); const rewire = require('rewire'); const remark = require('remark'); -const markdownTransform = rewire('../../../lib/utils/markdown-transform'); -const markdownRelativeToTransformReady = rewire('../../../lib/utils/markdown-relative-to-transform-ready'); +const markdownTransformModule = rewire('../../../lib/utils/markdown-transform'); +const markdownTransform = markdownTransformModule.default || markdownTransformModule; +const markdownRelativeToTransformReadyModule = rewire('../../../lib/utils/markdown-relative-to-transform-ready'); +const markdownRelativeToTransformReady = markdownRelativeToTransformReadyModule.default || markdownRelativeToTransformReadyModule; describe('utils: markdownRelativeToTransformReady()', function () { const siteUrl = 'http://my-ghost-blog.com'; @@ -143,8 +145,8 @@ Just testing beforeEach(function () { sandbox = sinon.createSandbox(); remarkSpy = sinon.spy(remark); - markdownTransform.__set__('remark', remarkSpy); - markdownRelativeToTransformReady.__set__('markdownTransform', markdownTransform); + markdownTransformModule.__set__('remark', remarkSpy); + markdownRelativeToTransformReadyModule.__set__('markdownTransform', markdownTransform); }); afterEach(function () { diff --git a/packages/url-utils/test/unit/utils/mobiledoc-absolute-to-relative.test.js b/packages/url-utils/test/unit/utils/mobiledoc-absolute-to-relative.test.js index b0d60684c..fd1e713e2 100644 --- a/packages/url-utils/test/unit/utils/mobiledoc-absolute-to-relative.test.js +++ b/packages/url-utils/test/unit/utils/mobiledoc-absolute-to-relative.test.js @@ -4,7 +4,7 @@ require('../../utils'); const sinon = require('sinon'); -const mobiledocAbsoluteToRelative = require('../../../lib/utils/mobiledoc-absolute-to-relative'); +const mobiledocAbsoluteToRelative = require('../../../lib/utils/mobiledoc-absolute-to-relative').default; describe('utils: mobiledocAbsoluteToRelative()', function () { const siteUrl = 'http://my-ghost-blog.com'; diff --git a/packages/url-utils/test/unit/utils/mobiledoc-absolute-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/mobiledoc-absolute-to-transform-ready.test.js index 5105f8f3f..5b42b0f94 100644 --- a/packages/url-utils/test/unit/utils/mobiledoc-absolute-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/mobiledoc-absolute-to-transform-ready.test.js @@ -4,7 +4,7 @@ require('../../utils'); const sinon = require('sinon'); -const mobiledocAbsoluteToTransformReady = require('../../../lib/utils/mobiledoc-absolute-to-transform-ready'); +const mobiledocAbsoluteToTransformReady = require('../../../lib/utils/mobiledoc-absolute-to-transform-ready').default; describe('utils: mobiledocAbsoluteToTransformReady()', function () { const siteUrl = 'http://my-ghost-blog.com'; diff --git a/packages/url-utils/test/unit/utils/mobiledoc-relative-to-absolute.test.js b/packages/url-utils/test/unit/utils/mobiledoc-relative-to-absolute.test.js index 1de7842e9..19eef6e9b 100644 --- a/packages/url-utils/test/unit/utils/mobiledoc-relative-to-absolute.test.js +++ b/packages/url-utils/test/unit/utils/mobiledoc-relative-to-absolute.test.js @@ -4,7 +4,7 @@ require('../../utils'); const sinon = require('sinon'); -const mobiledocRelativeToAbsolute = require('../../../lib/utils/mobiledoc-relative-to-absolute'); +const mobiledocRelativeToAbsolute = require('../../../lib/utils/mobiledoc-relative-to-absolute').default; describe('utils: mobiledocRelativeToAbsolute()', function () { const siteUrl = 'http://my-ghost-blog.com'; diff --git a/packages/url-utils/test/unit/utils/mobiledoc-relative-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/mobiledoc-relative-to-transform-ready.test.js index e6bf7cbb0..ca24afc5e 100644 --- a/packages/url-utils/test/unit/utils/mobiledoc-relative-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/mobiledoc-relative-to-transform-ready.test.js @@ -4,7 +4,7 @@ require('../../utils'); const sinon = require('sinon'); -const mobiledocRelativeToTransformReady = require('../../../lib/utils/mobiledoc-relative-to-transform-ready'); +const mobiledocRelativeToTransformReady = require('../../../lib/utils/mobiledoc-relative-to-transform-ready').default; describe('utils: mobiledocRelativeToTransformReady()', function () { const siteUrl = 'http://my-ghost-blog.com'; diff --git a/packages/url-utils/test/unit/utils/plaintext-absolute-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/plaintext-absolute-to-transform-ready.test.js index 0581bb495..abf569b9f 100644 --- a/packages/url-utils/test/unit/utils/plaintext-absolute-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/plaintext-absolute-to-transform-ready.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('../../utils'); -const plaintextAbsoluteToTransformReady = require('../../../lib/utils/plaintext-absolute-to-transform-ready'); +const plaintextAbsoluteToTransformReady = require('../../../lib/utils/plaintext-absolute-to-transform-ready').default; describe('utils: plaintextAbsoluteToTransformReady', function () { it('works', function () { diff --git a/packages/url-utils/test/unit/utils/plaintext-relative-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/plaintext-relative-to-transform-ready.test.js index 2b928df3b..86209c081 100644 --- a/packages/url-utils/test/unit/utils/plaintext-relative-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/plaintext-relative-to-transform-ready.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('../../utils'); -const plaintextRelativeToTransformReady = require('../../../lib/utils/plaintext-relative-to-transform-ready'); +const plaintextRelativeToTransformReady = require('../../../lib/utils/plaintext-relative-to-transform-ready').default; describe('utils: plaintextRelativeToTransformReady', function () { it('works', function () { diff --git a/packages/url-utils/test/unit/utils/plaintext-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/plaintext-to-transform-ready.test.js index e1a371c13..415a6eea4 100644 --- a/packages/url-utils/test/unit/utils/plaintext-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/plaintext-to-transform-ready.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('../../utils'); -const plaintextToTransformReady = require('../../../lib/utils/plaintext-to-transform-ready'); +const plaintextToTransformReady = require('../../../lib/utils/plaintext-to-transform-ready').default; describe('utils: plaintextToTransformReady', function () { it('works', function () { diff --git a/packages/url-utils/test/unit/utils/relative-to-absolute.test.js b/packages/url-utils/test/unit/utils/relative-to-absolute.test.js index 2c344a86d..5dcc3ad6c 100644 --- a/packages/url-utils/test/unit/utils/relative-to-absolute.test.js +++ b/packages/url-utils/test/unit/utils/relative-to-absolute.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('../../utils'); -const relativeToAbsolute = require('../../../lib/utils/relative-to-absolute'); +const relativeToAbsolute = require('../../../lib/utils/relative-to-absolute').default; describe('utils: relativeToAbsolute()', function () { it('ignores absolute URLs', function () { diff --git a/packages/url-utils/test/unit/utils/relative-to-transform-ready.test.js b/packages/url-utils/test/unit/utils/relative-to-transform-ready.test.js index 7d5cc30d5..4d2cb48bb 100644 --- a/packages/url-utils/test/unit/utils/relative-to-transform-ready.test.js +++ b/packages/url-utils/test/unit/utils/relative-to-transform-ready.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('../../utils'); -const relativeToTransformReady = require('../../../lib/utils/relative-to-transform-ready'); +const relativeToTransformReady = require('../../../lib/utils/relative-to-transform-ready').default; describe('utils: relativeToTransformReady()', function () { it('ignores absolute URLs', function () { diff --git a/packages/url-utils/test/unit/utils/replace-permalink.test.js b/packages/url-utils/test/unit/utils/replace-permalink.test.js index 10acfdecf..cb21fca92 100644 --- a/packages/url-utils/test/unit/utils/replace-permalink.test.js +++ b/packages/url-utils/test/unit/utils/replace-permalink.test.js @@ -3,7 +3,7 @@ require('../../utils'); const moment = require('moment-timezone'); -const replacePermalink = require('../../../lib/utils/replace-permalink'); +const replacePermalink = require('../../../lib/utils/replace-permalink').default; describe('utils: replacePermalink()', function () { it('permalink is /:slug/, timezone is default', function () { diff --git a/packages/url-utils/test/unit/utils/strip-subdirectory-from-path.test.js b/packages/url-utils/test/unit/utils/strip-subdirectory-from-path.test.js index 7855fd9fb..032456956 100644 --- a/packages/url-utils/test/unit/utils/strip-subdirectory-from-path.test.js +++ b/packages/url-utils/test/unit/utils/strip-subdirectory-from-path.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('../../utils'); -const stripSubdirectoryFromPath = require('../../../lib/utils/strip-subdirectory-from-path'); +const stripSubdirectoryFromPath = require('../../../lib/utils/strip-subdirectory-from-path').default; describe('utils: stripSubdomainFromPath()', function () { it('ignores rootUrl with no subdirectory', function () { diff --git a/packages/url-utils/test/unit/utils/transform-ready-to-absolute.test.js b/packages/url-utils/test/unit/utils/transform-ready-to-absolute.test.js index 536a3117c..4c06cf41c 100644 --- a/packages/url-utils/test/unit/utils/transform-ready-to-absolute.test.js +++ b/packages/url-utils/test/unit/utils/transform-ready-to-absolute.test.js @@ -4,7 +4,7 @@ require('../../utils'); const fs = require('fs'); const path = require('path'); -const transformReadyToAbsolute = require('../../../lib/utils/transform-ready-to-absolute'); +const transformReadyToAbsolute = require('../../../lib/utils/transform-ready-to-absolute').default; describe('utils: transformReadyToAbsolute()', function () { describe('single url', function () { diff --git a/packages/url-utils/test/unit/utils/transform-ready-to-relative.test.js b/packages/url-utils/test/unit/utils/transform-ready-to-relative.test.js index d261b9ae6..2dc6ccc78 100644 --- a/packages/url-utils/test/unit/utils/transform-ready-to-relative.test.js +++ b/packages/url-utils/test/unit/utils/transform-ready-to-relative.test.js @@ -4,7 +4,7 @@ require('../../utils'); const fs = require('fs'); const path = require('path'); -const transformReadyToRelative = require('../../../lib/utils/transform-ready-to-relative'); +const transformReadyToRelative = require('../../../lib/utils/transform-ready-to-relative').default; describe('utils: transformReadyToRelative()', function () { describe('single url', function () { diff --git a/packages/url-utils/test/unit/utils/url-join.test.js b/packages/url-utils/test/unit/utils/url-join.test.js index af36673ba..b8c97b2a5 100644 --- a/packages/url-utils/test/unit/utils/url-join.test.js +++ b/packages/url-utils/test/unit/utils/url-join.test.js @@ -2,7 +2,7 @@ // const testUtils = require('./utils'); require('../../utils'); -const urlJoin = require('../../../lib/utils/url-join'); +const urlJoin = require('../../../lib/utils/url-join').default; describe('utils: urlJoin()', function () { it('should deduplicate slashes', function () { diff --git a/packages/url-utils/tsconfig.json b/packages/url-utils/tsconfig.json new file mode 100644 index 000000000..d0d0c01ca --- /dev/null +++ b/packages/url-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2019", + "module": "CommonJS", + "rootDir": "src", + "outDir": "lib", + "declaration": true, + "sourceMap": true, + "inlineSources": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "Node", + "skipLibCheck": true, + "strict": false + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/url-utils/tsconfig.test.json b/packages/url-utils/tsconfig.test.json new file mode 100644 index 000000000..19bade918 --- /dev/null +++ b/packages/url-utils/tsconfig.test.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "test", + "outDir": "tmp/test", + "declaration": false + }, + "include": [ + "test/**/*.ts" + ] +} diff --git a/yarn.lock b/yarn.lock index 0ec82b7ad..7e6231d71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3348,6 +3348,13 @@ dependencies: "@types/deep-eql" "*" +"@types/cheerio@0.22.35": + version "0.22.35" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.35.tgz#0d16dc1f24d426231c181b9c31847f673867595f" + integrity sha512-yD57BchKRvTV+JD53UZ6PD8KWY5g5rvvMLRnZR3EQBCZXiDT/HR+pKpMzFGlWNhFrXlo7VPZXtKvIEwZkAWOIA== + dependencies: + "@types/node" "*" + "@types/color-convert@*": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.4.tgz#843398ae71e951dc5415d202dfd5e43108823eeb" @@ -3399,6 +3406,11 @@ dependencies: "@types/node" "*" +"@types/lodash@4.17.15": + version "4.17.15" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.15.tgz#12d4af0ed17cc7600ce1f9980cec48fc17ad1e89" + integrity sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw== + "@types/minimatch@^3.0.3": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -3421,6 +3433,13 @@ dependencies: undici-types "~7.12.0" +"@types/node@20.16.3": + version "20.16.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.16.3.tgz#7b4f9a37091cf03a0c2561bf76a9a55f03f4f523" + integrity sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ== + dependencies: + undici-types "~6.19.2" + "@types/node@^22.0.0": version "22.18.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.18.8.tgz#738d9dafa38f6e0c467687c158f8e1ca2d7d8eaa" @@ -3495,6 +3514,11 @@ resolved "https://registry.yarnpkg.com/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz#51b1c00b516a5774ada5d611e65eb123f988ef8d" integrity sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA== +"@types/unist@3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + "@types/unist@^2.0.0", "@types/unist@^2.0.2": version "2.0.11" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" @@ -12195,6 +12219,11 @@ typescript@5.2.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== +typescript@5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + typescript@5.8.2: version "5.8.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4" @@ -12238,6 +12267,11 @@ underscore.string@~3.3.4: sprintf-js "^1.1.1" util-deprecate "^1.0.2" +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + undici-types@~6.21.0: version "6.21.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"