Skip to content

Commit 621fcb3

Browse files
committed
feat(tools): add table-of-contents 11ty plugin
1 parent a5d6a3d commit 621fcb3

File tree

3 files changed

+125
-1
lines changed

3 files changed

+125
-1
lines changed

.changeset/cold-cooks-peel.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@patternfly/pfe-tools": minor
3+
---
4+
5+
Adds table-of-contents 11ty plugin
6+
Fixes bugs in 11ty plugins

.eleventy.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const todosPlugin = require('@patternfly/pfe-tools/11ty/plugins/todos.cjs');
1212
const markdownIt = require('markdown-it');
1313
const markdownItAnchor = require('markdown-it-anchor');
1414

15-
const pluginToc = require('eleventy-plugin-toc');
15+
const pluginToc = require('@patternfly/pfe-tools/11ty/plugins/table-of-contents.cjs');
1616

1717
const markdownLib = markdownIt({ html: true })
1818
.use(markdownItAnchor);
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/** @license portions MIT Jordan Shermer */
2+
const cheerio = require('cheerio');
3+
4+
/** Attribute which if found on a heading means the heading is excluded */
5+
const ignoreAttribute = 'data-toc-exclude';
6+
7+
const defaults = {
8+
tags: ['h2', 'h3', 'h4'],
9+
ignoredElements: [],
10+
wrapper: 'nav',
11+
wrapperClass: 'toc',
12+
headingText: '',
13+
headingTag: 'h2'
14+
};
15+
16+
function getParent(prev, current) {
17+
if (current.level > prev.level) {
18+
// child heading
19+
return prev;
20+
} else if (current.level === prev.level) {
21+
// sibling of previous
22+
return prev.parent;
23+
} else {
24+
// above the previous
25+
return getParent(prev.parent, current);
26+
}
27+
}
28+
29+
class Item {
30+
constructor($el) {
31+
if ($el) {
32+
this.slug = $el.attr('id');
33+
this.text = $el.text().trim();
34+
this.level = +$el.get(0).tagName.slice(1);
35+
} else {
36+
this.level = 0;
37+
}
38+
this.children = [];
39+
}
40+
41+
html() {
42+
let markup = '';
43+
if (this.slug && this.text) {
44+
markup += `
45+
<li><a href="#${this.slug}">${this.text}</a>
46+
`;
47+
}
48+
if (this.children.length > 0) {
49+
markup += `
50+
<ol>
51+
${this.children.map(item => item.html()).join('\n')}
52+
</ol>
53+
`;
54+
}
55+
56+
if (this.slug && this.text) {
57+
markup += '\t\t</li>';
58+
}
59+
60+
return markup;
61+
}
62+
}
63+
64+
class Toc {
65+
constructor(htmlstring = '', options = defaults) {
66+
this.options = { ...defaults, ...options };
67+
const selector = this.options.tags.join(',');
68+
this.root = new Item();
69+
this.root.parent = this.root;
70+
71+
const $ = cheerio.load(htmlstring);
72+
73+
const headings = $(selector)
74+
.filter('[id]')
75+
.filter(`:not([${ignoreAttribute}])`);
76+
77+
const ignoredElementsSelector = this.options.ignoredElements.join(',');
78+
headings.find(ignoredElementsSelector).remove();
79+
80+
if (headings.length) {
81+
let previous = this.root;
82+
headings.each((index, heading) => {
83+
const current = new Item($(heading));
84+
const parent = getParent(previous, current);
85+
current.parent = parent;
86+
parent.children.push(current);
87+
previous = current;
88+
});
89+
}
90+
}
91+
92+
get() {
93+
return this.root;
94+
}
95+
96+
html() {
97+
const { wrapper, wrapperClass, headingText, headingTag } = this.options;
98+
const root = this.get();
99+
100+
let html = '';
101+
102+
if (root.children.length) {
103+
html += `<${wrapper} class="${wrapperClass}"> ${!headingText ? '' : `<${headingTag}>${headingText}</${headingTag}>\n`}${root.html()}</${wrapper}>`;
104+
}
105+
106+
return html;
107+
}
108+
}
109+
110+
module.exports = {
111+
initArguments: {},
112+
configFunction: function(eleventyConfig, options = {}) {
113+
eleventyConfig.addFilter('toc', (content, opts) => {
114+
const toc = new Toc(content, { ...options, ...opts });
115+
return toc.html();
116+
});
117+
}
118+
};

0 commit comments

Comments
 (0)