Added conversion to beta editor format when creating content via ?source=html (#18000)

closes https://github.com/TryGhost/Product/issues/3803

Previously when the beta editor was enabled, using `?source=html` to create posts via the API would create posts in the old editor rather than the beta. This change switches conversion over to the new editor format when the beta is enabled so the full flow can be tested.

- added `htmlToLexicalConverter` method to our lexical library
- updated post and page input serializers to add html-to-lexical conversion when the beta editor is enabled
- updated post model to handle the mobiledoc+lexical co-existing state
  - this is a special case that is only valid for `?source=html` because providing both directly via the API is prohibited
  - we need the extra check here because at the input serializer layer we don't have access to the model to check if we're updating a mobiledoc post or a lexical post so the serializer sets both formats on a `?source=html` request when the beta is enabled and lets the model handle choosing the correct one
This commit is contained in:
Kevin Ansfield 2023-09-06 22:16:40 +01:00 committed by GitHub
parent 47c178b376
commit 8c91662a47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 711 additions and 34 deletions

View File

@ -6,6 +6,8 @@ const slugFilterOrder = require('./utils/slug-filter-order');
const localUtils = require('../../index'); const localUtils = require('../../index');
const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta; const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
const clean = require('./utils/clean'); const clean = require('./utils/clean');
const labs = require('../../../../../../shared/labs');
const lexical = require('../../../../../lib/lexical');
function removeSourceFormats(frame) { function removeSourceFormats(frame) {
if (frame.options.formats?.includes('mobiledoc') || frame.options.formats?.includes('lexical')) { if (frame.options.formats?.includes('mobiledoc') || frame.options.formats?.includes('lexical')) {
@ -133,6 +135,12 @@ module.exports = {
if (frame.options.source === 'html' && !_.isEmpty(html)) { if (frame.options.source === 'html' && !_.isEmpty(html)) {
frame.data.pages[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html)); frame.data.pages[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html));
// normally we don't allow both mobiledoc+lexical but the model layer will remove lexical
// if mobiledoc is already present to avoid migrating formats outside of an explicit conversion
if (labs.isSet('lexicalEditor')) {
frame.data.pages[0].lexical = JSON.stringify(lexical.htmlToLexicalConverter(html));
}
} }
} }

View File

@ -6,6 +6,8 @@ const localUtils = require('../../index');
const mobiledoc = require('../../../../../lib/mobiledoc'); const mobiledoc = require('../../../../../lib/mobiledoc');
const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta; const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
const clean = require('./utils/clean'); const clean = require('./utils/clean');
const labs = require('../../../../../../shared/labs');
const lexical = require('../../../../../lib/lexical');
function removeSourceFormats(frame) { function removeSourceFormats(frame) {
if (frame.options.formats?.includes('mobiledoc') || frame.options.formats?.includes('lexical')) { if (frame.options.formats?.includes('mobiledoc') || frame.options.formats?.includes('lexical')) {
@ -32,7 +34,7 @@ function defaultRelations(frame) {
// Apply same mapping as content API // Apply same mapping as content API
mapWithRelated(frame); mapWithRelated(frame);
// Addditional defaults for admin API // Additional defaults for admin API
if (frame.options.withRelated) { if (frame.options.withRelated) {
return; return;
} }
@ -167,6 +169,12 @@ module.exports = {
if (frame.options.source === 'html' && !_.isEmpty(html)) { if (frame.options.source === 'html' && !_.isEmpty(html)) {
frame.data.posts[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html)); frame.data.posts[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html));
// normally we don't allow both mobiledoc+lexical but the model layer will remove lexical
// if mobiledoc is already present to avoid migrating formats outside of an explicit conversion
if (labs.isSet('lexicalEditor')) {
frame.data.posts[0].lexical = JSON.stringify(lexical.htmlToLexicalConverter(html));
}
} }
} }

View File

@ -1,4 +1,5 @@
const path = require('path'); const path = require('path');
const errors = require('@tryghost/errors');
const urlUtils = require('../../shared/url-utils'); const urlUtils = require('../../shared/url-utils');
const config = require('../../shared/config'); const config = require('../../shared/config');
const storage = require('../adapters/storage'); const storage = require('../adapters/storage');
@ -78,5 +79,19 @@ module.exports = {
} }
return urlTransformMap; return urlTransformMap;
},
get htmlToLexicalConverter() {
try {
return require('@tryghost/kg-html-to-lexical').htmlToLexical;
} catch (err) {
throw new errors.InternalServerError({
message: 'Unable to convert from source HTML to Lexical',
context: 'The html-to-lexical package was not installed',
help: 'Please review any errors from the install process by checking the Ghost logs',
code: 'HTML_TO_LEXICAL_INSTALLATION',
err: err
});
}
} }
}; };

