Skip to content

Commit 2cce002

Browse files
committed
Added node (card) support to lexical URL transforms
refs TryGhost/Product#2225 - each Lexical node (representing a Ghost card) can have multiple data properties that need URLs transforming in different ways (plain url, html, markdown) - Ghost will pass `nodes` and `transformMap` options through to the lexical transform utilities - `nodes` is an array of node classes - `transformMap` is two-level object, with top-level keys representing the three transform types we support (`toTransformReady`, `absoluteToRelative`, `relativeToAbsolute`), and the second level keys representing the data type (`url`, `html`, `markdown`) with the value for each being a function that takes a url argument and transforms it as necessary - when transforming lexical content, we match any serialized node to one of the loaded nodes and use that node's `urlTransformMap` property to call the right transform method for each data property
1 parent 919dd8e commit 2cce002

9 files changed

+546
-8
lines changed

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
function lexicalTransform(serializedLexical, siteUrl, transformFunction, itemPath, _options = {}) {
2-
const defaultOptions = {assetsOnly: false, secure: false};
2+
const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}};
33
const options = Object.assign({}, defaultOptions, _options, {siteUrl, itemPath});
44

55
if (!serializedLexical) {
@@ -14,11 +14,21 @@ function lexicalTransform(serializedLexical, siteUrl, transformFunction, itemPat
1414
return serializedLexical;
1515
}
1616

17-
// any lexical links will be a child object with a `url` attribute,
18-
// recursively walk the tree transforming any `.url`s
17+
const nodeMap = new Map();
18+
options.nodes.forEach(node => node.urlTransformMap && nodeMap.set(node.getType(), node.urlTransformMap));
19+
1920
const transformChildren = function (children) {
2021
for (const child of children) {
21-
if (child.url) {
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+
}
28+
});
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
2232
child.url = transformFunction(child.url, siteUrl, itemPath, options);
2333
}
2434

packages/url-utils/lib/utils/lexical-absolute-to-relative.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const absoluteToRelative = require('./absolute-to-relative');
22
const lexicalTransform = require('./_lexical-transform');
33

44
function lexicalAbsoluteToRelative(serializedLexical, siteUrl, _options = {}) {
5-
const defaultOptions = {assetsOnly: false, secure: false, cardTransformers: []};
5+
const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}};
66
const overrideOptions = {siteUrl, transformType: 'absoluteToRelative'};
77
const options = Object.assign({}, defaultOptions, _options, overrideOptions);
88

packages/url-utils/lib/utils/lexical-absolute-to-transform-ready.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const absoluteToTransformReady = require('./absolute-to-transform-ready');
22
const lexicalTransform = require('./_lexical-transform');
33

44
function lexicalAbsoluteToRelative(serializedLexical, siteUrl, _options = {}) {
5-
const defaultOptions = {assetsOnly: false, secure: false, cardTransformers: []};
5+
const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}};
66
const overrideOptions = {siteUrl, transformType: 'toTransformReady'};
77
const options = Object.assign({}, defaultOptions, _options, overrideOptions);
88

packages/url-utils/lib/utils/lexical-relative-to-absolute.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const relativeToAbsolute = require('./relative-to-absolute');
22
const lexicalTransform = require('./_lexical-transform');
33

44
function lexicalRelativeToAbsolute(serializedLexical, siteUrl, itemPath, _options = {}) {
5-
const defaultOptions = {assetsOnly: false, secure: false, cardTransformers: []};
5+
const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}};
66
const overrideOptions = {siteUrl, itemPath, transformType: 'relativeToAbsolute'};
77
const options = Object.assign({}, defaultOptions, _options, overrideOptions);
88

packages/url-utils/lib/utils/lexical-relative-to-transform-ready.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const relativeToTransformReady = require('./relative-to-transform-ready');
22
const lexicalTransform = require('./_lexical-transform');
33

44
function lexicalRelativeToTransformReady(serializedLexical, siteUrl, itemPath, _options = {}) {
5-
const defaultOptions = {assetsOnly: false, secure: false, cardTransformers: []};
5+
const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}};
66
const overrideOptions = {siteUrl, transformType: 'toTransformReady'};
77
const options = Object.assign({}, defaultOptions, _options, overrideOptions);
88

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

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// const testUtils = require('./utils');
33
require('../../utils');
44

5+
const UrlUtils = require('../../../lib/url-utils');
56
const lexicalAbsoluteToRelative = require('../../../lib/utils/lexical-absolute-to-relative');
67

