@tryghost/url-utils 4.4.15 → 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 +98 -90
- package/lib/utils/absolute-to-transform-ready.js +49 -20
- package/lib/utils/build-early-exit-match.js +42 -0
- package/lib/utils/deduplicate-double-slashes.js +4 -1
- package/lib/utils/html-absolute-to-transform-ready.js +3 -3
- package/lib/utils/markdown-absolute-to-transform-ready.js +2 -2
- package/lib/utils/plaintext-absolute-to-transform-ready.js +19 -5
- package/lib/utils/transform-ready-to-absolute.js +24 -2
- package/package.json +5 -6
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
|
-
|
|
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
|
-
|
|
272
|
+
const _options = this._buildAssetOptions({}, options);
|
|
273
|
+
return utils.absoluteToTransformReady(url, this.getSiteUrl(), _options);
|
|
242
274
|
}
|
|
243
275
|
|
|
244
276
|
relativeToTransformReady(url, options) {
|
|
245
|
-
|
|
277
|
+
const _options = this._buildAssetOptions({}, options);
|
|
278
|
+
return utils.relativeToTransformReady(url, this.getSiteUrl(), _options);
|
|
246
279
|
}
|
|
247
280
|
|
|
248
281
|
transformReadyToAbsolute(url, options) {
|
|
249
|
-
|
|
282
|
+
const _options = this._buildAssetOptions({}, options);
|
|
283
|
+
return utils.transformReadyToAbsolute(url, this.getSiteUrl(), _options);
|
|
250
284
|
}
|
|
251
285
|
|
|
252
286
|
transformReadyToRelative(url, options) {
|
|
253
|
-
|
|
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
|
-
|
|
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
|
|
280
|
-
assetsOnly: false
|
|
281
|
-
|
|
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
|
|
293
|
-
assetsOnly: false
|
|
294
|
-
|
|
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
|
|
302
|
-
assetsOnly: false
|
|
303
|
-
|
|
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
|
|
311
|
-
assetsOnly: false
|
|
312
|
-
|
|
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
|
-
|
|
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
|
|
332
|
-
assetsOnly: false
|
|
333
|
-
|
|
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
|
|
345
|
-
assetsOnly: false
|
|
346
|
-
|
|
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
|
|
354
|
-
assetsOnly: false
|
|
355
|
-
|
|
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
|
|
363
|
-
assetsOnly: false
|
|
364
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
35
|
+
const rootRelativeUrl = absoluteToRelative(url, root, options);
|
|
24
36
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
4
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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.
|
|
26
|
+
"@tryghost/config-url-helpers": "^1.0.18",
|
|
27
27
|
"c8": "10.1.3",
|
|
28
|
-
"mocha": "11.
|
|
29
|
-
"rewire": "9.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": "860707f032864400017b0f176cbabf181f99b764"
|
|
41
|
+
}
|
|
43
42
|
}
|