Skip to content

Commit 146c663

Browse files
committed
Changes for Koenig and Ghost 2.0 (#9750)
refs #9742, refs #9724 - handle König Editor format for 2.0 - adapted importer to be able to import 1.0 and 2.0 exports - added migration scripts - remove labs flag for Koenig - migrate all old editor posts to new editor format - ensure we protect the code against mobiledoc or html field being null - ensure we create a blank mobiledoc structure if mobiledoc field is null (model layer) - ensure you can fully rollback 2.0 to 1.0 - keep mobiledoc/markdown version 1 logic to be able to rollback (deprecated code)
1 parent 4754e7d commit 146c663

File tree

15 files changed

+210
-185
lines changed

15 files changed

+210
-185
lines changed

core/server/data/importer/importers/data/posts.js

+28
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const debug = require('ghost-ignition').debug('importer:posts'),
22
_ = require('lodash'),
33
uuid = require('uuid'),
44
BaseImporter = require('./base'),
5+
converters = require('../../../../lib/mobiledoc/converters'),
56
validation = require('../../../validation');
67

78
class PostsImporter extends BaseImporter {
@@ -160,6 +161,33 @@ class PostsImporter extends BaseImporter {
160161
model.comment_id = model.id;
161162
}
162163
}
164+
165+
// CASE 1: you are importing old editor posts
166+
// CASE 2: you are importing Koenig Beta posts
167+
if (model.mobiledoc || (model.mobiledoc && model.html && model.html.match(/^<div class="kg-card-markdown">/))) {
168+
let mobiledoc;
169+
170+
try {
171+
mobiledoc = JSON.parse(model.mobiledoc);
172+
173+
if (!mobiledoc.cards || !_.isArray(mobiledoc.cards)) {
174+
model.mobiledoc = converters.mobiledocConverter.blankStructure();
175+
mobiledoc = model.mobiledoc;
176+
}
177+
} catch (err) {
178+
mobiledoc = converters.mobiledocConverter.blankStructure();
179+
}
180+
181+
mobiledoc.cards.forEach((card) => {
182+
if (card[0] === 'image') {
183+
card[1].cardWidth = card[1].imageStyle;
184+
delete card[1].imageStyle;
185+
}
186+
});
187+
188+
model.mobiledoc = JSON.stringify(mobiledoc);
189+
model.html = converters.mobiledocConverter.render(JSON.parse(model.mobiledoc));
190+
}
163191
});
164192

165193
// NOTE: We only support removing duplicate posts within the file to import.

core/server/data/migrations/versions/2.0/2-update-posts.js

+76-11
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,66 @@ const _ = require('lodash'),
22
Promise = require('bluebird'),
33
common = require('../../../../lib/common'),
44
models = require('../../../../models'),
5-
message1 = 'Updating post data (comment_id)',
6-
message2 = 'Updated post data (comment_id)',
7-
message3 = 'Rollback: Keep correct comment_id values in amp column.';
5+
converters = require('../../../../lib/mobiledoc/converters'),
6+
message1 = 'Updating posts: apply new editor format and set comment_id field.',
7+
message2 = 'Updated posts: apply new editor format and set comment_id field.',
8+
message3 = 'Rollback: Updating posts: use old editor format',
9+
message4 = 'Rollback: Updated posts: use old editor format';
810

911
module.exports.config = {
1012
transaction: true
1113
};
1214