78
describe('utils: lexicalAbsoluteToRelative()', function () {
@@ -339,4 +340,135 @@ describe('utils: lexicalAbsoluteToRelative()', function () {
339340
result.root.children[1].url.should.equal('i%20don’t%20believe%20that%20our%20platform%20should%20take%20that%20down%20because%20i%20think%20there%20are%20things%20that%20different%20people%20get%20wrong');
340341
result.root.children[2].url.should.equal('/sanity-check');
341342
});
343+
344+
it('handles cards', function () {
345+
const urlUtils = new UrlUtils({
346+
getSubdir: function () {
347+
return '';
348+
},
349+
getSiteUrl: function () {
350+
return siteUrl;
351+
}
352+
});
353+
354+
Object.assign(options, {
355+
nodes: [
356+
class ImageNode {
357+
static getType() {
358+
return 'image';
359+
}
360+
361+
static get urlTransformMap() {
362+
return {
363+
src: 'url',
364+
caption: 'html'
365+
};
366+
}
367+
}
368+
],
369+
transformMap: {
370+
absoluteToRelative: {
371+
url: urlUtils.absoluteToRelative.bind(urlUtils),
372+
html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils)
373+
}
374+
}
375+
});
376+
377+
const lexical = JSON.stringify({
378+
root: {
379+
children: [
380+
{type: 'image', src: 'http://my-ghost-blog.com/image.png', caption: 'Captions are HTML with only <a href="http://my-ghost-blog.com/image-caption-link">links transformed</a> - this is a plaintext url: http://my-ghost-blog.com/plaintext-url'}
381+
]
382+
}
383+
});
384+
385+
const serializedResult = lexicalAbsoluteToRelative(lexical, siteUrl, options);
386+
const result = JSON.parse(serializedResult);
387+
388+
result.root.children[0].src.should.equal('/image.png');
389+
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');
390+
});
391+
392+
it('does not transform unknown cards', 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+
transformMap: {
405+
absoluteToRelative: {
406+
url: urlUtils.absoluteToRelative.bind(urlUtils),
407+
html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils)
408+
}
409+
}
410+
});
411+
412+
const lexical = JSON.stringify({
413+
root: {
414+
children: [
415+
{type: 'image', src: 'http://my-ghost-blog.com/image.png', caption: 'Captions are HTML with only <a href="http://my-ghost-blog.com/image-caption-link">links transformed</a> - this is a plaintext url: http://my-ghost-blog.com/plaintext-url'}
416+
]
417+
}
418+
});
419+
420+
const serializedResult = lexicalAbsoluteToRelative(lexical, siteUrl, options);
421+
const result = JSON.parse(serializedResult);
422+
423+
result.root.children[0].src.should.equal('http://my-ghost-blog.com/image.png');
424+
result.root.children[0].caption.should.equal('Captions are HTML with only <a href="http://my-ghost-blog.com/image-caption-link">links transformed</a> - this is a plaintext url: http://my-ghost-blog.com/plaintext-url');
425+
});
426+
427+
it('does not transform unknown card properties', function () {
428+
const urlUtils = new UrlUtils({
429+
getSubdir: function () {
430+
return '';
431+
},
432+
getSiteUrl: function () {
433+
return siteUrl;
434+
}
435+
});
436+
437+
Object.assign(options, {
438+
nodes: [
439+
class ImageNode {
440+
static getType() {
441+
return 'image';
442+
}
443+
444+
static get urlTransformMap() {
445+
return {
446+
src: 'url',
447+
caption: 'html'
448+
};
449+
}
450+
}
451+
],
452+
transformMap: {
453+
absoluteToRelative: {
454+
url: urlUtils.absoluteToRelative.bind(urlUtils),
455+
html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils)
456+
}
457+
}
458+
});
459+
460+
const lexical = JSON.stringify({
461+
root: {
462+
children: [
463+
{type: 'image', src: 'http://my-ghost-blog.com/image.png', other: 'http://my-ghost-blog.com/unknown-card-property'}
464+
]
465+
}
466+
});
467+
468+
const serializedResult = lexicalAbsoluteToRelative(lexical, siteUrl, options);
469+
const result = JSON.parse(serializedResult);
470+
471+
result.root.children[0].src.should.equal('/image.png');
472+
result.root.children[0].other.should.equal('http://my-ghost-blog.com/unknown-card-property');
473+
});
342474
});

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

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// const testUtils = require('./utils');
33
require('../../utils');
44

5+
const UrlUtils = require('../../../lib/url-utils');
56
const lexicalAbsoluteToTransformReady = require('../../../lib/utils/lexical-absolute-to-transform-ready');
67

