Merge branch 'develop' into update-rewrite-gr-config

This commit is contained in:
Koy Zhuang 2024-07-28 16:55:42 +08:00 committed by GitHub
commit 5ea62fde7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 272 additions and 52 deletions

View File

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

View File

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

View File

@ -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) {

View File

@ -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

View File

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

View File

@ -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');
});
});

View 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');
});
});