15+
let mobiledocIsCompatibleWithV1 = function mobiledocIsCompatibleWithV1(doc) {
16+
if (doc
17+
&& doc.markups.length === 0
18+
&& doc.cards.length === 1
19+
&& doc.cards[0][0].match(/(?:card-)?markdown/)
20+
&& doc.sections.length === 1
21+
&& doc.sections[0].length === 2
22+
&& doc.sections[0][0] === 10
23+
&& doc.sections[0][1] === 0
24+
) {
25+
return true;
26+
}
27+
28+
return false;
29+
};
30+
1331
module.exports.up = (options) => {
14-
const postAllColumns = ['id', 'comment_id'];
32+
const postAllColumns = ['id', 'comment_id', 'html', 'mobiledoc'];
1533

1634
let localOptions = _.merge({
17-
context: {internal: true}
35+
context: {internal: true},
36+
migrating: true
1837
}, options);
1938

2039
common.logging.info(message1);
2140

2241
return models.Post.findAll(_.merge({columns: postAllColumns}, localOptions))
2342
.then(function (posts) {
2443
return Promise.map(posts.models, function (post) {
25-
if (post.get('comment_id')) {
26-
return Promise.resolve();
44+
let mobiledoc;
45+
let html;
46+
47+
try {
48+
mobiledoc = JSON.parse(post.get('mobiledoc') || null);
49+
} catch (err) {
50+
common.logging.warn(`Invalid mobiledoc structure for ${post.id}. Falling back to blank structure.`);
51+
mobiledoc = converters.mobiledocConverter.blankStructure();
52+
}
53+
54+
// CASE: convert all old editor posts to the new editor format
55+
// CASE: if mobiledoc field is null, we auto set a blank structure in the model layer
56+
// CASE: if html field is null, we auto generate the html in the model layer
57+
if (mobiledoc && post.get('html') && post.get('html').match(/^<div class="kg-card-markdown">/)) {
58+
html = converters.mobiledocConverter.render(mobiledoc);
2759
}
2860

2961
return models.Post.edit({
30-
comment_id: post.id
62+
comment_id: post.get('comment_id') || post.id,
63+
html: html || post.get('html'),
64+
mobiledoc: JSON.stringify(mobiledoc)
3165
}, _.merge({id: post.id}, localOptions));
3266
}, {concurrency: 100});
3367
})
@@ -36,7 +70,38 @@ module.exports.up = (options) => {
3670
});
3771
};
3872

