Skip to content

Commit

Permalink
Added node (card) support to lexical URL transforms
Browse files Browse the repository at this point in the history
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
  • Loading branch information
kevinansfield committed Nov 28, 2022
1 parent 919dd8e commit 2cce002
Show file tree
Hide file tree
Showing 9 changed files with 546 additions and 8 deletions.
18 changes: 14 additions & 4 deletions packages/url-utils/lib/utils/_lexical-transform.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
function lexicalTransform(serializedLexical, siteUrl, transformFunction, itemPath, _options = {}) {
const defaultOptions = {assetsOnly: false, secure: false};
const defaultOptions = {assetsOnly: false, secure: false, nodes: [], transformMap: {}};
const options = Object.assign({}, defaultOptions, _options, {siteUrl, itemPath});

if (!serializedLexical) {
Expand All @@ -14,11 +14,21 @@ function lexicalTransform(serializedLexical, siteUrl, transformFunction, itemPat
return serializedLexical;
}

// any lexical links will be a child object with a `url` attribute,
// recursively walk the tree transforming any `.url`s
const nodeMap = new Map();
options.nodes.forEach(node => node.urlTransformMap && nodeMap.set(node.getType(), node.urlTransformMap));

const transformChildren = function (children) {
for (const child of children) {
if (child.url) {
// cards (nodes) have a `type` attribute in their node data
if (child.type && nodeMap.has(child.type)) {
Object.entries(nodeMap.get(child.type)).forEach(([property, transform]) => {
if (child[property]) {
child[property] = options.transformMap[options.transformType][transform](child[property]);
}
});
} else if (child.url) {
// any lexical links will be a child object with a `url` attribute,
// recursively walk the tree transforming any `.url`s
child.url = transformFunction(child.url, siteUrl, itemPath, options);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const absoluteToRelative = require('./absolute-to-relative');
const lexicalTransform = require('./_lexical-transform');

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const absoluteToTransformReady = require('./absolute-to-transform-ready');
const lexicalTransform = require('./_lexical-transform');

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const relativeToAbsolute = require('./relative-to-absolute');
const lexicalTransform = require('./_lexical-transform');

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const relativeToTransformReady = require('./relative-to-transform-ready');
const lexicalTransform = require('./_lexical-transform');

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// const testUtils = require('./utils');
require('../../utils');

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

describe('utils: lexicalAbsoluteToRelative()', function () {
Expand Down Expand Up @@ -339,4 +340,135 @@ describe('utils: lexicalAbsoluteToRelative()', function () {
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');
result.root.children[2].url.should.equal('/sanity-check');
});

it('handles cards', function () {
const urlUtils = new UrlUtils({
getSubdir: function () {
return '';
},
getSiteUrl: function () {
return siteUrl;
}
});

Object.assign(options, {
nodes: [
class ImageNode {
static getType() {
return 'image';
}

static get urlTransformMap() {
return {
src: 'url',
caption: 'html'
};
}
}
],
transformMap: {
absoluteToRelative: {
url: urlUtils.absoluteToRelative.bind(urlUtils),
html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils)
}
}
});

const lexical = JSON.stringify({
root: {
children: [
{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'}
]
}
});

const serializedResult = lexicalAbsoluteToRelative(lexical, siteUrl, options);
const result = JSON.parse(serializedResult);

result.root.children[0].src.should.equal('/image.png');
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');
});

it('does not transform unknown cards', function () {
const urlUtils = new UrlUtils({
getSubdir: function () {
return '';
},
getSiteUrl: function () {
return siteUrl;
}
});

Object.assign(options, {
nodes: [],
transformMap: {
absoluteToRelative: {
url: urlUtils.absoluteToRelative.bind(urlUtils),
html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils)
}
}
});

const lexical = JSON.stringify({
root: {
children: [
{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'}
]
}
});

const serializedResult = lexicalAbsoluteToRelative(lexical, siteUrl, options);
const result = JSON.parse(serializedResult);

result.root.children[0].src.should.equal('http://my-ghost-blog.com/image.png');
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');
});

it('does not transform unknown card properties', function () {
const urlUtils = new UrlUtils({
getSubdir: function () {
return '';
},
getSiteUrl: function () {
return siteUrl;
}
});

Object.assign(options, {
nodes: [
class ImageNode {
static getType() {
return 'image';
}

static get urlTransformMap() {
return {
src: 'url',
caption: 'html'
};
}
}
],
transformMap: {
absoluteToRelative: {
url: urlUtils.absoluteToRelative.bind(urlUtils),
html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils)
}
}
});

const lexical = JSON.stringify({
root: {
children: [
{type: 'image', src: 'http://my-ghost-blog.com/image.png', other: 'http://my-ghost-blog.com/unknown-card-property'}
]
}
});

const serializedResult = lexicalAbsoluteToRelative(lexical, siteUrl, options);
const result = JSON.parse(serializedResult);

result.root.children[0].src.should.equal('/image.png');
result.root.children[0].other.should.equal('http://my-ghost-blog.com/unknown-card-property');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// const testUtils = require('./utils');
require('../../utils');

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

describe('utils: lexicalAbsoluteToTransformReady()', function () {
Expand Down Expand Up @@ -339,4 +340,135 @@ describe('utils: lexicalAbsoluteToTransformReady()', function () {
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');
result.root.children[2].url.should.equal('__GHOST_URL__/sanity-check');
});

it('handles cards', function () {
const urlUtils = new UrlUtils({
getSubdir: function () {
return '';
},
getSiteUrl: function () {
return siteUrl;
}
});

Object.assign(options, {
nodes: [
class ImageNode {
static getType() {
return 'image';
}

static get urlTransformMap() {
return {
src: 'url',
caption: 'html'
};
}
}
],
transformMap: {
toTransformReady: {
url: urlUtils.toTransformReady.bind(urlUtils),
html: urlUtils.htmlToTransformReady.bind(urlUtils)
}
}
});

const lexical = JSON.stringify({
root: {
children: [
{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'}
]
}
});

const serializedResult = lexicalAbsoluteToTransformReady(lexical, siteUrl, options);
const result = JSON.parse(serializedResult);

result.root.children[0].src.should.equal('__GHOST_URL__/image.png');
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');
});

it('does not transform unknown cards', function () {
const urlUtils = new UrlUtils({
getSubdir: function () {
return '';
},
getSiteUrl: function () {
return siteUrl;
}
});

Object.assign(options, {
nodes: [],
transformMap: {
toTransformReady: {
url: urlUtils.toTransformReady.bind(urlUtils),
html: urlUtils.htmlToTransformReady.bind(urlUtils)
}
}
});

const lexical = JSON.stringify({
root: {
children: [
{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'}
]
}
});

const serializedResult = lexicalAbsoluteToTransformReady(lexical, siteUrl, options);
const result = JSON.parse(serializedResult);

result.root.children[0].src.should.equal('http://my-ghost-blog.com/image.png');
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');
});

it('does not transform unknown card properties', function () {
const urlUtils = new UrlUtils({
getSubdir: function () {
return '';
},
getSiteUrl: function () {
return siteUrl;
}
});

Object.assign(options, {
nodes: [
class ImageNode {
static getType() {
return 'image';
}

static get urlTransformMap() {
return {
src: 'url',
caption: 'html'
};
}
}
],
transformMap: {
toTransformReady: {
url: urlUtils.toTransformReady.bind(urlUtils),
html: urlUtils.htmlToTransformReady.bind(urlUtils)
}
}
});

const lexical = JSON.stringify({
root: {
children: [
{type: 'image', src: 'http://my-ghost-blog.com/image.png', other: 'http://my-ghost-blog.com/unknown-card-property'}
]
}
});

const serializedResult = lexicalAbsoluteToTransformReady(lexical, siteUrl, options);
const result = JSON.parse(serializedResult);

result.root.children[0].src.should.equal('__GHOST_URL__/image.png');
result.root.children[0].other.should.equal('http://my-ghost-blog.com/unknown-card-property');
});
});
Loading

0 comments on commit 2cce002

Please sign in to comment.