diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..31adbef --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + env: { + browser: true, + commonjs: true, + es2021: true, + node: true, + jest: true + }, + extends: [ + 'standard' + ], + parserOptions: { + ecmaVersion: 12 + } +} diff --git a/index.js b/index.js index 5b4713e..59222b5 100644 --- a/index.js +++ b/index.js @@ -1,70 +1,70 @@ -const fs = require('fs'); -const path = require('path'); +const fs = require('fs') +const path = require('path') +const colors = require('colors/safe') -const colors = require("colors/safe"); -const { getMenuPath, getFilename, filterRootMarkdowns, groupBy, genSidebar, titleSort, sidebarSort, findGroupIndex, genNav } = require("./lib/utils"); -const sidebarOptions = require("./lib/options"); +const { getMenuPath, getFilename, filterRootMarkdowns, groupBy, genSidebar, titleSort, sidebarSort, findGroupIndex, genNav } = require('./lib/utils') +const sidebarOptions = require('./lib/options') -let SIDEBAR = Object.create(null); +let SIDEBAR = Object.create(null) module.exports = (options, ctx) => ({ - name: "vuepress-plugin-auto-sidebar", - async ready() { + name: 'vuepress-plugin-auto-sidebar', + async ready () { try { - const mergeOptions = Object.assign({}, sidebarOptions, options); - const { pages } = ctx; + const mergeOptions = Object.assign({}, sidebarOptions, options) + const { pages } = ctx // 整理 pages 数据 const mapPages = pages.filter(page => page.relativePath).map(page => ({ frontmatter: page.frontmatter, menuPath: getMenuPath(page.relativePath), filename: getFilename(page.relativePath) - })).filter(filterRootMarkdowns); + })).filter(filterRootMarkdowns) // 过滤出待排序的 - const sortQueue = mapPages.filter(page => page.frontmatter.autoPrev || page.frontmatter.autoNext); - const defaultPages = mapPages.filter(page => !page.frontmatter.autoPrev && !page.frontmatter.autoNext); + const sortQueue = mapPages.filter(page => page.frontmatter.autoPrev || page.frontmatter.autoNext) + const defaultPages = mapPages.filter(page => !page.frontmatter.autoPrev && !page.frontmatter.autoNext) - const groupByDepth = groupBy(defaultPages, "menuPath"); + const groupByDepth = groupBy(defaultPages, 'menuPath') - titleSort(groupByDepth, mergeOptions.sort); - let sortQueueCache = []; + titleSort(groupByDepth, mergeOptions.sort) + let sortQueueCache = [] while (sortQueue.length) { - const current = sortQueue.pop(); - const index = findGroupIndex(current, groupByDepth); + const current = sortQueue.pop() + const index = findGroupIndex(current, groupByDepth) if (index !== -1) { - current.frontmatter.autoPrev ? - groupByDepth[current.menuPath].splice(index + 1, 0, current) : - groupByDepth[current.menuPath].splice(index, 0, current) + current.frontmatter.autoPrev + ? groupByDepth[current.menuPath].splice(index + 1, 0, current) + : groupByDepth[current.menuPath].splice(index, 0, current) - sortQueue.push(...sortQueueCache); - sortQueueCache = []; + sortQueue.push(...sortQueueCache) + sortQueueCache = [] } else { - sortQueueCache.push(current); + sortQueueCache.push(current) } } if (sortQueueCache.length) { - console.log(colors.red("\nvuepress plugin auto sidebar(精准排序): "), `\n [${colors.green(sortQueueCache.map(q => `${q.filename}(${q.frontmatter.title})`).join("、"))}] \t共 ${sortQueueCache.length} 个文件指向了不存在的 prev 或 next`); + console.log(colors.red('\nvuepress plugin auto sidebar(精准排序): '), `\n [${colors.green(sortQueueCache.map(q => `${q.filename}(${q.frontmatter.title})`).join('、'))}] \t共 ${sortQueueCache.length} 个文件指向了不存在的 prev 或 next`) } - SIDEBAR = genSidebar(sidebarSort(groupByDepth), mergeOptions); + SIDEBAR = genSidebar(sidebarSort(groupByDepth), mergeOptions) - const nav = genNav(SIDEBAR); - const dest = path.join(ctx.sourceDir, ".vuepress/nav.js"); + const nav = genNav(SIDEBAR) + const dest = path.join(ctx.sourceDir, '.vuepress/nav.js') if (mergeOptions.nav && !fs.existsSync(dest)) { - await fs.writeFileSync(dest, `module.exports = ${JSON.stringify(nav)};`); + await fs.writeFileSync(dest, `module.exports = ${JSON.stringify(nav)};`) } } catch (ex) { - console.error(ex); + console.error(ex) } }, - async enhanceAppFiles() { + async enhanceAppFiles () { return { - name: "auto-sidebar-enhance", + name: 'auto-sidebar-enhance', content: `export default ({ siteData, options }) => { siteData.themeConfig.sidebar = ${JSON.stringify(SIDEBAR)} }` } } -}); \ No newline at end of file +}) diff --git a/lib/options.js b/lib/options.js index a4b37a7..100700c 100644 --- a/lib/options.js +++ b/lib/options.js @@ -1,10 +1,10 @@ module.exports = { - sort: "asc", // 排序 - titleMode: "default", // 标题模式 + sort: 'asc', // 排序 + titleMode: 'default', // 标题模式 titleMap: {}, // 标题映射 nav: false, // 导航栏 sidebarDepth: 1, // 标题深度 collapsable: false, // 折叠 collapseList: [], // 折叠列表 - uncollapseList: [], // 不折叠列表 -} \ No newline at end of file + uncollapseList: [] // 不折叠列表 +} diff --git a/lib/utils.js b/lib/utils.js index 7551276..04438f2 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,142 +1,151 @@ // base 基础 -const getMenuPath = path => padMenuPath(path.split("/").slice(0, -1).join("/")); -const getFilename = path => path.split("/").slice(-1).toString().replace(".md", ""); -const padMenuPath = path => `${path.startsWith("/") ? "" : "/"}${path}${path.endsWith("/") ? "" : "/"}`; +const getMenuPath = path => padMenuPath(path.split('/').slice(0, -1).join('/')) +const getFilename = path => path.split('/').slice(-1).toString().replace('.md', '') +const padMenuPath = path => `${path.startsWith('/') ? '' : '/'}${path}${path.endsWith('/') ? '' : '/'}` -const filterRootMarkdowns = page => page.menuPath !== "//"; +const filterRootMarkdowns = page => page.menuPath !== '//' const groupBy = (arr, fn) => arr.map(typeof fn === 'function' ? fn : val => val[fn]).reduce((acc, val, i) => { - acc[val] = (acc[val] || []).concat(arr[i]); - return acc; - }, {}); + acc[val] = (acc[val] || []).concat(arr[i]) + return acc + }, {}) const genGroup = (title, children = [''], collapsable = false, sidebarDepth = 1) => ({ title, collapsable, sidebarDepth, children -}); +}) const genSidebar = (groups, options) => Object.keys(groups).reduce((acc, group) => { - const defaultTitle = formatTitle(group.split("/").slice(-2, -1).toString(), options.titleMode, options.titleMap); - const { above, default: defaultGroup, below } = divideMoreGroups(groups[group]); - above.sort((a, b) => a.sort - b.sort >= 0 ? -1 : 1); - below.sort((a, b) => a.sort - b.sort >= 0 ? 1 : -1); + const defaultTitle = formatTitle(group.split('/').slice(-2, -1).toString(), options.titleMode, options.titleMap) + const { above, default: defaultGroup, below } = divideMoreGroups(groups[group]) + above.sort((a, b) => a.sort - b.sort >= 0 ? -1 : 1) + below.sort((a, b) => a.sort - b.sort >= 0 ? 1 : -1) - const collapsable = options.collapseList.find(co => co === group) ? true : options.collapseList.find(co => co === group) ? false : options.collapsable; + const collapsable = options.collapseList.find(co => co === group) ? true : options.collapseList.find(co => co === group) ? false : options.collapsable acc[group] = [ ...above.map(a => genGroup(a.groupName, a.children, collapsable, options.sidebarDepth)), genGroup(defaultTitle, defaultGroup, collapsable, options.sidebarDepth), - ...below.map(a => genGroup(a.groupName, a.children, collapsable, options.sidebarDepth)), - ]; + ...below.map(a => genGroup(a.groupName, a.children, collapsable, options.sidebarDepth)) + ] - return acc; + return acc }, {}) // title 相关函数 -const toDefaultCase = str => str; -const toLowerCase = str => str.toLowerCase(); -const toUpperCase = str => str.toUpperCase(); +const toDefaultCase = str => str +const toLowerCase = str => str.toLowerCase() +const toUpperCase = str => str.toUpperCase() const toCapitalize = ([first, ...rest], lowerRest = false) => - first.toUpperCase() + (lowerRest ? rest.join('').toLowerCase() : rest.join('')); + first.toUpperCase() + (lowerRest ? rest.join('').toLowerCase() : rest.join('')) const toCamelCase = str => { - let s = + const s = str && str .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) .map(x => x.slice(0, 1).toUpperCase() + x.slice(1).toLowerCase()) - .join(''); - return s.slice(0, 1).toLowerCase() + s.slice(1); -}; + .join('') + return s.slice(0, 1).toLowerCase() + s.slice(1) +} const toKebabCase = str => str && str .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) .map(x => x.toLowerCase()) - .join('-'); + .join('-') const toTitleCase = str => str .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) .map(x => x.charAt(0).toUpperCase() + x.slice(1)) - .join(' '); + .join(' ') const TITLE_MODE = { - "default": toDefaultCase, - "lowercase": toLowerCase, - "uppercase": toUpperCase, - "capitalize": toCapitalize, - "camelcase": toCamelCase, - "kebabcase": toKebabCase, - "titlecase": toTitleCase, -}; + default: toDefaultCase, + lowercase: toLowerCase, + uppercase: toUpperCase, + capitalize: toCapitalize, + camelcase: toCamelCase, + kebabcase: toKebabCase, + titlecase: toTitleCase +} -const formatTitle = (title = "", mode = "default", map = {}) => map[title] || (TITLE_MODE[mode.toLowerCase()] || TITLE_MODE["default"])(title); +const formatTitle = (title = '', mode = 'default', map = {}) => map[title] || (TITLE_MODE[mode.toLowerCase()] || TITLE_MODE.default)(title) // 排序 const SORT_OPTIONS = { - "asc": (key) => (a, b) => a[key] > b[key] ? 1 : -1, - "desc": (key) => (a, b) => a[key] > b[key] ? -1 : 1 -}; + asc: (key) => (a, b) => a[key] > b[key] ? 1 : -1, + desc: (key) => (a, b) => a[key] > b[key] ? -1 : 1 +} -const titleSort = (obj, mode, key = "filename") => Object.values(obj).forEach(s => { - if (typeof mode === "function") { - return s.sort(mode(key)); +const titleSort = (obj, mode, key = 'filename') => Object.values(obj).forEach(s => { + if (typeof mode === 'function') { + return s.sort(mode(key)) } - s.sort((SORT_OPTIONS[mode] || SORT_OPTIONS["asc"])(key)) -}); -const sidebarSort = sidebar => Object.keys(sidebar).sort((a, b) => a.length > b.length ? -1 : 1).reduce((acc, cur) => (acc[cur] = sidebar[cur], acc), {}) + s.sort((SORT_OPTIONS[mode] || SORT_OPTIONS.asc)(key)) +}) +const sidebarSort = sidebar => Object.keys(sidebar).sort((a, b) => a.length > b.length ? -1 : 1).reduce((acc, cur) => { acc[cur] = sidebar[cur]; return acc }, {}) const findGroupIndex = (cur, obj) => { - const arr = obj[cur.menuPath]; - const prev = cur.frontmatter.autoPrev; - const next = cur.frontmatter.autoNext; - if (!arr) return -1; + const arr = obj[cur.menuPath] + const prev = cur.frontmatter.autoPrev + const next = cur.frontmatter.autoNext + if (!arr) return -1 // 当两者均存在时,取 prev if (prev) { - return arr.findIndex(item => item.filename === prev); + return arr.findIndex(item => item.filename === prev) } else if (next) { - return arr.findIndex(a => a.filename === next); + return arr.findIndex(a => a.filename === next) } else { - return -1; + return -1 } } // group -const divideReg = /autoGroup([+-])(\d*)/; +const divideReg = /autoGroup([+-])(\d*)/ const divideMoreGroups = arr => arr.reduce((acc = {}, cur) => { - const autoGroup = Object.keys(cur.frontmatter).find(f => divideReg.test(f)); - const filename = cur.filename === 'README' ? '' : cur.filename; + const autoGroup = Object.keys(cur.frontmatter).find(f => divideReg.test(f)) + const filename = cur.filename === 'README' ? '' : cur.filename if (!autoGroup) { - acc.default.push(filename); + acc.default.push(filename) } else { - const autoGroupName = cur.frontmatter[autoGroup]; - const [, symbol, sort] = autoGroup.match(divideReg); + const autoGroupName = cur.frontmatter[autoGroup] + const [, symbol, sort] = autoGroup.match(divideReg) if (symbol === '+') { const findGroup = acc.above.find(a => a.groupName === autoGroupName) - findGroup ? findGroup.children.push(filename) : acc.above.push({ - groupName: autoGroupName, - sort, - children: [filename] - }) + + if (findGroup) { + findGroup.children.push(filename) + } else { + acc.above.push({ + groupName: autoGroupName, + sort, + children: [filename] + }) + } } if (symbol === '-') { const findGroup = acc.below.find(a => a.groupName === autoGroupName) - findGroup ? findGroup.children.push(filename) : acc.below.push({ - groupName: autoGroupName, - sort, - children: [filename] - }) + if (findGroup) { + findGroup.children.push(filename) + } else { + acc.below.push({ + groupName: autoGroupName, + sort, + children: [filename] + }) + } } } - return acc; + return acc }, { above: [], default: [], @@ -145,9 +154,9 @@ const divideMoreGroups = arr => arr.reduce((acc = {}, cur) => { // nav const genNav = sides => Object.keys(sides).reduce((acc, cur) => { - const [, menu] = cur.split("/"); - const [{ title }] = sides[cur]; - const re = acc.find(a => a.text === menu); + const [, menu] = cur.split('/') + const [{ title }] = sides[cur] + const re = acc.find(a => a.text === menu) if (re) { re.items.push({ text: title, link: cur }) @@ -155,7 +164,7 @@ const genNav = sides => Object.keys(sides).reduce((acc, cur) => { acc.push({ text: menu, items: [{ text: title, link: cur }] }) } - return acc; + return acc }, []) module.exports = { diff --git a/package.json b/package.json index 8288a06..97d7471 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "lib": "lib" }, "scripts": { - "test": "jest --watchAll --verbose" + "test": "jest --watchAll --verbose", + "lint": "eslint --ext .js ./ --fix" }, "repository": { "type": "git", @@ -25,6 +26,11 @@ }, "homepage": "https://github.com/shanyuhai123/vuepress-plugin-auto-sidebar#readme", "devDependencies": { + "eslint": "^7.15.0", + "eslint-config-standard": "^16.0.2", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^4.2.1", "jest": "^25.1.0" }, "dependencies": { diff --git a/test/sort.test.js b/test/sort.test.js index 2107f48..cb80591 100644 --- a/test/sort.test.js +++ b/test/sort.test.js @@ -1,4 +1,4 @@ -const { titleSort } = require("../lib/utils"); +const { titleSort } = require('../lib/utils') const sidebars = { '/exampleMenu1/exampleSubMenu1-2/': [ @@ -15,7 +15,7 @@ const sidebars = { filename: '09-file3' } ] -}; +} describe('sidebar sort', () => { it('should get ASCII ascending order', () => { @@ -34,11 +34,11 @@ describe('sidebar sort', () => { filename: '11-README' } ] - }; + } - titleSort(sidebars, "asc"); // sort - expect(sidebars).toEqual(asc); - }); + titleSort(sidebars, 'asc') // sort + expect(sidebars).toEqual(asc) + }) it('should get ASCII descending order', () => { const desc = { @@ -56,14 +56,14 @@ describe('sidebar sort', () => { filename: '01-file2' } ] - }; + } - titleSort(sidebars, "desc"); // sort - expect(sidebars).toEqual(desc); - }); + titleSort(sidebars, 'desc') // sort + expect(sidebars).toEqual(desc) + }) it('should sort by custom rules', () => { - const fn = key => (a, b) => a[key].split("-")[1][length - 1] > b[key].split("-")[1][length - 1] ? 1 : -1; + const fn = key => (a, b) => a[key].split('-')[1][length - 1] > b[key].split('-')[1][length - 1] ? 1 : -1 const fnResult = { '/exampleMenu1/exampleSubMenu1-2/': [ { @@ -79,9 +79,9 @@ describe('sidebar sort', () => { filename: '11-README' } ] - }; + } - titleSort(sidebars, fn); // sort - expect(sidebars).toEqual(fnResult); - }); -}); \ No newline at end of file + titleSort(sidebars, fn) // sort + expect(sidebars).toEqual(fnResult) + }) +})