78
describe('utils: lexicalAbsoluteToTransformReady()', function () {
@@ -339,4 +340,135 @@ describe('utils: lexicalAbsoluteToTransformReady()', function () {
339340
result.root.children[1].url.should.equal('i%20don’t%20believe%20that%20our%20platform%20should%20take%20that%20down%20because%20i%20think%20there%20are%20things%20that%20different%20people%20get%20wrong');
340341
result.root.children[2].url.should.equal('__GHOST_URL__/sanity-check');
341342
});
343+
344+
it('handles cards', function () {
345+
const urlUtils = new UrlUtils({
346+
getSubdir: function () {
347+
return '';
348+
},
349+
getSiteUrl: function () {
350+
return siteUrl;
351+
}
352+
});
353+
354+
Object.assign(options, {
355+
nodes: [
356+
class ImageNode {
357+
static getType() {
358+
return 'image';
359+
}
360+
361+
static get urlTransformMap() {
362+
return {
363+
src: 'url',
364+
caption: 'html'
365+
};
366+
}
367+
}
368+
],
369+
transformMap: {
370+
toTransformReady: {
371+
url: urlUtils.toTransformReady.bind(urlUtils),
372+
html: urlUtils.htmlToTransformReady.bind(urlUtils)
373+
}
374+
}
375+
});
376+
377+
const lexical = JSON.stringify({
378+
root: {
379+
children: [
380+
{type: 'image', src: 'http://my-ghost-blog.com/image.png', caption: 'Captions are HTML with only <a href="http://my-ghost-blog.com/image-caption-link">links transformed</a> - this is a plaintext url: http://my-ghost-blog.com/plaintext-url'}
381+
]
382+
}
383+
});
384+
385+
const serializedResult = lexicalAbsoluteToTransformReady(lexical, siteUrl, options);
386+
const result = JSON.parse(serializedResult);
387+
388+
result.root.children[0].src.should.equal('__GHOST_URL__/image.png');
389+
result.root.children[0].caption.should.equal('Captions are HTML with only <a href="__GHOST_URL__/image-caption-link">links transformed</a> - this is a plaintext url: http://my-ghost-blog.com/plaintext-url');
390+
});
391+
392+
it('does not transform unknown cards', 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+
transformMap: {
405+
toTransformReady: {
406+
url: urlUtils.toTransformReady.bind(urlUtils),
407+
html: urlUtils.htmlToTransformReady.bind(urlUtils)
408+
}
409+
}
410+
});
411+
412+
const lexical = JSON.stringify({
413+
root: {
414+
children: [
415+
{type: 'image', src: 'http://my-ghost-blog.com/image.png', caption: 'Captions are HTML with only <a href="http://my-ghost-blog.com/image-caption-link">links transformed</a> - this is a plaintext url: http://my-ghost-blog.com/plaintext-url'}
416+
]
417+
}
418+
});
419+
420+
const serializedResult = lexicalAbsoluteToTransformReady(lexical, siteUrl, options);
421+
const result = JSON.parse(serializedResult);
422+
423+
result.root.children[0].src.should.equal('http://my-ghost-blog.com/image.png');
424+
result.root.children[0].caption.should.equal('Captions are HTML with only <a href="http://my-ghost-blog.com/image-caption-link">links transformed</a> - this is a plaintext url: http://my-ghost-blog.com/plaintext-url');
425+
});
426+
427+
it('does not transform unknown card properties', function () {
428+
const urlUtils = new UrlUtils({
429+
getSubdir: function () {
430+
return '';
431+
},
432+
getSiteUrl: function () {
433+
return siteUrl;
434+
}
435+
});
436+
437+
Object.assign(options, {
438+
nodes: [
439+
class ImageNode {
440+
static getType() {
441+
return 'image';
442+
}
443+
444+
static get urlTransformMap() {
445+
return {
446+
src: 'url',
447+
caption: 'html'
448+
};
449+
}
450+
}
451+
],
452+
transformMap: {
453+
toTransformReady: {
454+
url: urlUtils.toTransformReady.bind(urlUtils),
455+
html: urlUtils.htmlToTransformReady.bind(urlUtils)
456+
}
457+
}
458+
});
459+
460+
const lexical = JSON.stringify({
461+
root: {
462+
children: [
463+
{type: 'image', src: 'http://my-ghost-blog.com/image.png', other: 'http://my-ghost-blog.com/unknown-card-property'}
464+
]
465+
}
466+
});
467+
468+
const serializedResult = lexicalAbsoluteToTransformReady(lexical, siteUrl, options);
469+
const result = JSON.parse(serializedResult);
470+
471+
result.root.children[0].src.should.equal('__GHOST_URL__/image.png');
472+
result.root.children[0].other.should.equal('http://my-ghost-blog.com/unknown-card-property');
473+
});
342474
});

0 commit comments

Comments
 (0)