Ghost/core/server/lib/mobiledoc/converters/mobiledoc-converter.js
Kevin Ansfield a4aab19403 🎨 Koenig - Added ID attributes to heading tags when rendering (#9720)
refs https://github.com/TryGhost/Ghost/issues/9623

- added `DomModifier` class to walk a SimpleDom document and modify as needed
  - adds `id` attributes to `h1`, `h2`, etc heading tags
    - converts H* tag content to a dasherized string for the id attribute (dasherized id's are different to the smushed ids that are generated by our markdown converted but there are no backwards-compatibility concerns here)
    - if a duplicate id is detected then add a `-1`, `-2`, etc suffix to the id
- use `DomModifier` after converting mobiledoc to SimpleDom but before serialising to html
- switched top-level var declarations to es6
2018-07-11 00:03:25 +02:00

131 lines
3.8 KiB
JavaScript

const SimpleDom = require('simple-dom');
const Renderer = require('mobiledoc-dom-renderer').default;
const common = require('../../common');
const atoms = require('../atoms');
const cards = require('../cards');
const options = {
dom: new SimpleDom.Document(),
cards: cards,
atoms: atoms,
unknownCardHandler: function (args) {
common.logging.error(new common.errors.InternalServerError({
message: 'Mobiledoc card \'' + args.env.name + '\' not found.'
}));
}
};
// 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 = this.getTextValue(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);
}
// extract to util?
getTextValue(node) {
let buffer = '';
let next = node.firstChild;
while (next !== null) {
buffer += this._extractTextValue(next);
next = next.nextSibling;
}
return buffer;
}
_extractTextValue(node) {
let buffer = '';
if (node.nodeType === 3) {
buffer += node.nodeValue;
}
buffer += this.getTextValue(node);
return buffer;
}
modifyChildren(node) {
let next = node.firstChild;
while (next !== null) {
this.modify(next);
next = next.nextSibling;
}
}
modify(node) {
// add id attributes to H* tags
if (node.nodeType === 1 && node.nodeName.match(/^h\d$/i)) {
this.addHeadingId(node);
}
this.modifyChildren(node);
}
}
module.exports = {
// version 1 === Ghost 1.0 markdown-only mobiledoc
// version 2 === Ghost 2.0 full mobiledoc
render(mobiledoc, version) {
version = version || 1;
// pass the version through to the card renderers.
// create a new object here to avoid modifying the default options
// object because the version can change per-render until 2.0 is released
let versionedOptions = Object.assign({}, options, {
cardOptions: {version}
});
let renderer = new Renderer(versionedOptions);
let rendered = renderer.render(mobiledoc);
let 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
let lastChild = rendered.result.lastChild;
if (lastChild && lastChild.tagName === 'P' && !lastChild.firstChild) {
rendered.result.removeChild(lastChild);
}
// Walk the DOM output and modify nodes as needed
// eg. to add ID attributes to heading elements
let modifier = new DomModifier();
modifier.modifyChildren(rendered.result);
let html = serializer.serializeChildren(rendered.result);
// full version of Koenig wraps the content with a specific class to
// be targetted with our default stylesheet for vertical rhythm and
// card-specific styles
if (version === 2) {
html = `<div class="kg-post">\n${html}\n</div>`;
}
return html;
}
};