Skip to content

Commit 2154b26

Browse files
committed
✨ Added URL transform support for lexical card property arrays and nested object paths
closes TryGhost/Product#2722 Cards can have complex objects in their data properties that need URLs transformed. This commit adds support for handling deeply nested arrays of objects and paths. Example card dataset : ``` { images: [{ src: '/image.png', sizes: { small: {src: '/image-small.png'}, large: {src: '/image-large.png'} } }] } ``` and the corresponding transform map: ``` urlTransformMap() { return { images: [{ src: 'url', sizes: { 'small.src': 'url', 'large.src': 'url' } }] } } ```
1 parent fdf312f commit 2154b26

File tree

5 files changed

+928
-9
lines changed

5 files changed

+928
-9
lines changed

packages/url-utils/lib/utils/_lexical-transform.js

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
const _ = require('lodash');
2+
3+
// options.transformMap = {
4+
// relativeToAbsolute: {
5+
// url: (url, siteUrl, itemPath, options) => 'transformedUrl',
6+
// html: (html, siteUrl, itemPath, options) => 'transformedHtml',
7+
// }
8+
// }
9+
// options.transformType = 'relativeToAbsolute'
10+
111
function lexicalTransform(serializedLexical, siteUrl, transformFunction, itemPath, _options = {}) {
212
const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}};
313
const options = Object.assign({}, defaultOptions, _options, {siteUrl, itemPath});
@@ -14,21 +24,42 @@ function lexicalTransform(serializedLexical, siteUrl, transformFunction, itemPat
1424
return serializedLexical;
1525
}
1626

27+
// create a map of node types to urlTransformMap objects
28+
// e.g. {'image': {src: 'url', caption: 'html'}
1729
const nodeMap = new Map();
1830
options.nodes.forEach(node => node.urlTransformMap && nodeMap.set(node.getType(), node.urlTransformMap));
1931

