mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-04 17:04:59 +03:00
🎨 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
This commit is contained in:
parent
541713d73f
commit
a4aab19403
@ -1,44 +1,96 @@
|
|||||||
var SimpleDom = require('simple-dom'),
|
const SimpleDom = require('simple-dom');
|
||||||
Renderer = require('mobiledoc-dom-renderer').default,
|
const Renderer = require('mobiledoc-dom-renderer').default;
|
||||||
common = require('../../common'),
|
const common = require('../../common');
|
||||||
atoms = require('../atoms'),
|
const atoms = require('../atoms');
|
||||||
cards = require('../cards'),
|
const cards = require('../cards');
|
||||||
options = {
|
const options = {
|
||||||
dom: new SimpleDom.Document(),
|
dom: new SimpleDom.Document(),
|
||||||
cards: cards,
|
cards: cards,
|
||||||
atoms: atoms,
|
atoms: atoms,
|
||||||
unknownCardHandler: function (args) {
|
unknownCardHandler: function (args) {
|
||||||
common.logging.error(new common.errors.InternalServerError({
|
common.logging.error(new common.errors.InternalServerError({
|
||||||
message: 'Mobiledoc card \'' + args.env.name + '\' not found.'
|
message: 'Mobiledoc card \'' + args.env.name + '\' not found.'
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// function getCards() {
|
// used to walk the rendered SimpleDOM output and modify elements before
|
||||||
// return config.get('apps:internal').reduce(
|
// serializing to HTML. Saves having a large HTML parsing dependency such as
|
||||||
// function (cards, appName) {
|
// jsdom that may break on malformed HTML in MD or HTML cards
|
||||||
// var app = require(path.join(config.get('paths').internalAppPath, appName));
|
class DomModifier {
|
||||||
// if (app.hasOwnProperty('cards')) {
|
constructor() {
|
||||||
// cards = cards.concat(app.cards);
|
this.usedIds = [];
|
||||||
// }
|
}
|
||||||
// return cards;
|
|
||||||
// }, [ ]);
|
addHeadingId(node) {
|
||||||
// }
|
if (!node.firstChild || node.getAttribute('id')) {
|
||||||
// function getAtoms() {
|
return;
|
||||||
// return config.get('apps:internal').reduce(
|
}
|
||||||
// function (atoms, appName) {
|
|
||||||
// var app = require(path.join(config.get('paths').internalAppPath, appName));
|
let text = this.getTextValue(node);
|
||||||
// if (app.hasOwnProperty('atoms')) {
|
let id = text
|
||||||
// atoms = atoms.concat(app.atoms);
|
.replace(/[<>&"?]/g, '')
|
||||||
// }
|
.trim()
|
||||||
// return atoms;
|
.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 = {
|
module.exports = {
|
||||||
// version 1 === Ghost 1.0 markdown-only mobiledoc
|
// version 1 === Ghost 1.0 markdown-only mobiledoc
|
||||||
// version 2 === Ghost 2.0 full mobiledoc
|
// version 2 === Ghost 2.0 full mobiledoc
|
||||||
render: function (mobiledoc, version) {
|
render(mobiledoc, version) {
|
||||||
version = version || 1;
|
version = version || 1;
|
||||||
|
|
||||||
// pass the version through to the card renderers.
|
// pass the version through to the card renderers.
|
||||||
@ -59,6 +111,11 @@ module.exports = {
|
|||||||
rendered.result.removeChild(lastChild);
|
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);
|
let html = serializer.serializeChildren(rendered.result);
|
||||||
|
|
||||||
// full version of Koenig wraps the content with a specific class to
|
// full version of Koenig wraps the content with a specific class to
|
||||||
|
@ -109,5 +109,94 @@ describe('Mobiledoc converter', function () {
|
|||||||
|
|
||||||
converter.render(mobiledoc, 2).should.eql('<div class="kg-post">\n<p>Test</p>\n</div>');
|
converter.render(mobiledoc, 2).should.eql('<div class="kg-post">\n<p>Test</p>\n</div>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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">< left < arrow <<\/h1>/);
|
||||||
|
output.should.match(/<h1 id="right-arrow">> right > arrow ><\/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">& ampersand&<\/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>/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user