mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-01 23:37:43 +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'),
|
||||
Renderer = require('mobiledoc-dom-renderer').default,
|
||||
common = require('../../common'),
|
||||
atoms = require('../atoms'),
|
||||
cards = require('../cards'),
|
||||
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.'
|
||||
}));
|
||||
}
|
||||
};
|
||||
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.'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// function getCards() {
|
||||
// return config.get('apps:internal').reduce(
|
||||
// function (cards, appName) {
|
||||
// var app = require(path.join(config.get('paths').internalAppPath, appName));
|
||||
// if (app.hasOwnProperty('cards')) {
|
||||
// cards = cards.concat(app.cards);
|
||||
// }
|
||||
// return cards;
|
||||
// }, [ ]);
|
||||
// }
|
||||
// function getAtoms() {
|
||||
// return config.get('apps:internal').reduce(
|
||||
// function (atoms, appName) {
|
||||
// var app = require(path.join(config.get('paths').internalAppPath, appName));
|
||||
// if (app.hasOwnProperty('atoms')) {
|
||||
// atoms = atoms.concat(app.atoms);
|
||||
// }
|
||||
// return atoms;
|
||||
// }, [ ]);
|
||||
// }
|
||||
// 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: function (mobiledoc, version) {
|
||||
render(mobiledoc, version) {
|
||||
version = version || 1;
|
||||
|
||||
// pass the version through to the card renderers.
|
||||
@ -59,6 +111,11 @@ module.exports = {
|
||||
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
|
||||
|
@ -109,5 +109,94 @@ describe('Mobiledoc converter', function () {
|
||||
|
||||
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