View File

@ -552,6 +552,16 @@ Post = ghostBookshelf.Model.extend({
let tagsToSave; let tagsToSave;
const ops = []; const ops = [];
// normally we don't allow both mobiledoc & lexical through at the API level but there's
// an exception for ?source=html which always sets both when the lexical editor is enabled.
// That's necessary because at the input serializer layer we don't have access to the
// actual model to check if this would result in a change of format
if (this.previous('mobiledoc') && this.get('lexical')) {
this.set('lexical', null);
} else if (this.get('mobiledoc') && this.get('lexical')) {
this.set('mobiledoc', null);
}
// CASE: disallow published -> scheduled // CASE: disallow published -> scheduled
// @TODO: remove when we have versioning based on updated_at // @TODO: remove when we have versioning based on updated_at
if (newStatus !== olderStatus && newStatus === 'scheduled' && olderStatus === 'published') { if (newStatus !== olderStatus && newStatus === 'scheduled' && olderStatus === 'published') {
@ -653,7 +663,7 @@ Post = ghostBookshelf.Model.extend({
// If we're force re-rendering we want to make sure that all image cards // If we're force re-rendering we want to make sure that all image cards
// have original dimensions stored in the payload for use by card renderers // have original dimensions stored in the payload for use by card renderers
if (options.force_rerender) { if (options.force_rerender && this.get('mobiledoc')) {
this.set('mobiledoc', await mobiledocLib.populateImageSizes(this.get('mobiledoc'))); this.set('mobiledoc', await mobiledocLib.populateImageSizes(this.get('mobiledoc')));
} }

View File

@ -105,6 +105,7 @@
"@tryghost/kg-default-atoms": "4.0.3", "@tryghost/kg-default-atoms": "4.0.3",
"@tryghost/kg-default-cards": "9.1.4", "@tryghost/kg-default-cards": "9.1.4",
"@tryghost/kg-default-nodes": "0.1.26", "@tryghost/kg-default-nodes": "0.1.26",
"@tryghost/kg-html-to-lexical": "0.0.1",
"@tryghost/kg-lexical-html-renderer": "0.3.22", "@tryghost/kg-lexical-html-renderer": "0.3.22",
"@tryghost/kg-mobiledoc-html-renderer": "6.0.10", "@tryghost/kg-mobiledoc-html-renderer": "6.0.10",
"@tryghost/limit-service": "1.2.6", "@tryghost/limit-service": "1.2.6",

View File

@ -304,6 +304,327 @@ Object {
} }
`; `;
exports[`Pages API Create Can create a page with html (labs.lexicalEditor) 1: [body] 1`] = `
Object {
"pages": Array [
Object {
"authors": Any<Array>,
"canonical_url": null,
"codeinjection_foot": null,
"codeinjection_head": null,
"comment_id": Any<String>,
"count": Object {
"negative_feedback": 0,
"paid_conversions": 0,
"positive_feedback": 0,
"signups": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"custom_excerpt": null,
"custom_template": null,
"excerpt": "Testing page creation with html",
"feature_image": null,
"feature_image_alt": null,
"feature_image_caption": null,
"featured": false,
"frontmatter": null,
"html": "<p>Testing page creation with html</p>",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[{\\"detail\\":0,\\"format\\":0,\\"mode\\":\\"normal\\",\\"style\\":\\"\\",\\"text\\":\\"Testing page creation with html\\",\\"type\\":\\"text\\",\\"version\\":1}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}",
"meta_description": null,
"meta_title": null,
"mobiledoc": null,
"og_description": null,
"og_image": null,
"og_title": null,
"post_revisions": Any<Array>,
"primary_author": Any<Object>,
"primary_tag": Any<Object>,
"published_at": null,
"reading_time": 0,
"show_title_and_feature_image": Any<Boolean>,
"slug": "html-test-2",
"status": "draft",
"tags": Any<Array>,
"tiers": Array [
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": null,
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": null,
"monthly_price_id": null,
"name": "Free",
"slug": "free",
"trial_days": 0,
"type": "free",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": null,
"yearly_price_id": null,
},
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": "usd",
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": 500,
"monthly_price_id": null,
"name": "Default Product",
"slug": "default-product",
"trial_days": 0,
"type": "paid",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": 5000,
"yearly_price_id": null,
},
],
"title": "HTML test",
"twitter_description": null,
"twitter_image": null,
"twitter_title": null,
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "public",
},
],
}
`;
exports[`Pages API Create Can create a page with html (labs.lexicalEditor) 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "5420",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/pages\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Pages API Create Can create a page with html 1: [body] 1`] = `
Object {
"pages": Array [
Object {
"authors": Any<Array>,
"canonical_url": null,
"codeinjection_foot": null,
"codeinjection_head": null,
"comment_id": Any<String>,
"count": Object {
"negative_feedback": 0,
"paid_conversions": 0,
"positive_feedback": 0,
"signups": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"custom_excerpt": null,
"custom_template": null,
"excerpt": "Testing page creation with html",
"feature_image": null,
"feature_image_alt": null,
"feature_image_caption": null,
"featured": false,
"frontmatter": null,
"html": "<p>Testing page creation with html</p>",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": null,
"meta_description": null,
"meta_title": null,
"mobiledoc": "{\\"version\\":\\"0.3.1\\",\\"atoms\\":[],\\"cards\\":[],\\"markups\\":[],\\"sections\\":[[1,\\"p\\",[[0,[],0,\\"Testing page creation with html\\"]]]]}",
"og_description": null,
"og_image": null,
"og_title": null,
"post_revisions": Any<Array>,
"primary_author": Any<Object>,
"primary_tag": Any<Object>,
"published_at": null,
"reading_time": 0,
"show_title_and_feature_image": Any<Boolean>,
"slug": "html-test",
"status": "draft",
"tags": Any<Array>,
"tiers": Array [
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": null,
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": null,
"monthly_price_id": null,
"name": "Free",
"slug": "free",
"trial_days": 0,
"type": "free",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": null,
"yearly_price_id": null,
},
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": "usd",
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": 500,
"monthly_price_id": null,
"name": "Default Product",
"slug": "default-product",
"trial_days": 0,
"type": "paid",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": 5000,
"yearly_price_id": null,
},
],
"title": "HTML test",
"twitter_description": null,
"twitter_image": null,
"twitter_title": null,
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "public",
},
],
}
`;
exports[`Pages API Create Can create a page with html 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "3815",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/pages\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Pages API Create Can create a post with html (labs.lexicalEditor) 1: [body] 1`] = `
Object {
"pages": Array [
Object {
"authors": Any<Array>,
"canonical_url": null,
"codeinjection_foot": null,
"codeinjection_head": null,
"comment_id": Any<String>,
"count": Object {
"negative_feedback": 0,
"paid_conversions": 0,
"positive_feedback": 0,
"signups": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"custom_excerpt": null,
"custom_template": null,
"excerpt": "Testing page creation with html",
"feature_image": null,
"feature_image_alt": null,
"feature_image_caption": null,
"featured": false,
"frontmatter": null,
"html": "<p>Testing page creation with html</p>",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[{\\"detail\\":0,\\"format\\":0,\\"mode\\":\\"normal\\",\\"style\\":\\"\\",\\"text\\":\\"Testing page creation with html\\",\\"type\\":\\"text\\",\\"version\\":1}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}",
"meta_description": null,
"meta_title": null,
"mobiledoc": null,
"og_description": null,
"og_image": null,
"og_title": null,
"post_revisions": Any<Array>,
"primary_author": Any<Object>,
"primary_tag": Any<Object>,
"published_at": null,
"reading_time": 0,
"show_title_and_feature_image": Any<Boolean>,
"slug": "html-test-2",
"status": "draft",
"tags": Any<Array>,
"tiers": Array [
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": null,
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": null,
"monthly_price_id": null,
"name": "Free",
"slug": "free",
"trial_days": 0,
"type": "free",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": null,
"yearly_price_id": null,
},
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": "usd",
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": 500,
"monthly_price_id": null,
"name": "Default Product",
"slug": "default-product",
"trial_days": 0,
"type": "paid",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": 5000,
"yearly_price_id": null,
},
],
"title": "HTML test",
"twitter_description": null,
"twitter_image": null,
"twitter_title": null,
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "public",
},
],
}
`;
exports[`Pages API Create Can create a post with html (labs.lexicalEditor) 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "5420",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/pages\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Pages API Update Can modify hide_title_and_feature_image property 1: [body] 1`] = ` exports[`Pages API Update Can modify hide_title_and_feature_image property 1: [body] 1`] = `
Object { Object {
"pages": Array [ "pages": Array [

View File

@ -534,19 +534,6 @@ Object {
} }
`; `;
exports[`Posts API Can browse filtering by collection using paging parameters 1: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "7639",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Posts API Can browse filtering by collection using paging parameters 2: [headers] 1`] = ` exports[`Posts API Can browse filtering by collection using paging parameters 2: [headers] 1`] = `
Object { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "access-control-allow-origin": "http://127.0.0.1:2369",
@ -1008,6 +995,226 @@ Object {
} }
`; `;
exports[`Posts API Create Can create a post with html (labs.lexicalEditor) 1: [body] 1`] = `
Object {
"posts": Array [
Object {
"authors": Any<Array>,
"canonical_url": null,
"codeinjection_foot": null,
"codeinjection_head": null,
"comment_id": Any<String>,
"count": Object {
"clicks": 0,
"negative_feedback": 0,
"positive_feedback": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"custom_excerpt": null,
"custom_template": null,
"email": null,
"email_only": false,
"email_segment": "all",
"email_subject": null,
"excerpt": "Testing post creation with html",
"feature_image": null,
"feature_image_alt": null,
"feature_image_caption": null,
"featured": false,
"frontmatter": null,
"html": "<p>Testing post creation with html</p>",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[{\\"detail\\":0,\\"format\\":0,\\"mode\\":\\"normal\\",\\"style\\":\\"\\",\\"text\\":\\"Testing post creation with html\\",\\"type\\":\\"text\\",\\"version\\":1}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}",
"meta_description": null,
"meta_title": null,
"mobiledoc": null,
"newsletter": null,
"og_description": null,
"og_image": null,
"og_title": null,
"post_revisions": Any<Array>,
"primary_author": Any<Object>,
"primary_tag": Any<Object>,
"published_at": null,
"reading_time": 0,
"slug": "html-test-2",
"status": "draft",
"tags": Any<Array>,
"tiers": Array [
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": null,
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": null,
"monthly_price_id": null,
"name": "Free",
"slug": "free",
"trial_days": 0,
"type": "free",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": null,
"yearly_price_id": null,
},
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": "usd",
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": 500,
"monthly_price_id": null,
"name": "Default Product",
"slug": "default-product",
"trial_days": 0,
"type": "paid",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": 5000,
"yearly_price_id": null,
},
],
"title": "HTML test",
"twitter_description": null,
"twitter_image": null,
"twitter_title": null,
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "public",
},
],
}
`;
exports[`Posts API Create Can create a post with html (labs.lexicalEditor) 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "5455",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/posts\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Posts API Create Can create a post with html 1: [body] 1`] = `
Object {
"posts": Array [
Object {
"authors": Any<Array>,
"canonical_url": null,
"codeinjection_foot": null,
"codeinjection_head": null,
"comment_id": Any<String>,
"count": Object {
"clicks": 0,
"negative_feedback": 0,
"positive_feedback": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"custom_excerpt": null,
"custom_template": null,
"email": null,
"email_only": false,
"email_segment": "all",
"email_subject": null,
"excerpt": "Testing post creation with html",
"feature_image": null,
"feature_image_alt": null,
"feature_image_caption": null,
"featured": false,
"frontmatter": null,
"html": "<p>Testing post creation with html</p>",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": null,
"meta_description": null,
"meta_title": null,
"mobiledoc": "{\\"version\\":\\"0.3.1\\",\\"atoms\\":[],\\"cards\\":[],\\"markups\\":[],\\"sections\\":[[1,\\"p\\",[[0,[],0,\\"Testing post creation with html\\"]]]]}",
"newsletter": null,
"og_description": null,
"og_image": null,
"og_title": null,
"post_revisions": Any<Array>,
"primary_author": Any<Object>,
"primary_tag": Any<Object>,
"published_at": null,
"reading_time": 0,
"slug": "html-test",
"status": "draft",
"tags": Any<Array>,
"tiers": Array [
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": null,
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": null,
"monthly_price_id": null,
"name": "Free",
"slug": "free",
"trial_days": 0,
"type": "free",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": null,
"yearly_price_id": null,
},
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": "usd",
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": 500,
"monthly_price_id": null,
"name": "Default Product",
"slug": "default-product",
"trial_days": 0,
"type": "paid",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": 5000,
"yearly_price_id": null,
},
],
"title": "HTML test",
"twitter_description": null,
"twitter_image": null,
"twitter_title": null,
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "public",
},
],
}
`;
exports[`Posts API Create Can create a post with html 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "3850",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/posts\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Posts API Create Can create a post with lexical 1: [body] 1`] = ` exports[`Posts API Create Can create a post with lexical 1: [body] 1`] = `
Object { Object {
"posts": Array [ "posts": Array [
@ -1302,18 +1509,6 @@ Object {
} }
`; `;
exports[`Posts API Delete Can destroy a post 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin",
"x-cache-invalidate": "/*",
"x-powered-by": "Express",
}
`;
exports[`Posts API Delete Cannot delete a non-existent posts 1: [body] 1`] = ` exports[`Posts API Delete Cannot delete a non-existent posts 1: [body] 1`] = `
Object { Object {
"errors": Array [ "errors": Array [
@ -1662,6 +1857,14 @@ Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"sort_order": 17, "sort_order": 17,
}, },
Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"sort_order": 18,
},
Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"sort_order": 19,
},
], ],
"slug": "latest", "slug": "latest",
"title": "Latest", "title": "Latest",
@ -1758,7 +1961,7 @@ exports[`Posts API Update Can add and remove collections 4: [headers] 1`] = `
Object { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "5207", "content-length": "5307",
"content-type": "application/json; charset=utf-8", "content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -1873,6 +2076,14 @@ Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"sort_order": 17, "sort_order": 17,
}, },
Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"sort_order": 18,
},
Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"sort_order": 19,
},
], ],
"slug": "latest", "slug": "latest",
"title": "Latest", "title": "Latest",
@ -1969,7 +2180,7 @@ exports[`Posts API Update Can add and remove collections 6: [headers] 1`] = `
Object { Object {
"access-control-allow-origin": "http://127.0.0.1:2369", "access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "5201", "content-length": "5301",
"content-type": "application/json; charset=utf-8", "content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View File

@ -37,6 +37,52 @@ describe('Pages API', function () {
mockManager.restore(); mockManager.restore();
}); });
describe('Create', function () {
it('Can create a page with html', async function () {
mockManager.mockLabsDisabled('lexicalEditor');
const page = {
title: 'HTML test',
html: '<p>Testing page creation with html</p>'
};
await agent
.post('/pages/?source=html&formats=mobiledoc,lexical,html')
.body({pages: [page]})
.expectStatus(201)
.matchBodySnapshot({
pages: [Object.assign({}, matchPageShallowIncludes, {published_at: null})]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('pages')
});
});
it('Can create a page with html (labs.lexicalEditor)', async function () {
mockManager.mockLabsEnabled('lexicalEditor');
const page = {
title: 'HTML test',
html: '<p>Testing page creation with html</p>'
};
await agent
.post('/pages/?source=html&formats=mobiledoc,lexical,html')
.body({pages: [page]})
.expectStatus(201)
.matchBodySnapshot({
pages: [Object.assign({}, matchPageShallowIncludes, {published_at: null})]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('pages')
});
});
});
describe('Update', function () { describe('Update', function () {
it('Can modify show_title_and_feature_image property', async function () { it('Can modify show_title_and_feature_image property', async function () {
const page = { const page = {
@ -115,7 +161,7 @@ describe('Pages API', function () {
}); });
describe('Convert', function () { describe('Convert', function () {
it('can convert a mobiledoc page to lexical', async function () { it('can convert a mobiledoc page to lexical', async function () {
const mobiledoc = JSON.stringify({ const mobiledoc = JSON.stringify({
version: '0.3.1', version: '0.3.1',
ghostVersion: '4.0', ghostVersion: '4.0',

View File

@ -312,6 +312,50 @@ describe('Posts API', function () {
mobiledocRevisions.length.should.equal(0); mobiledocRevisions.length.should.equal(0);
}); });
it('Can create a post with html', async function () {
mockManager.mockLabsDisabled('lexicalEditor');
const post = {
title: 'HTML test',
html: '<p>Testing post creation with html</p>'
};
await agent
.post('/posts/?source=html&formats=mobiledoc,lexical,html')
.body({posts: [post]})
.expectStatus(201)
.matchBodySnapshot({
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('posts')
});
});
it('Can create a post with html (labs.lexicalEditor)', async function () {
mockManager.mockLabsEnabled('lexicalEditor');
const post = {
title: 'HTML test',
html: '<p>Testing post creation with html</p>'
};
await agent
.post('/posts/?source=html&formats=mobiledoc,lexical,html')
.body({posts: [post]})
.expectStatus(201)
.matchBodySnapshot({
posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('posts')
});
});
it('Errors if both mobiledoc and lexical are present', async function () { it('Errors if both mobiledoc and lexical are present', async function () {
const post = { const post = {
title: 'Mobiledoc+lexical test', title: 'Mobiledoc+lexical test',
@ -538,7 +582,7 @@ describe('Posts API', function () {
// collectionToRemove // collectionToRemove
collectionMatcher, collectionMatcher,
// automatic "latest" collection which cannot be removed // automatic "latest" collection which cannot be removed
buildCollectionMatcher(18) buildCollectionMatcher(20)
]})] ]})]
}) })
.matchHeaderSnapshot({ .matchHeaderSnapshot({
@ -556,7 +600,7 @@ describe('Posts API', function () {
// collectionToAdd // collectionToAdd
collectionMatcher, collectionMatcher,
// automatic "latest" collection which cannot be removed // automatic "latest" collection which cannot be removed
buildCollectionMatcher(18) buildCollectionMatcher(20)
]})] ]})]
}) })
.matchHeaderSnapshot({ .matchHeaderSnapshot({
@ -630,7 +674,7 @@ describe('Posts API', function () {
}); });
describe('Convert', function () { describe('Convert', function () {
it('can convert a mobiledoc post to lexical', async function () { it('can convert a mobiledoc post to lexical', async function () {
const mobiledoc = createMobiledoc('This is some great content.'); const mobiledoc = createMobiledoc('This is some great content.');
const expectedLexical = createLexical('This is some great content.'); const expectedLexical = createLexical('This is some great content.');
const postData = { const postData = {

View File

@ -4056,7 +4056,7 @@
resolved "https://registry.yarnpkg.com/@lexical/headless/-/headless-0.12.0.tgz#8ee540b6c56369688a368db1cd9eac8fa59d2203" resolved "https://registry.yarnpkg.com/@lexical/headless/-/headless-0.12.0.tgz#8ee540b6c56369688a368db1cd9eac8fa59d2203"
integrity sha512-X9LwOXs5xJV4LY1uDRFwYadbY8D8gFfFR3+ios/ujLNuQ817k1CngCyyj4XIvSnvBqC+RYy8ridztjIlfhuNJA== integrity sha512-X9LwOXs5xJV4LY1uDRFwYadbY8D8gFfFR3+ios/ujLNuQ817k1CngCyyj4XIvSnvBqC+RYy8ridztjIlfhuNJA==
"@lexical/html@0.12.0": "@lexical/html@0.12.0", "@lexical/html@^0.12.0":
version "0.12.0" version "0.12.0"
resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.12.0.tgz#008929ef2ee7114772c00bb1fd07df721f0e6e53" resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.12.0.tgz#008929ef2ee7114772c00bb1fd07df721f0e6e53"
integrity sha512-90KKtMDMElS7AD3nYzknYe6eiu7/lw1neXFWc74XI4YLiYBD14Pb/FrEvGnQ+P1voGtr4sPk3KEf8huHdM9sJw== integrity sha512-90KKtMDMElS7AD3nYzknYe6eiu7/lw1neXFWc74XI4YLiYBD14Pb/FrEvGnQ+P1voGtr4sPk3KEf8huHdM9sJw==
@ -7852,6 +7852,19 @@
lodash "^4.17.21" lodash "^4.17.21"
luxon "^3.3.0" luxon "^3.3.0"
"@tryghost/kg-html-to-lexical@0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@tryghost/kg-html-to-lexical/-/kg-html-to-lexical-0.0.1.tgz#4386c19330a64c7c9a10c54d8e47ed6f41289676"
integrity sha512-J+3A335QHu0xWvklntxAPfAPByqXG9qzEb+9f+eG01HDXHcb4cCRILjdtTmUh2ipLkXXWM9KFf5r/tlTNq7ChQ==
dependencies:
"@lexical/headless" "^0.12.0"
"@lexical/html" "^0.12.0"
"@lexical/link" "^0.12.0"
"@lexical/list" "^0.12.0"
"@tryghost/kg-default-nodes" "^0.1.26"
jsdom "^22.1.0"
lexical "^0.12.0"
"@tryghost/kg-lexical-html-renderer@0.3.22": "@tryghost/kg-lexical-html-renderer@0.3.22":
version "0.3.22" version "0.3.22"
resolved "https://registry.yarnpkg.com/@tryghost/kg-lexical-html-renderer/-/kg-lexical-html-renderer-0.3.22.tgz#b93dc849cd7386fbcbc751561399a4bd449def9d" resolved "https://registry.yarnpkg.com/@tryghost/kg-lexical-html-renderer/-/kg-lexical-html-renderer-0.3.22.tgz#b93dc849cd7386fbcbc751561399a4bd449def9d"