@tryghost/url-utils 4.4.14 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/UrlUtils.js CHANGED
@@ -23,6 +23,12 @@ module.exports = class UrlUtils {
23
23
  * @param {Object} [options.slugs] object with 2 properties reserved and protected containing arrays of special case slugs
24
24
  * @param {Number} [options.redirectCacheMaxAge]
25
25
  * @param {String} [options.staticImageUrlPrefix='content/images'] static prefix for serving images. Should not be passed in, unless customizing ghost instance image storage
26
+ * @param {String} [options.staticFilesUrlPrefix='content/files'] static prefix for serving files. Should not be passed in, unless customizing ghost instance file storage
27
+ * @param {String} [options.staticMediaUrlPrefix='content/media'] static prefix for serving media. Should not be passed in, unless customizing ghost instance media storage
28
+ * @param {object} [options.assetBaseUrls] asset CDN base URLs
29
+ * @param {string} [options.assetBaseUrls.image] image asset CDN base URL
30
+ * @param {string} [options.assetBaseUrls.files] files asset CDN base URL
31
+ * @param {string} [options.assetBaseUrls.media] media asset CDN base URL
26
32
  */
27
33
  constructor(options = {}) {
28
34
  const defaultOptions = {
@@ -30,16 +36,40 @@ module.exports = class UrlUtils {
30
36
  redirectCacheMaxAge: null,
31
37
  baseApiPath: '/ghost/api',
32
38
  defaultApiType: 'content',
33
- staticImageUrlPrefix: 'content/images'
39
+ staticImageUrlPrefix: 'content/images',
40
+ staticFilesUrlPrefix: 'content/files',
41
+ staticMediaUrlPrefix: 'content/media'
34
42
  };
35
43
 
36
44
  this._config = assignOptions({}, defaultOptions, options);
37
45
 
46
+ const assetBaseUrls = options.assetBaseUrls || {};
47
+ this._assetBaseUrls = {
48
+ image: assetBaseUrls.image || null,
49
+ files: assetBaseUrls.files || null,
50
+ media: assetBaseUrls.media || null
51
+ };
52
+
38
53
  this.getSubdir = options.getSubdir;
39
54
  this.getSiteUrl = options.getSiteUrl;
40
55
  this.getAdminUrl = options.getAdminUrl;
41
56
  }
42
57
 
58
+ _assetOptionDefaults() {
59
+ return {
60
+ staticImageUrlPrefix: this._config.staticImageUrlPrefix,
61
+ staticFilesUrlPrefix: this._config.staticFilesUrlPrefix,
62
+ staticMediaUrlPrefix: this._config.staticMediaUrlPrefix,
63
+ imageBaseUrl: this._assetBaseUrls.image || null,
64
+ filesBaseUrl: this._assetBaseUrls.files || null,
65
+ mediaBaseUrl: this._assetBaseUrls.media || null
66
+ };
67
+ }
68
+
69
+ _buildAssetOptions(additionalDefaults = {}, options) {
70
+ return assignOptions({}, this._assetOptionDefaults(), additionalDefaults, options || {});
71
+ }
72
+
43
73
  getProtectedSlugs() {
44
74
  let subDir = this.getSubdir();
45
75
 
@@ -234,23 +264,28 @@ module.exports = class UrlUtils {
234
264
  options = itemPath;
235
265
  itemPath = null;
236
266
  }
237
- return utils.toTransformReady(url, this.getSiteUrl(), itemPath, options);
267
+ const _options = this._buildAssetOptions({}, options);
268
+ return utils.toTransformReady(url, this.getSiteUrl(), itemPath, _options);
238
269
  }
239
270
 
240
271
  absoluteToTransformReady(url, options) {
241
- return utils.absoluteToTransformReady(url, this.getSiteUrl(), options);
272
+ const _options = this._buildAssetOptions({}, options);
273
+ return utils.absoluteToTransformReady(url, this.getSiteUrl(), _options);
242
274
  }
243
275
 
244
276
  relativeToTransformReady(url, options) {
245
- return utils.relativeToTransformReady(url, this.getSiteUrl(), options);
277
+ const _options = this._buildAssetOptions({}, options);
278
+ return utils.relativeToTransformReady(url, this.getSiteUrl(), _options);
246
279
  }
247
280
 
248
281
  transformReadyToAbsolute(url, options) {
249
- return utils.transformReadyToAbsolute(url, this.getSiteUrl(), options);
282
+ const _options = this._buildAssetOptions({}, options);
283
+ return utils.transformReadyToAbsolute(url, this.getSiteUrl(), _options);
250
284
  }
251
285
 
252
286
  transformReadyToRelative(url, options) {
253
- return utils.transformReadyToRelative(url, this.getSiteUrl(), options);
287
+ const _options = this._buildAssetOptions({}, options);
288
+ return utils.transformReadyToRelative(url, this.getSiteUrl(), _options);
254
289
  }
255
290
 
256
291
  htmlToTransformReady(html, itemPath, options) {
@@ -258,7 +293,8 @@ module.exports = class UrlUtils {
258
293
  options = itemPath;
259
294
  itemPath = null;
260
295
  }
261
- return utils.htmlToTransformReady(html, this.getSiteUrl(), itemPath, options);
296
+ const _options = this._buildAssetOptions({}, options);
297
+ return utils.htmlToTransformReady(html, this.getSiteUrl(), itemPath, _options);
262
298
  }
263
299
 
264
300
  /**
@@ -276,11 +312,9 @@ module.exports = class UrlUtils {
276
312
  options = itemPath;
277
313
  itemPath = null;
278
314
  }
279
- const defaultOptions = {
280
- assetsOnly: false,
281
- staticImageUrlPrefix: this._config.staticImageUrlPrefix
282
- };
283
- const _options = assignOptions({}, defaultOptions, options || {});
315
+ const _options = this._buildAssetOptions({
316
+ assetsOnly: false
317
+ }, options);
284
318
  return utils.htmlRelativeToAbsolute(html, this.getSiteUrl(), itemPath, _options);
285
319
  }
286
320
 
@@ -289,29 +323,23 @@ module.exports = class UrlUtils {
289
323
  options = itemPath;
290
324
  itemPath = null;
291
325
  }
292
- const defaultOptions = {
293
- assetsOnly: false,
294
- staticImageUrlPrefix: this._config.staticImageUrlPrefix
295
- };
296
- const _options = assignOptions({}, defaultOptions, options || {});
326
+ const _options = this._buildAssetOptions({
327
+ assetsOnly: false
328
+ }, options);
297
329
  return utils.htmlRelativeToTransformReady(html, this.getSiteUrl(), itemPath, _options);
298
330
  }
299
331
 
300
332
  htmlAbsoluteToRelative(html, options = {}) {
301
- const defaultOptions = {
302
- assetsOnly: false,
303
- staticImageUrlPrefix: this._config.staticImageUrlPrefix
304
- };
305
- const _options = assignOptions({}, defaultOptions, options);
333
+ const _options = this._buildAssetOptions({
334
+ assetsOnly: false
335
+ }, options);
306
336
  return utils.htmlAbsoluteToRelative(html, this.getSiteUrl(), _options);
307
337
  }
308
338
 
309
339
  htmlAbsoluteToTransformReady(html, options = {}) {
310
- const defaultOptions = {
311
- assetsOnly: false,
312
- staticImageUrlPrefix: this._config.staticImageUrlPrefix
313
- };
314
- const _options = assignOptions({}, defaultOptions, options);
340
+ const _options = this._buildAssetOptions({
341
+ assetsOnly: false
342
+ }, options);
315
343
  return utils.htmlAbsoluteToTransformReady(html, this.getSiteUrl(), _options);
316
344
  }
317
345
 
@@ -320,7 +348,8 @@ module.exports = class UrlUtils {
320
348
  options = itemPath;
321
349
  itemPath = null;
322
350
  }
323
- return utils.markdownToTransformReady(markdown, this.getSiteUrl(), itemPath, options);
351
+ const _options = this._buildAssetOptions({}, options);
352
+ return utils.markdownToTransformReady(markdown, this.getSiteUrl(), itemPath, _options);
324
353
  }
325
354
 
326
355
  markdownRelativeToAbsolute(markdown, itemPath, options) {
@@ -328,11 +357,9 @@ module.exports = class UrlUtils {
328
357
  options = itemPath;
329
358
  itemPath = null;
330
359
  }
331
- const defaultOptions = {
332
- assetsOnly: false,
333
- staticImageUrlPrefix: this._config.staticImageUrlPrefix
334
- };
335
- const _options = assignOptions({}, defaultOptions, options || {});
360
+ const _options = this._buildAssetOptions({
361
+ assetsOnly: false
362
+ }, options);
336
363
  return utils.markdownRelativeToAbsolute(markdown, this.getSiteUrl(), itemPath, _options);
337
364
  }
338
365
 
@@ -341,29 +368,23 @@ module.exports = class UrlUtils {
341
368
  options = itemPath;
342
369
  itemPath = null;
343
370
  }
344
- const defaultOptions = {
345
- assetsOnly: false,
346
- staticImageUrlPrefix: this._config.staticImageUrlPrefix
347
- };
348
- const _options = assignOptions({}, defaultOptions, options || {});
371
+ const _options = this._buildAssetOptions({
372
+ assetsOnly: false
373
+ }, options);
349
374
  return utils.markdownRelativeToTransformReady(markdown, this.getSiteUrl(), itemPath, _options);
350
375
  }
351
376
 
352
377
  markdownAbsoluteToRelative(markdown, options = {}) {
353
- const defaultOptions = {
354
- assetsOnly: false,
355
- staticImageUrlPrefix: this._config.staticImageUrlPrefix
356
- };
357
- const _options = assignOptions({}, defaultOptions, options);
378
+ const _options = this._buildAssetOptions({
379
+ assetsOnly: false
380
+ }, options);
358
381
  return utils.markdownAbsoluteToRelative(markdown, this.getSiteUrl(), _options);
359
382
  }
360
383
 
361
384
  markdownAbsoluteToTransformReady(markdown, options) {
362
- const defaultOptions = {
363
- assetsOnly: false,
364
- staticImageUrlPrefix: this._config.staticImageUrlPrefix
365
- };
366
- const _options = assignOptions({}, defaultOptions, options);
385
+ const _options = this._buildAssetOptions({
386
+ assetsOnly: false
387
+ }, options);
367
388
  return utils.markdownAbsoluteToTransformReady(markdown, this.getSiteUrl(), _options);
368
389
  }
369
390
 
@@ -372,10 +393,9 @@ module.exports = class UrlUtils {
372
393
  options = itemPath;
373
394
  itemPath = null;
374
395
  }
375
- const defaultOptions = {
396
+ const _options = this._buildAssetOptions({
376
397
  cardTransformers: this._config.cardTransformers
377
- };
378
- const _options = assignOptions({}, defaultOptions, options || {});
398
+ }, options);
379
399
  return utils.mobiledocToTransformReady(serializedMobiledoc, this.getSiteUrl(), itemPath, _options);
380
400
  }
381
401
 
@@ -384,12 +404,10 @@ module.exports = class UrlUtils {
384
404
  options = itemPath;
385
405
  itemPath = null;
386
406
  }
387
- const defaultOptions = {
407
+ const _options = this._buildAssetOptions({
388
408
  assetsOnly: false,
389
- staticImageUrlPrefix: this._config.staticImageUrlPrefix,
390
409
  cardTransformers: this._config.cardTransformers
391
- };
392
- const _options = assignOptions({}, defaultOptions, options || {});
410
+ }, options);
393
411
  return utils.mobiledocRelativeToAbsolute(serializedMobiledoc, this.getSiteUrl(), itemPath, _options);
394
412
  }
395
413
 
@@ -398,32 +416,26 @@ module.exports = class UrlUtils {
398
416
  options = itemPath;
399
417
  itemPath = null;
400
418
  }
401
- const defaultOptions = {
419
+ const _options = this._buildAssetOptions({
402
420
  assetsOnly: false,
403
- staticImageUrlPrefix: this._config.staticImageUrlPrefix,
404
421
  cardTransformers: this._config.cardTransformers
405
- };
406
- const _options = assignOptions({}, defaultOptions, options || {});
422
+ }, options);
407
423
  return utils.mobiledocRelativeToTransformReady(serializedMobiledoc, this.getSiteUrl(), itemPath, _options);
408
424
  }
409
425
 
410
426
  mobiledocAbsoluteToRelative(serializedMobiledoc, options = {}) {
411
- const defaultOptions = {
427
+ const _options = this._buildAssetOptions({
412
428
  assetsOnly: false,
413
- staticImageUrlPrefix: this._config.staticImageUrlPrefix,
414
429
  cardTransformers: this._config.cardTransformers
415
- };
416
- const _options = assignOptions({}, defaultOptions, options);
430
+ }, options);
417
431
  return utils.mobiledocAbsoluteToRelative(serializedMobiledoc, this.getSiteUrl(), _options);
418
432
  }
419
433
 
420
434
  mobiledocAbsoluteToTransformReady(serializedMobiledoc, options = {}) {
421
- const defaultOptions = {
435
+ const _options = this._buildAssetOptions({
422
436
  assetsOnly: false,
423
- staticImageUrlPrefix: this._config.staticImageUrlPrefix,
424
437
  cardTransformers: this._config.cardTransformers
425
- };
426
- const _options = assignOptions({}, defaultOptions, options);
438
+ }, options);
427
439
  return utils.mobiledocAbsoluteToTransformReady(serializedMobiledoc, this.getSiteUrl(), _options);
428
440
  }
429
441
 
@@ -432,10 +444,9 @@ module.exports = class UrlUtils {
432
444
  options = itemPath;
433
445
  itemPath = null;
434
446
  }
435
- const defaultOptions = {
447
+ const _options = this._buildAssetOptions({
436
448
  cardTransformers: this._config.cardTransformers
437
- };
438
- const _options = assignOptions({}, defaultOptions, options || {});
449
+ }, options);
439
450
  return utils.lexicalToTransformReady(serializedLexical, this.getSiteUrl(), itemPath, _options);
440
451
  }
441
452
 
@@ -444,12 +455,10 @@ module.exports = class UrlUtils {
444
455
  options = itemPath;
445
456
  itemPath = null;
446
457
  }
447
- const defaultOptions = {
458
+ const _options = this._buildAssetOptions({
448
459
  assetsOnly: false,
449
- staticImageUrlPrefix: this._config.staticImageUrlPrefix,
450
460
  cardTransformers: this._config.cardTransformers
451
- };
452
- const _options = assignOptions({}, defaultOptions, options || {});
461
+ }, options);
453
462
  return utils.lexicalRelativeToAbsolute(serializedLexical, this.getSiteUrl(), itemPath, _options);
454
463
  }
455
464
 
@@ -458,40 +467,31 @@ module.exports = class UrlUtils {
458
467
  options = itemPath;
459
468
  itemPath = null;
460
469
  }
461
- const defaultOptions = {
470
+ const _options = this._buildAssetOptions({
462
471
  assetsOnly: false,
463
- staticImageUrlPrefix: this._config.staticImageUrlPrefix,
464
472
  cardTransformers: this._config.cardTransformers
465
- };
466
- const _options = assignOptions({}, defaultOptions, options || {});
473
+ }, options);
467
474
  return utils.lexicalRelativeToTransformReady(serializedLexical, this.getSiteUrl(), itemPath, _options);
468
475
  }
469
476
 
470
477
  lexicalAbsoluteToRelative(serializedLexical, options = {}) {
471
- const defaultOptions = {
478
+ const _options = this._buildAssetOptions({
472
479
  assetsOnly: false,
473
- staticImageUrlPrefix: this._config.staticImageUrlPrefix,
474
480
  cardTransformers: this._config.cardTransformers
475
- };
476
- const _options = assignOptions({}, defaultOptions, options);
481
+ }, options);
477
482
  return utils.lexicalAbsoluteToRelative(serializedLexical, this.getSiteUrl(), _options);
478
483
  }
479
484
 
480
485
  lexicalAbsoluteToTransformReady(serializedLexical, options = {}) {
481
- const defaultOptions = {
486
+ const _options = this._buildAssetOptions({
482
487
  assetsOnly: false,
483
- staticImageUrlPrefix: this._config.staticImageUrlPrefix,
484
488
  cardTransformers: this._config.cardTransformers
485
- };
486
- const _options = assignOptions({}, defaultOptions, options);
489
+ }, options);
487
490
  return utils.lexicalAbsoluteToTransformReady(serializedLexical, this.getSiteUrl(), _options);
488
491
  }
489
492
 
490
493
  plaintextToTransformReady(plaintext, options = {}) {
491
- const defaultOptions = {
492
- staticImageUrlPrefix: this._config.staticImageUrlPrefix
493
- };
494
- const _options = assignOptions({}, defaultOptions, options);
494
+ const _options = this._buildAssetOptions({}, options);
495
495
  return utils.plaintextToTransformReady(plaintext, this.getSiteUrl(), _options);
496
496
  }
497
497
 
@@ -537,6 +537,14 @@ module.exports = class UrlUtils {
537
537
  return this._config.staticImageUrlPrefix;
538
538
  }
539
539
 
540
+ get STATIC_FILES_URL_PREFIX() {
541
+ return this._config.staticFilesUrlPrefix;
542
+ }
543
+
544
+ get STATIC_MEDIA_URL_PREFIX() {
545
+ return this._config.staticMediaUrlPrefix;
546
+ }
547
+
540
548
  // expose underlying functions to ease testing
541
549
  get _utils() {
542
550
  return utils;
@@ -1,39 +1,68 @@
1
+ const {URL} = require('url');
1
2
  const absoluteToRelative = require('./absolute-to-relative');
2
3
 
3
- const absoluteToTransformReady = function (url, root, _options) {
4
+ function isRelative(url) {
5
+ let parsedInput;
6
+ try {
7
+ parsedInput = new URL(url, 'http://relative');
8
+ } catch (e) {
9
+ // url was unparseable
10
+ return false;
11
+ }
12
+
13
+ return parsedInput.origin === 'http://relative';
14
+ }
15
+
16
+ const absoluteToTransformReady = function (url, root, _options = {}) {
4
17
  const defaultOptions = {
5
18
  replacementStr: '__GHOST_URL__',
6
- withoutSubdirectory: true
19
+ withoutSubdirectory: true,
20
+ staticImageUrlPrefix: 'content/images',
21
+ staticFilesUrlPrefix: 'content/files',
22
+ staticMediaUrlPrefix: 'content/media',
23
+ imageBaseUrl: null,
24
+ filesBaseUrl: null,
25
+ mediaBaseUrl: null
7
26
  };
8
27
  const options = Object.assign({}, defaultOptions, _options);
9
28
 
10
- // return relative urls as-is
11
- try {
12
- const parsedURL = new URL(url, 'http://relative');
13
- if (parsedURL.origin === 'http://relative') {
14
- return url;
15
- }
16
- } catch (e) {
17
- // url was unparseable
29
+ if (isRelative(url)) {
18
30
  return url;
19
31
  }
20
32
 
21
33
  // convert to relative with stripped subdir
22
34
  // always returns root-relative starting with forward slash
23
- const relativeUrl = absoluteToRelative(url, root, options);
35
+ const rootRelativeUrl = absoluteToRelative(url, root, options);
24
36
 
25
- // return still absolute urls as-is (eg. external site, mailto, etc)
26
- try {
27
- const parsedURL = new URL(relativeUrl, 'http://relative');
28
- if (parsedURL.origin !== 'http://relative') {
29
- return url;
37
+ if (isRelative(rootRelativeUrl)) {
38
+ return `${options.replacementStr}${rootRelativeUrl}`;
39
+ }
40
+
41
+ if (options.mediaBaseUrl) {
42
+ const mediaRelativeUrl = absoluteToRelative(url, options.mediaBaseUrl, options);
43
+
44
+ if (isRelative(mediaRelativeUrl)) {
45
+ return `${options.replacementStr}${mediaRelativeUrl}`;
46
+ }
47
+ }
48
+
49
+ if (options.filesBaseUrl) {
50
+ const filesRelativeUrl = absoluteToRelative(url, options.filesBaseUrl, options);
51
+
52
+ if (isRelative(filesRelativeUrl)) {
53
+ return `${options.replacementStr}${filesRelativeUrl}`;
54
+ }
55
+ }
56
+
57
+ if (options.imageBaseUrl) {
58
+ const imageRelativeUrl = absoluteToRelative(url, options.imageBaseUrl, options);
59
+
60
+ if (isRelative(imageRelativeUrl)) {
61
+ return `${options.replacementStr}${imageRelativeUrl}`;
30
62
  }
31
- } catch (e) {
32
- // url was unparseable
33
- return url;
34
63
  }
35
64
 
36
- return `${options.replacementStr}${relativeUrl}`;
65
+ return url;
37
66
  };
38
67
 
39
68
  module.exports = absoluteToTransformReady;
@@ -0,0 +1,42 @@
1
+ function escapeRegExp(string) {
2
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3
+ }
4
+
5
+ /**
6
+ * Build a regex pattern that matches any of the configured base URLs (site URL + CDN URLs).
7
+ * This is used for early exit optimizations - if content doesn't contain any of these URLs,
8
+ * we can skip expensive parsing.
9
+ *
10
+ * @param {string} siteUrl - The site's base URL
11
+ * @param {Object} options - Options containing CDN base URLs
12
+ * @param {string} [options.imageBaseUrl] - CDN base URL for images
13
+ * @param {string} [options.filesBaseUrl] - CDN base URL for files
14
+ * @param {string} [options.mediaBaseUrl] - CDN base URL for media
15
+ * @param {boolean} [options.ignoreProtocol=true] - Whether to strip protocol from URLs
16
+ * @returns {string|null} Regex pattern matching any configured base URL, or null if none configured
17
+ */
18
+ function buildEarlyExitMatch(siteUrl, options = {}) {
19
+ const candidates = [siteUrl, options.imageBaseUrl, options.filesBaseUrl, options.mediaBaseUrl]
20
+ .filter(Boolean)
21
+ .map((value) => {
22
+ let normalized = options.ignoreProtocol ? value.replace(/http:|https:/, '') : value;
23
+ return normalized.replace(/\/$/, '');
24
+ })
25
+ .filter(Boolean)
26
+ .map(escapeRegExp);
27
+
28
+ if (!candidates.length) {
29
+ return null;
30
+ }
31
+
32
+ if (candidates.length === 1) {
33
+ return candidates[0];
34
+ }
35
+
36
+ return `(?:${candidates.join('|')})`;
37
+ }
38
+
39
+ module.exports = {
40
+ buildEarlyExitMatch,
41
+ escapeRegExp
42
+ };
@@ -1,5 +1,8 @@
1
1
  function deduplicateDoubleSlashes(url) {
2
- return url.replace(/\/\//g, '/');
2
+ // Preserve protocol slashes (e.g., http://, https://) and only deduplicate
3
+ // slashes in the path portion. The pattern (^|[^:])\/\/+ matches double slashes
4
+ // that are either at the start of the string or not preceded by a colon.
5
+ return url.replace(/(^|[^:])\/\/+/g, '$1/');
3
6
  }
4
7
 
5
8
  module.exports = deduplicateDoubleSlashes;
@@ -1,13 +1,13 @@
1
1
  const htmlTransform = require('./html-transform');
2
2
  const absoluteToTransformReady = require('./absolute-to-transform-ready');
3
+ const {buildEarlyExitMatch} = require('./build-early-exit-match');
3
4
 
4
5
  const htmlAbsoluteToTransformReady = function (html = '', siteUrl, _options) {
5
6
  const defaultOptions = {assetsOnly: false, ignoreProtocol: true};
6
7
  const options = Object.assign({}, defaultOptions, _options || {});
7
8
 
8
- // exit early and avoid parsing if the content does not contain the siteUrl
9
- options.earlyExitMatchStr = options.ignoreProtocol ? siteUrl.replace(/http:|https:/, '') : siteUrl;
10
- options.earlyExitMatchStr = options.earlyExitMatchStr.replace(/\/$/, '');
9
+ // exit early and avoid parsing if the content does not contain the siteUrl or configured asset bases
10
+ options.earlyExitMatchStr = buildEarlyExitMatch(siteUrl, options);
11
11
 
12
12
  // need to ignore itemPath because absoluteToRelative doesn't take that option
13
13
  const transformFunction = function (_url, _siteUrl, _itemPath, __options) {
@@ -1,13 +1,13 @@
1
1
  const markdownTransform = require('./markdown-transform');
2
2
  const absoluteToTransformReady = require('./absolute-to-transform-ready');
3
3
  const htmlAbsoluteToTransformReady = require('./html-absolute-to-transform-ready');
4
+ const {buildEarlyExitMatch} = require('./build-early-exit-match');
4
5
 
5
6
  function markdownAbsoluteToTransformReady(markdown = '', siteUrl, _options = {}) {
6
7
  const defaultOptions = {assetsOnly: false, ignoreProtocol: true};
7
8
  const options = Object.assign({}, defaultOptions, _options);
8
9
 
9
- options.earlyExitMatchStr = options.ignoreProtocol ? siteUrl.replace(/http:|https:/, '') : siteUrl;
10
- options.earlyExitMatchStr = options.earlyExitMatchStr.replace(/\/$/, '');
10
+ options.earlyExitMatchStr = buildEarlyExitMatch(siteUrl, options);
11
11
 
12
12
  // need to ignore itemPath because absoluteToTransformReady functions doen't take that option
13
13
  const transformFunctions = {
@@ -1,7 +1,23 @@
1
1
  const absoluteToTransformReady = require('./absolute-to-transform-ready');
2
+ const {escapeRegExp} = require('./build-early-exit-match');
2
3
 
3
- function escapeRegExp(string) {
4
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4
+ function buildLinkRegex(rootUrl, options = {}) {
5
+ // Build a regex that matches links from ANY configured base URL (site + CDNs)
6
+ const baseUrls = [rootUrl, options.imageBaseUrl, options.filesBaseUrl, options.mediaBaseUrl]
7
+ .filter(Boolean);
8
+
9
+ const patterns = baseUrls.map((baseUrl) => {
10
+ const parsed = new URL(baseUrl);
11
+ const escapedUrl = escapeRegExp(`${parsed.hostname}${parsed.pathname.replace(/\/$/, '')}`);
12
+ return escapedUrl;
13
+ });
14
+
15
+ if (!patterns.length) {
16
+ return null;
17
+ }
18
+
19
+ const pattern = patterns.length === 1 ? patterns[0] : `(?:${patterns.join('|')})`;
20
+ return new RegExp(` \\[(https?://${pattern}.*?)\\]`, 'g');
5
21
  }
6
22
 
7
23
  const plaintextAbsoluteToTransformReady = function plaintextAbsoluteToTransformReady(plaintext, rootUrl, itemPath, options) {
@@ -13,9 +29,7 @@ const plaintextAbsoluteToTransformReady = function plaintextAbsoluteToTransformR
13
29
 
14
30
  // plaintext links look like "Link title [url]"
15
31
  // those links are all we care about so we can do a fast regex here
16
- const rootURL = new URL(rootUrl);
17
- const escapedRootUrl = escapeRegExp(`${rootURL.hostname}${rootURL.pathname.replace(/\/$/, '')}`);
18
- const linkRegex = new RegExp(` \\[(https?://${escapedRootUrl}.*?)\\]`, 'g');
32
+ const linkRegex = buildLinkRegex(rootUrl, options);
19
33
 
20
34
  return plaintext.replace(linkRegex, function (fullMatch, url) {
21
35
  const newUrl = absoluteToTransformReady(`${url}`, rootUrl, options);
@@ -4,7 +4,13 @@ function escapeRegExp(string) {
4
4
 
5
5
  const transformReadyToAbsolute = function (str = '', root, _options = {}) {
6
6
  const defaultOptions = {
7
- replacementStr: '__GHOST_URL__'
7
+ replacementStr: '__GHOST_URL__',
8
+ staticImageUrlPrefix: 'content/images',
9
+ staticFilesUrlPrefix: 'content/files',
10
+ staticMediaUrlPrefix: 'content/media',
11
+ imageBaseUrl: null,
12
+ filesBaseUrl: null,
13
+ mediaBaseUrl: null
8
14
  };
9
15
  const options = Object.assign({}, defaultOptions, _options);
10
16
 
@@ -14,7 +20,23 @@ const transformReadyToAbsolute = function (str = '', root, _options = {}) {
14
20
 
15
21
  const replacementRegex = new RegExp(escapeRegExp(options.replacementStr), 'g');
16
22
 
17
- return str.replace(replacementRegex, root.replace(/\/$/, ''));
23
+ return str.replace(replacementRegex, (match, offset) => {
24
+ const remainder = str.slice(offset + match.length);
25
+
26
+ if (remainder.startsWith(`/${options.staticMediaUrlPrefix}`) && options.mediaBaseUrl) {
27
+ return options.mediaBaseUrl.replace(/\/$/, '');
28
+ }
29
+
30
+ if (remainder.startsWith(`/${options.staticFilesUrlPrefix}`) && options.filesBaseUrl) {
31
+ return options.filesBaseUrl.replace(/\/$/, '');
32
+ }
33
+
34
+ if (remainder.startsWith(`/${options.staticImageUrlPrefix}`) && options.imageBaseUrl) {
35
+ return options.imageBaseUrl.replace(/\/$/, '');
36
+ }
37
+
38
+ return root.replace(/\/$/, '');
39
+ });
18
40
  };
19
41
 
20
42
  module.exports = transformReadyToAbsolute;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tryghost/url-utils",
3
- "version": "4.4.14",
3
+ "version": "4.5.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/TryGhost/SDK.git",
@@ -23,10 +23,10 @@
23
23
  "access": "public"
24
24
  },
25
25
  "devDependencies": {
26
- "@tryghost/config-url-helpers": "^1.0.17",
26
+ "@tryghost/config-url-helpers": "^1.0.18",
27
27
  "c8": "10.1.3",
28
- "mocha": "11.2.2",
29
- "rewire": "8.0.0",
28
+ "mocha": "11.7.5",
29
+ "rewire": "9.0.1",
30
30
  "should": "13.2.3",
31
31
  "sinon": "21.0.0"
32
32
  },
@@ -38,6 +38,5 @@
38
38
  "remark": "^11.0.2",
39
39
  "remark-footnotes": "^1.0.0",
40
40
  "unist-util-visit": "^2.0.0"
41
- },
42
- "gitHead": "b10773947244536b8829fec0540819990c901987"
41
+ }
43
42
  }