39-
module.exports.down = () => {
40-
common.logging.warn(message3);
41-
return Promise.resolve();
73+
module.exports.down = (options) => {
74+
const postAllColumns = ['id', 'html', 'mobiledoc'];
75+
76+
let localOptions = _.merge({
77+
context: {internal: true},
78+
migrating: true
79+
}, options);
80+
81+
common.logging.info(message3);
82+
83+
return models.Post.findAll(_.merge({columns: postAllColumns}, localOptions))
84+
.then(function (posts) {
85+
return Promise.map(posts.models, function (post) {
86+
let version = 1;
87+
let html;
88+
let mobiledoc = JSON.parse(post.get('mobiledoc') || null);
89+
90+
if (!mobiledocIsCompatibleWithV1(mobiledoc)) {
91+
version = 2;
92+
}
93+
94+
// CASE: revert: all new editor posts to the old editor format
95+
if (mobiledoc && post.get('html')) {
96+
html = converters.mobiledocConverter.render(mobiledoc, version);
97+
}
98+
99+
return models.Post.edit({
100+
html: html || post.get('html')
101+
}, _.merge({id: post.id}, localOptions));
102+
}, {concurrency: 100});
103+
})
104+
.then(() => {
105+
common.logging.info(message4);
106+
});
42107
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const _ = require('lodash'),
2+
Promise = require('bluebird'),
3+
common = require('../../../../lib/common'),
4+
models = require('../../../../models'),
5+
message1 = 'Removing `koenigEditor` from labs.',
6+
message2 = 'Removed `koenigEditor` from labs.',
7+
message3 = 'Rollback: Please re-enable König Beta if required. We can\'t rollback this change.';
8+
9+
module.exports.config = {
10+
transaction: true
11+
};
12+
13+
module.exports.up = (options) => {
14+
let localOptions = _.merge({
15+
context: {internal: true}
16+
}, options);
17+
18+
return models.Settings.findOne({key: 'labs'}, localOptions)
19+
.then(function (settingsModel) {
20+
if (!settingsModel) {
21+
common.logging.warn('Labs field does not exist.');
22+
return;
23+
}
24+
25+
const labsValue = JSON.parse(settingsModel.get('value'));
26+
delete labsValue.koenigEditor;
27+
28+
common.logging.info(message1);
29+
return models.Settings.edit({
30+
key: 'labs',
31+
value: JSON.stringify(labsValue)
32+
}, localOptions);
33+
})
34+
.then(() => {
35+
common.logging.info(message2);
36+
});
37+
};
38+
39+
module.exports.down = () => {
40+
common.logging.warn(message3);
41+
return Promise.resolve();
42+
};

core/server/lib/mobiledoc/cards/markdown.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ module.exports = {
44
render: function (opts) {
55
let converters = require('../converters');
66
let payload = opts.payload;
7-
let version = opts.options.version;
7+
let version = opts.options && opts.options.version || 2;
88
// convert markdown to HTML ready for insertion into dom
99
let html = converters.markdownConverter.render(payload.markdown || '');
1010

11-
// Ghost 1.0's markdown-only renderer wrapped cards
11+
/**
12+
* @deprecated Ghost 1.0's markdown-only renderer wrapped cards
13+
*/
1214
if (version === 1) {
1315
html = `<div class="kg-card-markdown">${html}</div>`;
1416
}

core/server/lib/mobiledoc/converters/mobiledoc-converter.js

+22-9
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,15 @@ class DomModifier {
8888
}
8989

9090
module.exports = {
91-
// version 1 === Ghost 1.0 markdown-only mobiledoc
92-
// version 2 === Ghost 2.0 full mobiledoc
9391
render(mobiledoc, version) {
94-
version = version || 1;
92+
/**
93+
* @deprecated: version 1 === Ghost 1.0 markdown-only mobiledoc
94+
* We keep the version 1 logic till Ghost 3.0 to be able to rollback posts.
95+
*
96+
* version 2 (latest) === Ghost 2.0 full mobiledoc
97+
*/
98+
version = version || 2;
9599

96-
// pass the version through to the card renderers.
97-
// create a new object here to avoid modifying the default options
98-
// object because the version can change per-render until 2.0 is released
99100
let versionedOptions = Object.assign({}, options, {
100101
cardOptions: {version}
101102
});
@@ -116,8 +117,20 @@ module.exports = {
116117
let modifier = new DomModifier();
117118
modifier.modifyChildren(rendered.result);
118119

119-
let html = serializer.serializeChildren(rendered.result);
120-
121-
return html;
120+
return serializer.serializeChildren(rendered.result);
121+
},
122+
123+
blankStructure() {
124+
return {
125+
version: '0.3.1',
126+
markups: [],
127+
atoms: [],
128+
cards: [],
129+
sections: [
130+
[1, 'p', [
131+
[0, [], 0, '']
132+
]]
133+
]
134+
};
122135
}
123136
};

core/server/models/post.js

+12-35
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ var _ = require('lodash'),
88
htmlToText = require('html-to-text'),
99
ghostBookshelf = require('./base'),
1010
config = require('../config'),
11-
labs = require('../services/labs'),
1211
converters = require('../lib/mobiledoc/converters'),
1312
urlService = require('../services/url'),
1413
relations = require('./relations'),
@@ -198,7 +197,6 @@ Post = ghostBookshelf.Model.extend({
198197
prevSlug = this.previous('slug'),
199198
publishedAt = this.get('published_at'),
200199
publishedAtHasChanged = this.hasDateChanged('published_at', {beforeWrite: true}),
201-
mobiledoc = JSON.parse(this.get('mobiledoc') || null),
202200
generatedFields = ['html', 'plaintext'],
203201
tagsToSave,
204202
ops = [];
@@ -262,42 +260,21 @@ Post = ghostBookshelf.Model.extend({
262260
ghostBookshelf.Model.prototype.onSaving.call(this, model, attr, options);
263261

264262
// do not allow generated fields to be overridden via the API
265-
generatedFields.forEach((field) => {
266-
if (this.hasChanged(field)) {
267-
this.set(field, this.previous(field));
268-
}
269-
});
270-
271-
// render mobiledoc to HTML. Switch render version if Koenig is enabled
272-
// or has been edited with Koenig and is no longer compatible with the
273-
// Ghost 1.0 markdown-only renderer
274-
// TODO: re-render all content and remove the version toggle for Ghost 2.0
275-
if (mobiledoc) {
276-
let version = 1;
277-
let koenigEnabled = labs.isSet('koenigEditor') === true;
278-
279-
let mobiledocIsCompatibleWithV1 = function mobiledocIsCompatibleWithV1(doc) {
280-
if (doc
281-
&& doc.markups.length === 0
282-
&& doc.cards.length === 1
283-
&& doc.cards[0][0].match(/(?:card-)?markdown/)
284-
&& doc.sections.length === 1
285-
&& doc.sections[0].length === 2
286-
&& doc.sections[0][0] === 10
287-
&& doc.sections[0][1] === 0
288-
) {
289-
return true;
263+
if (!options.migrating) {
264+
generatedFields.forEach((field) => {
265+
if (this.hasChanged(field)) {
266+
this.set(field, this.previous(field));
290267
}
268+
});
269+
}
291270

292-
return false;
293-
};
294-
295-
if (koenigEnabled || !mobiledocIsCompatibleWithV1(mobiledoc)) {
296-
version = 2;
297-
}
271+
if (!this.get('mobiledoc')) {
272+
this.set('mobiledoc', JSON.stringify(converters.mobiledocConverter.blankStructure()));
273+
}
298274

299-
let html = converters.mobiledocConverter.render(mobiledoc, version);
300-
this.set('html', html);
275+
// render mobiledoc to HTML
276+
if (this.hasChanged('mobiledoc') || !this.get('html')) {
277+
this.set('html', converters.mobiledocConverter.render(JSON.parse(this.get('mobiledoc'))));
301278
}
302279

303280
if (this.hasChanged('html') || !this.get('plaintext')) {

core/test/integration/data/importer/importers/data_spec.js

-17
Original file line numberDiff line numberDiff line change
@@ -1402,20 +1402,3 @@ describe('LTS', function () {
14021402
});
14031403
});
14041404
});
1405-
1406-
describe('LTS', function () {
1407-
beforeEach(testUtils.teardown);
1408-
beforeEach(testUtils.setup('roles', 'owner', 'settings'));
1409-
1410-
it('disallows importing LTS imports', function () {
1411-
const exportData = exportedLegacyBody().db[0];
1412-
1413-
return dataImporter.doImport(exportData, importOptions)
1414-
.then(function () {
1415-
"0".should.eql(1, 'LTS import should fail');
1416-
})
1417-
.catch(function (err) {
1418-
err.message.should.eql('Importing a LTS export into Ghost 2.0 is not allowed.');
1419-
});
1420-
});
1421-
});

core/test/unit/helpers/reading_time_spec.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ var should = require('should'),
44
helpers = require('../../../server/helpers');
55

66
var almostOneMinute =
7-
'<div class="kg-card-markdown"><p>Ghost has a number of different user roles for your team</p>' +
7+
'<p>Ghost has a number of different user roles for your team</p>' +
88
'<h3 id="authors">Authors</h3><p>The base user level in Ghost is an author. Authors can write posts,' +
99
' edit their own posts, and publish their own posts. Authors are <strong>trusted</strong> users. If you ' +
1010
'don\'t trust users to be allowed to publish their own posts, you shouldn\'t invite them to Ghost admin.</p>' +
@@ -18,7 +18,7 @@ var almostOneMinute =
1818
'The Owner can never be deleted. And in some circumstances the owner will have access to additional special settings ' +
1919
'if applicable — for example, billing details, if using Ghost(Pro).</p><hr><p>It\'s a good idea to ask all of your' +
2020
' users to fill out their user profiles, including bio and social links. These will populate rich structured data ' +
21-
'for posts and generally create more opportunities for themes to fully populate their design.</p></div>';
21+
'for posts and generally create more opportunities for themes to fully populate their design.</p>';
2222

2323
var almostOneAndAHalfMinute = almostOneMinute +
2424
'<div>' +

0 commit comments

Comments
 (0)