32+
const transformProperty = function (obj, propertyPath, transform) {
33+
const propertyValue = _.get(obj, propertyPath);
34+
35+
if (Array.isArray(propertyValue)) {
36+
propertyValue.forEach((item) => {
37+
// arrays of objects need to be defined as a nested object in the urlTransformMap
38+
// so the `transform` value is that nested object
39+
Object.entries(transform).forEach(([itemPropertyPath, itemTransform]) => {
40+
transformProperty(item, itemPropertyPath, itemTransform);
41+
});
42+
});
43+
44+
return;
45+
}
46+
47+
if (propertyValue) {
48+
_.set(obj, propertyPath, options.transformMap[options.transformType][transform](propertyValue));
49+
}
50+
};
51+
52+
// recursively walk the Lexical node tree transforming any card data properties and links
2053
const transformChildren = function (children) {
2154
for (const child of children) {
22-
// cards (nodes) have a `type` attribute in their node data
23-
if (child.type && nodeMap.has(child.type)) {
24-
Object.entries(nodeMap.get(child.type)).forEach(([property, transform]) => {
25-
if (child[property]) {
26-
child[property] = options.transformMap[options.transformType][transform](child[property]);
27-
}
55+
const isCard = child.type && nodeMap.has(child.type);
56+
const isLink = !!child.url;
57+
58+
if (isCard) {
59+
Object.entries(nodeMap.get(child.type)).forEach(([propertyPath, transform]) => {
60+
transformProperty(child, propertyPath, transform);
2861
});
29-
} else if (child.url) {
30-
// any lexical links will be a child object with a `url` attribute,
31-
// recursively walk the tree transforming any `.url`s
62+
} else if (isLink) {
3263
child.url = transformFunction(child.url, siteUrl, itemPath, options);
3364
}
3465

packages/url-utils/test/unit/utils/lexical-absolute-to-relative.test.js

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,228 @@ describe('utils: lexicalAbsoluteToRelative()', function () {
389389
result.root.children[0].caption.should.equal('Captions are HTML with only <a href="/image-caption-link">links transformed</a> - this is a plaintext url: http://my-ghost-blog.com/plaintext-url');
390390
});
391391

392+
it('handles cards with array properties', function () {
393+
const urlUtils = new UrlUtils({
394+
getSubdir: function () {
395+
return '';
396+
},
397+
getSiteUrl: function () {
398+
return siteUrl;
399+
}
400+
});
401+
402+
Object.assign(options, {
403+
nodes: [
404+
class GalleryNode {
405+
static getType() {
406+
return 'gallery';
407+
}
408+
409+
static get urlTransformMap() {
410+
return {
411+
images: {src: 'url'}
412+
};
413+
}
414+
}
415+
],
416+
transformMap: {
417+
absoluteToRelative: {
418+
url: urlUtils.absoluteToRelative.bind(urlUtils),
419+
html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils)
420+
}
421+
}
422+
});
423+
424+
const lexical = JSON.stringify({
425+
root: {
426+
children: [{
427+
type: 'gallery',
428+
images: [
429+
{src: 'http://my-ghost-blog.com/image1.png'},
430+
{src: 'http://my-ghost-blog.com/image2.png'}
431+
]
432+
}]
433+
}
434+
});
435+
436+
const serializedResult = lexicalAbsoluteToRelative(lexical, siteUrl, options);
437+
const result = JSON.parse(serializedResult);
438+
439+
result.root.children[0].images[0].src.should.equal('/image1.png');
440+
result.root.children[0].images[1].src.should.equal('/image2.png');
441+
});
442+
443+
it('handles cards with deeply nested properties', function () {
444+
const urlUtils = new UrlUtils({
445+
getSubdir: function () {
446+
return '';
447+
},
448+
getSiteUrl: function () {
449+
return siteUrl;
450+
}
451+
});
452+
453+
Object.assign(options, {
454+
nodes: [
455+
class TestNode {
456+
static getType() {
457+
return 'test';
458+
}
459+
460+
static get urlTransformMap() {
461+
return {
462+
'meta.image.src': 'url'
463+
};
464+
}
465+
}
466+
],
467+
transformMap: {
468+
absoluteToRelative: {
469+
url: urlUtils.absoluteToRelative.bind(urlUtils),
470+
html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils)
471+
}
472+
}
473+
});
474+
475+
const lexical = JSON.stringify({
476+
root: {
477+
children: [{
478+
type: 'test',
479+
meta: {
480+
image: {
481+
src: 'http://my-ghost-blog.com/image.png'
482+
}
483+
}
484+
}]
485+
}
486+
});
487+
488+
const serializedResult = lexicalAbsoluteToRelative(lexical, siteUrl, options);
489+
const result = JSON.parse(serializedResult);
490+
491+
result.root.children[0].meta.image.src.should.equal('/image.png');
492+
});
493+
494+
it('handles cards with arrays of deeply nested properties', function () {
495+
const urlUtils = new UrlUtils({
496+
getSubdir: function () {
497+
return '';
498+
},
499+
getSiteUrl: function () {
500+
return siteUrl;
501+
}
502+
});
503+
504+
Object.assign(options, {
505+
nodes: [
506+
class GalleryNode {
507+
static getType() {
508+
return 'gallery';
509+
}
510+
511+
static get urlTransformMap() {
512+
return {
513+
images: {
514+
'srcs.main': 'url'
515+
}
516+
};
517+
}
518+
}
519+
],
520+
transformMap: {
521+
absoluteToRelative: {
522+
url: urlUtils.absoluteToRelative.bind(urlUtils),
523+
html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils)
524+
}
525+
}
526+
});
527+
528+
const lexical = JSON.stringify({
529+
root: {
530+
children: [{
531+
type: 'gallery',
532+
images: [
533+
{srcs: {main: 'http://my-ghost-blog.com/image1.png'}},
534+
{srcs: {main: 'http://my-ghost-blog.com/image2.png'}}
535+
]
536+
}]
537+
}
538+
});
539+
540+
const serializedResult = lexicalAbsoluteToRelative(lexical, siteUrl, options);
541+
const result = JSON.parse(serializedResult);
542+
543+
result.root.children[0].images[0].srcs.main.should.equal('/image1.png');
544+
result.root.children[0].images[1].srcs.main.should.equal('/image2.png');
545+
});
546+
547+
it('handles cards with arrays of arrays', function () {
548+
const urlUtils = new UrlUtils({
549+
getSubdir: function () {
550+
return '';
551+
},
552+
getSiteUrl: function () {
553+
return siteUrl;
554+
}
555+
});
556+
557+
Object.assign(options, {
558+
nodes: [
559+
class GalleryNode {
560+
static getType() {
561+
return 'gallery';
562+
}
563+
564+
static get urlTransformMap() {
565+
return {
566+
images: {
567+
sizes: {
568+
src: 'url'
569+
}
570+
}
571+
};
572+
}
573+
}
574+
],
575+
transformMap: {
576+
absoluteToRelative: {
577+
url: urlUtils.absoluteToRelative.bind(urlUtils),
578+
html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils)
579+
}
580+
}
581+
});
582+
583+
const lexical = JSON.stringify({
584+
root: {
585+
children: [{
586+
type: 'gallery',
587+
images: [
588+
{
589+
sizes: [
590+
{src: 'http://my-ghost-blog.com/image1.png'},
591+
{src: 'http://my-ghost-blog.com/image2.png'}
592+
]
593+
},
594+
{
595+
sizes: [
596+
{src: 'http://my-ghost-blog.com/image3.png'},
597+
{src: 'http://my-ghost-blog.com/image4.png'}
598+
]
599+
}
600+
]
601+
}]
602+
}
603+
});
604+
605+
const serializedResult = lexicalAbsoluteToRelative(lexical, siteUrl, options);
606+
const result = JSON.parse(serializedResult);
607+
608+
result.root.children[0].images[0].sizes[0].src.should.equal('/image1.png');
609+
result.root.children[0].images[0].sizes[1].src.should.equal('/image2.png');
610+
result.root.children[0].images[1].sizes[0].src.should.equal('/image3.png');
611+
result.root.children[0].images[1].sizes[1].src.should.equal('/image4.png');
612+
});
613+
392614
it('does not transform unknown cards', function () {
393615
const urlUtils = new UrlUtils({
394616
getSubdir: function () {

0 commit comments

Comments
 (0)