From 457c672c6a8977cc354e05e59926be9236c411e0 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Tue, 29 Nov 2022 16:57:01 +0000 Subject: [PATCH] Added URL transform for image cards in Lexical documents (#15890) refs https://github.com/TryGhost/Team/issues/2225 - updated the `formatOnWrite` transform map for posts to include the new `nodes` and `transformMap` options used by `urlUtils` for transforming node payload data - added `nodes` to the `lexicalLib` module that pulls in our default nodes to be passed in to the URL transform utilities - added `urlTransformMap` to the `lexicalLib` module that maps transform type and data type to URL transform utility functions that accept a single URL argument --- ghost/core/core/server/lib/lexical.js | 37 ++++++++ ghost/core/core/server/models/post.js | 8 +- ghost/core/package.json | 3 +- .../regression/models/model_posts.test.js | 6 +- .../core/test/unit/server/lib/lexical.test.js | 32 +++++++ yarn.lock | 93 +++++++++++++++++-- 6 files changed, 168 insertions(+), 11 deletions(-) diff --git a/ghost/core/core/server/lib/lexical.js b/ghost/core/core/server/lib/lexical.js index 6171b52c30..3b9fc95a0a 100644 --- a/ghost/core/core/server/lib/lexical.js +++ b/ghost/core/core/server/lib/lexical.js @@ -1,4 +1,8 @@ +const urlUtils = require('../../shared/url-utils'); + +let nodes; let lexicalHtmlRenderer; +let urlTransformMap; module.exports = { get lexicalHtmlRenderer() { @@ -8,5 +12,38 @@ module.exports = { } return lexicalHtmlRenderer; + }, + + get nodes() { + if (!nodes) { + const {DEFAULT_NODES} = require('@tryghost/kg-default-nodes'); + nodes = DEFAULT_NODES; + } + + return nodes; + }, + + get urlTransformMap() { + if (!urlTransformMap) { + urlTransformMap = { + absoluteToRelative: { + url: urlUtils.absoluteToRelative.bind(urlUtils), + html: urlUtils.htmlAbsoluteToRelative.bind(urlUtils), + markdown: urlUtils.markdownAbsoluteToRelative.bind(urlUtils) + }, + relativeToAbsolute: { + url: urlUtils.relativeToAbsolute.bind(urlUtils), + html: urlUtils.htmlRelativeToAbsolute.bind(urlUtils), + markdown: urlUtils.markdownRelativeToAbsolute.bind(urlUtils) + }, + toTransformReady: { + url: urlUtils.toTransformReady.bind(urlUtils), + html: urlUtils.htmlToTransformReady.bind(urlUtils), + markdown: urlUtils.markdownToTransformReady.bind(urlUtils) + } + }; + } + + return urlTransformMap; } }; diff --git a/ghost/core/core/server/models/post.js b/ghost/core/core/server/models/post.js index 8d36d636f9..a44a96127d 100644 --- a/ghost/core/core/server/models/post.js +++ b/ghost/core/core/server/models/post.js @@ -180,7 +180,13 @@ Post = ghostBookshelf.Model.extend({ cardTransformers: mobiledocLib.cards } }, - lexical: 'lexicalToTransformReady', + lexical: { + method: 'lexicalToTransformReady', + options: { + nodes: lexicalLib.nodes, + transformMap: lexicalLib.urlTransformMap + } + }, html: 'htmlToTransformReady', plaintext: 'plaintextToTransformReady', custom_excerpt: 'htmlToTransformReady', diff --git a/ghost/core/package.json b/ghost/core/package.json index 4130a1bed4..f940d2a495 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -86,6 +86,7 @@ "@tryghost/kg-card-factory": "3.1.7", "@tryghost/kg-default-atoms": "3.1.4", "@tryghost/kg-default-cards": "5.18.5", + "@tryghost/kg-default-nodes": "0.0.1", "@tryghost/kg-lexical-html-renderer": "0.0.11", "@tryghost/kg-mobiledoc-html-renderer": "5.3.9", "@tryghost/limit-service": "1.2.3", @@ -128,7 +129,7 @@ "@tryghost/tiers": "0.0.0", "@tryghost/tpl": "0.1.19", "@tryghost/update-check-service": "0.0.0", - "@tryghost/url-utils": "4.2.0", + "@tryghost/url-utils": "4.3.0", "@tryghost/validator": "0.1.29", "@tryghost/verification-trigger": "0.0.0", "@tryghost/version": "0.1.16", diff --git a/ghost/core/test/regression/models/model_posts.test.js b/ghost/core/test/regression/models/model_posts.test.js index eff9dc9b6c..c6edf7e21c 100644 --- a/ghost/core/test/regression/models/model_posts.test.js +++ b/ghost/core/test/regression/models/model_posts.test.js @@ -1190,15 +1190,15 @@ describe('Post Model', function () { it('stores lexical as transform-ready and reads as absolute', async function () { const post = { title: 'Absolute->Transform-ready Lexical URL Transform Test', - lexical: `{"root":{"children":[{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"local link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"http://127.0.0.1:2369/local"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"external link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"https://example.com/external"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}` + lexical: `{"root":{"children":[{"type":"image","src":"http://127.0.0.1:2369/content/images/card.jpg"},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"local link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"http://127.0.0.1:2369/local"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"external link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"https://example.com/external"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}` }; const createdPost = await models.Post.add(post, context); - createdPost.get('lexical').should.equal(`{"root":{"children":[{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"local link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"http://127.0.0.1:2369/local"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"external link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"https://example.com/external"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`, 'Post.add result'); + createdPost.get('lexical').should.equal(`{"root":{"children":[{"type":"image","src":"http://127.0.0.1:2369/content/images/card.jpg"},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"local link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"http://127.0.0.1:2369/local"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"external link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"https://example.com/external"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`, 'Post.add result'); const knexResult = await db.knex('posts').where({id: createdPost.id}); const [knexPost] = knexResult; - knexPost.lexical.should.equal('{"root":{"children":[{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"local link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"__GHOST_URL__/local"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"external link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"https://example.com/external"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}', 'knex result'); + knexPost.lexical.should.equal('{"root":{"children":[{"type":"image","src":"__GHOST_URL__/content/images/card.jpg"},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"local link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"__GHOST_URL__/local"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1},{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"external link","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"rel":null,"target":null,"url":"https://example.com/external"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}', 'knex result'); }); }); diff --git a/ghost/core/test/unit/server/lib/lexical.test.js b/ghost/core/test/unit/server/lib/lexical.test.js index 16d7d74ba9..1dd16af4b2 100644 --- a/ghost/core/test/unit/server/lib/lexical.test.js +++ b/ghost/core/test/unit/server/lib/lexical.test.js @@ -9,5 +9,37 @@ describe('lib/lexical', function () { lexicalLib.lexicalHtmlRenderer.render(lexical) .should.eql('

Lexical is rendering.

'); }); + + it('renders all default cards', function () { + const lexicalState = JSON.stringify({ + root: { + children: [ + { + type: 'image', + cardWidth: 'wide', + src: '/content/images/2018/04/NatGeo06.jpg', + width: 4000, + height: 2000, + caption: 'Birdies' + } + ], + direction: null, + format: '', + indent: 0, + type: 'root', + version: 1 + } + }); + + lexicalLib.lexicalHtmlRenderer.render(lexicalState) + .should.eql(` +
+ +
+ Birdies +
+
+ `); + }); }); }); diff --git a/yarn.lock b/yarn.lock index c8d52e1761..21dd231c7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4429,6 +4429,14 @@ lodash "^4.17.21" luxon "^2.1.1" +"@tryghost/kg-default-nodes@0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@tryghost/kg-default-nodes/-/kg-default-nodes-0.0.1.tgz#1d0d3b05aaaf99d016d1560c5d6b49538c604bf0" + integrity sha512-u+UJ/wxEMhiNlvRXZBp92nHj5PTgqrcja73HoA1S5WDOu97+8dZvAmGxhRHHHEY5VNGHEA0cXxi8S7owL9FUMg== + dependencies: + jsdom "^20.0.3" + lexical "^0.6.0" + "@tryghost/kg-lexical-html-renderer@0.0.11": version "0.0.11" resolved "https://registry.yarnpkg.com/@tryghost/kg-lexical-html-renderer/-/kg-lexical-html-renderer-0.0.11.tgz#0f4636660be9866cf5f030cc6a435ad958e8804b" @@ -4444,11 +4452,11 @@ lexical "^0.5.0" "@tryghost/kg-markdown-html-renderer@^5.1.9": - version "5.1.9" - resolved "https://registry.yarnpkg.com/@tryghost/kg-markdown-html-renderer/-/kg-markdown-html-renderer-5.1.9.tgz#cb043705fb3cb140aef8577f87237cc82277ee15" - integrity sha512-HPziXC+qdqzpXi8K00GGZg0U3suJAm1Z4mmiWDFDc5BkDO0Dlrl+whOXbmmev+XIsZgr6sOje/uMO9fbAefZuA== + version "5.1.11" + resolved "https://registry.yarnpkg.com/@tryghost/kg-markdown-html-renderer/-/kg-markdown-html-renderer-5.1.11.tgz#c0aa728bc2ec518cefa69353211bdb866f70090d" + integrity sha512-mMQmQbO5/l5vVfiKiqlq0Y81wPX83AdSXPEq5GcAklq8E9q1ITpVtIZKC0KGrnQNiuledGOTP6wf2t5tyjgTLg== dependencies: - "@tryghost/kg-utils" "^1.0.6" + "@tryghost/kg-utils" "^1.0.8" markdown-it "^12.2.0" markdown-it-footnote "^3.0.3" markdown-it-image-lazy-loading "^1.1.0" @@ -4479,6 +4487,13 @@ dependencies: semver "^7.3.5" +"@tryghost/kg-utils@^1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tryghost/kg-utils/-/kg-utils-1.0.8.tgz#305897a7199cc32062e5db1ef54f8ad33ba45755" + integrity sha512-uO8WDFG8Q2VQ6LCAa2oAryavxFLcewMeXAoQ2PCj+oJZ/MiUjmUeTPEHnQV9TjlHyoriWyhz51PBFvlegjSxRw== + dependencies: + semver "^7.3.5" + "@tryghost/limit-service@1.2.3": version "1.2.3" resolved "https://registry.yarnpkg.com/@tryghost/limit-service/-/limit-service-1.2.3.tgz#161acc62f180a47b2a326562a657f0b6c6dd96c0" @@ -4665,7 +4680,19 @@ dependencies: lodash.template "^4.5.0" -"@tryghost/url-utils@4.2.0", "@tryghost/url-utils@^4.0.0": +"@tryghost/url-utils@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@tryghost/url-utils/-/url-utils-4.3.0.tgz#f276ea0963f34547dca8f8f8a6c7bd4e6d4bb6e3" + integrity sha512-DtsCVFytzkB6pE3azrO8PVP4wzwLR6Mjljy0ckwgvC7hj7bKM1QeGpCIasPNIKF+mTo/U9dXYadSVs1oGod1rA== + dependencies: + cheerio "^0.22.0" + moment "^2.27.0" + moment-timezone "^0.5.31" + remark "^11.0.2" + remark-footnotes "^1.0.0" + unist-util-visit "^2.0.0" + +"@tryghost/url-utils@^4.0.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@tryghost/url-utils/-/url-utils-4.2.0.tgz#342c73c840dda4f2ba2316fc581167404e219554" integrity sha512-ipeGBj6CJau/17J+M1t2vUJvdptoTnCPVgph2mbKGclOEdAMv9h3/S15cNMnxIqoSNURAvwMKD+QxkOboM/7zg== @@ -5741,6 +5768,11 @@ acorn@^8.1.0, acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== +acorn@^8.8.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + address@^1.0.1, address@^1.1.2: version "1.2.1" resolved "https://registry.yarnpkg.com/address/-/address-1.2.1.tgz#25bb61095b7522d65b357baa11bc05492d4c8acd" @@ -10301,7 +10333,7 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -decimal.js@^10.2.1, decimal.js@^10.4.1: +decimal.js@^10.2.1, decimal.js@^10.4.1, decimal.js@^10.4.2: version "10.4.2" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.2.tgz#0341651d1d997d86065a2ce3a441fbd0d8e8b98e" integrity sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA== @@ -17231,6 +17263,38 @@ jsdom@^20.0.0, jsdom@~20.0.0: ws "^8.9.0" xml-name-validator "^4.0.0" +jsdom@^20.0.3: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db" + integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== + dependencies: + abab "^2.0.6" + acorn "^8.8.1" + acorn-globals "^7.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.2" + decimal.js "^10.4.2" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.2" + parse5 "^7.1.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.2" + w3c-xmlserializer "^4.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + ws "^8.11.0" + xml-name-validator "^4.0.0" + jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" @@ -17654,6 +17718,11 @@ lexical@^0.5.0: resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.5.0.tgz#03a8abc816f6d922384ecd01bd14ed36a1ea4d63" integrity sha512-J0cFuNPQQY5P9W5XW2/xgqp5W0eEQ2rxShLf8eevLvxFWsPSY3zjg3RCzTHyheSiGBBjKDIaM4gxtO8eNeJr2A== +lexical@^0.6.0: + version "0.6.4" + resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.6.4.tgz#00f5070a967fd1410aaa7d4c928f6e61af73c604" + integrity sha512-QBJEowv2FRkanu9V4HxCiR/V0rECt98zUeHsaYcgUCphrxbZseQgk8AO5UbHJMgAD4iQ0CCKZWbiqbv7Z8tYnw== + liftoff@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-3.1.0.tgz#c9ba6081f908670607ee79062d700df062c52ed3" @@ -26121,6 +26190,13 @@ w3c-xmlserializer@^3.0.0: dependencies: xml-name-validator "^4.0.0" +w3c-xmlserializer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" + integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== + dependencies: + xml-name-validator "^4.0.0" + walk-sync@3.0.0, walk-sync@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-3.0.0.tgz#67f882925021e20569a1edd560b8da31da8d171c" @@ -26823,6 +26899,11 @@ ws@^7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" + integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== + ws@^8.4.2, ws@^8.9.0: version "8.9.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e"