Skip to content

Commit

Permalink
馃帹 Koenig - Added ID attributes to heading tags when rendering (#9720)
Browse files Browse the repository at this point in the history
refs #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
  • Loading branch information
kevinansfield authored and kirrg001 committed Jul 10, 2018
1 parent 541713d commit a4aab19
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 36 deletions.
129 changes: 93 additions & 36 deletions core/server/lib/mobiledoc/converters/mobiledoc-converter.js
@@ -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.'
}));
}
};

// 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);
}
};

// 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;
// }, [ ]);
// }

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.
Expand All @@ -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
Expand Down
Expand Up @@ -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">&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>/);
});
});
});

0 comments on commit a4aab19

Please sign in to comment.