mirror of
https://gitee.com/docsifyjs/docsify.git
synced 2024-12-01 19:50:32 +08:00
Merge branch 'develop' into update-rewrite-gr-config
This commit is contained in:
commit
5ea62fde7e
@ -67,17 +67,19 @@ export function Fetch(Base) {
|
||||
|
||||
_loadSideAndNav(path, qs, loadSidebar, cb) {
|
||||
return () => {
|
||||
if (!loadSidebar) {
|
||||
return cb();
|
||||
}
|
||||
|
||||
const fn = result => {
|
||||
const renderSidebar = result => {
|
||||
this._renderSidebar(result);
|
||||
cb();
|
||||
};
|
||||
|
||||
// Load sidebar
|
||||
this.#loadNested(path, qs, loadSidebar, fn, this, true);
|
||||
if (!loadSidebar) {
|
||||
// Although, we don't load sidebar from sidebar file, we still need call the render to auto generate sidebar from headings toc
|
||||
renderSidebar();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load sidebar from the sidebar file
|
||||
this.#loadNested(path, qs, loadSidebar, renderSidebar, this, true);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -74,17 +74,16 @@ export class Compiler {
|
||||
this.linkTarget === '_blank' ? config.externalLinkRel || 'noopener' : '';
|
||||
this.contentBase = router.getBasePath();
|
||||
|
||||
const renderer = this._initRenderer();
|
||||
this.heading = renderer.heading;
|
||||
this.renderer = this._initRenderer();
|
||||
let compile;
|
||||
const mdConf = config.markdown || {};
|
||||
|
||||
if (isFn(mdConf)) {
|
||||
compile = mdConf(marked, renderer);
|
||||
compile = mdConf(marked, this.renderer);
|
||||
} else {
|
||||
marked.setOptions(
|
||||
Object.assign(mdConf, {
|
||||
renderer: Object.assign(renderer, mdConf.renderer),
|
||||
renderer: Object.assign(this.renderer, mdConf.renderer),
|
||||
}),
|
||||
);
|
||||
compile = marked;
|
||||
@ -252,8 +251,8 @@ export class Compiler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile sidebar
|
||||
* @param {String} text Text content
|
||||
* Compile sidebar, it uses _sidebar.md ( or specific file) or the content's headings toc to render sidebar.
|
||||
* @param {String} text Text content from the sidebar file, maybe empty
|
||||
* @param {Number} level Type of heading (h<level> tag)
|
||||
* @returns {String} Sidebar element
|
||||
*/
|
||||
@ -262,50 +261,53 @@ export class Compiler {
|
||||
const currentPath = this.router.getCurrentPath();
|
||||
let html = '';
|
||||
|
||||
// compile sidebar from _sidebar.md
|
||||
if (text) {
|
||||
html = this.compile(text);
|
||||
} else {
|
||||
for (let i = 0; i < toc.length; i++) {
|
||||
if (toc[i].ignoreSubHeading) {
|
||||
const deletedHeaderLevel = toc[i].level;
|
||||
toc.splice(i, 1);
|
||||
// Remove headers who are under current header
|
||||
for (
|
||||
let j = i;
|
||||
j < toc.length && deletedHeaderLevel < toc[j].level;
|
||||
j++
|
||||
) {
|
||||
toc.splice(j, 1) && j-- && i++;
|
||||
}
|
||||
|
||||
i--;
|
||||
return this.compile(text);
|
||||
}
|
||||
// compile sidebar from content's headings toc
|
||||
for (let i = 0; i < toc.length; i++) {
|
||||
if (toc[i].ignoreSubHeading) {
|
||||
const deletedHeaderLevel = toc[i].depth;
|
||||
toc.splice(i, 1);
|
||||
// Remove headers who are under current header
|
||||
for (
|
||||
let j = i;
|
||||
j < toc.length && deletedHeaderLevel < toc[j].depth;
|
||||
j++
|
||||
) {
|
||||
toc.splice(j, 1) && j-- && i++;
|
||||
}
|
||||
}
|
||||
|
||||
const tree = this.cacheTree[currentPath] || genTree(toc, level);
|
||||
html = treeTpl(tree, /* html */ '<ul>{inner}</ul>');
|
||||
this.cacheTree[currentPath] = tree;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
const tree = this.cacheTree[currentPath] || genTree(toc, level);
|
||||
html = treeTpl(tree);
|
||||
this.cacheTree[currentPath] = tree;
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* When current content redirect to a new path file, clean pre content headings toc
|
||||
*/
|
||||
resetToc() {
|
||||
this.toc = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile sub sidebar
|
||||
* @param {Number} level Type of heading (h<level> tag)
|
||||
* @returns {String} Sub-sidebar element
|
||||
*/
|
||||
subSidebar(level) {
|
||||
if (!level) {
|
||||
this.toc = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPath = this.router.getCurrentPath();
|
||||
const { cacheTree, toc } = this;
|
||||
|
||||
toc[0] && toc[0].ignoreAllSubs && toc.splice(0);
|
||||
toc[0] && toc[0].level === 1 && toc.shift();
|
||||
// remove the first heading from the toc if it is a top-level heading
|
||||
toc[0] && toc[0].depth === 1 && toc.shift();
|
||||
|
||||
for (let i = 0; i < toc.length; i++) {
|
||||
toc[i].ignoreSubHeading && toc.splice(i, 1) && i--;
|
||||
@ -318,12 +320,21 @@ export class Compiler {
|
||||
return treeTpl(tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the text to generate HTML heading element based on the level
|
||||
* @param {*} text Text content, for now it is only from the _sidebar.md file
|
||||
* @param {*} level Type of heading (h<level> tag), for now it is always 1
|
||||
* @returns
|
||||
*/
|
||||
header(text, level) {
|
||||
return this.heading(text, level);
|
||||
}
|
||||
|
||||
article(text) {
|
||||
return this.compile(text);
|
||||
const tokenHeading = {
|
||||
type: 'heading',
|
||||
raw: text,
|
||||
depth: level,
|
||||
text: text,
|
||||
tokens: [{ type: 'text', raw: text, text: text }],
|
||||
};
|
||||
return this.renderer.heading(tokenHeading);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,7 +10,7 @@ export function genTree(toc, maxLevel) {
|
||||
const last = {};
|
||||
|
||||
toc.forEach(headline => {
|
||||
const level = headline.level || 1;
|
||||
const level = headline.depth || 1;
|
||||
const len = level - 1;
|
||||
|
||||
if (level > maxLevel) {
|
||||
|
@ -84,9 +84,6 @@ export function Render(Base) {
|
||||
|
||||
this._renderTo(markdownElm, html);
|
||||
|
||||
// Render sidebar with the TOC
|
||||
!docsifyConfig.loadSidebar && this._renderSidebar();
|
||||
|
||||
// Execute markdown <script>
|
||||
if (
|
||||
docsifyConfig.executeScript ||
|
||||
@ -298,6 +295,7 @@ export function Render(Base) {
|
||||
}
|
||||
|
||||
this._renderTo('.sidebar-nav', this.compiler.sidebar(text, maxLevel));
|
||||
|
||||
sidebarToggleEl.setAttribute('aria-expanded', !isMobile());
|
||||
|
||||
const activeElmHref = this.router.toURL(this.route.path);
|
||||
@ -309,8 +307,7 @@ export function Render(Base) {
|
||||
activeEl.parentNode.innerHTML +=
|
||||
this.compiler.subSidebar(subMaxLevel) || '';
|
||||
} else {
|
||||
// Reset toc
|
||||
this.compiler.subSidebar();
|
||||
this.compiler.resetToc();
|
||||
}
|
||||
|
||||
// Bind event
|
||||
|
@ -101,10 +101,13 @@ export function tree(
|
||||
let innerHTML = '';
|
||||
toc.forEach(node => {
|
||||
const title = node.title.replace(/(<([^>]+)>)/g, '');
|
||||
innerHTML += /* html */ `<li><a class="section-link" href="${node.slug}" title="${title}">${node.title}</a></li>`;
|
||||
let current = `<li><a class="section-link" href="${node.slug}" title="${title}">${node.title}</a></li>`;
|
||||
if (node.children) {
|
||||
innerHTML += tree(node.children, tpl);
|
||||
// when current node has children, we need put them all in parent's <li> block without the `class="app-sub-sidebar"` attribute
|
||||
const children = tree(node.children, '<ul>{inner}</ul>');
|
||||
current = `<li><a class="section-link" href="${node.slug}" title="${title}">${node.title}</a>${children}</li>`;
|
||||
}
|
||||
innerHTML += current;
|
||||
});
|
||||
return tpl.replace('{inner}', innerHTML);
|
||||
}
|
||||
|
@ -69,3 +69,89 @@ test.describe('Sidebar Tests', () => {
|
||||
expect(page.url()).toMatch(/\/test%3Efoo$/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Configuration: autoHeader', () => {
|
||||
test('autoHeader=false', async ({ page }) => {
|
||||
const docsifyInitConfig = {
|
||||
config: {
|
||||
loadSidebar: '_sidebar.md',
|
||||
autoHeader: false,
|
||||
},
|
||||
markdown: {
|
||||
sidebar: `
|
||||
- [QuickStartAutoHeader](quickstart.md)
|
||||
`,
|
||||
},
|
||||
routes: {
|
||||
'/quickstart.md': `
|
||||
the content of quickstart space
|
||||
## In the main content there is no h1
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
await docsifyInit(docsifyInitConfig);
|
||||
|
||||
await page.click('a[href="#/quickstart"]');
|
||||
expect(page.url()).toMatch(/\/quickstart$/);
|
||||
// not heading
|
||||
await expect(page.locator('#quickstart')).toBeHidden();
|
||||
});
|
||||
|
||||
test('autoHeader=true', async ({ page }) => {
|
||||
const docsifyInitConfig = {
|
||||
config: {
|
||||
loadSidebar: '_sidebar.md',
|
||||
autoHeader: true,
|
||||
},
|
||||
markdown: {
|
||||
sidebar: `
|
||||
- [QuickStartAutoHeader](quickstart.md )
|
||||
`,
|
||||
},
|
||||
routes: {
|
||||
'/quickstart.md': `
|
||||
the content of quickstart space
|
||||
## In the main content there is no h1
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
await docsifyInit(docsifyInitConfig);
|
||||
|
||||
await page.click('a[href="#/quickstart"]');
|
||||
expect(page.url()).toMatch(/\/quickstart$/);
|
||||
|
||||
// auto generate default heading id
|
||||
const autoHeader = page.locator('#quickstartautoheader');
|
||||
expect(await autoHeader.innerText()).toContain('QuickStartAutoHeader');
|
||||
});
|
||||
|
||||
test('autoHeader=true and custom headingId', async ({ page }) => {
|
||||
const docsifyInitConfig = {
|
||||
config: {
|
||||
loadSidebar: '_sidebar.md',
|
||||
autoHeader: true,
|
||||
},
|
||||
markdown: {
|
||||
sidebar: `
|
||||
- [QuickStartAutoHeader](quickstart.md ":id=quickstartId")
|
||||
`,
|
||||
},
|
||||
routes: {
|
||||
'/quickstart.md': `
|
||||
the content of quickstart space
|
||||
## In the main content there is no h1
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
await docsifyInit(docsifyInitConfig);
|
||||
|
||||
await page.click('a[href="#/quickstart"]');
|
||||
expect(page.url()).toMatch(/\/quickstart$/);
|
||||
// auto generate custom heading id
|
||||
const autoHeader = page.locator('#quickstartId');
|
||||
expect(await autoHeader.innerText()).toContain('QuickStartAutoHeader');
|
||||
});
|
||||
});
|
||||
|
121
test/integration/sidebar.test.js
Normal file
121
test/integration/sidebar.test.js
Normal file
@ -0,0 +1,121 @@
|
||||
import docsifyInit from '../helpers/docsify-init.js';
|
||||
|
||||
describe('Test sidebar render toc structure', function () {
|
||||
test('Render sidebar with loadSidebar=true and the _sidebar.md file', async () => {
|
||||
await docsifyInit({
|
||||
config: {
|
||||
loadSidebar: '_sidebar.md',
|
||||
},
|
||||
markdown: {
|
||||
homepage: '# Hello World',
|
||||
sidebar: `
|
||||
- Getting started
|
||||
- [Level1](QuickStart.md)
|
||||
- [Level2](QuickStart2.md)
|
||||
`,
|
||||
},
|
||||
waitForSelector: '.sidebar-nav > ul',
|
||||
});
|
||||
|
||||
const sidebarElm = document.querySelector('.sidebar');
|
||||
/**
|
||||
* Expected render result
|
||||
* ==========================
|
||||
*<ul>
|
||||
* <li>
|
||||
* Getting started
|
||||
* <ul>
|
||||
* <li>
|
||||
* <a href="#/QuickStart" title="Level1">Level1</a>
|
||||
* <ul>
|
||||
* <li><a href="#/QuickStart2" title="Level2">Level2</a></li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
|
||||
const GettingStarted = document.querySelector('.sidebar-nav > ul > li');
|
||||
const level1_Elm = document.querySelector(
|
||||
'.sidebar-nav > ul > li > ul> li',
|
||||
);
|
||||
const level1_A_tag = level1_Elm.querySelector('a');
|
||||
|
||||
const level2_Elm = level1_Elm.querySelector(' ul > li ');
|
||||
|
||||
const level2_A_tag = level2_Elm.querySelector('a');
|
||||
|
||||
expect(sidebarElm).not.toBeNull();
|
||||
expect(GettingStarted).not.toBeNull();
|
||||
expect(level1_Elm).not.toBeNull();
|
||||
expect(level1_A_tag).not.toBeNull();
|
||||
expect(level2_Elm).not.toBeNull();
|
||||
expect(level2_A_tag).not.toBeNull();
|
||||
expect(level1_A_tag.textContent).toContain('Level1');
|
||||
expect(level2_A_tag.textContent).toContain('Level2');
|
||||
});
|
||||
|
||||
test('Render sidebar with loadSidebar=false should be same to loadSidebar=true sidebar structure', async () => {
|
||||
await docsifyInit({
|
||||
config: {
|
||||
loadSidebar: false,
|
||||
},
|
||||
markdown: {
|
||||
homepage: `
|
||||
# Getting started
|
||||
some thing
|
||||
## Level1
|
||||
foo
|
||||
### Level2
|
||||
bar
|
||||
`,
|
||||
},
|
||||
waitForSelector: '.sidebar-nav > ul',
|
||||
});
|
||||
|
||||
const sidebarElm = document.querySelector('.sidebar');
|
||||
/**
|
||||
* Expected render result
|
||||
* ==========================
|
||||
*<ul>
|
||||
* <li>
|
||||
* Getting started
|
||||
* <ul>
|
||||
* <li>
|
||||
* <a href="#/QuickStart" title="Level1">Level1</a>
|
||||
* <ul>
|
||||
* <li><a href="#/QuickStart2" title="Level2">Level2</a></li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
|
||||
const appSubSidebarTargetElm = document.querySelector('.sidebar-nav > ul');
|
||||
expect(appSubSidebarTargetElm).not.toBeNull();
|
||||
const ulClass = appSubSidebarTargetElm.className;
|
||||
// the sidebar-nav > ul should have the class app-sub-sidebar
|
||||
expect(ulClass).toContain('app-sub-sidebar');
|
||||
|
||||
const GettingStarted = document.querySelector('.sidebar-nav > ul > li');
|
||||
const level1_Elm = document.querySelector(
|
||||
'.sidebar-nav > ul > li > ul> li',
|
||||
);
|
||||
const level1_A_tag = level1_Elm.querySelector('a');
|
||||
|
||||
const level2_Elm = level1_Elm.querySelector(' ul > li ');
|
||||
|
||||
const level2_A_tag = level2_Elm.querySelector('a');
|
||||
|
||||
expect(sidebarElm).not.toBeNull();
|
||||
expect(GettingStarted).not.toBeNull();
|
||||
expect(level1_Elm).not.toBeNull();
|
||||
expect(level1_A_tag).not.toBeNull();
|
||||
expect(level2_Elm).not.toBeNull();
|
||||
expect(level2_A_tag).not.toBeNull();
|
||||
expect(level1_A_tag.textContent).toContain('Level1');
|
||||
expect(level2_A_tag.textContent).toContain('Level2');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user