Skip to content

Commit 7cbd532

Browse files
authored
fix: correct loadSider=false render structure issue (#2470)
1 parent 4bc5062 commit 7cbd532

File tree

6 files changed

+169
-43
lines changed

6 files changed

+169
-43
lines changed

src/core/fetch/index.js

+9-7
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,19 @@ export function Fetch(Base) {
6767

6868
_loadSideAndNav(path, qs, loadSidebar, cb) {
6969
return () => {
70-
if (!loadSidebar) {
71-
return cb();
72-
}
73-
74-
const fn = result => {
70+
const renderSidebar = result => {
7571
this._renderSidebar(result);
7672
cb();
7773
};
7874

79-
// Load sidebar
80-
this.#loadNested(path, qs, loadSidebar, fn, this, true);
75+
if (!loadSidebar) {
76+
// Although, we don't load sidebar from sidebar file, we still need call the render to auto generate sidebar from headings toc
77+
renderSidebar();
78+
return;
79+
}
80+
81+
// Load sidebar from the sidebar file
82+
this.#loadNested(path, qs, loadSidebar, renderSidebar, this, true);
8183
};
8284
}
8385

src/core/render/compiler.js

+31-28
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,8 @@ export class Compiler {
251251
}
252252

253253
/**
254-
* Compile sidebar
255-
* @param {String} text Text content
254+
* Compile sidebar, it uses _sidebar.md ( or specific file) or the content's headings toc to render sidebar.
255+
* @param {String} text Text content from the sidebar file, maybe empty
256256
* @param {Number} level Type of heading (h<level> tag)
257257
* @returns {String} Sidebar element
258258
*/
@@ -261,50 +261,53 @@ export class Compiler {
261261
const currentPath = this.router.getCurrentPath();
262262
let html = '';
263263

264+
// compile sidebar from _sidebar.md
264265
if (text) {
265-
html = this.compile(text);
266-
} else {
267-
for (let i = 0; i < toc.length; i++) {
268-
if (toc[i].ignoreSubHeading) {
269-
const deletedHeaderLevel = toc[i].level;
270-
toc.splice(i, 1);
271-
// Remove headers who are under current header
272-
for (
273-
let j = i;
274-
j < toc.length && deletedHeaderLevel < toc[j].level;
275-
j++
276-
) {
277-
toc.splice(j, 1) && j-- && i++;
278-
}
279-
280-
i--;
266+
return this.compile(text);
267+
}
268+
// compile sidebar from content's headings toc
269+
for (let i = 0; i < toc.length; i++) {
270+
if (toc[i].ignoreSubHeading) {
271+
const deletedHeaderLevel = toc[i].depth;
272+
toc.splice(i, 1);
273+
// Remove headers who are under current header
274+
for (
275+
let j = i;
276+
j < toc.length && deletedHeaderLevel < toc[j].depth;
277+
j++
278+
) {
279+
toc.splice(j, 1) && j-- && i++;
281280
}
282-
}
283281

284-
const tree = this.cacheTree[currentPath] || genTree(toc, level);
285-
html = treeTpl(tree, /* html */ '<ul>{inner}</ul>');
286-
this.cacheTree[currentPath] = tree;
282+
i--;
283+
}
287284
}
288285

286+
const tree = this.cacheTree[currentPath] || genTree(toc, level);
287+
html = treeTpl(tree);
288+
this.cacheTree[currentPath] = tree;
289289
return html;
290290
}
291291

292+
/**
293+
* When current content redirect to a new path file, clean pre content headings toc
294+
*/
295+
resetToc() {
296+
this.toc = [];
297+
}
298+
292299
/**
293300
* Compile sub sidebar
294301
* @param {Number} level Type of heading (h<level> tag)
295302
* @returns {String} Sub-sidebar element
296303
*/
297304
subSidebar(level) {
298-
if (!level) {
299-
this.toc = [];
300-
return;
301-
}
302-
303305
const currentPath = this.router.getCurrentPath();
304306
const { cacheTree, toc } = this;
305307

306308
toc[0] && toc[0].ignoreAllSubs && toc.splice(0);
307-
toc[0] && toc[0].level === 1 && toc.shift();
309+
// remove the first heading from the toc if it is a top-level heading
310+
toc[0] && toc[0].depth === 1 && toc.shift();
308311

309312
for (let i = 0; i < toc.length; i++) {
310313
toc[i].ignoreSubHeading && toc.splice(i, 1) && i--;

src/core/render/gen-tree.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function genTree(toc, maxLevel) {
1010
const last = {};
1111

1212
toc.forEach(headline => {
13-
const level = headline.level || 1;
13+
const level = headline.depth || 1;
1414
const len = level - 1;
1515

1616
if (level > maxLevel) {

src/core/render/index.js

+2-5
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,6 @@ export function Render(Base) {
8484

8585
this._renderTo(markdownElm, html);
8686

87-
// Render sidebar with the TOC
88-
!docsifyConfig.loadSidebar && this._renderSidebar();
89-
9087
// Execute markdown <script>
9188
if (
9289
docsifyConfig.executeScript ||
@@ -298,6 +295,7 @@ export function Render(Base) {
298295
}
299296

300297
this._renderTo('.sidebar-nav', this.compiler.sidebar(text, maxLevel));
298+
301299
sidebarToggleEl.setAttribute('aria-expanded', !isMobile());
302300

303301
const activeElmHref = this.router.toURL(this.route.path);
@@ -309,8 +307,7 @@ export function Render(Base) {
309307
activeEl.parentNode.innerHTML +=
310308
this.compiler.subSidebar(subMaxLevel) || '';
311309
} else {
312-
// Reset toc
313-
this.compiler.subSidebar();
310+
this.compiler.resetToc();
314311
}
315312

316313
// Bind event

src/core/render/tpl.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,13 @@ export function tree(
101101
let innerHTML = '';
102102
toc.forEach(node => {
103103
const title = node.title.replace(/(<([^>]+)>)/g, '');
104-
innerHTML += /* html */ `<li><a class="section-link" href="${node.slug}" title="${title}">${node.title}</a></li>`;
104+
let current = `<li><a class="section-link" href="${node.slug}" title="${title}">${node.title}</a></li>`;
105105
if (node.children) {
106-
innerHTML += tree(node.children, tpl);
106+
// when current node has children, we need put them all in parent's <li> block without the `class="app-sub-sidebar"` attribute
107+
const children = tree(node.children, '<ul>{inner}</ul>');
108+
current = `<li><a class="section-link" href="${node.slug}" title="${title}">${node.title}</a>${children}</li>`;
107109
}
110+
innerHTML += current;
108111
});
109112
return tpl.replace('{inner}', innerHTML);
110113
}

test/integration/sidebar.test.js

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import docsifyInit from '../helpers/docsify-init.js';
2+
3+
describe('Test sidebar render toc structure', function () {
4+
test('Render sidebar with loadSidebar=true and the _sidebar.md file', async () => {
5+
await docsifyInit({
6+
config: {
7+
loadSidebar: '_sidebar.md',
8+
},
9+
markdown: {
10+
homepage: '# Hello World',
11+
sidebar: `
12+
- Getting started
13+
- [Level1](QuickStart.md)
14+
- [Level2](QuickStart2.md)
15+
`,
16+
},
17+
waitForSelector: '.sidebar-nav > ul',
18+
});
19+
20+
const sidebarElm = document.querySelector('.sidebar');
21+
/**
22+
* Expected render result
23+
* ==========================
24+
*<ul>
25+
* <li>
26+
* Getting started
27+
* <ul>
28+
* <li>
29+
* <a href="#/QuickStart" title="Level1">Level1</a>
30+
* <ul>
31+
* <li><a href="#/QuickStart2" title="Level2">Level2</a></li>
32+
* </ul>
33+
* </li>
34+
* </ul>
35+
* </li>
36+
* </ul>
37+
*/
38+
39+
const GettingStarted = document.querySelector('.sidebar-nav > ul > li');
40+
const level1_Elm = document.querySelector(
41+
'.sidebar-nav > ul > li > ul> li',
42+
);
43+
const level1_A_tag = level1_Elm.querySelector('a');
44+
45+
const level2_Elm = level1_Elm.querySelector(' ul > li ');
46+
47+
const level2_A_tag = level2_Elm.querySelector('a');
48+
49+
expect(sidebarElm).not.toBeNull();
50+
expect(GettingStarted).not.toBeNull();
51+
expect(level1_Elm).not.toBeNull();
52+
expect(level1_A_tag).not.toBeNull();
53+
expect(level2_Elm).not.toBeNull();
54+
expect(level2_A_tag).not.toBeNull();
55+
expect(level1_A_tag.textContent).toContain('Level1');
56+
expect(level2_A_tag.textContent).toContain('Level2');
57+
});
58+
59+
test('Render sidebar with loadSidebar=false should be same to loadSidebar=true sidebar structure', async () => {
60+
await docsifyInit({
61+
config: {
62+
loadSidebar: false,
63+
},
64+
markdown: {
65+
homepage: `
66+
# Getting started
67+
some thing
68+
## Level1
69+
foo
70+
### Level2
71+
bar
72+
`,
73+
},
74+
waitForSelector: '.sidebar-nav > ul',
75+
});
76+
77+
const sidebarElm = document.querySelector('.sidebar');
78+
/**
79+
* Expected render result
80+
* ==========================
81+
*<ul>
82+
* <li>
83+
* Getting started
84+
* <ul>
85+
* <li>
86+
* <a href="#/QuickStart" title="Level1">Level1</a>
87+
* <ul>
88+
* <li><a href="#/QuickStart2" title="Level2">Level2</a></li>
89+
* </ul>
90+
* </li>
91+
* </ul>
92+
* </li>
93+
* </ul>
94+
*/
95+
96+
const appSubSidebarTargetElm = document.querySelector('.sidebar-nav > ul');
97+
expect(appSubSidebarTargetElm).not.toBeNull();
98+
const ulClass = appSubSidebarTargetElm.className;
99+
// the sidebar-nav > ul should have the class app-sub-sidebar
100+
expect(ulClass).toContain('app-sub-sidebar');
101+
102+
const GettingStarted = document.querySelector('.sidebar-nav > ul > li');
103+
const level1_Elm = document.querySelector(
104+
'.sidebar-nav > ul > li > ul> li',
105+
);
106+
const level1_A_tag = level1_Elm.querySelector('a');
107+
108+
const level2_Elm = level1_Elm.querySelector(' ul > li ');
109+
110+
const level2_A_tag = level2_Elm.querySelector('a');
111+
112+
expect(sidebarElm).not.toBeNull();
113+
expect(GettingStarted).not.toBeNull();
114+
expect(level1_Elm).not.toBeNull();
115+
expect(level1_A_tag).not.toBeNull();
116+
expect(level2_Elm).not.toBeNull();
117+
expect(level2_A_tag).not.toBeNull();
118+
expect(level1_A_tag.textContent).toContain('Level1');
119+
expect(level2_A_tag.textContent).toContain('Level2');
120+
});
121+
});

0 commit comments

Comments
 (0)