143 lines
4.3 KiB
JavaScript
143 lines
4.3 KiB
JavaScript
|
const SimpleDom = require('simple-dom');
|
||
|
const Renderer = require('mobiledoc-dom-renderer').default;
|
||
|
const {slugify} = require('@tryghost/kg-utils');
|
||
|
|
||
|
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, (currentNode) => {
|
||
|
if (currentNode.nodeType === 3) {
|
||
|
textContent += currentNode.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(options) {
|
||
|
this.usedIds = [];
|
||
|
this.options = options;
|
||
|
}
|
||
|
|
||
|
addHeadingId(node) {
|
||
|
if (!node.firstChild || node.getAttribute('id')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let text = nodeTextContent(node);
|
||
|
let id = slugify(text, this.options);
|
||
|
|
||
|
if (this.usedIds[id] !== undefined) {
|
||
|
this.usedIds[id] += 1;
|
||
|
id += `-${this.usedIds[id]}`;
|
||
|
} else {
|
||
|
this.usedIds[id] = 0;
|
||
|
}
|
||
|
|
||
|
node.setAttribute('id', id);
|
||
|
}
|
||
|
|
||
|
wrapBlockquoteContentInP(node) {
|
||
|
if (node.firstChild && node.firstChild.tagName === 'P') {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const p = this.options.dom.createElement('p');
|
||
|
while (node.firstChild) {
|
||
|
p.appendChild(node.firstChild);
|
||
|
}
|
||
|
|
||
|
node.appendChild(p);
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
// wrap blockquote content in P tag for emails
|
||
|
if (this.options.target === 'email' && node.nodeType === 1 && node.nodeName === 'BLOCKQUOTE') {
|
||
|
this.wrapBlockquoteContentInP(node);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class MobiledocHtmlRenderer {
|
||
|
constructor(options = {}) {
|
||
|
this.options = {
|
||
|
dom: new SimpleDom.Document(),
|
||
|
cards: options.cards || [],
|
||
|
atoms: options.atoms || [],
|
||
|
unknownCardHandler: options.unknownCardHandler || function () {}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
render(mobiledoc, _cardOptions = {}) {
|
||
|
const ghostVersion = mobiledoc.ghostVersion || '4.0';
|
||
|
|
||
|
const defaultCardOptions = {
|
||
|
ghostVersion,
|
||
|
target: 'html'
|
||
|
};
|
||
|
const cardOptions = Object.assign({}, defaultCardOptions, _cardOptions);
|
||
|
|
||
|
const sectionElementRenderer = {
|
||
|
ASIDE: function (tagName, dom) {
|
||
|
// we use ASIDE sections in Koenig as a workaround for applying
|
||
|
// a different blockquote style because mobiledoc doesn't support
|
||
|
// storing arbitrary attributes with sections
|
||
|
const blockquote = dom.createElement('blockquote');
|
||
|
blockquote.setAttribute('class', 'kg-blockquote-alt');
|
||
|
return blockquote;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const rendererOptions = Object.assign({}, this.options, {cardOptions, sectionElementRenderer});
|
||
|
const renderer = new Renderer(rendererOptions);
|
||
|
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(Object.assign({}, cardOptions, {dom: this.options.dom}));
|
||
|
modifier.modifyChildren(rendered.result);
|
||
|
|
||
|
const output = serializer.serializeChildren(rendered.result);
|
||
|
|
||
|
// clean up any DOM that could be kept around in our SimpleDom instance
|
||
|
rendered.teardown();
|
||
|
|
||
|
return output;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = MobiledocHtmlRenderer;
|