Switched to extracted `@tryghost/mobiledoc-dom-renderer` package

no issue

- moved `mobiledoc.renderers.mobiledocHtmlRenderer` to `mobiledoc.mobiledocHtmlRenderer` so that it's easier for the getter to access the parent objects getters
- removed all tests and dependencies that now live in @tryghost/mobiledoc-dom-renderer
  - kept the `mobiledocHtmlRenderer` test because that's testing that we've correctly wired up our cards and atoms and the output is what we expect
This commit is contained in:
Kevin Ansfield 2020-04-08 18:21:15 +01:00
parent 35e3e0708c
commit b37ac8ef1f
12 changed files with 160 additions and 366 deletions

View File

@ -216,7 +216,7 @@ class PostsImporter extends BaseImporter {
});
model.mobiledoc = JSON.stringify(mobiledoc);
model.html = mobiledocLib.renderers.mobiledocHtmlRenderer.render(JSON.parse(model.mobiledoc));
model.html = mobiledocLib.mobiledocHtmlRenderer.render(JSON.parse(model.mobiledoc));
}
this.sanitizePostsMeta(model);
});

View File

@ -1,7 +1,7 @@
const _ = require('lodash');
const Promise = require('bluebird');
const common = require('../../../../lib/common');
const renderers = require('../../../../lib/mobiledoc/renderers');
const mobiledocLib = require('../../../../lib/mobiledoc');
const models = require('../../../../models');
const message1 = 'Migrating Koenig beta post\'s mobiledoc/HTML to 2.0 format';
const message2 = 'Migrated Koenig beta post\'s mobiledoc/HTML to 2.0 format';
@ -54,7 +54,7 @@ module.exports.up = function regenerateKoenigBetaHTML(options) {
// re-render the html to remove .kg-post wrapper and adjust image classes
let version = 2;
let html = renderers.mobiledocHtmlRenderer.render(mobiledoc, version);
let html = mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc, version);
return models.Post.edit({
html,

View File

@ -61,7 +61,7 @@ module.exports.up = (options) => {
// CASE: if mobiledoc field is null, we auto set a blank structure in the model layer
// CASE: if html field is null, we auto generate the html in the model layer
if (mobiledoc && post.html && post.html.match(/^<div class="kg-card-markdown">/)) {
html = mobiledocLib.renderers.mobiledocHtmlRenderer.render(mobiledoc);
html = mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc);
}
return localOptions
.transacting('posts')
@ -101,7 +101,7 @@ module.exports.down = (options) => {
// CASE: revert: all new editor posts to the old editor format
if (mobiledoc && post.html) {
html = mobiledocLib.renderers.mobiledocHtmlRenderer.render(mobiledoc, version);
html = mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc, version);
}
return localOptions

View File

@ -2,7 +2,7 @@ const _ = require('lodash');
const Promise = require('bluebird');
const htmlToText = require('html-to-text');
const common = require('../../../../lib/common');
const renderers = require('../../../../lib/mobiledoc/renderers');
const mobiledocLib = require('../../../../lib/mobiledoc');
module.exports.config = {
transaction: true
@ -36,7 +36,7 @@ module.exports.up = (options) => {
return Promise.resolve();
}
const html = renderers.mobiledocHtmlRenderer.render(mobiledoc);
const html = mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc);
const updatedAttrs = {
html: html

View File

@ -1,7 +1,7 @@
const common = require('../common');
const config = require('../../config');
let cardFactory, cards;
let cardFactory, cards, mobiledocHtmlRenderer;
module.exports = {
get blankDocument() {
@ -19,21 +19,19 @@ module.exports = {
},
get cards() {
if (cards) {
return cards;
if (!cards) {
const CardFactory = require('@tryghost/kg-card-factory');
const defaultCards = require('@tryghost/kg-default-cards');
cardFactory = new CardFactory({
siteUrl: config.get('url')
});
cards = defaultCards.map((card) => {
return cardFactory.createCard(card);
});
}
const CardFactory = require('@tryghost/kg-card-factory');
const defaultCards = require('@tryghost/kg-default-cards');
cardFactory = new CardFactory({
siteUrl: config.get('url')
});
cards = defaultCards.map((card) => {
return cardFactory.createCard(card);
});
return cards;
},
@ -41,8 +39,22 @@ module.exports = {
return require('@tryghost/kg-default-atoms');
},
get renderers() {
return require('./renderers');
get mobiledocHtmlRenderer() {
if (!mobiledocHtmlRenderer) {
const MobiledocHtmlRenderer = require('@tryghost/kg-mobiledoc-html-renderer');
mobiledocHtmlRenderer = new MobiledocHtmlRenderer({
cards: this.cards,
atoms: this.atoms,
unknownCardHandler(args) {
common.logging.error(new common.errors.InternalServerError({
message: 'Mobiledoc card \'' + args.env.name + '\' not found.'
}));
}
});
}
return mobiledocHtmlRenderer;
},
get htmlToMobiledocConverter() {

View File

@ -1,5 +0,0 @@
module.exports = {
get mobiledocHtmlRenderer() {
return require('./mobiledoc-html-renderer');
}
};

View File

@ -1,115 +0,0 @@
const SimpleDom = require('simple-dom');
const Renderer = require('mobiledoc-dom-renderer').default;
const common = require('../../common');
const mobiledoc = require('../');
const options = {
dom: new SimpleDom.Document(),
cards: mobiledoc.cards,
atoms: mobiledoc.atoms,
unknownCardHandler: function (args) {
common.logging.error(new common.errors.InternalServerError({
message: 'Mobiledoc card \'' + args.env.name + '\' not found.'
}));
}
};
const walkDom = function (node, func) {
func(node);
node = node.firstChild;
while (node) {
walkDom(node, func);
node = node.nextSibling;
}
};
const nodeTextContent = function (node) {
let textContent = '';
walkDom(node, (node) => {
if (node.nodeType === 3) {
textContent += node.nodeValue;
}
});
return textContent;
};
// used to walk the rendered SimpleDOM output and modify elements before
// serializing to HTML. Saves having a large HTML parsing dependency such as
// jsdom that may break on malformed HTML in MD or HTML cards
class DomModifier {
constructor() {
this.usedIds = [];
}
addHeadingId(node) {
if (!node.firstChild || node.getAttribute('id')) {
return;
}
let text = nodeTextContent(node);
let id = text
.replace(/[<>&"?]/g, '')
.trim()
.replace(/[^\w]/g, '-')
.replace(/-{2,}/g, '-')
.toLowerCase();
if (this.usedIds[id] !== undefined) {
this.usedIds[id] += 1;
id += `-${this.usedIds[id]}`;
} else {
this.usedIds[id] = 0;
}
node.setAttribute('id', id);
}
modifyChildren(node) {
walkDom(node, this.modify.bind(this));
}
modify(node) {
// add id attributes to H* tags
if (node.nodeType === 1 && node.nodeName.match(/^h\d$/i)) {
this.addHeadingId(node);
}
}
}
module.exports = {
render(mobiledoc, version) {
/**
* @deprecated: version 1 === Ghost 1.0 markdown-only mobiledoc
* We keep the version 1 logic till Ghost 3.0 to be able to rollback posts.
*
* version 2 (latest) === Ghost 2.0 full mobiledoc
*/
version = version || 2;
const versionedOptions = Object.assign({}, options, {
cardOptions: {version}
});
const renderer = new Renderer(versionedOptions);
const rendered = renderer.render(mobiledoc);
const serializer = new SimpleDom.HTMLSerializer(SimpleDom.voidMap);
// Koenig keeps a blank paragraph at the end of a doc but we want to
// make sure it doesn't get rendered
const lastChild = rendered.result.lastChild;
if (lastChild && lastChild.tagName === 'P') {
if (!nodeTextContent(lastChild)) {
rendered.result.removeChild(lastChild);
}
}
// Walk the DOM output and modify nodes as needed
// eg. to add ID attributes to heading elements
const modifier = new DomModifier();
modifier.modifyChildren(rendered.result);
return serializer.serializeChildren(rendered.result);
}
};

View File

@ -405,7 +405,7 @@ Post = ghostBookshelf.Model.extend({
// CASE: html is null, but mobiledoc exists (only important for migrations & importing)
if (this.hasChanged('mobiledoc') || (!this.get('html') && (options.migrating || options.importing))) {
try {
this.set('html', mobiledocLib.renderers.mobiledocHtmlRenderer.render(JSON.parse(this.get('mobiledoc'))));
this.set('html', mobiledocLib.mobiledocHtmlRenderer.render(JSON.parse(this.get('mobiledoc'))));
} catch (err) {
throw new common.errors.ValidationError({
message: 'Invalid mobiledoc structure.',

View File

@ -49,6 +49,7 @@
"@tryghost/kg-default-atoms": "1.0.0",
"@tryghost/kg-default-cards": "1.0.1",
"@tryghost/kg-markdown-html-renderer": "1.0.2",
"@tryghost/kg-mobiledoc-html-renderer": "^1.0.0",
"@tryghost/members-api": "0.18.0",
"@tryghost/members-ssr": "0.7.4",
"@tryghost/mw-session-from-token": "0.1.0",
@ -109,7 +110,6 @@
"metascraper-publisher": "5.11.8",
"metascraper-title": "5.11.8",
"metascraper-url": "5.11.8",
"mobiledoc-dom-renderer": "0.7.0",
"moment": "2.24.0",
"moment-timezone": "0.5.23",
"multer": "1.4.2",
@ -124,7 +124,6 @@
"rss": "1.2.2",
"sanitize-html": "1.22.1",
"semver": "7.2.1",
"simple-dom": "0.3.2",
"uuid": "7.0.3",
"validator": "6.3.0",
"xml": "1.0.1"

View File

@ -1,215 +0,0 @@
const should = require('should');
const converter = require('../../../../../core/server/lib/mobiledoc/renderers/mobiledoc-html-renderer');
describe('Mobiledoc HTML renderer', function () {
describe('default', function () {
it('renders all default cards and atoms', function () {
let mobiledoc = {
version: '0.3.1',
atoms: [
['soft-return', '', {}]
],
cards: [
['markdown', {
markdown: '# Markdown card\nSome markdown'
}],
['hr', {}],
['image', {
cardWidth: 'wide',
src: '/content/images/2018/04/NatGeo06.jpg',
caption: 'Birdies'
}],
['html', {
html: '<h2>HTML card</h2>\n<div><p>Some HTML</p></div>'
}],
['embed', {
html: '<h2>Embed card</h2>'
}],
['gallery', {
images: [{
fileName: 'test.png',
src: '/test.png',
width: 1000,
height: 500
}]
}]
],
markups: [],
sections: [
[1, 'p', [
[0, [], 0, 'One'],
[1, [], 0, 0],
[0, [], 0, 'Two']
]],
[10, 0],
[1, 'p', [
[0, [], 0, 'Three']
]],
[10, 1],
[10, 2],
[1, 'p', [
[0, [], 0, 'Four']
]],
[10, 3],
[10, 4],
[10, 5],
[1, 'p', []]
]
};
converter.render(mobiledoc, 2).should.eql('<p>One<br>Two</p><!--kg-card-begin: markdown--><h1 id="markdowncard">Markdown card</h1>\n<p>Some markdown</p>\n<!--kg-card-end: markdown--><p>Three</p><hr><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="/content/images/2018/04/NatGeo06.jpg" class="kg-image"><figcaption>Birdies</figcaption></figure><p>Four</p><!--kg-card-begin: html--><h2>HTML card</h2>\n<div><p>Some HTML</p></div><!--kg-card-end: html--><figure class="kg-card kg-embed-card"><h2>Embed card</h2></figure><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"></div></figure>');
});
it('removes final blank paragraph', function () {
let mobiledoc = {
version: '0.3.1',
atoms: [],
cards: [],
markups: [],
sections: [
[1, 'p', [
[0, [], 0, 'Test']
]],
[1, 'p', []]
]
};
converter.render(mobiledoc, 2).should.eql('<p>Test</p>');
});
it('removes single blank paragraph', function () {
let mobiledoc = {
version: '0.3.1',
atoms: [],
cards: [],
markups: [],
sections: [
[1, 'p', []]
]
};
converter.render(mobiledoc, 2).should.eql('');
});
it('removes single blank paragraph with empty content', function () {
let mobiledoc = {
version: '0.3.1',
markups: [],
atoms: [],
cards: [],
sections: [
[1, 'p', [
[0, [], 0, '']
]]
]
};
converter.render(mobiledoc, 2).should.eql('');
});
it('doesn\'t remove last paragraph if it has markups', function () {
let mobiledoc = {
version: '0.3.1',
markups: [['em']],
atoms: [],
cards: [],
sections: [
[1, 'p', [
[0, [0], 1, 'This should be kept']
]]
]
};
converter.render(mobiledoc, 2).should.eql('<p><em>This should be kept</em></p>');
});
it('adds id attributes to headings', function () {
let mobiledoc = {
version: '0.3.1',
atoms: [],
cards: [],
markups: [
['a', ['href', 'http://example.com']]
],
sections: [
[1, 'h1', [
[0, [], 0, 'Heading One']
]],
[1, 'h2', [
[0, [], 0, 'Heading Two']
]],
[1, 'h3', [
[0, [], 0, 'Heading Three']
]],
[1, 'h4', [
[0, [], 0, 'Heading Four']
]],
[1, 'h5', [
[0, [], 0, 'Heading Five']
]],
[1, 'h6', [
[0, [], 0, 'Heading Six']
]],
// duplicate text
[1, 'h1', [
[0, [], 0, 'Heading One']
]],
[1, 'h3', [
[0, [], 0, 'Heading One']
]],
// invalid attr chars
[1, 'h1', [
[0, [], 0, '< left < arrow <']
]],
[1, 'h1', [
[0, [], 0, '> right > arrow >']
]],
[1, 'h1', [
[0, [], 0, '"quote" "test"']
]],
[1, 'h1', [
[0, [], 0, '? question?']
]],
[1, 'h1', [
[0, [], 0, '& ampersand&']
]],
// trailing link
[1, 'h1', [
[0, [], 0, 'trailing '],
[0, [0], 1, 'link']
]],
// preceding link
[1, 'h1', [
[0, [0], 1, 'preceding'],
[0, [], 0, ' link']
]]
]
};
let output = converter.render(mobiledoc, 2);
// normal headings
output.should.match(/<h1 id="heading-one">Heading One<\/h1>/);
output.should.match(/<h2 id="heading-two">Heading Two<\/h2>/);
output.should.match(/<h3 id="heading-three">Heading Three<\/h3>/);
output.should.match(/<h4 id="heading-four">Heading Four<\/h4>/);
output.should.match(/<h5 id="heading-five">Heading Five<\/h5>/);
output.should.match(/<h6 id="heading-six">Heading Six<\/h6>/);
// duplicate heading text
output.should.match(/<h1 id="heading-one-1">Heading One<\/h1>/);
output.should.match(/<h3 id="heading-one-2">Heading One<\/h3>/);
// invalid ID/hash-url chars
output.should.match(/<h1 id="left-arrow">&lt; left &lt; arrow &lt;<\/h1>/);
output.should.match(/<h1 id="right-arrow">&gt; right &gt; arrow &gt;<\/h1>/);
output.should.match(/<h1 id="quote-test">"quote" "test"<\/h1>/);
output.should.match(/<h1 id="question">\? question\?<\/h1>/);
output.should.match(/<h1 id="ampersand">&amp; ampersand&amp;<\/h1>/);
// heading with link
output.should.match(/<h1 id="trailing-link">trailing <a href="http:\/\/example\.com">link<\/a><\/h1>/);
output.should.match(/<h1 id="preceding-link"><a href="http:\/\/example\.com">preceding<\/a> link<\/h1>/);
});
});
});

View File

@ -0,0 +1,73 @@
const should = require('should');
const configUtils = require('../../utils/configUtils');
const mobiledocLib = require('../../../core/server/lib/mobiledoc');
describe('lib/mobiledoc', function () {
beforeEach(function () {
configUtils.set('url', 'https://example.com');
});
afterEach(function () {
configUtils.restore();
});
describe('mobiledocHtmlRenderer', function () {
it('renders all default cards and atoms', function () {
let mobiledoc = {
version: '0.3.1',
atoms: [
['soft-return', '', {}]
],
cards: [
['markdown', {
markdown: '# Markdown card\nSome markdown'
}],
['hr', {}],
['image', {
cardWidth: 'wide',
src: '/content/images/2018/04/NatGeo06.jpg',
caption: 'Birdies'
}],
['html', {
html: '<h2>HTML card</h2>\n<div><p>Some HTML</p></div>'
}],
['embed', {
html: '<h2>Embed card</h2>'
}],
['gallery', {
images: [{
fileName: 'test.png',
src: '/test.png',
width: 1000,
height: 500
}]
}]
],
markups: [],
sections: [
[1, 'p', [
[0, [], 0, 'One'],
[1, [], 0, 0],
[0, [], 0, 'Two']
]],
[10, 0],
[1, 'p', [
[0, [], 0, 'Three']
]],
[10, 1],
[10, 2],
[1, 'p', [
[0, [], 0, 'Four']
]],
[10, 3],
[10, 4],
[10, 5],
[1, 'p', []]
]
};
mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc)
.should.eql('<p>One<br>Two</p><!--kg-card-begin: markdown--><h1 id="markdowncard">Markdown card</h1>\n<p>Some markdown</p>\n<!--kg-card-end: markdown--><p>Three</p><hr><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="/content/images/2018/04/NatGeo06.jpg" class="kg-image"><figcaption>Birdies</figcaption></figure><p>Four</p><!--kg-card-begin: html--><h2>HTML card</h2>\n<div><p>Some HTML</p></div><!--kg-card-end: html--><figure class="kg-card kg-embed-card"><h2>Embed card</h2></figure><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"></div></figure>');
});
});
});

View File

@ -224,6 +224,37 @@
"@sentry/types" "5.15.4"
tslib "^1.9.3"
"@simple-dom/document@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@simple-dom/document/-/document-1.4.0.tgz#af60855f957f284d436983798ef1006cca1a1678"
integrity sha512-/RUeVH4kuD3rzo5/91+h4Z1meLSLP66eXqpVAw/4aZmYozkeqUkMprq0znL4psX/adEed5cBgiNJcfMz/eKZLg==
dependencies:
"@simple-dom/interface" "^1.4.0"
"@simple-dom/interface@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@simple-dom/interface/-/interface-1.4.0.tgz#e8feea579232017f89b0138e2726facda6fbb71f"
integrity sha512-l5qumKFWU0S+4ZzMaLXFU8tQZsicHEMEyAxI5kDFGhJsRqDwe0a7/iPA/GdxlGyDKseQQAgIz5kzU7eXTrlSpA==
"@simple-dom/parser@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@simple-dom/parser/-/parser-1.4.0.tgz#b1fee1a23f48a37d6bdd98f5242db0cab5b67abc"
integrity sha512-TNjDkOehueRIKr1df416qk9ELj+qWuVVJNIT25y1aZg3pQvxv4UPGrgaDFte7dsWBTbF3V8NYPNQ5FDUZQ8Wlg==
dependencies:
"@simple-dom/interface" "^1.4.0"
"@simple-dom/serializer@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@simple-dom/serializer/-/serializer-1.4.0.tgz#98470f357f418d72b1a1ec78d68191e60aefe215"
integrity sha512-mI1yRahsVs8atXLiQksineDsFEFqeG7RHwnnBTDOK6inbzl4tZQgjR+Z7edjgIJq5j5RhZvwPI6EuCji9B3eQw==
dependencies:
"@simple-dom/interface" "^1.4.0"
"@simple-dom/void-map@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@simple-dom/void-map/-/void-map-1.4.0.tgz#f15f07568fe1076740407266aa5e6eac249bc78c"
integrity sha512-VDhLEyVCbuhOBBgHol9ShzIv9O8UCzdXeH4FoXu2DOcu/nnvTjLTck+BgXsCLv5ynDiUdoqsREEVFnoyPpFKVw==
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@ -400,6 +431,14 @@
markdown-it-lazy-headers "^0.1.3"
markdown-it-mark "^3.0.0"
"@tryghost/kg-mobiledoc-html-renderer@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@tryghost/kg-mobiledoc-html-renderer/-/kg-mobiledoc-html-renderer-1.0.0.tgz#7892ead37a7d709dd3407b32e264f76df946bd71"
integrity sha512-/py2bsGEM+L98Tmurbom8Hc2GbcOXd1bPx9C/7PNALlXD92RiNzzrxXnTrb8XnJKAeIF5o1XxBSsx0YvwJcxCQ==
dependencies:
mobiledoc-dom-renderer "^0.7.0"
simple-dom "^1.4.0"
"@tryghost/kg-parser-plugins@0.9.2":
version "0.9.2"
resolved "https://registry.yarnpkg.com/@tryghost/kg-parser-plugins/-/kg-parser-plugins-0.9.2.tgz#ef7335dfa54446505e85b24bca2ff093f10a82a2"
@ -5992,7 +6031,7 @@ mkdirp@^1.0.3, mkdirp@~1.0.3:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea"
integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==
mobiledoc-dom-renderer@0.7.0:
mobiledoc-dom-renderer@0.7.0, mobiledoc-dom-renderer@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/mobiledoc-dom-renderer/-/mobiledoc-dom-renderer-0.7.0.tgz#53ab5f14dd612b16f03513390e5cbcc2b89f6979"
integrity sha512-A+gT6D4Ru3DKY7ZYOBRORmwhRJ7rDj2vy75D2dWuZS5NgX0mCmGs0yN7qs48YlxvfCif8RFpYsaaPg6Kc3MdJg==
@ -8199,10 +8238,16 @@ simple-concat@^1.0.0:
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6"
integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=
simple-dom@0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/simple-dom/-/simple-dom-0.3.2.tgz#0663d10f1556f1500551d518f56e3aba0871371d"
integrity sha1-BmPRDxVW8VAFUdUY9W46ughxNx0=
simple-dom@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/simple-dom/-/simple-dom-1.4.0.tgz#78ad1f41b8b70d16f82b7e0d458441c9262565b7"
integrity sha512-TnBPkmOyjdaOqyBMb4ick+n8c0Xv9Iwg1PykFV7hz9Se3UCiacTbRb+25cPmvozFNJLBUNvUzX/KsPfXF14ivA==
dependencies:
"@simple-dom/document" "^1.4.0"
"@simple-dom/interface" "^1.4.0"
"@simple-dom/parser" "^1.4.0"
"@simple-dom/serializer" "^1.4.0"
"@simple-dom/void-map" "^1.4.0"
simple-get@^3.0.3, simple-get@^3.1.0:
version "3.1.0"